fix: purge keychain credentials on profile delete, atomic owner-only secret writes, zeroize derived keys

- Secrets::delete_profile removes every profile credential kind from the OS
  keychain as well as the vault (previously only the vault was purged, so
  keychain passwords/tokens outlived the profile).
- write_private writes to a sibling temp file and renames it into place, so a
  crash mid-write cannot truncate the vault or an export, and a pre-existing
  world-readable file is replaced rather than reused.
- Argon2-derived vault/export keys are Zeroizing and wiped on drop.
- Recordings (which capture typed input), JSON exports and --data-dir
  directories are created owner-only.
This commit is contained in:
Andy Dixon
2026-10-12 07:20:11 +01:00
parent 8766846a3b
commit 9cf221779b
8 changed files with 102 additions and 23 deletions
+2 -2
View File
@@ -41,7 +41,7 @@ pub fn encrypt_and_export(profiles: &[Profile], password: &str, path: &Path) ->
let salt = random_bytes::<32>();
let nonce = random_bytes::<12>();
let key = derive_key(password, &salt)?;
let ct = Aes256Gcm::new((&key).into()).encrypt(Nonce::from_slice(&nonce), plain.as_slice()).map_err(|e| e.to_string())?;
let ct = Aes256Gcm::new((&*key).into()).encrypt(Nonce::from_slice(&nonce), plain.as_slice()).map_err(|e| e.to_string())?;
let mut out = vec![VERSION];
out.extend(salt);
out.extend(nonce);
@@ -64,7 +64,7 @@ pub fn decrypt_and_import(path: &Path, password: &str) -> Result<Vec<Profile>> {
return Err(format!("exportimport: unsupported format version {}", data[0]).into());
}
let key = derive_key(password, &data[1..33])?;
let plain = Aes256Gcm::new((&key).into())
let plain = Aes256Gcm::new((&*key).into())
.decrypt(Nonce::from_slice(&data[33..45]), &data[45..])
.map_err(|_| "exportimport: decryption failed — wrong password or corrupt file")?;
let bundle: Bundle = serde_json::from_slice(&plain).map_err(|e| format!("exportimport: invalid bundle JSON: {e}"))?;