/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.mozilla.goanna.background.fxa; import java.io.UnsupportedEncodingException; import java.math.BigInteger; import java.net.URI; import java.net.URISyntaxException; import java.security.GeneralSecurityException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import org.mozilla.goanna.AppConstants; import org.mozilla.goanna.R; import org.mozilla.goanna.background.common.log.Logger; import org.mozilla.goanna.background.nativecode.NativeCrypto; import org.mozilla.goanna.sync.Utils; import org.mozilla.goanna.sync.crypto.HKDF; import org.mozilla.goanna.sync.crypto.KeyBundle; import org.mozilla.goanna.sync.crypto.PBKDF2; import android.content.Context; public class FxAccountUtils { private static final String LOG_TAG = FxAccountUtils.class.getSimpleName(); public static final int SALT_LENGTH_BYTES = 32; public static final int SALT_LENGTH_HEX = 2 * SALT_LENGTH_BYTES; public static final int HASH_LENGTH_BYTES = 16; public static final int HASH_LENGTH_HEX = 2 * HASH_LENGTH_BYTES; public static final int CRYPTO_KEY_LENGTH_BYTES = 32; public static final int CRYPTO_KEY_LENGTH_HEX = 2 * CRYPTO_KEY_LENGTH_BYTES; public static final String KW_VERSION_STRING = "identity.mozilla.com/picl/v1/"; public static final int NUMBER_OF_QUICK_STRETCH_ROUNDS = 1000; // For extra debugging. Not final so it can be changed from Fennec, or from // an add-on. public static boolean LOG_PERSONAL_INFORMATION = false; public static void pii(String tag, String message) { if (FxAccountUtils.LOG_PERSONAL_INFORMATION) { Logger.info(tag, "$$FxA PII$$: " + message); } } public static String bytes(String string) throws UnsupportedEncodingException { return Utils.byte2Hex(string.getBytes("UTF-8")); } public static byte[] KW(String name) throws UnsupportedEncodingException { return Utils.concatAll( KW_VERSION_STRING.getBytes("UTF-8"), name.getBytes("UTF-8")); } public static byte[] KWE(String name, byte[] emailUTF8) throws UnsupportedEncodingException { return Utils.concatAll( KW_VERSION_STRING.getBytes("UTF-8"), name.getBytes("UTF-8"), ":".getBytes("UTF-8"), emailUTF8); } /** * Calculate the SRP verifier x value. */ public static BigInteger srpVerifierLowercaseX(byte[] emailUTF8, byte[] srpPWBytes, byte[] srpSaltBytes) throws NoSuchAlgorithmException, UnsupportedEncodingException { byte[] inner = Utils.sha256(Utils.concatAll(emailUTF8, ":".getBytes("UTF-8"), srpPWBytes)); byte[] outer = Utils.sha256(Utils.concatAll(srpSaltBytes, inner)); return new BigInteger(1, outer); } /** * Calculate the SRP verifier v value. */ public static BigInteger srpVerifierLowercaseV(byte[] emailUTF8, byte[] srpPWBytes, byte[] srpSaltBytes, BigInteger g, BigInteger N) throws NoSuchAlgorithmException, UnsupportedEncodingException { BigInteger x = srpVerifierLowercaseX(emailUTF8, srpPWBytes, srpSaltBytes); BigInteger v = g.modPow(x, N); return v; } /** * Format x modulo N in hexadecimal, using as many characters as N takes (in hexadecimal). * @param x to format. * @param N modulus. * @return x modulo N in hexadecimal. */ public static String hexModN(BigInteger x, BigInteger N) { int byteLength = (N.bitLength() + 7) / 8; int hexLength = 2 * byteLength; return Utils.byte2Hex(Utils.hex2Byte((x.mod(N)).toString(16), byteLength), hexLength); } /** * The first engineering milestone of PICL (Profile-in-the-Cloud) was * comprised of Sync 1.1 fronted by a Firefox Account. The sync key was * generated from the Firefox Account password-derived kB value using this * method. */ public static KeyBundle generateSyncKeyBundle(final byte[] kB) throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException { byte[] encryptionKey = new byte[32]; byte[] hmacKey = new byte[32]; byte[] derived = HKDF.derive(kB, new byte[0], FxAccountUtils.KW("oldsync"), 2*32); System.arraycopy(derived, 0*32, encryptionKey, 0, 1*32); System.arraycopy(derived, 1*32, hmacKey, 0, 1*32); return new KeyBundle(encryptionKey, hmacKey); } /** * Firefox Accounts are password authenticated, but clients should not store * the plain-text password for any amount of time. Equivalent, but slightly * more secure, is the quickly client-side stretched password. *

* We separate this since multiple login-time operations want it, and the * PBKDF2 operation is computationally expensive. */ public static byte[] generateQuickStretchedPW(byte[] emailUTF8, byte[] passwordUTF8) throws GeneralSecurityException, UnsupportedEncodingException { byte[] S = FxAccountUtils.KWE("quickStretch", emailUTF8); try { return NativeCrypto.pbkdf2SHA256(passwordUTF8, S, NUMBER_OF_QUICK_STRETCH_ROUNDS, 32); } catch (final LinkageError e) { // This will throw UnsatisfiedLinkError (missing mozglue) the first time it is called, and // ClassNotDefFoundError, for the uninitialized NativeCrypto class, each subsequent time this // is called; LinkageError is their common ancestor. Logger.warn(LOG_TAG, "Got throwable stretching password using native pbkdf2SHA256 " + "implementation; ignoring and using Java implementation.", e); return PBKDF2.pbkdf2SHA256(passwordUTF8, S, NUMBER_OF_QUICK_STRETCH_ROUNDS, 32); } } /** * The password-derived credential used to authenticate to the Firefox Account * auth server. */ public static byte[] generateAuthPW(byte[] quickStretchedPW) throws GeneralSecurityException, UnsupportedEncodingException { return HKDF.derive(quickStretchedPW, new byte[0], FxAccountUtils.KW("authPW"), 32); } /** * The password-derived credential used to unwrap keys managed by the Firefox * Account auth server. */ public static byte[] generateUnwrapBKey(byte[] quickStretchedPW) throws GeneralSecurityException, UnsupportedEncodingException { return HKDF.derive(quickStretchedPW, new byte[0], FxAccountUtils.KW("unwrapBkey"), 32); } public static byte[] unwrapkB(byte[] unwrapkB, byte[] wrapkB) { if (unwrapkB == null) { throw new IllegalArgumentException("unwrapkB must not be null"); } if (wrapkB == null) { throw new IllegalArgumentException("wrapkB must not be null"); } if (unwrapkB.length != CRYPTO_KEY_LENGTH_BYTES || wrapkB.length != CRYPTO_KEY_LENGTH_BYTES) { throw new IllegalArgumentException("unwrapkB and wrapkB must be " + CRYPTO_KEY_LENGTH_BYTES + " bytes long"); } byte[] kB = new byte[CRYPTO_KEY_LENGTH_BYTES]; for (int i = 0; i < wrapkB.length; i++) { kB[i] = (byte) (wrapkB[i] ^ unwrapkB[i]); } return kB; } /** * The token server accepts an X-Client-State header, which is the * lowercase-hex-encoded first 16 bytes of the SHA-256 hash of the * bytes of kB. * @param kB a byte array, expected to be 32 bytes long. * @return a 32-character string. * @throws NoSuchAlgorithmException */ public static String computeClientState(byte[] kB) throws NoSuchAlgorithmException { if (kB == null || kB.length != 32) { throw new IllegalArgumentException("Unexpected kB."); } byte[] sha256 = Utils.sha256(kB); byte[] truncated = new byte[16]; System.arraycopy(sha256, 0, truncated, 0, 16); return Utils.byte2Hex(truncated); // This is automatically lowercase. } /** * Given an endpoint, calculate the corresponding BrowserID audience. *

* This is the domain, in web parlance. * * @param serverURI endpoint. * @return BrowserID audience. * @throws URISyntaxException */ public static String getAudienceForURL(String serverURI) throws URISyntaxException { URI uri = new URI(serverURI); return new URI(uri.getScheme(), null, uri.getHost(), uri.getPort(), null, null, null).toString(); } public static String defaultClientName(Context context) { String name = AppConstants.MOZ_APP_DISPLAYNAME; // The display name is never translated. // Change "Firefox Aurora" or similar into "Aurora". if (name.contains("Aurora")) { name = "Aurora"; } else if (name.contains("Beta")) { name = "Beta"; } else if (name.contains("Nightly")) { name = "Nightly"; } return context.getResources().getString(R.string.sync_default_client_name, name, android.os.Build.MODEL); } }