UNPKG

edge-core-js

Version:

Edge account & wallet management library

237 lines (211 loc) 6.18 kB
import { wasChangePasswordPayload } from '../../types/server-cleaners' import { decrypt, encrypt } from '../../util/crypto/crypto' import { makeSnrp, scrypt, userIdSnrp } from '../scrypt/scrypt-selectors' import { applyKit, serverLogin, syncLogin } from './login' import { hashUsername } from './login-selectors' import { saveStash } from './login-stash' const passwordAuthSnrp = userIdSnrp function makeHashInput(username, password) { return username + password } /** * Extracts the loginKey from the login stash. */ async function loginPasswordOffline( ai, stashTree, password, opts ) { const { now = new Date() } = opts const { passwordBox, passwordKeySnrp, username } = stashTree if (passwordBox == null || passwordKeySnrp == null || username == null) { throw new Error('Missing data for offline password login') } const up = makeHashInput(username, password) const passwordKey = await scrypt(ai, up, passwordKeySnrp) const sessionKey = { loginId: stashTree.loginId, loginKey: decrypt(passwordBox, passwordKey) } // Save the date: stashTree.lastLogin = now saveStash(ai, stashTree).catch(() => {}) // Since we logged in offline, update the stash in the background: // TODO: If the user provides an OTP token, add that to the stash. const { log } = ai.props syncLogin(ai, sessionKey).catch(error => log.error(error)) return sessionKey } /** * Fetches the loginKey from the server. */ async function loginPasswordOnline( ai, stashTree, password, opts ) { const { username } = stashTree if (username == null) throw new Error('Password login requires a username') // Request: const up = makeHashInput(username, password) const [userId, passwordAuth] = await Promise.all([ hashUsername(ai, username), scrypt(ai, up, passwordAuthSnrp) ]) const request = { userId, passwordAuth } return await serverLogin( ai, stashTree, stashTree, opts, request, async reply => { const { passwordBox, passwordKeySnrp } = reply if ( passwordBox == null || passwordBox === true || passwordKeySnrp == null ) { throw new Error('Missing data for online password login') } const passwordKey = await scrypt(ai, up, passwordKeySnrp) return decrypt(passwordBox, passwordKey) } ) } /** * Logs a user in using a password. * @param username string * @param password string * @return A `Promise` for the new root login. */ export async function loginPassword( ai, stashTree, password, opts ) { return await loginPasswordOffline(ai, stashTree, password, opts).catch(() => loginPasswordOnline(ai, stashTree, password, opts) ) } export async function changePassword( ai, accountId, password ) { const accountState = ai.props.state.accounts[accountId] const { loginTree, sessionKey } = accountState const { username } = accountState.stashTree if (username == null) throw new Error('Password login requires a username') const kit = await makePasswordKit(ai, loginTree, username, password) await applyKit(ai, sessionKey, kit) } /** * Returns true if the given password is correct. * * Accepts an optional loginKey to check using encryption over decryption as * an optimization. */ export async function checkPassword( ai, stash, password, loginKey ) { if (loginKey != null) { const { passwordAuthBox, username } = stash if (passwordAuthBox == null || username == null) return false const passwordAuth = decrypt(passwordAuthBox, loginKey) // Derive passwordAuth: const up = makeHashInput(username, password) const newPasswordAuth = await scrypt(ai, up, passwordAuthSnrp) // Compare what we derived with what we have: for (let i = 0; i < passwordAuth.length; ++i) { if (newPasswordAuth[i] !== passwordAuth[i]) return false } return true } else { const { passwordBox, passwordKeySnrp, username } = stash if (passwordBox == null || passwordKeySnrp == null || username == null) { throw new Error('Missing data for offline password login') } const up = makeHashInput(username, password) const passwordKey = await scrypt(ai, up, passwordKeySnrp) try { decrypt(passwordBox, passwordKey) return true } catch (_) { return false } } } export async function deletePassword( ai, accountId ) { const { loginTree, sessionKey } = ai.props.state.accounts[accountId] const kit = { loginId: loginTree.loginId, server: undefined, serverMethod: 'DELETE', serverPath: '/v2/login/password', stash: { passwordAuthSnrp: undefined, passwordBox: undefined, passwordKeySnrp: undefined } } // Only remove `passwordAuth` if we have another way to get in: if (loginTree.loginAuth != null) { kit.stash.passwordAuthBox = undefined } await applyKit(ai, sessionKey, kit) } /** * Creates the data needed to attach a password to a login. */ export async function makePasswordKit( ai, login, username, password ) { const up = makeHashInput(username, password) const { io } = ai.props const [{ passwordKeySnrp, passwordBox }, { passwordAuth, passwordAuthBox }] = await Promise.all([ // The loginKey, encrypted by the passwordKey: makeSnrp(ai).then(async passwordKeySnrp => { const passwordKey = await scrypt(ai, up, passwordKeySnrp) const passwordBox = encrypt(io, login.loginKey, passwordKey) return { passwordKeySnrp, passwordBox } }), // The passwordAuth, encrypted by the loginKey: scrypt(ai, up, passwordAuthSnrp).then(passwordAuth => { const passwordAuthBox = encrypt(io, passwordAuth, login.loginKey) return { passwordAuth, passwordAuthBox } }) ]) return { loginId: login.loginId, server: wasChangePasswordPayload({ passwordAuth, passwordAuthSnrp, // TODO: Use this on the other side passwordKeySnrp, passwordBox, passwordAuthBox }), serverPath: '/v2/login/password', stash: { passwordKeySnrp, passwordBox, passwordAuthBox } } }