public abstract class DeviceProtection
- Object
- DeviceProtection
Known subtypesSecureStorageDeviceProtection
How one device holds the key that lets a vault be reopened without asking for the password again – the port’s half of “remember this device”.
Why wrap rather than fetch
The obvious shape for this is “give me the device key” followed by encryption in shared code.
It is the wrong shape, and the browser is why. A browser can hold an AES key as a CryptoKey
created with extractable: false, stored in IndexedDB: the page can ask it to encrypt and
decrypt, and crypto.subtle.exportKey on it rejects. There is no sequence of calls that turns
it back into bytes. An SPI that returned bytes could not express that, so the port would have
to fall back to a key the page can read – which is the plaintext-in-local-storage situation
this package exists to end.
So the operations are wrap and unwrap, and a port implements them however it can. The
portable implementation keeps a random key in
SecureStorage and performs AES-GCM in shared code, which is correct
on every port whose secure storage is the OS key store. The browser overrides it.
What a port must get right
ensureKeymust converge. Two browser tabs, or two Android processes, can reach it at once with nothing stored. Both must end up using the same key, which means a create-if-absent that is atomic in the store rather than a read followed by a write.- It must never replace a key it merely failed to read.
keyStateexists so a caller can tell absence from unreadability; a port that answersENTRY_ABSENTwhen it is not sure has destroyed every record that key protected. protectionmust describe what happened, not what is available. An API being present is not evidence the write survived.
Fields
public static final int KEY_PRESENT = 1 | The key is stored on this device. |
public static final int KEY_ABSENT = 0 | The store answered, and there is no key. |
public static final int KEY_UNKNOWN = -1 | The store could not be asked. |
Constructors
protected DeviceProtection() | Subclasses are constructed by the port. |
Methods
Inherited methods
Field details
KEY_PRESENT
public static final int KEY_PRESENT = 1KEY_ABSENT
public static final int KEY_ABSENT = 0KEY_UNKNOWN
public static final int KEY_UNKNOWN = -1Constructor details
DeviceProtection
protected DeviceProtection()Method details
protection
public abstract ProtectionReport protection()What this device protection actually provides, as observed rather than as advertised.
A port that has not yet tried an operation may only report what it can verify; several
answers are legitimately ProtectionReport.UNKNOWN – no browser can say whether a key
is hardware backed, and reporting NO there would understate an authenticator that uses a
secure element.
keyState
public abstract int keyState(String keyId)Parameters
keyIdString- the vault’s device key id
Returns
ensureKey
public abstract AsyncResource<Boolean> ensureKey(String keyId)Parameters
keyIdString- the vault’s device key id
Returns
true when a key is in place – whether this call created it
or found another one already there – and erroring with a
VaultException when no key could be establishedwrap
public abstract AsyncResource<byte[]> wrap(String keyId, byte[] plaintext, byte[] aad)Parameters
keyIdString- the vault’s device key id
plaintextbyte[]- what to protect, normally a vault’s 32 byte data key
aadbyte[]- associated data the result is bound to; the same bytes must be supplied to
unwrap
Returns
unwrap
public abstract AsyncResource<byte[]> unwrap(String keyId, byte[] wrapped, byte[] aad)Decrypts what wrap produced.
A failure to authenticate must arrive as
VaultError.AUTHENTICATION_FAILED, a missing key as
VaultError.KEY_MISSING, and a store that could not be read
as VaultError.TEMPORARILY_UNREADABLE. Collapsing the last
two is how a vault regenerates a key and orphans its data.
deleteKey
public abstract AsyncResource<Boolean> deleteKey(String keyId)Removes the device key, which is what “forget this device” does.
Everything wrapped under it becomes unopenable on this device. That is the intent; it is not revocation, because a copy taken while the key existed is beyond reach.
requiresUserVerification
public boolean requiresUserVerification()Whether reaching the key requires the user to verify themselves – a biometric, a passcode, a passkey with user verification.
Defaults to false. A port that gates its device key on user verification overrides this
and reports Protection.USER_VERIFICATION in
protection() to match.
setDeviceBoundRequired
public void setDeviceBoundRequired(boolean required)Requires that a key created by this protection cannot leave the device.
Set from VaultOptions.requireDeviceBoundPasskey() before
the key is created. The default ignores it, which is correct for a store whose key is
local by construction – an OS key store does not sync. It matters where the key is a
passkey, because most passkeys do.
A port that accepts this must refuse creation when it cannot verify the guarantee, rather than creating a key that may sync. An unverifiable answer is not a pass.
Parameters
requiredboolean- whether the key must be device bound
userVerifying
public DeviceProtection userVerifying()The variant of this protection whose key cannot be reached without the user verifying themselves, or null when this platform has none.
A port can have two genuinely different mechanisms rather than one with a flag. The
browser does: the unattended key is a non-extractable CryptoKey in IndexedDB, and the
gated one is a passkey whose authenticator derives key material through the WebAuthn PRF
extension – different storage, different failure modes, and material that does not exist
at all until the user verifies. A native port whose single key store can be created with a
user-authentication requirement returns this and reports
Protection.USER_VERIFICATION.
Returning null is the honest answer where there is no such mechanism, and
UnlockPolicy.REQUIRE_USER_VERIFICATION is then refused with
VaultError.POLICY_NOT_MET rather than quietly enrolled
under the weaker one.
Returns
requiresUserVerification() is true, or null