com.nimbusds.srp6.SRP6ClientSession Maven / Gradle / Ivy
package com.nimbusds.srp6;
import java.io.Serializable;
import java.math.BigInteger;
import java.nio.charset.Charset;
import java.security.MessageDigest;
/**
* Stateful client-side Secure Remote Password (SRP-6a) authentication session.
* Handles the computing and storing of SRP-6a variables between the protocol
* steps as well as timeouts.
*
* Usage:
*
*
* - Create a new SRP-6a client session for each authentication attempt.
*
- If you wish to use custom routines for the password key 'x', the
* server evidence message 'M1', and / or the client evidence message
* 'M2' specify them at this point.
*
- Proceed to {@link #step1 step one} by recording the input user
* identity 'I' (submitted to the server) and password 'P'.
*
- Proceed to {@link #step2 step two} on receiving the password salt
* 's' and the public server value 'B' from the server. At this point
* the SRP-6a crypto parameters 'N', 'g' and 'H' must also be
* specified. These can either be agreed in advance between server and
* client or suggested by the server in its step one response.
*
- Proceed to {@link #step3 step three} on receiving the server
* evidence message 'M2'.
*
*
* @author Vladimir Dzhuvinov
* @author Bernard Wittwer
*/
public class SRP6ClientSession extends SRP6Session implements Serializable {
/**
* Serializable class version number
*/
private static final long serialVersionUID = -479060216624675478L;
/**
* Enumerates the states of a client-side SRP-6a authentication
* session.
*/
public static enum State {
/**
* The session is initialised and ready to begin authentication
* by proceeding to {@link #STEP_1}.
*/
INIT,
/**
* The authenticating user has input their identity 'I'
* (username) and password 'P'. The session is ready to proceed
* to {@link #STEP_2}.
*/
STEP_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}.
*/
STEP_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).
*/
STEP_3
}
/**
* The user password 'P'.
*/
private String password;
/**
* The password key 'x'.
*/
private BigInteger x = null;
/**
* The client private value 'a'.
*/
private BigInteger a = null;
/**
* The current SRP-6a auth state.
*/
private State state;
/**
* Custom routine for password key 'x' computation.
*/
private XRoutine xRoutine = null;
/**
* Creates a new client-side SRP-6a authentication session and sets its
* state to {@link State#INIT}.
*
* @param timeout The SRP-6a authentication session timeout in seconds.
* If the authenticating counterparty (server or client)
* fails to respond within the specified time the session
* will be closed. If zero timeouts are disabled.
*/
public SRP6ClientSession(final int timeout) {
super(timeout);
state = State.INIT;
updateLastActivityTime();
}
/**
* Creates a new client-side SRP-6a authentication session and sets its
* state to {@link State#INIT}. Session timeouts are disabled.
*/
public SRP6ClientSession() {
this(0);
}
/**
* Sets a custom routine for the password key 'x' computation. Note that
* the custom routine must be set prior to {@link State#STEP_2}.
*
* @param routine The password key 'x' routine or {@code null} to use
* the {@link SRP6Routines#computeX default one} instead.
*/
public void setXRoutine(final XRoutine routine) {
xRoutine = routine;
}
/**
* Gets the custom routine for the password key 'x' computation.
*
* @return The routine instance or {@code null} if the default
* {@link SRP6Routines#computeX default one} is used.
*/
public XRoutine getXRoutine() {
return xRoutine;
}
/**
* 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}.
*/
public void step1(final String userID, final String password) {
if (userID == null || userID.trim().isEmpty())
throw new IllegalArgumentException("The user identity 'I' must not be null or empty");
this.userID = userID;
if (password == null)
throw new IllegalArgumentException("The user password 'P' must not be null");
this.password = password;
// Check current state
if (state != State.INIT)
throw new IllegalStateException("State violation: Session must be in INIT state");
state = State.STEP_1;
updateLastActivityTime();
}
/**
* 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'.
*
- From server or pre-agreed: crypto parameters prime 'N',
* generator 'g' and hash function 'H'.
*
*
* @param config The SRP-6a crypto parameters. Must not be {@code null}.
* @param s The password salt 's'. Must not be {@code null}.
* @param B The public server value 'B'. 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 session has timed out or the
* public server value 'B' is invalid.
*/
public SRP6ClientCredentials step2(final SRP6CryptoParams config, final BigInteger s, final BigInteger B)
throws SRP6Exception {
// Check arguments
if (config == null)
throw new IllegalArgumentException("The SRP-6a crypto parameters must not be null");
this.config = config;
MessageDigest digest = config.getMessageDigestInstance();
if (digest == null)
throw new IllegalArgumentException("Unsupported hash algorithm 'H': " + config.H);
if (s == null)
throw new IllegalArgumentException("The salt 's' must not be null");
this.s = s;
if (B == null)
throw new IllegalArgumentException("The public server value 'B' must not be null");
this.B = B;
// Check current state
if (state != State.STEP_1)
throw new IllegalStateException("State violation: Session must be in STEP_1 state");
// Check timeout
if (hasTimedOut())
throw new SRP6Exception("Session timeout", SRP6Exception.CauseType.TIMEOUT);
// Check B validity
if (! srp6Routines.isValidPublicValue(config.N, B))
throw new SRP6Exception("Bad server public value 'B'", SRP6Exception.CauseType.BAD_PUBLIC_VALUE);
// Compute the password key 'x'
if (xRoutine != null) {
// With custom routine
x = xRoutine.computeX(config.getMessageDigestInstance(),
BigIntegerUtils.bigIntegerToBytes(s),
userID.getBytes(Charset.forName("UTF-8")),
password.getBytes(Charset.forName("UTF-8")));
} else {
// With default routine
x = srp6Routines.computeX(digest, BigIntegerUtils.bigIntegerToBytes(s), password.getBytes(Charset.forName("UTF-8")));
digest.reset();
}
// Generate client private and public values
a = srp6Routines.generatePrivateValue(config.N, random);
digest.reset();
A = srp6Routines.computePublicClientValue(config.N, config.g, a);
// Compute the session key
k = srp6Routines.computeK(digest, config.N, config.g);
digest.reset();
if (hashedKeysRoutine != null) {
URoutineContext hashedKeysContext = new URoutineContext(A, B);
u = hashedKeysRoutine.computeU(config, hashedKeysContext);
} else {
u = srp6Routines.computeU(digest, config.N, A, B);
digest.reset();
}
S = srp6Routines.computeSessionKey(config.N, config.g, k, x, u, a, B);
// Compute the client evidence message
if (clientEvidenceRoutine != null) {
// With custom routine
SRP6ClientEvidenceContext ctx = new SRP6ClientEvidenceContext(userID, s, A, B, S);
M1 = clientEvidenceRoutine.computeClientEvidence(config, ctx);
} else {
// With default routine
M1 = srp6Routines.computeClientEvidence(digest, A, B, S);
digest.reset();
}
state = State.STEP_2;
updateLastActivityTime();
return new SRP6ClientCredentials(A, M1);
}
/**
* Receives the server evidence message 'M1'. The session is
* incremented to {@link State#STEP_3}.
*
*
* Argument origin:
*
*
* - From server: evidence message 'M2'.
*
*
* @param M2 The server evidence message 'M2'. 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.
*/
public void step3(final BigInteger M2)
throws SRP6Exception {
// Check argument
if (M2 == null)
throw new IllegalArgumentException("The server evidence message 'M2' must not be null");
this.M2 = M2;
// Check current state
if (state != State.STEP_2)
throw new IllegalStateException("State violation: Session must be in STEP_2 state");
// Check timeout
if (hasTimedOut())
throw new SRP6Exception("Session timeout", SRP6Exception.CauseType.TIMEOUT);
// Compute the own server evidence message 'M2'
BigInteger computedM2;
if (serverEvidenceRoutine != null) {
// With custom routine
SRP6ServerEvidenceContext ctx = new SRP6ServerEvidenceContext(A, M1, S);
computedM2 = serverEvidenceRoutine.computeServerEvidence(config, ctx);
} else {
// With default routine
MessageDigest digest = config.getMessageDigestInstance();
computedM2 = srp6Routines.computeServerEvidence(digest, A, M1, S);
}
if (! computedM2.equals(M2))
throw new SRP6Exception("Bad server credentials", SRP6Exception.CauseType.BAD_CREDENTIALS);
state = State.STEP_3;
updateLastActivityTime();
}
/**
* Returns the current state of this SRP-6a authentication session.
*
* @return The current state.
*/
public State getState() {
return state;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy