OSDN Git Service

Add RecoverableKeyStoreDb
authorRobert Berry <robertberry@google.com>
Fri, 15 Dec 2017 23:01:22 +0000 (23:01 +0000)
committerRobert Berry <robertberry@google.com>
Mon, 18 Dec 2017 16:06:00 +0000 (16:06 +0000)
Adds database for storing recoverable keys. They are indexed by the
uid of the application that created them, and the alias of the key.
This is the same alias that is used to get the key from
AndroidKeyStore. The database stores the wrapped key, and the version
of the platform key that did the wrapping. It also stores information
about when the key was last synced.

This is used to get the status of the key. e.g., if the platform key
id is not the current platform key id, this is now an unsyncable key.
If the last-synced time is not set, this is a valid key but one that
has not yet been synced. etc., etc.

Test: adb shell am instrument -w -e package com.android.server.locksettings.recoverablekeystore com.android.frameworks.servicestests/android.support.test.runner.AndroidJUnitRunner
Change-Id: I4960452abcdd16c9f39a1f166a086a52dd2f05c0

services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java [new file with mode: 0644]
services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbContract.java [new file with mode: 0644]
services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelper.java [new file with mode: 0644]
services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbTest.java [new file with mode: 0644]

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
new file mode 100644 (file)
index 0000000..79bf5aa
--- /dev/null
@@ -0,0 +1,185 @@
+/*
+ * 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.Nullable;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.util.Log;
+
+import com.android.server.locksettings.recoverablekeystore.WrappedKey;
+import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDbContract.KeysEntry;
+
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Database of recoverable key information.
+ *
+ * @hide
+ */
+public class RecoverableKeyStoreDb {
+    private static final String TAG = "RecoverableKeyStoreDb";
+    private static final int IDLE_TIMEOUT_SECONDS = 30;
+
+    private final RecoverableKeyStoreDbHelper mKeyStoreDbHelper;
+
+    /**
+     * 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;
+    }
+
+    /**
+     * Inserts a key into the database.
+     *
+     * @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 bytes of the key.
+     * @param generationId The generation ID of the platform key that wrapped the key.
+     * @return The primary key of the inserted row, or -1 if failed.
+     *
+     * @hide
+     */
+    public long insertKey(int uid, String alias, WrappedKey wrappedKey, int generationId) {
+        SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
+        ContentValues values = new ContentValues();
+        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, -1);
+        values.put(KeysEntry.COLUMN_NAME_GENERATION_ID, generationId);
+        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};
+        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));
+            return new WrappedKey(nonce, keyMaterial);
+        }
+    }
+
+    /**
+     * Returns all keys for the given {@code uid} and {@code platformKeyGenerationId}.
+     *
+     * @param uid User id of the profile to which all the keys are associated.
+     * @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 Map<String, WrappedKey> getAllKeys(int uid, int platformKeyGenerationId) {
+        SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase();
+        String[] projection = {
+                KeysEntry._ID,
+                KeysEntry.COLUMN_NAME_NONCE,
+                KeysEntry.COLUMN_NAME_WRAPPED_KEY,
+                KeysEntry.COLUMN_NAME_ALIAS};
+        String selection =
+                KeysEntry.COLUMN_NAME_UID + " = ? AND "
+                + KeysEntry.COLUMN_NAME_GENERATION_ID + " = ?";
+        String[] selectionArguments = {
+                Integer.toString(uid), 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));
+                keys.put(alias, new WrappedKey(nonce, keyMaterial));
+            }
+            return keys;
+        }
+    }
+
+    /**
+     * Closes all open connections to the database.
+     */
+    public void close() {
+        mKeyStoreDbHelper.close();
+    }
+
+    // TODO: Add method for updating the 'last synced' time.
+}
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
new file mode 100644 (file)
index 0000000..c54d0a6
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * 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.provider.BaseColumns;
+
+/**
+ * Contract for recoverable key database. Describes the tables present.
+ */
+class RecoverableKeyStoreDbContract {
+    /**
+     * Table holding wrapped keys, and information about when they were last synced.
+     */
+    static class KeysEntry implements BaseColumns {
+        static final String TABLE_NAME = "keys";
+
+        /**
+         * The uid of the application that generated the key.
+         */
+        static final String COLUMN_NAME_UID = "uid";
+
+        /**
+         * The alias of the key, as set in AndroidKeyStore.
+         */
+        static final String COLUMN_NAME_ALIAS = "alias";
+
+        /**
+         * Nonce with which the key was encrypted.
+         */
+        static final String COLUMN_NAME_NONCE = "nonce";
+
+        /**
+         * Encrypted bytes of the key.
+         */
+        static final String COLUMN_NAME_WRAPPED_KEY = "wrapped_key";
+
+        /**
+         * Generation ID of the platform key that was used to encrypt this key.
+         */
+        static final String COLUMN_NAME_GENERATION_ID = "platform_key_generation_id";
+
+        /**
+         * Timestamp of when this key was last synced with remote storage, or -1 if never synced.
+         */
+        static final String COLUMN_NAME_LAST_SYNCED_AT = "last_synced_at";
+    }
+}
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
new file mode 100644 (file)
index 0000000..e3783c4
--- /dev/null
@@ -0,0 +1,43 @@
+package com.android.server.locksettings.recoverablekeystore.storage;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDbContract.KeysEntry;
+
+/**
+ * Helper for creating the recoverable key database.
+ */
+class RecoverableKeyStoreDbHelper extends SQLiteOpenHelper {
+    private static final int DATABASE_VERSION = 1;
+    private static final String DATABASE_NAME = "recoverablekeystore.db";
+
+    private static final String SQL_CREATE_ENTRIES =
+            "CREATE TABLE " + KeysEntry.TABLE_NAME + "( "
+                    + KeysEntry._ID + " INTEGER PRIMARY KEY,"
+                    + KeysEntry.COLUMN_NAME_UID + " INTEGER UNIQUE,"
+                    + KeysEntry.COLUMN_NAME_ALIAS + " TEXT UNIQUE,"
+                    + KeysEntry.COLUMN_NAME_NONCE + " BLOB,"
+                    + KeysEntry.COLUMN_NAME_WRAPPED_KEY + " BLOB,"
+                    + KeysEntry.COLUMN_NAME_GENERATION_ID + " INTEGER,"
+                    + KeysEntry.COLUMN_NAME_LAST_SYNCED_AT + " INTEGER)";
+
+    private static final String SQL_DELETE_ENTRIES =
+            "DROP TABLE IF EXISTS " + KeysEntry.TABLE_NAME;
+
+    RecoverableKeyStoreDbHelper(Context context) {
+        super(context, DATABASE_NAME, null, DATABASE_VERSION);
+    }
+
+    @Override
+    public void onCreate(SQLiteDatabase db) {
+        db.execSQL(SQL_CREATE_ENTRIES);
+    }
+
+    @Override
+    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+        db.execSQL(SQL_DELETE_ENTRIES);
+        onCreate(db);
+    }
+}
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
new file mode 100644 (file)
index 0000000..5cb88dd
--- /dev/null
@@ -0,0 +1,155 @@
+/*
+ * 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 static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.server.locksettings.recoverablekeystore.WrappedKey;
+
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class RecoverableKeyStoreDbTest {
+    private static final String DATABASE_FILE_NAME = "recoverablekeystore.db";
+
+    private RecoverableKeyStoreDb mRecoverableKeyStoreDb;
+    private File mDatabaseFile;
+
+    @Before
+    public void setUp() {
+        Context context = InstrumentationRegistry.getTargetContext();
+        mDatabaseFile = context.getDatabasePath(DATABASE_FILE_NAME);
+        mRecoverableKeyStoreDb = RecoverableKeyStoreDb.newInstance(context);
+    }
+
+    @After
+    public void tearDown() {
+        mRecoverableKeyStoreDb.close();
+        mDatabaseFile.delete();
+    }
+
+    @Test
+    public void insertKey_replacesOldKey() {
+        int userId = 12;
+        String alias = "test";
+        WrappedKey oldWrappedKey = new WrappedKey(
+                getUtf8Bytes("nonce1"), getUtf8Bytes("keymaterial1"));
+        mRecoverableKeyStoreDb.insertKey(
+                userId, alias, oldWrappedKey, /*generationId=*/ 1);
+        byte[] nonce = getUtf8Bytes("nonce2");
+        byte[] keyMaterial = getUtf8Bytes("keymaterial2");
+        WrappedKey newWrappedKey = new WrappedKey(nonce, keyMaterial);
+
+        mRecoverableKeyStoreDb.insertKey(
+                userId, alias, newWrappedKey, /*generationId=*/ 2);
+
+        WrappedKey retrievedKey = mRecoverableKeyStoreDb.getKey(userId, alias);
+        assertArrayEquals(nonce, retrievedKey.getNonce());
+        assertArrayEquals(keyMaterial, retrievedKey.getKeyMaterial());
+    }
+
+    @Test
+    public void getKey_returnsNullIfNoKey() {
+        WrappedKey key = mRecoverableKeyStoreDb.getKey(
+                /*userId=*/ 1, /*alias=*/ "hello");
+
+        assertNull(key);
+    }
+
+    @Test
+    public void getKey_returnsInsertedKey() {
+        int userId = 12;
+        int generationId = 6;
+        String alias = "test";
+        byte[] nonce = getUtf8Bytes("nonce");
+        byte[] keyMaterial = getUtf8Bytes("keymaterial");
+        WrappedKey wrappedKey = new WrappedKey(nonce, keyMaterial);
+        mRecoverableKeyStoreDb.insertKey(userId, alias, wrappedKey, generationId);
+
+        WrappedKey retrievedKey = mRecoverableKeyStoreDb.getKey(userId, alias);
+
+        assertArrayEquals(nonce, retrievedKey.getNonce());
+        assertArrayEquals(keyMaterial, retrievedKey.getKeyMaterial());
+    }
+
+    @Test
+    public void getAllKeys_getsKeysWithUserIdAndGenerationId() {
+        int userId = 12;
+        int generationId = 6;
+        String alias = "test";
+        byte[] nonce = getUtf8Bytes("nonce");
+        byte[] keyMaterial = getUtf8Bytes("keymaterial");
+        WrappedKey wrappedKey = new WrappedKey(nonce, keyMaterial);
+        mRecoverableKeyStoreDb.insertKey(userId, alias, wrappedKey, generationId);
+
+        Map<String, WrappedKey> keys = mRecoverableKeyStoreDb.getAllKeys(userId, generationId);
+
+        assertEquals(1, keys.size());
+        assertTrue(keys.containsKey(alias));
+        WrappedKey retrievedKey = keys.get(alias);
+        assertArrayEquals(nonce, retrievedKey.getNonce());
+        assertArrayEquals(keyMaterial, retrievedKey.getKeyMaterial());
+    }
+
+    @Test
+    public void getAllKeys_doesNotReturnKeysWithBadGenerationId() {
+        int userId = 12;
+        WrappedKey wrappedKey = new WrappedKey(
+                getUtf8Bytes("nonce"), getUtf8Bytes("keymaterial"));
+        mRecoverableKeyStoreDb.insertKey(
+                userId, /*alias=*/ "test", wrappedKey, /*generationId=*/ 5);
+
+        Map<String, WrappedKey> keys = mRecoverableKeyStoreDb.getAllKeys(
+                userId, /*generationId=*/ 7);
+
+        assertTrue(keys.isEmpty());
+    }
+
+    @Test
+    public void getAllKeys_doesNotReturnKeysWithBadUserId() {
+        int generationId = 12;
+        WrappedKey wrappedKey = new WrappedKey(
+                getUtf8Bytes("nonce"), getUtf8Bytes("keymaterial"));
+        mRecoverableKeyStoreDb.insertKey(
+                /*userId=*/ 1, /*alias=*/ "test", wrappedKey, generationId);
+
+        Map<String, WrappedKey> keys = mRecoverableKeyStoreDb.getAllKeys(
+                /*userId=*/ 2, generationId);
+
+        assertTrue(keys.isEmpty());
+    }
+
+    private static byte[] getUtf8Bytes(String s) {
+        return s.getBytes(StandardCharsets.UTF_8);
+    }
+}