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