Wed, 31 Dec 2014 07:22:50 +0100
Correct previous dual key logic pending first delivery installment.
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 package org.mozilla.gecko.background.fxa;
7 import java.io.UnsupportedEncodingException;
8 import java.math.BigInteger;
9 import java.security.NoSuchAlgorithmException;
11 import org.json.simple.JSONObject;
12 import org.mozilla.gecko.sync.ExtendedJSONObject;
13 import org.mozilla.gecko.sync.Utils;
14 import org.mozilla.gecko.sync.net.SRPConstants;
16 public class FxAccount10AuthDelegate implements FxAccountClient10.AuthDelegate {
17 // Fixed by protocol.
18 protected final BigInteger N;
19 protected final BigInteger g;
20 protected final int modNLengthBytes;
22 // Configured at construction time.
23 protected final String email;
24 protected final byte[] stretchedPWBytes;
26 // Encapsulate state.
27 protected static class AuthState {
28 protected String srpToken;
29 protected String mainSalt;
30 protected String srpSalt;
32 protected BigInteger x;
33 protected BigInteger A;
34 protected byte[] Kbytes;
35 protected byte[] Mbytes;
36 }
38 // State should be written exactly once.
39 protected AuthState internalAuthState = null;
41 public FxAccount10AuthDelegate(String email, byte[] stretchedPWBytes) {
42 this.email = email;
43 this.stretchedPWBytes = stretchedPWBytes;
44 this.N = SRPConstants._2048.N;
45 this.g = SRPConstants._2048.g;
46 this.modNLengthBytes = SRPConstants._2048.byteLength;
47 }
49 protected BigInteger generateSecretValue() {
50 return Utils.generateBigIntegerLessThan(N);
51 }
53 public static class FxAccountClientMalformedAuthException extends FxAccountClientException {
54 private static final long serialVersionUID = 3585262174699395505L;
56 public FxAccountClientMalformedAuthException(String detailMessage) {
57 super(detailMessage);
58 }
59 }
61 @SuppressWarnings("unchecked")
62 @Override
63 public JSONObject getAuthStartBody() throws FxAccountClientException {
64 try {
65 final JSONObject body = new JSONObject();
66 body.put("email", FxAccountUtils.bytes(email));
67 return body;
68 } catch (UnsupportedEncodingException e) {
69 throw new FxAccountClientException(e);
70 }
71 }
73 @Override
74 public void onAuthStartResponse(final ExtendedJSONObject body) throws FxAccountClientException {
75 if (this.internalAuthState != null) {
76 throw new FxAccountClientException("auth must not be written before calling onAuthStartResponse");
77 }
79 String srpToken = null;
80 String srpSalt = null;
81 String srpB = null;
82 String mainSalt = null;
84 try {
85 srpToken = body.getString("srpToken");
86 if (srpToken == null) {
87 throw new FxAccountClientMalformedAuthException("srpToken must be a non-null object");
88 }
89 ExtendedJSONObject srp = body.getObject("srp");
90 if (srp == null) {
91 throw new FxAccountClientMalformedAuthException("srp must be a non-null object");
92 }
93 srpSalt = srp.getString("salt");
94 if (srpSalt == null) {
95 throw new FxAccountClientMalformedAuthException("srp.salt must not be null");
96 }
97 srpB = srp.getString("B");
98 if (srpB == null) {
99 throw new FxAccountClientMalformedAuthException("srp.B must not be null");
100 }
101 ExtendedJSONObject passwordStretching = body.getObject("passwordStretching");
102 if (passwordStretching == null) {
103 throw new FxAccountClientMalformedAuthException("passwordStretching must be a non-null object");
104 }
105 mainSalt = passwordStretching.getString("salt");
106 if (mainSalt == null) {
107 throw new FxAccountClientMalformedAuthException("srp.passwordStretching.salt must not be null");
108 }
109 throwIfParametersAreBad(passwordStretching);
111 this.internalAuthState = authStateFromParameters(srpToken, mainSalt, srpSalt, srpB, generateSecretValue());
112 } catch (FxAccountClientException e) {
113 throw e;
114 } catch (Exception e) {
115 throw new FxAccountClientException(e);
116 }
117 }
119 /**
120 * Expect object like:
121 * "passwordStretching": {
122 * "type": "PBKDF2/scrypt/PBKDF2/v1",
123 * "PBKDF2_rounds_1": 20000,
124 * "scrypt_N": 65536,
125 * "scrypt_r": 8,
126 * "scrypt_p": 1,
127 * "PBKDF2_rounds_2": 20000,
128 * "salt": "996bc6b1aa63cd69856a2ec81cbf19d5c8a604713362df9ee15c2bf07128efab"
129 * }
130 * @param params to verify.
131 * @throws FxAccountClientMalformedAuthException
132 */
133 protected void throwIfParametersAreBad(ExtendedJSONObject params) throws FxAccountClientMalformedAuthException {
134 if (params == null ||
135 params.size() != 7 ||
136 params.getString("salt") == null ||
137 !("PBKDF2/scrypt/PBKDF2/v1".equals(params.getString("type"))) ||
138 20000 != params.getLong("PBKDF2_rounds_1") ||
139 65536 != params.getLong("scrypt_N") ||
140 8 != params.getLong("scrypt_r") ||
141 1 != params.getLong("scrypt_p") ||
142 20000 != params.getLong("PBKDF2_rounds_2")) {
143 throw new FxAccountClientMalformedAuthException("malformed passwordStretching parameters: '" + params.toJSONString() + "'.");
144 }
145 }
147 /**
148 * All state is written in this method.
149 */
150 protected AuthState authStateFromParameters(String srpToken, String mainSalt, String srpSalt, String srpB, BigInteger a) throws NoSuchAlgorithmException, UnsupportedEncodingException {
151 AuthState authState = new AuthState();
152 authState.srpToken = srpToken;
153 authState.mainSalt = mainSalt;
154 authState.srpSalt = srpSalt;
156 authState.x = FxAccountUtils.srpVerifierLowercaseX(email.getBytes("UTF-8"), this.stretchedPWBytes, Utils.hex2Byte(srpSalt, FxAccountUtils.SALT_LENGTH_BYTES));
158 authState.A = g.modPow(a, N);
159 String srpA = FxAccountUtils.hexModN(authState.A, N);
160 BigInteger B = new BigInteger(srpB, 16);
162 byte[] srpABytes = Utils.hex2Byte(srpA, modNLengthBytes);
163 byte[] srpBBytes = Utils.hex2Byte(srpB, modNLengthBytes);
165 // u = H(pad(A) | pad(B))
166 byte[] uBytes = Utils.sha256(Utils.concatAll(
167 srpABytes,
168 srpBBytes));
169 BigInteger u = new BigInteger(Utils.byte2Hex(uBytes, FxAccountUtils.HASH_LENGTH_HEX), 16);
171 // S = (B - k*g^x)^(a u*x) % N
172 // k = H(pad(N) | pad(g))
173 int byteLength = (N.bitLength() + 7) / 8;
174 byte[] kBytes = Utils.sha256(Utils.concatAll(
175 Utils.hex2Byte(N.toString(16), byteLength),
176 Utils.hex2Byte(g.toString(16), byteLength)));
177 BigInteger k = new BigInteger(Utils.byte2Hex(kBytes, FxAccountUtils.HASH_LENGTH_HEX), 16);
179 BigInteger base = B.subtract(k.multiply(g.modPow(authState.x, N)).mod(N)).mod(N);
180 BigInteger pow = a.add(u.multiply(authState.x));
181 BigInteger S = base.modPow(pow, N);
182 String srpS = FxAccountUtils.hexModN(S, N);
184 byte[] sBytes = Utils.hex2Byte(srpS, modNLengthBytes);
186 // M = H(pad(A) | pad(B) | pad(S))
187 authState.Mbytes = Utils.sha256(Utils.concatAll(
188 srpABytes,
189 srpBBytes,
190 sBytes));
192 // K = H(pad(S))
193 authState.Kbytes = Utils.sha256(sBytes);
195 return authState;
196 }
198 @SuppressWarnings("unchecked")
199 @Override
200 public JSONObject getAuthFinishBody() throws FxAccountClientException {
201 if (internalAuthState == null) {
202 throw new FxAccountClientException("auth must be successfully written before calling getAuthFinishBody.");
203 }
204 JSONObject body = new JSONObject();
205 body.put("srpToken", internalAuthState.srpToken);
206 body.put("A", FxAccountUtils.hexModN(internalAuthState.A, N));
207 body.put("M", Utils.byte2Hex(internalAuthState.Mbytes, FxAccountUtils.HASH_LENGTH_HEX));
208 return body;
209 }
211 @Override
212 public byte[] getSharedBytes() throws FxAccountClientException {
213 if (internalAuthState == null) {
214 throw new FxAccountClientException("auth must be successfully finished before calling getSharedBytes.");
215 }
216 return internalAuthState.Kbytes;
217 }
218 }