Create vault files owner-only from the first byte

This commit is contained in:
Avi 2026-08-21 15:18:03 -05:00
commit 4461307162

View file

@ -1,7 +1,7 @@
use std::env; use std::env;
use std::fs; use std::fs;
use std::io::Write; use std::io::Write;
use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
@ -250,10 +250,15 @@ fn write_restricted(path: &Path, content: &str) -> Result<(), AppError> {
let tmp_path = path.with_extension("json.tmp"); let tmp_path = path.with_extension("json.tmp");
{ {
// `mode` only applies when the file is freshly created, so the tmp
// file is owner-only from its very first byte. The explicit
// `set_permissions` below covers a tmp file left over by a crashed
// earlier run, which would be opened without applying `mode`.
let mut file = fs::OpenOptions::new() let mut file = fs::OpenOptions::new()
.write(true) .write(true)
.create(true) .create(true)
.truncate(true) .truncate(true)
.mode(0o600)
.open(&tmp_path) .open(&tmp_path)
.map_err(|e| AppError::io("Could not write the data file", e))?; .map_err(|e| AppError::io("Could not write the data file", e))?;
file.set_permissions(fs::Permissions::from_mode(0o600)) file.set_permissions(fs::Permissions::from_mode(0o600))
@ -285,8 +290,24 @@ pub fn backup_file(path: &Path) -> Result<PathBuf, AppError> {
return Ok(backup); return Ok(backup);
} }
fs::copy(path, &backup) // Copy by hand instead of `fs::copy`, which gives the new file the
// source's permission bits: a group-readable legacy vault would sit there
// fully readable until the later chmod landed. Created with `mode(0o600)`,
// the backup is owner-only from its first byte.
let mut input =
fs::File::open(path).map_err(|e| AppError::io("Could not read your profile data", e))?;
let mut output = fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o600)
.open(&backup)
.map_err(|e| AppError::io("Could not create a backup of your profile data", e))?; .map_err(|e| AppError::io("Could not create a backup of your profile data", e))?;
std::io::copy(&mut input, &mut output)
.map_err(|e| AppError::io("Could not create a backup of your profile data", e))?;
output
.sync_all()
.map_err(|e| AppError::io("Could not save your profile data backup", e))?;
let _ = fs::set_permissions(&backup, fs::Permissions::from_mode(0o600)); let _ = fs::set_permissions(&backup, fs::Permissions::from_mode(0o600));
Ok(backup) Ok(backup)
} }
@ -394,6 +415,14 @@ mod tests {
} }
} }
/// The legacy flat-array form of [`sample_profile`], as on disk.
fn sample_profile_json() -> String {
r#"[
{ "label": "Alice", "public_key": "npub1test", "secret_key": "00ff", "created_at": 1700000000 }
]"#
.to_string()
}
#[test] #[test]
fn parse_vault_accepts_legacy_array_format() { fn parse_vault_accepts_legacy_array_format() {
let json = r#"[ let json = r#"[
@ -478,6 +507,23 @@ mod tests {
assert_eq!(mode & 0o777, 0o600, "vault must be readable only by owner"); assert_eq!(mode & 0o777, 0o600, "vault must be readable only by owner");
} }
#[test]
fn backup_of_group_readable_source_is_0600() {
let path = temp_vault_path();
fs::write(&path, sample_profile_json()).unwrap();
fs::set_permissions(&path, fs::Permissions::from_mode(0o664)).unwrap();
let backup = backup_file(&path).expect("backup should succeed");
let mode = fs::metadata(&backup).unwrap().permissions().mode();
assert_eq!(
mode & 0o777,
0o600,
"backup must be owner-only even when the source is group-readable"
);
assert_eq!(fs::read_to_string(backup).unwrap(), sample_profile_json());
}
#[test] #[test]
fn recognised_legacy_vault_is_tightened_to_0600() { fn recognised_legacy_vault_is_tightened_to_0600() {
let path = temp_vault_path(); let path = temp_vault_path();