feat(poc/storage): add secure-storage spike (Linux)
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.
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
//! 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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user