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 buffer = Vec::with_capacity(nonce.len() + ciphertext.len());
47 buffer.extend_from_slice(&nonce);
48 buffer.extend_from_slice(&ciphertext);
49 Ok(crate::simd::hex::bytes_to_hex_string(&buffer))
50}
51
52pub fn decrypt_with_key(hex_data: &str, key: &[u8; 32]) -> Result<String, String> {
56 let bytes = crate::simd::hex::hex_string_to_bytes(hex_data);
57 if bytes.len() < 12 {
58 return Err("Ciphertext too short".to_string());
59 }
60 let (nonce_bytes, ciphertext) = bytes.split_at(12);
61 let nonce_arr: [u8; 12] = nonce_bytes.try_into()
62 .map_err(|_| "Invalid nonce length".to_string())?;
63 let nonce = chacha20poly1305::Nonce::from(nonce_arr);
64 let cipher = ChaCha20Poly1305::new(key.into());
65
66 let plaintext = cipher.decrypt(&nonce, ciphertext)
67 .map_err(|_| "Decryption failed (wrong key or corrupted data)".to_string())?;
68
69 Ok(unsafe { String::from_utf8_unchecked(plaintext) })
72}
73
74pub fn is_encryption_enabled() -> bool {
82 crate::state::resolve_encryption_enabled_from_db()
83}
84
85mod hex {
87 pub fn encode(bytes: &[u8]) -> String {
88 crate::simd::hex::bytes_to_hex_string(bytes)
91 }
92
93 pub fn decode(hex: &str) -> Result<Vec<u8>, String> {
94 if hex.len() % 2 != 0 {
95 return Err("Odd-length hex string".to_string());
96 }
97 crate::simd::hex::hex_string_to_bytes_checked(hex)
100 .ok_or_else(|| "Invalid hex character".to_string())
101 }
102}
103
104#[derive(Debug)]
110pub struct EncryptionParams {
111 pub key: String, pub nonce: String, }
114
115pub fn generate_encryption_params() -> EncryptionParams {
117 use rand::Rng;
118 let mut rng = rand::thread_rng();
119 let mut key: [u8; 32] = rng.gen();
120 let nonce: [u8; 16] = rng.gen();
121 let params = EncryptionParams {
122 key: hex::encode(&key),
123 nonce: hex::encode(&nonce),
124 };
125 key.iter_mut().for_each(|b| *b = 0); params
127}
128
129pub fn encrypt_data(data: &[u8], params: &EncryptionParams) -> Result<Vec<u8>, String> {
131 use aes::Aes256;
132 use aes::cipher::typenum::U16;
133 use aes_gcm::{AesGcm, AeadInPlace, KeyInit as AesKeyInit};
134
135 let key_bytes = hex::decode(¶ms.key).map_err(|e| format!("Invalid key: {}", e))?;
136 let nonce_bytes = hex::decode(¶ms.nonce).map_err(|e| format!("Invalid nonce: {}", e))?;
137
138 let cipher = AesGcm::<Aes256, U16>::new_from_slice(&key_bytes)
139 .map_err(|_| "Invalid encryption key".to_string())?;
140
141 let nonce_arr: [u8; 16] = nonce_bytes.try_into()
142 .map_err(|_| "Invalid nonce length".to_string())?;
143 let nonce = aes_gcm::Nonce::<U16>::from(nonce_arr);
144
145 let mut buffer = data.to_vec();
146 let tag = cipher.encrypt_in_place_detached(&nonce, &[], &mut buffer)
147 .map_err(|_| "Encryption failed".to_string())?;
148
149 buffer.extend_from_slice(&tag);
150 Ok(buffer)
151}
152
153pub fn decrypt_data(encrypted_data: &[u8], key_hex: &str, nonce_hex: &str) -> Result<Vec<u8>, String> {
156 use aes::Aes256;
157 use aes::cipher::typenum::U16;
158 use aes_gcm::{AesGcm, AeadInPlace, KeyInit as AesKeyInit};
159
160 if encrypted_data.len() < 16 {
161 return Err(format!("Invalid Input: encrypted data too small ({} bytes, minimum 16 bytes required for authentication tag)", encrypted_data.len()));
162 }
163
164 let key_bytes = hex::decode(key_hex).map_err(|e| format!("Invalid key: {}", e))?;
165 let nonce_bytes = hex::decode(nonce_hex).map_err(|e| format!("Invalid nonce: {}", e))?;
166
167 let (ciphertext, tag_bytes) = encrypted_data.split_at(encrypted_data.len() - 16);
168
169 let cipher = AesGcm::<Aes256, U16>::new_from_slice(&key_bytes)
170 .map_err(|_| "Invalid decryption key".to_string())?;
171
172 let nonce_arr: [u8; 16] = nonce_bytes.try_into()
173 .map_err(|_| "Invalid nonce length".to_string())?;
174 let nonce = aes_gcm::Nonce::<U16>::from(nonce_arr);
175 let tag_arr: [u8; 16] = tag_bytes.try_into()
176 .map_err(|_| "Invalid tag length".to_string())?;
177 let tag = aes_gcm::Tag::<U16>::from(tag_arr);
178
179 let mut buffer = ciphertext.to_vec();
180 cipher.decrypt_in_place_detached(&nonce, &[], &mut buffer, &tag)
181 .map_err(|e| e.to_string())?;
182
183 Ok(buffer)
184}
185
186pub fn sha256_hex(data: &[u8]) -> String {
188 use sha2::{Sha256, Digest};
189 let mut hasher = Sha256::new();
190 hasher.update(data);
191 hex::encode(&hasher.finalize())
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 _ => {
412 let lower = mime.trim().to_lowercase();
413 return lower.split('/').nth(1).unwrap_or("bin").to_string();
414 }
415 }.to_string()
416}
417
418pub fn sanitize_filename(name: &str) -> String {
421 let base = name.rsplit('/').next().unwrap_or(name);
422 let base = base.rsplit('\\').next().unwrap_or(base);
423
424 let sanitized: String = base.chars().filter(|c| {
425 !matches!(c, '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' | '\0')
426 }).collect();
427
428 let sanitized = sanitized.trim_matches(|c: char| c == '.' || c == ' ');
429
430 if sanitized.is_empty() {
431 return String::new();
432 }
433
434 if let Some(dot_pos) = sanitized.rfind('.') {
435 let stem = &sanitized[..dot_pos];
436 let ext = &sanitized[dot_pos..];
437 if stem.len() > 64 {
438 let truncated = &stem[..stem.floor_char_boundary(64)];
439 return format!("{}{}", truncated, ext);
440 }
441 } else if sanitized.len() > 64 {
442 let truncated = &sanitized[..sanitized.floor_char_boundary(64)];
443 return truncated.to_string();
444 }
445
446 sanitized.to_string()
447}
448
449pub fn resolve_unique_filename(dir: &std::path::Path, name: &str) -> std::path::PathBuf {
454 let candidate = dir.join(name);
455 if !candidate.exists() {
456 return candidate;
457 }
458
459 let stem = std::path::Path::new(name)
460 .file_stem()
461 .and_then(|s| s.to_str())
462 .unwrap_or(name);
463 let ext = std::path::Path::new(name)
464 .extension()
465 .and_then(|s| s.to_str())
466 .unwrap_or("");
467
468 let mut counter = 1u32;
469 loop {
470 let suffixed = if ext.is_empty() {
471 format!("{}-{}", stem, counter)
472 } else {
473 format!("{}-{}.{}", stem, counter, ext)
474 };
475 let candidate = dir.join(&suffixed);
476 if !candidate.exists() {
477 return candidate;
478 }
479 counter += 1;
480 }
481}
482
483pub fn decrypt_and_save_attachment(
489 encrypted_data: &[u8],
490 key: &str,
491 nonce: &str,
492 name: &str,
493 extension: &str,
494) -> Result<(std::path::PathBuf, String), String> {
495 let decrypted = if key.is_empty() || nonce.is_empty() {
499 encrypted_data.to_vec()
500 } else {
501 decrypt_data(encrypted_data, key, nonce)?
502 };
503 let file_hash = sha256_hex(&decrypted);
504
505 let dir = crate::db::get_download_dir();
506 std::fs::create_dir_all(&dir).map_err(|e| format!("Failed to create directory: {}", e))?;
507
508 let target_name = if name.is_empty() {
509 format!("{}.{}", file_hash, extension)
510 } else {
511 name.to_string()
512 };
513
514 let candidate = dir.join(&target_name);
516 let already_exists = candidate.exists()
517 && std::fs::metadata(&candidate).map(|m| m.len() == decrypted.len() as u64).unwrap_or(false)
518 && std::fs::read(&candidate).map(|b| sha256_hex(&b) == file_hash).unwrap_or(false);
519
520 if already_exists {
521 return Ok((candidate, file_hash));
522 }
523
524 let file_path = resolve_unique_filename(&dir, &target_name);
525
526 let tmp_path = dir.join(format!(".{}.{}.tmp", file_hash, extension));
528 std::fs::write(&tmp_path, &decrypted).map_err(|e| format!("Failed to write file: {}", e))?;
529 std::fs::rename(&tmp_path, &file_path).map_err(|e| format!("Failed to rename file: {}", e))?;
530
531 Ok((file_path, file_hash))
532}
533
534pub fn format_bytes(bytes: u64) -> String {
536 const KB: f64 = 1024.0;
537 const MB: f64 = KB * 1024.0;
538 const GB: f64 = MB * 1024.0;
539
540 if bytes < KB as u64 {
541 format!("{} B", bytes)
542 } else if bytes < MB as u64 {
543 format!("{:.1} KB", bytes as f64 / KB)
544 } else if bytes < GB as u64 {
545 format!("{:.1} MB", bytes as f64 / MB)
546 } else {
547 format!("{:.1} GB", bytes as f64 / GB)
548 }
549}
550
551pub fn is_image_mime(mime: &str) -> bool {
553 mime.trim().starts_with("image/")
554}
555
556pub fn mime_from_extension_safe(extension: &str, image_only: bool) -> Result<String, String> {
558 let mime = mime_from_extension(extension).to_string();
559 if image_only && !is_image_mime(&mime) {
560 return Err(mime);
561 }
562 Ok(mime)
563}
564
565pub const RECOGNISED_MIMES: &[&str] = &[
574 "image/png",
575 "image/jpeg",
576 "image/gif",
577 "image/webp",
578 "image/tiff",
579 "image/x-icon",
580 "image/svg+xml",
581 "image/bmp",
582 "video/webm",
583 "video/x-matroska",
584 "video/quicktime",
585 "video/mp4",
586];
587
588pub fn mime_from_magic_bytes(bytes: &[u8]) -> &'static str {
589 if bytes.len() < 4 {
590 return "application/octet-stream";
591 }
592 match bytes[0] {
593 0x89 if bytes[1..4] == [0x50, 0x4E, 0x47] => "image/png",
594 0xFF if bytes[1..3] == [0xD8, 0xFF] => "image/jpeg",
595 b'G' if bytes.len() >= 6 && (bytes[..6] == *b"GIF87a" || bytes[..6] == *b"GIF89a") => "image/gif",
596 b'R' if bytes.len() > 12 && bytes[..4] == *b"RIFF" && bytes[8..12] == *b"WEBP" => "image/webp",
597 0x49 if bytes[1..4] == [0x49, 0x2A, 0x00] => "image/tiff",
598 0x4D if bytes[1..4] == [0x4D, 0x00, 0x2A] => "image/tiff",
599 0x00 if bytes[1..4] == [0x00, 0x01, 0x00] => "image/x-icon",
600 b'<' if bytes.starts_with(b"<?xml") || bytes.starts_with(b"<svg") => "image/svg+xml",
601 b'B' if bytes[1] == b'M' => "image/bmp",
602 0x1A if bytes[1..4] == [0x45, 0xDF, 0xA3] => {
604 if bytes.len() >= 64 && bytes[..64].windows(4).any(|w| w == b"webm") {
605 "video/webm"
606 } else {
607 "video/x-matroska"
608 }
609 }
610 _ if bytes.len() >= 12 && bytes[4..8] == *b"ftyp" => match &bytes[8..12] {
612 b"qt " => "video/quicktime",
613 _ => "video/mp4",
614 },
615 _ => "application/octet-stream",
616 }
617}
618
619use rand::Rng;
624use chacha20poly1305::Nonce;
625
626#[inline]
629pub fn looks_encrypted(s: &str) -> bool {
630 if s.len() < 56 { return false; }
631 is_all_lowercase_hex(s.as_bytes())
632}
633
634#[cfg(target_arch = "aarch64")]
636#[inline]
637fn is_all_lowercase_hex(bytes: &[u8]) -> bool {
638 use std::arch::aarch64::*;
639 unsafe {
640 let mut i = 0;
641 while i + 16 <= bytes.len() {
642 let chunk = vld1q_u8(bytes.as_ptr().add(i));
643 let is_digit = vandq_u8(vcgeq_u8(chunk, vdupq_n_u8(b'0')),
644 vcleq_u8(chunk, vdupq_n_u8(b'9')));
645 let is_af = vandq_u8(vcgeq_u8(chunk, vdupq_n_u8(b'a')),
646 vcleq_u8(chunk, vdupq_n_u8(b'f')));
647 if vminvq_u8(vorrq_u8(is_digit, is_af)) == 0 { return false; }
648 i += 16;
649 }
650 while i < bytes.len() {
651 let b = bytes[i];
652 if !matches!(b, b'0'..=b'9' | b'a'..=b'f') { return false; }
653 i += 1;
654 }
655 }
656 true
657}
658
659#[cfg(target_arch = "x86_64")]
661#[inline]
662fn is_all_lowercase_hex(bytes: &[u8]) -> bool {
663 use std::arch::x86_64::*;
664 unsafe {
665 let mut i = 0;
666 while i + 16 <= bytes.len() {
667 let chunk = _mm_loadu_si128(bytes.as_ptr().add(i) as *const __m128i);
668 let is_digit = _mm_cmpeq_epi8(
669 _mm_subs_epu8(_mm_sub_epi8(chunk, _mm_set1_epi8(b'0' as i8)), _mm_set1_epi8(9)),
670 _mm_setzero_si128());
671 let is_af = _mm_cmpeq_epi8(
672 _mm_subs_epu8(_mm_sub_epi8(chunk, _mm_set1_epi8(b'a' as i8)), _mm_set1_epi8(5)),
673 _mm_setzero_si128());
674 if _mm_movemask_epi8(_mm_or_si128(is_digit, is_af)) != 0xFFFF { return false; }
675 i += 16;
676 }
677 while i < bytes.len() {
678 let b = bytes[i];
679 if !matches!(b, b'0'..=b'9' | b'a'..=b'f') { return false; }
680 i += 1;
681 }
682 }
683 true
684}
685
686#[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
688#[inline]
689fn is_all_lowercase_hex(bytes: &[u8]) -> bool {
690 const IS_LOWER_HEX: [bool; 256] = {
691 let mut t = [false; 256];
692 t[b'0' as usize] = true; t[b'1' as usize] = true; t[b'2' as usize] = true;
693 t[b'3' as usize] = true; t[b'4' as usize] = true; t[b'5' as usize] = true;
694 t[b'6' as usize] = true; t[b'7' as usize] = true; t[b'8' as usize] = true;
695 t[b'9' as usize] = true; t[b'a' as usize] = true; t[b'b' as usize] = true;
696 t[b'c' as usize] = true; t[b'd' as usize] = true; t[b'e' as usize] = true;
697 t[b'f' as usize] = true;
698 t
699 };
700 bytes.iter().all(|&b| IS_LOWER_HEX[b as usize])
701}
702
703pub async fn maybe_encrypt_inner(mut input: String, password: Option<String>) -> String {
706 let mut key: [u8; 32] = if password.is_none() {
707 crate::state::ENCRYPTION_KEY.get().expect("Encryption key must be set")
708 } else {
709 hash_pass(&password.unwrap()).await
710 };
711
712 let mut rng = rand::thread_rng();
713 let nonce_bytes: [u8; 12] = rng.gen();
714
715 let cipher = ChaCha20Poly1305::new_from_slice(&key)
716 .expect("Key should be valid");
717 let nonce: Nonce = nonce_bytes.into();
718
719 let ciphertext = cipher
720 .encrypt(&nonce, input.as_bytes())
721 .expect("Encryption should not fail");
722 input.zeroize();
723
724 let mut buffer = Vec::with_capacity(nonce_bytes.len() + ciphertext.len());
725 buffer.extend_from_slice(&nonce_bytes);
726 buffer.extend_from_slice(&ciphertext);
727
728 if !crate::state::ENCRYPTION_KEY.has_key() {
729 crate::state::ENCRYPTION_KEY.set(key, &[&crate::state::MY_SECRET_KEY]);
730 }
731
732 key.zeroize();
733
734 crate::simd::hex::bytes_to_hex_string(&buffer)
735}
736
737pub async fn maybe_decrypt_inner(ciphertext: String, password: Option<String>) -> Result<String, ()> {
740 let has_password = password.is_some();
741
742 let mut key: [u8; 32] = if let Some(pass) = password {
743 hash_pass(&pass).await
744 } else {
745 match crate::state::ENCRYPTION_KEY.get() {
746 Some(k) => k,
747 None => return Err(()),
748 }
749 };
750
751 let encrypted_data = crate::simd::hex::hex_string_to_bytes(ciphertext.as_str());
752 if encrypted_data.len() < 12 {
753 key.zeroize();
754 return Err(());
755 }
756
757 let (nonce_bytes, actual_ciphertext) = encrypted_data.split_at(12);
758
759 let cipher = match ChaCha20Poly1305::new_from_slice(&key) {
760 Ok(c) => c,
761 Err(_) => { key.zeroize(); return Err(()) }
762 };
763
764 let nonce_arr: [u8; 12] = match nonce_bytes.try_into() {
765 Ok(n) => n,
766 Err(_) => { key.zeroize(); return Err(()) }
767 };
768 let nonce: Nonce = nonce_arr.into();
769 let plaintext = match cipher.decrypt(&nonce, actual_ciphertext) {
770 Ok(pt) => pt,
771 Err(_) => { key.zeroize(); return Err(()) }
772 };
773
774 if has_password && !crate::state::ENCRYPTION_KEY.has_key() {
775 crate::state::ENCRYPTION_KEY.set(key, &[&crate::state::MY_SECRET_KEY]);
776 }
777
778 key.zeroize();
779
780 unsafe { Ok(String::from_utf8_unchecked(plaintext)) }
782}
783
784pub async fn maybe_encrypt(input: String) -> String {
786 if crate::state::is_encryption_enabled_fast() {
787 maybe_encrypt_inner(input, None).await
788 } else {
789 input
790 }
791}
792
793pub async fn maybe_decrypt(input: String) -> Result<String, ()> {
796 if crate::state::is_encryption_enabled_fast() {
797 match maybe_decrypt_inner(input.clone(), None).await {
798 Ok(decrypted) => Ok(decrypted),
799 Err(_) => {
800 if looks_encrypted(&input) { Err(()) } else { Ok(input) }
801 }
802 }
803 } else {
804 if looks_encrypted(&input) {
805 match maybe_decrypt_inner(input.clone(), None).await {
806 Ok(decrypted) => Ok(decrypted),
807 Err(_) => Ok(input),
808 }
809 } else {
810 Ok(input)
811 }
812 }
813}
814
815pub fn encrypt_blob_with_key(plaintext: &[u8], key: &[u8; 32]) -> Result<Vec<u8>, String> {
828 use rand::Rng;
829 let cipher = ChaCha20Poly1305::new_from_slice(key).map_err(|e| e.to_string())?;
830 let nonce_bytes: [u8; 12] = rand::thread_rng().gen();
831 let nonce = chacha20poly1305::Nonce::from(nonce_bytes);
832 let ct = cipher
833 .encrypt(&nonce, plaintext)
834 .map_err(|e| format!("blob encryption failed: {}", e))?;
835 let mut out = Vec::with_capacity(12 + ct.len());
836 out.extend_from_slice(&nonce_bytes);
837 out.extend_from_slice(&ct);
838 Ok(out)
839}
840
841pub fn decrypt_blob_with_key(stored: &[u8], key: &[u8; 32]) -> Result<Vec<u8>, String> {
843 if stored.len() < 12 + 16 {
844 return Err("blob too short to be ciphertext".to_string());
845 }
846 let cipher = ChaCha20Poly1305::new_from_slice(key).map_err(|e| e.to_string())?;
847 let (nonce_bytes, ct) = stored.split_at(12);
848 let nonce_arr: [u8; 12] = nonce_bytes.try_into().map_err(|_| "bad nonce".to_string())?;
849 let nonce = chacha20poly1305::Nonce::from(nonce_arr);
850 cipher
851 .decrypt(&nonce, ct)
852 .map_err(|_| "blob decryption failed (wrong key or corrupted)".to_string())
853}
854
855pub fn maybe_encrypt_blob(plaintext: &[u8]) -> Result<Vec<u8>, String> {
858 if !crate::state::is_encryption_enabled_fast() {
859 return Ok(plaintext.to_vec());
860 }
861 let mut key = crate::state::ENCRYPTION_KEY
862 .get()
863 .ok_or_else(|| "encryption enabled but key vault is empty".to_string())?;
864 let out = encrypt_blob_with_key(plaintext, &key);
865 key.zeroize();
866 out
867}
868
869pub fn maybe_decrypt_blob(stored: &[u8]) -> Vec<u8> {
873 if stored.len() == 32 {
875 return stored.to_vec();
876 }
877 match crate::state::ENCRYPTION_KEY.get() {
878 Some(mut key) => {
879 let out = decrypt_blob_with_key(stored, &key).unwrap_or_else(|_| stored.to_vec());
880 key.zeroize();
881 out
882 }
883 None => stored.to_vec(),
884 }
885}
886
887pub fn maybe_encrypt_text(plaintext: &str) -> Result<String, String> {
890 if !crate::state::is_encryption_enabled_fast() {
891 return Ok(plaintext.to_string());
892 }
893 let mut key = crate::state::ENCRYPTION_KEY
894 .get()
895 .ok_or_else(|| "encryption enabled but key vault is empty".to_string())?;
896 let out = encrypt_with_key(plaintext, &key);
897 key.zeroize();
898 out
899}
900
901pub fn maybe_decrypt_text(stored: &str) -> String {
904 if !looks_encrypted(stored) {
905 return stored.to_string();
906 }
907 match crate::state::ENCRYPTION_KEY.get() {
908 Some(mut key) => {
909 let out = decrypt_with_key(stored, &key).unwrap_or_else(|_| stored.to_string());
910 key.zeroize();
911 out
912 }
913 None => stored.to_string(),
914 }
915}
916
917#[cfg(test)]
918mod at_rest_tests {
919 use super::*;
920
921 #[test]
922 fn blob_roundtrip_with_explicit_key() {
923 let key = [7u8; 32];
924 let secret = [0x42u8; 32];
925 let ct = encrypt_blob_with_key(&secret, &key).unwrap();
926 assert_eq!(ct.len(), 12 + 32 + 16, "nonce + ciphertext + tag");
927 assert_ne!(&ct[12..44], &secret[..], "ciphertext must not equal plaintext");
928 assert_eq!(decrypt_blob_with_key(&ct, &key).unwrap(), secret.to_vec());
929 }
930
931 #[test]
932 fn blob_wrong_key_fails() {
933 let ct = encrypt_blob_with_key(&[1u8; 32], &[7u8; 32]).unwrap();
934 assert!(decrypt_blob_with_key(&ct, &[9u8; 32]).is_err());
935 }
936
937 #[test]
938 fn encrypted_text_is_always_detected_as_encrypted() {
939 let key = [3u8; 32];
942 for s in ["", "[]", "{}", "a"] {
943 let ct = encrypt_with_key(s, &key).unwrap();
944 assert!(looks_encrypted(&ct), "ciphertext for {:?} must look encrypted", s);
945 assert_eq!(decrypt_with_key(&ct, &key).unwrap(), s);
946 }
947 }
948}
949
950pub use crate::simd::image::nearest_neighbor_downsample_rgba;
956
957pub fn generate_thumbhash_from_rgba(pixels: &[u8], width: u32, height: u32) -> Option<String> {
962 use fast_thumbhash::{rgba_to_thumb_hash, base91_encode};
963
964 const MAX_DIM: u32 = 100;
965
966 let (thumb_w, thumb_h) = if width <= MAX_DIM && height <= MAX_DIM {
967 (width, height)
968 } else if width > height {
969 (MAX_DIM, (MAX_DIM * height / width).max(1))
970 } else {
971 ((MAX_DIM * width / height).max(1), MAX_DIM)
972 };
973
974 let thumbnail = if thumb_w == width && thumb_h == height {
975 pixels.to_vec()
976 } else {
977 nearest_neighbor_downsample_rgba(pixels, width, height, thumb_w, thumb_h)
978 };
979
980 let hash = rgba_to_thumb_hash(thumb_w as usize, thumb_h as usize, &thumbnail);
981 Some(base91_encode(&hash))
982}
983
984pub fn decode_image_bounded(bytes: &[u8]) -> Result<image::DynamicImage, String> {
994 use image::ImageDecoder;
995 let mut reader = image::ImageReader::new(std::io::Cursor::new(bytes))
996 .with_guessed_format()
997 .map_err(|e| format!("image format: {e}"))?;
998 reader.limits(bounded_image_limits());
999 let mut decoder = reader.into_decoder().map_err(|e| format!("image decode: {e}"))?;
1000 let orientation = decoder.orientation().map_err(|e| format!("image orientation: {e}"))?;
1002 let mut img = image::DynamicImage::from_decoder(decoder)
1003 .map_err(|e| format!("image decode: {e}"))?;
1004 img.apply_orientation(orientation);
1005 Ok(img)
1006}
1007
1008pub fn bounded_image_limits() -> image::Limits {
1011 let mut limits = image::Limits::default();
1012 limits.max_image_width = Some(16_384);
1013 limits.max_image_height = Some(16_384);
1014 limits.max_alloc = Some(256 * 1024 * 1024);
1015 limits
1016}
1017
1018pub fn generate_image_metadata(file_bytes: &[u8]) -> Option<crate::types::ImageMetadata> {
1023 let img = decode_image_bounded(file_bytes).ok()?;
1024 let width = img.width();
1025 let height = img.height();
1026
1027 let rgba = img.to_rgba8();
1028 let thumbhash = generate_thumbhash_from_rgba(rgba.as_raw(), width, height)?;
1029
1030 Some(crate::types::ImageMetadata {
1031 thumbhash,
1032 width,
1033 height,
1034 })
1035}
1036
1037#[cfg(test)]
1038mod tests {
1039 use super::*;
1040
1041 #[test]
1045 fn magic_bytes_name_video_and_bitmaps() {
1046 let mut mp4 = vec![0u8, 0, 0, 0x20];
1047 mp4.extend_from_slice(b"ftypisom");
1048 assert_eq!(mime_from_magic_bytes(&mp4), "video/mp4");
1049
1050 let mut mov = vec![0u8, 0, 0, 0x14];
1051 mov.extend_from_slice(b"ftypqt ");
1052 assert_eq!(mime_from_magic_bytes(&mov), "video/quicktime");
1053
1054 let mut webm = vec![0x1A, 0x45, 0xDF, 0xA3];
1055 webm.extend_from_slice(&[0u8; 20]);
1056 webm.extend_from_slice(b"webm");
1057 webm.extend_from_slice(&[0u8; 40]);
1058 assert_eq!(mime_from_magic_bytes(&webm), "video/webm");
1059
1060 let mut mkv = vec![0x1A, 0x45, 0xDF, 0xA3];
1061 mkv.extend_from_slice(&[0u8; 64]);
1062 assert_eq!(mime_from_magic_bytes(&mkv), "video/x-matroska");
1063
1064 let mut bmp = b"BM".to_vec();
1065 bmp.extend_from_slice(&[0u8; 20]);
1066 assert_eq!(mime_from_magic_bytes(&bmp), "image/bmp");
1067 }
1068
1069 #[test]
1072 fn every_recognised_mime_is_reachable_from_some_bytes() {
1073 let mut webm = vec![0x1A, 0x45, 0xDF, 0xA3];
1074 webm.extend_from_slice(&[0u8; 20]);
1075 webm.extend_from_slice(b"webm");
1076 webm.resize(80, 0);
1077 let mut mkv = vec![0x1A, 0x45, 0xDF, 0xA3];
1078 mkv.resize(80, 0);
1079 let mut webp = b"RIFF".to_vec();
1080 webp.extend_from_slice(&[0u8; 4]);
1081 webp.extend_from_slice(b"WEBP____");
1082
1083 let samples: Vec<Vec<u8>> = vec![
1084 vec![0x89, b'P', b'N', b'G'],
1085 vec![0xFF, 0xD8, 0xFF, 0xE0],
1086 b"GIF89a__".to_vec(),
1087 webp,
1088 vec![0x49, 0x49, 0x2A, 0x00],
1089 vec![0x00, 0x00, 0x01, 0x00],
1090 b"<svg xmlns".to_vec(),
1091 b"BM______".to_vec(),
1092 webm,
1093 mkv,
1094 b"\0\0\0\x14ftypqt ".to_vec(),
1095 b"\0\0\0\x20ftypisom".to_vec(),
1096 ];
1097 let reachable: std::collections::HashSet<&str> =
1098 samples.iter().map(|b| mime_from_magic_bytes(b)).collect();
1099 for m in RECOGNISED_MIMES {
1100 assert!(reachable.contains(m), "{m} is listed but no bytes produce it");
1101 }
1102 assert_eq!(reachable.len(), RECOGNISED_MIMES.len(), "and nothing is produced that is not listed");
1103 }
1104
1105 #[test]
1107 fn magic_bytes_still_name_the_image_types() {
1108 assert_eq!(mime_from_magic_bytes(&[0x89, b'P', b'N', b'G']), "image/png");
1109 assert_eq!(mime_from_magic_bytes(&[0xFF, 0xD8, 0xFF, 0xE0]), "image/jpeg");
1110 assert_eq!(mime_from_magic_bytes(b"GIF89a__"), "image/gif");
1111 let mut webp = b"RIFF".to_vec();
1112 webp.extend_from_slice(&[0u8; 4]);
1113 webp.extend_from_slice(b"WEBP____");
1114 assert_eq!(mime_from_magic_bytes(&webp), "image/webp");
1115 assert_eq!(mime_from_magic_bytes(b"<svg xmlns"), "image/svg+xml");
1116 assert_eq!(mime_from_magic_bytes(&[0xDE, 0xAD, 0xBE, 0xEF]), "application/octet-stream");
1117 assert_eq!(mime_from_magic_bytes(&[]), "application/octet-stream", "a short read is not a type");
1118 assert_eq!(mime_from_magic_bytes(b"Bm"), "application/octet-stream", "and BMP is case-sensitive");
1119 }
1120
1121 fn test_key() -> [u8; 32] {
1126 [0x42u8; 32]
1127 }
1128
1129 fn alt_key() -> [u8; 32] {
1130 [0x99u8; 32]
1131 }
1132
1133 #[test]
1134 fn encrypt_decrypt_roundtrip_simple() {
1135 let key = test_key();
1136 let plaintext = "hello world";
1137 let encrypted = encrypt_with_key(plaintext, &key).expect("encryption should succeed");
1138 let decrypted = decrypt_with_key(&encrypted, &key).expect("decryption should succeed");
1139 assert_eq!(decrypted, plaintext, "roundtrip should preserve plaintext");
1140 }
1141
1142 #[test]
1143 fn encrypt_decrypt_100_random_strings() {
1144 use rand::Rng;
1145 let key = test_key();
1146 let mut rng = rand::thread_rng();
1147 for i in 0..100 {
1148 let len = rng.gen_range(1..=200);
1149 let s: String = (0..len).map(|_| rng.gen_range(0x20u8..0x7f) as char).collect();
1150 let enc = encrypt_with_key(&s, &key)
1151 .unwrap_or_else(|e| panic!("encryption failed on iteration {}: {}", i, e));
1152 let dec = decrypt_with_key(&enc, &key)
1153 .unwrap_or_else(|e| panic!("decryption failed on iteration {}: {}", i, e));
1154 assert_eq!(dec, s, "roundtrip failed on iteration {}", i);
1155 }
1156 }
1157
1158 #[test]
1159 fn encrypt_decrypt_empty_string() {
1160 let key = test_key();
1161 let encrypted = encrypt_with_key("", &key).expect("encrypting empty string should succeed");
1162 let decrypted = decrypt_with_key(&encrypted, &key).expect("decrypting empty string should succeed");
1163 assert_eq!(decrypted, "", "empty string roundtrip should produce empty string");
1164 }
1165
1166 #[test]
1167 fn encrypt_decrypt_large_string() {
1168 let key = test_key();
1169 let plaintext = "A".repeat(10 * 1024); let encrypted = encrypt_with_key(&plaintext, &key).expect("encrypting 10KB should succeed");
1171 let decrypted = decrypt_with_key(&encrypted, &key).expect("decrypting 10KB should succeed");
1172 assert_eq!(decrypted, plaintext, "10KB roundtrip should preserve content");
1173 }
1174
1175 #[test]
1176 fn decrypt_with_wrong_key_fails() {
1177 let key = test_key();
1178 let wrong_key = alt_key();
1179 let encrypted = encrypt_with_key("secret data", &key).expect("encryption should succeed");
1180 let result = decrypt_with_key(&encrypted, &wrong_key);
1181 assert!(result.is_err(), "decryption with wrong key should fail");
1182 }
1183
1184 #[test]
1185 fn decrypt_corrupted_ciphertext_fails() {
1186 let key = test_key();
1187 let mut encrypted = encrypt_with_key("secret data", &key).expect("encryption should succeed");
1188 let bytes: Vec<u8> = encrypted.bytes().collect();
1190 if bytes.len() > 30 {
1191 let mut chars: Vec<char> = encrypted.chars().collect();
1192 chars[30] = if chars[30] == '0' { 'f' } else { '0' };
1194 encrypted = chars.into_iter().collect();
1195 }
1196 let result = decrypt_with_key(&encrypted, &key);
1197 assert!(result.is_err(), "decryption of corrupted ciphertext should fail");
1198 }
1199
1200 #[test]
1201 fn different_keys_produce_different_ciphertext() {
1202 let key1 = test_key();
1203 let key2 = alt_key();
1204 let plaintext = "same plaintext";
1205 let enc1 = encrypt_with_key(plaintext, &key1).expect("enc1 should succeed");
1206 let enc2 = encrypt_with_key(plaintext, &key2).expect("enc2 should succeed");
1207 assert_ne!(enc1, enc2, "different keys should produce different ciphertext");
1209 }
1210
1211 #[test]
1212 fn nonce_is_always_different() {
1213 let key = test_key();
1214 let plaintext = "same string encrypted twice";
1215 let enc1 = encrypt_with_key(plaintext, &key).expect("enc1 should succeed");
1216 let enc2 = encrypt_with_key(plaintext, &key).expect("enc2 should succeed");
1217 let nonce1 = &enc1[..24];
1219 let nonce2 = &enc2[..24];
1220 assert_ne!(nonce1, nonce2, "nonces should differ between encryptions of the same plaintext");
1221 }
1222
1223 #[test]
1224 fn unicode_content_preserved() {
1225 let key = test_key();
1226 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}";
1227 let encrypted = encrypt_with_key(plaintext, &key).expect("encrypting unicode should succeed");
1228 let decrypted = decrypt_with_key(&encrypted, &key).expect("decrypting unicode should succeed");
1229 assert_eq!(decrypted, plaintext, "unicode content should be preserved through encrypt/decrypt");
1230 }
1231
1232 #[test]
1233 fn decrypt_too_short_ciphertext_fails() {
1234 let key = test_key();
1235 let result = decrypt_with_key("abcdef", &key);
1236 assert!(result.is_err(), "ciphertext shorter than 24 hex chars should fail");
1237 assert!(result.unwrap_err().contains("too short"), "error should mention too short");
1238 }
1239
1240 #[test]
1241 fn encrypt_output_is_hex_encoded() {
1242 let key = test_key();
1243 let encrypted = encrypt_with_key("test", &key).expect("encryption should succeed");
1244 assert!(encrypted.chars().all(|c| c.is_ascii_hexdigit()),
1245 "encrypted output should be entirely hex characters");
1246 }
1247
1248 #[test]
1249 fn encrypt_output_has_correct_structure() {
1250 let key = test_key();
1251 let encrypted = encrypt_with_key("test", &key).expect("encryption should succeed");
1252 assert!(encrypted.len() > 24,
1254 "encrypted output should have nonce (24 hex chars) plus ciphertext");
1255 assert_eq!(encrypted.len() % 2, 0,
1257 "encrypted output length should be even (hex pairs)");
1258 }
1259
1260 #[test]
1261 fn encrypt_decrypt_special_characters() {
1262 let key = test_key();
1263 let plaintext = r#"!@#$%^&*()_+-=[]{}|;':",.<>?/\`~"#;
1264 let encrypted = encrypt_with_key(plaintext, &key).expect("encrypting special chars should succeed");
1265 let decrypted = decrypt_with_key(&encrypted, &key).expect("decrypting special chars should succeed");
1266 assert_eq!(decrypted, plaintext, "special characters should survive roundtrip");
1267 }
1268
1269 #[test]
1270 fn encrypt_decrypt_newlines_and_whitespace() {
1271 let key = test_key();
1272 let plaintext = "line1\nline2\r\nline3\ttab\0null";
1273 let encrypted = encrypt_with_key(plaintext, &key).expect("encrypting whitespace should succeed");
1274 let decrypted = decrypt_with_key(&encrypted, &key).expect("decrypting whitespace should succeed");
1275 assert_eq!(decrypted, plaintext, "whitespace and control chars should survive roundtrip");
1276 }
1277
1278 #[test]
1279 fn decrypt_invalid_hex_in_nonce_fails() {
1280 let key = test_key();
1281 let bad = "zzzzzzzzzzzzzzzzzzzzzzzz0000000000000000";
1283 let result = decrypt_with_key(bad, &key);
1284 assert!(result.is_err(), "invalid hex in nonce should fail");
1285 }
1286
1287 #[test]
1288 fn decrypt_invalid_hex_in_ciphertext_fails() {
1289 let key = test_key();
1290 let bad = "000000000000000000000000gggggggg";
1292 let result = decrypt_with_key(bad, &key);
1293 assert!(result.is_err(), "invalid hex in ciphertext should fail");
1294 }
1295
1296 #[test]
1301 fn hex_encode_decode_roundtrip() {
1302 let data = vec![0x00, 0x01, 0xff, 0x80, 0x7f];
1303 let encoded = hex::encode(&data);
1304 let decoded = hex::decode(&encoded).expect("decode should succeed");
1305 assert_eq!(decoded, data, "hex encode/decode roundtrip should preserve bytes");
1306 }
1307
1308 #[test]
1309 fn hex_decode_odd_length_error() {
1310 let result = hex::decode("abc");
1311 assert!(result.is_err(), "odd-length hex string should fail");
1312 assert!(result.unwrap_err().contains("Odd-length"), "error should mention odd-length");
1313 }
1314
1315 #[test]
1316 fn hex_decode_invalid_hex_error() {
1317 let result = hex::decode("zzzz");
1318 assert!(result.is_err(), "invalid hex characters should fail");
1319 }
1320
1321 #[test]
1322 fn hex_encode_empty() {
1323 assert_eq!(hex::encode(&[]), "", "encoding empty bytes should produce empty string");
1324 }
1325
1326 #[test]
1327 fn hex_decode_empty() {
1328 let decoded = hex::decode("").expect("decoding empty string should succeed");
1329 assert!(decoded.is_empty(), "decoding empty string should produce empty vec");
1330 }
1331
1332 #[tokio::test]
1337 async fn hash_pass_deterministic() {
1338 let key1 = hash_pass("my_password").await;
1339 let key2 = hash_pass("my_password").await;
1340 assert_eq!(key1, key2, "same password should always produce the same key");
1341 }
1342
1343 #[tokio::test]
1344 async fn hash_pass_different_passwords_different_keys() {
1345 let key1 = hash_pass("password_one").await;
1346 let key2 = hash_pass("password_two").await;
1347 assert_ne!(key1, key2, "different passwords should produce different keys");
1348 }
1349
1350 #[tokio::test]
1351 async fn hash_pass_output_is_32_bytes() {
1352 let key = hash_pass("test_password").await;
1353 assert_eq!(key.len(), 32, "hash_pass should produce exactly 32 bytes");
1354 assert!(key.iter().any(|&b| b != 0), "hash output should not be all zeros");
1356 }
1357
1358 #[test]
1359 fn generate_thumbhash_from_bytes_basic() {
1360 let pixels: Vec<u8> = vec![
1362 255, 0, 0, 255, 255, 0, 0, 255,
1363 255, 0, 0, 255, 255, 0, 0, 255,
1364 ];
1365 let result = super::generate_thumbhash_from_rgba(&pixels, 2, 2);
1366 assert!(result.is_some());
1367 assert!(!result.unwrap().is_empty());
1368 }
1369
1370 #[test]
1371 fn generate_image_metadata_from_bytes() {
1372 let img = image::RgbaImage::from_pixel(4, 4, image::Rgba([0, 0, 255, 255]));
1374 let mut buf = Vec::new();
1375 let mut cursor = std::io::Cursor::new(&mut buf);
1376 img.write_to(&mut cursor, image::ImageFormat::Png).unwrap();
1377
1378 let meta = super::generate_image_metadata(&buf);
1379 assert!(meta.is_some());
1380 let meta = meta.unwrap();
1381 assert_eq!(meta.width, 4);
1382 assert_eq!(meta.height, 4);
1383 assert!(!meta.thumbhash.is_empty());
1384 }
1385
1386 #[test]
1387 fn generate_image_metadata_non_image() {
1388 let text_bytes = b"this is not an image";
1389 let meta = super::generate_image_metadata(text_bytes);
1390 assert!(meta.is_none());
1391 }
1392}