use crate::config::Config; use sodiumoxide::base64; use std::sync::{Arc, RwLock}; lazy_static::lazy_static! { pub static ref TEMPORARY_PASSWORD:Arc> = Arc::new(RwLock::new(get_auto_password())); } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum VerificationMethod { OnlyUseTemporaryPassword, OnlyUsePermanentPassword, UseBothPasswords, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ApproveMode { Both, Password, Click, } fn get_auto_password() -> String { let len = temporary_password_length(); if Config::get_bool_option(crate::config::keys::OPTION_ALLOW_NUMERNIC_ONE_TIME_PASSWORD) { Config::get_auto_numeric_password(len) } else { Config::get_auto_password(len) } } // Should only be called in server pub fn update_temporary_password() { *TEMPORARY_PASSWORD.write().unwrap() = get_auto_password(); } // Should only be called in server pub fn temporary_password() -> String { TEMPORARY_PASSWORD.read().unwrap().clone() } fn verification_method() -> VerificationMethod { let method = Config::get_option("verification-method"); if method == "use-temporary-password" { VerificationMethod::OnlyUseTemporaryPassword } else if method == "use-permanent-password" { VerificationMethod::OnlyUsePermanentPassword } else { VerificationMethod::UseBothPasswords // default } } pub fn temporary_password_length() -> usize { let length = Config::get_option("temporary-password-length"); if length == "8" { 8 } else if length == "10" { 10 } else { 6 // default } } pub fn temporary_enabled() -> bool { verification_method() != VerificationMethod::OnlyUsePermanentPassword } pub fn permanent_enabled() -> bool { verification_method() != VerificationMethod::OnlyUseTemporaryPassword } pub fn has_valid_password() -> bool { temporary_enabled() && !temporary_password().is_empty() || permanent_enabled() && Config::has_permanent_password() } pub fn approve_mode() -> ApproveMode { let mode = Config::get_option("approve-mode"); if mode == "password" { ApproveMode::Password } else if mode == "click" { ApproveMode::Click } else { ApproveMode::Both } } pub fn hide_cm() -> bool { approve_mode() == ApproveMode::Password && verification_method() == VerificationMethod::OnlyUsePermanentPassword && crate::config::option2bool("allow-hide-cm", &Config::get_option("allow-hide-cm")) } const VERSION_LEN: usize = 2; // Check if data is already encrypted by verifying: // 1) version prefix "00" // 2) valid base64 payload // 3) decoded payload length >= secretbox::MACBYTES // // We intentionally avoid trying to decrypt here because key mismatch would cause // false negatives. // Reference: secretbox::seal returns ciphertext length = plaintext length + MACBYTES // https://github.com/sodiumoxide/sodiumoxide/blob/3057acb1a030ad86ed8892a223d64036ab5e8523/src/crypto/secretbox/xsalsa20poly1305.rs#L67 fn is_encrypted(v: &[u8]) -> bool { if v.len() <= VERSION_LEN || !v.starts_with(b"00") { return false; } match base64::decode(&v[VERSION_LEN..], base64::Variant::Original) { Ok(decoded) => decoded.len() >= sodiumoxide::crypto::secretbox::MACBYTES, Err(_) => false, } } pub fn encrypt_str_or_original(s: &str, version: &str, max_len: usize) -> String { if is_encrypted(s.as_bytes()) { log::error!("Duplicate encryption!"); return s.to_owned(); } if s.chars().count() > max_len { return String::default(); } if version == "00" { if let Ok(s) = encrypt(s.as_bytes()) { return version.to_owned() + &s; } } s.to_owned() } // String: password // bool: whether decryption is successful // bool: whether should store to re-encrypt when load // note: s.len() return length in bytes, s.chars().count() return char count // &[..2] return the left 2 bytes, s.chars().take(2) return the left 2 chars pub fn decrypt_str_or_original(s: &str, current_version: &str) -> (String, bool, bool) { if s.len() > VERSION_LEN { if s.starts_with("00") { if let Ok(v) = decrypt(s[VERSION_LEN..].as_bytes()) { return ( String::from_utf8_lossy(&v).to_string(), true, "00" != current_version, ); } } } // For values that already look encrypted (version prefix + base64), avoid // repeated store on each load when decryption fails. ( s.to_owned(), false, !s.is_empty() && !is_encrypted(s.as_bytes()), ) } pub fn encrypt_vec_or_original(v: &[u8], version: &str, max_len: usize) -> Vec { if is_encrypted(v) { log::error!("Duplicate encryption!"); return v.to_owned(); } if v.len() > max_len { return vec![]; } if version == "00" { if let Ok(s) = encrypt(v) { let mut version = version.to_owned().into_bytes(); version.append(&mut s.into_bytes()); return version; } } v.to_owned() } // Vec: password // bool: whether decryption is successful // bool: whether should store to re-encrypt when load pub fn decrypt_vec_or_original(v: &[u8], current_version: &str) -> (Vec, bool, bool) { if v.len() > VERSION_LEN { let version = String::from_utf8_lossy(&v[..VERSION_LEN]); if version == "00" { if let Ok(v) = decrypt(&v[VERSION_LEN..]) { return (v, true, version != current_version); } } } // For values that already look encrypted (version prefix + base64), avoid // repeated store on each load when decryption fails. (v.to_owned(), false, !v.is_empty() && !is_encrypted(v)) } fn encrypt(v: &[u8]) -> Result { if !v.is_empty() { symmetric_crypt(v, true).map(|v| base64::encode(v, base64::Variant::Original)) } else { Err(()) } } fn decrypt(v: &[u8]) -> Result, ()> { if !v.is_empty() { base64::decode(v, base64::Variant::Original).and_then(|v| symmetric_crypt(&v, false)) } else { Err(()) } } pub fn symmetric_crypt(data: &[u8], encrypt: bool) -> Result, ()> { use sodiumoxide::crypto::secretbox; use std::convert::TryInto; let uuid = crate::get_uuid(); let mut keybuf = uuid.clone(); keybuf.resize(secretbox::KEYBYTES, 0); let key = secretbox::Key(keybuf.try_into().map_err(|_| ())?); let nonce = secretbox::Nonce([0; secretbox::NONCEBYTES]); if encrypt { Ok(secretbox::seal(data, &nonce, &key)) } else { let res = secretbox::open(data, &nonce, &key); #[cfg(not(any(target_os = "android", target_os = "ios")))] if res.is_err() { // Fallback: try pk if uuid decryption failed (in case encryption used pk due to machine_uid failure) if let Some(key_pair) = Config::get_existing_key_pair() { let pk = key_pair.1; if pk != uuid { let mut keybuf = pk; keybuf.resize(secretbox::KEYBYTES, 0); let pk_key = secretbox::Key(keybuf.try_into().map_err(|_| ())?); return secretbox::open(data, &nonce, &pk_key); } } } res } } mod test { #[test] fn test() { use super::*; use rand::{thread_rng, Rng}; use std::time::Instant; let version = "00"; let max_len = 128; println!("test str"); let data = "1ü1111"; let encrypted = encrypt_str_or_original(data, version, max_len); let (decrypted, succ, store) = decrypt_str_or_original(&encrypted, version); println!("data: {data}"); println!("encrypted: {encrypted}"); println!("decrypted: {decrypted}"); assert_eq!(data, decrypted); assert_eq!(version, &encrypted[..2]); assert!(succ); assert!(!store); let (_, _, store) = decrypt_str_or_original(&encrypted, "99"); assert!(store); assert!(!decrypt_str_or_original(&decrypted, version).1); assert_eq!( encrypt_str_or_original(&encrypted, version, max_len), encrypted ); println!("test vec"); let data: Vec = "1ü1111".as_bytes().to_vec(); let encrypted = encrypt_vec_or_original(&data, version, max_len); let (decrypted, succ, store) = decrypt_vec_or_original(&encrypted, version); println!("data: {data:?}"); println!("encrypted: {encrypted:?}"); println!("decrypted: {decrypted:?}"); assert_eq!(data, decrypted); assert_eq!(version.as_bytes(), &encrypted[..2]); assert!(!store); assert!(succ); let (_, _, store) = decrypt_vec_or_original(&encrypted, "99"); assert!(store); assert!(!decrypt_vec_or_original(&decrypted, version).1); assert_eq!( encrypt_vec_or_original(&encrypted, version, max_len), encrypted ); println!("test original"); let data = version.to_string() + "Hello World"; let (decrypted, succ, store) = decrypt_str_or_original(&data, version); assert_eq!(data, decrypted); assert!(store); assert!(!succ); let verbytes = version.as_bytes(); let data: Vec = vec![verbytes[0], verbytes[1], 1, 2, 3, 4, 5, 6]; let (decrypted, succ, store) = decrypt_vec_or_original(&data, version); assert_eq!(data, decrypted); assert!(store); assert!(!succ); let (_, succ, store) = decrypt_str_or_original("", version); assert!(!store); assert!(!succ); let (_, succ, store) = decrypt_vec_or_original(&[], version); assert!(!store); assert!(!succ); let data = "1ü1111"; assert_eq!(decrypt_str_or_original(data, version).0, data); let data: Vec = "1ü1111".as_bytes().to_vec(); assert_eq!(decrypt_vec_or_original(&data, version).0, data); // Base64-shaped "00" prefixed values shorter than MACBYTES are treated // as original/plain values and should be stored. let data = "00YWJjZA=="; let (decrypted, succ, store) = decrypt_str_or_original(data, version); assert_eq!(decrypted, data); assert!(!succ); assert!(store); let data = b"00YWJjZA==".to_vec(); let (decrypted, succ, store) = decrypt_vec_or_original(&data, version); assert_eq!(decrypted, data); assert!(!succ); assert!(store); // When decoded length reaches MACBYTES, it is treated as encrypted-like // and should not trigger repeated store. let exact_mac = vec![0u8; sodiumoxide::crypto::secretbox::MACBYTES]; let exact_mac_b64 = sodiumoxide::base64::encode(&exact_mac, sodiumoxide::base64::Variant::Original); let data = format!("00{exact_mac_b64}"); let (_, succ, store) = decrypt_str_or_original(&data, version); assert!(!succ); assert!(!store); let data = data.into_bytes(); let (_, succ, store) = decrypt_vec_or_original(&data, version); assert!(!succ); assert!(!store); println!("test speed"); let test_speed = |len: usize, name: &str| { let mut data: Vec = vec![]; let mut rng = thread_rng(); for _ in 0..len { data.push(rng.gen_range(0..255)); } let start: Instant = Instant::now(); let encrypted = encrypt_vec_or_original(&data, version, len); assert_ne!(data, decrypted); let t1 = start.elapsed(); let start = Instant::now(); let (decrypted, _, _) = decrypt_vec_or_original(&encrypted, version); let t2 = start.elapsed(); assert_eq!(data, decrypted); println!("{name}"); println!("encrypt:{:?}, decrypt:{:?}", t1, t2); let start: Instant = Instant::now(); let encrypted = base64::encode(&data, base64::Variant::Original); let t1 = start.elapsed(); let start = Instant::now(); let decrypted = base64::decode(&encrypted, base64::Variant::Original).unwrap(); let t2 = start.elapsed(); assert_eq!(data, decrypted); println!("base64, encrypt:{:?}, decrypt:{:?}", t1, t2,); }; test_speed(128, "128"); test_speed(1024, "1k"); test_speed(1024 * 1024, "1M"); test_speed(10 * 1024 * 1024, "10M"); test_speed(100 * 1024 * 1024, "100M"); } #[test] fn test_is_encrypted() { use super::*; use sodiumoxide::base64::{encode, Variant}; use sodiumoxide::crypto::secretbox; // Empty data should not be considered encrypted assert!(!is_encrypted(b"")); assert!(!is_encrypted(b"0")); assert!(!is_encrypted(b"00")); // Data without "00" prefix should not be considered encrypted assert!(!is_encrypted(b"01abcd")); assert!(!is_encrypted(b"99abcd")); assert!(!is_encrypted(b"hello world")); // Data with "00" prefix but invalid base64 should not be considered encrypted assert!(!is_encrypted(b"00!!!invalid base64!!!")); assert!(!is_encrypted(b"00@#$%")); // Data with "00" prefix and valid base64 but shorter than MACBYTES is not encrypted assert!(!is_encrypted(b"00YWJjZA==")); // "abcd" in base64 assert!(!is_encrypted(b"00SGVsbG8gV29ybGQ=")); // "Hello World" in base64 // Data with "00" prefix and valid base64 with decoded len == MACBYTES is considered encrypted let exact_mac = vec![0u8; secretbox::MACBYTES]; let exact_mac_b64 = encode(&exact_mac, Variant::Original); let exact_mac_candidate = format!("00{exact_mac_b64}"); assert!(is_encrypted(exact_mac_candidate.as_bytes())); // Real encrypted data should be detected let version = "00"; let max_len = 128; let encrypted_str = encrypt_str_or_original("1", version, max_len); assert!(is_encrypted(encrypted_str.as_bytes())); let encrypted_vec = encrypt_vec_or_original(b"1", version, max_len); assert!(is_encrypted(&encrypted_vec)); // Original unencrypted data should not be detected as encrypted assert!(!is_encrypted(b"1")); assert!(!is_encrypted("1".as_bytes())); } #[test] fn test_encrypted_payload_min_len_macbytes() { use super::*; use sodiumoxide::base64::{decode, Variant}; use sodiumoxide::crypto::secretbox; let version = "00"; let max_len = 128; let encrypted_str = encrypt_str_or_original("1", version, max_len); let decoded = decode(&encrypted_str.as_bytes()[VERSION_LEN..], Variant::Original).unwrap(); assert!( decoded.len() >= secretbox::MACBYTES, "decoded encrypted payload must be at least MACBYTES" ); let encrypted_vec = encrypt_vec_or_original(b"1", version, max_len); let decoded = decode(&encrypted_vec[VERSION_LEN..], Variant::Original).unwrap(); assert!( decoded.len() >= secretbox::MACBYTES, "decoded encrypted payload must be at least MACBYTES" ); } // Test decryption fallback when data was encrypted with key_pair but decryption tries machine_uid first #[test] #[cfg(not(any(target_os = "android", target_os = "ios")))] fn test_decrypt_with_pk_fallback() { use sodiumoxide::crypto::secretbox; use std::convert::TryInto; let uuid = crate::get_uuid(); let pk = crate::config::Config::get_key_pair().1; // Ensure uuid != pk, otherwise fallback branch won't be tested if uuid == pk { eprintln!("skip: uuid == pk, fallback branch won't be tested"); return; } let data = b"test password 123"; let nonce = secretbox::Nonce([0; secretbox::NONCEBYTES]); // Encrypt with pk (simulating machine_uid failure during encryption) let mut pk_keybuf = pk; pk_keybuf.resize(secretbox::KEYBYTES, 0); let pk_key = secretbox::Key(pk_keybuf.try_into().unwrap()); let encrypted = secretbox::seal(data, &nonce, &pk_key); // Decrypt using symmetric_crypt (should fallback to pk since uuid differs) let decrypted = super::symmetric_crypt(&encrypted, false); assert!( decrypted.is_ok(), "Decryption with pk fallback should succeed" ); assert_eq!(decrypted.unwrap(), data); } }