|
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/. */ |
|
4 |
|
5 package org.mozilla.gecko.background.fxa; |
|
6 |
|
7 import java.io.UnsupportedEncodingException; |
|
8 import java.math.BigInteger; |
|
9 import java.security.NoSuchAlgorithmException; |
|
10 |
|
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; |
|
15 |
|
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; |
|
21 |
|
22 // Configured at construction time. |
|
23 protected final String email; |
|
24 protected final byte[] stretchedPWBytes; |
|
25 |
|
26 // Encapsulate state. |
|
27 protected static class AuthState { |
|
28 protected String srpToken; |
|
29 protected String mainSalt; |
|
30 protected String srpSalt; |
|
31 |
|
32 protected BigInteger x; |
|
33 protected BigInteger A; |
|
34 protected byte[] Kbytes; |
|
35 protected byte[] Mbytes; |
|
36 } |
|
37 |
|
38 // State should be written exactly once. |
|
39 protected AuthState internalAuthState = null; |
|
40 |
|
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 } |
|
48 |
|
49 protected BigInteger generateSecretValue() { |
|
50 return Utils.generateBigIntegerLessThan(N); |
|
51 } |
|
52 |
|
53 public static class FxAccountClientMalformedAuthException extends FxAccountClientException { |
|
54 private static final long serialVersionUID = 3585262174699395505L; |
|
55 |
|
56 public FxAccountClientMalformedAuthException(String detailMessage) { |
|
57 super(detailMessage); |
|
58 } |
|
59 } |
|
60 |
|
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 } |
|
72 |
|
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 } |
|
78 |
|
79 String srpToken = null; |
|
80 String srpSalt = null; |
|
81 String srpB = null; |
|
82 String mainSalt = null; |
|
83 |
|
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); |
|
110 |
|
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 } |
|
118 |
|
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 } |
|
146 |
|
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; |
|
155 |
|
156 authState.x = FxAccountUtils.srpVerifierLowercaseX(email.getBytes("UTF-8"), this.stretchedPWBytes, Utils.hex2Byte(srpSalt, FxAccountUtils.SALT_LENGTH_BYTES)); |
|
157 |
|
158 authState.A = g.modPow(a, N); |
|
159 String srpA = FxAccountUtils.hexModN(authState.A, N); |
|
160 BigInteger B = new BigInteger(srpB, 16); |
|
161 |
|
162 byte[] srpABytes = Utils.hex2Byte(srpA, modNLengthBytes); |
|
163 byte[] srpBBytes = Utils.hex2Byte(srpB, modNLengthBytes); |
|
164 |
|
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); |
|
170 |
|
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); |
|
178 |
|
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); |
|
183 |
|
184 byte[] sBytes = Utils.hex2Byte(srpS, modNLengthBytes); |
|
185 |
|
186 // M = H(pad(A) | pad(B) | pad(S)) |
|
187 authState.Mbytes = Utils.sha256(Utils.concatAll( |
|
188 srpABytes, |
|
189 srpBBytes, |
|
190 sBytes)); |
|
191 |
|
192 // K = H(pad(S)) |
|
193 authState.Kbytes = Utils.sha256(sBytes); |
|
194 |
|
195 return authState; |
|
196 } |
|
197 |
|
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 } |
|
210 |
|
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 } |