content: /*
   * Copyright (C) 2017 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.android.server.locksettings.recoverablekeystore.storage;
  
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.content.ContentValues;
  import android.content.Context;
  import android.database.Cursor;
  import android.database.sqlite.SQLiteDatabase;
  import android.security.keystore.recovery.RecoveryController;
  import android.text.TextUtils;
  import android.util.ArrayMap;
  import android.util.Log;
  
  import com.android.server.locksettings.recoverablekeystore.TestOnlyInsecureCertificateHelper;
  import com.android.server.locksettings.recoverablekeystore.WrappedKey;
  import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDbContract.KeysEntry;
  import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDbContract.RecoveryServiceMetadataEntry;
  import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDbContract.RootOfTrustEntry;
  import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDbContract.UserMetadataEntry;
  
  import java.io.ByteArrayInputStream;
  import java.security.KeyFactory;
  import java.security.NoSuchAlgorithmException;
  import java.security.PublicKey;
  import java.security.cert.CertPath;
  import java.security.cert.CertificateEncodingException;
  import java.security.cert.CertificateException;
  import java.security.cert.CertificateFactory;
  import java.security.spec.InvalidKeySpecException;
  import java.security.spec.X509EncodedKeySpec;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Locale;
  import java.util.Map;
  import java.util.StringJoiner;
  
  /**
   * Database of recoverable key information.
   *
   * @hide
   */
  public class RecoverableKeyStoreDb {
      private static final String TAG = "RecoverableKeyStoreDb";
      private static final int IDLE_TIMEOUT_SECONDS = 30;
      private static final int LAST_SYNCED_AT_UNSYNCED = -1;
      private static final String CERT_PATH_ENCODING = "PkiPath";
  
      private final RecoverableKeyStoreDbHelper mKeyStoreDbHelper;
      private final TestOnlyInsecureCertificateHelper mTestOnlyInsecureCertificateHelper;
  
      /**
       * A new instance, storing the database in the user directory of {@code context}.
       *
       * @hide
       */
      public static RecoverableKeyStoreDb newInstance(Context context) {
          RecoverableKeyStoreDbHelper helper = new RecoverableKeyStoreDbHelper(context);
          helper.setWriteAheadLoggingEnabled(true);
          helper.setIdleConnectionTimeout(IDLE_TIMEOUT_SECONDS);
          return new RecoverableKeyStoreDb(helper);
      }
  
      private RecoverableKeyStoreDb(RecoverableKeyStoreDbHelper keyStoreDbHelper) {
          this.mKeyStoreDbHelper = keyStoreDbHelper;
          this.mTestOnlyInsecureCertificateHelper = new TestOnlyInsecureCertificateHelper();
      }
  
      /**
       * Inserts a key into the database.
       *
       * @param userId The uid of the profile the application is running under.
       * @param uid Uid of the application to whom the key belongs.
       * @param alias The alias of the key in the AndroidKeyStore.
       * @param wrappedKey The wrapped key.
       * @return The primary key of the inserted row, or -1 if failed.
       *
       * @hide
       */
      public long insertKey(int userId, int uid, String alias, WrappedKey wrappedKey) {
          SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
          ContentValues values = new ContentValues();
          values.put(KeysEntry.COLUMN_NAME_USER_ID, userId);
          values.put(KeysEntry.COLUMN_NAME_UID, uid);
          values.put(KeysEntry.COLUMN_NAME_ALIAS, alias);
          values.put(KeysEntry.COLUMN_NAME_NONCE, wrappedKey.getNonce());
          values.put(KeysEntry.COLUMN_NAME_WRAPPED_KEY, wrappedKey.getKeyMaterial());
          values.put(KeysEntry.COLUMN_NAME_LAST_SYNCED_AT, LAST_SYNCED_AT_UNSYNCED);
          values.put(KeysEntry.COLUMN_NAME_GENERATION_ID, wrappedKey.getPlatformKeyGenerationId());
          values.put(KeysEntry.COLUMN_NAME_RECOVERY_STATUS, wrappedKey.getRecoveryStatus());
          byte[] keyMetadata = wrappedKey.getKeyMetadata();
          if (keyMetadata == null) {
              values.putNull(KeysEntry.COLUMN_NAME_KEY_METADATA);
          } else {
              values.put(KeysEntry.COLUMN_NAME_KEY_METADATA, keyMetadata);
          }
          return db.replace(KeysEntry.TABLE_NAME, /*nullColumnHack=*/ null, values);
      }
  
      /**
       * Gets the key with {@code alias} for the app with {@code uid}.
       *
       * @hide
       */
      @Nullable public WrappedKey getKey(int uid, String alias) {
          SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase();
          String[] projection = {
                  KeysEntry._ID,
                  KeysEntry.COLUMN_NAME_NONCE,
                  KeysEntry.COLUMN_NAME_WRAPPED_KEY,
                  KeysEntry.COLUMN_NAME_GENERATION_ID,
                  KeysEntry.COLUMN_NAME_RECOVERY_STATUS,
                  KeysEntry.COLUMN_NAME_KEY_METADATA};
          String selection =
                  KeysEntry.COLUMN_NAME_UID + " = ? AND "
                  + KeysEntry.COLUMN_NAME_ALIAS + " = ?";
          String[] selectionArguments = { Integer.toString(uid), alias };
  
          try (
              Cursor cursor = db.query(
                  KeysEntry.TABLE_NAME,
                  projection,
                  selection,
                  selectionArguments,
                  /*groupBy=*/ null,
                  /*having=*/ null,
                  /*orderBy=*/ null)
          ) {
              int count = cursor.getCount();
              if (count == 0) {
                  return null;
              }
              if (count > 1) {
                  Log.wtf(TAG,
                          String.format(Locale.US,
                                  "%d WrappedKey entries found for uid=%d alias='%s'. "
                                          + "Should only ever be 0 or 1.", count, uid, alias));
                  return null;
              }
              cursor.moveToFirst();
              byte[] nonce = cursor.getBlob(
                      cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_NONCE));
              byte[] keyMaterial = cursor.getBlob(
                      cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_WRAPPED_KEY));
              int generationId = cursor.getInt(
                      cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_GENERATION_ID));
              int recoveryStatus = cursor.getInt(
                      cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_RECOVERY_STATUS));
  
              // Retrieve the metadata associated with the key
              byte[] keyMetadata;
              int metadataIdx = cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_KEY_METADATA);
              if (cursor.isNull(metadataIdx)) {
                  keyMetadata = null;
              } else {
                  keyMetadata = cursor.getBlob(metadataIdx);
              }
  
              return new WrappedKey(nonce, keyMaterial, keyMetadata, generationId, recoveryStatus);
          }
      }
  
      /**
       * Removes key with {@code alias} for app with {@code uid}.
       *
       * @return {@code true} if deleted a row.
       */
      public boolean removeKey(int uid, String alias) {
          SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
          String selection = KeysEntry.COLUMN_NAME_UID + " = ? AND " +
                  KeysEntry.COLUMN_NAME_ALIAS + " = ?";
          String[] selectionArgs = { Integer.toString(uid), alias };
          return db.delete(KeysEntry.TABLE_NAME, selection, selectionArgs) > 0;
      }
  
      /**
       * Returns all statuses for keys {@code uid} and {@code platformKeyGenerationId}.
       *
       * @param uid of the application
       *
       * @return Map from Aliases to status.
       *
       * @hide
       */
      public @NonNull Map<String, Integer> getStatusForAllKeys(int uid) {
          SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase();
          String[] projection = {
                  KeysEntry._ID,
                  KeysEntry.COLUMN_NAME_ALIAS,
                  KeysEntry.COLUMN_NAME_RECOVERY_STATUS};
          String selection =
                  KeysEntry.COLUMN_NAME_UID + " = ?";
          String[] selectionArguments = {Integer.toString(uid)};
  
          try (
              Cursor cursor = db.query(
                  KeysEntry.TABLE_NAME,
                  projection,
                  selection,
                  selectionArguments,
                  /*groupBy=*/ null,
                  /*having=*/ null,
                  /*orderBy=*/ null)
          ) {
              HashMap<String, Integer> statuses = new HashMap<>();
              while (cursor.moveToNext()) {
                  String alias = cursor.getString(
                          cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_ALIAS));
                  int recoveryStatus = cursor.getInt(
                          cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_RECOVERY_STATUS));
                  statuses.put(alias, recoveryStatus);
              }
              return statuses;
          }
      }
  
      /**
       * Updates status for given key.
       * @param uid of the application
       * @param alias of the key
       * @param status - new status
       * @return number of updated entries.
       * @hide
       **/
      public int setRecoveryStatus(int uid, String alias, int status) {
          SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
          ContentValues values = new ContentValues();
          values.put(KeysEntry.COLUMN_NAME_RECOVERY_STATUS, status);
          String selection =
                  KeysEntry.COLUMN_NAME_UID + " = ? AND "
                  + KeysEntry.COLUMN_NAME_ALIAS + " = ?";
          return db.update(KeysEntry.TABLE_NAME, values, selection,
              new String[] {String.valueOf(uid), alias});
      }
  
      /**
       * Returns all keys for the given {@code userId} {@code recoveryAgentUid}
       * and {@code platformKeyGenerationId}.
       *
       * @param userId User id of the profile to which all the keys are associated.
       * @param recoveryAgentUid Uid of the recovery agent which will perform the sync
       * @param platformKeyGenerationId The generation ID of the platform key that wrapped these keys.
       *     (i.e., this should be the most recent generation ID, as older platform keys are not
       *     usable.)
       *
       * @hide
       */
      public @NonNull Map<String, WrappedKey> getAllKeys(int userId, int recoveryAgentUid,
              int platformKeyGenerationId) {
          SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase();
          String[] projection = {
                  KeysEntry._ID,
                  KeysEntry.COLUMN_NAME_NONCE,
                  KeysEntry.COLUMN_NAME_WRAPPED_KEY,
                  KeysEntry.COLUMN_NAME_ALIAS,
                  KeysEntry.COLUMN_NAME_RECOVERY_STATUS,
                  KeysEntry.COLUMN_NAME_KEY_METADATA};
          String selection =
                  KeysEntry.COLUMN_NAME_USER_ID + " = ? AND "
                  + KeysEntry.COLUMN_NAME_UID + " = ? AND "
                  + KeysEntry.COLUMN_NAME_GENERATION_ID + " = ?";
          String[] selectionArguments = {
                  Integer.toString(userId),
                  Integer.toString(recoveryAgentUid),
                  Integer.toString(platformKeyGenerationId)
              };
  
          try (
              Cursor cursor = db.query(
                  KeysEntry.TABLE_NAME,
                  projection,
                  selection,
                  selectionArguments,
                  /*groupBy=*/ null,
                  /*having=*/ null,
                  /*orderBy=*/ null)
          ) {
              HashMap<String, WrappedKey> keys = new HashMap<>();
              while (cursor.moveToNext()) {
                  byte[] nonce = cursor.getBlob(
                          cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_NONCE));
                  byte[] keyMaterial = cursor.getBlob(
                          cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_WRAPPED_KEY));
                  String alias = cursor.getString(
                          cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_ALIAS));
                  int recoveryStatus = cursor.getInt(
                          cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_RECOVERY_STATUS));
  
                  // Retrieve the metadata associated with the key
                  byte[] keyMetadata;
                  int metadataIdx = cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_KEY_METADATA);
                  if (cursor.isNull(metadataIdx)) {
                      keyMetadata = null;
                  } else {
                      keyMetadata = cursor.getBlob(metadataIdx);
                  }
  
                  keys.put(alias, new WrappedKey(nonce, keyMaterial, keyMetadata,
                          platformKeyGenerationId, recoveryStatus));
              }
              return keys;
          }
      }
  
      /**
       * Sets the {@code generationId} of the platform key for user {@code userId}.
       *
       * @return The number of updated rows.
       */
      public long setPlatformKeyGenerationId(int userId, int generationId) {
          SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
          ContentValues values = new ContentValues();
          values.put(UserMetadataEntry.COLUMN_NAME_USER_ID, userId);
          values.put(UserMetadataEntry.COLUMN_NAME_PLATFORM_KEY_GENERATION_ID, generationId);
          String selection = UserMetadataEntry.COLUMN_NAME_USER_ID + " = ?";
          String[] selectionArguments = new String[] {String.valueOf(userId)};
  
          ensureUserMetadataEntryExists(userId);
          invalidateKeysForUser(userId);
          return db.update(UserMetadataEntry.TABLE_NAME, values, selection, selectionArguments);
      }
  
      /**
       * Returns serial numbers associated with all known users.
       * -1 is used for uninitialized serial numbers.
       *
       * See {@code UserHandle.getSerialNumberForUser}.
       * @return Map from userId to serial numbers.
       */
      public @NonNull Map<Integer, Long> getUserSerialNumbers() {
          SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase();
          String[] projection = {
                  UserMetadataEntry.COLUMN_NAME_USER_ID,
                  UserMetadataEntry.COLUMN_NAME_USER_SERIAL_NUMBER};
          String selection = null; // get all rows.
          String[] selectionArguments = {};
  
          try (
              Cursor cursor = db.query(
                  UserMetadataEntry.TABLE_NAME,
                  projection,
                  selection,
                  selectionArguments,
                  /*groupBy=*/ null,
                  /*having=*/ null,
                  /*orderBy=*/ null)
          ) {
              Map<Integer, Long> serialNumbers = new ArrayMap<>();
              while (cursor.moveToNext()) {
                  int userId = cursor.getInt(
                          cursor.getColumnIndexOrThrow(UserMetadataEntry.COLUMN_NAME_USER_ID));
                  long serialNumber = cursor.getLong(cursor.getColumnIndexOrThrow(
                          UserMetadataEntry.COLUMN_NAME_USER_SERIAL_NUMBER));
                  serialNumbers.put(userId, serialNumber);
              }
              return serialNumbers;
          }
      }
  
      /**
       * Sets the {@code serialNumber} for the user {@code userId}.
       *
       * @return The number of updated rows.
       */
      public long setUserSerialNumber(int userId, long serialNumber) {
          SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
          ContentValues values = new ContentValues();
          values.put(UserMetadataEntry.COLUMN_NAME_USER_ID, userId);
          values.put(UserMetadataEntry.COLUMN_NAME_USER_SERIAL_NUMBER, serialNumber);
          String selection = UserMetadataEntry.COLUMN_NAME_USER_ID + " = ?";
          String[] selectionArguments = new String[] {String.valueOf(userId)};
  
          ensureUserMetadataEntryExists(userId);
          return db.update(UserMetadataEntry.TABLE_NAME, values, selection, selectionArguments);
  
      }
  
      /**
       * Updates status of old keys to {@code RecoveryController.RECOVERY_STATUS_PERMANENT_FAILURE}.
       */
      public void invalidateKeysForUser(int userId) {
          SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
          ContentValues values = new ContentValues();
          values.put(KeysEntry.COLUMN_NAME_RECOVERY_STATUS,
                  RecoveryController.RECOVERY_STATUS_PERMANENT_FAILURE);
          String selection = KeysEntry.COLUMN_NAME_USER_ID + " = ?";
          db.update(KeysEntry.TABLE_NAME, values, selection, new String[] {String.valueOf(userId)});
      }
  
      /**
       * Updates status of old keys to {@code RecoveryController.RECOVERY_STATUS_PERMANENT_FAILURE}.
       */
      public void invalidateKeysForUserIdOnCustomScreenLock(int userId) {
          SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
          ContentValues values = new ContentValues();
          values.put(KeysEntry.COLUMN_NAME_RECOVERY_STATUS,
              RecoveryController.RECOVERY_STATUS_PERMANENT_FAILURE);
          String selection =
              KeysEntry.COLUMN_NAME_USER_ID + " = ?";
          db.update(KeysEntry.TABLE_NAME, values, selection,
              new String[] {String.valueOf(userId)});
      }
  
      /**
       * Returns the generation ID associated with the platform key of the user with {@code userId}.
       */
      public int getPlatformKeyGenerationId(int userId) {
          SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase();
          String[] projection = {
                  UserMetadataEntry.COLUMN_NAME_PLATFORM_KEY_GENERATION_ID};
          String selection =
                  UserMetadataEntry.COLUMN_NAME_USER_ID + " = ?";
          String[] selectionArguments = {
                  Integer.toString(userId)};
  
          try (
              Cursor cursor = db.query(
                  UserMetadataEntry.TABLE_NAME,
                  projection,
                  selection,
                  selectionArguments,
                  /*groupBy=*/ null,
                  /*having=*/ null,
                  /*orderBy=*/ null)
          ) {
              if (cursor.getCount() == 0) {
                  return -1;
              }
              cursor.moveToFirst();
              return cursor.getInt(
                      cursor.getColumnIndexOrThrow(
                              UserMetadataEntry.COLUMN_NAME_PLATFORM_KEY_GENERATION_ID));
          }
      }
  
      /**
       * Updates the public key of the recovery service into the database.
       *
       * @param userId The uid of the profile the application is running under.
       * @param uid The uid of the application to whom the key belongs.
       * @param publicKey The public key of the recovery service.
       * @return The primary key of the inserted row, or -1 if failed.
       *
       * @hide
       */
      public long setRecoveryServicePublicKey(int userId, int uid, PublicKey publicKey) {
          return setBytes(userId, uid, RecoveryServiceMetadataEntry.COLUMN_NAME_PUBLIC_KEY,
                  publicKey.getEncoded());
      }
  
      /**
       * Returns the serial number of the XML file containing certificates of the recovery service.
       *
       * @param userId The userId of the profile the application is running under.
       * @param uid The uid of the application who initializes the local recovery components.
       * @param rootAlias The root of trust alias.
       * @return The value that were previously set, or null if there's none.
       *
       * @hide
       */
      @Nullable
      public Long getRecoveryServiceCertSerial(int userId, int uid, @NonNull String rootAlias) {
          return getLong(userId, uid, rootAlias, RootOfTrustEntry.COLUMN_NAME_CERT_SERIAL);
      }
  
      /**
       * Records the serial number of the XML file containing certificates of the recovery service.
       *
       * @param userId The userId of the profile the application is running under.
       * @param uid The uid of the application who initializes the local recovery components.
       * @param rootAlias The root of trust alias.
       * @param serial The serial number contained in the XML file for recovery service certificates.
       * @return The primary key of the inserted row, or -1 if failed.
       *
       * @hide
       */
      public long setRecoveryServiceCertSerial(int userId, int uid, @NonNull String rootAlias,
              long serial) {
          return setLong(userId, uid, rootAlias, RootOfTrustEntry.COLUMN_NAME_CERT_SERIAL,
                  serial);
      }
  
      /**
       * Returns the {@code CertPath} of the recovery service.
       *
       * @param userId The userId of the profile the application is running under.
       * @param uid The uid of the application who initializes the local recovery components.
       * @param rootAlias The root of trust alias.
       * @return The value that were previously set, or null if there's none.
       *
       * @hide
       */
      @Nullable
      public CertPath getRecoveryServiceCertPath(int userId, int uid, @NonNull String rootAlias) {
          byte[] bytes = getBytes(userId, uid, rootAlias, RootOfTrustEntry.COLUMN_NAME_CERT_PATH);
          if (bytes == null) {
              return null;
          }
          try {
              return decodeCertPath(bytes);
          } catch (CertificateException e) {
              Log.wtf(TAG,
                      String.format(Locale.US,
                              "Recovery service CertPath entry cannot be decoded for "
                                      + "userId=%d uid=%d.",
                              userId, uid), e);
              return null;
          }
      }
  
      /**
       * Sets the {@code CertPath} of the recovery service.
       *
       * @param userId The userId of the profile the application is running under.
       * @param uid The uid of the application who initializes the local recovery components.
       * @param rootAlias The root of trust alias.
       * @param certPath The certificate path of the recovery service.
       * @return The primary key of the inserted row, or -1 if failed.
       * @hide
       */
      public long setRecoveryServiceCertPath(int userId, int uid, @NonNull String rootAlias,
              CertPath certPath) throws CertificateEncodingException {
          if (certPath.getCertificates().size() == 0) {
              throw new CertificateEncodingException("No certificate contained in the cert path.");
          }
          return setBytes(userId, uid, rootAlias, RootOfTrustEntry.COLUMN_NAME_CERT_PATH,
                  certPath.getEncoded(CERT_PATH_ENCODING));
      }
  
      /**
       * Returns the list of recovery agents initialized for given {@code userId}
       * @param userId The userId of the profile the application is running under.
       * @return The list of recovery agents
       * @hide
       */
      public @NonNull List<Integer> getRecoveryAgents(int userId) {
          SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase();
  
          String[] projection = { RecoveryServiceMetadataEntry.COLUMN_NAME_UID };
          String selection = RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID + " = ?";
          String[] selectionArguments = { Integer.toString(userId) };
  
          try (
              Cursor cursor = db.query(
                      RecoveryServiceMetadataEntry.TABLE_NAME,
                      projection,
                      selection,
                      selectionArguments,
                      /*groupBy=*/ null,
                      /*having=*/ null,
                      /*orderBy=*/ null)
          ) {
              int count = cursor.getCount();
              ArrayList<Integer> result = new ArrayList<>(count);
              while (cursor.moveToNext()) {
                  int uid = cursor.getInt(
                          cursor.getColumnIndexOrThrow(RecoveryServiceMetadataEntry.COLUMN_NAME_UID));
                  result.add(uid);
              }
              return result;
          }
      }
  
      /**
       * Returns the public key of the recovery service.
       *
       * @param userId The userId of the profile the application is running under.
       * @param uid The uid of the application who initializes the local recovery components.
       *
       * @hide
       */
      @Nullable
      public PublicKey getRecoveryServicePublicKey(int userId, int uid) {
          byte[] keyBytes =
                  getBytes(userId, uid, RecoveryServiceMetadataEntry.COLUMN_NAME_PUBLIC_KEY);
          if (keyBytes == null) {
              return null;
          }
          try {
              return decodeX509Key(keyBytes);
          } catch (InvalidKeySpecException e) {
              Log.wtf(TAG,
                      String.format(Locale.US,
                              "Recovery service public key entry cannot be decoded for "
                                      + "userId=%d uid=%d.",
                              userId, uid));
              return null;
          }
      }
  
      /**
       * Updates the list of user secret types used for end-to-end encryption.
       * If no secret types are set, recovery snapshot will not be created.
       * See {@code KeyChainProtectionParams}
       *
       * @param userId The userId of the profile the application is running under.
       * @param uid The uid of the application.
       * @param secretTypes list of secret types
       * @return The primary key of the updated row, or -1 if failed.
       *
       * @hide
       */
      public long setRecoverySecretTypes(int userId, int uid, int[] secretTypes) {
          SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
          ContentValues values = new ContentValues();
          StringJoiner joiner = new StringJoiner(",");
          Arrays.stream(secretTypes).forEach(i -> joiner.add(Integer.toString(i)));
          String typesAsCsv = joiner.toString();
          values.put(RecoveryServiceMetadataEntry.COLUMN_NAME_SECRET_TYPES, typesAsCsv);
          String selection =
                  RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID + " = ? AND "
                  + RecoveryServiceMetadataEntry.COLUMN_NAME_UID + " = ?";
          ensureRecoveryServiceMetadataEntryExists(userId, uid);
          return db.update(RecoveryServiceMetadataEntry.TABLE_NAME, values, selection,
              new String[] {String.valueOf(userId), String.valueOf(uid)});
      }
  
      /**
       * Returns the list of secret types used for end-to-end encryption.
       *
       * @param userId The userId of the profile the application is running under.
       * @param uid The uid of the application who initialized the local recovery components.
       * @return Secret types or empty array, if types were not set.
       *
       * @hide
       */
      public @NonNull int[] getRecoverySecretTypes(int userId, int uid) {
          SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase();
  
          String[] projection = {
                  RecoveryServiceMetadataEntry._ID,
                  RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID,
                  RecoveryServiceMetadataEntry.COLUMN_NAME_UID,
                  RecoveryServiceMetadataEntry.COLUMN_NAME_SECRET_TYPES};
          String selection =
                  RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID + " = ? AND "
                          + RecoveryServiceMetadataEntry.COLUMN_NAME_UID + " = ?";
          String[] selectionArguments = {Integer.toString(userId), Integer.toString(uid)};
  
          try (
                  Cursor cursor = db.query(
                          RecoveryServiceMetadataEntry.TABLE_NAME,
                          projection,
                          selection,
                          selectionArguments,
                          /*groupBy=*/ null,
                          /*having=*/ null,
                          /*orderBy=*/ null)
          ) {
              int count = cursor.getCount();
              if (count == 0) {
                  return new int[]{};
              }
              if (count > 1) {
                  Log.wtf(TAG,
                          String.format(Locale.US,
                                  "%d deviceId entries found for userId=%d uid=%d. "
                                          + "Should only ever be 0 or 1.", count, userId, uid));
                  return new int[]{};
              }
              cursor.moveToFirst();
              int idx = cursor.getColumnIndexOrThrow(
                      RecoveryServiceMetadataEntry.COLUMN_NAME_SECRET_TYPES);
              if (cursor.isNull(idx)) {
                  return new int[]{};
              }
              String csv = cursor.getString(idx);
              if (TextUtils.isEmpty(csv)) {
                  return new int[]{};
              }
              String[] types = csv.split(",");
              int[] result = new int[types.length];
              for (int i = 0; i < types.length; i++) {
                  try {
                      result[i] = Integer.parseInt(types[i]);
                  } catch (NumberFormatException e) {
                      Log.wtf(TAG, "String format error " + e);
                  }
              }
              return result;
          }
      }
  
      /**
       * Active root of trust for the recovery agent.
       *
       * @param userId The userId of the profile the application is running under.
       * @param uid The uid of the application.
       * @param rootAlias The root of trust alias.
       * @return The primary key of the updated row, or -1 if failed.
       *
       * @hide
       */
      public long setActiveRootOfTrust(int userId, int uid, @Nullable String rootAlias) {
          SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
          ContentValues values = new ContentValues();
          values.put(RecoveryServiceMetadataEntry.COLUMN_NAME_ACTIVE_ROOT_OF_TRUST, rootAlias);
          String selection =
                  RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID + " = ? AND "
                  + RecoveryServiceMetadataEntry.COLUMN_NAME_UID + " = ?";
          ensureRecoveryServiceMetadataEntryExists(userId, uid);
          return db.update(RecoveryServiceMetadataEntry.TABLE_NAME, values,
              selection, new String[] {String.valueOf(userId), String.valueOf(uid)});
      }
  
      /**
       * Active root of trust for the recovery agent.
       *
       * @param userId The userId of the profile the application is running under.
       * @param uid The uid of the application who initialized the local recovery components.
       * @return Active root of trust alias of null if it was not set
       *
       * @hide
       */
      public @Nullable String getActiveRootOfTrust(int userId, int uid) {
          SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase();
  
          String[] projection = {
                  RecoveryServiceMetadataEntry._ID,
                  RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID,
                  RecoveryServiceMetadataEntry.COLUMN_NAME_UID,
                  RecoveryServiceMetadataEntry.COLUMN_NAME_ACTIVE_ROOT_OF_TRUST};
          String selection =
                  RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID + " = ? AND "
                          + RecoveryServiceMetadataEntry.COLUMN_NAME_UID + " = ?";
          String[] selectionArguments = {Integer.toString(userId), Integer.toString(uid)};
  
          try (
                  Cursor cursor = db.query(
                          RecoveryServiceMetadataEntry.TABLE_NAME,
                          projection,
                          selection,
                          selectionArguments,
                          /*groupBy=*/ null,
                          /*having=*/ null,
                          /*orderBy=*/ null)
          ) {
              int count = cursor.getCount();
              if (count == 0) {
                  return null;
              }
              if (count > 1) {
                  Log.wtf(TAG,
                          String.format(Locale.US,
                                  "%d deviceId entries found for userId=%d uid=%d. "
                                          + "Should only ever be 0 or 1.", count, userId, uid));
                  return null;
              }
              cursor.moveToFirst();
              int idx = cursor.getColumnIndexOrThrow(
                      RecoveryServiceMetadataEntry.COLUMN_NAME_ACTIVE_ROOT_OF_TRUST);
              if (cursor.isNull(idx)) {
                  return null;
              }
              String result = cursor.getString(idx);
              if (TextUtils.isEmpty(result)) {
                  return null;
              }
              return result;
          }
      }
  
      /**
       * Updates the counterId
       *
       * @param userId The userId of the profile the application is running under.
       * @param uid The uid of the application.
       * @param counterId The counterId.
       * @return The primary key of the inserted row, or -1 if failed.
       *
       * @hide
       */
      public long setCounterId(int userId, int uid, long counterId) {
          return setLong(userId, uid,
                  RecoveryServiceMetadataEntry.COLUMN_NAME_COUNTER_ID, counterId);
      }
  
      /**
       * Returns the counter id.
       *
       * @param userId The userId of the profile the application is running under.
       * @param uid The uid of the application who initialized the local recovery components.
       * @return The counter id
       *
       * @hide
       */
      @Nullable
      public Long getCounterId(int userId, int uid) {
          return getLong(userId, uid, RecoveryServiceMetadataEntry.COLUMN_NAME_COUNTER_ID);
      }
  
      /**
       * Updates the server parameters given by the application initializing the local recovery
       * components.
       *
       * @param userId The userId of the profile the application is running under.
       * @param uid The uid of the application.
       * @param serverParams The server parameters.
       * @return The primary key of the inserted row, or -1 if failed.
       *
       * @hide
       */
      public long setServerParams(int userId, int uid, byte[] serverParams) {
          return setBytes(userId, uid,
                  RecoveryServiceMetadataEntry.COLUMN_NAME_SERVER_PARAMS, serverParams);
      }
  
      /**
       * Returns the server paramters that was previously set by the application who initialized the
       * local recovery service components.
       *
       * @param userId The userId of the profile the application is running under.
       * @param uid The uid of the application who initialized the local recovery components.
       * @return The server parameters that were previously set, or null if there's none.
       *
       * @hide
       */
      @Nullable
      public byte[] getServerParams(int userId, int uid) {
          return getBytes(userId, uid, RecoveryServiceMetadataEntry.COLUMN_NAME_SERVER_PARAMS);
      }
  
      /**
       * Updates the snapshot version.
       *
       * @param userId The userId of the profile the application is running under.
       * @param uid The uid of the application.
       * @param snapshotVersion The snapshot version
       * @return The primary key of the inserted row, or -1 if failed.
       *
       * @hide
       */
      public long setSnapshotVersion(int userId, int uid, long snapshotVersion) {
          return setLong(userId, uid,
                  RecoveryServiceMetadataEntry.COLUMN_NAME_SNAPSHOT_VERSION, snapshotVersion);
      }
  
      /**
       * Returns the snapshot version
       *
       * @param userId The userId of the profile the application is running under.
       * @param uid The uid of the application who initialized the local recovery components.
       * @return The server parameters that were previously set, or null if there's none.
       *
       * @hide
       */
      @Nullable
      public Long getSnapshotVersion(int userId, int uid) {
          return getLong(userId, uid,
              RecoveryServiceMetadataEntry.COLUMN_NAME_SNAPSHOT_VERSION);
      }
  
      /**
       * Updates a flag indicating that a new snapshot should be created.
       * It will be {@code false} until the first application key is added.
       * After that, the flag will be set to true, if one of the following values is updated:
       * <ul>
       *     <li> List of application keys
       *     <li> Server params.
       *     <li> Lock-screen secret.
       *     <li> Lock-screen secret type.
       *     <li> Trusted hardware certificate.
       * </ul>
       *
       * @param userId The userId of the profile the application is running under.
       * @param uid The uid of the application.
       * @param pending Should create snapshot flag.
       * @return The primary key of the inserted row, or -1 if failed.
       *
       * @hide
       */
      public long setShouldCreateSnapshot(int userId, int uid, boolean pending) {
          return setLong(userId, uid,
                  RecoveryServiceMetadataEntry.COLUMN_NAME_SHOULD_CREATE_SNAPSHOT, pending ? 1 : 0);
      }
  
      /**
       * Returns {@code true} if new snapshot should be created.
       * Returns {@code false} if the flag was never set.
       *
       * @param userId The userId of the profile the application is running under.
       * @param uid The uid of the application who initialized the local recovery components.
       * @return should create snapshot flag
       *
       * @hide
       */
      public boolean getShouldCreateSnapshot(int userId, int uid) {
          Long res = getLong(userId, uid,
                  RecoveryServiceMetadataEntry.COLUMN_NAME_SHOULD_CREATE_SNAPSHOT);
          return res != null && res != 0L;
      }
  
  
      /**
       * Returns given long value from the database.
       *
       * @param userId The userId of the profile the application is running under.
       * @param uid The uid of the application who initialized the local recovery components.
       * @param key from {@code RecoveryServiceMetadataEntry}
       * @return The value that were previously set, or null if there's none.
       *
       * @hide
       */
      private Long getLong(int userId, int uid, String key) {
          SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase();
  
          String[] projection = {
                  RecoveryServiceMetadataEntry._ID,
                  RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID,
                  RecoveryServiceMetadataEntry.COLUMN_NAME_UID,
                  key};
          String selection =
                  RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID + " = ? AND "
                          + RecoveryServiceMetadataEntry.COLUMN_NAME_UID + " = ?";
          String[] selectionArguments = {Integer.toString(userId), Integer.toString(uid)};
  
          try (
              Cursor cursor = db.query(
                      RecoveryServiceMetadataEntry.TABLE_NAME,
                      projection,
                      selection,
                      selectionArguments,
                      /*groupBy=*/ null,
                      /*having=*/ null,
                      /*orderBy=*/ null)
          ) {
              int count = cursor.getCount();
              if (count == 0) {
                  return null;
              }
              if (count > 1) {
                  Log.wtf(TAG,
                          String.format(Locale.US,
                                  "%d entries found for userId=%d uid=%d. "
                                          + "Should only ever be 0 or 1.", count, userId, uid));
                  return null;
              }
              cursor.moveToFirst();
              int idx = cursor.getColumnIndexOrThrow(key);
              if (cursor.isNull(idx)) {
                  return null;
              } else {
                  return cursor.getLong(idx);
              }
          }
      }
  
      /**
       * Sets a long value in the database.
       *
       * @param userId The userId of the profile the application is running under.
       * @param uid The uid of the application who initialized the local recovery components.
       * @param key defined in {@code RecoveryServiceMetadataEntry}
       * @param value new value.
       * @return The primary key of the inserted row, or -1 if failed.
       *
       * @hide
       */
  
      private long setLong(int userId, int uid, String key, long value) {
          SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
          ContentValues values = new ContentValues();
          values.put(key, value);
          String selection =
                  RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID + " = ? AND "
                          + RecoveryServiceMetadataEntry.COLUMN_NAME_UID + " = ?";
          String[] selectionArguments = {Integer.toString(userId), Integer.toString(uid)};
  
          ensureRecoveryServiceMetadataEntryExists(userId, uid);
          return db.update(
                  RecoveryServiceMetadataEntry.TABLE_NAME, values, selection, selectionArguments);
      }
  
      /**
       * Returns given binary value from the database.
       *
       * @param userId The userId of the profile the application is running under.
       * @param uid The uid of the application who initialized the local recovery components.
       * @param key from {@code RecoveryServiceMetadataEntry}
       * @return The value that were previously set, or null if there's none.
       *
       * @hide
       */
      private byte[] getBytes(int userId, int uid, String key) {
          SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase();
  
          String[] projection = {
                  RecoveryServiceMetadataEntry._ID,
                  RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID,
                  RecoveryServiceMetadataEntry.COLUMN_NAME_UID,
                  key};
          String selection =
                  RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID + " = ? AND "
                          + RecoveryServiceMetadataEntry.COLUMN_NAME_UID + " = ?";
          String[] selectionArguments = {Integer.toString(userId), Integer.toString(uid)};
  
          try (
              Cursor cursor = db.query(
                      RecoveryServiceMetadataEntry.TABLE_NAME,
                      projection,
                      selection,
                      selectionArguments,
                      /*groupBy=*/ null,
                      /*having=*/ null,
                      /*orderBy=*/ null)
          ) {
              int count = cursor.getCount();
              if (count == 0) {
                  return null;
              }
              if (count > 1) {
                  Log.wtf(TAG,
                          String.format(Locale.US,
                                  "%d entries found for userId=%d uid=%d. "
                                          + "Should only ever be 0 or 1.", count, userId, uid));
                  return null;
              }
              cursor.moveToFirst();
              int idx = cursor.getColumnIndexOrThrow(key);
              if (cursor.isNull(idx)) {
                  return null;
              } else {
                  return cursor.getBlob(idx);
              }
          }
      }
  
      /**
       * Sets a binary value in the database.
       *
       * @param userId The userId of the profile the application is running under.
       * @param uid The uid of the application who initialized the local recovery components.
       * @param key defined in {@code RecoveryServiceMetadataEntry}
       * @param value new value.
       * @return The primary key of the inserted row, or -1 if failed.
       *
       * @hide
       */
      private long setBytes(int userId, int uid, String key, byte[] value) {
          SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
          ContentValues values = new ContentValues();
          values.put(key, value);
          String selection =
                  RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID + " = ? AND "
                          + RecoveryServiceMetadataEntry.COLUMN_NAME_UID + " = ?";
          String[] selectionArguments = {Integer.toString(userId), Integer.toString(uid)};
  
          ensureRecoveryServiceMetadataEntryExists(userId, uid);
          return db.update(
                  RecoveryServiceMetadataEntry.TABLE_NAME, values, selection, selectionArguments);
      }
  
      /**
       * Returns given binary value from the database.
       *
       * @param userId The userId of the profile the application is running under.
       * @param uid The uid of the application who initialized the local recovery components.
       * @param rootAlias The root of trust alias.
       * @param key from {@code RootOfTrustEntry}
       * @return The value that were previously set, or null if there's none.
       *
       * @hide
       */
      private byte[] getBytes(int userId, int uid, String rootAlias, String key) {
          rootAlias = mTestOnlyInsecureCertificateHelper.getDefaultCertificateAliasIfEmpty(rootAlias);
          SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase();
  
          String[] projection = {
                  RootOfTrustEntry._ID,
                  RootOfTrustEntry.COLUMN_NAME_USER_ID,
                  RootOfTrustEntry.COLUMN_NAME_UID,
                  RootOfTrustEntry.COLUMN_NAME_ROOT_ALIAS,
                  key};
          String selection =
                  RootOfTrustEntry.COLUMN_NAME_USER_ID + " = ? AND "
                          + RootOfTrustEntry.COLUMN_NAME_UID + " = ? AND "
                          + RootOfTrustEntry.COLUMN_NAME_ROOT_ALIAS + " = ?";
          String[] selectionArguments = {Integer.toString(userId), Integer.toString(uid), rootAlias};
  
          try (
              Cursor cursor = db.query(
                      RootOfTrustEntry.TABLE_NAME,
                      projection,
                      selection,
                      selectionArguments,
                      /*groupBy=*/ null,
                      /*having=*/ null,
                      /*orderBy=*/ null)
          ) {
              int count = cursor.getCount();
              if (count == 0) {
                  return null;
              }
              if (count > 1) {
                  Log.wtf(TAG,
                          String.format(Locale.US,
                                  "%d entries found for userId=%d uid=%d. "
                                          + "Should only ever be 0 or 1.", count, userId, uid));
                  return null;
              }
              cursor.moveToFirst();
              int idx = cursor.getColumnIndexOrThrow(key);
              if (cursor.isNull(idx)) {
                  return null;
              } else {
                  return cursor.getBlob(idx);
              }
          }
      }
  
      /**
       * Sets a binary value in the database.
       *
       * @param userId The userId of the profile the application is running under.
       * @param uid The uid of the application who initialized the local recovery components.
       * @param rootAlias The root of trust alias.
       * @param key defined in {@code RootOfTrustEntry}
       * @param value new value.
       * @return The primary key of the inserted row, or -1 if failed.
       *
       * @hide
       */
      private long setBytes(int userId, int uid, String rootAlias, String key, byte[] value) {
          rootAlias = mTestOnlyInsecureCertificateHelper.getDefaultCertificateAliasIfEmpty(rootAlias);
          SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
          ContentValues values = new ContentValues();
          values.put(key, value);
          String selection =
                  RootOfTrustEntry.COLUMN_NAME_USER_ID + " = ? AND "
                          + RootOfTrustEntry.COLUMN_NAME_UID + " = ? AND "
                          + RootOfTrustEntry.COLUMN_NAME_ROOT_ALIAS + " = ?";
          String[] selectionArguments = {Integer.toString(userId), Integer.toString(uid), rootAlias};
  
          ensureRootOfTrustEntryExists(userId, uid, rootAlias);
          return db.update(
                  RootOfTrustEntry.TABLE_NAME, values, selection, selectionArguments);
      }
  
      /**
       * Returns given long value from the database.
       *
       * @param userId The userId of the profile the application is running under.
       * @param uid The uid of the application who initialized the local recovery components.
       * @param rootAlias The root of trust alias.
       * @param key from {@code RootOfTrustEntry}
       * @return The value that were previously set, or null if there's none.
       *
       * @hide
       */
      private Long getLong(int userId, int uid, String rootAlias, String key) {
          rootAlias = mTestOnlyInsecureCertificateHelper.getDefaultCertificateAliasIfEmpty(rootAlias);
          SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase();
  
          String[] projection = {
                  RootOfTrustEntry._ID,
                  RootOfTrustEntry.COLUMN_NAME_USER_ID,
                  RootOfTrustEntry.COLUMN_NAME_UID,
                  RootOfTrustEntry.COLUMN_NAME_ROOT_ALIAS,
                  key};
          String selection =
                  RootOfTrustEntry.COLUMN_NAME_USER_ID + " = ? AND "
                          + RootOfTrustEntry.COLUMN_NAME_UID + " = ? AND "
                          + RootOfTrustEntry.COLUMN_NAME_ROOT_ALIAS + " = ?";
          String[] selectionArguments = {Integer.toString(userId), Integer.toString(uid), rootAlias};
  
          try (
              Cursor cursor = db.query(
                      RootOfTrustEntry.TABLE_NAME,
                      projection,
                      selection,
                      selectionArguments,
                      /*groupBy=*/ null,
                      /*having=*/ null,
                      /*orderBy=*/ null)
          ) {
              int count = cursor.getCount();
              if (count == 0) {
                  return null;
              }
              if (count > 1) {
                  Log.wtf(TAG,
                          String.format(Locale.US,
                                  "%d entries found for userId=%d uid=%d. "
                                          + "Should only ever be 0 or 1.", count, userId, uid));
                  return null;
              }
              cursor.moveToFirst();
              int idx = cursor.getColumnIndexOrThrow(key);
              if (cursor.isNull(idx)) {
                  return null;
              } else {
                  return cursor.getLong(idx);
              }
          }
      }
  
      /**
       * Sets a long value in the database.
       *
       * @param userId The userId of the profile the application is running under.
       * @param uid The uid of the application who initialized the local recovery components.
       * @param rootAlias The root of trust alias.
       * @param key defined in {@code RootOfTrustEntry}
       * @param value new value.
       * @return The primary key of the inserted row, or -1 if failed.
       *
       * @hide
       */
  
      private long setLong(int userId, int uid, String rootAlias, String key, long value) {
          rootAlias = mTestOnlyInsecureCertificateHelper.getDefaultCertificateAliasIfEmpty(rootAlias);
          SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
          ContentValues values = new ContentValues();
          values.put(key, value);
          String selection =
                  RootOfTrustEntry.COLUMN_NAME_USER_ID + " = ? AND "
                          + RootOfTrustEntry.COLUMN_NAME_UID + " = ? AND "
                          + RootOfTrustEntry.COLUMN_NAME_ROOT_ALIAS + " = ?";
          String[] selectionArguments = {Integer.toString(userId), Integer.toString(uid), rootAlias};
  
          ensureRootOfTrustEntryExists(userId, uid, rootAlias);
          return db.update(
                  RootOfTrustEntry.TABLE_NAME, values, selection, selectionArguments);
      }
  
      /**
       * Removes all entries for given {@code userId}.
       */
      public void removeUserFromAllTables(int userId) {
          removeUserFromKeysTable(userId);
          removeUserFromUserMetadataTable(userId);
          removeUserFromRecoveryServiceMetadataTable(userId);
          removeUserFromRootOfTrustTable(userId);
      }
  
      /**
       * Removes all entries for given userId from Keys table.
       *
       * @return {@code true} if deleted a row.
       */
      private boolean removeUserFromKeysTable(int userId) {
          SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
          String selection = KeysEntry.COLUMN_NAME_USER_ID + " = ?";
          String[] selectionArgs = {Integer.toString(userId)};
          return db.delete(KeysEntry.TABLE_NAME, selection, selectionArgs) > 0;
      }
  
      /**
       * Removes all entries for given userId from UserMetadata table.
       *
       * @return {@code true} if deleted a row.
       */
      private boolean removeUserFromUserMetadataTable(int userId) {
          SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
          String selection = UserMetadataEntry.COLUMN_NAME_USER_ID + " = ?";
          String[] selectionArgs = {Integer.toString(userId)};
          return db.delete(UserMetadataEntry.TABLE_NAME, selection, selectionArgs) > 0;
      }
  
      /**
       * Removes all entries for given userId from RecoveryServiceMetadata table.
       *
       * @return {@code true} if deleted a row.
       */
      private boolean removeUserFromRecoveryServiceMetadataTable(int userId) {
          SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
          String selection = RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID + " = ?";
          String[] selectionArgs = {Integer.toString(userId)};
          return db.delete(RecoveryServiceMetadataEntry.TABLE_NAME, selection, selectionArgs) > 0;
      }
  
      /**
       * Removes all entries for given userId from RootOfTrust table.
       *
       * @return {@code true} if deleted a row.
       */
      private boolean removeUserFromRootOfTrustTable(int userId) {
          SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
          String selection = RootOfTrustEntry.COLUMN_NAME_USER_ID + " = ?";
          String[] selectionArgs = {Integer.toString(userId)};
          return db.delete(RootOfTrustEntry.TABLE_NAME, selection, selectionArgs) > 0;
      }
  
      /**
       * Creates an empty row in the recovery service metadata table if such a row doesn't exist for
       * the given userId and uid, so db.update will succeed.
       */
      private void ensureRecoveryServiceMetadataEntryExists(int userId, int uid) {
          SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
          ContentValues values = new ContentValues();
          values.put(RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID, userId);
          values.put(RecoveryServiceMetadataEntry.COLUMN_NAME_UID, uid);
          db.insertWithOnConflict(RecoveryServiceMetadataEntry.TABLE_NAME, /*nullColumnHack=*/ null,
                  values, SQLiteDatabase.CONFLICT_IGNORE);
      }
  
      /**
       * Creates an empty row in the root of trust table if such a row doesn't exist for
       * the given userId and uid, so db.update will succeed.
       */
      private void ensureRootOfTrustEntryExists(int userId, int uid, String rootAlias) {
          SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
          ContentValues values = new ContentValues();
          values.put(RootOfTrustEntry.COLUMN_NAME_USER_ID, userId);
          values.put(RootOfTrustEntry.COLUMN_NAME_UID, uid);
          values.put(RootOfTrustEntry.COLUMN_NAME_ROOT_ALIAS, rootAlias);
          db.insertWithOnConflict(RootOfTrustEntry.TABLE_NAME, /*nullColumnHack=*/ null,
                  values, SQLiteDatabase.CONFLICT_IGNORE);
      }
  
      /**
       * Creates an empty row in the user metadata table if such a row doesn't exist for
       * the given userId, so db.update will succeed.
       */
      private void ensureUserMetadataEntryExists(int userId) {
          SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
          ContentValues values = new ContentValues();
          values.put(UserMetadataEntry.COLUMN_NAME_USER_ID, userId);
          db.insertWithOnConflict(UserMetadataEntry.TABLE_NAME, /*nullColumnHack=*/ null,
                  values, SQLiteDatabase.CONFLICT_IGNORE);
      }
  
      /**
       * Closes all open connections to the database.
       */
      public void close() {
          mKeyStoreDbHelper.close();
      }
  
      @Nullable
      private static PublicKey decodeX509Key(byte[] keyBytes) throws InvalidKeySpecException {
          X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(keyBytes);
          try {
              return KeyFactory.getInstance("EC").generatePublic(publicKeySpec);
          } catch (NoSuchAlgorithmException e) {
              // Should never happen
              throw new RuntimeException(e);
          }
      }
  
      @Nullable
      private static CertPath decodeCertPath(byte[] bytes) throws CertificateException {
          CertificateFactory certFactory;
          try {
              certFactory = CertificateFactory.getInstance("X.509");
          } catch (CertificateException e) {
              // Should not happen, as X.509 is mandatory for all providers.
              throw new RuntimeException(e);
          }
          return certFactory.generateCertPath(new ByteArrayInputStream(bytes), CERT_PATH_ENCODING);
      }
  }
  