All Downloads are FREE. Search and download functionalities are using the official Maven repository.

js.thinbus-srp6client.js Maven / Gradle / Ivy

Go to download

Complete Secure Remote Password (SRP-6a) client session implementation written in Javascript / EMCAScript with a compatible server implementation written with Java Nimbus SRP6a.

There is a newer version: 1.6.2
Show newest version
/**
 * 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; };




© 2015 - 2025 Weber Informatics LLC | Privacy Policy