public class SecureStorage
- Object
- 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) withkSecAccessControlTouchIDCurrentSet. Sharing entries with App Extensions requires both theios.keychainAccessGroupbuild hint AND a call tosetKeychainAccessGroup(String)passing the same Team-ID-prefixed group identifier. - Android – AES/CBC/PKCS7 ciphertext stored in
SharedPreferenceswith the key in theAndroidKeyStore, locked viasetUserAuthenticationRequired(true). TheBiometricPrompt(API 29+) orFingerprintManager(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 byBiometrics. Useful for testing the round-trip andKEY_REVOKEDpaths 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 platformifstatements.
Fields
public static final int ENTRY_PRESENT = 1 | |
public static final int ENTRY_ABSENT = 0 | The store answered, and there is nothing under that account. |
public static final int ENTRY_UNKNOWN = -1 | The store could not be asked, so nothing is known about the entry. |
Constructors
protected SecureStorage() | Subclasses are constructed by the port. |
Methods
Inherited methods
Field details
ENTRY_PRESENT
public static final int ENTRY_PRESENT = 1ENTRY_ABSENT
public static final int ENTRY_ABSENT = 0ENTRY_UNKNOWN
public static final int ENTRY_UNKNOWN = -1Constructor details
SecureStorage
protected SecureStorage()getInstance().Method details
getInstance
public static SecureStorage getInstance()SecureStorage instance whose methods report
BiometricError.NOT_AVAILABLE.get
public AsyncResource<String> get(String reason, String account)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)BiometricError.NOT_AVAILABLE.remove
public AsyncResource<Boolean> remove(String reason, String account)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)account. The
user is not prompted. Returns false on the fallback base
class.get
public String get(String account)null
when the entry does not exist or when the platform does not
provide non-prompting storage.remove
public boolean remove(String account)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
accountString- the account to create
valueString- the value to store if there is none
Returns
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
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
preferredString- an identity the port already knows, or null to ask
Display
Returns
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
accountString- the account being created
Returns
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
accountString- the entry to ask about
Returns
#ENTRY_PRESENT, #ENTRY_ABSENT or #ENTRY_UNKNOWN