134 lines
3.5 KiB
JavaScript
134 lines
3.5 KiB
JavaScript
var bitops = require('./lib/bitops');
|
|
var utils = require('./lib/utils');
|
|
|
|
var RESP = {};
|
|
var CLIENT_KEY = 'Client Key';
|
|
var SERVER_KEY = 'Server Key';
|
|
|
|
function base64decode(s) {
|
|
if (atob) {
|
|
return Uint8Array.from(atob(s), function(c) { return c.charCodeAt(0); });
|
|
} else {
|
|
return Uint8Array.from(Buffer.from(s, 'base64'));
|
|
}
|
|
}
|
|
|
|
function base64encode(s) {
|
|
if (btoa) {
|
|
return btoa(s);
|
|
} else {
|
|
return Buffer.from(s).toString('base64');
|
|
}
|
|
}
|
|
|
|
function Mechanism(options) {
|
|
options = options || {};
|
|
this._genNonce = options.genNonce || utils.genNonce;
|
|
this._stage = 'initial';
|
|
}
|
|
|
|
// Conform to the SASL lib's expectations
|
|
Mechanism.Mechanism = Mechanism;
|
|
|
|
|
|
Mechanism.prototype.name = 'SCRAM-SHA-1';
|
|
Mechanism.prototype.clientFirst = true;
|
|
|
|
|
|
Mechanism.prototype.response = function (cred) {
|
|
return RESP[this._stage](this, cred);
|
|
};
|
|
|
|
Mechanism.prototype.challenge = function (chal) {
|
|
var values = utils.parse(chal);
|
|
|
|
this._salt = base64decode(values.s || '');
|
|
this._iterationCount = parseInt(values.i, 10);
|
|
this._nonce = values.r;
|
|
this._verifier = values.v;
|
|
this._error = values.e;
|
|
this._challenge = chal;
|
|
|
|
return this;
|
|
};
|
|
|
|
|
|
RESP.initial = function (mech, cred) {
|
|
mech._cnonce = mech._genNonce();
|
|
|
|
var authzid = '';
|
|
if (cred.authzid) {
|
|
authzid = 'a=' + utils.saslname(cred.authzid);
|
|
}
|
|
|
|
mech._gs2Header = 'n,' + authzid + ',';
|
|
|
|
var nonce = 'r=' + mech._cnonce;
|
|
var username = 'n=' + utils.saslname(cred.username || '');
|
|
|
|
mech._clientFirstMessageBare = username + ',' + nonce;
|
|
var result = mech._gs2Header + mech._clientFirstMessageBare;
|
|
|
|
mech._stage = 'challenge';
|
|
|
|
return result;
|
|
};
|
|
|
|
|
|
RESP.challenge = function (mech, cred) {
|
|
var gs2Header = base64encode(mech._gs2Header);
|
|
|
|
mech._clientFinalMessageWithoutProof = 'c=' + gs2Header + ',r=' + mech._nonce;
|
|
|
|
var saltedPassword, clientKey, serverKey;
|
|
|
|
// If our cached salt is the same, we can reuse cached credentials to speed
|
|
// up the hashing process.
|
|
if (cred.salt && cred.salt.every(function(value, index) { return value === mech._salt[index]; })) {
|
|
if (cred.clientKey && cred.serverKey) {
|
|
clientKey = cred.clientKey;
|
|
serverKey = cred.serverKey;
|
|
} else if (cred.saltedPassword) {
|
|
saltedPassword = cred.saltedPassword;
|
|
clientKey = bitops.HMAC(saltedPassword, CLIENT_KEY);
|
|
serverKey = bitops.HMAC(saltedPassword, SERVER_KEY);
|
|
}
|
|
} else {
|
|
saltedPassword = bitops.Hi(cred.password || '', mech._salt, mech._iterationCount);
|
|
clientKey = bitops.HMAC(saltedPassword, CLIENT_KEY);
|
|
serverKey = bitops.HMAC(saltedPassword, SERVER_KEY);
|
|
}
|
|
|
|
var storedKey = bitops.H(clientKey);
|
|
var authMessage = mech._clientFirstMessageBare + ',' +
|
|
mech._challenge + ',' +
|
|
mech._clientFinalMessageWithoutProof;
|
|
var clientSignature = bitops.HMAC(storedKey, authMessage);
|
|
|
|
var clientProof = base64encode(String.fromCharCode.apply(null, bitops.XOR(clientKey, clientSignature)));
|
|
|
|
mech._serverSignature = bitops.HMAC(serverKey, authMessage);
|
|
|
|
var result = mech._clientFinalMessageWithoutProof + ',p=' + clientProof;
|
|
|
|
mech._stage = 'final';
|
|
|
|
mech.cache = {
|
|
salt: mech._salt,
|
|
saltedPassword: saltedPassword,
|
|
clientKey: clientKey,
|
|
serverKey: serverKey
|
|
};
|
|
|
|
return result;
|
|
};
|
|
|
|
RESP.final = function () {
|
|
// TODO: Signal errors
|
|
return '';
|
|
};
|
|
|
|
|
|
|
|
module.exports = Mechanism;
|