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.net.URI; michael@0: import java.net.URISyntaxException; michael@0: import java.security.GeneralSecurityException; michael@0: import java.security.InvalidKeyException; michael@0: import java.security.NoSuchAlgorithmException; michael@0: michael@0: import org.mozilla.gecko.background.common.log.Logger; michael@0: import org.mozilla.gecko.background.nativecode.NativeCrypto; michael@0: import org.mozilla.gecko.sync.Utils; michael@0: import org.mozilla.gecko.sync.crypto.HKDF; michael@0: import org.mozilla.gecko.sync.crypto.KeyBundle; michael@0: import org.mozilla.gecko.sync.crypto.PBKDF2; michael@0: michael@0: public class FxAccountUtils { michael@0: private static final String LOG_TAG = FxAccountUtils.class.getSimpleName(); michael@0: michael@0: public static final int SALT_LENGTH_BYTES = 32; michael@0: public static final int SALT_LENGTH_HEX = 2 * SALT_LENGTH_BYTES; michael@0: michael@0: public static final int HASH_LENGTH_BYTES = 16; michael@0: public static final int HASH_LENGTH_HEX = 2 * HASH_LENGTH_BYTES; michael@0: michael@0: public static final int CRYPTO_KEY_LENGTH_BYTES = 32; michael@0: public static final int CRYPTO_KEY_LENGTH_HEX = 2 * CRYPTO_KEY_LENGTH_BYTES; michael@0: michael@0: public static final String KW_VERSION_STRING = "identity.mozilla.com/picl/v1/"; michael@0: michael@0: public static final int NUMBER_OF_QUICK_STRETCH_ROUNDS = 1000; michael@0: michael@0: public static String bytes(String string) throws UnsupportedEncodingException { michael@0: return Utils.byte2Hex(string.getBytes("UTF-8")); michael@0: } michael@0: michael@0: public static byte[] KW(String name) throws UnsupportedEncodingException { michael@0: return Utils.concatAll( michael@0: KW_VERSION_STRING.getBytes("UTF-8"), michael@0: name.getBytes("UTF-8")); michael@0: } michael@0: michael@0: public static byte[] KWE(String name, byte[] emailUTF8) throws UnsupportedEncodingException { michael@0: return Utils.concatAll( michael@0: KW_VERSION_STRING.getBytes("UTF-8"), michael@0: name.getBytes("UTF-8"), michael@0: ":".getBytes("UTF-8"), michael@0: emailUTF8); michael@0: } michael@0: michael@0: /** michael@0: * Calculate the SRP verifier x value. michael@0: */ michael@0: public static BigInteger srpVerifierLowercaseX(byte[] emailUTF8, byte[] srpPWBytes, byte[] srpSaltBytes) michael@0: throws NoSuchAlgorithmException, UnsupportedEncodingException { michael@0: byte[] inner = Utils.sha256(Utils.concatAll(emailUTF8, ":".getBytes("UTF-8"), srpPWBytes)); michael@0: byte[] outer = Utils.sha256(Utils.concatAll(srpSaltBytes, inner)); michael@0: return new BigInteger(1, outer); michael@0: } michael@0: michael@0: /** michael@0: * Calculate the SRP verifier v value. michael@0: */ michael@0: public static BigInteger srpVerifierLowercaseV(byte[] emailUTF8, byte[] srpPWBytes, byte[] srpSaltBytes, BigInteger g, BigInteger N) michael@0: throws NoSuchAlgorithmException, UnsupportedEncodingException { michael@0: BigInteger x = srpVerifierLowercaseX(emailUTF8, srpPWBytes, srpSaltBytes); michael@0: BigInteger v = g.modPow(x, N); michael@0: return v; michael@0: } michael@0: michael@0: /** michael@0: * Format x modulo N in hexadecimal, using as many characters as N takes (in hexadecimal). michael@0: * @param x to format. michael@0: * @param N modulus. michael@0: * @return x modulo N in hexadecimal. michael@0: */ michael@0: public static String hexModN(BigInteger x, BigInteger N) { michael@0: int byteLength = (N.bitLength() + 7) / 8; michael@0: int hexLength = 2 * byteLength; michael@0: return Utils.byte2Hex(Utils.hex2Byte((x.mod(N)).toString(16), byteLength), hexLength); michael@0: } michael@0: michael@0: /** michael@0: * The first engineering milestone of PICL (Profile-in-the-Cloud) was michael@0: * comprised of Sync 1.1 fronted by a Firefox Account. The sync key was michael@0: * generated from the Firefox Account password-derived kB value using this michael@0: * method. michael@0: */ michael@0: public static KeyBundle generateSyncKeyBundle(final byte[] kB) throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException { michael@0: byte[] encryptionKey = new byte[32]; michael@0: byte[] hmacKey = new byte[32]; michael@0: byte[] derived = HKDF.derive(kB, new byte[0], FxAccountUtils.KW("oldsync"), 2*32); michael@0: System.arraycopy(derived, 0*32, encryptionKey, 0, 1*32); michael@0: System.arraycopy(derived, 1*32, hmacKey, 0, 1*32); michael@0: return new KeyBundle(encryptionKey, hmacKey); michael@0: } michael@0: michael@0: /** michael@0: * Firefox Accounts are password authenticated, but clients should not store michael@0: * the plain-text password for any amount of time. Equivalent, but slightly michael@0: * more secure, is the quickly client-side stretched password. michael@0: *
michael@0: * We separate this since multiple login-time operations want it, and the michael@0: * PBKDF2 operation is computationally expensive. michael@0: */ michael@0: public static byte[] generateQuickStretchedPW(byte[] emailUTF8, byte[] passwordUTF8) throws GeneralSecurityException, UnsupportedEncodingException { michael@0: byte[] S = FxAccountUtils.KWE("quickStretch", emailUTF8); michael@0: try { michael@0: return NativeCrypto.pbkdf2SHA256(passwordUTF8, S, NUMBER_OF_QUICK_STRETCH_ROUNDS, 32); michael@0: } catch (final LinkageError e) { michael@0: // This will throw UnsatisifiedLinkError (missing mozglue) the first time it is called, and michael@0: // ClassNotDefFoundError, for the uninitialized NativeCrypto class, each subsequent time this michael@0: // is called; LinkageError is their common ancestor. michael@0: Logger.warn(LOG_TAG, "Got throwable stretching password using native pbkdf2SHA256 " + michael@0: "implementation; ignoring and using Java implementation.", e); michael@0: return PBKDF2.pbkdf2SHA256(passwordUTF8, S, NUMBER_OF_QUICK_STRETCH_ROUNDS, 32); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * The password-derived credential used to authenticate to the Firefox Account michael@0: * auth server. michael@0: */ michael@0: public static byte[] generateAuthPW(byte[] quickStretchedPW) throws GeneralSecurityException, UnsupportedEncodingException { michael@0: return HKDF.derive(quickStretchedPW, new byte[0], FxAccountUtils.KW("authPW"), 32); michael@0: } michael@0: michael@0: /** michael@0: * The password-derived credential used to unwrap keys managed by the Firefox michael@0: * Account auth server. michael@0: */ michael@0: public static byte[] generateUnwrapBKey(byte[] quickStretchedPW) throws GeneralSecurityException, UnsupportedEncodingException { michael@0: return HKDF.derive(quickStretchedPW, new byte[0], FxAccountUtils.KW("unwrapBkey"), 32); michael@0: } michael@0: michael@0: public static byte[] unwrapkB(byte[] unwrapkB, byte[] wrapkB) { michael@0: if (unwrapkB == null) { michael@0: throw new IllegalArgumentException("unwrapkB must not be null"); michael@0: } michael@0: if (wrapkB == null) { michael@0: throw new IllegalArgumentException("wrapkB must not be null"); michael@0: } michael@0: if (unwrapkB.length != CRYPTO_KEY_LENGTH_BYTES || wrapkB.length != CRYPTO_KEY_LENGTH_BYTES) { michael@0: throw new IllegalArgumentException("unwrapkB and wrapkB must be " + CRYPTO_KEY_LENGTH_BYTES + " bytes long"); michael@0: } michael@0: byte[] kB = new byte[CRYPTO_KEY_LENGTH_BYTES]; michael@0: for (int i = 0; i < wrapkB.length; i++) { michael@0: kB[i] = (byte) (wrapkB[i] ^ unwrapkB[i]); michael@0: } michael@0: return kB; michael@0: } michael@0: michael@0: /** michael@0: * The token server accepts an X-Client-State header, which is the michael@0: * lowercase-hex-encoded first 16 bytes of the SHA-256 hash of the michael@0: * bytes of kB. michael@0: * @param kB a byte array, expected to be 32 bytes long. michael@0: * @return a 32-character string. michael@0: * @throws NoSuchAlgorithmException michael@0: */ michael@0: public static String computeClientState(byte[] kB) throws NoSuchAlgorithmException { michael@0: if (kB == null || michael@0: kB.length != 32) { michael@0: throw new IllegalArgumentException("Unexpected kB."); michael@0: } michael@0: byte[] sha256 = Utils.sha256(kB); michael@0: byte[] truncated = new byte[16]; michael@0: System.arraycopy(sha256, 0, truncated, 0, 16); michael@0: return Utils.byte2Hex(truncated); // This is automatically lowercase. michael@0: } michael@0: michael@0: /** michael@0: * Given an endpoint, calculate the corresponding BrowserID audience. michael@0: *
michael@0: * This is the domain, in web parlance. michael@0: * michael@0: * @param serverURI endpoint. michael@0: * @return BrowserID audience. michael@0: * @throws URISyntaxException michael@0: */ michael@0: public static String getAudienceForURL(String serverURI) throws URISyntaxException { michael@0: URI uri = new URI(serverURI); michael@0: return new URI(uri.getScheme(), uri.getHost(), null, null).toString(); michael@0: } michael@0: }