1/*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.server.locksettings.recoverablekeystore;
18
19import static junit.framework.Assert.fail;
20
21import static org.junit.Assert.assertArrayEquals;
22import static org.junit.Assert.assertEquals;
23import static org.junit.Assert.assertFalse;
24
25import android.support.test.filters.SmallTest;
26import android.support.test.runner.AndroidJUnit4;
27
28import com.google.common.collect.ImmutableMap;
29
30import org.junit.Test;
31import org.junit.runner.RunWith;
32
33import java.nio.ByteBuffer;
34import java.nio.ByteOrder;
35import java.nio.charset.StandardCharsets;
36import java.security.KeyPair;
37import java.security.MessageDigest;
38import java.security.PublicKey;
39import java.util.Arrays;
40import java.util.Map;
41import java.util.Random;
42
43import javax.crypto.AEADBadTagException;
44import javax.crypto.KeyGenerator;
45import javax.crypto.SecretKey;
46
47@SmallTest
48@RunWith(AndroidJUnit4.class)
49public class KeySyncUtilsTest {
50    private static final int RECOVERY_KEY_LENGTH_BITS = 256;
51    private static final int THM_KF_HASH_SIZE = 256;
52    private static final int KEY_CLAIMANT_LENGTH_BYTES = 16;
53    private static final byte[] TEST_VAULT_HANDLE =
54            new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17};
55    private static final int VAULT_PARAMS_LENGTH_BYTES = 94;
56    private static final int VAULT_HANDLE_LENGTH_BYTES = 17;
57    private static final String SHA_256_ALGORITHM = "SHA-256";
58    private static final String APPLICATION_KEY_ALGORITHM = "AES";
59    private static final byte[] LOCK_SCREEN_HASH_1 =
60            utf8Bytes("g09TEvo6XqVdNaYdRggzn5w2C5rCeE1F");
61    private static final byte[] LOCK_SCREEN_HASH_2 =
62            utf8Bytes("snQzsbvclkSsG6PwasAp1oFLzbq3KtFe");
63    private static final byte[] RECOVERY_CLAIM_HEADER =
64            "V1 KF_claim".getBytes(StandardCharsets.UTF_8);
65    private static final byte[] RECOVERY_RESPONSE_HEADER =
66            "V1 reencrypted_recovery_key".getBytes(StandardCharsets.UTF_8);
67    private static final int PUBLIC_KEY_LENGTH_BYTES = 65;
68
69
70    @Test
71    public void calculateThmKfHash_isShaOfLockScreenHashWithPrefix() throws Exception {
72        byte[] lockScreenHash = utf8Bytes("012345678910");
73
74        byte[] thmKfHash = KeySyncUtils.calculateThmKfHash(lockScreenHash);
75
76        assertArrayEquals(calculateSha256(utf8Bytes("THM_KF_hash012345678910")), thmKfHash);
77    }
78
79    @Test
80    public void calculateThmKfHash_is256BitsLong() throws Exception {
81        byte[] thmKfHash = KeySyncUtils.calculateThmKfHash(utf8Bytes("1234"));
82
83        assertEquals(THM_KF_HASH_SIZE / Byte.SIZE, thmKfHash.length);
84    }
85
86    @Test
87    public void generateRecoveryKey_returnsA256BitKey() throws Exception {
88        SecretKey key = KeySyncUtils.generateRecoveryKey();
89
90        assertEquals(RECOVERY_KEY_LENGTH_BITS / Byte.SIZE, key.getEncoded().length);
91    }
92
93    @Test
94    public void generateRecoveryKey_generatesANewKeyEachTime() throws Exception {
95        SecretKey a = KeySyncUtils.generateRecoveryKey();
96        SecretKey b = KeySyncUtils.generateRecoveryKey();
97
98        assertFalse(Arrays.equals(a.getEncoded(), b.getEncoded()));
99    }
100
101    @Test
102    public void generateKeyClaimant_returns16Bytes() throws Exception {
103        byte[] keyClaimant = KeySyncUtils.generateKeyClaimant();
104
105        assertEquals(KEY_CLAIMANT_LENGTH_BYTES, keyClaimant.length);
106    }
107
108    @Test
109    public void generateKeyClaimant_generatesANewClaimantEachTime() {
110        byte[] a = KeySyncUtils.generateKeyClaimant();
111        byte[] b = KeySyncUtils.generateKeyClaimant();
112
113        assertFalse(Arrays.equals(a, b));
114    }
115
116    @Test
117    public void concat_concatenatesArrays() {
118        assertArrayEquals(
119                utf8Bytes("hello, world!"),
120                KeySyncUtils.concat(
121                        utf8Bytes("hello"),
122                        utf8Bytes(", "),
123                        utf8Bytes("world"),
124                        utf8Bytes("!")));
125    }
126
127    @Test
128    public void decryptApplicationKey_decryptsAnApplicationKeyEncryptedWithSecureBox()
129            throws Exception {
130        String alias = "phoebe";
131        SecretKey recoveryKey = KeySyncUtils.generateRecoveryKey();
132        SecretKey applicationKey = generateApplicationKey();
133        Map<String, byte[]> encryptedKeys =
134                KeySyncUtils.encryptKeysWithRecoveryKey(
135                        recoveryKey, ImmutableMap.of(alias, applicationKey));
136        byte[] encryptedKey = encryptedKeys.get(alias);
137
138        byte[] keyMaterial =
139                KeySyncUtils.decryptApplicationKey(recoveryKey.getEncoded(), encryptedKey);
140
141        assertArrayEquals(applicationKey.getEncoded(), keyMaterial);
142    }
143
144    @Test
145    public void decryptApplicationKey_throwsIfUnableToDecrypt() throws Exception {
146        String alias = "casper";
147        Map<String, byte[]> encryptedKeys =
148                KeySyncUtils.encryptKeysWithRecoveryKey(
149                        KeySyncUtils.generateRecoveryKey(),
150                        ImmutableMap.of("casper", generateApplicationKey()));
151        byte[] encryptedKey = encryptedKeys.get(alias);
152
153        try {
154            KeySyncUtils.decryptApplicationKey(
155                    KeySyncUtils.generateRecoveryKey().getEncoded(), encryptedKey);
156            fail("Did not throw decrypting with bad key.");
157        } catch (AEADBadTagException error) {
158            // expected
159        }
160    }
161
162    @Test
163    public void decryptRecoveryKey_decryptsALocallyEncryptedKey() throws Exception {
164        SecretKey recoveryKey = KeySyncUtils.generateRecoveryKey();
165        byte[] encrypted = KeySyncUtils.locallyEncryptRecoveryKey(
166                LOCK_SCREEN_HASH_1, recoveryKey);
167
168        byte[] keyMaterial = KeySyncUtils.decryptRecoveryKey(LOCK_SCREEN_HASH_1, encrypted);
169
170        assertArrayEquals(recoveryKey.getEncoded(), keyMaterial);
171    }
172
173    @Test
174    public void decryptRecoveryKey_throwsIfCannotDecrypt() throws Exception {
175        SecretKey recoveryKey = KeySyncUtils.generateRecoveryKey();
176        byte[] encrypted = KeySyncUtils.locallyEncryptRecoveryKey(LOCK_SCREEN_HASH_1, recoveryKey);
177
178        try {
179            KeySyncUtils.decryptRecoveryKey(LOCK_SCREEN_HASH_2, encrypted);
180            fail("Did not throw decrypting with bad key.");
181        } catch (AEADBadTagException error) {
182            // expected
183        }
184    }
185
186    @Test
187    public void decryptRecoveryClaimResponse_decryptsAValidResponse() throws Exception {
188        byte[] keyClaimant = KeySyncUtils.generateKeyClaimant();
189        byte[] vaultParams = randomBytes(100);
190        byte[] recoveryKey = randomBytes(32);
191        byte[] encryptedPayload = SecureBox.encrypt(
192                /*theirPublicKey=*/ null,
193                /*sharedSecret=*/ keyClaimant,
194                /*header=*/ KeySyncUtils.concat(RECOVERY_RESPONSE_HEADER, vaultParams),
195                /*payload=*/ recoveryKey);
196
197        byte[] decrypted = KeySyncUtils.decryptRecoveryClaimResponse(
198                keyClaimant, vaultParams, encryptedPayload);
199
200        assertArrayEquals(recoveryKey, decrypted);
201    }
202
203    @Test
204    public void decryptRecoveryClaimResponse_throwsIfCannotDecrypt() throws Exception {
205        byte[] vaultParams = randomBytes(100);
206        byte[] recoveryKey = randomBytes(32);
207        byte[] encryptedPayload = SecureBox.encrypt(
208                /*theirPublicKey=*/ null,
209                /*sharedSecret=*/ KeySyncUtils.generateKeyClaimant(),
210                /*header=*/ KeySyncUtils.concat(RECOVERY_RESPONSE_HEADER, vaultParams),
211                /*payload=*/ recoveryKey);
212
213        try {
214            KeySyncUtils.decryptRecoveryClaimResponse(
215                    KeySyncUtils.generateKeyClaimant(), vaultParams, encryptedPayload);
216            fail("Did not throw decrypting with bad keyClaimant");
217        } catch (AEADBadTagException error) {
218            // expected
219        }
220    }
221
222    @Test
223    public void encryptRecoveryClaim_encryptsLockScreenAndKeyClaimant() throws Exception {
224        KeyPair keyPair = SecureBox.genKeyPair();
225        byte[] keyClaimant = KeySyncUtils.generateKeyClaimant();
226        byte[] challenge = randomBytes(32);
227        byte[] vaultParams = randomBytes(100);
228
229        byte[] encryptedRecoveryClaim = KeySyncUtils.encryptRecoveryClaim(
230                keyPair.getPublic(),
231                vaultParams,
232                challenge,
233                LOCK_SCREEN_HASH_1,
234                keyClaimant);
235
236        byte[] decrypted = SecureBox.decrypt(
237                keyPair.getPrivate(),
238                /*sharedSecret=*/ null,
239                /*header=*/ KeySyncUtils.concat(RECOVERY_CLAIM_HEADER, vaultParams, challenge),
240                encryptedRecoveryClaim);
241        assertArrayEquals(KeySyncUtils.concat(LOCK_SCREEN_HASH_1, keyClaimant), decrypted);
242    }
243
244    @Test
245    public void encryptRecoveryClaim_cannotBeDecryptedWithoutChallenge() throws Exception {
246        KeyPair keyPair = SecureBox.genKeyPair();
247        byte[] keyClaimant = KeySyncUtils.generateKeyClaimant();
248        byte[] vaultParams = randomBytes(100);
249
250        byte[] encryptedRecoveryClaim = KeySyncUtils.encryptRecoveryClaim(
251                keyPair.getPublic(),
252                vaultParams,
253                /*challenge=*/ randomBytes(32),
254                LOCK_SCREEN_HASH_1,
255                keyClaimant);
256
257        try {
258            SecureBox.decrypt(
259                    keyPair.getPrivate(),
260                    /*sharedSecret=*/ null,
261                    /*header=*/ KeySyncUtils.concat(
262                            RECOVERY_CLAIM_HEADER, vaultParams, randomBytes(32)),
263                    encryptedRecoveryClaim);
264            fail("Should throw if challenge is incorrect.");
265        } catch (AEADBadTagException e) {
266            // expected
267        }
268    }
269
270    @Test
271    public void encryptRecoveryClaim_cannotBeDecryptedWithoutCorrectSecretKey() throws Exception {
272        byte[] keyClaimant = KeySyncUtils.generateKeyClaimant();
273        byte[] challenge = randomBytes(32);
274        byte[] vaultParams = randomBytes(100);
275
276        byte[] encryptedRecoveryClaim = KeySyncUtils.encryptRecoveryClaim(
277                SecureBox.genKeyPair().getPublic(),
278                vaultParams,
279                challenge,
280                LOCK_SCREEN_HASH_1,
281                keyClaimant);
282
283        try {
284            SecureBox.decrypt(
285                    SecureBox.genKeyPair().getPrivate(),
286                    /*sharedSecret=*/ null,
287                    /*header=*/ KeySyncUtils.concat(
288                            RECOVERY_CLAIM_HEADER, vaultParams, challenge),
289                    encryptedRecoveryClaim);
290            fail("Should throw if secret key is incorrect.");
291        } catch (AEADBadTagException e) {
292            // expected
293        }
294    }
295
296    @Test
297    public void encryptRecoveryClaim_cannotBeDecryptedWithoutCorrectVaultParams() throws Exception {
298        KeyPair keyPair = SecureBox.genKeyPair();
299        byte[] keyClaimant = KeySyncUtils.generateKeyClaimant();
300        byte[] challenge = randomBytes(32);
301
302        byte[] encryptedRecoveryClaim = KeySyncUtils.encryptRecoveryClaim(
303                keyPair.getPublic(),
304                /*vaultParams=*/ randomBytes(100),
305                challenge,
306                LOCK_SCREEN_HASH_1,
307                keyClaimant);
308
309        try {
310            SecureBox.decrypt(
311                    keyPair.getPrivate(),
312                    /*sharedSecret=*/ null,
313                    /*header=*/ KeySyncUtils.concat(
314                            RECOVERY_CLAIM_HEADER, randomBytes(100), challenge),
315                    encryptedRecoveryClaim);
316            fail("Should throw if vault params is incorrect.");
317        } catch (AEADBadTagException e) {
318            // expected
319        }
320    }
321
322    @Test
323    public void encryptRecoveryClaim_cannotBeDecryptedWithoutCorrectHeader() throws Exception {
324        KeyPair keyPair = SecureBox.genKeyPair();
325        byte[] keyClaimant = KeySyncUtils.generateKeyClaimant();
326        byte[] challenge = randomBytes(32);
327        byte[] vaultParams = randomBytes(100);
328
329        byte[] encryptedRecoveryClaim = KeySyncUtils.encryptRecoveryClaim(
330                keyPair.getPublic(),
331                vaultParams,
332                challenge,
333                LOCK_SCREEN_HASH_1,
334                keyClaimant);
335
336        try {
337            SecureBox.decrypt(
338                    keyPair.getPrivate(),
339                    /*sharedSecret=*/ null,
340                    /*header=*/ KeySyncUtils.concat(randomBytes(10), vaultParams, challenge),
341                    encryptedRecoveryClaim);
342            fail("Should throw if header is incorrect.");
343        } catch (AEADBadTagException e) {
344            // expected
345        }
346    }
347
348    @Test
349    public void packVaultParams_returnsCorrectSize() throws Exception {
350        PublicKey thmPublicKey = SecureBox.genKeyPair().getPublic();
351
352        byte[] packedForm = KeySyncUtils.packVaultParams(
353                thmPublicKey,
354                /*counterId=*/ 1001L,
355                /*maxAttempts=*/ 10,
356                TEST_VAULT_HANDLE);
357
358        assertEquals(VAULT_PARAMS_LENGTH_BYTES, packedForm.length);
359    }
360
361    @Test
362    public void packVaultParams_encodesPublicKeyInFirst65Bytes() throws Exception {
363        PublicKey thmPublicKey = SecureBox.genKeyPair().getPublic();
364
365        byte[] packedForm = KeySyncUtils.packVaultParams(
366                thmPublicKey,
367                /*counterId=*/ 1001L,
368                /*maxAttempts=*/ 10,
369                TEST_VAULT_HANDLE);
370
371        assertArrayEquals(
372                SecureBox.encodePublicKey(thmPublicKey),
373                Arrays.copyOf(packedForm, PUBLIC_KEY_LENGTH_BYTES));
374    }
375
376    @Test
377    public void packVaultParams_encodesCounterIdAsSecondParam() throws Exception {
378        long counterId = 103502L;
379
380        byte[] packedForm = KeySyncUtils.packVaultParams(
381                SecureBox.genKeyPair().getPublic(),
382                counterId,
383                /*maxAttempts=*/ 10,
384                TEST_VAULT_HANDLE);
385
386        ByteBuffer byteBuffer = ByteBuffer.wrap(packedForm)
387                .order(ByteOrder.LITTLE_ENDIAN);
388        byteBuffer.position(PUBLIC_KEY_LENGTH_BYTES);
389        assertEquals(counterId, byteBuffer.getLong());
390    }
391
392    @Test
393    public void packVaultParams_encodesMaxAttemptsAsThirdParam() throws Exception {
394        int maxAttempts = 10;
395
396        byte[] packedForm = KeySyncUtils.packVaultParams(
397                SecureBox.genKeyPair().getPublic(),
398                /*counterId=*/ 1001L,
399                maxAttempts,
400                TEST_VAULT_HANDLE);
401
402        ByteBuffer byteBuffer = ByteBuffer.wrap(packedForm)
403                .order(ByteOrder.LITTLE_ENDIAN);
404        byteBuffer.position(PUBLIC_KEY_LENGTH_BYTES + Long.BYTES);
405        assertEquals(maxAttempts, byteBuffer.getInt());
406    }
407
408    @Test
409    public void packVaultParams_encodesVaultHandleAsLastParam() throws Exception {
410        byte[] packedForm = KeySyncUtils.packVaultParams(
411                SecureBox.genKeyPair().getPublic(),
412                /*counterId=*/ 10021L,
413                /*maxAttempts=*/ 10,
414                TEST_VAULT_HANDLE);
415
416        ByteBuffer byteBuffer = ByteBuffer.wrap(packedForm)
417                .order(ByteOrder.LITTLE_ENDIAN);
418        byteBuffer.position(PUBLIC_KEY_LENGTH_BYTES + Long.BYTES + Integer.BYTES);
419        byte[] vaultHandle = new byte[VAULT_HANDLE_LENGTH_BYTES];
420        byteBuffer.get(vaultHandle);
421        assertArrayEquals(TEST_VAULT_HANDLE, vaultHandle);
422    }
423
424    @Test
425    public void packVaultParams_encodesVaultHandleWithLength8AsLastParam() throws Exception {
426        byte[] vaultHandleWithLenght8 = new byte[] {1, 2, 3, 4, 1, 2, 3, 4};
427        byte[] packedForm = KeySyncUtils.packVaultParams(
428                SecureBox.genKeyPair().getPublic(),
429                /*counterId=*/ 10021L,
430                /*maxAttempts=*/ 10,
431                vaultHandleWithLenght8);
432
433        ByteBuffer byteBuffer = ByteBuffer.wrap(packedForm)
434                .order(ByteOrder.LITTLE_ENDIAN);
435        assertEquals(PUBLIC_KEY_LENGTH_BYTES + Long.BYTES + Integer.BYTES + 8, packedForm.length);
436        byteBuffer.position(PUBLIC_KEY_LENGTH_BYTES + Long.BYTES + Integer.BYTES);
437        byte[] vaultHandle = new byte[8];
438        byteBuffer.get(vaultHandle);
439        assertArrayEquals(vaultHandleWithLenght8, vaultHandle);
440    }
441
442    private static byte[] randomBytes(int n) {
443        byte[] bytes = new byte[n];
444        new Random().nextBytes(bytes);
445        return bytes;
446    }
447
448    private static byte[] utf8Bytes(String s) {
449        return s.getBytes(StandardCharsets.UTF_8);
450    }
451
452    private static byte[] calculateSha256(byte[] bytes) throws Exception {
453        MessageDigest messageDigest = MessageDigest.getInstance(SHA_256_ALGORITHM);
454        messageDigest.update(bytes);
455        return messageDigest.digest();
456    }
457
458    private static SecretKey generateApplicationKey() throws Exception {
459        KeyGenerator keyGenerator = KeyGenerator.getInstance(APPLICATION_KEY_ALGORITHM);
460        keyGenerator.init(/*keySize=*/ 256);
461        return keyGenerator.generateKey();
462    }
463}
464