UNPKG

fast-srp-hap

Version:
507 lines 18.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SrpServer = exports.SrpClient = exports.SRP = void 0; const crypto_1 = __importDefault(require("crypto")); const assert_1 = __importDefault(require("assert")); const BigInteger = require("../jsbn/jsbn"); const params_1 = require("./params"); const zero = new BigInteger(0, 10); function assert_(val, msg) { if (!val) { throw new Error(msg || "assertion"); } } function assertIsBuffer(arg, argname = "arg") { assert_(Buffer.isBuffer(arg), `Type error: ${argname} must be a buffer`); } function assertIsBigInteger(arg, argname = "arg") { assert_(arg instanceof BigInteger, `Type error: ${argname} must be a BigInteger`); } /** * If a conversion is explicitly specified with the operator PAD(), * the integer will first be implicitly converted, then the resultant * byte-string will be left-padded with zeros (if necessary) until its * length equals the implicitly-converted length of N. * * @param {Buffer} n Number to pad * @param {number} len Length of the resulting Buffer * @return {Buffer} */ function padTo(n, len) { assertIsBuffer(n, "n"); const padding = len - n.length; assert_(padding > -1, "Negative padding. Very uncomfortable."); const result = Buffer.alloc(len); result.fill(0, 0, padding); n.copy(result, padding); assert_1.default.strictEqual(result.length, len); return result; } function padToN(number, params) { assertIsBigInteger(number); const n = number.toString(16).length % 2 !== 0 ? "0" + number.toString(16) : number.toString(16); return padTo(Buffer.from(n, "hex"), params.N_length_bits / 8); } /** * Compute the intermediate value x as a hash of three buffers: * salt, identity, and password. And a colon. FOUR buffers. * * x = H(s | H(I | ":" | P)) * * @param {object} params * @param {Buffer} salt * @param {Buffer} I User identity * @param {Buffer} P User password * @return {BigInteger} User secret */ function getx(params, salt, I, P) { assertIsBuffer(salt, "salt (salt)"); assertIsBuffer(I, "identity (I)"); assertIsBuffer(P, "password (P)"); const hashIP = crypto_1.default.createHash(params.hash) .update(Buffer.concat([I, Buffer.from(":"), P])) .digest(); const hashX = crypto_1.default.createHash(params.hash) .update(salt) .update(hashIP) .digest(); return new BigInteger(hashX); } class SRP { /** * The verifier is calculated as described in Section 3 of [SRP-RFC]. * We give the algorithm here for convenience. * * The verifier (v) is computed based on the salt (s), user name (I), * password (P), and group parameters (N, g). * * x = H(s | H(I | ":" | P)) * v = g^x % N * * @param {object} params Group parameters, with .N, .g, .hash * @param {Buffer} salt * @param {Buffer} I User identity * @param {Buffer} P User password * @return {Buffer} */ static computeVerifier(params, salt, I, P) { assertIsBuffer(salt, "salt (salt)"); assertIsBuffer(I, "identity (I)"); assertIsBuffer(P, "password (P)"); const v_num = params.g.modPow(getx(params, salt, I, P), params.N); return v_num.toBuffer(params.N_length_bits / 8); } static genKey(bytes = 32, callback) { // bytes is optional if (typeof bytes !== "number") { callback = bytes; bytes = 32; } if (!callback) { return new Promise((rs, rj) => SRP.genKey(bytes, (err, data) => err ? rj(err) : rs(data))); } crypto_1.default.randomBytes(bytes, (err, buf) => { if (err) { return callback(err, null); } return callback(null, buf); }); } } exports.SRP = SRP; SRP.params = params_1.params; /** * Calculate the SRP-6 multiplier. * * @param {object} params Group parameters, with .N, .g, .hash * @return {BigInteger} */ function getk(params) { const k_buf = crypto_1.default .createHash(params.hash) .update(padToN(params.N, params)) .update(padToN(params.g, params)) .digest(); return new BigInteger(k_buf); } /** * The server key exchange message also contains the server's public * value (B). The server calculates this value as B = k*v + g^b % N, * where b is a random number that SHOULD be at least 256 bits in length * and k = H(N | PAD(g)). * * Note: as the tests imply, the entire expression is mod N. * * @param {SrpParams} params Group parameters, with .N, .g, .hash * @param {BigInteger} k * @param {BigInteger} v Verifier (stored) * @param {BigInteger} b Server secret exponent * @return {Buffer} B - The server public message */ function getB(params, k, v, b) { assertIsBigInteger(v); assertIsBigInteger(k); assertIsBigInteger(b); const r = k.multiply(v).add(params.g.modPow(b, params.N)).mod(params.N); return r.toBuffer(params.N_length_bits / 8); } /** * The client key exchange message carries the client's public value * (A). The client calculates this value as A = g^a % N, where a is a * random number that SHOULD be at least 256 bits in length. * * Note: for this implementation, we take that to mean 256/8 bytes. * * @param {object} params Group parameters, with .N, .g, .hash * @param {BigInteger} a_num Client secret exponent * @return {Buffer} A - The client public message */ function getA(params, a_num) { assertIsBigInteger(a_num); if (Math.ceil(a_num.toString(16).length / 2) < 32) { console.warn("getA: client key length %d is less than the recommended 256 bits", a_num.bitLength()); } return params.g.modPow(a_num, params.N).toBuffer(params.N_length_bits / 8); } /** * getu() hashes the two public messages together, to obtain a scrambling * parameter "u" which cannot be predicted by either party ahead of time. * This makes it safe to use the message ordering defined in the SRP-6a * paper, in which the server reveals their "B" value before the client * commits to their "A" value. * * @param {object} params Group parameters, with .N, .g, .hash * @param {Buffer} A Client ephemeral public key * @param {Buffer} B Server ephemeral public key * @return {BigInteger} u - Shared scrambling parameter */ function getu(params, A, B) { assertIsBuffer(A, "A"); assertIsBuffer(B, "B"); const u_buf = crypto_1.default.createHash(params.hash) .update(padTo(A, params.N_length_bits / 8)) .update(padTo(B, params.N_length_bits / 8)) .digest(); return new BigInteger(u_buf); } /** * The TLS premaster secret as calculated by the client * * @param {SrpParams} params Group parameters, with .N, .g, .hash * @param {BigInteger} k_num * @param {BigInteger} x_num * @param {BigInteger} a_num * @param {BigInteger} B_num * @param {BigInteger} u_num * @return {Buffer} */ function client_getS(params, k_num, x_num, a_num, B_num, u_num) { assertIsBigInteger(k_num); assertIsBigInteger(x_num); assertIsBigInteger(a_num); assertIsBigInteger(B_num); assertIsBigInteger(u_num); if ((zero.compareTo(B_num) >= 0) || (params.N.compareTo(B_num) <= 0)) { throw new Error("invalid server-supplied \"B\", must be 1..N-1"); } const S_num = B_num.subtract(k_num.multiply(params.g.modPow(x_num, params.N))) .modPow(a_num.add(u_num.multiply(x_num)), params.N) .mod(params.N); return S_num.toBuffer(params.N_length_bits / 8); } /** * The TLS premastersecret as calculated by the server * * @param {BigInteger} params Group parameters, with .N, .g, .hash * @param {BigInteger} v_num Verifier (stored on server) * @param {BigInteger} A_num Ephemeral client public key (read from client) * @param {BigInteger} b_num Server ephemeral private key (generated for session) * @param {BigInteger} u_num {@see getu} * @return {Buffer} */ function server_getS(params, v_num, A_num, b_num, u_num) { assertIsBigInteger(v_num); assertIsBigInteger(A_num); assertIsBigInteger(b_num); assertIsBigInteger(u_num); if ((zero.compareTo(A_num) >= 0) || (params.N.compareTo(A_num) <= 0)) { throw new Error("invalid client-supplied \"A\", must be 1..N-1"); } const S_num = A_num.multiply(v_num.modPow(u_num, params.N)) .modPow(b_num, params.N) .mod(params.N); return S_num.toBuffer(params.N_length_bits / 8); } /** * Compute the shared session key K from S * * @param {object} params Group parameters, with .N, .g, .hash * @param {Buffer} S_buf Session key * @return {Buffer} */ function getK(params, S_buf) { assertIsBuffer(S_buf, "S"); if (params.hash === "sha1") { // use t_mgf1 interleave for short sha1 hashes return Buffer.concat([ crypto_1.default.createHash(params.hash).update(S_buf).update(Buffer.from([0, 0, 0, 0])).digest(), crypto_1.default.createHash(params.hash).update(S_buf).update(Buffer.from([0, 0, 0, 1])).digest(), ]); } else { // use hash as-is otherwise return crypto_1.default.createHash(params.hash).update(S_buf).digest(); } } function getM1(params, u_buf, s_buf, A_buf, B_buf, K_buf) { if (arguments.length > 4) { assertIsBuffer(u_buf, "identity (I)"); assertIsBuffer(s_buf, "salt (s)"); assertIsBuffer(A_buf, "client public key (A)"); assertIsBuffer(B_buf, "server public key (B)"); assertIsBuffer(K_buf, "session key (K)"); const hN = crypto_1.default.createHash(params.hash).update(params.N.toBuffer(true)).digest(); const hG = crypto_1.default.createHash(params.hash).update(params.g.toBuffer(true)).digest(); for (let i = 0; i < hN.length; i++) { hN[i] ^= hG[i]; } const hU = crypto_1.default.createHash(params.hash).update(u_buf).digest(); return crypto_1.default.createHash(params.hash) .update(hN).update(hU).update(s_buf) .update(A_buf).update(B_buf).update(K_buf) .digest(); } else { [A_buf, B_buf, s_buf] = [u_buf, s_buf, A_buf]; assertIsBuffer(A_buf, "A"); assertIsBuffer(B_buf, "B"); assertIsBuffer(s_buf, "S"); return crypto_1.default.createHash(params.hash) .update(A_buf).update(B_buf).update(s_buf) .digest(); } } function getM2(params, A_buf, M1_buf, K_buf) { assertIsBuffer(A_buf, "A"); assertIsBuffer(M1_buf, "M1"); assertIsBuffer(K_buf, "K"); return crypto_1.default.createHash(params.hash) .update(A_buf).update(M1_buf).update(K_buf) .digest(); } function equal(buf1, buf2) { // constant-time comparison. A drop in the ocean compared to our // non-constant-time modexp operations, but still good practice. return buf1.toString("hex") === buf2.toString("hex"); } class SrpClient { /** * Create an SRP client. * * @param {object} params Group parameters, with .N, .g, .hash * @param {Buffer} salt_buf User salt (from server) * @param {Buffer} identity_buf Identity/username * @param {Buffer} password_buf Password * @param {Buffer} secret1_buf Client private key {@see genKey} * @param {boolean} hap */ constructor(params, salt_buf, identity_buf, password_buf, secret1_buf, hap = true) { assertIsBuffer(salt_buf, "salt (s)"); assertIsBuffer(identity_buf, "identity (I)"); assertIsBuffer(password_buf, "password (P)"); assertIsBuffer(secret1_buf, "secret1"); this._params = params; this._k = getk(params); this._x = getx(params, salt_buf, identity_buf, password_buf); this._a = new BigInteger(secret1_buf); if (hap) { this._I = identity_buf; this._s = salt_buf; } this._A = getA(params, this._a); } /** * Returns the client's public key (A). * * @return {Buffer} */ computeA() { return this._A; } /** * Sets the server's public key (B). * * @param {Buffer} B_buf The server's public key */ setB(B_buf) { const u_num = getu(this._params, this._A, B_buf); const S_buf_x = client_getS(this._params, this._k, this._x, this._a, new BigInteger(B_buf), u_num); this._K = getK(this._params, S_buf_x); this._u = u_num; // only for tests this._S = S_buf_x; // only for tests this._B = B_buf; if (this._I && this._s) { this._M1 = getM1(this._params, this._I, this._s, this._A, this._B, this._K); } else { this._M1 = getM1(this._params, this._A, this._B, this._S); } this._M2 = getM2(this._params, this._A, this._M1, this._K); } /** * Gets the M1 value. * This requires setting the server's public key {@see Client.setB}. * * @return {Buffer} */ computeM1() { if (this._M1 === undefined) { throw new Error("incomplete protocol"); } return this._M1; } /** * Checks the server was able to calculate M2. * This requires setting the server's public key {@see Client.setB}. * * @param M2 The server's M2 value */ checkM2(M2) { if (!equal(this._M2, M2)) { throw new Error("server is not authentic"); } } /** * Returns the shared session key. * * @return {Buffer} */ computeK() { if (this._K === undefined) { throw new Error("incomplete protocol"); } return this._K; } } exports.SrpClient = SrpClient; class SrpServer { constructor(params, salt_buf, identity_buf, password_buf, secret2_buf) { this._params = params; this._k = getk(params); if (arguments.length > 3) { assertIsBuffer(salt_buf, "salt (salt)"); assertIsBuffer(identity_buf, "identity (I)"); assertIsBuffer(password_buf, "password (P)"); assertIsBuffer(secret2_buf, "secret2"); this._b = new BigInteger(secret2_buf); this._v = new BigInteger(SRP.computeVerifier(params, salt_buf, identity_buf, password_buf)); this._I = identity_buf; this._s = salt_buf; } else if (salt_buf instanceof Buffer) { const verifier_buf = salt_buf; // noinspection JSUnusedAssignment [secret2_buf, salt_buf, identity_buf, password_buf] = [identity_buf, undefined, undefined, undefined]; assertIsBuffer(verifier_buf, "verifier (v)"); assertIsBuffer(secret2_buf, "secret2"); this._b = new BigInteger(secret2_buf); this._v = new BigInteger(verifier_buf); } else { const identity = salt_buf; // noinspection JSUnusedAssignment [secret2_buf, salt_buf, identity_buf, password_buf] = [identity_buf, undefined, undefined, undefined]; // noinspection SuspiciousTypeOfGuard (0, assert_1.default)(identity.username instanceof Buffer || typeof identity.username === "string", "identity.username (I) must be a string or Buffer"); assertIsBuffer(identity.salt, "identity.salt (s)"); (0, assert_1.default)("password" in identity || "verifier" in identity, "identity requires a password or verifier"); if ("verifier" in identity) { assertIsBuffer(identity.verifier, "identity.verifier (v)"); } else { // noinspection SuspiciousTypeOfGuard (0, assert_1.default)(identity.password instanceof Buffer || typeof identity.password === "string", "identity.password (p) must be a string or Buffer"); } assertIsBuffer(secret2_buf, "secret2"); const username = typeof identity.username === "string" ? Buffer.from(identity.username) : identity.username; this._b = new BigInteger(secret2_buf); if ("verifier" in identity) { this._v = new BigInteger(identity.verifier); } else { this._v = new BigInteger(SRP.computeVerifier(params, identity.salt, username, typeof identity.password === "string" ? Buffer.from(identity.password) : identity.password)); } this._I = username; this._s = identity.salt; } this._B = getB(params, this._k, this._v, this._b); } /** * Returns the server's public key (B). * * @return {Buffer} */ computeB() { return this._B; } /** * Sets the client's public key (A). * * @param {Buffer} A The client's public key */ setA(A) { const u_num = getu(this._params, A, this._B); const S_buf = server_getS(this._params, this._v, new BigInteger(A), this._b, u_num); this._K = getK(this._params, S_buf); this._u = u_num; // only for tests this._S = S_buf; // only for tests if (this._I && this._s) { this._M1 = getM1(this._params, this._I, this._s, A, this._B, this._K); } else { this._M1 = getM1(this._params, A, this._B, this._S); } this._M2 = getM2(this._params, A, this._M1, this._K); } /** * Checks the client was able to calculate M1. * * @param {Buffer} M1 The client's M1 value */ checkM1(M1) { if (this._M1 === undefined) { throw new Error("incomplete protocol"); } if (!equal(this._M1, M1)) { throw new Error("client did not use the same password"); } } /** * Returns the shared session key. * * @return {Buffer} */ computeK() { if (this._K === undefined) { throw new Error("incomplete protocol"); } return this._K; } /** * Gets the M2 value. * This requires setting the client's public key {@see Server.setA}. * * @return {Buffer} */ computeM2() { if (this._M2 === undefined) { throw new Error("incomplete protocol"); } return this._M2; } } exports.SrpServer = SrpServer; //# sourceMappingURL=srp.js.map