Class Vault

java.lang.Object
com.codename1.security.vault.Vault

public final class Vault extends Object

A password-protected store an application opens once and then uses, without choosing a cipher, a nonce, a KDF or a wrapping scheme.

What it is

One random 32 byte data key protects everything the vault holds. The data key itself is never stored: what is stored are wraps of it -- a copy sealed under a key derived from the user's password, optionally a copy sealed under a recovery code, and, if the user asked to be remembered, a copy sealed by this device's key store or by a non-extractable browser CryptoKey. Unlocking means unwrapping one of those copies. Changing the password rewrites one wrap and touches no data; forgetting a device deletes one wrap and touches no data.

That separation is what makes cross-device synchronization work without sending anything usable to a server. The record describing a vault -- the wraps included -- is ciphertext, and the records the vault seals are ciphertext, so a sync server stores both and can read neither.

Vault vault = Vault.named("notes");
if (!vault.isEnrolled()) {
    vault.enroll(password, new VaultOptions().policy(UnlockPolicy.REMEMBER_DEVICE)).get();
} else {
    try {
        // Not isDone(): that answers whether the operation has FINISHED, and it is true
        // for the ordinary KEY_MISSING this returns on a device that was never remembered.
        vault.unlockRemembered().get();
    } catch (RuntimeException notRemembered) {
        vault.unlockWithPassword(password).get();
    }
}
vault.putSecret("api.token", token).get();
What it protects against, and what it does not
Against Protected
A stolen ciphertext file, a stolen database, a backup Yes. Everything at rest is AEAD ciphertext under a key that is not stored beside it
A copied browser profile or device image, vault locked Yes for UnlockPolicy.SESSION_ONLY. No for UnlockPolicy.REMEMBER_DEVICE in a browser: a full profile copy carries the IndexedDB the wrapping key lives in, and the key works in the copy
A copied browser profile, vault unlocked No. The data key is in memory
Malicious script in the application's own origin, vault unlocked No. It calls the same decrypt the application calls. Nothing in this package prevents that, and non-extractable keys and Web Workers do not either
Malicious script, vault locked It cannot read the data, and it can wait, log keystrokes and read the password when the user next types it
A browser extension, or malware on the device No
A compromised server serving new application code No. New code runs in the origin and inherits everything the application has
Data already copied elsewhere No. forgetDevice() and destroyLocalData() remove local access; they cannot reach a copy

The row that gets misread is the second. "The database file alone is useless" and "a copy of the whole profile is useless" are different claims, and only the first is true of a browser with a remembered device. Say so to users rather than implying the stronger one.

Threads and the EDT

Every operation returns an AsyncResource. The expensive ones -- anything that derives a key from a password, which is six hundred thousand iterations by default -- run on a background thread, so calling get() on the result from the EDT parks the EDT through CN.invokeAndBlock(Runnable) rather than freezing it. Calls that only touch memory complete before they return and get() on them is free.

