UNPKG

edge-core-js

Version:

Edge account & wallet management library

415 lines (358 loc) 11.3 kB
function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } }import { asMaybe } from 'cleaners' import { base16, base64 } from 'rfc4648' import { wasCreateKeysPayload } from '../../types/server-cleaners' import { decrypt, decryptText, encrypt } from '../../util/crypto/crypto' import { hmacSha256 } from '../../util/crypto/hashes' import { utf8 } from '../../util/encoding' import { changeWalletStates } from '../account/account-files' import { waitForCurrencyWallet } from '../currency/currency-selectors' import { findCurrencyPluginId, getCurrencyTools } from '../plugins/plugins-selectors' import { getChildStash } from './login-selectors' import { asEdgeWalletInfo, wasEdgeWalletInfo } from './login-types' import { asEdgeStorageKeys, createStorageKeys, wasEdgeStorageKeys } from './storage-keys' /** * Returns the first keyInfo with a matching type. */ export function findFirstKey( keyInfos, type ) { return keyInfos.find(info => info.type === type) } export function makeAccountType(appId) { return appId === '' ? 'account-repo:co.airbitz.wallet' : `account-repo:${appId}` } /** * Assembles the key metadata structure that is encrypted within a keyBox. * @param idKey Used to derive the wallet id. It's usually `dataKey`. */ export function makeKeyInfo( type, keys, idKey ) { const hash = hmacSha256( utf8.parse(type), _nullishCoalesce(idKey, () => ( asEdgeStorageKeys(keys).dataKey)) ) return { id: base64.stringify(hash), type, keys } } /** * Assembles all the resources needed to attach new keys to the account. */ export function makeKeysKit( ai, sessionKey, keyInfos ) { // For crash errors: ai.props.log.breadcrumb('makeKeysKit', {}) const { io } = ai.props const keyBoxes = keyInfos.map(info => ({ created: new Date(), ...encrypt( io, utf8.parse(JSON.stringify(wasEdgeWalletInfo(info))), sessionKey.loginKey ) })) const newSyncKeys = [] for (const info of keyInfos) { const storageKeys = asMaybe(asEdgeStorageKeys)(info.keys) if (storageKeys == null) continue newSyncKeys.push(base16.stringify(storageKeys.syncKey).toLowerCase()) } return { loginId: sessionKey.loginId, server: wasCreateKeysPayload({ keyBoxes, newSyncKeys }), serverPath: '/v2/login/keys', stash: { keyBoxes } } } /** * Flattens an array of key structures, removing duplicates. */ export function mergeKeyInfos(keyInfos) { const out = [] const ids = new Map() // Maps ID's to output array indexes for (const keyInfo of keyInfos) { const { id, keys, type } = keyInfo if (id == null || base64.parse(id).length !== 32) { throw new Error(`Key integrity violation: invalid id ${id}`) } const index = ids.get(id) if (index != null) { // We have already seen this id, so check for conflicts: const old = out[index] if (old.type !== type) { throw new Error( `Key integrity violation for ${id}: type ${type} does not match ${old.type}` ) } for (const key of Object.keys(keys)) { if (old.keys[key] != null && old.keys[key] !== keys[key]) { throw new Error( `Key integrity violation for ${id}: ${key} keys do not match` ) } } // Do the update: out[index] = { id, keys: { ...old.keys, ...keys }, type } } else { // We haven't seen this id, so insert it: ids.set(id, out.length) out.push(keyInfo) } } return out } /** * Decrypts the private keys contained in a login. */ export function decryptKeyInfos( stash, loginKey, keyDates = new Map() ) { const { appId, keyBoxes = [] } = stash const legacyKeys = [] // BitID wallet: const { mnemonicBox, rootKeyBox } = stash if (mnemonicBox != null && rootKeyBox != null) { const rootKey = decrypt(rootKeyBox, loginKey) const infoKey = hmacSha256(rootKey, utf8.parse('infoKey')) const keys = { mnemonic: decryptText(mnemonicBox, infoKey), rootKey: base64.stringify(rootKey) } legacyKeys.push(makeKeyInfo('wallet:bitid', keys, rootKey)) } // Account settings: if (stash.syncKeyBox != null) { const syncKey = decrypt(stash.syncKeyBox, loginKey) const type = makeAccountType(appId) const keys = wasEdgeStorageKeys({ dataKey: loginKey, syncKey }) legacyKeys.push(makeKeyInfo(type, keys, loginKey)) } // Keys: const keyInfos = keyBoxes.map(box => { const keys = asEdgeWalletInfo(JSON.parse(decryptText(box, loginKey))) const created = mergeKeyDate(box.created, keyDates.get(keys.id)) if (created != null) keyDates.set(keys.id, created) return keys }) return mergeKeyInfos([...legacyKeys, ...keyInfos]).map(walletInfo => fixWalletInfo(walletInfo) ) } /** * Returns all the wallet infos accessible from this login object. */ export function decryptAllWalletInfos( stashTree, sessionKey, legacyWalletInfos, walletStates ) { // Maps from walletId's to appId's: const dates = new Map() const appIdMap = new Map() const walletInfos = [...legacyWalletInfos] // Navigate to the starting node: const stash = getChildStash(stashTree, sessionKey.loginId) // Add the legacy wallets first: for (const info of legacyWalletInfos) { walletInfos.push(info) const appIds = appIdMap.get(info.id) if (appIds != null) appIds.push(stash.appId) else appIdMap.set(info.id, [stash.appId]) } function getAllWalletInfosLoop( stash, loginKey ) { // Add our own walletInfos: const keyInfos = decryptKeyInfos(stash, loginKey, dates) for (const info of keyInfos) { walletInfos.push(info) const appIds = appIdMap.get(info.id) if (appIds != null) appIds.push(stash.appId) else appIdMap.set(info.id, [stash.appId]) } // Add our children's walletInfos: for (const child of _nullishCoalesce(stash.children, () => ( []))) { if (child.parentBox == null) continue getAllWalletInfosLoop(child, decrypt(child.parentBox, loginKey)) } } getAllWalletInfosLoop(stash, sessionKey.loginKey) return mergeKeyInfos(walletInfos).map(info => { return { appId: getLast(_nullishCoalesce(appIdMap.get(info.id), () => ( []))), appIds: _nullishCoalesce(appIdMap.get(info.id), () => ( [])), // Defaults to be overwritten: archived: false, created: dates.get(info.id), deleted: false, hidden: false, sortIndex: walletInfos.length, // Copy the `imported` field from the raw keys if it exists imported: info.keys.imported, // Actual info: ...walletStates[info.id], ...info } }) } /** * Upgrades legacy wallet info structures into the new format. * * Wallets normally have `wallet:pluginId` as their type, * but some legacy wallets also put format information into the wallet type. * This routine moves the information out of the wallet type into the keys. * * It also provides some other default values as a historical accident, * but the bitcoin plugin can just provide its own fallback values if * `format` or `coinType` are missing. Please don't make the problem worse * by adding more code here! */ export function fixWalletInfo(walletInfo) { const { id, keys, type } = walletInfo // Wallet types we need to fix: const defaults = { // BTC: 'wallet:bitcoin-bip44': { format: 'bip44', coinType: 0 }, 'wallet:bitcoin-bip49': { format: 'bip49', coinType: 0 }, // BCH: 'wallet:bitcoincash-bip32': { format: 'bip32' }, 'wallet:bitcoincash-bip44': { format: 'bip44', coinType: 145 }, // BCH testnet: 'wallet:bitcoincash-bip44-testnet': { format: 'bip44', coinType: 1 }, // BTC testnet: 'wallet:bitcoin-bip44-testnet': { format: 'bip44', coinType: 1 }, 'wallet:bitcoin-bip49-testnet': { format: 'bip49', coinType: 1 }, // DASH: 'wallet:dash-bip44': { format: 'bip44', coinType: 5 }, // DOGE: 'wallet:dogecoin-bip44': { format: 'bip44', coinType: 3 }, // LTC: 'wallet:litecoin-bip44': { format: 'bip44', coinType: 2 }, 'wallet:litecoin-bip49': { format: 'bip49', coinType: 2 }, // FTC: 'wallet:feathercoin-bip49': { format: 'bip49', coinType: 8 }, 'wallet:feathercoin-bip44': { format: 'bip44', coinType: 8 }, // QTUM: 'wallet:qtum-bip44': { format: 'bip44', coinType: 2301 }, // UFO: 'wallet:ufo-bip49': { format: 'bip49', coinType: 202 }, 'wallet:ufo-bip84': { format: 'bip84', coinType: 202 }, // XZC: 'wallet:zcoin-bip44': { format: 'bip44', coinType: 136 }, // The plugin itself could handle these lines, but they are here // as a historical accident. Please don't add more: 'wallet:bitcoin-testnet': { format: 'bip32' }, 'wallet:bitcoin': { format: 'bip32' }, 'wallet:bitcoincash-testnet': { format: 'bip32' }, 'wallet:litecoin': { format: 'bip32', coinType: 2 }, 'wallet:zcoin': { format: 'bip32', coinType: 136 } } if (defaults[type] != null) { return { id, keys: { ...defaults[type], ...keys }, type: type.replace(/-bip[0-9]+/, '') } } return walletInfo } export async function makeCurrencyWalletKeys( ai, walletType, opts ) { const { importText, keyOptions, keys } = opts // Helper function to bundle up the keys: function finalizeKeys(newKeys, imported) { if (imported != null) newKeys = { ...newKeys, imported } return fixWalletInfo( makeKeyInfo(walletType, { ...wasEdgeStorageKeys(createStorageKeys(ai)), ...newKeys }) ) } // If we have raw keys, just return those: if (keys != null) return finalizeKeys(keys) // Grab the currency tools: const pluginId = findCurrencyPluginId( ai.props.state.plugins.currency, walletType ) const tools = await getCurrencyTools(ai, pluginId) // If we have text to import, use that: if (importText != null) { if (tools.importPrivateKey == null) { throw new Error('This wallet does not support importing keys') } return finalizeKeys( await tools.importPrivateKey(importText, keyOptions), true ) } // Derive fresh keys: return finalizeKeys( await tools.createPrivateKey(walletType, keyOptions), false ) } export async function finishWalletCreation( ai, accountId, walletId, opts ) { const { enabledTokenIds, fiatCurrencyCode, migratedFromWalletId, name } = opts const wallet = await waitForCurrencyWallet(ai, walletId) // Write ancillary files to disk: if (migratedFromWalletId != null) { await changeWalletStates(ai, accountId, { [walletId]: { migratedFromWalletId } }) } if (name != null) { await wallet.renameWallet(name) } if (fiatCurrencyCode != null) { await wallet.setFiatCurrencyCode(fiatCurrencyCode) } if (enabledTokenIds != null && enabledTokenIds.length > 0) { await wallet.changeEnabledTokenIds(enabledTokenIds) } return wallet } function getLast(array) { return array[array.length - 1] } /** * Returns the earliest date, or undefined if neither date exists. */ function mergeKeyDate( a, b ) { if (a == null) return b if (b == null) return a return new Date(Math.min(a.valueOf(), b.valueOf())) }