152 lines
3.9 KiB
JavaScript
152 lines
3.9 KiB
JavaScript
'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,
|
|
}
|