Skip to content

Issue #5: Implement Secure Storage Manager for Browser Extension #20

@wheval

Description

@wheval

Implement Secure Storage Manager for Browser Extension

Description:
Create a secure storage manager that wraps chrome.storage / browser.storage APIs with automatic encryption for sensitive data (private keys, account state). Must work seamlessly across Chrome and Firefox with transparent encryption/decryption.

Context:
Browser extensions use chrome.storage.local for persistent data storage. However, this storage is NOT encrypted by default - any data stored is accessible to the extension and potentially to malware or other extensions with storage permissions. We need a wrapper that:

  1. Automatically encrypts sensitive data before storage
  2. Derives encryption keys from user passwords (never stores keys)
  3. Works across Chrome (chrome.storage) and Firefox (browser.storage)
  4. Handles session timeouts and re-authentication
  5. Provides a clean API for account and session key storage

Important Security Principle: Encryption keys must NEVER be stored. They should be derived from the user's password using PBKDF2 and kept in memory only during the active session.

Requirements:

  • Create SecureStorageManager class with cross-browser support (chrome.storage / browser.storage)
  • Implement Web Crypto API encryption (PBKDF2 + AES-GCM)
  • Derive encryption keys from user password (100k iterations, never store keys)
  • Generate unique IV for each encryption operation
  • Implement saveAccount() with automatic private key encryption
  • Implement getAccount() with automatic decryption
  • Session key storage (encrypted)
  • Session timeout and auto-lock functionality
  • Clear sensitive data from memory after use
  • Storage quota monitoring and error handling
  • Export/import for backup (encrypted format)
  • Unit tests with mocked chrome.storage API (>85% coverage)
  • Browser compatibility tests (Chrome + Firefox)

Implementation Guide:

// 1. Cross-browser storage wrapper
const storage = typeof browser !== 'undefined' 
  ? browser.storage 
  : chrome.storage;

export class SecureStorageManager {
  private encryptionKey: CryptoKey | null = null;
  private salt: Uint8Array | null = null;
  private sessionTimeout: number = 15 * 60 * 1000; // 15 minutes
  private lastActivity: number = Date.now();
  
  constructor() {
    // Set up auto-lock on inactivity
    setInterval(() => this.checkSessionTimeout(), 60000);
  }

  // 2. Derive encryption key from password (NEVER store this key!)
  private async deriveKey(password: string, salt: Uint8Array): Promise<CryptoKey> {
    const encoder = new TextEncoder();
    const keyMaterial = await crypto.subtle.importKey(
      'raw',
      encoder.encode(password),
      'PBKDF2',
      false,
      ['deriveBits', 'deriveKey']
    );

    return crypto.subtle.deriveKey(
      {
        name: 'PBKDF2',
        salt: salt,
        iterations: 100_000, // 100k iterations
        hash: 'SHA-256'
      },
      keyMaterial,
      { name: 'AES-GCM', length: 256 },
      false,
      ['encrypt', 'decrypt']
    );
  }

  // 3. Encrypt data with AES-GCM
  private async encryptData(data: any): Promise<{
    encrypted: number[];
    iv: number[];
  }> {
    if (!this.encryptionKey) {
      throw new Error('Storage locked. Please unlock first.');
    }

    const encoder = new TextEncoder();
    const iv = crypto.getRandomValues(new Uint8Array(12)); // Unique IV
    
    const encrypted = await crypto.subtle.encrypt(
      { name: 'AES-GCM', iv: iv },
      this.encryptionKey,
      encoder.encode(JSON.stringify(data))
    );

    return {
      encrypted: Array.from(new Uint8Array(encrypted)),
      iv: Array.from(iv)
    };
  }

  // 4. Decrypt data
  private async decryptData(
    encryptedData: number[],
    iv: number[]
  ): Promise<any> {
    if (!this.encryptionKey) {
      throw new Error('Storage locked. Please unlock first.');
    }

    const decrypted = await crypto.subtle.decrypt(
      { name: 'AES-GCM', iv: new Uint8Array(iv) },
      this.encryptionKey,
      new Uint8Array(encryptedData)
    );

    const decoder = new TextDecoder();
    return JSON.parse(decoder.decode(decrypted));
  }

  // 5. Unlock storage with password
  async unlock(password: string): Promise<boolean> {
    try {
      // Get or generate salt
      const stored = await storage.local.get('salt');
      
      if (!stored.salt) {
        // First time - generate salt
        this.salt = crypto.getRandomValues(new Uint8Array(16));
        await storage.local.set({ salt: Array.from(this.salt) });
      } else {
        this.salt = new Uint8Array(stored.salt);
      }

      // Derive key from password
      this.encryptionKey = await this.deriveKey(password, this.salt);
      this.lastActivity = Date.now();
      
      // Verify password by trying to decrypt existing data
      const test = await storage.local.get('accountData');
      if (test.accountData) {
        await this.decryptData(
          test.accountData.encrypted,
          test.accountData.iv
        );
      }
      
      return true;
    } catch (error) {
      this.encryptionKey = null;
      return false;
    }
  }

