Implement RecoverableKeyStore API to set/get recovery secret types.
Bug: 66499222
Test: adb shell am instrument -w -e package \
com.android.server.locksettings.recoverablekeystore \
com.android.frameworks.servicestests/android.support.test.runner.AndroidJUnitRunner
Change-Id: If29f22f24438a9d050fabebf970b9ae56b0df805
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java
index 11f4e9c..fe1cad4 100644
--- a/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java
@@ -229,7 +229,8 @@
@NonNull @KeyStoreRecoveryMetadata.UserSecretType int[] secretTypes, int userId)
throws RemoteException {
checkRecoverKeyStorePermission();
- throw new UnsupportedOperationException();
+ mDatabase.setRecoverySecretTypes(UserHandle.getCallingUserId(), Binder.getCallingUid(),
+ secretTypes);
}
/**
@@ -240,7 +241,8 @@
*/
public @NonNull int[] getRecoverySecretTypes(int userId) throws RemoteException {
checkRecoverKeyStorePermission();
- throw new UnsupportedOperationException();
+ return mDatabase.getRecoverySecretTypes(UserHandle.getCallingUserId(),
+ Binder.getCallingUid());
}
/**
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java
index 156d5ba..e6efad5 100644
--- a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java
@@ -23,6 +23,7 @@
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.security.recoverablekeystore.RecoverableKeyStoreLoader;
+import android.text.TextUtils;
import android.util.Log;
import com.android.server.locksettings.recoverablekeystore.WrappedKey;
@@ -30,18 +31,18 @@
import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDbContract.RecoveryServiceMetadataEntry;
import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDbContract.UserMetadataEntry;
-
-
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
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.
@@ -426,6 +427,99 @@
}
/**
+ * 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 KeyStoreRecoveryMetadata}
+ *
+ * @param userId The uid 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 uid 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;
+ }
+ }
+
+ /**
* Updates the server parameters given by the application initializing the local recovery
* components.
*
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbContract.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbContract.java
index a232771..8f773dd 100644
--- a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbContract.java
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbContract.java
@@ -109,6 +109,11 @@
static final String COLUMN_NAME_PUBLIC_KEY = "public_key";
/**
+ * Secret types used for end-to-end encryption.
+ */
+ static final String COLUMN_NAME_SECRET_TYPES = "secret_types";
+
+ /**
* The server parameters of the recovery service.
*/
static final String COLUMN_NAME_SERVER_PARAMETERS = "server_parameters";
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelper.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelper.java
index c87812d..5b07f3e 100644
--- a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelper.java
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelper.java
@@ -57,6 +57,7 @@
+ RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID + " INTEGER,"
+ RecoveryServiceMetadataEntry.COLUMN_NAME_UID + " INTEGER,"
+ RecoveryServiceMetadataEntry.COLUMN_NAME_PUBLIC_KEY + " BLOB,"
+ + RecoveryServiceMetadataEntry.COLUMN_NAME_SECRET_TYPES + " TEXT,"
+ RecoveryServiceMetadataEntry.COLUMN_NAME_SERVER_PARAMETERS + " INTEGER,"
+ "UNIQUE("
+ RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID + ","
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java
index 2c9b356..88df62b 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java
@@ -362,6 +362,26 @@
}
@Test
+ public void setRecoverySecretTypes() throws Exception {
+ int userId = UserHandle.getCallingUserId();
+ int[] types1 = new int[]{11, 2000};
+ int[] types2 = new int[]{1, 2, 3};
+ int[] types3 = new int[]{};
+
+ mRecoverableKeyStoreManager.setRecoverySecretTypes(types1, userId);
+ assertThat(mRecoverableKeyStoreManager.getRecoverySecretTypes(userId)).isEqualTo(
+ types1);
+
+ mRecoverableKeyStoreManager.setRecoverySecretTypes(types2, userId);
+ assertThat(mRecoverableKeyStoreManager.getRecoverySecretTypes(userId)).isEqualTo(
+ types2);
+
+ mRecoverableKeyStoreManager.setRecoverySecretTypes(types3, userId);
+ assertThat(mRecoverableKeyStoreManager.getRecoverySecretTypes(userId)).isEqualTo(
+ types3);
+ }
+
+ @Test
public void setRecoveryStatus_forOneAlias() throws Exception {
int userId = UserHandle.getCallingUserId();
int uid = Binder.getCallingUid();
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbTest.java
index 373a7bc..a5b67af 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbTest.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbTest.java
@@ -328,6 +328,7 @@
}
@Test
+
public void getRecoveryAgentUid_returnsUidIfSet() throws Exception {
int userId = 12;
int uid = 190992;
@@ -341,6 +342,100 @@
assertThat(mRecoverableKeyStoreDb.getRecoveryAgentUid(12)).isEqualTo(-1);
}
+ public void setRecoverySecretTypes_emptyDefaultValue() throws Exception {
+ int userId = 12;
+ int uid = 10009;
+ assertThat(mRecoverableKeyStoreDb.getRecoverySecretTypes(userId, uid)).isEqualTo(
+ new int[]{}); // default
+ }
+
+ @Test
+ public void setRecoverySecretTypes_updateValue() throws Exception {
+ int userId = 12;
+ int uid = 10009;
+ int[] types1 = new int[]{1};
+ int[] types2 = new int[]{2};
+
+ mRecoverableKeyStoreDb.setRecoverySecretTypes(userId, uid, types1);
+ assertThat(mRecoverableKeyStoreDb.getRecoverySecretTypes(userId, uid)).isEqualTo(
+ types1);
+ mRecoverableKeyStoreDb.setRecoverySecretTypes(userId, uid, types2);
+ assertThat(mRecoverableKeyStoreDb.getRecoverySecretTypes(userId, uid)).isEqualTo(
+ types2);
+ }
+
+ @Test
+ public void setRecoverySecretTypes_withMultiElementArrays() throws Exception {
+ int userId = 12;
+ int uid = 10009;
+ int[] types1 = new int[]{11, 2000};
+ int[] types2 = new int[]{1, 2, 3};
+ int[] types3 = new int[]{};
+
+ mRecoverableKeyStoreDb.setRecoverySecretTypes(userId, uid, types1);
+ assertThat(mRecoverableKeyStoreDb.getRecoverySecretTypes(userId, uid)).isEqualTo(
+ types1);
+ mRecoverableKeyStoreDb.setRecoverySecretTypes(userId, uid, types2);
+ assertThat(mRecoverableKeyStoreDb.getRecoverySecretTypes(userId, uid)).isEqualTo(
+ types2);
+ mRecoverableKeyStoreDb.setRecoverySecretTypes(userId, uid, types3);
+ assertThat(mRecoverableKeyStoreDb.getRecoverySecretTypes(userId, uid)).isEqualTo(
+ types3);
+ }
+
+ @Test
+ public void setRecoverySecretTypes_withDifferentUid() throws Exception {
+ int userId = 12;
+ int uid1 = 10011;
+ int uid2 = 10012;
+ int[] types1 = new int[]{1};
+ int[] types2 = new int[]{2};
+
+ mRecoverableKeyStoreDb.setRecoverySecretTypes(userId, uid1, types1);
+ mRecoverableKeyStoreDb.setRecoverySecretTypes(userId, uid2, types2);
+ assertThat(mRecoverableKeyStoreDb.getRecoverySecretTypes(userId, uid1)).isEqualTo(
+ types1);
+ assertThat(mRecoverableKeyStoreDb.getRecoverySecretTypes(userId, uid2)).isEqualTo(
+ types2);
+ }
+
+ @Test
+ public void setRecoveryServiceMetadataMethods() throws Exception {
+ int userId = 12;
+ int uid = 10009;
+
+ PublicKey pubkey1 = genRandomPublicKey();
+ int[] types1 = new int[]{1};
+ long serverParams1 = 111L;
+
+ PublicKey pubkey2 = genRandomPublicKey();
+ int[] types2 = new int[]{2};
+ long serverParams2 = 222L;
+
+ mRecoverableKeyStoreDb.setRecoveryServicePublicKey(userId, uid, pubkey1);
+ mRecoverableKeyStoreDb.setRecoverySecretTypes(userId, uid, types1);
+ mRecoverableKeyStoreDb.setServerParameters(userId, uid, serverParams1);
+
+ assertThat(mRecoverableKeyStoreDb.getRecoverySecretTypes(userId, uid)).isEqualTo(
+ types1);
+ assertThat(mRecoverableKeyStoreDb.getServerParameters(userId, uid)).isEqualTo(
+ serverParams1);
+ assertThat(mRecoverableKeyStoreDb.getRecoveryServicePublicKey(userId, uid)).isEqualTo(
+ pubkey1);
+
+ // Check that the methods don't interfere with each other.
+ mRecoverableKeyStoreDb.setRecoveryServicePublicKey(userId, uid, pubkey2);
+ mRecoverableKeyStoreDb.setRecoverySecretTypes(userId, uid, types2);
+ mRecoverableKeyStoreDb.setServerParameters(userId, uid, serverParams2);
+
+ assertThat(mRecoverableKeyStoreDb.getRecoverySecretTypes(userId, uid)).isEqualTo(
+ types2);
+ assertThat(mRecoverableKeyStoreDb.getServerParameters(userId, uid)).isEqualTo(
+ serverParams2);
+ assertThat(mRecoverableKeyStoreDb.getRecoveryServicePublicKey(userId, uid)).isEqualTo(
+ pubkey2);
+ }
+
@Test
public void setServerParameters_replaceOldValue() throws Exception {
int userId = 12;