edge-core-js
Version:
Edge account & wallet management library
195 lines (173 loc) • 4.55 kB
JavaScript
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)