'use strict' const crypto = require('crypto') function startSession(mechanisms) { if (mechanisms.indexOf('SCRAM-SHA-256') === -1) { throw new Error('SASL: Only mechanism SCRAM-SHA-256 is currently supported') } const clientNonce = crypto.randomBytes(18).toString('base64') return { mechanism: 'SCRAM-SHA-256', clientNonce, response: 'n,,n=*,r=' + clientNonce, message: 'SASLInitialResponse', } } function continueSession(session, password, serverData) { if (session.message !== 'SASLInitialResponse') { throw new Error('SASL: Last message was not SASLInitialResponse') } const sv = extractVariablesFromFirstServerMessage(serverData) if (!sv.nonce.startsWith(session.clientNonce)) { throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: server nonce does not start with client nonce') } var saltBytes = Buffer.from(sv.salt, 'base64') var saltedPassword = Hi(password, saltBytes, sv.iteration) var clientKey = createHMAC(saltedPassword, 'Client Key') var storedKey = crypto.createHash('sha256').update(clientKey).digest() var clientFirstMessageBare = 'n=*,r=' + session.clientNonce var serverFirstMessage = 'r=' + sv.nonce + ',s=' + sv.salt + ',i=' + sv.iteration var clientFinalMessageWithoutProof = 'c=biws,r=' + sv.nonce var authMessage = clientFirstMessageBare + ',' + serverFirstMessage + ',' + clientFinalMessageWithoutProof var clientSignature = createHMAC(storedKey, authMessage) var clientProofBytes = xorBuffers(clientKey, clientSignature) var clientProof = clientProofBytes.toString('base64') var serverKey = createHMAC(saltedPassword, 'Server Key') var serverSignatureBytes = createHMAC(serverKey, authMessage) session.message = 'SASLResponse' session.serverSignature = serverSignatureBytes.toString('base64') session.response = clientFinalMessageWithoutProof + ',p=' + clientProof } function finalizeSession(session, serverData) { if (session.message !== 'SASLResponse') { throw new Error('SASL: Last message was not SASLResponse') } var serverSignature String(serverData) .split(',') .forEach(function (part) { switch (part[0]) { case 'v': serverSignature = part.substr(2) break } }) if (serverSignature !== session.serverSignature) { throw new Error('SASL: SCRAM-SERVER-FINAL-MESSAGE: server signature does not match') } } function extractVariablesFromFirstServerMessage(data) { var nonce, salt, iteration String(data) .split(',') .forEach(function (part) { switch (part[0]) { case 'r': nonce = part.substr(2) break case 's': salt = part.substr(2) break case 'i': iteration = parseInt(part.substr(2), 10) break } }) if (!nonce) { throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: nonce missing') } if (!salt) { throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: salt missing') } if (!iteration) { throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: iteration missing') } return { nonce, salt, iteration, } } function xorBuffers(a, b) { if (!Buffer.isBuffer(a)) a = Buffer.from(a) if (!Buffer.isBuffer(b)) b = Buffer.from(b) var res = [] if (a.length > b.length) { for (var i = 0; i < b.length; i++) { res.push(a[i] ^ b[i]) } } else { for (var j = 0; j < a.length; j++) { res.push(a[j] ^ b[j]) } } return Buffer.from(res) } function createHMAC(key, msg) { return crypto.createHmac('sha256', key).update(msg).digest() } function Hi(password, saltBytes, iterations) { var ui1 = createHMAC(password, Buffer.concat([saltBytes, Buffer.from([0, 0, 0, 1])])) var ui = ui1 for (var i = 0; i < iterations - 1; i++) { ui1 = createHMAC(password, ui1) ui = xorBuffers(ui, ui1) } return ui } module.exports = { startSession, continueSession, finalizeSession, }