One Logo Pool Ball
wwwwwwwwwwwwwwwwwww

Protected Store

Keep device secrets behind biometric or passcode authentication

One.ProtectedStore keeps a separate protected namespace for secrets that require device authentication to read or change. Unlike One.SecureStore, every item carries an access control policy fixed at creation. All calls are async, so a system authentication prompt never blocks the JavaScript thread.

On iOS items live in the Keychain behind SecAccessControl, unlocking through LAContext. On Android each item gets its own Keystore key with encrypted records on API 30 and later, unlocking per operation through BiometricPrompt. Web and older Android never read or write an unprotected substitute; they stay unavailable.

Usage

import { One } from 'one'
async function saveRecoveryKey(secret: string, nextSecret: string) {
await One.ProtectedStore.createItem('recovery-key', secret, 'biometryCurrentSet')
const value = await One.ProtectedStore.getItem(
'recovery-key',
'Unlock your recovery key',
'biometryCurrentSet'
)
await One.ProtectedStore.updateItem(
'recovery-key',
nextSecret,
'Update your recovery key',
'biometryCurrentSet'
)
await One.ProtectedStore.deleteItem('recovery-key', 'Remove your recovery key', 'biometryCurrentSet')
return value
}

Every read and mutation takes the reason the system shows and the policy fixed at creation. Creation takes no reason on Android, where it prompts nothing.

How it works

Set native.app.ios.faceIdUsageDescription before prebuilding, including for Touch ID apps. One’s Android library manifest declares the normal install-time biometric permission, which the manifest merger adds to the app; an app that strips it rejects every call.

userPresence accepts biometrics or the device passcode. biometryCurrentSet requires the currently enrolled biometrics and invalidates the item when that enrollment changes. Both keep the item on this device and available only while unlocked. Pass the same policy to later reads and mutations; a mismatch rejects. updateItem changes the value while keeping the policy. createItem rejects when the key exists; delete first to recreate under another policy.

getItem returns null for a missing item. An item invalidated by an enrollment change cannot be read, but deleteItem removes it so the key can be reused. updateItem rejects for a missing key while deleteItem succeeds for one. A cancelled prompt rejects as cancelled; authentication failures reject as auth errors.

On Android every read or update authenticates one fresh private-key operation, and deleting an existing item needs fresh authentication too. Calls made while locked reject without prompting or changing the item. A missing result applies only when both the encrypted record and its Keystore alias are absent. Values and temporary encryption keys are never stored as plaintext.

API reference

MethodsTypeDescription
createItem(key: string, value: string, policy: ProtectedStorePolicy) => Promise<void>Store a secret, fixing its policy. Rejects when the key exists.
(key: string, value: string, policy: ProtectedStorePolicy) => Promise<void>Store a secret, fixing its policy. Rejects when the key exists.
getItem(key: string, reason: string, policy: ProtectedStorePolicy) => Promise<string | null>Read a secret after authentication. Null when missing.
(key: string, reason: string, policy: ProtectedStorePolicy) => Promise<string | null>Read a secret after authentication. Null when missing.
updateItem(key: string, value: string, reason: string, policy: ProtectedStorePolicy) => Promise<void>Replace a secret, keeping its policy.
(key: string, value: string, reason: string, policy: ProtectedStorePolicy) => Promise<void>Replace a secret, keeping its policy.
deleteItem(key: string, reason: string, policy: ProtectedStorePolicy) => Promise<void>Remove a secret. Succeeds when missing.
(key: string, reason: string, policy: ProtectedStorePolicy) => Promise<void>Remove a secret. Succeeds when missing.

The policy is userPresence or biometryCurrentSet.

Platform differences

iOSAndroidWeb
StoreKeychainKeystoreNone
UnlockSystem promptPrompt each opUnavailable
FloorAll versionsAPI 30 plusNone
MissingNullNullNull

Limits and gotchas

Examples

Guard a recovery key

import { One } from 'one'
const POLICY = 'biometryCurrentSet' as const
async function readRecoveryKey() {
const value = await One.ProtectedStore.getItem(
'recovery-key',
'Unlock your recovery key',
POLICY
)
if (value === null) {
await One.ProtectedStore.deleteItem('recovery-key', 'Clear stale key', POLICY)
return null
}
return value
}

A null read after an enrollment change means an invalidated item; the explicit delete frees the key for a fresh secret on the next setup.

Rotate a secret in place

import { One } from 'one'
async function rotateToken(next: string) {
try {
await One.ProtectedStore.updateItem(
'api-token',
next,
'Rotate your API token',
'userPresence'
)
} catch (error) {
if ((error as { code?: string }).code === 'E_PROTECTED_STORE_NOT_FOUND') {
await One.ProtectedStore.createItem('api-token', next, 'userPresence')
return
}
throw error
}
}

Update keeps the policy while swapping the value. The not-found branch covers first-run, creating under the same policy the update would have kept.

Edit this page on GitHub.