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

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions