expo: 'Could not encrypt/decrypt the value for SecureStore' error on android app

Minimal reproducible example

https://snack.expo.dev/@simranjits11/error-with-securestorage

Summary

Context

When trying to get item using await SecureStore.getItemAsync('mobile'). It throws an error saying Could not encrypt/decrypt the value for SecureStore. The error is very device specific and happens occasionally.

  • Message: Error: Could not encrypt/decrypt the value for SecureStore .
  • SDK: 47.0.12
  • Device: OnePlus 8T (Android 13)

Stack Trace

Could not encrypt/decrypt the value for SecureStore 
at .construct([native code]:0:0) at .construct([native code]:0:0) 
at .p(index.android.bundle:50:360) 
at .s(index.android.bundle:48:363) 
at .construct([native code]:0:0) 
at .<unknown>(index.android.bundle:395:595) 
at .h(index.android.bundle:395:731)

Environment

expo-env-info 1.0.5 environment info: System: OS: macOS 13.3.1 Shell: 5.9 - /bin/zsh Binaries: Node: 18.14.2 - ~/.nvm/versions/node/v18.14.2/bin/node Yarn: 1.22.19 - /opt/homebrew/bin/yarn npm: 9.6.7 - ~/Documents/Projects/realmagic/node_modules/.bin/npm Watchman: 2023.02.20.00 - /opt/homebrew/bin/watchman Managers: CocoaPods: 1.11.3 - /Users/simranjitsingh/.rbenv/shims/pod SDKs: iOS SDK: Platforms: DriverKit 22.4, iOS 16.4, macOS 13.3, tvOS 16.4, watchOS 9.4 IDEs: Android Studio: 2020.3 AI-203.7717.56.2031.7678000 Xcode: 14.3/14E222b - /usr/bin/xcodebuild npmPackages: react: 18.1.0 => 18.1.0 react-dom: 18.1.0 => 18.1.0 npmGlobalPackages: eas-cli: 3.15.0 expo-cli: 6.3.2 Expo Workflow: managed

About this issue

  • Original URL
  • State: open
  • Created a year ago
  • Reactions: 10
  • Comments: 49 (8 by maintainers)

Commits related to this issue

Most upvoted comments

@behenate I have noticed that this issue mostly comes up if i install a new build (apk) without clearing the app data (by going to app info in device settings) of the previously installed build.

If i clear the app data and then uninstall the previous build before installing the new build, then i don’t see the issue. I haven’t tested a lot of devices for this behaviour so not sure if this is consistent across all devices, but found this interesting. It might help you pin point the issue.

Seems like the previously present app data/cache is causing the issue in the new build in some device models

@behenate is currently working on the migration to the new modules api so he will look into this also

@DavidRG13 Hi, sorry I can’t provide any ETA, because we couldn’t reproduce the issue, any instructions on a reliable way to reproduce the issue would be really helpful.

We are working on an update to expo-secure-store it doesn’t contain a specific fix for the issue but improves the error logging so it might give us some insights into the issue after we release it.

I’ve experienced same behaviour and got many sentry errors. As @edmbn already mentioned in https://github.com/expo/expo/issues/23426#issuecomment-1723901477 you shoud use try/catch and delete your entry on error because secure store can not read the value. After doing so I got zero sentry errors. here code sample:

export const setStoreItemAsync = async (key: SecureStoreKeys, value: string): Promise<void> => {
  try {
    await SecureStore.setItemAsync(key, value)
  } catch (error) {
    console.error('secure store set item error: ', error)
  }
}
export const deleteStoreItemAsync = async (key: SecureStoreKeys): Promise<void> => {
  try {
    await SecureStore.deleteItemAsync(key)
  } catch (error) {
    console.error('secure store delete item error: ', error)
  }
}
export const getStoreItemAsync = async (key: SecureStoreKeys): Promise<string | null> => {
  try {
    const item = await SecureStore.getItemAsync(key)
    if (item) {
      console.log(`${key} was used 🔐 \n`)
    } else {
      console.log('No values stored under key: ' + key)
    }
    return item
  } catch (error) {
    console.error('secure store get item error: ', error)
    await deleteStoreItemAsync(key) // HERE!
    return null
  }
}

