-
Notifications
You must be signed in to change notification settings - Fork 50
/
recovery2.js
198 lines (181 loc) · 5.1 KB
/
recovery2.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
// @flow
import { base64 } from 'rfc4648'
import { decrypt, encrypt, hmacSha256 } from '../../util/crypto/crypto.js'
import { fixOtpKey, totp } from '../../util/crypto/hotp.js'
import { utf8 } from '../../util/encoding.js'
import { type ApiInput } from '../root-pixie.js'
import { authRequest } from './authServer.js'
import { fixUsername, getStash } from './login-selectors.js'
import {
type LoginKit,
type LoginStash,
type LoginTree
} from './login-types.js'
import { applyKit, applyLoginReply, makeLoginTree } from './login.js'
import { saveStash } from './loginStore.js'
function recovery2Id(recovery2Key: Uint8Array, username: string) {
const data = utf8.parse(fixUsername(username))
return hmacSha256(data, recovery2Key)
}
function recovery2Auth(recovery2Key, answers) {
return answers.map(answer => {
const data = utf8.parse(answer)
return base64.stringify(hmacSha256(data, recovery2Key))
})
}
/**
* Fetches and decrypts the loginKey from the server.
* @return Promise<{loginKey, loginReply}>
*/
async function fetchLoginKey(
ai: ApiInput,
recovery2Key: Uint8Array,
username: string,
answers: string[],
otp: string | void
) {
const request = {
recovery2Id: base64.stringify(recovery2Id(recovery2Key, username)),
recovery2Auth: recovery2Auth(recovery2Key, answers),
otp
}
const reply = await authRequest(ai, 'POST', '/v2/login', request)
if (reply.recovery2Box == null) {
throw new Error('Missing data for recovery v2 login')
}
return {
loginKey: decrypt(reply.recovery2Box, recovery2Key),
loginReply: reply
}
}
/**
* Returns a copy of the recovery key if one exists on the local device.
*/
export function getRecovery2Key(stashTree: LoginStash) {
if (stashTree.recovery2Key != null) {
return base64.parse(stashTree.recovery2Key)
}
}
/**
* Logs a user in using recovery answers.
* @return A `Promise` for the new root login.
*/
export async function loginRecovery2(
ai: ApiInput,
recovery2Key: Uint8Array,
username: string,
answers: string[],
otpKey: string | void
) {
let stashTree = getStash(ai, username)
const { loginKey, loginReply } = await fetchLoginKey(
ai,
recovery2Key,
username,
answers,
totp(otpKey || stashTree.otpKey)
)
stashTree = applyLoginReply(stashTree, loginKey, loginReply)
if (otpKey) stashTree.otpKey = fixOtpKey(otpKey)
await saveStash(ai, stashTree)
return makeLoginTree(stashTree, loginKey)
}
/**
* Fetches the questions for a login
* @param username string
* @param recovery2Key an ArrayBuffer recovery key
* @param Question array promise
*/
export function getQuestions2(
ai: ApiInput,
recovery2Key: Uint8Array,
username: string
) {
const request = {
recovery2Id: base64.stringify(recovery2Id(recovery2Key, username))
// "otp": null
}
return authRequest(ai, 'POST', '/v2/login', request).then(reply => {
// Recovery login:
const question2Box = reply.question2Box
if (question2Box == null) {
throw new Error('Login has no recovery questions')
}
// Decrypt the questions:
const questions = decrypt(question2Box, recovery2Key)
return JSON.parse(utf8.stringify(questions))
})
}
export async function changeRecovery(
ai: ApiInput,
accountId: string,
questions: string[],
answers: string[]
) {
const { loginTree, username } = ai.props.state.accounts[accountId]
const kit = makeRecovery2Kit(ai, loginTree, username, questions, answers)
await applyKit(ai, loginTree, kit)
}
export async function deleteRecovery(ai: ApiInput, accountId: string) {
const { loginTree } = ai.props.state.accounts[accountId]
const kit = {
serverMethod: 'DELETE',
serverPath: '/v2/login/recovery2',
stash: {
recovery2Key: undefined
},
login: {
recovery2Key: undefined
},
loginId: loginTree.loginId
}
await applyKit(ai, loginTree, kit)
}
/**
* Creates the data needed to attach recovery questions to a login.
*/
export function makeRecovery2Kit(
ai: ApiInput,
login: LoginTree,
username: string,
questions: string[],
answers: string[]
): LoginKit {
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 recovery2Key = login.recovery2Key || io.random(32)
const question2Box = encrypt(
io,
utf8.parse(JSON.stringify(questions)),
recovery2Key
)
const recovery2Box = encrypt(io, login.loginKey, recovery2Key)
const recovery2KeyBox = encrypt(io, recovery2Key, login.loginKey)
return {
serverPath: '/v2/login/recovery2',
server: {
recovery2Id: base64.stringify(recovery2Id(recovery2Key, username)),
recovery2Auth: recovery2Auth(recovery2Key, answers),
recovery2Box,
recovery2KeyBox,
question2Box
},
stash: {
recovery2Key: base64.stringify(recovery2Key)
},
login: {
recovery2Key
},
loginId: login.loginId
}
}
export const listRecoveryQuestionChoices = function listRecoveryQuestionChoices(
ai: ApiInput
) {
return authRequest(ai, 'POST', '/v1/questions', {})
}