Proof-of-concept proving the secure-storage exit criterion from docs/architecture/proof-of-concept-plan.md §2: "Secret write/read/delete works through platform secure storage." Implements a typed SecretStorageRepository trait per ADR-006 (SecureStore + per-platform adapters) and a Linux adapter (the only adapter in PoC scope) that supports both equivalent Linux backends per SysRS-053/SysRS-162: Secret Service (libsecret) and kernel keyutils. The audit test suite covers: SS-AUD-001 identity secret absent from local DB (raw file scan) SS-AUD-002 server password absent from local DB SS-AUD-003 secrets absent from logs (Secret newtype redaction) SS-AUD-005 failure returns safe typed error (NotFound) SS-AUD-006 delete removes entry SS-TC-003 Linux round-trip set/get/delete Verified on 2026-05-13 against the local keyutils backend (cargo test runs need 'keyctl session -' to provide a valid session keyring under non-interactive shells, documented in the spike README). The CLI driver additionally observed a real locked gnome-keyring collection and exercised the typed-error → fallback path live. Surfaced finding for the decision register: DEC-013 does not pin a Linux secure-storage backend policy. Both Secret Service and keyutils are 'equivalent' per the requirements; production code needs an owner ruling. Out of scope: Windows DPAPI, macOS/iOS Keychain, Android Keystore, SS-AUD-004 (covered by diagnostics-redaction spike), SS-AUD-007/008 (process / migration items). Authority: PoC plan §2, ADR-006, SDD-078, SRS-091..095, SysRS-158..162. Not product code; not promoted into chanora_storage.
55 lines
1.5 KiB
Rust
55 lines
1.5 KiB
Rust
//! In-memory log capture, used by the audit tests to assert that
|
|
//! `tracing` output contains no secret material (SS-AUD-003).
|
|
//!
|
|
//! Production code will use the redaction policy defined in
|
|
//! `docs/security/diagnostic-redaction-audit-report.md` and the
|
|
//! `chanora_diagnostics` crate. This PoC is intentionally simpler:
|
|
//! it captures *all* log output verbatim and lets the test grep it.
|
|
|
|
use std::io::{self, Write};
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
#[derive(Clone, Default)]
|
|
pub struct CapturedLog {
|
|
buf: Arc<Mutex<Vec<u8>>>,
|
|
}
|
|
|
|
impl CapturedLog {
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
/// Snapshot the captured bytes as a UTF-8 string (lossy if there's
|
|
/// any non-UTF-8 — we never expect non-UTF-8 from `tracing`).
|
|
pub fn snapshot(&self) -> String {
|
|
let buf = self.buf.lock().unwrap();
|
|
String::from_utf8_lossy(&buf).into_owned()
|
|
}
|
|
|
|
pub fn writer(&self) -> CapturedLogWriter {
|
|
CapturedLogWriter { buf: self.buf.clone() }
|
|
}
|
|
}
|
|
|
|
pub struct CapturedLogWriter {
|
|
buf: Arc<Mutex<Vec<u8>>>,
|
|
}
|
|
|
|
impl Write for CapturedLogWriter {
|
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
|
let mut g = self.buf.lock().unwrap();
|
|
g.extend_from_slice(buf);
|
|
Ok(buf.len())
|
|
}
|
|
fn flush(&mut self) -> io::Result<()> {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for CapturedLog {
|
|
type Writer = CapturedLogWriter;
|
|
fn make_writer(&'a self) -> Self::Writer {
|
|
self.writer()
|
|
}
|
|
}
|