Initial commit of SRP package.

This commit is contained in:
Nick Martin
2012-06-28 23:17:35 -07:00
parent 67b9ee0d10
commit 2cdf596bfc
5 changed files with 1894 additions and 0 deletions

1279
packages/srp/biginteger.js Normal file

File diff suppressed because it is too large Load Diff

15
packages/srp/package.js Normal file
View File

@@ -0,0 +1,15 @@
Package.describe({
summary: "Library for Secure Remote Password (SRP) exchanges",
internal: true
});
Package.on_use(function (api) {
api.use('uuid', ['client', 'server']);
api.add_files(['biginteger.js', 'sha256.js', 'srp.js'],
['client', 'server']);
});
Package.on_test(function (api) {
api.use('srp', ['client', 'server']);
api.add_files(['srp_tests.js'], ['client', 'server']);
});

140
packages/srp/sha256.js Normal file
View File

@@ -0,0 +1,140 @@
/// METEOR WRAPPER
//
// XXX this should get packaged and moved into the Meteor.crypto
// namespace, along with other hash functions.
if (typeof Meteor._srp === "undefined")
Meteor._srp = {};
Meteor._srp.SHA256 = (function () {
/**
*
* Secure Hash Algorithm (SHA256)
* http://www.webtoolkit.info/
*
* Original code by Angel Marin, Paul Johnston.
*
**/
function SHA256(s){
var chrsz = 8;
var hexcase = 0;
function safe_add (x, y) {
var lsw = (x & 0xFFFF) + (y & 0xFFFF);
var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
return (msw << 16) | (lsw & 0xFFFF);
}
function S (X, n) { return ( X >>> n ) | (X << (32 - n)); }
function R (X, n) { return ( X >>> n ); }
function Ch(x, y, z) { return ((x & y) ^ ((~x) & z)); }
function Maj(x, y, z) { return ((x & y) ^ (x & z) ^ (y & z)); }
function Sigma0256(x) { return (S(x, 2) ^ S(x, 13) ^ S(x, 22)); }
function Sigma1256(x) { return (S(x, 6) ^ S(x, 11) ^ S(x, 25)); }
function Gamma0256(x) { return (S(x, 7) ^ S(x, 18) ^ R(x, 3)); }
function Gamma1256(x) { return (S(x, 17) ^ S(x, 19) ^ R(x, 10)); }
function core_sha256 (m, l) {
var K = new Array(0x428A2F98, 0x71374491, 0xB5C0FBCF, 0xE9B5DBA5, 0x3956C25B, 0x59F111F1, 0x923F82A4, 0xAB1C5ED5, 0xD807AA98, 0x12835B01, 0x243185BE, 0x550C7DC3, 0x72BE5D74, 0x80DEB1FE, 0x9BDC06A7, 0xC19BF174, 0xE49B69C1, 0xEFBE4786, 0xFC19DC6, 0x240CA1CC, 0x2DE92C6F, 0x4A7484AA, 0x5CB0A9DC, 0x76F988DA, 0x983E5152, 0xA831C66D, 0xB00327C8, 0xBF597FC7, 0xC6E00BF3, 0xD5A79147, 0x6CA6351, 0x14292967, 0x27B70A85, 0x2E1B2138, 0x4D2C6DFC, 0x53380D13, 0x650A7354, 0x766A0ABB, 0x81C2C92E, 0x92722C85, 0xA2BFE8A1, 0xA81A664B, 0xC24B8B70, 0xC76C51A3, 0xD192E819, 0xD6990624, 0xF40E3585, 0x106AA070, 0x19A4C116, 0x1E376C08, 0x2748774C, 0x34B0BCB5, 0x391C0CB3, 0x4ED8AA4A, 0x5B9CCA4F, 0x682E6FF3, 0x748F82EE, 0x78A5636F, 0x84C87814, 0x8CC70208, 0x90BEFFFA, 0xA4506CEB, 0xBEF9A3F7, 0xC67178F2);
var HASH = new Array(0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19);
var W = new Array(64);
var a, b, c, d, e, f, g, h, i, j;
var T1, T2;
m[l >> 5] |= 0x80 << (24 - l % 32);
m[((l + 64 >> 9) << 4) + 15] = l;
for ( var i = 0; i<m.length; i+=16 ) {
a = HASH[0];
b = HASH[1];
c = HASH[2];
d = HASH[3];
e = HASH[4];
f = HASH[5];
g = HASH[6];
h = HASH[7];
for ( var j = 0; j<64; j++) {
if (j < 16) W[j] = m[j + i];
else W[j] = safe_add(safe_add(safe_add(Gamma1256(W[j - 2]), W[j - 7]), Gamma0256(W[j - 15])), W[j - 16]);
T1 = safe_add(safe_add(safe_add(safe_add(h, Sigma1256(e)), Ch(e, f, g)), K[j]), W[j]);
T2 = safe_add(Sigma0256(a), Maj(a, b, c));
h = g;
g = f;
f = e;
e = safe_add(d, T1);
d = c;
c = b;
b = a;
a = safe_add(T1, T2);
}
HASH[0] = safe_add(a, HASH[0]);
HASH[1] = safe_add(b, HASH[1]);
HASH[2] = safe_add(c, HASH[2]);
HASH[3] = safe_add(d, HASH[3]);
HASH[4] = safe_add(e, HASH[4]);
HASH[5] = safe_add(f, HASH[5]);
HASH[6] = safe_add(g, HASH[6]);
HASH[7] = safe_add(h, HASH[7]);
}
return HASH;
}
function str2binb (str) {
var bin = Array();
var mask = (1 << chrsz) - 1;
for(var i = 0; i < str.length * chrsz; i += chrsz) {
bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (24 - i%32);
}
return bin;
}
function Utf8Encode(string) {
string = string.replace(/\r\n/g,"\n");
var utftext = "";
for (var n = 0; n < string.length; n++) {
var c = string.charCodeAt(n);
if (c < 128) {
utftext += String.fromCharCode(c);
}
else if((c > 127) && (c < 2048)) {
utftext += String.fromCharCode((c >> 6) | 192);
utftext += String.fromCharCode((c & 63) | 128);
}
else {
utftext += String.fromCharCode((c >> 12) | 224);
utftext += String.fromCharCode(((c >> 6) & 63) | 128);
utftext += String.fromCharCode((c & 63) | 128);
}
}
return utftext;
}
function binb2hex (binarray) {
var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
var str = "";
for(var i = 0; i < binarray.length * 4; i++) {
str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) +
hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8 )) & 0xF);
}
return str;
}
s = Utf8Encode(s);
return binb2hex(core_sha256(str2binb(s), s.length * chrsz));
}
/// METEOR WRAPPER
return SHA256;
})();

