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