diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 1fd22b1..3317ab2 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -2,6 +2,7 @@ mod commands; mod db; mod error; mod models; +mod services; use std::sync::{Arc, Mutex}; use tauri::Manager; diff --git a/src-tauri/src/services/crypto.rs b/src-tauri/src/services/crypto.rs new file mode 100644 index 0000000..979547d --- /dev/null +++ b/src-tauri/src/services/crypto.rs @@ -0,0 +1,105 @@ +use aes_gcm::{ + aead::{Aead, KeyInit, OsRng}, + Aes256Gcm, Key, Nonce, +}; +use base64::{engine::general_purpose::STANDARD, Engine}; +use rand::RngCore; + +pub fn encrypt(key: &[u8; 32], plaintext: &str) -> Result { + let cipher = Aes256Gcm::new(Key::::from_slice(key)); + let mut nonce_bytes = [0u8; 12]; + OsRng.fill_bytes(&mut nonce_bytes); + let nonce = Nonce::from_slice(&nonce_bytes); + let ciphertext = cipher + .encrypt(nonce, plaintext.as_bytes()) + .map_err(|e| format!("encryption failed: {}", e))?; + let mut combined = nonce_bytes.to_vec(); + combined.extend(ciphertext); + Ok(STANDARD.encode(&combined)) +} + +pub fn decrypt(key: &[u8; 32], encrypted: &str) -> Result { + let combined = STANDARD + .decode(encrypted) + .map_err(|e| format!("base64 decode failed: {}", e))?; + if combined.len() < 13 { + return Err("encrypted data too short".to_string()); + } + let (nonce_bytes, ciphertext) = combined.split_at(12); + let cipher = Aes256Gcm::new(Key::::from_slice(key)); + let nonce = Nonce::from_slice(nonce_bytes); + let plaintext = cipher + .decrypt(nonce, ciphertext) + .map_err(|_| "decryption failed (wrong key or corrupted data)".to_string())?; + String::from_utf8(plaintext).map_err(|e| format!("invalid UTF-8: {}", e)) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn test_key() -> [u8; 32] { + [42u8; 32] + } + + #[test] + fn test_encrypt_decrypt_roundtrip() { + let key = test_key(); + let plaintext = "hello world"; + let encrypted = encrypt(&key, plaintext).unwrap(); + let decrypted = decrypt(&key, &encrypted).unwrap(); + assert_eq!(decrypted, plaintext); + } + + #[test] + fn test_encrypt_produces_different_ciphertext() { + let key = test_key(); + let plaintext = "same input"; + let enc1 = encrypt(&key, plaintext).unwrap(); + let enc2 = encrypt(&key, plaintext).unwrap(); + assert_ne!(enc1, enc2); + } + + #[test] + fn test_decrypt_with_wrong_key_fails() { + let key = test_key(); + let wrong_key = [99u8; 32]; + let encrypted = encrypt(&key, "secret").unwrap(); + let result = decrypt(&wrong_key, &encrypted); + assert!(result.is_err()); + } + + #[test] + fn test_decrypt_invalid_base64_fails() { + let key = test_key(); + let result = decrypt(&key, "not valid base64!!!"); + assert!(result.is_err()); + } + + #[test] + fn test_decrypt_too_short_fails() { + let key = test_key(); + // Base64 of 5 bytes (less than 13) + let short = STANDARD.encode(&[0u8; 5]); + let result = decrypt(&key, &short); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("too short")); + } + + #[test] + fn test_encrypt_empty_string() { + let key = test_key(); + let encrypted = encrypt(&key, "").unwrap(); + let decrypted = decrypt(&key, &encrypted).unwrap(); + assert_eq!(decrypted, ""); + } + + #[test] + fn test_encrypt_unicode() { + let key = test_key(); + let plaintext = "héllo wörld àèìòù"; + let encrypted = encrypt(&key, plaintext).unwrap(); + let decrypted = decrypt(&key, &encrypted).unwrap(); + assert_eq!(decrypted, plaintext); + } +} diff --git a/src-tauri/src/services/mod.rs b/src-tauri/src/services/mod.rs new file mode 100644 index 0000000..274f0ed --- /dev/null +++ b/src-tauri/src/services/mod.rs @@ -0,0 +1 @@ +pub mod crypto;