Skip to main content

Threat model

What the package protects against and what it does not: data at rest, other apps, a stolen phone, backups; and root, jailbreak, process memory and key names.

The package puts secrets under the protection the operating system gives its own: the iOS Keychain, and keys of the Android Keystore in the TEE or StrongBox. This page says what that protection covers, so you can choose the options of an item and know what is left to your app.

What it protects against​

Protected
  • Reading the data at rest. Values are encrypted by keys that never leave the Secure Enclave or the TEE; a copy of the app's files doesn't decrypt.
  • Other apps. The sandbox and the Keychain's access rules keep other apps out; only apps of your team, through an accessGroup, share items.
  • A locked, stolen phone. whenUnlocked* items can't be read while the phone is locked; after a restart, no item can be read before the first unlock.
  • A changed app on a compromised device, for items behind biometrics. The key opens only after the user passes the prompt, inside the hardware; there is no answer in JavaScript to patch.
  • Backups. On Android the data is kept out of backups; on iOS, *ThisDeviceOnly items never move to another device.
  • Moving a ciphertext. On Android each ciphertext is bound to its service and key: put under another key, it fails to decrypt (DATA_CORRUPTED).
Not protected
  • Root and jailbreak, for items without accessControl. Code running as your app can call the same API your app calls and read every item it can read.
  • Process memory. A value read into JavaScript is a string in the JS heap, and the app can't wipe it. Read secrets when you need them; don't keep them in long-lived state.
  • Key names and labels. They are readable without a prompt by design; on Android they are in the clear in the app's sandbox.
  • Someone who knows the passcode, for items whose accessControl accepts it, and for every item on an unlocked phone in their hands.
  • What your app does with a value: logs, crash reports, analytics, screenshots of a screen that shows it.
  • The server side: a stolen refresh token is revoked by your backend, not by the keychain.

Choose protection by what the value can do​

ValueSuggested optionsWhy
A short-lived access tokenafterFirstUnlock, no accessControlBackground requests read it; it expires anyway
A refresh token, a session secretwhenUnlockedThisDeviceOnly, biometryCurrentSetIt can make new sessions: it should need the user
The key of a local databaseafterFirstUnlockThisDeviceOnly, no accessControlThe app opens the database at launch and in the background
A password for "Sign in as …"biometryCurrentSetOrPasscode, the username as a labelThe name shows without a prompt; the password needs the user
A setting that follows the userafterFirstUnlock, synchronizable: trueiCloud Keychain moves it; nothing device-bound

biometryCurrentSet is stronger than biometryAny: it also stops someone who knows the passcode from enrolling their own face. The …OrPasscode options are kinder to people whose biometrics fails often, and as strong as the passcode.

Root and jailbreak​

On a rooted or jailbroken device the operating system's guarantees are weaker, and an attacker can run code inside your app. That code can call the package's API like your own code does. What still holds:

  • items behind biometrics or the passcode need the user, because the check and the key are in hardware;
  • keys in the TEE or StrongBox can't be copied off the device, even when the files can.

Jailbreak and root detection are outside this package; if your app needs them, use a library made for them, and remember that detection can be bypassed too.

Out of scope​

The package stores secrets. It isn't a cryptography library: signing, keys created in the Secure Enclave, and passkeys belong to a separate package. Password AutoFill and shared web credentials belong to AuthenticationServices and Credential Manager.

How the protection is built on each platform: On iOS, On Android.