Class DeviceProtection

java.lang.Object
com.codename1.security.vault.spi.DeviceProtection
Direct Known Subclasses:
SecureStorageDeviceProtection

public abstract class DeviceProtection extends Object

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 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.
  • Field Details

    • KEY_PRESENT

      public static final int KEY_PRESENT
      The key is stored on this device.
      See Also:
    • KEY_ABSENT

      public static final int KEY_ABSENT
      The 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_UNKNOWN
      The 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

      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: the vault's device key id
      Returns

      KEY_PRESENT, KEY_ABSENT or KEY_UNKNOWN

    • ensureKey

      public abstract AsyncResource<Boolean> ensureKey(String keyId)

      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 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: 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 to unwrap(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

      public abstract AsyncResource<byte[]> unwrap(String keyId, byte[] wrapped, byte[] aad)

      Decrypts what wrap(String, byte[], byte[]) 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: 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