Applications should serialize state-changing operations on a vault instance. Locking can cancel work already running on a worker; generation checks share a monitor with key changes so the cancellation is visible across threads. Ports where an application runs in more than one process converge device-key creation (see DeviceProtection.ensureKey(String)), and the metadata counter detects another writer's changes.

  • Field Details

    • NOT_ENROLLED

      public static final int NOT_ENROLLED
      Vault is not set up on this device and has nothing stored.
      See Also:
    • LOCKED

      public static final int LOCKED
      Vault exists and is locked.
      See Also:
    • UNLOCKED

      public static final int UNLOCKED
      Vault exists and is unlocked.
      See Also:
    • STATE_UNKNOWN

      public static final int STATE_UNKNOWN
      The store could not be read, so nothing is known. Callers must not enroll over this.
      See Also:
  • Method Details

    • named

      public static Vault named(String name)

      Returns the vault with this name, creating the object but not the vault.

      The name separates one vault from another within an application; it is part of the binding every envelope carries, so two vaults cannot open each other's records even with the same password.

      Parameters
      • name: a short stable name, e.g. "notes". Must not be null or empty
    • getName

      public String getName()
      The name this vault was opened with.
    • configure

      public Vault configure(VaultOptions newOptions)

      Applies options -- required protections, KDF profile, unlock policy, auto lock.

      Call before enroll(char[], VaultOptions). Changing the policy afterwards is setPolicy(UnlockPolicy), which does the work of removing wraps the new policy forbids.

    • capabilities

      public VaultCapabilities capabilities()

      What this device can actually provide, per unlock policy.

      Ask before offering the user a choice: a "remember this device" switch on a platform that cannot remember is worse than no switch.

    • protection

      public ProtectionReport protection()

      What currently protects this vault on this device, as observed.

      This describes the state that exists, not the state that could: a vault enrolled UnlockPolicy.SESSION_ONLY reports no device key because there is none, on the same platform where capabilities() says one is available.

    • isEnrolled

      public boolean isEnrolled()
      Whether this vault has been set up on this device.
    • isUnlocked

      public boolean isUnlocked()
      Whether the data key is currently available.
    • state

      public int state()
    • passwordNeedsRewrap

      public boolean passwordNeedsRewrap()

      Whether the password wrap was written under a weaker KDF profile than KdfProfile.current().

      True only after a successful unlockWithPassword(char[]). An application that sees this and still holds the password should call changePassword(char[], char[]) with the same password on both sides, which rewraps under today's profile.

    • enroll

      public AsyncResource<Boolean> enroll(char[] password, VaultOptions opts)

      Sets up the vault for the first time.

      What happens, in this order
      1. The required protections in VaultOptions are checked against what this device provides. An unmet one fails here, before anything is written.
      2. A random vault id and a random 32 byte data key are generated.
      3. The data key is wrapped under the password and the record is written and read back and unwrapped. Only a wrap that has been proven to reopen counts as written.
      4. Only then, if the policy asks for it, is the device wrap created -- also written and verified.

      The ordering is the recoverable one. A crash after step 3 leaves a vault the password opens; a crash between 3 and 4 leaves the same. The reverse order would leave a window in which the vault opens on this device and nowhere else, and no password can rescue it.

      Parameters
      • password: the user's password. Cleared by this method once it has been used

      • opts: options, or null for the defaults

      Returns

      a resource completing when the vault is set up and unlocked, or erroring with VaultError.CONFLICT if one already exists, VaultError.POLICY_NOT_MET if a required protection is missing, or VaultError.STORAGE_UNAVAILABLE if nothing could be written. VaultError.LOCKED means locking interrupted setup; a committed password record can remain, so check state() and unlock it instead of enrolling again.

    • unlockWithPassword

      public AsyncResource<Boolean> unlockWithPassword(char[] password)

      Unlocks with the user's password.

      Parameters
      • password: the user's password, cleared by this method
      Returns

      a resource completing when the vault is unlocked, or erroring with VaultError.AUTHENTICATION_FAILED for a wrong password, VaultError.KEY_MISSING when there is no vault here, or VaultError.TEMPORARILY_UNREADABLE when the record could not be read

    • unlockRemembered

      public AsyncResource<Boolean> unlockRemembered()

      Unlocks from this device's remembered key, without a password.

      Fails with VaultError.KEY_MISSING when the device was never remembered or has been forgotten -- which is the ordinary case an application handles by asking for the password. Under UnlockPolicy.REQUIRE_USER_VERIFICATION the platform prompts here and a dismissed prompt is VaultError.CANCELLED, which is not a failure to report as an error to the user.

    • rememberDevice

      public AsyncResource<Boolean> rememberDevice()

      Remembers this device, so the vault reopens without a password.

      Requires the vault to be unlocked, because it is the data key that gets wrapped. Refused with VaultError.POLICY_NOT_MET when the policy is UnlockPolicy.REQUIRE_USER_VERIFICATION and this device's key store cannot gate on user verification -- an unattended wrap under a policy that promises a prompt is the failure that policy exists to prevent.

    • forgetDevice

      public AsyncResource<Boolean> forgetDevice()

      Forgets this device: the local wrap is deleted and so is the device key behind it.

      The vault still opens with the password, here and everywhere else. What this cannot do is reach a copy of the data key that was taken while the device was remembered -- see the table on this class.

    • lock

      public void lock()

      Locks the vault: the data key is overwritten and dropped, and every handle this vault issued stops working.

      An operation already in flight when this runs does not deliver its result -- it fails with VaultError.LOCKED instead. What locking cannot do is reach a plaintext or a key already handed to a caller, including to hostile code that got one while the vault was open.

    • destroyLocalData

      public AsyncResource<Boolean> destroyLocalData()

      Deletes everything this vault stores on this device: the record, the secrets, the device wrap and the device key.

      This is "delete my data from this device", not "delete my data". It does not reach a sync server, another device, a backup or an operating system snapshot, and a browser that has already written the storage to disk may leave the blocks recoverable. Say "removed from this browser", not "erased".

    • putSecret

      public AsyncResource<Boolean> putSecret(String secretName, char[] value)

      Stores a secret string under a name.

      The value is taken as characters rather than a String so the caller can clear it; this method clears the array it is given once the secret is sealed.

    • getSecret

      public AsyncResource<char[]> getSecret(String secretName)

      Reads a secret back. The caller owns the returned characters and should clear them.

      Errors with VaultError.KEY_MISSING when there is no such secret, and with VaultError.AUTHENTICATION_FAILED when there is one and it does not authenticate -- which means it was altered or was written under a different vault.

    • removeSecret

      public AsyncResource<Boolean> removeSecret(String secretName)
      Removes a secret.
    • seal

      public AsyncResource<byte[]> seal(String recordId, byte[] plaintext)

      Seals application data into a portable envelope, bound to a record id.

      Nothing is stored: the bytes come back for the caller to put wherever the data belongs -- a file, a database column, a sync server. They are readable only by a vault holding this data key, which means the same user's other devices after they enroll from exportSyncState().

    • open

      public AsyncResource<byte[]> open(String recordId, byte[] sealed)

      Opens what seal(String, byte[]) produced, including envelopes written before a

      invalid reference
      #rotateDataKey()
      .

      A record sealed under a retired key version is opened by walking the key chain in the vault record. A record whose key version is newer than this vault knows about is VaultError.UNSUPPORTED_FORMAT: another device rotated and this one has not synced yet, which is a state to report rather than to guess through.

    • operationalKey

      public AsyncResource<KeyHandle> operationalKey(String purpose)

      An opaque key for a named purpose, derived from the data key.

      Two purposes produce two independent keys -- the derivation is HMAC-SHA-256 over the data key, which is uniformly random, so this needs no salt and no stretching. Use it when a component wants its own key without being handed the vault's: a cache encryptor, a per-feature sealer, a MAC for an integrity check.

      The handle stops working when the vault locks.

    • databaseKey

      public AsyncResource<byte[]> databaseKey(String alias)

      Raw key bytes for an encrypted database, which is the one deliberate exposure in this package.

      Why this exists and why it is not hidden

      SQLCipher keys from bytes. That is true of the native builds and of the WASM build the browser uses, and no opaque handle changes it: at some point 32 bytes have to reach the engine. The choices were to pretend otherwise by quietly exporting a KeyHandle behind the caller's back, or to have one method, named for what it does, that says so in its documentation and can be refused by policy. This is the second.

      What it actually costs

      While the database is open the key is in the process: in this array until the caller clears it, inside the engine for as long as the connection lives, and in whatever the runtime copied it into. In a browser that means a heap any script in the origin shares. It is bounded by the vault being unlocked -- there is no key at all before that -- and it is not bounded by anything else.

      The key is derived from the vault's data key, so it changes when rotateDataKey(char[]) runs and the database has to be rekeyed in the same operation. It is not the data key itself, so a leak of it does not open the vault's records.

      Parameters
      • alias: the database's alias, normally its name. Two aliases get two unrelated keys
      Returns

      a resource completing with 32 bytes the caller owns and should clear, or erroring with VaultError.POLICY_NOT_MET when the vault was configured with VaultOptions.requireOpaqueKeysOnly(), or VaultError.LOCKED when it is locked

    • databaseKey

      public AsyncResource<byte[]> databaseKey(String alias, int version)

      Derives a database key from a specific current or retired data-key version.

      Use the version stored alongside a database to open it after a local or imported rotation, then explicitly rekey it with the current version. The retired chain retains these keys across restarts. The same lock and opaque-key restrictions as databaseKey(String) apply.

    • getDataKeyVersion

      public int getDataKeyVersion()
      The current data-key version. Store this non-secret value alongside a database when creating or rekeying it, so an imported rotation cannot hide which key opens the file. Requires an unlocked vault.
    • databaseKeyProtection

      public ProtectionReport databaseKeyProtection()

      What protects the key databaseKey(String) produces, reported honestly.

      It is the vault's own protection with one flag forced off: the bytes exist, so Protection.NON_EXTRACTABLE_KEY is NO here even where the vault reports YES. A database keyed this way is protected at rest by whatever protects the vault, and not at all from code running while it is open.

    • changePassword

      public AsyncResource<Boolean> changePassword(char[] oldPassword, char[] newPassword)

      Changes the password by rewrapping the data key. No record is re-encrypted, so this is constant time in the amount of data the vault holds.

      Requires the old password even when the vault is already unlocked: the alternative is that anyone who finds an unlocked application can lock the real owner out of every other device.

    • createRecoveryCode

      public AsyncResource<char[]> createRecoveryCode()

      Creates a recovery code and wraps the data key under it.

      The returned characters are the only copy: they are not stored anywhere, and a vault whose password is forgotten and whose recovery code was not written down is not recoverable by anybody, which is the property that makes the rest of this worth anything. Show them once, tell the user to keep them, and clear the array.

      A code is twenty bytes of randomness in Base32 -- 160 bits, which needs no stretching, so unlockWithRecoveryCode(char[]) is fast where unlockWithPassword(char[]) is deliberately slow.

    • unlockWithRecoveryCode

      public AsyncResource<Boolean> unlockWithRecoveryCode(char[] code)
      Unlocks with a recovery code from createRecoveryCode().
    • rotateDataKey

      public AsyncResource<Boolean> rotateDataKey(char[] password)

      Generates a new data key and retires the current one.

      The outgoing key is sealed under the incoming one and kept in the vault record, so everything sealed before this call still opens through open(String, byte[]). New records use the new key. The password and recovery wraps are rewritten to wrap the new key, which is why this needs the password.

      What rotation does not do is make an already-copied record unreadable. Anyone holding the old key and the old ciphertext keeps both.

      An existing recovery code stops working. It wrapped the outgoing key, and rewrapping it would need the code, which is not stored anywhere -- by design. Call createRecoveryCode() afterwards and tell the user the old one is void, or they will find out when they need it.

    • exportSyncState

      public byte[] exportSyncState()

      The vault record, as bytes to hand to a sync server or copy to a new device.

      Contains the wrapped data key and nothing usable: a server storing this cannot open the vault and cannot help anybody else to. The device wrap is deliberately excluded, because it is the one piece that would let a copy of this blob open the vault without the password.

      Keep the credentials the user logs into the sync server with separate from the vault password. A server that can verify the login must never be able to derive the vault key, which it could if they were the same string.

    • importSyncState

      public AsyncResource<Boolean> importSyncState(byte[] state, char[] password)

      Enrolls this device from another device's exportSyncState().

      Importing a rotation changes the current database key. Existing database files keep their previous keys: open them with databaseKey(String,int) or a versioned DatabaseConfig.vault and rekey explicitly. Keep the database's key version with its local metadata; the retired key chain remains available after this import.

      A policy failure rolls back an untouched import and locks this session. If rollback is unsafe or cannot be confirmed, VaultError.IMPORT_COMMITTED reports that the import reached committed state; reread it and finish policy setup rather than assuming no change.

      Replay and rollback

      The record carries a counter that increases on every change. This refuses a record whose counter is below the one already stored, because accepting one is how a server that has been compromised, or that simply serves a stale replica, rolls a device back to a password the user has since changed. AEAD proves the record was not altered; it says nothing about whether it is the latest, and no amount of cryptography inside the record can establish that on its own. An application that needs stronger freshness has to get it from the server -- a monotonic version the server refuses to decrease, an authenticated timestamp -- and an offline client cannot detect a rollback at all beyond what this counter catches.

      Parameters
      • state: bytes from exportSyncState() on another device

      • password: the vault password, cleared by this method

    • setPolicy

      public AsyncResource<Boolean> setPolicy(UnlockPolicy policy)

      Changes the unlock policy, doing the work the new policy implies.

      Moving to UnlockPolicy.SESSION_ONLY or UnlockPolicy.REQUIRE_USER_VERIFICATION deletes the unattended device wrap. That deletion is the whole point: a policy that promises a prompt while an alternative unlock sits beside it promises nothing.

    • getPolicy

      public UnlockPolicy getPolicy()
      The policy this device is operating under.