feat(security): encrypt AsyncStorage form cache entries with AES-256-GCM (#587)#719
Merged
RUKAYAT-CODER merged 2 commits intoJun 28, 2026
Merged
Conversation
…GCM (rinafcode#587) Plain-text form data (names, addresses, emails) persisted to AsyncStorage was readable by any process with storage access. Each storage key now gets its own AES-256-GCM key stored in SecureStore (hardware-backed on Android, Secure Enclave on iOS); encrypted blobs are written as iv_b64.ciphertext_b64. logout() wipes the user's form cache from both AsyncStorage and SecureStore so no residue survives a session change.
|
@Vox-d-glitch Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
Contributor
|
Great job so far There’s just one blocker — there is a merge conflict that needs to be resolved. Could you take a look and fix it so all ? |
Contributor
Author
|
Conflicts resolved |
Contributor
|
Thank you for contributing to the project. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #587
Form data cached in AsyncStorage — names, addresses, email addresses — was stored as plain JSON, readable by any process or backup tool with access to the device's app storage. This replaces all direct AsyncStorage reads and writes in the form cache service with AES-256-GCM encryption: each storage key gets its own 256-bit symmetric key held in SecureStore (Secure Enclave on iOS, Android Keystore on Android), and encrypted blobs are persisted as
<iv_b64>.<ciphertext_b64>. Logout now wipes the user's encrypted cache entry so no residue survives a session change.Summary
src/utils/encryptedStorage.ts:encryptedSetItem/encryptedGetItem/encryptedRemoveItem— AES-256-GCM viaexpo-cryptoSubtleCrypto; per-key 256-bit key generated on first write, exported and stored in SecureStore under afce.-prefixed key derived from the storage key; 12-byte random IV prepended to each ciphertext blob; decryption failure returnsnullgracefullysrc/services/formCache.ts: replaced allAsyncStorage.{getItem,setItem,removeItem}calls with the encrypted equivalents; also removed the malformed duplicatesaveFormCachedefinition and the dangling deprecatedsafeStorageWriteimport that were left by a prior incomplete refactorsrc/store/index.ts: changed store creator to(set, get) =>sologout()can captureuserIdbefore clearing state;logout()now callsclearFormCache(getFormCacheStorageKey(userId))to wipe the encrypted cache and its SecureStore key on sign-outexpo-crypto ~14.0.1topackage.json(runnpx expo install expo-cryptoafter pulling)src/__tests__/services/formCache.test.ts: replacedjest.mock('@react-native-async-storage/async-storage')withjest.mock('../../utils/encryptedStorage')andjest.mocked()typed helpers; all existing user-scoped key assertions preservedsrc/__tests__/utils/encryptedStorage.test.ts: mocksexpo-cryptowith an XOR cipher to verify the stored AsyncStorage value is not plaintext and does not contain raw field values; asserts SecureStore key usesfce.prefix; tests graceful null on missing/malformed payload; round-trip test confirms decrypt(encrypt(x)) === xType of Change
Testing Done
Security Considerations
SENSITIVE_FIELD_KEYSguard from the original service is preserved; TTL pruning on read/write unchangedPerformance Considerations
useCallback,useMemo) used appropriately to prevent unnecessary renders? — N/A to this changeFlatListoptimized (e.g., usinggetItemLayout,keyExtractor)? — N/A to this changeuseEffectcleanup to avoid memory leaks)? — All encrypted storage functions are async;clearFormCacheinlogoutuses.catch(() => {})so a SecureStore failure never blocks sign-outexpo-cryptoadds no native binary overhead on SDK 54 (it wraps the platform WebCrypto API);expo-secure-storewas already installedChecklist
useFormCache.tsare unchanged; the service interface is identical