package com.beowulfe.hap.impl.pairing; import java.math.BigInteger; import com.nimbusds.srp6.SRP6ClientEvidenceContext; import com.nimbusds.srp6.SRP6CryptoParams; import com.nimbusds.srp6.SRP6Exception; import com.nimbusds.srp6.SRP6Routines; import com.nimbusds.srp6.SRP6ServerEvidenceContext; import com.nimbusds.srp6.SRP6Session; import com.nimbusds.srp6.URoutineContext; /** * This is a slightly modified version of the SRP6ServerSession class included * with nimbus. The only change made for homekit compatability is a change to the * size of the b key. Homekit pairing fails if b is not 3072 bytes. * * Stateful server-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. * * <p>Usage: * * <ul> * <li>Create a new SRP-6a server session for each client authentication * attempt. * <li>If you wish to use custom routines for the server evidence message * 'M1' and / or the client evidence message 'M2' specify them at this * point. * <li>Proceed to {@link #step1 step one} on receiving a valid user identity * 'I' from the authenticating client. 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. * <li>Proceed to {@link #step2 step two} on receiving the public client * value 'A' and evidence message 'M1'. If the client credentials are * valid signal success and return the server evidence message 'M2'. The * established session key 'S' may be {@link #getSessionKey retrieved} to * encrypt further communication with the client. Else signal an * authentication failure to the client. * </ul> * * @author Vladimir Dzhuvinov */ public class HomekitSRP6ServerSession extends SRP6Session { /** * Enumerates the states of a server-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 user identity 'I' is received from the client and the * server has returned its public value 'B' based on the * matching password verifier 'v'. The session is ready to * proceed to {@link #STEP_2}. */ STEP_1, /** * The client public key 'A' and evidence message 'M1' are * received and the server has replied with its own evidence * message 'M2'. The session is finished (authentication was * successful or failed). */ STEP_2 } /** * Indicates a non-existing use identity and implies mock salt 's' and * verifier 'v' values. */ private boolean noSuchUserIdentity = false; /** * The password verifier 'v'. */ private BigInteger v = null; /** * The server private value 'b'. */ private BigInteger b = null; /** * The current SRP-6a auth state. */ private State state; /** * Creates a new server-side SRP-6a authentication session and sets its * state to {@link State#INIT}. * * @param config The SRP-6a crypto parameters configuration. Must not * be {@code null}. * @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 HomekitSRP6ServerSession(final SRP6CryptoParams config, final int timeout) { super(timeout); if (config == null) throw new IllegalArgumentException("The SRP-6a crypto parameters must not be null"); this.config = config; digest = config.getMessageDigestInstance(); if (digest == null) throw new IllegalArgumentException("Unsupported hash algorithm 'H': " + config.H); state = State.INIT; updateLastActivityTime(); } /** * Creates a new server-side SRP-6a authentication session and sets its * state to {@link State#INIT}. Session timeouts are disabled. * * @param config The SRP-6a crypto parameters configuration. Must not * be {@code null}. */ public HomekitSRP6ServerSession(final SRP6CryptoParams config) { this(config, 0); } /** * Increments this SRP-6a authentication session to * {@link State#STEP_1}. * * <p>Argument origin: * * <ul> * <li>From client: user identity 'I'. * <li>From server database: matching salt 's' and password verifier * 'v' values. * </ul> * * @param userID The identity 'I' of the authenticating user. Must not * be {@code null} or empty. * @param s The password salt 's'. Must not be {@code null}. * @param v The password verifier 'v'. Must not be {@code null}. * * @return The server public value 'B'. * * @throws IllegalStateException If the mehod is invoked in a state * other than {@link State#INIT}. */ public BigInteger step1(final String userID, final BigInteger s, final BigInteger v) { // Check arguments if (userID == null || userID.trim().isEmpty()) throw new IllegalArgumentException("The user identity 'I' must not be null or empty"); this.userID = userID; if (s == null) throw new IllegalArgumentException("The salt 's' must not be null"); this.s = s; if (v == null) throw new IllegalArgumentException("The verifier 'v' must not be null"); this.v = v; // Check current state if (state != State.INIT) throw new IllegalStateException("State violation: Session must be in INIT state"); // Generate server private and public values k = SRP6Routines.computeK(digest, config.N, config.g); digest.reset(); b = HomekitSRP6Routines.generatePrivateValue(config.N, random); digest.reset(); B = SRP6Routines.computePublicServerValue(config.N, config.g, k, v, b); state = State.STEP_1; updateLastActivityTime(); return B; } /** * Increments this SRP-6a authentication session to * {@link State#STEP_1} indicating a non-existing user identity 'I' * with mock (simulated) salt 's' and password verifier 'v' values. * * <p>This method can be used to avoid informing the client at step one * that the user identity is bad and throw instead a guaranteed general * "bad credentials" SRP-6a exception at step two. * * <p>Argument origin: * * <ul> * <li>From client: user identity 'I'. * <li>Simulated by server, preferably consistently for the * specified identity 'I': salt 's' and password verifier 'v' * values. * </ul> * * @param userID The identity 'I' of the authenticating user. Must not * be {@code null} or empty. * @param s The password salt 's'. Must not be {@code null}. * @param v The password verifier 'v'. Must not be {@code null}. * * @return The server public value 'B'. * * @throws IllegalStateException If the method is invoked in a state * other than {@link State#INIT}. */ public BigInteger mockStep1(final String userID, final BigInteger s, final BigInteger v) { noSuchUserIdentity = true; return step1(userID, s, v); } /** * Increments this SRP-6a authentication session to * {@link State#STEP_2}. * * <p>Argument origin: * * <ul> * <li>From client: public value 'A' and evidence message 'M1'. * </ul> * * @param A The client public value. Must not be {@code null}. * @param M1 The client evidence message. Must not be {@code null}. * * @return The server evidence message 'M2'. * * @throws SRP6Exception If the session has timed out, the client public * value 'A' is invalid or the user credentials * are invalid. * * @throws IllegalStateException If the method is invoked in a state * other than {@link State#STEP_1}. */ public BigInteger step2(final BigInteger A, final BigInteger M1) throws SRP6Exception { // Check arguments if (A == null) throw new IllegalArgumentException("The client public value 'A' must not be null"); this.A = A; if (M1 == null) throw new IllegalArgumentException("The client evidence message 'M1' must not be null"); this.M1 = M1; // 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 A validity if (! SRP6Routines.isValidPublicValue(config.N, A)) throw new SRP6Exception("Bad client public value 'A'", SRP6Exception.CauseType.BAD_PUBLIC_VALUE); // Check for previous mock step 1 if (noSuchUserIdentity) throw new SRP6Exception("Bad client credentials", SRP6Exception.CauseType.BAD_CREDENTIALS); 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, v, u, A, b); // Compute the own client evidence message 'M1' BigInteger computedM1; if (clientEvidenceRoutine != null) { // With custom routine SRP6ClientEvidenceContext ctx = new SRP6ClientEvidenceContext(userID, s, A, B, S); computedM1 = clientEvidenceRoutine.computeClientEvidence(config, ctx); } else { // With default routine computedM1 = SRP6Routines.computeClientEvidence(digest, A, B, S); digest.reset(); } if (! computedM1.equals(M1)) throw new SRP6Exception("Bad client credentials", SRP6Exception.CauseType.BAD_CREDENTIALS); state = State.STEP_2; if (serverEvidenceRoutine != null) { // With custom routine SRP6ServerEvidenceContext ctx = new SRP6ServerEvidenceContext(A, M1, S); M2 = serverEvidenceRoutine.computeServerEvidence(config, ctx); } updateLastActivityTime(); return M2; } /** * Returns the current state of this SRP-6a authentication session. * * @return The current state. */ public State getState() { return state; } }