Skip to main content
‹ Storage
Interface

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
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​

src/vault.ts
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>;
ReturnsPromise<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
ParameterTypeDescription
keyRequiredstringThe name of the item in the storage.
valueRequiredstringAny text, the empty string too, without U+0000 and without a lone surrogate. Binary data goes through `setBytes`.
optionsSetOptionsThe 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 for biometryCurrentSetOrPasscode: the passcode doesn't make up for the missing biometrics), BIOMETRY_NOT_AVAILABLE, PASSCODE_NOT_SET, UNSUPPORTED (a passcode option, or authValidity, before Android 11), MISSING_USAGE_DESCRIPTION (iOS: a biometric option without NSFaceIDUsageDescription), HARDWARE_UNAVAILABLE (strongboxRequired without StrongBox).
  • The label goes with every write. A write without label removes 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 /migrate first.
  • Locked device. A write of a whenUnlocked* or whenPasscodeSetThisDeviceOnly item while the device is locked fails with INTERACTION_NOT_ALLOWED.

getItem​

getItem(key: string, options?: GetOptions): Promise<string | null>;
ReturnsPromise<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
ParameterTypeDescription
keyRequiredstringThe name of the item.
optionsGetOptionsThe 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;
}
  • null means nothing is stored. An item that is there but can't be read is an error: KEY_INVALIDATED when the enrolled biometrics changed since a biometryCurrentSet* write (the item is removed), INTERACTION_NOT_ALLOWED for a whenUnlocked* item while the device is locked.
  • Bytes as text. An item written with setBytes whose bytes aren't UTF-8 text fails with INVALID_ARGUMENT: read it with getBytes.
  • 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 authValidity window.
  • Android needs an activity for the prompt: NO_ACTIVITY without one.

setBytes​

setBytes(key: string, value: Uint8Array, options?: SetOptions): Promise<void>;
ReturnsPromise<void>PromptNeverThrowsAs `setItem`PlatformsiOSAndroid
ParameterTypeDescription
keyRequiredstringThe name of the item.
valueRequiredUint8ArrayAny bytes, zeros included, and an empty array.
optionsSetOptionsAs 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>;
ReturnsPromise<Uint8Array | null>PromptWhen the item has an accessControlThrowsAs `getItem`PlatformsiOSAndroid

Reads 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>;
ReturnsPromise<void>PromptNever, even for an item with an accessControlThrowsINVALID_ARGUMENT for a key that isn't validPlatformsiOSAndroid

Deletes 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>;
ReturnsPromise<boolean>PromptNeverThrowsINVALID_ARGUMENT for a key that isn't validPlatformsiOSAndroid

Whether 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>;
ReturnsPromise<ItemInfo | null>PromptNeverThrowsINVALID_ARGUMENT for a key that isn't validPlatformsiOSAndroid

What 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[]>;
ReturnsPromise<string[]>PromptNeverPlatformsiOSAndroid

The 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>;
ReturnsPromise<void>PromptNever, even for items with an accessControlPlatformsiOSAndroid

Deletes 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;
Returnsstring | nullPromptNeverThrowsAUTH_REQUIRED for an item with an accessControl; INTERACTION_NOT_ALLOWED; INVALID_ARGUMENT; DATA_CORRUPTEDPlatformsiOSAndroid

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

App.tsx
// 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;
ReturnsvoidPromptNeverThrowsAs `setItem`PlatformsiOSAndroid

Writes a string, as setItem does, with the same options and errors.

keychain.setItemSync('lastEnvironment', 'staging');

hasItemSync​

hasItemSync(key: string): boolean;
ReturnsbooleanPromptNeverThrowsINVALID_ARGUMENT for a key that isn't validPlatformsiOSAndroid

Whether there is an item under the key.

removeItemSync​

removeItemSync(key: string): void;
ReturnsvoidPromptNeverThrowsINVALID_ARGUMENT for a key that isn't validPlatformsiOSAndroid

Deletes 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>;
ReturnsPromise<void>PromptNeverThrowsAs `setItem`; INVALID_ARGUMENT for an empty username or a label optionPlatformsiOSAndroid
ParameterTypeDescription
credentialsRequiredCredentials{ username, password }. The username can't be empty; the password can. Neither can hold U+0000.
optionsSetOptions & CredentialsOptionsThe 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>;
ReturnsPromise<Credentials | null>PromptWhen the item has an accessControlThrowsAs `getItem`; INVALID_ARGUMENT for an item that has no labelPlatformsiOSAndroid

Reads 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>;
ReturnsPromise<void>PromptNeverThrowsINVALID_ARGUMENT for a server that isn't validPlatformsiOSAndroid

Deletes the credentials of the app, or of the server. Missing credentials are no error.

await vault.removeCredentials({ server: 'api.example.com' });

See also​