feat: AES-256-GCM crypto service for credential encryption
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e72372fca8
commit
9a451061ea
3 changed files with 107 additions and 0 deletions
|
|
@ -2,6 +2,7 @@ mod commands;
|
|||
mod db;
|
||||
mod error;
|
||||
mod models;
|
||||
mod services;
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tauri::Manager;
|
||||
|
|
|
|||
105
src-tauri/src/services/crypto.rs
Normal file
105
src-tauri/src/services/crypto.rs
Normal file
|
|
@ -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<String, String> {
|
||||
let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::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<String, String> {
|
||||
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::<Aes256Gcm>::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);
|
||||
}
|
||||
}
|
||||
1
src-tauri/src/services/mod.rs
Normal file
1
src-tauri/src/services/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub mod crypto;
|
||||
Loading…
Reference in a new issue