UNPKG

ndwallet-core

Version:

Core cryptographic library for NDWallet browser environments

243 lines (242 loc) 11.4 kB
// NDWallet Core - Wallet Module import * as core from '../../pkg/ndwallet_core'; import * as user from '../user'; import * as webauthn from '../webauthn'; import * as storage from '../storage'; import * as crypto from '../crypto'; // Initialize panic hook for better error messages core.init_panic_hook(); // Constants for encryption contexts const LOCAL_SHARE_ENCRYPTION_CONTEXT = 'local_share'; const SERVER_SHARE_ENCRYPTION_CONTEXT = 'server_share'; const BACKUP_SHARE_ENCRYPTION_CONTEXT = 'backup_share'; /** * Generates a new BIP39 seed phrase * @returns {string} A new random seed phrase */ export function generateSeedPhrase() { try { return core.generate_seed_phrase(); } catch (error) { console.error('Error generating seed phrase:', error); if (error instanceof Error) { throw error; } throw new Error(`Failed to generate seed phrase: ${String(error)}`); } } /** * Creates a new wallet using the options provided * @param {string} email - User email * @param {WalletCreateOptions} options - Wallet creation options * @returns {Wallet} The created wallet */ export async function createWallet(email, options) { try { const { seedPhrase, name, accountIndex, backup } = options; if (!seedPhrase || !email || !backup) { throw new Error('Missing required options: seedPhrase, email, backup'); } // Step 1: Convert seed phrase to shares console.log('Converting seed phrase to shares...'); const shares = core.split_secret(seedPhrase, 3, 2); const localShare = shares[0]; const serverShare = shares[1]; const backupShare = shares[2]; console.log('Shares generated successfully'); // Step 2: Encrypt server and backup shares const password = backup.recoveryPassword; const salt = backup.secretAnswer && backup.secretQuestionId ? backup.secretAnswer : BACKUP_SHARE_ENCRYPTION_CONTEXT; const encryptedServerShare = await crypto.encryptData(serverShare, await crypto.derivePbkdf2EncryptionKey(password, SERVER_SHARE_ENCRYPTION_CONTEXT)); const encryptedBackupShare = await crypto.encryptData(backupShare, await crypto.derivePbkdf2EncryptionKey(password, salt)); // Step 3: Send registration request to API const regStartResponse = await user.startUserRegistration(email, encryptedServerShare, encryptedBackupShare, backup.secretQuestionId); console.log('Adding PRF extension to registration options...'); const prfSalt = crypto.generateRandomSalt(); const optionsWithPrf = webauthn.addPrfExtensionToRegistrationOptions(regStartResponse.options, prfSalt); // Step 4: Complete registration console.log('Completing registration...'); // Load browser dependencies const { startRegistration: startWebAuthnRegistration } = await webauthn.loadBrowserDependencies(); // Step 6: Start WebAuthn registration console.log('Starting WebAuthn registration...'); const registrationResponse = await startWebAuthnRegistration({ optionsJSON: optionsWithPrf }); // Step 8: Derive registration-specific encryption keys for shares const masterKey = await webauthn.deriveMasterKeyFromPrf(registrationResponse); console.log('Deriving registration-specific encryption keys for local and server shares...'); const localShareKey = await crypto.deriveHkdfEncryptionKey(masterKey, LOCAL_SHARE_ENCRYPTION_CONTEXT); const deviceServerShareKey = await crypto.deriveHkdfEncryptionKey(masterKey, SERVER_SHARE_ENCRYPTION_CONTEXT); // Step 9: Complete registration with server console.log('Completing registration with server...'); const encryptedDeviceServerShare = await crypto.encryptData(serverShare, deviceServerShareKey); const prfSaltBase64 = Buffer.from(prfSalt).toString('base64'); const regCompleteResponse = await user.completeUserRegistration(email, regStartResponse.deviceId, encryptedDeviceServerShare, registrationResponse, prfSaltBase64); // Step 10: Save auth data locally console.log('Saving auth data locally...'); storage.saveLocalData(email, storage.KEY_AUTH_TOKEN, regCompleteResponse.token); storage.saveLocalData(email, storage.KEY_CREDENTIAL, regCompleteResponse.credentialId); storage.saveLocalData(email, storage.KEY_DEVICE_SECRET, regStartResponse.deviceSecret); storage.saveLocalData(email, storage.KEY_SHARE, await crypto.encryptData(localShare, localShareKey)); // Additional logic for creating multiple wallets const networkTypes = ['bitcoin', 'ethereum', 'solana', 'tron', 'monero']; // TODO: Add other networks as needed const addressMap = new Map(); const seed = core.seed_phrase_to_seed(seedPhrase); for (const network of networkTypes) { const address = core.derive_address(seed, network, accountIndex || 0); addressMap.set(network, address); } console.log('Wallet creation completed successfully'); return { name: name || 'Wallet', addresses: addressMap, accountIndex: accountIndex || 0, storedShares: { local: localShare, server: serverShare, backup: backupShare }, _seedPhrase: seedPhrase }; } catch (error) { console.error('Wallet creation error:', error); if (error instanceof Error) { throw error; } throw new Error(`Failed to create wallet: ${String(error)}`); } } /** * Gets the address for a specific network and account index * @param {Object} wallet - The wallet object * @param {string} [network] - Optional network to get address for (defaults to wallet's network) * @param {number} [accountIndex] - Optional account index (defaults to wallet's account index) * @returns {string} The wallet address */ // export function getAddress(wallet: Wallet, network?: NetworkType, accountIndex?: number): string { // try { // // Use provided values or defaults from the wallet // const targetNetwork = network || wallet.network; // const targetAccountIndex = accountIndex !== undefined ? accountIndex : wallet.accountIndex; // // If we're requesting the same network and account index, return the cached address // if (targetNetwork === wallet.network && targetAccountIndex === wallet.accountIndex) { // return wallet.address; // } // // Otherwise, derive a new address // // In a real implementation, you would need to have access to the seed // // This is a placeholder implementation // const seedPhrase = wallet._seedPhrase; // if (!seedPhrase) { // throw new Error('Cannot derive new address: seed phrase not available'); // } // const seed = core.seed_phrase_to_seed(seedPhrase); // return core.derive_address(seed, targetNetwork, targetAccountIndex); // } catch (error) { // console.error('Error getting address:', error); // if (error instanceof Error) { // throw error; // } // throw new Error(`Failed to get address: ${String(error)}`); // } // } /** * Reconstructs and returns the seed phrase from a wallet * @param {Object} wallet - The wallet object * @param {Object} [options] - Options for retrieving the seed phrase * @param {Array<string>} [options.shares] - Shares to use for reconstruction (if not using the wallet's stored shares) * @returns {string} The retrieved seed phrase */ // export function retrieveSeedPhrase(wallet, options = {}) { // try { // If the wallet has the seed phrase cached, return it // if (wallet._seedPhrase) { // return wallet._seedPhrase; // } // // If shares are provided, use them // if (options.shares && Array.isArray(options.shares) && options.shares.length >= 2) { // return core.combine_shares(options.shares); // } // // Otherwise, try to use the wallet's stored shares // const shares = []; // // Get local share if available // if (wallet.storedShares.local) { // const localShare = wallet.storedShares.local; // const iv = new Uint8Array(localShare.iv); // const data = new Uint8Array(localShare.data); // const encryptionKey = new Uint8Array(localShare.encryptionKey); // const encryptedData = new core.EncryptedData(iv, data); // const decryptedShare = core.decrypt_data(encryptedData, encryptionKey); // shares.push(decryptedShare); // } // // Get server share if available // if (wallet.storedShares.server) { // shares.push(wallet.storedShares.server); // } // // Get backup share if available // if (wallet.storedShares.backup) { // shares.push(wallet.storedShares.backup); // } // if (shares.length < 2) { // throw new Error('Not enough shares available to retrieve the seed phrase'); // } // Combine shares to recover the seed phrase // return core.combine_shares(shares); // return null; // } catch (error) { // console.error('Error retrieving seed phrase:', error); // if (error instanceof Error) { // throw error; // } // throw new Error(`Failed to retrieve seed phrase: ${String(error)}`); // } // } /** * Helper function to derive an address from a seed * @param {Uint8Array} seed - The seed bytes * @param {string} network - The network * @param {number} accountIndex - The account index * @returns {string} The derived address */ // function deriveAddress(seed, network, accountIndex) { // try { // // Use the native address derivation function from the WASM module // return core.derive_address(seed, network, accountIndex); // } catch (error) { // console.error(`Error deriving ${network} address:`, error); // // Fallback to simplified implementation if the native function fails // // This could happen if the network is not supported by the native implementation // const seedHex = Array.from(seed) // .map(b => b.toString(16).padStart(2, '0')) // .join(''); // // Create a deterministic but simplified address based on the inputs // const networkLower = network.toLowerCase(); // // Check if this is an EVM-compatible chain (Ethereum, Polygon, Binance, etc.) // const isEvmChain = ['ethereum', 'polygon', 'binance'].includes(networkLower); // // Set the appropriate prefix based on the network // let networkPrefix; // if (isEvmChain) { // networkPrefix = '0x'; // All EVM chains use the same prefix // } else { // networkPrefix = { // 'bitcoin': 'bc1', // 'litecoin': 'ltc1', // 'solana': 'So', // 'monero': '4', // 'zcash': 't1', // 'xrp': 'r', // 'avalanche': 'X-', // 'cardano': 'addr1', // 'polkadot': '1' // }[networkLower] || '0x'; // } // // Use first 40 chars of seed hex for Ethereum-like addresses, or 32 for others // const addressLength = isEvmChain ? 40 : 32; // // Create a deterministic but simplified address based on the inputs // const addressPart = seedHex.slice(accountIndex * 2, accountIndex * 2 + addressLength); // return `${networkPrefix}${addressPart}`; // } // return ''; // }