Encrypted string storage in the iOS Keychain and under an Android Keystore key
One.SecureStore keeps small secrets: session tokens, refresh tokens, API
keys. On iOS each value is a Keychain item; on Android each value is
encrypted with AES-256-GCM under a key that lives in the Android Keystore and
never leaves the device.
Reach for it for anything you would not want in a plain file. Set
requireAuthentication for biometric protection. For explicit per-operation
policies including device passcode authentication, use
Protected Store; for ordinary settings, use
Storage.
import { One } from 'one'
async function signIn(token: string) { await One.SecureStore.setItem('session', token)}
async function signOut() { await One.SecureStore.deleteItem('session')}
function savedSession() { return One.SecureStore.getItemSync('session')}The async methods run off the JavaScript thread. The sync ones block it, so use them for a value the first render needs, such as the saved session.
iOS. Each key is a generic password item in the Keychain under the
service One.SecureStore by default, with the key as its account. Items are
readable while unlocked unless keychainAccessible selects another policy.
A write updates the item, or adds it when it
does not exist yet.
Android. One creates an AES key in the Android Keystore the first time it writes. Each value is encrypted with a fresh 12-byte IV, which is stored in front of the ciphertext in a private preferences file. Writes and deletes commit to disk before they resolve, so a relaunch right after a write still reads it. If the Keystore loses the key, as can happen after a restore, the next write creates a new key and writes again under it.
Both forms, one store. getItem and getItemSync read the same items;
only the thread differs. Async calls run on a background thread; sync calls
run on the JavaScript thread.
Errors. A missing key reads null, and deleting a missing key succeeds.
Platform failures reject, or throw for the sync forms, with
E_SECURE_STORE_GET, E_SECURE_STORE_SET, or E_SECURE_STORE_DELETE as the
error’s code.
| Method | Returns |
|---|---|
| getItem(key, options?) | A promise of the value, or null |
| setItem(key, value, options?) | A promise |
| deleteItem(key, options?) | A promise |
| getItemSync(key, options?) | The value, or null |
| setItemSync(key, value, options?) | Nothing |
| deleteItemSync(key, options?) | Nothing |
Every method takes Expo SecureStore’s options. keychainService names a
separate store: the keychain service on iOS, the Keystore key alias on
Android; an item written under a service is read and deleted with the same
one. keychainAccessible is iOS’s item accessibility as a string:
'whenUnlocked' (the default), 'whenUnlockedThisDeviceOnly',
'afterFirstUnlock', 'afterFirstUnlockThisDeviceOnly',
'whenPasscodeSetThisDeviceOnly', 'always', or 'alwaysThisDeviceOnly';
Android accepts and ignores it, as in Expo. accessGroup is the iOS keychain
access group.
| SecureStoreOptions | Type | Description |
|---|---|---|
keychainServicestringService or alias identifying the store; reuse it for reads and deletes. | string | Service or alias identifying the store; reuse it for reads and deletes. |
keychainAccessibleSecureStoreAccessibilityiOS accessibility policy; defaults to 'whenUnlocked', ignored on Android. | SecureStoreAccessibility | iOS accessibility policy; defaults to 'whenUnlocked', ignored on Android. |
requireAuthenticationbooleanProtect the item with the current biometric enrollment; defaults to false. | boolean | Protect the item with the current biometric enrollment; defaults to false. |
authenticationPromptstringCustom message for the biometric prompt. | string | Custom message for the biometric prompt. |
accessGroupstringiOS Keychain sharing group; requires matching app entitlements. | string | iOS Keychain sharing group; requires matching app entitlements. |
requireAuthentication binds the item to the user’s biometrics:
authenticationPrompt is the message shown. On iOS the item gets a
biometryCurrentSet access control, so reads and updates prompt. On Android
its key requires authentication for every use and is invalidated when
biometrics change, after which reads return null. An
authenticated item answers a plain getItem too (with a prompt), and writing
one mode replaces the other. On Android the prompt needs a foreground screen,
so sync reads and authenticated writes throw; use the async methods.
Deletes never prompt. A cancelled or failed prompt rejects with the operation’s
error code. Set native.app.ios.faceIdUsageDescription before prebuilding an
app that uses Face ID; for an existing native project, provide
NSFaceIDUsageDescription in its Info.plist.
From Expo, rename setItemAsync(key, value, options)
to setItem and replace each numeric accessibility constant with its string:
SecureStore.WHEN_UNLOCKED is 'whenUnlocked'.
| iOS | Android | Web | |
|---|---|---|---|
| Storage | Keychain | Encrypted preferences | None, rejects |
| Key | The Keychain’s own | Keystore AES key | |
| Readable | While unlocked | After first unlock | |
| After uninstall | Can remain | Removed |
'afterFirstUnlock' allows later background reads while
locked, but cannot read before the first unlock after a restart.allowBackup to false. If you turn backup on, the preferences file
restores without the Keystore key, and reads fail with
E_SECURE_STORE_GET until the value is written again.import { One } from 'one'
const options = { keychainService: 'session-tokens', requireAuthentication: true, authenticationPrompt: 'Unlock your saved session',}
async function saveToken(token: string) { await One.SecureStore.setItem('session', token, options)}
async function readToken() { return One.SecureStore.getItem('session', options)}Use async calls so authentication can finish without blocking JavaScript. On iOS, creating an authenticated item does not prompt; reading or updating it does. On Android both writing and reading prompt. If biometrics change, the old item cannot be recovered; obtain a new token from your sign-in flow.
import { useState } from 'react'import { One } from 'one'
function useSession() { const [token, setToken] = useState(() => One.platform === 'web' ? null : One.SecureStore.getItemSync('session') ) return { token, async save(next: string) { await One.SecureStore.setItem('session', next) setToken(next) }, async clear() { await One.SecureStore.deleteItem('session') setToken(null) }, }}The one sync read decides the first screen without a loading state. Writes stay async, and the web check keeps the hook from throwing in a browser.
import { One } from 'one'
async function readToken(): Promise<string | null> { try { return await One.SecureStore.getItem('session') } catch (error) { if ((error as { code?: string }).code === 'E_SECURE_STORE_GET') { await One.SecureStore.deleteItem('session') } return null }}A value that cannot be decrypted, such as one restored without its key, is deleted so the app asks people to sign in again instead of failing on every launch.
Edit this page on GitHub.