One Logo Pool Ball
wwwwwwwwwwwwwwwwwww

Secure Store

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.

Usage

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.

How it works

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.

API reference

MethodReturns
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.

SecureStoreOptionsTypeDescription
keychainServicestringService or alias identifying the store; reuse it for reads and deletes.
stringService or alias identifying the store; reuse it for reads and deletes.
keychainAccessibleSecureStoreAccessibilityiOS accessibility policy; defaults to 'whenUnlocked', ignored on Android.
SecureStoreAccessibilityiOS accessibility policy; defaults to 'whenUnlocked', ignored on Android.
requireAuthenticationbooleanProtect the item with the current biometric enrollment; defaults to false.
booleanProtect the item with the current biometric enrollment; defaults to false.
authenticationPromptstringCustom message for the biometric prompt.
stringCustom message for the biometric prompt.
accessGroupstringiOS Keychain sharing group; requires matching app entitlements.
stringiOS 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'.

Platform differences

iOSAndroidWeb
StorageKeychainEncrypted preferencesNone, rejects
KeyThe Keychain’s ownKeystore AES key
ReadableWhile unlockedAfter first unlock
After uninstallCan remainRemoved

Limits and gotchas

Examples

Protect a token with biometrics

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.

Restore a session on launch

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.

Recover from a failed read

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.