# Class SecureStorage

java.lang.Object
com.codename1.security.SecureStorage

public class SecureStorage extends Object

Biometric-gated secure storage backed by the platform keychain. Reading an entry prompts the user for biometric authentication; writing or deleting may or may not, depending on the platform.

Entries are bound to the current set of enrolled biometrics. If the user adds a fingerprint, enrols a new face, or disables device security, every stored entry is automatically invalidated and subsequent `get(String, String)` calls fail with `BiometricError.KEY_REVOKED`. The application must then re-prompt the user for the original value and `set(String, String, String)` it again.

Use this for short, secret strings (auth tokens, refresh tokens, encryption keys). For larger data, encrypt with a key stored here.

##### Platform support

- iOS -- backed by Security.framework (`SecItemAdd` / `SecItemCopyMatching` / `SecItemDelete`) with `kSecAccessControlTouchIDCurrentSet`. Sharing entries with App Extensions requires both the `ios.keychainAccessGroup` build hint AND a call to `setKeychainAccessGroup(String)` passing the same Team-ID-prefixed group identifier.
- Android -- AES/CBC/PKCS7 ciphertext stored in `SharedPreferences` with the key in the `AndroidKeyStore`, locked via `setUserAuthenticationRequired(true)`. The `BiometricPrompt` (API 29+) or `FingerprintManager` (API 23-28) unlocks the cipher for one operation per prompt.
- JavaSE simulator -- backed by `java.util.prefs.Preferences`, gated on the same Biometric Simulation menu used by `Biometrics`. Useful for testing the round-trip and `KEY_REVOKED` paths without a device.
- All other platforms -- this base class is returned as-is and acts as a non-supporting fallback: every method completes with `BiometricError.NOT_AVAILABLE`. Application code does not need platform `if` statements.

- ## Field Summary

  Fields
  Modifier and Type
  Field
  Description
  `static final int`
  `ENTRY_ABSENT`
  The store answered, and there is nothing under that account.
  `static final int`
  `ENTRY_PRESENT`
  `static final int`
  `ENTRY_UNKNOWN`
  The store could not be asked, so nothing is known about the entry.

- ## Constructor Summary

  Constructors
  Modifier
  Constructor
  Description
  `protected`
  `SecureStorage()`
  Subclasses are constructed by the port.

- ## Method Summary

  Modifier and Type
  Method
  Description
  `protected static String`
  `applicationNamespace()`
  A name unique to this application, for a store the platform shares between applications.
  `protected static String`
  `applicationNamespace(String preferred)`
  The same identifier, for a port that knows the application before `Display` can say.
  `int`
  `entryState(String account)`
  Whether an entry exists, as distinct from whether it can be read.
  `protected static String`
  `gateName(String account)`
  The file name a port uses to gate the creation of one account, for the ports whose create-if-absent is a file.
  `String`
  `get(String account)`
  Quietly retrieves a previously-stored entry.
  `AsyncResource<String>`
  `get(String reason, String account)`
  Retrieves a previously-stored entry, prompting for biometric authentication.
  `static SecureStorage`
  `getInstance()`
  Returns the platform-specific singleton owned by the current port.
  `boolean`
  `remove(String account)`
  Quietly removes an entry.
  `AsyncResource<Boolean>`
  `remove(String reason, String account)`
  Removes a previously-stored entry.
  `boolean`
  `set(String account, String value)`
  Quietly stores or overwrites an entry under `account`.
  `AsyncResource<Boolean>`
  `set(String reason, String account, String value)`
  Stores or overwrites a value for the given account.
  `String`
  `setIfAbsent(String account, String value)`
  The entry is there.
  `void`
  `setKeychainAccessGroup(String group)`
  Configures the iOS keychain access group for sharing entries between the main app and its extensions.

  ### Methods inherited from class Object

  `clone, equals, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait`

- ## Field Details

  - ### ENTRY_PRESENT

    public static final int ENTRY_PRESENT
    See Also:
      - Constant Field Values

  - ### ENTRY_ABSENT

    public static final int ENTRY_ABSENT
    The store answered, and there is nothing under that account.
    See Also:
      - Constant Field Values

  - ### ENTRY_UNKNOWN

    public static final int ENTRY_UNKNOWN
    The store could not be asked, so nothing is known about the entry.
    See Also:
      - Constant Field Values

- ## Constructor Details

  - ### SecureStorage

    protected SecureStorage()
    Subclasses are constructed by the port. Application code obtains the active instance via `getInstance()`.

