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();