use std::fs::{self, File};
use std::io::Write;
use std::path::{Path, PathBuf};
use crate::coordinate::Coordinate;
use crate::crypto::{KEY_LEN, SealedRecord, open};
use crate::error::CoreError;
use crate::record::SecretRecord;
pub const FRAME_MAGIC: &[u8; 4] = b"KOVR";
pub const FRAME_VERSION: u32 = 1;
pub const RECORD_EXT: &str = "sec";
const HEADER_LEN: usize = 4 + 4;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Quarantined {
pub id: String,
pub reason: String,
}
#[derive(Debug, Default)]
pub struct LoadOutcome {
pub records: Vec<(String, SecretRecord)>,
pub quarantined: Vec<Quarantined>,
}
pub fn record_path_for_id(dir: &Path, id: &str) -> PathBuf {
dir.join(format!("{id}.{RECORD_EXT}"))
}
pub fn record_path(dir: &Path, coord: &Coordinate) -> Result<PathBuf, CoreError> {
Ok(record_path_for_id(dir, &coord.storage_id()?))
}
fn frame(sealed: &SealedRecord) -> Result<Vec<u8>, CoreError> {
let payload =
serde_json::to_vec(sealed).map_err(|e| CoreError::Serialization(e.to_string()))?;
let mut out = Vec::with_capacity(HEADER_LEN + payload.len());
out.extend_from_slice(FRAME_MAGIC);
out.extend_from_slice(&FRAME_VERSION.to_le_bytes());
out.extend_from_slice(&payload);
Ok(out)
}
fn unframe(bytes: &[u8]) -> Result<SealedRecord, CoreError> {
if bytes.len() < HEADER_LEN || &bytes[..4] != FRAME_MAGIC {
return Err(CoreError::Serialization(
"not a kovra record frame".to_string(),
));
}
let version = u32::from_le_bytes(bytes[4..8].try_into().expect("checked length"));
if version != FRAME_VERSION {
return Err(CoreError::Serialization(format!(
"unsupported record frame version {version}"
)));
}
serde_json::from_slice(&bytes[HEADER_LEN..])
.map_err(|e| CoreError::Serialization(e.to_string()))
}
#[cfg(unix)]
pub(crate) fn restrict(path: &Path, mode: u32) -> Result<(), CoreError> {
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(path, fs::Permissions::from_mode(mode))
.map_err(|e| CoreError::Io(format!("chmod {mode:o}: {e}")))
}
#[cfg(windows)]
pub(crate) fn restrict(path: &Path, _mode: u32) -> Result<(), CoreError> {
windows_acl::restrict_owner_only(path)
}
#[cfg(not(any(unix, windows)))]
pub(crate) fn restrict(_path: &Path, _mode: u32) -> Result<(), CoreError> {
Ok(())
}
#[cfg(windows)]
mod windows_acl {
use std::os::windows::ffi::OsStrExt;
use std::path::Path;
use windows::Win32::Foundation::{CloseHandle, HANDLE, HLOCAL, LocalFree};
use windows::Win32::Security::Authorization::{
EXPLICIT_ACCESS_W, MULTIPLE_TRUSTEE_OPERATION, SE_FILE_OBJECT, SET_ACCESS,
SetEntriesInAclW, SetNamedSecurityInfoW, TRUSTEE_IS_SID, TRUSTEE_IS_UNKNOWN, TRUSTEE_W,
};
use windows::Win32::Security::{
ACE_FLAGS, ACL, CreateWellKnownSid, DACL_SECURITY_INFORMATION, GetTokenInformation,
PROTECTED_DACL_SECURITY_INFORMATION, PSID, SECURITY_MAX_SID_SIZE, TOKEN_QUERY, TOKEN_USER,
TokenUser, WinBuiltinAdministratorsSid, WinLocalSystemSid,
};
use windows::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
use windows::core::{PCWSTR, PWSTR};
use crate::error::CoreError;
const GENERIC_ALL: u32 = 0x1000_0000;
pub(crate) fn restrict_owner_only(path: &Path) -> Result<(), CoreError> {
unsafe { apply(path) }.map_err(|e| CoreError::Io(format!("set ACL on {path:?}: {e}")))
}
fn explicit_access(sid: PSID) -> EXPLICIT_ACCESS_W {
EXPLICIT_ACCESS_W {
grfAccessPermissions: GENERIC_ALL,
grfAccessMode: SET_ACCESS,
grfInheritance: ACE_FLAGS(0), Trustee: TRUSTEE_W {
pMultipleTrustee: core::ptr::null_mut(),
MultipleTrusteeOperation: MULTIPLE_TRUSTEE_OPERATION(0),
TrusteeForm: TRUSTEE_IS_SID,
TrusteeType: TRUSTEE_IS_UNKNOWN,
ptstrName: PWSTR(sid.0 as *mut u16),
},
}
}
unsafe fn apply(path: &Path) -> windows::core::Result<()> {
let mut token = HANDLE::default();
unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token)? };
let mut len = 0u32;
let _ = unsafe { GetTokenInformation(token, TokenUser, None, 0, &mut len) };
let mut buf = vec![0u8; len as usize];
let info = unsafe {
GetTokenInformation(
token,
TokenUser,
Some(buf.as_mut_ptr().cast()),
len,
&mut len,
)
};
unsafe { CloseHandle(token).ok() };
info?;
let token_user = unsafe { &*(buf.as_ptr() as *const TOKEN_USER) };
let user_sid = token_user.User.Sid;
let mut system_buf = [0u8; SECURITY_MAX_SID_SIZE as usize];
let mut admins_buf = [0u8; SECURITY_MAX_SID_SIZE as usize];
let mut n = SECURITY_MAX_SID_SIZE;
unsafe {
CreateWellKnownSid(
WinLocalSystemSid,
None,
Some(PSID(system_buf.as_mut_ptr().cast())),
&mut n,
)?
};
let mut n2 = SECURITY_MAX_SID_SIZE;
unsafe {
CreateWellKnownSid(
WinBuiltinAdministratorsSid,
None,
Some(PSID(admins_buf.as_mut_ptr().cast())),
&mut n2,
)?
};
let system_sid = PSID(system_buf.as_mut_ptr().cast());
let admins_sid = PSID(admins_buf.as_mut_ptr().cast());
let entries = [
explicit_access(user_sid),
explicit_access(system_sid),
explicit_access(admins_sid),
];
let mut new_acl: *mut ACL = core::ptr::null_mut();
let rc = unsafe { SetEntriesInAclW(Some(&entries), None, &mut new_acl) };
if rc.0 != 0 {
return Err(windows::core::Error::from_hresult(
windows::core::HRESULT::from_win32(rc.0),
));
}
let wide: Vec<u16> = path
.as_os_str()
.encode_wide()
.chain(core::iter::once(0))
.collect();
let rc = unsafe {
SetNamedSecurityInfoW(
PCWSTR(wide.as_ptr()),
SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION,
None,
None,
Some(new_acl as *const ACL),
None,
)
};
unsafe { LocalFree(Some(HLOCAL(new_acl.cast()))) };
if rc.0 != 0 {
return Err(windows::core::Error::from_hresult(
windows::core::HRESULT::from_win32(rc.0),
));
}
Ok(())
}
}
pub fn ensure_dir(dir: &Path) -> Result<(), CoreError> {
if !dir.exists() {
fs::create_dir_all(dir).map_err(|e| CoreError::Io(format!("create {dir:?}: {e}")))?;
restrict(dir, 0o700)?;
}
Ok(())
}
pub fn write_record(
dir: &Path,
coord: &Coordinate,
sealed: &SealedRecord,
) -> Result<(), CoreError> {
ensure_dir(dir)?;
let path = record_path(dir, coord)?;
let tmp = path.with_extension(format!("{RECORD_EXT}.tmp"));
let bytes = frame(sealed)?;
{
let mut f = File::create(&tmp).map_err(|e| CoreError::Io(format!("create tmp: {e}")))?;
f.write_all(&bytes)
.map_err(|e| CoreError::Io(format!("write tmp: {e}")))?;
f.sync_all()
.map_err(|e| CoreError::Io(format!("fsync tmp: {e}")))?;
}
restrict(&tmp, 0o600)?;
if path.exists() {
let bak = path.with_extension(format!("{RECORD_EXT}.bak"));
fs::rename(&path, &bak).map_err(|e| CoreError::Io(format!("rotate .bak: {e}")))?;
}
fs::rename(&tmp, &path).map_err(|e| CoreError::Io(format!("rename into place: {e}")))?;
Ok(())
}
pub fn read_record(
dir: &Path,
coord: &Coordinate,
key: &[u8; KEY_LEN],
) -> Result<Option<SecretRecord>, CoreError> {
let path = record_path(dir, coord)?;
if !path.exists() {
return Ok(None);
}
let bytes = fs::read(&path).map_err(|e| CoreError::Io(format!("read record: {e}")))?;
let sealed = unframe(&bytes)?;
Ok(Some(open(&sealed, key)?))
}
pub fn load_all(dir: &Path, key: &[u8; KEY_LEN]) -> Result<LoadOutcome, CoreError> {
let mut outcome = LoadOutcome::default();
if !dir.exists() {
return Ok(outcome);
}
let entries = fs::read_dir(dir).map_err(|e| CoreError::Io(format!("read_dir: {e}")))?;
for entry in entries {
let entry = entry.map_err(|e| CoreError::Io(format!("dir entry: {e}")))?;
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some(RECORD_EXT) {
continue; }
let id = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or_default()
.to_string();
let opened = fs::read(&path)
.map_err(|e| CoreError::Io(format!("read: {e}")))
.and_then(|bytes| unframe(&bytes))
.and_then(|sealed| open(&sealed, key));
match opened {
Ok(record) => outcome.records.push((id, record)),
Err(e) => outcome.quarantined.push(Quarantined {
id,
reason: e.to_string(),
}),
}
}
Ok(outcome)
}
pub fn delete_record(dir: &Path, coord: &Coordinate) -> Result<(), CoreError> {
let path = record_path(dir, coord)?;
if path.exists() {
fs::remove_file(&path).map_err(|e| CoreError::Io(format!("remove record: {e}")))?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crypto::seal;
use crate::secret::SecretValue;
use crate::sensitivity::Sensitivity;
fn key() -> [u8; KEY_LEN] {
[0x11; KEY_LEN]
}
fn coord(s: &str) -> Coordinate {
s.parse().unwrap()
}
fn literal(value: &str) -> SecretRecord {
SecretRecord::Literal {
value: SecretValue::from(value),
sensitivity: Sensitivity::Medium,
revealable: false,
environment: "prod".to_string(),
component: "db".to_string(),
key: "password".to_string(),
description: None,
created: "2026-05-30T00:00:00Z".to_string(),
updated: "2026-05-30T00:00:00Z".to_string(),
}
}
#[test]
fn write_then_read_round_trips() {
let dir = tempfile::tempdir().unwrap();
let c = coord("secret:prod/db/password");
let sealed = seal(&literal("hunter2"), &key()).unwrap();
write_record(dir.path(), &c, &sealed).unwrap();
let got = read_record(dir.path(), &c, &key()).unwrap().unwrap();
match got {
SecretRecord::Literal { value, .. } => assert_eq!(value.expose(), b"hunter2"),
other => panic!("expected literal, got {other:?}"),
}
}
#[test]
fn read_missing_is_none() {
let dir = tempfile::tempdir().unwrap();
let c = coord("secret:prod/db/absent");
assert!(read_record(dir.path(), &c, &key()).unwrap().is_none());
}
#[test]
fn record_file_has_extension_and_hashed_name() {
let dir = tempfile::tempdir().unwrap();
let c = coord("secret:prod/db/password");
write_record(dir.path(), &c, &seal(&literal("x"), &key()).unwrap()).unwrap();
let path = record_path(dir.path(), &c).unwrap();
assert!(path.exists());
let name = path.file_name().unwrap().to_str().unwrap();
assert!(name.ends_with(".sec"));
assert!(!name.contains("password"));
}
#[cfg(unix)]
#[test]
fn written_record_is_0600() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let c = coord("secret:prod/db/password");
write_record(dir.path(), &c, &seal(&literal("x"), &key()).unwrap()).unwrap();
let mode = fs::metadata(record_path(dir.path(), &c).unwrap())
.unwrap()
.permissions()
.mode();
assert_eq!(mode & 0o777, 0o600);
}
#[cfg(windows)]
#[test]
fn windows_restrict_makes_dacl_protected() {
use std::os::windows::ffi::OsStrExt;
use windows::Win32::Foundation::{HLOCAL, LocalFree};
use windows::Win32::Security::Authorization::{GetNamedSecurityInfoW, SE_FILE_OBJECT};
use windows::Win32::Security::{
DACL_SECURITY_INFORMATION, GetSecurityDescriptorControl, PSECURITY_DESCRIPTOR,
SE_DACL_PROTECTED,
};
use windows::core::PCWSTR;
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("secret.bin");
fs::write(&path, b"x").unwrap();
restrict(&path, 0o600).unwrap();
let wide: Vec<u16> = path
.as_os_str()
.encode_wide()
.chain(core::iter::once(0))
.collect();
let mut psd = PSECURITY_DESCRIPTOR::default();
let rc = unsafe {
GetNamedSecurityInfoW(
PCWSTR(wide.as_ptr()),
SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION,
None,
None,
None,
None,
&mut psd,
)
};
assert_eq!(rc.0, 0, "GetNamedSecurityInfoW failed: {}", rc.0);
let mut control = 0u16;
let mut revision = 0u32;
unsafe { GetSecurityDescriptorControl(psd, &mut control, &mut revision).unwrap() };
unsafe { LocalFree(Some(HLOCAL(psd.0.cast()))) };
assert!(
control & SE_DACL_PROTECTED.0 != 0,
"DACL must be protected (inheritance stripped); control={control:#x}"
);
}
#[test]
fn overwrite_rotates_previous_to_bak() {
let dir = tempfile::tempdir().unwrap();
let c = coord("secret:prod/db/password");
write_record(dir.path(), &c, &seal(&literal("v1"), &key()).unwrap()).unwrap();
write_record(dir.path(), &c, &seal(&literal("v2"), &key()).unwrap()).unwrap();
let current = read_record(dir.path(), &c, &key()).unwrap().unwrap();
match current {
SecretRecord::Literal { value, .. } => assert_eq!(value.expose(), b"v2"),
other => panic!("expected literal, got {other:?}"),
}
let bak = record_path(dir.path(), &c)
.unwrap()
.with_extension(format!("{RECORD_EXT}.bak"));
assert!(bak.exists());
}
#[test]
fn load_all_quarantines_corrupt_and_loads_siblings() {
let dir = tempfile::tempdir().unwrap();
let good = coord("secret:prod/db/good");
let bad = coord("secret:prod/db/bad");
write_record(
dir.path(),
&good,
&seal(&literal("good-val"), &key()).unwrap(),
)
.unwrap();
write_record(
dir.path(),
&bad,
&seal(&literal("bad-val"), &key()).unwrap(),
)
.unwrap();
let bad_path = record_path(dir.path(), &bad).unwrap();
let mut bytes = fs::read(&bad_path).unwrap();
let last = bytes.len() - 1;
bytes[last] ^= 0xff;
fs::write(&bad_path, &bytes).unwrap();
let outcome = load_all(dir.path(), &key()).unwrap();
assert_eq!(outcome.records.len(), 1, "the good record still loads");
assert_eq!(
outcome.quarantined.len(),
1,
"the bad record is quarantined"
);
assert_eq!(outcome.quarantined[0].id, bad.storage_id().unwrap());
assert!(!outcome.quarantined[0].reason.contains("bad-val"));
}
#[test]
fn load_all_quarantines_garbage_file() {
let dir = tempfile::tempdir().unwrap();
ensure_dir(dir.path()).unwrap();
fs::write(dir.path().join("deadbeef.sec"), b"not a frame").unwrap();
let outcome = load_all(dir.path(), &key()).unwrap();
assert!(outcome.records.is_empty());
assert_eq!(outcome.quarantined.len(), 1);
}
#[test]
fn delete_removes_record() {
let dir = tempfile::tempdir().unwrap();
let c = coord("secret:prod/db/password");
write_record(dir.path(), &c, &seal(&literal("x"), &key()).unwrap()).unwrap();
delete_record(dir.path(), &c).unwrap();
assert!(read_record(dir.path(), &c, &key()).unwrap().is_none());
}
#[test]
fn placeholder_coordinate_is_rejected() {
let dir = tempfile::tempdir().unwrap();
let c = coord("secret:${ENV}/db/password");
assert!(matches!(
write_record(dir.path(), &c, &seal(&literal("x"), &key()).unwrap()),
Err(CoreError::NotStorable(_))
));
}
}