diff --git a/backend/zkppSalt.js b/backend/zkppSalt.js index ea746d5790..00a6548a84 100644 --- a/backend/zkppSalt.js +++ b/backend/zkppSalt.js @@ -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 === '') { + 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: '' })) + 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) @@ -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) @@ -91,10 +122,10 @@ export const getChallenge = async (contract: string, b: string): Promise { +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 } @@ -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 => { - const record = await getZkppSaltRecord(contract) +export const registrationKey = async (contractID: string, b: string): Promise => { + 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, @@ -134,26 +165,26 @@ export const registrationKey = async (contract: string, b: string): Promise => { - 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 => { + 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 } @@ -161,13 +192,13 @@ export const register = async (contract: string, clientPublicKey: string, encryp // 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 } diff --git a/data/secret_zkpp_parameters.json b/data/secret_zkpp_parameters.json new file mode 100644 index 0000000000..fb6c160b8e --- /dev/null +++ b/data/secret_zkpp_parameters.json @@ -0,0 +1 @@ +{"secret":""}