Wed, 31 Dec 2014 07:22:50 +0100
Correct previous dual key logic pending first delivery installment.
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 package org.mozilla.gecko.background.fxa;
7 import java.io.UnsupportedEncodingException;
8 import java.math.BigInteger;
9 import java.net.URI;
10 import java.net.URISyntaxException;
11 import java.security.GeneralSecurityException;
12 import java.security.InvalidKeyException;
13 import java.security.NoSuchAlgorithmException;
15 import org.mozilla.gecko.background.common.log.Logger;
16 import org.mozilla.gecko.background.nativecode.NativeCrypto;
17 import org.mozilla.gecko.sync.Utils;
18 import org.mozilla.gecko.sync.crypto.HKDF;
19 import org.mozilla.gecko.sync.crypto.KeyBundle;
20 import org.mozilla.gecko.sync.crypto.PBKDF2;
22 public class FxAccountUtils {
23 private static final String LOG_TAG = FxAccountUtils.class.getSimpleName();
25 public static final int SALT_LENGTH_BYTES = 32;
26 public static final int SALT_LENGTH_HEX = 2 * SALT_LENGTH_BYTES;
28 public static final int HASH_LENGTH_BYTES = 16;
29 public static final int HASH_LENGTH_HEX = 2 * HASH_LENGTH_BYTES;
31 public static final int CRYPTO_KEY_LENGTH_BYTES = 32;
32 public static final int CRYPTO_KEY_LENGTH_HEX = 2 * CRYPTO_KEY_LENGTH_BYTES;
34 public static final String KW_VERSION_STRING = "identity.mozilla.com/picl/v1/";
36 public static final int NUMBER_OF_QUICK_STRETCH_ROUNDS = 1000;
38 public static String bytes(String string) throws UnsupportedEncodingException {
39 return Utils.byte2Hex(string.getBytes("UTF-8"));
40 }
42 public static byte[] KW(String name) throws UnsupportedEncodingException {
43 return Utils.concatAll(
44 KW_VERSION_STRING.getBytes("UTF-8"),
45 name.getBytes("UTF-8"));
46 }
48 public static byte[] KWE(String name, byte[] emailUTF8) throws UnsupportedEncodingException {
49 return Utils.concatAll(
50 KW_VERSION_STRING.getBytes("UTF-8"),
51 name.getBytes("UTF-8"),
52 ":".getBytes("UTF-8"),
53 emailUTF8);
54 }
56 /**
57 * Calculate the SRP verifier <tt>x</tt> value.
58 */
59 public static BigInteger srpVerifierLowercaseX(byte[] emailUTF8, byte[] srpPWBytes, byte[] srpSaltBytes)
60 throws NoSuchAlgorithmException, UnsupportedEncodingException {
61 byte[] inner = Utils.sha256(Utils.concatAll(emailUTF8, ":".getBytes("UTF-8"), srpPWBytes));
62 byte[] outer = Utils.sha256(Utils.concatAll(srpSaltBytes, inner));
63 return new BigInteger(1, outer);
64 }
66 /**
67 * Calculate the SRP verifier <tt>v</tt> value.
68 */
69 public static BigInteger srpVerifierLowercaseV(byte[] emailUTF8, byte[] srpPWBytes, byte[] srpSaltBytes, BigInteger g, BigInteger N)
70 throws NoSuchAlgorithmException, UnsupportedEncodingException {
71 BigInteger x = srpVerifierLowercaseX(emailUTF8, srpPWBytes, srpSaltBytes);
72 BigInteger v = g.modPow(x, N);
73 return v;
74 }
76 /**
77 * Format x modulo N in hexadecimal, using as many characters as N takes (in hexadecimal).
78 * @param x to format.
79 * @param N modulus.
80 * @return x modulo N in hexadecimal.
81 */
82 public static String hexModN(BigInteger x, BigInteger N) {
83 int byteLength = (N.bitLength() + 7) / 8;
84 int hexLength = 2 * byteLength;
85 return Utils.byte2Hex(Utils.hex2Byte((x.mod(N)).toString(16), byteLength), hexLength);
86 }
88 /**
89 * The first engineering milestone of PICL (Profile-in-the-Cloud) was
90 * comprised of Sync 1.1 fronted by a Firefox Account. The sync key was
91 * generated from the Firefox Account password-derived kB value using this
92 * method.
93 */
94 public static KeyBundle generateSyncKeyBundle(final byte[] kB) throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException {
95 byte[] encryptionKey = new byte[32];
96 byte[] hmacKey = new byte[32];
97 byte[] derived = HKDF.derive(kB, new byte[0], FxAccountUtils.KW("oldsync"), 2*32);
98 System.arraycopy(derived, 0*32, encryptionKey, 0, 1*32);
99 System.arraycopy(derived, 1*32, hmacKey, 0, 1*32);
100 return new KeyBundle(encryptionKey, hmacKey);
101 }
103 /**
104 * Firefox Accounts are password authenticated, but clients should not store
105 * the plain-text password for any amount of time. Equivalent, but slightly
106 * more secure, is the quickly client-side stretched password.
107 * <p>
108 * We separate this since multiple login-time operations want it, and the
109 * PBKDF2 operation is computationally expensive.
110 */
111 public static byte[] generateQuickStretchedPW(byte[] emailUTF8, byte[] passwordUTF8) throws GeneralSecurityException, UnsupportedEncodingException {
112 byte[] S = FxAccountUtils.KWE("quickStretch", emailUTF8);
113 try {
114 return NativeCrypto.pbkdf2SHA256(passwordUTF8, S, NUMBER_OF_QUICK_STRETCH_ROUNDS, 32);
115 } catch (final LinkageError e) {
116 // This will throw UnsatisifiedLinkError (missing mozglue) the first time it is called, and
117 // ClassNotDefFoundError, for the uninitialized NativeCrypto class, each subsequent time this
118 // is called; LinkageError is their common ancestor.
119 Logger.warn(LOG_TAG, "Got throwable stretching password using native pbkdf2SHA256 " +
120 "implementation; ignoring and using Java implementation.", e);
121 return PBKDF2.pbkdf2SHA256(passwordUTF8, S, NUMBER_OF_QUICK_STRETCH_ROUNDS, 32);
122 }
123 }
125 /**
126 * The password-derived credential used to authenticate to the Firefox Account
127 * auth server.
128 */
129 public static byte[] generateAuthPW(byte[] quickStretchedPW) throws GeneralSecurityException, UnsupportedEncodingException {
130 return HKDF.derive(quickStretchedPW, new byte[0], FxAccountUtils.KW("authPW"), 32);
131 }
133 /**
134 * The password-derived credential used to unwrap keys managed by the Firefox
135 * Account auth server.
136 */
137 public static byte[] generateUnwrapBKey(byte[] quickStretchedPW) throws GeneralSecurityException, UnsupportedEncodingException {
138 return HKDF.derive(quickStretchedPW, new byte[0], FxAccountUtils.KW("unwrapBkey"), 32);
139 }
141 public static byte[] unwrapkB(byte[] unwrapkB, byte[] wrapkB) {
142 if (unwrapkB == null) {
143 throw new IllegalArgumentException("unwrapkB must not be null");
144 }
145 if (wrapkB == null) {
146 throw new IllegalArgumentException("wrapkB must not be null");
147 }
148 if (unwrapkB.length != CRYPTO_KEY_LENGTH_BYTES || wrapkB.length != CRYPTO_KEY_LENGTH_BYTES) {
149 throw new IllegalArgumentException("unwrapkB and wrapkB must be " + CRYPTO_KEY_LENGTH_BYTES + " bytes long");
150 }
151 byte[] kB = new byte[CRYPTO_KEY_LENGTH_BYTES];
152 for (int i = 0; i < wrapkB.length; i++) {
153 kB[i] = (byte) (wrapkB[i] ^ unwrapkB[i]);
154 }
155 return kB;
156 }
158 /**
159 * The token server accepts an X-Client-State header, which is the
160 * lowercase-hex-encoded first 16 bytes of the SHA-256 hash of the
161 * bytes of kB.
162 * @param kB a byte array, expected to be 32 bytes long.
163 * @return a 32-character string.
164 * @throws NoSuchAlgorithmException
165 */
166 public static String computeClientState(byte[] kB) throws NoSuchAlgorithmException {
167 if (kB == null ||
168 kB.length != 32) {
169 throw new IllegalArgumentException("Unexpected kB.");
170 }
171 byte[] sha256 = Utils.sha256(kB);
172 byte[] truncated = new byte[16];
173 System.arraycopy(sha256, 0, truncated, 0, 16);
174 return Utils.byte2Hex(truncated); // This is automatically lowercase.
175 }
177 /**
178 * Given an endpoint, calculate the corresponding BrowserID audience.
179 * <p>
180 * This is the domain, in web parlance.
181 *
182 * @param serverURI endpoint.
183 * @return BrowserID audience.
184 * @throws URISyntaxException
185 */
186 public static String getAudienceForURL(String serverURI) throws URISyntaxException {
187 URI uri = new URI(serverURI);
188 return new URI(uri.getScheme(), uri.getHost(), null, null).toString();
189 }
190 }