1 /* 2 * Copyright 2020 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.bluetooth.btservice.bluetoothkeystore; 18 19 import android.annotation.Nullable; 20 import android.os.SystemProperties; 21 import android.security.keystore.KeyGenParameterSpec; 22 import android.security.keystore.KeyProperties; 23 import android.util.Log; 24 25 import com.android.bluetooth.BluetoothKeystoreProto; 26 import com.android.internal.annotations.VisibleForTesting; 27 28 import com.google.protobuf.ByteString; 29 30 import java.io.IOException; 31 import java.io.InputStream; 32 import java.nio.file.Files; 33 import java.nio.file.Paths; 34 import java.security.InvalidAlgorithmParameterException; 35 import java.security.InvalidKeyException; 36 import java.security.KeyStore; 37 import java.security.KeyStoreException; 38 import java.security.MessageDigest; 39 import java.security.NoSuchAlgorithmException; 40 import java.security.NoSuchProviderException; 41 import java.security.ProviderException; 42 import java.security.UnrecoverableEntryException; 43 import java.security.cert.CertificateException; 44 import java.util.ArrayList; 45 import java.util.Base64; 46 import java.util.HashMap; 47 import java.util.List; 48 import java.util.Map; 49 import java.util.Objects; 50 import java.util.concurrent.BlockingQueue; 51 import java.util.concurrent.LinkedBlockingQueue; 52 53 import javax.crypto.BadPaddingException; 54 import javax.crypto.Cipher; 55 import javax.crypto.IllegalBlockSizeException; 56 import javax.crypto.KeyGenerator; 57 import javax.crypto.NoSuchPaddingException; 58 import javax.crypto.SecretKey; 59 import javax.crypto.spec.GCMParameterSpec; 60 61 /** 62 * Service used for handling encryption and decryption of the bt_config.conf 63 */ 64 public class BluetoothKeystoreService { 65 private static final String TAG = "BluetoothKeystoreService"; 66 67 private static final boolean DBG = false; 68 69 private static BluetoothKeystoreService sBluetoothKeystoreService; 70 private boolean mCleaningUp; 71 private boolean mIsCommonCriteriaMode; 72 73 private static final String CIPHER_ALGORITHM = "AES/GCM/NoPadding"; 74 private static final int GCM_TAG_LENGTH = 128; 75 private static final int KEY_LENGTH = 256; 76 private static final String KEYALIAS = "bluetooth-key-encrypted"; 77 private static final String KEY_STORE = "AndroidKeyStore"; 78 private static final int TRY_MAX = 3; 79 80 private static final String CONFIG_FILE_PREFIX = "bt_config-origin"; 81 private static final String CONFIG_BACKUP_PREFIX = "bt_config-backup"; 82 83 private static final String CONFIG_FILE_HASH = "hash"; 84 85 private static final String CONFIG_CHECKSUM_ENCRYPTION_PATH = 86 "/data/misc/bluedroid/bt_config.checksum.encrypted"; 87 private static final String CONFIG_FILE_ENCRYPTION_PATH = 88 "/data/misc/bluedroid/bt_config.conf.encrypted"; 89 private static final String CONFIG_BACKUP_ENCRYPTION_PATH = 90 "/data/misc/bluedroid/bt_config.bak.encrypted"; 91 92 private static final String CONFIG_FILE_PATH = "/data/misc/bluedroid/bt_config.conf"; 93 private static final String CONFIG_BACKUP_PATH = "/data/misc/bluedroid/bt_config.bak"; 94 95 private static final int BUFFER_SIZE = 400 * 10; 96 97 private static final int CONFIG_COMPARE_INIT = 0b00; 98 private static final int CONFIG_FILE_COMPARE_PASS = 0b01; 99 private static final int CONFIG_BACKUP_COMPARE_PASS = 0b10; 100 private int mCompareResult; 101 102 BluetoothKeystoreNativeInterface mBluetoothKeystoreNativeInterface; 103 104 private ComputeDataThread mEncryptDataThread; 105 private ComputeDataThread mDecryptDataThread; 106 private Map<String, String> mNameEncryptKey = new HashMap<>(); 107 private Map<String, String> mNameDecryptKey = new HashMap<>(); 108 private BlockingQueue<String> mPendingDecryptKey = new LinkedBlockingQueue<>(); 109 private BlockingQueue<String> mPendingEncryptKey = new LinkedBlockingQueue<>(); 110 private final List<String> mEncryptKeyNameList = List.of("LinkKey", "LE_KEY_PENC", "LE_KEY_PID", 111 "LE_KEY_LID", "LE_KEY_PCSRK", "LE_KEY_LENC", "LE_KEY_LCSRK"); 112 113 private Base64.Decoder mDecoder = Base64.getDecoder(); 114 private Base64.Encoder mEncoder = Base64.getEncoder(); 115 BluetoothKeystoreService(boolean isCommonCriteriaMode)116 public BluetoothKeystoreService(boolean isCommonCriteriaMode) { 117 debugLog("new BluetoothKeystoreService isCommonCriteriaMode: " + isCommonCriteriaMode); 118 mIsCommonCriteriaMode = isCommonCriteriaMode; 119 mCompareResult = CONFIG_COMPARE_INIT; 120 startThread(); 121 } 122 123 /** 124 * Start and initialize the BluetoothKeystoreService 125 */ start()126 public void start() { 127 debugLog("start"); 128 KeyStore keyStore; 129 130 if (sBluetoothKeystoreService != null) { 131 errorLog("start() called twice"); 132 return; 133 } 134 135 keyStore = getKeyStore(); 136 137 // Confirm whether to enable Common Criteria mode for the first time. 138 if (keyStore == null) { 139 debugLog("cannot find the keystore."); 140 return; 141 } 142 143 mBluetoothKeystoreNativeInterface = Objects.requireNonNull( 144 BluetoothKeystoreNativeInterface.getInstance(), 145 "BluetoothKeystoreNativeInterface cannot be null when BluetoothKeystore starts"); 146 147 // Mark service as started 148 setBluetoothKeystoreService(this); 149 150 try { 151 if (!keyStore.containsAlias(KEYALIAS) && mIsCommonCriteriaMode) { 152 infoLog("Enable Common Criteria mode for the first time, pass hash check."); 153 mCompareResult = 0b11; 154 return; 155 } 156 } catch (KeyStoreException e) { 157 reportKeystoreException(e, "cannot find the keystore"); 158 return; 159 } 160 161 loadConfigData(); 162 } 163 164 /** 165 * Factory reset the keystore service. 166 */ factoryReset()167 public void factoryReset() { 168 try { 169 cleanupAll(); 170 } catch (IOException e) { 171 reportBluetoothKeystoreException(e, "IO error while file operating."); 172 } 173 } 174 175 /** 176 * Cleans up the keystore service. 177 */ cleanup()178 public void cleanup() { 179 debugLog("cleanup"); 180 if (mCleaningUp) { 181 debugLog("already doing cleanup"); 182 } 183 184 mCleaningUp = true; 185 186 if (sBluetoothKeystoreService == null) { 187 debugLog("cleanup() called before start()"); 188 return; 189 } 190 // Mark service as stopped 191 setBluetoothKeystoreService(null); 192 193 // Cleanup native interface 194 mBluetoothKeystoreNativeInterface.cleanup(); 195 mBluetoothKeystoreNativeInterface = null; 196 197 if (mIsCommonCriteriaMode) { 198 cleanupForCommonCriteriaModeEnable(); 199 } else { 200 cleanupForCommonCriteriaModeDisable(); 201 } 202 } 203 204 /** 205 * Clean up if Common Criteria mode is enabled. 206 */ 207 @VisibleForTesting cleanupForCommonCriteriaModeEnable()208 public void cleanupForCommonCriteriaModeEnable() { 209 try { 210 setEncryptKeyOrRemoveKey(CONFIG_FILE_PREFIX, CONFIG_FILE_HASH); 211 } catch (InterruptedException e) { 212 reportBluetoothKeystoreException(e, "Interrupted while operating."); 213 } catch (IOException e) { 214 reportBluetoothKeystoreException(e, "IO error while file operating."); 215 } catch (NoSuchAlgorithmException e) { 216 reportBluetoothKeystoreException(e, "encrypt could not find the algorithm: SHA256"); 217 } 218 cleanupMemory(); 219 stopThread(); 220 } 221 222 /** 223 * Clean up if Common Criteria mode is disabled. 224 */ 225 @VisibleForTesting cleanupForCommonCriteriaModeDisable()226 public void cleanupForCommonCriteriaModeDisable() { 227 mNameDecryptKey.clear(); 228 mNameEncryptKey.clear(); 229 } 230 231 /** 232 * Load decryption data from file. 233 */ 234 @VisibleForTesting loadConfigData()235 public void loadConfigData() { 236 try { 237 debugLog("loadConfigData"); 238 239 if (isFactoryReset()) { 240 cleanupAll(); 241 } 242 243 if (Files.exists(Paths.get(CONFIG_CHECKSUM_ENCRYPTION_PATH))) { 244 debugLog("Load encryption file."); 245 // Step2: Load checksum file. 246 loadEncryptionFile(CONFIG_CHECKSUM_ENCRYPTION_PATH, false); 247 // Step3: Compare hash file. 248 if (compareFileHash(CONFIG_FILE_PATH)) { 249 debugLog("bt_config.conf checksum pass."); 250 mCompareResult = mCompareResult | CONFIG_FILE_COMPARE_PASS; 251 } 252 if (compareFileHash(CONFIG_BACKUP_PATH)) { 253 debugLog("bt_config.bak checksum pass."); 254 mCompareResult = mCompareResult | CONFIG_BACKUP_COMPARE_PASS; 255 } 256 // Step4: choose which encryption file loads. 257 if (doesComparePass(CONFIG_FILE_COMPARE_PASS)) { 258 loadEncryptionFile(CONFIG_FILE_ENCRYPTION_PATH, true); 259 } else if (doesComparePass(CONFIG_BACKUP_COMPARE_PASS)) { 260 Files.deleteIfExists(Paths.get(CONFIG_FILE_ENCRYPTION_PATH)); 261 mNameEncryptKey.remove(CONFIG_FILE_PREFIX); 262 loadEncryptionFile(CONFIG_BACKUP_ENCRYPTION_PATH, true); 263 } else { 264 // if the Common Criteria mode is disable, don't show the log. 265 if (mIsCommonCriteriaMode) { 266 debugLog("Config file conf and bak checksum check fail."); 267 } 268 cleanupAll(); 269 return; 270 } 271 } 272 // keep memory data for get decrypted key if Common Criteria mode disable. 273 if (!mIsCommonCriteriaMode) { 274 stopThread(); 275 cleanupFile(); 276 } 277 } catch (IOException e) { 278 reportBluetoothKeystoreException(e, "IO error while file operating."); 279 } catch (InterruptedException e) { 280 reportBluetoothKeystoreException(e, "Interrupted while operating."); 281 } catch (NoSuchAlgorithmException e) { 282 reportBluetoothKeystoreException(e, "could not find the algorithm: SHA256"); 283 } 284 } 285 isFactoryReset()286 private boolean isFactoryReset() { 287 return SystemProperties.getBoolean("persist.bluetooth.factoryreset", false); 288 } 289 290 /** 291 * Init JNI 292 */ initJni()293 public void initJni() { 294 debugLog("initJni()"); 295 // Need to make sure all keys are decrypted. 296 stopThread(); 297 startThread(); 298 // Initialize native interface 299 if (mBluetoothKeystoreNativeInterface != null) { 300 mBluetoothKeystoreNativeInterface.init(); 301 } 302 } 303 isAvailable()304 private boolean isAvailable() { 305 return !mCleaningUp; 306 } 307 308 /** 309 * Get the BluetoothKeystoreService instance 310 */ getBluetoothKeystoreService()311 public static synchronized BluetoothKeystoreService getBluetoothKeystoreService() { 312 if (sBluetoothKeystoreService == null) { 313 debugLog("getBluetoothKeystoreService(): service is NULL"); 314 return null; 315 } 316 317 if (!sBluetoothKeystoreService.isAvailable()) { 318 debugLog("getBluetoothKeystoreService(): service is not available"); 319 return null; 320 } 321 return sBluetoothKeystoreService; 322 } 323 setBluetoothKeystoreService( BluetoothKeystoreService instance)324 private static synchronized void setBluetoothKeystoreService( 325 BluetoothKeystoreService instance) { 326 debugLog("setBluetoothKeystoreService(): set to: " + instance); 327 sBluetoothKeystoreService = instance; 328 } 329 330 /** 331 * Gets result of the checksum comparison 332 */ getCompareResult()333 public int getCompareResult() { 334 debugLog("getCompareResult: " + mCompareResult); 335 return mCompareResult; 336 } 337 338 /** 339 * Sets or removes the encryption key value. 340 * 341 * <p>If the value of decryptedString matches {@link #CONFIG_FILE_HASH} then 342 * read the hash file and decrypt the keys and place them into {@link mPendingEncryptKey} 343 * otherwise cleanup all data and remove the keys. 344 * 345 * @param prefixString key to use 346 * @param decryptedString string to decrypt 347 */ setEncryptKeyOrRemoveKey(String prefixString, String decryptedString)348 public void setEncryptKeyOrRemoveKey(String prefixString, String decryptedString) 349 throws InterruptedException, IOException, NoSuchAlgorithmException { 350 infoLog("setEncryptKeyOrRemoveKey: prefix: " + prefixString); 351 if (prefixString == null || decryptedString == null) { 352 return; 353 } 354 if (prefixString.equals(CONFIG_FILE_PREFIX)) { 355 if (decryptedString.isEmpty()) { 356 cleanupAll(); 357 } else if (decryptedString.equals(CONFIG_FILE_HASH)) { 358 readHashFile(CONFIG_FILE_PATH, CONFIG_FILE_PREFIX); 359 mPendingEncryptKey.put(CONFIG_FILE_PREFIX); 360 readHashFile(CONFIG_BACKUP_PATH, CONFIG_BACKUP_PREFIX); 361 mPendingEncryptKey.put(CONFIG_BACKUP_PREFIX); 362 saveEncryptedKey(); 363 } 364 return; 365 } 366 367 if (decryptedString.isEmpty()) { 368 // clear the item by prefixString. 369 mNameDecryptKey.remove(prefixString); 370 mNameEncryptKey.remove(prefixString); 371 } else { 372 mNameDecryptKey.put(prefixString, decryptedString); 373 mPendingEncryptKey.put(prefixString); 374 } 375 } 376 377 /** 378 * Clean up memory and all files. 379 */ 380 @VisibleForTesting cleanupAll()381 public void cleanupAll() throws IOException { 382 cleanupFile(); 383 cleanupMemory(); 384 } 385 cleanupFile()386 private void cleanupFile() throws IOException { 387 Files.deleteIfExists(Paths.get(CONFIG_CHECKSUM_ENCRYPTION_PATH)); 388 Files.deleteIfExists(Paths.get(CONFIG_FILE_ENCRYPTION_PATH)); 389 Files.deleteIfExists(Paths.get(CONFIG_BACKUP_ENCRYPTION_PATH)); 390 } 391 392 /** 393 * Clean up memory. 394 */ 395 @VisibleForTesting cleanupMemory()396 public void cleanupMemory() { 397 stopThread(); 398 mNameEncryptKey.clear(); 399 mNameDecryptKey.clear(); 400 startThread(); 401 } 402 403 /** 404 * Stop encrypt/decrypt thread. 405 */ 406 @VisibleForTesting stopThread()407 public void stopThread() { 408 try { 409 if (mEncryptDataThread != null) { 410 mEncryptDataThread.setWaitQueueEmptyForStop(); 411 mEncryptDataThread.join(); 412 } 413 if (mDecryptDataThread != null) { 414 mDecryptDataThread.setWaitQueueEmptyForStop(); 415 mDecryptDataThread.join(); 416 } 417 } catch (InterruptedException e) { 418 reportBluetoothKeystoreException(e, "Interrupted while operating."); 419 } 420 } 421 startThread()422 private void startThread() { 423 mEncryptDataThread = new ComputeDataThread(true); 424 mDecryptDataThread = new ComputeDataThread(false); 425 mEncryptDataThread.start(); 426 mDecryptDataThread.start(); 427 } 428 429 /** 430 * Get key value from the mNameDecryptKey. 431 */ getKey(String prefixString)432 public String getKey(String prefixString) { 433 infoLog("getKey: prefix: " + prefixString); 434 if (!mNameDecryptKey.containsKey(prefixString)) { 435 return null; 436 } 437 438 return mNameDecryptKey.get(prefixString); 439 } 440 441 /** 442 * Save encryption key into the encryption file. 443 */ 444 @VisibleForTesting saveEncryptedKey()445 public void saveEncryptedKey() { 446 stopThread(); 447 List<String> configEncryptedLines = new ArrayList<>(); 448 List<String> keyEncryptedLines = new ArrayList<>(); 449 for (String key : mNameEncryptKey.keySet()) { 450 if (key.equals(CONFIG_FILE_PREFIX) || key.equals(CONFIG_BACKUP_PREFIX)) { 451 configEncryptedLines.add(getEncryptedKeyData(key)); 452 } else { 453 keyEncryptedLines.add(getEncryptedKeyData(key)); 454 } 455 } 456 startThread(); 457 458 try { 459 if (!configEncryptedLines.isEmpty()) { 460 Files.write(Paths.get(CONFIG_CHECKSUM_ENCRYPTION_PATH), configEncryptedLines); 461 } 462 if (!keyEncryptedLines.isEmpty()) { 463 Files.write(Paths.get(CONFIG_FILE_ENCRYPTION_PATH), keyEncryptedLines); 464 Files.write(Paths.get(CONFIG_BACKUP_ENCRYPTION_PATH), keyEncryptedLines); 465 } 466 } catch (IOException e) { 467 throw new RuntimeException("write encryption file fail"); 468 } 469 } 470 getEncryptedKeyData(String prefixString)471 private String getEncryptedKeyData(String prefixString) { 472 if (prefixString == null) { 473 return null; 474 } 475 return prefixString.concat("-").concat(mNameEncryptKey.get(prefixString)); 476 } 477 478 /* 479 * Get the mNameEncryptKey hashMap. 480 */ 481 @VisibleForTesting getNameEncryptKey()482 public Map<String, String> getNameEncryptKey() { 483 return mNameEncryptKey; 484 } 485 486 /* 487 * Get the mNameDecryptKey hashMap. 488 */ 489 @VisibleForTesting getNameDecryptKey()490 public Map<String, String> getNameDecryptKey() { 491 return mNameDecryptKey; 492 } 493 doesComparePass(int item)494 private boolean doesComparePass(int item) { 495 return (mCompareResult & item) == item; 496 } 497 498 /** 499 * Compare config file checksum. 500 */ 501 @VisibleForTesting compareFileHash(String hashFilePathString)502 public boolean compareFileHash(String hashFilePathString) 503 throws InterruptedException, IOException, NoSuchAlgorithmException { 504 if (!Files.exists(Paths.get(hashFilePathString))) { 505 infoLog("compareFileHash: File does not exist, path: " + hashFilePathString); 506 return false; 507 } 508 509 String prefixString = null; 510 if (CONFIG_FILE_PATH.equals(hashFilePathString)) { 511 prefixString = CONFIG_FILE_PREFIX; 512 } else if (CONFIG_BACKUP_PATH.equals(hashFilePathString)) { 513 prefixString = CONFIG_BACKUP_PREFIX; 514 } 515 if (prefixString == null) { 516 errorLog("compareFileHash: Unexpected hash file path: " + hashFilePathString); 517 return false; 518 } 519 520 readHashFile(hashFilePathString, prefixString); 521 522 if (!mNameEncryptKey.containsKey(prefixString)) { 523 errorLog("compareFileHash: NameEncryptKey doesn't contain the key, prefix:" 524 + prefixString); 525 return false; 526 } 527 String encryptedData = mNameEncryptKey.get(prefixString); 528 String decryptedData = tryCompute(encryptedData, false); 529 if (decryptedData == null) { 530 errorLog("compareFileHash: decrypt encrypted hash data fail, prefix: " + prefixString); 531 return false; 532 } 533 534 return decryptedData.equals(mNameDecryptKey.get(prefixString)); 535 } 536 readHashFile(String filePathString, String prefixString)537 private void readHashFile(String filePathString, String prefixString) 538 throws InterruptedException, NoSuchAlgorithmException { 539 byte[] dataBuffer = new byte[BUFFER_SIZE]; 540 int bytesRead = 0; 541 boolean successful = false; 542 int counter = 0; 543 while (!successful && counter < TRY_MAX) { 544 try { 545 MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); 546 InputStream fileStream = Files.newInputStream(Paths.get(filePathString)); 547 while ((bytesRead = fileStream.read(dataBuffer)) != -1) { 548 messageDigest.update(dataBuffer, 0, bytesRead); 549 } 550 551 byte[] messageDigestBytes = messageDigest.digest(); 552 StringBuilder hashString = new StringBuilder(); 553 for (int index = 0; index < messageDigestBytes.length; index++) { 554 hashString.append(Integer.toString(( 555 messageDigestBytes[index] & 0xff) + 0x100, 16).substring(1)); 556 } 557 558 mNameDecryptKey.put(prefixString, hashString.toString()); 559 successful = true; 560 } catch (IOException e) { 561 infoLog("Fail to open file, try again. counter: " + counter); 562 Thread.sleep(50); 563 counter++; 564 } 565 } 566 if (counter > 3) { 567 errorLog("Fail to open file"); 568 } 569 } 570 571 /** 572 * Parses a file to search for the key and put it into the pending compute queue 573 */ 574 @VisibleForTesting parseConfigFile(String filePathString)575 public void parseConfigFile(String filePathString) 576 throws IOException, InterruptedException { 577 String prefixString = null; 578 String dataString = null; 579 String name = null; 580 String key = null; 581 int index; 582 583 if (!Files.exists(Paths.get(filePathString))) { 584 return; 585 } 586 List<String> allLinesString = Files.readAllLines(Paths.get(filePathString)); 587 for (String line : allLinesString) { 588 if (line.startsWith("[")) { 589 name = line.replace("[", "").replace("]", ""); 590 continue; 591 } 592 593 index = line.indexOf(" = "); 594 if (index < 0) { 595 continue; 596 } 597 key = line.substring(0, index); 598 599 if (!mEncryptKeyNameList.contains(key)) { 600 continue; 601 } 602 603 if (name == null) { 604 continue; 605 } 606 607 prefixString = name + "-" + key; 608 dataString = line.substring(index + 3); 609 if (dataString.length() == 0) { 610 continue; 611 } 612 613 mNameDecryptKey.put(prefixString, dataString); 614 mPendingEncryptKey.put(prefixString); 615 } 616 } 617 618 /** 619 * Load encryption file and push into mNameEncryptKey and pendingDecryptKey. 620 */ 621 @VisibleForTesting loadEncryptionFile(String filePathString, boolean doDecrypt)622 public void loadEncryptionFile(String filePathString, boolean doDecrypt) 623 throws InterruptedException { 624 try { 625 if (!Files.exists(Paths.get(filePathString))) { 626 return; 627 } 628 List<String> allLinesString = Files.readAllLines(Paths.get(filePathString)); 629 for (String line : allLinesString) { 630 int index = line.lastIndexOf("-"); 631 if (index < 0) { 632 continue; 633 } 634 String prefixString = line.substring(0, index); 635 String encryptedString = line.substring(index + 1); 636 637 mNameEncryptKey.put(prefixString, encryptedString); 638 if (doDecrypt) { 639 mPendingDecryptKey.put(prefixString); 640 } 641 } 642 } catch (IOException e) { 643 throw new RuntimeException("read encryption file all line fail"); 644 } 645 } 646 647 // will retry TRY_MAX times. tryCompute(String sourceData, boolean doEncrypt)648 private String tryCompute(String sourceData, boolean doEncrypt) { 649 int counter = 0; 650 String targetData = null; 651 652 if (sourceData == null) { 653 return null; 654 } 655 656 while (targetData == null && counter < TRY_MAX) { 657 if (doEncrypt) { 658 targetData = encrypt(sourceData); 659 } else { 660 targetData = decrypt(sourceData); 661 } 662 counter++; 663 } 664 return targetData; 665 } 666 667 /** 668 * Encrypt the provided data blob. 669 * 670 * @param data String to be encrypted. 671 * @return String as base64. 672 */ encrypt(String data)673 private @Nullable String encrypt(String data) { 674 BluetoothKeystoreProto.EncryptedData protobuf; 675 byte[] outputBytes; 676 String outputBase64 = null; 677 678 try { 679 if (data == null) { 680 errorLog("encrypt: data is null"); 681 return outputBase64; 682 } 683 Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); 684 SecretKey secretKeyReference = getOrCreateSecretKey(); 685 686 if (secretKeyReference != null) { 687 cipher.init(Cipher.ENCRYPT_MODE, secretKeyReference); 688 protobuf = BluetoothKeystoreProto.EncryptedData.newBuilder() 689 .setEncryptedData(ByteString.copyFrom(cipher.doFinal(data.getBytes()))) 690 .setInitVector(ByteString.copyFrom(cipher.getIV())).build(); 691 692 outputBytes = protobuf.toByteArray(); 693 if (outputBytes == null) { 694 errorLog("encrypt: Failed to serialize EncryptedData protobuf."); 695 return outputBase64; 696 } 697 outputBase64 = mEncoder.encodeToString(outputBytes); 698 } else { 699 errorLog("encrypt: secretKeyReference is null."); 700 } 701 } catch (NoSuchAlgorithmException e) { 702 reportKeystoreException(e, "encrypt could not find the algorithm: " + CIPHER_ALGORITHM); 703 } catch (NoSuchPaddingException e) { 704 reportKeystoreException(e, "encrypt had a padding exception"); 705 } catch (InvalidKeyException e) { 706 reportKeystoreException(e, "encrypt received an invalid key"); 707 } catch (BadPaddingException e) { 708 reportKeystoreException(e, "encrypt had a padding problem"); 709 } catch (IllegalBlockSizeException e) { 710 reportKeystoreException(e, "encrypt had an illegal block size"); 711 } 712 return outputBase64; 713 } 714 715 /** 716 * Decrypt the original data blob from the provided {@link EncryptedData}. 717 * 718 * @param encryptedDataBase64 String as base64 to be decrypted. 719 * @return String. 720 */ decrypt(String encryptedDataBase64)721 private @Nullable String decrypt(String encryptedDataBase64) { 722 BluetoothKeystoreProto.EncryptedData protobuf; 723 byte[] encryptedDataBytes; 724 byte[] decryptedDataBytes; 725 String output = null; 726 727 try { 728 if (encryptedDataBase64 == null) { 729 errorLog("decrypt: encryptedDataBase64 is null"); 730 return output; 731 } 732 encryptedDataBytes = mDecoder.decode(encryptedDataBase64); 733 protobuf = BluetoothKeystoreProto.EncryptedData.parser().parseFrom(encryptedDataBytes); 734 Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); 735 GCMParameterSpec spec = 736 new GCMParameterSpec(GCM_TAG_LENGTH, protobuf.getInitVector().toByteArray()); 737 SecretKey secretKeyReference = getOrCreateSecretKey(); 738 739 if (secretKeyReference != null) { 740 cipher.init(Cipher.DECRYPT_MODE, secretKeyReference, spec); 741 decryptedDataBytes = cipher.doFinal(protobuf.getEncryptedData().toByteArray()); 742 output = new String(decryptedDataBytes); 743 } else { 744 errorLog("decrypt: secretKeyReference is null."); 745 } 746 } catch (com.google.protobuf.InvalidProtocolBufferException e) { 747 reportBluetoothKeystoreException(e, "decrypt: Failed to parse EncryptedData protobuf."); 748 } catch (NoSuchAlgorithmException e) { 749 reportKeystoreException(e, 750 "decrypt could not find cipher algorithm " + CIPHER_ALGORITHM); 751 } catch (NoSuchPaddingException e) { 752 reportKeystoreException(e, "decrypt could not find padding algorithm"); 753 } catch (IllegalBlockSizeException e) { 754 reportKeystoreException(e, "decrypt had a illegal block size"); 755 } catch (BadPaddingException e) { 756 reportKeystoreException(e, "decrypt had bad padding"); 757 } catch (InvalidKeyException e) { 758 reportKeystoreException(e, "decrypt had an invalid key"); 759 } catch (InvalidAlgorithmParameterException e) { 760 reportKeystoreException(e, "decrypt had an invalid algorithm parameter"); 761 } 762 return output; 763 } 764 getKeyStore()765 private KeyStore getKeyStore() { 766 KeyStore keyStore = null; 767 int counter = 0; 768 769 while ((counter <= TRY_MAX) && (keyStore == null)) { 770 try { 771 keyStore = KeyStore.getInstance("AndroidKeyStore"); 772 keyStore.load(null); 773 } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException 774 | IOException e) { 775 reportKeystoreException(e, "cannot open keystore"); 776 } 777 counter++; 778 } 779 return keyStore; 780 } 781 782 // The getOrGenerate semantic on keystore is not thread safe, need to synchronized it. getOrCreateSecretKey()783 private synchronized SecretKey getOrCreateSecretKey() { 784 SecretKey secretKey = null; 785 try { 786 KeyStore keyStore = getKeyStore(); 787 if (keyStore.containsAlias(KEYALIAS)) { // The key exists in key store. Get the key. 788 KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry) keyStore 789 .getEntry(KEYALIAS, null); 790 791 if (secretKeyEntry != null) { 792 secretKey = secretKeyEntry.getSecretKey(); 793 } else { 794 errorLog("decrypt: secretKeyEntry is null."); 795 } 796 } else { 797 // The key does not exist in key store. Create the key and store it. 798 KeyGenerator keyGenerator = KeyGenerator 799 .getInstance(KeyProperties.KEY_ALGORITHM_AES, KEY_STORE); 800 801 KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder(KEYALIAS, 802 KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) 803 .setBlockModes(KeyProperties.BLOCK_MODE_GCM) 804 .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) 805 .setKeySize(KEY_LENGTH) 806 .build(); 807 808 keyGenerator.init(keyGenParameterSpec); 809 secretKey = keyGenerator.generateKey(); 810 } 811 } catch (KeyStoreException e) { 812 reportKeystoreException(e, "cannot find the keystore: " + KEY_STORE); 813 } catch (InvalidAlgorithmParameterException e) { 814 reportKeystoreException(e, "getOrCreateSecretKey had an invalid algorithm parameter"); 815 } catch (NoSuchAlgorithmException e) { 816 reportKeystoreException(e, "getOrCreateSecretKey cannot find algorithm"); 817 } catch (NoSuchProviderException e) { 818 reportKeystoreException(e, "getOrCreateSecretKey cannot find crypto provider"); 819 } catch (UnrecoverableEntryException e) { 820 reportKeystoreException(e, 821 "getOrCreateSecretKey had an unrecoverable entry exception."); 822 } catch (ProviderException e) { 823 reportKeystoreException(e, "getOrCreateSecretKey had a provider exception."); 824 } 825 return secretKey; 826 } 827 reportKeystoreException(Exception exception, String error)828 private static void reportKeystoreException(Exception exception, String error) { 829 Log.wtf(TAG, "A keystore error was encountered: " + error, exception); 830 } 831 reportBluetoothKeystoreException(Exception exception, String error)832 private static void reportBluetoothKeystoreException(Exception exception, String error) { 833 Log.wtf(TAG, "A bluetoothkey store error was encountered: " + error, exception); 834 } 835 infoLog(String msg)836 private static void infoLog(String msg) { 837 if (DBG) { 838 Log.i(TAG, msg); 839 } 840 } 841 debugLog(String msg)842 private static void debugLog(String msg) { 843 Log.d(TAG, msg); 844 } 845 errorLog(String msg)846 private static void errorLog(String msg) { 847 Log.e(TAG, msg); 848 } 849 850 /** 851 * A thread that decrypt data if the queue has new decrypt task. 852 */ 853 private class ComputeDataThread extends Thread { 854 private Map<String, String> mSourceDataMap; 855 private Map<String, String> mTargetDataMap; 856 private BlockingQueue<String> mSourceQueue; 857 private boolean mDoEncrypt; 858 859 private boolean mWaitQueueEmptyForStop; 860 ComputeDataThread(boolean doEncrypt)861 ComputeDataThread(boolean doEncrypt) { 862 infoLog("ComputeDataThread: create, doEncrypt: " + doEncrypt); 863 mWaitQueueEmptyForStop = false; 864 mDoEncrypt = doEncrypt; 865 866 if (mDoEncrypt) { 867 mSourceDataMap = mNameDecryptKey; 868 mTargetDataMap = mNameEncryptKey; 869 mSourceQueue = mPendingEncryptKey; 870 } else { 871 mSourceDataMap = mNameEncryptKey; 872 mTargetDataMap = mNameDecryptKey; 873 mSourceQueue = mPendingDecryptKey; 874 } 875 } 876 877 @Override run()878 public void run() { 879 infoLog("ComputeDataThread: run, doEncrypt: " + mDoEncrypt); 880 String prefixString; 881 String sourceData; 882 String targetData; 883 while (!mSourceQueue.isEmpty() || !mWaitQueueEmptyForStop) { 884 try { 885 prefixString = mSourceQueue.take(); 886 if (mSourceDataMap.containsKey(prefixString)) { 887 sourceData = mSourceDataMap.get(prefixString); 888 targetData = tryCompute(sourceData, mDoEncrypt); 889 if (targetData != null) { 890 mTargetDataMap.put(prefixString, targetData); 891 } else { 892 errorLog("Computing of Data failed with prefixString: " + prefixString 893 + ", doEncrypt: " + mDoEncrypt); 894 } 895 } 896 } catch (InterruptedException e) { 897 infoLog("Interrupted while operating."); 898 } 899 } 900 infoLog("ComputeDataThread: Stop, doEncrypt: " + mDoEncrypt); 901 } 902 setWaitQueueEmptyForStop()903 public void setWaitQueueEmptyForStop() { 904 mWaitQueueEmptyForStop = true; 905 if (mPendingEncryptKey.isEmpty()) { 906 interrupt(); 907 } 908 } 909 } 910 } 911