Nimbus SRP usage

Initial setup and prerequisites

To setup SRP-6a authentication in your environment you must first settle on certain protocol settings which should then remain permanent:

  • Crypto parameters - safe prime 'N' and generator 'g'. These affect the cryptographic strength of the SRP-6a protocol. Choosing a larger prime 'N' increases security but may slow down computation somewhat. The SRP6CryptoParams class provides a range of precomputed and read-to-use safe primes from 256 to 1024 bit length.
  • Hash algorithm 'H' for the message digests. The default is "SHA-1" but you may switch to a stronger one as long as it's supported by the underlying Java runtime.
  • The preferred salt 's' length. The default salt length is 16 bytes or you may choose a different size.
  • The routines for the password key 'x' as well as for the client and server evidence messages 'M1' and 'M2'. You can stick to the default ones or define your own if required.

Important notes:

The settings that you settled upon must then be employed consistently by all clients and servers in your SRP-6a authentication environment. If just a single setting varies between client and server authentication will fail. If third-party client apps are going to participate make sure all SRP settings are clearly published. If you decide to change the parameters at some point in future you will have to re-register all affected users so that their verifiers match.

The SRP-6a settings may be published by various means, for example through a JSON message.

{
  "N"         : "115b8b692e0e045692cf280b436735c77a5a9e8a9e7ed56c965f87db5b2a2ece3",
  "g"         : "2",
  "H"         : "SHA-256",
  "saltBytes" : 16,
  "xRoutine"  : "H(s|H(P))",
  "m1Routine" : "H(A|B|S)",
  "m2Routine" : "H(A|M1|S)"
}

The 'N' and 'g' crypto parameters are hex (base-16) encoded. The library includes a BigIntegerUtils class to help you with encoding / decoding to / from hex.

User registration

A user registers their credentials with an SRP-6a server by providing their user identity (username) 'I', a salt 's' and a verifier 'v'. The verifier is a cryptographic value derived from the salt 's' and password 'P'.

The default Nimbus SRP routine for computing the verifier:

x = H(s | H(P))
v = g^x (mod N)

The server must store the salt and the verifier values of the user for the subsequent authentication requests.

The salt 's' and verifier 'v' can be generated on the client side with the SRP6VerifierGenerator. The resulting values are then passed to the server to register the user credentials.

// Default crypto params
SRP6CryptoParams config = SRP6CryptoParams.getInstance();

// Create verifier generator
SRP6VerifierGenerator gen = new SRP6VerifierGenerator(config);

// Random 16 byte salt 's'
BigInteger salt = new BigInteger(SRP6VerifierGenerator.generateRandomSalt());

// Username and password
String username = "alice";
String password = "secret";

// Compute verifier 'v'
BigInteger verifier = gen.generateVerifier(salt, username, password));

Packed into a JSON message for submission to the server these may look like this:

{
  "username" : "alice",
  "salt"     : "da641e73455174c1c5e8",
  "verifier" : "fd415d055ebebcaab205b65ad309990ffa22cb9f3ebd27d85ebb91281047abea"
}

It's recommended that the server checks the salt length to ensure it matches the expected size.

The SRP-6a authentication process

The actual Secure Remote Password authentication is a three-step process. It can be handled with help of the SRP6ServerSession class on the server side and with SRP6ClientSession on the client side.

Step Client / Server Action Library Calls
1.

Client

On a new user authentication request:

  • Create new client SRP-6a auth session.
  • Store input user identity 'I' and password 'P'.
  • Send user identity 'I' to server.
SRP6ClientSession() SRP6ClientSession.step1()

Server

On receiving the user identity 'I' from the client:

  • Create new server SRP-6a auth session.
  • Look up stored salt 's' and verifier 'v' for the authenticating user identity 'I'.
  • Compute the server public value 'B'.
  • Respond with the server public value 'B' and password salt 's'. If the SRP-6a crypto parameters 'N', 'g' and 'H' were not agreed in advance between server and client append them to the response.
SRP6ServerSession() SRP6ServerSession.step1()
2.

Client

On receiving the server public value 'B' (and crypto parameters):

  • Set the SRP-6a crypto parameters.
  • Compute the client public value 'A' and evidence message 'M1'.
  • Send the client public value 'A' and evidence message 'M1' to the server.
SRP6ClientSession.step2()

Server

On receiving the client public value 'A' and evidence message 'M1':

  • Complete user authentication.
  • Compute server evidence message 'M2'.
  • Respond with the server evidence message 'M2'.
SRP6ServerSession.step2()
3.

Client

On receiving the server evidence message 'M2':

  • Validate the server evidence message 'M2' after which user and server are mutually authenticated.
SRP6ClientSession.step3()

Client + Server

On completing mutual authentication:

  • The established session key 'S' can be used to encrypt further communication between client and server.
SRP6Session.getSessionKey()

Example SRP authentication session

Step 1.

Client: begin new authentication session:

String username = "alice";
String password = "secret";

SRP6ClientSession client = new SRP6ClientSession();
client.step1(username, password);

// Send username to server...
// ...

Example JSON request message from client to server:

{
  "username" : "alice"
}

Server: begin new authentication session on receiving the client request:

SRP6CryptoParams config = SRP6CryptoParams.getInstance();

SRP6ServerSession server = new SRP6ServerSession(config);

// Retrieve user verifier 'v' + salt 's' from database
BigInteger v = Hex.decodeToBigInterger("...");
BigInteger s = Hex.decodeToBigInterger("...");

// Compute the public server value 'B'
BigInteger B = server.step1(username, s, v);

// Respond with salt 's' and public server value 'B',
// optionally crypto params...
// ...

Example JSON response message from server to client (hexadecimal number encoding):

{
  "salt" : "1f1659a77b66bfbf5fef68ef73d82f37",
  "B"    : "99e293aa2027839d8cf63b79246410bf61566d7b93a9b78e2639039b8e443228b0c6ed85878f52d5d44803170a31cb601886133dc37f4cccdb60591a692e84d"
}

Step 2.

Client: set crypto parameters, then compute client public value 'A' and evidence message 'M1':

SRP6CryptoParams config = SRP6CryptoParams.getInstance();

SRP6ClientCredentials cred = null;

try {
        cred = client.step2(config, s, B);

} catch (SRP6Exception e) {
        // Invalid server 'B'
}

// Send client public value 'A' and evidence message 'M1' to server
// ...

Example JSON request message from client to server:

{
  "A"  : "91b446bc56cdf02c74d2f494b3131b7a0e724de8527b36fc67a96c5e9d23836dfe9aaf06db64deea6009bb1f4bc0b423cbddbdb4906bd6569ff87b55811b2e1b",
  "M1" : "7d3d92fd1cb39380b3dfc80319ff283cf3e93ca6"
}

Server: complete user authentication and compute own evidence message 'M2':

BigInteger M2 = null;

try {
        M2 = server.step2(cred.A, cred.M1);

} catch (SRP6Exception e) {
        // User authentication failed
}

// Respond with server evidence message 'M2'
// ...

Example JSON response message from server to client:

{
  "M2" : "c228309fa2df478291006160c850b4d03b6f152f"
}

Step 3.

Client: Validate server evidence message 'M2' and complete mutual authentication:

try {
        client.step3(M2);

} catch (SRP6Exception e) {
        // Server not authenticated
}