Skip to content

Commit

Permalink
Remove pepper, read secret from file and minimise the number of secrets
Browse files Browse the repository at this point in the history
  • Loading branch information
corrideat committed Feb 12, 2024
1 parent e2295fe commit 85b0ce2
Show file tree
Hide file tree
Showing 2 changed files with 61 additions and 29 deletions.
89 changes: 60 additions & 29 deletions backend/zkppSalt.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,58 @@
import { blake32Hash } from '~/shared/functions.js'
import { timingSafeEqual } from 'crypto'
import { timingSafeEqual, randomBytes } from 'crypto'
import nacl from 'tweetnacl'
import sbp from '@sbp/sbp'
import { boxKeyPair, encryptContractSalt, hashStringArray, hashRawStringArray, hash, parseRegisterSalt, randomNonce, computeCAndHc, base64urlToBase64, base64ToBase64url } from '~/shared/zkpp.js'
import { readFileSync } from 'fs'

// Input keying material used to derive various secret keys used in this protocol
let IKM: string

try {
const secretZkppParams = readFileSync('data/secret_zkpp_parameters.json', 'utf-8')
const parsedSecretZkppParams = JSON.parse(secretZkppParams)
if (!parsedSecretZkppParams.secret) {
throw new Error('Missing ZKPP secret.')
}
if (typeof parsedSecretZkppParams.secret !== 'string') {
throw new Error('ZKPP secret has the wrong type.')
}
if (parsedSecretZkppParams.secret.length < 16) {
throw new Error('ZKPP secret is too short. It must be at least 16 characters long.')
}
if (parsedSecretZkppParams.secret === '<REPLACE THIS WITH A LONG RANDOM STRING>') {
if (process.env.NODE_ENV === 'production') {
throw new Error('Please change the ZKPP from its default value')
}
console.warn('Using the default value for the ZKPP secret. This is fine during development, but you must give it a different value in production mode.')
console.warn('If you are setting up this server for the first time, you can create a file at `data/secret_zkpp_parameters.json` with the following freshly-generated contents:')
console.warn(JSON.stringify({ secret: randomBytes(33).toString('base64') }))
}
IKM = parsedSecretZkppParams.secret
} catch (e) {
console.error('Error reading ZKPP secret params: ', e.message)
console.error('Create a JSON file at `data/secret_zkpp_parameters.json` with the following contents and ensure that it\'s readable:')
console.error(JSON.stringify({ secret: '<REPLACE THIS WITH A LONG RANDOM STRING>' }))
console.error('If you are setting up this server for the first time, you can use the following freshly-generated contents:')
console.error(JSON.stringify({ secret: randomBytes(33).toString('base64') }))
throw e
}

// TODO HARDCODED VALUES
// These values will eventually come from server the configuration
const recordPepper = 'pepper' // TODO: get rid of `recordPepper`, just use `private/rid/${contractID}`
// used to encrypt salts in database
const recordMasterKey = 'masterKey' // TODO: store in file that's got root privs (after dropping privs)
const recordSecret = Buffer.from(hashStringArray('private/recordSecret', IKM)).toString('base64')
// corresponds to the key for the keyed Hash function in "Log in / session establishment"
const challengeSecret = 'secret' // TODO: generate randomly and store in DB under private prefix
const challengeSecret = Buffer.from(hashStringArray('private/challengeSecret', IKM)).toString('base64')
// corresponds to a component of s in Step 3 of "Salt registration"
const registrationSecret = 'secret' // TODO: generate randomly and store in DB under private prefix
const registrationSecret = Buffer.from(hashStringArray('private/registrationSecret', IKM)).toString('base64')

const maxAge = 30

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

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

const recordBuf = Buffer.from(base64urlToBase64(record), 'base64')
const nonce = recordBuf.slice(0, nacl.secretbox.nonceLength)
Expand Down Expand Up @@ -57,11 +88,11 @@ const getZkppSaltRecord = async (contract: string) => {
return null
}

const setZkppSaltRecord = async (contract: string, hashedPassword: string, authSalt: string, contractSalt: string) => {
const recordId = blake32Hash(hashStringArray('RID', contract, recordPepper))
const setZkppSaltRecord = async (contractID: string, hashedPassword: string, authSalt: string, contractSalt: string) => {
const recordId = blake32Hash(`private/rid/${contractID}`)
// TODO: replace the above line with:
// const recordId = `private/rid/${contractID}`
const encryptionKey = hashStringArray('REK', contract, recordMasterKey).slice(0, nacl.secretbox.keyLength)
const encryptionKey = hashStringArray('REK', contractID, recordSecret).slice(0, nacl.secretbox.keyLength)
const nonce = nacl.randomBytes(nacl.secretbox.nonceLength)
const recordPlaintext = JSON.stringify([hashedPassword, authSalt, contractSalt])
const recordCiphertext = nacl.secretbox(Buffer.from(recordPlaintext), nonce, encryptionKey)
Expand Down Expand Up @@ -91,10 +122,10 @@ export const getChallenge = async (contract: string, b: string): Promise<false |
}
}

