Loading android/app/jni/com_android_bluetooth_btservice_AdapterService.cpp +21 −1 Original line number Diff line number Diff line Loading @@ -1217,6 +1217,25 @@ static void interopDatabaseAddNative(JNIEnv* env, jobject obj, int feature, 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}, Loading Loading @@ -1251,7 +1270,8 @@ static JNINativeMethod sMethods[] = { {"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( Loading android/app/src/com/android/bluetooth/btservice/AdapterService.java +13 −0 Original line number Diff line number Diff line Loading @@ -72,6 +72,7 @@ import com.android.internal.R; 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; Loading Loading @@ -2608,6 +2609,16 @@ public class AdapterService extends Service { } } /** * 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(); Loading Loading @@ -2691,6 +2702,8 @@ public class AdapterService extends Service { 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. Loading android/app/tests/unit/src/com/android/bluetooth/TestUtils.java +39 −0 Original line number Diff line number Diff line Loading @@ -33,9 +33,13 @@ import org.junit.Assert; 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; Loading Loading @@ -270,6 +274,41 @@ public class TestUtils { } } /** * 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. */ Loading android/app/tests/unit/src/com/android/bluetooth/btservice/AdapterServiceTest.java +247 −12 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -42,20 +38,38 @@ import android.support.test.InstrumentationRegistry; 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; Loading @@ -77,6 +91,23 @@ public class AdapterServiceTest { 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 { Loading @@ -85,12 +116,8 @@ public class AdapterServiceTest { } 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); Loading Loading @@ -128,6 +155,9 @@ public class AdapterServiceTest { mAdapterService.registerCallback(mIBluetoothCallback); Config.init(mMockContext); mAdapterConfig = TestUtils.readAdapterConfig(); Assert.assertNotNull(mAdapterConfig); } @After Loading Loading @@ -463,4 +493,209 @@ public class AdapterServiceTest { // 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; } } Loading
android/app/jni/com_android_bluetooth_btservice_AdapterService.cpp +21 −1 Original line number Diff line number Diff line Loading @@ -1217,6 +1217,25 @@ static void interopDatabaseAddNative(JNIEnv* env, jobject obj, int feature, 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}, Loading Loading @@ -1251,7 +1270,8 @@ static JNINativeMethod sMethods[] = { {"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( Loading
android/app/src/com/android/bluetooth/btservice/AdapterService.java +13 −0 Original line number Diff line number Diff line Loading @@ -72,6 +72,7 @@ import com.android.internal.R; 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; Loading Loading @@ -2608,6 +2609,16 @@ public class AdapterService extends Service { } } /** * 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(); Loading Loading @@ -2691,6 +2702,8 @@ public class AdapterService extends Service { 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. Loading
android/app/tests/unit/src/com/android/bluetooth/TestUtils.java +39 −0 Original line number Diff line number Diff line Loading @@ -33,9 +33,13 @@ import org.junit.Assert; 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; Loading Loading @@ -270,6 +274,41 @@ public class TestUtils { } } /** * 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. */ Loading
android/app/tests/unit/src/com/android/bluetooth/btservice/AdapterServiceTest.java +247 −12 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -42,20 +38,38 @@ import android.support.test.InstrumentationRegistry; 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; Loading @@ -77,6 +91,23 @@ public class AdapterServiceTest { 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 { Loading @@ -85,12 +116,8 @@ public class AdapterServiceTest { } 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); Loading Loading @@ -128,6 +155,9 @@ public class AdapterServiceTest { mAdapterService.registerCallback(mIBluetoothCallback); Config.init(mMockContext); mAdapterConfig = TestUtils.readAdapterConfig(); Assert.assertNotNull(mAdapterConfig); } @After Loading Loading @@ -463,4 +493,209 @@ public class AdapterServiceTest { // 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; } }