341
packages/srp/srp.js Normal file
View File

@@ -0,0 +1,341 @@
(function () {
if (typeof Meteor._srp === "undefined")
Meteor._srp = {};
/////// PUBLIC CLIENT
/**
* Generate a new SRP verifier. Password is the plaintext password.
*
* options is optional and can include:
* - identity: String. The SRP username to user. Mostly this is passed
* in for testing. Random UUID if not provided.
* - salt: String. A salt to use. Mostly this is passed in for
* testing. Random UUID if not provided.
* - SRP parameters (see _defaults and paramsFromOptions below)
*/
Meteor._srp.generateVerifier = function (password, options) {
var params = paramsFromOptions(options);
var identity = (options && options.identity) || Meteor.uuid();
var salt = (options && options.salt) || Meteor.uuid();
var x = params.hash(salt + params.hash(identity + ":" + password));
var xi = new Meteor._srp.BigInteger(x, 16);
var v = params.g.modPow(xi, params.N);
return {
identity: identity,
salt: salt,
verifier: v.toString(16)
};
};
/**
* Generate a new SRP client object. Password is the plaintext password.
*
* options is optional and can include:
* - a: client's private ephemeral value. String or
* BigInteger. Normally, this is picked randomly, but it can be
* passed in for testing.
* - SRP parameters (see _defaults and paramsFromOptions below)
*/
Meteor._srp.Client = function (password, options) {
var self = this;
self.params = paramsFromOptions(options);
self.password = password;
// shorthand
var N = self.params.N;
var g = self.params.g;
// construct public and private keys.
var a, A;
if (options && options.a) {
if (typeof options.a === "string")
a = new Meteor._srp.BigInteger(options.a, 16);
else if (options.a instanceof Meteor._srp.BigInteger)
a = options.a;
else
throw new Error("Invalid parameter: a");
A = g.modPow(a, N);
if (A.mod(N) === 0)
throw new Error("Invalid parameter: a: A mod N == 0.");
} else {
while (!A || A.mod(N) === 0) {
a = randInt();
A = g.modPow(a, N);
}
}
self.a = a;
self.A = A;
self.Astr = A.toString(16);
};
/**
* Initiate an SRP exchange.
*
* returns { A: 'client public ephemeral key. hex encoded integer.' }
*/
Meteor._srp.Client.prototype.startExchange = function () {
var self = this;
return {
A: self.Astr
};
};
/**
* Respond to the server's challenge with a proof of password.
*
* challenge is an object with
* - B: server public ephemeral key. hex encoded integer.
* - identity: user's identity (SRP username).
* - salt: user's salt.
*
* returns { M: 'client proof of password. hex encoded integer.' }
* throws an error if it got an invalid challenge.
*/
Meteor._srp.Client.prototype.respondToChallenge = function (challenge) {
var self = this;
// shorthand
var N = self.params.N;
var g = self.params.g;
var k = self.params.k;
var H = self.params.hash;
// XXX check for missing / bad parameters.
self.identity = challenge.identity;
self.salt = challenge.salt;
self.Bstr = challenge.B;
self.B = new Meteor._srp.BigInteger(self.Bstr, 16);
if (self.B.mod(N) === 0)
throw new Error("Server sent invalid key: B mod N == 0.");
var u = new Meteor._srp.BigInteger(H(self.Astr + self.Bstr), 16);
var x = new Meteor._srp.BigInteger(
H(self.salt + H(self.identity + ":" + self.password)), 16);
var kgx = k.multiply(g.modPow(x, N));
var aux = self.a.add(u.multiply(x));
var S = self.B.subtract(kgx).modPow(aux, N);
var M = H(self.Astr + self.Bstr + S.toString(16));
var HAMK = H(self.Astr + M + S.toString(16));
self.S = S;
self.HAMK = HAMK;
return {
M: M
};
};
/**
* Verify server's confirmation message.
*
* confirmation is an object with
* - HAMK: server's proof of password.
*
* returns true or false.
*/
Meteor._srp.Client.prototype.verifyConfirmation = function (confirmation) {
var self = this;
return (self.HAMK && (confirmation.HAMK === self.HAMK));
};
/////// PUBLIC SERVER
/**
* Generate a new SRP server object. Password is the plaintext password.
*
* options is optional and can include:
* - b: server's private ephemeral value. String or
* BigInteger. Normally, this is picked randomly, but it can be
* passed in for testing.
* - SRP parameters (see _defaults and paramsFromOptions below)
*/
Meteor._srp.Server = function (verifier, options) {
var self = this;
self.params = paramsFromOptions(options);
self.verifier = verifier;
// shorthand
var N = self.params.N;
var g = self.params.g;
var k = self.params.k;
var v = new Meteor._srp.BigInteger(self.verifier.verifier, 16);
// construct public and private keys.
var b, B;
if (options && options.b) {
if (typeof options.b === "string")
b = new Meteor._srp.BigInteger(options.b, 16);
else if (options.b instanceof Meteor._srp.BigInteger)
b = options.b;
else
throw new Error("Invalid parameter: b");
B = k.multiply(v).add(g.modPow(b, N)).mod(N);
if (B.mod(N) === 0)
throw new Error("Invalid parameter: b: B mod N == 0.");
} else {
while (!B || B.mod(N) === 0) {
b = randInt();
B = k.multiply(v).add(g.modPow(b, N)).mod(N);
}
}
self.b = b;
self.B = B;
self.Bstr = B.toString(16);
};
/**
* Issue a challenge to the client.
*
* Takes a request from the client containing:
* - A: hex encoded int.
*
* Returns a challenge with:
* - B: server public ephemeral key. hex encoded integer.
* - identity: user's identity (SRP username).
* - salt: user's salt.
*
* Throws an error if issued a bad request.
*/
Meteor._srp.Server.prototype.issueChallenge = function (request) {
var self = this;
// XXX check for missing / bad parameters.
self.Astr = request.A;
self.A = new Meteor._srp.BigInteger(self.Astr, 16);
if (self.A.mod(self.params.N) === 0)
throw new Error("Client sent invalid key: A mod N == 0.");
// shorthand
var N = self.params.N;
var H = self.params.hash;
// Compute M and HAMK in advance. Don't send to client yet.
var u = new Meteor._srp.BigInteger(H(self.Astr + self.Bstr), 16);
var v = new Meteor._srp.BigInteger(self.verifier.verifier, 16);
var avu = self.A.multiply(v.modPow(u, N));
self.S = avu.modPow(self.b, N);
self.M = H(self.Astr + self.Bstr + self.S.toString(16));
self.HAMK = H(self.Astr + self.M + self.S.toString(16));
return {
identity: self.verifier.identity,
salt: self.verifier.salt,
B: self.Bstr
};
};
/**
* Verify a response from the client and return confirmation.
*
* Takes a challenge response from the client containing:
* - M: client proof of password. hex encoded int.
*
* Returns a confirmation if the client's proof is good:
* - HAMK: server proof of password. hex encoded integer.
* OR null if the client's proof doesn't match.
*/
Meteor._srp.Server.prototype.verifyResponse = function (response) {
var self = this;
if (response.M !== self.M)
return null;
return {
HAMK: self.HAMK
};
};
/////// INTERNAL
/**
* Default parameter values for SRP.
*
*/
Meteor._srp._defaults = {
hash: function (x) { return Meteor._srp.SHA256(x).toLowerCase(); },
N: new Meteor._srp.BigInteger("EEAF0AB9ADB38DD69C33F80AFA8FC5E86072618775FF3C0B9EA2314C9C256576D674DF7496EA81D3383B4813D692C6E0E0D5D8E250B98BE48E495C1D6089DAD15DC7D7B46154D6B6CE8EF4AD69B15D4982559B297BCF1885C529F566660E57EC68EDBC3C05726CC02FD4CBF4976EAA9AFD5138FE8376435B9FC61D2FC0EB06E3", 16),
g: new Meteor._srp.BigInteger("2")
};
Meteor._srp._defaults.k = new Meteor._srp.BigInteger(
Meteor._srp._defaults.hash(
Meteor._srp._defaults.N.toString(16) +
Meteor._srp._defaults.g.toString(16)),
16);
/**
* Process an options hash to create SRP parameters.
*
* Options can include:
* - hash: Function. Defaults to SHA256.
* - N: String or BigInteger. Defaults to 1024 bit value from RFC 5054
* - g: String or BigInteger. Defaults to 2.
* - k: String or BigInteger. Defaults to hash(N, g)
*/
var paramsFromOptions = function (options) {
if (!options) // fast path
return Meteor._srp._defaults;
var ret = _.extend({}, Meteor._srp._defaults);
_.each(['N', 'g', 'k'], function (p) {
if (options[p]) {
if (typeof options[p] === "string")
ret[p] = new Meteor._srp.BigInteger(options[p], 16);
else if (options[p] instanceof Meteor._srp.BigInteger)
ret[p] = options[p];
else
throw new Error("Invalid parameter: " + p);
}
});
if (options.hash)
ret.hash = function (x) { return options.hash(x).toLowerCase(); };
if (!options.k && (options.N || options.g || options.hash)) {
ret.k = ret.hash(ret.N.toString(16) + ret.g.toString(16));
}
return ret;
};
var randInt = function () {
// XXX XXX need a better implementation!
return new Meteor._srp.BigInteger(Meteor.uuid().replace(/-/g, ''), 16);
};
})();

