|
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/. */ |
|
4 |
|
5 package org.mozilla.gecko.background.fxa; |
|
6 |
|
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; |
|
14 |
|
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; |
|
21 |
|
22 public class FxAccountUtils { |
|
23 private static final String LOG_TAG = FxAccountUtils.class.getSimpleName(); |
|
24 |
|
25 public static final int SALT_LENGTH_BYTES = 32; |
|
26 public static final int SALT_LENGTH_HEX = 2 * SALT_LENGTH_BYTES; |
|
27 |
|
28 public static final int HASH_LENGTH_BYTES = 16; |
|
29 public static final int HASH_LENGTH_HEX = 2 * HASH_LENGTH_BYTES; |
|
30 |
|
31 public static final int CRYPTO_KEY_LENGTH_BYTES = 32; |
|
32 public static final int CRYPTO_KEY_LENGTH_HEX = 2 * CRYPTO_KEY_LENGTH_BYTES; |
|
33 |
|
34 public static final String KW_VERSION_STRING = "identity.mozilla.com/picl/v1/"; |
|
35 |
|
36 public static final int NUMBER_OF_QUICK_STRETCH_ROUNDS = 1000; |
|
37 |
|
38 public static String bytes(String string) throws UnsupportedEncodingException { |
|
39 return Utils.byte2Hex(string.getBytes("UTF-8")); |
|
40 } |
|
41 |
|
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 } |
|
47 |
|
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 } |
|
55 |
|
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 } |
|
65 |
|
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 } |
|
75 |
|
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 } |
|
87 |
|
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 } |
|
102 |
|
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 } |
|
124 |
|
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 } |
|
132 |
|
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 } |
|
140 |
|
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 } |
|
157 |
|
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 } |
|
176 |
|
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 } |