- ## Method Details

  - ### getInstance

    public static SecureStorage getInstance()
    Returns the platform-specific singleton owned by the current port. On ports that do not implement secure storage this returns a base `SecureStorage` instance whose methods report `BiometricError.NOT_AVAILABLE`.

  - ### get

    public AsyncResource<String> get(String reason, String account)
    Retrieves a previously-stored entry, prompting for biometric authentication. The returned `AsyncResource` completes with the value, or with a `BiometricException` on failure (including `BiometricError.KEY_REVOKED` when biometrics have been re-enrolled since the entry was written). On the fallback base class this completes immediately with `BiometricError.NOT_AVAILABLE`.

  - ### set

    public AsyncResource<Boolean> set(String reason, String account, String value)
    Stores or overwrites a value for the given account. On iOS the user is typically not prompted (Apple's keychain accepts writes without re-authenticating); on Android the user is prompted because the underlying cipher requires biometric authentication. On the fallback base class this completes immediately with `BiometricError.NOT_AVAILABLE`.

  - ### remove

    public AsyncResource<Boolean> remove(String reason, String account)
    Removes a previously-stored entry. No authentication is required since deletion does not reveal the value. On the fallback base class this completes immediately with `BiometricError.NOT_AVAILABLE`.

  - ### setKeychainAccessGroup

    public void setKeychainAccessGroup(String group)

    Configures the iOS keychain access group for sharing entries between the main app and its extensions. The argument must include the Team ID prefix (e.g. `"ABCDE12345.group.com.example.app"`). Pass `null` or empty to clear. Ignored on non-iOS platforms and on the fallback base class.

    The `ios.keychainAccessGroup` build hint must declare the same group in the app's entitlements for this to work.

  - ### set

    public boolean set(String account, String value)
    Quietly stores or overwrites an entry under `account`. The user is not prompted. Returns `false` on the fallback base class.

  - ### get

    public String get(String account)
    Quietly retrieves a previously-stored entry. Returns `null` when the entry does not exist or when the platform does not provide non-prompting storage.

  - ### remove

    public boolean remove(String account)
    Quietly removes an entry. Returns `false` on the fallback base class.

  - ### setIfAbsent

    public String setIfAbsent(String account, String value)

    The entry is there. Stores a value only if this account has none, and reports what the store ended up holding.

    The operation a first-time key needs. Reading, generating and storing as three steps is safe within one process and not between two: `synchronized` covers threads in one VM, while an application can be opened from more than one -- Android components declared with their own `android:process`, or simply two runs of a desktop build -- and both can see nothing stored, generate different keys, and each overwrite the other. The database is then encrypted with whichever key did not survive, and nothing can open it again.

    The return value is what makes racing callers agree: a caller that lost stores nothing and is handed the value that won, so both go on to open the database with the same key.

    This default is the best a store with no create-if-absent of its own can do -- the check and the write are still two operations, so a second process can land between them, and what it prevents is the divergence rather than the race. A port whose store can do this in one step overrides it; iOS does, because the keychain's own add fails when the item exists.

    ###### Parameters

    - `account`: the account to create

    - `value`: the value to store if there is none

    ###### Returns

    the value now stored, which may be another caller's, or null if the store cannot say

  - ### applicationNamespace

    protected static String applicationNamespace()

    A name unique to this application, for a store the platform shares between applications.

    The mobile ports do not need this: an OS sandbox already separates one application's keychain or keystore from another's. A native desktop build has no sandbox -- its storage is a plain directory under the user account -- so two applications that ask for the same account name reach the same entry. For a managed database key that means one application reading another's key, and forgetting it in either one removing the other's only copy.

    The package is what the installer, the store and the build all treat as the application's identity. A display name is the fallback because a build without a package still has one, though it is weaker: two vendors can both ship "Notes".

    ###### Returns

    an identifier safe to embed in a storage name, never null and never empty

  - ### applicationNamespace

    protected static String applicationNamespace(String preferred)

    The same identifier, for a port that knows the application before `Display` can say.

    The simulator is that case: it builds its store while the port is still coming up, so `Display` cannot answer yet -- and it has the launcher's main class in hand, which is where its `package_name` comes from in the first place.

    ###### Parameters

    - `preferred`: an identity the port already knows, or null to ask `Display`

    ###### Returns

    an identifier safe to embed in a storage name, never null and never empty

  - ### gateName

    protected static String gateName(String account)

    The file name a port uses to gate the creation of one account, for the ports whose create-if-absent is a file.

    Derived from the account itself rather than from its hash, because a hash is not a name: `Aa` and `BB` hash alike, so two aliases would share one gate and whichever asked second could never create its key -- it would find no value of its own and no gate to take. The escape is the one `#applicationNamespace()` uses, so the result is reversible and two accounts that differ keep different gates.

    A name too long to be a file gets its first part plus a hash of the whole, which is the one place a hash is the right answer: the alternative is a name the filesystem refuses.

    ###### Parameters

    - `account`: the account being created

    ###### Returns

    a file name, unique to this application and account

  - ### entryState

    public int entryState(String account)

    Whether an entry exists, as distinct from whether it can be read.

    `#get(String)` cannot answer this: it returns null for an entry that is not there and for one it could not read, and a caller that treats those alike will eventually treat a store that is briefly unavailable as a store that is empty. Where that caller then writes -- a managed database key is the case this was added for -- it overwrites a key that was there all along, and the database encrypted under the old one can never be opened again.

    A port answers `#ENTRY_PRESENT` for an entry it can see even if it cannot decrypt it: the question is existence, not readability. The default is `#ENTRY_UNKNOWN`, which is the honest answer for a platform with no non-prompting store, and callers must treat it as "do not write".

    ###### Parameters

    - `account`: the entry to ask about

    ###### Returns

    one of `#ENTRY_PRESENT`, `#ENTRY_ABSENT` or `#ENTRY_UNKNOWN`