1pub mod guarded_key;
2pub use guarded_key::GuardedKey;
3
4mod signer;
5pub use signer::GuardedSigner;
6
7use argon2::Argon2;
8use chacha20poly1305::{aead::Aead, ChaCha20Poly1305, KeyInit};
9use zeroize::Zeroize;
10
11pub async fn hash_pass(password: &str) -> [u8; 32] {
14 let password = password.to_string();
15 tokio::task::spawn_blocking(move || {
16 let salt = b"vectorvectovectvecvev";
17 let mut output = [0u8; 32];
18
19 let params = argon2::Params::new(
20 150_000, 10, 1, Some(32),
24 ).unwrap();
25
26 let argon2 = Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params);
27 argon2.hash_password_into(password.as_bytes(), salt, &mut output).unwrap();
28
29 output
30 }).await.unwrap()
31}
32
33pub fn encrypt_with_key(plaintext: &str, key: &[u8; 32]) -> Result<String, String> {
35 use chacha20poly1305::aead::OsRng;
36 use chacha20poly1305::AeadCore;
37
38 let cipher = ChaCha20Poly1305::new(key.into());
39 let nonce = ChaCha20Poly1305::generate_nonce(&mut OsRng);
40
41 let ciphertext = cipher.encrypt(&nonce, plaintext.as_bytes())
42 .map_err(|e| format!("Encryption failed: {}", e))?;
43
44 let mut result = hex::encode(&nonce[..]);
46 result.push_str(&hex::encode(&ciphertext));
47 Ok(result)
48}
49
50pub fn decrypt_with_key(hex_data: &str, key: &[u8; 32]) -> Result<String, String> {
52 if hex_data.len() < 24 {
53 return Err("Ciphertext too short".to_string());
54 }
55
56 let nonce_hex = &hex_data[..24]; let ciphertext_hex = &hex_data[24..];
58
59 let nonce_bytes = hex::decode(nonce_hex)
60 .map_err(|e| format!("Invalid nonce hex: {}", e))?;
61 let ciphertext = hex::decode(ciphertext_hex)
62 .map_err(|e| format!("Invalid ciphertext hex: {}", e))?;
63
64 let nonce_arr: [u8; 12] = nonce_bytes.try_into()
65 .map_err(|_| "Invalid nonce length".to_string())?;
66 let nonce = chacha20poly1305::Nonce::from(nonce_arr);
67 let cipher = ChaCha20Poly1305::new(key.into());
68
69 let mut plaintext = cipher.decrypt(&nonce, ciphertext.as_ref())
70 .map_err(|_| "Decryption failed (wrong key or corrupted data)".to_string())?;
71
72 let result = String::from_utf8(plaintext.clone())
73 .map_err(|_| "Decrypted data is not valid UTF-8".to_string())?;
74
75 plaintext.zeroize();
76 Ok(result)
77}
78
79pub fn is_encryption_enabled() -> bool {
87 crate::state::resolve_encryption_enabled_from_db()
88}
89
90mod hex {
92 pub fn encode(bytes: &[u8]) -> String {
93 crate::simd::hex::bytes_to_hex_string(bytes)
96 }
97
98 pub fn decode(hex: &str) -> Result<Vec<u8>, String> {
99 if hex.len() % 2 != 0 {
100 return Err("Odd-length hex string".to_string());
101 }
102 crate::simd::hex::hex_string_to_bytes_checked(hex)
105 .ok_or_else(|| "Invalid hex character".to_string())
106 }
107}
108
109#[derive(Debug)]
115pub struct EncryptionParams {
116 pub key: String, pub nonce: String, }
119
120pub fn generate_encryption_params() -> EncryptionParams {
122 use rand::Rng;
123 let mut rng = rand::thread_rng();
124 let mut key: [u8; 32] = rng.gen();
125 let nonce: [u8; 16] = rng.gen();
126 let params = EncryptionParams {
127 key: hex::encode(&key),
128 nonce: hex::encode(&nonce),
129 };
130 key.iter_mut().for_each(|b| *b = 0); params
132}
133
134pub fn encrypt_data(data: &[u8], params: &EncryptionParams) -> Result<Vec<u8>, String> {
136 use aes::Aes256;
137 use aes::cipher::typenum::U16;
138 use aes_gcm::{AesGcm, AeadInPlace, KeyInit as AesKeyInit};
139
140 let key_bytes = hex::decode(¶ms.key).map_err(|e| format!("Invalid key: {}", e))?;
141 let nonce_bytes = hex::decode(¶ms.nonce).map_err(|e| format!("Invalid nonce: {}", e))?;
142
143 let cipher = AesGcm::<Aes256, U16>::new_from_slice(&key_bytes)
144 .map_err(|_| "Invalid encryption key".to_string())?;
145
146 let nonce_arr: [u8; 16] = nonce_bytes.try_into()
147 .map_err(|_| "Invalid nonce length".to_string())?;
148 let nonce = aes_gcm::Nonce::<U16>::from(nonce_arr);
149
150 let mut buffer = data.to_vec();
151 let tag = cipher.encrypt_in_place_detached(&nonce, &[], &mut buffer)
152 .map_err(|_| "Encryption failed".to_string())?;
153
154 buffer.extend_from_slice(&tag);
155 Ok(buffer)
156}
157
158pub fn decrypt_data(encrypted_data: &[u8], key_hex: &str, nonce_hex: &str) -> Result<Vec<u8>, String> {
161 use aes::Aes256;
162 use aes::cipher::typenum::U16;
163 use aes_gcm::{AesGcm, AeadInPlace, KeyInit as AesKeyInit};
164
165 if encrypted_data.len() < 16 {
166 return Err(format!("Invalid Input: encrypted data too small ({} bytes, minimum 16 bytes required for authentication tag)", encrypted_data.len()));
167 }
168
169 let key_bytes = hex::decode(key_hex).map_err(|e| format!("Invalid key: {}", e))?;
170 let nonce_bytes = hex::decode(nonce_hex).map_err(|e| format!("Invalid nonce: {}", e))?;
171
172 let (ciphertext, tag_bytes) = encrypted_data.split_at(encrypted_data.len() - 16);
173
174 let cipher = AesGcm::<Aes256, U16>::new_from_slice(&key_bytes)
175 .map_err(|_| "Invalid decryption key".to_string())?;
176
177 let nonce_arr: [u8; 16] = nonce_bytes.try_into()
178 .map_err(|_| "Invalid nonce length".to_string())?;
179 let nonce = aes_gcm::Nonce::<U16>::from(nonce_arr);
180 let tag_arr: [u8; 16] = tag_bytes.try_into()
181 .map_err(|_| "Invalid tag length".to_string())?;
182 let tag = aes_gcm::Tag::<U16>::from(tag_arr);
183
184 let mut buffer = ciphertext.to_vec();
185 cipher.decrypt_in_place_detached(&nonce, &[], &mut buffer, &tag)
186 .map_err(|e| e.to_string())?;
187
188 Ok(buffer)
189}
190
191pub fn sha256_hex(data: &[u8]) -> String {
193 use sha2::{Sha256, Digest};
194 let mut hasher = Sha256::new();
195 hasher.update(data);
196 hex::encode(&hasher.finalize())
197}
198
199pub fn attachment_identity_basis(ox: Option<&str>, nonce: &str, url: &str) -> String {
209 match ox.filter(|h| !h.is_empty()) {
210 Some(h) => h.to_string(),
211 None => {
212 let mut buf = Vec::with_capacity(nonce.len() + 1 + url.len());
213 buf.extend_from_slice(nonce.as_bytes());
214 buf.push(0);
215 buf.extend_from_slice(url.as_bytes());
216 sha256_hex(&buf)
217 }
218 }
219}
220
221pub fn mime_from_extension(ext: &str) -> &'static str {
223 match ext.to_lowercase().as_str() {
224 "png" => "image/png",
225 "jpg" | "jpeg" => "image/jpeg",
226 "gif" => "image/gif",
227 "webp" => "image/webp",
228 "svg" => "image/svg+xml",
229 "bmp" => "image/bmp",
230 "ico" => "image/x-icon",
231 "tiff" | "tif" => "image/tiff",
232 "dng" => "image/x-adobe-dng",
233 "cr2" => "image/x-canon-cr2",
234 "nef" => "image/x-nikon-nef",
235 "arw" => "image/x-sony-arw",
236 "mp4" => "video/mp4",
237 "webm" => "video/webm",
238 "mov" => "video/quicktime",
239 "avi" => "video/x-msvideo",
240 "mkv" => "video/x-matroska",
241 "flv" => "video/x-flv",
242 "wmv" => "video/x-ms-wmv",
243 "mpg" | "mpeg" => "video/mpeg",
244 "3gp" => "video/3gpp",
245 "ogv" => "video/ogg",
246 "ts" => "video/mp2t",
247 "mp3" => "audio/mpeg",
248 "ogg" => "audio/ogg",
249 "wav" => "audio/wav",
250 "flac" => "audio/flac",
251 "m4a" => "audio/mp4",
252 "aac" => "audio/aac",
253 "wma" => "audio/x-ms-wma",
254 "opus" => "audio/opus",
255 "pdf" => "application/pdf",
256 "doc" => "application/msword",
257 "docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
258 "xls" => "application/vnd.ms-excel",
259 "xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
260 "ppt" => "application/vnd.ms-powerpoint",
261 "pptx" => "application/vnd.openxmlformats-officedocument.presentationml.presentation",
262 "odt" => "application/vnd.oasis.opendocument.text",
263 "ods" => "application/vnd.oasis.opendocument.spreadsheet",
264 "odp" => "application/vnd.oasis.opendocument.presentation",
265 "rtf" => "application/rtf",
266 "txt" => "text/plain",
267 "md" => "text/markdown",
268 "csv" => "text/csv",
269 "json" => "application/json",
270 "xml" => "application/xml",
271 "yaml" | "yml" => "application/x-yaml",
272 "toml" => "application/toml",
273 "sql" => "application/sql",
274 "zip" => "application/zip",
275 "rar" => "application/vnd.rar",
276 "7z" => "application/x-7z-compressed",
277 "tar" => "application/x-tar",
278 "gz" => "application/gzip",
279 "bz2" => "application/x-bzip2",
280 "xz" => "application/x-xz",
281 "iso" => "application/x-iso9660-image",
282 "dmg" => "application/x-apple-diskimage",
283 "apk" => "application/vnd.android.package-archive",
284 "jar" => "application/java-archive",
285 "xdc" => "application/vnd.webxdc+zip",
286 "obj" => "model/obj",
287 "gltf" => "model/gltf+json",
288 "glb" => "model/gltf-binary",
289 "stl" => "model/stl",
290 "dae" => "model/vnd.collada+xml",
291 "js" => "text/javascript",
292 "py" => "text/x-python",
293 "rs" => "text/x-rust",
294 "go" => "text/x-go",
295 "java" => "text/x-java",
296 "c" => "text/x-c",
297 "cpp" => "text/x-c++",
298 "cs" => "text/x-csharp",
299 "rb" => "text/x-ruby",
300 "php" => "text/x-php",
301 "swift" => "text/x-swift",
302 "html" | "htm" => "text/html",
303 "css" => "text/css",
304 "exe" => "application/x-msdownload",
305 "msi" => "application/x-msi",
306 "ttf" => "font/ttf",
307 "otf" => "font/otf",
308 "woff" => "font/woff",
309 "woff2" => "font/woff2",
310 _ => "application/octet-stream",
311 }
312}
313
314pub fn extension_from_mime(mime: &str) -> String {
317 match mime.trim().to_lowercase().as_str() {
318 "image/png" => "png",
320 "image/jpeg" | "image/jpg" => "jpg",
321 "image/gif" => "gif",
322 "image/webp" => "webp",
323 "image/svg+xml" => "svg",
324 "image/bmp" | "image/x-ms-bmp" => "bmp",
325 "image/x-icon" | "image/vnd.microsoft.icon" => "ico",
326 "image/tiff" => "tiff",
327 "image/x-adobe-dng" => "dng",
328 "image/x-canon-cr2" => "cr2",
329 "image/x-nikon-nef" => "nef",
330 "image/x-sony-arw" => "arw",
331 "audio/wav" | "audio/x-wav" | "audio/wave" => "wav",
333 "audio/mp3" | "audio/mpeg" => "mp3",
334 "audio/flac" => "flac",
335 "audio/ogg" => "ogg",
336 "audio/mp4" => "m4a",
337 "audio/aac" | "audio/x-aac" => "aac",
338 "audio/x-ms-wma" => "wma",
339 "audio/opus" => "opus",
340 "video/mp4" => "mp4",
342 "video/webm" => "webm",
343 "video/quicktime" => "mov",
344 "video/x-msvideo" => "avi",
345 "video/x-matroska" => "mkv",
346 "video/x-flv" => "flv",
347 "video/x-ms-wmv" => "wmv",
348 "video/mpeg" => "mpg",
349 "video/3gpp" => "3gp",
350 "video/ogg" => "ogv",
351 "video/mp2t" => "ts",
352 "application/pdf" => "pdf",
354 "application/msword" => "doc",
355 "application/vnd.openxmlformats-officedocument.wordprocessingml.document" => "docx",
356 "application/vnd.ms-excel" => "xls",
357 "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" => "xlsx",
358 "application/vnd.ms-powerpoint" => "ppt",
359 "application/vnd.openxmlformats-officedocument.presentationml.presentation" => "pptx",
360 "application/vnd.oasis.opendocument.text" => "odt",
361 "application/vnd.oasis.opendocument.spreadsheet" => "ods",
362 "application/vnd.oasis.opendocument.presentation" => "odp",
363 "application/rtf" => "rtf",
364 "text/plain" => "txt",
366 "text/markdown" => "md",
367 "text/csv" => "csv",
368 "application/json" => "json",
369 "application/xml" | "text/xml" => "xml",
370 "application/x-yaml" | "text/yaml" => "yaml",
371 "application/toml" => "toml",
372 "application/sql" => "sql",
373 "application/zip" => "zip",
375 "application/x-rar-compressed" | "application/vnd.rar" => "rar",
376 "application/x-7z-compressed" => "7z",
377 "application/x-tar" => "tar",
378 "application/gzip" => "gz",
379 "application/x-bzip2" => "bz2",
380 "application/x-xz" => "xz",
381 "application/x-iso9660-image" => "iso",
382 "application/x-apple-diskimage" => "dmg",
383 "application/vnd.android.package-archive" => "apk",
384 "application/java-archive" => "jar",
385 "application/vnd.webxdc+zip" => "xdc",
386 "model/obj" => "obj",
388 "model/gltf+json" => "gltf",
389 "model/gltf-binary" => "glb",
390 "model/stl" | "application/sla" => "stl",
391 "model/vnd.collada+xml" => "dae",
392 "text/javascript" | "application/javascript" => "js",
394 "text/typescript" | "application/typescript" => "ts",
395 "text/x-python" | "application/x-python" => "py",
396 "text/x-rust" => "rs",
397 "text/x-go" => "go",
398 "text/x-java" => "java",
399 "text/x-c" => "c",
400 "text/x-c++" => "cpp",
401 "text/x-csharp" => "cs",
402 "text/x-ruby" => "rb",
403 "text/x-php" => "php",
404 "text/x-swift" => "swift",
405 "text/html" => "html",
407 "text/css" => "css",
408 "application/x-msdownload" | "application/x-dosexec" => "exe",
410 "application/x-msi" => "msi",
411 "application/x-font-ttf" | "font/ttf" => "ttf",
412 "application/x-font-otf" | "font/otf" => "otf",
413 "font/woff" => "woff",
414 "font/woff2" => "woff2",
415 _ => {
417 let lower = mime.trim().to_lowercase();
418 return lower.split('/').nth(1).unwrap_or("bin").to_string();
419 }
420 }.to_string()
421}
422
423pub fn sanitize_filename(name: &str) -> String {
426 let base = name.rsplit('/').next().unwrap_or(name);
427 let base = base.rsplit('\\').next().unwrap_or(base);
428
429 let sanitized: String = base.chars().filter(|c| {
430 !matches!(c, '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' | '\0')
431 }).collect();
432
433 let sanitized = sanitized.trim_matches(|c: char| c == '.' || c == ' ');
434
435 if sanitized.is_empty() {
436 return String::new();
437 }
438
439 if let Some(dot_pos) = sanitized.rfind('.') {
440 let stem = &sanitized[..dot_pos];
441 let ext = &sanitized[dot_pos..];
442 if stem.len() > 64 {
443 let truncated = &stem[..stem.floor_char_boundary(64)];
444 return format!("{}{}", truncated, ext);
445 }
446 } else if sanitized.len() > 64 {
447 let truncated = &sanitized[..sanitized.floor_char_boundary(64)];
448 return truncated.to_string();
449 }
450
451 sanitized.to_string()
452}
453
454pub fn resolve_unique_filename(dir: &std::path::Path, name: &str) -> std::path::PathBuf {
459 let candidate = dir.join(name);
460 if !candidate.exists() {
461 return candidate;
462 }
463
464 let stem = std::path::Path::new(name)
465 .file_stem()
466 .and_then(|s| s.to_str())
467 .unwrap_or(name);
468 let ext = std::path::Path::new(name)
469 .extension()
470 .and_then(|s| s.to_str())
471 .unwrap_or("");
472
473 let mut counter = 1u32;
474 loop {
475 let suffixed = if ext.is_empty() {
476 format!("{}-{}", stem, counter)
477 } else {
478 format!("{}-{}.{}", stem, counter, ext)
479 };
480 let candidate = dir.join(&suffixed);
481 if !candidate.exists() {
482 return candidate;
483 }
484 counter += 1;
485 }
486}
487
488pub fn decrypt_and_save_attachment(
494 encrypted_data: &[u8],
495 key: &str,
496 nonce: &str,
497 name: &str,
498 extension: &str,
499) -> Result<(std::path::PathBuf, String), String> {
500 let decrypted = if key.is_empty() || nonce.is_empty() {
504 encrypted_data.to_vec()
505 } else {
506 decrypt_data(encrypted_data, key, nonce)?
507 };
508 let file_hash = sha256_hex(&decrypted);
509
510 let dir = crate::db::get_download_dir();
511 std::fs::create_dir_all(&dir).map_err(|e| format!("Failed to create directory: {}", e))?;
512
513 let target_name = if name.is_empty() {
514 format!("{}.{}", file_hash, extension)
515 } else {
516 name.to_string()
517 };
518
519 let candidate = dir.join(&target_name);
521 let already_exists = candidate.exists()
522 && std::fs::metadata(&candidate).map(|m| m.len() == decrypted.len() as u64).unwrap_or(false)
523 && std::fs::read(&candidate).map(|b| sha256_hex(&b) == file_hash).unwrap_or(false);
524
525 if already_exists {
526 return Ok((candidate, file_hash));
527 }
528
529 let file_path = resolve_unique_filename(&dir, &target_name);
530
531 let tmp_path = dir.join(format!(".{}.{}.tmp", file_hash, extension));
533 std::fs::write(&tmp_path, &decrypted).map_err(|e| format!("Failed to write file: {}", e))?;
534 std::fs::rename(&tmp_path, &file_path).map_err(|e| format!("Failed to rename file: {}", e))?;
535
536 Ok((file_path, file_hash))
537}
538
539pub fn format_bytes(bytes: u64) -> String {
541 const KB: f64 = 1024.0;
542 const MB: f64 = KB * 1024.0;
543 const GB: f64 = MB * 1024.0;
544
545 if bytes < KB as u64 {
546 format!("{} B", bytes)
547 } else if bytes < MB as u64 {
548 format!("{:.1} KB", bytes as f64 / KB)
549 } else if bytes < GB as u64 {
550 format!("{:.1} MB", bytes as f64 / MB)
551 } else {
552 format!("{:.1} GB", bytes as f64 / GB)
553 }
554}
555
556pub fn is_image_mime(mime: &str) -> bool {
558 mime.trim().starts_with("image/")
559}
560
561pub fn mime_from_extension_safe(extension: &str, image_only: bool) -> Result<String, String> {
563 let mime = mime_from_extension(extension).to_string();
564 if image_only && !is_image_mime(&mime) {
565 return Err(mime);
566 }
567 Ok(mime)
568}
569
570pub fn mime_from_magic_bytes(bytes: &[u8]) -> &'static str {
574 if bytes.len() < 4 {
575 return "application/octet-stream";
576 }
577 match bytes[0] {
578 0x89 if bytes[1..4] == [0x50, 0x4E, 0x47] => "image/png",
579 0xFF if bytes[1..3] == [0xD8, 0xFF] => "image/jpeg",
580 b'G' if bytes.len() >= 6 && (bytes[..6] == *b"GIF87a" || bytes[..6] == *b"GIF89a") => "image/gif",
581 b'R' if bytes.len() > 12 && bytes[..4] == *b"RIFF" && bytes[8..12] == *b"WEBP" => "image/webp",
582 0x49 if bytes[1..4] == [0x49, 0x2A, 0x00] => "image/tiff",
583 0x4D if bytes[1..4] == [0x4D, 0x00, 0x2A] => "image/tiff",
584 0x00 if bytes[1..4] == [0x00, 0x01, 0x00] => "image/x-icon",
585 b'<' if bytes.starts_with(b"<?xml") || bytes.starts_with(b"<svg") => "image/svg+xml",
586 _ => "application/octet-stream",
587 }
588}
589
590use rand::Rng;
595use chacha20poly1305::Nonce;
596
597#[inline]
600pub fn looks_encrypted(s: &str) -> bool {
601 if s.len() < 56 { return false; }
602 is_all_lowercase_hex(s.as_bytes())
603}
604
605#[cfg(target_arch = "aarch64")]
607#[inline]
608fn is_all_lowercase_hex(bytes: &[u8]) -> bool {
609 use std::arch::aarch64::*;
610 unsafe {
611 let mut i = 0;
612 while i + 16 <= bytes.len() {
613 let chunk = vld1q_u8(bytes.as_ptr().add(i));
614 let is_digit = vandq_u8(vcgeq_u8(chunk, vdupq_n_u8(b'0')),
615 vcleq_u8(chunk, vdupq_n_u8(b'9')));
616 let is_af = vandq_u8(vcgeq_u8(chunk, vdupq_n_u8(b'a')),
617 vcleq_u8(chunk, vdupq_n_u8(b'f')));
618 if vminvq_u8(vorrq_u8(is_digit, is_af)) == 0 { return false; }
619 i += 16;
620 }
621 while i < bytes.len() {
622 let b = bytes[i];
623 if !matches!(b, b'0'..=b'9' | b'a'..=b'f') { return false; }
624 i += 1;
625 }
626 }
627 true
628}
629
630#[cfg(target_arch = "x86_64")]
632#[inline]
633fn is_all_lowercase_hex(bytes: &[u8]) -> bool {
634 use std::arch::x86_64::*;
635 unsafe {
636 let mut i = 0;
637 while i + 16 <= bytes.len() {
638 let chunk = _mm_loadu_si128(bytes.as_ptr().add(i) as *const __m128i);
639 let is_digit = _mm_cmpeq_epi8(
640 _mm_subs_epu8(_mm_sub_epi8(chunk, _mm_set1_epi8(b'0' as i8)), _mm_set1_epi8(9)),
641 _mm_setzero_si128());
642 let is_af = _mm_cmpeq_epi8(
643 _mm_subs_epu8(_mm_sub_epi8(chunk, _mm_set1_epi8(b'a' as i8)), _mm_set1_epi8(5)),
644 _mm_setzero_si128());
645 if _mm_movemask_epi8(_mm_or_si128(is_digit, is_af)) != 0xFFFF { return false; }
646 i += 16;
647 }
648 while i < bytes.len() {
649 let b = bytes[i];
650 if !matches!(b, b'0'..=b'9' | b'a'..=b'f') { return false; }
651 i += 1;
652 }
653 }
654 true
655}
656
657#[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
659#[inline]
660fn is_all_lowercase_hex(bytes: &[u8]) -> bool {
661 const IS_LOWER_HEX: [bool; 256] = {
662 let mut t = [false; 256];
663 t[b'0' as usize] = true; t[b'1' as usize] = true; t[b'2' as usize] = true;
664 t[b'3' as usize] = true; t[b'4' as usize] = true; t[b'5' as usize] = true;
665 t[b'6' as usize] = true; t[b'7' as usize] = true; t[b'8' as usize] = true;
666 t[b'9' as usize] = true; t[b'a' as usize] = true; t[b'b' as usize] = true;
667 t[b'c' as usize] = true; t[b'd' as usize] = true; t[b'e' as usize] = true;
668 t[b'f' as usize] = true;
669 t
670 };
671 bytes.iter().all(|&b| IS_LOWER_HEX[b as usize])
672}
673
674pub async fn maybe_encrypt_inner(mut input: String, password: Option<String>) -> String {
677 let mut key: [u8; 32] = if password.is_none() {
678 crate::state::ENCRYPTION_KEY.get().expect("Encryption key must be set")
679 } else {
680 hash_pass(&password.unwrap()).await
681 };
682
683 let mut rng = rand::thread_rng();
684 let nonce_bytes: [u8; 12] = rng.gen();
685
686 let cipher = ChaCha20Poly1305::new_from_slice(&key)
687 .expect("Key should be valid");
688 let nonce: Nonce = nonce_bytes.into();
689
690 let ciphertext = cipher
691 .encrypt(&nonce, input.as_bytes())
692 .expect("Encryption should not fail");
693 input.zeroize();
694
695 let mut buffer = Vec::with_capacity(nonce_bytes.len() + ciphertext.len());
696 buffer.extend_from_slice(&nonce_bytes);
697 buffer.extend_from_slice(&ciphertext);
698
699 if !crate::state::ENCRYPTION_KEY.has_key() {
700 crate::state::ENCRYPTION_KEY.set(key, &[&crate::state::MY_SECRET_KEY]);
701 }
702
703 key.zeroize();
704
705 crate::simd::hex::bytes_to_hex_string(&buffer)
706}
707
708pub async fn maybe_decrypt_inner(ciphertext: String, password: Option<String>) -> Result<String, ()> {
711 let has_password = password.is_some();
712
713 let mut key: [u8; 32] = if let Some(pass) = password {
714 hash_pass(&pass).await
715 } else {
716 match crate::state::ENCRYPTION_KEY.get() {
717 Some(k) => k,
718 None => return Err(()),
719 }
720 };
721
722 let encrypted_data = crate::simd::hex::hex_string_to_bytes(ciphertext.as_str());
723 if encrypted_data.len() < 12 {
724 key.zeroize();
725 return Err(());
726 }
727
728 let (nonce_bytes, actual_ciphertext) = encrypted_data.split_at(12);
729
730 let cipher = match ChaCha20Poly1305::new_from_slice(&key) {
731 Ok(c) => c,
732 Err(_) => { key.zeroize(); return Err(()) }
733 };
734
735 let nonce_arr: [u8; 12] = match nonce_bytes.try_into() {
736 Ok(n) => n,
737 Err(_) => { key.zeroize(); return Err(()) }
738 };
739 let nonce: Nonce = nonce_arr.into();
740 let plaintext = match cipher.decrypt(&nonce, actual_ciphertext) {
741 Ok(pt) => pt,
742 Err(_) => { key.zeroize(); return Err(()) }
743 };
744
745 if has_password && !crate::state::ENCRYPTION_KEY.has_key() {
746 crate::state::ENCRYPTION_KEY.set(key, &[&crate::state::MY_SECRET_KEY]);
747 }
748
749 key.zeroize();
750
751 unsafe { Ok(String::from_utf8_unchecked(plaintext)) }
753}
754
755pub async fn maybe_encrypt(input: String) -> String {
757 if crate::state::is_encryption_enabled_fast() {
758 maybe_encrypt_inner(input, None).await
759 } else {
760 input
761 }
762}
763
764pub async fn maybe_decrypt(input: String) -> Result<String, ()> {
767 if crate::state::is_encryption_enabled_fast() {
768 match maybe_decrypt_inner(input.clone(), None).await {
769 Ok(decrypted) => Ok(decrypted),
770 Err(_) => {
771 if looks_encrypted(&input) { Err(()) } else { Ok(input) }
772 }
773 }
774 } else {
775 if looks_encrypted(&input) {
776 match maybe_decrypt_inner(input.clone(), None).await {
777 Ok(decrypted) => Ok(decrypted),
778 Err(_) => Ok(input),
779 }
780 } else {
781 Ok(input)
782 }
783 }
784}
785
786pub fn encrypt_blob_with_key(plaintext: &[u8], key: &[u8; 32]) -> Result<Vec<u8>, String> {
799 use rand::Rng;
800 let cipher = ChaCha20Poly1305::new_from_slice(key).map_err(|e| e.to_string())?;
801 let nonce_bytes: [u8; 12] = rand::thread_rng().gen();
802 let nonce = chacha20poly1305::Nonce::from(nonce_bytes);
803 let ct = cipher
804 .encrypt(&nonce, plaintext)
805 .map_err(|e| format!("blob encryption failed: {}", e))?;
806 let mut out = Vec::with_capacity(12 + ct.len());
807 out.extend_from_slice(&nonce_bytes);
808 out.extend_from_slice(&ct);
809 Ok(out)
810}
811
812pub fn decrypt_blob_with_key(stored: &[u8], key: &[u8; 32]) -> Result<Vec<u8>, String> {
814 if stored.len() < 12 + 16 {
815 return Err("blob too short to be ciphertext".to_string());
816 }
817 let cipher = ChaCha20Poly1305::new_from_slice(key).map_err(|e| e.to_string())?;
818 let (nonce_bytes, ct) = stored.split_at(12);
819 let nonce_arr: [u8; 12] = nonce_bytes.try_into().map_err(|_| "bad nonce".to_string())?;
820 let nonce = chacha20poly1305::Nonce::from(nonce_arr);
821 cipher
822 .decrypt(&nonce, ct)
823 .map_err(|_| "blob decryption failed (wrong key or corrupted)".to_string())
824}
825
826pub fn maybe_encrypt_blob(plaintext: &[u8]) -> Result<Vec<u8>, String> {
829 if !crate::state::is_encryption_enabled_fast() {
830 return Ok(plaintext.to_vec());
831 }
832 let mut key = crate::state::ENCRYPTION_KEY
833 .get()
834 .ok_or_else(|| "encryption enabled but key vault is empty".to_string())?;
835 let out = encrypt_blob_with_key(plaintext, &key);
836 key.zeroize();
837 out
838}
839
840pub fn maybe_decrypt_blob(stored: &[u8]) -> Vec<u8> {
844 if stored.len() == 32 {
846 return stored.to_vec();
847 }
848 match crate::state::ENCRYPTION_KEY.get() {
849 Some(mut key) => {
850 let out = decrypt_blob_with_key(stored, &key).unwrap_or_else(|_| stored.to_vec());
851 key.zeroize();
852 out
853 }
854 None => stored.to_vec(),
855 }
856}
857
858pub fn maybe_encrypt_text(plaintext: &str) -> Result<String, String> {
861 if !crate::state::is_encryption_enabled_fast() {
862 return Ok(plaintext.to_string());
863 }
864 let mut key = crate::state::ENCRYPTION_KEY
865 .get()
866 .ok_or_else(|| "encryption enabled but key vault is empty".to_string())?;
867 let out = encrypt_with_key(plaintext, &key);
868 key.zeroize();
869 out
870}
871
872pub fn maybe_decrypt_text(stored: &str) -> String {
875 if !looks_encrypted(stored) {
876 return stored.to_string();
877 }
878 match crate::state::ENCRYPTION_KEY.get() {
879 Some(mut key) => {
880 let out = decrypt_with_key(stored, &key).unwrap_or_else(|_| stored.to_string());
881 key.zeroize();
882 out
883 }
884 None => stored.to_string(),
885 }
886}
887
888#[cfg(test)]
889mod at_rest_tests {
890 use super::*;
891
892 #[test]
893 fn blob_roundtrip_with_explicit_key() {
894 let key = [7u8; 32];
895 let secret = [0x42u8; 32];
896 let ct = encrypt_blob_with_key(&secret, &key).unwrap();
897 assert_eq!(ct.len(), 12 + 32 + 16, "nonce + ciphertext + tag");
898 assert_ne!(&ct[12..44], &secret[..], "ciphertext must not equal plaintext");
899 assert_eq!(decrypt_blob_with_key(&ct, &key).unwrap(), secret.to_vec());
900 }
901
902 #[test]
903 fn blob_wrong_key_fails() {
904 let ct = encrypt_blob_with_key(&[1u8; 32], &[7u8; 32]).unwrap();
905 assert!(decrypt_blob_with_key(&ct, &[9u8; 32]).is_err());
906 }
907
908 #[test]
909 fn encrypted_text_is_always_detected_as_encrypted() {
910 let key = [3u8; 32];
913 for s in ["", "[]", "{}", "a"] {
914 let ct = encrypt_with_key(s, &key).unwrap();
915 assert!(looks_encrypted(&ct), "ciphertext for {:?} must look encrypted", s);
916 assert_eq!(decrypt_with_key(&ct, &key).unwrap(), s);
917 }
918 }
919}
920
921pub use crate::simd::image::nearest_neighbor_downsample_rgba;
927
928pub fn generate_thumbhash_from_rgba(pixels: &[u8], width: u32, height: u32) -> Option<String> {
933 use fast_thumbhash::{rgba_to_thumb_hash, base91_encode};
934
935 const MAX_DIM: u32 = 100;
936
937 let (thumb_w, thumb_h) = if width <= MAX_DIM && height <= MAX_DIM {
938 (width, height)
939 } else if width > height {
940 (MAX_DIM, (MAX_DIM * height / width).max(1))
941 } else {
942 ((MAX_DIM * width / height).max(1), MAX_DIM)
943 };
944
945 let thumbnail = if thumb_w == width && thumb_h == height {
946 pixels.to_vec()
947 } else {
948 nearest_neighbor_downsample_rgba(pixels, width, height, thumb_w, thumb_h)
949 };
950
951 let hash = rgba_to_thumb_hash(thumb_w as usize, thumb_h as usize, &thumbnail);
952 Some(base91_encode(&hash))
953}
954
955pub fn decode_image_bounded(bytes: &[u8]) -> Result<image::DynamicImage, String> {
965 use image::ImageDecoder;
966 let mut reader = image::ImageReader::new(std::io::Cursor::new(bytes))
967 .with_guessed_format()
968 .map_err(|e| format!("image format: {e}"))?;
969 reader.limits(bounded_image_limits());
970 let mut decoder = reader.into_decoder().map_err(|e| format!("image decode: {e}"))?;
971 let orientation = decoder.orientation().map_err(|e| format!("image orientation: {e}"))?;
973 let mut img = image::DynamicImage::from_decoder(decoder)
974 .map_err(|e| format!("image decode: {e}"))?;
975 img.apply_orientation(orientation);
976 Ok(img)
977}
978
979pub fn bounded_image_limits() -> image::Limits {
982 let mut limits = image::Limits::default();
983 limits.max_image_width = Some(16_384);
984 limits.max_image_height = Some(16_384);
985 limits.max_alloc = Some(256 * 1024 * 1024);
986 limits
987}
988
989pub fn generate_image_metadata(file_bytes: &[u8]) -> Option<crate::types::ImageMetadata> {
994 let img = decode_image_bounded(file_bytes).ok()?;
995 let width = img.width();
996 let height = img.height();
997
998 let rgba = img.to_rgba8();
999 let thumbhash = generate_thumbhash_from_rgba(rgba.as_raw(), width, height)?;
1000
1001 Some(crate::types::ImageMetadata {
1002 thumbhash,
1003 width,
1004 height,
1005 })
1006}
1007
1008#[cfg(test)]
1009mod tests {
1010 use super::*;
1011
1012 fn test_key() -> [u8; 32] {
1017 [0x42u8; 32]
1018 }
1019
1020 fn alt_key() -> [u8; 32] {
1021 [0x99u8; 32]
1022 }
1023
1024 #[test]
1025 fn encrypt_decrypt_roundtrip_simple() {
1026 let key = test_key();
1027 let plaintext = "hello world";
1028 let encrypted = encrypt_with_key(plaintext, &key).expect("encryption should succeed");
1029 let decrypted = decrypt_with_key(&encrypted, &key).expect("decryption should succeed");
1030 assert_eq!(decrypted, plaintext, "roundtrip should preserve plaintext");
1031 }
1032
1033 #[test]
1034 fn encrypt_decrypt_100_random_strings() {
1035 use rand::Rng;
1036 let key = test_key();
1037 let mut rng = rand::thread_rng();
1038 for i in 0..100 {
1039 let len = rng.gen_range(1..=200);
1040 let s: String = (0..len).map(|_| rng.gen_range(0x20u8..0x7f) as char).collect();
1041 let enc = encrypt_with_key(&s, &key)
1042 .unwrap_or_else(|e| panic!("encryption failed on iteration {}: {}", i, e));
1043 let dec = decrypt_with_key(&enc, &key)
1044 .unwrap_or_else(|e| panic!("decryption failed on iteration {}: {}", i, e));
1045 assert_eq!(dec, s, "roundtrip failed on iteration {}", i);
1046 }
1047 }
1048
1049 #[test]
1050 fn encrypt_decrypt_empty_string() {
1051 let key = test_key();
1052 let encrypted = encrypt_with_key("", &key).expect("encrypting empty string should succeed");
1053 let decrypted = decrypt_with_key(&encrypted, &key).expect("decrypting empty string should succeed");
1054 assert_eq!(decrypted, "", "empty string roundtrip should produce empty string");
1055 }
1056
1057 #[test]
1058 fn encrypt_decrypt_large_string() {
1059 let key = test_key();
1060 let plaintext = "A".repeat(10 * 1024); let encrypted = encrypt_with_key(&plaintext, &key).expect("encrypting 10KB should succeed");
1062 let decrypted = decrypt_with_key(&encrypted, &key).expect("decrypting 10KB should succeed");
1063 assert_eq!(decrypted, plaintext, "10KB roundtrip should preserve content");
1064 }
1065
1066 #[test]
1067 fn decrypt_with_wrong_key_fails() {
1068 let key = test_key();
1069 let wrong_key = alt_key();
1070 let encrypted = encrypt_with_key("secret data", &key).expect("encryption should succeed");
1071 let result = decrypt_with_key(&encrypted, &wrong_key);
1072 assert!(result.is_err(), "decryption with wrong key should fail");
1073 }
1074
1075 #[test]
1076 fn decrypt_corrupted_ciphertext_fails() {
1077 let key = test_key();
1078 let mut encrypted = encrypt_with_key("secret data", &key).expect("encryption should succeed");
1079 let bytes: Vec<u8> = encrypted.bytes().collect();
1081 if bytes.len() > 30 {
1082 let mut chars: Vec<char> = encrypted.chars().collect();
1083 chars[30] = if chars[30] == '0' { 'f' } else { '0' };
1085 encrypted = chars.into_iter().collect();
1086 }
1087 let result = decrypt_with_key(&encrypted, &key);
1088 assert!(result.is_err(), "decryption of corrupted ciphertext should fail");
1089 }
1090
1091 #[test]
1092 fn different_keys_produce_different_ciphertext() {
1093 let key1 = test_key();
1094 let key2 = alt_key();
1095 let plaintext = "same plaintext";
1096 let enc1 = encrypt_with_key(plaintext, &key1).expect("enc1 should succeed");
1097 let enc2 = encrypt_with_key(plaintext, &key2).expect("enc2 should succeed");
1098 assert_ne!(enc1, enc2, "different keys should produce different ciphertext");
1100 }
1101
1102 #[test]
1103 fn nonce_is_always_different() {
1104 let key = test_key();
1105 let plaintext = "same string encrypted twice";
1106 let enc1 = encrypt_with_key(plaintext, &key).expect("enc1 should succeed");
1107 let enc2 = encrypt_with_key(plaintext, &key).expect("enc2 should succeed");
1108 let nonce1 = &enc1[..24];
1110 let nonce2 = &enc2[..24];
1111 assert_ne!(nonce1, nonce2, "nonces should differ between encryptions of the same plaintext");
1112 }
1113
1114 #[test]
1115 fn unicode_content_preserved() {
1116 let key = test_key();
1117 let plaintext = "Hello \u{1F600} \u{1F4A9} \u{1F30D} \u{00E9}\u{00E0}\u{00FC} \u{4E16}\u{754C} \u{0410}\u{0411}\u{0412}";
1118 let encrypted = encrypt_with_key(plaintext, &key).expect("encrypting unicode should succeed");
1119 let decrypted = decrypt_with_key(&encrypted, &key).expect("decrypting unicode should succeed");
1120 assert_eq!(decrypted, plaintext, "unicode content should be preserved through encrypt/decrypt");
1121 }
1122
1123 #[test]
1124 fn decrypt_too_short_ciphertext_fails() {
1125 let key = test_key();
1126 let result = decrypt_with_key("abcdef", &key);
1127 assert!(result.is_err(), "ciphertext shorter than 24 hex chars should fail");
1128 assert!(result.unwrap_err().contains("too short"), "error should mention too short");
1129 }
1130
1131 #[test]
1132 fn encrypt_output_is_hex_encoded() {
1133 let key = test_key();
1134 let encrypted = encrypt_with_key("test", &key).expect("encryption should succeed");
1135 assert!(encrypted.chars().all(|c| c.is_ascii_hexdigit()),
1136 "encrypted output should be entirely hex characters");
1137 }
1138
1139 #[test]
1140 fn encrypt_output_has_correct_structure() {
1141 let key = test_key();
1142 let encrypted = encrypt_with_key("test", &key).expect("encryption should succeed");
1143 assert!(encrypted.len() > 24,
1145 "encrypted output should have nonce (24 hex chars) plus ciphertext");
1146 assert_eq!(encrypted.len() % 2, 0,
1148 "encrypted output length should be even (hex pairs)");
1149 }
1150
1151 #[test]
1152 fn encrypt_decrypt_special_characters() {
1153 let key = test_key();
1154 let plaintext = r#"!@#$%^&*()_+-=[]{}|;':",.<>?/\`~"#;
1155 let encrypted = encrypt_with_key(plaintext, &key).expect("encrypting special chars should succeed");
1156 let decrypted = decrypt_with_key(&encrypted, &key).expect("decrypting special chars should succeed");
1157 assert_eq!(decrypted, plaintext, "special characters should survive roundtrip");
1158 }
1159
1160 #[test]
1161 fn encrypt_decrypt_newlines_and_whitespace() {
1162 let key = test_key();
1163 let plaintext = "line1\nline2\r\nline3\ttab\0null";
1164 let encrypted = encrypt_with_key(plaintext, &key).expect("encrypting whitespace should succeed");
1165 let decrypted = decrypt_with_key(&encrypted, &key).expect("decrypting whitespace should succeed");
1166 assert_eq!(decrypted, plaintext, "whitespace and control chars should survive roundtrip");
1167 }
1168
1169 #[test]
1170 fn decrypt_invalid_hex_in_nonce_fails() {
1171 let key = test_key();
1172 let bad = "zzzzzzzzzzzzzzzzzzzzzzzz0000000000000000";
1174 let result = decrypt_with_key(bad, &key);
1175 assert!(result.is_err(), "invalid hex in nonce should fail");
1176 }
1177
1178 #[test]
1179 fn decrypt_invalid_hex_in_ciphertext_fails() {
1180 let key = test_key();
1181 let bad = "000000000000000000000000gggggggg";
1183 let result = decrypt_with_key(bad, &key);
1184 assert!(result.is_err(), "invalid hex in ciphertext should fail");
1185 }
1186
1187 #[test]
1192 fn hex_encode_decode_roundtrip() {
1193 let data = vec![0x00, 0x01, 0xff, 0x80, 0x7f];
1194 let encoded = hex::encode(&data);
1195 let decoded = hex::decode(&encoded).expect("decode should succeed");
1196 assert_eq!(decoded, data, "hex encode/decode roundtrip should preserve bytes");
1197 }
1198
1199 #[test]
1200 fn hex_decode_odd_length_error() {
1201 let result = hex::decode("abc");
1202 assert!(result.is_err(), "odd-length hex string should fail");
1203 assert!(result.unwrap_err().contains("Odd-length"), "error should mention odd-length");
1204 }
1205
1206 #[test]
1207 fn hex_decode_invalid_hex_error() {
1208 let result = hex::decode("zzzz");
1209 assert!(result.is_err(), "invalid hex characters should fail");
1210 }
1211
1212 #[test]
1213 fn hex_encode_empty() {
1214 assert_eq!(hex::encode(&[]), "", "encoding empty bytes should produce empty string");
1215 }
1216
1217 #[test]
1218 fn hex_decode_empty() {
1219 let decoded = hex::decode("").expect("decoding empty string should succeed");
1220 assert!(decoded.is_empty(), "decoding empty string should produce empty vec");
1221 }
1222
1223 #[tokio::test]
1228 async fn hash_pass_deterministic() {
1229 let key1 = hash_pass("my_password").await;
1230 let key2 = hash_pass("my_password").await;
1231 assert_eq!(key1, key2, "same password should always produce the same key");
1232 }
1233
1234 #[tokio::test]
1235 async fn hash_pass_different_passwords_different_keys() {
1236 let key1 = hash_pass("password_one").await;
1237 let key2 = hash_pass("password_two").await;
1238 assert_ne!(key1, key2, "different passwords should produce different keys");
1239 }
1240
1241 #[tokio::test]
1242 async fn hash_pass_output_is_32_bytes() {
1243 let key = hash_pass("test_password").await;
1244 assert_eq!(key.len(), 32, "hash_pass should produce exactly 32 bytes");
1245 assert!(key.iter().any(|&b| b != 0), "hash output should not be all zeros");
1247 }
1248
1249 #[test]
1250 fn generate_thumbhash_from_bytes_basic() {
1251 let pixels: Vec<u8> = vec![
1253 255, 0, 0, 255, 255, 0, 0, 255,
1254 255, 0, 0, 255, 255, 0, 0, 255,
1255 ];
1256 let result = super::generate_thumbhash_from_rgba(&pixels, 2, 2);
1257 assert!(result.is_some());
1258 assert!(!result.unwrap().is_empty());
1259 }
1260
1261 #[test]
1262 fn generate_image_metadata_from_bytes() {
1263 let img = image::RgbaImage::from_pixel(4, 4, image::Rgba([0, 0, 255, 255]));
1265 let mut buf = Vec::new();
1266 let mut cursor = std::io::Cursor::new(&mut buf);
1267 img.write_to(&mut cursor, image::ImageFormat::Png).unwrap();
1268
1269 let meta = super::generate_image_metadata(&buf);
1270 assert!(meta.is_some());
1271 let meta = meta.unwrap();
1272 assert_eq!(meta.width, 4);
1273 assert_eq!(meta.height, 4);
1274 assert!(!meta.thumbhash.is_empty());
1275 }
1276
1277 #[test]
1278 fn generate_image_metadata_non_image() {
1279 let text_bytes = b"this is not an image";
1280 let meta = super::generate_image_metadata(text_bytes);
1281 assert!(meta.is_none());
1282 }
1283}