Skip to content

Commit

Permalink
Handle Android secure store corruption.
Browse files Browse the repository at this point in the history
  • Loading branch information
jameswilddev committed Jul 8, 2024
1 parent 5a0798a commit 170bd1b
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 34 deletions.
6 changes: 5 additions & 1 deletion jest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,11 @@ jest.mock('expo-secure-store', () => {
async getItemAsync (key: string, options?: unknown): Promise<null | string> {
if (options === undefined) {
await new Promise<void>((resolve) => setTimeout(resolve, 50))
return encryptedStorage.get(key) ?? null
if (key === 'Test Error-Throwing Key') {
throw new Error('Test Error')
} else {
return encryptedStorage.get(key) ?? null
}
} else {
throw new Error(
'expo-secure-store.getItemAsync\'s mock does not support options.'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { createSessionStoreManagerComponent, SessionStore } from '../../..'
type TestSession = { readonly value: number }

test('displays the loading screen', async () => {
const sessionStore = new SessionStore<TestSession>({ value: 5 }, randomUUID().toLowerCase())
const sessionStore = new SessionStore<TestSession>({ value: 5 }, randomUUID().toLowerCase(), { report: jest.fn() })
const SessionStoreManager = createSessionStoreManagerComponent(sessionStore)

const renderer = TestRenderer.create(
Expand Down Expand Up @@ -45,7 +45,7 @@ test('displays the loading screen', async () => {
})

test('shows the ready screen once given time to load', async () => {
const sessionStore = new SessionStore<TestSession>({ value: 5 }, randomUUID().toLowerCase())
const sessionStore = new SessionStore<TestSession>({ value: 5 }, randomUUID().toLowerCase(), { report: jest.fn() })
const SessionStoreManager = createSessionStoreManagerComponent(sessionStore)

const renderer = TestRenderer.create(
Expand Down Expand Up @@ -84,7 +84,7 @@ test('shows the ready screen once given time to load', async () => {
})

test('re-renders when the session is changed externally once', async () => {
const sessionStore = new SessionStore<TestSession>({ value: 5 }, randomUUID().toLowerCase())
const sessionStore = new SessionStore<TestSession>({ value: 5 }, randomUUID().toLowerCase(), { report: jest.fn() })
const SessionStoreManager = createSessionStoreManagerComponent(sessionStore)

const renderer = TestRenderer.create(
Expand Down Expand Up @@ -124,7 +124,7 @@ test('re-renders when the session is changed externally once', async () => {
})

test('re-renders when the session is changed externally twice', async () => {
const sessionStore = new SessionStore<TestSession>({ value: 5 }, randomUUID().toLowerCase())
const sessionStore = new SessionStore<TestSession>({ value: 5 }, randomUUID().toLowerCase(), { report: jest.fn() })
const SessionStoreManager = createSessionStoreManagerComponent(sessionStore)

const renderer = TestRenderer.create(
Expand Down Expand Up @@ -165,7 +165,7 @@ test('re-renders when the session is changed externally twice', async () => {
})

test('re-renders when the session is changed internally once', async () => {
const sessionStore = new SessionStore<TestSession>({ value: 5 }, randomUUID().toLowerCase())
const sessionStore = new SessionStore<TestSession>({ value: 5 }, randomUUID().toLowerCase(), { report: jest.fn() })
const SessionStoreManager = createSessionStoreManagerComponent(sessionStore)

const renderer = TestRenderer.create(
Expand Down Expand Up @@ -207,7 +207,7 @@ test('re-renders when the session is changed internally once', async () => {
})

test('re-renders when the session is changed internally twice', async () => {
const sessionStore = new SessionStore<TestSession>({ value: 5 }, randomUUID().toLowerCase())
const sessionStore = new SessionStore<TestSession>({ value: 5 }, randomUUID().toLowerCase(), { report: jest.fn() })
const SessionStoreManager = createSessionStoreManagerComponent(sessionStore)

const renderer = TestRenderer.create(
Expand Down
13 changes: 11 additions & 2 deletions react-native/services/SessionStore/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as SecureStore from 'expo-secure-store'
import { EventEmitter } from 'events'
import type { Json } from '../../types/Json'
import type { ErrorReporterInterface } from '../../types/ErrorReporterInterface'

/**
* A wrapper around expo-secure-store which adds:
Expand All @@ -23,10 +24,12 @@ export class SessionStore<T extends Json> {
* store.
* @param secureStorageKey The key of the record to read from/write to
* expo-secure-store.
* @param errorReporter The error reporter to use.
*/
constructor (
private readonly initial: T,
private readonly secureStorageKey: string
private readonly secureStorageKey: string,
private readonly errorReporter: ErrorReporterInterface
) {}

/**
Expand Down Expand Up @@ -64,7 +67,13 @@ export class SessionStore<T extends Json> {
} else {
this.unloaded = false

const raw = await SecureStore.getItemAsync(this.secureStorageKey)
let raw: null | string = null

try {
raw = await SecureStore.getItemAsync(this.secureStorageKey)
} catch (e) {
this.errorReporter.report(e)
}

if (raw === null) {
this.value = this.initial
Expand Down
14 changes: 12 additions & 2 deletions react-native/services/SessionStore/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,24 @@ A wrapper around `expo-secure-store` which adds:
- Change events.
- A synchronous read/write API (with asynchronous write-back).

## Android decryption failure handling

Expo on Android unfortunately has a tendency to lose the ability to decrypt the
secure store. It's not known why this is, but when it happens, the only
workaround is to catch the exception thrown by `expo-secure-store` and continue
as though the store is empty.

For this reason, any exceptions thrown by `expo-secure-store` during the load
phase are ignored.

## Usage

```tsx
import type { SessionStore } from "react-native-app-helpers";
import type { SessionStore, errorReporter } from "react-native-app-helpers";

type Session = `Session A` | `Session B`;

const store = new SessionStore<Session>(`Session A`, `SecureStorage Key`);
const store = new SessionStore<Session>(`Session A`, `SecureStorage Key`, errorReporter);


await store.load();
Expand Down
Loading

0 comments on commit 170bd1b

Please sign in to comment.