Skip to content

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

Description

@grantfox-oss

Problem

Web/PWA build stores the full Stellar secret key (S...) in plaintext localStorage under key stellar_keypair.

Root Cause

micopay/frontend/src/services/secureStorage.ts selects storage backend at runtime:

  • Native (Capacitor): @aparajita/capacitor-secure-storage → Android Keystore (encrypted)
  • Web/PWA: custom webStore wrapper → window.localStorage.setItem('stellar_keypair', JSON.stringify({ publicKey, secretKey })) PLAINTEXT

Impact

Vector Risk
XSS Any XSS in the SPA → localStorage.stellar_keypair → full wallet control
Malicious extension Browser extensions with host permissions read all localStorage
DevTools / debugging Secret visible in Application tab, crash reports, HAR files
Shared device Anyone with browser access extracts the seed

Severity: HIGH — Single XSS = total account compromise (all funds).

Evidence

// micopay/frontend/src/services/secureStorage.ts:13-23
const webStore: KvStore = {
  async get(key: string) {
    const v = localStorage.getItem(key);
    return v ? JSON.parse(v) : null;
  },
  async set(key: string, value: unknown) {
    localStorage.setItem(key, JSON.stringify(value)); // ← secretKey stored in plaintext
  },
  // ...
};
// micopay/frontend/src/services/secureStorage.ts:27-30
async function getStore(): Promise<KvStore> {
  if (!Capacitor.isNativePlatform()) return webStore; // ← Web/PWA gets plaintext store
  // ...
}

Solution Options

Option Approach Effort Security
A WebCrypto SubtleCrypto.encrypt() with key derived from user passphrase (PBKDF2) Medium 🟢 Encrypted at rest
B WebCrypto generateKey({ extractable: false }) — non-extractable key in CryptoKey object, only usable via sign()/unwrapKey() Medium 🟢 Key never leaves browser crypto subsystem
C indexedDB + CryptoKey storage (same as B, persistent) Medium 🟢 Persistent, non-extractable
D Deprecate web secret storage — require native app for custodial features Low 🟢 Eliminates web risk

Recommended: Option B — Non-extractable CryptoKey for signing operations, secret never in JS memory as string.

Acceptance Criteria

  • secretKey never written to localStorage / indexedDB / any persistent storage in plaintext
  • Web build uses SubtleCrypto non-extractable key for all signing operations
  • Secret export (backup) only via user-initiated flow: QR code / encrypted file / password manager — never clipboard
  • Migration: existing localStorage.stellar_keypair detected → re-encrypted or purged on next load
  • Tests: XSS simulation cannot extract usable secret

Files to Modify

  • micopay/frontend/src/services/secureStorage.ts (new webCryptoStore)
  • micopay/frontend/src/lib/keystore.ts (use crypto store for signing)
  • micopay/frontend/src/pages/Profile.tsx (remove clipboard export, add secure backup)
  • New test: micopay/frontend/src/services/secureStorage.test.ts

References

  • SEC-05 security report (full analysis)
  • SEC-22 localstorage secure storage (related)
  • SEC-25 secret key clipboard (related — web clipboard also affected)

Labels

security, complexity: high, GrantFox OSS, frontend, web, pwa, crypto, stellar

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions