1 /* 2 * Copyright (C) 2023 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.car.remoteaccess; 18 19 import android.annotation.Nullable; 20 import android.car.builtin.util.Slogf; 21 import android.content.ContentValues; 22 import android.content.Context; 23 import android.database.Cursor; 24 import android.database.SQLException; 25 import android.database.sqlite.SQLiteDatabase; 26 import android.database.sqlite.SQLiteOpenHelper; 27 28 import com.android.car.CarServiceUtils; 29 import com.android.car.CarServiceUtils.EncryptedData; 30 import com.android.car.systeminterface.SystemInterface; 31 import com.android.internal.annotations.VisibleForTesting; 32 import com.android.internal.util.Preconditions; 33 34 import java.io.File; 35 import java.io.UnsupportedEncodingException; 36 import java.util.ArrayList; 37 import java.util.List; 38 import java.util.Objects; 39 40 final class RemoteAccessStorage { 41 42 private static final String TAG = RemoteAccessStorage.class.getSimpleName(); 43 private static final String REMOTE_ACCESS_KEY_ALIAS = "REMOTE_ACCESS_KEY_ALIAS"; 44 45 private static String sKeyAlias = REMOTE_ACCESS_KEY_ALIAS; 46 47 private final RemoteAccessDbHelper mDbHelper; 48 RemoteAccessStorage(Context context, SystemInterface systemInterface, boolean inMemoryStorage)49 RemoteAccessStorage(Context context, SystemInterface systemInterface, boolean inMemoryStorage) { 50 mDbHelper = new RemoteAccessDbHelper(context, 51 systemInterface.getSystemCarDir().getAbsolutePath(), inMemoryStorage); 52 } 53 release()54 void release() { 55 mDbHelper.close(); 56 } 57 58 @Nullable getClientIdEntry(String packageName)59 ClientIdEntry getClientIdEntry(String packageName) { 60 return ClientIdTable.queryClientIdEntry(mDbHelper.getReadableDatabase(), packageName); 61 } 62 63 @Nullable getClientIdEntries()64 List<ClientIdEntry> getClientIdEntries() { 65 return ClientIdTable.queryClientIdEntries(mDbHelper.getReadableDatabase()); 66 } 67 updateClientId(ClientIdEntry entry)68 boolean updateClientId(ClientIdEntry entry) { 69 SQLiteDatabase db = mDbHelper.getWritableDatabase(); 70 try { 71 db.beginTransaction(); 72 if (!ClientIdTable.replaceEntry(db, entry)) { 73 return false; 74 } 75 db.setTransactionSuccessful(); 76 return true; 77 } finally { 78 db.endTransaction(); 79 } 80 } 81 deleteClientId(String clientId)82 boolean deleteClientId(String clientId) { 83 // TODO(b/260523566): Implement the logic. 84 return false; 85 } 86 87 @VisibleForTesting setKeyAlias(String keyAlias)88 static void setKeyAlias(String keyAlias) { 89 sKeyAlias = keyAlias; 90 } 91 92 // Defines the client token entry stored in the ClientIdTable. 93 static final class ClientIdEntry { 94 public final String clientId; 95 public final long idCreationTime; 96 public final String uidName; 97 ClientIdEntry(String clientId, long idCreationTime, String uidName)98 ClientIdEntry(String clientId, long idCreationTime, String uidName) { 99 Preconditions.checkArgument(uidName != null, "uidName cannot be null"); 100 this.clientId = clientId; 101 this.idCreationTime = idCreationTime; 102 this.uidName = uidName; 103 } 104 105 @Override equals(Object obj)106 public boolean equals(Object obj) { 107 if (obj == this) { 108 return true; 109 } 110 if (!(obj instanceof ClientIdEntry)) { 111 return false; 112 } 113 ClientIdEntry other = (ClientIdEntry) obj; 114 return clientId.equals(other.clientId) && idCreationTime == other.idCreationTime 115 && uidName.equals(other.uidName); 116 } 117 118 @Override hashCode()119 public int hashCode() { 120 return Objects.hash(clientId, idCreationTime, uidName); 121 } 122 123 @Override toString()124 public String toString() { 125 return new StringBuilder().append("ClientIdEntry{clientId: ").append(clientId) 126 .append(", idCreationTime: ").append(idCreationTime).append(", uidName: ") 127 .append(uidName).append('}').toString(); 128 } 129 } 130 131 /** 132 * Defines the contents and queries for the client ID table. 133 */ 134 static final class ClientIdTable { 135 // TODO(b/266371728): Add columns to handle client ID expiration. 136 public static final String TABLE_NAME = "client_token_table"; 137 public static final String INDEX_NAME = "index_package_name"; 138 public static final String COLUMN_CLIENT_ID = "client_id"; 139 public static final String COLUMN_CLIENT_ID_CREATION_TIME = "id_creation_time"; 140 public static final String COLUMN_UID_NAME = "uid_name"; 141 public static final String COLUMN_SECRET_KEY_IV = "secret_key_iv"; 142 143 private static final String STRING_ENCODING = "UTF-8"; 144 createDb(SQLiteDatabase db)145 public static void createDb(SQLiteDatabase db) { 146 StringBuilder createCommand = new StringBuilder(); 147 createCommand.append("CREATE TABLE ").append(TABLE_NAME).append(" (") 148 .append(COLUMN_UID_NAME).append(" TEXT NOT NULL PRIMARY KEY, ") 149 .append(COLUMN_CLIENT_ID).append(" BLOB NOT NULL, ") 150 .append(COLUMN_CLIENT_ID_CREATION_TIME).append(" BIGINT NOT NULL, ") 151 .append(COLUMN_SECRET_KEY_IV).append(" BLOB NOT NULL") 152 .append(")"); 153 db.execSQL(createCommand.toString()); 154 Slogf.i(TAG, "%s table is successfully created in the %s database (version %d)", 155 TABLE_NAME, RemoteAccessDbHelper.DATABASE_NAME, 156 RemoteAccessDbHelper.DATABASE_VERSION); 157 } 158 159 // Returns ClientIdEntry for the given package name. 160 @SuppressWarnings({"FormatString", "StringCharset"}) 161 @Nullable queryClientIdEntry(SQLiteDatabase db, String packageName)162 public static ClientIdEntry queryClientIdEntry(SQLiteDatabase db, String packageName) { 163 String queryCommand = String.format("SELECT %s, %s, %s, %s FROM %s WHERE %s = ?", 164 COLUMN_UID_NAME, COLUMN_CLIENT_ID, COLUMN_CLIENT_ID_CREATION_TIME, 165 COLUMN_SECRET_KEY_IV, TABLE_NAME, COLUMN_UID_NAME); 166 String[] selectionArgs = new String[]{packageName}; 167 168 try (Cursor cursor = db.rawQuery(queryCommand, selectionArgs)) { 169 while (cursor.moveToNext()) { 170 // Query by a primary key must return one result. 171 byte[] data = CarServiceUtils.decryptData(new EncryptedData(cursor.getBlob(1), 172 cursor.getBlob(3)), sKeyAlias); 173 if (data == null) { 174 Slogf.e(TAG, "Failed to query package name(%s): cannot decrypt data", 175 packageName); 176 return null; 177 } 178 try { 179 return new ClientIdEntry(new String(data, STRING_ENCODING), 180 cursor.getLong(2), cursor.getString(0)); 181 } catch (UnsupportedEncodingException e) { 182 Slogf.e(TAG, e, "Failed to query package name(%s)", packageName); 183 return null; 184 } 185 } 186 } 187 Slogf.w(TAG, "No entry for package name(%s) is found", packageName); 188 return null; 189 } 190 191 // Returns all client ID entries. 192 @SuppressWarnings("FormatString") 193 @Nullable queryClientIdEntries(SQLiteDatabase db)194 public static List<ClientIdEntry> queryClientIdEntries(SQLiteDatabase db) { 195 String queryCommand = String.format("SELECT %s, %s, %s, %s FROM %s", 196 COLUMN_UID_NAME, COLUMN_CLIENT_ID, COLUMN_CLIENT_ID_CREATION_TIME, 197 COLUMN_SECRET_KEY_IV, TABLE_NAME); 198 199 try (Cursor cursor = db.rawQuery(queryCommand, new String[]{})) { 200 int entryCount = cursor.getCount(); 201 if (entryCount == 0) return null; 202 List<ClientIdEntry> entries = new ArrayList<>(entryCount); 203 while (cursor.moveToNext()) { 204 byte[] data = CarServiceUtils.decryptData(new EncryptedData(cursor.getBlob(1), 205 cursor.getBlob(3)), sKeyAlias); 206 if (data == null) { 207 Slogf.e(TAG, "Failed to query all client IDs: cannot decrypt data"); 208 return null; 209 } 210 try { 211 entries.add(new ClientIdEntry(new String(data, STRING_ENCODING), 212 cursor.getLong(2), cursor.getString(0))); 213 } catch (UnsupportedEncodingException e) { 214 Slogf.e(TAG, "Failed to query all client IDs", e); 215 return null; 216 } 217 } 218 return entries; 219 } 220 } 221 replaceEntry(SQLiteDatabase db, ClientIdEntry entry)222 public static boolean replaceEntry(SQLiteDatabase db, ClientIdEntry entry) { 223 EncryptedData data; 224 try { 225 data = CarServiceUtils.encryptData(entry.clientId.getBytes(STRING_ENCODING), 226 sKeyAlias); 227 } catch (UnsupportedEncodingException e) { 228 Slogf.e(TAG, e, "Failed to replace %s entry[%s]", TABLE_NAME, entry); 229 return false; 230 } 231 if (data == null) { 232 Slogf.e(TAG, "Failed to replace %s entry[%s]: cannot encrypt client ID", 233 TABLE_NAME, entry); 234 return false; 235 } 236 ContentValues values = new ContentValues(); 237 values.put(COLUMN_UID_NAME, entry.uidName); 238 values.put(COLUMN_CLIENT_ID, data.getEncryptedData()); 239 values.put(COLUMN_CLIENT_ID_CREATION_TIME, entry.idCreationTime); 240 values.put(COLUMN_SECRET_KEY_IV, data.getIv()); 241 242 try { 243 if (db.replaceOrThrow(ClientIdTable.TABLE_NAME, /* nullColumnHack= */ null, 244 values) == -1) { 245 Slogf.e(TAG, "Failed to replace %s entry [%s]", TABLE_NAME, entry); 246 return false; 247 } 248 } catch (SQLException e) { 249 Slogf.e(TAG, e, "Failed to replace %s entry [%s]", TABLE_NAME, entry); 250 return false; 251 } 252 return true; 253 } 254 } 255 256 static final class RemoteAccessDbHelper extends SQLiteOpenHelper { 257 public static final String DATABASE_NAME = "car_remoteaccess.db"; 258 259 private static final int DATABASE_VERSION = 1; 260 getName(String systemCarDirPath, boolean inMemoryStorage)261 private static String getName(String systemCarDirPath, boolean inMemoryStorage) { 262 return inMemoryStorage ? null : new File(systemCarDirPath, DATABASE_NAME) 263 .getAbsolutePath(); 264 } 265 RemoteAccessDbHelper(Context context, String systemCarDirPath, boolean inMemoryStorage)266 RemoteAccessDbHelper(Context context, String systemCarDirPath, boolean inMemoryStorage) { 267 super(context.createDeviceProtectedStorageContext(), 268 getName(systemCarDirPath, inMemoryStorage), /* factory= */ null, 269 DATABASE_VERSION); 270 } 271 272 @Override onCreate(SQLiteDatabase db)273 public void onCreate(SQLiteDatabase db) { 274 ClientIdTable.createDb(db); 275 } 276 277 @Override onConfigure(SQLiteDatabase db)278 public void onConfigure(SQLiteDatabase db) { 279 } 280 281 @Override onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion)282 public void onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion) { 283 // Do nothing. We have only one version. 284 } 285 } 286 } 287