UNPKG

edge-core-js

Version:

Edge account & wallet management library

195 lines (173 loc) 4.55 kB
import { asArray, asString, uncleaner } from 'cleaners' import { asRecovery2InfoPayload, wasChangeRecovery2IdPayload, wasChangeRecovery2Payload } from '../../types/server-cleaners' import { decrypt, decryptText, encrypt } from '../../util/crypto/crypto' import { hmacSha256 } from '../../util/crypto/hashes' import { utf8 } from '../../util/encoding' import { applyKit, serverLogin } from './login' import { loginFetch } from './login-fetch' function makeRecovery2Id( recovery2Key, username ) { return hmacSha256(utf8.parse(username), recovery2Key) } function makeRecovery2Auth( recovery2Key, answers ) { return answers.map(answer => { return hmacSha256(utf8.parse(answer), recovery2Key) }) } /** * Logs a user in using recovery answers. * @return A `Promise` for the new root login. */ export async function loginRecovery2( ai, stashTree, recovery2Key, answers, opts ) { const { username } = stashTree if (username == null) throw new Error('Recovery login requires a username') // Request: const request = { recovery2Id: makeRecovery2Id(recovery2Key, username), recovery2Auth: makeRecovery2Auth(recovery2Key, answers) } return await serverLogin( ai, stashTree, stashTree, opts, request, async reply => { if (reply.recovery2Box == null || reply.recovery2Box === true) { throw new Error('Missing data for recovery v2 login') } return decrypt(reply.recovery2Box, recovery2Key) } ) } /** * Fetches the questions for a login * @param username string * @param recovery2Key an ArrayBuffer recovery key * @param Question array promise */ export async function getQuestions2( ai, recovery2Key, username ) { const request = { recovery2Id: makeRecovery2Id(recovery2Key, username) // "otp": null } const reply = await loginFetch(ai, 'POST', '/v2/login', request) const { question2Box } = asRecovery2InfoPayload(reply) if (question2Box == null) { throw new Error('Login has no recovery questions') } // Decrypt the questions: return asQuestions(JSON.parse(decryptText(question2Box, recovery2Key))) } export async function changeRecovery( ai, accountId, questions, answers ) { const accountState = ai.props.state.accounts[accountId] const { loginTree, sessionKey } = accountState const { username } = accountState.stashTree if (username == null) throw new Error('Recovery login requires a username') const kit = makeRecovery2Kit(ai, loginTree, username, questions, answers) await applyKit(ai, sessionKey, kit) } export async function deleteRecovery( ai, accountId ) { const { loginTree, sessionKey } = ai.props.state.accounts[accountId] const kit = { login: { recovery2Key: undefined }, loginId: loginTree.loginId, server: undefined, serverMethod: 'DELETE', serverPath: '/v2/login/recovery2', stash: { recovery2Key: undefined } } await applyKit(ai, sessionKey, kit) } /** * Used when changing the username. * This won't return anything if the recovery is missing. */ export function makeChangeRecovery2IdKit( login, newUsername ) { const { loginId, recovery2Key } = login if (recovery2Key == null) return return { loginId, server: wasChangeRecovery2IdPayload({ recovery2Id: makeRecovery2Id(recovery2Key, newUsername) }), serverPath: '', stash: {} } } /** * Creates the data needed to attach recovery questions to a login. */ export function makeRecovery2Kit( ai, login, username, questions, answers ) { const { io } = ai.props if (!Array.isArray(questions)) { throw new TypeError('Questions must be an array of strings') } if (!Array.isArray(answers)) { throw new TypeError('Answers must be an array of strings') } const { loginId, loginKey, recovery2Key = io.random(32) } = login const question2Box = encrypt( io, utf8.parse(JSON.stringify(wasQuestions(questions))), recovery2Key ) const recovery2Box = encrypt(io, loginKey, recovery2Key) const recovery2KeyBox = encrypt(io, recovery2Key, loginKey) return { loginId, server: wasChangeRecovery2Payload({ recovery2Id: makeRecovery2Id(recovery2Key, username), recovery2Auth: makeRecovery2Auth(recovery2Key, answers), recovery2Box, recovery2KeyBox, question2Box }), serverPath: '/v2/login/recovery2', stash: { recovery2Key } } } const asQuestions = asArray(asString) const wasQuestions = uncleaner(asQuestions)