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
+11 -1
View File
@@ -116,7 +116,12 @@ impl Recorder {
fn with_limit(dir: &Path, id: &str, title: &str, cols: i64, rows: i64, max_bytes: u64) -> Result<Recorder> {
mkdir_private(dir).map_err(|e| format!("recording: failed to create directory: {e}"))?;
let path = dir.join(format!("{id}.cast"));
let mut f = File::create(&path).map_err(|e| format!("recording: failed to create file: {e}"))?;
// Recordings capture typed input (passwords included): owner-only, like the vault.
let mut opts = std::fs::OpenOptions::new();
opts.write(true).create(true).truncate(true);
#[cfg(unix)]
std::os::unix::fs::OpenOptionsExt::mode(&mut opts, 0o600);
let mut f = opts.open(&path).map_err(|e| format!("recording: failed to create file: {e}"))?;
let start = Instant::now();
let hdr = Header {
version: 2,
@@ -391,6 +396,11 @@ mod tests {
assert_eq!(rec.event_count(), 4);
rec.close().unwrap();
rec.close().unwrap(); // double close is a no-op
#[cfg(unix)]
{
let mode = std::os::unix::fs::PermissionsExt::mode(&std::fs::metadata(dir.join("test-session-001.cast")).unwrap().permissions());
assert_eq!(mode & 0o777, 0o600);
}
let text = std::fs::read_to_string(dir.join("test-session-001.cast")).unwrap();
let lines: Vec<&str> = text.trim().lines().collect();
assert_eq!(lines.len(), 5);