Robert Berry | e16fa98 | 2017-12-20 15:59:37 +0000 | [diff] [blame] | 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 | |
| 17 | package com.android.server.locksettings.recoverablekeystore; |
| 18 | |
| 19 | import static android.security.recoverablekeystore.KeyStoreRecoveryMetadata.TYPE_LOCKSCREEN; |
| 20 | import static android.security.recoverablekeystore.KeyStoreRecoveryMetadata.TYPE_PASSWORD; |
| 21 | |
Dmitry Dementyev | ad88471 | 2017-12-20 12:38:36 -0800 | [diff] [blame] | 22 | import static com.google.common.truth.Truth.assertThat; |
Robert Berry | e16fa98 | 2017-12-20 15:59:37 +0000 | [diff] [blame] | 23 | import static org.junit.Assert.assertArrayEquals; |
| 24 | import static org.junit.Assert.assertEquals; |
Robert Berry | b9a220b | 2017-12-21 12:41:01 +0000 | [diff] [blame] | 25 | import static org.junit.Assert.fail; |
Robert Berry | e16fa98 | 2017-12-20 15:59:37 +0000 | [diff] [blame] | 26 | import static org.mockito.ArgumentMatchers.any; |
Robert Berry | cfc990a | 2017-12-22 15:54:30 +0000 | [diff] [blame^] | 27 | import static org.mockito.ArgumentMatchers.anyInt; |
| 28 | import static org.mockito.ArgumentMatchers.anyString; |
Robert Berry | e16fa98 | 2017-12-20 15:59:37 +0000 | [diff] [blame] | 29 | import static org.mockito.ArgumentMatchers.eq; |
| 30 | import static org.mockito.Mockito.times; |
| 31 | import static org.mockito.Mockito.verify; |
Robert Berry | cfc990a | 2017-12-22 15:54:30 +0000 | [diff] [blame^] | 32 | import static org.mockito.Mockito.when; |
Robert Berry | e16fa98 | 2017-12-20 15:59:37 +0000 | [diff] [blame] | 33 | |
Robert Berry | cfc990a | 2017-12-22 15:54:30 +0000 | [diff] [blame^] | 34 | import android.app.KeyguardManager; |
Dmitry Dementyev | 3b17c63 | 2017-12-21 17:30:48 -0800 | [diff] [blame] | 35 | import android.app.PendingIntent; |
Robert Berry | e16fa98 | 2017-12-20 15:59:37 +0000 | [diff] [blame] | 36 | import android.content.Context; |
Dmitry Dementyev | 3b17c63 | 2017-12-21 17:30:48 -0800 | [diff] [blame] | 37 | import android.content.Intent; |
| 38 | import android.os.Binder; |
Robert Berry | e16fa98 | 2017-12-20 15:59:37 +0000 | [diff] [blame] | 39 | import android.os.RemoteException; |
Dmitry Dementyev | ad88471 | 2017-12-20 12:38:36 -0800 | [diff] [blame] | 40 | import android.os.UserHandle; |
Robert Berry | e16fa98 | 2017-12-20 15:59:37 +0000 | [diff] [blame] | 41 | import android.security.recoverablekeystore.KeyDerivationParameters; |
Robert Berry | b9a220b | 2017-12-21 12:41:01 +0000 | [diff] [blame] | 42 | import android.security.recoverablekeystore.KeyEntryRecoveryData; |
Robert Berry | e16fa98 | 2017-12-20 15:59:37 +0000 | [diff] [blame] | 43 | import android.security.recoverablekeystore.KeyStoreRecoveryMetadata; |
| 44 | import android.security.recoverablekeystore.RecoverableKeyStoreLoader; |
| 45 | import android.support.test.InstrumentationRegistry; |
| 46 | import android.support.test.filters.SmallTest; |
| 47 | import android.support.test.runner.AndroidJUnit4; |
| 48 | |
| 49 | import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb; |
| 50 | import com.android.server.locksettings.recoverablekeystore.storage.RecoverySessionStorage; |
| 51 | |
| 52 | import com.google.common.collect.ImmutableList; |
Robert Berry | b9a220b | 2017-12-21 12:41:01 +0000 | [diff] [blame] | 53 | import com.google.common.collect.ImmutableMap; |
Robert Berry | e16fa98 | 2017-12-20 15:59:37 +0000 | [diff] [blame] | 54 | |
| 55 | import org.junit.After; |
| 56 | import org.junit.Before; |
| 57 | import org.junit.Test; |
| 58 | import org.junit.runner.RunWith; |
| 59 | import org.mockito.Mock; |
| 60 | import org.mockito.MockitoAnnotations; |
| 61 | |
| 62 | import java.io.File; |
| 63 | import java.nio.charset.StandardCharsets; |
Robert Berry | 4a534ec | 2017-12-21 15:44:02 +0000 | [diff] [blame] | 64 | import java.util.concurrent.Executors; |
Dmitry Dementyev | ad88471 | 2017-12-20 12:38:36 -0800 | [diff] [blame] | 65 | import java.util.Map; |
Robert Berry | b9a220b | 2017-12-21 12:41:01 +0000 | [diff] [blame] | 66 | import java.util.Random; |
| 67 | |
Robert Berry | cfc990a | 2017-12-22 15:54:30 +0000 | [diff] [blame^] | 68 | import javax.crypto.Cipher; |
Robert Berry | b9a220b | 2017-12-21 12:41:01 +0000 | [diff] [blame] | 69 | import javax.crypto.SecretKey; |
Robert Berry | cfc990a | 2017-12-22 15:54:30 +0000 | [diff] [blame^] | 70 | import javax.crypto.spec.GCMParameterSpec; |
Robert Berry | b9a220b | 2017-12-21 12:41:01 +0000 | [diff] [blame] | 71 | import javax.crypto.spec.SecretKeySpec; |
Robert Berry | e16fa98 | 2017-12-20 15:59:37 +0000 | [diff] [blame] | 72 | |
| 73 | @SmallTest |
| 74 | @RunWith(AndroidJUnit4.class) |
| 75 | public class RecoverableKeyStoreManagerTest { |
| 76 | private static final String DATABASE_FILE_NAME = "recoverablekeystore.db"; |
| 77 | |
Robert Berry | cfc990a | 2017-12-22 15:54:30 +0000 | [diff] [blame^] | 78 | private static final String KEY_WRAP_CIPHER_ALGORITHM = "AES/GCM/NoPadding"; |
Robert Berry | e16fa98 | 2017-12-20 15:59:37 +0000 | [diff] [blame] | 79 | private static final String TEST_SESSION_ID = "karlin"; |
| 80 | private static final byte[] TEST_PUBLIC_KEY = new byte[] { |
| 81 | (byte) 0x30, (byte) 0x59, (byte) 0x30, (byte) 0x13, (byte) 0x06, (byte) 0x07, (byte) 0x2a, |
| 82 | (byte) 0x86, (byte) 0x48, (byte) 0xce, (byte) 0x3d, (byte) 0x02, (byte) 0x01, (byte) 0x06, |
| 83 | (byte) 0x08, (byte) 0x2a, (byte) 0x86, (byte) 0x48, (byte) 0xce, (byte) 0x3d, (byte) 0x03, |
| 84 | (byte) 0x01, (byte) 0x07, (byte) 0x03, (byte) 0x42, (byte) 0x00, (byte) 0x04, (byte) 0xb8, |
| 85 | (byte) 0x00, (byte) 0x11, (byte) 0x18, (byte) 0x98, (byte) 0x1d, (byte) 0xf0, (byte) 0x6e, |
| 86 | (byte) 0xb4, (byte) 0x94, (byte) 0xfe, (byte) 0x86, (byte) 0xda, (byte) 0x1c, (byte) 0x07, |
| 87 | (byte) 0x8d, (byte) 0x01, (byte) 0xb4, (byte) 0x3a, (byte) 0xf6, (byte) 0x8d, (byte) 0xdc, |
| 88 | (byte) 0x61, (byte) 0xd0, (byte) 0x46, (byte) 0x49, (byte) 0x95, (byte) 0x0f, (byte) 0x10, |
| 89 | (byte) 0x86, (byte) 0x93, (byte) 0x24, (byte) 0x66, (byte) 0xe0, (byte) 0x3f, (byte) 0xd2, |
| 90 | (byte) 0xdf, (byte) 0xf3, (byte) 0x79, (byte) 0x20, (byte) 0x1d, (byte) 0x91, (byte) 0x55, |
| 91 | (byte) 0xb0, (byte) 0xe5, (byte) 0xbd, (byte) 0x7a, (byte) 0x8b, (byte) 0x32, (byte) 0x7d, |
| 92 | (byte) 0x25, (byte) 0x53, (byte) 0xa2, (byte) 0xfc, (byte) 0xa5, (byte) 0x65, (byte) 0xe1, |
| 93 | (byte) 0xbd, (byte) 0x21, (byte) 0x44, (byte) 0x7e, (byte) 0x78, (byte) 0x52, (byte) 0xfa}; |
| 94 | private static final byte[] TEST_SALT = getUtf8Bytes("salt"); |
| 95 | private static final byte[] TEST_SECRET = getUtf8Bytes("password1234"); |
| 96 | private static final byte[] TEST_VAULT_CHALLENGE = getUtf8Bytes("vault_challenge"); |
| 97 | private static final byte[] TEST_VAULT_PARAMS = getUtf8Bytes("vault_params"); |
| 98 | private static final int TEST_USER_ID = 10009; |
| 99 | private static final int KEY_CLAIMANT_LENGTH_BYTES = 16; |
Robert Berry | b9a220b | 2017-12-21 12:41:01 +0000 | [diff] [blame] | 100 | private static final byte[] RECOVERY_RESPONSE_HEADER = |
| 101 | "V1 reencrypted_recovery_key".getBytes(StandardCharsets.UTF_8); |
| 102 | private static final String TEST_ALIAS = "nick"; |
Robert Berry | cfc990a | 2017-12-22 15:54:30 +0000 | [diff] [blame^] | 103 | private static final int RECOVERABLE_KEY_SIZE_BYTES = 32; |
Dmitry Dementyev | ad88471 | 2017-12-20 12:38:36 -0800 | [diff] [blame] | 104 | private static final int GENERATION_ID = 1; |
| 105 | private static final byte[] NONCE = getUtf8Bytes("nonce"); |
| 106 | private static final byte[] KEY_MATERIAL = getUtf8Bytes("keymaterial"); |
Robert Berry | cfc990a | 2017-12-22 15:54:30 +0000 | [diff] [blame^] | 107 | private static final int GCM_TAG_SIZE_BITS = 128; |
Dmitry Dementyev | ad88471 | 2017-12-20 12:38:36 -0800 | [diff] [blame] | 108 | |
Robert Berry | e16fa98 | 2017-12-20 15:59:37 +0000 | [diff] [blame] | 109 | @Mock private Context mMockContext; |
Dmitry Dementyev | 3b17c63 | 2017-12-21 17:30:48 -0800 | [diff] [blame] | 110 | @Mock private ListenersStorage mMockListenersStorage; |
Robert Berry | cfc990a | 2017-12-22 15:54:30 +0000 | [diff] [blame^] | 111 | @Mock private KeyguardManager mKeyguardManager; |
Robert Berry | e16fa98 | 2017-12-20 15:59:37 +0000 | [diff] [blame] | 112 | |
| 113 | private RecoverableKeyStoreDb mRecoverableKeyStoreDb; |
| 114 | private File mDatabaseFile; |
| 115 | private RecoverableKeyStoreManager mRecoverableKeyStoreManager; |
| 116 | private RecoverySessionStorage mRecoverySessionStorage; |
| 117 | |
| 118 | @Before |
| 119 | public void setUp() { |
| 120 | MockitoAnnotations.initMocks(this); |
| 121 | |
| 122 | Context context = InstrumentationRegistry.getTargetContext(); |
| 123 | mDatabaseFile = context.getDatabasePath(DATABASE_FILE_NAME); |
| 124 | mRecoverableKeyStoreDb = RecoverableKeyStoreDb.newInstance(context); |
Robert Berry | cfc990a | 2017-12-22 15:54:30 +0000 | [diff] [blame^] | 125 | |
Robert Berry | e16fa98 | 2017-12-20 15:59:37 +0000 | [diff] [blame] | 126 | mRecoverySessionStorage = new RecoverySessionStorage(); |
Robert Berry | cfc990a | 2017-12-22 15:54:30 +0000 | [diff] [blame^] | 127 | |
| 128 | when(mMockContext.getSystemService(anyString())).thenReturn(mKeyguardManager); |
| 129 | when(mMockContext.getSystemServiceName(any())).thenReturn("test"); |
| 130 | when(mKeyguardManager.isDeviceSecure(anyInt())).thenReturn(true); |
| 131 | |
Robert Berry | e16fa98 | 2017-12-20 15:59:37 +0000 | [diff] [blame] | 132 | mRecoverableKeyStoreManager = new RecoverableKeyStoreManager( |
| 133 | mMockContext, |
| 134 | mRecoverableKeyStoreDb, |
Robert Berry | 4a534ec | 2017-12-21 15:44:02 +0000 | [diff] [blame] | 135 | mRecoverySessionStorage, |
Dmitry Dementyev | 3b17c63 | 2017-12-21 17:30:48 -0800 | [diff] [blame] | 136 | Executors.newSingleThreadExecutor(), |
| 137 | mMockListenersStorage); |
Robert Berry | e16fa98 | 2017-12-20 15:59:37 +0000 | [diff] [blame] | 138 | } |
| 139 | |
| 140 | @After |
| 141 | public void tearDown() { |
| 142 | mRecoverableKeyStoreDb.close(); |
| 143 | mDatabaseFile.delete(); |
| 144 | } |
| 145 | |
| 146 | @Test |
Robert Berry | cfc990a | 2017-12-22 15:54:30 +0000 | [diff] [blame^] | 147 | public void generateAndStoreKey_storesTheKey() throws Exception { |
| 148 | int uid = Binder.getCallingUid(); |
| 149 | |
| 150 | mRecoverableKeyStoreManager.generateAndStoreKey(TEST_ALIAS); |
| 151 | |
| 152 | assertThat(mRecoverableKeyStoreDb.getKey(uid, TEST_ALIAS)).isNotNull(); |
| 153 | } |
| 154 | |
| 155 | @Test |
| 156 | public void generateAndStoreKey_returnsAKeyOfAppropriateSize() throws Exception { |
| 157 | assertThat(mRecoverableKeyStoreManager.generateAndStoreKey(TEST_ALIAS)) |
| 158 | .hasLength(RECOVERABLE_KEY_SIZE_BYTES); |
| 159 | } |
| 160 | |
| 161 | @Test |
| 162 | public void generateAndStoreKey_storesTheWrappedFormOfTheReturnedBytes() throws Exception { |
| 163 | int uid = Binder.getCallingUid(); |
| 164 | |
| 165 | byte[] rawKey = mRecoverableKeyStoreManager.generateAndStoreKey(TEST_ALIAS); |
| 166 | |
| 167 | WrappedKey wrappedKey = mRecoverableKeyStoreDb.getKey(uid, TEST_ALIAS); |
| 168 | PlatformEncryptionKey encryptionKey = PlatformKeyManager.getInstance( |
| 169 | mMockContext, |
| 170 | mRecoverableKeyStoreDb, |
| 171 | Binder.getCallingUserHandle().getIdentifier()) |
| 172 | .getEncryptKey(); |
| 173 | Cipher cipher = Cipher.getInstance(KEY_WRAP_CIPHER_ALGORITHM); |
| 174 | cipher.init( |
| 175 | Cipher.ENCRYPT_MODE, |
| 176 | encryptionKey.getKey(), |
| 177 | new GCMParameterSpec(GCM_TAG_SIZE_BITS, wrappedKey.getNonce())); |
| 178 | byte[] encryptedBytes = cipher.update(rawKey); |
| 179 | assertArrayEquals(encryptedBytes, wrappedKey.getKeyMaterial()); |
| 180 | } |
| 181 | |
| 182 | @Test |
Robert Berry | e16fa98 | 2017-12-20 15:59:37 +0000 | [diff] [blame] | 183 | public void startRecoverySession_checksPermissionFirst() throws Exception { |
| 184 | mRecoverableKeyStoreManager.startRecoverySession( |
| 185 | TEST_SESSION_ID, |
| 186 | TEST_PUBLIC_KEY, |
| 187 | TEST_VAULT_PARAMS, |
| 188 | TEST_VAULT_CHALLENGE, |
Dmitry Dementyev | ad88471 | 2017-12-20 12:38:36 -0800 | [diff] [blame] | 189 | ImmutableList.of( |
| 190 | new KeyStoreRecoveryMetadata( |
| 191 | TYPE_LOCKSCREEN, |
| 192 | TYPE_PASSWORD, |
| 193 | KeyDerivationParameters.createSHA256Parameters(TEST_SALT), |
| 194 | TEST_SECRET)), |
Robert Berry | e16fa98 | 2017-12-20 15:59:37 +0000 | [diff] [blame] | 195 | TEST_USER_ID); |
| 196 | |
Dmitry Dementyev | ad88471 | 2017-12-20 12:38:36 -0800 | [diff] [blame] | 197 | verify(mMockContext, times(1)) |
| 198 | .enforceCallingOrSelfPermission( |
| 199 | eq(RecoverableKeyStoreLoader.PERMISSION_RECOVER_KEYSTORE), any()); |
Robert Berry | e16fa98 | 2017-12-20 15:59:37 +0000 | [diff] [blame] | 200 | } |
| 201 | |
| 202 | @Test |
| 203 | public void startRecoverySession_storesTheSessionInfo() throws Exception { |
| 204 | mRecoverableKeyStoreManager.startRecoverySession( |
| 205 | TEST_SESSION_ID, |
| 206 | TEST_PUBLIC_KEY, |
| 207 | TEST_VAULT_PARAMS, |
| 208 | TEST_VAULT_CHALLENGE, |
Dmitry Dementyev | ad88471 | 2017-12-20 12:38:36 -0800 | [diff] [blame] | 209 | ImmutableList.of( |
| 210 | new KeyStoreRecoveryMetadata( |
| 211 | TYPE_LOCKSCREEN, |
| 212 | TYPE_PASSWORD, |
| 213 | KeyDerivationParameters.createSHA256Parameters(TEST_SALT), |
| 214 | TEST_SECRET)), |
Robert Berry | e16fa98 | 2017-12-20 15:59:37 +0000 | [diff] [blame] | 215 | TEST_USER_ID); |
| 216 | |
| 217 | assertEquals(1, mRecoverySessionStorage.size()); |
Dmitry Dementyev | ad88471 | 2017-12-20 12:38:36 -0800 | [diff] [blame] | 218 | RecoverySessionStorage.Entry entry = |
| 219 | mRecoverySessionStorage.get(TEST_USER_ID, TEST_SESSION_ID); |
Robert Berry | e16fa98 | 2017-12-20 15:59:37 +0000 | [diff] [blame] | 220 | assertArrayEquals(TEST_SECRET, entry.getLskfHash()); |
| 221 | assertEquals(KEY_CLAIMANT_LENGTH_BYTES, entry.getKeyClaimant().length); |
| 222 | } |
| 223 | |
| 224 | @Test |
| 225 | public void startRecoverySession_throwsIfBadNumberOfSecrets() throws Exception { |
| 226 | try { |
| 227 | mRecoverableKeyStoreManager.startRecoverySession( |
| 228 | TEST_SESSION_ID, |
| 229 | TEST_PUBLIC_KEY, |
| 230 | TEST_VAULT_PARAMS, |
| 231 | TEST_VAULT_CHALLENGE, |
| 232 | ImmutableList.of(), |
| 233 | TEST_USER_ID); |
Robert Berry | b9a220b | 2017-12-21 12:41:01 +0000 | [diff] [blame] | 234 | fail("should have thrown"); |
Robert Berry | e16fa98 | 2017-12-20 15:59:37 +0000 | [diff] [blame] | 235 | } catch (RemoteException e) { |
Dmitry Dementyev | ad88471 | 2017-12-20 12:38:36 -0800 | [diff] [blame] | 236 | assertEquals("Only a single KeyStoreRecoveryMetadata is supported", e.getMessage()); |
Robert Berry | e16fa98 | 2017-12-20 15:59:37 +0000 | [diff] [blame] | 237 | } |
| 238 | } |
| 239 | |
| 240 | @Test |
| 241 | public void startRecoverySession_throwsIfBadKey() throws Exception { |
| 242 | try { |
| 243 | mRecoverableKeyStoreManager.startRecoverySession( |
| 244 | TEST_SESSION_ID, |
| 245 | getUtf8Bytes("0"), |
| 246 | TEST_VAULT_PARAMS, |
| 247 | TEST_VAULT_CHALLENGE, |
Dmitry Dementyev | ad88471 | 2017-12-20 12:38:36 -0800 | [diff] [blame] | 248 | ImmutableList.of( |
| 249 | new KeyStoreRecoveryMetadata( |
| 250 | TYPE_LOCKSCREEN, |
| 251 | TYPE_PASSWORD, |
| 252 | KeyDerivationParameters.createSHA256Parameters(TEST_SALT), |
| 253 | TEST_SECRET)), |
Robert Berry | e16fa98 | 2017-12-20 15:59:37 +0000 | [diff] [blame] | 254 | TEST_USER_ID); |
Robert Berry | b9a220b | 2017-12-21 12:41:01 +0000 | [diff] [blame] | 255 | fail("should have thrown"); |
Robert Berry | e16fa98 | 2017-12-20 15:59:37 +0000 | [diff] [blame] | 256 | } catch (RemoteException e) { |
Dmitry Dementyev | ad88471 | 2017-12-20 12:38:36 -0800 | [diff] [blame] | 257 | assertEquals("Not a valid X509 key", e.getMessage()); |
Robert Berry | e16fa98 | 2017-12-20 15:59:37 +0000 | [diff] [blame] | 258 | } |
| 259 | } |
| 260 | |
Robert Berry | b9a220b | 2017-12-21 12:41:01 +0000 | [diff] [blame] | 261 | @Test |
| 262 | public void recoverKeys_throwsIfNoSessionIsPresent() throws Exception { |
| 263 | try { |
| 264 | mRecoverableKeyStoreManager.recoverKeys( |
| 265 | TEST_SESSION_ID, |
| 266 | /*recoveryKeyBlob=*/ randomBytes(32), |
| 267 | /*applicationKeys=*/ ImmutableList.of( |
| 268 | new KeyEntryRecoveryData(getUtf8Bytes("alias"), randomBytes(32)) |
| 269 | ), |
| 270 | TEST_USER_ID); |
| 271 | fail("should have thrown"); |
| 272 | } catch (RemoteException e) { |
| 273 | assertEquals("User 10009 does not have pending session 'karlin'", |
| 274 | e.getMessage()); |
| 275 | } |
| 276 | } |
| 277 | |
| 278 | @Test |
| 279 | public void recoverKeys_throwsIfRecoveryClaimCannotBeDecrypted() throws Exception { |
| 280 | mRecoverableKeyStoreManager.startRecoverySession( |
| 281 | TEST_SESSION_ID, |
| 282 | TEST_PUBLIC_KEY, |
| 283 | TEST_VAULT_PARAMS, |
| 284 | TEST_VAULT_CHALLENGE, |
| 285 | ImmutableList.of(new KeyStoreRecoveryMetadata( |
| 286 | TYPE_LOCKSCREEN, |
| 287 | TYPE_PASSWORD, |
| 288 | KeyDerivationParameters.createSHA256Parameters(TEST_SALT), |
| 289 | TEST_SECRET)), |
| 290 | TEST_USER_ID); |
| 291 | |
| 292 | try { |
| 293 | mRecoverableKeyStoreManager.recoverKeys( |
| 294 | TEST_SESSION_ID, |
| 295 | /*encryptedRecoveryKey=*/ randomBytes(60), |
| 296 | /*applicationKeys=*/ ImmutableList.of(), |
| 297 | /*uid=*/ TEST_USER_ID); |
| 298 | fail("should have thrown"); |
| 299 | } catch (RemoteException e) { |
| 300 | assertEquals("Failed to decrypt recovery key", e.getMessage()); |
| 301 | } |
| 302 | } |
| 303 | |
| 304 | @Test |
| 305 | public void recoverKeys_throwsIfFailedToDecryptAnApplicationKey() throws Exception { |
| 306 | mRecoverableKeyStoreManager.startRecoverySession( |
| 307 | TEST_SESSION_ID, |
| 308 | TEST_PUBLIC_KEY, |
| 309 | TEST_VAULT_PARAMS, |
| 310 | TEST_VAULT_CHALLENGE, |
| 311 | ImmutableList.of(new KeyStoreRecoveryMetadata( |
| 312 | TYPE_LOCKSCREEN, |
| 313 | TYPE_PASSWORD, |
| 314 | KeyDerivationParameters.createSHA256Parameters(TEST_SALT), |
| 315 | TEST_SECRET)), |
| 316 | TEST_USER_ID); |
| 317 | byte[] keyClaimant = mRecoverySessionStorage.get(TEST_USER_ID, TEST_SESSION_ID) |
| 318 | .getKeyClaimant(); |
| 319 | SecretKey recoveryKey = randomRecoveryKey(); |
| 320 | byte[] encryptedClaimResponse = encryptClaimResponse( |
| 321 | keyClaimant, TEST_SECRET, TEST_VAULT_PARAMS, recoveryKey); |
| 322 | KeyEntryRecoveryData badApplicationKey = new KeyEntryRecoveryData( |
| 323 | TEST_ALIAS.getBytes(StandardCharsets.UTF_8), |
| 324 | randomBytes(32)); |
| 325 | |
| 326 | try { |
| 327 | mRecoverableKeyStoreManager.recoverKeys( |
| 328 | TEST_SESSION_ID, |
| 329 | /*encryptedRecoveryKey=*/ encryptedClaimResponse, |
| 330 | /*applicationKeys=*/ ImmutableList.of(badApplicationKey), |
| 331 | /*uid=*/ TEST_USER_ID); |
| 332 | fail("should have thrown"); |
| 333 | } catch (RemoteException e) { |
| 334 | assertEquals("Failed to recover key with alias 'nick'", e.getMessage()); |
| 335 | } |
| 336 | } |
| 337 | |
| 338 | @Test |
| 339 | public void recoverKeys_doesNotThrowIfAllIsOk() throws Exception { |
| 340 | mRecoverableKeyStoreManager.startRecoverySession( |
| 341 | TEST_SESSION_ID, |
| 342 | TEST_PUBLIC_KEY, |
| 343 | TEST_VAULT_PARAMS, |
| 344 | TEST_VAULT_CHALLENGE, |
| 345 | ImmutableList.of(new KeyStoreRecoveryMetadata( |
| 346 | TYPE_LOCKSCREEN, |
| 347 | TYPE_PASSWORD, |
| 348 | KeyDerivationParameters.createSHA256Parameters(TEST_SALT), |
| 349 | TEST_SECRET)), |
| 350 | TEST_USER_ID); |
| 351 | byte[] keyClaimant = mRecoverySessionStorage.get(TEST_USER_ID, TEST_SESSION_ID) |
| 352 | .getKeyClaimant(); |
| 353 | SecretKey recoveryKey = randomRecoveryKey(); |
| 354 | byte[] encryptedClaimResponse = encryptClaimResponse( |
| 355 | keyClaimant, TEST_SECRET, TEST_VAULT_PARAMS, recoveryKey); |
| 356 | KeyEntryRecoveryData applicationKey = new KeyEntryRecoveryData( |
| 357 | TEST_ALIAS.getBytes(StandardCharsets.UTF_8), |
| 358 | randomEncryptedApplicationKey(recoveryKey) |
| 359 | ); |
| 360 | |
| 361 | mRecoverableKeyStoreManager.recoverKeys( |
| 362 | TEST_SESSION_ID, |
| 363 | encryptedClaimResponse, |
| 364 | ImmutableList.of(applicationKey), |
| 365 | TEST_USER_ID); |
| 366 | } |
| 367 | |
Dmitry Dementyev | 3b17c63 | 2017-12-21 17:30:48 -0800 | [diff] [blame] | 368 | @Test |
| 369 | public void setSnapshotCreatedPendingIntent() throws Exception { |
| 370 | int uid = Binder.getCallingUid(); |
| 371 | PendingIntent intent = PendingIntent.getBroadcast( |
| 372 | InstrumentationRegistry.getTargetContext(), /*requestCode=*/1, |
| 373 | new Intent(), /*flags=*/ 0); |
| 374 | mRecoverableKeyStoreManager.setSnapshotCreatedPendingIntent(intent, /*userId=*/ 0); |
| 375 | verify(mMockListenersStorage).setSnapshotListener(eq(uid), any(PendingIntent.class)); |
| 376 | } |
| 377 | |
Dmitry Dementyev | ad88471 | 2017-12-20 12:38:36 -0800 | [diff] [blame] | 378 | @Test |
| 379 | public void setRecoveryStatus_forOneAlias() throws Exception { |
| 380 | int userId = UserHandle.getCallingUserId(); |
| 381 | int uid = Binder.getCallingUid(); |
| 382 | int status = 100; |
| 383 | int status2 = 200; |
| 384 | String alias = "key1"; |
| 385 | WrappedKey wrappedKey = new WrappedKey(NONCE, KEY_MATERIAL, GENERATION_ID, status); |
| 386 | mRecoverableKeyStoreDb.insertKey(userId, uid, alias, wrappedKey); |
| 387 | Map<String, Integer> statuses = |
| 388 | mRecoverableKeyStoreManager.getRecoveryStatus(/*packageName=*/ null, userId); |
| 389 | assertThat(statuses).hasSize(1); |
| 390 | assertThat(statuses).containsEntry(alias, status); |
| 391 | |
| 392 | mRecoverableKeyStoreManager.setRecoveryStatus( |
| 393 | /*packageName=*/ null, new String[] {alias}, status2, userId); |
| 394 | statuses = mRecoverableKeyStoreManager.getRecoveryStatus(/*packageName=*/ null, userId); |
| 395 | assertThat(statuses).hasSize(1); |
| 396 | assertThat(statuses).containsEntry(alias, status2); // updated |
| 397 | } |
| 398 | |
| 399 | @Test |
| 400 | public void setRecoveryStatus_for2Aliases() throws Exception { |
| 401 | int userId = UserHandle.getCallingUserId(); |
| 402 | int uid = Binder.getCallingUid(); |
| 403 | int status = 100; |
| 404 | int status2 = 200; |
| 405 | int status3 = 300; |
| 406 | String alias = "key1"; |
| 407 | String alias2 = "key2"; |
| 408 | WrappedKey wrappedKey = new WrappedKey(NONCE, KEY_MATERIAL, GENERATION_ID, status); |
| 409 | mRecoverableKeyStoreDb.insertKey(userId, uid, alias, wrappedKey); |
| 410 | mRecoverableKeyStoreDb.insertKey(userId, uid, alias2, wrappedKey); |
| 411 | Map<String, Integer> statuses = |
| 412 | mRecoverableKeyStoreManager.getRecoveryStatus(/*packageName=*/ null, userId); |
| 413 | assertThat(statuses).hasSize(2); |
| 414 | assertThat(statuses).containsEntry(alias, status); |
| 415 | assertThat(statuses).containsEntry(alias2, status); |
| 416 | |
| 417 | mRecoverableKeyStoreManager.setRecoveryStatus( |
| 418 | /*packageName=*/ null, /*aliases=*/ null, status2, userId); |
| 419 | statuses = mRecoverableKeyStoreManager.getRecoveryStatus(/*packageName=*/ null, userId); |
| 420 | assertThat(statuses).hasSize(2); |
| 421 | assertThat(statuses).containsEntry(alias, status2); // updated |
| 422 | assertThat(statuses).containsEntry(alias2, status2); // updated |
| 423 | |
| 424 | mRecoverableKeyStoreManager.setRecoveryStatus( |
| 425 | /*packageName=*/ null, new String[] {alias2}, status3, userId); |
| 426 | |
| 427 | statuses = mRecoverableKeyStoreManager.getRecoveryStatus(/*packageName=*/ null, userId); |
| 428 | assertThat(statuses).hasSize(2); |
| 429 | assertThat(statuses).containsEntry(alias, status2); |
| 430 | assertThat(statuses).containsEntry(alias2, status3); // updated |
| 431 | |
| 432 | mRecoverableKeyStoreManager.setRecoveryStatus( |
| 433 | /*packageName=*/ null, new String[] {alias, alias2}, status, userId); |
| 434 | |
| 435 | statuses = mRecoverableKeyStoreManager.getRecoveryStatus(/*packageName=*/ null, userId); |
| 436 | assertThat(statuses).hasSize(2); |
| 437 | assertThat(statuses).containsEntry(alias, status); // updated |
| 438 | assertThat(statuses).containsEntry(alias2, status); // updated |
| 439 | } |
| 440 | |
Robert Berry | b9a220b | 2017-12-21 12:41:01 +0000 | [diff] [blame] | 441 | private static byte[] randomEncryptedApplicationKey(SecretKey recoveryKey) throws Exception { |
| 442 | return KeySyncUtils.encryptKeysWithRecoveryKey(recoveryKey, ImmutableMap.of( |
| 443 | "alias", new SecretKeySpec(randomBytes(32), "AES") |
| 444 | )).get("alias"); |
| 445 | } |
| 446 | |
| 447 | private static byte[] encryptClaimResponse( |
| 448 | byte[] keyClaimant, |
| 449 | byte[] lskfHash, |
| 450 | byte[] vaultParams, |
| 451 | SecretKey recoveryKey) throws Exception { |
| 452 | byte[] locallyEncryptedRecoveryKey = KeySyncUtils.locallyEncryptRecoveryKey( |
| 453 | lskfHash, recoveryKey); |
| 454 | return SecureBox.encrypt( |
| 455 | /*theirPublicKey=*/ null, |
| 456 | /*sharedSecret=*/ keyClaimant, |
| 457 | /*header=*/ KeySyncUtils.concat(RECOVERY_RESPONSE_HEADER, vaultParams), |
| 458 | /*payload=*/ locallyEncryptedRecoveryKey); |
| 459 | } |
| 460 | |
| 461 | private static SecretKey randomRecoveryKey() { |
| 462 | return new SecretKeySpec(randomBytes(32), "AES"); |
| 463 | } |
| 464 | |
Robert Berry | e16fa98 | 2017-12-20 15:59:37 +0000 | [diff] [blame] | 465 | private static byte[] getUtf8Bytes(String s) { |
| 466 | return s.getBytes(StandardCharsets.UTF_8); |
| 467 | } |
Robert Berry | b9a220b | 2017-12-21 12:41:01 +0000 | [diff] [blame] | 468 | |
| 469 | private static byte[] randomBytes(int n) { |
| 470 | byte[] bytes = new byte[n]; |
| 471 | new Random().nextBytes(bytes); |
| 472 | return bytes; |
| 473 | } |
Robert Berry | e16fa98 | 2017-12-20 15:59:37 +0000 | [diff] [blame] | 474 | } |