blob: 60eff1c03c569f418e8cae85f3bfa5a64ef7a469 [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.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 androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.car.connecteddevice.storage.ConnectedDeviceStorage;
import com.android.car.connecteddevice.util.ByteUtils;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* A secure channel established with the reconnection flow.
*/
class ReconnectSecureChannel extends SecureBleChannel {
private static final String TAG = "ReconnectSecureChannel";
private final ConnectedDeviceStorage mStorage;
private final String mDeviceId;
private final byte[] mExpectedChallengeResponse;
@HandshakeState
private int mState = HandshakeState.UNKNOWN;
private AtomicBoolean mHasVerifiedDevice = new AtomicBoolean(false);
/**
* Create a new secure reconnection channel.
*
* @param stream The {@link BleDeviceMessageStream} for communication with the device.
* @param storage {@link ConnectedDeviceStorage} for secure storage.
* @param deviceId Id of the device being reconnected.
* @param expectedChallengeResponse Expected response to challenge issued in reconnect.
*/
ReconnectSecureChannel(@NonNull BleDeviceMessageStream stream,
@NonNull ConnectedDeviceStorage storage, @NonNull String deviceId,
@NonNull byte[] expectedChallengeResponse) {
super(stream, newReconnectRunner());
mStorage = storage;
mDeviceId = deviceId;
mExpectedChallengeResponse = expectedChallengeResponse;
}
private static EncryptionRunner newReconnectRunner() {
EncryptionRunner encryptionRunner = EncryptionRunnerFactory.newRunner();
encryptionRunner.setIsReconnect(true);
return encryptionRunner;
}
@Override
void processHandshake(byte[] message) throws HandshakeException {
switch (mState) {
case HandshakeState.UNKNOWN:
if (!mHasVerifiedDevice.get()) {
processHandshakeDeviceVerification(message);
} else {
processHandshakeInitialization(message);
}
break;
case HandshakeState.IN_PROGRESS:
processHandshakeInProgress(message);
break;
case HandshakeState.RESUMING_SESSION:
processHandshakeResumingSession(message);
break;
default:
loge(TAG, "Encountered unexpected handshake state: " + mState + ".");
notifySecureChannelFailure(CHANNEL_ERROR_INVALID_STATE);
}
}
private void processHandshakeDeviceVerification(byte[] message) {
byte[] challengeResponse = Arrays.copyOf(message,
mExpectedChallengeResponse.length);
byte[] deviceChallenge = Arrays.copyOfRange(message,
mExpectedChallengeResponse.length, message.length);
if (!Arrays.equals(mExpectedChallengeResponse, challengeResponse)) {
notifySecureChannelFailure(CHANNEL_ERROR_INVALID_ENCRYPTION_KEY);
return;
}
logd(TAG, "Responding to challenge " + ByteUtils.byteArrayToHexString(deviceChallenge)
+ ".");
byte[] deviceChallengeResponse = mStorage.hashWithChallengeSecret(mDeviceId,
deviceChallenge);
if (deviceChallengeResponse == null) {
notifySecureChannelFailure(CHANNEL_ERROR_STORAGE_ERROR);
}
sendHandshakeMessage(deviceChallengeResponse, /* isEncrypted= */ false);
mHasVerifiedDevice.set(true);
}
private void processHandshakeInitialization(byte[] message) throws HandshakeException {
logd(TAG, "Responding to handshake init request.");
HandshakeMessage handshakeMessage = getEncryptionRunner().respondToInitRequest(message);
mState = handshakeMessage.getHandshakeState();
sendHandshakeMessage(handshakeMessage.getNextMessage(), /* isEncrypted= */ false);
}
private void processHandshakeInProgress(@NonNull byte[] message) throws HandshakeException {
logd(TAG, "Continuing handshake.");
HandshakeMessage handshakeMessage = getEncryptionRunner().continueHandshake(message);
mState = handshakeMessage.getHandshakeState();
}
private void processHandshakeResumingSession(@NonNull byte[] message)
throws HandshakeException {
logd(TAG, "Start reconnection authentication.");
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 = getEncryptionRunner().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());
setEncryptionKey(newKey);
sendServerAuthToClient(handshakeMessage.getNextMessage());
notifyCallback(Callback::onSecureChannelEstablished);
}
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;
}
sendHandshakeMessage(message, /* isEncrypted= */ false);
}
}