  // 6. Lock storage (clear key from memory)
  lock(): void {
    this.encryptionKey = null;
    this.salt = null;
  }

  // 7. Save account with encrypted secret key
  async saveAccount(account: {
    publicKey: string;
    secretKey: string; // Will be encrypted
    contractId: string;
    nonce: number;
  }): Promise<void> {
    this.updateActivity();
    const encrypted = await this.encryptData(account);
    await storage.local.set({ accountData: encrypted });
  }

  // 8. Get account (auto-decrypt)
  async getAccount(): Promise<any> {
    this.updateActivity();
    const result = await storage.local.get('accountData');
    if (!result.accountData) return null;
    
    return await this.decryptData(
      result.accountData.encrypted,
      result.accountData.iv
    );
  }

  // 9. Save session keys (encrypted)
  async saveSessionKeys(keys: any[]): Promise<void> {
    this.updateActivity();
    const encrypted = await this.encryptData(keys);
    await storage.local.set({ sessionKeys: encrypted });
  }

  // 10. Get session keys (auto-decrypt)
  async getSessionKeys(): Promise<any[]> {
    this.updateActivity();
    const result = await storage.local.get('sessionKeys');
    if (!result.sessionKeys) return [];
    
    return await this.decryptData(
      result.sessionKeys.encrypted,
      result.sessionKeys.iv
    );
  }

  // 11. Auto-lock on inactivity
  private checkSessionTimeout(): void {
    if (this.encryptionKey && Date.now() - this.lastActivity > this.sessionTimeout) {
      this.lock();
      console.log('Storage auto-locked due to inactivity');
    }
  }

  private updateActivity(): void {
    this.lastActivity = Date.now();
  }

  // 12. Clear all data
  async clear(): Promise<void> {
    await storage.local.clear();
    this.lock();
  }

  // 13. Export encrypted backup
  async export(): Promise<string> {
    const data = await storage.local.get(null); // Get all data
    return JSON.stringify(data);
  }

  // 14. Import encrypted backup
  async import(backup: string): Promise<void> {
    const data = JSON.parse(backup);
    await storage.local.set(data);
  }
}

Usage:

import { SecureStorageManager } from '@ancore/core-sdk';

const storage = new SecureStorageManager();

// Unlock with user password
const unlocked = await storage.unlock('userPassword123!');
if (!unlocked) {
  throw new Error('Invalid password');
}

// Save account (secret key encrypted automatically)
await storage.saveAccount({
  publicKey: keypair.publicKey(),
  secretKey: keypair.secret(), // Will be encrypted
  contractId: 'CABC...',
  nonce: 0
});

// Get account (auto-decrypts)
const account = await storage.getAccount();
console.log(account.publicKey); // Decrypted

// Lock storage (clears key from memory)
storage.lock();

// Later, unlock again
await storage.unlock('userPassword123!');

Files to Create:

  • packages/core-sdk/src/storage/secure-storage-manager.ts (main class)
  • packages/core-sdk/src/storage/types.ts (EncryptedData type)
  • packages/core-sdk/src/storage/__tests__/manager.test.ts (unit tests)
  • packages/core-sdk/src/storage/__tests__/chrome-compat.test.ts (browser tests)
  • apps/extension-wallet/src/background/storage.ts (extension integration)

Dependencies:

  • Web Crypto API (built-in browser API, no external deps)
  • @types/chrome (TypeScript types for chrome.storage)
  • webextension-polyfill (optional, for Firefox compatibility)

Note: This implementation uses Web Crypto API (built-in) instead of external crypto libraries to reduce bundle size and leverage browser-native security features.

Success Criteria:

  • Works in both Chrome and Firefox without polyfills
  • Encryption keys NEVER stored (only derived from password)
  • Session timeout auto-locks storage after 15 minutes of inactivity
  • Handles wrong password gracefully (returns false, doesn't crash)
  • Handles storage quota exceeded errors
  • No sensitive data remains in memory after lock()
  • Export/import preserves encrypted format

Definition of Done:

  • All requirements implemented with Web Crypto API
  • Tested in Chrome 90+ and Firefox 90+
  • Encryption verified: PBKDF2 (100k iterations) + AES-256-GCM
  • Session timeout works correctly
  • Unit tests with mocked chrome.storage (>85% coverage)
  • Manual browser testing completed
  • Security audit checklist passed (no key storage, unique IVs, proper salt)

Security Checklist:

  • Encryption key never stored (only in memory during session)
  • Unique IV generated for each encryption
  • Salt generated on first use and persisted (non-sensitive)
  • PBKDF2 with 100,000 iterations
  • AES-256-GCM for authenticated encryption
  • Auto-lock after 15 minutes of inactivity
  • Memory cleared on lock() call

Additional Resources:

Labels: storage, sdk, security, extension
Complexity: 200 points (High)
Estimated Effort: 3-4 days
Priority: High


Questions or need help? Join our Telegram community

Metadata

Metadata

Assignees

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions