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