blob: a821186dd5be5ff9ef1c15cda4753bbd11a67d98 [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.connecteddevice.ble;
import static com.android.car.connecteddevice.util.SafeLog.logd;
import static com.android.car.connecteddevice.util.SafeLog.loge;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.car.encryptionrunner.EncryptionRunner;
import android.car.encryptionrunner.EncryptionRunnerFactory;
import android.car.encryptionrunner.HandshakeException;
import android.car.encryptionrunner.HandshakeMessage;
import android.car.encryptionrunner.HandshakeMessage.HandshakeState;
import android.car.encryptionrunner.Key;
import com.android.car.connecteddevice.BleStreamProtos.BleOperationProto.OperationType;
import com.android.car.connecteddevice.storage.ConnectedDeviceStorage;
import com.android.car.connecteddevice.util.ByteUtils;
import com.android.internal.annotations.VisibleForTesting;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.security.SignatureException;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
/**
* Establishes a secure channel with {@link EncryptionRunner} over {@link BleDeviceMessageStream} as
* server side, sends and receives messages securely after the secure channel has been established.
*/
class SecureBleChannel {
@Retention(RetentionPolicy.SOURCE)
@IntDef(prefix = { "CHANNEL_ERROR" },
value = {
CHANNEL_ERROR_INVALID_HANDSHAKE,
CHANNEL_ERROR_INVALID_MSG,
CHANNEL_ERROR_INVALID_DEVICE_ID,
CHANNEL_ERROR_INVALID_VERIFICATION,
CHANNEL_ERROR_INVALID_STATE,
CHANNEL_ERROR_INVALID_ENCRYPTION_KEY,
CHANNEL_ERROR_STORAGE_ERROR
}
)
@interface ChannelError { }
/** Indicates an error during a Handshake of EncryptionRunner. */
static final int CHANNEL_ERROR_INVALID_HANDSHAKE = 0;
/** Received an invalid handshake message or has an invalid handshake message to send. */
static final int CHANNEL_ERROR_INVALID_MSG = 1;
/** Unable to retrieve a valid id. */
static final int CHANNEL_ERROR_INVALID_DEVICE_ID = 2;
/** Unable to get verification code or there's a error during pin verification. */
static final int CHANNEL_ERROR_INVALID_VERIFICATION = 3;
/** Encountered an unexpected handshake state. */
static final int CHANNEL_ERROR_INVALID_STATE = 4;
/** Failed to get a valid previous/new encryption key.*/
static final int CHANNEL_ERROR_INVALID_ENCRYPTION_KEY = 5;
/** Failed to save the encryption key*/
static final int CHANNEL_ERROR_STORAGE_ERROR = 6;
@VisibleForTesting
static final byte[] CONFIRMATION_SIGNAL = "True".getBytes();
private static final String TAG = "SecureBleChannel";
private final BleDeviceMessageStream mStream;
private final ConnectedDeviceStorage mStorage;
private final boolean mIsReconnect;
private final EncryptionRunner mEncryptionRunner;
private final AtomicReference<Key> mEncryptionKey = new AtomicReference<>();
private @HandshakeState int mState = HandshakeState.UNKNOWN;
private String mDeviceId;
private Callback mCallback;
private ShowVerificationCodeListener mShowVerificationCodeListener;
SecureBleChannel(@NonNull BleDeviceMessageStream stream,
@NonNull ConnectedDeviceStorage storage) {
this(stream, storage, /* isReconnect = */ true, EncryptionRunnerFactory.newRunner());
}
SecureBleChannel(@NonNull BleDeviceMessageStream stream,
@NonNull ConnectedDeviceStorage storage, boolean isReconnect,
@NonNull EncryptionRunner encryptionRunner) {
mStream = stream;
mStorage = storage;
mIsReconnect = isReconnect;
mEncryptionRunner = encryptionRunner;
mEncryptionRunner.setIsReconnect(isReconnect);
mStream.setMessageReceivedListener(mStreamListener);
}
private void processHandshake(@NonNull byte[] message) throws HandshakeException {
switch (mState) {
case HandshakeState.UNKNOWN:
processHandshakeUnknown(message);
break;
case HandshakeState.IN_PROGRESS:
processHandshakeInProgress(message);
break;
case HandshakeState.RESUMING_SESSION:
processHandshakeResumingSession(message);
break;
default:
loge(TAG, "Encountered unexpected handshake state: " + mState + ". Received "
+ "message: " + ByteUtils.byteArrayToHexString(message) + ".");
notifySecureChannelFailure(CHANNEL_ERROR_INVALID_STATE);
}
}
private void processHandshakeUnknown(@NonNull byte[] message) throws HandshakeException {
if (mDeviceId != null) {
logd(TAG, "Responding to handshake init request.");
HandshakeMessage handshakeMessage = mEncryptionRunner.respondToInitRequest(message);
mState = handshakeMessage.getHandshakeState();
sendHandshakeMessage(handshakeMessage.getNextMessage());
return;
}
UUID deviceId = ByteUtils.bytesToUUID(message);
if (deviceId == null) {
loge(TAG, "Received invalid device id. Ignoring.");
return;
}
mDeviceId = deviceId.toString();
if (mIsReconnect && !hasEncryptionKey(mDeviceId)) {
loge(TAG, "Attempted to reconnect device but no key found. Aborting secure channel.");
notifySecureChannelFailure(CHANNEL_ERROR_INVALID_DEVICE_ID);
return;
}
notifyCallback(callback -> callback.onDeviceIdReceived(mDeviceId));
sendUniqueIdToClient();
}
private void processHandshakeInProgress(@NonNull byte[] message) throws HandshakeException {
logd(TAG, "Continuing handshake.");
HandshakeMessage handshakeMessage = mEncryptionRunner.continueHandshake(message);
mState = handshakeMessage.getHandshakeState();
boolean isValidStateForAssociation = !mIsReconnect
&& mState == HandshakeState.VERIFICATION_NEEDED;
boolean isValidStateForReconnect = mIsReconnect
&& mState == HandshakeState.RESUMING_SESSION;
if (!isValidStateForAssociation && !isValidStateForReconnect) {
loge(TAG, "processHandshakeInProgress: Encountered unexpected handshake state: "
+ mState + ".");
notifySecureChannelFailure(CHANNEL_ERROR_INVALID_STATE);
return;
}
if (!isValidStateForAssociation) {
return;
}
String code = handshakeMessage.getVerificationCode();
if (code == null) {
loge(TAG, "Unable to get verification code.");
notifySecureChannelFailure(CHANNEL_ERROR_INVALID_VERIFICATION);
return;
}
if (mShowVerificationCodeListener != null) {
logd(TAG, "Showing pairing code: " + code);
mShowVerificationCodeListener.showVerificationCode(code);
}
}
private void processHandshakeResumingSession(@NonNull byte[] message)
throws HandshakeException {
logd(TAG, "Start reconnection authentication.");
if (mDeviceId == null) {
loge(TAG, "processHandshakeResumingSession: Unable to resume session, device id is "
+ "null.");
notifySecureChannelFailure(CHANNEL_ERROR_INVALID_DEVICE_ID);
return;
}
byte[] previousKey = mStorage.getEncryptionKey(mDeviceId);
if (previousKey == null) {
loge(TAG, "Unable to resume session, previous key is null.");
notifySecureChannelFailure(CHANNEL_ERROR_INVALID_ENCRYPTION_KEY);
return;
}
HandshakeMessage handshakeMessage = mEncryptionRunner.authenticateReconnection(message,
previousKey);
mState = handshakeMessage.getHandshakeState();
if (mState != HandshakeState.FINISHED) {
loge(TAG, "Unable to resume session, unexpected next handshake state: " + mState + ".");
notifySecureChannelFailure(CHANNEL_ERROR_INVALID_STATE);
return;
}
Key newKey = handshakeMessage.getKey();
if (newKey == null) {
loge(TAG, "Unable to resume session, new key is null.");
notifySecureChannelFailure(CHANNEL_ERROR_INVALID_ENCRYPTION_KEY);
return;
}
logd(TAG, "Saved new key for reconnection.");
mStorage.saveEncryptionKey(mDeviceId, newKey.asBytes());
mEncryptionKey.set(newKey);
sendServerAuthToClient(handshakeMessage.getNextMessage());
notifyCallback(callback -> callback.onSecureChannelEstablished());
}
private void sendUniqueIdToClient() {
UUID uniqueId = mStorage.getUniqueId();
DeviceMessage deviceMessage = new DeviceMessage(/* recipient = */ null,
/* isMessageEncrypted = */ false, ByteUtils.uuidToBytes(uniqueId));
logd(TAG, "Sending car's device id of " + uniqueId + " to device.");
mStream.writeMessage(deviceMessage, OperationType.ENCRYPTION_HANDSHAKE);
}
private boolean hasEncryptionKey(@NonNull String id) {
return mStorage.getEncryptionKey(id) != null;
}
private void sendHandshakeMessage(@Nullable byte[] message) {
if (message == null) {
loge(TAG, "Unable to send next handshake message, message is null.");
notifySecureChannelFailure(CHANNEL_ERROR_INVALID_MSG);
return;
}
logd(TAG, "Send handshake message: " + ByteUtils.byteArrayToHexString(message) + ".");
DeviceMessage deviceMessage = new DeviceMessage(/* recipient = */ null,
/* isMessageEncrypted = */ false, message);
mStream.writeMessage(deviceMessage, OperationType.ENCRYPTION_HANDSHAKE);
}
private void sendServerAuthToClient(@Nullable byte[] message) {
if (message == null) {
loge(TAG, "Unable to send server authentication message to client, message is null.");
notifySecureChannelFailure(CHANNEL_ERROR_INVALID_MSG);
return;
}
DeviceMessage deviceMessage = new DeviceMessage(/* recipient = */ null,
/* isMessageEncrypted = */ false, message);
mStream.writeMessage(deviceMessage, OperationType.ENCRYPTION_HANDSHAKE);
}
/**
* Send an encrypted message.
* <p>Note: This should be called only after the secure channel has been established.</p>
*
* @param deviceMessage The {@link DeviceMessage} to encrypt and send.
*/
void sendEncryptedMessage(@NonNull DeviceMessage deviceMessage) throws IllegalStateException {
if (!deviceMessage.isMessageEncrypted()) {
loge(TAG, "Encryption not required for this message " + deviceMessage + ".");
return;
}
Key key = mEncryptionKey.get();
if (key == null) {
throw new IllegalStateException("Secure channel has not been established.");
}
byte[] encryptedMessage = key.encryptData(deviceMessage.getMessage());
deviceMessage.setMessage(encryptedMessage);
mStream.writeMessage(deviceMessage, OperationType.CLIENT_MESSAGE);
}
/**
* Called by the client to notify that the user has accepted a pairing code or any out-of-band
* confirmation, and send confirmation signals to remote bluetooth device.
*/
void notifyOutOfBandAccepted() {
HandshakeMessage message;
try {
message = mEncryptionRunner.verifyPin();
} catch (HandshakeException e) {
loge(TAG, "Error during PIN verification", e);
notifySecureChannelFailure(CHANNEL_ERROR_INVALID_VERIFICATION);
return;
}
if (message.getHandshakeState() != HandshakeState.FINISHED) {
loge(TAG, "Handshake not finished after calling verify PIN. Instead got "
+ "state: " + message.getHandshakeState() + ".");
notifySecureChannelFailure(CHANNEL_ERROR_INVALID_STATE);
return;
}
Key localKey = message.getKey();
if (localKey == null) {
loge(TAG, "Unable to finish association, generated key is null.");
notifySecureChannelFailure(CHANNEL_ERROR_INVALID_ENCRYPTION_KEY);
return;
}
mState = message.getHandshakeState();
mStorage.saveEncryptionKey(mDeviceId, localKey.asBytes());
mEncryptionKey.set(localKey);
if (mDeviceId == null) {
loge(TAG, "Unable to finish association, device id is null.");
notifySecureChannelFailure(CHANNEL_ERROR_INVALID_DEVICE_ID);
return;
}
logd(TAG, "Pairing code successfully verified and encryption key saved. Sending "
+ "confirmation to device.");
notifyCallback(Callback::onSecureChannelEstablished);
DeviceMessage deviceMessage = new DeviceMessage(/* recipient = */ null,
/* isMessageEncrypted = */ false, CONFIRMATION_SIGNAL);
mStream.writeMessage(deviceMessage, OperationType.ENCRYPTION_HANDSHAKE);
}
/** Get the BLE stream backing this channel. */
@NonNull
BleDeviceMessageStream getStream() {
return mStream;
}
/**Set the listener that notifies to show verification code. {@code null} to clear.*/
void setShowVerificationCodeListener(@Nullable ShowVerificationCodeListener listener) {
mShowVerificationCodeListener = listener;
}
@VisibleForTesting
@Nullable
ShowVerificationCodeListener getShowVerificationCodeListener() {
return mShowVerificationCodeListener;
}
/** Register a callback that notifies secure channel events. */
void registerCallback(Callback callback) {
mCallback = callback;
}
/** Unregister a callback. */
void unregisterCallback(Callback callback) {
if (callback == mCallback) {
mCallback = null;
}
}
@VisibleForTesting
@Nullable
Callback getCallback() {
return mCallback;
}
private void notifyCallback(Consumer<Callback> notification) {
if (mCallback != null) {
notification.accept(mCallback);
}
}
private void notifySecureChannelFailure(@ChannelError int error) {
loge(TAG, "Secure channel error: " + error);
notifyCallback(callback -> callback.onEstablishSecureChannelFailure(error));
}
private final BleDeviceMessageStream.MessageReceivedListener mStreamListener =
new BleDeviceMessageStream.MessageReceivedListener() {
@Override
public void onMessageReceived(DeviceMessage deviceMessage,
OperationType operationType) {
byte[] message = deviceMessage.getMessage();
switch(operationType) {
case ENCRYPTION_HANDSHAKE:
logd(TAG, "Message received and handed off to handshake.");
try {
processHandshake(message);
} catch (HandshakeException e) {
loge(TAG, "Handshake failed.", e);
notifyCallback(callback -> callback.onEstablishSecureChannelFailure(
CHANNEL_ERROR_INVALID_HANDSHAKE));
}
break;
case CLIENT_MESSAGE:
logd(TAG, "Received client message.");
if (!deviceMessage.isMessageEncrypted()) {
notifyCallback(callback -> callback.onMessageReceived(
deviceMessage));
return;
}
Key key = mEncryptionKey.get();
if (key == null) {
loge(TAG, "Received encrypted message before secure channel has "
+ "been established.");
notifyCallback(callback -> callback.onMessageReceivedError(null));
return;
}
try {
byte[] decryptedPayload =
key.decryptData(deviceMessage.getMessage());
deviceMessage.setMessage(decryptedPayload);
notifyCallback(
callback -> callback.onMessageReceived(deviceMessage));
} catch (SignatureException e) {
loge(TAG, "Could not decrypt client credentials.", e);
notifyCallback(callback -> callback.onMessageReceivedError(e));
}
break;
default:
loge(TAG, "Received unexpected operation type: " + operationType + ".");
}
}
};
/**
* Callbacks that will be invoked during establishing secure channel, sending and receiving
* messages securely.
*/
interface Callback {
/**
* Invoked when secure channel has been established successfully.
*/
void onSecureChannelEstablished();
/**
* Invoked when a {@link ChannelError} has been encountered in attempting to establish
* a secure channel.
*
* @param error The failure indication.
*/
void onEstablishSecureChannelFailure(@SecureBleChannel.ChannelError int error);
/**
* Invoked when a complete message is received securely from the client and decrypted.
*
* @param deviceMessage The {@link DeviceMessage} with decrypted message.
*/
void onMessageReceived(@NonNull DeviceMessage deviceMessage);
/**
* Invoked when there was an error during a processing or decrypting of a client message.
*
* @param exception The error.
*/
void onMessageReceivedError(@Nullable Exception exception);
/**
* Invoked when the device id was received from the client.
*
* @param deviceId The unique device id of client.
*/
void onDeviceIdReceived(@NonNull String deviceId);
}
/**
* Listener that will be invoked to display verification code.
*/
interface ShowVerificationCodeListener {
/**
* Invoke when a verification need to be displayed during device association.
*
* @param code The verification code to show.
*/
void showVerificationCode(@NonNull String code);
}
}