Metrics: Add Bluetooth address obfuscator in Java

* Expose Bluetooth address obfuscator to Java layer as
  AdapterService.obfuscateAddress(BluetoothDevice)
* Add unit tests to verify different usage scenarios
* Add unit tests that read metrics salt value from disk and compare
  native layer obfuscation result with Java layer HMAC-SHA256
  calculation result

Bug: 112969790
Test: make, BluetoothInstrumentationTests
Change-Id: Iac151616413073b3602fd61d9e620a932ae0340c
diff --git a/android/app/jni/com_android_bluetooth_btservice_AdapterService.cpp b/android/app/jni/com_android_bluetooth_btservice_AdapterService.cpp
index 2d0960c..c8a9d56 100644
--- a/android/app/jni/com_android_bluetooth_btservice_AdapterService.cpp
+++ b/android/app/jni/com_android_bluetooth_btservice_AdapterService.cpp
@@ -1217,6 +1217,25 @@
   env->ReleaseByteArrayElements(address, addr, 0);
 }
 
+static jbyteArray obfuscateAddressNative(JNIEnv* env, jobject obj,
+                                         jbyteArray address) {
+  ALOGV("%s", __func__);
+  if (!sBluetoothInterface) return env->NewByteArray(0);
+  jbyte* addr = env->GetByteArrayElements(address, nullptr);
+  if (addr == nullptr) {
+    jniThrowIOException(env, EINVAL);
+    return env->NewByteArray(0);
+  }
+  RawAddress addr_obj = {};
+  addr_obj.FromOctets((uint8_t*)addr);
+  std::string output = sBluetoothInterface->obfuscate_address(addr_obj);
+  jsize output_size = output.size() * sizeof(char);
+  jbyteArray output_bytes = env->NewByteArray(output_size);
+  env->SetByteArrayRegion(output_bytes, 0, output_size,
+                          (const jbyte*)output.data());
+  return output_bytes;
+}
+
 static JNINativeMethod sMethods[] = {
     /* name, signature, funcPtr */
     {"classInitNative", "()V", (void*)classInitNative},
@@ -1251,7 +1270,8 @@
     {"dumpMetricsNative", "()[B", (void*)dumpMetricsNative},
     {"factoryResetNative", "()Z", (void*)factoryResetNative},
     {"interopDatabaseClearNative", "()V", (void*)interopDatabaseClearNative},
-    {"interopDatabaseAddNative", "(I[BI)V", (void*)interopDatabaseAddNative}};
+    {"interopDatabaseAddNative", "(I[BI)V", (void*)interopDatabaseAddNative},
+    {"obfuscateAddressNative", "([B)[B", (void*)obfuscateAddressNative}};
 
 int register_com_android_bluetooth_btservice_AdapterService(JNIEnv* env) {
   return jniRegisterNativeMethods(
diff --git a/android/app/src/com/android/bluetooth/btservice/AdapterService.java b/android/app/src/com/android/bluetooth/btservice/AdapterService.java
index 7966da3..04eefa8 100644
--- a/android/app/src/com/android/bluetooth/btservice/AdapterService.java
+++ b/android/app/src/com/android/bluetooth/btservice/AdapterService.java
@@ -72,6 +72,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.IBatteryStats;
 
+import com.google.protobuf.ByteString;
 import com.google.protobuf.InvalidProtocolBufferException;
 
 import java.io.FileDescriptor;
@@ -2608,6 +2609,16 @@
         }
     }
 
+    /**
+     *  Obfuscate Bluetooth MAC address into a PII free ID string
+     *
+     *  @param device Bluetooth device whose MAC address will be obfuscated
+     *  @return a {@link ByteString} that is unique to this MAC address on this device
+     */
+    public ByteString obfuscateAddress(BluetoothDevice device) {
+        return ByteString.copyFrom(obfuscateAddressNative(Utils.getByteAddress(device)));
+    }
+
     static native void classInitNative();
 
     native boolean initNative();
@@ -2691,6 +2702,8 @@
 
     private native void interopDatabaseAddNative(int feature, byte[] address, int length);
 
+    private native byte[] obfuscateAddressNative(byte[] address);
+
     // Returns if this is a mock object. This is currently used in testing so that we may not call
     // System.exit() while finalizing the object. Otherwise GC of mock objects unfortunately ends up
     // calling finalize() which in turn calls System.exit() and the process crashes.
