feat: add vault encrypt/decrypt for entries and manifest
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
99
crates/idfoto-core/src/vault.rs
Normal file
99
crates/idfoto-core/src/vault.rs
Normal file
@@ -0,0 +1,99 @@
|
||||
use crate::crypto;
|
||||
use crate::entry::{Entry, Manifest};
|
||||
use crate::error::Result;
|
||||
|
||||
pub fn encrypt_entry(master_key: &[u8; 32], entry: &Entry) -> Result<Vec<u8>> {
|
||||
let json = serde_json::to_vec(entry)?;
|
||||
crypto::encrypt(master_key, &json)
|
||||
}
|
||||
|
||||
pub fn decrypt_entry(master_key: &[u8; 32], data: &[u8]) -> Result<Entry> {
|
||||
let json = crypto::decrypt(master_key, data)?;
|
||||
let entry: Entry = serde_json::from_slice(&json)?;
|
||||
Ok(entry)
|
||||
}
|
||||
|
||||
pub fn encrypt_manifest(master_key: &[u8; 32], manifest: &Manifest) -> Result<Vec<u8>> {
|
||||
let json = serde_json::to_vec(manifest)?;
|
||||
crypto::encrypt(master_key, &json)
|
||||
}
|
||||
|
||||
pub fn decrypt_manifest(master_key: &[u8; 32], data: &[u8]) -> Result<Manifest> {
|
||||
let json = crypto::decrypt(master_key, data)?;
|
||||
let manifest: Manifest = serde_json::from_slice(&json)?;
|
||||
Ok(manifest)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::entry::ManifestEntry;
|
||||
|
||||
fn test_key_a() -> [u8; 32] {
|
||||
[0x42u8; 32]
|
||||
}
|
||||
|
||||
fn test_key_b() -> [u8; 32] {
|
||||
[0x99u8; 32]
|
||||
}
|
||||
|
||||
fn sample_entry() -> Entry {
|
||||
Entry {
|
||||
name: "GitHub".to_string(),
|
||||
url: Some("https://github.com".to_string()),
|
||||
username: Some("alice".to_string()),
|
||||
password: "secret123".to_string(),
|
||||
notes: None,
|
||||
totp_secret: None,
|
||||
created_at: "2024-01-01T00:00:00Z".to_string(),
|
||||
updated_at: "2024-01-01T00:00:00Z".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entry_encrypt_decrypt_round_trip() {
|
||||
let key = test_key_a();
|
||||
let entry = sample_entry();
|
||||
|
||||
let ciphertext = encrypt_entry(&key, &entry).unwrap();
|
||||
let decoded = decrypt_entry(&key, &ciphertext).unwrap();
|
||||
|
||||
assert_eq!(decoded.name, "GitHub");
|
||||
assert_eq!(decoded.password, "secret123");
|
||||
assert_eq!(decoded.username, Some("alice".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_encrypt_decrypt_round_trip() {
|
||||
let key = test_key_a();
|
||||
let mut manifest = Manifest::new();
|
||||
manifest.add_entry(
|
||||
"deadbeef".to_string(),
|
||||
ManifestEntry {
|
||||
name: "GitHub".to_string(),
|
||||
url: Some("https://github.com".to_string()),
|
||||
username: Some("alice".to_string()),
|
||||
updated_at: "2024-01-01T00:00:00Z".to_string(),
|
||||
},
|
||||
);
|
||||
|
||||
let ciphertext = encrypt_manifest(&key, &manifest).unwrap();
|
||||
let decoded = decrypt_manifest(&key, &ciphertext).unwrap();
|
||||
|
||||
assert_eq!(decoded.version, 1);
|
||||
assert!(decoded.entries.contains_key("deadbeef"));
|
||||
assert_eq!(decoded.entries["deadbeef"].name, "GitHub");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entry_wrong_key_fails() {
|
||||
let key_a = test_key_a();
|
||||
let key_b = test_key_b();
|
||||
let entry = sample_entry();
|
||||
|
||||
let ciphertext = encrypt_entry(&key_a, &entry).unwrap();
|
||||
let result = decrypt_entry(&key_b, &ciphertext);
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user