import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.KeyStoreException;
+import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.UnrecoverableKeyException;
import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
}
}
- public int initRecoveryService(
+ public void initRecoveryService(
@NonNull String rootCertificateAlias, @NonNull byte[] signedPublicKeyList, int userId)
throws RemoteException {
checkRecoverKeyStorePermission();
- // TODO open /system/etc/security/... cert file
- throw new UnsupportedOperationException();
+ // TODO: open /system/etc/security/... cert file, and check the signature on the public keys
+ PublicKey publicKey;
+ try {
+ KeyFactory kf = KeyFactory.getInstance("EC");
+ // TODO: Randomly choose a key from the list -- right now we just use the whole input
+ X509EncodedKeySpec pkSpec = new X509EncodedKeySpec(signedPublicKeyList);
+ publicKey = kf.generatePublic(pkSpec);
+ } catch (NoSuchAlgorithmException e) {
+ // Should never happen
+ throw new RuntimeException(e);
+ } catch (InvalidKeySpecException e) {
+ throw new RemoteException("Invalid public key for the recovery service");
+ }
+ mDatabase.setRecoveryServicePublicKey(userId, Binder.getCallingUid(), publicKey);
}
/**
import com.android.server.locksettings.recoverablekeystore.WrappedKey;
import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDbContract.KeysEntry;
+import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDbContract.RecoveryServicePublicKeyEntry;
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.HashMap;
+import java.util.List;
import java.util.Locale;
import java.util.Map;
}
/**
+ * Inserts or 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) {
+ SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
+ ContentValues values = new ContentValues();
+ values.put(RecoveryServicePublicKeyEntry.COLUMN_NAME_USER_ID, userId);
+ values.put(RecoveryServicePublicKeyEntry.COLUMN_NAME_UID, uid);
+ values.put(RecoveryServicePublicKeyEntry.COLUMN_NAME_PUBLIC_KEY, publicKey.getEncoded());
+ return db.replace(RecoveryServicePublicKeyEntry.TABLE_NAME, /*nullColumnHack=*/ null,
+ values);
+ }
+
+ /**
+ * Returns the public key of the recovery service.
+ *
+ * @param userId The uid of the profile the application is running under.
+ * @param uid The uid of the application who initializes the local recovery components.
+ *
+ * @hide
+ */
+ public PublicKey getRecoveryServicePublicKey(int userId, int uid) {
+ SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase();
+
+ String[] projection = {
+ RecoveryServicePublicKeyEntry._ID,
+ RecoveryServicePublicKeyEntry.COLUMN_NAME_USER_ID,
+ RecoveryServicePublicKeyEntry.COLUMN_NAME_UID,
+ RecoveryServicePublicKeyEntry.COLUMN_NAME_PUBLIC_KEY};
+ String selection =
+ RecoveryServicePublicKeyEntry.COLUMN_NAME_USER_ID + " = ? AND "
+ + RecoveryServicePublicKeyEntry.COLUMN_NAME_UID + " = ?";
+ String[] selectionArguments = { Integer.toString(userId), Integer.toString(uid)};
+
+ try (
+ Cursor cursor = db.query(
+ RecoveryServicePublicKeyEntry.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 PublicKey entries found for userId=%d uid=%d. "
+ + "Should only ever be 0 or 1.", count, userId, uid));
+ return null;
+ }
+ cursor.moveToFirst();
+ byte[] keyBytes = cursor.getBlob(cursor.getColumnIndexOrThrow(
+ RecoveryServicePublicKeyEntry.COLUMN_NAME_PUBLIC_KEY));
+ X509EncodedKeySpec pkSpec = new X509EncodedKeySpec(keyBytes);
+ try {
+ return KeyFactory.getInstance("EC").generatePublic(pkSpec);
+ } catch (NoSuchAlgorithmException e) {
+ // Should never happen
+ throw new RuntimeException(e);
+ } 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;
+ }
+ }
+ }
+
+ /**
* Closes all open connections to the database.
*/
public void close() {
*/
static final String COLUMN_NAME_PLATFORM_KEY_GENERATION_ID = "platform_key_generation_id";
}
+
+ /**
+ * Table holding public keys of the recovery service.
+ */
+ static class RecoveryServicePublicKeyEntry implements BaseColumns {
+ static final String TABLE_NAME = "recovery_service_public_keys";
+
+ /**
+ * The user id of the profile the application is running under.
+ */
+ static final String COLUMN_NAME_USER_ID = "user_id";
+
+ /**
+ * The uid of the application that initializes the local recovery components.
+ */
+ static final String COLUMN_NAME_UID = "uid";
+
+ /**
+ * The public key of the recovery service.
+ */
+ static final String COLUMN_NAME_PUBLIC_KEY = "public_key";
+ }
}
import android.database.sqlite.SQLiteOpenHelper;
import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDbContract.KeysEntry;
+import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDbContract.RecoveryServicePublicKeyEntry;
import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDbContract.UserMetadataEntry;
/**
+ UserMetadataEntry.COLUMN_NAME_USER_ID + " INTEGER UNIQUE,"
+ UserMetadataEntry.COLUMN_NAME_PLATFORM_KEY_GENERATION_ID + " INTEGER)";
+ private static final String SQL_CREATE_RECOVERY_SERVICE_PUBLIC_KEY_ENTRY =
+ "CREATE TABLE " + RecoveryServicePublicKeyEntry.TABLE_NAME + " ("
+ + RecoveryServicePublicKeyEntry._ID + " INTEGER PRIMARY KEY,"
+ + RecoveryServicePublicKeyEntry.COLUMN_NAME_USER_ID + " INTEGER,"
+ + RecoveryServicePublicKeyEntry.COLUMN_NAME_UID + " INTEGER,"
+ + RecoveryServicePublicKeyEntry.COLUMN_NAME_PUBLIC_KEY + " BLOB,"
+ + "UNIQUE("
+ + RecoveryServicePublicKeyEntry.COLUMN_NAME_USER_ID + ","
+ + RecoveryServicePublicKeyEntry.COLUMN_NAME_UID + "))";
+
private static final String SQL_DELETE_KEYS_ENTRY =
"DROP TABLE IF EXISTS " + KeysEntry.TABLE_NAME;
private static final String SQL_DELETE_USER_METADATA_ENTRY =
"DROP TABLE IF EXISTS " + UserMetadataEntry.TABLE_NAME;
+ private static final String SQL_DELETE_RECOVERY_SERVICE_PUBLIC_KEY_ENTRY =
+ "DROP TABLE IF EXISTS " + RecoveryServicePublicKeyEntry.TABLE_NAME;
+
RecoverableKeyStoreDbHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
public void onCreate(SQLiteDatabase db) {
db.execSQL(SQL_CREATE_KEYS_ENTRY);
db.execSQL(SQL_CREATE_USER_METADATA_ENTRY);
+ db.execSQL(SQL_CREATE_RECOVERY_SERVICE_PUBLIC_KEY_ENTRY);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL(SQL_DELETE_KEYS_ENTRY);
db.execSQL(SQL_DELETE_USER_METADATA_ENTRY);
+ db.execSQL(SQL_DELETE_RECOVERY_SERVICE_PUBLIC_KEY_ENTRY);
onCreate(db);
}
}
import java.io.File;
import java.nio.charset.StandardCharsets;
+import java.security.KeyPairGenerator;
+import java.security.PublicKey;
+import java.security.spec.ECGenParameterSpec;
import java.util.Map;
@SmallTest
assertThat(statuses).hasSize(0);
}
+ @Test
+ public void setRecoveryServicePublicKey_replaceOldKey() throws Exception {
+ int userId = 12;
+ int uid = 10009;
+ PublicKey pubkey1 = genRandomPublicKey();
+ PublicKey pubkey2 = genRandomPublicKey();
+ mRecoverableKeyStoreDb.setRecoveryServicePublicKey(userId, uid, pubkey1);
+ mRecoverableKeyStoreDb.setRecoveryServicePublicKey(userId, uid, pubkey2);
+ assertThat(mRecoverableKeyStoreDb.getRecoveryServicePublicKey(userId, uid)).isEqualTo(
+ pubkey2);
+ }
+
+ @Test
+ public void setRecoveryServicePublicKey_allowsTwoUsersToHaveTheSameUidAndKey()
+ throws Exception {
+ int userId1 = 12;
+ int userId2 = 23;
+ int uid = 10009;
+ PublicKey pubkey = genRandomPublicKey();
+ mRecoverableKeyStoreDb.setRecoveryServicePublicKey(userId1, uid, pubkey);
+ mRecoverableKeyStoreDb.setRecoveryServicePublicKey(userId2, uid, pubkey);
+ assertThat(mRecoverableKeyStoreDb.getRecoveryServicePublicKey(userId1, uid)).isEqualTo(
+ pubkey);
+ assertThat(mRecoverableKeyStoreDb.getRecoveryServicePublicKey(userId2, uid)).isEqualTo(
+ pubkey);
+ }
+
+ @Test
+ public void setRecoveryServicePublicKey_allowsAUserToHaveTwoUids() throws Exception {
+ int userId = 12;
+ int uid1 = 10009;
+ int uid2 = 20009;
+ PublicKey pubkey = genRandomPublicKey();
+ mRecoverableKeyStoreDb.setRecoveryServicePublicKey(userId, uid1, pubkey);
+ mRecoverableKeyStoreDb.setRecoveryServicePublicKey(userId, uid2, pubkey);
+ assertThat(mRecoverableKeyStoreDb.getRecoveryServicePublicKey(userId, uid1)).isEqualTo(
+ pubkey);
+ assertThat(mRecoverableKeyStoreDb.getRecoveryServicePublicKey(userId, uid2)).isEqualTo(
+ pubkey);
+ }
+
+ @Test
+ public void getRecoveryServicePublicKey_returnsNullIfNoKey() throws Exception {
+ int userId = 12;
+ int uid = 10009;
+ assertNull(mRecoverableKeyStoreDb.getRecoveryServicePublicKey(userId, uid));
+ }
+
+ @Test
+ public void getRecoveryServicePublicKey_returnsInsertedKey() throws Exception {
+ int userId = 12;
+ int uid = 10009;
+ PublicKey pubkey = genRandomPublicKey();
+ mRecoverableKeyStoreDb.setRecoveryServicePublicKey(userId, uid, pubkey);
+ assertThat(mRecoverableKeyStoreDb.getRecoveryServicePublicKey(userId, uid)).isEqualTo(
+ pubkey);
+ }
+
private static byte[] getUtf8Bytes(String s) {
return s.getBytes(StandardCharsets.UTF_8);
}
+
+ private static PublicKey genRandomPublicKey() throws Exception {
+ KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
+ keyPairGenerator.initialize(new ECGenParameterSpec("secp256r1"));
+ return keyPairGenerator.generateKeyPair().getPublic();
+ }
}