app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit f62325ac8832f455a820b62c050657fdc7923a8f
parent 28135c0f5299dcf7c0e293a2ee7c34a772b8efd4
Author: triesap <tyson@radroots.org>
Date:   Sun,  2 Aug 2026 18:51:19 +0000

test(core): add public redaction assertions

- scan public snapshot and safe-error debug representations
- inspect representative SQLite schema and durable records
- reject known secret hex nsec and secret-prefix fixtures
- document the reusable pre-command redaction guard lane

Diffstat:
Acore/crates/application/tests/redaction.rs | 47+++++++++++++++++++++++++++++++++++++++++++++++
Acore/crates/storage/tests/redaction.rs | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdocs/implementation/nostr-runtime-rcld.md | 6+++---
Mdocs/security/key-management.md | 5+++++
4 files changed, 108 insertions(+), 3 deletions(-)

diff --git a/core/crates/application/tests/redaction.rs b/core/crates/application/tests/redaction.rs @@ -0,0 +1,47 @@ +use radroots_studio_application::{ + AppSnapshot, RelayConfiguration, SessionState, SnapshotRevision, +}; +use radroots_studio_domain::{ + AccountCreatedAt, AccountSummary, KeyAvailability, Npub, PublicKey, SafeError, SafeErrorCode, + SafeMessage, SignerKind, UnixTimestamp, +}; + +const SECRET_HEX: &str = "1111111111111111111111111111111111111111111111111111111111111111"; +const SECRET_NSEC: &str = "nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5"; +const NPUB: &str = "npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg"; + +fn assert_redacted(text: &str) { + assert!(!text.contains(SECRET_HEX)); + assert!(!text.contains(SECRET_NSEC)); + assert!(!text.contains("nsec1")); +} + +#[test] +fn redaction_guards_public_snapshot_and_safe_error_debug() { + let account = AccountSummary::new( + PublicKey::from_bytes([2; 32]), + Npub::from_encoded(NPUB.to_owned()).expect("npub"), + SignerKind::LocalSecret, + KeyAvailability::Available, + None, + AccountCreatedAt::new(UnixTimestamp::from_seconds(1).expect("time")), + None, + ); + let snapshot = AppSnapshot::ready( + SnapshotRevision::from_value(1), + RelayConfiguration::default(), + vec![account.clone()], + Some(account.public_key()), + SessionState::SignedOut, + None, + None, + ) + .expect("snapshot"); + let error = SafeError::new( + SafeErrorCode::KeyringUnavailable, + SafeMessage::new("The operating system credential store is unavailable."), + ); + + assert_redacted(&format!("{snapshot:?}")); + assert_redacted(&format!("{error:?} {error}")); +} diff --git a/core/crates/storage/tests/redaction.rs b/core/crates/storage/tests/redaction.rs @@ -0,0 +1,53 @@ +use std::fs; + +use radroots_studio_application::{AccountOperationKind, AccountRepository, OperationJournal}; +use radroots_studio_domain::{ + AccountCreatedAt, AccountSummary, KeyAvailability, Npub, PublicKey, SignerKind, UnixTimestamp, +}; +use radroots_studio_storage::Database; +use tempfile::tempdir; + +const SECRET_HEX: &str = "1111111111111111111111111111111111111111111111111111111111111111"; +const SECRET_NSEC: &str = "nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5"; +const NPUB: &str = "npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg"; + +fn assert_redacted(bytes: &[u8]) { + assert!( + !bytes + .windows(SECRET_HEX.len()) + .any(|value| value == SECRET_HEX.as_bytes()) + ); + assert!( + !bytes + .windows(SECRET_NSEC.len()) + .any(|value| value == SECRET_NSEC.as_bytes()) + ); + assert!(!bytes.windows(5).any(|value| value == b"nsec1")); +} + +#[test] +fn redaction_guards_sqlite_schema_and_non_secret_records() { + let directory = tempdir().expect("directory"); + let path = directory.path().join("studio.sqlite3"); + { + let database = Database::open(&path).expect("database"); + let account = AccountSummary::new( + PublicKey::from_bytes([2; 32]), + Npub::from_encoded(NPUB.to_owned()).expect("npub"), + SignerKind::LocalSecret, + KeyAvailability::Available, + None, + AccountCreatedAt::new(UnixTimestamp::from_seconds(1).expect("time")), + None, + ); + database.insert_account(&account).expect("account"); + database + .begin_operation( + AccountOperationKind::Add, + account.public_key(), + UnixTimestamp::from_seconds(2).expect("time"), + ) + .expect("journal"); + } + assert_redacted(&fs::read(path).expect("database bytes")); +} diff --git a/docs/implementation/nostr-runtime-rcld.md b/docs/implementation/nostr-runtime-rcld.md @@ -308,7 +308,7 @@ failure-injection tests, database-byte secret guards, and workspace tests. ### RCLD-05: Credential boundary -Status: pending. +Status: completed. Scope: checkpoints 21 through 24. Add SecretStore, in-memory and failure-injection fakes, OS keyring adapter, safe platform errors, and global @@ -486,7 +486,7 @@ handoff commit sequence. - [x] 21. Add SecretStore trait and in-memory fake. - [x] 22. Add failure-injection SecretStore fake. - [x] 23. Implement OS keyring secret adapter. -- [ ] 24. Add global no-secret snapshot and storage assertions. +- [x] 24. Add global no-secret snapshot and storage assertions. ### RCLD-06 @@ -555,7 +555,7 @@ handoff commit sequence. - [x] RCLD-02: Rust workspace and domain. - [x] RCLD-03: Application state machine. - [x] RCLD-04: SQLite persistence. -- [ ] RCLD-05: Credential boundary. +- [x] RCLD-05: Credential boundary. - [ ] RCLD-06: Account generation and import. - [ ] RCLD-07: Account lifecycle and recovery. - [ ] RCLD-08: Relay and profile runtime. diff --git a/docs/security/key-management.md b/docs/security/key-management.md @@ -35,3 +35,8 @@ The journal contains only an operation identifier, operation kind, canonical public key, phase, safe timestamp, and optional safe diagnostic code. It cannot store credential text or arbitrary payloads, and it survives account metadata removal until cleanup has been finalized. + +Workspace redaction tests scan public snapshot and safe-error debug output plus +the SQLite schema and representative durable records for known secret-hex, +nsec, and secret-prefix fixtures. These guards run before account commands are +allowed to carry production credentials.