From ec5636a7eedb036d65dd37f0ba6f14ca778910e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Wed, 23 May 2018 11:55:31 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=A5=20Pad=20values=20following=20RFC50?= =?UTF-8?q?54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #12 Migration guide: While verifiers generated with an earlier version of this library will continue to work, the client and the server must either both be using the padding, or both not. The recommended upgrade path is to have both versions running on your server, which gives you time to upgrade all the clients. This can be acomplished by depending on this library two times: ```js { // ... "dependencies": { // ... "secure-remote-password": "^0.4.0", "secure-remote-password-pre-rfc5054": "LinusU/secure-remote-password#pre-rfc5054" } } ``` You will then have to implement another version of your endpoint, which uses the newer version. ```js const loginCtrl1 = require('./controller/login-v1') const loginCtrl2 = require('./controller/login-v2') app.post('/v1/login-sessions', loginCtrl1.initiate) app.post('/v1/login-sessions/:sessionId/finalize', loginCtrl1.finalize) app.post('/v2/login-sessions', loginCtrl2.initiate) app.post('/v2/login-sessions/:sessionId/finalize', loginCtrl2.finalize) ``` The `login-v1` controller should import the library as such: ```js require('secure-remote-password-pre-rfc5054/server') ``` The `login-v2` controller should import the library as usual: ```js require('secure-remote-password/server') ``` --- browser/sha256.js | 3 +++ client.js | 55 +++++++++++++++++++++++----------------------- lib/params.js | 22 ++++++++++++++----- lib/sha256.js | 3 +++ lib/srp-integer.js | 8 +++++++ server.js | 37 ++++++++++++++++--------------- 6 files changed, 77 insertions(+), 51 deletions(-) diff --git a/browser/sha256.js b/browser/sha256.js index 34cb349..edd7932 100644 --- a/browser/sha256.js +++ b/browser/sha256.js @@ -19,6 +19,9 @@ function concat (buffers) { return combined.buffer } +/** + * @param {(string | SRPInteger)[]} args + */ module.exports = function sha256 (...args) { const buffer = concat(args.map((arg) => { if (arg instanceof SRPInteger) { diff --git a/client.js b/client.js index 80d8013..f3ba81a 100644 --- a/client.js +++ b/client.js @@ -4,19 +4,19 @@ const params = require('./lib/params') const SRPInteger = require('./lib/srp-integer') exports.generateSalt = function () { - // s User's salt + // s User's salt const s = SRPInteger.randomInteger(params.hashOutputBytes) return s.toHex() } exports.derivePrivateKey = function (salt, username, password) { - // H() One-way hash function + // H() One-way hash function const { H } = params - // s User's salt - // I Username - // p Cleartext Password + // s User's salt + // I Username + // p Cleartext Password const s = SRPInteger.fromHex(salt) const I = String(username) const p = String(password) @@ -28,11 +28,11 @@ exports.derivePrivateKey = function (salt, username, password) { } exports.deriveVerifier = function (privateKey) { - // N A large safe prime (N = 2q+1, where q is prime) - // g A generator modulo N + // N A large safe prime (N = 2q+1, where q is prime) + // g A generator modulo N const { N, g } = params - // x Private key (derived from p and s) + // x Private key (derived from p and s) const x = SRPInteger.fromHex(privateKey) // v = g^x (computes password verifier) @@ -42,8 +42,8 @@ exports.deriveVerifier = function (privateKey) { } exports.generateEphemeral = function () { - // N A large safe prime (N = 2q+1, where q is prime) - // g A generator modulo N + // N A large safe prime (N = 2q+1, where q is prime) + // g A generator modulo N const { N, g } = params // A = g^a (a = random number) @@ -57,17 +57,18 @@ exports.generateEphemeral = function () { } exports.deriveSession = function (clientSecretEphemeral, serverPublicEphemeral, salt, username, privateKey) { - // N A large safe prime (N = 2q+1, where q is prime) - // g A generator modulo N - // k Multiplier parameter (k = H(N, g) in SRP-6a, k = 3 for legacy SRP-6) - // H() One-way hash function - const { N, g, k, H } = params - - // a Secret ephemeral values - // B Public ephemeral values - // s User's salt - // I Username - // x Private key (derived from p and s) + // N A large safe prime (N = 2q+1, where q is prime) + // g A generator modulo N + // k Multiplier parameter (k = H(N, g) in SRP-6a, k = 3 for legacy SRP-6) + // H() One-way hash function + // PAD() Pad the number to have the same number of bytes as N + const { N, g, k, H, PAD } = params + + // a Secret ephemeral values + // B Public ephemeral values + // s User's salt + // I Username + // x Private key (derived from p and s) const a = SRPInteger.fromHex(clientSecretEphemeral) const B = SRPInteger.fromHex(serverPublicEphemeral) const s = SRPInteger.fromHex(salt) @@ -83,8 +84,8 @@ exports.deriveSession = function (clientSecretEphemeral, serverPublicEphemeral, throw new Error('The server sent an invalid public ephemeral') } - // u = H(A, B) - const u = H(A, B) + // u = H(PAD(A), PAD(B)) + const u = H(PAD(A), PAD(B)) // S = (B - kg^x) ^ (a + ux) const S = B.subtract(k.multiply(g.modPow(x, N))).modPow(a.add(u.multiply(x)), N) @@ -102,12 +103,12 @@ exports.deriveSession = function (clientSecretEphemeral, serverPublicEphemeral, } exports.verifySession = function (clientPublicEphemeral, clientSession, serverSessionProof) { - // H() One-way hash function + // H() One-way hash function const { H } = params - // A Public ephemeral values - // M Proof of K - // K Shared, strong session key + // A Public ephemeral values + // M Proof of K + // K Shared, strong session key const A = SRPInteger.fromHex(clientPublicEphemeral) const M = SRPInteger.fromHex(clientSession.proof) const K = SRPInteger.fromHex(clientSession.key) diff --git a/lib/params.js b/lib/params.js index f21b262..0b76633 100644 --- a/lib/params.js +++ b/lib/params.js @@ -18,16 +18,26 @@ const input = { `, generatorModulo: '02', hashFunction: 'sha256', - hashOutputBytes: (256 / 8) + hashOutputBytes: (256 / 8), + paddedLength: 512 } -// N A large safe prime (N = 2q+1, where q is prime) -// g A generator modulo N -// k Multiplier parameter (k = H(N, g) in SRP-6a, k = 3 for legacy SRP-6) -// H() One-way hash function +/** + * @param {SRPInteger} integer + */ +function pad (integer) { + return integer.pad(input.paddedLength) +} + +// N A large safe prime (N = 2q+1, where q is prime) +// g A generator modulo N +// k Multiplier parameter (k = H(N, g) in SRP-6a, k = 3 for legacy SRP-6) +// H() One-way hash function +// PAD() Pad the number to have the same number of bytes as N exports.N = SRPInteger.fromHex(input.largeSafePrime.replace(/\s+/g, '')) exports.g = SRPInteger.fromHex(input.generatorModulo.replace(/\s+/g, '')) -exports.k = sha256(exports.N, exports.g) +exports.k = sha256(exports.N, pad(exports.g)) exports.H = sha256 +exports.PAD = pad exports.hashOutputBytes = input.hashOutputBytes diff --git a/lib/sha256.js b/lib/sha256.js index 229e884..12ac521 100644 --- a/lib/sha256.js +++ b/lib/sha256.js @@ -4,6 +4,9 @@ const crypto = require('crypto') const SRPInteger = require('./srp-integer') +/** + * @param {(string | SRPInteger)[]} args + */ module.exports = function sha256 (...args) { const h = crypto.createHash('sha256') diff --git a/lib/srp-integer.js b/lib/srp-integer.js index 9cc67c9..93cc18c 100644 --- a/lib/srp-integer.js +++ b/lib/srp-integer.js @@ -41,6 +41,14 @@ class SRPInteger { return new SRPInteger(this[kBigInteger].xor(val[kBigInteger]), this[kHexLength]) } + pad (hexLength) { + if (hexLength < this[kHexLength]) { + throw new Error('Cannot pad to a shorter length') + } + + return new SRPInteger(this[kBigInteger], hexLength) + } + inspect () { const hex = this[kBigInteger].toString(16) diff --git a/server.js b/server.js index 9674064..c8b0ab6 100644 --- a/server.js +++ b/server.js @@ -4,12 +4,12 @@ const params = require('./lib/params') const SRPInteger = require('./lib/srp-integer') exports.generateEphemeral = function (verifier) { - // N A large safe prime (N = 2q+1, where q is prime) - // g A generator modulo N - // k Multiplier parameter (k = H(N, g) in SRP-6a, k = 3 for legacy SRP-6) + // N A large safe prime (N = 2q+1, where q is prime) + // g A generator modulo N + // k Multiplier parameter (k = H(N, g) in SRP-6a, k = 3 for legacy SRP-6) const { N, g, k } = params - // v Password verifier + // v Password verifier const v = SRPInteger.fromHex(verifier) // B = kv + g^b (b = random number) @@ -23,18 +23,19 @@ exports.generateEphemeral = function (verifier) { } exports.deriveSession = function (serverSecretEphemeral, clientPublicEphemeral, salt, username, verifier, clientSessionProof) { - // N A large safe prime (N = 2q+1, where q is prime) - // g A generator modulo N - // k Multiplier parameter (k = H(N, g) in SRP-6a, k = 3 for legacy SRP-6) - // H() One-way hash function - const { N, g, k, H } = params - - // b Secret ephemeral values - // A Public ephemeral values - // s User's salt - // p Cleartext Password - // I Username - // v Password verifier + // N A large safe prime (N = 2q+1, where q is prime) + // g A generator modulo N + // k Multiplier parameter (k = H(N, g) in SRP-6a, k = 3 for legacy SRP-6) + // H() One-way hash function + // PAD() Pad the number to have the same number of bytes as N + const { N, g, k, H, PAD } = params + + // b Secret ephemeral values + // A Public ephemeral values + // s User's salt + // p Cleartext Password + // I Username + // v Password verifier const b = SRPInteger.fromHex(serverSecretEphemeral) const A = SRPInteger.fromHex(clientPublicEphemeral) const s = SRPInteger.fromHex(salt) @@ -50,8 +51,8 @@ exports.deriveSession = function (serverSecretEphemeral, clientPublicEphemeral, throw new Error('The client sent an invalid public ephemeral') } - // u = H(A, B) - const u = H(A, B) + // u = H(PAD(A), PAD(B)) + const u = H(PAD(A), PAD(B)) // S = (Av^u) ^ b (computes session key) const S = A.multiply(v.modPow(u, N)).modPow(b, N)