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