Skip to content

Commit

Permalink
ZKPP: Frontend implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
corrideat committed Aug 8, 2022
1 parent 94a7601 commit e9e3851
Show file tree
Hide file tree
Showing 4 changed files with 204 additions and 68 deletions.
62 changes: 19 additions & 43 deletions backend/zkppSalt.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { blake32Hash } from '~/shared/functions.js'
import { timingSafeEqual } from 'crypto'
import nacl from 'tweetnacl'
import sbp from '@sbp/sbp'
import { boxKeyPair, encryptContractSalt, hashStringArray, hashRawStringArray, hash, parseRegisterSalt, randomNonce, computeCAndHc } from '~/shared/zkpp.js'

// TODO HARDCODED VALUES
const recordPepper = 'pepper'
Expand All @@ -10,22 +11,14 @@ const challengeSecret = 'secret'
const registrationSecret = 'secret'
const maxAge = 30

const hashStringArray = (...args: Array<Uint8Array | string>) => {
return nacl.hash(Buffer.concat(args.map((s) => nacl.hash(Buffer.from(s)))))
}

const hashRawStringArray = (...args: Array<Uint8Array | string>) => {
return nacl.hash(Buffer.concat(args.map((s) => Buffer.from(s))))
}

const getZkppSaltRecord = async (contract: string) => {
const recordId = blake32Hash(hashStringArray('RID', contract, recordPepper))
const record = await sbp('chelonia/db/get', recordId)

if (record) {
const encryptionKey = hashStringArray('REK', contract, recordMasterKey).slice(0, nacl.secretbox.keyLength)

const recordBuf = Buffer.from(record, 'base64url')
const recordBuf = Buffer.from(record.replace(/_/g, '/').replace(/-/g, '+'), 'base64')
const nonce = recordBuf.slice(0, nacl.secretbox.nonceLength)
const recordCiphertext = recordBuf.slice(nacl.secretbox.nonceLength)
const recordPlaintext = nacl.secretbox.open(recordCiphertext, nonce, encryptionKey)
Expand Down Expand Up @@ -65,7 +58,7 @@ const setZkppSaltRecord = async (contract: string, hashedPassword: string, authS
const recordPlaintext = JSON.stringify([hashedPassword, authSalt, contractSalt])
const recordCiphertext = nacl.secretbox(Buffer.from(recordPlaintext), nonce, encryptionKey)
const recordBuf = Buffer.concat([nonce, recordCiphertext])
const record = recordBuf.toString('base64url')
const record = recordBuf.toString('base64').replace(/\//g, '_').replace(/\+/g, '-').replace(/=*$/, '')
await sbp('chelonia/db/set', recordId, record)
}

Expand All @@ -75,9 +68,9 @@ export const getChallenge = async (contract: string, b: string): Promise<false |
return false
}
const { authSalt } = record
const s = Buffer.from(nacl.randomBytes(12)).toString('base64url')
const s = randomNonce()
const now = (Date.now() / 1000 | 0).toString(16)
const sig = [now, Buffer.from(hashStringArray(contract, b, s, now, challengeSecret)).toString('base64url')].join(',')
const sig = [now, Buffer.from(hashStringArray(contract, b, s, now, challengeSecret)).toString('base64').replace(/\//g, '_').replace(/\+/g, '-').replace(/=*$/, '')].join(',')

return {
authSalt,
Expand All @@ -101,9 +94,9 @@ const verifyChallenge = (contract: string, r: string, s: string, userSig: string
return false
}

const b = Buffer.from(nacl.hash(Buffer.from(r))).toString('base64url')
const b = hash(r)
const sig = hashStringArray(contract, b, s, then, challengeSecret)
const macBuf = Buffer.from(mac, 'base64url')
const macBuf = Buffer.from(mac.replace(/_/g, '/').replace(/-/g, '+'), 'base64')

return sig.byteLength === macBuf.byteLength && timingSafeEqual(sig, macBuf)
}
Expand All @@ -116,14 +109,14 @@ export const registrationKey = async (contract: string, b: string): Promise<fals

const encryptionKey = hashStringArray('REG', contract, registrationSecret).slice(0, nacl.secretbox.keyLength)
const nonce = nacl.randomBytes(nacl.secretbox.nonceLength)
const keyPair = nacl.box.keyPair()
const s = Buffer.concat([nonce, nacl.secretbox(keyPair.secretKey, nonce, encryptionKey)]).toString('base64url')
const keyPair = boxKeyPair()
const s = Buffer.concat([nonce, nacl.secretbox(keyPair.secretKey, nonce, encryptionKey)]).toString('base64').replace(/\//g, '_').replace(/\+/g, '-').replace(/=*$/, '')
const now = (Date.now() / 1000 | 0).toString(16)
const sig = [now, Buffer.from(hashStringArray(contract, b, s, now, challengeSecret)).toString('base64url')].join(',')
const sig = [now, Buffer.from(hashStringArray(contract, b, s, now, challengeSecret)).toString('base64').replace(/\//g, '_').replace(/\+/g, '-').replace(/=*$/, '')].join(',')

return {
s,
p: Buffer.from(keyPair.publicKey).toString('base64url'),
p: Buffer.from(keyPair.publicKey).toString('base64').replace(/\//g, '_').replace(/\+/g, '-').replace(/=*$/, ''),
sig
}
}
Expand All @@ -139,38 +132,26 @@ export const register = async (contract: string, clientPublicKey: string, encryp
return false
}

const clientPublicKeyBuf = Buffer.from(clientPublicKey, 'base64url')
const encryptedSecretKeyBuf = Buffer.from(encryptedSecretKey, 'base64url')
const encryptedSecretKeyBuf = Buffer.from(encryptedSecretKey.replace(/_/g, '/').replace(/-/g, '+'), 'base64')
const encryptionKey = hashStringArray('REG', contract, registrationSecret).slice(0, nacl.secretbox.keyLength)
const secretKeyBuf = nacl.secretbox.open(encryptedSecretKeyBuf.slice(nacl.secretbox.nonceLength), encryptedSecretKeyBuf.slice(0, nacl.secretbox.nonceLength), encryptionKey)

if (clientPublicKeyBuf.byteLength !== nacl.box.publicKeyLength || !secretKeyBuf || secretKeyBuf.byteLength !== nacl.box.secretKeyLength) {
return false
}

const dhKey = nacl.box.before(clientPublicKeyBuf, secretKeyBuf)

const encryptedHashedPasswordBuf = Buffer.from(encryptedHashedPassword, 'base64url')

const hashedPasswordBuf = nacl.box.open.after(encryptedHashedPasswordBuf.slice(nacl.box.nonceLength), encryptedHashedPasswordBuf.slice(0, nacl.box.nonceLength), dhKey)
const parseRegisterSaltRes = parseRegisterSalt(clientPublicKey, secretKeyBuf, encryptedHashedPassword)

if (!hashedPasswordBuf) {
if (!parseRegisterSaltRes) {
return false
}

const authSalt = Buffer.from(hashStringArray('AUTHSALT', dhKey)).slice(0, 18).toString('base64url')
const contractSalt = Buffer.from(hashStringArray('CONTRACTSALT', dhKey)).slice(0, 18).toString('base64url')
const [authSalt, contractSalt, hashedPasswordBuf] = parseRegisterSaltRes

await setZkppSaltRecord(contract, Buffer.from(hashedPasswordBuf).toString(), authSalt, contractSalt)

return true
}

const contractSaltVerifyC = (h: string, r: string, s: string, userHc: string) => {
const ħ = hashStringArray(r, s)
const c = hashStringArray(h, ħ)
const hc = nacl.hash(c)
const userHcBuf = Buffer.from(userHc, 'base64url')
const [c, hc] = computeCAndHc(r, s, h)
const userHcBuf = Buffer.from(userHc.replace(/_/g, '/').replace(/-/g, '+'), 'base64')

if (hc.byteLength === userHcBuf.byteLength && timingSafeEqual(hc, userHcBuf)) {
return c
Expand All @@ -197,12 +178,7 @@ export const getContractSalt = async (contract: string, r: string, s: string, si
return false
}

const encryptionKey = hashRawStringArray('CS', c).slice(0, nacl.secretbox.keyLength)
const nonce = nacl.randomBytes(nacl.secretbox.nonceLength)

const encryptedContractSalt = nacl.secretbox(Buffer.from(contractSalt), nonce, encryptionKey)

return Buffer.concat([nonce, encryptedContractSalt]).toString('base64url')
return encryptContractSalt(c, contractSalt)
}

export const update = async (contract: string, r: string, s: string, sig: string, hc: string, encryptedArgs: string): Promise<boolean> => {
Expand All @@ -223,7 +199,7 @@ export const update = async (contract: string, r: string, s: string, sig: string
}

const encryptionKey = hashRawStringArray('SU', c).slice(0, nacl.secretbox.keyLength)
const encryptedArgsBuf = Buffer.from(encryptedArgs, 'base64url')
const encryptedArgsBuf = Buffer.from(encryptedArgs.replace(/_/g, '/').replace(/-/g, '+'), 'base64')
const nonce = encryptedArgsBuf.slice(0, nacl.secretbox.nonceLength)
const encrytedArgsCiphertext = encryptedArgsBuf.slice(nacl.secretbox.nonceLength)

Expand Down
33 changes: 18 additions & 15 deletions backend/zkppSalt.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,50 +6,56 @@ import 'should-sinon'

import { registrationKey, register, getChallenge, getContractSalt, update } from './zkppSalt.js'

const saltsAndEncryptedHashedPassword = (p: string, secretKey: Uint8Array, hash: string) => {
const nonce = nacl.randomBytes(nacl.secretbox.nonceLength)
const dhKey = nacl.hash(nacl.box.before(Buffer.from(p, 'base64url'), secretKey))
const authSalt = Buffer.from(nacl.hash(Buffer.concat([nacl.hash(Buffer.from('AUTHSALT')), dhKey]))).slice(0, 18).toString('base64')
const contractSalt = Buffer.from(nacl.hash(Buffer.concat([nacl.hash(Buffer.from('CONTRACTSALT')), dhKey]))).slice(0, 18).toString('base64')
const encryptionKey = nacl.hash(Buffer.from(authSalt + contractSalt)).slice(0, nacl.secretbox.keyLength)
const encryptedHashedPassword = Buffer.concat([nonce, nacl.secretbox(Buffer.from(hash), nonce, encryptionKey)]).toString('base64url')

return [authSalt, contractSalt, encryptedHashedPassword]
}

describe('ZKPP Salt functions', () => {
it('register', async () => {
const keyPair = nacl.box.keyPair()
const nonce = nacl.randomBytes(nacl.box.nonceLength)
const publicKey = Buffer.from(keyPair.publicKey).toString('base64url')
const publicKeyHash = Buffer.from(nacl.hash(Buffer.from(publicKey))).toString('base64url')

const regKeyAlice1 = await registrationKey('alice', publicKeyHash)
const regKeyAlice2 = await registrationKey('alice', publicKeyHash)
should(regKeyAlice1).be.of.type('object')
should(regKeyAlice2).be.of.type('object')
const encryptedHashedPasswordAlice1 = Buffer.concat([nonce, nacl.box(Buffer.from('hash'), nonce, Buffer.from(regKeyAlice1.p, 'base64url'), keyPair.secretKey)]).toString('base64url')
const [, , encryptedHashedPasswordAlice1] = saltsAndEncryptedHashedPassword(regKeyAlice1.p, keyPair.secretKey, 'hash')
const res1 = await register('alice', publicKey, regKeyAlice1.s, regKeyAlice1.sig, encryptedHashedPasswordAlice1)
should(res1).equal(true, 'register should allow new entry (alice)')

const encryptedHashedPasswordAlice2 = Buffer.concat([nonce, nacl.box(Buffer.from('hash'), nonce, Buffer.from(regKeyAlice2.p, 'base64url'), keyPair.secretKey)]).toString('base64url')
const [, , encryptedHashedPasswordAlice2] = saltsAndEncryptedHashedPassword(regKeyAlice1.p, keyPair.secretKey, 'hash')
const res2 = await register('alice', publicKey, regKeyAlice2.s, regKeyAlice2.sig, encryptedHashedPasswordAlice2)
should(res2).equal(false, 'register should not overwrite entry (alice)')

const regKeyBob1 = await registrationKey('bob', publicKeyHash)
should(regKeyBob1).be.of.type('object')
const encryptedHashedPasswordBob1 = Buffer.concat([nonce, nacl.box(Buffer.from('hash'), nonce, Buffer.from(regKeyBob1.p, 'base64url'), keyPair.secretKey)]).toString('base64url')
const [, , encryptedHashedPasswordBob1] = saltsAndEncryptedHashedPassword(regKeyBob1.p, keyPair.secretKey, 'hash')
const res3 = await register('bob', publicKey, regKeyBob1.s, regKeyBob1.sig, encryptedHashedPasswordBob1)
should(res3).equal(true, 'register should allow new entry (bob)')
})

it('getContractSalt', async () => {
const keyPair = nacl.box.keyPair()
const eNonce = nacl.randomBytes(nacl.box.nonceLength)
const publicKey = Buffer.from(keyPair.publicKey).toString('base64url')
const publicKeyHash = Buffer.from(nacl.hash(Buffer.from(publicKey))).toString('base64url')

const [contract, hash, r] = ['getContractSalt', 'hash', 'r']
const regKey = await registrationKey(contract, publicKeyHash)
should(regKey).be.of.type('object')

const encryptedHashedPassword = Buffer.concat([eNonce, nacl.box(Buffer.from('hash'), eNonce, Buffer.from(regKey.p, 'base64url'), keyPair.secretKey)]).toString('base64url')
const [authSalt, contractSalt, encryptedHashedPassword] = saltsAndEncryptedHashedPassword(regKey.p, keyPair.secretKey, hash)

const res = await register(contract, publicKey, regKey.s, regKey.sig, encryptedHashedPassword)
should(res).equal(true, 'register should allow new entry (' + contract + ')')

const dhKey = nacl.hash(nacl.box.before(Buffer.from(regKey.p, 'base64url'), keyPair.secretKey))
const authSalt = Buffer.from(nacl.hash(Buffer.concat([nacl.hash(Buffer.from('AUTHSALT')), dhKey]))).slice(0, 18).toString('base64url')
const contractSalt = Buffer.from(nacl.hash(Buffer.concat([nacl.hash(Buffer.from('CONTRACTSALT')), dhKey]))).slice(0, 18).toString('base64url')

const b = Buffer.from(nacl.hash(Buffer.from(r))).toString('base64url')
const challenge = await getChallenge(contract, b)
should(challenge).be.of.type('object', 'challenge should be object')
Expand All @@ -71,21 +77,18 @@ describe('ZKPP Salt functions', () => {

it('update', async () => {
const keyPair = nacl.box.keyPair()
const eNonce = nacl.randomBytes(nacl.box.nonceLength)
const publicKey = Buffer.from(keyPair.publicKey).toString('base64url')
const publicKeyHash = Buffer.from(nacl.hash(Buffer.from(publicKey))).toString('base64url')

const [contract, hash, r] = ['update', 'hash', 'r']
const regKey = await registrationKey(contract, publicKeyHash)
should(regKey).be.of.type('object')

const encryptedHashedPassword = Buffer.concat([eNonce, nacl.box(Buffer.from('hash'), eNonce, Buffer.from(regKey.p, 'base64url'), keyPair.secretKey)]).toString('base64url')
const [authSalt, , encryptedHashedPassword] = saltsAndEncryptedHashedPassword(regKey.p, keyPair.secretKey, hash)

const res = await register(contract, publicKey, regKey.s, regKey.sig, encryptedHashedPassword)
should(res).equal(true, 'register should allow new entry (' + contract + ')')

const dhKey = nacl.hash(nacl.box.before(Buffer.from(regKey.p, 'base64url'), keyPair.secretKey))
const authSalt = Buffer.from(nacl.hash(Buffer.concat([nacl.hash(Buffer.from('AUTHSALT')), dhKey]))).slice(0, 18).toString('base64url')

const b = Buffer.from(nacl.hash(Buffer.from(r))).toString('base64url')
const challenge = await getChallenge(contract, b)
should(challenge).be.of.type('object', 'challenge should be object')
Expand Down
66 changes: 56 additions & 10 deletions frontend/controller/actions/identity.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,10 @@ import { LOGIN, LOGOUT } from '~/frontend/utils/events.js'
import './mailbox.js'

import { encryptedAction } from './utils.js'
import { handleFetchResult } from '../utils/misc.js'
import { GIMessage } from '~/shared/domains/chelonia/GIMessage.js'
import type { GIKey } from '~/shared/domains/chelonia/GIMessage.js'

// eslint-disable-next-line camelcase
const salt_TODO_CHANGEME_NEEDS_TO_BE_DYNAMIC = 'SALT CHANGEME'
import { boxKeyPair, buildRegisterSaltRequest, computeCAndHc, decryptContractSalt, hash, hashPassword, randomNonce } from '~/shared/zkpp.js'

function generatedLoginState () {
const { contracts } = sbp('state/vuex/state')
Expand All @@ -36,8 +35,25 @@ function diffLoginStates (s1: ?Object, s2: ?Object) {

export default (sbp('sbp/selectors/register', {
'gi.actions/identity/retrieveSalt': async (username: string, password: string) => {
// TODO RETRIEVE FROM SERVER
return await Promise.resolve(salt_TODO_CHANGEME_NEEDS_TO_BE_DYNAMIC)
const r = randomNonce()
const b = hash(r)
const authHash = await fetch(`${sbp('okTurtles.data/get', 'API_URL')}/zkpp/user=${encodeURIComponent(username)}/auth_hash?b=${encodeURIComponent(b)}`)
.then(handleFetchResult('json'))

const { authSalt, s, sig } = authHash

const h = await hashPassword(password, authSalt)

const [c, hc] = computeCAndHc(r, s, h)

const contractHash = await fetch(`${sbp('okTurtles.data/get', 'API_URL')}/zkpp/user=${encodeURIComponent(username)}/contract_hash?${(new URLSearchParams({
'r': r,
's': s,
'sig': sig,
'hc': Buffer.from(hc).toString('base64').replace(/\//g, '_').replace(/\+/g, '-').replace(/=*$/, '')
})).toString()}`).then(handleFetchResult('text'))

return decryptContractSalt(c, contractHash)
},
'gi.actions/identity/create': async function ({
data: { username, email, password, picture },
Expand Down Expand Up @@ -69,12 +85,42 @@ export default (sbp('sbp/selectors/register', {
const mailbox = await sbp('gi.actions/mailbox/create', { options: { sync: true } })
const mailboxID = mailbox.contractID()

const keyPair = boxKeyPair()
const r = Buffer.from(keyPair.publicKey).toString('base64').replace(/\//g, '_').replace(/\+/g, '-')
const b = hash(r)
const registrationRes = await fetch(`${sbp('okTurtles.data/get', 'API_URL')}/zkpp/user=${encodeURIComponent(username)}`, {
method: 'POST',
headers: {
'content-type': 'application/x-www-form-urlencoded'
},
body: `b=${encodeURIComponent(b)}`
})
.then(handleFetchResult('json'))

const { p, s, sig } = registrationRes

const [contractSalt, Eh] = await buildRegisterSaltRequest(p, keyPair.secretKey, password)

const res = await fetch(`${sbp('okTurtles.data/get', 'API_URL')}/zkpp/user=${encodeURIComponent(username)}`, {
method: 'POST',
headers: {
'content-type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
'r': r,
's': s,
'sig': sig,
'Eh': Eh
})
})

if (!res.ok) {
throw new Error('Unable to register hash')
}

// Create the necessary keys to initialise the contract
// TODO: The salt needs to be dynamically generated
// eslint-disable-next-line camelcase
const salt = salt_TODO_CHANGEME_NEEDS_TO_BE_DYNAMIC
const IPK = await deriveKeyFromPassword(EDWARDS25519SHA512BATCH, password, salt)
const IEK = await deriveKeyFromPassword(CURVE25519XSALSA20POLY1305, password, salt)
const IPK = await deriveKeyFromPassword(EDWARDS25519SHA512BATCH, password, contractSalt)
const IEK = await deriveKeyFromPassword(CURVE25519XSALSA20POLY1305, password, contractSalt)
const CSK = keygen(EDWARDS25519SHA512BATCH)
const CEK = keygen(CURVE25519XSALSA20POLY1305)

Expand Down
Loading

0 comments on commit e9e3851

Please sign in to comment.