/* * 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.keychain.internal; import android.content.ContentValues; import android.content.Context; import android.content.pm.PackageManager; import android.database.Cursor; import android.database.DatabaseUtils; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.util.Log; import java.util.ArrayList; import java.util.List; public class GrantsDatabase { private static final String TAG = "KeyChain"; private static final String DATABASE_NAME = "grants.db"; private static final int DATABASE_VERSION = 2; private static final String TABLE_GRANTS = "grants"; private static final String GRANTS_ALIAS = "alias"; private static final String GRANTS_GRANTEE_UID = "uid"; private static final String SELECTION_COUNT_OF_MATCHING_GRANTS = "SELECT COUNT(*) FROM " + TABLE_GRANTS + " WHERE " + GRANTS_GRANTEE_UID + "=? AND " + GRANTS_ALIAS + "=?"; private static final String SELECTION_GRANTEE_UIDS_FOR_ALIAS = "SELECT " + GRANTS_GRANTEE_UID + " FROM " + TABLE_GRANTS + " WHERE " + GRANTS_ALIAS + "=?"; private static final String SELECT_GRANTS_BY_UID_AND_ALIAS = GRANTS_GRANTEE_UID + "=? AND " + GRANTS_ALIAS + "=?"; private static final String SELECTION_GRANTS_BY_UID = GRANTS_GRANTEE_UID + "=?"; private static final String SELECTION_GRANTS_BY_ALIAS = GRANTS_ALIAS + "=?"; private static final String TABLE_SELECTABLE = "userselectable"; private static final String SELECTABLE_IS_SELECTABLE = "is_selectable"; private static final String COUNT_SELECTABILITY_FOR_ALIAS = "SELECT COUNT(*) FROM " + TABLE_SELECTABLE + " WHERE " + GRANTS_ALIAS + "=?"; public DatabaseHelper mDatabaseHelper; private class DatabaseHelper extends SQLiteOpenHelper { private final ExistingKeysProvider mKeysProvider; public DatabaseHelper(Context context, ExistingKeysProvider keysProvider) { super(context, DATABASE_NAME, null /* CursorFactory */, DATABASE_VERSION); mKeysProvider = keysProvider; } void createSelectableTable(final SQLiteDatabase db) { // There are some broken V1 databases that actually have the 'userselectable' // already created. Only create it if it does not exist. db.execSQL( "CREATE TABLE IF NOT EXISTS " + TABLE_SELECTABLE + " ( " + GRANTS_ALIAS + " STRING NOT NULL, " + SELECTABLE_IS_SELECTABLE + " STRING NOT NULL, " + "UNIQUE (" + GRANTS_ALIAS + "))"); } @Override public void onCreate(final SQLiteDatabase db) { Log.w(TAG, "Creating new DB."); db.execSQL( "CREATE TABLE " + TABLE_GRANTS + " ( " + GRANTS_ALIAS + " STRING NOT NULL, " + GRANTS_GRANTEE_UID + " INTEGER NOT NULL, " + "UNIQUE (" + GRANTS_ALIAS + "," + GRANTS_GRANTEE_UID + "))"); createSelectableTable(db); markExistingKeysAsSelectable(db); } private void markExistingKeysAsSelectable(final SQLiteDatabase db) { for (String alias: mKeysProvider.getExistingKeyAliases()) { Log.w(TAG, "Existing alias: " + alias); if (!hasEntryInUserSelectableTable(db, alias)) { Log.w(TAG, "Marking as selectable: " + alias); markKeyAsSelectable(db, alias); } } } private void markKeyAsSelectable(final SQLiteDatabase db, final String alias) { final ContentValues values = new ContentValues(); values.put(GRANTS_ALIAS, alias); values.put(SELECTABLE_IS_SELECTABLE, Boolean.toString(true)); db.replace(TABLE_SELECTABLE, null, values); } private boolean hasEntryInUserSelectableTable(final SQLiteDatabase db, final String alias) { final long numMatches = DatabaseUtils.longForQuery( db, COUNT_SELECTABILITY_FOR_ALIAS, new String[] {alias}); return numMatches > 0; } @Override public void onUpgrade(final SQLiteDatabase db, int oldVersion, final int newVersion) { Log.w(TAG, "upgrade from version " + oldVersion + " to version " + newVersion); if (oldVersion == 1) { // Version 1 of the database does not have the 'userselectable' table, meaning // upgraded keys could not be selected by users. // The upgrade from version 1 to 2 consists of creating the 'userselectable' // table and adding all existing keys as user-selectable ones into that table. oldVersion++; createSelectableTable(db); markExistingKeysAsSelectable(db); } } } public GrantsDatabase(Context context, ExistingKeysProvider keysProvider) { mDatabaseHelper = new DatabaseHelper(context, keysProvider); } public void destroy() { mDatabaseHelper.close(); mDatabaseHelper = null; } boolean hasGrantInternal(final SQLiteDatabase db, final int uid, final String alias) { final long numMatches = DatabaseUtils.longForQuery( db, SELECTION_COUNT_OF_MATCHING_GRANTS, new String[] {String.valueOf(uid), alias}); return numMatches > 0; } public boolean hasGrant(final int uid, final String alias) { final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase(); return hasGrantInternal(db, uid, alias); } public void setGrant(final int uid, final String alias, final boolean value) { final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase(); if (value) { if (!hasGrantInternal(db, uid, alias)) { final ContentValues values = new ContentValues(); values.put(GRANTS_ALIAS, alias); values.put(GRANTS_GRANTEE_UID, uid); db.insert(TABLE_GRANTS, GRANTS_ALIAS, values); } } else { db.delete( TABLE_GRANTS, SELECT_GRANTS_BY_UID_AND_ALIAS, new String[] {String.valueOf(uid), alias}); } } public int[] getGrants(String alias) { final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase(); try (Cursor cursor = db.query( TABLE_GRANTS, new String[] {GRANTS_GRANTEE_UID}, SELECTION_GRANTS_BY_ALIAS, new String[] {alias}, null /* group by */, null /* having */, null /* order by */)) { final List result = new ArrayList<>(); while (cursor.moveToNext()) { result.add(cursor.getInt(0)); } return result.stream().mapToInt(Integer::intValue).toArray(); } } public void removeAliasInformation(String alias) { final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase(); db.delete(TABLE_GRANTS, SELECTION_GRANTS_BY_ALIAS, new String[] {alias}); db.delete(TABLE_SELECTABLE, SELECTION_GRANTS_BY_ALIAS, new String[] {alias}); } public void removeAllAliasesInformation() { final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase(); db.delete(TABLE_GRANTS, null /* whereClause */, null /* whereArgs */); db.delete(TABLE_SELECTABLE, null /* whereClause */, null /* whereArgs */); } public void purgeOldGrants(PackageManager pm) { final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase(); db.beginTransaction(); try (Cursor cursor = db.query( TABLE_GRANTS, new String[] {GRANTS_GRANTEE_UID}, null, null, GRANTS_GRANTEE_UID, null, null)) { while ((cursor != null) && (cursor.moveToNext())) { final int uid = cursor.getInt(0); final boolean packageExists = pm.getPackagesForUid(uid) != null; if (packageExists) { continue; } Log.d(TAG, String.format( "deleting grants for UID %d because its package is no longer installed", uid)); db.delete( TABLE_GRANTS, SELECTION_GRANTS_BY_UID, new String[] {Integer.toString(uid)}); } db.setTransactionSuccessful(); } db.endTransaction(); } public void setIsUserSelectable(final String alias, final boolean userSelectable) { final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase(); final ContentValues values = new ContentValues(); values.put(GRANTS_ALIAS, alias); values.put(SELECTABLE_IS_SELECTABLE, Boolean.toString(userSelectable)); db.replace(TABLE_SELECTABLE, null, values); } public boolean isUserSelectable(final String alias) { final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase(); try (Cursor res = db.query( TABLE_SELECTABLE, new String[] {SELECTABLE_IS_SELECTABLE}, SELECTION_GRANTS_BY_ALIAS, new String[] {alias}, null /* group by */, null /* having */, null /* order by */)) { if (res == null || !res.moveToNext()) { return false; } boolean isSelectable = Boolean.parseBoolean(res.getString(0)); if (res.getCount() > 1) { // BUG! Should not have more than one result for any given alias. Log.w(TAG, String.format("Have more than one result for alias %s", alias)); } return isSelectable; } } }