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 Details

    • ENTRY_PRESENT

      public static final int ENTRY_PRESENT
      See Also:
    • ENTRY_ABSENT

      public static final int ENTRY_ABSENT
      The store answered, and there is nothing under that account.
      See Also:
    • ENTRY_UNKNOWN

      public static final int ENTRY_UNKNOWN
      The store could not be asked, so nothing is known about the entry.
      See Also:
  • 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