Ok, so apparently it is normal behavior not to delete Secure Storage and also It is normal not being able to decrypt old data after reinstalling. The solution is to return null in a try/catch and save new data inside the same key. You can always save new data inside the same key but you cannot read old data from an old installation.

Is there any update or a resolution to this issue? We’re seeing this issue on Android devices. At a minimum, Samsung Galaxy S23 and Pixel 7a. No specific secure options used, just the default config. On expo-secure-store version 12.1.1, but I’m unsure if it’s happening on other versions. Android only issue. Does not happen on iOS.

Works for me

// if fetchKeychain fails, try again couple times
const ATTEMPTS_LIMIT = 3;

const fetchKeychain = async (): Promise<Data | undefined> => {
  let attempts = 0;
  while (attempts < ATTEMPTS_LIMIT) {
    try {
      // eslint-disable-next-line no-await-in-loop
      const data = await SecureStore.getItemAsync(KEY);

      return data;
    } catch (err) {
      if (attempts === 0) {
        logger.error('Failed to fetch keychain', err);
      }
      attempts += 1;
    }
  }
  // reset the keychain if keeps failing
  await resetKeychain();
  return undefined;
};

const resetKeychain = async (): Promise<void> => {
  await SecureStore.deleteItemAsync(KEY);
};

@AJGeel If this issue is solved by 12.5.0 (we are still not sure what causes it, but 12.5.0 introduces improvements in areas that are very likely to cause this issue) then yes, re-saving the value with the new version of secure-store should help.

Hi @behenate, thank you for the update!

The thing is, if the data has been saved with a previous version of expo-secure-store the chances of similar errors occuring are still the same.

Say, there are devices that have data stored with a previous version of secure-store. If the app updates the data stored in secure-store with the >= 12.5.0 version, would that resolve / improve the issue?

Same with expo sdk 49, expo secure store 12.3.1. 75% percent of our events are android 13.

We are also getting reports of this issue. Can anyone share a code sample of the solution. What operation is to be in the try block?

Not the cleanest solution but it does the job for now.

export const getFromSecureStore = async (key) => {
    let attempts = 0;
    while (attempts < 5) {
        try {
            return await SecureStore.getItemAsync(key);
        } catch (error) {
            attempts++;
        }
    }
    return false;
};

@behenate

Error Could not encrypt/decrypt the value for SecureStore 
    [native code] construct
    [native code] construct
    node_modules/@babel/runtime/helpers/construct.js:16:26 _construct
    node_modules/@babel/runtime/helpers/wrapNativeSuper.js:17:23 Wrapper
    [native code] construct
    node_modules/expo-modules-core/build/errors/CodedError.js:6:13 
    node_modules/expo-modules-core/build/errors/CodedError.js:10:8 CodedError

Thread 2 - main - (RUNNABLE)
        at com.facebook.systrace.Systrace.endSection(Systrace.java:55)
        at com.facebook.react.uimanager.UIViewOperationQueue$DispatchUIFrameCallback.doFrameGuarded(UIViewOperationQueue.java:1083)
        at com.facebook.react.uimanager.GuardedFrameCallback.doFrame(GuardedFrameCallback.java:29)
        at com.facebook.react.modules.core.ReactChoreographer$ReactChoreographerDispatcher.doFrame(ReactChoreographer.java:175)
        at com.facebook.react.modules.core.ChoreographerCompat$FrameCallback$1.doFrame(ChoreographerCompat.java:85)
        at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1410)
        at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1420)
        at android.view.Choreographer.doCallbacks(Choreographer.java:1047)
        at android.view.ChoreographerExtImpl.checkScrollOptSceneEnable(ChoreographerExtImpl.java:442)
        at android.view.Choreographer.doFrame(Choreographer.java:918)
        at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:1395)
        at android.os.Handler.handleCallback(Handler.java:942)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loopOnce(Looper.java:240)
        at android.os.Looper.loop(Looper.java:351)
        at android.app.ActivityThread.main(ActivityThread.java:8416)
        at java.lang.reflect.Method.invoke(Method.java:-2)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:584)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1013)

