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