1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.keychain.internal; 18 19 import android.content.ContentValues; 20 import android.content.Context; 21 import android.content.pm.PackageManager; 22 import android.database.Cursor; 23 import android.database.DatabaseUtils; 24 import android.database.sqlite.SQLiteDatabase; 25 import android.database.sqlite.SQLiteOpenHelper; 26 import android.util.Log; 27 28 public class GrantsDatabase { 29 private static final String TAG = "KeyChain"; 30 31 private static final String DATABASE_NAME = "grants.db"; 32 private static final int DATABASE_VERSION = 2; 33 private static final String TABLE_GRANTS = "grants"; 34 private static final String GRANTS_ALIAS = "alias"; 35 private static final String GRANTS_GRANTEE_UID = "uid"; 36 37 private static final String SELECTION_COUNT_OF_MATCHING_GRANTS = 38 "SELECT COUNT(*) FROM " 39 + TABLE_GRANTS 40 + " WHERE " 41 + GRANTS_GRANTEE_UID 42 + "=? AND " 43 + GRANTS_ALIAS 44 + "=?"; 45 46 private static final String SELECT_GRANTS_BY_UID_AND_ALIAS = 47 GRANTS_GRANTEE_UID + "=? AND " + GRANTS_ALIAS + "=?"; 48 49 private static final String SELECTION_GRANTS_BY_UID = GRANTS_GRANTEE_UID + "=?"; 50 51 private static final String SELECTION_GRANTS_BY_ALIAS = GRANTS_ALIAS + "=?"; 52 53 private static final String TABLE_SELECTABLE = "userselectable"; 54 private static final String SELECTABLE_IS_SELECTABLE = "is_selectable"; 55 private static final String COUNT_SELECTABILITY_FOR_ALIAS = 56 "SELECT COUNT(*) FROM " + TABLE_SELECTABLE + " WHERE " + GRANTS_ALIAS + "=?"; 57 58 public DatabaseHelper mDatabaseHelper; 59 60 private class DatabaseHelper extends SQLiteOpenHelper { 61 private final ExistingKeysProvider mKeysProvider; 62 DatabaseHelper(Context context, ExistingKeysProvider keysProvider)63 public DatabaseHelper(Context context, ExistingKeysProvider keysProvider) { 64 super(context, DATABASE_NAME, null /* CursorFactory */, DATABASE_VERSION); 65 mKeysProvider = keysProvider; 66 } 67 createSelectableTable(final SQLiteDatabase db)68 void createSelectableTable(final SQLiteDatabase db) { 69 // There are some broken V1 databases that actually have the 'userselectable' 70 // already created. Only create it if it does not exist. 71 db.execSQL( 72 "CREATE TABLE IF NOT EXISTS " 73 + TABLE_SELECTABLE 74 + " ( " 75 + GRANTS_ALIAS 76 + " STRING NOT NULL, " 77 + SELECTABLE_IS_SELECTABLE 78 + " STRING NOT NULL, " 79 + "UNIQUE (" 80 + GRANTS_ALIAS 81 + "))"); 82 } 83 84 @Override onCreate(final SQLiteDatabase db)85 public void onCreate(final SQLiteDatabase db) { 86 Log.w(TAG, "Creating new DB."); 87 db.execSQL( 88 "CREATE TABLE " 89 + TABLE_GRANTS 90 + " ( " 91 + GRANTS_ALIAS 92 + " STRING NOT NULL, " 93 + GRANTS_GRANTEE_UID 94 + " INTEGER NOT NULL, " 95 + "UNIQUE (" 96 + GRANTS_ALIAS 97 + "," 98 + GRANTS_GRANTEE_UID 99 + "))"); 100 101 createSelectableTable(db); 102 markExistingKeysAsSelectable(db); 103 } 104 markExistingKeysAsSelectable(final SQLiteDatabase db)105 private void markExistingKeysAsSelectable(final SQLiteDatabase db) { 106 for (String alias: mKeysProvider.getExistingKeyAliases()) { 107 Log.w(TAG, "Existing alias: " + alias); 108 if (!hasEntryInUserSelectableTable(db, alias)) { 109 Log.w(TAG, "Marking as selectable: " + alias); 110 markKeyAsSelectable(db, alias); 111 } 112 } 113 114 } 115 markKeyAsSelectable(final SQLiteDatabase db, final String alias)116 private void markKeyAsSelectable(final SQLiteDatabase db, final String alias) { 117 final ContentValues values = new ContentValues(); 118 values.put(GRANTS_ALIAS, alias); 119 values.put(SELECTABLE_IS_SELECTABLE, Boolean.toString(true)); 120 db.replace(TABLE_SELECTABLE, null, values); 121 } 122 hasEntryInUserSelectableTable(final SQLiteDatabase db, final String alias)123 private boolean hasEntryInUserSelectableTable(final SQLiteDatabase db, final String alias) { 124 final long numMatches = 125 DatabaseUtils.longForQuery( 126 db, 127 COUNT_SELECTABILITY_FOR_ALIAS, 128 new String[] {alias}); 129 return numMatches > 0; 130 } 131 132 @Override onUpgrade(final SQLiteDatabase db, int oldVersion, final int newVersion)133 public void onUpgrade(final SQLiteDatabase db, int oldVersion, final int newVersion) { 134 Log.w(TAG, "upgrade from version " + oldVersion + " to version " + newVersion); 135 136 if (oldVersion == 1) { 137 // Version 1 of the database does not have the 'userselectable' table, meaning 138 // upgraded keys could not be selected by users. 139 // The upgrade from version 1 to 2 consists of creating the 'userselectable' 140 // table and adding all existing keys as user-selectable ones into that table. 141 oldVersion++; 142 createSelectableTable(db); 143 markExistingKeysAsSelectable(db); 144 } 145 } 146 } 147 GrantsDatabase(Context context, ExistingKeysProvider keysProvider)148 public GrantsDatabase(Context context, ExistingKeysProvider keysProvider) { 149 mDatabaseHelper = new DatabaseHelper(context, keysProvider); 150 } 151 destroy()152 public void destroy() { 153 mDatabaseHelper.close(); 154 mDatabaseHelper = null; 155 } 156 hasGrantInternal(final SQLiteDatabase db, final int uid, final String alias)157 boolean hasGrantInternal(final SQLiteDatabase db, final int uid, final String alias) { 158 final long numMatches = 159 DatabaseUtils.longForQuery( 160 db, 161 SELECTION_COUNT_OF_MATCHING_GRANTS, 162 new String[] {String.valueOf(uid), alias}); 163 return numMatches > 0; 164 } 165 hasGrant(final int uid, final String alias)166 public boolean hasGrant(final int uid, final String alias) { 167 final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase(); 168 return hasGrantInternal(db, uid, alias); 169 } 170 setGrant(final int uid, final String alias, final boolean value)171 public void setGrant(final int uid, final String alias, final boolean value) { 172 final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase(); 173 if (value) { 174 if (!hasGrantInternal(db, uid, alias)) { 175 final ContentValues values = new ContentValues(); 176 values.put(GRANTS_ALIAS, alias); 177 values.put(GRANTS_GRANTEE_UID, uid); 178 db.insert(TABLE_GRANTS, GRANTS_ALIAS, values); 179 } 180 } else { 181 db.delete( 182 TABLE_GRANTS, 183 SELECT_GRANTS_BY_UID_AND_ALIAS, 184 new String[] {String.valueOf(uid), alias}); 185 } 186 } 187 removeAliasInformation(String alias)188 public void removeAliasInformation(String alias) { 189 final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase(); 190 db.delete(TABLE_GRANTS, SELECTION_GRANTS_BY_ALIAS, new String[] {alias}); 191 db.delete(TABLE_SELECTABLE, SELECTION_GRANTS_BY_ALIAS, new String[] {alias}); 192 } 193 removeAllAliasesInformation()194 public void removeAllAliasesInformation() { 195 final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase(); 196 db.delete(TABLE_GRANTS, null /* whereClause */, null /* whereArgs */); 197 db.delete(TABLE_SELECTABLE, null /* whereClause */, null /* whereArgs */); 198 } 199 purgeOldGrants(PackageManager pm)200 public void purgeOldGrants(PackageManager pm) { 201 final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase(); 202 db.beginTransaction(); 203 try (Cursor cursor = db.query( 204 TABLE_GRANTS, 205 new String[] {GRANTS_GRANTEE_UID}, null, null, GRANTS_GRANTEE_UID, null, null)) { 206 while ((cursor != null) && (cursor.moveToNext())) { 207 final int uid = cursor.getInt(0); 208 final boolean packageExists = pm.getPackagesForUid(uid) != null; 209 if (packageExists) { 210 continue; 211 } 212 Log.d(TAG, String.format( 213 "deleting grants for UID %d because its package is no longer installed", 214 uid)); 215 db.delete( 216 TABLE_GRANTS, 217 SELECTION_GRANTS_BY_UID, 218 new String[] {Integer.toString(uid)}); 219 } 220 db.setTransactionSuccessful(); 221 } 222 223 db.endTransaction(); 224 } 225 setIsUserSelectable(final String alias, final boolean userSelectable)226 public void setIsUserSelectable(final String alias, final boolean userSelectable) { 227 final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase(); 228 final ContentValues values = new ContentValues(); 229 values.put(GRANTS_ALIAS, alias); 230 values.put(SELECTABLE_IS_SELECTABLE, Boolean.toString(userSelectable)); 231 232 db.replace(TABLE_SELECTABLE, null, values); 233 } 234 isUserSelectable(final String alias)235 public boolean isUserSelectable(final String alias) { 236 final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase(); 237 try (Cursor res = 238 db.query( 239 TABLE_SELECTABLE, 240 new String[] {SELECTABLE_IS_SELECTABLE}, 241 SELECTION_GRANTS_BY_ALIAS, 242 new String[] {alias}, 243 null /* group by */, 244 null /* having */, 245 null /* order by */)) { 246 if (res == null || !res.moveToNext()) { 247 return false; 248 } 249 250 boolean isSelectable = Boolean.parseBoolean(res.getString(0)); 251 if (res.getCount() > 1) { 252 // BUG! Should not have more than one result for any given alias. 253 Log.w(TAG, String.format("Have more than one result for alias %s", alias)); 254 } 255 return isSelectable; 256 } 257 } 258 } 259