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