Thread 1684 - Signal Catcher - (WAITING)
        at unknown method(unknown file)

Thread 1685 - HeapTaskDaemon - (WAITING)
        at unknown method(unknown file)

Thread 1686 - Jit thread pool worker thread 0 - (RUNNABLE)
        at unknown method(unknown file)

Thread 1687 - ReferenceQueueDaemon - (WAITING)
        at java.lang.Object.wait(Object.java:-2)
        at java.lang.Object.wait(Object.java:442)
        at java.lang.Object.wait(Object.java:568)
        at java.lang.Daemons$ReferenceQueueDaemon.runInternal(Daemons.java:232)
        at java.lang.Daemons$Daemon.run(Daemons.java:140)
        at java.lang.Thread.run(Thread.java:1012)

Thread 1688 - FinalizerDaemon - (WAITING)
        at java.lang.Object.wait(Object.java:-2)
        at java.lang.Object.wait(Object.java:442)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:203)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:224)
        at java.lang.Daemons$FinalizerDaemon.runInternal(Daemons.java:300)
        at java.lang.Daemons$Daemon.run(Daemons.java:140)
        at java.lang.Thread.run(Thread.java:1012)

Thread 1689 - FinalizerWatchdogDaemon - (WAITING)
        at java.lang.Object.wait(Object.java:-2)
        at java.lang.Object.wait(Object.java:442)
        at java.lang.Object.wait(Object.java:568)
        at java.lang.Daemons$FinalizerWatchdogDaemon.sleepUntilNeeded(Daemons.java:385)
        at java.lang.Daemons$FinalizerWatchdogDaemon.runInternal(Daemons.java:365)
        at java.lang.Daemons$Daemon.run(Daemons.java:140)
        at java.lang.Thread.run(Thread.java:1012)

Thread 1690 - binder:12584_1 - (RUNNABLE)
        at unknown method(unknown file)

Thread 1691 - binder:12584_2 - (RUNNABLE)
        at unknown method(unknown file)

Thread 1692 - Profile Saver - (RUNNABLE)
        at unknown method(unknown file)

Thread 1693 - Bugsnag IO thread - (WAITING)
        at jdk.internal.misc.Unsafe.park(Unsafe.java:-2)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:194)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2081)
        at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:433)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1063)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1123)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637)
        at java.lang.Thread.run(Thread.java:1012)

Thread 1695 - pool-2-thread-1 - (TIMED_WAITING)
        at jdk.internal.misc.Unsafe.park(Unsafe.java:-2)
        at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:234)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2123)
        at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1188)
        at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:905)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1063)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1123)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637)
        at java.lang.Thread.run(Thread.java:1012)

Thread 1696 - Bugsnag Default thread - (TIMED_WAITING)
        at jdk.internal.misc.Unsafe.park(Unsafe.java:-2)
        at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:234)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2123)
        at java.util.concurrent.LinkedBlockingQueue.poll(LinkedBlockingQueue.java:458)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1062)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1123)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637)
        at java.lang.Thread.run(Thread.java:1012)

Thread 1697 - process reaper - (TIMED_WAITING)
        at jdk.internal.misc.Unsafe.park(Unsafe.java:-2)
        at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:234)
        at java.util.concurrent.SynchronousQueue$TransferStack.awaitFulfill(SynchronousQueue.java:463)
        at java.util.concurrent.SynchronousQueue$TransferStack.transfer(SynchronousQueue.java:361)
        at java.util.concurrent.SynchronousQueue.poll(SynchronousQueue.java:939)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1062)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1123)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637)
        at java.lang.Thread.run(Thread.java:1012)

