Keychain
The methods of a storage: strings and bytes, what can be known without a prompt, listing and clearing, the synchronous calls, and credentials. The default keychain and every storage of createKeychain have them all.
import { type Keychain } from '@apexnative/keychain';
A Keychain is the items of one service. You don't make one with new: keychain is the storage of the app's own service, and createKeychain makes the others. The type is exported for the code that takes a storage as a parameter.
Usage
import { AccessControl, createKeychain, type Keychain } from '@apexnative/keychain';
export const vault = createKeychain({ service: 'vault', accessControl: AccessControl.BiometryCurrentSet });
await vault.setItem('refreshToken', token, { label: 'ivan@example.com' }); // no prompt
await vault.hasItem('refreshToken'); // true, no prompt
await vault.getItemInfo('refreshToken'); // { label: 'ivan@example.com', … }, no prompt
await vault.getItem('refreshToken'); // the prompt, then the value
export async function signOut(store: Keychain) {
await store.clear(); // no prompt
}
Every method checks its key and options in JavaScript first, the same way on both platforms, and fails with a KeychainError: an async method rejects, a sync method throws. A key is a non-empty string of up to 256 characters (code points) without U+0000. A missing item is null, never an error. Only getItem, getBytes and getCredentials can show the system prompt.
Signature
interface Keychain {
readonly service: string;
setItem(key: string, value: string, options?: SetOptions): Promise<void>;
getItem(key: string, options?: GetOptions): Promise<string | null>;
setBytes(key: string, value: Uint8Array, options?: SetOptions): Promise<void>;
getBytes(key: string, options?: GetOptions): Promise<Uint8Array | null>;
removeItem(key: string): Promise<void>;
hasItem(key: string): Promise<boolean>;
getItemInfo(key: string): Promise<ItemInfo | null>;
getAllKeys(): Promise<string[]>;
clear(): Promise<void>;
getItemSync(key: string): string | null;
setItemSync(key: string, value: string, options?: SetOptions): void;
hasItemSync(key: string): boolean;
removeItemSync(key: string): void;
setCredentials(
credentials: Credentials,
options?: Omit<SetOptions, 'label'> & CredentialsOptions
): Promise<void>;
getCredentials(options?: GetOptions & CredentialsOptions): Promise<Credentials | null>;
removeCredentials(options?: CredentialsOptions): Promise<void>;
}
A method without a value resolves to undefined on both platforms.
Values
service
readonly service: string;
The name of the storage: the service it was made with, or the app's identifier for keychain and a storage made without one (the bundle identifier on iOS, the application id on Android). The default is asked from the native side on first use; without the native module, reading it throws UNSUPPORTED.
setItem
setItem(key: string, value: string, options?: SetOptions): Promise<void>;
Promise<void>PromptNever, even for an item with an accessControlThrowsINVALID_ARGUMENT; when the device can't give the protection: BIOMETRY_NOT_ENROLLED, BIOMETRY_NOT_AVAILABLE, PASSCODE_NOT_SET, UNSUPPORTED, MISSING_USAGE_DESCRIPTION, HARDWARE_UNAVAILABLE; MISSING_ENTITLEMENT; INTERACTION_NOT_ALLOWEDPlatformsiOSAndroid| Parameter | Type | Description |
|---|---|---|
| keyRequired | string | The name of the item in the storage. |
| valueRequired | string | Any text, the empty string too, without U+0000 and without a lone surrogate. Binary data goes through `setBytes`. |
| options | SetOptions | The protection of this item and its label, over the storage's defaults. See SetOptions. |
Stores a string, kept as UTF-8, and replaces the item under the key: the value, the protection and the label together. The write is atomic: when the new item can't be stored, the old one stays.
import { AccessControl, keychain } from '@apexnative/keychain';
await keychain.setItem('accessToken', token);
await keychain.setItem('refreshToken', refreshToken, {
accessControl: AccessControl.BiometryCurrentSet,
label: 'ivan@example.com',
});
- The protection is never lowered. When the device can't give the item its
accessControl, the write fails with a code that says why, and nothing is stored with less:BIOMETRY_NOT_ENROLLED(also forbiometryCurrentSetOrPasscode: the passcode doesn't make up for the missing biometrics),BIOMETRY_NOT_AVAILABLE,PASSCODE_NOT_SET,UNSUPPORTED(a passcode option, orauthValidity, before Android 11),MISSING_USAGE_DESCRIPTION(iOS: a biometric option withoutNSFaceIDUsageDescription),HARDWARE_UNAVAILABLE(strongboxRequiredwithout StrongBox). - The label goes with every write. A write without
labelremoves the label the item had;''is a label. - Only its own items. On iOS an item of another library with the same service and key (react-native-keychain, expo-secure-store) isn't overwritten: the write fails with
INVALID_ARGUMENT. Use another key, or move the old item with/migratefirst. - Locked device. A write of a
whenUnlocked*orwhenPasscodeSetThisDeviceOnlyitem while the device is locked fails withINTERACTION_NOT_ALLOWED.
getItem
getItem(key: string, options?: GetOptions): Promise<string | null>;
Promise<string | null>PromptWhen the item has an accessControlThrowsThe prompt's codes (USER_CANCELED, USER_FALLBACK, SYSTEM_CANCELED, AUTH_FAILED, LOCKOUT, LOCKOUT_PERMANENT); KEY_INVALIDATED; INTERACTION_NOT_ALLOWED; NO_ACTIVITY; MISSING_USAGE_DESCRIPTION; INVALID_ARGUMENT; DATA_CORRUPTEDPlatformsiOSAndroid| Parameter | Type | Description |
|---|---|---|
| keyRequired | string | The name of the item. |
| options | GetOptions | The prompt of this read, merged field by field over the storage's. See GetOptions. |
Reads a string, or null when there is no item under the key. A protected item shows the system prompt first: the value comes back after the user passes it, or the call rejects with the code of how the prompt ended.
import { KeychainErrorCode, isKeychainError } from '@apexnative/keychain';
try {
const token = await vault.getItem('refreshToken', { prompt: { title: 'Sign in to Example' } });
if (token === null) showSignIn(); // never stored, or removed
} catch (error) {
if (isKeychainError(error, KeychainErrorCode.KeyInvalidated)) signInAgain();
else if (isKeychainError(error, KeychainErrorCode.UserCanceled)) showPasswordForm();
else throw error;
}
nullmeans nothing is stored. An item that is there but can't be read is an error:KEY_INVALIDATEDwhen the enrolled biometrics changed since abiometryCurrentSet*write (the item is removed),INTERACTION_NOT_ALLOWEDfor awhenUnlocked*item while the device is locked.- Bytes as text. An item written with
setByteswhose bytes aren't UTF-8 text fails withINVALID_ARGUMENT: read it withgetBytes. - Several reads. Prompts come one at a time, in the order the reads asked. Two reads of protected items show two prompts, unless the item has an
authValiditywindow. - Android needs an activity for the prompt:
NO_ACTIVITYwithout one.
setBytes
setBytes(key: string, value: Uint8Array, options?: SetOptions): Promise<void>;
| Parameter | Type | Description |
|---|---|---|
| keyRequired | string | The name of the item. |
| valueRequired | Uint8Array | Any bytes, zeros included, and an empty array. |
| options | SetOptions | As for `setItem`. |
Stores bytes as they are: an encryption key, a certificate, data that isn't text. They travel to the native side in Base64 and are kept as bytes, so nothing is lost on the way. Everything else is as setItem: the same options, the same errors, the same replacing of the item.
const key = crypto.getRandomValues(new Uint8Array(32));
await keys.setBytes('database', key);
getBytes
getBytes(key: string, options?: GetOptions): Promise<Uint8Array | null>;
Promise<Uint8Array | null>PromptWhen the item has an accessControlThrowsAs `getItem`PlatformsiOSAndroidReads bytes, or null. It reads any item: one written with setItem comes back as the UTF-8 bytes of its string.
const key = await keys.getBytes('database'); // Uint8Array | null
removeItem
removeItem(key: string): Promise<void>;
Promise<void>PromptNever, even for an item with an accessControlThrowsINVALID_ARGUMENT for a key that isn't validPlatformsiOSAndroidDeletes the item. A key with no item is no error. On iOS an item of another library under the same key stays: the package removes only its own.
await keychain.removeItem('accessToken');
Without a prompt
These methods read what is stored about the items, never their values, and never show a prompt, whatever protects the items.
hasItem
hasItem(key: string): Promise<boolean>;
Promise<boolean>PromptNeverThrowsINVALID_ARGUMENT for a key that isn't validPlatformsiOSAndroidWhether there is an item under the key. Use it to choose between the sign-in screen and the Face ID screen without bothering the user:
if (await vault.hasItem('refreshToken')) showUnlockWithFaceId();
else showSignIn();
getItemInfo
getItemInfo(key: string): Promise<ItemInfo | null>;
Promise<ItemInfo | null>PromptNeverThrowsINVALID_ARGUMENT for a key that isn't validPlatformsiOSAndroidWhat the item is and how it is protected, or null when there is no item: its key, its label, its accessible and accessControl, whether it is in iCloud Keychain, when it was written, and on Android where its key lives. See ItemInfo.
const info = await vault.getItemInfo('refreshToken');
// {
// key: 'refreshToken',
// label: 'ivan@example.com',
// accessible: 'whenUnlockedThisDeviceOnly',
// accessControl: 'biometryCurrentSet',
// synchronizable: false,
// createdAt: 1730000000000,
// modifiedAt: 1730000000000,
// android: { securityLevel: 'tee' }, // Android only
// }
if (info) showContinueAs(info.label);
The label is the one thing an app may show before the prompt: "Continue as ivan@example.com".
getAllKeys
getAllKeys(): Promise<string[]>;
Promise<string[]>PromptNeverPlatformsiOSAndroidThe keys of every item of the storage, sorted. Only the package's own items: on iOS the items of other libraries under the same service aren't listed. Credentials are listed under their keys, credentials and credentials:<server>.
await vault.getAllKeys(); // ['credentials', 'pin', 'refreshToken']
clear
clear(): Promise<void>;
Promise<void>PromptNever, even for items with an accessControlPlatformsiOSAndroidDeletes every item of the storage and, on Android, the Keystore keys of its service. The other storages keep their items, and on iOS so do the items of other libraries under the same service.
export async function signOut() {
await vault.clear();
}
Synchronous
The synchronous methods run on the JavaScript thread, for values needed before the first render. They never show a prompt. A sync call runs after the async calls made before it, so it sees a write that wasn't awaited; the JavaScript thread waits for that work, never for a user. A failure is thrown as the same KeychainError. See Synchronous API.
getItemSync
getItemSync(key: string): string | null;
string | nullPromptNeverThrowsAUTH_REQUIRED for an item with an accessControl; INTERACTION_NOT_ALLOWED; INVALID_ARGUMENT; DATA_CORRUPTEDPlatformsiOSAndroidReads a string, or null. An item that needs a prompt throws AUTH_REQUIRED at once, instead of stopping the JavaScript thread while the user looks at the phone: read it with getItem.
// Read once, when the module loads: no promise and no loading state.
const initialToken = keychain.getItemSync('accessToken');
setItemSync
setItemSync(key: string, value: string, options?: SetOptions): void;
Writes a string, as setItem does, with the same options and errors.
keychain.setItemSync('lastEnvironment', 'staging');
hasItemSync
hasItemSync(key: string): boolean;
booleanPromptNeverThrowsINVALID_ARGUMENT for a key that isn't validPlatformsiOSAndroidWhether there is an item under the key.
removeItemSync
removeItemSync(key: string): void;
voidPromptNeverThrowsINVALID_ARGUMENT for a key that isn't validPlatformsiOSAndroidDeletes the item; a key with no item is no error.
Credentials
A username and a password, kept as one item: the password is the value, the username its label. getItemInfo shows the username without a prompt. See Credentials.
setCredentials
setCredentials(
credentials: Credentials,
options?: Omit<SetOptions, 'label'> & CredentialsOptions
): Promise<void>;
Promise<void>PromptNeverThrowsAs `setItem`; INVALID_ARGUMENT for an empty username or a label optionPlatformsiOSAndroid| Parameter | Type | Description |
|---|---|---|
| credentialsRequired | Credentials | { username, password }. The username can't be empty; the password can. Neither can hold U+0000. |
| options | SetOptions & CredentialsOptions | The protection of the item, without label (the label is the username), and the server the credentials are for. |
Writes the credentials under the key credentials, or credentials:<server> with a server. One storage keeps one pair per server and one of the app's own.
await vault.setCredentials(
{ username: 'ivan@example.com', password },
{ accessControl: AccessControl.BiometryCurrentSet }
);
await vault.setCredentials({ username, password }, { server: 'api.example.com' });
The keys are ordinary keys of the storage: setItem('credentials', …) would replace the app's own pair. credentials:<server> must fit the rules of a key: up to 256 characters with the prefix.
getCredentials
getCredentials(options?: GetOptions & CredentialsOptions): Promise<Credentials | null>;
Promise<Credentials | null>PromptWhen the item has an accessControlThrowsAs `getItem`; INVALID_ARGUMENT for an item that has no labelPlatformsiOSAndroidReads the credentials of the app, or of the server, or null. An item under that key that wasn't written by setCredentials, and so has no label, fails with INVALID_ARGUMENT rather than make up a username.
const credentials = await vault.getCredentials(); // { username, password } | null
const api = await vault.getCredentials({ server: 'api.example.com', prompt: { title: 'Sign in' } });
removeCredentials
removeCredentials(options?: CredentialsOptions): Promise<void>;
Promise<void>PromptNeverThrowsINVALID_ARGUMENT for a server that isn't validPlatformsiOSAndroidDeletes the credentials of the app, or of the server. Missing credentials are no error.
await vault.removeCredentials({ server: 'api.example.com' });