diff --git a/android/app/tests/unit/src/com/android/bluetooth/TestUtils.java b/android/app/tests/unit/src/com/android/bluetooth/TestUtils.java
index d0e3324..4a9054b 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/TestUtils.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/TestUtils.java
@@ -33,9 +33,13 @@
 import org.mockito.ArgumentCaptor;
 import org.mockito.internal.util.MockUtil;
 
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
 import java.lang.reflect.Field;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
+import java.util.HashMap;
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
@@ -271,6 +275,41 @@
     }
 
     /**
+     * Read Bluetooth adapter configuration from the filesystem
+     *
+     * @return A {@link HashMap} of Bluetooth configs in the format:
+     *  section -> key1 -> value1
+     *          -> key2 -> value2
+     *  Assume no empty section name, no duplicate keys in the same section
+     */
+    public static HashMap<String, HashMap<String, String>> readAdapterConfig() {
+        HashMap<String, HashMap<String, String>> adapterConfig = new HashMap<>();
+        try (BufferedReader reader =
+                new BufferedReader(new FileReader("/data/misc/bluedroid/bt_config.conf"))) {
+            String section = "";
+            for (String line; (line = reader.readLine()) != null;) {
+                line = line.trim();
+                if (line.isEmpty() || line.startsWith("#")) {
+                    continue;
+                }
+                if (line.startsWith("[")) {
+                    if (line.charAt(line.length() - 1) != ']') {
+                        return null;
+                    }
+                    section = line.substring(1, line.length() - 1);
+                    adapterConfig.put(section, new HashMap<>());
+                } else {
+                    String[] keyValue = line.split("=");
+                    adapterConfig.get(section).put(keyValue[0].trim(), keyValue[1].trim());
+                }
+            }
+        } catch (IOException e) {
+            return null;
+        }
+        return adapterConfig;
+    }
+
+    /**
      * Helper class used to run synchronously a runnable action on a looper.
      */
     private static final class SyncRunnable implements Runnable {
diff --git a/android/app/tests/unit/src/com/android/bluetooth/btservice/AdapterServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/btservice/AdapterServiceTest.java
index 2b19a20..2b98a03 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/btservice/AdapterServiceTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/btservice/AdapterServiceTest.java
@@ -17,15 +17,11 @@
 package com.android.bluetooth.btservice;
 
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.timeout;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.*;
 
 import android.app.AlarmManager;
 import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
 import android.bluetooth.IBluetoothCallback;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
@@ -42,20 +38,38 @@
 import android.support.test.filters.MediumTest;
 import android.support.test.runner.AndroidJUnit4;
 import android.test.mock.MockContentResolver;
+import android.util.ByteStringUtils;
+import android.util.Log;
 
 import com.android.bluetooth.R;
+import com.android.bluetooth.TestUtils;
+import com.android.bluetooth.Utils;
+
+import com.google.protobuf.ByteString;
 
 import org.junit.After;
 import org.junit.Assert;
 import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.HashMap;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
 @MediumTest
 @RunWith(AndroidJUnit4.class)
 public class AdapterServiceTest {
+    private static final String TAG = AdapterServiceTest.class.getSimpleName();
+
     private AdapterService mAdapterService;
 
     private @Mock Context mMockContext;
@@ -77,6 +91,23 @@
     private PowerManager mPowerManager;
     private PackageManager mMockPackageManager;
     private MockContentResolver mMockContentResolver;
+    private HashMap<String, HashMap<String, String>> mAdapterConfig;
+
+    @BeforeClass
+    public static void setupClass() {
+        // Bring native layer up and down to make sure config files are properly loaded
+        if (Looper.myLooper() == null) {
+            Looper.prepare();
+        }
+        Assert.assertNotNull(Looper.myLooper());
+        AdapterService adapterService = new AdapterService();
+        adapterService.initNative();
+        adapterService.cleanupNative();
+        HashMap<String, HashMap<String, String>> adapterConfig = TestUtils.readAdapterConfig();
+        Assert.assertNotNull(adapterConfig);
+        Assert.assertNotNull("metrics salt is null: " + adapterConfig.toString(),
+                getMetricsSalt(adapterConfig));
+    }
 
     @Before
     public void setUp() throws PackageManager.NameNotFoundException {
@@ -85,12 +116,8 @@
         }
         Assert.assertNotNull(Looper.myLooper());
 
-        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
-            @Override
-            public void run() {
-                mAdapterService = new AdapterService();
-            }
-        });
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(
+                () -> mAdapterService = new AdapterService());
         mMockPackageManager = mock(PackageManager.class);
         mMockContentResolver = new MockContentResolver(mMockContext);
         MockitoAnnotations.initMocks(this);
