public abstract class DeviceProtection

  1. Object
  2. 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

  • ensureKey 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 exists so a caller can tell absence from unreadability; a port that answers ENTRY_ABSENT when 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.

Fields

public static final int KEY_PRESENT = 1The key is stored on this device.
public static final int KEY_ABSENT = 0The store answered, and there is no key.
public static final int KEY_UNKNOWN = -1The store could not be asked.

Constructors

protected DeviceProtection()Subclasses are constructed by the port.

Methods

public abstract ProtectionReport protection()What this device protection actually provides, as observed rather than as advertised.
public abstract int keyState(String keyId)Whether a device key exists for this id.
public abstract AsyncResource<Boolean> ensureKey(String keyId)Creates the device key if there is none, and converges when two callers race.
public abstract AsyncResource<byte[]> wrap(String keyId, byte[] plaintext, byte[] aad)Encrypts under the device key.
public abstract AsyncResource<byte[]> unwrap(String keyId, byte[] wrapped, byte[] aad)Decrypts what wrap produced.
public abstract AsyncResource<Boolean> deleteKey(String keyId)Removes the device key, which is what “forget this device” does.
public boolean requiresUserVerification()Whether reaching the key requires the user to verify themselves – a biometric, a passcode, a passkey with user verification.
public void setDeviceBoundRequired(boolean required)Requires that a key created by this protection cannot leave the device.
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.

Inherited methods

Field details

KEY_PRESENT

public static final int KEY_PRESENT = 1
The key is stored on this device.

KEY_ABSENT

public static final int KEY_ABSENT = 0
The store answered, and there is no key. The only state in which creating one is safe.

KEY_UNKNOWN

public static final int KEY_UNKNOWN = -1
The store could not be asked. Not the same as absent, and callers must not write.

Constructor details

DeviceProtection

protected DeviceProtection()
Subclasses are constructed by the port.

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)
Whether a device key exists for this id.

Parameters

keyId String
the vault’s device key id

ensureKey

public abstract AsyncResource<Boolean> ensureKey(String keyId)
Creates the device key if there is none, and converges when two callers race.

Parameters

keyId String
the vault’s device key id

Returns

a resource completing with 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 established

wrap

public abstract AsyncResource<byte[]> wrap(String keyId, byte[] plaintext, byte[] aad)
Encrypts under the device key.

Parameters

keyId String
the vault’s device key id
plaintext byte[]
what to protect, normally a vault’s 32 byte data key
aad byte[]
associated data the result is bound to; the same bytes must be supplied to unwrap

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

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

required boolean
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

a protection whose requiresUserVerification() is true, or null