Add secure credential storage, sessions, and a launch screen
Login now persists properly rather than through the JSON file the test
harness was using.
dr-plat SecretStore trait plus a Secret Service backend.
Verified against the live GNOME Keyring: store,
retrieve, delete, confirm-gone all round-trip.
Session/SessionStore splits credentials from settings — the app
password goes to the keyring (FR-NC-2), while
server, login, chosen root and format selection are
ordinary config. A test asserts the credential never
appears in the config file.
LaunchModel the launch-screen state machine, testable without a
display server: sign in, approve in browser, choose
folder, tick formats, sign out.
launch.slint the screen itself, in its own file.
Absence of a secrets daemon is an explicit degraded mode, not a silent
fallback to plaintext — the screen says sign-in will not persist rather
than letting the user find out next launch. Android's Keystore backend
fails loudly for the same reason: a no-op store would look like it
worked and then lose the credential.
Two bugs caught by tests rather than by running it:
- fail() after busy() signed the user out, because busy() had already
discarded the session. A failed *scan* would have logged you out.
Busy now carries the session.
- normalise_server upgrades http:// to https:// rather than accepting
it. NFR-SEC-3 requires TLS, and silently sending a credential in the
clear is not a decision to make on the user's behalf.
launch.slint is not yet wired into app.slint. Calling slint_build::compile
twice replaces the generated module rather than adding to it, which broke
the other in-flight work on dr-ui; I reverted that immediately. Wiring it
needs an import inside app.slint, which is that work's file to change.
419 tests passing across ten crates.
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
//! Verify credentials round-trip through the real platform secret store.
|
||||
//!
|
||||
//! cargo run -p dr-plat --example keyring_check
|
||||
//!
|
||||
//! Writes a test value, reads it back, deletes it. Touches nothing else.
|
||||
|
||||
use dr_plat::{PlatformSecretStore, SecretRef, SecretStore};
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
let store = PlatformSecretStore::new();
|
||||
|
||||
println!("store available: {}", store.is_available());
|
||||
if !store.is_available() {
|
||||
println!("no secrets daemon — the app would run in degraded mode");
|
||||
return;
|
||||
}
|
||||
|
||||
let r = SecretRef::app_password("https://test.invalid", "darkroom-selftest");
|
||||
let secret = "test-token-do-not-reuse";
|
||||
|
||||
print!("store … ");
|
||||
match store.store(&r, secret) {
|
||||
Ok(()) => println!("ok"),
|
||||
Err(e) => {
|
||||
println!("FAILED: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
print!("retrieve … ");
|
||||
match store.retrieve(&r) {
|
||||
Ok(v) if v == secret => println!("ok (round-tripped)"),
|
||||
Ok(_) => {
|
||||
println!("FAILED: wrong value");
|
||||
std::process::exit(1);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("FAILED: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
print!("delete … ");
|
||||
match store.delete(&r) {
|
||||
Ok(()) => println!("ok"),
|
||||
Err(e) => {
|
||||
println!("FAILED: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
print!("confirm gone … ");
|
||||
match store.retrieve(&r) {
|
||||
Err(dr_plat::SecretError::NotFound) => println!("ok"),
|
||||
Ok(_) => {
|
||||
println!("FAILED: still present after delete");
|
||||
std::process::exit(1);
|
||||
}
|
||||
Err(e) => println!("unexpected: {e}"),
|
||||
}
|
||||
|
||||
println!("\nplatform secret store works (FR-NC-2)");
|
||||
}
|
||||
Reference in New Issue
Block a user