1import ExpoSecureStore from './ExpoSecureStore'; 2 3export type KeychainAccessibilityConstant = number; 4 5// @needsAudit 6/** 7 * The data in the keychain item cannot be accessed after a restart until the device has been 8 * unlocked once by the user. This may be useful if you need to access the item when the phone 9 * is locked. 10 */ 11export const AFTER_FIRST_UNLOCK: KeychainAccessibilityConstant = ExpoSecureStore.AFTER_FIRST_UNLOCK; 12 13// @needsAudit 14/** 15 * Similar to `AFTER_FIRST_UNLOCK`, except the entry is not migrated to a new device when restoring 16 * from a backup. 17 */ 18export const AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY: KeychainAccessibilityConstant = 19 ExpoSecureStore.AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY; 20 21// @needsAudit 22/** 23 * The data in the keychain item can always be accessed regardless of whether the device is locked. 24 * This is the least secure option. 25 */ 26export const ALWAYS: KeychainAccessibilityConstant = ExpoSecureStore.ALWAYS; 27 28// @needsAudit 29/** 30 * Similar to `WHEN_UNLOCKED_THIS_DEVICE_ONLY`, except the user must have set a passcode in order to 31 * store an entry. If the user removes their passcode, the entry will be deleted. 32 */ 33export const WHEN_PASSCODE_SET_THIS_DEVICE_ONLY: KeychainAccessibilityConstant = 34 ExpoSecureStore.WHEN_PASSCODE_SET_THIS_DEVICE_ONLY; 35 36// @needsAudit 37/** 38 * Similar to `ALWAYS`, except the entry is not migrated to a new device when restoring from a backup. 39 */ 40export const ALWAYS_THIS_DEVICE_ONLY: KeychainAccessibilityConstant = 41 ExpoSecureStore.ALWAYS_THIS_DEVICE_ONLY; 42 43// @needsAudit 44/** 45 * The data in the keychain item can be accessed only while the device is unlocked by the user. 46 */ 47export const WHEN_UNLOCKED: KeychainAccessibilityConstant = ExpoSecureStore.WHEN_UNLOCKED; 48 49// @needsAudit 50/** 51 * Similar to `WHEN_UNLOCKED`, except the entry is not migrated to a new device when restoring from 52 * a backup. 53 */ 54export const WHEN_UNLOCKED_THIS_DEVICE_ONLY: KeychainAccessibilityConstant = 55 ExpoSecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY; 56 57const VALUE_BYTES_LIMIT = 2048; 58 59// @needsAudit 60export type SecureStoreOptions = { 61 /** 62 * - Android: Equivalent of the public/private key pair `Alias`. 63 * - iOS: The item's service, equivalent to [`kSecAttrService`](https://developer.apple.com/documentation/security/ksecattrservice/). 64 * > If the item is set with the `keychainService` option, it will be required to later fetch the value. 65 */ 66 keychainService?: string; 67 /** 68 * Option responsible for enabling the usage of the user authentication methods available on the device while 69 * accessing data stored in SecureStore. 70 * - Android: Equivalent to [`setUserAuthenticationRequired(true)`](https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.Builder#setUserAuthenticationRequired(boolean)) 71 * (requires API 23). 72 * - iOS: Equivalent to [`kSecAccessControlBiometryCurrentSet`](https://developer.apple.com/documentation/security/secaccesscontrolcreateflags/ksecaccesscontrolbiometrycurrentset/). 73 * Complete functionality is unlocked only with a freshly generated key - this would not work in tandem with the `keychainService` 74 * value used for the others non-authenticated operations. 75 * 76 * Warning: This option is not supported in Expo Go when biometric authentication is available due to a missing NSFaceIDUsageDescription. 77 * In release builds or when using continuous native generation, make sure to use the `expo-secure-store` config plugin. 78 * 79 */ 80 requireAuthentication?: boolean; 81 /** 82 * Custom message displayed to the user while `requireAuthentication` option is turned on. 83 */ 84 authenticationPrompt?: string; 85 /** 86 * Specifies when the stored entry is accessible, using iOS's `kSecAttrAccessible` property. 87 * @see Apple's documentation on [keychain item accessibility](https://developer.apple.com/documentation/security/ksecattraccessible/). 88 * @default SecureStore.WHEN_UNLOCKED 89 * @platform ios 90 */ 91 keychainAccessible?: KeychainAccessibilityConstant; 92}; 93 94// @needsAudit 95/** 96 * Returns whether the SecureStore API is enabled on the current device. This does not check the app 97 * permissions. 98 * 99 * @return Promise which fulfils witch `boolean`, indicating whether the SecureStore API is available 100 * on the current device. Currently, this resolves `true` on Android and iOS only. 101 */ 102export async function isAvailableAsync(): Promise<boolean> { 103 return !!ExpoSecureStore.getValueWithKeyAsync; 104} 105 106// @needsAudit 107/** 108 * Delete the value associated with the provided key. 109 * 110 * @param key The key that was used to store the associated value. 111 * @param options An [`SecureStoreOptions`](#securestoreoptions) object. 112 * 113 * @return A promise that will reject if the value couldn't be deleted. 114 */ 115export async function deleteItemAsync( 116 key: string, 117 options: SecureStoreOptions = {} 118): Promise<void> { 119 ensureValidKey(key); 120 121 await ExpoSecureStore.deleteValueWithKeyAsync(key, options); 122} 123 124// @needsAudit 125/** 126 * Reads the stored value associated with the provided key. 127 * 128 * @param key The key that was used to store the associated value. 129 * @param options An [`SecureStoreOptions`](#securestoreoptions) object. 130 * 131 * @return A promise that resolves to the previously stored value. It will return `null` if there is no entry 132 * for the given key or if the key has been invalidated. It will reject if an error occurs while retrieving the value. 133 * 134 * > Keys are invalidated by the system when biometrics change, such as adding a new fingerprint or changing the face profile used for face recognition. 135 * > After a key has been invalidated, it becomes impossible to read its value. 136 * > This only applies to values stored with `requireAuthentication` set to `true`. 137 */ 138export async function getItemAsync( 139 key: string, 140 options: SecureStoreOptions = {} 141): Promise<string | null> { 142 ensureValidKey(key); 143 return await ExpoSecureStore.getValueWithKeyAsync(key, options); 144} 145 146// @needsAudit 147/** 148 * Stores a key–value pair. 149 * 150 * @param key The key to associate with the stored value. Keys may contain alphanumeric characters, `.`, `-`, and `_`. 151 * @param value The value to store. Size limit is 2048 bytes. 152 * @param options An [`SecureStoreOptions`](#securestoreoptions) object. 153 * 154 * @return A promise that will reject if value cannot be stored on the device. 155 */ 156export async function setItemAsync( 157 key: string, 158 value: string, 159 options: SecureStoreOptions = {} 160): Promise<void> { 161 ensureValidKey(key); 162 if (!isValidValue(value)) { 163 throw new Error( 164 `Invalid value provided to SecureStore. Values must be strings; consider JSON-encoding your values if they are serializable.` 165 ); 166 } 167 168 await ExpoSecureStore.setValueWithKeyAsync(value, key, options); 169} 170 171/** 172 * Stores a key–value pair synchronously. 173 * > **Note:** This function blocks the JavaScript thread, so the application may not be interactive when the `requireAuthentication` option is set to `true` until the user authenticates. 174 * 175 * @param key The key to associate with the stored value. Keys may contain alphanumeric characters, `.`, `-`, and `_`. 176 * @param value The value to store. Size limit is 2048 bytes. 177 * @param options An [`SecureStoreOptions`](#securestoreoptions) object. 178 * 179 */ 180export function setItem(key: string, value: string, options: SecureStoreOptions = {}): void { 181 ensureValidKey(key); 182 if (!isValidValue(value)) { 183 throw new Error( 184 `Invalid value provided to SecureStore. Values must be strings; consider JSON-encoding your values if they are serializable.` 185 ); 186 } 187 188 return ExpoSecureStore.setValueWithKeySync(value, key, options); 189} 190 191/** 192 * Synchronously reads the stored value associated with the provided key. 193 * > **Note:** This function blocks the JavaScript thread, so the application may not be interactive when reading a value with `requireAuthentication` 194 * > option set to `true` until the user authenticates. 195 * @param key The key that was used to store the associated value. 196 * @param options An [`SecureStoreOptions`](#securestoreoptions) object. 197 * 198 * @return Previously stored value. It will return `null` if there is no entry for the given key or if the key has been invalidated. 199 */ 200export function getItem(key: string, options: SecureStoreOptions = {}): string | null { 201 ensureValidKey(key); 202 return ExpoSecureStore.getValueWithKeySync(key, options); 203} 204 205function ensureValidKey(key: string) { 206 if (!isValidKey(key)) { 207 throw new Error( 208 `Invalid key provided to SecureStore. Keys must not be empty and contain only alphanumeric characters, ".", "-", and "_".` 209 ); 210 } 211} 212 213function isValidKey(key: string) { 214 return typeof key === 'string' && /^[\w.-]+$/.test(key); 215} 216 217function isValidValue(value: string) { 218 if (typeof value !== 'string') { 219 return false; 220 } 221 if (byteCount(value) > VALUE_BYTES_LIMIT) { 222 console.warn( 223 'Provided value to SecureStore is larger than 2048 bytes. An attempt to store such a value will throw an error in SDK 35.' 224 ); 225 } 226 return true; 227} 228 229// copy-pasted from https://stackoverflow.com/a/39488643 230function byteCount(value: string) { 231 let bytes = 0; 232 233 for (let i = 0; i < value.length; i++) { 234 const codePoint = value.charCodeAt(i); 235 236 // Lone surrogates cannot be passed to encodeURI 237 if (codePoint >= 0xd800 && codePoint < 0xe000) { 238 if (codePoint < 0xdc00 && i + 1 < value.length) { 239 const next = value.charCodeAt(i + 1); 240 241 if (next >= 0xdc00 && next < 0xe000) { 242 bytes += 4; 243 i++; 244 continue; 245 } 246 } 247 } 248 249 bytes += codePoint < 0x80 ? 1 : codePoint < 0x800 ? 2 : 3; 250 } 251 252 return bytes; 253} 254