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.
57 lines
1.8 KiB
Rust
57 lines
1.8 KiB
Rust
//! Minimal `LocalDatabaseRepository` stand-in (SDD-077). Holds *non-secret*
|
|
//! state only — bookmarks, in this PoC. Used by the audit tests to prove
|
|
//! SS-AUD-001 / SS-AUD-002: a secret stored via the secure-storage
|
|
//! adapter never appears in the SQLite file.
|
|
|
|
use std::path::Path;
|
|
|
|
use rusqlite::{params, Connection};
|
|
use thiserror::Error;
|
|
|
|
#[derive(Debug, Error)]
|
|
pub enum LocalDbError {
|
|
#[error("sqlite error: {0}")]
|
|
Sqlite(#[from] rusqlite::Error),
|
|
}
|
|
|
|
pub struct LocalDatabaseRepository {
|
|
conn: Connection,
|
|
}
|
|
|
|
impl LocalDatabaseRepository {
|
|
pub fn open(path: &Path) -> Result<Self, LocalDbError> {
|
|
let conn = Connection::open(path)?;
|
|
conn.execute_batch(
|
|
"CREATE TABLE IF NOT EXISTS bookmarks (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
server_name TEXT NOT NULL,
|
|
server_host TEXT NOT NULL,
|
|
identity_ref TEXT NOT NULL
|
|
);",
|
|
)?;
|
|
Ok(Self { conn })
|
|
}
|
|
|
|
/// Insert a bookmark. `identity_ref` is a *reference* (e.g. a stable
|
|
/// secret name like `identity.primary`), **not** the secret itself.
|
|
/// This is the SAD-067 separation in action.
|
|
pub fn insert_bookmark(
|
|
&self,
|
|
server_name: &str,
|
|
server_host: &str,
|
|
identity_ref: &str,
|
|
) -> Result<i64, LocalDbError> {
|
|
self.conn.execute(
|
|
"INSERT INTO bookmarks (server_name, server_host, identity_ref) VALUES (?1, ?2, ?3)",
|
|
params![server_name, server_host, identity_ref],
|
|
)?;
|
|
Ok(self.conn.last_insert_rowid())
|
|
}
|
|
|
|
/// For the audit: read the raw bytes of the underlying SQLite file
|
|
/// so a test can grep for forbidden plaintext (SS-AUD-001).
|
|
pub fn raw_db_bytes(path: &Path) -> std::io::Result<Vec<u8>> {
|
|
std::fs::read(path)
|
|
}
|
|
}
|