const verifyChallenge = (contract: string, r: string, s: string, userSig: string): boolean => {
const verifyChallenge = (contractID: string, r: string, s: string, userSig: string): boolean => {
// Check sig has the right format
if (!/^[a-fA-F0-9]{1,11},[a-zA-Z0-9_-]{86}(?:==)?$/.test(userSig)) {
console.info(`wrong signature format for challenge for contract: ${contract}`)
console.info(`wrong signature format for challenge for contract: ${contractID}`)
return false
}

Expand All @@ -108,24 +139,24 @@ const verifyChallenge = (contract: string, r: string, s: string, userSig: string
}

const b = hash(r)
const sig = hashStringArray(contract, b, s, then, challengeSecret)
const sig = hashStringArray(contractID, b, s, then, challengeSecret)
const macBuf = Buffer.from(base64urlToBase64(mac), 'base64')

return sig.byteLength === macBuf.byteLength && timingSafeEqual(sig, macBuf)
}

export const registrationKey = async (contract: string, b: string): Promise<false | {s: string; p: string; sig: string;}> => {
const record = await getZkppSaltRecord(contract)
export const registrationKey = async (contractID: string, b: string): Promise<false | {s: string; p: string; sig: string;}> => {
const record = await getZkppSaltRecord(contractID)
if (record) {
throw new Error('registrationKey: User record already exists')
}

const encryptionKey = hashStringArray('REG', contract, registrationSecret).slice(0, nacl.secretbox.keyLength)
const encryptionKey = hashStringArray('REG', contractID, registrationSecret).slice(0, nacl.secretbox.keyLength)
const nonce = nacl.randomBytes(nacl.secretbox.nonceLength)
const keyPair = boxKeyPair()
const s = base64ToBase64url(Buffer.concat([nonce, nacl.secretbox(keyPair.secretKey, nonce, encryptionKey)]).toString('base64'))
const now = (Date.now() / 1000 | 0).toString(16)
const sig = [now, base64ToBase64url(Buffer.from(hashStringArray(contract, b, s, now, challengeSecret)).toString('base64'))].join(',')
const sig = [now, base64ToBase64url(Buffer.from(hashStringArray(contractID, b, s, now, challengeSecret)).toString('base64'))].join(',')

return {
s,
Expand All @@ -134,40 +165,40 @@ export const registrationKey = async (contract: string, b: string): Promise<fals
}
}

export const register = async (contract: string, clientPublicKey: string, encryptedSecretKey: string, userSig: string, encryptedHashedPassword: string): Promise<boolean> => {
if (!verifyChallenge(contract, clientPublicKey, encryptedSecretKey, userSig)) {
console.warn('register: Error validating challenge: ' + JSON.stringify({ contract, clientPublicKey, userSig }))
export const register = async (contractID: string, clientPublicKey: string, encryptedSecretKey: string, userSig: string, encryptedHashedPassword: string): Promise<boolean> => {
if (!verifyChallenge(contractID, clientPublicKey, encryptedSecretKey, userSig)) {
console.warn('register: Error validating challenge: ' + JSON.stringify({ contract: contractID, clientPublicKey, userSig }))
throw new Error('register: Invalid challenge')
}

const record = await getZkppSaltRecord(contract)
const record = await getZkppSaltRecord(contractID)

if (record) {
console.warn('register: Error: ZKPP salt record for contract ID ' + contract + ' already exists')
console.warn('register: Error: ZKPP salt record for contract ID ' + contractID + ' already exists')
return false
}

const encryptedSecretKeyBuf = Buffer.from(base64urlToBase64(encryptedSecretKey), 'base64')
const encryptionKey = hashStringArray('REG', contract, registrationSecret).slice(0, nacl.secretbox.keyLength)
const encryptionKey = hashStringArray('REG', contractID, registrationSecret).slice(0, nacl.secretbox.keyLength)
const secretKeyBuf = nacl.secretbox.open(encryptedSecretKeyBuf.slice(nacl.secretbox.nonceLength), encryptedSecretKeyBuf.slice(0, nacl.secretbox.nonceLength), encryptionKey)

// Likely a bad implementation on the client side
if (!secretKeyBuf) {
console.warn(`register: Error decrypting arguments for contract ID ${contract} (${JSON.stringify({ clientPublicKey, userSig })})`)
console.warn(`register: Error decrypting arguments for contract ID ${contractID} (${JSON.stringify({ clientPublicKey, userSig })})`)
return false
}

const parseRegisterSaltRes = parseRegisterSalt(clientPublicKey, secretKeyBuf, encryptedHashedPassword)

// Likely a bad implementation on the client side
if (!parseRegisterSaltRes) {
console.warn(`register: Error parsing registration salt for contract ID ${contract} (${JSON.stringify({ clientPublicKey, userSig })})`)
console.warn(`register: Error parsing registration salt for contract ID ${contractID} (${JSON.stringify({ clientPublicKey, userSig })})`)
return false
}

const [authSalt, contractSalt, hashedPasswordBuf] = parseRegisterSaltRes

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

return true
}
Expand Down
1 change: 1 addition & 0 deletions data/secret_zkpp_parameters.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"secret":"<REPLACE THIS WITH A LONG RANDOM STRING>"}

0 comments on commit 85b0ce2

Please sign in to comment.