feat(core): length-prefixed Argon2 input + NFC + Zeroize (audit H1, H2)
derive_master_key now: - length-prefixes passphrase and image_secret to eliminate concatenation ambiguity (H1) - normalizes passphrase to UTF-8 NFC before hashing - returns Zeroizing<[u8; 32]> so the master key is wiped on drop (H2) - wraps the intermediate password buffer in Zeroizing for the same reason Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -20,3 +20,4 @@ ed25519-dalek = { version = "2", features = ["rand_core"] }
|
||||
rand = "0.8"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
zeroize = "1"
|
||||
|
||||
@@ -43,6 +43,7 @@ use idfoto_core::{
|
||||
decrypt_entry, decrypt_manifest, encrypt_entry, encrypt_manifest, generate_entry_id,
|
||||
Entry, KdfParams, Manifest, ManifestEntry,
|
||||
};
|
||||
use zeroize::Zeroizing;
|
||||
use rand::rngs::OsRng;
|
||||
use rand::RngCore;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -201,7 +202,7 @@ fn get_image_path() -> Result<PathBuf> {
|
||||
/// 2. Read and decode the reference JPEG, extracting the steganographic secret.
|
||||
/// 3. Load the vault salt and KDF params.
|
||||
/// 4. Derive the master key via Argon2id(passphrase || image_secret, salt).
|
||||
fn unlock(image_path: &PathBuf) -> Result<[u8; 32]> {
|
||||
fn unlock(image_path: &PathBuf) -> Result<Zeroizing<[u8; 32]>> {
|
||||
let passphrase = rpassword::prompt_password_stderr("Passphrase: ").context("failed to read passphrase")?;
|
||||
|
||||
let jpeg_data = fs::read(image_path).context("failed to read reference image")?;
|
||||
@@ -389,7 +390,7 @@ fn cmd_init(image: PathBuf, output: PathBuf) -> Result<()> {
|
||||
|
||||
// 10. Encrypt empty manifest
|
||||
let manifest = Manifest::new();
|
||||
let manifest_enc = encrypt_manifest(&master_key, &manifest).context("failed to encrypt manifest")?;
|
||||
let manifest_enc = encrypt_manifest(&*master_key, &manifest).context("failed to encrypt manifest")?;
|
||||
fs::write(vault_dir().join("manifest.enc"), manifest_enc)
|
||||
.context("failed to write manifest.enc")?;
|
||||
|
||||
@@ -463,14 +464,14 @@ fn cmd_add() -> Result<()> {
|
||||
};
|
||||
|
||||
let entry_id = generate_entry_id();
|
||||
let encrypted = encrypt_entry(&master_key, &entry).context("failed to encrypt entry")?;
|
||||
let encrypted = encrypt_entry(&*master_key, &entry).context("failed to encrypt entry")?;
|
||||
fs::write(
|
||||
vault_dir().join("entries").join(format!("{}.enc", entry_id)),
|
||||
encrypted,
|
||||
)
|
||||
.context("failed to write entry file")?;
|
||||
|
||||
let mut manifest = read_manifest(&master_key)?;
|
||||
let mut manifest = read_manifest(&*master_key)?;
|
||||
manifest.add_entry(
|
||||
entry_id.clone(),
|
||||
ManifestEntry {
|
||||
@@ -481,7 +482,7 @@ fn cmd_add() -> Result<()> {
|
||||
updated_at: now,
|
||||
},
|
||||
);
|
||||
write_manifest(&master_key, &manifest)?;
|
||||
write_manifest(&*master_key, &manifest)?;
|
||||
|
||||
git_commit(&format!("feat: add entry '{}'", name))?;
|
||||
eprintln!("Entry '{}' added (id: {})", name, entry_id);
|
||||
@@ -534,12 +535,12 @@ fn cmd_get(query: String) -> Result<()> {
|
||||
let image_path = get_image_path()?;
|
||||
let master_key = unlock(&image_path)?;
|
||||
|
||||
let manifest = read_manifest(&master_key)?;
|
||||
let manifest = read_manifest(&*master_key)?;
|
||||
let (entry_id, _) = search_and_select(&manifest, &query)?;
|
||||
|
||||
let data = fs::read(vault_dir().join("entries").join(format!("{}.enc", entry_id)))
|
||||
.context("failed to read entry file")?;
|
||||
let entry = decrypt_entry(&master_key, &data).context("failed to decrypt entry")?;
|
||||
let entry = decrypt_entry(&*master_key, &data).context("failed to decrypt entry")?;
|
||||
|
||||
println!("Name: {}", entry.name);
|
||||
println!(
|
||||
@@ -595,7 +596,7 @@ fn cmd_list() -> Result<()> {
|
||||
let image_path = get_image_path()?;
|
||||
let master_key = unlock(&image_path)?;
|
||||
|
||||
let manifest = read_manifest(&master_key)?;
|
||||
let manifest = read_manifest(&*master_key)?;
|
||||
|
||||
let mut entries: Vec<_> = manifest.entries.iter().collect();
|
||||
entries.sort_by(|a, b| a.1.name.to_lowercase().cmp(&b.1.name.to_lowercase()));
|
||||
@@ -626,12 +627,12 @@ fn cmd_edit(query: String) -> Result<()> {
|
||||
let image_path = get_image_path()?;
|
||||
let master_key = unlock(&image_path)?;
|
||||
|
||||
let manifest = read_manifest(&master_key)?;
|
||||
let manifest = read_manifest(&*master_key)?;
|
||||
let (entry_id, _) = search_and_select(&manifest, &query)?;
|
||||
|
||||
let data = fs::read(vault_dir().join("entries").join(format!("{}.enc", entry_id)))
|
||||
.context("failed to read entry file")?;
|
||||
let entry = decrypt_entry(&master_key, &data).context("failed to decrypt entry")?;
|
||||
let entry = decrypt_entry(&*master_key, &data).context("failed to decrypt entry")?;
|
||||
|
||||
eprintln!("Editing '{}' (Enter to keep current value)", entry.name);
|
||||
|
||||
@@ -667,14 +668,14 @@ fn cmd_edit(query: String) -> Result<()> {
|
||||
updated_at: now.clone(),
|
||||
};
|
||||
|
||||
let encrypted = encrypt_entry(&master_key, &updated_entry).context("failed to encrypt entry")?;
|
||||
let encrypted = encrypt_entry(&*master_key, &updated_entry).context("failed to encrypt entry")?;
|
||||
fs::write(
|
||||
vault_dir().join("entries").join(format!("{}.enc", entry_id)),
|
||||
encrypted,
|
||||
)
|
||||
.context("failed to write entry file")?;
|
||||
|
||||
let mut manifest = read_manifest(&master_key)?;
|
||||
let mut manifest = read_manifest(&*master_key)?;
|
||||
manifest.add_entry(
|
||||
entry_id,
|
||||
ManifestEntry {
|
||||
@@ -685,7 +686,7 @@ fn cmd_edit(query: String) -> Result<()> {
|
||||
updated_at: now,
|
||||
},
|
||||
);
|
||||
write_manifest(&master_key, &manifest)?;
|
||||
write_manifest(&*master_key, &manifest)?;
|
||||
|
||||
git_commit(&format!("feat: edit entry '{}'", name))?;
|
||||
eprintln!("Entry '{}' updated.", name);
|
||||
@@ -701,7 +702,7 @@ fn cmd_rm(query: String) -> Result<()> {
|
||||
let image_path = get_image_path()?;
|
||||
let master_key = unlock(&image_path)?;
|
||||
|
||||
let manifest = read_manifest(&master_key)?;
|
||||
let manifest = read_manifest(&*master_key)?;
|
||||
let (entry_id, entry) = search_and_select(&manifest, &query)?;
|
||||
|
||||
let confirm = prompt(&format!("Delete '{}' (id: {})? [y/N]", entry.name, entry_id))?;
|
||||
@@ -717,9 +718,9 @@ fn cmd_rm(query: String) -> Result<()> {
|
||||
fs::remove_file(&entry_path).context("failed to remove entry file")?;
|
||||
}
|
||||
|
||||
let mut manifest = read_manifest(&master_key)?;
|
||||
let mut manifest = read_manifest(&*master_key)?;
|
||||
manifest.remove_entry(&entry_id);
|
||||
write_manifest(&master_key, &manifest)?;
|
||||
write_manifest(&*master_key, &manifest)?;
|
||||
|
||||
git_commit(&format!("feat: remove entry '{}'", entry.name))?;
|
||||
eprintln!("Entry '{}' removed.", entry.name);
|
||||
|
||||
Reference in New Issue
Block a user