Add an OobUkey2EncryptionRunner to IHU libraries.
Fixes: 154947661
Test: unit tests pass
Change-Id: I3ada75c7e94018a77244ef4c22aa739f715dcdf7
diff --git a/EncryptionRunner/src/android/car/encryptionrunner/EncryptionRunnerFactory.java b/EncryptionRunner/src/android/car/encryptionrunner/EncryptionRunnerFactory.java
index 156abd8..5b81c87 100644
--- a/EncryptionRunner/src/android/car/encryptionrunner/EncryptionRunnerFactory.java
+++ b/EncryptionRunner/src/android/car/encryptionrunner/EncryptionRunnerFactory.java
@@ -16,6 +16,8 @@
package android.car.encryptionrunner;
+import android.annotation.IntDef;
+
import com.android.internal.annotations.VisibleForTesting;
/**
@@ -27,11 +29,36 @@
// prevent instantiation.
}
+ @IntDef({EncryptionRunnerType.UKEY2, EncryptionRunnerType.OOB_UKEY2})
+ public @interface EncryptionRunnerType {
+ /** Use Ukey2 as underlying key exchange. */
+ int UKEY2 = 0;
+ /** Use Ukey2 and an out of band channel as underlying key exchange. */
+ int OOB_UKEY2 = 1;
+ }
+
+ /**
+ * Creates a new {@link EncryptionRunner} based on {@param type}.
+ */
+ public static EncryptionRunner newRunner(@EncryptionRunnerType int type) {
+ switch (type) {
+ case EncryptionRunnerType.UKEY2:
+ return new Ukey2EncryptionRunner();
+ case EncryptionRunnerType.OOB_UKEY2:
+ return new OobUkey2EncryptionRunner();
+ default:
+ throw new IllegalArgumentException("Unknown EncryptionRunnerType: " + type);
+ }
+ }
+
/**
* Creates a new {@link EncryptionRunner}.
+ *
+ * @deprecated Use {@link #newRunner(int)} instead.
*/
+ @Deprecated
public static EncryptionRunner newRunner() {
- return new Ukey2EncryptionRunner();
+ return newRunner(EncryptionRunnerType.UKEY2);
}
/**
diff --git a/EncryptionRunner/src/android/car/encryptionrunner/HandshakeMessage.java b/EncryptionRunner/src/android/car/encryptionrunner/HandshakeMessage.java
index fa6705d..e88d482 100644
--- a/EncryptionRunner/src/android/car/encryptionrunner/HandshakeMessage.java
+++ b/EncryptionRunner/src/android/car/encryptionrunner/HandshakeMessage.java
@@ -17,6 +17,7 @@
package android.car.encryptionrunner;
import android.annotation.IntDef;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.text.TextUtils;
@@ -34,7 +35,8 @@
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({HandshakeState.UNKNOWN, HandshakeState.IN_PROGRESS, HandshakeState.VERIFICATION_NEEDED,
- HandshakeState.FINISHED, HandshakeState.INVALID, HandshakeState.RESUMING_SESSION,})
+ HandshakeState.FINISHED, HandshakeState.INVALID, HandshakeState.RESUMING_SESSION,
+ HandshakeState.OOB_VERIFICATION_NEEDED})
public @interface HandshakeState {
/**
* The initial state, this value is not expected to be returned.
@@ -60,6 +62,10 @@
* The handshake is complete, but extra verification is needed.
*/
int RESUMING_SESSION = 5;
+ /**
+ * The handshake is complete, but out of band verification of the code is needed.
+ */
+ int OOB_VERIFICATION_NEEDED = 6;
}
@HandshakeState
@@ -67,6 +73,7 @@
private final Key mKey;
private final byte[] mNextMessage;
private final String mVerificationCode;
+ private final byte[] mOobVerificationCode;
/**
* @return Returns a builder for {@link HandshakeMessage}.
@@ -82,11 +89,13 @@
@HandshakeState int handshakeState,
@Nullable Key key,
@Nullable byte[] nextMessage,
- @Nullable String verificationCode) {
+ @Nullable String verificationCode,
+ @Nullable byte[] oobVerificationCode) {
mHandshakeState = handshakeState;
mKey = key;
mNextMessage = nextMessage;
mVerificationCode = verificationCode;
+ mOobVerificationCode = oobVerificationCode;
}
/**
@@ -121,12 +130,22 @@
return mVerificationCode;
}
+ /**
+ * Returns a verification code to be encrypted using an out-of-band key and sent to the remote
+ * device.
+ */
+ @Nullable
+ public byte[] getOobVerificationCode() {
+ return mOobVerificationCode;
+ }
+
static class Builder {
@HandshakeState
int mHandshakeState;
Key mKey;
byte[] mNextMessage;
String mVerificationCode;
+ byte[] mOobVerificationCode;
Builder setHandshakeState(@HandshakeState int handshakeState) {
mHandshakeState = handshakeState;
@@ -148,6 +167,11 @@
return this;
}
+ Builder setOobVerificationCode(@NonNull byte[] oobVerificationCode) {
+ mOobVerificationCode = oobVerificationCode;
+ return this;
+ }
+
HandshakeMessage build() {
if (mHandshakeState == HandshakeState.UNKNOWN) {
throw new IllegalStateException("must set handshake state before calling build");
@@ -155,9 +179,15 @@
if (mHandshakeState == HandshakeState.VERIFICATION_NEEDED
&& TextUtils.isEmpty(mVerificationCode)) {
throw new IllegalStateException(
- "if state is verification needed, must have verification code");
+ "State is verification needed, but verification code null.");
}
- return new HandshakeMessage(mHandshakeState, mKey, mNextMessage, mVerificationCode);
+ if (mHandshakeState == HandshakeState.OOB_VERIFICATION_NEEDED
+ && (mOobVerificationCode == null || mOobVerificationCode.length == 0)) {
+ throw new IllegalStateException(
+ "State is OOB verification needed, but OOB verification code null.");
+ }
+ return new HandshakeMessage(mHandshakeState, mKey, mNextMessage, mVerificationCode,
+ mOobVerificationCode);
}
}
diff --git a/EncryptionRunner/src/android/car/encryptionrunner/OobUkey2EncryptionRunner.java b/EncryptionRunner/src/android/car/encryptionrunner/OobUkey2EncryptionRunner.java
new file mode 100644
index 0000000..9474bd4
--- /dev/null
+++ b/EncryptionRunner/src/android/car/encryptionrunner/OobUkey2EncryptionRunner.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 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 android.car.encryptionrunner;
+
+import com.google.security.cryptauth.lib.securegcm.Ukey2Handshake;
+
+/**
+ * An {@link EncryptionRunner} that uses Ukey2 as the underlying implementation, and generates a
+ * longer token for the out of band verification step.
+ *
+ * <p>To use this class:
+ *
+ * <p>1. As a client.
+ *
+ * <p>{@code
+ * HandshakeMessage initialClientMessage = clientRunner.initHandshake();
+ * sendToServer(initialClientMessage.getNextMessage());
+ * byte message = getServerResponse();
+ * HandshakeMessage message = clientRunner.continueHandshake(message);
+ * }
+ *
+ * <p>If it is a first-time connection,
+ *
+ * <p>{@code message.getHandshakeState()} should be OOB_VERIFICATION_NEEDED. Wait for an encrypted
+ * message sent from the server, and decrypt that message with an out of band key that was generated
+ * before the start of the handshake.
+ *
+ * <p>After confirming that the decrypted message matches the verification code, send an encrypted
+ * message back to the server, and call {@code HandshakeMessage lastMessage =
+ * clientRunner.verifyPin();} otherwise {@code clientRunner.invalidPin(); }
+ *
+ * <p>Use {@code lastMessage.getKey()} to get the key for encryption.
+ *
+ * <p>If it is a reconnection,
+ *
+ * <p>{@code message.getHandshakeState()} should be RESUMING_SESSION, PIN has been verified blindly,
+ * send the authentication message over to server, then authenticate the message from server.
+ *
+ * <p>{@code
+ * clientMessage = clientRunner.initReconnectAuthentication(previousKey)
+ * sendToServer(clientMessage.getNextMessage());
+ * HandshakeMessage lastMessage = clientRunner.authenticateReconnection(previousKey, message)
+ * }
+ *
+ * <p>{@code lastMessage.getHandshakeState()} should be FINISHED if reconnection handshake is done.
+ *
+ * <p>2. As a server.
+ *
+ * <p>{@code
+ * byte[] initialMessage = getClientMessageBytes();
+ * HandshakeMessage message = serverRunner.respondToInitRequest(initialMessage);
+ * sendToClient(message.getNextMessage());
+ * byte[] clientMessage = getClientResponse();
+ * HandshakeMessage message = serverRunner.continueHandshake(clientMessage);}
+ *
+ * <p>if it is a first-time connection,
+ *
+ * <p>{@code message.getHandshakeState()} should be OOB_VERIFICATION_NEEDED, send the verification
+ * code to the client, encrypted using an out of band key generated before the start of the
+ * handshake, and wait for a response from the client.
+ * If the decrypted message from the client matches the verification code, call {@code
+ * HandshakeMessage lastMessage = serverRunner.verifyPin}, otherwise
+ * {@code clientRunner.invalidPin(); }
+ * Use {@code lastMessage.getKey()} to get the key for encryption.
+ *
+ * <p>If it is a reconnection,
+ *
+ * <p>{@code message.getHandshakeState()} should be RESUMING_SESSION,PIN has been verified blindly,
+ * waiting for client message.
+ * After client message been received,
+ * {@code serverMessage = serverRunner.authenticateReconnection(previousKey, message);
+ * sendToClient(serverMessage.getNextMessage());}
+ * {@code serverMessage.getHandshakeState()} should be FINISHED if reconnection handshake is done.
+ *
+ * <p>Also see {@link EncryptionRunnerTest} for examples.
+ */
+public final class OobUkey2EncryptionRunner extends Ukey2EncryptionRunner {
+ // Choose max verification string length supported by Ukey2
+ private static final int VERIFICATION_STRING_LENGTH = 32;
+
+ @Override
+ public HandshakeMessage continueHandshake(byte[] response) throws HandshakeException {
+ checkInitialized();
+
+ Ukey2Handshake uKey2Client = getUkey2Client();
+
+ try {
+ if (uKey2Client.getHandshakeState() != Ukey2Handshake.State.IN_PROGRESS) {
+ throw new IllegalStateException(
+ "handshake is not in progress, state =" + uKey2Client.getHandshakeState());
+ }
+ uKey2Client.parseHandshakeMessage(response);
+
+ // Not obvious from ukey2 api, but getting the next message can change the state.
+ // calling getNext message might go from in progress to verification needed, on
+ // the assumption that we already send this message to the peer.
+ byte[] nextMessage = null;
+ if (uKey2Client.getHandshakeState() == Ukey2Handshake.State.IN_PROGRESS) {
+ nextMessage = uKey2Client.getNextHandshakeMessage();
+ }
+
+ byte[] verificationCode = null;
+ if (uKey2Client.getHandshakeState() == Ukey2Handshake.State.VERIFICATION_NEEDED) {
+ // getVerificationString() needs to be called before notifyPinVerified().
+ verificationCode = uKey2Client.getVerificationString(VERIFICATION_STRING_LENGTH);
+ if (isReconnect()) {
+ HandshakeMessage handshakeMessage = verifyPin();
+ return HandshakeMessage.newBuilder()
+ .setHandshakeState(handshakeMessage.getHandshakeState())
+ .setNextMessage(nextMessage)
+ .build();
+ }
+ }
+
+ return HandshakeMessage.newBuilder()
+ .setHandshakeState(HandshakeMessage.HandshakeState.OOB_VERIFICATION_NEEDED)
+ .setNextMessage(nextMessage)
+ .setOobVerificationCode(verificationCode)
+ .build();
+ } catch (com.google.security.cryptauth.lib.securegcm.HandshakeException
+ | Ukey2Handshake.AlertException e) {
+ throw new HandshakeException(e);
+ }
+ }
+}
diff --git a/EncryptionRunner/src/android/car/encryptionrunner/Ukey2EncryptionRunner.java b/EncryptionRunner/src/android/car/encryptionrunner/Ukey2EncryptionRunner.java
index 904d5c2..454a48b 100644
--- a/EncryptionRunner/src/android/car/encryptionrunner/Ukey2EncryptionRunner.java
+++ b/EncryptionRunner/src/android/car/encryptionrunner/Ukey2EncryptionRunner.java
@@ -323,6 +323,14 @@
}
}
+ protected final Ukey2Handshake getUkey2Client() {
+ return mUkey2client;
+ }
+
+ protected final boolean isReconnect() {
+ return mIsReconnect;
+ }
+
@HandshakeMessage.HandshakeState
private int getHandshakeState() {
checkInitialized();
@@ -362,7 +370,7 @@
return (UKey2Key) key;
}
- private void checkInitialized() {
+ protected void checkInitialized() {
if (mUkey2client == null) {
throw new IllegalStateException("runner not initialized");
}
diff --git a/tests/carservice_unit_test/src/android/car/encryptionrunner/EncryptionRunnerTest.java b/tests/carservice_unit_test/src/android/car/encryptionrunner/EncryptionRunnerTest.java
index d4e6e2d..38802e4 100644
--- a/tests/carservice_unit_test/src/android/car/encryptionrunner/EncryptionRunnerTest.java
+++ b/tests/carservice_unit_test/src/android/car/encryptionrunner/EncryptionRunnerTest.java
@@ -37,54 +37,98 @@
EncryptionRunner newRunner();
}
+ private interface HandshakeVerifier {
+ void verifyHandshake(EncryptionRunner clientRunner, EncryptionRunner serverRunner)
+ throws Exception;
+ }
+
@Test
public void happyFlow_dummyRunner() throws Exception {
- verifyRunners(EncryptionRunnerFactory::newDummyRunner);
+ verifyRunners(EncryptionRunnerFactory::newDummyRunner,
+ EncryptionRunnerTest::verifyHandshake);
}
@Test
public void happyFlow_ukey2Runner() throws Exception {
- verifyRunners(EncryptionRunnerFactory::newRunner);
+ verifyRunners(EncryptionRunnerTest::newRunner, EncryptionRunnerTest::verifyHandshake);
+ }
+
+ @Test
+ public void happyFlow_oobUkey2Runner() throws Exception {
+ verifyRunners(EncryptionRunnerTest::newOobRunner, EncryptionRunnerTest::verifyOobHandshake);
}
@Test
public void happyFlow_dummyRunner_reconnect() throws Exception {
- setUpFirstConnection(EncryptionRunnerFactory::newDummyRunner);
+ setUpFirstConnection(EncryptionRunnerFactory::newDummyRunner,
+ EncryptionRunnerTest::verifyHandshake);
verifyRunnersReconnect(EncryptionRunnerFactory::newDummyRunner);
}
@Test
public void happyFlow_uKey2Runner_reconnect() throws Exception {
- setUpFirstConnection(EncryptionRunnerFactory::newRunner);
- verifyRunnersReconnect(EncryptionRunnerFactory::newRunner);
+ setUpFirstConnection(EncryptionRunnerTest::newRunner,
+ EncryptionRunnerTest::verifyHandshake);
+ verifyRunnersReconnect(EncryptionRunnerTest::newRunner);
+ }
+
+ @Test
+ public void happyFlow_oobUey2Runner_reconnect() throws Exception {
+ setUpFirstConnection(EncryptionRunnerTest::newOobRunner,
+ EncryptionRunnerTest::verifyOobHandshake);
+ verifyRunnersReconnect(EncryptionRunnerTest::newOobRunner);
}
@Test
public void uKey2Runner_reconnect_encrypt_and_decrypt() throws Exception {
- setUpFirstConnection(EncryptionRunnerFactory::newRunner);
- setUpReconnection(EncryptionRunnerFactory::newRunner);
+ setUpFirstConnection(EncryptionRunnerTest::newRunner,
+ EncryptionRunnerTest::verifyHandshake);
+ setUpReconnection(EncryptionRunnerTest::newRunner, EncryptionRunnerTest::verifyHandshake);
assertThat(mClientKey.decryptData(mServerKey.encryptData(mData))).isEqualTo(mData);
}
@Test
public void dummyRunner_reconnect_encrypt_and_decrypt() throws Exception {
- setUpFirstConnection(EncryptionRunnerFactory::newDummyRunner);
- setUpReconnection(EncryptionRunnerFactory::newDummyRunner);
+ setUpFirstConnection(EncryptionRunnerFactory::newDummyRunner,
+ EncryptionRunnerTest::verifyHandshake);
+ setUpReconnection(EncryptionRunnerFactory::newDummyRunner,
+ EncryptionRunnerTest::verifyHandshake);
assertThat(mClientKey.decryptData(mServerKey.encryptData(mData))).isEqualTo(mData);
}
- private void setUpFirstConnection(RunnerFactory runnerFactory) throws Exception {
+ @Test
+ public void oobUkey2Runner_reconnect_encrypt_and_decrypt() throws Exception {
+ setUpFirstConnection(EncryptionRunnerTest::newOobRunner,
+ EncryptionRunnerTest::verifyOobHandshake);
+ setUpReconnection(EncryptionRunnerTest::newOobRunner,
+ EncryptionRunnerTest::verifyOobHandshake);
+ assertThat(mClientKey.decryptData(mServerKey.encryptData(mData))).isEqualTo(mData);
+ }
+
+ private static EncryptionRunner newRunner() {
+ return EncryptionRunnerFactory.newRunner(
+ EncryptionRunnerFactory.EncryptionRunnerType.UKEY2);
+ }
+
+ private static EncryptionRunner newOobRunner() {
+ return EncryptionRunnerFactory.newRunner(
+ EncryptionRunnerFactory.EncryptionRunnerType.OOB_UKEY2);
+ }
+
+ private void setUpFirstConnection(RunnerFactory runnerFactory,
+ HandshakeVerifier handshakeVerifier) throws Exception {
EncryptionRunner clientRunner = runnerFactory.newRunner();
EncryptionRunner serverRunner = runnerFactory.newRunner();
- verifyHandshake(clientRunner, serverRunner);
+ handshakeVerifier.verifyHandshake(clientRunner, serverRunner);
HandshakeMessage finalServerMessage = serverRunner.verifyPin();
HandshakeMessage finalClientMessage = clientRunner.verifyPin();
mServerKey = finalServerMessage.getKey();
mClientKey = finalClientMessage.getKey();
}
- private void setUpReconnection(RunnerFactory runnerFactory) throws Exception {
- setUpFirstConnection(runnerFactory);
+ private void setUpReconnection(RunnerFactory runnerFactory, HandshakeVerifier handshakeVerifier)
+ throws Exception {
+ setUpFirstConnection(runnerFactory, handshakeVerifier);
EncryptionRunner clientRunner = runnerFactory.newRunner();
EncryptionRunner serverRunner = runnerFactory.newRunner();
verifyHandshakeReconnect(clientRunner, serverRunner);
@@ -103,11 +147,12 @@
* Some * of the test is implementation specific because the interface doesn't specify how many
* round * trips may be needed but this test makes assumptions( i.e. white box testing).
*/
- private void verifyRunners(RunnerFactory runnerFactory) throws Exception {
+ private void verifyRunners(RunnerFactory runnerFactory, HandshakeVerifier handshakeVerifier)
+ throws Exception {
EncryptionRunner clientRunner = runnerFactory.newRunner();
EncryptionRunner serverRunner = runnerFactory.newRunner();
- verifyHandshake(clientRunner, serverRunner);
+ handshakeVerifier.verifyHandshake(clientRunner, serverRunner);
HandshakeMessage finalServerMessage = serverRunner.verifyPin();
assertThat(finalServerMessage.getHandshakeState())
@@ -156,7 +201,8 @@
assertThat(finalClientMessage.getNextMessage()).isNull();
}
- private void verifyHandshake(EncryptionRunner clientRunner, EncryptionRunner serverRunner)
+ private static void verifyHandshake(EncryptionRunner clientRunner,
+ EncryptionRunner serverRunner)
throws Exception {
HandshakeMessage initialClientMessage = clientRunner.initHandshake();
@@ -204,6 +250,40 @@
"last server message size:" + clientMessage.getNextMessage().length);
}
+ private static void verifyOobHandshake(
+ EncryptionRunner clientRunner, EncryptionRunner serverRunner) throws Exception {
+ HandshakeMessage initialClientMessage = clientRunner.initHandshake();
+
+ assertThat(initialClientMessage.getHandshakeState())
+ .isEqualTo(HandshakeMessage.HandshakeState.IN_PROGRESS);
+ assertThat(initialClientMessage.getKey()).isNull();
+ assertThat(initialClientMessage.getNextMessage()).isNotNull();
+
+ HandshakeMessage initialServerMessage =
+ serverRunner.respondToInitRequest(initialClientMessage.getNextMessage());
+
+ assertThat(initialServerMessage.getHandshakeState())
+ .isEqualTo(HandshakeMessage.HandshakeState.IN_PROGRESS);
+ assertThat(initialServerMessage.getKey()).isNull();
+ assertThat(initialServerMessage.getNextMessage()).isNotNull();
+
+ HandshakeMessage clientMessage =
+ clientRunner.continueHandshake(initialServerMessage.getNextMessage());
+
+ assertThat(clientMessage.getHandshakeState())
+ .isEqualTo(HandshakeMessage.HandshakeState.OOB_VERIFICATION_NEEDED);
+ assertThat(clientMessage.getKey()).isNull();
+ assertThat(clientMessage.getOobVerificationCode()).isNotEmpty();
+ assertThat(clientMessage.getNextMessage()).isNotNull();
+
+ HandshakeMessage serverMessage = serverRunner.continueHandshake(
+ clientMessage.getNextMessage());
+ assertThat(serverMessage.getHandshakeState())
+ .isEqualTo(HandshakeMessage.HandshakeState.OOB_VERIFICATION_NEEDED);
+ assertThat(serverMessage.getKey()).isNull();
+ assertThat(serverMessage.getNextMessage()).isNull();
+ }
+
private void verifyHandshakeReconnect(
EncryptionRunner clientRunner, EncryptionRunner serverRunner)
throws HandshakeException {
@@ -249,19 +329,27 @@
@Test
public void invalidPin_ukey2() throws Exception {
- invalidPinTest(EncryptionRunnerFactory::newRunner);
+ invalidPinTest(EncryptionRunnerTest::newRunner, EncryptionRunnerTest::verifyHandshake);
}
@Test
public void invalidPin_dummy() throws Exception {
- invalidPinTest(EncryptionRunnerFactory::newDummyRunner);
+ invalidPinTest(EncryptionRunnerFactory::newDummyRunner,
+ EncryptionRunnerTest::verifyHandshake);
}
- private void invalidPinTest(RunnerFactory runnerFactory) throws Exception {
+ @Test
+ public void invalidPin_oobUkey2() throws Exception {
+ invalidPinTest(EncryptionRunnerTest::newOobRunner,
+ EncryptionRunnerTest::verifyOobHandshake);
+ }
+
+ private void invalidPinTest(RunnerFactory runnerFactory, HandshakeVerifier handshakeVerifier)
+ throws Exception {
EncryptionRunner clientRunner = runnerFactory.newRunner();
EncryptionRunner serverRunner = runnerFactory.newRunner();
- verifyHandshake(clientRunner, serverRunner);
+ handshakeVerifier.verifyHandshake(clientRunner, serverRunner);
clientRunner.invalidPin();
serverRunner.invalidPin();