Files
chanora/poc/secure-storage-spike/src/sqlite_repo.rs
T
EdisonJwa 50c95b61ad 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.
2026-05-14 12:26:23 +08:00

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)
}
}