Skip to content

[MEDIUM] Storage encryption key stored as raw Base64 in localStorage — XSS exposes all conversation keys #67

@eltociear

Description

@eltociear

Summary

src/lib/crypto/key-manager.js stores the AES-256-GCM storage encryption key as raw Base64 in localStorage. While conversation keys are now encrypted (fix from b769853), the encryption key itself is unprotected.

Impact

Any XSS vulnerability allows an attacker to:

  1. Read qryptchat_storage_enc_key from localStorage
  2. Base64-decode it to get the raw AES-256-GCM key
  3. Decrypt all conversation keys stored in localStorage
  4. Read all past and future messages

This is the "bootstrap paradox" of client-side encryption — the key that protects the keys is itself unprotected.

Current Code (key-manager.js ~line 40)

localStorage.setItem(this.storageEncryptionKeyName, Base64.encode(new Uint8Array(exported)));

Recommendation

Use Web Crypto API with extractable: false and store in IndexedDB:

// Generate non-extractable key
this._storageEncKey = await crypto.subtle.generateKey(
    { name: 'AES-GCM', length: 256 }, 
    false,  // non-extractable — cannot be read by JS
    ['encrypt', 'decrypt']
);
// Store handle in IndexedDB (key material never leaves Web Crypto)
await indexedDBManager.storeKey('storage_enc_key', this._storageEncKey);

This way, even if XSS occurs, the attacker cannot extract the raw key bytes. Web Crypto operations work with the key handle but never expose the underlying material.

Severity

Medium — requires XSS as prerequisite, but fully compromises E2E encryption guarantees.

Filed by: eltociear (AI security auditor via ugig.net)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions