fix(auth): unique tmp per auth.json write; tighten loose credential perms
Port of upstream grok-build 0.2.113-window fixes: - write_auth_json_atomic tmp path gains a monotonic sequence next to the pid, so two concurrent in-process writers (background mint + proactive refresher) cannot share one tmp path and tear each other's bytes; a tmp write that fails partway is reclaimed instead of orphaned. - New secure_file::ensure_owner_only_permissions (0o600 / Windows ACL) tightens pre-existing world-readable credential files on read, backup, write, replace, and restore. Best-effort with warns after durable content — a chmod-only failure must not masquerade as a failed write — and propagating in restore_prior_bytes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KSy9S31pZNWXtRZNVJ1sDf
This commit is contained in:
1 parent
257caed650
commit
6e0464fa74
2 files changed
+241
-20
No files matched your search
@@ -26,7 +26,7 @@ use std::io::{self, Write};
|
||||
use std::path::Path;
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
|
||||
|
||||
/// Creates or opens a file with secure permissions (owner read/write only).
|
||||
///
|
||||
@@ -58,17 +58,20 @@ pub fn write_secure_file(path: &Path, contents: &[u8]) -> io::Result<()> {
|
||||
file.write_all(contents)?;
|
||||
file.flush()?;
|
||||
|
||||
// On Windows, we need to set permissions after file creation
|
||||
#[cfg(windows)]
|
||||
{
|
||||
set_windows_secure_permissions(path)?;
|
||||
}
|
||||
// Re-assert owner-only bits: `OpenOptions::mode` only applies on create,
|
||||
// so an existing world-readable file would otherwise keep open perms.
|
||||
ensure_owner_only_permissions(path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Opens a file for writing with secure permissions set during creation (Unix)
|
||||
/// or prepares it for permission setting after creation (Windows).
|
||||
///
|
||||
/// Callers that write secret material should also call
|
||||
/// [`ensure_owner_only_permissions`] after the write (or use
|
||||
/// [`write_secure_file`]), because `mode(0o600)` only applies when the file
|
||||
/// is newly created — not when truncating an existing path.
|
||||
pub fn open_secure_file(path: &Path) -> io::Result<File> {
|
||||
let mut options = OpenOptions::new();
|
||||
options.truncate(true).write(true).create(true);
|
||||
@@ -82,6 +85,44 @@ pub fn open_secure_file(path: &Path) -> io::Result<File> {
|
||||
options.open(path)
|
||||
}
|
||||
|
||||
/// Ensure `path` is owner-read/write only (Unix `0o600` / Windows user ACL).
|
||||
///
|
||||
/// Best-effort on missing files (`NotFound` is ignored). Other errors
|
||||
/// propagate so callers can fail closed when tightening a secret store.
|
||||
///
|
||||
/// Use on **load** of credential files so a hand-copied or restored
|
||||
/// world-readable `auth.json` is tightened before the process continues.
|
||||
pub fn ensure_owner_only_permissions(path: &Path) -> io::Result<()> {
|
||||
match ensure_owner_only_permissions_inner(path) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_owner_only_permissions_inner(path: &Path) -> io::Result<()> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let metadata = std::fs::metadata(path)?;
|
||||
let mode = metadata.permissions().mode();
|
||||
if mode & 0o777 != 0o600 {
|
||||
let mut perms = metadata.permissions();
|
||||
perms.set_mode(0o600);
|
||||
std::fs::set_permissions(path, perms)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
set_windows_secure_permissions(path)
|
||||
}
|
||||
#[cfg(not(any(unix, windows)))]
|
||||
{
|
||||
let _ = path;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets Windows-specific secure permissions on a file.
|
||||
///
|
||||
/// This function modifies the file's ACL to:
|
||||
@@ -219,8 +260,6 @@ mod tests {
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn test_unix_permissions() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let file_path = temp_dir.path().join("test_perms.txt");
|
||||
|
||||
@@ -231,4 +270,43 @@ mod tests {
|
||||
// Check that only owner has read/write (0o600), ignoring file type bits
|
||||
assert_eq!(mode & 0o777, 0o600);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn ensure_owner_only_tightens_world_readable_file() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let file_path = temp_dir.path().join("loose.txt");
|
||||
fs::write(&file_path, b"secret").unwrap();
|
||||
let mut loose = fs::metadata(&file_path).unwrap().permissions();
|
||||
loose.set_mode(0o644);
|
||||
fs::set_permissions(&file_path, loose).unwrap();
|
||||
assert_eq!(
|
||||
fs::metadata(&file_path).unwrap().permissions().mode() & 0o777,
|
||||
0o644
|
||||
);
|
||||
|
||||
ensure_owner_only_permissions(&file_path).unwrap();
|
||||
assert_eq!(
|
||||
fs::metadata(&file_path).unwrap().permissions().mode() & 0o777,
|
||||
0o600
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn write_secure_file_tightens_existing_world_readable_file() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let file_path = temp_dir.path().join("existing.txt");
|
||||
fs::write(&file_path, b"old").unwrap();
|
||||
let mut loose = fs::metadata(&file_path).unwrap().permissions();
|
||||
loose.set_mode(0o666);
|
||||
fs::set_permissions(&file_path, loose).unwrap();
|
||||
|
||||
write_secure_file(&file_path, b"new secret").unwrap();
|
||||
assert_eq!(
|
||||
fs::metadata(&file_path).unwrap().permissions().mode() & 0o777,
|
||||
0o600
|
||||
);
|
||||
assert_eq!(fs::read_to_string(&file_path).unwrap(), "new secret");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user