Skip to content

feat(security): encrypt AsyncStorage form cache entries with AES-256-GCM (#587)#719

Merged
RUKAYAT-CODER merged 2 commits into
rinafcode:mainfrom
Vox-d-glitch:security/encrypted-form-cache
Jun 28, 2026
Merged

feat(security): encrypt AsyncStorage form cache entries with AES-256-GCM (#587)#719
RUKAYAT-CODER merged 2 commits into
rinafcode:mainfrom
Vox-d-glitch:security/encrypted-form-cache

Conversation

@Vox-d-glitch

Copy link
Copy Markdown
Contributor

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

  • Created src/utils/encryptedStorage.ts: encryptedSetItem / encryptedGetItem / encryptedRemoveItem — AES-256-GCM via expo-crypto SubtleCrypto; per-key 256-bit key generated on first write, exported and stored in SecureStore under a fce.-prefixed key derived from the storage key; 12-byte random IV prepended to each ciphertext blob; decryption failure returns null gracefully
  • Rewrote src/services/formCache.ts: replaced all AsyncStorage.{getItem,setItem,removeItem} calls with the encrypted equivalents; also removed the malformed duplicate saveFormCache definition and the dangling deprecated safeStorageWrite import that were left by a prior incomplete refactor
  • Updated src/store/index.ts: changed store creator to (set, get) => so logout() can capture userId before clearing state; logout() now calls clearFormCache(getFormCacheStorageKey(userId)) to wipe the encrypted cache and its SecureStore key on sign-out
  • Added expo-crypto ~14.0.1 to package.json (run npx expo install expo-crypto after pulling)
  • Updated src/__tests__/services/formCache.test.ts: replaced jest.mock('@react-native-async-storage/async-storage') with jest.mock('../../utils/encryptedStorage') and jest.mocked() typed helpers; all existing user-scoped key assertions preserved
  • Created src/__tests__/utils/encryptedStorage.test.ts: mocks expo-crypto with an XOR cipher to verify the stored AsyncStorage value is not plaintext and does not contain raw field values; asserts SecureStore key uses fce. prefix; tests graceful null on missing/malformed payload; round-trip test confirms decrypt(encrypt(x)) === x

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Chore / Refactor (no functional changes)

Testing Done

  • Unit Tests
  • Integration Tests
  • Manual Verification (e.g., iOS/Android UI checks)

Security Considerations

  • Does this store user data securely (e.g., avoiding plain AsyncStorage for sensitive data)? — Form cache is now AES-256-GCM encrypted; key material lives in SecureStore, never in AsyncStorage
  • Is token handling secure (no token exposure in logs or UI)? — No tokens involved; decryption errors are silently swallowed without logging raw ciphertext
  • Are all user inputs validated? — SENSITIVE_FIELD_KEYS guard from the original service is preserved; TTL pruning on read/write unchanged
  • Is deep link handling safe from malicious payloads? — N/A to this change

Performance Considerations

  • Are React hooks (useCallback, useMemo) used appropriately to prevent unnecessary renders? — N/A to this change
  • Is FlatList optimized (e.g., using getItemLayout, keyExtractor)? — N/A to this change
  • Are asynchronous patterns handled correctly (e.g., useEffect cleanup to avoid memory leaks)? — All encrypted storage functions are async; clearFormCache in logout uses .catch(() => {}) so a SecureStore failure never blocks sign-out
  • Have bundle size impacts been considered? — expo-crypto adds no native binary overhead on SDK 54 (it wraps the platform WebCrypto API); expo-secure-store was already installed

Checklist

  • I have read the CONTRIBUTING guide.
  • My code follows the style guidelines of this project.
  • I have updated the documentation accordingly. — N/A; encryption is an internal implementation detail with no public API surface
  • Are there architectural changes? If so, is there an Architectural Decision Record (ADR)? — No architectural changes; all call sites in useFormCache.ts are unchanged; the service interface is identical

…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.
@drips-wave

drips-wave Bot commented Jun 27, 2026

Copy link
Copy Markdown

@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! 🚀

Learn more about application limits

@RUKAYAT-CODER

Copy link
Copy Markdown
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 ?

@Vox-d-glitch

Copy link
Copy Markdown
Contributor Author

Conflicts resolved

@RUKAYAT-CODER

Copy link
Copy Markdown
Contributor

Thank you for contributing to the project.

@RUKAYAT-CODER RUKAYAT-CODER merged commit a499025 into rinafcode:main Jun 28, 2026
2 of 14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Security] Form autocomplete cache stores user PII in unencrypted AsyncStorage

2 participants