1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/background/fxa/FxAccount10AuthDelegate.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,218 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +package org.mozilla.gecko.background.fxa; 1.9 + 1.10 +import java.io.UnsupportedEncodingException; 1.11 +import java.math.BigInteger; 1.12 +import java.security.NoSuchAlgorithmException; 1.13 + 1.14 +import org.json.simple.JSONObject; 1.15 +import org.mozilla.gecko.sync.ExtendedJSONObject; 1.16 +import org.mozilla.gecko.sync.Utils; 1.17 +import org.mozilla.gecko.sync.net.SRPConstants; 1.18 + 1.19 +public class FxAccount10AuthDelegate implements FxAccountClient10.AuthDelegate { 1.20 + // Fixed by protocol. 1.21 + protected final BigInteger N; 1.22 + protected final BigInteger g; 1.23 + protected final int modNLengthBytes; 1.24 + 1.25 + // Configured at construction time. 1.26 + protected final String email; 1.27 + protected final byte[] stretchedPWBytes; 1.28 + 1.29 + // Encapsulate state. 1.30 + protected static class AuthState { 1.31 + protected String srpToken; 1.32 + protected String mainSalt; 1.33 + protected String srpSalt; 1.34 + 1.35 + protected BigInteger x; 1.36 + protected BigInteger A; 1.37 + protected byte[] Kbytes; 1.38 + protected byte[] Mbytes; 1.39 + } 1.40 + 1.41 + // State should be written exactly once. 1.42 + protected AuthState internalAuthState = null; 1.43 + 1.44 + public FxAccount10AuthDelegate(String email, byte[] stretchedPWBytes) { 1.45 + this.email = email; 1.46 + this.stretchedPWBytes = stretchedPWBytes; 1.47 + this.N = SRPConstants._2048.N; 1.48 + this.g = SRPConstants._2048.g; 1.49 + this.modNLengthBytes = SRPConstants._2048.byteLength; 1.50 + } 1.51 + 1.52 + protected BigInteger generateSecretValue() { 1.53 + return Utils.generateBigIntegerLessThan(N); 1.54 + } 1.55 + 1.56 + public static class FxAccountClientMalformedAuthException extends FxAccountClientException { 1.57 + private static final long serialVersionUID = 3585262174699395505L; 1.58 + 1.59 + public FxAccountClientMalformedAuthException(String detailMessage) { 1.60 + super(detailMessage); 1.61 + } 1.62 + } 1.63 + 1.64 + @SuppressWarnings("unchecked") 1.65 + @Override 1.66 + public JSONObject getAuthStartBody() throws FxAccountClientException { 1.67 + try { 1.68 + final JSONObject body = new JSONObject(); 1.69 + body.put("email", FxAccountUtils.bytes(email)); 1.70 + return body; 1.71 + } catch (UnsupportedEncodingException e) { 1.72 + throw new FxAccountClientException(e); 1.73 + } 1.74 + } 1.75 + 1.76 + @Override 1.77 + public void onAuthStartResponse(final ExtendedJSONObject body) throws FxAccountClientException { 1.78 + if (this.internalAuthState != null) { 1.79 + throw new FxAccountClientException("auth must not be written before calling onAuthStartResponse"); 1.80 + } 1.81 + 1.82 + String srpToken = null; 1.83 + String srpSalt = null; 1.84 + String srpB = null; 1.85 + String mainSalt = null; 1.86 + 1.87 + try { 1.88 + srpToken = body.getString("srpToken"); 1.89 + if (srpToken == null) { 1.90 + throw new FxAccountClientMalformedAuthException("srpToken must be a non-null object"); 1.91 + } 1.92 + ExtendedJSONObject srp = body.getObject("srp"); 1.93 + if (srp == null) { 1.94 + throw new FxAccountClientMalformedAuthException("srp must be a non-null object"); 1.95 + } 1.96 + srpSalt = srp.getString("salt"); 1.97 + if (srpSalt == null) { 1.98 + throw new FxAccountClientMalformedAuthException("srp.salt must not be null"); 1.99 + } 1.100 + srpB = srp.getString("B"); 1.101 + if (srpB == null) { 1.102 + throw new FxAccountClientMalformedAuthException("srp.B must not be null"); 1.103 + } 1.104 + ExtendedJSONObject passwordStretching = body.getObject("passwordStretching"); 1.105 + if (passwordStretching == null) { 1.106 + throw new FxAccountClientMalformedAuthException("passwordStretching must be a non-null object"); 1.107 + } 1.108 + mainSalt = passwordStretching.getString("salt"); 1.109 + if (mainSalt == null) { 1.110 + throw new FxAccountClientMalformedAuthException("srp.passwordStretching.salt must not be null"); 1.111 + } 1.112 + throwIfParametersAreBad(passwordStretching); 1.113 + 1.114 + this.internalAuthState = authStateFromParameters(srpToken, mainSalt, srpSalt, srpB, generateSecretValue()); 1.115 + } catch (FxAccountClientException e) { 1.116 + throw e; 1.117 + } catch (Exception e) { 1.118 + throw new FxAccountClientException(e); 1.119 + } 1.120 + } 1.121 + 1.122 + /** 1.123 + * Expect object like: 1.124 + * "passwordStretching": { 1.125 + * "type": "PBKDF2/scrypt/PBKDF2/v1", 1.126 + * "PBKDF2_rounds_1": 20000, 1.127 + * "scrypt_N": 65536, 1.128 + * "scrypt_r": 8, 1.129 + * "scrypt_p": 1, 1.130 + * "PBKDF2_rounds_2": 20000, 1.131 + * "salt": "996bc6b1aa63cd69856a2ec81cbf19d5c8a604713362df9ee15c2bf07128efab" 1.132 + * } 1.133 + * @param params to verify. 1.134 + * @throws FxAccountClientMalformedAuthException 1.135 + */ 1.136 + protected void throwIfParametersAreBad(ExtendedJSONObject params) throws FxAccountClientMalformedAuthException { 1.137 + if (params == null || 1.138 + params.size() != 7 || 1.139 + params.getString("salt") == null || 1.140 + !("PBKDF2/scrypt/PBKDF2/v1".equals(params.getString("type"))) || 1.141 + 20000 != params.getLong("PBKDF2_rounds_1") || 1.142 + 65536 != params.getLong("scrypt_N") || 1.143 + 8 != params.getLong("scrypt_r") || 1.144 + 1 != params.getLong("scrypt_p") || 1.145 + 20000 != params.getLong("PBKDF2_rounds_2")) { 1.146 + throw new FxAccountClientMalformedAuthException("malformed passwordStretching parameters: '" + params.toJSONString() + "'."); 1.147 + } 1.148 + } 1.149 + 1.150 + /** 1.151 + * All state is written in this method. 1.152 + */ 1.153 + protected AuthState authStateFromParameters(String srpToken, String mainSalt, String srpSalt, String srpB, BigInteger a) throws NoSuchAlgorithmException, UnsupportedEncodingException { 1.154 + AuthState authState = new AuthState(); 1.155 + authState.srpToken = srpToken; 1.156 + authState.mainSalt = mainSalt; 1.157 + authState.srpSalt = srpSalt; 1.158 + 1.159 + authState.x = FxAccountUtils.srpVerifierLowercaseX(email.getBytes("UTF-8"), this.stretchedPWBytes, Utils.hex2Byte(srpSalt, FxAccountUtils.SALT_LENGTH_BYTES)); 1.160 + 1.161 + authState.A = g.modPow(a, N); 1.162 + String srpA = FxAccountUtils.hexModN(authState.A, N); 1.163 + BigInteger B = new BigInteger(srpB, 16); 1.164 + 1.165 + byte[] srpABytes = Utils.hex2Byte(srpA, modNLengthBytes); 1.166 + byte[] srpBBytes = Utils.hex2Byte(srpB, modNLengthBytes); 1.167 + 1.168 + // u = H(pad(A) | pad(B)) 1.169 + byte[] uBytes = Utils.sha256(Utils.concatAll( 1.170 + srpABytes, 1.171 + srpBBytes)); 1.172 + BigInteger u = new BigInteger(Utils.byte2Hex(uBytes, FxAccountUtils.HASH_LENGTH_HEX), 16); 1.173 + 1.174 + // S = (B - k*g^x)^(a u*x) % N 1.175 + // k = H(pad(N) | pad(g)) 1.176 + int byteLength = (N.bitLength() + 7) / 8; 1.177 + byte[] kBytes = Utils.sha256(Utils.concatAll( 1.178 + Utils.hex2Byte(N.toString(16), byteLength), 1.179 + Utils.hex2Byte(g.toString(16), byteLength))); 1.180 + BigInteger k = new BigInteger(Utils.byte2Hex(kBytes, FxAccountUtils.HASH_LENGTH_HEX), 16); 1.181 + 1.182 + BigInteger base = B.subtract(k.multiply(g.modPow(authState.x, N)).mod(N)).mod(N); 1.183 + BigInteger pow = a.add(u.multiply(authState.x)); 1.184 + BigInteger S = base.modPow(pow, N); 1.185 + String srpS = FxAccountUtils.hexModN(S, N); 1.186 + 1.187 + byte[] sBytes = Utils.hex2Byte(srpS, modNLengthBytes); 1.188 + 1.189 + // M = H(pad(A) | pad(B) | pad(S)) 1.190 + authState.Mbytes = Utils.sha256(Utils.concatAll( 1.191 + srpABytes, 1.192 + srpBBytes, 1.193 + sBytes)); 1.194 + 1.195 + // K = H(pad(S)) 1.196 + authState.Kbytes = Utils.sha256(sBytes); 1.197 + 1.198 + return authState; 1.199 + } 1.200 + 1.201 + @SuppressWarnings("unchecked") 1.202 + @Override 1.203 + public JSONObject getAuthFinishBody() throws FxAccountClientException { 1.204 + if (internalAuthState == null) { 1.205 + throw new FxAccountClientException("auth must be successfully written before calling getAuthFinishBody."); 1.206 + } 1.207 + JSONObject body = new JSONObject(); 1.208 + body.put("srpToken", internalAuthState.srpToken); 1.209 + body.put("A", FxAccountUtils.hexModN(internalAuthState.A, N)); 1.210 + body.put("M", Utils.byte2Hex(internalAuthState.Mbytes, FxAccountUtils.HASH_LENGTH_HEX)); 1.211 + return body; 1.212 + } 1.213 + 1.214 + @Override 1.215 + public byte[] getSharedBytes() throws FxAccountClientException { 1.216 + if (internalAuthState == null) { 1.217 + throw new FxAccountClientException("auth must be successfully finished before calling getSharedBytes."); 1.218 + } 1.219 + return internalAuthState.Kbytes; 1.220 + } 1.221 +}