fur_cli/security/
unlock.rs1use std::fs;
2use std::path::Path;
3
4use clap::Parser;
5use walkdir::WalkDir;
6
7use crate::schema::rebuild::{LOCK_CHECK, LOCK_SENTINEL};
8use crate::security::{crypto, io, state};
9
10#[derive(Parser, Clone)]
11pub struct UnlockArgs {
12 #[arg(long)]
13 pub hide: bool,
14}
15
16pub fn run_unlock(args: UnlockArgs) {
17 if !state::is_locked() {
18 println!("🔓 Project already unlocked.");
19 return;
20 }
21
22 let _ = args.hide;
23
24 let pass = io::read_password("🔑 Enter password: ");
25
26 if !verify_password(&pass) {
27 println!("❌ Incorrect password. Project remains locked.");
28 return;
29 }
30
31 decrypt_project(&pass);
32
33 remove_sentinel();
34 state::remove_lock();
35
36 println!("🔓 Project decrypted.");
37}
38
39fn verify_password(password: &str) -> bool {
46 for check_path in [
47 Path::new("chats").join(LOCK_CHECK),
48 Path::new(".fur/.lockcheck").to_path_buf(),
49 ] {
50 if let Ok(bytes) = fs::read(&check_path) {
51 if let Ok(decrypted) = crypto::decrypt(&bytes, password) {
52 if std::str::from_utf8(&decrypted)
53 .map(|t| t.trim() == "FUR_LOCK_CHECK_V1")
54 .unwrap_or(false)
55 {
56 return true;
57 }
58 }
59 return false;
60 }
61 }
62
63 trial_decrypt(password)
64}
65
66fn trial_decrypt(password: &str) -> bool {
69 for entry in WalkDir::new("chats")
70 .follow_links(false)
71 .into_iter()
72 .filter_map(|e| e.ok())
73 {
74 let p = entry.path();
75
76 if !p.is_file() || is_sentinel(p) {
77 continue;
78 }
79
80 if let Ok(bytes) = fs::read(p) {
81 if crypto::decrypt(&bytes, password).is_ok() {
82 return true;
83 }
84 }
85 }
86
87 eprintln!("⚠ No verifiable encrypted content found under chats/.");
88 false
89}
90
91fn remove_sentinel() {
94 for name in [LOCK_SENTINEL, LOCK_CHECK] {
95 let path = Path::new("chats").join(name);
96
97 if path.exists() {
98 let _ = fs::remove_file(path);
99 }
100 }
101}
102
103fn decrypt_project(password: &str) {
104 decrypt_dir(".fur/messages", password);
105 decrypt_dir(".fur/threads", password);
106
107 decrypt_file(".fur/index.json", password);
108 decrypt_file(".fur/.lockcheck", password);
109
110 decrypt_markdowns(password);
111}
112
113fn decrypt_dir(dir: &str, password: &str) {
114 let path = Path::new(dir);
115
116 if let Ok(entries) = fs::read_dir(path) {
117 for e in entries.flatten() {
118 let p = e.path();
119
120 if p.is_file() {
121 io::decrypt_file(&p, password);
122 }
123 }
124 }
125}
126
127fn decrypt_file(path: &str, password: &str) {
128 let p = Path::new(path);
129
130 if p.exists() {
131 io::decrypt_file(p, password);
132 }
133}
134
135fn decrypt_markdowns(password: &str) {
138 let chats = Path::new("chats");
139
140 if !chats.exists() {
141 return;
142 }
143
144 for entry in WalkDir::new(chats)
145 .follow_links(false)
146 .into_iter()
147 .filter_map(|e| e.ok())
148 {
149 let p = entry.path();
150
151 if !p.is_file() || is_sentinel(p) {
152 continue;
153 }
154
155 io::decrypt_file(p, password);
156 }
157}
158
159fn is_sentinel(path: &Path) -> bool {
160 matches!(
161 path.file_name().and_then(|f| f.to_str()),
162 Some(LOCK_SENTINEL) | Some(LOCK_CHECK)
163 )
164}