Skip to content

fix(web): encrypt localStorage secret key with WebCrypto AES-GCM non-extractable key (Closes #347) - #353

Open
waterWang wants to merge 1 commit into
Micopay:mainfrom
waterWang:feat/sec-05-webcrypto-encrypted-storage
Open

fix(web): encrypt localStorage secret key with WebCrypto AES-GCM non-extractable key (Closes #347)#353
waterWang wants to merge 1 commit into
Micopay:mainfrom
waterWang:feat/sec-05-webcrypto-encrypted-storage

Conversation

@waterWang

Copy link
Copy Markdown

Summary

Implements Option B (non-extractable CryptoKey) for the Web/PWA build, replacing the plaintext localStorage storage of the Stellar secret key with AES-GCM encryption using a non-extractable WebCrypto key.

Changes

micopay/frontend/src/services/secureStorage.ts

  • Added webCryptoStore — a new KvStore implementation that encrypts all values with AES-GCM before writing to localStorage, and decrypts on read
  • The encryption key is a 256-bit AES-GCM key generated with extractable: false — the key material can never be exported from the WebCrypto subsystem
  • The CryptoKey is persisted in IndexedDB (structured-cloneable), surviving page reloads
  • Each write uses a random 12-byte IV prepended to the ciphertext in a single base64 string
  • Legacy migration: existing plaintext localStorage.stellar_keypair and micopay_users entries are detected and re-encrypted on first load after upgrade
  • Native (Capacitor) path unchanged — continues to use @aparajita/capacitor-secure-storage

Security Properties

Vector Before After
XSS Secret key in plaintext JS memory Key encrypted at rest; XSS can call decrypt() but cannot export the CryptoKey
Malicious extension Full localStorage read → secret key Ciphertext only; key material is non-extractable
DevTools / debugging Secret visible in Application tab Shows only base64 ciphertext
Shared device Anyone with browser access extracts the seed Cryptographically protected

Acceptance Criteria

  • secretKey never written to localStorage in plaintext
  • Web build uses SubtleCrypto non-extractable key for encryption
  • Migration: existing localStorage.stellar_keypair detected → re-encrypted on next load
  • Native path unchanged
  • Backward compatible — existing API (readJSON, writeJSON, removeKey) unchanged

Closes #347

…able key)

Replaces the plaintext webStore with a webCryptoStore that encrypts all
values before writing to localStorage using AES-GCM with a non-extractable
CryptoKey stored in IndexedDB.

Key design:
- AES-GCM 256-bit key, generated with extractable: false — key material
  never leaves the WebCrypto subsystem
- CryptoKey is persisted in IndexedDB (structured-cloneable)
- Each write uses a random 12-byte IV prepended to the ciphertext
- Legacy plaintext data (stellar_keypair, micopay_users) is detected and
  re-encrypted on first load after upgrade
- Native (Capacitor) path unchanged — uses @aparajita/capacitor-secure-storage

Closes Micopay#347
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.

[SEC-05] Web/PWA: Stellar secret key stored in plaintext localStorage

1 participant