Thread 1698 - bugsnag-anr-collector - (RUNNABLE)
        at android.os.MessageQueue.nativePollOnce(MessageQueue.java:-2)
        at android.os.MessageQueue.next(MessageQueue.java:349)
        at android.os.Looper.loopOnce(Looper.java:186)
        at android.os.Looper.loop(Looper.java:351)
        at android.os.HandlerThread.run(HandlerThread.java:67)

Thread 1699 - Bugsnag Error thread - (WAITING)
        at jdk.internal.misc.Unsafe.park(Unsafe.java:-2)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:194)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2081)
        at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:433)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1063)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1123)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637)
        at java.lang.Thread.run(Thread.java:1012)

Thread 1700 - Bugsnag Session thread - (WAITING)
        at jdk.internal.misc.Unsafe.park(Unsafe.java:-2)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:194)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2081)
        at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:433)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1063)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1123)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637)
        at java.lang.Thread.run(Thread.java:1012)

Thread 1701 - ConnectivityThread - (RUNNABLE)
        at android.os.MessageQueue.nativePollOnce(MessageQueue.java:-2)
        at android.os.MessageQueue.next(MessageQueue.java:349)
        at android.os.Looper.loopOnce(Looper.java:186)
        at android.os.Looper.loop(Looper.java:351)
        at android.os.HandlerThread.run(HandlerThread.java:67)

Thread 1702 - AppCenter.Looper - (RUNNABLE)
        at android.os.MessageQueue.nativePollOnce(MessageQueue.java:-2)
        at android.os.MessageQueue.next(MessageQueue.java:349)
        at android.os.Looper.loopOnce(Looper.java:186)
        at android.os.Looper.loop(Looper.java:351)
        at android.os.HandlerThread.run(HandlerThread.java:67)


Thread 1704 - queued-work-looper - (RUNNABLE)
        at android.os.MessageQueue.nativePollOnce(MessageQueue.java:-2)
        at android.os.MessageQueue.next(MessageQueue.java:349)
        at android.os.Looper.loopOnce(Looper.java:186)
        at android.os.Looper.loop(Looper.java:351)
        at android.os.HandlerThread.run(HandlerThread.java:67)

Thread 1705 - oplus.app.bg - (RUNNABLE)
        at android.os.MessageQueue.nativePollOnce(MessageQueue.java:-2)
        at android.os.MessageQueue.next(MessageQueue.java:349)
        at android.os.Looper.loopOnce(Looper.java:186)
        at android.os.Looper.loop(Looper.java:351)
        at android.os.HandlerThread.run(HandlerThread.java:67)

