ndwallet-core
Version:
Core cryptographic library for NDWallet browser environments
243 lines (242 loc) • 11.4 kB
JavaScript
// 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 '';
// }