UNPKG

edge-core-js

Version:

Edge account & wallet management library

508 lines (437 loc) 16.2 kB
function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }import { bridgifyObject, onMethod, watchMethod } from 'yaob' import { checkPasswordRules, fixUsername } from '../../client-side' import { asChallengeErrorPayload, asMaybePasswordError, asMaybePinDisabledError, PasswordError, PinDisabledError } from '../../types/types' import { verifyData } from '../../util/crypto/verify' import { base58 } from '../../util/encoding' import { makeAccount } from '../account/account-init' import { createLogin, usernameAvailable } from '../login/create' import { requestEdgeLogin } from '../login/edge' import { decryptChildKey, makeAuthJson, searchTree, syncLogin } from '../login/login' import { loginFetch } from '../login/login-fetch' import { fetchLoginMessages } from '../login/login-messages' import { getEmptyStash, getStashById, getStashByUsername } from '../login/login-selectors' import { removeStash, saveStash } from '../login/login-stash' import { resetOtp } from '../login/otp' import { loginPassword } from '../login/password' import { findPin2Stash, loginPin2 } from '../login/pin2' import { getQuestions2, loginRecovery2 } from '../login/recovery2' import { CLIENT_FILE_NAME, clientFile } from './client-file' import { EdgeInternalStuff } from './internal-api' export function makeContextApi(ai) { const appId = ai.props.state.login.contextAppId const clientId = base58.stringify(ai.props.state.clientInfo.clientId) const $internalStuff = new EdgeInternalStuff(ai) let pauseTimer async function disableDuressMode() { // Persist disabled duress mode await clientFile.save(ai.props.io.disklet, CLIENT_FILE_NAME, { ...ai.props.state.clientInfo, duressEnabled: false }) // Disable duress mode ai.props.dispatch({ type: 'LOGIN_DURESS_MODE_DISABLED' }) } async function enableDuressMode() { // Persist enabled duress mode await clientFile.save(ai.props.io.disklet, CLIENT_FILE_NAME, { ...ai.props.state.clientInfo, duressEnabled: true }) // Enable duress mode ai.props.dispatch({ type: 'LOGIN_DURESS_MODE_ENABLED' }) } async function updateLoginWaitTimestamp( loginId, timestamp ) { await clientFile.save(ai.props.io.disklet, CLIENT_FILE_NAME, { ...ai.props.state.clientInfo, loginWaitTimestamps: { ...ai.props.state.clientInfo.loginWaitTimestamps, [loginId]: timestamp } }) ai.props.dispatch({ type: 'LOGIN_WAIT_TIMESTAMP_UPDATED', payload: { loginId, timestamp } }) } const out = { on: onMethod, watch: watchMethod, appId, clientId, async close() { ai.props.close() }, $internalStuff, fixUsername, get localUsers() { return ai.props.state.login.localUsers }, async forgetAccount(rootLoginId) { const loginId = base58.parse(rootLoginId) // Safety check: for (const accountId of ai.props.state.accountIds) { const accountState = ai.props.state.accounts[accountId] if (verifyData(accountState.stashTree.loginId, loginId)) { throw new Error('Cannot remove logged-in user') } } await removeStash(ai, loginId) }, async fetchChallenge() { const response = await loginFetch(ai, 'POST', '/v2/captcha/create', {}) const { challengeId, challengeUri } = asChallengeErrorPayload(response) return { challengeId, challengeUri } }, async usernameAvailable(username, opts = {}) { const { challengeId } = opts username = fixUsername(username) return await usernameAvailable(ai, username, challengeId) }, async createAccount( opts ) { // For crash errors: ai.props.log.breadcrumb('EdgeContext.createAccount', {}) if (opts.username != null) { opts.username = fixUsername(opts.username) } const sessionKey = await createLogin(ai, opts, opts) return await makeAccount(ai, sessionKey, 'newAccount', opts) }, async loginWithKey( usernameOrLoginId, loginKey, opts = {} ) { const { now = new Date(), useLoginId = false } = opts const inDuressMode = ai.props.state.clientInfo.duressEnabled const stashTree = useLoginId ? getStashById(ai, base58.parse(usernameOrLoginId)).stashTree : getStashByUsername(ai, fixUsername(usernameOrLoginId)) if (stashTree == null) { throw new Error('User does not exist on this device') } const appStash = searchTree(stashTree, stash => stash.appId === appId) if (appStash == null) { throw new Error(`Cannot find requested appId: "${appId}"`) } // Get the duress stash for existence check: const duressAppId = appId + '.duress' const duressStash = searchTree( stashTree, stash => stash.appId === duressAppId ) let sessionKey try { sessionKey = { loginId: appStash.loginId, loginKey: base58.parse(loginKey) } // Verify that the provided key works for decryption: makeAuthJson(stashTree, sessionKey) } catch (error) { if (error instanceof Error && error.message === 'Invalid checksum') { if (duressStash == null) { throw error } sessionKey = { loginId: duressStash.loginId, loginKey: base58.parse(loginKey) } // Verify that the provided key works for decryption: makeAuthJson(stashTree, sessionKey) } else { throw error } } // Save the date: stashTree.lastLogin = now saveStash(ai, stashTree).catch(() => {}) // Since we logged in offline, update the stash in the background: syncLogin(ai, sessionKey).catch(error => ai.props.onError(error)) return await makeAccount(ai, sessionKey, 'keyLogin', { ...opts, // We must require that the duress account is active. // Duress account is active if it exists and has a PIN key: duressMode: inDuressMode && _optionalChain([duressStash, 'optionalAccess', _ => _.pin2Key]) != null }) }, async loginWithPassword( username, password, opts = {} ) { // For crash errors: ai.props.log.breadcrumb('EdgeContext.loginWithPassword', {}) username = fixUsername(username) // If we don't have a stash for this username, // then this must be a first-time login on this device: const stash = _nullishCoalesce(getStashByUsername(ai, username), () => ( getEmptyStash(username))) const sessionKey = await loginPassword(ai, stash, password, opts) // Attempt to log into duress account if duress mode is enabled: if (ai.props.state.clientInfo.duressEnabled) { const duressAppId = appId + '.duress' const stash = getStashByUsername(ai, username) if (stash == null) { // This should never happen. throw new Error('Missing stash after login with password') } const duressStash = searchTree( stash, stash => stash.appId === duressAppId ) // We may still be in duress mode but do not log-in to a duress account // if it does not exist. It's important that we do not disable duress // mode from this routine to make sure other accounts with duress mode // still are protected. // Duress account is active if it exists and has a PIN key: if (_optionalChain([duressStash, 'optionalAccess', _2 => _2.pin2Key]) != null) { const duressSessionKey = decryptChildKey( stash, sessionKey, duressStash.loginId ) return await makeAccount(ai, duressSessionKey, 'passwordLogin', { ...opts, duressMode: true }) } } return await makeAccount(ai, sessionKey, 'passwordLogin', opts) }, checkPasswordRules, async loginWithPIN( usernameOrLoginId, pin, opts = {} ) { // For crash errors: ai.props.log.breadcrumb('EdgeContext.loginWithPIN', {}) const { useLoginId = false } = opts const stashTree = useLoginId ? getStashById(ai, base58.parse(usernameOrLoginId)).stashTree : getStashByUsername(ai, fixUsername(usernameOrLoginId)) if (stashTree == null) { throw new Error('User does not exist on this device') } const mainStash = findPin2Stash(stashTree, appId) const duressAppId = appId + '.duress' const duressStash = searchTree( stashTree, stash => stash.appId === duressAppId ) async function loginMainAccount( stashTree, mainStash ) { const sessionKey = await loginPin2(ai, stashTree, mainStash, pin, opts) // Make the account for the main account return await makeAccount(ai, sessionKey, 'pinLogin', opts) } async function loginDuressAccount( stashTree, duressStash ) { if (duressStash.fakePinDisabled === true) { throw new PinDisabledError( 'PIN login is not enabled for this account on this device' ) } // Try login with duress account const sessionKey = await loginPin2( ai, stashTree, duressStash, pin, opts ) // Make the account with duress mode enabled return await makeAccount(ai, sessionKey, 'pinLogin', { ...opts, duressMode: true }) } if (mainStash == null) { if (duressStash == null) { throw new PinDisabledError( 'PIN login is not enabled for this account on this device' ) } // Just try PIN-login on duress account since PIN-login is not enabled // on the main account: return await loginDuressAccount(stashTree, duressStash) } // No duress account configured, so just login to the main account: if (_optionalChain([duressStash, 'optionalAccess', _3 => _3.pin2Key]) == null) { // It's important that we don't disable duress mode here because // we want to protect account that have duress mode enabled and only // allow those accounts to suspend duress mode. return await loginMainAccount(stashTree, mainStash) } // Check if we are in duress mode: const inDuressMode = ai.props.state.clientInfo.duressEnabled // Check if we are in a wait period for account as a whole: const mainLoginId = base58.stringify(mainStash.loginId) const loginWaitTimestamp = ai.props.state.clientInfo.loginWaitTimestamps[mainLoginId] if (loginWaitTimestamp != null && loginWaitTimestamp > Date.now()) { throw new PasswordError({ wait_seconds: Math.ceil((loginWaitTimestamp - Date.now()) / 1000) }) } // Try pin-login on either the duress or main accounts, smartly: try { return inDuressMode ? await loginDuressAccount(stashTree, duressStash) : await loginMainAccount(stashTree, mainStash) } catch (originalError) { // If the error is not a failed login, rethrow it: if (asMaybePasswordError(originalError) == null) { throw originalError } try { const account = inDuressMode ? await loginMainAccount(stashTree, mainStash) : await loginDuressAccount(stashTree, duressStash) // Only Enable/Disable duress mode if account creation was success. if (inDuressMode) { await disableDuressMode() } else { await enableDuressMode() } return account } catch (error) { /** * We need to store the max wait time for the account as a whole (or * both main and duress accounts) because we don't know which account * the user will try to login. We will block the login on this stored * timestamp. */ const maxWaitError = [ asMaybePasswordError(error), asMaybePasswordError(originalError) ].reduce((a, b) => { const aWait = _nullishCoalesce(_optionalChain([a, 'optionalAccess', _4 => _4.wait]), () => ( 0)) const bWait = _nullishCoalesce(_optionalChain([b, 'optionalAccess', _5 => _5.wait]), () => ( 0)) if (aWait > bWait) return a return b }) // Convert wait time to milliseconds: const maxWaitMilliseconds = (_nullishCoalesce(_optionalChain([maxWaitError, 'optionalAccess', _6 => _6.wait]), () => ( 0))) * 1000 if (maxWaitError != null && maxWaitMilliseconds > 0) { const timestamp = Date.now() + maxWaitMilliseconds await updateLoginWaitTimestamp(mainLoginId, timestamp) throw maxWaitError } // Throw the original error if pin-login is disabled: if (asMaybePinDisabledError(error) != null) { throw originalError } throw error } } }, async loginWithRecovery2( recovery2Key, username, answers, opts = {} ) { // For crash errors: ai.props.log.breadcrumb('EdgeContext.loginWithRecovery2', {}) username = fixUsername(username) const stashTree = getStashByUsername(ai, username) const sessionKey = await loginRecovery2( ai, _nullishCoalesce(stashTree, () => ( getEmptyStash(username))), base58.parse(recovery2Key), answers, opts ) return await makeAccount(ai, sessionKey, 'recoveryLogin', opts) }, async fetchRecovery2Questions( recovery2Key, username ) { username = fixUsername(username) return await getQuestions2(ai, base58.parse(recovery2Key), username) }, async requestEdgeLogin( opts ) { // For crash errors: ai.props.log.breadcrumb('EdgeContext.requestEdgeLogin', {}) return await requestEdgeLogin(ai, appId, opts) }, async requestOtpReset( username, otpResetToken ) { username = fixUsername(username) return await resetOtp(ai, username, otpResetToken) }, async fetchLoginMessages() { return await fetchLoginMessages(ai) }, get paused() { return ai.props.state.paused }, async changePaused( paused, opts = {} ) { const { secondsDelay = 0 } = opts // If a timer is already running, stop that: if (pauseTimer != null) { clearTimeout(pauseTimer) pauseTimer = undefined } // If the state is the same, do nothing: if (ai.props.state.paused === paused) return // Otherwise, make the change: if (secondsDelay === 0) { ai.props.dispatch({ type: 'PAUSE', payload: paused }) } else { pauseTimer = setTimeout(() => { pauseTimer = undefined ai.props.dispatch({ type: 'PAUSE', payload: paused }) }, secondsDelay * 1000) } }, get logSettings() { return ai.props.state.logSettings }, async changeLogSettings(settings) { const newSettings = { ...ai.props.state.logSettings, ...settings } ai.props.dispatch({ type: 'CHANGE_LOG_SETTINGS', payload: newSettings }) } } bridgifyObject(out) return out }