119
packages/srp/srp_tests.js Normal file
View File

@@ -0,0 +1,119 @@
(function() {
Tinytest.add("srp - good exchange", function(test) {
var password = 'hi there!';
var verifier = Meteor._srp.generateVerifier(password);
var C = new Meteor._srp.Client(password);
var S = new Meteor._srp.Server(verifier);
var request = C.startExchange();
var challenge = S.issueChallenge(request);
var response = C.respondToChallenge(challenge);
var confirmation = S.verifyResponse(response);
test.isTrue(confirmation);
test.isTrue(C.verifyConfirmation(confirmation));
});
Tinytest.add("srp - bad exchange", function(test) {
var verifier = Meteor._srp.generateVerifier('one password');
var C = new Meteor._srp.Client('another password');
var S = new Meteor._srp.Server(verifier);
var request = C.startExchange();
var challenge = S.issueChallenge(request);
var response = C.respondToChallenge(challenge);
var confirmation = S.verifyResponse(response);
test.isFalse(confirmation);
});
Tinytest.add("srp - fixed values", function(test) {
// Test exact values during the exchange. We have to be very careful
// about changing the SRP code, because changes could render
// people's existing user database unusable. This test is
// intentionally brittle to catch change that could affect the
// validity of user passwords.
var identity = "b73d9af9-4e74-4ce0-879c-484828b08436";
var salt = "85f8b9d3-744a-487d-8982-a50e4c9f552a";
var password = "95109251-3d8a-4777-bdec-44ffe8d86dfb";
var a = "dc99c646fa4cb7c24314bb6f4ca2d391297acd0dacb0430a13bbf1e37dcf8071";
var b = "cf878e00c9f2b6aa48a10f66df9706e64fef2ca399f396d65f5b0a27cb8ae237";
var verifier = Meteor._srp.generateVerifier(
password, {identity: identity, salt: salt});
var C = new Meteor._srp.Client(password, {a: a});
var S = new Meteor._srp.Server(verifier, {b: b});
var request = C.startExchange();
test.equal(request.A, "8a75aa61471a92d4c3b5d53698c910af5ef013c42799876c40612d1d5e0dc41d01f669bc022fadcd8a704030483401a1b86b8670191bd9dfb1fb506dd11c688b2f08e9946756263954db2040c1df1894af7af5f839c9215bb445268439157e65e8f100469d575d5d0458e19e8bd4dd4ea2c0b30b1b3f4f39264de4ec596e0bb7");
var challenge = S.issueChallenge(request);
test.equal(challenge.B, "77ab0a40ef428aa2fa2bc257c905f352c7f75fbcfdb8761393c9dc0f730bbb0270ba9f837545b410c955c3f761494b329ad23c6efdec7e63509e538c2f68a3526e072550a11dac46017718362205e0c698b5bed67d6ff475aa92c191ca169f865c81a1a577373c449b98df720c7b7ff50536f9919d781e698025fd7164932ba7");
var response = C.respondToChallenge(challenge);
test.equal(response.M, "8705d31bb61497279adf44eef6c167dcb7e03aa7a42102c1ea7e73025fbd4cd9");
var confirmation = S.verifyResponse(response);
test.equal(confirmation.HAMK, "07a0f200392fa9a084db7acc2021fbc174bfb36956b46835cc12506b68b27bba");
test.isTrue(C.verifyConfirmation(confirmation));
});
Tinytest.add("srp - options", function(test) {
// test that all options are respected.
//
// Note, all test strings here should be hex, because the 'hash'
// function needs to output numbers.
var baseOptions = {
hash: function (x) { return x; },
N: 'b',
g: '2',
k: '1'
};
var verifierOptions = _.extend({
identity: 'a',
salt: 'b'
}, baseOptions);
var clientOptions = _.extend({
a: "2"
}, baseOptions);
var serverOptions = _.extend({
b: "2"
}, baseOptions);
var verifier = Meteor._srp.generateVerifier('c', verifierOptions);;
test.equal(verifier.identity, 'a');
test.equal(verifier.salt, 'b');
test.equal(verifier.verifier, '3');
var C = new Meteor._srp.Client('c', clientOptions);
var S = new Meteor._srp.Server(verifier, serverOptions);
var request = C.startExchange();
test.equal(request.A, '4');
var challenge = S.issueChallenge(request);
test.equal(challenge.identity, 'a');
test.equal(challenge.salt, 'b');
test.equal(challenge.B, '7');
var response = C.respondToChallenge(challenge);
test.equal(response.M, '471');
var confirmation = S.verifyResponse(response);
test.isTrue(confirmation);
test.equal(confirmation.HAMK, '44711');
});
})();