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