001package com.bitbucket.thinbus.srp6.js;
002
003import static com.nimbusds.srp6.BigIntegerUtils.fromHex;
004import static com.nimbusds.srp6.BigIntegerUtils.toHex;
005
006import java.math.BigInteger;
007import java.security.MessageDigest;
008
009import com.nimbusds.srp6.SRP6ClientCredentials;
010import com.nimbusds.srp6.SRP6ClientSession;
011import com.nimbusds.srp6.SRP6ClientSession.State;
012import com.nimbusds.srp6.SRP6CryptoParams;
013import com.nimbusds.srp6.SRP6Exception;
014import com.nimbusds.srp6.SRP6Routines;
015
016/**
017 * If you want to have both Java clients and JavaScript clients authenticate to
018 * the same Java server then this class is a workalike to the JavaScript client
019 * session. This class is a thin wrapper to a Nimbus SRP6ClientSession which is
020 * configured to work with the Thinbus server session.
021 */
022abstract public class SRP6JavaClientSession {
023
024        final SRP6Routines srp6Routines = new SRP6Routines();
025
026        /**
027         * The crypto parameters for the SRP-6a protocol. These must be agreed
028         * between client and server before authentication and consist of a large
029         * safe prime 'N', a corresponding generator 'g' and a hash function
030         * algorithm 'H'. You can generate your own with openssl using
031         * {@link OpenSSLCryptoConfigConverter}
032         * 
033         */
034        protected final SRP6CryptoParams config;
035
036        /**
037         * The underlying Nimbus session which will be configure for JavaScript
038         * interactions
039         */
040        protected final SRP6ClientSession session;
041
042        /**
043         * Records the identity 'I' and password 'P' of the authenticating user. The
044         * session is incremented to {@link State#STEP_1}.
045         * 
046         * <p>
047         * Argument origin:
048         * 
049         * <ul>
050         * <li>From user: user identity 'I' and password 'P'.
051         * </ul>
052         * 
053         * @param userID
054         *            The identity 'I' of the authenticating user, UTF-8 encoded.
055         *            Must not be {@code null} or empty.
056         * @param password
057         *            The user password 'P', UTF-8 encoded. Must not be {@code null}
058         *            .
059         * 
060         * @throws IllegalStateException
061         *             If the method is invoked in a state other than
062         *             {@link State#INIT}.
063         */
064        public void step1(String userID, String password) {
065                session.step1(userID, password);
066        }
067
068        /**
069         * Receives the password salt 's' and public value 'B' from the server. The
070         * SRP-6a crypto parameters are also set. The session is incremented to
071         * {@link State#STEP_2}.
072         *
073         * <p>
074         * Argument origin:
075         * 
076         * <ul>
077         * <li>From server: password salt 's', public value 'B'.
078         * <li>From server or pre-agreed: crypto parameters prime 'N', generator 'g'
079         * <li>Pre-agreed: crypto parameters prime 'H'
080         * </ul>
081         *
082         * @param s
083         *            The password salt 's'. Must not be {@code null}.
084         * @param B
085         *            The public server value 'B'. Must not be {@code null}.
086         *
087         * @return The client credentials consisting of the client public key 'A'
088         *         and the client evidence message 'M1'.
089         *
090         * @throws IllegalStateException
091         *             If the method is invoked in a state other than
092         *             {@link State#STEP_1}.
093         * @throws SRP6Exception
094         *             If the session has timed out or the public server value 'B'
095         *             is invalid.
096         */
097        public SRP6ClientCredentials step2(String s, String B) throws SRP6Exception {
098                return session.step2(config, fromHex(s), fromHex(B));
099        }
100
101        /**
102         * Receives the server evidence message 'M1'. The session is incremented to
103         * {@link State#STEP_3}.
104         * 
105         * <p>
106         * Argument origin:
107         * 
108         * <ul>
109         * <li>From server: evidence message 'M2'.
110         * </ul>
111         * 
112         * @param M2
113         *            The server evidence message 'M2'. Must not be {@code null}.
114         * 
115         * @throws IllegalStateException
116         *             If the method is invoked in a state other than
117         *             {@link State#STEP_2}.
118         * @throws SRP6Exception
119         *             If the session has timed out or the server evidence message
120         *             'M2' is invalid.
121         */
122        public void step3(String M2) throws SRP6Exception {
123                session.step3(fromHex(M2));
124        }
125
126        /**
127         * Constructs a Java client session compatible with the server session which
128         * words with Java. underlying Nimbus SRP6ClientSession.
129         * 
130         * @param srp6CryptoParams
131         *            cryptographic constants which must match those being used by
132         *            the client.
133         */
134        public SRP6JavaClientSession(SRP6CryptoParams srp6CryptoParams) {
135                this.config = srp6CryptoParams;
136                session = new SRP6ClientSession();
137                session.setHashedKeysRoutine(new HexHashedURoutine());
138                session.setClientEvidenceRoutine(new HexHashedClientEvidenceRoutine());
139                session.setServerEvidenceRoutine(new HexHashedServerEvidenceRoutine());
140                session.setXRoutine(new HexHashedXRoutine(this.config.N));
141        }
142
143        /**
144         * Generates a salt value 's'. The salt s is a public value in the protocol
145         * which is fixed per user and would be stored in the user database. The
146         * desired property is that it is unique for every user in your system. This
147         * can be ensured by adding a uniqueness constraint to a not null salt
148         * column within the database which is strongly recommended. Then it does
149         * not matter whether this public value has been generated using a good
150         * secure random number at the server or using a weaker random number
151         * generator at the browser. You simply reduce the probability of database
152         * constraint exceptions if you use a better random number. The Thinbus
153         * Javascript client session provides a method generateRandomSalt to run at
154         * the browser to create 's' which can be invoked with, or without, passing
155         * a sever generated secure random number or avoided entirely by generating
156         * the salt at the server. This method is the server version which you can
157         * use exclusively else mix with a client generated value.
158         * 
159         * @param numBytes
160         *            Number of random bytes. Recommended is greater than the bit
161         *            length of the chosen hash e.g. HASH_HEX_LENGTH constant of
162         *            server session is x2 hash length so a reasonable choice.
163         * 
164         * @return A hex encoded random salt value.
165         */
166        public String generateRandomSalt(final int numBytes) {
167                byte[] bytes = this.srp6Routines.generateRandomSalt(numBytes);
168                MessageDigest digest = config.getMessageDigestInstance();
169                digest.reset();
170                digest.update(bytes, 0, bytes.length);
171                BigInteger bi = new BigInteger(1, digest.digest());
172                return toHex(bi);
173        }
174
175        /**
176         * Gets the identity 'I' of the authenticating user.
177         *
178         * @return The user identity 'I', {@code null} if undefined.
179         */
180        public String getUserID() {
181                return session.getUserID();
182        }
183
184        /**
185         * Gets the password salt 's'.
186         * 
187         * @return The salt 's' if available, else {@code null}.
188         */
189        public String getSalt() {
190                return toHex(session.getSalt());
191        }
192
193        /**
194         * Gets the public client value 'A'.
195         *
196         * @return The public client value 'A' if available, else {@code null}.
197         */
198        public String getPublicClientValue() {
199                return toHex(session.getPublicClientValue());
200        }
201
202        /**
203         * Gets the client evidence message 'M1'.
204         *
205         * @return The client evidence message 'M1' if available, else {@code null}.
206         */
207        public String getClientEvidenceMessage() {
208                return toHex(session.getClientEvidenceMessage());
209        }
210
211        /**
212         * Returns the current state of this SRP-6a authentication session.
213         *
214         * @return The current state.
215         */
216        public State getState() {
217                return session.getState();
218        }
219
220        /**
221         * Gets the shared session key 'S' or its hash H(S).
222         *
223         * @param doHash
224         *            If {@code true} the hash H(S) of the session key will be
225         *            returned instead of the raw value.
226         *
227         * @return The shared session key 'S' or its hash H(S). {@code null} will be
228         *         returned if authentication failed or the method is invoked in a
229         *         session state when the session key 'S' has not been computed yet.
230         */
231        public String getSessionKey(boolean doHash) {
232                String S = toHex(session.getSessionKey());
233                if (doHash) {
234                        String K = HexHashedRoutines.toHexString(this.config
235                                        .getMessageDigestInstance().digest(
236                                                        S.getBytes(HexHashedRoutines.utf8)));
237                        return K;
238                } else {
239                        return S;
240                }
241        }
242}