diff --git a/backend/routes.js b/backend/routes.js index 80476a344d..e255760078 100644 --- a/backend/routes.js +++ b/backend/routes.js @@ -821,7 +821,7 @@ route.GET('/zkpp/{name}/contract_hash', { return Boom.internal('internal error') }) -route.POST('/zkpp/updatePasswordHash/{name}', { +route.POST('/zkpp/{name}/updatePasswordHash', { validate: { payload: Joi.object({ r: Joi.string().required(), @@ -841,7 +841,7 @@ route.POST('/zkpp/updatePasswordHash/{name}', { } } catch (e) { e.ip = req.headers['x-real-ip'] || req.info.remoteAddress - console.error(e, 'Error at POST /zkpp/updatePasswordHash/{name}: ' + e.message) + console.error(e, 'Error at POST /zkpp/{name}/updatePasswordHash: ' + e.message) } return Boom.internal('internal error') diff --git a/frontend/controller/app/identity.js b/frontend/controller/app/identity.js index 6d2ede4a80..064455028c 100644 --- a/frontend/controller/app/identity.js +++ b/frontend/controller/app/identity.js @@ -6,7 +6,7 @@ import sbp from '@sbp/sbp' import Vue from 'vue' import { LOGIN, LOGIN_COMPLETE, LOGIN_ERROR } from '~/frontend/utils/events.js' import { Secret } from '~/shared/domains/chelonia/Secret.js' -import { boxKeyPair, buildRegisterSaltRequest, computeCAndHc, decryptContractSalt, hash, hashPassword, randomNonce } from '~/shared/zkpp.js' +import { boxKeyPair, buildRegisterSaltRequest, buildUpdateSaltRequestEa, computeCAndHc, decryptContractSalt, hash, hashPassword, randomNonce } from '~/shared/zkpp.js' // Using relative path to crypto.js instead of ~-path to workaround some esbuild bug import { CURVE25519XSALSA20POLY1305, EDWARDS25519SHA512BATCH, deriveKeyFromPassword, serializeKey } from '../../../shared/domains/chelonia/crypto.js' import { handleFetchResult } from '../utils/misc.js' @@ -177,6 +177,37 @@ export default (sbp('sbp/selectors/register', { return decryptContractSalt(c, contractHash) }, + 'gi.app/identity/updateSalt': async (username: string, oldPassword: Secret, newPassword: Secret) => { + const r = randomNonce() + const b = hash(r) + const authHash = await fetch(`${sbp('okTurtles.data/get', 'API_URL')}/zkpp/${encodeURIComponent(username)}/auth_hash?b=${encodeURIComponent(b)}`) + .then(handleFetchResult('json')) + + const { authSalt, s, sig } = authHash + + const h = await hashPassword(oldPassword.valueOf(), authSalt) + + const [c, hc] = computeCAndHc(r, s, h) + + const [contractHash, encryptedArgs] = await buildUpdateSaltRequestEa(newPassword.valueOf(), c) + + await fetch(`${sbp('okTurtles.data/get', 'API_URL')}/zkpp/${encodeURIComponent(username)}/updatePasswordHash`, { + method: 'POST', + headers: { + 'content-type': 'application/x-www-form-urlencoded' + }, + body: + `${(new URLSearchParams({ + 'r': r, + 's': s, + 'sig': sig, + 'hc': Buffer.from(hc).toString('base64').replace(/\//g, '_').replace(/\+/g, '-').replace(/=*$/, ''), + 'Ea': encryptedArgs.toString('base64').replace(/\//g, '_').replace(/\+/g, '-').replace(/=*$/, '') + })).toString()}` + }).then(handleFetchResult('text')) + + return contractHash + }, 'gi.app/identity/create': async function ({ data: { username, email, password, picture }, publishOptions @@ -409,5 +440,22 @@ export default (sbp('sbp/selectors/register', { }, 'gi.app/identity/logout': (...params) => { return sbp('okTurtles.eventQueue/queueEvent', 'APP-LOGIN', ['gi.app/identity/_private/logout', ...params]) + }, + 'gi.app/identity/changePassword': async (woldPassword: Secret, wnewPassword: Secret) => { + const state = sbp('state/vuex/state') + if (!state.loggedIn) return + const getters = sbp('state/vuex/getters') + + const { identityContractID } = state.loggedIn + const username = getters.usernameFromID(identityContractID) + // const oldPassword = woldPassword.valueOf() + // const newPassword = wnewPassword.valueOf() + + const contractSalt = await sbp('gi.app/identity/updateSalt', username, woldPassword, wnewPassword) + + return contractSalt + + /* const IPK = await deriveKeyFromPassword(EDWARDS25519SHA512BATCH, newPassword, contractSalt) + const IEK = await deriveKeyFromPassword(CURVE25519XSALSA20POLY1305, newPassword, contractSalt) */ } }): string[]) diff --git a/package-lock.json b/package-lock.json index 666ba223a7..8b2b25b402 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "group-income", - "version": "1.0.7", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "group-income", - "version": "1.0.7", + "version": "1.1.0", "license": "AGPL-3.0", "dependencies": { "@babel/core": "7.23.7", diff --git a/shared/zkpp.js b/shared/zkpp.js index 55ce56bf24..044d34c693 100644 --- a/shared/zkpp.js +++ b/shared/zkpp.js @@ -114,3 +114,29 @@ export const buildRegisterSaltRequest = async (publicKey: string, secretKey: Uin return [contractSalt, base64ToBase64url(Buffer.concat([nonce, encryptedHashedPasswordBuf]).toString('base64'))] } + +export const buildUpdateSaltRequestEa = async (password: string, c: Uint8Array): Promise<[string, string]> => { + // TODO: Derive S_A and S_C as follows: + // -> q -< random + // -> r -< SHA-512(SHA-512('SU') + SHA-512(q)) + // -> b -< SHA-512(r) // as it's now + // Then, + // -> S_T -< BASE64(SHA-512(SHA-512(T) + SHA-512(q))[0..18]) with T being + // `AUTHSALT` or `CONTRACTSALT` + // This way, we ensure both the server and the client contribute to the + // salts' entropy. + // When sending the encrypted data, the encrypted information would be + // `[hashedPassword, q]`, which needs to be verified server-side to verify + // it matches p and would be used to derive S_A and S_C. + const [authSalt, contractSalt] = ['a', 'b'] + + const encryptionKey = nacl.hash(Buffer.concat([Buffer.from('SU'), c])).slice(0, nacl.secretbox.keyLength) + const nonce = nacl.randomBytes(nacl.secretbox.nonceLength) + + const hashedPassword = await hashPassword(password, authSalt) + const encryptedArgsCiphertext = nacl.secretbox(Buffer.from(JSON.stringify([hashedPassword, authSalt, contractSalt])), nonce, encryptionKey) + + const encryptedArgs = Buffer.concat([nonce, encryptedArgsCiphertext]) + + return [contractSalt, base64ToBase64url(encryptedArgs.toString('base64'))] +}