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