1 /*
2  * Copyright (C) 2019 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.connecteddevice.storage;
18 
19 import static com.android.car.connecteddevice.util.SafeLog.logd;
20 import static com.android.car.connecteddevice.util.SafeLog.loge;
21 import static com.android.car.connecteddevice.util.SafeLog.logw;
22 
23 import android.annotation.NonNull;
24 import android.annotation.Nullable;
25 import android.app.ActivityManager;
26 import android.content.Context;
27 import android.content.SharedPreferences;
28 import android.security.keystore.KeyGenParameterSpec;
29 import android.security.keystore.KeyProperties;
30 import android.util.Base64;
31 
32 import androidx.room.Room;
33 
34 import com.android.car.connecteddevice.R;
35 import com.android.car.connecteddevice.model.AssociatedDevice;
36 
37 import java.io.IOException;
38 import java.security.InvalidAlgorithmParameterException;
39 import java.security.InvalidKeyException;
40 import java.security.Key;
41 import java.security.KeyStore;
42 import java.security.KeyStoreException;
43 import java.security.NoSuchAlgorithmException;
44 import java.security.NoSuchProviderException;
45 import java.security.UnrecoverableKeyException;
46 import java.security.cert.CertificateException;
47 import java.util.ArrayList;
48 import java.util.List;
49 import java.util.UUID;
50 
51 import javax.crypto.BadPaddingException;
52 import javax.crypto.Cipher;
53 import javax.crypto.IllegalBlockSizeException;
54 import javax.crypto.KeyGenerator;
55 import javax.crypto.NoSuchPaddingException;
56 import javax.crypto.spec.GCMParameterSpec;
57 
58 /** Storage for connected devices in a car. */
59 public class ConnectedDeviceStorage {
60     private static final String TAG = "CompanionStorage";
61 
62     private static final String UNIQUE_ID_KEY = "CTABM_unique_id";
63     private static final String BT_NAME_KEY = "CTABM_bt_name";
64     private static final String KEY_ALIAS = "Ukey2Key";
65     private static final String CIPHER_TRANSFORMATION = "AES/GCM/NoPadding";
66     private static final String KEYSTORE_PROVIDER = "AndroidKeyStore";
67     private static final String DATABASE_NAME = "connected-device-database";
68     private static final String IV_SPEC_SEPARATOR = ";";
69     // This delimiter separates deviceId and deviceInfo, so it has to differ from the
70     // TrustedDeviceInfo delimiter. Once new API can be added, deviceId will be added to
71     // TrustedDeviceInfo and this delimiter will be removed.
72 
73     // The length of the authentication tag for a cipher in GCM mode. The GCM specification states
74     // that this length can only have the values {128, 120, 112, 104, 96}. Using the highest
75     // possible value.
76     private static final int GCM_AUTHENTICATION_TAG_LENGTH = 128;
77 
78     private final Context mContext;
79 
80     private SharedPreferences mSharedPreferences;
81 
82     private UUID mUniqueId;
83 
84     private AssociatedDeviceDao mAssociatedDeviceDatabase;
85 
86     private AssociatedDeviceCallback mAssociatedDeviceCallback;
87 
ConnectedDeviceStorage(@onNull Context context)88     public ConnectedDeviceStorage(@NonNull Context context) {
89         mContext = context;
90         mAssociatedDeviceDatabase = Room.databaseBuilder(context, ConnectedDeviceDatabase.class,
91                 DATABASE_NAME)
92                 .fallbackToDestructiveMigration()
93                 .build()
94                 .associatedDeviceDao();
95     }
96 
97     /**
98      * Set a callback for associated device updates.
99      *
100      * @param callback {@link AssociatedDeviceCallback} to set.
101      */
setAssociatedDeviceCallback( @onNull AssociatedDeviceCallback callback)102     public void setAssociatedDeviceCallback(
103             @NonNull AssociatedDeviceCallback callback) {
104         mAssociatedDeviceCallback = callback;
105     }
106 
107     /** Clear the callback for association device callback updates. */
clearAssociationDeviceCallback()108     public void clearAssociationDeviceCallback() {
109         mAssociatedDeviceCallback = null;
110     }
111 
112     /**
113      * Get communication encryption key for the given device
114      *
115      * @param deviceId id of trusted device
116      * @return encryption key, null if device id is not recognized
117      */
118     @Nullable
getEncryptionKey(@onNull String deviceId)119     public byte[] getEncryptionKey(@NonNull String deviceId) {
120         AssociatedDeviceKeyEntity entity =
121                 mAssociatedDeviceDatabase.getAssociatedDeviceKey(deviceId);
122         if (entity == null) {
123             logd(TAG, "Encryption key not found!");
124             return null;
125         }
126         String[] values = entity.encryptedKey.split(IV_SPEC_SEPARATOR, -1);
127 
128         if (values.length != 2) {
129             logd(TAG, "Stored encryption key had the wrong length.");
130             return null;
131         }
132 
133         byte[] encryptedKey = Base64.decode(values[0], Base64.DEFAULT);
134         byte[] ivSpec = Base64.decode(values[1], Base64.DEFAULT);
135         return decryptWithKeyStore(KEY_ALIAS, encryptedKey, ivSpec);
136     }
137 
138     /**
139      * Save encryption key for the given device
140      *
141      * @param deviceId      did of trusted device
142      * @param encryptionKey encryption key
143      */
saveEncryptionKey(@onNull String deviceId, @NonNull byte[] encryptionKey)144     public void saveEncryptionKey(@NonNull String deviceId, @NonNull byte[] encryptionKey) {
145         String encryptedKey = encryptWithKeyStore(KEY_ALIAS, encryptionKey);
146         AssociatedDeviceKeyEntity entity = new AssociatedDeviceKeyEntity(deviceId, encryptedKey);
147         mAssociatedDeviceDatabase.addOrReplaceAssociatedDeviceKey(entity);
148         logd(TAG, "Successfully wrote encryption key.");
149     }
150 
151     /**
152      * Encrypt value with designated key
153      *
154      * <p>The encrypted value is of the form:
155      *
156      * <p>key + IV_SPEC_SEPARATOR + ivSpec
157      *
158      * <p>The {@code ivSpec} is needed to decrypt this key later on.
159      *
160      * @param keyAlias KeyStore alias for key to use
161      * @param value    a value to encrypt
162      * @return encrypted value, null if unable to encrypt
163      */
164     @Nullable
encryptWithKeyStore(@onNull String keyAlias, @Nullable byte[] value)165     private String encryptWithKeyStore(@NonNull String keyAlias, @Nullable byte[] value) {
166         if (value == null) {
167             logw(TAG, "Received a null key value.");
168             return null;
169         }
170 
171         Key key = getKeyStoreKey(keyAlias);
172         try {
173             Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
174             cipher.init(Cipher.ENCRYPT_MODE, key);
175             return Base64.encodeToString(cipher.doFinal(value), Base64.DEFAULT)
176                     + IV_SPEC_SEPARATOR
177                     + Base64.encodeToString(cipher.getIV(), Base64.DEFAULT);
178         } catch (IllegalBlockSizeException
179                 | BadPaddingException
180                 | NoSuchAlgorithmException
181                 | NoSuchPaddingException
182                 | IllegalStateException
183                 | InvalidKeyException e) {
184             loge(TAG, "Unable to encrypt value with key " + keyAlias, e);
185             return null;
186         }
187     }
188 
189     /**
190      * Decrypt value with designated key
191      *
192      * @param keyAlias KeyStore alias for key to use
193      * @param value    encrypted value
194      * @return decrypted value, null if unable to decrypt
195      */
196     @Nullable
decryptWithKeyStore( @onNull String keyAlias, @Nullable byte[] value, @NonNull byte[] ivSpec)197     private byte[] decryptWithKeyStore(
198             @NonNull String keyAlias, @Nullable byte[] value, @NonNull byte[] ivSpec) {
199         if (value == null) {
200             return null;
201         }
202 
203         try {
204             Key key = getKeyStoreKey(keyAlias);
205             Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
206             cipher.init(
207                     Cipher.DECRYPT_MODE, key,
208                     new GCMParameterSpec(GCM_AUTHENTICATION_TAG_LENGTH, ivSpec));
209             return cipher.doFinal(value);
210         } catch (IllegalBlockSizeException
211                 | BadPaddingException
212                 | NoSuchAlgorithmException
213                 | NoSuchPaddingException
214                 | IllegalStateException
215                 | InvalidKeyException
216                 | InvalidAlgorithmParameterException e) {
217             loge(TAG, "Unable to decrypt value with key " + keyAlias, e);
218             return null;
219         }
220     }
221 
222     @Nullable
getKeyStoreKey(@onNull String keyAlias)223     private static Key getKeyStoreKey(@NonNull String keyAlias) {
224         KeyStore keyStore;
225         try {
226             keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER);
227             keyStore.load(null);
228             if (!keyStore.containsAlias(keyAlias)) {
229                 KeyGenerator keyGenerator =
230                         KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES,
231                                 KEYSTORE_PROVIDER);
232                 keyGenerator.init(
233                         new KeyGenParameterSpec.Builder(
234                                 keyAlias,
235                                 KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
236                                 .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
237                                 .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
238                                 .build());
239                 keyGenerator.generateKey();
240             }
241             return keyStore.getKey(keyAlias, null);
242 
243         } catch (KeyStoreException
244                 | NoSuchAlgorithmException
245                 | UnrecoverableKeyException
246                 | NoSuchProviderException
247                 | CertificateException
248                 | IOException
249                 | InvalidAlgorithmParameterException e) {
250             loge(TAG, "Unable to retrieve key " + keyAlias + " from KeyStore.", e);
251             throw new IllegalStateException(e);
252         }
253     }
254 
255     @NonNull
getSharedPrefs()256     private SharedPreferences getSharedPrefs() {
257         // This should be called only after user 0 is unlocked.
258         if (mSharedPreferences != null) {
259             return mSharedPreferences;
260         }
261         mSharedPreferences = mContext.getSharedPreferences(
262                 mContext.getString(R.string.connected_device_shared_preferences),
263                 Context.MODE_PRIVATE);
264         return mSharedPreferences;
265 
266     }
267 
268     /**
269      * Get the unique id for head unit. Persists on device until factory reset. This should be
270      * called only after user 0 is unlocked.
271      *
272      * @return unique id
273      */
274     @NonNull
getUniqueId()275     public UUID getUniqueId() {
276         if (mUniqueId != null) {
277             return mUniqueId;
278         }
279 
280         SharedPreferences prefs = getSharedPrefs();
281         if (prefs.contains(UNIQUE_ID_KEY)) {
282             mUniqueId = UUID.fromString(prefs.getString(UNIQUE_ID_KEY, null));
283             logd(TAG,
284                     "Found existing trusted unique id: " + prefs.getString(UNIQUE_ID_KEY, ""));
285         }
286 
287         if (mUniqueId == null) {
288             mUniqueId = UUID.randomUUID();
289             prefs.edit().putString(UNIQUE_ID_KEY, mUniqueId.toString()).apply();
290             logd(TAG,
291                     "Generated new trusted unique id: " + prefs.getString(UNIQUE_ID_KEY, ""));
292         }
293 
294         return mUniqueId;
295     }
296 
297     /** Store the current bluetooth adapter name. */
storeBluetoothName(@onNull String name)298     public void storeBluetoothName(@NonNull String name) {
299         getSharedPrefs().edit().putString(BT_NAME_KEY, name).apply();
300     }
301 
302     /** Get the previously stored bluetooth adapter name or {@code null} if not found. */
303     @Nullable
getStoredBluetoothName()304     public String getStoredBluetoothName() {
305         return getSharedPrefs().getString(BT_NAME_KEY, null);
306     }
307 
308     /** Remove the previously stored bluetooth adapter name from storage. */
removeStoredBluetoothName()309     public void removeStoredBluetoothName() {
310         getSharedPrefs().edit().remove(BT_NAME_KEY).apply();
311     }
312 
313     /**
314      * Get a list of associated devices for the given user.
315      *
316      * @param userId The identifier of the user.
317      * @return Associated device list.
318      */
319     @NonNull
getAssociatedDevicesForUser(@onNull int userId)320     public List<AssociatedDevice> getAssociatedDevicesForUser(@NonNull int userId) {
321         List<AssociatedDeviceEntity> entities =
322                 mAssociatedDeviceDatabase.getAssociatedDevicesForUser(userId);
323 
324         if (entities == null) {
325             return new ArrayList<>();
326         }
327 
328         ArrayList<AssociatedDevice> userDevices = new ArrayList<>();
329         for (AssociatedDeviceEntity entity : entities) {
330             userDevices.add(entity.toAssociatedDevice());
331         }
332 
333         return userDevices;
334     }
335 
336     /**
337      * Get a list of associated devices for the current user.
338      *
339      * @return Associated device list.
340      */
341     @NonNull
getActiveUserAssociatedDevices()342     public List<AssociatedDevice> getActiveUserAssociatedDevices() {
343         return getAssociatedDevicesForUser(ActivityManager.getCurrentUser());
344     }
345 
346     /**
347      * Returns a list of device ids of associated devices for the given user.
348      *
349      * @param userId The user id for whom we want to know the device ids.
350      * @return List of device ids.
351      */
352     @NonNull
getAssociatedDeviceIdsForUser(@onNull int userId)353     public List<String> getAssociatedDeviceIdsForUser(@NonNull int userId) {
354         List<AssociatedDevice> userDevices = getAssociatedDevicesForUser(userId);
355         ArrayList<String> userDeviceIds = new ArrayList<>();
356 
357         for (AssociatedDevice device : userDevices) {
358             userDeviceIds.add(device.getDeviceId());
359         }
360 
361         return userDeviceIds;
362     }
363 
364     /**
365      * Returns a list of device ids of associated devices for the current user.
366      *
367      * @return List of device ids.
368      */
369     @NonNull
getActiveUserAssociatedDeviceIds()370     public List<String> getActiveUserAssociatedDeviceIds() {
371         return getAssociatedDeviceIdsForUser(ActivityManager.getCurrentUser());
372     }
373 
374     /**
375      * Add the associated device of the given deviceId for the currently active user.
376      *
377      * @param device New associated device to be added.
378      */
addAssociatedDeviceForActiveUser(@onNull AssociatedDevice device)379     public void addAssociatedDeviceForActiveUser(@NonNull AssociatedDevice device) {
380         addAssociatedDeviceForUser(ActivityManager.getCurrentUser(), device);
381         if (mAssociatedDeviceCallback != null) {
382             mAssociatedDeviceCallback.onAssociatedDeviceAdded(device);
383         }
384     }
385 
386 
387     /**
388      * Add the associated device of the given deviceId for the given user.
389      *
390      * @param userId The identifier of the user.
391      * @param device New associated device to be added.
392      */
addAssociatedDeviceForUser(int userId, @NonNull AssociatedDevice device)393     public void addAssociatedDeviceForUser(int userId, @NonNull AssociatedDevice device) {
394         AssociatedDeviceEntity entity = new AssociatedDeviceEntity(userId, device,
395                 /* isConnectionEnabled= */ true);
396         mAssociatedDeviceDatabase.addOrReplaceAssociatedDevice(entity);
397     }
398 
399     /**
400      * Update the name for an associated device.
401      *
402      * @param deviceId The id of the associated device.
403      * @param name The name to replace with.
404      */
updateAssociatedDeviceName(@onNull String deviceId, @NonNull String name)405     public void updateAssociatedDeviceName(@NonNull String deviceId, @NonNull String name) {
406         AssociatedDeviceEntity entity = mAssociatedDeviceDatabase.getAssociatedDevice(deviceId);
407         if (entity == null) {
408             logw(TAG, "Attempt to update name on an unrecognized device " + deviceId
409                     + ". Ignoring.");
410             return;
411         }
412         entity.name = name;
413         mAssociatedDeviceDatabase.addOrReplaceAssociatedDevice(entity);
414         if (mAssociatedDeviceCallback != null) {
415             mAssociatedDeviceCallback.onAssociatedDeviceUpdated(new AssociatedDevice(deviceId,
416                     entity.address, name, entity.isConnectionEnabled));
417         }
418     }
419 
420     /**
421      * Remove the associated device of the given deviceId for the given user.
422      *
423      * @param userId The identifier of the user.
424      * @param deviceId The identifier of the device to be cleared.
425      */
removeAssociatedDevice(int userId, @NonNull String deviceId)426     public void removeAssociatedDevice(int userId, @NonNull String deviceId) {
427         AssociatedDeviceEntity entity = mAssociatedDeviceDatabase.getAssociatedDevice(deviceId);
428         if (entity == null || entity.userId != userId) {
429             return;
430         }
431         mAssociatedDeviceDatabase.removeAssociatedDevice(entity);
432         if (mAssociatedDeviceCallback != null) {
433             mAssociatedDeviceCallback.onAssociatedDeviceRemoved(new AssociatedDevice(deviceId,
434                     entity.address, entity.name, entity.isConnectionEnabled));
435         }
436     }
437 
438     /**
439      * Clear the associated device of the given deviceId for the current user.
440      *
441      * @param deviceId The identifier of the device to be cleared.
442      */
removeAssociatedDeviceForActiveUser(@onNull String deviceId)443     public void removeAssociatedDeviceForActiveUser(@NonNull String deviceId) {
444         removeAssociatedDevice(ActivityManager.getCurrentUser(), deviceId);
445     }
446 
447     /**
448      * Set if connection is enabled for an associated device.
449      *
450      * @param deviceId The id of the associated device.
451      * @param isConnectionEnabled If connection enabled for this device.
452      */
updateAssociatedDeviceConnectionEnabled(@onNull String deviceId, boolean isConnectionEnabled)453     public void updateAssociatedDeviceConnectionEnabled(@NonNull String deviceId,
454             boolean isConnectionEnabled) {
455         AssociatedDeviceEntity entity = mAssociatedDeviceDatabase.getAssociatedDevice(deviceId);
456         if (entity == null) {
457             logw(TAG, "Attempt to enable or disable connection on an unrecognized device "
458                     + deviceId + ". Ignoring.");
459             return;
460         }
461         if (entity.isConnectionEnabled == isConnectionEnabled) {
462             return;
463         }
464         entity.isConnectionEnabled = isConnectionEnabled;
465         mAssociatedDeviceDatabase.addOrReplaceAssociatedDevice(entity);
466         if (mAssociatedDeviceCallback != null) {
467             mAssociatedDeviceCallback.onAssociatedDeviceUpdated(new AssociatedDevice(deviceId,
468                     entity.address, entity.name, isConnectionEnabled));
469         }
470     }
471 
472     /** Callback for association device related events. */
473     public interface AssociatedDeviceCallback {
474         /** Triggered when an associated device has been added. */
onAssociatedDeviceAdded(@onNull AssociatedDevice device)475         void onAssociatedDeviceAdded(@NonNull AssociatedDevice device);
476 
477         /** Triggered when an associated device has been removed. */
onAssociatedDeviceRemoved(@onNull AssociatedDevice device)478         void onAssociatedDeviceRemoved(@NonNull AssociatedDevice device);
479 
480         /** Triggered when an associated device has been updated. */
onAssociatedDeviceUpdated(@onNull AssociatedDevice device)481         void onAssociatedDeviceUpdated(@NonNull AssociatedDevice device);
482     }
483 }
484