/**
* @typedef {import("./connection.js").default} Connection
*/
import utils from './utils';
import log from './log.js';
/**
* @param {string} authMessage
* @param {ArrayBufferLike} clientKey
* @param {string} hashName
*/
async function scramClientProof(authMessage, clientKey, hashName) {
const storedKey = await crypto.subtle.importKey(
'raw',
await crypto.subtle.digest(hashName, clientKey),
{ 'name': 'HMAC', 'hash': hashName },
false,
['sign']
);
const clientSignature = await crypto.subtle.sign('HMAC', storedKey, utils.stringToArrayBuf(authMessage));
return utils.xorArrayBuffers(clientKey, clientSignature);
}
/**
* This function parses the information in a SASL SCRAM challenge response,
* into an object of the form
* { nonce: String,
* salt: ArrayBuffer,
* iter: Int
* }
* Returns undefined on failure.
* @param {string} challenge
*/
function scramParseChallenge(challenge) {
let nonce, salt, iter;
const attribMatch = /([a-z]+)=([^,]+)(,|$)/;
while (challenge.match(attribMatch)) {
const matches = challenge.match(attribMatch);
challenge = challenge.replace(matches[0], '');
switch (matches[1]) {
case 'r':
nonce = matches[2];
break;
case 's':
salt = utils.base64ToArrayBuf(matches[2]);
break;
case 'i':
iter = parseInt(matches[2], 10);
break;
case 'm':
// Mandatory but unknown extension, per RFC 5802 we should abort
return undefined;
default:
// Non-mandatory extension, per RFC 5802 we should ignore it
break;
}
}
// Consider iteration counts less than 4096 insecure, as reccommended by
// RFC 5802
if (isNaN(iter) || iter < 4096) {
log.warn('Failing SCRAM authentication because server supplied iteration count < 4096.');
return undefined;
}
if (!salt) {
log.warn('Failing SCRAM authentication because server supplied incorrect salt.');
return undefined;
}
return { 'nonce': nonce, 'salt': salt, 'iter': iter };
}
/**
* Derive the client and server keys given a string password,
* a hash name, and a bit length.
* Returns an object of the following form:
* { ck: ArrayBuffer, the client key
* sk: ArrayBuffer, the server key
* }
* @param {string} password
* @param {BufferSource} salt
* @param {number} iter
* @param {string} hashName
* @param {number} hashBits
*/
async function scramDeriveKeys(password, salt, iter, hashName, hashBits) {
const saltedPasswordBits = await crypto.subtle.deriveBits(
{ 'name': 'PBKDF2', 'salt': salt, 'iterations': iter, 'hash': { 'name': hashName } },
await crypto.subtle.importKey('raw', utils.stringToArrayBuf(password), 'PBKDF2', false, ['deriveBits']),
hashBits
);
const saltedPassword = await crypto.subtle.importKey(
'raw',
saltedPasswordBits,
{ 'name': 'HMAC', 'hash': hashName },
false,
['sign']
);
return {
'ck': await crypto.subtle.sign('HMAC', saltedPassword, utils.stringToArrayBuf('Client Key')),
'sk': await crypto.subtle.sign('HMAC', saltedPassword, utils.stringToArrayBuf('Server Key')),
};
}
/**
* @param {string} authMessage
* @param {BufferSource} sk
* @param {string} hashName
*/
async function scramServerSign(authMessage, sk, hashName) {
const serverKey = await crypto.subtle.importKey('raw', sk, { 'name': 'HMAC', 'hash': hashName }, false, ['sign']);
return crypto.subtle.sign('HMAC', serverKey, utils.stringToArrayBuf(authMessage));
}
/**
* Generate an ASCII nonce (not containing the ',' character)
* @return {string}
*/
function generate_cnonce() {
// generate 16 random bytes of nonce, base64 encoded
const bytes = new Uint8Array(16);
return utils.arrayBufToBase64(crypto.getRandomValues(bytes).buffer);
}
/**
* @typedef {Object} Password
* @property {string} Password.name
* @property {string} Password.ck
* @property {string} Password.sk
* @property {number} Password.iter
* @property {string} salt
*/
const scram = {
/**
* On success, sets
* connection_sasl_data["server-signature"]
* and
* connection._sasl_data.keys
*
* The server signature should be verified after this function completes..
*
* On failure, returns connection._sasl_failure_cb();
* @param {Connection} connection
* @param {string} challenge
* @param {string} hashName
* @param {number} hashBits
*/
async scramResponse(connection, challenge, hashName, hashBits) {
const cnonce = connection._sasl_data.cnonce;
const challengeData = scramParseChallenge(challenge);
// The RFC requires that we verify the (server) nonce has the client
// nonce as an initial substring.
if (!challengeData && challengeData?.nonce.slice(0, cnonce.length) !== cnonce) {
log.warn('Failing SCRAM authentication because server supplied incorrect nonce.');
connection._sasl_data = {};
return connection._sasl_failure_cb();
}
let clientKey, serverKey;
const { pass } = connection;
if (typeof connection.pass === 'string' || connection.pass instanceof String) {
const keys = await scramDeriveKeys(
/** @type {string} */ (pass),
challengeData.salt,
challengeData.iter,
hashName,
hashBits
);
clientKey = keys.ck;
serverKey = keys.sk;
} else if (
// Either restore the client key and server key passed in, or derive new ones
/** @type {Password} */ (pass)?.name === hashName &&
/** @type {Password} */ (pass)?.salt === utils.arrayBufToBase64(challengeData.salt) &&
/** @type {Password} */ (pass)?.iter === challengeData.iter
) {
const { ck, sk } = /** @type {Password} */ (pass);
clientKey = utils.base64ToArrayBuf(ck);
serverKey = utils.base64ToArrayBuf(sk);
} else {
return connection._sasl_failure_cb();
}
const clientFirstMessageBare = connection._sasl_data['client-first-message-bare'];
const serverFirstMessage = challenge;
const clientFinalMessageBare = `c=biws,r=${challengeData.nonce}`;
const authMessage = `${clientFirstMessageBare},${serverFirstMessage},${clientFinalMessageBare}`;
const clientProof = await scramClientProof(authMessage, clientKey, hashName);
const serverSignature = await scramServerSign(authMessage, serverKey, hashName);
connection._sasl_data['server-signature'] = utils.arrayBufToBase64(serverSignature);
connection._sasl_data.keys = {
'name': hashName,
'iter': challengeData.iter,
'salt': utils.arrayBufToBase64(challengeData.salt),
'ck': utils.arrayBufToBase64(clientKey),
'sk': utils.arrayBufToBase64(serverKey),
};
return `${clientFinalMessageBare},p=${utils.arrayBufToBase64(clientProof)}`;
},
/**
* Returns a string containing the client first message
* @param {Connection} connection
* @param {string} test_cnonce
*/
clientChallenge(connection, test_cnonce) {
const cnonce = test_cnonce || generate_cnonce();
const client_first_message_bare = `n=${connection.authcid},r=${cnonce}`;
connection._sasl_data.cnonce = cnonce;
connection._sasl_data['client-first-message-bare'] = client_first_message_bare;
return `n,,${client_first_message_bare}`;
},
};
export { scram as default };