public class SecureStorage

  1. Object
  2. SecureStorage

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.

Fields

public static final int ENTRY_PRESENT = 1
public static final int ENTRY_ABSENT = 0The store answered, and there is nothing under that account.
public static final int ENTRY_UNKNOWN = -1The store could not be asked, so nothing is known about the entry.

Constructors

protected SecureStorage()Subclasses are constructed by the port.

Methods

public static SecureStorage getInstance()Returns the platform-specific singleton owned by the current port.
public AsyncResource<String> get(String reason, String account)Retrieves a previously-stored entry, prompting for biometric authentication.
public AsyncResource<Boolean> set(String reason, String account, String value)Stores or overwrites a value for the given account.
public AsyncResource<Boolean> remove(String reason, String account)Removes a previously-stored entry.
public void setKeychainAccessGroup(String group)Configures the iOS keychain access group for sharing entries between the main app and its extensions.
public boolean set(String account, String value)Quietly stores or overwrites an entry under account.
public String get(String account)Quietly retrieves a previously-stored entry.
public boolean remove(String account)Quietly removes an entry.
public String setIfAbsent(String account, String value)The entry is there.
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.
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.
public int entryState(String account)Whether an entry exists, as distinct from whether it can be read.

Inherited methods

Field details

ENTRY_PRESENT

public static final int ENTRY_PRESENT = 1

ENTRY_ABSENT

public static final int ENTRY_ABSENT = 0
The store answered, and there is nothing under that account.

ENTRY_UNKNOWN

public static final int ENTRY_UNKNOWN = -1
The store could not be asked, so nothing is known about the entry.

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 String
the account to create
value String
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 String
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 String
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 String
the entry to ask about

Returns

one of #ENTRY_PRESENT, #ENTRY_ABSENT or #ENTRY_UNKNOWN