001package com.nimbusds.srp6;
002
003
004import java.math.BigInteger;
005
006
007/**
008 * Stateful server-side Secure Remote Password (SRP-6a) authentication session.
009 * Handles the computing and storing of SRP-6a variables between the protocol 
010 * steps as well as timeouts.
011 *
012 * <p>Usage:
013 *
014 * <ul>
015 *     <li>Create a new SRP-6a server session for each client authentication
016 *         attempt.
017 *     <li>If you wish to use custom routines for the server evidence message
018 *         'M1' and / or the client evidence message 'M2' specify them at this
019 *         point.
020 *     <li>Proceed to {@link #step1 step one} on receiving a valid user identity
021 *         'I' from the authenticating client. Respond with the server public
022 *         value 'B' and password salt 's'. If the SRP-6a crypto parameters 'N',
023 *         'g' and 'H' were not agreed in advance between server and client 
024 *         append them to the response.
025 *     <li>Proceed to {@link #step2 step two} on receiving the public client 
026 *         value 'A' and evidence message 'M1'. If the client credentials are
027 *         valid signal success and return the server evidence message 'M2'. The
028 *         established session key 'S' may be {@link #getSessionKey retrieved} to
029 *         encrypt further communication with the client. Else signal an 
030 *         authentication failure to the client.
031 * </ul>
032 *
033 * @author Vladimir Dzhuvinov
034 */
035public class SRP6ServerSession extends SRP6Session {
036        
037        
038        /**
039         * Enumerates the states of a server-side SRP-6a authentication session.
040         */
041        public static enum State {
042        
043        
044                /**
045                 * The session is initialised and ready to begin authentication,
046                 * by proceeding to {@link #STEP_1}.
047                 */
048                INIT,
049                
050                
051                /**
052                 * The user identity 'I' is received from the client and the 
053                 * server has returned its public value 'B' based on the 
054                 * matching password verifier 'v'. The session is ready to 
055                 * proceed to {@link #STEP_2}.
056                 */
057                STEP_1,
058                
059                
060                /**
061                 * The client public key 'A' and evidence message 'M1' are
062                 * received and the server has replied with its own evidence
063                 * message 'M2'. The session is finished (authentication was 
064                 * successful or failed).
065                 */
066                STEP_2
067        }
068        
069        
070        /**
071         * Indicates a non-existing use identity and implies mock salt 's' and
072         * verifier 'v' values.
073         */
074        private boolean noSuchUserIdentity = false;
075        
076        
077        /**
078         * The password verifier 'v'.
079         */
080        private BigInteger v = null;
081        
082        
083        /**
084         * The server private value 'b'.
085         */
086        private BigInteger b = null;
087        
088        
089        /**
090         * The current SRP-6a auth state.
091         */
092        private State state;
093        
094        
095        /**
096         * Creates a new server-side SRP-6a authentication session and sets its 
097         * state to {@link State#INIT}.
098         *
099         * @param config  The SRP-6a crypto parameters configuration. Must not 
100         *                be {@code null}.
101         * @param timeout The SRP-6a authentication session timeout in seconds. 
102         *                If the authenticating counterparty (server or client) 
103         *                fails to respond within the specified time the session
104         *                will be closed. If zero timeouts are disabled.
105         */
106        public SRP6ServerSession(final SRP6CryptoParams config, final int timeout) {
107        
108                super(timeout);
109                
110                if (config == null)
111                        throw new IllegalArgumentException("The SRP-6a crypto parameters must not be null");
112
113                this.config = config;
114                
115                digest = config.getMessageDigestInstance();
116                
117                if (digest == null)
118                        throw new IllegalArgumentException("Unsupported hash algorithm 'H': " + config.H);
119
120                state = State.INIT;
121                
122                updateLastActivityTime();
123        }
124        
125        
126        /**
127         * Creates a new server-side SRP-6a authentication session and sets its 
128         * state to {@link State#INIT}. Session timeouts are disabled.
129         *
130         * @param config  The SRP-6a crypto parameters configuration. Must not 
131         *                be {@code null}.
132         */
133        public SRP6ServerSession(final SRP6CryptoParams config) {
134        
135                this(config, 0);
136        }
137        
138        
139        /**
140         * Increments this SRP-6a authentication session to 
141         * {@link State#STEP_1}.
142         *
143         * <p>Argument origin:
144         * 
145         * <ul>
146         *     <li>From client: user identity 'I'.
147         *     <li>From server database: matching salt 's' and password verifier
148         *        'v' values.
149         * </ul>
150         *
151         * @param userID The identity 'I' of the authenticating user. Must not 
152         *               be {@code null} or empty.
153         * @param s      The password salt 's'. Must not be {@code null}.
154         * @param v      The password verifier 'v'. Must not be {@code null}.
155         *
156         * @return The server public value 'B'.
157         *
158         * @throws IllegalStateException If the mehod is invoked in a state 
159         *                               other than {@link State#INIT}.
160         */
161        public BigInteger step1(final String userID, final BigInteger s, final BigInteger v) {
162        
163                // Check arguments
164                
165                if (userID == null || userID.trim().isEmpty())
166                        throw new IllegalArgumentException("The user identity 'I' must not be null or empty");
167                        
168                this.userID = userID;
169                
170                
171                if (s == null)
172                        throw new IllegalArgumentException("The salt 's' must not be null");
173                        
174                this.s = s;
175                
176                
177                if (v == null)
178                        throw new IllegalArgumentException("The verifier 'v' must not be null");
179                        
180                this.v = v;
181                
182                
183                // Check current state
184                if (state != State.INIT)
185                        throw new IllegalStateException("State violation: Session must be in INIT state");
186                
187                // Generate server private and public values
188                k = SRP6Routines.computeK(digest, config.N, config.g);
189                digest.reset();
190                
191                b = SRP6Routines.generatePrivateValue(config.N, random);
192                digest.reset();
193                
194                B = SRP6Routines.computePublicServerValue(config.N, config.g, k, v, b);
195
196                state = State.STEP_1;
197                
198                updateLastActivityTime();
199                
200                return B;
201        }
202        
203        
204        /**
205         * Increments this SRP-6a authentication session to 
206         * {@link State#STEP_1} indicating a non-existing user identity 'I'
207         * with mock (simulated) salt 's' and password verifier 'v' values.
208         *
209         * <p>This method can be used to avoid informing the client at step one
210         * that the user identity is bad and throw instead a guaranteed general 
211         * "bad credentials" SRP-6a exception at step two.
212         *
213         * <p>Argument origin:
214         * 
215         * <ul>
216         *     <li>From client: user identity 'I'.
217         *     <li>Simulated by server, preferably consistently for the
218         *         specified identity 'I': salt 's' and password verifier 'v' 
219         *         values.
220         * </ul>
221         *
222         * @param userID The identity 'I' of the authenticating user. Must not 
223         *               be {@code null} or empty.
224         * @param s      The password salt 's'. Must not be {@code null}.
225         * @param v      The password verifier 'v'. Must not be {@code null}.
226         *
227         * @return The server public value 'B'.
228         *
229         * @throws IllegalStateException If the method is invoked in a state
230         *                               other than {@link State#INIT}.
231         */
232        public BigInteger mockStep1(final String userID, final BigInteger s, final BigInteger v) {
233        
234                noSuchUserIdentity = true;
235                
236                return step1(userID, s, v);
237        }
238        
239        
240        /**
241         * Increments this SRP-6a authentication session to 
242         * {@link State#STEP_2}.
243         *
244         * <p>Argument origin:
245         * 
246         * <ul>
247         *     <li>From client: public value 'A' and evidence message 'M1'.
248         * </ul>
249         *
250         * @param A  The client public value. Must not be {@code null}.
251         * @param M1 The client evidence message. Must not be {@code null}.
252         *
253         * @return The server evidence message 'M2'.
254         *
255         * @throws SRP6Exception If the session has timed out, the client public
256         *                       value 'A' is invalid or the user credentials
257         *                       are invalid.
258         *
259         * @throws IllegalStateException If the method is invoked in a state
260         *                               other than {@link State#STEP_1}.
261         */
262        public BigInteger step2(final BigInteger A, final BigInteger M1)
263                throws SRP6Exception {
264        
265                // Check arguments
266                
267                if (A == null)
268                        throw new IllegalArgumentException("The client public value 'A' must not be null");
269                        
270                this.A = A;
271                
272                if (M1 == null)
273                        throw new IllegalArgumentException("The client evidence message 'M1' must not be null");
274                
275                this.M1 = M1;
276        
277                // Check current state
278                if (state != State.STEP_1)
279                        throw new IllegalStateException("State violation: Session must be in STEP_1 state");
280                
281                // Check timeout
282                if (hasTimedOut())
283                        throw new SRP6Exception("Session timeout", SRP6Exception.CauseType.TIMEOUT);
284        
285                // Check A validity
286                if (! SRP6Routines.isValidPublicValue(config.N, A))
287                        throw new SRP6Exception("Bad client public value 'A'", SRP6Exception.CauseType.BAD_PUBLIC_VALUE);
288                
289                // Check for previous mock step 1
290                if (noSuchUserIdentity)
291                        throw new SRP6Exception("Bad client credentials", SRP6Exception.CauseType.BAD_CREDENTIALS);
292                
293                if (hashedKeysRoutine != null) {
294                        URoutineContext hashedKeysContext = new URoutineContext(A, B);
295                        u = hashedKeysRoutine.computeU(config, hashedKeysContext);
296                } else {
297                        u = SRP6Routines.computeU(digest, config.N, A, B);
298                        digest.reset();
299                }
300                
301                S = SRP6Routines.computeSessionKey(config.N, v, u, A, b);
302                
303                // Compute the own client evidence message 'M1'
304                BigInteger computedM1;
305                
306                if (clientEvidenceRoutine != null) {
307                
308                        // With custom routine
309                        SRP6ClientEvidenceContext ctx = new SRP6ClientEvidenceContext(userID, s, A, B, S);
310                        computedM1 = clientEvidenceRoutine.computeClientEvidence(config, ctx);
311                }
312                else {
313                        // With default routine
314                        computedM1 = SRP6Routines.computeClientEvidence(digest, A, B, S);
315                        digest.reset();
316                }
317                
318                if (! computedM1.equals(M1))
319                        throw new SRP6Exception("Bad client credentials", SRP6Exception.CauseType.BAD_CREDENTIALS);
320        
321                state = State.STEP_2;
322                
323                
324                if (serverEvidenceRoutine != null) {
325                
326                        // With custom routine
327                        SRP6ServerEvidenceContext ctx = new SRP6ServerEvidenceContext(A, M1, S);
328                        
329                        M2 = serverEvidenceRoutine.computeServerEvidence(config, ctx);
330                }
331                else {
332                        // With default routine
333                        M2 = SRP6Routines.computeServerEvidence(digest, A, M1, S);
334                }
335                
336                updateLastActivityTime();
337                
338                return M2;
339        }
340        
341        
342        /**
343         * Returns the current state of this SRP-6a authentication session.
344         *
345         * @return The current state.
346         */
347        public State getState() {
348        
349                return state;
350        }
351}