//! Typed encryption/decryption wrappers for vault entries and manifests. //! //! This module bridges the gap between the raw bytes-in/bytes-out layer in //! [`crate::crypto`] and the typed data model in [`crate::entry`]. Each function //! follows the same pattern: //! //! - **Encrypt**: serialize the struct to JSON via serde, then encrypt the JSON //! bytes with [`crate::crypto::encrypt`]. //! - **Decrypt**: decrypt the ciphertext with [`crate::crypto::decrypt`], then //! deserialize the resulting JSON bytes back into the typed struct. //! //! ## Why a single master key //! //! All entries and the manifest are encrypted under the same `master_key`. This is //! simpler than a per-entry subkey hierarchy and sufficient for family-scale vaults //! (typically < 1000 entries). The security properties are equivalent: an attacker //! who compromises the master key can decrypt everything regardless of whether //! subkeys exist, and the vault's threat model already assumes the master key is //! the single point of trust (protected by the two-factor KDF). use crate::crypto; use crate::entry::{Entry, Manifest}; use crate::error::Result; /// Serialize an [`Entry`] to JSON and encrypt it under the master key. /// /// The resulting bytes are written to `entries/.enc` by the CLI. /// /// # Errors /// /// - [`crate::IdfotoError::Json`] if JSON serialization fails (should not happen /// with well-formed Entry structs). /// - [`crate::IdfotoError::Encrypt`] if the underlying AEAD operation fails. pub fn encrypt_entry(master_key: &[u8; 32], entry: &Entry) -> Result> { let json = serde_json::to_vec(entry)?; crypto::encrypt(master_key, &json) } /// Decrypt an entry blob and deserialize it back into an [`Entry`]. /// /// # Errors /// /// - [`crate::IdfotoError::Decrypt`] if the master key is wrong or the data is /// tampered. /// - [`crate::IdfotoError::Format`] if the ciphertext blob has an invalid header. /// - [`crate::IdfotoError::Json`] if the decrypted JSON is malformed. pub fn decrypt_entry(master_key: &[u8; 32], data: &[u8]) -> Result { let json = crypto::decrypt(master_key, data)?; let entry: Entry = serde_json::from_slice(&json)?; Ok(entry) } /// Serialize a [`Manifest`] to JSON and encrypt it under the master key. /// /// The resulting bytes are written to `manifest.enc` by the CLI. /// /// # Errors /// /// Same as [`encrypt_entry`]. pub fn encrypt_manifest(master_key: &[u8; 32], manifest: &Manifest) -> Result> { let json = serde_json::to_vec(manifest)?; crypto::encrypt(master_key, &json) } /// Decrypt a manifest blob and deserialize it back into a [`Manifest`]. /// /// # Errors /// /// Same as [`decrypt_entry`]. pub fn decrypt_manifest(master_key: &[u8; 32], data: &[u8]) -> Result { 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, group: 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()), group: None, 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()); } }