Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 3c74b2cd authored by Jack He's avatar Jack He Committed by android-build-merger
Browse files

Merge "Metrics: Add Bluetooth address obfuscator in Java" am: 8a206b16

am: 950b7585

Change-Id: Ica373465ddce36fc812d75db35314b4d038f03fb
parents 801f7566 950b7585
Loading
Loading
Loading
Loading
+21 −1
Original line number Diff line number Diff line
@@ -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},
@@ -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(
+13 −0
Original line number Diff line number Diff line
@@ -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;
@@ -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();
@@ -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.
+39 −0
Original line number Diff line number Diff line
@@ -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;
@@ -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.
     */
+247 −12
Original line number Diff line number Diff line
@@ -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.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;
@@ -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 {
@@ -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);
@@ -128,6 +155,9 @@ public class AdapterServiceTest {
        mAdapterService.registerCallback(mIBluetoothCallback);

        Config.init(mMockContext);

        mAdapterConfig = TestUtils.readAdapterConfig();
        Assert.assertNotNull(mAdapterConfig);
    }

    @After
@@ -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;
    }
}