1pub mod chachapoly;
2pub mod gcm;
3pub mod guarded_key;
4pub mod sha256;
5pub mod stream;
6pub use chachapoly::{wipe, wipe_vec};
7pub use guarded_key::GuardedKey;
8
9mod signer;
10pub use signer::GuardedSigner;
11
12use argon2::Argon2;
13use chachapoly::{ChaCha20Poly1305, NONCE_LEN, TAG_LEN};
14use zeroize::Zeroize;
15
16pub async fn hash_pass(password: &str) -> [u8; 32] {
19 let password = password.to_string();
20 crate::rt::spawn_blocking(move || {
21 let salt = b"vectorvectovectvecvev";
22 let mut output = [0u8; 32];
23
24 let params = argon2::Params::new(
25 150_000, 10, 1, Some(32),
29 ).unwrap();
30
31 let argon2 = Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params);
32 argon2.hash_password_into(password.as_bytes(), salt, &mut output).unwrap();
33
34 output
35 }).await.unwrap()
36}
37
38fn seal_framed(plaintext: &[u8], key: &[u8; 32]) -> Result<Vec<u8>, chachapoly::Error> {
40 use rand::Rng;
41 let nonce: [u8; NONCE_LEN] = rand::thread_rng().gen();
42 let mut out = Vec::with_capacity(NONCE_LEN + plaintext.len() + TAG_LEN);
43 out.extend_from_slice(&nonce);
44 out.extend_from_slice(plaintext);
45 let tag = ChaCha20Poly1305::new(key).seal_in_place(&nonce, &[], &mut out[NONCE_LEN..])?;
46 out.extend_from_slice(&tag);
47 Ok(out)
48}
49
50fn open_framed(framed: Vec<u8>, key: &[u8; 32]) -> Option<Vec<u8>> {
52 open_framed_with(&ChaCha20Poly1305::new(key), framed)
53}
54
55fn open_framed_with(cipher: &ChaCha20Poly1305, mut framed: Vec<u8>) -> Option<Vec<u8>> {
56 let len = cipher.open_framed_in_place(&mut framed).ok()?.len();
57 framed.copy_within(NONCE_LEN..NONCE_LEN + len, 0);
58 framed.truncate(len);
59 Some(framed)
60}
61
62fn open_hex(hex_data: &str, key: &[u8; 32]) -> Option<String> {
64 open_hex_with(&ChaCha20Poly1305::new(key), hex_data)
65}
66
67fn open_hex_with(cipher: &ChaCha20Poly1305, hex_data: &str) -> Option<String> {
68 let plaintext = open_framed_with(cipher, crate::simd::hex::hex_string_to_bytes(hex_data))?;
69 Some(unsafe { String::from_utf8_unchecked(plaintext) })
71}
72
73pub fn encrypt_with_key(plaintext: &str, key: &[u8; 32]) -> Result<String, String> {
75 let framed = seal_framed(plaintext.as_bytes(), key).map_err(|e| format!("Encryption failed: {}", e))?;
76 Ok(crate::simd::hex::bytes_to_hex_string(&framed))
77}
78
79pub fn decrypt_with_key(hex_data: &str, key: &[u8; 32]) -> Result<String, String> {
81 if hex_data.len() < 2 * NONCE_LEN {
82 return Err("Ciphertext too short".to_string());
83 }
84 open_hex(hex_data, key).ok_or_else(|| "Decryption failed (wrong key or corrupted data)".to_string())
85}
86
87pub fn is_encryption_enabled() -> bool {
95 crate::state::resolve_encryption_enabled_from_db()
96}
97
98mod hex {
100 pub fn encode(bytes: &[u8]) -> String {
101 crate::simd::hex::bytes_to_hex_string(bytes)
104 }
105
106 pub fn decode(hex: &str) -> Result<Vec<u8>, String> {
107 if !hex.len().is_multiple_of(2) {
108 return Err("Odd-length hex string".to_string());
109 }
110 crate::simd::hex::hex_string_to_bytes_checked(hex)
113 .ok_or_else(|| "Invalid hex character".to_string())
114 }
115}
116
117#[derive(Debug)]
123pub struct EncryptionParams {
124 pub key: String, pub nonce: String, }
127
128pub fn generate_encryption_params() -> EncryptionParams {
130 use rand::Rng;
131 let mut rng = rand::thread_rng();
132 let mut key: [u8; 32] = rng.gen();
133 let nonce: [u8; 16] = rng.gen();
134 let params = EncryptionParams {
135 key: hex::encode(&key),
136 nonce: hex::encode(&nonce),
137 };
138 key.iter_mut().for_each(|b| *b = 0); params
140}
141
142pub(crate) fn attachment_params(key_hex: &str, nonce_hex: &str, key_error: &str) -> Result<([u8; 32], [u8; 16]), String> {
145 let mut key_bytes = hex::decode(key_hex).map_err(|e| format!("Invalid key: {}", e))?;
146 let nonce_bytes = hex::decode(nonce_hex).map_err(|e| format!("Invalid nonce: {}", e))?;
147 let key: Result<[u8; 32], _> = key_bytes.as_slice().try_into();
148 key_bytes.zeroize();
149 let key = key.map_err(|_| key_error.to_string())?;
150 let nonce: [u8; 16] = nonce_bytes.try_into().map_err(|_| "Invalid nonce length".to_string())?;
151 Ok((key, nonce))
152}
153
154pub fn encrypt_data(data: &[u8], params: &EncryptionParams) -> Result<Vec<u8>, String> {
156 let (mut key, nonce) = attachment_params(¶ms.key, ¶ms.nonce, "Invalid encryption key")?;
157 let mut buffer = Vec::with_capacity(data.len() + gcm::TAG_LEN);
160 buffer.extend_from_slice(data);
161 let tag = gcm::seal(&key, &nonce, &mut buffer);
162 key.zeroize();
163 buffer.extend_from_slice(&tag);
164 Ok(buffer)
165}
166
167pub fn decrypt_data(encrypted_data: &[u8], key_hex: &str, nonce_hex: &str) -> Result<Vec<u8>, String> {
170 decrypt_data_owned(encrypted_data.to_vec(), key_hex, nonce_hex)
171}
172
173pub fn decrypt_data_owned(mut data: Vec<u8>, key_hex: &str, nonce_hex: &str) -> Result<Vec<u8>, String> {
176 if data.len() < gcm::TAG_LEN {
177 return Err(format!("Invalid Input: encrypted data too small ({} bytes, minimum 16 bytes required for authentication tag)", data.len()));
178 }
179 let (mut key, nonce) = attachment_params(key_hex, nonce_hex, "Invalid decryption key")?;
180 let body = data.len() - gcm::TAG_LEN;
181 let tag: [u8; gcm::TAG_LEN] = data[body..].try_into().unwrap();
182 data.truncate(body);
183 let opened = gcm::open(&key, &nonce, &mut data, &tag);
184 key.zeroize();
185 opened.map_err(|_| "aead::Error".to_string())?;
186 Ok(data)
187}
188
189pub fn sha256_hex(data: &[u8]) -> String {
191 hex::encode(&sha256::digest(data))
192}
193
194pub fn attachment_identity_basis(ox: Option<&str>, nonce: &str, url: &str) -> String {
204 match ox.filter(|h| !h.is_empty()) {
205 Some(h) => h.to_string(),
206 None => {
207 let mut buf = Vec::with_capacity(nonce.len() + 1 + url.len());
208 buf.extend_from_slice(nonce.as_bytes());
209 buf.push(0);
210 buf.extend_from_slice(url.as_bytes());
211 sha256_hex(&buf)
212 }
213 }
214}
215
216pub fn mime_from_extension(ext: &str) -> &'static str {
218 match ext.to_lowercase().as_str() {
219 "png" => "image/png",
220 "jpg" | "jpeg" => "image/jpeg",
221 "gif" => "image/gif",
222 "webp" => "image/webp",
223 "svg" => "image/svg+xml",
224 "bmp" => "image/bmp",
225 "ico" => "image/x-icon",
226 "tiff" | "tif" => "image/tiff",
227 "dng" => "image/x-adobe-dng",
228 "cr2" => "image/x-canon-cr2",
229 "nef" => "image/x-nikon-nef",
230 "arw" => "image/x-sony-arw",
231 "mp4" => "video/mp4",
232 "webm" => "video/webm",
233 "mov" => "video/quicktime",
234 "avi" => "video/x-msvideo",
235 "mkv" => "video/x-matroska",
236 "flv" => "video/x-flv",
237 "wmv" => "video/x-ms-wmv",
238 "mpg" | "mpeg" => "video/mpeg",
239 "3gp" => "video/3gpp",
240 "ogv" => "video/ogg",
241 "ts" => "video/mp2t",
242 "mp3" => "audio/mpeg",
243 "ogg" => "audio/ogg",
244 "wav" => "audio/wav",
245 "flac" => "audio/flac",
246 "m4a" => "audio/mp4",
247 "aac" => "audio/aac",
248 "wma" => "audio/x-ms-wma",
249 "opus" => "audio/opus",
250 "pdf" => "application/pdf",
251 "doc" => "application/msword",
252 "docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
253 "xls" => "application/vnd.ms-excel",
254 "xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
255 "ppt" => "application/vnd.ms-powerpoint",
256 "pptx" => "application/vnd.openxmlformats-officedocument.presentationml.presentation",
257 "odt" => "application/vnd.oasis.opendocument.text",
258 "ods" => "application/vnd.oasis.opendocument.spreadsheet",
259 "odp" => "application/vnd.oasis.opendocument.presentation",
260 "rtf" => "application/rtf",
261 "txt" => "text/plain",
262 "md" => "text/markdown",
263 "csv" => "text/csv",
264 "json" => "application/json",
265 "xml" => "application/xml",
266 "yaml" | "yml" => "application/x-yaml",
267 "toml" => "application/toml",
268 "sql" => "application/sql",
269 "zip" => "application/zip",
270 "rar" => "application/vnd.rar",
271 "7z" => "application/x-7z-compressed",
272 "tar" => "application/x-tar",
273 "gz" => "application/gzip",
274 "bz2" => "application/x-bzip2",
275 "xz" => "application/x-xz",
276 "iso" => "application/x-iso9660-image",
277 "dmg" => "application/x-apple-diskimage",
278 "apk" => "application/vnd.android.package-archive",
279 "jar" => "application/java-archive",
280 "xdc" => "application/vnd.webxdc+zip",
281 "obj" => "model/obj",
282 "gltf" => "model/gltf+json",
283 "glb" => "model/gltf-binary",
284 "stl" => "model/stl",
285 "dae" => "model/vnd.collada+xml",
286 "js" => "text/javascript",
287 "py" => "text/x-python",
288 "rs" => "text/x-rust",
289 "go" => "text/x-go",
290 "java" => "text/x-java",
291 "c" => "text/x-c",
292 "cpp" => "text/x-c++",
293 "cs" => "text/x-csharp",
294 "rb" => "text/x-ruby",
295 "php" => "text/x-php",
296 "swift" => "text/x-swift",
297 "html" | "htm" => "text/html",
298 "css" => "text/css",
299 "exe" => "application/x-msdownload",
300 "msi" => "application/x-msi",
301 "ttf" => "font/ttf",
302 "otf" => "font/otf",
303 "woff" => "font/woff",
304 "woff2" => "font/woff2",
305 _ => "application/octet-stream",
306 }
307}
308
309pub fn extension_from_mime(mime: &str) -> String {
312 match mime.trim().to_lowercase().as_str() {
313 "image/png" => "png",
315 "image/jpeg" | "image/jpg" => "jpg",
316 "image/gif" => "gif",
317 "image/webp" => "webp",
318 "image/svg+xml" => "svg",
319 "image/bmp" | "image/x-ms-bmp" => "bmp",
320 "image/x-icon" | "image/vnd.microsoft.icon" => "ico",
321 "image/tiff" => "tiff",
322 "image/x-adobe-dng" => "dng",
323 "image/x-canon-cr2" => "cr2",
324 "image/x-nikon-nef" => "nef",
325 "image/x-sony-arw" => "arw",
326 "audio/wav" | "audio/x-wav" | "audio/wave" => "wav",
328 "audio/mp3" | "audio/mpeg" => "mp3",
329 "audio/flac" => "flac",
330 "audio/ogg" => "ogg",
331 "audio/mp4" => "m4a",
332 "audio/aac" | "audio/x-aac" => "aac",
333 "audio/x-ms-wma" => "wma",
334 "audio/opus" => "opus",
335 "video/mp4" => "mp4",
337 "video/webm" => "webm",
338 "video/quicktime" => "mov",
339 "video/x-msvideo" => "avi",
340 "video/x-matroska" => "mkv",
341 "video/x-flv" => "flv",
342 "video/x-ms-wmv" => "wmv",
343 "video/mpeg" => "mpg",
344 "video/3gpp" => "3gp",
345 "video/ogg" => "ogv",
346 "video/mp2t" => "ts",
347 "application/pdf" => "pdf",
349 "application/msword" => "doc",
350 "application/vnd.openxmlformats-officedocument.wordprocessingml.document" => "docx",
351 "application/vnd.ms-excel" => "xls",
352 "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" => "xlsx",
353 "application/vnd.ms-powerpoint" => "ppt",
354 "application/vnd.openxmlformats-officedocument.presentationml.presentation" => "pptx",
355 "application/vnd.oasis.opendocument.text" => "odt",
356 "application/vnd.oasis.opendocument.spreadsheet" => "ods",
357 "application/vnd.oasis.opendocument.presentation" => "odp",
358 "application/rtf" => "rtf",
359 "text/plain" => "txt",
361 "text/markdown" => "md",
362 "text/csv" => "csv",
363 "application/json" => "json",
364 "application/xml" | "text/xml" => "xml",
365 "application/x-yaml" | "text/yaml" => "yaml",
366 "application/toml" => "toml",
367 "application/sql" => "sql",
368 "application/zip" => "zip",
370 "application/x-rar-compressed" | "application/vnd.rar" => "rar",
371 "application/x-7z-compressed" => "7z",
372 "application/x-tar" => "tar",
373 "application/gzip" => "gz",
374 "application/x-bzip2" => "bz2",
375 "application/x-xz" => "xz",
376 "application/x-iso9660-image" => "iso",
377 "application/x-apple-diskimage" => "dmg",
378 "application/vnd.android.package-archive" => "apk",
379 "application/java-archive" => "jar",
380 "application/vnd.webxdc+zip" => "xdc",
381 "model/obj" => "obj",
383 "model/gltf+json" => "gltf",
384 "model/gltf-binary" => "glb",
385 "model/stl" | "application/sla" => "stl",
386 "model/vnd.collada+xml" => "dae",
387 "text/javascript" | "application/javascript" => "js",
389 "text/typescript" | "application/typescript" => "ts",
390 "text/x-python" | "application/x-python" => "py",
391 "text/x-rust" => "rs",
392 "text/x-go" => "go",
393 "text/x-java" => "java",
394 "text/x-c" => "c",
395 "text/x-c++" => "cpp",
396 "text/x-csharp" => "cs",
397 "text/x-ruby" => "rb",
398 "text/x-php" => "php",
399 "text/x-swift" => "swift",
400 "text/html" => "html",
402 "text/css" => "css",
403 "application/x-msdownload" | "application/x-dosexec" => "exe",
405 "application/x-msi" => "msi",
406 "application/x-font-ttf" | "font/ttf" => "ttf",
407 "application/x-font-otf" | "font/otf" => "otf",
408 "font/woff" => "woff",
409 "font/woff2" => "woff2",
410 _ => {
413 let lower = mime.trim().to_lowercase();
414 let sub = lower.split('/').nth(1).unwrap_or("");
415 let safe = !sub.is_empty() && sub.len() <= 16 && sub.bytes().all(|b| b.is_ascii_alphanumeric());
416 return if safe { sub.to_string() } else { "bin".to_string() };
417 }
418 }.to_string()
419}
420
421pub fn sanitize_filename(name: &str) -> String {
424 let base = name.rsplit('/').next().unwrap_or(name);
425 let base = base.rsplit('\\').next().unwrap_or(base);
426
427 let sanitized: String = base.chars().filter(|c| {
428 !matches!(c, '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' | '\0')
429 }).collect();
430
431 let sanitized = sanitized.trim_matches(|c: char| c == '.' || c == ' ');
432
433 if sanitized.is_empty() {
434 return String::new();
435 }
436
437 if let Some(dot_pos) = sanitized.rfind('.') {
438 let stem = &sanitized[..dot_pos];
439 let ext = &sanitized[dot_pos..];
440 if stem.len() > 64 {
441 let truncated = &stem[..stem.floor_char_boundary(64)];
442 return format!("{}{}", truncated, ext);
443 }
444 } else if sanitized.len() > 64 {
445 let truncated = &sanitized[..sanitized.floor_char_boundary(64)];
446 return truncated.to_string();
447 }
448
449 sanitized.to_string()
450}
451
452pub fn resolve_unique_filename(dir: &std::path::Path, name: &str) -> std::path::PathBuf {
457 let candidate = dir.join(name);
458 if !candidate.exists() {
459 return candidate;
460 }
461
462 let stem = std::path::Path::new(name)
463 .file_stem()
464 .and_then(|s| s.to_str())
465 .unwrap_or(name);
466 let ext = std::path::Path::new(name)
467 .extension()
468 .and_then(|s| s.to_str())
469 .unwrap_or("");
470
471 let mut counter = 1u32;
472 loop {
473 let suffixed = if ext.is_empty() {
474 format!("{}-{}", stem, counter)
475 } else {
476 format!("{}-{}.{}", stem, counter, ext)
477 };
478 let candidate = dir.join(&suffixed);
479 if !candidate.exists() {
480 return candidate;
481 }
482 counter += 1;
483 }
484}
485
486pub fn decrypt_and_save_attachment(
492 encrypted_data: &[u8],
493 key: &str,
494 nonce: &str,
495 name: &str,
496 extension: &str,
497) -> Result<(std::path::PathBuf, String), String> {
498 decrypt_and_save_attachment_owned(encrypted_data.to_vec(), key, nonce, name, extension)
499}
500
501pub fn decrypt_and_save_attachment_owned(
504 encrypted_data: Vec<u8>,
505 key: &str,
506 nonce: &str,
507 name: &str,
508 extension: &str,
509) -> Result<(std::path::PathBuf, String), String> {
510 let decrypted = if key.is_empty() || nonce.is_empty() {
514 encrypted_data
515 } else {
516 decrypt_data_owned(encrypted_data, key, nonce)?
517 };
518 let file_hash = sha256_hex(&decrypted);
519
520 let dir = crate::db::get_download_dir();
521 std::fs::create_dir_all(&dir).map_err(|e| format!("Failed to create directory: {}", e))?;
522
523 let target_name = if name.is_empty() {
524 format!("{}.{}", file_hash, extension)
525 } else {
526 name.to_string()
527 };
528
529 let candidate = dir.join(&target_name);
531 let already_exists = candidate.exists()
532 && std::fs::metadata(&candidate).map(|m| m.len() == decrypted.len() as u64).unwrap_or(false)
533 && std::fs::read(&candidate).map(|b| sha256_hex(&b) == file_hash).unwrap_or(false);
534
535 if already_exists {
536 return Ok((candidate, file_hash));
537 }
538
539 let file_path = resolve_unique_filename(&dir, &target_name);
540
541 let tmp_path = dir.join(format!(".{}.{}.tmp", file_hash, extension));
543 std::fs::write(&tmp_path, &decrypted).map_err(|e| format!("Failed to write file: {}", e))?;
544 std::fs::rename(&tmp_path, &file_path).map_err(|e| format!("Failed to rename file: {}", e))?;
545
546 Ok((file_path, file_hash))
547}
548
549pub fn format_bytes(bytes: u64) -> String {
551 const KB: f64 = 1024.0;
552 const MB: f64 = KB * 1024.0;
553 const GB: f64 = MB * 1024.0;
554
555 if bytes < KB as u64 {
556 format!("{} B", bytes)
557 } else if bytes < MB as u64 {
558 format!("{:.1} KB", bytes as f64 / KB)
559 } else if bytes < GB as u64 {
560 format!("{:.1} MB", bytes as f64 / MB)
561 } else {
562 format!("{:.1} GB", bytes as f64 / GB)
563 }
564}
565
566pub fn is_image_mime(mime: &str) -> bool {
568 mime.trim().starts_with("image/")
569}
570
571pub fn mime_from_extension_safe(extension: &str, image_only: bool) -> Result<String, String> {
573 let mime = mime_from_extension(extension).to_string();
574 if image_only && !is_image_mime(&mime) {
575 return Err(mime);
576 }
577 Ok(mime)
578}
579
580pub const RECOGNISED_MIMES: &[&str] = &[
589 "image/png",
590 "image/jpeg",
591 "image/gif",
592 "image/webp",
593 "image/tiff",
594 "image/x-icon",
595 "image/svg+xml",
596 "image/bmp",
597 "video/webm",
598 "video/x-matroska",
599 "video/quicktime",
600 "video/mp4",
601];
602
603pub fn mime_from_magic_bytes(bytes: &[u8]) -> &'static str {
604 if bytes.len() < 4 {
605 return "application/octet-stream";
606 }
607 match bytes[0] {
608 0x89 if bytes[1..4] == [0x50, 0x4E, 0x47] => "image/png",
609 0xFF if bytes[1..3] == [0xD8, 0xFF] => "image/jpeg",
610 b'G' if bytes.len() >= 6 && (bytes[..6] == *b"GIF87a" || bytes[..6] == *b"GIF89a") => "image/gif",
611 b'R' if bytes.len() > 12 && bytes[..4] == *b"RIFF" && bytes[8..12] == *b"WEBP" => "image/webp",
612 0x49 if bytes[1..4] == [0x49, 0x2A, 0x00] => "image/tiff",
613 0x4D if bytes[1..4] == [0x4D, 0x00, 0x2A] => "image/tiff",
614 0x00 if bytes[1..4] == [0x00, 0x01, 0x00] => "image/x-icon",
615 b'<' if bytes.starts_with(b"<?xml") || bytes.starts_with(b"<svg") => "image/svg+xml",
616 b'B' if bytes[1] == b'M' => "image/bmp",
617 0x1A if bytes[1..4] == [0x45, 0xDF, 0xA3] => {
619 if bytes.len() >= 64 && bytes[..64].windows(4).any(|w| w == b"webm") {
620 "video/webm"
621 } else {
622 "video/x-matroska"
623 }
624 }
625 _ if bytes.len() >= 12 && bytes[4..8] == *b"ftyp" => match &bytes[8..12] {
627 b"qt " => "video/quicktime",
628 _ => "video/mp4",
629 },
630 _ => "application/octet-stream",
631 }
632}
633
634#[inline]
641pub fn looks_encrypted(s: &str) -> bool {
642 if s.len() < 56 { return false; }
643 is_all_lowercase_hex(s.as_bytes())
644}
645
646#[cfg(target_arch = "aarch64")]
648#[inline]
649fn is_all_lowercase_hex(bytes: &[u8]) -> bool {
650 use std::arch::aarch64::*;
651 unsafe {
652 let mut i = 0;
653 while i + 16 <= bytes.len() {
654 let chunk = vld1q_u8(bytes.as_ptr().add(i));
655 let is_digit = vandq_u8(vcgeq_u8(chunk, vdupq_n_u8(b'0')),
656 vcleq_u8(chunk, vdupq_n_u8(b'9')));
657 let is_af = vandq_u8(vcgeq_u8(chunk, vdupq_n_u8(b'a')),
658 vcleq_u8(chunk, vdupq_n_u8(b'f')));
659 if vminvq_u8(vorrq_u8(is_digit, is_af)) == 0 { return false; }
660 i += 16;
661 }
662 while i < bytes.len() {
663 let b = bytes[i];
664 if !matches!(b, b'0'..=b'9' | b'a'..=b'f') { return false; }
665 i += 1;
666 }
667 }
668 true
669}
670
671#[cfg(target_arch = "x86_64")]
673#[inline]
674fn is_all_lowercase_hex(bytes: &[u8]) -> bool {
675 use std::arch::x86_64::*;
676 unsafe {
677 let mut i = 0;
678 while i + 16 <= bytes.len() {
679 let chunk = _mm_loadu_si128(bytes.as_ptr().add(i) as *const __m128i);
680 let is_digit = _mm_cmpeq_epi8(
681 _mm_subs_epu8(_mm_sub_epi8(chunk, _mm_set1_epi8(b'0' as i8)), _mm_set1_epi8(9)),
682 _mm_setzero_si128());
683 let is_af = _mm_cmpeq_epi8(
684 _mm_subs_epu8(_mm_sub_epi8(chunk, _mm_set1_epi8(b'a' as i8)), _mm_set1_epi8(5)),
685 _mm_setzero_si128());
686 if _mm_movemask_epi8(_mm_or_si128(is_digit, is_af)) != 0xFFFF { return false; }
687 i += 16;
688 }
689 while i < bytes.len() {
690 let b = bytes[i];
691 if !matches!(b, b'0'..=b'9' | b'a'..=b'f') { return false; }
692 i += 1;
693 }
694 }
695 true
696}
697
698#[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
700#[inline]
701fn is_all_lowercase_hex(bytes: &[u8]) -> bool {
702 const IS_LOWER_HEX: [bool; 256] = {
703 let mut t = [false; 256];
704 t[b'0' as usize] = true; t[b'1' as usize] = true; t[b'2' as usize] = true;
705 t[b'3' as usize] = true; t[b'4' as usize] = true; t[b'5' as usize] = true;
706 t[b'6' as usize] = true; t[b'7' as usize] = true; t[b'8' as usize] = true;
707 t[b'9' as usize] = true; t[b'a' as usize] = true; t[b'b' as usize] = true;
708 t[b'c' as usize] = true; t[b'd' as usize] = true; t[b'e' as usize] = true;
709 t[b'f' as usize] = true;
710 t
711 };
712 bytes.iter().all(|&b| IS_LOWER_HEX[b as usize])
713}
714
715pub async fn maybe_encrypt_inner(mut input: String, password: Option<String>) -> String {
718 let mut key: [u8; 32] = match password {
719 None => crate::state::ENCRYPTION_KEY.get().expect("Encryption key must be set"),
720 Some(password) => hash_pass(&password).await,
721 };
722
723 let framed = seal_framed(input.as_bytes(), &key).expect("Encryption should not fail");
724 wipe_vec(unsafe { input.as_mut_vec() });
726
727 if !crate::state::ENCRYPTION_KEY.has_key() {
728 crate::state::ENCRYPTION_KEY.set(key, &[&crate::state::MY_SECRET_KEY]);
729 }
730
731 key.zeroize();
732
733 crate::simd::hex::bytes_to_hex_string(&framed)
734}
735
736pub async fn maybe_decrypt_inner(ciphertext: String, password: Option<String>) -> Result<String, ()> {
739 let has_password = password.is_some();
740
741 let mut key: [u8; 32] = if let Some(pass) = password {
742 hash_pass(&pass).await
743 } else {
744 match crate::state::ENCRYPTION_KEY.get() {
745 Some(k) => k,
746 None => return Err(()),
747 }
748 };
749
750 let Some(plaintext) = open_hex(&ciphertext, &key) else {
751 key.zeroize();
752 return Err(());
753 };
754
755 if has_password && !crate::state::ENCRYPTION_KEY.has_key() {
756 crate::state::ENCRYPTION_KEY.set(key, &[&crate::state::MY_SECRET_KEY]);
757 }
758
759 key.zeroize();
760 Ok(plaintext)
761}
762
763pub async fn maybe_encrypt(input: String) -> String {
765 if crate::state::is_encryption_enabled_fast() {
766 maybe_encrypt_inner(input, None).await
767 } else {
768 input
769 }
770}
771
772pub async fn maybe_decrypt(input: String) -> Result<String, ()> {
775 if !looks_encrypted(&input) {
778 return Ok(input);
779 }
780 if let Some(plaintext) = open_with_vault(&input) {
781 return Ok(plaintext);
782 }
783 if crate::state::is_encryption_enabled_fast() { Err(()) } else { Ok(input) }
784}
785
786fn open_with_vault(hex_data: &str) -> Option<String> {
787 let mut key = crate::state::ENCRYPTION_KEY.get()?;
788 let out = open_hex(hex_data, &key);
789 key.zeroize();
790 out
791}
792
793pub fn open_batch<R>(f: impl FnOnce(&mut AtRestOpener) -> R) -> R {
797 f(&mut AtRestOpener { cipher: None, loaded: false })
798}
799
800pub struct AtRestOpener {
803 cipher: Option<ChaCha20Poly1305>,
804 loaded: bool,
805}
806
807impl AtRestOpener {
808 #[allow(clippy::result_unit_err)] pub fn open(&mut self, input: String) -> Result<String, ()> {
811 if !looks_encrypted(&input) {
812 return Ok(input);
813 }
814 if !self.loaded {
815 self.loaded = true;
816 if let Some(mut key) = crate::state::ENCRYPTION_KEY.get() {
817 self.cipher = Some(ChaCha20Poly1305::new(&key));
818 key.zeroize();
819 }
820 }
821 if let Some(plaintext) = self.cipher.as_ref().and_then(|c| open_hex_with(c, &input)) {
822 return Ok(plaintext);
823 }
824 if crate::state::is_encryption_enabled_fast() { Err(()) } else { Ok(input) }
825 }
826}
827
828pub fn encrypt_blob_with_key(plaintext: &[u8], key: &[u8; 32]) -> Result<Vec<u8>, String> {
841 seal_framed(plaintext, key).map_err(|e| format!("blob encryption failed: {}", e))
842}
843
844pub fn decrypt_blob_with_key(stored: &[u8], key: &[u8; 32]) -> Result<Vec<u8>, String> {
846 if stored.len() < NONCE_LEN + TAG_LEN {
847 return Err("blob too short to be ciphertext".to_string());
848 }
849 open_framed(stored.to_vec(), key).ok_or_else(|| "blob decryption failed (wrong key or corrupted)".to_string())
850}
851
852pub fn maybe_encrypt_blob(plaintext: &[u8]) -> Result<Vec<u8>, String> {
855 if !crate::state::is_encryption_enabled_fast() {
856 return Ok(plaintext.to_vec());
857 }
858 let mut key = crate::state::ENCRYPTION_KEY
859 .get()
860 .ok_or_else(|| "encryption enabled but key vault is empty".to_string())?;
861 let out = encrypt_blob_with_key(plaintext, &key);
862 key.zeroize();
863 out
864}
865
866pub fn maybe_decrypt_blob(stored: &[u8]) -> Vec<u8> {
870 if stored.len() == 32 {
872 return stored.to_vec();
873 }
874 match crate::state::ENCRYPTION_KEY.get() {
875 Some(mut key) => {
876 let out = decrypt_blob_with_key(stored, &key).unwrap_or_else(|_| stored.to_vec());
877 key.zeroize();
878 out
879 }
880 None => stored.to_vec(),
881 }
882}
883
884pub fn maybe_encrypt_text(plaintext: &str) -> Result<String, String> {
887 if !crate::state::is_encryption_enabled_fast() {
888 return Ok(plaintext.to_string());
889 }
890 let mut key = crate::state::ENCRYPTION_KEY
891 .get()
892 .ok_or_else(|| "encryption enabled but key vault is empty".to_string())?;
893 let out = encrypt_with_key(plaintext, &key);
894 key.zeroize();
895 out
896}
897
898pub fn maybe_decrypt_text(stored: &str) -> String {
901 if !looks_encrypted(stored) {
902 return stored.to_string();
903 }
904 match crate::state::ENCRYPTION_KEY.get() {
905 Some(mut key) => {
906 let out = decrypt_with_key(stored, &key).unwrap_or_else(|_| stored.to_string());
907 key.zeroize();
908 out
909 }
910 None => stored.to_string(),
911 }
912}
913
914#[cfg(test)]
915mod at_rest_batch_tests {
916 use super::*;
917
918 fn with_vault<R>(key: Option<[u8; 32]>, enabled: bool, f: impl FnOnce() -> R) -> R {
920 let _guard = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
921 match key {
922 Some(k) => crate::state::ENCRYPTION_KEY.set(k, &[]),
923 None => crate::state::ENCRYPTION_KEY.clear(&[]),
924 }
925 crate::state::set_encryption_enabled(enabled);
926 let out = f();
927 crate::state::set_encryption_enabled(false);
928 crate::state::ENCRYPTION_KEY.clear(&[]);
929 out
930 }
931
932 fn rows(key: &[u8; 32]) -> Vec<String> {
933 vec![
934 encrypt_with_key("first", key).unwrap(),
935 "plain row".to_string(),
936 encrypt_with_key("", key).unwrap(),
937 encrypt_with_key(&"long ".repeat(300), key).unwrap(),
938 encrypt_with_key("wrong key", &[9u8; 32]).unwrap(),
939 "cafe".repeat(20),
940 ]
941 }
942
943 #[test]
944 fn a_batch_answers_every_row_as_maybe_decrypt_does() {
945 use futures_util::FutureExt;
946 let key = [0x55u8; 32];
947 let rows = rows(&key);
948 for enabled in [true, false] {
949 with_vault(Some(key), enabled, || {
950 let expected: Vec<_> = rows.iter().map(|r| maybe_decrypt(r.clone()).now_or_never().unwrap()).collect();
952 let batched = open_batch(|at_rest| rows.iter().map(|r| at_rest.open(r.clone())).collect::<Vec<_>>());
953 assert_eq!(batched, expected, "encryption enabled: {enabled}");
954 });
955 }
956 }
957
958 #[test]
959 fn plaintext_rows_never_need_the_key() {
960 let got = with_vault(None, true, || open_batch(|at_rest| vec![at_rest.open("hello".into()), at_rest.open("a b c".into())]));
961 assert_eq!(got, vec![Ok("hello".to_string()), Ok("a b c".to_string())]);
962 }
963
964 #[test]
965 fn a_ciphertext_with_no_key_in_the_vault_is_refused_while_encryption_is_on() {
966 let sealed = encrypt_with_key("secret", &[0x55u8; 32]).unwrap();
967 let on = with_vault(None, true, || open_batch(|at_rest| at_rest.open(sealed.clone())));
968 assert_eq!(on, Err(()));
969 let off = with_vault(None, false, || open_batch(|at_rest| at_rest.open(sealed.clone())));
970 assert_eq!(off, Ok(sealed), "encryption off: an undecryptable row reads through as-is");
971 }
972}
973
974#[cfg(test)]
975mod attachment_cipher_tests {
976 use super::*;
977
978 const PINNED: [(usize, &str); 6] = [
980 (0, "022f841c702b66b0bec8111f67de221d"),
981 (1, "292017643f9281af327f9487e3df3e3d7c"),
982 (15, "29a932a407ebe042f8dc56e1ba4ae84d9c12aa8198954368a48d9deebb7ab9"),
983 (16, "29a932a407ebe042f8dc56e1ba4ae8fc890ce0e3fa7321a07ba4f5bedc8e26e7"),
984 (17, "29a932a407ebe042f8dc56e1ba4ae8fcf1bed63597cf6053bc96b53c32185e0ecd"),
985 (100, "29a932a407ebe042f8dc56e1ba4ae8fcf13fc2f1f1a2cc0856a0d376defbf35bef7b179b6ad5b2a9f52b19922f7cf2bc4960c60abb73d0a56dabe7c406cb1555ca5427dad72a529c7f8f0c592ce2d0c49b46f9f9011bda808e760db92ad6ecc171a88a1ce072e4f68ec1b5cc3a5e23c37c645be7"),
986 ];
987
988 #[test]
989 fn attachment_cipher_matches_pinned_vectors() {
990 let params = EncryptionParams {
991 key: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f".to_string(),
992 nonce: "a0a1a2a3a4a5a6a7a8a9aaabacadaeaf".to_string(),
993 };
994 for (len, want) in PINNED {
995 let pt: Vec<u8> = (0..len).map(|i| (i * 7 + 3) as u8).collect();
996 let sealed = encrypt_data(&pt, ¶ms).unwrap();
997 assert_eq!(hex::encode(&sealed), want, "seal len={len}");
998 let ct = hex::decode(want).unwrap();
999 assert_eq!(decrypt_data(&ct, ¶ms.key, ¶ms.nonce).unwrap(), pt, "open len={len}");
1000 }
1001 }
1002}
1003
1004#[cfg(test)]
1005mod at_rest_tests {
1006 use super::*;
1007
1008 #[test]
1009 fn blob_roundtrip_with_explicit_key() {
1010 let key = [7u8; 32];
1011 let secret = [0x42u8; 32];
1012 let ct = encrypt_blob_with_key(&secret, &key).unwrap();
1013 assert_eq!(ct.len(), 12 + 32 + 16, "nonce + ciphertext + tag");
1014 assert_ne!(&ct[12..44], &secret[..], "ciphertext must not equal plaintext");
1015 assert_eq!(decrypt_blob_with_key(&ct, &key).unwrap(), secret.to_vec());
1016 }
1017
1018 #[test]
1019 fn blob_wrong_key_fails() {
1020 let ct = encrypt_blob_with_key(&[1u8; 32], &[7u8; 32]).unwrap();
1021 assert!(decrypt_blob_with_key(&ct, &[9u8; 32]).is_err());
1022 }
1023
1024 #[test]
1025 fn encrypted_text_is_always_detected_as_encrypted() {
1026 let key = [3u8; 32];
1029 for s in ["", "[]", "{}", "a"] {
1030 let ct = encrypt_with_key(s, &key).unwrap();
1031 assert!(looks_encrypted(&ct), "ciphertext for {:?} must look encrypted", s);
1032 assert_eq!(decrypt_with_key(&ct, &key).unwrap(), s);
1033 }
1034 }
1035}
1036
1037pub use crate::simd::image::nearest_neighbor_downsample_rgba;
1043
1044pub fn generate_thumbhash_from_rgba(pixels: &[u8], width: u32, height: u32) -> Option<String> {
1049 use fast_thumbhash::{rgba_to_thumb_hash, base91_encode};
1050
1051 const MAX_DIM: u32 = 100;
1052
1053 let (thumb_w, thumb_h) = if width <= MAX_DIM && height <= MAX_DIM {
1054 (width, height)
1055 } else if width > height {
1056 (MAX_DIM, (MAX_DIM * height / width).max(1))
1057 } else {
1058 ((MAX_DIM * width / height).max(1), MAX_DIM)
1059 };
1060
1061 let thumbnail = if thumb_w == width && thumb_h == height {
1062 pixels.to_vec()
1063 } else {
1064 nearest_neighbor_downsample_rgba(pixels, width, height, thumb_w, thumb_h)
1065 };
1066
1067 let hash = rgba_to_thumb_hash(thumb_w as usize, thumb_h as usize, &thumbnail);
1068 Some(base91_encode(&hash))
1069}
1070
1071pub fn decode_image_bounded(bytes: &[u8]) -> Result<image::DynamicImage, String> {
1081 use image::ImageDecoder;
1082 let mut reader = image::ImageReader::new(std::io::Cursor::new(bytes))
1083 .with_guessed_format()
1084 .map_err(|e| format!("image format: {e}"))?;
1085 reader.limits(bounded_image_limits());
1086 let mut decoder = reader.into_decoder().map_err(|e| format!("image decode: {e}"))?;
1087 let orientation = decoder.orientation().map_err(|e| format!("image orientation: {e}"))?;
1089 let mut img = image::DynamicImage::from_decoder(decoder)
1090 .map_err(|e| format!("image decode: {e}"))?;
1091 img.apply_orientation(orientation);
1092 Ok(img)
1093}
1094
1095pub fn bounded_image_limits() -> image::Limits {
1098 let mut limits = image::Limits::default();
1099 limits.max_image_width = Some(16_384);
1100 limits.max_image_height = Some(16_384);
1101 limits.max_alloc = Some(256 * 1024 * 1024);
1102 limits
1103}
1104
1105pub fn generate_image_metadata(file_bytes: &[u8]) -> Option<crate::types::ImageMetadata> {
1110 let img = decode_image_bounded(file_bytes).ok()?;
1111 let width = img.width();
1112 let height = img.height();
1113
1114 let small = if width > 100 || height > 100 { img.thumbnail(100, 100) } else { img };
1117 let rgba = small.to_rgba8();
1118 let thumbhash = generate_thumbhash_from_rgba(rgba.as_raw(), rgba.width(), rgba.height())?;
1119
1120 Some(crate::types::ImageMetadata {
1121 thumbhash,
1122 width,
1123 height,
1124 })
1125}
1126
1127#[cfg(test)]
1128mod tests {
1129 #[test]
1130 fn a_senders_mime_can_only_name_a_plain_extension() {
1131 use super::extension_from_mime;
1132 assert_eq!(extension_from_mime("image/png"), "png");
1133 assert_eq!(extension_from_mime("application/x-bittorrent"), "bin");
1134 assert_eq!(extension_from_mime("audio/opus"), "opus");
1135 assert_eq!(extension_from_mime("x/.\\..\\..\\AppData\\Startup\\a.bat"), "bin");
1136 assert_eq!(extension_from_mime("x/../../etc"), "bin");
1137 assert_eq!(extension_from_mime("nothing"), "bin");
1138 }
1139
1140 use super::*;
1141
1142 #[test]
1146 fn magic_bytes_name_video_and_bitmaps() {
1147 let mut mp4 = vec![0u8, 0, 0, 0x20];
1148 mp4.extend_from_slice(b"ftypisom");
1149 assert_eq!(mime_from_magic_bytes(&mp4), "video/mp4");
1150
1151 let mut mov = vec![0u8, 0, 0, 0x14];
1152 mov.extend_from_slice(b"ftypqt ");
1153 assert_eq!(mime_from_magic_bytes(&mov), "video/quicktime");
1154
1155 let mut webm = vec![0x1A, 0x45, 0xDF, 0xA3];
1156 webm.extend_from_slice(&[0u8; 20]);
1157 webm.extend_from_slice(b"webm");
1158 webm.extend_from_slice(&[0u8; 40]);
1159 assert_eq!(mime_from_magic_bytes(&webm), "video/webm");
1160
1161 let mut mkv = vec![0x1A, 0x45, 0xDF, 0xA3];
1162 mkv.extend_from_slice(&[0u8; 64]);
1163 assert_eq!(mime_from_magic_bytes(&mkv), "video/x-matroska");
1164
1165 let mut bmp = b"BM".to_vec();
1166 bmp.extend_from_slice(&[0u8; 20]);
1167 assert_eq!(mime_from_magic_bytes(&bmp), "image/bmp");
1168 }
1169
1170 #[test]
1173 fn every_recognised_mime_is_reachable_from_some_bytes() {
1174 let mut webm = vec![0x1A, 0x45, 0xDF, 0xA3];
1175 webm.extend_from_slice(&[0u8; 20]);
1176 webm.extend_from_slice(b"webm");
1177 webm.resize(80, 0);
1178 let mut mkv = vec![0x1A, 0x45, 0xDF, 0xA3];
1179 mkv.resize(80, 0);
1180 let mut webp = b"RIFF".to_vec();
1181 webp.extend_from_slice(&[0u8; 4]);
1182 webp.extend_from_slice(b"WEBP____");
1183
1184 let samples: Vec<Vec<u8>> = vec![
1185 vec![0x89, b'P', b'N', b'G'],
1186 vec![0xFF, 0xD8, 0xFF, 0xE0],
1187 b"GIF89a__".to_vec(),
1188 webp,
1189 vec![0x49, 0x49, 0x2A, 0x00],
1190 vec![0x00, 0x00, 0x01, 0x00],
1191 b"<svg xmlns".to_vec(),
1192 b"BM______".to_vec(),
1193 webm,
1194 mkv,
1195 b"\0\0\0\x14ftypqt ".to_vec(),
1196 b"\0\0\0\x20ftypisom".to_vec(),
1197 ];
1198 let reachable: std::collections::HashSet<&str> =
1199 samples.iter().map(|b| mime_from_magic_bytes(b)).collect();
1200 for m in RECOGNISED_MIMES {
1201 assert!(reachable.contains(m), "{m} is listed but no bytes produce it");
1202 }
1203 assert_eq!(reachable.len(), RECOGNISED_MIMES.len(), "and nothing is produced that is not listed");
1204 }
1205
1206 #[test]
1208 fn magic_bytes_still_name_the_image_types() {
1209 assert_eq!(mime_from_magic_bytes(&[0x89, b'P', b'N', b'G']), "image/png");
1210 assert_eq!(mime_from_magic_bytes(&[0xFF, 0xD8, 0xFF, 0xE0]), "image/jpeg");
1211 assert_eq!(mime_from_magic_bytes(b"GIF89a__"), "image/gif");
1212 let mut webp = b"RIFF".to_vec();
1213 webp.extend_from_slice(&[0u8; 4]);
1214 webp.extend_from_slice(b"WEBP____");
1215 assert_eq!(mime_from_magic_bytes(&webp), "image/webp");
1216 assert_eq!(mime_from_magic_bytes(b"<svg xmlns"), "image/svg+xml");
1217 assert_eq!(mime_from_magic_bytes(&[0xDE, 0xAD, 0xBE, 0xEF]), "application/octet-stream");
1218 assert_eq!(mime_from_magic_bytes(&[]), "application/octet-stream", "a short read is not a type");
1219 assert_eq!(mime_from_magic_bytes(b"Bm"), "application/octet-stream", "and BMP is case-sensitive");
1220 }
1221
1222 fn test_key() -> [u8; 32] {
1227 [0x42u8; 32]
1228 }
1229
1230 fn alt_key() -> [u8; 32] {
1231 [0x99u8; 32]
1232 }
1233
1234 #[test]
1235 fn encrypt_decrypt_roundtrip_simple() {
1236 let key = test_key();
1237 let plaintext = "hello world";
1238 let encrypted = encrypt_with_key(plaintext, &key).expect("encryption should succeed");
1239 let decrypted = decrypt_with_key(&encrypted, &key).expect("decryption should succeed");
1240 assert_eq!(decrypted, plaintext, "roundtrip should preserve plaintext");
1241 }
1242
1243 #[test]
1244 fn encrypt_decrypt_100_random_strings() {
1245 use rand::Rng;
1246 let key = test_key();
1247 let mut rng = rand::thread_rng();
1248 for i in 0..100 {
1249 let len = rng.gen_range(1..=200);
1250 let s: String = (0..len).map(|_| rng.gen_range(0x20u8..0x7f) as char).collect();
1251 let enc = encrypt_with_key(&s, &key)
1252 .unwrap_or_else(|e| panic!("encryption failed on iteration {}: {}", i, e));
1253 let dec = decrypt_with_key(&enc, &key)
1254 .unwrap_or_else(|e| panic!("decryption failed on iteration {}: {}", i, e));
1255 assert_eq!(dec, s, "roundtrip failed on iteration {}", i);
1256 }
1257 }
1258
1259 #[test]
1260 fn encrypt_decrypt_empty_string() {
1261 let key = test_key();
1262 let encrypted = encrypt_with_key("", &key).expect("encrypting empty string should succeed");
1263 let decrypted = decrypt_with_key(&encrypted, &key).expect("decrypting empty string should succeed");
1264 assert_eq!(decrypted, "", "empty string roundtrip should produce empty string");
1265 }
1266
1267 #[test]
1268 fn encrypt_decrypt_large_string() {
1269 let key = test_key();
1270 let plaintext = "A".repeat(10 * 1024); let encrypted = encrypt_with_key(&plaintext, &key).expect("encrypting 10KB should succeed");
1272 let decrypted = decrypt_with_key(&encrypted, &key).expect("decrypting 10KB should succeed");
1273 assert_eq!(decrypted, plaintext, "10KB roundtrip should preserve content");
1274 }
1275
1276 #[test]
1277 fn decrypt_with_wrong_key_fails() {
1278 let key = test_key();
1279 let wrong_key = alt_key();
1280 let encrypted = encrypt_with_key("secret data", &key).expect("encryption should succeed");
1281 let result = decrypt_with_key(&encrypted, &wrong_key);
1282 assert!(result.is_err(), "decryption with wrong key should fail");
1283 }
1284
1285 #[test]
1286 fn decrypt_corrupted_ciphertext_fails() {
1287 let key = test_key();
1288 let mut encrypted = encrypt_with_key("secret data", &key).expect("encryption should succeed");
1289 let bytes: Vec<u8> = encrypted.bytes().collect();
1291 if bytes.len() > 30 {
1292 let mut chars: Vec<char> = encrypted.chars().collect();
1293 chars[30] = if chars[30] == '0' { 'f' } else { '0' };
1295 encrypted = chars.into_iter().collect();
1296 }
1297 let result = decrypt_with_key(&encrypted, &key);
1298 assert!(result.is_err(), "decryption of corrupted ciphertext should fail");
1299 }
1300
1301 #[test]
1302 fn different_keys_produce_different_ciphertext() {
1303 let key1 = test_key();
1304 let key2 = alt_key();
1305 let plaintext = "same plaintext";
1306 let enc1 = encrypt_with_key(plaintext, &key1).expect("enc1 should succeed");
1307 let enc2 = encrypt_with_key(plaintext, &key2).expect("enc2 should succeed");
1308 assert_ne!(enc1, enc2, "different keys should produce different ciphertext");
1310 }
1311
1312 #[test]
1313 fn nonce_is_always_different() {
1314 let key = test_key();
1315 let plaintext = "same string encrypted twice";
1316 let enc1 = encrypt_with_key(plaintext, &key).expect("enc1 should succeed");
1317 let enc2 = encrypt_with_key(plaintext, &key).expect("enc2 should succeed");
1318 let nonce1 = &enc1[..24];
1320 let nonce2 = &enc2[..24];
1321 assert_ne!(nonce1, nonce2, "nonces should differ between encryptions of the same plaintext");
1322 }
1323
1324 #[test]
1325 fn unicode_content_preserved() {
1326 let key = test_key();
1327 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}";
1328 let encrypted = encrypt_with_key(plaintext, &key).expect("encrypting unicode should succeed");
1329 let decrypted = decrypt_with_key(&encrypted, &key).expect("decrypting unicode should succeed");
1330 assert_eq!(decrypted, plaintext, "unicode content should be preserved through encrypt/decrypt");
1331 }
1332
1333 #[test]
1334 fn decrypt_too_short_ciphertext_fails() {
1335 let key = test_key();
1336 let result = decrypt_with_key("abcdef", &key);
1337 assert!(result.is_err(), "ciphertext shorter than 24 hex chars should fail");
1338 assert!(result.unwrap_err().contains("too short"), "error should mention too short");
1339 }
1340
1341 #[test]
1342 fn encrypt_output_is_hex_encoded() {
1343 let key = test_key();
1344 let encrypted = encrypt_with_key("test", &key).expect("encryption should succeed");
1345 assert!(encrypted.chars().all(|c| c.is_ascii_hexdigit()),
1346 "encrypted output should be entirely hex characters");
1347 }
1348
1349 #[test]
1350 fn encrypt_output_has_correct_structure() {
1351 let key = test_key();
1352 let encrypted = encrypt_with_key("test", &key).expect("encryption should succeed");
1353 assert!(encrypted.len() > 24,
1355 "encrypted output should have nonce (24 hex chars) plus ciphertext");
1356 assert_eq!(encrypted.len() % 2, 0,
1358 "encrypted output length should be even (hex pairs)");
1359 }
1360
1361 #[test]
1362 fn encrypt_decrypt_special_characters() {
1363 let key = test_key();
1364 let plaintext = r#"!@#$%^&*()_+-=[]{}|;':",.<>?/\`~"#;
1365 let encrypted = encrypt_with_key(plaintext, &key).expect("encrypting special chars should succeed");
1366 let decrypted = decrypt_with_key(&encrypted, &key).expect("decrypting special chars should succeed");
1367 assert_eq!(decrypted, plaintext, "special characters should survive roundtrip");
1368 }
1369
1370 #[test]
1371 fn encrypt_decrypt_newlines_and_whitespace() {
1372 let key = test_key();
1373 let plaintext = "line1\nline2\r\nline3\ttab\0null";
1374 let encrypted = encrypt_with_key(plaintext, &key).expect("encrypting whitespace should succeed");
1375 let decrypted = decrypt_with_key(&encrypted, &key).expect("decrypting whitespace should succeed");
1376 assert_eq!(decrypted, plaintext, "whitespace and control chars should survive roundtrip");
1377 }
1378
1379 #[test]
1380 fn decrypt_invalid_hex_in_nonce_fails() {
1381 let key = test_key();
1382 let bad = "zzzzzzzzzzzzzzzzzzzzzzzz0000000000000000";
1384 let result = decrypt_with_key(bad, &key);
1385 assert!(result.is_err(), "invalid hex in nonce should fail");
1386 }
1387
1388 #[test]
1389 fn decrypt_invalid_hex_in_ciphertext_fails() {
1390 let key = test_key();
1391 let bad = "000000000000000000000000gggggggg";
1393 let result = decrypt_with_key(bad, &key);
1394 assert!(result.is_err(), "invalid hex in ciphertext should fail");
1395 }
1396
1397 #[test]
1402 fn hex_encode_decode_roundtrip() {
1403 let data = vec![0x00, 0x01, 0xff, 0x80, 0x7f];
1404 let encoded = hex::encode(&data);
1405 let decoded = hex::decode(&encoded).expect("decode should succeed");
1406 assert_eq!(decoded, data, "hex encode/decode roundtrip should preserve bytes");
1407 }
1408
1409 #[test]
1410 fn hex_decode_odd_length_error() {
1411 let result = hex::decode("abc");
1412 assert!(result.is_err(), "odd-length hex string should fail");
1413 assert!(result.unwrap_err().contains("Odd-length"), "error should mention odd-length");
1414 }
1415
1416 #[test]
1417 fn hex_decode_invalid_hex_error() {
1418 let result = hex::decode("zzzz");
1419 assert!(result.is_err(), "invalid hex characters should fail");
1420 }
1421
1422 #[test]
1423 fn hex_encode_empty() {
1424 assert_eq!(hex::encode(&[]), "", "encoding empty bytes should produce empty string");
1425 }
1426
1427 #[test]
1428 fn hex_decode_empty() {
1429 let decoded = hex::decode("").expect("decoding empty string should succeed");
1430 assert!(decoded.is_empty(), "decoding empty string should produce empty vec");
1431 }
1432
1433 #[tokio::test]
1438 async fn hash_pass_deterministic() {
1439 let key1 = hash_pass("my_password").await;
1440 let key2 = hash_pass("my_password").await;
1441 assert_eq!(key1, key2, "same password should always produce the same key");
1442 }
1443
1444 #[tokio::test]
1445 async fn hash_pass_different_passwords_different_keys() {
1446 let key1 = hash_pass("password_one").await;
1447 let key2 = hash_pass("password_two").await;
1448 assert_ne!(key1, key2, "different passwords should produce different keys");
1449 }
1450
1451 #[tokio::test]
1452 async fn hash_pass_output_is_32_bytes() {
1453 let key = hash_pass("test_password").await;
1454 assert_eq!(key.len(), 32, "hash_pass should produce exactly 32 bytes");
1455 assert!(key.iter().any(|&b| b != 0), "hash output should not be all zeros");
1457 }
1458
1459 #[test]
1460 fn generate_thumbhash_from_bytes_basic() {
1461 let pixels: Vec<u8> = vec![
1463 255, 0, 0, 255, 255, 0, 0, 255,
1464 255, 0, 0, 255, 255, 0, 0, 255,
1465 ];
1466 let result = super::generate_thumbhash_from_rgba(&pixels, 2, 2);
1467 assert!(result.is_some());
1468 assert!(!result.unwrap().is_empty());
1469 }
1470
1471 #[test]
1472 fn generate_image_metadata_from_bytes() {
1473 let img = image::RgbaImage::from_pixel(4, 4, image::Rgba([0, 0, 255, 255]));
1475 let mut buf = Vec::new();
1476 let mut cursor = std::io::Cursor::new(&mut buf);
1477 img.write_to(&mut cursor, image::ImageFormat::Png).unwrap();
1478
1479 let meta = super::generate_image_metadata(&buf);
1480 assert!(meta.is_some());
1481 let meta = meta.unwrap();
1482 assert_eq!(meta.width, 4);
1483 assert_eq!(meta.height, 4);
1484 assert!(!meta.thumbhash.is_empty());
1485 }
1486
1487 #[test]
1488 fn generate_image_metadata_non_image() {
1489 let text_bytes = b"this is not an image";
1490 let meta = super::generate_image_metadata(text_bytes);
1491 assert!(meta.is_none());
1492 }
1493}