Class DeviceProtection
- Direct Known Subclasses:
SecureStorageDeviceProtection
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(String, byte[], byte[]) and unwrap(String, byte[], byte[]), 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
ensureKey(String)must 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.
keyState(String)exists so a caller can tell absence from unreadability; a port that answersENTRY_ABSENTwhen it is not sure has destroyed every record that key protected. protection()must describe what happened, not what is available. An API being present is not evidence the write survived.
-
Field Summary
FieldsModifier and TypeFieldDescriptionstatic final intThe store answered, and there is no key.static final intThe key is stored on this device.static final intThe store could not be asked. -
Constructor Summary
ConstructorsModifierConstructorDescriptionprotectedSubclasses are constructed by the port. -
Method Summary
Modifier and TypeMethodDescriptionabstract AsyncResource<Boolean> Removes the device key, which is what "forget this device" does.abstract AsyncResource<Boolean> Creates the device key if there is none, and converges when two callers race.abstract intWhether a device key exists for this id.abstract ProtectionReportWhat this device protection actually provides, as observed rather than as advertised.booleanWhether reaching the key requires the user to verify themselves -- a biometric, a passcode, a passkey with user verification.voidsetDeviceBoundRequired(boolean required) Requires that a key created by this protection cannot leave the device.abstract AsyncResource<byte[]> Decrypts whatwrap(String, byte[], byte[])produced.The variant of this protection whose key cannot be reached without the user verifying themselves, or null when this platform has none.abstract AsyncResource<byte[]> Encrypts under the device key.
-
Field Details
-
KEY_PRESENT
public static final int KEY_PRESENTThe key is stored on this device.- See Also:
-
KEY_ABSENT
public static final int KEY_ABSENTThe store answered, and there is no key. The only state in which creating one is safe.- See Also:
-
KEY_UNKNOWN
public static final int KEY_UNKNOWNThe store could not be asked. Not the same as absent, and callers must not write.- See Also:
-
-
Constructor Details
-
DeviceProtection
protected DeviceProtection()Subclasses are constructed by the port.
-
-
Method Details
-
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 reportingNOthere would understate an authenticator that uses a secure element. -
keyState
Whether a device key exists for this id.
Parameters
keyId: the vault's device key id
Returns
-
ensureKey
Creates the device key if there is none, and converges when two callers race.
Parameters
keyId: the vault's device key id
Returns
a resource completing with
truewhen a key is in place -- whether this call created it or found another one already there -- and erroring with aVaultExceptionwhen no key could be established -
wrap
Encrypts under the device key.
Parameters
-
keyId: the vault's device key id -
plaintext: what to protect, normally a vault's 32 byte data key -
aad: associated data the result is bound to; the same bytes must be supplied tounwrap(String, byte[], byte[])
Returns
a resource completing with the wrapped bytes. The format is the port's own and is never interpreted by shared code, but it must be authenticated
-
-
unwrap
Decrypts what
wrap(String, byte[], byte[])produced.A failure to authenticate must arrive as
VaultError.AUTHENTICATION_FAILED, a missing key asVaultError.KEY_MISSING, and a store that could not be read asVaultError.TEMPORARILY_UNREADABLE. Collapsing the last two is how a vault regenerates a key and orphans its data. -
deleteKey
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_VERIFICATIONinprotection()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
required: whether the key must be device bound
-
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
CryptoKeyin 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 returnsthisand reportsProtection.USER_VERIFICATION.Returning null is the honest answer where there is no such mechanism, and
UnlockPolicy.REQUIRE_USER_VERIFICATIONis then refused withVaultError.POLICY_NOT_METrather than quietly enrolled under the weaker one.Returns
a protection whose
requiresUserVerification()is true, or null
-