123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210 |
- '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')
- }
- if (typeof password !== 'string') {
- throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: client password must be a 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 saltBytes = Buffer.from(sv.salt, 'base64')
- var saltedPassword = Hi(password, saltBytes, sv.iteration)
- var clientKey = hmacSha256(saltedPassword, 'Client Key')
- var storedKey = sha256(clientKey)
- 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 = hmacSha256(storedKey, authMessage)
- var clientProofBytes = xorBuffers(clientKey, clientSignature)
- var clientProof = clientProofBytes.toString('base64')
- var serverKey = hmacSha256(saltedPassword, 'Server Key')
- var serverSignatureBytes = hmacSha256(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')
- }
- 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]))
- }
- function sha256(text) {
- return crypto.createHash('sha256').update(text).digest()
- }
- function hmacSha256(key, msg) {
- return crypto.createHmac('sha256', key).update(msg).digest()
- }
- function Hi(password, saltBytes, iterations) {
- var ui1 = hmacSha256(password, Buffer.concat([saltBytes, Buffer.from([0, 0, 0, 1])]))
- var ui = ui1
- for (var i = 0; i < iterations - 1; i++) {
- ui1 = hmacSha256(password, ui1)
- ui = xorBuffers(ui, ui1)
- }
- return ui
- }
- module.exports = {
- startSession,
- continueSession,
- finalizeSession,
- }
|