thinbus-srp
Version:
Secure Remote Password SRP SRP6a implementation.
153 lines (129 loc) • 11.5 kB
JavaScript
// RFC 5054 2048bit constants
var rfc5054 = {
N_base10: "21766174458617435773191008891802753781907668374255538511144643224689886235383840957210909013086056401571399717235807266581649606472148410291413364152197364477180887395655483738115072677402235101762521901569820740293149529620419333266262073471054548368736039519702486226506248861060256971802984953561121442680157668000761429988222457090413873973970171927093992114751765168063614761119615476233422096442783117971236371647333871414335895773474667308967050807005509320424799678417036867928316761272274230314067548291133582479583061439577559347101961771406173684378522703483495337037655006751328447510550299250924469288819",
g_base10: "2",
k_base16: "5b9e8ef059c6b32ea59fc1d322d37f04aa30bae5aa9003b8321e21ddb04e300"
}
// generate the client session class from the client session factory using the safe prime constants
const SRP6JavascriptClientSession = require('../client.js')(rfc5054.N_base10, rfc5054.g_base10, rfc5054.k_base16);
// generate the server session class from the server session factory using the safe prime constants
const SRP6JavascriptServerSession = require('../server.js')(rfc5054.N_base10, rfc5054.g_base10, rfc5054.k_base16);
// ----------------------------------------------------------------------------
// CLIENT REGISTRATION FLOW
// https://simonmassey.bitbucket.io/thinbus/register.png
// Note as per RFC 2945 the user ID (usually their email) is concatenated to
// their password when generating the verifier. This means that if a user
// changes either their email address or their password you need to generate
// a new verifier and replace the old one in the database.
// ┌──────────────┐ ┌──────────────┐
// │ Browser │ │ Web Server │
// └──────────────┘ └──────┬───────┘
// │
// │
// .─. ┌─┴─┐ GET /register.html ┌───┐
// ( ) │ │◀────────────────────────────────│ │
// `┬' │ │ └───┘
// ────┼──── │ │ │
// │ email,passwd │ │
// ┌┴┐ ─────────────▶ ├──┐ │
// │ │ │ │ │ generateSalt()
// │ │ │ │ │ generateVerifier(email,passwd) │
// ──┘ └── │ │◀─┘
// │ │ │
// │ │
// │ │ │
// │ │ POST {email,salt,verifier} ┌───┐
// │ ├────────────────────────────────▶│ │
// │ │ └───┘
// └───┘ │
// │
// instantiate a client session
const client = new SRP6JavascriptClientSession();
// generate a random salt that should be stored with the user verifier
const salt = client.generateRandomSalt();
const username = "tom@arcot.com";
const password = "password1234";
// generate the users password verifier that should be stored with their salt.
const verifier = client.generateVerifier(salt, username, password);
// ┌──────────────┐ ┌──────────────┐
// │ Browser │ │ Web Server │
// └──────────────┘ └──────────────┘
// │ │
// .─. ┌───┐ GET /login.html ┌───┐
// ( ) email,passwd │ │◀────────────────────────────────│ │
// `┬' ─────────────▶│ │ └───┘ .───────────.
// ────┼──── │ │ AJAX /challenge {email} │ ( Database )
// │ │ ├ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ▶───┐ (`───────────')
// ┌┴┐ │ │ ┌─┤ │◀──────────────(`───────────')
// │ │ │ │ step1(email,salt,verifier)│ │ │{salt,verifier}(`───────────')
// │ │ │ │ │ │ │ `───────────'
// ──┘ └── │ │ └▶│ │
// │ │ {salt,B} │ │ store b .───────────.
// ┌─┤ │◀─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┤ ├──────────────▶( Cache )
// step1(email,passwd)│ │ │ └─┬─┘ (`───────────')
// step2(salt,B)│ │ │ POST /auth {email,A,M1} ┌───┐ load b (`───────────')
// └▶│ ├─────────────────────────────────│ │◀──────────────(`───────────')
// └───┘ ┌─┤ │ `───────────'
// │ step2(A,M1)│ │ │ ┌───────────────────┐
// │ │ │ │You have to retain │
// ┌─┴─┐ {M2} └▶│ │ │the private "b" │
// step3(M2)│ │◀────────────────────────────────┤ │ │which matches the │
// └─┬─┘ REDIRECT /home.html OR └─┬─┘ │public challenge │
// ┌──────────────────────┐ /login.html │"B". This can be in│
// │step3 confirms a │ │ │ │the main DB or a │
// │shared private key. A │ │cache. │
// │mobile running │ │ │ └───────────────────┘
// │embedded JavaScript │ ▼ ▼
// │also confirms the │
// │server knows the │
// │verifier that the user│
// │registered with. │
// └──────────────────────┘
// client starts with the username and password.
client.step1(username, password);
// server generates B and b, sends B to client and b to a cache
var serverWillDie = new SRP6JavascriptServerSession();
const B = serverWillDie.step1(username, salt, verifier);
const privateState = serverWillDie.toPrivateStoreState();
const cacheJson = JSON.stringify(privateState);
// store the dbJson in a temporary cache or the main DB and await client to respond to challenge B.
// return B and salt to the client.
// client creates a password proof from the salt, challenge and the username and password provided at step1. this generates `A` the cliehnt public ephemeral number and `M1` the hash of `M1` of a shared session key derived from both `A` and `B`. You post `A` and `M1` to the server (e.g. seperated by a colon) instead of a password.
var credentials = client.step2(salt, B);
// we now need to load the challenge data from the cache to check the credentials {A,M1}
const newPrivate = JSON.parse(cacheJson);
server = new SRP6JavascriptServerSession();
server.fromPrivateStoreState(newPrivate);
// the server takes `A`, internally computes `M1` based on the verifier, and checks that its `M1` matches the value sent from the client. If not it throws an exception. If the `M1` match then the password proof is valid. It then generates `M2` which is a proof that the server has the shared session key.
var M2 = server.step2(credentials.A, credentials.M1);
// client verifies that the server shows proof of the shared session key which demonstrates that it knows the verifier that matchews the password.
client.step3(M2);
// we can now use the shared session key that hasn't crossed the network for follow on cryptography (such as JWT token signing or whatever)
const clientSessionKey = client.getSessionKey();
//console.log("clientSessionKey:"+clientSessionKey);
const serverSessionKey = server.getSessionKey();
//console.log("serverSessionKey:"+serverSessionKey);
// load Unit.js module
const test = require('unit.js');
// the javascript client defaults to hashing the session key as that is additional protection of the password in case the key is accidentally exposed to an attacker.
// This the strong session key `K` as described on the [SRP design page](http://srp.stanford.edu/design.html).
// This can be used for follow on cryptography such as HMAC signing of JWT web tokens using HS256.
test.assert.equal(clientSessionKey, serverSessionKey);
// regrettibly if you browserify the client code it comes in at 694k.
// so we also ship the light weight original thinbus for browsers
// note that the verifier being used below was created by the node verion
// of the client. that proves that you can generate a temporary password verifier
// and email that to a user who can then login with a browser.
const BrowserSRP6JavascriptClientSession = require('../browser.js')(rfc5054.N_base10, rfc5054.g_base10, rfc5054.k_base16);;
//console.log(JSON.stringify(BrowserSRP6JavascriptClientSession));
const bclient = new BrowserSRP6JavascriptClientSession();
bclient.step1(username, password);
var bserver = new SRP6JavascriptServerSession();
const bB = bserver.step1(username, salt, verifier);
var bcredentials = bclient.step2(salt, bB);
var bM2 = bserver.step2(bcredentials.A, bcredentials.M1);
const bclientSessionKey = bclient.getSessionKey();
const bserverSessionKey = bserver.getSessionKey();
test.assert.equal(bclientSessionKey, bserverSessionKey);
// console.log(bclientSessionKey);
// console.log(bserverSessionKey);