michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko.background.fxa; michael@0: michael@0: import java.io.UnsupportedEncodingException; michael@0: import java.math.BigInteger; michael@0: import java.security.NoSuchAlgorithmException; michael@0: michael@0: import org.json.simple.JSONObject; michael@0: import org.mozilla.gecko.sync.ExtendedJSONObject; michael@0: import org.mozilla.gecko.sync.Utils; michael@0: import org.mozilla.gecko.sync.net.SRPConstants; michael@0: michael@0: public class FxAccount10AuthDelegate implements FxAccountClient10.AuthDelegate { michael@0: // Fixed by protocol. michael@0: protected final BigInteger N; michael@0: protected final BigInteger g; michael@0: protected final int modNLengthBytes; michael@0: michael@0: // Configured at construction time. michael@0: protected final String email; michael@0: protected final byte[] stretchedPWBytes; michael@0: michael@0: // Encapsulate state. michael@0: protected static class AuthState { michael@0: protected String srpToken; michael@0: protected String mainSalt; michael@0: protected String srpSalt; michael@0: michael@0: protected BigInteger x; michael@0: protected BigInteger A; michael@0: protected byte[] Kbytes; michael@0: protected byte[] Mbytes; michael@0: } michael@0: michael@0: // State should be written exactly once. michael@0: protected AuthState internalAuthState = null; michael@0: michael@0: public FxAccount10AuthDelegate(String email, byte[] stretchedPWBytes) { michael@0: this.email = email; michael@0: this.stretchedPWBytes = stretchedPWBytes; michael@0: this.N = SRPConstants._2048.N; michael@0: this.g = SRPConstants._2048.g; michael@0: this.modNLengthBytes = SRPConstants._2048.byteLength; michael@0: } michael@0: michael@0: protected BigInteger generateSecretValue() { michael@0: return Utils.generateBigIntegerLessThan(N); michael@0: } michael@0: michael@0: public static class FxAccountClientMalformedAuthException extends FxAccountClientException { michael@0: private static final long serialVersionUID = 3585262174699395505L; michael@0: michael@0: public FxAccountClientMalformedAuthException(String detailMessage) { michael@0: super(detailMessage); michael@0: } michael@0: } michael@0: michael@0: @SuppressWarnings("unchecked") michael@0: @Override michael@0: public JSONObject getAuthStartBody() throws FxAccountClientException { michael@0: try { michael@0: final JSONObject body = new JSONObject(); michael@0: body.put("email", FxAccountUtils.bytes(email)); michael@0: return body; michael@0: } catch (UnsupportedEncodingException e) { michael@0: throw new FxAccountClientException(e); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public void onAuthStartResponse(final ExtendedJSONObject body) throws FxAccountClientException { michael@0: if (this.internalAuthState != null) { michael@0: throw new FxAccountClientException("auth must not be written before calling onAuthStartResponse"); michael@0: } michael@0: michael@0: String srpToken = null; michael@0: String srpSalt = null; michael@0: String srpB = null; michael@0: String mainSalt = null; michael@0: michael@0: try { michael@0: srpToken = body.getString("srpToken"); michael@0: if (srpToken == null) { michael@0: throw new FxAccountClientMalformedAuthException("srpToken must be a non-null object"); michael@0: } michael@0: ExtendedJSONObject srp = body.getObject("srp"); michael@0: if (srp == null) { michael@0: throw new FxAccountClientMalformedAuthException("srp must be a non-null object"); michael@0: } michael@0: srpSalt = srp.getString("salt"); michael@0: if (srpSalt == null) { michael@0: throw new FxAccountClientMalformedAuthException("srp.salt must not be null"); michael@0: } michael@0: srpB = srp.getString("B"); michael@0: if (srpB == null) { michael@0: throw new FxAccountClientMalformedAuthException("srp.B must not be null"); michael@0: } michael@0: ExtendedJSONObject passwordStretching = body.getObject("passwordStretching"); michael@0: if (passwordStretching == null) { michael@0: throw new FxAccountClientMalformedAuthException("passwordStretching must be a non-null object"); michael@0: } michael@0: mainSalt = passwordStretching.getString("salt"); michael@0: if (mainSalt == null) { michael@0: throw new FxAccountClientMalformedAuthException("srp.passwordStretching.salt must not be null"); michael@0: } michael@0: throwIfParametersAreBad(passwordStretching); michael@0: michael@0: this.internalAuthState = authStateFromParameters(srpToken, mainSalt, srpSalt, srpB, generateSecretValue()); michael@0: } catch (FxAccountClientException e) { michael@0: throw e; michael@0: } catch (Exception e) { michael@0: throw new FxAccountClientException(e); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Expect object like: michael@0: * "passwordStretching": { michael@0: * "type": "PBKDF2/scrypt/PBKDF2/v1", michael@0: * "PBKDF2_rounds_1": 20000, michael@0: * "scrypt_N": 65536, michael@0: * "scrypt_r": 8, michael@0: * "scrypt_p": 1, michael@0: * "PBKDF2_rounds_2": 20000, michael@0: * "salt": "996bc6b1aa63cd69856a2ec81cbf19d5c8a604713362df9ee15c2bf07128efab" michael@0: * } michael@0: * @param params to verify. michael@0: * @throws FxAccountClientMalformedAuthException michael@0: */ michael@0: protected void throwIfParametersAreBad(ExtendedJSONObject params) throws FxAccountClientMalformedAuthException { michael@0: if (params == null || michael@0: params.size() != 7 || michael@0: params.getString("salt") == null || michael@0: !("PBKDF2/scrypt/PBKDF2/v1".equals(params.getString("type"))) || michael@0: 20000 != params.getLong("PBKDF2_rounds_1") || michael@0: 65536 != params.getLong("scrypt_N") || michael@0: 8 != params.getLong("scrypt_r") || michael@0: 1 != params.getLong("scrypt_p") || michael@0: 20000 != params.getLong("PBKDF2_rounds_2")) { michael@0: throw new FxAccountClientMalformedAuthException("malformed passwordStretching parameters: '" + params.toJSONString() + "'."); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * All state is written in this method. michael@0: */ michael@0: protected AuthState authStateFromParameters(String srpToken, String mainSalt, String srpSalt, String srpB, BigInteger a) throws NoSuchAlgorithmException, UnsupportedEncodingException { michael@0: AuthState authState = new AuthState(); michael@0: authState.srpToken = srpToken; michael@0: authState.mainSalt = mainSalt; michael@0: authState.srpSalt = srpSalt; michael@0: michael@0: authState.x = FxAccountUtils.srpVerifierLowercaseX(email.getBytes("UTF-8"), this.stretchedPWBytes, Utils.hex2Byte(srpSalt, FxAccountUtils.SALT_LENGTH_BYTES)); michael@0: michael@0: authState.A = g.modPow(a, N); michael@0: String srpA = FxAccountUtils.hexModN(authState.A, N); michael@0: BigInteger B = new BigInteger(srpB, 16); michael@0: michael@0: byte[] srpABytes = Utils.hex2Byte(srpA, modNLengthBytes); michael@0: byte[] srpBBytes = Utils.hex2Byte(srpB, modNLengthBytes); michael@0: michael@0: // u = H(pad(A) | pad(B)) michael@0: byte[] uBytes = Utils.sha256(Utils.concatAll( michael@0: srpABytes, michael@0: srpBBytes)); michael@0: BigInteger u = new BigInteger(Utils.byte2Hex(uBytes, FxAccountUtils.HASH_LENGTH_HEX), 16); michael@0: michael@0: // S = (B - k*g^x)^(a u*x) % N michael@0: // k = H(pad(N) | pad(g)) michael@0: int byteLength = (N.bitLength() + 7) / 8; michael@0: byte[] kBytes = Utils.sha256(Utils.concatAll( michael@0: Utils.hex2Byte(N.toString(16), byteLength), michael@0: Utils.hex2Byte(g.toString(16), byteLength))); michael@0: BigInteger k = new BigInteger(Utils.byte2Hex(kBytes, FxAccountUtils.HASH_LENGTH_HEX), 16); michael@0: michael@0: BigInteger base = B.subtract(k.multiply(g.modPow(authState.x, N)).mod(N)).mod(N); michael@0: BigInteger pow = a.add(u.multiply(authState.x)); michael@0: BigInteger S = base.modPow(pow, N); michael@0: String srpS = FxAccountUtils.hexModN(S, N); michael@0: michael@0: byte[] sBytes = Utils.hex2Byte(srpS, modNLengthBytes); michael@0: michael@0: // M = H(pad(A) | pad(B) | pad(S)) michael@0: authState.Mbytes = Utils.sha256(Utils.concatAll( michael@0: srpABytes, michael@0: srpBBytes, michael@0: sBytes)); michael@0: michael@0: // K = H(pad(S)) michael@0: authState.Kbytes = Utils.sha256(sBytes); michael@0: michael@0: return authState; michael@0: } michael@0: michael@0: @SuppressWarnings("unchecked") michael@0: @Override michael@0: public JSONObject getAuthFinishBody() throws FxAccountClientException { michael@0: if (internalAuthState == null) { michael@0: throw new FxAccountClientException("auth must be successfully written before calling getAuthFinishBody."); michael@0: } michael@0: JSONObject body = new JSONObject(); michael@0: body.put("srpToken", internalAuthState.srpToken); michael@0: body.put("A", FxAccountUtils.hexModN(internalAuthState.A, N)); michael@0: body.put("M", Utils.byte2Hex(internalAuthState.Mbytes, FxAccountUtils.HASH_LENGTH_HEX)); michael@0: return body; michael@0: } michael@0: michael@0: @Override michael@0: public byte[] getSharedBytes() throws FxAccountClientException { michael@0: if (internalAuthState == null) { michael@0: throw new FxAccountClientException("auth must be successfully finished before calling getSharedBytes."); michael@0: } michael@0: return internalAuthState.Kbytes; michael@0: } michael@0: }