pub struct HardwareBoundBackend { /* private fields */ }Expand description
A KeychainBackend that hardware-binds every blob it stores, degrading to
the underlying software envelope when the host has no usable hardware.
§Example
use std::sync::Arc;
use dig_keystore::backend::{BackendKey, FileBackend, KeychainBackend};
use dig_keystore::hardware::{HardwareBoundBackend, HardwarePolicy};
let inner = FileBackend::new("/var/lib/dig/keys");
// No provider available in this build: opens, and says so honestly.
let backend = HardwareBoundBackend::new(inner, None, HardwarePolicy::Optional)?;
// What the HOST can do — the tier a new write would get.
println!("host: {}", backend.tier());
// What protects THIS key — the only answer fit to show a user, because a
// capable host can still hold a keystore that was never wrapped.
let key = BackendKey::new("identity");
let tier = backend.blob_tier(&key)?;
if tier.is_hardware_bound() {
println!("this key is {tier}");
} else {
// Never claim protection this key does not have.
println!("this key is {tier}");
}Implementations§
Source§impl HardwareBoundBackend
impl HardwareBoundBackend
Sourcepub fn new<B: KeychainBackend>(
inner: B,
provider: Option<Arc<dyn HardwareProvider>>,
policy: HardwarePolicy,
) -> Result<Self>
pub fn new<B: KeychainBackend>( inner: B, provider: Option<Arc<dyn HardwareProvider>>, policy: HardwarePolicy, ) -> Result<Self>
Decorate inner, resolving the protection tier once.
Pass provider = None to store through inner unchanged while reporting
DegradeReason::NotRequested.
§Errors
Fails closed rather than silently degrading, per policy:
HardwarePolicy::Required— any outcome short of self-tested hardware isKeystoreError::HardwareRequired.HardwarePolicy::Preferred(default) — a confident absence degrades, but anIndeterminateprobe isKeystoreError::HardwareProbeIndeterminate: “I could not determine whether this host has hardware” must not be downgraded into “it has none”, which would quietly strip protection from a machine that has it.HardwarePolicy::Optional— always opens, always reports the reason.
Sourcepub fn with_inner(
inner: Arc<dyn KeychainBackend>,
provider: Option<Arc<dyn HardwareProvider>>,
policy: HardwarePolicy,
) -> Result<Self>
pub fn with_inner( inner: Arc<dyn KeychainBackend>, provider: Option<Arc<dyn HardwareProvider>>, policy: HardwarePolicy, ) -> Result<Self>
As new, for an already shared backend.
Sourcepub fn tier(&self) -> &ProtectionTier
pub fn tier(&self) -> &ProtectionTier
What this host is bound to — the tier every newly written blob gets.
This is a statement about the machine, not about any particular stored
key. On a hardware-capable host it reports Hardware even if a given
keystore predates hardware binding and is still a bare §3 blob, because a
capable host does not retroactively protect bytes already at rest.
Before telling a user that a specific key is hardware-protected, ask
blob_tier instead. Rendering “protected by your
TPM” from this method would claim copy-resistance that an unwrapped
legacy blob does not have.
Sourcepub fn blob_tier(&self, key: &BackendKey) -> Result<ProtectionTier>
pub fn blob_tier(&self, key: &BackendKey) -> Result<ProtectionTier>
What protects the key material stored at key, read from the blob
itself.
This is the question a UI actually has, and it is not the same as
tier: a hardware-capable host can hold a keystore written
before hardware binding existed, which is protected by the passphrase
envelope alone and does open on another machine. Answering from the
stored bytes is what keeps that distinction honest.
The tier reported is the blob’s own, independent of this host: a blob
sealed by an Apple Secure Enclave reads as Hardware(MacSecureEnclave)
even on Windows. Whether this host can open it is a separate question,
answered by read.
§Errors
- The inner backend’s error if
keycannot be read (e.g.NotFound). KeystoreError::MalformedEnvelopeif the blob claims to be an envelope but is structurally invalid.KeystoreError::UnknownHardwareClassif it was sealed by hardware this build cannot name.
Both error cases fail closed: a wrapped blob is never reported as software-protected just because this build cannot fully classify it. Guessing in either direction is what this method exists to avoid.
Sourcepub fn inner(&self) -> &Arc<dyn KeychainBackend>
pub fn inner(&self) -> &Arc<dyn KeychainBackend>
The underlying storage, for callers that need it directly.
Sourcepub fn unbind(&self, key: &BackendKey) -> Result<ProtectionTier>
pub fn unbind(&self, key: &BackendKey) -> Result<ProtectionTier>
Return the key at key to the portable software form, so it opens on a
host that no longer has this hardware. Returns the tier the blob is in
afterwards.
§Why this exists
Hardware binding makes the trusted component a second required
factor. A TPM is cleared by a firmware update, a mainboard swap or a
BIOS reset — routine events — and after one the correct passphrase is no
longer enough: the sealed blob is unopenable, by design and permanently.
unbind is the way back, and it must be taken while the hardware still
answers. There is no recovery afterwards; that is what non-exportable
custody means.
Unbinding does not expose a secret. What it stores is the AES-256-GCM +
Argon2id passphrase envelope that was always the floor (SPEC.md §3) —
the same bytes a host with no hardware writes. It gives up cross-machine
binding, nothing else.
Nothing is written until the plaintext is in hand, and the result is verified from storage before this reports success: telling a user their seed is portable when it is not is the one failure here with a catastrophic follow-on action, since they may then clear the TPM.
§Errors
KeystoreError::NotHardwareBound— the blob is wrapped but this backend has no provider to open it (the hardware is already gone).KeystoreError::HardwareUnwrapFailed— the hardware would not open it. The stored bytes are left exactly as they were.KeystoreError::HardwareStillBound— the write did not take.
Sourcepub fn bind(&self, key: &BackendKey) -> Result<ProtectionTier>
pub fn bind(&self, key: &BackendKey) -> Result<ProtectionTier>
Bind the key at key to this host’s hardware, migrating a blob written
before hardware binding existed. Returns the tier the blob is in
afterwards.
Already-bound blobs are left alone: sealing an envelope inside a second envelope would produce a blob whose unwrap yields another envelope, which nothing can open.
This is the operation that can strand a seed, because it overwrites
the only copy with bytes only this hardware can open. So the new blob is
read back from storage and reopened through the hardware BEFORE the call
reports success, and the previous bytes are restored if it cannot be. See
unbind for the way back out.
§Errors
KeystoreError::NotHardwareBound— this backend resolved a software tier, so there is no hardware to bind to.KeystoreError::HardwareWrapFailed/KeystoreError::HardwareUnwrapFailed— the seal could not be made, or could not be proven reopenable. The previous bytes are restored in both cases.
Trait Implementations§
Source§impl Debug for HardwareBoundBackend
impl Debug for HardwareBoundBackend
Source§impl KeychainBackend for HardwareBoundBackend
impl KeychainBackend for HardwareBoundBackend
Source§fn read(&self, key: &BackendKey) -> Result<Vec<u8>>
fn read(&self, key: &BackendKey) -> Result<Vec<u8>>
Read a blob, unwrapping it when it carries a hardware envelope.
A blob without the envelope prefix is returned untouched, which is what lets every keystore written before this feature — and any future inner format — keep opening (§5.1).
A blob with an envelope that this host cannot open is an error, never the raw envelope bytes: an envelope copied to a machine without the sealing hardware must fail loudly rather than hand back ciphertext that a caller would then try to parse as a keystore.
Source§fn write(&self, key: &BackendKey, data: &[u8]) -> Result<()>
fn write(&self, key: &BackendKey, data: &[u8]) -> Result<()>
Write a blob, sealing it into a hardware envelope in the hardware tier and passing the software-sealed bytes straight through otherwise.
Source§fn delete(&self, key: &BackendKey) -> Result<()>
fn delete(&self, key: &BackendKey) -> Result<()>
key. Implementations should best-effort overwrite
the storage before removing so residual disk sectors do not retain the
ciphertext.