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