js.thinbus-srp6client.js Maven / Gradle / Ivy
Show all versions of thinbus-srp6a-js Show documentation
/**
* Thinbus Javascript Secure Remote Password (SRP)
* Version ${project.version}
* Copyright 2014-2015 Simon Massey
* http://www.apache.org/licenses/LICENSE-2.0
*/
function SRP6JavascriptClientSession() {
"use strict";
/**
* The session is initialised and ready to begin authentication
* by proceeding to {@link #STEP_1}.
*/
this.INIT = 0;
/**
* The authenticating user has input their identity 'I'
* (username) and password 'P'. The session is ready to proceed
* to {@link #STEP_2}.
*/
this.STEP_1 = 1;
/**
* The user identity 'I' is submitted to the server which has
* replied with the matching salt 's' and its public value 'B'
* based on the user's password verifier 'v'. The session is
* ready to proceed to {@link #STEP_3}.
*/
this.STEP_2 = 2;
/**
* The client public key 'A' and evidence message 'M1' are
* submitted and the server has replied with own evidence
* message 'M2'. The session is finished (authentication was
* successful or failed).
*/
this.STEP_3 = 3;
this.state = this.INIT;
this.x = null; // salted hashed password
this.v = null; // verifier
this.I = null; // identity
this.P = null; // password, nulled after use
this.salt = null; // salt
this.B = null; // server public key
this.A = null; // client public key
this.a = null; // client private key
this.k = null; // constant computed by the server
this.u = null; // blended public keys
this.S = null; // shared secret key long form
this.K = null; // shared secret hashed form
this.M1str = null; // password proof
// private
this.check = function(v, name) {
if( typeof v === 'undefined' || v === null || v === "" || v === "0" ) {
throw new Error(name+" must not be null, empty or zero");
}
};
/** private
*
* Computes x = H(s | H(I | ":" | P))
*
Uses string concatenation before hashing.
*
Specification RFC 2945
*
* @param salt The salt 's'. Must not be null or empty.
* @param identity The user identity/email 'I'. Must not be null or empty.
* @param password The user password 'P'. Must not be null or empty
* @return The resulting 'x' value as BigInteger.
*/
this.generateX = function(salt, identity, password) {
this.check(salt, "salt");
this.check(identity, "identity");
this.check(password, "password");
//console.log("js salt:"+salt);
//console.log("js i:"+identity);
//console.log("js p:"+password);
this.salt = salt;
var hash1 = this.H(identity+':'+password);
// server BigInteger math will trim leading zeros so we must do likewise to get a match
while (hash1.substring(0, 1) === '0') {
//console.log("stripping leading zero from M1");
hash1 = hash1.substring(1);
}
//console.log("js hash1:"+hash1);
//console.log("js salt:"+salt);
var concat = (salt+hash1).toUpperCase();
//console.log("js concat:"+concat);
var hash = this.H(concat);
// Java BigInteger math will trim leading zeros so we do likewise
while (hash.substring(0, 1) === '0') {
//console.log("stripping leading zero from M1");
hash = hash.substring(1);
}
//console.log("js hash:"+hash)
//console.log("js x before modN "+this.fromHex(hash));
this.x = this.fromHex(hash).mod(this.N());
return this.x;
};
/**
* Computes the session key S = (B - k * g^x) ^ (a + u * x) (mod N)
* from client-side parameters.
*
*
Specification: RFC 5054
*
* @param N The prime parameter 'N'. Must not be {@code null}.
* @param g The generator parameter 'g'. Must not be {@code null}.
* @param k The SRP-6a multiplier 'k'. Must not be {@code null}.
* @param x The 'x' value, see {@link #computeX}. Must not be
* {@code null}.
* @param u The random scrambling parameter 'u'. Must not be
* {@code null}.
* @param a The private client value 'a'. Must not be {@code null}.
* @param B The public server value 'B'. Must note be {@code null}.
*
* @return The resulting session key 'S'.
*/
this.computeSessionKey = function(k, x, u, a, B) {
this.check(k, "k");
this.check(x, "x");
this.check(u, "u");
this.check(a, "a");
this.check(B, "B");
var exp = u.multiply(x).add(a);
var tmp = this.g().modPow(x, this.N()).multiply(k);
return B.subtract(tmp).modPow(exp, this.N());
};
}
// public helper
SRP6JavascriptClientSession.prototype.toHex = function(n) {
"use strict";
return n.toString(16);
};
// public helper
/* jshint ignore:start */
SRP6JavascriptClientSession.prototype.fromHex = function(s) {
"use strict";
return new BigInteger(""+s, 16); // jdk1.7 rhino requires string concat
};
/* jshint ignore:end */
// public helper to hide BigInteger from the linter
/* jshint ignore:start */
SRP6JavascriptClientSession.prototype.BigInteger = function(string, radix) {
"use strict";
return new BigInteger(""+string, radix); // jdk1.7 rhino requires string concat
};
/* jshint ignore:end */
// public getter of the current workflow state.
SRP6JavascriptClientSession.prototype.getState = function() {
"use strict";
return this.state;
};
/**
* Gets the shared sessionkey
*
* @param hash Boolean With to return the large session key 'S' or 'K=H(S)'
*/
SRP6JavascriptClientSession.prototype.getSessionKey = function(hash) {
"use strict";
if( this.S === null ) {
return null;
}
this.SS = this.toHex(this.S);
if(typeof hash !== 'undefined' && hash === false){
return this.SS;
} else {
if( this.K === null ) {
this.K = this.H(this.SS);
}
return this.K;
}
};
// public getter
SRP6JavascriptClientSession.prototype.getUserID = function() {
"use strict";
return this.I;
};
/*
* Generates a new salt 's'. This takes the current time, a pure browser random value, and an optional server generated random, and hashes them all together.
* This should ensure that the salt is unique to every use registration regardless of the quality of the browser random generation routine.
* Note that this method is optional as you can choose to always generate the salt at the server and sent it to the browser as it is a public value.
*
* Always add a unique constraint to where you store this in your database to force that all users on the system have a unique salt.
*
* @param opionalServerSalt An optional server salt which is hashed into a locally generated random number. Can be left undefined when calling this function.
* @return 's' Salt as a hex string of length driven by the bit size of the hash algorithm 'H'.
*/
SRP6JavascriptClientSession.prototype.generateRandomSalt = function(opionalServerSalt) {
"use strict";
var s = null;
/* jshint ignore:start */
s = random16byteHex.random();
/* jshint ignore:end */
// if you invoke without passing the string parameter the '+' operator uses 'undefined' so no nullpointer risk here
var ss = this.H((new Date())+':'+opionalServerSalt+':'+s);
return ss;
};
/*
* Generates a new verifier 'v' from the specified parameters.
*
The verifier is computed as v = g^x (mod N).
*
Specification RFC 2945
*
* @param salt The salt 's'. Must not be null or empty.
* @param identity The user identity/email 'I'. Must not be null or empty.
* @param password The user password 'P'. Must not be null or empty
* @return The resulting verifier 'v' as a hex string
*/
SRP6JavascriptClientSession.prototype.generateVerifier = function(salt, identity, password) {
"use strict";
//console.log("SRP6JavascriptClientSession.prototype.generateVerifier");
// no need to check the parameters as generateX will do this
var x = this.generateX(salt, identity, password);
//console.log("js x: "+this.toHex(x));
this.v = this.g().modPow(x, this.N());
//console.log("js v: "+this.toHex(this.v));
return this.toHex(this.v);
};
/**
* Records the identity 'I' and password 'P' of the authenticating user.
* The session is incremented to {@link State#STEP_1}.
*
Argument origin:
*
* - From user: user identity 'I' and password 'P'.
*
* @param userID The identity 'I' of the authenticating user, UTF-8
* encoded. Must not be {@code null} or empty.
* @param password The user password 'P', UTF-8 encoded. Must not be
* {@code null}.
* @throws IllegalStateException If the method is invoked in a state
* other than {@link State#INIT}.
*/
SRP6JavascriptClientSession.prototype.step1 = function(identity, password) {
"use strict";
//console.log("SRP6JavascriptClientSession.prototype.step1");
//console.log("N: "+this.N());
//console.log("g: "+this.g());
//console.log("k: "+this.toHex(this.k));
this.check(identity, "identity");
this.check(password, "password");
this.I = identity;
this.P = password;
if( this.state !== this.INIT ) {
throw new Error("IllegalStateException not in state INIT");
}
this.state = this.STEP_1;
};
/**
* Computes the random scrambling parameter u = H(A | B)
* Specification RFC 2945
* Will throw an error if
*
* @param A The public client value 'A'. Must not be {@code null}.
* @param B The public server value 'B'. Must not be {@code null}.
*
* @return The resulting 'u' value.
*/
SRP6JavascriptClientSession.prototype.computeU = function(Astr, Bstr) {
"use strict";
//console.log("SRP6JavascriptClientSession.prototype.computeU");
this.check(Astr, "Astr");
this.check(Bstr, "Bstr");
/* jshint ignore:start */
var output = this.H(Astr+Bstr);
//console.log("js raw u:"+output);
var u = new BigInteger(""+output,16);
//console.log("js u:"+this.toHex(u));
if( BigInteger.ZERO.equals(u) ) {
throw new Error("SRP6Exception bad shared public value 'u' as u==0");
}
return u;
/* jshint ignore:end */
};
SRP6JavascriptClientSession.prototype.random16byteHex = function() {
"use strict";
var r1 = null;
/* jshint ignore:start */
r1 = random16byteHex.random();
/* jshint ignore:end */
return r1;
};
/**
* Generate a random value in the range `[1,N)` using a minimum of 256 random bits.
*
* See specification RFC 5054.
* This method users the best random numbers available. Just in case the random number
* generate in the client web browser is totally buggy it also adds `H(I+":"+salt+":"+time())`
* to the generated random number.
* @param N The safe prime.
*/
SRP6JavascriptClientSession.prototype.randomA = function(N) {
"use strict";
//console.log("N:"+N);
// our ideal number of random bits to use for `a` as long as its bigger than 256 bits
var hexLength = this.toHex(N).length;
var ZERO = this.BigInteger("0", 10);
var ONE = this.BigInteger("1", 10);
var r = ZERO;
// loop until we don't have a ZERO value. we would have to generate exactly N to loop so very rare.
while(ZERO.equals(r)){
// in theory we get 256 bits of good random numbers here
var rstr = this.random16byteHex() + this.random16byteHex();
//console.log("rstr:"+rstr);
// add more random bytes until we are at least as large as N and ignore any overshoot
while( rstr.length < hexLength ) {
rstr = rstr + this.random16byteHex();
}
//console.log("rstr:"+rstr);
// we now have a random just at lest 256 bits but typically more bits than N for large N
var rBi = this.BigInteger(rstr, 16);
//console.log("rBi:"+rBi);
// this hashes the time in ms such that we wont get repeated numbers for successive attempts
// it also hashes the salt which can itself be salted by a server strong random which protects
// against rainbow tables. it also hashes the user identity which is unique to each user
// to protect against having simply no good random numbers anywhere
var oneTimeBi = this.BigInteger(this.H(this.I+":"+this.salt+':'+(new Date()).getTime()), 16);
//console.log("oneTimeBi:"+oneTimeBi);
// here we add the "one time" hashed time number to our random number to the random number
// this protected against a buggy browser random number generated generating a constant value
// we mod(N) to wrap to the range [0,N) then loop if we get 0 to give [1,N)
// mod(N) is broken due to buggy library code so we workaround with modPow(1,N)
r = (oneTimeBi.add(rBi)).modPow(ONE, N);
}
//console.log("r:"+r);
// the result will in the range [1,N) using more random bits than size N
return r;
};
/**
* Receives the password salt 's' and public value 'B' from the server.
* The SRP-6a crypto parameters are also set. The session is incremented
* to {@link State#STEP_2}.
*
Argument origin:
*
* - From server: password salt 's', public value 'B'.
*
- Pre-agreed: crypto parameters prime 'N',
* generator 'g' and hash function 'H'.
*
* @param s The password salt 's' as a hex string. Must not be {@code null}.
* @param B The public server value 'B' as a hex string. Must not be {@code null}.
* @param k k is H(N,g) with padding by the server. Must not be {@code null}.
* @return The client credentials consisting of the client public key
* 'A' and the client evidence message 'M1'.
* @throws IllegalStateException If the method is invoked in a state
* other than {@link State#STEP_1}.
* @throws SRP6Exception If the public server value 'B' is invalid.
*/
SRP6JavascriptClientSession.prototype.step2 = function(s, BB) {
"use strict";
//console.log("SRP6JavascriptClientSession.prototype.step2");
this.check(s, "s");
//console.log("s:" + s);
this.check(BB, "BB");
//console.log("BB:" + BB);
if( this.state !== this.STEP_1 ) {
throw new Error("IllegalStateException not in state STEP_1");
}
// this is checked when passed to computeSessionKey
this.B = this.fromHex(BB);
var ZERO = null;
/* jshint ignore:start */
ZERO = BigInteger.ZERO;
/* jshint ignore:end */
if (this.B.mod(this.N()).equals(ZERO)) {
throw new Error("SRP6Exception bad server public value 'B' as B == 0 (mod N)");
}
//console.log("k:" + this.k);
// this is checked when passed to computeSessionKey
var x = this.generateX(s, this.I, this.P);
//console.log("x:" + x);
// blank the password as there is no reason to keep it around in memory.
this.P = null;
//console.log("N:"+this.toHex(this.N).toString(16));
this.a = this.randomA(this.N);
//console.log("a:" + this.toHex(this.a));
this.A = this.g().modPow(this.a, this.N());
//console.log("A:" + this.toHex(this.A));
this.check(this.A, "A");
this.u = this.computeU(this.A.toString(16),BB);
//console.log("u:" + this.u);
this.S = this.computeSessionKey(this.k, x, this.u, this.a, this.B);
this.check(this.S, "S");
//console.log("jsU:" + this.toHex(this.u));
//console.log("jsS:" + this.toHex(this.S));
var AA = this.toHex(this.A);
this.M1str = this.H(AA+BB+this.toHex(this.S));
this.check(this.M1str, "M1str");
// server BigInteger math will trim leading zeros so we must do likewise to get a match
while (this.M1str.substring(0, 1) === '0') {
//console.log("stripping leading zero from M1");
this.M1str = this.M1str.substring(1);
}
//console.log("M1str:" + this.M1str);
//console.log("js ABS:" + AA+BB+this.toHex(this.S));
//console.log("js A:" + AA);
//console.log("js B:" + BB);
//console.log("js v:" + this.v);
//console.log("js u:" + this.u);
//console.log("js A:" + this.A);
//console.log("js b:" + this.B);
//console.log("js S:" + this.S);
//console.log("js S:" + this.toHex(this.S));
//console.log("js M1:" + this.M1str);
this.state = this.STEP_2;
return { A: AA, M1: this.M1str };
};
/**
* Receives the server evidence message 'M1'. The session is incremented
* to {@link State#STEP_3}.
*
* Argument origin:
*
* - From server: evidence message 'M2'.
*
* @param serverM2 The server evidence message 'M2' as string. Must not be {@code null}.
* @throws IllegalStateException If the method is invoked in a state
* other than {@link State#STEP_2}.
* @throws SRP6Exception If the session has timed out or the
* server evidence message 'M2' is
* invalid.
*/
SRP6JavascriptClientSession.prototype.step3 = function(M2) {
"use strict";
this.check(M2);
//console.log("SRP6JavascriptClientSession.prototype.step3");
// Check current state
if (this.state !== this.STEP_2)
throw new Error("IllegalStateException State violation: Session must be in STEP_2 state");
//console.log("js A:" + this.toHex(this.A));
//console.log("jsM1:" + this.M1str);
//console.log("js S:" + this.toHex(this.S));
var computedM2 = this.H(this.toHex(this.A)+this.M1str+this.toHex(this.S));
//console.log("jsServerM2:" + M2);
//console.log("jsClientM2:" + computedM2);
// server BigInteger math will trim leading zeros so we must do likewise to get a match
while (computedM2.substring(0, 1) === '0') {
//console.log("stripping leading zero from computedM2");
computedM2 = computedM2.substring(1);
}
//console.log("server M2:"+M2+"\ncomputedM2:"+computedM2);
if ( ""+computedM2 !== ""+M2) {
throw new Error("SRP6Exception Bad server credentials");
}
this.state = this.STEP_3;
return true;
};