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