Merge "Made changes to VmsBrokerService and ICarImpl to string through PackageManager that allows VmsPublisherService to retrieve a VMS publisher's java package name. Moreover in VmsPublisherService:" into qt-dev
diff --git a/car_product/overlay/frameworks/base/core/res/res/values/config.xml b/car_product/overlay/frameworks/base/core/res/res/values/config.xml
index 904542d..13673b9 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values/config.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values/config.xml
@@ -80,5 +80,8 @@
display, this value should be true. -->
<bool name="config_perDisplayFocusEnabled">true</bool>
+ <!-- True if the device supports split screen as a form of multi-window. -->
+ <bool name="config_supportsSplitScreenMultiWindow">false</bool>
+
<string name="config_dataUsageSummaryComponent">com.android.car.settings/com.android.car.settings.datausage.DataWarningAndLimitActivity</string>
</resources>
diff --git a/service/res/values/config.xml b/service/res/values/config.xml
index 9d90a7c..4aef4a5 100644
--- a/service/res/values/config.xml
+++ b/service/res/values/config.xml
@@ -202,8 +202,8 @@
<!-- service/characteristics uuid for unlocking a device -->
<string name="unlock_service_uuid" translatable="false">00003ac5-0000-1000-8000-00805f9b34fb</string>
- <string name="unlock_escrow_token_uuid" translatable="false">5e2a68a2-27be-43f9-8d1e-4546976fabd7</string>
- <string name="unlock_handle_uuid" translatable="false">5e2a68a3-27be-43f9-8d1e-4546976fabd7</string>
+ <string name="unlock_client_write_uuid" translatable="false">5e2a68a2-27be-43f9-8d1e-4546976fabd7</string>
+ <string name="unlock_server_write_uuid" translatable="false">5e2a68a3-27be-43f9-8d1e-4546976fabd7</string>
<string name="token_handle_shared_preferences" translatable="false">com.android.car.trust.TOKEN_HANDLE</string>
diff --git a/service/src/com/android/car/CarLocationService.java b/service/src/com/android/car/CarLocationService.java
index 43d4a46..cc57856 100644
--- a/service/src/com/android/car/CarLocationService.java
+++ b/service/src/com/android/car/CarLocationService.java
@@ -16,6 +16,8 @@
package com.android.car;
+import android.car.drivingstate.CarDrivingStateEvent;
+import android.car.drivingstate.ICarDrivingStateChangeListener;
import android.car.hardware.power.CarPowerManager;
import android.car.hardware.power.CarPowerManager.CarPowerStateListener;
import android.car.hardware.power.CarPowerManager.CarPowerStateListenerWithCompletion;
@@ -52,8 +54,8 @@
* This service stores the last known location from {@link LocationManager} when a car is parked
* and restores the location when the car is powered on.
*/
-public class CarLocationService extends BroadcastReceiver implements
- CarServiceBase, CarPowerStateListenerWithCompletion {
+public class CarLocationService extends BroadcastReceiver implements CarServiceBase,
+ CarPowerStateListenerWithCompletion {
private static final String TAG = "CarLocationService";
private static final String FILENAME = "location_cache.json";
private static final boolean DBG = true;
@@ -73,10 +75,9 @@
private HandlerThread mHandlerThread;
private Handler mHandler;
private CarPowerManager mCarPowerManager;
+ private CarDrivingStateService mCarDrivingStateService;
- public CarLocationService(
- Context context,
- CarUserManagerHelper carUserManagerHelper) {
+ public CarLocationService(Context context, CarUserManagerHelper carUserManagerHelper) {
logd("constructed");
mContext = context;
mCarUserManagerHelper = carUserManagerHelper;
@@ -90,6 +91,16 @@
filter.addAction(LocationManager.MODE_CHANGED_ACTION);
filter.addAction(LocationManager.PROVIDERS_CHANGED_ACTION);
mContext.registerReceiver(this, filter);
+ mCarDrivingStateService = CarLocalServices.getService(CarDrivingStateService.class);
+ if (mCarDrivingStateService != null) {
+ CarDrivingStateEvent event = mCarDrivingStateService.getCurrentDrivingState();
+ if (event != null && event.eventValue == CarDrivingStateEvent.DRIVING_STATE_MOVING) {
+ deleteCacheFile();
+ } else {
+ mCarDrivingStateService.registerDrivingStateChangeListener(
+ mICarDrivingStateChangeEventListener);
+ }
+ }
mCarPowerManager = CarLocalServices.createCarPowerManager(mContext);
if (mCarPowerManager != null) { // null case happens for testing.
mCarPowerManager.setListenerWithCompletion(CarLocationService.this);
@@ -102,6 +113,10 @@
if (mCarPowerManager != null) {
mCarPowerManager.clearListener();
}
+ if (mCarDrivingStateService != null) {
+ mCarDrivingStateService.unregisterDrivingStateChangeListener(
+ mICarDrivingStateChangeEventListener);
+ }
mContext.unregisterReceiver(this);
}
@@ -173,6 +188,22 @@
}
}
+ private final ICarDrivingStateChangeListener mICarDrivingStateChangeEventListener =
+ new ICarDrivingStateChangeListener.Stub() {
+ @Override
+ public void onDrivingStateChanged(CarDrivingStateEvent event) {
+ logd("onDrivingStateChanged " + event);
+ if (event != null
+ && event.eventValue == CarDrivingStateEvent.DRIVING_STATE_MOVING) {
+ deleteCacheFile();
+ if (mCarDrivingStateService != null) {
+ mCarDrivingStateService.unregisterDrivingStateChangeListener(
+ mICarDrivingStateChangeEventListener);
+ }
+ }
+ }
+ };
+
/**
* Tells whether or not we should check location permissions for the sake of deleting the
* location cache file when permissions are lacking. If the system user is headless but the
diff --git a/service/src/com/android/car/ICarImpl.java b/service/src/com/android/car/ICarImpl.java
index cb18ff8..31627b9 100644
--- a/service/src/com/android/car/ICarImpl.java
+++ b/service/src/com/android/car/ICarImpl.java
@@ -169,9 +169,9 @@
CarLocalServices.addService(CarPowerManagementService.class, mCarPowerManagementService);
CarLocalServices.addService(CarUserService.class, mCarUserService);
- CarLocalServices.addService(CarTrustedDeviceService.class,
- mCarTrustedDeviceService);
+ CarLocalServices.addService(CarTrustedDeviceService.class, mCarTrustedDeviceService);
CarLocalServices.addService(SystemInterface.class, mSystemInterface);
+ CarLocalServices.addService(CarDrivingStateService.class, mCarDrivingStateService);
// Be careful with order. Service depending on other service should be inited later.
List<CarServiceBase> allServices = new ArrayList<>();
diff --git a/service/src/com/android/car/Utils.java b/service/src/com/android/car/Utils.java
index 1b7c029..2b468b7 100644
--- a/service/src/com/android/car/Utils.java
+++ b/service/src/com/android/car/Utils.java
@@ -15,15 +15,16 @@
*/
package com.android.car;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.util.SparseArray;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.UUID;
@@ -253,4 +254,28 @@
ThreadLocalRandom.current().nextInt((int) Math.pow(10, length)));
}
+
+ /**
+ * Concatentate the given 2 byte arrays
+ *
+ * @param a input array 1
+ * @param b input array 2
+ * @return concatenated array of arrays 1 and 2
+ */
+ @Nullable
+ public static byte[] concatByteArrays(@Nullable byte[] a, @Nullable byte[] b) {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ try {
+ if (a != null) {
+ outputStream.write(a);
+ }
+ if (b != null) {
+ outputStream.write(b);
+ }
+ } catch (IOException e) {
+ return null;
+ }
+ return outputStream.toByteArray();
+ }
+
}
diff --git a/service/src/com/android/car/trust/BLEMessageV1Factory.java b/service/src/com/android/car/trust/BLEMessageV1Factory.java
index 42acafd..2478eca 100644
--- a/service/src/com/android/car/trust/BLEMessageV1Factory.java
+++ b/service/src/com/android/car/trust/BLEMessageV1Factory.java
@@ -88,8 +88,7 @@
+ (FIXED_32_SIZE + FIELD_NUMBER_ENCODING_SIZE)
+ (FIXED_32_SIZE + FIELD_NUMBER_ENCODING_SIZE);
- private BLEMessageV1Factory() {
- }
+ private BLEMessageV1Factory() {}
/**
* Method used to generate a single message, the packet number and total packets will set to 1
diff --git a/service/src/com/android/car/trust/BleManager.java b/service/src/com/android/car/trust/BleManager.java
index 24a3b79..28a8fee 100644
--- a/service/src/com/android/car/trust/BleManager.java
+++ b/service/src/com/android/car/trust/BleManager.java
@@ -22,6 +22,7 @@
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattServer;
import android.bluetooth.BluetoothGattServerCallback;
import android.bluetooth.BluetoothGattService;
@@ -176,8 +177,14 @@
*/
protected void notifyCharacteristicChanged(BluetoothDevice device,
BluetoothGattCharacteristic characteristic, boolean confirm) {
- if (mGattServer != null) {
- mGattServer.notifyCharacteristicChanged(device, characteristic, confirm);
+ if (mGattServer == null) {
+ return;
+ }
+
+ boolean result = mGattServer.notifyCharacteristicChanged(device, characteristic, confirm);
+
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "notifyCharacteristicChanged succeeded: " + result);
}
}
@@ -344,6 +351,19 @@
}
@Override
+ public void onDescriptorWriteRequest(BluetoothDevice device, int requestId,
+ BluetoothGattDescriptor descriptor, boolean preparedWrite,
+ boolean responseNeeded, int offset, byte[] value) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Write request for descriptor: " + descriptor.getUuid()
+ + "; value: " + Utils.byteArrayToHexString(value));
+ }
+
+ mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS,
+ offset, value);
+ }
+
+ @Override
public void onMtuChanged(BluetoothDevice device, int mtu) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onMtuChanged: " + mtu + " for device " + device.getAddress());
diff --git a/service/src/com/android/car/trust/CarTrustAgentBleManager.java b/service/src/com/android/car/trust/CarTrustAgentBleManager.java
index f42c1e4..8517c98 100644
--- a/service/src/com/android/car/trust/CarTrustAgentBleManager.java
+++ b/service/src/com/android/car/trust/CarTrustAgentBleManager.java
@@ -20,6 +20,7 @@
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.le.AdvertiseCallback;
import android.bluetooth.le.AdvertiseData;
@@ -51,6 +52,16 @@
private static final String TAG = "CarTrustBLEManager";
+ /**
+ * The UUID of the Client Characteristic Configuration Descriptor. This descriptor is
+ * responsible for specifying if a characteristic can be subscribed to for notifications.
+ *
+ * @see <a href="https://www.bluetooth.com/specifications/gatt/descriptors/">
+ * GATT Descriptors</a>
+ */
+ private static final UUID CLIENT_CHARACTERISTIC_CONFIG =
+ UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
+
/** @hide */
@IntDef(prefix = {"TRUSTED_DEVICE_OPERATION_"}, value = {
TRUSTED_DEVICE_OPERATION_NONE,
@@ -83,8 +94,8 @@
// Unlock Service and Characteristic UUIDs
private UUID mUnlockServiceUuid;
- private UUID mUnlockEscrowTokenUuid;
- private UUID mUnlockTokenHandleUuid;
+ private UUID mUnlockClientWriteUuid;
+ private UUID mUnlockServerWriteUuid;
private BluetoothGattService mUnlockGattService;
private BLEMessagePayloadStream mBleMessagePayloadStream = new BLEMessagePayloadStream();
@@ -141,21 +152,18 @@
if (!mBleMessagePayloadStream.isComplete()) {
return;
}
+
if (uuid.equals(mEnrollmentClientWriteUuid)) {
if (getEnrollmentService() != null) {
getEnrollmentService().onEnrollmentDataReceived(
mBleMessagePayloadStream.toByteArray());
}
- } else if (uuid.equals(mUnlockEscrowTokenUuid)) {
+ } else if (uuid.equals(mUnlockClientWriteUuid)) {
if (getUnlockService() != null) {
- getUnlockService().onUnlockTokenReceived(mBleMessagePayloadStream.toByteArray());
-
- }
- } else if (uuid.equals(mUnlockTokenHandleUuid)) {
- if (getUnlockService() != null) {
- getUnlockService().onUnlockHandleReceived(mBleMessagePayloadStream.toByteArray());
+ getUnlockService().onUnlockDataReceived(mBleMessagePayloadStream.toByteArray());
}
}
+
mBleMessagePayloadStream.reset();
}
@@ -224,8 +232,8 @@
/**
* Setup the BLE GATT server for Enrollment. The GATT server for Enrollment comprises of one
- * GATT Service and 2 characteristics - one for the escrow token to be generated and sent from
- * the phone and the other for the handle generated and sent by the Head unit.
+ * GATT Service and 2 characteristics - one for the phone to write to and one for the head unit
+ * to write to.
*/
void setupEnrollmentBleServer() {
mEnrollmentServiceUuid = UUID.fromString(
@@ -238,18 +246,20 @@
mEnrollmentGattService = new BluetoothGattService(mEnrollmentServiceUuid,
BluetoothGattService.SERVICE_TYPE_PRIMARY);
- // Characteristic to describe the escrow token being used for unlock
+ // Characteristic the connected bluetooth device will write to.
BluetoothGattCharacteristic clientCharacteristic =
new BluetoothGattCharacteristic(mEnrollmentClientWriteUuid,
BluetoothGattCharacteristic.PROPERTY_WRITE,
BluetoothGattCharacteristic.PERMISSION_WRITE);
- // Characteristic to describe the handle being used for this escrow token
+ // Characteristic that this manager will write to.
BluetoothGattCharacteristic serverCharacteristic =
new BluetoothGattCharacteristic(mEnrollmentServerWriteUuid,
BluetoothGattCharacteristic.PROPERTY_NOTIFY,
BluetoothGattCharacteristic.PERMISSION_READ);
+ addDescriptorToCharacteristic(serverCharacteristic);
+
mEnrollmentGattService.addCharacteristic(clientCharacteristic);
mEnrollmentGattService.addCharacteristic(serverCharacteristic);
}
@@ -257,32 +267,42 @@
/**
* Setup the BLE GATT server for Unlocking the Head unit. The GATT server for this phase also
* comprises of 1 Service and 2 characteristics. However both the token and the handle are sent
- * ftrom the phone to the head unit.
+ * from the phone to the head unit.
*/
void setupUnlockBleServer() {
mUnlockServiceUuid = UUID.fromString(getContext().getString(R.string.unlock_service_uuid));
- mUnlockEscrowTokenUuid = UUID
- .fromString(getContext().getString(R.string.unlock_escrow_token_uuid));
- mUnlockTokenHandleUuid = UUID
- .fromString(getContext().getString(R.string.unlock_handle_uuid));
+ mUnlockClientWriteUuid = UUID
+ .fromString(getContext().getString(R.string.unlock_client_write_uuid));
+ mUnlockServerWriteUuid = UUID
+ .fromString(getContext().getString(R.string.unlock_server_write_uuid));
mUnlockGattService = new BluetoothGattService(mUnlockServiceUuid,
BluetoothGattService.SERVICE_TYPE_PRIMARY);
- // Characteristic to describe the escrow token being used for unlock
- BluetoothGattCharacteristic tokenCharacteristic = new BluetoothGattCharacteristic(
- mUnlockEscrowTokenUuid,
+ // Characteristic the connected bluetooth device will write to.
+ BluetoothGattCharacteristic clientCharacteristic = new BluetoothGattCharacteristic(
+ mUnlockClientWriteUuid,
BluetoothGattCharacteristic.PROPERTY_WRITE,
BluetoothGattCharacteristic.PERMISSION_WRITE);
- // Characteristic to describe the handle being used for this escrow token
- BluetoothGattCharacteristic handleCharacteristic = new BluetoothGattCharacteristic(
- mUnlockTokenHandleUuid,
- BluetoothGattCharacteristic.PROPERTY_WRITE,
- BluetoothGattCharacteristic.PERMISSION_WRITE);
+ // Characteristic that this manager will write to.
+ BluetoothGattCharacteristic serverCharacteristic = new BluetoothGattCharacteristic(
+ mUnlockServerWriteUuid,
+ BluetoothGattCharacteristic.PROPERTY_NOTIFY,
+ BluetoothGattCharacteristic.PERMISSION_READ);
- mUnlockGattService.addCharacteristic(tokenCharacteristic);
- mUnlockGattService.addCharacteristic(handleCharacteristic);
+ addDescriptorToCharacteristic(serverCharacteristic);
+
+ mUnlockGattService.addCharacteristic(clientCharacteristic);
+ mUnlockGattService.addCharacteristic(serverCharacteristic);
+ }
+
+ private void addDescriptorToCharacteristic(BluetoothGattCharacteristic characteristic) {
+ BluetoothGattDescriptor descriptor = new BluetoothGattDescriptor(
+ CLIENT_CHARACTERISTIC_CONFIG,
+ BluetoothGattDescriptor.PERMISSION_READ | BluetoothGattDescriptor.PERMISSION_WRITE);
+ descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
+ characteristic.addDescriptor(descriptor);
}
void startEnrollmentAdvertising() {
@@ -340,28 +360,49 @@
stopGattServer();
}
- /**
- * Sends the given message to the specified device.
- *
- * @param device The device to send the message to.
- * @param message A message to send.
- */
- void sendMessage(BluetoothDevice device, byte[] message, OperationType operation,
+ void sendUnlockMessage(BluetoothDevice device, byte[] message, OperationType operation,
boolean isPayloadEncrypted) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "sendMessage to: " + device.getAddress());
- }
- BluetoothGattCharacteristic serverCharacteristic = mEnrollmentGattService
+ BluetoothGattCharacteristic writeCharacteristic = mUnlockGattService
+ .getCharacteristic(mUnlockServerWriteUuid);
+
+ sendMessage(device, writeCharacteristic, message, operation, isPayloadEncrypted);
+ }
+
+ void sendEnrollmentMessage(BluetoothDevice device, byte[] message, OperationType operation,
+ boolean isPayloadEncrypted) {
+ BluetoothGattCharacteristic writeCharacteristic = mEnrollmentGattService
.getCharacteristic(mEnrollmentServerWriteUuid);
+
+ sendMessage(device, writeCharacteristic, message, operation, isPayloadEncrypted);
+ }
+
+ /**
+ * Sends the given message to the specified device and characteristic.
+ *
+ * @param device The device to send the message to.
+ * @param characteristic The characteristic to write to.
+ * @param message A message to send.
+ * @param operation The type of operation this message represents.
+ * @param isPayloadEncrypted {@code true} if the message is encrypted.
+ */
+ private void sendMessage(BluetoothDevice device, BluetoothGattCharacteristic characteristic,
+ byte[] message, OperationType operation, boolean isPayloadEncrypted) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "sendMessage to: " + device.getAddress() + "; and characteristic UUID: "
+ + characteristic.getUuid());
+ }
+
List<BLEMessage> bleMessages = BLEMessageV1Factory.makeBLEMessages(message, operation,
mMtuSize, isPayloadEncrypted);
+
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "sending " + bleMessages.size() + " messages to device");
}
+
for (BLEMessage bleMessage : bleMessages) {
// TODO(b/131719066) get acknowledgement from the phone then continue to send packets
- serverCharacteristic.setValue(bleMessage.toByteArray());
- notifyCharacteristicChanged(device, serverCharacteristic, false);
+ characteristic.setValue(bleMessage.toByteArray());
+ notifyCharacteristicChanged(device, characteristic, false);
}
}
diff --git a/service/src/com/android/car/trust/CarTrustAgentEnrollmentService.java b/service/src/com/android/car/trust/CarTrustAgentEnrollmentService.java
index 6743375..6c703e9 100644
--- a/service/src/com/android/car/trust/CarTrustAgentEnrollmentService.java
+++ b/service/src/com/android/car/trust/CarTrustAgentEnrollmentService.java
@@ -19,6 +19,9 @@
import static android.car.trust.CarTrustAgentEnrollmentManager.ENROLLMENT_HANDSHAKE_FAILURE;
import static android.car.trust.CarTrustAgentEnrollmentManager.ENROLLMENT_NOT_ALLOWED;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.app.ActivityManager;
import android.bluetooth.BluetoothDevice;
import android.car.encryptionrunner.EncryptionRunner;
@@ -37,10 +40,6 @@
import android.os.RemoteException;
import android.util.Log;
-import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
import com.android.car.BLEStreamProtos.BLEOperationProto.OperationType;
import com.android.car.R;
import com.android.car.Utils;
@@ -183,7 +182,7 @@
@Override
public void enrollmentHandshakeAccepted(BluetoothDevice device) {
addEnrollmentServiceLog("enrollmentHandshakeAccepted");
- mCarTrustAgentBleManager.sendMessage(device, CONFIRMATION_SIGNAL,
+ mCarTrustAgentBleManager.sendEnrollmentMessage(device, CONFIRMATION_SIGNAL,
OperationType.ENCRYPTION_HANDSHAKE, /* isPayloadEncrypted= */ false);
setEnrollmentHandshakeAccepted();
}
@@ -412,6 +411,9 @@
deviceName = mRemoteEnrollmentDevice.getName();
} else if (mDeviceName != null) {
deviceName = mDeviceName;
+ mCarTrustAgentBleManager.sendEnrollmentMessage(mRemoteEnrollmentDevice,
+ mEncryptionKey.encryptData(Utils.longToBytes(handle)),
+ OperationType.CLIENT_MESSAGE, /* isPayloadEncrypted= */ true);
} else {
deviceName = mContext.getString(R.string.trust_device_default_name);
}
@@ -446,7 +448,7 @@
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Sending handle: " + handle);
}
- mCarTrustAgentBleManager.sendMessage(mRemoteEnrollmentDevice,
+ mCarTrustAgentBleManager.sendEnrollmentMessage(mRemoteEnrollmentDevice,
mEncryptionKey.encryptData(Utils.longToBytes(handle)),
OperationType.CLIENT_MESSAGE, /* isPayloadEncrypted= */ true);
dispatchEscrowTokenActiveStateChanged(handle, isTokenActive);
@@ -563,8 +565,9 @@
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Sending device id: " + uniqueId.toString());
}
- mCarTrustAgentBleManager.sendMessage(mRemoteEnrollmentDevice, Utils.uuidToBytes(uniqueId),
- OperationType.CLIENT_MESSAGE, /* isPayloadEncrypted= */ false);
+ mCarTrustAgentBleManager.sendEnrollmentMessage(mRemoteEnrollmentDevice,
+ Utils.uuidToBytes(uniqueId), OperationType.CLIENT_MESSAGE,
+ /* isPayloadEncrypted= */ false);
mEnrollmentState++;
}
@@ -599,7 +602,7 @@
mHandshakeMessage = mEncryptionRunner.respondToInitRequest(message);
mEncryptionState = mHandshakeMessage.getHandshakeState();
- mCarTrustAgentBleManager.sendMessage(
+ mCarTrustAgentBleManager.sendEnrollmentMessage(
mRemoteEnrollmentDevice, mHandshakeMessage.getNextMessage(),
OperationType.ENCRYPTION_HANDSHAKE, /* isPayloadEncrypted= */ false);
@@ -626,7 +629,7 @@
showVerificationCode();
return;
}
- mCarTrustAgentBleManager.sendMessage(mRemoteEnrollmentDevice,
+ mCarTrustAgentBleManager.sendEnrollmentMessage(mRemoteEnrollmentDevice,
mHandshakeMessage.getNextMessage(), OperationType.ENCRYPTION_HANDSHAKE,
/* isPayloadEncrypted= */ false);
break;
diff --git a/service/src/com/android/car/trust/CarTrustAgentUnlockService.java b/service/src/com/android/car/trust/CarTrustAgentUnlockService.java
index 8808f50..1151c4e 100644
--- a/service/src/com/android/car/trust/CarTrustAgentUnlockService.java
+++ b/service/src/com/android/car/trust/CarTrustAgentUnlockService.java
@@ -16,31 +16,109 @@
package com.android.car.trust;
+import android.annotation.IntDef;
+import android.annotation.Nullable;
import android.bluetooth.BluetoothDevice;
+import android.car.encryptionrunner.EncryptionRunner;
+import android.car.encryptionrunner.EncryptionRunnerFactory;
+import android.car.encryptionrunner.HandshakeException;
+import android.car.encryptionrunner.HandshakeMessage;
+import android.car.encryptionrunner.Key;
import android.content.SharedPreferences;
import android.util.Log;
+import com.android.car.BLEStreamProtos.BLEOperationProto.OperationType;
import com.android.car.Utils;
import com.android.internal.annotations.GuardedBy;
+import com.google.security.cryptauth.lib.securegcm.D2DConnectionContext;
+import com.google.security.cryptauth.lib.securemessage.CryptoOps;
+
import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
import java.util.LinkedList;
import java.util.Queue;
+import java.util.UUID;
+
+import javax.crypto.spec.SecretKeySpec;
/**
* A service that interacts with the Trust Agent {@link CarBleTrustAgent} and a comms (BLE) service
* {@link CarTrustAgentBleManager} to receive the necessary credentials to authenticate
* an Android user.
+ *
+ * <p>
+ * The unlock flow is as follows:
+ * <ol>
+ * <li>IHU advertises via BLE when it is in a locked state. The advertisement includes its
+ * identifier.
+ * <li>Phone (Trusted device) scans, finds and connects to the IHU.
+ * <li>Protocol versions are exchanged and verified.
+ * <li>Phone sends its identifier in plain text.
+ * <li>IHU verifies that the phone is enrolled as a trusted device from its identifier.
+ * <li>IHU, then sends an ACK back to the phone.
+ * <li>Phone & IHU go over the key exchange (using UKEY2) for encrypting this new session.
+ * <li>Key exchange is completed without any numeric comparison.
+ * <li>Phone sends its MAC (digest) that is computed from the context from this new session and the
+ * previous session.
+ * <li>IHU computes Phone's MAC and validates against what the phone sent. On validation failure,
+ * the stored encryption keys for the phone are deleted. This would require the phone to re-enroll
+ * again.
+ * <li>IHU sends its MAC that is computed similarly from the new session and previous session
+ * contexts.
+ * <li>Phone computes IHU's MAC internally and validates it against what it received.
+ * <li>At this point, the devices have mutually authenticated each other and also have keys to
+ * encrypt
+ * current session.
+ * <li>IHU saves the current session keys. This would serve for authenticating the next session.
+ * <li>Phone sends the encrypted escrow token and handle to the IHU.
+ * <li>IHU retrieves the user id and authenticates the user.
+ * </ol>
*/
public class CarTrustAgentUnlockService {
private static final String TAG = "CarTrustAgentUnlock";
private static final String TRUSTED_DEVICE_UNLOCK_ENABLED_KEY = "trusted_device_unlock_enabled";
- //Arbirary log size
+
+ // Arbitrary log size
private static final int MAX_LOG_SIZE = 20;
+ private static final byte[] RESUME = "RESUME".getBytes();
+ private static final byte[] SERVER = "SERVER".getBytes();
+ private static final byte[] CLIENT = "CLIENT".getBytes();
+ private static final int RESUME_HMAC_LENGTH = 32;
+
+ private static final byte[] ACKNOWLEDGEMENT_MESSAGE = "ACK".getBytes();
+
+ // State of the unlock process. Important to maintain the same order in both phone and IHU.
+ // State increments to the next state on successful completion.
+ private static final int UNLOCK_STATE_WAITING_FOR_UNIQUE_ID = 0;
+ private static final int UNLOCK_STATE_KEY_EXCHANGE_IN_PROGRESS = 1;
+ private static final int UNLOCK_STATE_WAITING_FOR_CLIENT_AUTH = 2;
+ private static final int UNLOCK_STATE_MUTUAL_AUTH_ESTABLISHED = 3;
+ private static final int UNLOCK_STATE_TOKEN_RECEIVED = 4;
+ private static final int UNLOCK_STATE_HANDLE_RECEIVED = 5;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = {"UNLOCK_STATE_"}, value = {UNLOCK_STATE_WAITING_FOR_UNIQUE_ID,
+ UNLOCK_STATE_KEY_EXCHANGE_IN_PROGRESS, UNLOCK_STATE_WAITING_FOR_CLIENT_AUTH,
+ UNLOCK_STATE_MUTUAL_AUTH_ESTABLISHED, UNLOCK_STATE_TOKEN_RECEIVED,
+ UNLOCK_STATE_HANDLE_RECEIVED})
+ @interface UnlockState {
+ }
+
+ @UnlockState
+ private int mCurrentUnlockState = UNLOCK_STATE_WAITING_FOR_UNIQUE_ID;
+
private final CarTrustedDeviceService mTrustedDeviceService;
private final CarTrustAgentBleManager mCarTrustAgentBleManager;
private CarTrustAgentUnlockDelegate mUnlockDelegate;
+ private String mClientDeviceId;
private final Queue<String> mLogQueue = new LinkedList<>();
+
// Locks
private final Object mTokenLock = new Object();
private final Object mHandleLock = new Object();
@@ -50,9 +128,19 @@
private byte[] mUnlockToken;
@GuardedBy("mHandleLock")
private byte[] mUnlockHandle;
+
@GuardedBy("mDeviceLock")
private BluetoothDevice mRemoteUnlockDevice;
+ private EncryptionRunner mEncryptionRunner = EncryptionRunnerFactory.newRunner();
+ private HandshakeMessage mHandshakeMessage;
+ private Key mEncryptionKey;
+ @HandshakeMessage.HandshakeState
+ private int mEncryptionState = HandshakeMessage.HandshakeState.UNKNOWN;
+
+ private D2DConnectionContext mPrevContext;
+ private D2DConnectionContext mCurrentContext;
+
CarTrustAgentUnlockService(CarTrustedDeviceService service,
CarTrustAgentBleManager bleService) {
mTrustedDeviceService = service;
@@ -78,7 +166,8 @@
* Enable or disable authentication of the head unit with a trusted device.
*
* @param isEnabled when set to {@code false}, head unit will not be
- * discoverable to unlock the user. Setting it to {@code true} will enable it back.
+ * discoverable to unlock the user. Setting it to {@code true} will enable it
+ * back.
*/
public void setTrustedDeviceUnlockEnabled(boolean isEnabled) {
SharedPreferences.Editor editor = mTrustedDeviceService.getSharedPrefs().edit();
@@ -87,6 +176,7 @@
Log.wtf(TAG, "Unlock Enable Failed. Enable? " + isEnabled);
}
}
+
/**
* Set a delegate that implements {@link CarTrustAgentUnlockDelegate}. The delegate will be
* handed the auth related data (token and handle) when it is received from the remote
@@ -140,6 +230,8 @@
synchronized (mDeviceLock) {
mRemoteUnlockDevice = null;
}
+ mPrevContext = null;
+ mCurrentContext = null;
}
void onRemoteDeviceConnected(BluetoothDevice device) {
@@ -151,6 +243,7 @@
queueMessageForLog("onRemoteDeviceConnected (addr:" + device.getAddress() + ")");
mRemoteUnlockDevice = device;
}
+ mCurrentUnlockState = UNLOCK_STATE_WAITING_FOR_UNIQUE_ID;
}
void onRemoteDeviceDisconnected(BluetoothDevice device) {
@@ -162,38 +255,282 @@
synchronized (mDeviceLock) {
mRemoteUnlockDevice = null;
}
+ mCurrentUnlockState = UNLOCK_STATE_WAITING_FOR_UNIQUE_ID;
+ }
+
+ void onUnlockDataReceived(byte[] value) {
+ switch (mCurrentUnlockState) {
+ case UNLOCK_STATE_WAITING_FOR_UNIQUE_ID:
+ mClientDeviceId = convertToDeviceId(value);
+ if (mClientDeviceId == null) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Phone not enrolled as a trusted device");
+ }
+ resetUnlockStateOnFailure();
+ return;
+ }
+ sendAckToClient(/* isEncrypted = */ false);
+ // Next step is to wait for the client to start the encryption handshake.
+ mCurrentUnlockState = UNLOCK_STATE_KEY_EXCHANGE_IN_PROGRESS;
+ break;
+ case UNLOCK_STATE_KEY_EXCHANGE_IN_PROGRESS:
+ try {
+ processKeyExchangeHandshakeMessage(value);
+ } catch (HandshakeException e) {
+ Log.e(TAG, "Handshake failure", e);
+ resetUnlockStateOnFailure();
+ }
+ break;
+ case UNLOCK_STATE_WAITING_FOR_CLIENT_AUTH:
+ if (!authenticateClient(value)) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG,
+ "HMAC from the phone is not correct. Cannot resume session. Need"
+ + " to re-enroll");
+ }
+ mTrustedDeviceService.clearEncryptionKey(mClientDeviceId);
+ resetUnlockStateOnFailure();
+
+ return;
+ }
+ sendServerAuthToClient();
+ mCurrentUnlockState = UNLOCK_STATE_MUTUAL_AUTH_ESTABLISHED;
+ break;
+ case UNLOCK_STATE_MUTUAL_AUTH_ESTABLISHED:
+ if (mEncryptionKey == null) {
+ Log.e(TAG, "Current session key null. Unexpected at this stage: "
+ + mCurrentUnlockState);
+ // Clear the previous session key. Need to re-enroll the trusted device.
+ mTrustedDeviceService.clearEncryptionKey(mClientDeviceId);
+ resetUnlockStateOnFailure();
+ }
+ // Save the current session to be used for authenticating the next session
+ mTrustedDeviceService.saveEncryptionKey(mClientDeviceId, mEncryptionKey.asBytes());
+
+ onUnlockTokenReceived(value);
+ mCurrentUnlockState = UNLOCK_STATE_TOKEN_RECEIVED;
+ // Let the phone know that the token was received.
+ sendAckToClient(/* isEncrypted = */ true);
+ break;
+ // TODO(b/131124919) Combine token and handle in the same packet
+ case UNLOCK_STATE_TOKEN_RECEIVED:
+ onUnlockHandleReceived(value);
+ mCurrentUnlockState = UNLOCK_STATE_HANDLE_RECEIVED;
+ break;
+ case UNLOCK_STATE_HANDLE_RECEIVED:
+ // Should never get here because the unlock process should be completed now.
+ Log.e(TAG, "Landed on unexpected state: " + mCurrentUnlockState);
+ break;
+ default:
+ break;
+ }
+ }
+
+ private void sendAckToClient(boolean isEncrypted) {
+ // Let the phone know that the handle was received.
+ byte[] ack = isEncrypted ? mEncryptionKey.encryptData(ACKNOWLEDGEMENT_MESSAGE)
+ : ACKNOWLEDGEMENT_MESSAGE;
+ mCarTrustAgentBleManager.sendUnlockMessage(mRemoteUnlockDevice, ack,
+ OperationType.CLIENT_MESSAGE, /* isPayloadEncrypted= */ isEncrypted);
+ }
+
+ @Nullable
+ private String convertToDeviceId(byte[] id) {
+ // Validate if the id exists i.e., if the phone is enrolled already
+ UUID deviceId = Utils.bytesToUUID(id);
+ if (deviceId == null
+ || mTrustedDeviceService.getEncryptionKey(deviceId.toString()) == null) {
+ if (deviceId != null) {
+ Log.e(TAG, "Unknown phone connected: " + deviceId.toString());
+ }
+ return null;
+ }
+
+ return deviceId.toString();
+ }
+
+ private void processKeyExchangeHandshakeMessage(byte[] message) throws HandshakeException {
+ switch (mEncryptionState) {
+ case HandshakeMessage.HandshakeState.UNKNOWN:
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Responding to handshake init request.");
+ }
+
+ mHandshakeMessage = mEncryptionRunner.respondToInitRequest(message);
+ mEncryptionState = mHandshakeMessage.getHandshakeState();
+ mCarTrustAgentBleManager.sendUnlockMessage(mRemoteUnlockDevice,
+ mHandshakeMessage.getNextMessage(),
+ OperationType.ENCRYPTION_HANDSHAKE,
+ /* isPayloadEncrypted= */ false);
+
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Updated encryption state: " + mEncryptionState);
+ }
+ break;
+
+ case HandshakeMessage.HandshakeState.IN_PROGRESS:
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Continuing handshake.");
+ }
+
+ mHandshakeMessage = mEncryptionRunner.continueHandshake(message);
+ mEncryptionState = mHandshakeMessage.getHandshakeState();
+
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Updated encryption state: " + mEncryptionState);
+ }
+
+ // The state is updated after a call to continueHandshake(). Thus, need to check
+ // if we're in the next stage.
+ if (mEncryptionState == HandshakeMessage.HandshakeState.VERIFICATION_NEEDED) {
+ showVerificationCode();
+ return;
+ }
+
+ // control shouldn't get here with Ukey2
+ mCarTrustAgentBleManager.sendUnlockMessage(mRemoteUnlockDevice,
+ mHandshakeMessage.getNextMessage(),
+ OperationType.ENCRYPTION_HANDSHAKE, /*isPayloadEncrypted= */false);
+ break;
+ case HandshakeMessage.HandshakeState.VERIFICATION_NEEDED:
+ case HandshakeMessage.HandshakeState.FINISHED:
+ // Should never reach this case since this state should occur after a verification
+ // code has been accepted. But it should mean handshake is done and the message
+ // is one for the escrow token. Start Mutual Auth from server - compute MACs and
+ // send it over
+ showVerificationCode();
+ break;
+
+ default:
+ Log.w(TAG, "Encountered invalid handshake state: " + mEncryptionState);
+ break;
+ }
+ }
+
+ /**
+ * Verify the handshake.
+ * TODO(b/134073741) combine this with the method in CarTrustAgentEnrollmentService and
+ * have this take a boolean to blindly confirm the numeric code.
+ */
+ private void showVerificationCode() {
+ HandshakeMessage handshakeMessage;
+
+ // Blindly accept the verification code.
+ try {
+ handshakeMessage = mEncryptionRunner.verifyPin();
+ } catch (HandshakeException e) {
+ Log.e(TAG, "Verify pin failed for new keys - Unexpected");
+ resetUnlockStateOnFailure();
+ return;
+ }
+
+ if (handshakeMessage.getHandshakeState() != HandshakeMessage.HandshakeState.FINISHED) {
+ Log.e(TAG, "Handshake not finished after calling verify PIN. Instead got state: "
+ + handshakeMessage.getHandshakeState());
+ resetUnlockStateOnFailure();
+ return;
+ }
+
+ mEncryptionState = HandshakeMessage.HandshakeState.FINISHED;
+ mEncryptionKey = handshakeMessage.getKey();
+ mCurrentContext = D2DConnectionContext.fromSavedSession(mEncryptionKey.asBytes());
+
+ if (mClientDeviceId == null) {
+ resetUnlockStateOnFailure();
+ return;
+ }
+ byte[] oldSessionKeyBytes = mTrustedDeviceService.getEncryptionKey(mClientDeviceId);
+ if (oldSessionKeyBytes == null) {
+ Log.e(TAG,
+ "Could not retrieve previous session keys! Have to re-enroll trusted device");
+ resetUnlockStateOnFailure();
+ return;
+ }
+
+ mPrevContext = D2DConnectionContext.fromSavedSession(oldSessionKeyBytes);
+ if (mPrevContext == null) {
+ resetUnlockStateOnFailure();
+ return;
+ }
+
+ // Now wait for the phone to send its MAC.
+ mCurrentUnlockState = UNLOCK_STATE_WAITING_FOR_CLIENT_AUTH;
+ }
+
+ private void sendServerAuthToClient() {
+ byte[] resumeBytes = computeMAC(mPrevContext, mCurrentContext, SERVER);
+ if (resumeBytes == null) {
+ return;
+ }
+ // send to client
+ mCarTrustAgentBleManager.sendUnlockMessage(mRemoteUnlockDevice, resumeBytes,
+ OperationType.CLIENT_MESSAGE, /* isPayloadEncrypted= */false);
+ }
+
+ @Nullable
+ private byte[] computeMAC(D2DConnectionContext previous, D2DConnectionContext next,
+ byte[] info) {
+ try {
+ SecretKeySpec inputKeyMaterial = new SecretKeySpec(
+ Utils.concatByteArrays(previous.getSessionUnique(), next.getSessionUnique()),
+ "" /* key type is just plain raw bytes */);
+ return CryptoOps.hkdf(inputKeyMaterial, RESUME, info);
+ } catch (NoSuchAlgorithmException | InvalidKeyException e) {
+ // Does not happen in practice
+ Log.e(TAG, "Compute MAC failed");
+ return null;
+ }
+ }
+
+ private boolean authenticateClient(byte[] message) {
+ if (message.length != RESUME_HMAC_LENGTH) {
+ Log.e(TAG, "failing because message.length is " + message.length);
+ return false;
+ }
+ return MessageDigest.isEqual(message,
+ computeMAC(mPrevContext, mCurrentContext, CLIENT));
}
void onUnlockTokenReceived(byte[] value) {
synchronized (mTokenLock) {
mUnlockToken = value;
}
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Unlock Token: " + Utils.byteArrayToHexString(mUnlockToken));
- }
- queueMessageForLog("onUnlockTokenReceived");
- if (mUnlockToken == null || mUnlockHandle == null) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Unlock Handle not available yet");
- }
- return;
- }
- if (mUnlockDelegate == null) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "No Unlock delegate");
- }
- return;
- }
- mUnlockDelegate.onUnlockDataReceived(
- mTrustedDeviceService.getUserHandleByTokenHandle(Utils.bytesToLong(mUnlockHandle)),
- mUnlockToken,
- Utils.bytesToLong(mUnlockHandle));
- synchronized (mTokenLock) {
- mUnlockToken = null;
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Unlock Token received: " + Utils.byteArrayToHexString(mUnlockToken));
}
- synchronized (mHandleLock) {
- mUnlockHandle = null;
+
+ queueMessageForLog("onUnlockTokenReceived");
+ }
+
+ /**
+ * Reset the whole unlock state. Disconnects from the peer device
+ *
+ * <p>This method should be called from any stage in the middle of unlock where we
+ * encounter a failure.
+ */
+ private void resetUnlockStateOnFailure() {
+ mCarTrustAgentBleManager.disconnectRemoteDevice();
+ resetEncryptionState();
+ }
+
+ /**
+ * Resets the encryption status of this service.
+ *
+ * <p>This method should be called each time a device connects so that a new handshake can be
+ * started and encryption keys exchanged.
+ */
+ private void resetEncryptionState() {
+ mEncryptionRunner = EncryptionRunnerFactory.newRunner();
+ mHandshakeMessage = null;
+ mEncryptionKey = null;
+ mEncryptionState = HandshakeMessage.HandshakeState.UNKNOWN;
+ mCurrentUnlockState = UNLOCK_STATE_WAITING_FOR_UNIQUE_ID;
+ if (mCurrentContext != null) {
+ mCurrentContext = null;
+ }
+ if (mPrevContext != null) {
+ mPrevContext = null;
}
}
@@ -201,10 +538,13 @@
synchronized (mHandleLock) {
mUnlockHandle = value;
}
+
if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Unlock Handle: " + Utils.byteArrayToHexString(mUnlockHandle));
+ Log.d(TAG, "Unlock Handl received: " + Utils.byteArrayToHexString(mUnlockHandle));
}
+
queueMessageForLog("onUnlockHandleReceived");
+
if (mUnlockToken == null || mUnlockHandle == null) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Unlock Token not available yet");
@@ -218,6 +558,7 @@
}
return;
}
+
mUnlockDelegate.onUnlockDataReceived(
mTrustedDeviceService.getUserHandleByTokenHandle(Utils.bytesToLong(mUnlockHandle)),
mUnlockToken,
diff --git a/service/src/com/android/car/trust/CarTrustedDeviceService.java b/service/src/com/android/car/trust/CarTrustedDeviceService.java
index 9995aa7..cc79eff 100644
--- a/service/src/com/android/car/trust/CarTrustedDeviceService.java
+++ b/service/src/com/android/car/trust/CarTrustedDeviceService.java
@@ -16,6 +16,7 @@
package com.android.car.trust;
+import android.annotation.Nullable;
import android.app.ActivityManager;
import android.bluetooth.BluetoothDevice;
import android.car.trust.TrustedDeviceInfo;
@@ -26,8 +27,6 @@
import android.util.Base64;
import android.util.Log;
-import androidx.annotation.Nullable;
-
import com.android.car.CarServiceBase;
import com.android.car.R;
import com.android.car.Utils;
@@ -51,6 +50,7 @@
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
+import javax.crypto.spec.GCMParameterSpec;
/**
* The part of the Car service that enables the Trusted device feature. Trusted Device is a feature
@@ -63,12 +63,21 @@
*/
public class CarTrustedDeviceService implements CarServiceBase {
private static final String TAG = CarTrustedDeviceService.class.getSimpleName();
+
private static final String UNIQUE_ID_KEY = "CTABM_unique_id";
private static final String PREF_ENCRYPTION_KEY_PREFIX = "CTABM_encryption_key";
private static final String KEY_ALIAS = "Ukey2Key";
private static final String CIPHER_TRANSFORMATION = "AES/GCM/NoPadding";
private static final String KEYSTORE_PROVIDER = "AndroidKeyStore";
+ private static final String IV_SPEC_SEPARATOR = ";";
+
+ // The length of the authentication tag for a cipher in GCM mode. The GCM specification states
+ // that this length can only have the values {128, 120, 112, 104, 96}. Using the highest
+ // possible value.
+ private static final int GCM_AUTHENTICATION_TAG_LENGTH = 128;
+
private static final int RANDOM_NAME_LENGTH = 6;
+
private final Context mContext;
private CarTrustAgentEnrollmentService mCarTrustAgentEnrollmentService;
private CarTrustAgentUnlockService mCarTrustAgentUnlockService;
@@ -222,13 +231,21 @@
@Nullable
byte[] getEncryptionKey(String deviceId) {
SharedPreferences prefs = getSharedPrefs();
- if (!prefs.contains(deviceId)) {
+ String key = PREF_ENCRYPTION_KEY_PREFIX + deviceId;
+ if (!prefs.contains(key)) {
return null;
}
- byte[] encryptedKey = Base64.decode(
- prefs.getString(PREF_ENCRYPTION_KEY_PREFIX + deviceId, null),
- Base64.DEFAULT);
- return decryptWithKeyStore(KEY_ALIAS, encryptedKey);
+
+ // This value will not be "null" because we already checked via a call to contains().
+ String[] values = prefs.getString(key, null).split(IV_SPEC_SEPARATOR);
+
+ if (values.length != 2) {
+ return null;
+ }
+
+ byte[] encryptedKey = Base64.decode(values[0], Base64.DEFAULT);
+ byte[] ivSpec = Base64.decode(values[1], Base64.DEFAULT);
+ return decryptWithKeyStore(KEY_ALIAS, encryptedKey, ivSpec);
}
/**
@@ -238,20 +255,37 @@
* @param encryptionKey encryption key
* @return {@code true} if the operation succeeded
*/
- boolean saveEncryptionKey(String deviceId, byte[] encryptionKey) {
- byte[] encryptedKey = encryptWithKeyStore(KEY_ALIAS, encryptionKey);
+ boolean saveEncryptionKey(@Nullable String deviceId, @Nullable byte[] encryptionKey) {
+ if (encryptionKey == null || deviceId == null) {
+ return false;
+ }
+ String encryptedKey = encryptWithKeyStore(KEY_ALIAS, encryptionKey);
if (encryptedKey == null) {
return false;
}
+ if (getSharedPrefs().contains(deviceId)) {
+ clearEncryptionKey(deviceId);
+ }
return getSharedPrefs()
.edit()
- .putString(PREF_ENCRYPTION_KEY_PREFIX + deviceId,
- Base64.encodeToString(encryptedKey, Base64.DEFAULT))
+ .putString(PREF_ENCRYPTION_KEY_PREFIX + deviceId, encryptedKey)
.commit();
}
/**
+ * Clear the encryption key for the given device
+ *
+ * @param deviceId id of the peer device
+ */
+ void clearEncryptionKey(@Nullable String deviceId) {
+ if (deviceId == null) {
+ return;
+ }
+ getSharedPrefs().edit().remove(deviceId);
+ }
+
+ /**
* Get generated random name for enrollment
*
* @return a random name for enrollment
@@ -268,12 +302,18 @@
/**
* Encrypt value with designated key
*
+ * <p>The encrypted value is of the form:
+ *
+ * <p>key + IV_SPEC_SEPARATOR + ivSpec
+ *
+ * <p>The {@code ivSpec} is needed to decrypt this key later on.
+ *
* @param keyAlias KeyStore alias for key to use
* @param value a value to encrypt
* @return encrypted value, null if unable to encrypt
*/
@Nullable
- byte[] encryptWithKeyStore(String keyAlias, byte[] value) {
+ String encryptWithKeyStore(String keyAlias, byte[] value) {
if (value == null) {
return null;
}
@@ -282,7 +322,10 @@
try {
Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
cipher.init(Cipher.ENCRYPT_MODE, key);
- return cipher.doFinal(value);
+ return new StringBuffer(Base64.encodeToString(cipher.doFinal(value), Base64.DEFAULT))
+ .append(IV_SPEC_SEPARATOR)
+ .append(Base64.encodeToString(cipher.getIV(), Base64.DEFAULT))
+ .toString();
} catch (IllegalBlockSizeException
| BadPaddingException
| NoSuchAlgorithmException
@@ -302,7 +345,7 @@
* @return decrypted value, null if unable to decrypt
*/
@Nullable
- byte[] decryptWithKeyStore(String keyAlias, byte[] value) {
+ byte[] decryptWithKeyStore(String keyAlias, byte[] value, byte[] ivSpec) {
if (value == null) {
return null;
}
@@ -310,14 +353,16 @@
try {
Key key = getKeyStoreKey(keyAlias);
Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
- cipher.init(Cipher.DECRYPT_MODE, key);
+ cipher.init(Cipher.DECRYPT_MODE, key,
+ new GCMParameterSpec(GCM_AUTHENTICATION_TAG_LENGTH, ivSpec));
return cipher.doFinal(value);
} catch (IllegalBlockSizeException
| BadPaddingException
| NoSuchAlgorithmException
| NoSuchPaddingException
| IllegalStateException
- | InvalidKeyException e) {
+ | InvalidKeyException
+ | InvalidAlgorithmParameterException e) {
Log.e(TAG, "Unable to decrypt value with key " + keyAlias, e);
return null;
}
diff --git a/tests/carservice_unit_test/src/com/android/car/CarLocationServiceTest.java b/tests/carservice_unit_test/src/com/android/car/CarLocationServiceTest.java
index 4290354..d08d32a 100644
--- a/tests/carservice_unit_test/src/com/android/car/CarLocationServiceTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/CarLocationServiceTest.java
@@ -28,6 +28,8 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.car.drivingstate.CarDrivingStateEvent;
+import android.car.drivingstate.ICarDrivingStateChangeListener;
import android.car.hardware.power.CarPowerManager.CarPowerStateListener;
import android.car.userlib.CarUserManagerHelper;
import android.content.Context;
@@ -73,6 +75,7 @@
* 2. {@link LocationManager} provides dummy {@link Location}s.
* 3. {@link CarUserManagerHelper} tells whether or not the system user is headless.
* 4. {@link SystemInterface} tells where to store system files.
+ * 5. {@link CarDrivingStateService} tells about driving state changes.
*/
@RunWith(AndroidJUnit4.class)
public class CarLocationServiceTest {
@@ -90,6 +93,8 @@
private CarUserManagerHelper mMockCarUserManagerHelper;
@Mock
private SystemInterface mMockSystemInterface;
+ @Mock
+ private CarDrivingStateService mMockCarDrivingStateService;
/**
* Initialize all of the objects with the @Mock annotation.
@@ -111,6 +116,9 @@
};
CarLocalServices.removeServiceForTest(SystemInterface.class);
CarLocalServices.addService(SystemInterface.class, mMockSystemInterface);
+ CarLocalServices.removeServiceForTest(CarDrivingStateService.class);
+ CarLocalServices.addService(CarDrivingStateService.class, mMockCarDrivingStateService);
+ when(mMockSystemInterface.getSystemCarDir()).thenReturn(mTempDirectory);
}
@After
@@ -142,10 +150,13 @@
*/
@Test
public void testRegistersToReceiveEvents() {
- ArgumentCaptor<IntentFilter> argument = ArgumentCaptor.forClass(IntentFilter.class);
+ ArgumentCaptor<IntentFilter> intentFilterArgument = ArgumentCaptor.forClass(
+ IntentFilter.class);
mCarLocationService.init();
- verify(mMockContext).registerReceiver(eq(mCarLocationService), argument.capture());
- IntentFilter intentFilter = argument.getValue();
+ verify(mMockContext).registerReceiver(eq(mCarLocationService),
+ intentFilterArgument.capture());
+ verify(mMockCarDrivingStateService).registerDrivingStateChangeListener(any());
+ IntentFilter intentFilter = intentFilterArgument.getValue();
assertEquals(3, intentFilter.countActions());
String[] actions = {intentFilter.getAction(0), intentFilter.getAction(1),
intentFilter.getAction(2)};
@@ -159,8 +170,10 @@
*/
@Test
public void testUnregistersEventReceivers() {
+ mCarLocationService.init();
mCarLocationService.release();
verify(mMockContext).unregisterReceiver(mCarLocationService);
+ verify(mMockCarDrivingStateService).unregisterDrivingStateChangeListener(any());
}
/**
@@ -177,7 +190,6 @@
ArgumentCaptor<Location> argument = ArgumentCaptor.forClass(Location.class);
when(mMockContext.getSystemService(Context.LOCATION_SERVICE))
.thenReturn(mMockLocationManager);
- when(mMockSystemInterface.getSystemCarDir()).thenReturn(mTempDirectory);
when(mMockLocationManager.injectLocation(argument.capture())).thenReturn(true);
when(mMockCarUserManagerHelper.isHeadlessSystemUser()).thenReturn(true);
@@ -304,7 +316,6 @@
timbuktu.setAccuracy(13.75f);
timbuktu.setTime(currentTime);
timbuktu.setElapsedRealtimeNanos(elapsedTime);
- when(mMockSystemInterface.getSystemCarDir()).thenReturn(mTempDirectory);
when(mMockContext.getSystemService(Context.LOCATION_SERVICE))
.thenReturn(mMockLocationManager);
when(mMockLocationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER))
@@ -326,7 +337,7 @@
* Test that the {@link CarLocationService} does not throw an exception on SUSPEND_EXIT events.
*/
@Test
- public void testDoesNotThrowExceptionUponStateChanged() {
+ public void testDoesNotThrowExceptionUponPowerStateChanged() {
try {
mCarLocationService.onStateChanged(CarPowerStateListener.SUSPEND_ENTER, null);
mCarLocationService.onStateChanged(CarPowerStateListener.SUSPEND_EXIT, null);
@@ -361,12 +372,16 @@
*/
@Test
public void testDeletesCacheFileWhenLocationIsDisabled() throws Exception {
+ writeCacheFile("{\"provider\":\"latitude\":16.7666,\"longitude\": \"accuracy\":1.0}");
when(mMockContext.getSystemService(Context.LOCATION_SERVICE))
.thenReturn(mMockLocationManager);
when(mMockLocationManager.isLocationEnabled()).thenReturn(false);
mCarLocationService.init();
+ assertTrue(getLocationCacheFile().exists());
+
mCarLocationService.onReceive(mMockContext,
new Intent(LocationManager.MODE_CHANGED_ACTION));
+
verify(mMockLocationManager, times(1)).isLocationEnabled();
assertFalse(getLocationCacheFile().exists());
}
@@ -394,18 +409,49 @@
*/
@Test
public void testDeletesCacheFileWhenTheGPSProviderIsDisabled() throws Exception {
+ writeCacheFile("{\"provider\":\"latitude\":16.7666,\"longitude\": \"accuracy\":1.0}");
when(mMockContext.getSystemService(Context.LOCATION_SERVICE))
.thenReturn(mMockLocationManager);
when(mMockLocationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)).thenReturn(
false);
mCarLocationService.init();
+ assertTrue(getLocationCacheFile().exists());
+
mCarLocationService.onReceive(mMockContext,
new Intent(LocationManager.PROVIDERS_CHANGED_ACTION));
+
verify(mMockLocationManager, times(1))
.isProviderEnabled(LocationManager.GPS_PROVIDER);
assertFalse(getLocationCacheFile().exists());
}
+ /**
+ * Test that the {@link CarLocationService} deletes location_cache.json when the car enters a
+ * moving driving state.
+ */
+ @Test
+ public void testDeletesCacheFileWhenDrivingStateBecomesMoving() throws Exception {
+ writeCacheFile("{\"provider\":\"latitude\":16.7666,\"longitude\": \"accuracy\":1.0}");
+ when(mMockContext.getSystemService(Context.LOCATION_SERVICE))
+ .thenReturn(mMockLocationManager);
+ when(mMockLocationManager.isLocationEnabled()).thenReturn(false);
+ mCarLocationService.init();
+ ArgumentCaptor<ICarDrivingStateChangeListener> changeListenerArgument =
+ ArgumentCaptor.forClass(ICarDrivingStateChangeListener.class);
+ verify(mMockCarDrivingStateService).registerDrivingStateChangeListener(
+ changeListenerArgument.capture());
+ ICarDrivingStateChangeListener changeListener = changeListenerArgument.getValue();
+ assertTrue(getLocationCacheFile().exists());
+
+ changeListener.onDrivingStateChanged(
+ new CarDrivingStateEvent(CarDrivingStateEvent.DRIVING_STATE_MOVING,
+ SystemClock.elapsedRealtimeNanos()));
+
+ verify(mMockLocationManager, times(0)).isLocationEnabled();
+ verify(mMockCarDrivingStateService, times(1)).unregisterDrivingStateChangeListener(any());
+ assertFalse(getLocationCacheFile().exists());
+ }
+
private void writeCacheFile(String json) throws IOException {
FileOutputStream fos = new FileOutputStream(getLocationCacheFile());
fos.write(json.getBytes());