@@ -128,6 +155,9 @@
         mAdapterService.registerCallback(mIBluetoothCallback);
 
         Config.init(mMockContext);
+
+        mAdapterConfig = TestUtils.readAdapterConfig();
+        Assert.assertNotNull(mAdapterConfig);
     }
 
     @After
@@ -463,4 +493,209 @@
         // Restore earlier setting
         SystemProperties.set(AdapterService.BLUETOOTH_BTSNOOP_ENABLE_PROPERTY, snoopSetting);
     }
+
+    /**
+     * Test: Obfuscate Bluetooth address when Bluetooth is disabled
+     * Check whether the returned value meets expectation
+     */
+    @Test
+    public void testObfuscateBluetoothAddress_BluetoothDisabled() {
+        Assert.assertFalse(mAdapterService.isEnabled());
+        byte[] metricsSalt = getMetricsSalt(mAdapterConfig);
+        Assert.assertNotNull(metricsSalt);
+        BluetoothDevice device = TestUtils.getTestDevice(BluetoothAdapter.getDefaultAdapter(), 0);
+        ByteString obfuscatedAddress = mAdapterService.obfuscateAddress(device);
+        Assert.assertFalse(obfuscatedAddress.isEmpty());
+        Assert.assertFalse(isByteArrayAllZero(obfuscatedAddress.toByteArray()));
+        Assert.assertArrayEquals(obfuscateInJava(metricsSalt, device),
+                obfuscatedAddress.toByteArray());
+    }
+
+    /**
+     * Test: Obfuscate Bluetooth address when Bluetooth is enabled
+     * Check whether the returned value meets expectation
+     */
+    @Test
+    public void testObfuscateBluetoothAddress_BluetoothEnabled() {
+        Assert.assertFalse(mAdapterService.isEnabled());
+        doEnable(0, false);
+        Assert.assertTrue(mAdapterService.isEnabled());
+        byte[] metricsSalt = getMetricsSalt(mAdapterConfig);
+        Assert.assertNotNull(metricsSalt);
+        BluetoothDevice device = TestUtils.getTestDevice(BluetoothAdapter.getDefaultAdapter(), 0);
+        ByteString obfuscatedAddress = mAdapterService.obfuscateAddress(device);
+        Assert.assertFalse(obfuscatedAddress.isEmpty());
+        Assert.assertFalse(isByteArrayAllZero(obfuscatedAddress.toByteArray()));
+        Assert.assertArrayEquals(obfuscateInJava(metricsSalt, device),
+                obfuscatedAddress.toByteArray());
+    }
+
+    /**
+     * Test: Check if obfuscated Bluetooth address stays the same after toggling Bluetooth
+     */
+    @Test
+    public void testObfuscateBluetoothAddress_PersistentBetweenToggle() {
+        Assert.assertFalse(mAdapterService.isEnabled());
+        byte[] metricsSalt = getMetricsSalt(mAdapterConfig);
+        Assert.assertNotNull(metricsSalt);
+        BluetoothDevice device = TestUtils.getTestDevice(BluetoothAdapter.getDefaultAdapter(), 0);
+        ByteString obfuscatedAddress1 = mAdapterService.obfuscateAddress(device);
+        Assert.assertFalse(obfuscatedAddress1.isEmpty());
+        Assert.assertFalse(isByteArrayAllZero(obfuscatedAddress1.toByteArray()));
+        Assert.assertArrayEquals(obfuscateInJava(metricsSalt, device),
+                obfuscatedAddress1.toByteArray());
+        // Enable
+        doEnable(0, false);
+        Assert.assertTrue(mAdapterService.isEnabled());
+        ByteString obfuscatedAddress3 = mAdapterService.obfuscateAddress(device);
+        Assert.assertFalse(obfuscatedAddress3.isEmpty());
+        Assert.assertFalse(isByteArrayAllZero(obfuscatedAddress3.toByteArray()));
+        Assert.assertArrayEquals(obfuscatedAddress3.toByteArray(),
+                obfuscatedAddress1.toByteArray());
+        // Disable
+        doDisable(0, false);
+        Assert.assertFalse(mAdapterService.isEnabled());
+        ByteString obfuscatedAddress4 = mAdapterService.obfuscateAddress(device);
+        Assert.assertFalse(obfuscatedAddress4.isEmpty());
+        Assert.assertFalse(isByteArrayAllZero(obfuscatedAddress4.toByteArray()));
+        Assert.assertArrayEquals(obfuscatedAddress4.toByteArray(),
+                obfuscatedAddress1.toByteArray());
+    }
+
+    /**
+     * Test: Check if obfuscated Bluetooth address stays the same after re-initializing
+     *       {@link AdapterService}
+     */
+    @Test
+    public void testObfuscateBluetoothAddress_PersistentBetweenAdapterServiceInitialization() throws
+            PackageManager.NameNotFoundException {
+        byte[] metricsSalt = getMetricsSalt(mAdapterConfig);
+        Assert.assertNotNull(metricsSalt);
+        Assert.assertFalse(mAdapterService.isEnabled());
+        BluetoothDevice device = TestUtils.getTestDevice(BluetoothAdapter.getDefaultAdapter(), 0);
+        ByteString obfuscatedAddress1 = mAdapterService.obfuscateAddress(device);
+        Assert.assertFalse(obfuscatedAddress1.isEmpty());
+        Assert.assertFalse(isByteArrayAllZero(obfuscatedAddress1.toByteArray()));
+        Assert.assertArrayEquals(obfuscateInJava(metricsSalt, device),
+                obfuscatedAddress1.toByteArray());
+        tearDown();
+        setUp();
+        Assert.assertFalse(mAdapterService.isEnabled());
+        ByteString obfuscatedAddress2 = mAdapterService.obfuscateAddress(device);
+        Assert.assertFalse(obfuscatedAddress2.isEmpty());
+        Assert.assertFalse(isByteArrayAllZero(obfuscatedAddress2.toByteArray()));
+        Assert.assertArrayEquals(obfuscatedAddress2.toByteArray(),
+                obfuscatedAddress1.toByteArray());
+    }
+
+    /**
+     * Test: Verify that obfuscated Bluetooth address changes after factory reset
+     *
+     * There are 4 types of factory reset that we are talking about:
+     * 1. Factory reset all user data from Settings -> Will restart phone
+     * 2. Factory reset WiFi and Bluetooth from Settings -> Will only restart WiFi and BT
+     * 3. Call BluetoothAdapter.factoryReset() -> Will disable Bluetooth and reset config in
+     * memory and disk
+     * 4. Call AdapterService.factoryReset() -> Will only reset config in memory
+     *
+     * We can only use No. 4 here
+     */
+    @Ignore("AdapterService.factoryReset() does not reload config into memory and hence old salt"
+            + " is still used until next time Bluetooth library is initialized. However Bluetooth"
+            + " cannot be used until Bluetooth process restart any way. Thus it is almost"
+            + " guaranteed that user has to re-enable Bluetooth and hence re-generate new salt"
+            + " after factory reset")
+    @Test
+    public void testObfuscateBluetoothAddress_FactoryReset() {
+        Assert.assertFalse(mAdapterService.isEnabled());
+        BluetoothDevice device = TestUtils.getTestDevice(BluetoothAdapter.getDefaultAdapter(), 0);
+        ByteString obfuscatedAddress1 = mAdapterService.obfuscateAddress(device);
+        Assert.assertFalse(obfuscatedAddress1.isEmpty());
+        Assert.assertFalse(isByteArrayAllZero(obfuscatedAddress1.toByteArray()));
+        mAdapterService.factoryReset();
+        ByteString obfuscatedAddress2 = mAdapterService.obfuscateAddress(device);
+        Assert.assertFalse(obfuscatedAddress2.isEmpty());
+        Assert.assertFalse(isByteArrayAllZero(obfuscatedAddress2.toByteArray()));
+        Assert.assertFalse(Arrays.equals(obfuscatedAddress2.toByteArray(),
+                obfuscatedAddress1.toByteArray()));
+        doEnable(0, false);
+        ByteString obfuscatedAddress3 = mAdapterService.obfuscateAddress(device);
+        Assert.assertFalse(obfuscatedAddress3.isEmpty());
+        Assert.assertFalse(isByteArrayAllZero(obfuscatedAddress3.toByteArray()));
+        Assert.assertArrayEquals(obfuscatedAddress3.toByteArray(),
+                obfuscatedAddress2.toByteArray());
+        mAdapterService.factoryReset();
+        ByteString obfuscatedAddress4 = mAdapterService.obfuscateAddress(device);
+        Assert.assertFalse(obfuscatedAddress4.isEmpty());
+        Assert.assertFalse(isByteArrayAllZero(obfuscatedAddress4.toByteArray()));
+        Assert.assertFalse(Arrays.equals(obfuscatedAddress4.toByteArray(),
+                obfuscatedAddress3.toByteArray()));
+    }
+
+    /**
+     * Test: Verify that obfuscated Bluetooth address changes after factory reset and reloading
+     *       native layer
+     */
+    @Test
+    public void testObfuscateBluetoothAddress_FactoryResetAndReloadNativeLayer() throws
+            PackageManager.NameNotFoundException {
+        byte[] metricsSalt1 = getMetricsSalt(mAdapterConfig);
+        Assert.assertNotNull(metricsSalt1);
+        Assert.assertFalse(mAdapterService.isEnabled());
+        BluetoothDevice device = TestUtils.getTestDevice(BluetoothAdapter.getDefaultAdapter(), 0);
+        ByteString obfuscatedAddress1 = mAdapterService.obfuscateAddress(device);
+        Assert.assertFalse(obfuscatedAddress1.isEmpty());
+        Assert.assertFalse(isByteArrayAllZero(obfuscatedAddress1.toByteArray()));
+        Assert.assertArrayEquals(obfuscateInJava(metricsSalt1, device),
+                obfuscatedAddress1.toByteArray());
+        mAdapterService.factoryReset();
+        tearDown();
+        setUp();
+        // Cannot verify metrics salt since it is not written to disk until native cleanup
+        ByteString obfuscatedAddress2 = mAdapterService.obfuscateAddress(device);
+        Assert.assertFalse(obfuscatedAddress2.isEmpty());
+        Assert.assertFalse(isByteArrayAllZero(obfuscatedAddress2.toByteArray()));
+        Assert.assertFalse(Arrays.equals(obfuscatedAddress2.toByteArray(),
+                obfuscatedAddress1.toByteArray()));
+    }
+
+    private static byte[] getMetricsSalt(HashMap<String, HashMap<String, String>> adapterConfig) {
+        HashMap<String, String> metricsSection = adapterConfig.get("Metrics");
+        if (metricsSection == null) {
+            Log.e(TAG, "Metrics section is null: " + adapterConfig.toString());
+            return null;
+        }
+        String saltString = metricsSection.get("Salt256Bit");
+        if (saltString == null) {
+            Log.e(TAG, "Salt256Bit is null: " + metricsSection.toString());
+            return null;
+        }
+        byte[] metricsSalt = ByteStringUtils.fromHexToByteArray(saltString);
+        if (metricsSalt.length != 32) {
+            Log.e(TAG, "Salt length is not 32 bit, but is " + metricsSalt.length);
+            return null;
+        }
+        return metricsSalt;
+    }
+
+    private static byte[] obfuscateInJava(byte[] key, BluetoothDevice device) {
+        String algorithm = "HmacSHA256";
+        try {
+            Mac hmac256 = Mac.getInstance(algorithm);
+            hmac256.init(new SecretKeySpec(key, algorithm));
+            return hmac256.doFinal(Utils.getByteAddress(device));
+        } catch (NoSuchAlgorithmException | IllegalStateException | InvalidKeyException exp) {
+            exp.printStackTrace();
+            return null;
+        }
+    }
+
+    private static boolean isByteArrayAllZero(byte[] byteArray) {
+        for (byte i : byteArray) {
+            if (i != 0) {
+                return false;
+            }
+        }
+        return true;
+    }
 }