import { Balance, ChainWalletConfig, TokenInfo, vmTypes } from "./types"; import * as bip39 from "@scure/bip39"; import CryptoJS from "crypto-js"; import { PriceResponse } from "./price.types"; import { EntropyToMnemonic, mnemonicToSeed } from "./walletBip32"; // Abstract Base Classes export abstract class VM { protected seed: string; type: vmTypes private disposed: boolean = false; constructor(seed: string, vm: vmTypes) { this.type = vm; this.seed = seed } static mnemonicToSeed = mnemonicToSeed /** * Clear sensitive data from memory * * IMPORTANT: After calling dispose(), the VM instance should not be used. * JavaScript strings are immutable, so this only clears references. * The actual memory will be cleared by garbage collection. * * @remarks * Call this method when: * - User locks the wallet * - Application goes to background (mobile) * - Extension popup closes (browser extension) * - Session ends * * @example * ```typescript * const vm = EVMVM.fromMnemonic(mnemonic); * // ... use vm ... * vm.dispose(); * vm = null; // Remove reference for garbage collection * ``` */ dispose(): void { if (this.disposed) { return; // Already disposed } // Clear seed reference (this as any).seed = ''; this.disposed = true; } /** * Check if VM has been disposed * * @returns true if dispose() has been called */ isDisposed(): boolean { return this.disposed || !this.seed || this.seed === ''; } /** * Throw error if VM has been disposed * * @throws Error if VM is disposed * @internal */ protected checkNotDisposed(): void { if (this.isDisposed()) { throw new Error('VM has been disposed. Create a new instance to perform operations.'); } } static generateSalt(): string { return CryptoJS.lib.WordArray.random(16).toString(); // 128-bit salt } static getMnemonicFromEntropy = EntropyToMnemonic /** * Derive encryption key using PBKDF2 * * @param password - User password * @param salt - Hex salt string * @param iterations - PBKDF2 iterations (default: 600,000 - OWASP recommendation) * @param keySize - Key size in 32-bit words (default: 8 = 256 bits) * @returns Derived key as hex string * * @remarks * OWASP recommends at least 600,000 iterations for PBKDF2-SHA256. * Using fewer iterations is a security risk. * * @security * - 10,000 iterations (old default): INSECURE - deprecated * - 100,000 iterations: Minimum acceptable * - 600,000 iterations: Recommended */ static deriveKey( password: string, salt: string, iterations = 600000, // ✅ Updated to OWASP recommendation keySize = 256 / 32 ) { // Validate inputs if (!password || password.length < 8) { throw new Error('Password must be at least 8 characters'); } if (!salt) { throw new Error('Salt is required'); } // Warn about weak iteration counts if (iterations < 100000) { console.warn( `⚠️ WARNING: Using ${iterations} PBKDF2 iterations is insecure. ` + `Minimum recommended: 100,000. Recommended: 600,000.` ); } return CryptoJS.PBKDF2(password, CryptoJS.enc.Hex.parse(salt), { keySize: keySize, iterations: iterations, hasher: CryptoJS.algo.SHA256 // Explicitly specify hasher }).toString(); } /** * Encrypt seed phrase with strong encryption * * @param seedPhrase - Seed phrase to encrypt * @param password - User password (min 8 characters) * @param iterations - PBKDF2 iterations (default: 600,000) * @returns Encrypted data, salt, and iteration count * * @throws Error if inputs are invalid * * @example * ```typescript * const { encrypted, salt, iterations } = VM.encryptSeedPhrase( * mnemonic, * userPassword * ); * // Store encrypted, salt, and iterations * await storage.save({ encrypted, salt, iterations }); * ``` */ static encryptSeedPhrase(seedPhrase: string, password: string, iterations: number = 600000) { // Validate inputs if (!seedPhrase || seedPhrase.trim().length === 0) { throw new Error('Seed phrase cannot be empty'); } if (!password || password.length < 8) { throw new Error('Password must be at least 8 characters'); } const salt = this.generateSalt(); // Generate a unique salt for this encryption const key = this.deriveKey(password, salt, iterations); // Derive a key using PBKDF2 // Encrypt the seed phrase with AES using the derived key const encrypted = CryptoJS.AES.encrypt(seedPhrase, key).toString(); // Return the encrypted data, salt, and iterations (needed for decryption) return { encrypted, salt, iterations }; } /** * Legacy encryption method for backwards compatibility * * @deprecated Use encryptSeedPhrase() instead which returns iteration count */ static encryptSeedPhraseLegacy(seedPhrase: string, password: string) { const result = this.encryptSeedPhrase(seedPhrase, password, 10000); return { encrypted: result.encrypted, salt: result.salt }; } /** * Decrypt seed phrase * * @param encryptedSeedPhrase - Encrypted seed phrase * @param password - User password * @param salt - Salt used for encryption * @param iterations - PBKDF2 iterations (default: 600,000) * @returns Decrypted seed phrase or null if failed * * @remarks * If you encrypted with the old default (10,000 iterations), pass iterations=10000. * New encryptions use 600,000 iterations. * * @example * ```typescript * // Decrypt with stored iteration count * const seedPhrase = VM.decryptSeedPhrase( * encrypted, * userPassword, * salt, * storedIterations || 600000 * ); * ``` */ static decryptSeedPhrase( encryptedSeedPhrase: string, password: string, salt: string, iterations: number = 600000 ): string | null { try { // Validate inputs if (!encryptedSeedPhrase) { throw new Error('Encrypted seed phrase is required'); } if (!password || password.length < 8) { throw new Error('Password must be at least 8 characters'); } if (!salt) { throw new Error('Salt is required'); } const key = this.deriveKey(password, salt, iterations); // Derive the key using the same salt const bytes = CryptoJS.AES.decrypt(encryptedSeedPhrase, key); const seedPhrase = bytes.toString(CryptoJS.enc.Utf8); // Check if decryption was successful if (!seedPhrase || seedPhrase.trim().length === 0) { throw new Error("Decryption failed - invalid password or corrupted data"); } return seedPhrase; } catch (e: any) { // Log sanitized error (no password in logs) console.error("Decryption failed:", e.message); return null; } } /** * Legacy decryption method for backwards compatibility * * @deprecated Use decryptSeedPhrase() with explicit iterations parameter */ static decryptSeedPhraseLegacy( encryptedSeedPhrase: string, password: string, salt: string ): string | null { // Try with old default (10,000 iterations) return this.decryptSeedPhrase(encryptedSeedPhrase, password, salt, 10000); } generateSalt = VM.generateSalt deriveKey = VM.deriveKey encryptSeedPhrase = VM.encryptSeedPhrase decryptSeedPhrase = VM.decryptSeedPhrase abstract derivationPath: string abstract generatePrivateKey(index: number, mnemonic?: string, derivationPath?: string): { privateKey: PrivateKeyType, index: number }; abstract getTokenInfo(tokenAddress: AddressType, connection: ConnectionType): Promise }