Asking for real keyring backends made the four round-trip tests write a genuine login-Keychain item, and macOS binds an item's ACL to the exact binary that created it. So every run after the first, with any source edit in between, was a new binary against an existing item, and the OS blocked all four tests on an authorization dialog that a headless run can never answer. Reproduced: four tests hanging past 60s, where a fresh keychain passes in 0.39s. The comment claiming the sandboxed tempdir guaranteed a fresh state was the thing that hid it. try_open_keyring ignores storage_dir entirely and looks the entry up by service name alone, so the tempdir bought nothing. Backend::kdf_only pins them to the path they were always documented as taking. What they assert, wrap/unwrap round-trips and tamper detection, is identical on either backend, so nothing is lost. survives_a_restart.rs still exercises the real backend, which is where it belongs: persistence across processes is what that test is about. Also names Linux's backend here instead of inheriting it. qbz-credentials enables async-secret-service and Cargo unifies features, which holds for every shipping build but makes this crate's correctness depend on a sibling: built on its own, cargo tree showed keyring with no secret-service, i.e. the same silent mock this PR exists to remove. Verified: secret-service v4.0.0 on Linux, security-framework on macOS, and still zero keyring on iOS and Android.
65 lines
2.5 KiB
TOML
65 lines
2.5 KiB
TOML
[package]
|
|
name = "qbz-secrets"
|
|
version.workspace = true
|
|
edition.workspace = true
|
|
license.workspace = true
|
|
description = "Device-bound secret wrapping for QBZ (offline cache keys, tokens)"
|
|
|
|
[dependencies]
|
|
# AES-256-GCM for the at-rest wrap/unwrap
|
|
aes-gcm = "0.10"
|
|
|
|
# KDF for the fallback path when OS keyring isn't available
|
|
hkdf = "0.12"
|
|
sha2 = "0.10"
|
|
|
|
# RNG for nonces + install UUID generation
|
|
rand = "0.9"
|
|
|
|
# UUID for the persistent install identifier (salt component for fallback)
|
|
uuid = { version = "1", features = ["v4", "serde"] }
|
|
|
|
# Serde for the on-disk wrapper blob envelope
|
|
serde = { workspace = true }
|
|
|
|
# Base64 for keyring-roundtripped master key (secret-service expects a UTF-8 string)
|
|
base64 = { workspace = true }
|
|
|
|
# Errors
|
|
thiserror = { workspace = true }
|
|
log = { workspace = true }
|
|
|
|
[dev-dependencies]
|
|
tempfile = "3"
|
|
|
|
# OS keyring backends: secret-service (Linux), Keychain (macOS), Credential
|
|
# Manager (Windows). **Desktop only, deliberately** — see the target gate below.
|
|
#
|
|
# Every backend is opt-in. keyring 3.x ships no `default` feature contents at
|
|
# all, and each platform arm reads `not(feature = "…-native")` → `pub use mock
|
|
# as default`. That mock is an in-memory store whose writes *succeed*, so
|
|
# depending on it does not fail loudly: it hands back a master key that never
|
|
# outlives the process, and every blob wrapped in one run is undecryptable in
|
|
# the next. `Backend::new` still reports `BackendKind::Keyring`, so the logs
|
|
# say nothing is wrong.
|
|
#
|
|
# Linux names its backend here too, rather than relying on qbz-credentials
|
|
# enabling `async-secret-service` and Cargo unifying features across the graph.
|
|
# That does hold for any build containing qbz-credentials, which today is every
|
|
# shipping one, but it makes this crate's correctness depend on a sibling: build
|
|
# qbz-secrets on its own and `cargo tree` shows keyring with no secret-service,
|
|
# which is the silent mock described above. A crate meant to be reusable should
|
|
# not be one dependency-graph change away from storing secrets in a hashmap.
|
|
#
|
|
# `async-secret-service` and not `sync-secret-service`: keyring's lib.rs has a
|
|
# `compile_error!` for enabling both halves of a store, and qbz-credentials
|
|
# already picked the async one. `crypto-rust` is not optional, secret-service
|
|
# does not build without a crypto backend selected.
|
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
|
keyring = { version = "3", features = [
|
|
"apple-native",
|
|
"windows-native",
|
|
"async-secret-service",
|
|
"crypto-rust",
|
|
] }
|