| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186 | 'use strict'const crypto = require('./utils')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',  }}async function continueSession(session, password, serverData) {  if (session.message !== 'SASLInitialResponse') {    throw new Error('SASL: Last message was not SASLInitialResponse')  }  if (typeof password !== 'string') {    throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: client password must be a string')  }  if (password === '') {    throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: client password must be a non-empty string')  }  if (typeof serverData !== 'string') {    throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: serverData must be a string')  }  const sv = parseServerFirstMessage(serverData)  if (!sv.nonce.startsWith(session.clientNonce)) {    throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: server nonce does not start with client nonce')  } else if (sv.nonce.length === session.clientNonce.length) {    throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: server nonce is too short')  }  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 saltBytes = Buffer.from(sv.salt, 'base64')  var saltedPassword = await crypto.deriveKey(password, saltBytes, sv.iteration)  var clientKey = await crypto.hmacSha256(saltedPassword, 'Client Key')  var storedKey = await crypto.sha256(clientKey)  var clientSignature = await crypto.hmacSha256(storedKey, authMessage)  var clientProof = xorBuffers(Buffer.from(clientKey), Buffer.from(clientSignature)).toString('base64')  var serverKey = await crypto.hmacSha256(saltedPassword, 'Server Key')  var serverSignatureBytes = await crypto.hmacSha256(serverKey, authMessage)  session.message = 'SASLResponse'  session.serverSignature = Buffer.from(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')  }  if (typeof serverData !== 'string') {    throw new Error('SASL: SCRAM-SERVER-FINAL-MESSAGE: serverData must be a string')  }  const { serverSignature } = parseServerFinalMessage(serverData)  if (serverSignature !== session.serverSignature) {    throw new Error('SASL: SCRAM-SERVER-FINAL-MESSAGE: server signature does not match')  }}/** * printable       = %x21-2B / %x2D-7E *                   ;; Printable ASCII except ",". *                   ;; Note that any "printable" is also *                   ;; a valid "value". */function isPrintableChars(text) {  if (typeof text !== 'string') {    throw new TypeError('SASL: text must be a string')  }  return text    .split('')    .map((_, i) => text.charCodeAt(i))    .every((c) => (c >= 0x21 && c <= 0x2b) || (c >= 0x2d && c <= 0x7e))}/** * base64-char     = ALPHA / DIGIT / "/" / "+" * * base64-4        = 4base64-char * * base64-3        = 3base64-char "=" * * base64-2        = 2base64-char "==" * * base64          = *base64-4 [base64-3 / base64-2] */function isBase64(text) {  return /^(?:[a-zA-Z0-9+/]{4})*(?:[a-zA-Z0-9+/]{2}==|[a-zA-Z0-9+/]{3}=)?$/.test(text)}function parseAttributePairs(text) {  if (typeof text !== 'string') {    throw new TypeError('SASL: attribute pairs text must be a string')  }  return new Map(    text.split(',').map((attrValue) => {      if (!/^.=/.test(attrValue)) {        throw new Error('SASL: Invalid attribute pair entry')      }      const name = attrValue[0]      const value = attrValue.substring(2)      return [name, value]    })  )}function parseServerFirstMessage(data) {  const attrPairs = parseAttributePairs(data)  const nonce = attrPairs.get('r')  if (!nonce) {    throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: nonce missing')  } else if (!isPrintableChars(nonce)) {    throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: nonce must only contain printable characters')  }  const salt = attrPairs.get('s')  if (!salt) {    throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: salt missing')  } else if (!isBase64(salt)) {    throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: salt must be base64')  }  const iterationText = attrPairs.get('i')  if (!iterationText) {    throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: iteration missing')  } else if (!/^[1-9][0-9]*$/.test(iterationText)) {    throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: invalid iteration count')  }  const iteration = parseInt(iterationText, 10)  return {    nonce,    salt,    iteration,  }}function parseServerFinalMessage(serverData) {  const attrPairs = parseAttributePairs(serverData)  const serverSignature = attrPairs.get('v')  if (!serverSignature) {    throw new Error('SASL: SCRAM-SERVER-FINAL-MESSAGE: server signature is missing')  } else if (!isBase64(serverSignature)) {    throw new Error('SASL: SCRAM-SERVER-FINAL-MESSAGE: server signature must be base64')  }  return {    serverSignature,  }}function xorBuffers(a, b) {  if (!Buffer.isBuffer(a)) {    throw new TypeError('first argument must be a Buffer')  }  if (!Buffer.isBuffer(b)) {    throw new TypeError('second argument must be a Buffer')  }  if (a.length !== b.length) {    throw new Error('Buffer lengths must match')  }  if (a.length === 0) {    throw new Error('Buffers cannot be empty')  }  return Buffer.from(a.map((_, i) => a[i] ^ b[i]))}module.exports = {  startSession,  continueSession,  finalizeSession,}
 |