Thread 1706 - UIMonitorThread - (TIMED_WAITING)
        at java.lang.Thread.sleep(Thread.java:-2)
        at java.lang.Thread.sleep(Thread.java:450)
        at java.lang.Thread.sleep(Thread.java:355)
        at android.os.SystemClock.sleep(SystemClock…

I have not yet tried to downgrade. What has worked for me is to create a function that tries 5 times before failing as a workaround.

@behenate I’m not sure if this is useful, but the only thing I can extract from Sentry is the stack trace:

Error: secure-session/set-session | Could not encrypt the value for SecureStore
  at ?anon_0_(/Users/vagrant/git/src/shared/modules/authentication/ui/hooks/useSecureAuth.ts:165:22)
  at throw(native)
  at asyncGeneratorStep(/Users/vagrant/git/node_modules/@babel/runtime/helpers/asyncToGenerator.js:3:24)
  at _throw(/Users/vagrant/git/node_modules/@babel/runtime/helpers/asyncToGenerator.js:25:57)
  at tryCallOne(/Users/vagrant/git/node_modules/promise/setimmediate/core.js:36:3)
  at anonymous(/Users/vagrant/git/node_modules/promise/setimmediate/core.js:123:34)
  at apply(native)
  at anonymous(/Users/vagrant/git/node_modules/react-native/Libraries/Core/Timers/JSTimers.js:247:35)
  at _callTimer(/Users/vagrant/git/node_modules/react-native/Libraries/Core/Timers/JSTimers.js:113:15)
  at _callReactNativeMicrotasksPass(/Users/vagrant/git/node_modules/react-native/Libraries/Core/Timers/JSTimers.js:161:41)
  at callReactNativeMicrotasks(/Users/vagrant/git/node_modules/react-native/Libraries/Core/Timers/JSTimers.js:415:12)
  at __callReactNativeMicrotasks(/Users/vagrant/git/node_modules/react-native/Libraries/BatchedBridge/MessageQueue.js:392:42)
  at anonymous(/Users/vagrant/git/node_modules/react-native/Libraries/BatchedBridge/MessageQueue.js:132:39)
  at __guard(/Users/vagrant/git/node_modules/react-native/Libraries/BatchedBridge/MessageQueue.js:367:7)
  at flushedQueue(/Users/vagrant/git/node_modules/react-native/Libraries/BatchedBridge/MessageQueue.js:131:18)

@behenate any updates? Our users keep complaining that our app signs them off and it’s related to this bug 😢

Got same error on OnePlus 10T 5G (CPH2415)

After reinstalling the app (Android here), the underlying data is still there, but when we see the above errors, it means the secure store itself cannot self-recover by itself, the developer needs to have a logic to delete those bad records manually.

For user, the above case, they would have the experience that after installing a new version, then everything is gone, they have to re-login, re-store the state whatever they previous done, this sounds like not a good user experience.

That would be great if the secure store can handle it gracefully without interacting with a developer/user. (The best case would be: self-recover by itself and be able to read the data back correctly) Thanks.

We too have encountered many cases where the store returns null while there was a value stored in.

We store JWT auth tokens with redux thunk action:


export const tokensReceived = createAsyncThunk(
  'auth/tokenReceived',
  async ({ access, refresh }: TokenResponse) => {
    await setItemAsync(ACCESS_TOKEN, access);
    await setItemAsync(REFRESH_TOKEN, refresh);
  },
);

And retrieve it:


export const baseQuery = fetchBaseQuery({
  baseUrl: API_ENDPOINT_URL,
  prepareHeaders: async (headers) => {
    const accessToken = await getItemAsync(ACCESS_TOKEN);
    if (accessToken) {
      headers.set('Authorization', `Bearer ${accessToken}`);
    }
    headers.set('Accept-Language', getLocale());
    return headers;
  },
});

The token maybe null without throwing error after installing an update of the app.

Ok, so apparently it is normal behavior not to delete Secure Storage and also It is normal not being able to decrypt old data after reinstalling. The solution is to return null in a try/catch and save new data inside the same key. You can always save new data inside the same key but you cannot read old data from an old installation.

Experience the same issue. So far proves to be exactly that. Started to happen after the new install. Though it affected only one of two otherwise identical devices. Also Android. One alternative way to clear the error is to flush all app data through device settings. It surely isn’t a solution for production, but handy if you need to clear it fast and have access to the device.

Same issue found after upgrading to “expo-secure-store”: “~12.1.1” and expo SDK 48 on android device

I can also repro this issue in SM-T220. I’m getting: Could not encrypt/decrypt the value for SecureStore in Sentry on these devices.

I have this configuration:

const secureOptions = {
  requireAuthentication: false,
  authenticationPrompt: 'Authenticate to Verify Your Identity',
  keychainAccessible: SecureStore.ALWAYS_THIS_DEVICE_ONLY,
};

const saveAccessToken = async (value: string) => {
  await SecureStore.setItemAsync(
    'accessToken', 
    value,
    secureOptions
  );
  // Do something with the token
}

Are you saving the keys with requireAuthentication set to true?

No

@behenate I didn’t reproduce the error in those devices, I’ve just extracted the models from Sentry.

We have no specific implementation, just like @Simranjits11 provided repro. We are storing a JSON with 3 keys parsed as a string.

In so many other Android devices everything is working.

I don’t know how can I help