2 files changed
+29
-9
No files matched your search
@@ -16,11 +16,6 @@ sha2 = "0.10"
|
||||
# RNG for nonces + install UUID generation
|
||||
rand = "0.9"
|
||||
|
||||
# OS keyring backends: libsecret/secret-service (Linux), Keychain (macOS),
|
||||
# DPAPI (Windows). Default features on v3 already pick the right backend
|
||||
# per platform; if it fails at runtime we fall through to the KDF path.
|
||||
keyring = "3"
|
||||
|
||||
# UUID for the persistent install identifier (salt component for fallback)
|
||||
uuid = { version = "1", features = ["v4", "serde"] }
|
||||
|
||||
@@ -36,3 +31,14 @@ log = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
||||
# OS keyring backends: libsecret/secret-service (Linux), Keychain (macOS),
|
||||
# DPAPI (Windows). **Desktop only, deliberately.** keyring 3.x compiles no real
|
||||
# backend for Android, and none for iOS without its `apple-native` feature — it
|
||||
# routes both to an in-memory `mock` whose writes *succeed*. Depending on it
|
||||
# there would not fail loudly; it would hand back a master key that never
|
||||
# leaves the process, so every blob wrapped in one run is undecryptable in the
|
||||
# next. Mobile takes the KDF path, which is derived from a persisted install id
|
||||
# and is stable across launches. See `backend.rs`.
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
keyring = "3"
|
||||
@@ -5,6 +5,16 @@
|
||||
//! AES-256-GCM wraps. Failure (for any reason) = we fall back to HKDF
|
||||
//! over device identifiers.
|
||||
//!
|
||||
//! **Mobile skips the keyring entirely and goes straight to the KDF path.**
|
||||
//! Not a preference — a correctness requirement. `keyring` 3.x compiles no real
|
||||
//! backend for Android, and none for iOS without its `apple-native` feature;
|
||||
//! both resolve to an in-memory `mock`. The mock's writes *succeed*, so the
|
||||
//! attempt below would not fail into the fallback: it would report success and
|
||||
//! hand back a key that dies with the process, silently making every blob
|
||||
//! wrapped in one run undecryptable in the next. The KDF path derives from a
|
||||
//! persisted install id and is stable across launches, which is what a phone
|
||||
//! needs. `keyring` is a desktop-only dependency for the same reason.
|
||||
//!
|
||||
//! The backend discriminator is baked into every wrapped blob so a blob
|
||||
//! produced by one backend can't be silently decrypted by another (and
|
||||
//! so that if a user later gains keyring access, we don't accidentally
|
||||
@@ -13,6 +23,8 @@
|
||||
use std::path::Path;
|
||||
|
||||
use hkdf::Hkdf;
|
||||
// Only the keyring path mints a random master key; the KDF path derives one.
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
use rand::RngCore;
|
||||
use sha2::Sha256;
|
||||
|
||||
@@ -21,6 +33,7 @@ use crate::error::SecretError;
|
||||
use crate::install_id;
|
||||
|
||||
const MASTER_KEY_LEN: usize = 32;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
const KEYRING_ENTRY_NAME: &str = "master-key-v1";
|
||||
const HKDF_INFO: &[u8] = b"qbz-secrets master-key derivation v1";
|
||||
|
||||
@@ -47,7 +60,9 @@ pub struct Backend {
|
||||
|
||||
impl Backend {
|
||||
pub fn new(service_name: &str, storage_dir: &Path) -> Result<Self, SecretError> {
|
||||
// Try keyring first.
|
||||
// Try keyring first — desktop only; see the module comment for why
|
||||
// mobile must not, and why a mock's *success* is the dangerous case.
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
match try_open_keyring(service_name) {
|
||||
Ok(master_key) => {
|
||||
log::info!("[qbz-secrets] Using OS keyring backend");
|
||||
@@ -94,6 +109,7 @@ impl Backend {
|
||||
}
|
||||
|
||||
/// Read (or create and store) the master key from the OS keyring.
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
fn try_open_keyring(service_name: &str) -> Result<[u8; MASTER_KEY_LEN], SecretError> {
|
||||
use base64::engine::general_purpose::STANDARD as B64;
|
||||
use base64::Engine;
|
||||
@@ -125,9 +141,7 @@ fn try_open_keyring(service_name: &str) -> Result<[u8; MASTER_KEY_LEN], SecretEr
|
||||
entry
|
||||
.set_password(&encoded)
|
||||
.map_err(|e| SecretError::Keyring(format!("set_password: {}", e)))?;
|
||||
log::info!(
|
||||
"[qbz-secrets] Generated fresh 256-bit master key and stored in OS keyring"
|
||||
);
|
||||
log::info!("[qbz-secrets] Generated fresh 256-bit master key and stored in OS keyring");
|
||||
Ok(key)
|
||||
}
|
||||
Err(e) => Err(SecretError::Keyring(format!("get_password: {}", e))),
|
||||
|
||||
Reference in new issue
Block a user