blob: 2676ee8897af4fe322affbc4adb592842970e621 [file] [log] [blame]
Robert Berry76cf0832017-12-15 23:01:22 +00001/*
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.storage;
18
Dmitry Dementyevad884712017-12-20 12:38:36 -080019import android.annotation.NonNull;
Robert Berry76cf0832017-12-15 23:01:22 +000020import android.annotation.Nullable;
21import android.content.ContentValues;
22import android.content.Context;
23import android.database.Cursor;
24import android.database.sqlite.SQLiteDatabase;
Dmitry Dementyev3f2d1712018-01-24 11:11:40 -080025import android.security.keystore.recovery.RecoveryController;
Dmitry Dementyevbdfdf532017-12-27 11:58:45 -080026import android.text.TextUtils;
Robert Berry76cf0832017-12-15 23:01:22 +000027import android.util.Log;
28
29import com.android.server.locksettings.recoverablekeystore.WrappedKey;
30import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDbContract.KeysEntry;
Bo Zhu584b923f2017-12-22 16:05:15 -080031import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDbContract.RecoveryServiceMetadataEntry;
Robert Berrybc088402017-12-18 13:10:41 +000032import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDbContract.UserMetadataEntry;
Robert Berry76cf0832017-12-15 23:01:22 +000033
Bo Zhu14d993d2018-02-03 21:38:48 -080034import java.io.ByteArrayInputStream;
Bo Zhu5b81fa62017-12-21 14:36:11 -080035import java.security.KeyFactory;
36import java.security.NoSuchAlgorithmException;
37import java.security.PublicKey;
Bo Zhu14d993d2018-02-03 21:38:48 -080038import java.security.cert.CertPath;
39import java.security.cert.CertificateEncodingException;
40import java.security.cert.CertificateException;
41import java.security.cert.CertificateFactory;
Bo Zhu5b81fa62017-12-21 14:36:11 -080042import java.security.spec.InvalidKeySpecException;
43import java.security.spec.X509EncodedKeySpec;
Dmitry Dementyev77183ef2018-01-05 15:46:00 -080044import java.util.ArrayList;
Dmitry Dementyevbdfdf532017-12-27 11:58:45 -080045import java.util.Arrays;
Robert Berry76cf0832017-12-15 23:01:22 +000046import java.util.HashMap;
Dmitry Dementyev77183ef2018-01-05 15:46:00 -080047import java.util.List;
Robert Berry76cf0832017-12-15 23:01:22 +000048import java.util.Locale;
49import java.util.Map;
Dmitry Dementyevbdfdf532017-12-27 11:58:45 -080050import java.util.StringJoiner;
Robert Berry76cf0832017-12-15 23:01:22 +000051
52/**
53 * Database of recoverable key information.
54 *
55 * @hide
56 */
57public class RecoverableKeyStoreDb {
58 private static final String TAG = "RecoverableKeyStoreDb";
59 private static final int IDLE_TIMEOUT_SECONDS = 30;
Robert Berryb7c06ea2017-12-21 13:37:23 +000060 private static final int LAST_SYNCED_AT_UNSYNCED = -1;
Bo Zhu14d993d2018-02-03 21:38:48 -080061 private static final String CERT_PATH_ENCODING = "PkiPath";
Robert Berry76cf0832017-12-15 23:01:22 +000062
63 private final RecoverableKeyStoreDbHelper mKeyStoreDbHelper;
64
65 /**
66 * A new instance, storing the database in the user directory of {@code context}.
67 *
68 * @hide
69 */
70 public static RecoverableKeyStoreDb newInstance(Context context) {
71 RecoverableKeyStoreDbHelper helper = new RecoverableKeyStoreDbHelper(context);
72 helper.setWriteAheadLoggingEnabled(true);
73 helper.setIdleConnectionTimeout(IDLE_TIMEOUT_SECONDS);
74 return new RecoverableKeyStoreDb(helper);
75 }
76
77 private RecoverableKeyStoreDb(RecoverableKeyStoreDbHelper keyStoreDbHelper) {
78 this.mKeyStoreDbHelper = keyStoreDbHelper;
79 }
80
81 /**
82 * Inserts a key into the database.
83 *
Robert Berryb7c06ea2017-12-21 13:37:23 +000084 * @param userId The uid of the profile the application is running under.
Robert Berry76cf0832017-12-15 23:01:22 +000085 * @param uid Uid of the application to whom the key belongs.
86 * @param alias The alias of the key in the AndroidKeyStore.
Robert Berry67b228c2017-12-18 19:26:22 +000087 * @param wrappedKey The wrapped key.
Robert Berry76cf0832017-12-15 23:01:22 +000088 * @return The primary key of the inserted row, or -1 if failed.
89 *
90 * @hide
91 */
Robert Berryb7c06ea2017-12-21 13:37:23 +000092 public long insertKey(int userId, int uid, String alias, WrappedKey wrappedKey) {
Robert Berry76cf0832017-12-15 23:01:22 +000093 SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
94 ContentValues values = new ContentValues();
Robert Berryb7c06ea2017-12-21 13:37:23 +000095 values.put(KeysEntry.COLUMN_NAME_USER_ID, userId);
Robert Berry76cf0832017-12-15 23:01:22 +000096 values.put(KeysEntry.COLUMN_NAME_UID, uid);
97 values.put(KeysEntry.COLUMN_NAME_ALIAS, alias);
98 values.put(KeysEntry.COLUMN_NAME_NONCE, wrappedKey.getNonce());
99 values.put(KeysEntry.COLUMN_NAME_WRAPPED_KEY, wrappedKey.getKeyMaterial());
Robert Berryb7c06ea2017-12-21 13:37:23 +0000100 values.put(KeysEntry.COLUMN_NAME_LAST_SYNCED_AT, LAST_SYNCED_AT_UNSYNCED);
Robert Berry67b228c2017-12-18 19:26:22 +0000101 values.put(KeysEntry.COLUMN_NAME_GENERATION_ID, wrappedKey.getPlatformKeyGenerationId());
Dmitry Dementyevad884712017-12-20 12:38:36 -0800102 values.put(KeysEntry.COLUMN_NAME_RECOVERY_STATUS, wrappedKey.getRecoveryStatus());
Robert Berry76cf0832017-12-15 23:01:22 +0000103 return db.replace(KeysEntry.TABLE_NAME, /*nullColumnHack=*/ null, values);
104 }
105
106 /**
107 * Gets the key with {@code alias} for the app with {@code uid}.
108 *
109 * @hide
110 */
111 @Nullable public WrappedKey getKey(int uid, String alias) {
112 SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase();
113 String[] projection = {
114 KeysEntry._ID,
115 KeysEntry.COLUMN_NAME_NONCE,
116 KeysEntry.COLUMN_NAME_WRAPPED_KEY,
Dmitry Dementyevad884712017-12-20 12:38:36 -0800117 KeysEntry.COLUMN_NAME_GENERATION_ID,
118 KeysEntry.COLUMN_NAME_RECOVERY_STATUS};
Robert Berry76cf0832017-12-15 23:01:22 +0000119 String selection =
120 KeysEntry.COLUMN_NAME_UID + " = ? AND "
121 + KeysEntry.COLUMN_NAME_ALIAS + " = ?";
122 String[] selectionArguments = { Integer.toString(uid), alias };
123
124 try (
125 Cursor cursor = db.query(
126 KeysEntry.TABLE_NAME,
127 projection,
128 selection,
129 selectionArguments,
130 /*groupBy=*/ null,
131 /*having=*/ null,
132 /*orderBy=*/ null)
133 ) {
134 int count = cursor.getCount();
135 if (count == 0) {
136 return null;
137 }
138 if (count > 1) {
139 Log.wtf(TAG,
140 String.format(Locale.US,
141 "%d WrappedKey entries found for uid=%d alias='%s'. "
142 + "Should only ever be 0 or 1.", count, uid, alias));
143 return null;
144 }
145 cursor.moveToFirst();
146 byte[] nonce = cursor.getBlob(
147 cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_NONCE));
148 byte[] keyMaterial = cursor.getBlob(
149 cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_WRAPPED_KEY));
Robert Berry67b228c2017-12-18 19:26:22 +0000150 int generationId = cursor.getInt(
151 cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_GENERATION_ID));
Dmitry Dementyevad884712017-12-20 12:38:36 -0800152 int recoveryStatus = cursor.getInt(
153 cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_RECOVERY_STATUS));
154 return new WrappedKey(nonce, keyMaterial, generationId, recoveryStatus);
Robert Berry76cf0832017-12-15 23:01:22 +0000155 }
156 }
157
158 /**
Robert Berry5daccec2018-01-06 19:16:25 +0000159 * Removes key with {@code alias} for app with {@code uid}.
160 *
161 * @return {@code true} if deleted a row.
162 */
163 public boolean removeKey(int uid, String alias) {
164 SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
165 String selection = KeysEntry.COLUMN_NAME_UID + " = ? AND " +
166 KeysEntry.COLUMN_NAME_ALIAS + " = ?";
167 String[] selectionArgs = { Integer.toString(uid), alias };
168 return db.delete(KeysEntry.TABLE_NAME, selection, selectionArgs) > 0;
169 }
170
171 /**
Dmitry Dementyevad884712017-12-20 12:38:36 -0800172 * Returns all statuses for keys {@code uid} and {@code platformKeyGenerationId}.
173 *
174 * @param uid of the application
175 *
176 * @return Map from Aliases to status.
177 *
178 * @hide
179 */
180 public @NonNull Map<String, Integer> getStatusForAllKeys(int uid) {
181 SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase();
182 String[] projection = {
183 KeysEntry._ID,
184 KeysEntry.COLUMN_NAME_ALIAS,
185 KeysEntry.COLUMN_NAME_RECOVERY_STATUS};
186 String selection =
187 KeysEntry.COLUMN_NAME_UID + " = ?";
188 String[] selectionArguments = {Integer.toString(uid)};
189
190 try (
191 Cursor cursor = db.query(
192 KeysEntry.TABLE_NAME,
193 projection,
194 selection,
195 selectionArguments,
196 /*groupBy=*/ null,
197 /*having=*/ null,
198 /*orderBy=*/ null)
199 ) {
200 HashMap<String, Integer> statuses = new HashMap<>();
201 while (cursor.moveToNext()) {
202 String alias = cursor.getString(
203 cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_ALIAS));
204 int recoveryStatus = cursor.getInt(
205 cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_RECOVERY_STATUS));
206 statuses.put(alias, recoveryStatus);
207 }
208 return statuses;
209 }
210 }
211
212 /**
213 * Updates status for given key.
214 * @param uid of the application
215 * @param alias of the key
216 * @param status - new status
217 * @return number of updated entries.
218 * @hide
219 **/
220 public int setRecoveryStatus(int uid, String alias, int status) {
221 SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
222 ContentValues values = new ContentValues();
223 values.put(KeysEntry.COLUMN_NAME_RECOVERY_STATUS, status);
224 String selection =
225 KeysEntry.COLUMN_NAME_UID + " = ? AND "
226 + KeysEntry.COLUMN_NAME_ALIAS + " = ?";
227 return db.update(KeysEntry.TABLE_NAME, values, selection,
228 new String[] {String.valueOf(uid), alias});
229 }
230
231 /**
Dmitry Dementyev77183ef2018-01-05 15:46:00 -0800232 * Returns all keys for the given {@code userId} {@code recoveryAgentUid}
233 * and {@code platformKeyGenerationId}.
Robert Berry76cf0832017-12-15 23:01:22 +0000234 *
Robert Berryb7c06ea2017-12-21 13:37:23 +0000235 * @param userId User id of the profile to which all the keys are associated.
Dmitry Dementyev77183ef2018-01-05 15:46:00 -0800236 * @param recoveryAgentUid Uid of the recovery agent which will perform the sync
Robert Berry76cf0832017-12-15 23:01:22 +0000237 * @param platformKeyGenerationId The generation ID of the platform key that wrapped these keys.
238 * (i.e., this should be the most recent generation ID, as older platform keys are not
239 * usable.)
240 *
241 * @hide
242 */
Dmitry Dementyev77183ef2018-01-05 15:46:00 -0800243 public Map<String, WrappedKey> getAllKeys(int userId, int recoveryAgentUid,
244 int platformKeyGenerationId) {
Robert Berry76cf0832017-12-15 23:01:22 +0000245 SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase();
246 String[] projection = {
247 KeysEntry._ID,
248 KeysEntry.COLUMN_NAME_NONCE,
249 KeysEntry.COLUMN_NAME_WRAPPED_KEY,
Dmitry Dementyevad884712017-12-20 12:38:36 -0800250 KeysEntry.COLUMN_NAME_ALIAS,
251 KeysEntry.COLUMN_NAME_RECOVERY_STATUS};
Robert Berry76cf0832017-12-15 23:01:22 +0000252 String selection =
Robert Berryb7c06ea2017-12-21 13:37:23 +0000253 KeysEntry.COLUMN_NAME_USER_ID + " = ? AND "
Dmitry Dementyev77183ef2018-01-05 15:46:00 -0800254 + KeysEntry.COLUMN_NAME_UID + " = ? AND "
Robert Berry76cf0832017-12-15 23:01:22 +0000255 + KeysEntry.COLUMN_NAME_GENERATION_ID + " = ?";
256 String[] selectionArguments = {
Dmitry Dementyev77183ef2018-01-05 15:46:00 -0800257 Integer.toString(userId),
258 Integer.toString(recoveryAgentUid),
259 Integer.toString(platformKeyGenerationId)
260 };
Robert Berry76cf0832017-12-15 23:01:22 +0000261
262 try (
263 Cursor cursor = db.query(
264 KeysEntry.TABLE_NAME,
265 projection,
266 selection,
267 selectionArguments,
268 /*groupBy=*/ null,
269 /*having=*/ null,
270 /*orderBy=*/ null)
271 ) {
272 HashMap<String, WrappedKey> keys = new HashMap<>();
273 while (cursor.moveToNext()) {
274 byte[] nonce = cursor.getBlob(
275 cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_NONCE));
276 byte[] keyMaterial = cursor.getBlob(
277 cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_WRAPPED_KEY));
278 String alias = cursor.getString(
279 cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_ALIAS));
Dmitry Dementyevad884712017-12-20 12:38:36 -0800280 int recoveryStatus = cursor.getInt(
281 cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_RECOVERY_STATUS));
282 keys.put(alias, new WrappedKey(nonce, keyMaterial, platformKeyGenerationId,
283 recoveryStatus));
Robert Berry76cf0832017-12-15 23:01:22 +0000284 }
285 return keys;
286 }
287 }
288
289 /**
Robert Berrybc088402017-12-18 13:10:41 +0000290 * Sets the {@code generationId} of the platform key for the account owned by {@code userId}.
291 *
292 * @return The primary key ID of the relation.
293 */
294 public long setPlatformKeyGenerationId(int userId, int generationId) {
295 SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
296 ContentValues values = new ContentValues();
297 values.put(UserMetadataEntry.COLUMN_NAME_USER_ID, userId);
298 values.put(UserMetadataEntry.COLUMN_NAME_PLATFORM_KEY_GENERATION_ID, generationId);
Dmitry Dementyev3f2d1712018-01-24 11:11:40 -0800299 long result = db.replace(
Robert Berrybc088402017-12-18 13:10:41 +0000300 UserMetadataEntry.TABLE_NAME, /*nullColumnHack=*/ null, values);
Dmitry Dementyev3f2d1712018-01-24 11:11:40 -0800301 if (result != -1) {
302 invalidateKeysWithOldGenerationId(userId, generationId);
303 }
304 return result;
305 }
306
307 /**
308 * Updates status of old keys to {@code RecoveryController.RECOVERY_STATUS_PERMANENT_FAILURE}.
309 */
310 public void invalidateKeysWithOldGenerationId(int userId, int newGenerationId) {
311 SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
312 ContentValues values = new ContentValues();
313 values.put(KeysEntry.COLUMN_NAME_RECOVERY_STATUS,
314 RecoveryController.RECOVERY_STATUS_PERMANENT_FAILURE);
315 String selection =
316 KeysEntry.COLUMN_NAME_USER_ID + " = ? AND "
317 + KeysEntry.COLUMN_NAME_GENERATION_ID + " < ?";
318 db.update(KeysEntry.TABLE_NAME, values, selection,
319 new String[] {String.valueOf(userId), String.valueOf(newGenerationId)});
Robert Berrybc088402017-12-18 13:10:41 +0000320 }
321
322 /**
Aseem Kumar3326da52018-03-12 18:05:16 -0700323 * Updates status of old keys to {@code RecoveryController.RECOVERY_STATUS_PERMANENT_FAILURE}.
324 */
325 public void invalidateKeysForUserIdOnCustomScreenLock(int userId) {
326 SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
327 ContentValues values = new ContentValues();
328 values.put(KeysEntry.COLUMN_NAME_RECOVERY_STATUS,
329 RecoveryController.RECOVERY_STATUS_PERMANENT_FAILURE);
330 String selection =
331 KeysEntry.COLUMN_NAME_USER_ID + " = ?";
332 db.update(KeysEntry.TABLE_NAME, values, selection,
333 new String[] {String.valueOf(userId)});
334 }
335
336 /**
Robert Berrybc088402017-12-18 13:10:41 +0000337 * Returns the generation ID associated with the platform key of the user with {@code userId}.
338 */
339 public int getPlatformKeyGenerationId(int userId) {
340 SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase();
341 String[] projection = {
342 UserMetadataEntry.COLUMN_NAME_PLATFORM_KEY_GENERATION_ID};
343 String selection =
344 UserMetadataEntry.COLUMN_NAME_USER_ID + " = ?";
345 String[] selectionArguments = {
346 Integer.toString(userId)};
347
348 try (
349 Cursor cursor = db.query(
350 UserMetadataEntry.TABLE_NAME,
351 projection,
352 selection,
353 selectionArguments,
354 /*groupBy=*/ null,
355 /*having=*/ null,
356 /*orderBy=*/ null)
357 ) {
358 if (cursor.getCount() == 0) {
359 return -1;
360 }
361 cursor.moveToFirst();
362 return cursor.getInt(
363 cursor.getColumnIndexOrThrow(
364 UserMetadataEntry.COLUMN_NAME_PLATFORM_KEY_GENERATION_ID));
365 }
366 }
367
368 /**
Bo Zhu584b923f2017-12-22 16:05:15 -0800369 * Updates the public key of the recovery service into the database.
Bo Zhu5b81fa62017-12-21 14:36:11 -0800370 *
371 * @param userId The uid of the profile the application is running under.
372 * @param uid The uid of the application to whom the key belongs.
373 * @param publicKey The public key of the recovery service.
374 * @return The primary key of the inserted row, or -1 if failed.
375 *
376 * @hide
377 */
378 public long setRecoveryServicePublicKey(int userId, int uid, PublicKey publicKey) {
Dmitry Dementyev7d8c78a2018-01-12 19:14:07 -0800379 return setBytes(userId, uid, RecoveryServiceMetadataEntry.COLUMN_NAME_PUBLIC_KEY,
380 publicKey.getEncoded());
Bo Zhu5b81fa62017-12-21 14:36:11 -0800381 }
382
383 /**
Bo Zhu14d993d2018-02-03 21:38:48 -0800384 * Returns the serial number of the XML file containing certificates of the recovery service.
385 *
386 * @param userId The userId of the profile the application is running under.
387 * @param uid The uid of the application who initializes the local recovery components.
388 * @return The value that were previously set, or null if there's none.
389 *
390 * @hide
391 */
392 @Nullable
393 public Long getRecoveryServiceCertSerial(int userId, int uid) {
394 return getLong(userId, uid, RecoveryServiceMetadataEntry.COLUMN_NAME_CERT_SERIAL);
395 }
396
397 /**
398 * Records the serial number of the XML file containing certificates of the recovery service.
399 *
400 * @param userId The userId of the profile the application is running under.
401 * @param uid The uid of the application who initializes the local recovery components.
402 * @param serial The serial number contained in the XML file for recovery service certificates.
403 * @return The primary key of the inserted row, or -1 if failed.
404 *
405 * @hide
406 */
407 public long setRecoveryServiceCertSerial(int userId, int uid, long serial) {
408 return setLong(userId, uid, RecoveryServiceMetadataEntry.COLUMN_NAME_CERT_SERIAL, serial);
409 }
410
411 /**
412 * Returns the {@code CertPath} of the recovery service.
413 *
414 * @param userId The userId of the profile the application is running under.
415 * @param uid The uid of the application who initializes the local recovery components.
416 * @return The value that were previously set, or null if there's none.
417 *
418 * @hide
419 */
420 @Nullable
421 public CertPath getRecoveryServiceCertPath(int userId, int uid) {
422 byte[] bytes = getBytes(userId, uid, RecoveryServiceMetadataEntry.COLUMN_NAME_CERT_PATH);
423 if (bytes == null) {
424 return null;
425 }
426 try {
427 return decodeCertPath(bytes);
428 } catch (CertificateException e) {
429 Log.wtf(TAG,
430 String.format(Locale.US,
431 "Recovery service CertPath entry cannot be decoded for "
432 + "userId=%d uid=%d.",
433 userId, uid), e);
434 return null;
435 }
436 }
437
438 /**
439 * Sets the {@code CertPath} of the recovery service.
440 *
441 * @param userId The userId of the profile the application is running under.
442 * @param uid The uid of the application who initializes the local recovery components.
443 * @param certPath The certificate path of the recovery service.
444 * @return The primary key of the inserted row, or -1 if failed.
445 * @hide
446 */
447 public long setRecoveryServiceCertPath(int userId, int uid, CertPath certPath) throws
448 CertificateEncodingException {
449 if (certPath.getCertificates().size() == 0) {
450 throw new CertificateEncodingException("No certificate contained in the cert path.");
451 }
452 return setBytes(userId, uid, RecoveryServiceMetadataEntry.COLUMN_NAME_CERT_PATH,
453 certPath.getEncoded(CERT_PATH_ENCODING));
454 }
455
456 /**
Dmitry Dementyev77183ef2018-01-05 15:46:00 -0800457 * Returns the list of recovery agents initialized for given {@code userId}
458 * @param userId The userId of the profile the application is running under.
459 * @return The list of recovery agents
460 * @hide
Robert Berry91044042017-12-27 12:05:58 +0000461 */
Dmitry Dementyev77183ef2018-01-05 15:46:00 -0800462 public @NonNull List<Integer> getRecoveryAgents(int userId) {
Robert Berry91044042017-12-27 12:05:58 +0000463 SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase();
464
465 String[] projection = { RecoveryServiceMetadataEntry.COLUMN_NAME_UID };
466 String selection = RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID + " = ?";
467 String[] selectionArguments = { Integer.toString(userId) };
468
469 try (
470 Cursor cursor = db.query(
471 RecoveryServiceMetadataEntry.TABLE_NAME,
472 projection,
473 selection,
474 selectionArguments,
475 /*groupBy=*/ null,
476 /*having=*/ null,
477 /*orderBy=*/ null)
478 ) {
479 int count = cursor.getCount();
Dmitry Dementyev77183ef2018-01-05 15:46:00 -0800480 ArrayList<Integer> result = new ArrayList<>(count);
481 while (cursor.moveToNext()) {
482 int uid = cursor.getInt(
483 cursor.getColumnIndexOrThrow(RecoveryServiceMetadataEntry.COLUMN_NAME_UID));
484 result.add(uid);
Robert Berry91044042017-12-27 12:05:58 +0000485 }
Dmitry Dementyev77183ef2018-01-05 15:46:00 -0800486 return result;
Robert Berry91044042017-12-27 12:05:58 +0000487 }
488 }
489
490 /**
Bo Zhu5b81fa62017-12-21 14:36:11 -0800491 * Returns the public key of the recovery service.
492 *
Dmitry Dementyev77183ef2018-01-05 15:46:00 -0800493 * @param userId The userId of the profile the application is running under.
Bo Zhu5b81fa62017-12-21 14:36:11 -0800494 * @param uid The uid of the application who initializes the local recovery components.
495 *
496 * @hide
497 */
Robert Berryaa3f4ca2017-12-27 10:53:58 +0000498 @Nullable
Bo Zhu5b81fa62017-12-21 14:36:11 -0800499 public PublicKey getRecoveryServicePublicKey(int userId, int uid) {
Dmitry Dementyev7d8c78a2018-01-12 19:14:07 -0800500 byte[] keyBytes =
501 getBytes(userId, uid, RecoveryServiceMetadataEntry.COLUMN_NAME_PUBLIC_KEY);
502 if (keyBytes == null) {
503 return null;
504 }
505 try {
506 return decodeX509Key(keyBytes);
507 } catch (InvalidKeySpecException e) {
508 Log.wtf(TAG,
509 String.format(Locale.US,
510 "Recovery service public key entry cannot be decoded for "
511 + "userId=%d uid=%d.",
512 userId, uid));
513 return null;
Bo Zhu5b81fa62017-12-21 14:36:11 -0800514 }
515 }
516
517 /**
Dmitry Dementyevbdfdf532017-12-27 11:58:45 -0800518 * Updates the list of user secret types used for end-to-end encryption.
519 * If no secret types are set, recovery snapshot will not be created.
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800520 * See {@code KeyChainProtectionParams}
Dmitry Dementyevbdfdf532017-12-27 11:58:45 -0800521 *
Dmitry Dementyev77183ef2018-01-05 15:46:00 -0800522 * @param userId The userId of the profile the application is running under.
Dmitry Dementyevbdfdf532017-12-27 11:58:45 -0800523 * @param uid The uid of the application.
524 * @param secretTypes list of secret types
525 * @return The primary key of the updated row, or -1 if failed.
526 *
527 * @hide
528 */
529 public long setRecoverySecretTypes(int userId, int uid, int[] secretTypes) {
530 SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
531 ContentValues values = new ContentValues();
532 StringJoiner joiner = new StringJoiner(",");
533 Arrays.stream(secretTypes).forEach(i -> joiner.add(Integer.toString(i)));
534 String typesAsCsv = joiner.toString();
535 values.put(RecoveryServiceMetadataEntry.COLUMN_NAME_SECRET_TYPES, typesAsCsv);
536 String selection =
537 RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID + " = ? AND "
538 + RecoveryServiceMetadataEntry.COLUMN_NAME_UID + " = ?";
539 ensureRecoveryServiceMetadataEntryExists(userId, uid);
540 return db.update(RecoveryServiceMetadataEntry.TABLE_NAME, values, selection,
541 new String[] {String.valueOf(userId), String.valueOf(uid)});
542 }
543
544 /**
545 * Returns the list of secret types used for end-to-end encryption.
546 *
Dmitry Dementyev77183ef2018-01-05 15:46:00 -0800547 * @param userId The userId of the profile the application is running under.
Dmitry Dementyevbdfdf532017-12-27 11:58:45 -0800548 * @param uid The uid of the application who initialized the local recovery components.
549 * @return Secret types or empty array, if types were not set.
550 *
551 * @hide
552 */
553 public @NonNull int[] getRecoverySecretTypes(int userId, int uid) {
554 SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase();
555
556 String[] projection = {
557 RecoveryServiceMetadataEntry._ID,
558 RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID,
559 RecoveryServiceMetadataEntry.COLUMN_NAME_UID,
560 RecoveryServiceMetadataEntry.COLUMN_NAME_SECRET_TYPES};
561 String selection =
562 RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID + " = ? AND "
563 + RecoveryServiceMetadataEntry.COLUMN_NAME_UID + " = ?";
564 String[] selectionArguments = {Integer.toString(userId), Integer.toString(uid)};
565
566 try (
567 Cursor cursor = db.query(
568 RecoveryServiceMetadataEntry.TABLE_NAME,
569 projection,
570 selection,
571 selectionArguments,
572 /*groupBy=*/ null,
573 /*having=*/ null,
574 /*orderBy=*/ null)
575 ) {
576 int count = cursor.getCount();
577 if (count == 0) {
578 return new int[]{};
579 }
580 if (count > 1) {
581 Log.wtf(TAG,
582 String.format(Locale.US,
583 "%d deviceId entries found for userId=%d uid=%d. "
584 + "Should only ever be 0 or 1.", count, userId, uid));
585 return new int[]{};
586 }
587 cursor.moveToFirst();
588 int idx = cursor.getColumnIndexOrThrow(
589 RecoveryServiceMetadataEntry.COLUMN_NAME_SECRET_TYPES);
590 if (cursor.isNull(idx)) {
591 return new int[]{};
592 }
593 String csv = cursor.getString(idx);
594 if (TextUtils.isEmpty(csv)) {
595 return new int[]{};
596 }
597 String[] types = csv.split(",");
Robert Berryaa3f4ca2017-12-27 10:53:58 +0000598 int[] result = new int[types.length];
Dmitry Dementyevbdfdf532017-12-27 11:58:45 -0800599 for (int i = 0; i < types.length; i++) {
600 try {
601 result[i] = Integer.parseInt(types[i]);
602 } catch (NumberFormatException e) {
603 Log.wtf(TAG, "String format error " + e);
604 }
605 }
606 return result;
607 }
608 }
609
610 /**
Dmitry Dementyev77183ef2018-01-05 15:46:00 -0800611 * Updates the counterId
612 *
613 * @param userId The userId of the profile the application is running under.
614 * @param uid The uid of the application.
615 * @param counterId The counterId.
616 * @return The primary key of the inserted row, or -1 if failed.
617 *
618 * @hide
619 */
620 public long setCounterId(int userId, int uid, long counterId) {
621 return setLong(userId, uid,
622 RecoveryServiceMetadataEntry.COLUMN_NAME_COUNTER_ID, counterId);
623 }
624
625 /**
626 * Returns the counter id.
627 *
628 * @param userId The userId of the profile the application is running under.
629 * @param uid The uid of the application who initialized the local recovery components.
630 * @return The counter id
631 *
632 * @hide
633 */
634 @Nullable
635 public Long getCounterId(int userId, int uid) {
636 return getLong(userId, uid, RecoveryServiceMetadataEntry.COLUMN_NAME_COUNTER_ID);
637 }
638
Dmitry Dementyev77183ef2018-01-05 15:46:00 -0800639 /**
Bo Zhu584b923f2017-12-22 16:05:15 -0800640 * Updates the server parameters given by the application initializing the local recovery
641 * components.
642 *
Dmitry Dementyev77183ef2018-01-05 15:46:00 -0800643 * @param userId The userId of the profile the application is running under.
Bo Zhu584b923f2017-12-22 16:05:15 -0800644 * @param uid The uid of the application.
Dmitry Dementyev7d8c78a2018-01-12 19:14:07 -0800645 * @param serverParams The server parameters.
Bo Zhu584b923f2017-12-22 16:05:15 -0800646 * @return The primary key of the inserted row, or -1 if failed.
647 *
648 * @hide
649 */
Dmitry Dementyev7d8c78a2018-01-12 19:14:07 -0800650 public long setServerParams(int userId, int uid, byte[] serverParams) {
651 return setBytes(userId, uid,
652 RecoveryServiceMetadataEntry.COLUMN_NAME_SERVER_PARAMS, serverParams);
Bo Zhu584b923f2017-12-22 16:05:15 -0800653 }
654
655 /**
656 * Returns the server paramters that was previously set by the application who initialized the
657 * local recovery service components.
658 *
Dmitry Dementyev77183ef2018-01-05 15:46:00 -0800659 * @param userId The userId of the profile the application is running under.
Bo Zhu584b923f2017-12-22 16:05:15 -0800660 * @param uid The uid of the application who initialized the local recovery components.
661 * @return The server parameters that were previously set, or null if there's none.
662 *
663 * @hide
664 */
Bo Zhu57e77f72018-01-03 14:49:43 -0800665 @Nullable
Dmitry Dementyev7d8c78a2018-01-12 19:14:07 -0800666 public byte[] getServerParams(int userId, int uid) {
667 return getBytes(userId, uid, RecoveryServiceMetadataEntry.COLUMN_NAME_SERVER_PARAMS);
Dmitry Dementyev77183ef2018-01-05 15:46:00 -0800668 }
669
670 /**
671 * Updates the snapshot version.
672 *
673 * @param userId The userId of the profile the application is running under.
674 * @param uid The uid of the application.
675 * @param snapshotVersion The snapshot version
676 * @return The primary key of the inserted row, or -1 if failed.
677 *
678 * @hide
679 */
680 public long setSnapshotVersion(int userId, int uid, long snapshotVersion) {
681 return setLong(userId, uid,
682 RecoveryServiceMetadataEntry.COLUMN_NAME_SNAPSHOT_VERSION, snapshotVersion);
683 }
684
685 /**
686 * Returns the snapshot version
687 *
688 * @param userId The userId of the profile the application is running under.
689 * @param uid The uid of the application who initialized the local recovery components.
690 * @return The server parameters that were previously set, or null if there's none.
691 *
692 * @hide
693 */
694 @Nullable
695 public Long getSnapshotVersion(int userId, int uid) {
696 return getLong(userId, uid,
697 RecoveryServiceMetadataEntry.COLUMN_NAME_SNAPSHOT_VERSION);
698 }
699
700 /**
701 * Updates the snapshot version.
702 *
703 * @param userId The userId of the profile the application is running under.
704 * @param uid The uid of the application.
705 * @param pending The server parameters.
706 * @return The primary key of the inserted row, or -1 if failed.
707 *
708 * @hide
709 */
710 public long setShouldCreateSnapshot(int userId, int uid, boolean pending) {
711 return setLong(userId, uid,
712 RecoveryServiceMetadataEntry.COLUMN_NAME_SHOULD_CREATE_SNAPSHOT, pending ? 1 : 0);
713 }
714
715 /**
716 * Returns {@code true} if new snapshot should be created.
717 * Returns {@code false} if the flag was never set.
718 *
719 * @param userId The userId of the profile the application is running under.
720 * @param uid The uid of the application who initialized the local recovery components.
721 * @return snapshot outdated flag.
722 *
723 * @hide
724 */
725 public boolean getShouldCreateSnapshot(int userId, int uid) {
726 Long res = getLong(userId, uid,
727 RecoveryServiceMetadataEntry.COLUMN_NAME_SHOULD_CREATE_SNAPSHOT);
728 return res != null && res != 0L;
729 }
730
Dmitry Dementyev7d8c78a2018-01-12 19:14:07 -0800731
Dmitry Dementyev77183ef2018-01-05 15:46:00 -0800732 /**
733 * Returns given long value from the database.
734 *
735 * @param userId The userId of the profile the application is running under.
736 * @param uid The uid of the application who initialized the local recovery components.
737 * @param key from {@code RecoveryServiceMetadataEntry}
738 * @return The value that were previously set, or null if there's none.
739 *
740 * @hide
741 */
742 private Long getLong(int userId, int uid, String key) {
Bo Zhu584b923f2017-12-22 16:05:15 -0800743 SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase();
744
745 String[] projection = {
746 RecoveryServiceMetadataEntry._ID,
747 RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID,
748 RecoveryServiceMetadataEntry.COLUMN_NAME_UID,
Dmitry Dementyev77183ef2018-01-05 15:46:00 -0800749 key};
Bo Zhu584b923f2017-12-22 16:05:15 -0800750 String selection =
751 RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID + " = ? AND "
752 + RecoveryServiceMetadataEntry.COLUMN_NAME_UID + " = ?";
753 String[] selectionArguments = {Integer.toString(userId), Integer.toString(uid)};
754
755 try (
Dmitry Dementyev77183ef2018-01-05 15:46:00 -0800756 Cursor cursor = db.query(
757 RecoveryServiceMetadataEntry.TABLE_NAME,
758 projection,
759 selection,
760 selectionArguments,
761 /*groupBy=*/ null,
762 /*having=*/ null,
763 /*orderBy=*/ null)
Bo Zhu584b923f2017-12-22 16:05:15 -0800764 ) {
765 int count = cursor.getCount();
766 if (count == 0) {
767 return null;
768 }
769 if (count > 1) {
770 Log.wtf(TAG,
771 String.format(Locale.US,
Dmitry Dementyev77183ef2018-01-05 15:46:00 -0800772 "%d entries found for userId=%d uid=%d. "
Bo Zhu584b923f2017-12-22 16:05:15 -0800773 + "Should only ever be 0 or 1.", count, userId, uid));
774 return null;
775 }
776 cursor.moveToFirst();
Dmitry Dementyev77183ef2018-01-05 15:46:00 -0800777 int idx = cursor.getColumnIndexOrThrow(key);
Bo Zhu584b923f2017-12-22 16:05:15 -0800778 if (cursor.isNull(idx)) {
779 return null;
780 } else {
781 return cursor.getLong(idx);
782 }
783 }
784 }
785
786 /**
Dmitry Dementyev77183ef2018-01-05 15:46:00 -0800787 * Sets a long value in the database.
788 *
789 * @param userId The userId of the profile the application is running under.
790 * @param uid The uid of the application who initialized the local recovery components.
791 * @param key defined in {@code RecoveryServiceMetadataEntry}
792 * @param value new value.
793 * @return The primary key of the inserted row, or -1 if failed.
794 *
795 * @hide
796 */
797
798 private long setLong(int userId, int uid, String key, long value) {
799 SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
800 ContentValues values = new ContentValues();
801 values.put(key, value);
802 String selection =
803 RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID + " = ? AND "
804 + RecoveryServiceMetadataEntry.COLUMN_NAME_UID + " = ?";
805 String[] selectionArguments = {Integer.toString(userId), Integer.toString(uid)};
806
807 ensureRecoveryServiceMetadataEntryExists(userId, uid);
808 return db.update(
809 RecoveryServiceMetadataEntry.TABLE_NAME, values, selection, selectionArguments);
810 }
811
812 /**
Dmitry Dementyev7d8c78a2018-01-12 19:14:07 -0800813 * Returns given binary value from the database.
814 *
815 * @param userId The userId of the profile the application is running under.
816 * @param uid The uid of the application who initialized the local recovery components.
817 * @param key from {@code RecoveryServiceMetadataEntry}
818 * @return The value that were previously set, or null if there's none.
819 *
820 * @hide
821 */
822 private byte[] getBytes(int userId, int uid, String key) {
823 SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase();
824
825 String[] projection = {
826 RecoveryServiceMetadataEntry._ID,
827 RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID,
828 RecoveryServiceMetadataEntry.COLUMN_NAME_UID,
829 key};
830 String selection =
831 RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID + " = ? AND "
832 + RecoveryServiceMetadataEntry.COLUMN_NAME_UID + " = ?";
833 String[] selectionArguments = {Integer.toString(userId), Integer.toString(uid)};
834
835 try (
836 Cursor cursor = db.query(
837 RecoveryServiceMetadataEntry.TABLE_NAME,
838 projection,
839 selection,
840 selectionArguments,
841 /*groupBy=*/ null,
842 /*having=*/ null,
843 /*orderBy=*/ null)
844 ) {
845 int count = cursor.getCount();
846 if (count == 0) {
847 return null;
848 }
849 if (count > 1) {
850 Log.wtf(TAG,
851 String.format(Locale.US,
852 "%d entries found for userId=%d uid=%d. "
853 + "Should only ever be 0 or 1.", count, userId, uid));
854 return null;
855 }
856 cursor.moveToFirst();
857 int idx = cursor.getColumnIndexOrThrow(key);
858 if (cursor.isNull(idx)) {
859 return null;
860 } else {
861 return cursor.getBlob(idx);
862 }
863 }
864 }
865
866 /**
867 * Sets a binary value in the database.
868 *
869 * @param userId The userId of the profile the application is running under.
870 * @param uid The uid of the application who initialized the local recovery components.
871 * @param key defined in {@code RecoveryServiceMetadataEntry}
872 * @param value new value.
873 * @return The primary key of the inserted row, or -1 if failed.
874 *
875 * @hide
876 */
877
878 private long setBytes(int userId, int uid, String key, byte[] value) {
879 SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
880 ContentValues values = new ContentValues();
881 values.put(key, value);
882 String selection =
883 RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID + " = ? AND "
884 + RecoveryServiceMetadataEntry.COLUMN_NAME_UID + " = ?";
885 String[] selectionArguments = {Integer.toString(userId), Integer.toString(uid)};
886
887 ensureRecoveryServiceMetadataEntryExists(userId, uid);
888 return db.update(
889 RecoveryServiceMetadataEntry.TABLE_NAME, values, selection, selectionArguments);
890 }
891
892 /**
Bo Zhu584b923f2017-12-22 16:05:15 -0800893 * Creates an empty row in the recovery service metadata table if such a row doesn't exist for
894 * the given userId and uid, so db.update will succeed.
895 */
896 private void ensureRecoveryServiceMetadataEntryExists(int userId, int uid) {
897 SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
898 ContentValues values = new ContentValues();
899 values.put(RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID, userId);
900 values.put(RecoveryServiceMetadataEntry.COLUMN_NAME_UID, uid);
901 db.insertWithOnConflict(RecoveryServiceMetadataEntry.TABLE_NAME, /*nullColumnHack=*/ null,
902 values, SQLiteDatabase.CONFLICT_IGNORE);
903 }
904
905 /**
Robert Berry76cf0832017-12-15 23:01:22 +0000906 * Closes all open connections to the database.
907 */
908 public void close() {
909 mKeyStoreDbHelper.close();
910 }
911
Robert Berryaa3f4ca2017-12-27 10:53:58 +0000912 @Nullable
913 private static PublicKey decodeX509Key(byte[] keyBytes) throws InvalidKeySpecException {
914 X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(keyBytes);
915 try {
916 return KeyFactory.getInstance("EC").generatePublic(publicKeySpec);
917 } catch (NoSuchAlgorithmException e) {
918 // Should never happen
919 throw new RuntimeException(e);
920 }
921 }
Bo Zhu14d993d2018-02-03 21:38:48 -0800922
923 @Nullable
924 private static CertPath decodeCertPath(byte[] bytes) throws CertificateException {
925 CertificateFactory certFactory;
926 try {
927 certFactory = CertificateFactory.getInstance("X.509");
928 } catch (CertificateException e) {
929 // Should not happen, as X.509 is mandatory for all providers.
930 throw new RuntimeException(e);
931 }
932 return certFactory.generateCertPath(new ByteArrayInputStream(bytes), CERT_PATH_ENCODING);
933 }
Robert Berry76cf0832017-12-15 23:01:22 +0000934}