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.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 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 Map<String, String> mNameEncryptKey = new HashMap<>();
96     private Map<String, String> mNameDecryptKey = new HashMap<>();
97     private BlockingQueue<String> mPendingDecryptKey = new LinkedBlockingQueue<>();
98     private 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 Base64.Decoder mDecoder = Base64.getDecoder();
110     private 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 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 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                     hashString.append(
469                             Integer.toString((messageDigestBytes[index] & 0xff) + 0x100, 16)
470                                     .substring(1));
471                 }
472 
473                 mNameDecryptKey.put(prefixString, hashString.toString());
474                 successful = true;
475             } catch (IOException e) {
476                 infoLog("Fail to open file, try again. counter: " + counter);
477                 Thread.sleep(50);
478                 counter++;
479             }
480         }
481         if (counter > 3) {
482             errorLog("Fail to open file");
483         }
484     }
485 
486     /** Parses a file to search for the key and put it into the pending compute queue */
487     @VisibleForTesting
parseConfigFile(String filePathString)488     public void parseConfigFile(String filePathString) throws IOException, InterruptedException {
489         String prefixString = null;
490         String dataString = null;
491         String name = null;
492         String key = null;
493         int index;
494 
495         if (!Files.exists(Paths.get(filePathString))) {
496             return;
497         }
498         List<String> allLinesString = Files.readAllLines(Paths.get(filePathString));
499         for (String line : allLinesString) {
500             if (line.startsWith("[")) {
501                 name = line.replace("[", "").replace("]", "");
502                 continue;
503             }
504 
505             index = line.indexOf(" = ");
506             if (index < 0) {
507                 continue;
508             }
509             key = line.substring(0, index);
510 
511             if (!mEncryptKeyNameList.contains(key)) {
512                 continue;
513             }
514 
515             if (name == null) {
516                 continue;
517             }
518 
519             prefixString = name + "-" + key;
520             dataString = line.substring(index + 3);
521             if (dataString.length() == 0) {
522                 continue;
523             }
524 
525             mNameDecryptKey.put(prefixString, dataString);
526             mPendingEncryptKey.put(prefixString);
527         }
528     }
529 
530     /** Load encryption file and push into mNameEncryptKey and pendingDecryptKey. */
531     @VisibleForTesting
loadEncryptionFile(String filePathString, boolean doDecrypt)532     public void loadEncryptionFile(String filePathString, boolean doDecrypt)
533             throws InterruptedException {
534         try {
535             if (!Files.exists(Paths.get(filePathString))) {
536                 return;
537             }
538             List<String> allLinesString = Files.readAllLines(Paths.get(filePathString));
539             for (String line : allLinesString) {
540                 int index = line.lastIndexOf("-");
541                 if (index < 0) {
542                     continue;
543                 }
544                 String prefixString = line.substring(0, index);
545                 String encryptedString = line.substring(index + 1);
546 
547                 mNameEncryptKey.put(prefixString, encryptedString);
548                 if (doDecrypt) {
549                     mPendingDecryptKey.put(prefixString);
550                 }
551             }
552         } catch (IOException e) {
553             throw new RuntimeException("read encryption file all line fail");
554         }
555     }
556 
557     // will retry TRY_MAX times.
tryCompute(String sourceData, boolean doEncrypt)558     private String tryCompute(String sourceData, boolean doEncrypt) {
559         int counter = 0;
560         String targetData = null;
561 
562         if (sourceData == null) {
563             return null;
564         }
565 
566         while (targetData == null && counter < TRY_MAX) {
567             if (doEncrypt) {
568                 targetData = encrypt(sourceData);
569             } else {
570                 targetData = decrypt(sourceData);
571             }
572             counter++;
573         }
574         return targetData;
575     }
576 
577     /**
578      * Encrypt the provided data blob.
579      *
580      * @param data String to be encrypted.
581      * @return String as base64.
582      */
encrypt(String data)583     private @Nullable String encrypt(String data) {
584         BluetoothKeystoreProto.EncryptedData protobuf;
585         byte[] outputBytes;
586         String outputBase64 = null;
587 
588         try {
589             if (data == null) {
590                 errorLog("encrypt: data is null");
591                 return outputBase64;
592             }
593             Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
594             SecretKey secretKeyReference = getOrCreateSecretKey();
595 
596             if (secretKeyReference != null) {
597                 cipher.init(Cipher.ENCRYPT_MODE, secretKeyReference);
598                 protobuf =
599                         BluetoothKeystoreProto.EncryptedData.newBuilder()
600                                 .setEncryptedData(
601                                         ByteString.copyFrom(cipher.doFinal(data.getBytes())))
602                                 .setInitVector(ByteString.copyFrom(cipher.getIV()))
603                                 .build();
604 
605                 outputBytes = protobuf.toByteArray();
606                 if (outputBytes == null) {
607                     errorLog("encrypt: Failed to serialize EncryptedData protobuf.");
608                     return outputBase64;
609                 }
610                 outputBase64 = mEncoder.encodeToString(outputBytes);
611             } else {
612                 errorLog("encrypt: secretKeyReference is null.");
613             }
614         } catch (NoSuchAlgorithmException e) {
615             reportKeystoreException(e, "encrypt could not find the algorithm: " + CIPHER_ALGORITHM);
616         } catch (NoSuchPaddingException e) {
617             reportKeystoreException(e, "encrypt had a padding exception");
618         } catch (InvalidKeyException e) {
619             reportKeystoreException(e, "encrypt received an invalid key");
620         } catch (BadPaddingException e) {
621             reportKeystoreException(e, "encrypt had a padding problem");
622         } catch (IllegalBlockSizeException e) {
623             reportKeystoreException(e, "encrypt had an illegal block size");
624         }
625         return outputBase64;
626     }
627 
628     /**
629      * Decrypt the original data blob from the provided {@link EncryptedData}.
630      *
631      * @param encryptedDataBase64 String as base64 to be decrypted.
632      * @return String.
633      */
decrypt(String encryptedDataBase64)634     private @Nullable String decrypt(String encryptedDataBase64) {
635         BluetoothKeystoreProto.EncryptedData protobuf;
636         byte[] encryptedDataBytes;
637         byte[] decryptedDataBytes;
638         String output = null;
639 
640         try {
641             if (encryptedDataBase64 == null) {
642                 errorLog("decrypt: encryptedDataBase64 is null");
643                 return output;
644             }
645             encryptedDataBytes = mDecoder.decode(encryptedDataBase64);
646             protobuf = BluetoothKeystoreProto.EncryptedData.parser().parseFrom(encryptedDataBytes);
647             Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
648             GCMParameterSpec spec =
649                     new GCMParameterSpec(GCM_TAG_LENGTH, protobuf.getInitVector().toByteArray());
650             SecretKey secretKeyReference = getOrCreateSecretKey();
651 
652             if (secretKeyReference != null) {
653                 cipher.init(Cipher.DECRYPT_MODE, secretKeyReference, spec);
654                 decryptedDataBytes = cipher.doFinal(protobuf.getEncryptedData().toByteArray());
655                 output = new String(decryptedDataBytes);
656             } else {
657                 errorLog("decrypt: secretKeyReference is null.");
658             }
659         } catch (com.google.protobuf.InvalidProtocolBufferException e) {
660             reportBluetoothKeystoreException(e, "decrypt: Failed to parse EncryptedData protobuf.");
661         } catch (NoSuchAlgorithmException e) {
662             reportKeystoreException(
663                     e, "decrypt could not find cipher algorithm " + CIPHER_ALGORITHM);
664         } catch (NoSuchPaddingException e) {
665             reportKeystoreException(e, "decrypt could not find padding algorithm");
666         } catch (IllegalBlockSizeException e) {
667             reportKeystoreException(e, "decrypt had a illegal block size");
668         } catch (BadPaddingException e) {
669             reportKeystoreException(e, "decrypt had bad padding");
670         } catch (InvalidKeyException e) {
671             reportKeystoreException(e, "decrypt had an invalid key");
672         } catch (InvalidAlgorithmParameterException e) {
673             reportKeystoreException(e, "decrypt had an invalid algorithm parameter");
674         }
675         return output;
676     }
677 
getKeyStore()678     private KeyStore getKeyStore() {
679         KeyStore keyStore = null;
680         int counter = 0;
681 
682         while ((counter <= TRY_MAX) && (keyStore == null)) {
683             try {
684                 keyStore = KeyStore.getInstance("AndroidKeyStore");
685                 keyStore.load(null);
686             } catch (KeyStoreException
687                     | CertificateException
688                     | NoSuchAlgorithmException
689                     | IOException e) {
690                 reportKeystoreException(e, "cannot open keystore");
691             }
692             counter++;
693         }
694         return keyStore;
695     }
696 
697     // The getOrGenerate semantic on keystore is not thread safe, need to synchronized it.
getOrCreateSecretKey()698     private synchronized SecretKey getOrCreateSecretKey() {
699         SecretKey secretKey = null;
700         try {
701             KeyStore keyStore = getKeyStore();
702             if (keyStore.containsAlias(KEYALIAS)) { // The key exists in key store. Get the key.
703                 KeyStore.SecretKeyEntry secretKeyEntry =
704                         (KeyStore.SecretKeyEntry) keyStore.getEntry(KEYALIAS, null);
705 
706                 if (secretKeyEntry != null) {
707                     secretKey = secretKeyEntry.getSecretKey();
708                 } else {
709                     errorLog("decrypt: secretKeyEntry is null.");
710                 }
711             } else {
712                 // The key does not exist in key store. Create the key and store it.
713                 KeyGenerator keyGenerator =
714                         KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEY_STORE);
715 
716                 KeyGenParameterSpec keyGenParameterSpec =
717                         new KeyGenParameterSpec.Builder(
718                                         KEYALIAS,
719                                         KeyProperties.PURPOSE_ENCRYPT
720                                                 | KeyProperties.PURPOSE_DECRYPT)
721                                 .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
722                                 .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
723                                 .setKeySize(KEY_LENGTH)
724                                 .build();
725 
726                 keyGenerator.init(keyGenParameterSpec);
727                 secretKey = keyGenerator.generateKey();
728             }
729         } catch (KeyStoreException e) {
730             reportKeystoreException(e, "cannot find the keystore: " + KEY_STORE);
731         } catch (InvalidAlgorithmParameterException e) {
732             reportKeystoreException(e, "getOrCreateSecretKey had an invalid algorithm parameter");
733         } catch (NoSuchAlgorithmException e) {
734             reportKeystoreException(e, "getOrCreateSecretKey cannot find algorithm");
735         } catch (NoSuchProviderException e) {
736             reportKeystoreException(e, "getOrCreateSecretKey cannot find crypto provider");
737         } catch (UnrecoverableEntryException e) {
738             reportKeystoreException(
739                     e, "getOrCreateSecretKey had an unrecoverable entry exception.");
740         } catch (ProviderException e) {
741             reportKeystoreException(e, "getOrCreateSecretKey had a provider exception.");
742         }
743         return secretKey;
744     }
745 
reportKeystoreException(Exception exception, String error)746     private static void reportKeystoreException(Exception exception, String error) {
747         Log.wtf(TAG, "A keystore error was encountered: " + error, exception);
748     }
749 
reportBluetoothKeystoreException(Exception exception, String error)750     private static void reportBluetoothKeystoreException(Exception exception, String error) {
751         Log.wtf(TAG, "A bluetoothkey store error was encountered: " + error, exception);
752     }
753 
infoLog(String msg)754     private static void infoLog(String msg) {
755         Log.i(TAG, msg);
756     }
757 
debugLog(String msg)758     private static void debugLog(String msg) {
759         Log.d(TAG, msg);
760     }
761 
errorLog(String msg)762     private static void errorLog(String msg) {
763         Log.e(TAG, msg);
764     }
765 
766     /** A thread that decrypt data if the queue has new decrypt task. */
767     private class ComputeDataThread extends Thread {
768         private Map<String, String> mSourceDataMap;
769         private Map<String, String> mTargetDataMap;
770         private BlockingQueue<String> mSourceQueue;
771         private boolean mDoEncrypt;
772 
773         private boolean mWaitQueueEmptyForStop;
774 
ComputeDataThread(boolean doEncrypt)775         ComputeDataThread(boolean doEncrypt) {
776             infoLog("ComputeDataThread: create, doEncrypt: " + doEncrypt);
777             mWaitQueueEmptyForStop = false;
778             mDoEncrypt = doEncrypt;
779 
780             if (mDoEncrypt) {
781                 mSourceDataMap = mNameDecryptKey;
782                 mTargetDataMap = mNameEncryptKey;
783                 mSourceQueue = mPendingEncryptKey;
784             } else {
785                 mSourceDataMap = mNameEncryptKey;
786                 mTargetDataMap = mNameDecryptKey;
787                 mSourceQueue = mPendingDecryptKey;
788             }
789         }
790 
791         @Override
run()792         public void run() {
793             infoLog("ComputeDataThread: run, doEncrypt: " + mDoEncrypt);
794             String prefixString;
795             String sourceData;
796             String targetData;
797             while (!mSourceQueue.isEmpty() || !mWaitQueueEmptyForStop) {
798                 try {
799                     prefixString = mSourceQueue.take();
800                     if (mSourceDataMap.containsKey(prefixString)) {
801                         sourceData = mSourceDataMap.get(prefixString);
802                         targetData = tryCompute(sourceData, mDoEncrypt);
803                         if (targetData != null) {
804                             mTargetDataMap.put(prefixString, targetData);
805                         } else {
806                             errorLog(
807                                     "Computing of Data failed with prefixString: "
808                                             + prefixString
809                                             + ", doEncrypt: "
810                                             + mDoEncrypt);
811                         }
812                     }
813                 } catch (InterruptedException e) {
814                     infoLog("Interrupted while operating.");
815                 }
816             }
817             infoLog("ComputeDataThread: Stop, doEncrypt: " + mDoEncrypt);
818         }
819 
setWaitQueueEmptyForStop()820         public void setWaitQueueEmptyForStop() {
821             mWaitQueueEmptyForStop = true;
822             if (mPendingEncryptKey.isEmpty()) {
823                 interrupt();
824             }
825         }
826     }
827 }
828