Mark ab/7061308 as merged in stage.

Bug: 180401296
Merged-In: I2c2a81b364f8b636fd02db7923951c4da73731fd
Change-Id: I902548e770b4e3ed9bdc4ef12460f30bdcb346d9
diff --git a/.gitignore b/.gitignore
index a676aea..f53951a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,3 @@
 build/**
 .gradle/**
 .idea/**
-.gitignore
-.gitmodules
-
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..2624f19
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,16 @@
+[submodule "third_party/secure_message"]
+	path = third_party/secure_message
+	url = https://github.com/google/securemessage
+	branch = master
+[submodule "third_party/gtest"]
+	path = third_party/gtest
+	url = https://github.com/google/googletest
+	branch = master
+[submodule "third_party/protobuf"]
+	path = third_party/protobuf
+	url = https://github.com/protocolbuffers/protobuf
+	branch = master
+[submodule "third_party/absl"]
+	path = third_party/absl
+	url = https://github.com/abseil/abseil-cpp
+	branch = master
diff --git a/Android.bp b/Android.bp
index b99ed63..1376405 100644
--- a/Android.bp
+++ b/Android.bp
@@ -15,6 +15,23 @@
 // Android build file for building the library in AOSP
 // https://android.googlesource.com/platform/external/ukey2
 
+package {
+    default_applicable_licenses: ["external_ukey2_license"],
+}
+
+// Added automatically by a large-scale-change
+// http://go/android-license-faq
+license {
+    name: "external_ukey2_license",
+    visibility: [":__subpackages__"],
+    license_kinds: [
+        "SPDX-license-identifier-Apache-2.0",
+    ],
+    license_text: [
+        "LICENSE",
+    ],
+}
+
 java_library {
   name: "ukey2",
   proto: {
@@ -23,7 +40,7 @@
   },
   srcs: [
     "**/*.proto",
-    "**/*.java",
+    "src/main/java/**/*.java",
   ],
   libs: [
     "guava",
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..02ddda2
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,50 @@
+# Copyright 2020 Google LLC
+#
+# 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
+#
+#     https://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.
+
+cmake_minimum_required(VERSION 3.0.2)
+
+project(ukey2)
+
+option(ukey2_USE_LOCAL_PROTOBUF
+       "Use local copy of protobuf library and compiler" OFF)
+
+option(ukey2_USE_LOCAL_ABSL
+       "Use local copy of abseil-cpp library" OFF)
+
+include(cmake/proto_defs.cmake)
+include(cmake/local_build_setup.cmake)
+
+if (ukey2_USE_LOCAL_PROTOBUF)
+  include(cmake/local_build_protobuf.cmake)
+endif()
+
+if (ukey2_USE_LOCAL_ABSL)
+  if (NOT TARGET absl::base)
+    add_subdirectory(third_party/absl)
+  endif()
+else()
+  find_package(absl REQUIRED)
+endif()
+
+find_package(Protobuf REQUIRED)
+
+enable_testing()
+
+add_subdirectory(src/main)
+if (NOT TARGET securemessage)
+add_subdirectory(third_party/secure_message)
+endif()
+if (NOT TARGET gtest)
+add_subdirectory(third_party/gtest)
+endif()
diff --git a/METADATA b/METADATA
index 537e5cd..d08b2ea 100644
--- a/METADATA
+++ b/METADATA
@@ -5,9 +5,9 @@
 third_party {
   url {
     type: ARCHIVE
-    value: "https://user.git.corp.google.com/michalp/ukey2/"
+    value: "https://github.com/google/ukey2.git"
   }
-  version: "1.0"
+  version: "0275885d8e6038c39b8a8ca55e75d1d4d1727f47"
   license_type: NOTICE
-  last_upgrade_date { year: 2018 month: 12 day: 28 }
+  last_upgrade_date { year: 2021 month: 2 day: 8 }
 }
diff --git a/build.gradle b/build.gradle
index 8989eb0..e319422 100644
--- a/build.gradle
+++ b/build.gradle
@@ -29,7 +29,31 @@
 }
 
 dependencies {
-    implementation  "com.google.code.findbugs:jsr305:3.0.0"
-    implementation "com.google.protobuf:protobuf-java:3.8.0"
-    implementation "com.google.guava:guava:19.0"
+    compile group: 'com.google.truth.extensions', name: 'truth-java8-extension', version: '0.41'
+    testCompile group: 'com.google.guava', name: 'guava-testlib', version: '29.0-jre'
+    testImplementation 'junit:junit:4.13'
+    compile  "com.google.code.findbugs:jsr305:3.0.0"
+    compile "com.google.protobuf:protobuf-java:3.8.0"
+    compile "com.google.guava:guava:19.0"
+}
+
+sourceSets {
+    main {
+        java {
+            srcDir 'src/main/java'
+            srcDir 'build/generated/source/proto/main/java'
+        }
+    }
+    test {
+        java {
+            srcDir 'src/main/javatest'
+            srcDir 'build/generated/source/proto/main/java'
+        }
+    }
+}
+
+test {
+    useJUnit()
+
+    maxHeapSize = '1G'
 }
diff --git a/cmake/local_build_protobuf.cmake b/cmake/local_build_protobuf.cmake
new file mode 100644
index 0000000..3a04d55
--- /dev/null
+++ b/cmake/local_build_protobuf.cmake
@@ -0,0 +1,42 @@
+# Copyright 2020 Google LLC
+#
+# 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
+#
+#     https://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.
+
+if (NOT EXISTS ${TOOLS_INSTALL_PREFIX}/bin/protoc)
+  set(PKG_BUILD_ROOT ${TOOLS_BUILD_ROOT}/protobuf)
+  set(PKG_SRC_ROOT ${CMAKE_SOURCE_DIR}/third_party/protobuf)
+  execute_process(
+    COMMAND mkdir -p ${PKG_BUILD_ROOT}
+  )
+  execute_process(
+    COMMAND cmake ${PKG_SRC_ROOT}/cmake
+    WORKING_DIRECTORY ${PKG_BUILD_ROOT}
+  )
+  execute_process(
+    COMMAND make -j${N_CPUS}
+    WORKING_DIRECTORY ${PKG_BUILD_ROOT}
+  )
+  execute_process(
+    COMMAND make check
+    WORKING_DIRECTORY ${PKG_BUILD_ROOT}
+    RESULT_VARIABLE test_exit_code
+    ERROR_QUIET
+  )
+  if (NOT ${test_exit_code} EQUAL "0")
+    message(FATAL_ERROR "Protobuf tests failed; can't use this protobuf")
+  endif()
+  execute_process(
+    COMMAND /bin/bash -c "DESTDIR=${TOOLS_INSTALL_ROOT} make install"
+    WORKING_DIRECTORY ${PKG_BUILD_ROOT}
+  )
+endif()
diff --git a/cmake/local_build_setup.cmake b/cmake/local_build_setup.cmake
new file mode 100644
index 0000000..a7917fe
--- /dev/null
+++ b/cmake/local_build_setup.cmake
@@ -0,0 +1,26 @@
+# Copyright 2020 Google LLC
+#
+# 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
+#
+#     https://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.
+
+include(ProcessorCount)
+ProcessorCount(N_CPUS)
+
+if (N_CPUS EQUAL 0)
+  set (N_CPUS 1)
+endif()
+
+set (TOOLS_ROOT ${CMAKE_BINARY_DIR}/stage)
+set (TOOLS_BUILD_ROOT ${TOOLS_ROOT}/build)
+set (TOOLS_INSTALL_ROOT ${TOOLS_ROOT}/install)
+set (TOOLS_INSTALL_PREFIX ${TOOLS_INSTALL_ROOT}/usr/local)
+set (CMAKE_FIND_ROOT_PATH ${TOOLS_INSTALL_ROOT})
diff --git a/cmake/proto_defs.cmake b/cmake/proto_defs.cmake
new file mode 100644
index 0000000..aae0ce9
--- /dev/null
+++ b/cmake/proto_defs.cmake
@@ -0,0 +1,42 @@
+# Copyright 2020 Google LLC
+#
+# 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
+#
+#     https://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.
+
+function(add_cc_proto_library NAME)
+  set(single)
+  set(multi_args PROTOS INCS DEPS)
+  cmake_parse_arguments(PARSE_ARGV 1 args "" "${single}" "${multi_args}")
+
+  protobuf_generate(
+    PROTOS ${args_PROTOS}
+    LANGUAGE cpp
+    OUT_VAR ${NAME}_var
+  )
+
+  add_library(${NAME}
+    ${${NAME}_var}
+  )
+
+  target_link_libraries(${NAME}
+    PUBLIC
+      ${Protobuf_LIBRARIES}
+      ${args_DEPS}
+  )
+
+  target_include_directories(${NAME}
+    PUBLIC
+      ${Protobuf_INCLUDE_DIRS}
+      ${args_INCS}
+      ${CMAKE_CURRENT_BINARY_DIR}
+  )
+endfunction()
diff --git a/src/main/CMakeLists.txt b/src/main/CMakeLists.txt
new file mode 100644
index 0000000..776826c
--- /dev/null
+++ b/src/main/CMakeLists.txt
@@ -0,0 +1,18 @@
+# Copyright 2020 Google LLC
+#
+# 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
+#
+#     https://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.
+
+set(UKEY_SRC_ROOT ${CMAKE_CURRENT_LIST_DIR})
+set(UKEY_BINARY_ROOT ${CMAKE_CURRENT_BINARY_DIR})
+add_subdirectory(cpp)
+add_subdirectory(proto)
diff --git a/src/main/cpp/CMakeLists.txt b/src/main/cpp/CMakeLists.txt
new file mode 100644
index 0000000..919e096
--- /dev/null
+++ b/src/main/cpp/CMakeLists.txt
@@ -0,0 +1,17 @@
+# Copyright 2020 Google LLC
+#
+# 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
+#
+#     https://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.
+
+include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include)
+add_subdirectory(src/securegcm)
+add_subdirectory(test/securegcm)
diff --git a/src/main/cpp/include/securegcm/d2d_connection_context_v1.h b/src/main/cpp/include/securegcm/d2d_connection_context_v1.h
new file mode 100644
index 0000000..098e654
--- /dev/null
+++ b/src/main/cpp/include/securegcm/d2d_connection_context_v1.h
@@ -0,0 +1,89 @@
+// Copyright 2020 Google LLC
+//
+// 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
+//
+//     https://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.
+
+#ifndef SECURITY_CRYPTAUTH_LIB_SECUREGCM_D2D_CONNECTION_CONTEXT_V1_H_
+#define SECURITY_CRYPTAUTH_LIB_SECUREGCM_D2D_CONNECTION_CONTEXT_V1_H_
+
+#include <memory>
+#include <string>
+
+#include "securemessage/crypto_ops.h"
+
+namespace securegcm {
+
+// The full context of a secure connection. This class has methods to encode and
+// decode messages that are to be sent to another device.
+//
+// This class should be kept compatible with the Java implementation in
+// java/com/google/security/cryptauth/lib/securegcm/D2DConnectionContextV1.java
+class D2DConnectionContextV1 {
+ public:
+  D2DConnectionContextV1(const securemessage::CryptoOps::SecretKey& encode_key,
+                         const securemessage::CryptoOps::SecretKey& decode_key,
+                         uint32_t encode_sequence_number,
+                         uint32_t decode_sequence_number);
+
+  // Once the initiator and responder have negotiated a secret key, use this
+  // method to encrypt and sign |payload|. Both initiator and responder devices
+  // can use this message.
+  //
+  // On failure, nullptr is returned.
+  std::unique_ptr<string> EncodeMessageToPeer(const string& payload);
+
+  // Once the initiator and responder have negotiated a secret key, use this
+  // method to decrypt and verify a |message| received from the other device.
+  // Both initiator and responder devices can use this message.
+  //
+  // On failure, nullptr is returned.
+  std::unique_ptr<string> DecodeMessageFromPeer(const string& message);
+
+  // Returns a cryptographic digest (SHA256) of the session keys prepended by
+  // the SHA256 hash of the ASCII string "D2D".
+  //
+  // On failure, nullptr is returned.
+  std::unique_ptr<string> GetSessionUnique();
+
+  // Creates a saved session that can be later used for resumption. Note,
+  // this must be stored in a secure location.
+  std::unique_ptr<string> SaveSession();
+
+  // Parse a saved session info and attempt to construct a resumed context.
+  //
+  // The session info passed to this method should be one that was generated
+  // by |SaveSession|.
+  //
+  // On failure, nullptr is returned.
+  static std::unique_ptr<D2DConnectionContextV1> FromSavedSession(
+      const string& savedSessionInfo);
+
+ private:
+  // The key used to encode payloads.
+  const securemessage::CryptoOps::SecretKey encode_key_;
+
+  // The key used to decode received messages.
+  const securemessage::CryptoOps::SecretKey decode_key_;
+
+  // The current sequence number for encoding.
+  uint32_t encode_sequence_number_;
+
+  // The current sequence number for decoding.
+  uint32_t decode_sequence_number_;
+
+  // A friend to access private variables for testing.
+  friend class D2DConnectionContextV1Peer;
+};
+
+}  // namespace securegcm
+
+#endif  // SECURITY_CRYPTAUTH_LIB_SECUREGCM_D2D_CONNECTION_CONTEXT_V1_H_
diff --git a/src/main/cpp/include/securegcm/d2d_crypto_ops.h b/src/main/cpp/include/securegcm/d2d_crypto_ops.h
new file mode 100644
index 0000000..eeeeb20
--- /dev/null
+++ b/src/main/cpp/include/securegcm/d2d_crypto_ops.h
@@ -0,0 +1,78 @@
+// Copyright 2020 Google LLC
+//
+// 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
+//
+//     https://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.
+
+#ifndef SECURITY_CRYPTAUTH_LIB_SECUREGCM_D2D_CRYPTO_OPS_H_
+#define SECURITY_CRYPTAUTH_LIB_SECUREGCM_D2D_CRYPTO_OPS_H_
+
+#include <memory>
+#include <string>
+
+#include "proto/securegcm.pb.h"
+#include "securemessage/crypto_ops.h"
+
+namespace securegcm {
+
+// A collection of static utility methods for the Device to Device communication
+// (D2D) library.
+//
+// A class is used here in preference to a namespace to provide a closer
+// correspondence with the Java equivalent class:
+// //java/com/google/security/cryptauth/lib/securegcm/D2DCryptoOps.java
+class D2DCryptoOps {
+ public:
+  // Encapsulates a payload type specifier, and a corresponding message as the
+  // raw payload.
+  //
+  // Note: Type is defined in securegcm.proto.
+  class Payload {
+   public:
+    Payload(Type type, const std::string& message);
+
+    Type type() const { return type_; }
+
+    const std::string& message() const { return message_; }
+
+   private:
+    const Type type_;
+    const std::string message_;
+  };
+
+  // The salt, SHA256 of "D2D".
+  static const uint8_t kSalt[];
+  static const size_t kSaltLength;
+
+  // Used by a device to send a secure |Payload| to another device.
+  static std::unique_ptr<std::string> SigncryptPayload(
+      const Payload& payload,
+      const securemessage::CryptoOps::SecretKey& secret_key);
+
+  // Used by a device to recover a secure |Payload| sent by another device.
+  static std::unique_ptr<Payload> VerifyDecryptPayload(
+      const std::string& signcrypted_message,
+      const securemessage::CryptoOps::SecretKey& secret_key);
+
+  // Used to derive a distinct key for each initiator and responder from the
+  // |master_key|. Use a different |purpose| for each role.
+  static std::unique_ptr<securemessage::CryptoOps::SecretKey>
+  DeriveNewKeyForPurpose(const securemessage::CryptoOps::SecretKey& master_key,
+                         const std::string& purpose);
+
+ private:
+  // Prevent instantiation.
+  D2DCryptoOps();
+};
+
+}  // namespace securegcm
+
+#endif  // SECURITY_CRYPTAUTH_LIB_SECUREGCM_D2D_CRYPTO_OPS_H_
diff --git a/src/main/cpp/include/securegcm/java_util.h b/src/main/cpp/include/securegcm/java_util.h
new file mode 100644
index 0000000..8783af4
--- /dev/null
+++ b/src/main/cpp/include/securegcm/java_util.h
@@ -0,0 +1,57 @@
+// Copyright 2020 Google LLC
+//
+// 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
+//
+//     https://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.
+
+// Utility functions for Java-compatible operations.
+#ifndef SECURITY_CRYPTAUTH_LIB_SECUREGCM_JAVA_UTIL_H_
+#define SECURITY_CRYPTAUTH_LIB_SECUREGCM_JAVA_UTIL_H_
+
+#include "securemessage/byte_buffer.h"
+
+namespace securegcm {
+namespace java_util {
+
+// Perform multiplication with Java overflow semantics
+// (https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html):
+//   If an integer multiplication overflows, then the result is the low-order
+//   bits of the mathematical product as represented in some sufficiently
+//   large two's-complement format.
+int32_t JavaMultiply(int32_t lhs, int32_t rhs);
+
+// Perform addition with Java overflow semantics:
+// (https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html):
+//   If an integer addition overflows, then the result is the low-order bits of
+//   the mathematical sum as represented in some sufficiently large
+//   two's-complement format.
+int32_t JavaAdd(int32_t lhs, int32_t rhs);
+
+// To be compatible with the Java implementation, we need to use the same
+// algorithm as the Arrays#hashCode(byte[]) function in Java:
+//  "The value returned by this method is the same value that would be obtained
+//  by invoking the hashCode method on a List containing a sequence of Byte
+//  instances representing the elements of a in the same order."
+//
+// According to List#hashCode(), this algorithm is:
+//   int hashCode = 1;
+//   for (Byte b : list) {
+//     hashCode = 31 * hashCode + (b == null ? b : b.hashCode());
+//   }
+//
+// Finally, Byte#hashCode() is defined as "equal to the result of invoking
+// Byte#intValue()".
+int32_t JavaHashCode(const securemessage::ByteBuffer& byte_buffer);
+
+}  // namespace java_util
+}  // namespace securegcm
+
+#endif  // SECURITY_CRYPTAUTH_LIB_SECUREGCM_JAVA_UTIL_H_
diff --git a/src/main/cpp/include/securegcm/ukey2_handshake.h b/src/main/cpp/include/securegcm/ukey2_handshake.h
new file mode 100644
index 0000000..8455fd7
--- /dev/null
+++ b/src/main/cpp/include/securegcm/ukey2_handshake.h
@@ -0,0 +1,263 @@
+// Copyright 2020 Google LLC
+//
+// 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
+//
+//     https://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.
+
+#ifndef SECURITY_CRYPTAUTH_LIB_SECUREGCM_UKEY2_HANDSHAKE_H_
+#define SECURITY_CRYPTAUTH_LIB_SECUREGCM_UKEY2_HANDSHAKE_H_
+
+#include <map>
+#include <memory>
+
+#include "proto/ukey.pb.h"
+#include "securegcm/d2d_connection_context_v1.h"
+#include "securemessage/crypto_ops.h"
+
+namespace securegcm {
+
+// Implements UKEY2 and produces a |D2DConnectionContextV1|.
+// This class should be kept compatible with the Java implementation in
+//     //java/com/google/security/cryptauth/lib/securegcm/Ukey2Handshake.java
+//
+// For usage examples, see ukey2_shell.cc. This file contains a shell exercising
+// both the initiator and responder handshake roles.
+class UKey2Handshake {
+ public:
+  // Handshake states:
+  //   kInProgress:
+  //       The handshake is in progress, caller should use
+  //       |GetNextHandshakeMessage()| and |ParseHandshakeMessage()| to continue
+  //       the handshake.
+  //
+  //   kVerificationNeeded:
+  //       The handshake is complete, but pending verification of the
+  //       authentication string. Clients should use |GetVerificationString()|
+  //       to get the verification string and use out-of-band methods to
+  //       authenticate the handshake.
+  //
+  //   kVerificationInProgress:
+  //       The handshake is complete, verification string has been generated,
+  //       but has not been confirmed. After authenticating the handshake
+  //       out-of-band, use |VerifyHandshake()| to mark the handshake as
+  //       verified.
+  //
+  //   kFinished:
+  //       The handshake is finished, and the caller can use
+  //       |ToConnectionContext()| to produce a |D2DConnectionContextV1|.
+  //
+  //   kAlreadyUsed:
+  //       The hanshake has already been used and should be destroyed.
+  //
+  //   kError:
+  //       The handshake produced an error and should be destroyed.
+  enum class State {
+    kInProgress,
+    kVerificationNeeded,
+    kVerificationInProgress,
+    kFinished,
+    kAlreadyUsed,
+    kError,
+  };
+
+  // Currently implemented UKEY2 handshake ciphers. Each cipher is a tuple
+  // consisting of a key negotiation cipher and a hash function used for a
+  // commitment. Currently the ciphers are:
+  //   +-----------------------------------------------------+
+  //   | Enum        | Key negotiation       | Hash function |
+  //   +-------------+-----------------------+---------------+
+  //   | P256_SHA512 | ECDH using NIST P-256 | SHA512        |
+  //   +-----------------------------------------------------+
+  //
+  // Note that these should correspond to values in
+  // device_to_device_messages.proto.
+  enum class HandshakeCipher : int {
+    // TODO(aczeskis): add CURVE25519_SHA512
+
+    P256_SHA512 = securegcm::P256_SHA512,
+  };
+
+  // Creates a |UKey2Handshake| with a particular |cipher| that can be used by
+  // an initiator / client.
+  static std::unique_ptr<UKey2Handshake> ForInitiator(HandshakeCipher cipher);
+
+  // Creates a |UKey2Handshake| with a particular |cipher| that can be used by
+  // a responder / server.
+  static std::unique_ptr<UKey2Handshake> ForResponder(HandshakeCipher cipher);
+
+  // Returns the current state of the handshake.
+  State GetHandshakeState() const;
+
+  // Returns the last error message. Empty string if there was no error.
+  const string& GetLastError() const;
+
+  // Gets the next handshake message suitable for sending on the wire.
+  // If |nullptr| is returned, check |GetLastError()| for the error message.
+  std::unique_ptr<string> GetNextHandshakeMessage();
+
+  // Parses the given |handshake_message|, updating the internal state.
+  struct ParseResult {
+    // True if |handshake_message| is parsed successfully. If |false|, call
+    // |GetLastError()| for the error message.
+    bool success;
+
+    // May be set if parsing fails. This value should be sent to the remote
+    // device before disconnecting.
+    std::unique_ptr<string> alert_to_send;
+  };
+  ParseResult ParseHandshakeMessage(const string& handshake_message);
+
+  // Returns an authentication string suitable for authenticating the handshake
+  // out-of-band. Note that the authentication string can be short (e.g., a 6
+  // digit visual confirmation code).
+  //
+  // Note: This should only be called when the state returned from
+  // |GetHandshakeState()| is |State::VERIFICATION_NEEDED|, which means this can
+  // only be called once.
+  //
+  // |byte_length|: The length of the output. Min length is 1; max length is 32.
+  // If |nullptr| is returned, check |GetLastError()| for the error message.
+  std::unique_ptr<string> GetVerificationString(int byte_length);
+
+  // Invoked to let the handshake state machine know that caller has validated
+  // the authentication string obtained via |GetVerificationString()|.
+  // Note: This should only be called when the state returned by
+  // |GetHandshakeState()| is |State::VERIFICATION_IN_PROGRESS|.
+  //
+  // If |false| is returned, check |GetLastError()| for the error message.
+  bool VerifyHandshake();
+
+  // Can be called to generate a |D2DConnectionContextV1|. Returns nullptr on
+  // failure.
+  // Note: This should only be called when the state returned by
+  // |GetHandshakeState()| is |State::FINISHED|.
+  //
+  // If |nullptr| is returned, check |GetLastError()| for the error message.
+  std::unique_ptr<D2DConnectionContextV1> ToConnectionContext();
+
+ private:
+  // Enums for internal state machinery.
+  enum class InternalState : int {
+    CLIENT_START,
+    CLIENT_WAITING_FOR_SERVER_INIT,
+    CLIENT_AFTER_SERVER_INIT,
+
+    // Responder/server state
+    SERVER_START,
+    SERVER_AFTER_CLIENT_INIT,
+    SERVER_WAITING_FOR_CLIENT_FINISHED,
+
+    // Common completion state
+    HANDSHAKE_VERIFICATION_NEEDED,
+    HANDSHAKE_VERIFICATION_IN_PROGRESS,
+    HANDSHAKE_FINISHED,
+    HANDSHAKE_ALREADY_USED,
+    HANDSHAKE_ERROR,
+  };
+
+  // Helps us remember our role in the handshake.
+  enum class HandshakeRole {
+    CLIENT,
+    SERVER
+  };
+
+  // Prevent public instantiation. Callers should use |ForInitiator()| or
+  // |ForResponder()|.
+  UKey2Handshake(InternalState state, HandshakeCipher cipher);
+
+  // Attempts to parse Ukey2ClientInit, wrapped inside a Ukey2Message.
+  // See go/ukey2 for details.
+  ParseResult ParseClientInitUkey2Message(const string& handshake_message);
+
+  // Attempts to parse Ukey2ServerInit, wrapped inside a Ukey2Message.
+  // See go/ukey2 for details.
+  ParseResult ParseServerInitUkey2Message(const string& handshake_message);
+
+  // Attempts to parse Ukey2ClientFinish, wrapped inside a Ukey2Message.
+  // See go/ukey2 for details.
+  ParseResult ParseClientFinishUkey2Message(const string& handshake_message);
+
+  // Convenience function to set |last_error_| and create a ParseResult with a
+  // given alert.
+  ParseResult CreateFailedResultWithAlert(Ukey2Alert::AlertType alert_type,
+                                          const string& error_message);
+
+  // Convenience function to set |last_error_| and create a failed ParseResult
+  // without an alert.
+  ParseResult CreateFailedResultWithoutAlert(const string& error_message);
+
+  // Convenience function to create a successful ParseResult.
+  ParseResult CreateSuccessResult();
+
+  // Verifies that the peer's commitment stored in |peer_commitment_| is the
+  // same as that obtained from |handshake_message|.
+  bool VerifyCommitment(const string& handshake_message);
+
+  // Generates a commitment for the P256_SHA512 cipher.
+  std::unique_ptr<Ukey2ClientInit::CipherCommitment>
+  GenerateP256Sha512Commitment();
+
+  // Creates a serialized Ukey2Message, wrapping an inner ClientInit message.
+  std::unique_ptr<string> MakeClientInitUkey2Message();
+
+  // Creates a serialized Ukey2Message, wrapping an inner ServerInit message.
+  std::unique_ptr<string> MakeServerInitUkey2Message();
+
+  // Creates a serialized Ukey2Message of a given |type|, wrapping |data|.
+  std::unique_ptr<string> MakeUkey2Message(Ukey2Message::Type type,
+                                           const string& data);
+
+  // Called when an error occurs to set |handshake_state_| and |last_error_|.
+  void SetError(const string& error_message);
+
+  // The current state of the handshake.
+  InternalState handshake_state_;
+
+  // The cipher to use for the handshake.
+  const HandshakeCipher handshake_cipher_;
+
+  // The role to perform, i.e. client or server.
+  const HandshakeRole handshake_role_;
+
+  // A newly generated key-pair for this handshake.
+  std::unique_ptr<securemessage::CryptoOps::KeyPair> our_key_pair_;
+
+  // The peer's public key retrieved from a handshake message.
+  std::unique_ptr<securemessage::CryptoOps::PublicKey> their_public_key_;
+
+  // The secret key derived from |our_key_pair_| and |their_public_key_|.
+  std::unique_ptr<securemessage::CryptoOps::SecretKey> derived_secret_key_;
+
+  // The raw bytes of the Ukey2ClientInit, wrapped inside a Ukey2Message.
+  // Empty string if not initialized.
+  string wrapped_client_init_;
+
+  // The raw bytes of the Ukey2ServerInit, wrapped inside a Ukey2Message.
+  // Empty string if not initialized.
+  string wrapped_server_init_;
+
+  // The commitment of the peer retrieved from a handshake message. Empty string
+  // if not initialized.
+  string peer_commitment_;
+
+  // Map from ciphers to the raw bytes of message 3 (which is a wrapped
+  // Ukey2ClientFinished message).
+  // Note: Currently only one cipher is supported, so at most one entry exists
+  // in this map.
+  std::map<HandshakeCipher, string> raw_message3_map_;
+
+  // Contains the last error message.
+  string last_error_;
+};
+
+}  // namespace securegcm
+
+#endif  // SECURITY_CRYPTAUTH_LIB_SECUREGCM_UKEY2_HANDSHAKE_H_
diff --git a/src/main/cpp/src/securegcm/CMakeLists.txt b/src/main/cpp/src/securegcm/CMakeLists.txt
new file mode 100644
index 0000000..b562756
--- /dev/null
+++ b/src/main/cpp/src/securegcm/CMakeLists.txt
@@ -0,0 +1,47 @@
+# Copyright 2020 Google LLC
+#
+# 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
+#
+#     https://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.
+
+add_library(ukey2 STATIC
+  d2d_connection_context_v1.cc
+  d2d_crypto_ops.cc
+  java_util.cc
+  ukey2_handshake.cc
+)
+
+target_include_directories(ukey2
+  PUBLIC
+    ${PROJECT_SOURCE_DIR}/src/main/cpp/include
+)
+
+target_link_libraries(ukey2
+  PUBLIC
+    proto_device_to_device_messages_cc_proto
+    proto_securegcm_cc_proto
+    proto_ukey_cc_proto
+    securemessage
+)
+
+add_executable(ukey2_shell
+  ukey2_shell.cc
+)
+
+target_link_libraries(ukey2_shell
+  PUBLIC
+    securemessage
+    ukey2
+    absl::base
+    absl::container
+    absl::flags
+    absl::flags_parse
+)
diff --git a/src/main/cpp/src/securegcm/d2d_connection_context_v1.cc b/src/main/cpp/src/securegcm/d2d_connection_context_v1.cc
new file mode 100644
index 0000000..8a9a612
--- /dev/null
+++ b/src/main/cpp/src/securegcm/d2d_connection_context_v1.cc
@@ -0,0 +1,228 @@
+// Copyright 2020 Google LLC
+//
+// 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
+//
+//     https://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.
+
+#include "securegcm/d2d_connection_context_v1.h"
+
+#include <limits>
+#include <sstream>
+
+#include "proto/device_to_device_messages.pb.h"
+#include "proto/securegcm.pb.h"
+#include "securegcm/d2d_crypto_ops.h"
+#include "securegcm/java_util.h"
+#include "securemessage/secure_message_builder.h"
+#include "securemessage/util.h"
+
+namespace securegcm {
+
+using securemessage::CryptoOps;
+using securemessage::ByteBuffer;
+using securemessage::Util;
+
+namespace {
+
+// Fields to fill in the GcmMetadata proto.
+const Type kGcmMetadataType = DEVICE_TO_DEVICE_MESSAGE;
+
+// Represents the version of this context.
+const uint8_t kProtocolVersion = 1;
+
+// The following represent the starting positions of the each entry within
+// the string representation of this D2DConnectionContextV1.
+//
+// The saved session has a 1 byte protocol version, two 4 byte sequence numbers,
+// and two 32 byte AES keys: (1 + 4 + 4 + 32 + 32 = 73).
+
+// The two sequence numbers are 4 bytes each.
+const int kSequenceNumberLength = 4;
+
+// 32 byte AES keys.
+const int kAesKeyLength = 32;
+
+// The encode sequence number starts at 1 to account for the 1 byte version
+// number.
+const int kEncodeSequenceStart = 1;
+const int kEncodeSequenceEnd = kEncodeSequenceStart + kSequenceNumberLength;
+
+const int kDecodeSequenceStart = kEncodeSequenceEnd;
+const int kDecodeSequenceEnd = kDecodeSequenceStart + kSequenceNumberLength;
+
+const int kEncodeKeyStart = kDecodeSequenceEnd;
+const int kEncodeKeyEnd = kEncodeKeyStart + kAesKeyLength;
+
+const int kDecodeKeyStart = kEncodeKeyEnd;
+const int kSavedSessionLength = kDecodeKeyStart + kAesKeyLength;
+
+// Convenience function to creates a DeviceToDeviceMessage proto with |payload|
+// and |sequence_number|.
+DeviceToDeviceMessage CreateDeviceToDeviceMessage(const std::string& payload,
+                                                  uint32_t sequence_number) {
+  DeviceToDeviceMessage device_to_device_message;
+  device_to_device_message.set_sequence_number(sequence_number);
+  device_to_device_message.set_message(payload);
+  return device_to_device_message;
+}
+
+// Convert 4 bytes in big-endian representation into an unsigned int.
+uint32_t BytesToUnsignedInt(std::vector<uint8_t> bytes) {
+  return bytes[0] << 24 | bytes[1] << 12 | bytes[2] << 8 | bytes[3];
+}
+
+// Convert an unsigned int into a 4 byte big-endian representation.
+std::vector<uint8_t> UnsignedIntToBytes(uint32_t val) {
+  return {static_cast<uint8_t>(val >> 24), static_cast<uint8_t>(val >> 12),
+          static_cast<uint8_t>(val >> 8), static_cast<uint8_t>(val)};
+}
+
+}  // namespace
+
+D2DConnectionContextV1::D2DConnectionContextV1(
+    const CryptoOps::SecretKey& encode_key,
+    const CryptoOps::SecretKey& decode_key, uint32_t encode_sequence_number,
+    uint32_t decode_sequence_number)
+    : encode_key_(encode_key),
+      decode_key_(decode_key),
+      encode_sequence_number_(encode_sequence_number),
+      decode_sequence_number_(decode_sequence_number) {}
+
+std::unique_ptr<std::string> D2DConnectionContextV1::EncodeMessageToPeer(
+    const std::string& payload) {
+  encode_sequence_number_++;
+  const DeviceToDeviceMessage message =
+      CreateDeviceToDeviceMessage(payload, encode_sequence_number_);
+
+  const D2DCryptoOps::Payload payload_with_type(kGcmMetadataType,
+                                                message.SerializeAsString());
+  return D2DCryptoOps::SigncryptPayload(payload_with_type, encode_key_);
+}
+
+std::unique_ptr<std::string> D2DConnectionContextV1::DecodeMessageFromPeer(
+    const std::string& message) {
+  std::unique_ptr<D2DCryptoOps::Payload> payload =
+      D2DCryptoOps::VerifyDecryptPayload(message, decode_key_);
+  if (!payload) {
+    Util::LogError("DecodeMessageFromPeer: Failed to verify message.");
+    return nullptr;
+  }
+
+  if (kGcmMetadataType != payload->type()) {
+    Util::LogError("DecodeMessageFromPeer: Wrong message type in D2D message.");
+    return nullptr;
+  }
+
+  DeviceToDeviceMessage d2d_message;
+  if (!d2d_message.ParseFromString(payload->message())) {
+    Util::LogError("DecodeMessageFromPeer: Unable to parse D2D message proto.");
+    return nullptr;
+  }
+
+  decode_sequence_number_++;
+  if (d2d_message.sequence_number() != decode_sequence_number_) {
+    std::ostringstream stream;
+    stream << "DecodeMessageFromPeer: Seqno in D2D message ("
+           << d2d_message.sequence_number()
+           << ") does not match expected seqno (" << decode_sequence_number_
+           << ").";
+    Util::LogError(stream.str());
+    return nullptr;
+  }
+
+  return std::unique_ptr<std::string>(d2d_message.release_message());
+}
+
+std::unique_ptr<std::string> D2DConnectionContextV1::GetSessionUnique() {
+  const ByteBuffer encode_key_data = encode_key_.data();
+  const ByteBuffer decode_key_data = decode_key_.data();
+  const int32_t encode_key_hash = java_util::JavaHashCode(encode_key_data);
+  const int32_t decode_key_hash = java_util::JavaHashCode(decode_key_data);
+
+  const ByteBuffer& first_buffer =
+      encode_key_hash < decode_key_hash ? encode_key_data : decode_key_data;
+  const ByteBuffer& second_buffer =
+      encode_key_hash < decode_key_hash ? decode_key_data : encode_key_data;
+
+  ByteBuffer data_to_hash(D2DCryptoOps::kSalt, D2DCryptoOps::kSaltLength);
+  data_to_hash = ByteBuffer::Concat(data_to_hash, first_buffer);
+  data_to_hash = ByteBuffer::Concat(data_to_hash, second_buffer);
+
+  std::unique_ptr<ByteBuffer> hash = CryptoOps::Sha256(data_to_hash);
+  if (!hash) {
+    Util::LogError("GetSessionUnique: SHA-256 hash failed.");
+    return nullptr;
+  }
+
+  return std::unique_ptr<std::string>(new std::string(hash->String()));
+}
+
+// Structure of saved session is:
+//
+// +---------------------------------------------------------------------------+
+// | 1 Byte  |      4 Bytes      |      4 Bytes      |  32 Bytes  |  32 Bytes  |
+// +---------------------------------------------------------------------------+
+// | Version | encode seq number | decode seq number | encode key | decode key |
+// +---------------------------------------------------------------------------+
+//
+// The sequence numbers are represented in big-endian.
+std::unique_ptr<std::string> D2DConnectionContextV1::SaveSession() {
+  ByteBuffer byteBuffer = ByteBuffer(&kProtocolVersion, static_cast<size_t>(1));
+
+  // Append encode sequence number.
+  std::vector<uint8_t> encode_sequence_number_bytes =
+      UnsignedIntToBytes(encode_sequence_number_);
+  for (int i = 0; i < encode_sequence_number_bytes.size(); i++) {
+    byteBuffer.Append(static_cast<size_t>(1), encode_sequence_number_bytes[i]);
+  }
+
+  // Append decode sequence number.
+  std::vector<uint8_t> decode_sequence_number_bytes =
+      UnsignedIntToBytes(decode_sequence_number_);
+  for (int i = 0; i < decode_sequence_number_bytes.size(); i++) {
+    byteBuffer.Append(static_cast<size_t>(1), decode_sequence_number_bytes[i]);
+  }
+
+  // Append encode key.
+  byteBuffer = ByteBuffer::Concat(byteBuffer, encode_key_.data());
+
+  // Append decode key.
+  byteBuffer = ByteBuffer::Concat(byteBuffer, decode_key_.data());
+
+  return std::unique_ptr<std::string>(new std::string(byteBuffer.String()));
+}
+
+// static.
+std::unique_ptr<D2DConnectionContextV1>
+D2DConnectionContextV1::FromSavedSession(const std::string& savedSessionInfo) {
+  ByteBuffer byteBuffer = ByteBuffer(savedSessionInfo);
+
+  if (byteBuffer.size() != kSavedSessionLength) {
+    return nullptr;
+  }
+
+  uint32_t encode_sequence_number = BytesToUnsignedInt(
+      byteBuffer.SubArray(kEncodeSequenceStart, kEncodeSequenceEnd)->Vector());
+  uint32_t decode_sequence_number = BytesToUnsignedInt(
+      byteBuffer.SubArray(kDecodeSequenceStart, kDecodeSequenceEnd)->Vector());
+
+  const CryptoOps::SecretKey encode_key =
+      CryptoOps::SecretKey(*byteBuffer.SubArray(kEncodeKeyStart, kAesKeyLength),
+                           CryptoOps::KeyAlgorithm::AES_256_KEY);
+  const CryptoOps::SecretKey decode_key =
+      CryptoOps::SecretKey(*byteBuffer.SubArray(kDecodeKeyStart, kAesKeyLength),
+                           CryptoOps::KeyAlgorithm::AES_256_KEY);
+
+  return std::unique_ptr<D2DConnectionContextV1>(new D2DConnectionContextV1(
+      encode_key, decode_key, encode_sequence_number, decode_sequence_number));
+}
+
+}  // namespace securegcm
diff --git a/src/main/cpp/src/securegcm/d2d_crypto_ops.cc b/src/main/cpp/src/securegcm/d2d_crypto_ops.cc
new file mode 100644
index 0000000..49f0b85
--- /dev/null
+++ b/src/main/cpp/src/securegcm/d2d_crypto_ops.cc
@@ -0,0 +1,151 @@
+// Copyright 2020 Google LLC
+//
+// 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
+//
+//     https://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.
+
+#include "securegcm/d2d_crypto_ops.h"
+
+#include <sstream>
+
+#include "securemessage/secure_message_builder.h"
+#include "securemessage/secure_message_parser.h"
+#include "securemessage/util.h"
+
+namespace securegcm {
+
+using securemessage::CryptoOps;
+using securemessage::HeaderAndBody;
+using securemessage::SecureMessage;
+using securemessage::SecureMessageBuilder;
+using securemessage::SecureMessageParser;
+using securemessage::Util;
+
+namespace {
+
+// The current protocol version.
+const int kSecureGcmProtocolVersion = 1;
+
+// The number of bytes in an expected AES256 key.
+const int kAes256KeyLength = 32;
+}
+
+// static.
+const uint8_t D2DCryptoOps::kSalt[] = {
+    0x82, 0xAA, 0x55, 0xA0, 0xD3, 0x97, 0xF8, 0x83, 0x46, 0xCA, 0x1C,
+    0xEE, 0x8D, 0x39, 0x09, 0xB9, 0x5F, 0x13, 0xFA, 0x7D, 0xEB, 0x1D,
+    0x4A, 0xB3, 0x83, 0x76, 0xB8, 0x25, 0x6D, 0xA8, 0x55, 0x10};
+
+// static.
+const size_t D2DCryptoOps::kSaltLength = sizeof(D2DCryptoOps::kSalt);
+
+D2DCryptoOps::Payload::Payload(Type type, const string& message)
+    : type_(type), message_(message) {}
+
+D2DCryptoOps::D2DCryptoOps() {}
+
+// static.
+std::unique_ptr<string> D2DCryptoOps::SigncryptPayload(
+    const Payload& payload, const CryptoOps::SecretKey& secret_key) {
+  GcmMetadata gcm_metadata;
+  gcm_metadata.set_type(payload.type());
+  gcm_metadata.set_version(kSecureGcmProtocolVersion);
+
+  SecureMessageBuilder builder;
+  builder.SetPublicMetadata(gcm_metadata.SerializeAsString());
+
+  std::unique_ptr<SecureMessage> secure_message =
+      builder.BuildSignCryptedMessage(secret_key, CryptoOps::HMAC_SHA256,
+                                      secret_key, CryptoOps::AES_256_CBC,
+                                      payload.message());
+  if (!secure_message) {
+    Util::LogError("Unable to encrypt payload.");
+    return nullptr;
+  }
+
+  return std::unique_ptr<string>(
+      new string(secure_message->SerializeAsString()));
+}
+
+// static.
+std::unique_ptr<D2DCryptoOps::Payload> D2DCryptoOps::VerifyDecryptPayload(
+    const string& signcrypted_message, const CryptoOps::SecretKey& secret_key) {
+  SecureMessage secure_message;
+  if (!secure_message.ParseFromString(signcrypted_message)) {
+    Util::LogError("VerifyDecryptPayload: error parsing SecureMessage.");
+    return nullptr;
+  }
+
+  std::unique_ptr<HeaderAndBody> header_and_body =
+      SecureMessageParser::ParseSignCryptedMessage(
+          secure_message, secret_key, CryptoOps::HMAC_SHA256, secret_key,
+          CryptoOps::AES_256_CBC, string() /* associated_data */);
+  if (!header_and_body) {
+    Util::LogError("VerifyDecryptPayload: error verifying SecureMessage.");
+    return nullptr;
+  }
+
+  if (!header_and_body->header().has_public_metadata()) {
+    Util::LogError("VerifyDecryptPayload: no public metadata in header.");
+    return nullptr;
+  }
+
+  GcmMetadata metadata;
+  if (!metadata.ParseFromString(header_and_body->header().public_metadata())) {
+    Util::LogError("VerifyDecryptPayload: Failed to parse GcmMetadata.");
+    return nullptr;
+  }
+
+  if (metadata.version() != kSecureGcmProtocolVersion) {
+    std::ostringstream stream;
+    stream << "VerifyDecryptPayload: Unsupported protocol version "
+           << metadata.version();
+    Util::LogError(stream.str());
+    return nullptr;
+  }
+
+  return std::unique_ptr<Payload>(
+      new Payload(metadata.type(), header_and_body->body()));
+}
+
+// static.
+std::unique_ptr<CryptoOps::SecretKey> D2DCryptoOps::DeriveNewKeyForPurpose(
+    const securemessage::CryptoOps::SecretKey& master_key,
+    const string& purpose) {
+  if (master_key.data().size() != kAes256KeyLength) {
+    Util::LogError("DeriveNewKeyForPurpose: Invalid master_key length.");
+    return nullptr;
+  }
+
+  if (purpose.empty()) {
+    Util::LogError("DeriveNewKeyForPurpose: purpose is empty.");
+    return nullptr;
+  }
+
+  std::unique_ptr<string> raw_derived_key = CryptoOps::Hkdf(
+      master_key.data().String(),
+      string(reinterpret_cast<const char *>(kSalt), kSaltLength),
+      purpose);
+  if (!raw_derived_key) {
+    Util::LogError("DeriveNewKeyForPurpose: hkdf failed.");
+    return nullptr;
+  }
+
+  if (raw_derived_key->size() != kAes256KeyLength) {
+    Util::LogError("DeriveNewKeyForPurpose: Unexpected size of derived key.");
+    return nullptr;
+  }
+
+  return std::unique_ptr<CryptoOps::SecretKey>(
+      new CryptoOps::SecretKey(*raw_derived_key, CryptoOps::AES_256_KEY));
+}
+
+}  // namespace securegcm
diff --git a/src/main/cpp/src/securegcm/java_util.cc b/src/main/cpp/src/securegcm/java_util.cc
new file mode 100644
index 0000000..1ce4d7b
--- /dev/null
+++ b/src/main/cpp/src/securegcm/java_util.cc
@@ -0,0 +1,60 @@
+// Copyright 2020 Google LLC
+//
+// 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
+//
+//     https://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.
+
+#include "securegcm/java_util.h"
+
+#include <cstring>
+
+namespace securegcm {
+namespace java_util {
+
+namespace {
+
+// Returns the lower 32-bits of a int64_t |value| as an int32_t.
+int32_t Lower32Bits(int64_t value) {
+  const uint32_t lower_bits = static_cast<uint32_t>(value & 0xFFFFFFFF);
+  int32_t return_value;
+  std::memcpy(&return_value, &lower_bits, sizeof(uint32_t));
+  return return_value;
+}
+
+}  // namespace
+
+int32_t JavaMultiply(int32_t lhs, int32_t rhs) {
+  // Multiplication guaranteed to fit in int64_t, range from [2^63, 2^63 - 1].
+  // Minimum value is (-2^31)^2 = 2^62.
+  const int64_t result = static_cast<int64_t>(lhs) * static_cast<int64_t>(rhs);
+  return Lower32Bits(result);
+}
+
+int32_t JavaAdd(int32_t lhs, int32_t rhs) {
+  const int64_t result = static_cast<int64_t>(lhs) + static_cast<int64_t>(rhs);
+  return Lower32Bits(result);
+}
+
+int32_t JavaHashCode(const securemessage::ByteBuffer& byte_buffer) {
+  const string bytes = byte_buffer.String();
+  int32_t hash_code = 1;
+  for (const int8_t byte : bytes) {
+    int32_t int_value = static_cast<int32_t>(byte);
+    // Java relies on the overflow/underflow behaviour of arithmetic operations,
+    // which is undefined in C++, so we call our own Java-compatible versions of
+    // + and * here.
+    hash_code = JavaAdd(JavaMultiply(31, hash_code), int_value);
+  }
+  return hash_code;
+}
+
+}  // namespace java_util
+}  // namespace securegcm
diff --git a/src/main/cpp/src/securegcm/ukey2_handshake.cc b/src/main/cpp/src/securegcm/ukey2_handshake.cc
new file mode 100644
index 0000000..dc5c131
--- /dev/null
+++ b/src/main/cpp/src/securegcm/ukey2_handshake.cc
@@ -0,0 +1,715 @@
+// Copyright 2020 Google LLC
+//
+// 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
+//
+//     https://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.
+
+#include "securegcm/ukey2_handshake.h"
+
+#include <sstream>
+
+#include "securegcm/d2d_crypto_ops.h"
+#include "securemessage/public_key_proto_util.h"
+
+namespace securegcm {
+
+using securemessage::ByteBuffer;
+using securemessage::CryptoOps;
+using securemessage::GenericPublicKey;
+using securemessage::PublicKeyProtoUtil;
+
+namespace {
+
+// Salt value used to derive client and server keys for next protocol.
+const char kUkey2HkdfSalt[] = "UKEY2 v1 next";
+
+// Salt value used to derive verification string.
+const char kUkey2VerificationStringSalt[] = "UKEY2 v1 auth";
+
+// Maximum version of the handshake supported by this class.
+const uint32_t kVersion = 1;
+
+// Random nonce is fixed at 32 bytes (as per go/ukey2).
+const uint32_t kNonceLengthInBytes = 32;
+
+// Currently, we only support one next protocol.
+const char kNextProtocol[] = "AES_256_CBC-HMAC_SHA256";
+
+// Creates the appropriate KeyPair for |cipher|.
+std::unique_ptr<CryptoOps::KeyPair> GenerateKeyPair(
+    UKey2Handshake::HandshakeCipher cipher) {
+  switch (cipher) {
+    case UKey2Handshake::HandshakeCipher::P256_SHA512:
+      return CryptoOps::GenerateEcP256KeyPair();
+    default:
+      return nullptr;
+  }
+}
+
+// Parses a CryptoOps::PublicKey from a serialized GenericPublicKey.
+std::unique_ptr<securemessage::CryptoOps::PublicKey> ParsePublicKey(
+    const string& serialized_generic_public_key) {
+  GenericPublicKey generic_public_key;
+  if (!generic_public_key.ParseFromString(serialized_generic_public_key)) {
+    return nullptr;
+  }
+  return PublicKeyProtoUtil::ParsePublicKey(generic_public_key);
+}
+
+}  // namespace
+
+// static.
+std::unique_ptr<UKey2Handshake> UKey2Handshake::ForInitiator(
+    HandshakeCipher cipher) {
+  return std::unique_ptr<UKey2Handshake>(
+      new UKey2Handshake(InternalState::CLIENT_START, cipher));
+}
+
+// static.
+std::unique_ptr<UKey2Handshake> UKey2Handshake::ForResponder(
+    HandshakeCipher cipher) {
+  return std::unique_ptr<UKey2Handshake>(
+      new UKey2Handshake(InternalState::SERVER_START, cipher));
+}
+
+UKey2Handshake::UKey2Handshake(InternalState state, HandshakeCipher cipher)
+    : handshake_state_(state),
+      handshake_cipher_(cipher),
+      handshake_role_(state == InternalState::CLIENT_START
+                          ? HandshakeRole::CLIENT
+                          : HandshakeRole::SERVER),
+      our_key_pair_(GenerateKeyPair(cipher)) {}
+
+UKey2Handshake::State UKey2Handshake::GetHandshakeState() const {
+  switch (handshake_state_) {
+    case InternalState::CLIENT_START:
+    case InternalState::CLIENT_WAITING_FOR_SERVER_INIT:
+    case InternalState::CLIENT_AFTER_SERVER_INIT:
+    case InternalState::SERVER_START:
+    case InternalState::SERVER_AFTER_CLIENT_INIT:
+    case InternalState::SERVER_WAITING_FOR_CLIENT_FINISHED:
+      // Fallthrough intended -- these are all in-progress states.
+      return State::kInProgress;
+    case InternalState::HANDSHAKE_VERIFICATION_NEEDED:
+      return State::kVerificationNeeded;
+    case InternalState::HANDSHAKE_VERIFICATION_IN_PROGRESS:
+      return State::kVerificationInProgress;
+    case InternalState::HANDSHAKE_FINISHED:
+      return State::kFinished;
+    case InternalState::HANDSHAKE_ALREADY_USED:
+      return State::kAlreadyUsed;
+    case InternalState::HANDSHAKE_ERROR:
+      return State::kError;
+    default:
+      // Unreachable.
+      return State::kError;
+  }
+}
+
+const string& UKey2Handshake::GetLastError() const {
+  return last_error_;
+}
+
+std::unique_ptr<string> UKey2Handshake::GetNextHandshakeMessage() {
+  switch (handshake_state_) {
+    case InternalState::CLIENT_START: {
+      std::unique_ptr<string> client_init = MakeClientInitUkey2Message();
+      if (!client_init) {
+        // |last_error_| is already set.
+        return nullptr;
+      }
+
+      wrapped_client_init_ = *client_init;
+      handshake_state_ = InternalState::CLIENT_WAITING_FOR_SERVER_INIT;
+      return client_init;
+    }
+
+    case InternalState::SERVER_AFTER_CLIENT_INIT: {
+      std::unique_ptr<string> server_init = MakeServerInitUkey2Message();
+      if (!server_init) {
+        // |last_error_| is already set.
+        return nullptr;
+      }
+
+      wrapped_server_init_ = *server_init;
+      handshake_state_ = InternalState::SERVER_WAITING_FOR_CLIENT_FINISHED;
+      return server_init;
+    }
+
+    case InternalState::CLIENT_AFTER_SERVER_INIT: {
+      // Make sure we have a message 3 for the chosen cipher.
+      if (raw_message3_map_.count(handshake_cipher_) == 0) {
+        std::ostringstream stream;
+        stream << "Client state is CLIENT_AFTER_SERVER_INIT, and cipher is "
+               << static_cast<int>(handshake_cipher_)
+               << ", but no corresponding raw "
+               << "[Client Finished] message has been generated.";
+        SetError(stream.str());
+        return nullptr;
+      }
+      handshake_state_ = InternalState::HANDSHAKE_VERIFICATION_NEEDED;
+      return std::unique_ptr<string>(
+          new string(raw_message3_map_[handshake_cipher_]));
+    }
+
+    default: {
+      std::ostringstream stream;
+      stream << "Cannot get next message in state "
+             << static_cast<int>(handshake_state_);
+      SetError(stream.str());
+      return nullptr;
+    }
+  }
+}
+
+UKey2Handshake::ParseResult
+UKey2Handshake::ParseHandshakeMessage(const string& handshake_message) {
+  switch (handshake_state_) {
+    case InternalState::SERVER_START:
+      return ParseClientInitUkey2Message(handshake_message);
+    case InternalState::CLIENT_WAITING_FOR_SERVER_INIT:
+      return ParseServerInitUkey2Message(handshake_message);
+    case InternalState::SERVER_WAITING_FOR_CLIENT_FINISHED:
+      return ParseClientFinishUkey2Message(handshake_message);
+    default:
+      std::ostringstream stream;
+      stream << "Cannot parse message in state "
+             << static_cast<int>(handshake_state_);
+      SetError(stream.str());
+      return {false, nullptr};
+  }
+}
+
+std::unique_ptr<string> UKey2Handshake::GetVerificationString(int byte_length) {
+  if (byte_length < 1 || byte_length > 32) {
+    SetError("Minimum length is 1 byte, max is 32 bytes.");
+    return nullptr;
+  }
+
+  if (handshake_state_ != InternalState::HANDSHAKE_VERIFICATION_NEEDED) {
+    std::ostringstream stream;
+    stream << "Unexpected state: " << static_cast<int>(handshake_state_);
+    SetError(stream.str());
+    return nullptr;
+  }
+
+  if (!our_key_pair_ || !our_key_pair_->private_key || !their_public_key_) {
+    SetError("One of our private key or their public key is null.");
+    return nullptr;
+  }
+
+  switch (handshake_cipher_) {
+    case HandshakeCipher::P256_SHA512:
+      derived_secret_key_ = CryptoOps::KeyAgreementSha256(
+          *(our_key_pair_->private_key), *their_public_key_);
+      break;
+    default:
+      // Unreachable.
+      return nullptr;
+  }
+
+  if (!derived_secret_key_) {
+    SetError("Failed to derive shared secret key.");
+    return nullptr;
+  }
+
+  std::unique_ptr<string> auth_string = CryptoOps::Hkdf(
+      derived_secret_key_->data().String(),
+      string(kUkey2VerificationStringSalt, sizeof(kUkey2VerificationStringSalt)),
+      wrapped_client_init_ + wrapped_server_init_);
+
+  handshake_state_ = InternalState::HANDSHAKE_VERIFICATION_IN_PROGRESS;
+  return auth_string;
+}
+
+bool UKey2Handshake::VerifyHandshake() {
+  if (handshake_state_ != InternalState::HANDSHAKE_VERIFICATION_IN_PROGRESS) {
+    std::ostringstream stream;
+    stream << "Unexpected state: " << static_cast<int>(handshake_state_);
+    SetError(stream.str());
+    return false;
+  }
+
+  handshake_state_ = InternalState::HANDSHAKE_FINISHED;
+  return true;
+}
+
+std::unique_ptr<D2DConnectionContextV1> UKey2Handshake::ToConnectionContext() {
+  if (InternalState::HANDSHAKE_FINISHED != handshake_state_) {
+    std::ostringstream stream;
+    stream << "ToConnectionContext can only be called when handshake is "
+           << "completed, but current state is "
+           << static_cast<int>(handshake_state_);
+    SetError(stream.str());
+    return nullptr;
+  }
+
+  if (!derived_secret_key_) {
+    SetError("Derived key is null.");
+    return nullptr;
+  }
+
+  string info = wrapped_client_init_ + wrapped_server_init_;
+  std::unique_ptr<string> master_key_data = CryptoOps::Hkdf(
+      derived_secret_key_->data().String(), kUkey2HkdfSalt, info);
+
+  if (!master_key_data) {
+    SetError("Failed to create master key.");
+    return nullptr;
+  }
+
+  // Derive separate encode keys for both client and server.
+  CryptoOps::SecretKey master_key(*master_key_data, CryptoOps::AES_256_KEY);
+  std::unique_ptr<CryptoOps::SecretKey> client_key =
+      D2DCryptoOps::DeriveNewKeyForPurpose(master_key, "client");
+  std::unique_ptr<CryptoOps::SecretKey> server_key =
+      D2DCryptoOps::DeriveNewKeyForPurpose(master_key, "server");
+  if (!client_key || !server_key) {
+    SetError("Failed to derive client or server key.");
+    return nullptr;
+  }
+
+  handshake_state_ = InternalState::HANDSHAKE_ALREADY_USED;
+
+  return std::unique_ptr<D2DConnectionContextV1>(new D2DConnectionContextV1(
+      handshake_role_ == HandshakeRole::CLIENT ? *client_key : *server_key,
+      handshake_role_ == HandshakeRole::CLIENT ? *server_key : *client_key,
+      0 /* initial encode sequence number */,
+      0 /* initial decode sequence number */));
+}
+
+UKey2Handshake::ParseResult UKey2Handshake::ParseClientInitUkey2Message(
+    const string& handshake_message) {
+  // Deserialize the protobuf.
+  Ukey2Message message;
+  if (!message.ParseFromString(handshake_message)) {
+    return CreateFailedResultWithAlert(Ukey2Alert::BAD_MESSAGE,
+                                       "Can't parse message 1.");
+  }
+
+  // Verify that message_type == CLIENT_INIT.
+  if (!message.has_message_type() ||
+      message.message_type() != Ukey2Message::CLIENT_INIT) {
+    return CreateFailedResultWithAlert(
+        Ukey2Alert::BAD_MESSAGE,
+        "Expected, but did not find ClientInit message type.");
+  }
+
+  // Derserialize message_data as a ClientInit message.
+  if (!message.has_message_data()) {
+    return CreateFailedResultWithAlert(
+        Ukey2Alert::BAD_MESSAGE_DATA,
+        "Expected message data, but did not find it.");
+  }
+
+  Ukey2ClientInit client_init;
+  if (!client_init.ParseFromString(message.message_data())) {
+    return CreateFailedResultWithAlert(
+        Ukey2Alert::BAD_MESSAGE_DATA,
+        "Can't parse message data into ClientInit.");
+  }
+
+  // Check that version == VERSION.
+  if (!client_init.has_version()) {
+    return CreateFailedResultWithAlert(Ukey2Alert::BAD_VERSION,
+                                       "ClientInit missing version.");
+  }
+  if (client_init.version() != kVersion) {
+    return CreateFailedResultWithAlert(Ukey2Alert::BAD_VERSION,
+                                       "ClientInit version mismatch.");
+  }
+
+  // Check that random is exactly kNonceLengthInBytes.
+  if (!client_init.has_random()) {
+    return CreateFailedResultWithAlert(Ukey2Alert::BAD_RANDOM,
+                                       "ClientInit missing random.");
+  }
+  if (client_init.random().length() != kNonceLengthInBytes) {
+    return CreateFailedResultWithAlert(
+        Ukey2Alert::BAD_RANDOM, "ClientInit has incorrect nonce length.");
+  }
+
+  // Check to see if any of the handshake_cipher in handshake_cipher_commitment
+  // are acceptable. Servers should select the first ahdnshake_cipher that it
+  // finds acceptable to support clients signalling deprecated but supported
+  // HandshakeCiphers. If no handshake_cipher is acceptable (or there are no
+  // HandshakeCiphers in the message), the server sends a BAD_HANDSHAKE_CIPHER
+  // alert message.
+  if (client_init.cipher_commitments_size() == 0) {
+    return CreateFailedResultWithAlert(
+        Ukey2Alert::BAD_HANDSHAKE_CIPHER,
+        "ClientInit is missing cipher commitments.");
+  }
+
+  for (const Ukey2ClientInit::CipherCommitment& commitment :
+       client_init.cipher_commitments()) {
+    if (!commitment.has_handshake_cipher() || !commitment.has_commitment() ||
+        commitment.commitment().empty()) {
+      return CreateFailedResultWithAlert(
+          Ukey2Alert::BAD_HANDSHAKE_CIPHER,
+          "ClientInit has improperly formatted cipher commitment.");
+    }
+
+    // TODO(aczeskis): for now we only support one cipher, eventually support
+    // more.
+    if (commitment.handshake_cipher() == static_cast<int>(handshake_cipher_)) {
+      peer_commitment_ = commitment.commitment();
+    }
+  }
+
+  if (peer_commitment_.empty()) {
+    return CreateFailedResultWithAlert(Ukey2Alert::BAD_HANDSHAKE_CIPHER,
+                                       "No acceptable commitments found");
+  }
+
+  // Checks that next_protocol contains a protocol that the server supports. We
+  // currently only support one protocol.
+  if (!client_init.has_next_protocol() ||
+      client_init.next_protocol() != kNextProtocol) {
+    return CreateFailedResultWithAlert(Ukey2Alert::BAD_NEXT_PROTOCOL,
+                                       "Incorrect next protocol.");
+  }
+
+  // Store raw message for AUTH_STRING computation.
+  wrapped_client_init_ = handshake_message;
+  handshake_state_ = InternalState::SERVER_AFTER_CLIENT_INIT;
+  return CreateSuccessResult();
+}
+
+UKey2Handshake::ParseResult UKey2Handshake::ParseServerInitUkey2Message(
+    const string& handshake_message) {
+  // Deserialize the protobuf.
+  Ukey2Message message;
+  if (!message.ParseFromString(handshake_message)) {
+    return CreateFailedResultWithAlert(Ukey2Alert::BAD_MESSAGE,
+                                       "Can't parse message 2.");
+  }
+
+  // Verify that message_type == SERVER_INIT.
+  if (!message.has_message_type() ||
+      message.message_type() != Ukey2Message::SERVER_INIT) {
+    return CreateFailedResultWithAlert(
+        Ukey2Alert::BAD_MESSAGE,
+        "Expected, but did not find SERVER_INIT message type.");
+  }
+
+  // Derserialize message_data as a ServerInit message.
+  if (!message.has_message_data()) {
+    return CreateFailedResultWithAlert(
+        Ukey2Alert::BAD_MESSAGE_DATA,
+        "Expected message data, but did not find it.");
+  }
+
+  Ukey2ServerInit server_init;
+  if (!server_init.ParseFromString(message.message_data())) {
+    return CreateFailedResultWithAlert(
+        Ukey2Alert::BAD_MESSAGE_DATA,
+        "Can't parse message data into ServerInit.");
+  }
+
+  // Check that version == VERSION.
+  if (!server_init.has_version()) {
+    return CreateFailedResultWithAlert(Ukey2Alert::BAD_VERSION,
+                                       "ServerInit missing version.");
+  }
+  if (server_init.version() != kVersion) {
+    return CreateFailedResultWithAlert(Ukey2Alert::BAD_VERSION,
+                                       "ServerInit version mismatch.");
+  }
+
+  // Check that random is exactly kNonceLengthInBytes.
+  if (!server_init.has_random()) {
+    return CreateFailedResultWithAlert(Ukey2Alert::BAD_RANDOM,
+                                       "ServerInit missing random.");
+  }
+  if (server_init.random().length() != kNonceLengthInBytes) {
+    return CreateFailedResultWithAlert(
+        Ukey2Alert::BAD_RANDOM, "ServerInit has incorrect nonce length.");
+  }
+
+  // Check that the handshake_cipher matches a handshake cipher that was sent in
+  // ClientInit::cipher_commitments().
+  if (!server_init.has_handshake_cipher()) {
+    return CreateFailedResultWithAlert(Ukey2Alert::BAD_HANDSHAKE_CIPHER,
+                                       "No handshake cipher found.");
+  }
+
+  Ukey2HandshakeCipher cipher = server_init.handshake_cipher();
+  HandshakeCipher server_cipher;
+  switch (static_cast<HandshakeCipher>(cipher)) {
+    case HandshakeCipher::P256_SHA512:
+      server_cipher = static_cast<HandshakeCipher>(cipher);
+      break;
+    default:
+      return CreateFailedResultWithAlert(Ukey2Alert::BAD_HANDSHAKE_CIPHER,
+                                         "No acceptable handshake found.");
+  }
+
+  // Check that public_key parses into a correct public key structure.
+  if (!server_init.has_public_key()) {
+    return CreateFailedResultWithAlert(Ukey2Alert::BAD_PUBLIC_KEY,
+                                       "No public key found in ServerInit.");
+  }
+
+  their_public_key_ = ParsePublicKey(server_init.public_key());
+  if (!their_public_key_) {
+    return CreateFailedResultWithAlert(Ukey2Alert::BAD_PUBLIC_KEY,
+                                       "Failed to parse public key.");
+  }
+
+  // Store raw message for AUTH_STRING computation.
+  wrapped_server_init_ = handshake_message;
+  handshake_state_ = InternalState::CLIENT_AFTER_SERVER_INIT;
+  return CreateSuccessResult();
+}
+
+UKey2Handshake::ParseResult UKey2Handshake::ParseClientFinishUkey2Message(
+    const string& handshake_message) {
+  // Deserialize the protobuf.
+  Ukey2Message message;
+  if (!message.ParseFromString(handshake_message)) {
+    return CreateFailedResultWithoutAlert("Can't parse message 3.");
+  }
+
+  // Verify that message_type == CLIENT_FINISH.
+  if (!message.has_message_type() ||
+      message.message_type() != Ukey2Message::CLIENT_FINISH) {
+    return CreateFailedResultWithoutAlert(
+        "Expected, but did not find CLIENT_FINISH message type.");
+  }
+
+  // Verify that the hash of the CLientFinished message matches the expected
+  // commitment from ClientInit.
+  if (!VerifyCommitment(handshake_message)) {
+    return CreateFailedResultWithoutAlert(last_error_);
+  }
+
+  // Deserialize message_data as a ClientFinished message.
+  if (!message.has_message_data()) {
+    return CreateFailedResultWithoutAlert(
+        "Expected message data, but didn't find it.");
+  }
+
+  Ukey2ClientFinished client_finished;
+  if (!client_finished.ParseFromString(message.message_data())) {
+    return CreateFailedResultWithoutAlert("Failed to parse ClientFinished.");
+  }
+
+  // Check that public_key parses into a correct public key structure.
+  if (!client_finished.has_public_key()) {
+    return CreateFailedResultWithoutAlert(
+        "No public key found in ClientFinished.");
+  }
+
+  their_public_key_ = ParsePublicKey(client_finished.public_key());
+  if (!their_public_key_) {
+    return CreateFailedResultWithoutAlert("Failed to parse public key.");
+  }
+
+  handshake_state_ = InternalState::HANDSHAKE_VERIFICATION_NEEDED;
+  return CreateSuccessResult();
+}
+
+UKey2Handshake::ParseResult UKey2Handshake::CreateFailedResultWithAlert(
+    Ukey2Alert::AlertType alert_type, const string& error_message) {
+  if (!Ukey2Alert_AlertType_IsValid(alert_type)) {
+    std::ostringstream stream;
+    stream << "Unknown alert type: " << static_cast<int>(alert_type);
+    SetError(stream.str());
+    return {false, nullptr};
+  }
+
+  Ukey2Alert alert;
+  alert.set_type(alert_type);
+  if (!error_message.empty()) {
+    alert.set_error_message(error_message);
+  }
+
+  std::unique_ptr<string> alert_message =
+      MakeUkey2Message(Ukey2Message::ALERT, alert.SerializeAsString());
+
+  SetError(error_message);
+  ParseResult result{false, std::move(alert_message)};
+  return result;
+}
+
+UKey2Handshake::ParseResult
+UKey2Handshake::CreateFailedResultWithoutAlert(const string& error_message) {
+  SetError(error_message);
+  return {false, nullptr};
+}
+
+UKey2Handshake::ParseResult UKey2Handshake::CreateSuccessResult() {
+  return {true, nullptr};
+}
+
+bool UKey2Handshake::VerifyCommitment(const string& handshake_message) {
+  std::unique_ptr<ByteBuffer> actual_client_finish_hash;
+  switch (handshake_cipher_) {
+    case HandshakeCipher::P256_SHA512:
+      actual_client_finish_hash =
+          CryptoOps::Sha512(ByteBuffer(handshake_message));
+      break;
+    default:
+      // Unreachable.
+      return false;
+  }
+
+  if (!actual_client_finish_hash) {
+    SetError("Failed to hash ClientFinish message.");
+    return false;
+  }
+
+  // Note: Equals() is a time constant comparison operation.
+  if (!actual_client_finish_hash->Equals(peer_commitment_)) {
+    SetError("Failed to verify commitment.");
+    return false;
+  }
+
+  return true;
+}
+
+std::unique_ptr<Ukey2ClientInit::CipherCommitment>
+UKey2Handshake::GenerateP256Sha512Commitment() {
+  // Generate the corresponding ClientFinished message if it's not done yet.
+  if (raw_message3_map_.count(HandshakeCipher::P256_SHA512) == 0) {
+    if (!our_key_pair_ || !our_key_pair_->public_key) {
+      SetError("Invalid public key.");
+      return nullptr;
+    }
+
+    std::unique_ptr<GenericPublicKey> generic_public_key =
+        PublicKeyProtoUtil::EncodePublicKey(*(our_key_pair_->public_key));
+    if (!generic_public_key) {
+      SetError("Failed to encode generic public key.");
+      return nullptr;
+    }
+
+    Ukey2ClientFinished client_finished;
+    client_finished.set_public_key(generic_public_key->SerializeAsString());
+    std::unique_ptr<string> serialized_ukey2_message = MakeUkey2Message(
+        Ukey2Message::CLIENT_FINISH, client_finished.SerializeAsString());
+    if (!serialized_ukey2_message) {
+      SetError("Failed to serialized Ukey2Message.");
+      return nullptr;
+    }
+
+    raw_message3_map_[HandshakeCipher::P256_SHA512] = *serialized_ukey2_message;
+  }
+
+  // Create the SHA512 commitment from raw message 3.
+  std::unique_ptr<ByteBuffer> commitment = CryptoOps::Sha512(
+      ByteBuffer(raw_message3_map_[HandshakeCipher::P256_SHA512]));
+  if (!commitment) {
+    SetError("Failed to hash message for commitment.");
+    return nullptr;
+  }
+
+  // Wrap the commitment in a proto.
+  std::unique_ptr<Ukey2ClientInit::CipherCommitment>
+      handshake_cipher_commitment(new Ukey2ClientInit::CipherCommitment());
+  handshake_cipher_commitment->set_handshake_cipher(P256_SHA512);
+  handshake_cipher_commitment->set_commitment(commitment->String());
+
+  return handshake_cipher_commitment;
+}
+
+std::unique_ptr<string> UKey2Handshake::MakeClientInitUkey2Message() {
+  std::unique_ptr<ByteBuffer> nonce =
+      CryptoOps::SecureRandom(kNonceLengthInBytes);
+  if (!nonce) {
+    SetError("Failed to generate nonce.");
+    return nullptr;
+  }
+
+  Ukey2ClientInit client_init;
+  client_init.set_version(kVersion);
+  client_init.set_random(nonce->String());
+  client_init.set_next_protocol(kNextProtocol);
+
+  // At the moment, we only support one cipher.
+  std::unique_ptr<Ukey2ClientInit::CipherCommitment>
+      handshake_cipher_commitment = GenerateP256Sha512Commitment();
+  if (!handshake_cipher_commitment) {
+    // |last_error_| already set.
+    return nullptr;
+  }
+  *(client_init.add_cipher_commitments()) = *handshake_cipher_commitment;
+
+  return MakeUkey2Message(Ukey2Message::CLIENT_INIT,
+                          client_init.SerializeAsString());
+}
+
+std::unique_ptr<string> UKey2Handshake::MakeServerInitUkey2Message() {
+  std::unique_ptr<ByteBuffer> nonce =
+      CryptoOps::SecureRandom(kNonceLengthInBytes);
+  if (!nonce) {
+    SetError("Failed to generate nonce.");
+    return nullptr;
+  }
+
+  if (!our_key_pair_ || !our_key_pair_->public_key) {
+    SetError("Invalid key pair.");
+    return nullptr;
+  }
+
+  std::unique_ptr<GenericPublicKey> public_key =
+      PublicKeyProtoUtil::EncodePublicKey(*(our_key_pair_->public_key));
+  if (!public_key) {
+    SetError("Failed to encode public key.");
+    return nullptr;
+  }
+
+  Ukey2ServerInit server_init;
+  server_init.set_version(kVersion);
+  server_init.set_random(nonce->String());
+  server_init.set_handshake_cipher(
+      static_cast<Ukey2HandshakeCipher>(handshake_cipher_));
+  server_init.set_public_key(public_key->SerializeAsString());
+
+  return MakeUkey2Message(Ukey2Message::SERVER_INIT,
+                          server_init.SerializeAsString());
+}
+
+// Generates the serialized representation of a Ukey2Message based on the
+// provided |type| and |data|. On error, returns nullptr and writes error
+// message to |out_error|.
+std::unique_ptr<string> UKey2Handshake::MakeUkey2Message(
+    Ukey2Message::Type type, const string& data) {
+  Ukey2Message message;
+  if (!Ukey2Message::Type_IsValid(type)) {
+    std::ostringstream stream;
+    stream << "Invalid message type: " << type;
+    SetError(stream.str());
+    return nullptr;
+  }
+  message.set_message_type(type);
+
+  // Only ALERT messages can have a blank data field.
+  if (type != Ukey2Message::ALERT) {
+    if (data.length() == 0) {
+      SetError("Cannot send empty message data for non-alert messages");
+      return nullptr;
+    }
+  }
+  message.set_message_data(data);
+
+  std::unique_ptr<string> serialized(new string());
+  message.SerializeToString(serialized.get());
+  return serialized;
+}
+
+void UKey2Handshake::SetError(const string& error_message) {
+  handshake_state_ = InternalState::HANDSHAKE_ERROR;
+  last_error_ = error_message;
+}
+
+}  // namespace securegcm
diff --git a/src/main/cpp/src/securegcm/ukey2_shell.cc b/src/main/cpp/src/securegcm/ukey2_shell.cc
new file mode 100644
index 0000000..99a35a8
--- /dev/null
+++ b/src/main/cpp/src/securegcm/ukey2_shell.cc
@@ -0,0 +1,297 @@
+// Copyright 2020 Google LLC
+//
+// 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
+//
+//     https://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.
+
+// The ukey2_shell binary is a command-line based wrapper, exercising the
+// UKey2Handshake class. Its main use is to be run in a Java test, testing the
+// compatibility of the Java and C++ implementations.
+//
+// This program can be run in two modes, initiator or responder (default is
+// initiator):
+//   ukey2_shell --mode=initiator --verification_string_length=32
+//   ukey2_shell --mode=responder --verification_string_length=32
+//
+// In initiator mode, the program performs the initiator handshake, and in
+// responder mode, it performs the responder handshake.
+//
+// After the handshake is done, the program establishes a secure connection and
+// enters a loop in which it processes the following commands:
+//    * encrypt <payload>: encrypts the payload and prints it.
+//    * decrypt <message>: decrypts the message and prints the payload.
+//    * session_unique:    prints the session unique value.
+//
+// IO is performed on stdin and stdout. To provide frame control, all frames
+// will have the following simple format:
+//   [ length | bytes ]
+// where |length| is a 4 byte big-endian encoded unsigned integer.
+#include <cassert>
+#include <cstdio>
+#include <iostream>
+#include <memory>
+
+#include "securegcm/ukey2_handshake.h"
+#include "absl/container/fixed_array.h"
+#include "absl/flags/flag.h"
+#include "absl/flags/parse.h"
+
+#define LOG(ERROR) std::cerr
+#define CHECK_EQ(a, b) do { if ((a) != (b)) abort(); } while(0)
+
+ABSL_FLAG(
+    int, verification_string_length, 32,
+    "The length in bytes of the verification string. Must be a value between 1"
+    "and 32.");
+ABSL_FLAG(string, mode, "initiator",
+          "The mode to run as: one of [initiator, responder]");
+
+namespace securegcm {
+
+namespace {
+
+// Writes |message| to stdout in the frame format.
+void WriteFrame(const string& message) {
+  // Write length of |message| in little-endian.
+  const uint32_t length = message.length();
+  fputc((length >> (3 * 8)) & 0xFF, stdout);
+  fputc((length >> (2 * 8)) & 0xFF, stdout);
+  fputc((length >> (1 * 8)) & 0xFF, stdout);
+  fputc((length >> (0 * 8)) & 0xFF, stdout);
+
+  // Write message to stdout.
+  CHECK_EQ(message.length(),
+           fwrite(message.c_str(), 1, message.length(), stdout));
+  CHECK_EQ(0, fflush(stdout));
+}
+
+// Returns a message read from stdin after parsing it from the frame format.
+string ReadFrame() {
+  // Read length of the frame from the stream.
+  uint8_t length_data[sizeof(uint32_t)];
+  CHECK_EQ(sizeof(uint32_t), fread(&length_data, 1, sizeof(uint32_t), stdin));
+
+  uint32_t length = 0;
+  length |= static_cast<uint32_t>(length_data[0]) << (3 * 8);
+  length |= static_cast<uint32_t>(length_data[1]) << (2 * 8);
+  length |= static_cast<uint32_t>(length_data[2]) << (1 * 8);
+  length |= static_cast<uint32_t>(length_data[3]) << (0 * 8);
+
+  // Read |length| bytes from the stream.
+  absl::FixedArray<char> buffer(length);
+  CHECK_EQ(length, fread(buffer.data(), 1, length, stdin));
+
+  return string(buffer.data(), length);
+}
+
+}  // namespace
+
+// Handles the runtime of the program in initiator or responder mode.
+class UKey2Shell {
+ public:
+  explicit UKey2Shell(int verification_string_length);
+  ~UKey2Shell();
+
+  // Runs the shell, performing the initiator handshake for authentication.
+  bool RunAsInitiator();
+
+  // Runs the shell, performing the responder handshake for authentication.
+  bool RunAsResponder();
+
+ private:
+  // Writes the next handshake message obtained from |ukey2_handshake_| to
+  // stdout.
+  // If an error occurs, |tag| is logged.
+  bool WriteNextHandshakeMessage(const string& tag);
+
+  // Reads the next handshake message from stdin and parses it using
+  // |ukey2_handshake_|.
+  // If an error occurs, |tag| is logged.
+  bool ReadNextHandshakeMessage(const string& tag);
+
+  // Writes the verification string to stdout and waits for a confirmation from
+  // stdin.
+  bool ConfirmVerificationString();
+
+  // After authentication is completed, this function runs the loop handing the
+  // secure connection.
+  bool RunSecureConnectionLoop();
+
+  std::unique_ptr<UKey2Handshake> ukey2_handshake_;
+  const int verification_string_length_;
+};
+
+UKey2Shell::UKey2Shell(int verification_string_length)
+    : verification_string_length_(verification_string_length) {}
+
+UKey2Shell::~UKey2Shell() {}
+
+bool UKey2Shell::WriteNextHandshakeMessage(const string& tag) {
+  const std::unique_ptr<string> message =
+      ukey2_handshake_->GetNextHandshakeMessage();
+  if (!message) {
+    LOG(ERROR) << "Failed to create [" << tag
+               << "] message: " << ukey2_handshake_->GetLastError();
+    return false;
+  }
+  WriteFrame(*message);
+  return true;
+}
+
+bool UKey2Shell::ReadNextHandshakeMessage(const string& tag) {
+  const string message = ReadFrame();
+  const UKey2Handshake::ParseResult result =
+      ukey2_handshake_->ParseHandshakeMessage(message);
+  if (!result.success) {
+    LOG(ERROR) << "Failed to parse [" << tag
+               << "] message: " << ukey2_handshake_->GetLastError();
+    if (result.alert_to_send) {
+      WriteFrame(*result.alert_to_send);
+    }
+    return false;
+  }
+  return true;
+}
+
+bool UKey2Shell::ConfirmVerificationString() {
+  const std::unique_ptr<string> auth_string =
+      ukey2_handshake_->GetVerificationString(verification_string_length_);
+  if (!auth_string) {
+    LOG(ERROR) << "Failed to get verification string: "
+               << ukey2_handshake_->GetLastError();
+    return false;
+  }
+  WriteFrame(*auth_string);
+
+  // Wait for ack message.
+  const string message = ReadFrame();
+  if (message != "ok") {
+    LOG(ERROR) << "Expected string 'ok'";
+    return false;
+  }
+  ukey2_handshake_->VerifyHandshake();
+  return true;
+}
+
+bool UKey2Shell::RunSecureConnectionLoop() {
+  const std::unique_ptr<D2DConnectionContextV1> connection_context =
+      ukey2_handshake_->ToConnectionContext();
+  if (!connection_context) {
+    LOG(ERROR) << "Failed to create connection context: "
+               << ukey2_handshake_->GetLastError();
+    return false;
+  }
+
+  for (;;) {
+    // Parse the next expression.
+    const string expression = ReadFrame();
+    const size_t pos = expression.find(" ");
+    if (pos == std::string::npos) {
+      LOG(ERROR) << "Invalid command in connection loop.";
+      return false;
+    }
+    const string command = expression.substr(0, pos);
+
+    if (command == "encrypt") {
+      const string payload = expression.substr(pos + 1, expression.length());
+      std::unique_ptr<string> encoded_message =
+          connection_context->EncodeMessageToPeer(payload);
+      if (!encoded_message) {
+        LOG(ERROR) << "Failed to encode payload of size " << payload.length();
+        return false;
+      }
+      WriteFrame(*encoded_message);
+    } else if (command == "decrypt") {
+      const string message = expression.substr(pos + 1, expression.length());
+      std::unique_ptr<string> decoded_payload =
+          connection_context->DecodeMessageFromPeer(message);
+      if (!decoded_payload) {
+        LOG(ERROR) << "Failed to decode message of size " << message.length();
+        return false;
+      }
+      WriteFrame(*decoded_payload);
+    } else if (command == "session_unique") {
+      std::unique_ptr<string> session_unique =
+          connection_context->GetSessionUnique();
+      if (!session_unique) {
+        LOG(ERROR) << "Failed to get session unique.";
+        return false;
+      }
+      WriteFrame(*session_unique);
+    } else {
+      LOG(ERROR) << "Unrecognized command: " << command;
+      return false;
+    }
+  }
+}
+
+bool UKey2Shell::RunAsInitiator() {
+  ukey2_handshake_ = UKey2Handshake::ForInitiator(
+      UKey2Handshake::HandshakeCipher::P256_SHA512);
+  if (!ukey2_handshake_) {
+    LOG(ERROR) << "Unable to create UKey2Handshake";
+    return false;
+  }
+
+  // Perform handshake.
+  if (!WriteNextHandshakeMessage("Initiator Init")) return false;
+  if (!ReadNextHandshakeMessage("Responder Init")) return false;
+  if (!WriteNextHandshakeMessage("Initiator Finish")) return false;
+  if (!ConfirmVerificationString()) return false;
+
+  // Create a connection context.
+  return RunSecureConnectionLoop();
+}
+
+bool UKey2Shell::RunAsResponder() {
+  ukey2_handshake_ = UKey2Handshake::ForResponder(
+      UKey2Handshake::HandshakeCipher::P256_SHA512);
+  if (!ukey2_handshake_) {
+    LOG(ERROR) << "Unable to create UKey2Handshake";
+    return false;
+  }
+
+  // Perform handshake.
+  if (!ReadNextHandshakeMessage("Initiator Init")) return false;
+  if (!WriteNextHandshakeMessage("Responder Init")) return false;
+  if (!ReadNextHandshakeMessage("Initiator Finish")) return false;
+  if (!ConfirmVerificationString()) return false;
+
+  // Create a connection context.
+  return RunSecureConnectionLoop();
+}
+
+}  // namespace securegcm
+
+int main(int argc, char** argv) {
+  absl::ParseCommandLine(argc, argv);
+
+  const int verification_string_length =
+      absl::GetFlag(FLAGS_verification_string_length);
+  if (verification_string_length < 1 || verification_string_length > 32) {
+    LOG(ERROR) << "Invalid flag value, verification_string_length: "
+               << verification_string_length;
+    return 1;
+  }
+
+  securegcm::UKey2Shell shell(verification_string_length);
+  int exit_code = 0;
+  const string mode = absl::GetFlag(FLAGS_mode);
+  if (mode == "initiator") {
+    exit_code = !shell.RunAsInitiator();
+  } else if (mode == "responder") {
+    exit_code = !shell.RunAsResponder();
+  } else {
+    LOG(ERROR) << "Invalid flag value, mode: " << mode;
+    exit_code = 1;
+  }
+  return exit_code;
+}
diff --git a/src/main/cpp/test/securegcm/CMakeLists.txt b/src/main/cpp/test/securegcm/CMakeLists.txt
new file mode 100644
index 0000000..272f919
--- /dev/null
+++ b/src/main/cpp/test/securegcm/CMakeLists.txt
@@ -0,0 +1,31 @@
+# Copyright 2020 Google LLC
+#
+# 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
+#
+#     https://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.
+
+add_executable(ukey2_test
+  d2d_connection_context_v1_test.cc
+  d2d_crypto_ops_test.cc
+  java_util_test.cc
+)
+
+target_link_libraries(ukey2_test
+  PUBLIC
+    ukey2
+    gtest
+    gtest_main
+)
+
+add_test(
+  NAME ukey2_test
+  COMMAND ukey2_test
+)
diff --git a/src/main/cpp/test/securegcm/d2d_connection_context_v1_test.cc b/src/main/cpp/test/securegcm/d2d_connection_context_v1_test.cc
new file mode 100644
index 0000000..daf69d1
--- /dev/null
+++ b/src/main/cpp/test/securegcm/d2d_connection_context_v1_test.cc
@@ -0,0 +1,124 @@
+// Copyright 2020 Google LLC
+//
+// 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
+//
+//     https://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.
+
+#include "securegcm/d2d_connection_context_v1.h"
+
+#include "securegcm/d2d_crypto_ops.h"
+#include "gtest/gtest.h"
+
+namespace securegcm {
+
+using securemessage::CryptoOps;
+
+namespace {
+
+// The encode and decode keys should be 32 bytes.
+const char kEncodeKeyData[] = "initiator_encode_key_for_aes_256";
+const char kDecodeKeyData[] = "initiator_decode_key_for_aes_256";
+
+}  // namespace
+
+// A friend to access the private variables of D2DConnectionContextV1.
+class D2DConnectionContextV1Peer {
+ public:
+  explicit D2DConnectionContextV1Peer(const std::string& savedSessionInfo) {
+    context_ = D2DConnectionContextV1::FromSavedSession(savedSessionInfo);
+  }
+
+  D2DConnectionContextV1* GetContext() { return context_.get(); }
+
+  uint32_t GetEncodeSequenceNumber() {
+    return context_->encode_sequence_number_;
+  }
+
+  uint32_t GetDecodeSequenceNumber() {
+    return context_->decode_sequence_number_;
+  }
+
+ private:
+  std::unique_ptr<D2DConnectionContextV1> context_;
+};
+
+TEST(D2DConnectionContextionV1Test, SaveSession) {
+  CryptoOps::SecretKey encodeKey = CryptoOps::SecretKey(
+      kEncodeKeyData, CryptoOps::KeyAlgorithm::AES_256_KEY);
+  CryptoOps::SecretKey decodeKey = CryptoOps::SecretKey(
+      kDecodeKeyData, CryptoOps::KeyAlgorithm::AES_256_KEY);
+
+  D2DConnectionContextV1 initiator =
+      D2DConnectionContextV1(encodeKey, decodeKey, 0, 1);
+  D2DConnectionContextV1 responder =
+      D2DConnectionContextV1(decodeKey, encodeKey, 1, 0);
+
+  std::unique_ptr<std::string> initiatorSavedSessionState =
+      initiator.SaveSession();
+  std::unique_ptr<std::string> responderSavedSessionState =
+      responder.SaveSession();
+
+  D2DConnectionContextV1Peer restoredInitiator =
+      D2DConnectionContextV1Peer(*initiatorSavedSessionState);
+  D2DConnectionContextV1Peer restoredResponder =
+      D2DConnectionContextV1Peer(*responderSavedSessionState);
+
+  // Verify internal state matches initialization.
+  EXPECT_EQ(0, restoredInitiator.GetEncodeSequenceNumber());
+  EXPECT_EQ(1, restoredInitiator.GetDecodeSequenceNumber());
+  EXPECT_EQ(1, restoredResponder.GetEncodeSequenceNumber());
+  EXPECT_EQ(0, restoredResponder.GetDecodeSequenceNumber());
+
+  EXPECT_EQ(*restoredInitiator.GetContext()->GetSessionUnique(),
+            *restoredResponder.GetContext()->GetSessionUnique());
+
+  const std::string message = "ping";
+
+  // Ensure that they can still talk to one another.
+  std::string encodedMessage =
+      *restoredInitiator.GetContext()->EncodeMessageToPeer(message);
+  std::string decodedMessage =
+      *restoredResponder.GetContext()->DecodeMessageFromPeer(encodedMessage);
+
+  EXPECT_EQ(message, decodedMessage);
+
+  encodedMessage =
+      *restoredResponder.GetContext()->EncodeMessageToPeer(message);
+  decodedMessage =
+      *restoredInitiator.GetContext()->DecodeMessageFromPeer(encodedMessage);
+
+  EXPECT_EQ(message, decodedMessage);
+}
+
+TEST(D2DConnectionContextionV1Test, SaveSession_TooShort) {
+  CryptoOps::SecretKey encodeKey = CryptoOps::SecretKey(
+      kEncodeKeyData, CryptoOps::KeyAlgorithm::AES_256_KEY);
+  CryptoOps::SecretKey decodeKey = CryptoOps::SecretKey(
+      kDecodeKeyData, CryptoOps::KeyAlgorithm::AES_256_KEY);
+
+  D2DConnectionContextV1 initiator =
+      D2DConnectionContextV1(encodeKey, decodeKey, 0, 1);
+
+  std::unique_ptr<std::string> initiatorSavedSessionState =
+      initiator.SaveSession();
+
+  // Try to rebuild the context with a shorter session state.
+  std::string shortSessionState = initiatorSavedSessionState->substr(
+      0, initiatorSavedSessionState->size() - 1);
+
+  D2DConnectionContextV1Peer restoredInitiator =
+      D2DConnectionContextV1Peer(shortSessionState);
+
+  // nullptr is returned on error. It should not crash.
+  EXPECT_EQ(restoredInitiator.GetContext(), nullptr);
+}
+
+}  // namespace securegcm
diff --git a/src/main/cpp/test/securegcm/d2d_crypto_ops_test.cc b/src/main/cpp/test/securegcm/d2d_crypto_ops_test.cc
new file mode 100644
index 0000000..5acbb89
--- /dev/null
+++ b/src/main/cpp/test/securegcm/d2d_crypto_ops_test.cc
@@ -0,0 +1,158 @@
+// Copyright 2020 Google LLC
+//
+// 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
+//
+//     https://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.
+
+#include "securegcm/d2d_crypto_ops.h"
+
+#include "gtest/gtest.h"
+#include "securemessage/crypto_ops.h"
+#include "securemessage/secure_message_builder.h"
+
+namespace securegcm {
+
+using securemessage::CryptoOps;
+using securemessage::HeaderAndBody;
+using securemessage::SecureMessage;
+
+namespace {
+
+const char kPayloadData[] = "Test payload";
+const char kSecretKeyData[] = "secret key must be 32 bytes long";
+const char kOtherSecretKeyData[] = "other secret key****************";
+const char kInvalidSigncryptedMessage[] = "Not a protobuf";
+const int kSupportedProtocolVersion = 1;
+const int kUnsupportedProtocolVersion = 2;
+
+}  // namespace
+
+class D2DCryptoOpsTest : public testing::Test {
+ public:
+  D2DCryptoOpsTest()
+      : payload_(DEVICE_TO_DEVICE_MESSAGE, kPayloadData),
+        secret_key_(kSecretKeyData, CryptoOps::AES_256_KEY) {}
+
+ protected:
+  const D2DCryptoOps::Payload payload_;
+  const CryptoOps::SecretKey secret_key_;
+};
+
+TEST_F(D2DCryptoOpsTest, Signcrypt_EmptyPayload) {
+  // Signcrypting an empty payload should fail.
+  D2DCryptoOps::Payload empty_payload(DEVICE_TO_DEVICE_MESSAGE, string());
+  EXPECT_FALSE(D2DCryptoOps::SigncryptPayload(empty_payload, secret_key_));
+}
+
+TEST_F(D2DCryptoOpsTest, VerifyDecrypt_InvalidMessage) {
+  // VerifyDecrypting an invalid payload should fail.
+  EXPECT_FALSE(D2DCryptoOps::VerifyDecryptPayload(kInvalidSigncryptedMessage,
+                                                  secret_key_));
+}
+
+TEST_F(D2DCryptoOpsTest, VerifyDecrypt_NoPublicMetadata) {
+  std::unique_ptr<string> signcrypted_message =
+      D2DCryptoOps::SigncryptPayload(payload_, secret_key_);
+
+  // Clear metadata field in header.
+  SecureMessage secure_message;
+  ASSERT_TRUE(secure_message.ParseFromString(*signcrypted_message));
+  HeaderAndBody header_and_body;
+  ASSERT_TRUE(
+      header_and_body.ParseFromString(secure_message.header_and_body()));
+  header_and_body.mutable_header()->clear_public_metadata();
+  secure_message.set_header_and_body(header_and_body.SerializeAsString());
+
+  // Decrypting the message should now fail.
+  EXPECT_FALSE(D2DCryptoOps::VerifyDecryptPayload(
+      secure_message.SerializeAsString(), secret_key_));
+}
+
+TEST_F(D2DCryptoOpsTest, VerifyDecrypt_ProtocolVersionNotSupported) {
+  std::unique_ptr<string> signcrypted_message =
+      D2DCryptoOps::SigncryptPayload(payload_, secret_key_);
+
+  // Change version in metadata field in header to an unsupported version.
+  SecureMessage secure_message;
+  ASSERT_TRUE(secure_message.ParseFromString(*signcrypted_message));
+  HeaderAndBody header_and_body;
+  ASSERT_TRUE(
+      header_and_body.ParseFromString(secure_message.header_and_body()));
+  GcmMetadata metadata;
+  ASSERT_TRUE(
+      metadata.ParseFromString(header_and_body.header().public_metadata()));
+  EXPECT_EQ(kSupportedProtocolVersion, metadata.version());
+  metadata.set_version(kUnsupportedProtocolVersion);
+  header_and_body.mutable_header()->set_public_metadata(
+      metadata.SerializeAsString());
+  secure_message.set_header_and_body(header_and_body.SerializeAsString());
+
+  // Decrypting the message should now fail.
+  EXPECT_FALSE(D2DCryptoOps::VerifyDecryptPayload(
+      secure_message.SerializeAsString(), secret_key_));
+}
+
+TEST_F(D2DCryptoOpsTest, SigncryptThenVerifyDecrypt_SuccessWithSameKey) {
+  // Signcrypt the payload.
+  std::unique_ptr<string> signcrypted_message =
+      D2DCryptoOps::SigncryptPayload(payload_, secret_key_);
+  ASSERT_TRUE(signcrypted_message);
+
+  // Decrypt the signcrypted message.
+  std::unique_ptr<D2DCryptoOps::Payload> decrypted_payload =
+      D2DCryptoOps::VerifyDecryptPayload(*signcrypted_message, secret_key_);
+  ASSERT_TRUE(decrypted_payload);
+
+  // Check that decrypted payload is the same.
+  EXPECT_EQ(payload_.type(), decrypted_payload->type());
+  EXPECT_EQ(payload_.message(), decrypted_payload->message());
+}
+
+TEST_F(D2DCryptoOpsTest, SigncryptThenVerifyDecrypt_FailsWithDifferentKey) {
+  CryptoOps::SecretKey other_secret_key(kOtherSecretKeyData,
+                                        CryptoOps::AES_256_KEY);
+
+  // Signcrypt the payload with first secret key.
+  std::unique_ptr<string> signcrypted_message =
+      D2DCryptoOps::SigncryptPayload(payload_, secret_key_);
+  ASSERT_TRUE(signcrypted_message);
+
+  // Decrypting the signcrypted message with the other secret key should fail.
+  EXPECT_FALSE(D2DCryptoOps::VerifyDecryptPayload(*signcrypted_message,
+                                                  other_secret_key));
+}
+
+TEST_F(D2DCryptoOpsTest, DeriveNewKeyForPurpose_ClientServer) {
+  CryptoOps::SecretKey master_key(kSecretKeyData, CryptoOps::AES_256_KEY);
+
+  std::unique_ptr<CryptoOps::SecretKey> derived_key1 =
+      D2DCryptoOps::DeriveNewKeyForPurpose(master_key, "client");
+  std::unique_ptr<CryptoOps::SecretKey> derived_key2 =
+      D2DCryptoOps::DeriveNewKeyForPurpose(master_key, "server");
+
+  ASSERT_TRUE(derived_key1);
+  ASSERT_TRUE(derived_key2);
+  EXPECT_EQ(CryptoOps::AES_256_KEY, derived_key1->algorithm());
+  EXPECT_EQ(CryptoOps::AES_256_KEY, derived_key2->algorithm());
+  EXPECT_NE(derived_key1->data().String(), derived_key2->data().String());
+}
+
+TEST_F(D2DCryptoOpsTest, DeriveNewKeyForPurpose_InvalidMasterKeySize) {
+  CryptoOps::SecretKey master_key("Invalid Size", CryptoOps::AES_256_KEY);
+  EXPECT_FALSE(D2DCryptoOps::DeriveNewKeyForPurpose(master_key, "purpose"));
+}
+
+TEST_F(D2DCryptoOpsTest, DeriveNewKeyForPurpose_PurposeEmpty) {
+  CryptoOps::SecretKey master_key(kSecretKeyData, CryptoOps::AES_256_KEY);
+  EXPECT_FALSE(D2DCryptoOps::DeriveNewKeyForPurpose(master_key, string()));
+}
+
+}  // namespace securegcm
diff --git a/src/main/cpp/test/securegcm/java_util_test.cc b/src/main/cpp/test/securegcm/java_util_test.cc
new file mode 100644
index 0000000..20928fa
--- /dev/null
+++ b/src/main/cpp/test/securegcm/java_util_test.cc
@@ -0,0 +1,84 @@
+// Copyright 2020 Google LLC
+//
+// 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
+//
+//     https://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.
+
+#include "securegcm/java_util.h"
+
+#include <limits>
+
+#include "gtest/gtest.h"
+
+namespace securegcm {
+
+using securemessage::ByteBuffer;
+
+namespace {
+
+int32_t kMinInt32 = std::numeric_limits<int32_t>::min();
+int32_t kMaxInt32 = std::numeric_limits<int32_t>::max();
+
+}  // namespace
+
+
+TEST(JavaUtilTest, TestJavaAdd_InRange) {
+  EXPECT_EQ(2, java_util::JavaAdd(1, 1));
+}
+
+TEST(JavaUtilTest, TestJavaAdd_Underflow) {
+  EXPECT_EQ(kMaxInt32, java_util::JavaAdd(kMinInt32, -1));
+  EXPECT_EQ(kMaxInt32 - 1, java_util::JavaAdd(kMinInt32, -2));
+  EXPECT_EQ(1, java_util::JavaAdd(kMinInt32, -kMaxInt32));
+}
+
+TEST(JavaUtilTest, TestJavaAdd_Overflow) {
+  EXPECT_EQ(kMinInt32, java_util::JavaAdd(kMaxInt32, 1));
+  EXPECT_EQ(kMinInt32 + 1, java_util::JavaAdd(kMaxInt32, 2));
+  EXPECT_EQ(-2, java_util::JavaAdd(kMaxInt32, kMaxInt32));
+}
+
+TEST(JavaUtilTest, TestJavaMultiply_InRange) {
+  EXPECT_EQ(4, java_util::JavaAdd(2, 2));
+}
+
+TEST(JavaUtilTest, TestJavaMultiply_Underflow) {
+  EXPECT_EQ(0, java_util::JavaMultiply(kMinInt32, 2));
+  EXPECT_EQ(-(kMinInt32 / 2), java_util::JavaMultiply(kMinInt32 / 2, 3));
+  EXPECT_EQ(kMinInt32, java_util::JavaMultiply(kMinInt32, kMaxInt32));
+}
+
+TEST(JavaUtilTest, TestJavaMultiply_Overflow) {
+  EXPECT_EQ(-2, java_util::JavaMultiply(kMaxInt32, 2));
+  EXPECT_EQ(kMaxInt32 - 2, java_util::JavaMultiply(kMaxInt32, 3));
+  EXPECT_EQ(1, java_util::JavaMultiply(kMaxInt32, kMaxInt32));
+}
+
+TEST(JavaUtilTest, TestJavaHashCode_EmptyBytes) {
+  EXPECT_EQ(1, java_util::JavaHashCode(ByteBuffer()));
+}
+
+TEST(JavaUtilTest, TestJavaHashCode_LongByteArray) {
+  const uint8_t kBytes[] = {
+      0x93, 0x75, 0xE1, 0x2E, 0x26, 0x28, 0x54, 0x8C, 0xD9, 0x5C, 0x48, 0x7A,
+      0x07, 0x53, 0x4E, 0xED, 0x28, 0x52, 0x5D, 0x41, 0xE3, 0x18, 0x84, 0x84,
+      0x5F, 0xF6, 0x89, 0x98, 0x25, 0x1E, 0xD9, 0x6C, 0x85, 0xF3, 0x5A, 0x83,
+      0x39, 0x37, 0x4E, 0x77, 0x95, 0xB5, 0x58, 0x7C, 0xD2, 0x55, 0xA0, 0x86,
+      0x13, 0x3F, 0xBF, 0x85, 0xD3, 0xE0, 0x28, 0x90, 0x17, 0x3D, 0x2E, 0xD4,
+      0x4D, 0x95, 0x9C, 0xAE, 0xAD, 0x8A, 0x05, 0x91, 0x5D, 0xC6, 0x4B, 0x09,
+      0xB2, 0xD9, 0x34, 0x64, 0x07, 0x7B, 0x07, 0x8C, 0xA6, 0xC7, 0x1C, 0x10,
+      0x34, 0xD4, 0x30, 0x80, 0x03, 0x4F, 0x2C, 0x70};
+  const int32_t kExpectedHashCode = 1983685004;
+  EXPECT_EQ(kExpectedHashCode,
+            java_util::JavaHashCode(ByteBuffer(kBytes, sizeof(kBytes))));
+}
+
+}  // namespace securegcm
diff --git a/src/main/javatest/com/google/security/cryptauth/lib/securegcm/D2DConnectionContextTest.java b/src/main/javatest/com/google/security/cryptauth/lib/securegcm/D2DConnectionContextTest.java
new file mode 100644
index 0000000..e671e8c
--- /dev/null
+++ b/src/main/javatest/com/google/security/cryptauth/lib/securegcm/D2DConnectionContextTest.java
@@ -0,0 +1,568 @@
+// Copyright 2020 Google LLC
+//
+// 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
+//
+//     https://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.google.security.cryptauth.lib.securegcm;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.security.SignatureException;
+import java.util.Arrays;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Base class for Android compatible tests for {@link D2DConnectionContext} subclasses.
+ * Note: We would use a Parameterized test runner to test different versions, but this
+ * functionality is not supported by Android tests.
+ */
+@RunWith(JUnit4.class)
+public class D2DConnectionContextTest {
+  private static final String PING = "ping";
+  private static final String PONG = "pong";
+
+  // Key is: "initiator_encode_key_for_aes_256"
+  private static final SecretKey INITIATOR_ENCODE_KEY = new SecretKeySpec(
+      new byte[] {
+          (byte) 0x69, (byte) 0x6e, (byte) 0x69, (byte) 0x74, (byte) 0x69, (byte) 0x61, (byte) 0x74,
+          (byte) 0x6f, (byte) 0x72, (byte) 0x5f, (byte) 0x65, (byte) 0x6e, (byte) 0x63, (byte) 0x6f,
+          (byte) 0x64, (byte) 0x65, (byte) 0x5f, (byte) 0x6b, (byte) 0x65, (byte) 0x79, (byte) 0x5f,
+          (byte) 0x66, (byte) 0x6f, (byte) 0x72, (byte) 0x5f, (byte) 0x61, (byte) 0x65, (byte) 0x73,
+          (byte) 0x5f, (byte) 0x32, (byte) 0x35, (byte) 0x36
+      },
+      "AES");
+
+  // Key is: "initiator_decode_key_for_aes_256"
+  private static final SecretKey INITIATOR_DECODE_KEY = new SecretKeySpec(
+      new byte[] {
+          (byte) 0x69, (byte) 0x6e, (byte) 0x69, (byte) 0x74, (byte) 0x69, (byte) 0x61, (byte) 0x74,
+          (byte) 0x6f, (byte) 0x72, (byte) 0x5f, (byte) 0x64, (byte) 0x65, (byte) 0x63, (byte) 0x6f,
+          (byte) 0x64, (byte) 0x65, (byte) 0x5f, (byte) 0x6b, (byte) 0x65, (byte) 0x79, (byte) 0x5f,
+          (byte) 0x66, (byte) 0x6f, (byte) 0x72, (byte) 0x5f, (byte) 0x61, (byte) 0x65, (byte) 0x73,
+          (byte) 0x5f, (byte) 0x32, (byte) 0x35, (byte) 0x36
+      },
+      "AES");
+
+  private D2DConnectionContext initiatorCtx;
+  private D2DConnectionContext responderCtx;
+
+  @Before
+  public void setUp() throws Exception {
+    KeyEncodingTest.installSunEcSecurityProviderIfNecessary();
+  }
+
+  protected void testPeerToPeerProtocol(int protocolVersion) throws Exception {
+
+    if (KeyEncoding.isLegacyCryptoRequired()) {
+      // this means we're running on an old SDK, which doesn't support the
+      // necessary crypto. Let's not test anything in this case.
+      return;
+    }
+
+    initiatorCtx = createConnectionContext(protocolVersion, true /** isInitiator */);
+    responderCtx = createConnectionContext(protocolVersion, false /** isInitiator */);
+
+    byte[] pingMessage = initiatorCtx.encodeMessageToPeer(PING);
+    // (send message to responder)
+
+    // responder
+    String messageStr = responderCtx.decodeMessageFromPeerAsString(pingMessage);
+    assertEquals(PING, messageStr);
+
+    byte[] pongMessage = responderCtx.encodeMessageToPeer(PONG);
+    // (send message to initiator)
+
+    // initiator
+    messageStr = initiatorCtx.decodeMessageFromPeerAsString(pongMessage);
+    assertEquals(PONG, messageStr);
+
+    // let's make sure there is actually some crypto involved.
+    pingMessage = initiatorCtx.encodeMessageToPeer("can you see this?");
+    pingMessage[2] = (byte) (pingMessage[2] + 1); // twiddle with the message
+    try {
+      responderCtx.decodeMessageFromPeerAsString(pingMessage);
+      fail("expected exception, but didn't get it");
+    } catch (SignatureException expected) {
+      assertTrue(expected.getMessage().contains("failed verification"));
+    }
+
+    // Try and replay the previous encoded message to the initiator (replays should not work).
+    try {
+      initiatorCtx.decodeMessageFromPeerAsString(pongMessage);
+      fail("expected exception, but didn't get it");
+    } catch (SignatureException expected) {
+      assertTrue(expected.getMessage().contains("sequence"));
+    }
+
+    assertEquals(protocolVersion, initiatorCtx.getProtocolVersion());
+    assertEquals(protocolVersion, responderCtx.getProtocolVersion());
+  }
+
+  @Test
+  public void testPeerToPeerProtocol_V0() throws Exception {
+    testPeerToPeerProtocol(D2DConnectionContextV0.PROTOCOL_VERSION);
+  }
+
+  @Test
+  public void testPeerToPeerProtocol_V1() throws Exception {
+    testPeerToPeerProtocol(D2DConnectionContextV1.PROTOCOL_VERSION);
+  }
+
+  protected void testResponderSendsFirst(int protocolVersion) throws Exception {
+
+    if (KeyEncoding.isLegacyCryptoRequired()) {
+      // this means we're running on an old SDK, which doesn't support the
+      // necessary crypto. Let's not test anything in this case.
+      return;
+    }
+
+    initiatorCtx = createConnectionContext(protocolVersion, true /** isInitiator */);
+    responderCtx = createConnectionContext(protocolVersion, false /** isInitiator */);
+
+    byte[] pongMessage = responderCtx.encodeMessageToPeer(PONG);
+    assertEquals(PONG, initiatorCtx.decodeMessageFromPeerAsString(pongMessage));
+
+    pongMessage = responderCtx.encodeMessageToPeer(PONG);
+    assertEquals(PONG, initiatorCtx.decodeMessageFromPeerAsString(pongMessage));
+
+    // for good measure, if the initiator now responds, it should also work:
+    byte[] pingMessage = initiatorCtx.encodeMessageToPeer(PING);
+    assertEquals(PING, responderCtx.decodeMessageFromPeerAsString(pingMessage));
+
+    pingMessage = initiatorCtx.encodeMessageToPeer(PING);
+    assertEquals(PING, responderCtx.decodeMessageFromPeerAsString(pingMessage));
+
+    pingMessage = initiatorCtx.encodeMessageToPeer(PING);
+    assertEquals(PING, responderCtx.decodeMessageFromPeerAsString(pingMessage));
+  }
+
+  @Test
+  public void testResponderSendsFirst_V0() throws Exception {
+    testResponderSendsFirst(D2DConnectionContextV0.PROTOCOL_VERSION);
+  }
+
+  @Test
+  public void testResponderSendsFirst_V1() throws Exception {
+    testResponderSendsFirst(D2DConnectionContextV1.PROTOCOL_VERSION);
+  }
+
+  protected void testAssymmetricFlows(int protocolVersion) throws Exception {
+
+    if (KeyEncoding.isLegacyCryptoRequired()) {
+      // this means we're running on an old SDK, which doesn't support the
+      // necessary crypto. Let's not test anything in this case.
+      return;
+    }
+
+    initiatorCtx = createConnectionContext(protocolVersion, true /** isInitiator */);
+    responderCtx = createConnectionContext(protocolVersion, false /** isInitiator */);
+
+    // Let's test that this still works if one side sends a few messages in a row.
+    byte[] pingMessage = initiatorCtx.encodeMessageToPeer(PING);
+    assertEquals(PING, responderCtx.decodeMessageFromPeerAsString(pingMessage));
+
+    pingMessage = initiatorCtx.encodeMessageToPeer(PING);
+    assertEquals(PING, responderCtx.decodeMessageFromPeerAsString(pingMessage));
+
+    pingMessage = initiatorCtx.encodeMessageToPeer(PING);
+    assertEquals(PING, responderCtx.decodeMessageFromPeerAsString(pingMessage));
+
+
+    byte[] pongMessage = responderCtx.encodeMessageToPeer(PONG);
+    assertEquals(PONG, initiatorCtx.decodeMessageFromPeerAsString(pongMessage));
+
+    pongMessage = responderCtx.encodeMessageToPeer(PONG);
+    assertEquals(PONG, initiatorCtx.decodeMessageFromPeerAsString(pongMessage));
+  }
+
+  @Test
+  public void testAssymmetricFlows_V0() throws Exception {
+    testAssymmetricFlows(D2DConnectionContextV0.PROTOCOL_VERSION);
+  }
+
+  @Test
+  public void testAssymmetricFlows_V1() throws Exception {
+    testAssymmetricFlows(D2DConnectionContextV1.PROTOCOL_VERSION);
+  }
+
+  public void testErrorWhenResponderResendsMessage(int protocolVersion) throws Exception {
+    if (KeyEncoding.isLegacyCryptoRequired()) {
+      // this means we're running on an old SDK, which doesn't support the
+      // necessary crypto. Let's not test anything in this case.
+      return;
+    }
+
+    initiatorCtx = createConnectionContext(protocolVersion, true /** isInitiator */);
+    responderCtx = createConnectionContext(protocolVersion, false /** isInitiator */);
+
+    byte[] pongMessage = responderCtx.encodeMessageToPeer(PONG);
+    assertEquals(PONG, initiatorCtx.decodeMessageFromPeerAsString(pongMessage));
+
+    try {
+      // send pongMessage again to the initiator
+      initiatorCtx.decodeMessageFromPeerAsString(pongMessage);
+      fail("expected exception, but didn't get it");
+    } catch (SignatureException expected) {
+      assertTrue(expected.getMessage().contains("sequence"));
+    }
+  }
+
+  @Test
+  public void testErrorWhenResponderResendsMessage_V0() throws Exception {
+    testErrorWhenResponderResendsMessage(D2DConnectionContextV0.PROTOCOL_VERSION);
+  }
+
+  @Test
+  public void testErrorWhenResponderResendsMessage_V1() throws Exception {
+    testErrorWhenResponderResendsMessage(D2DConnectionContextV1.PROTOCOL_VERSION);
+  }
+
+  protected void testErrorWhenResponderEchoesInitiatorMessage(
+      int protocolVersion) throws Exception {
+    if (KeyEncoding.isLegacyCryptoRequired()) {
+      return;
+    }
+
+    initiatorCtx = createConnectionContext(protocolVersion, true /** isInitiator */);
+    responderCtx = createConnectionContext(protocolVersion, false /** isInitiator */);
+
+    byte[] pingMessage = initiatorCtx.encodeMessageToPeer(PING);
+    assertEquals(PING, responderCtx.decodeMessageFromPeerAsString(pingMessage));
+
+    try {
+      initiatorCtx.decodeMessageFromPeerAsString(pingMessage);
+      fail("expected exception, but didn't get it");
+    } catch (SignatureException expected) {
+    }
+  }
+
+  @Test
+  public void testErrorWhenResponderEchoesInitiatorMessage_V0() throws Exception {
+    testErrorWhenResponderEchoesInitiatorMessage(D2DConnectionContextV0.PROTOCOL_VERSION);
+  }
+
+  @Test
+  public void testErrorWhenResponderEchoesInitiatorMessage_V1() throws Exception {
+    testErrorWhenResponderEchoesInitiatorMessage(D2DConnectionContextV1.PROTOCOL_VERSION);
+  }
+
+  @Test
+  public void testErrorUsingV1InitiatorWithV0Responder() throws SignatureException {
+    if (KeyEncoding.isLegacyCryptoRequired()) {
+      // this means we're running on an old SDK, which doesn't support the
+      // necessary crypto. Let's not test anything in this case.
+      return;
+    }
+
+    initiatorCtx = new D2DConnectionContextV1(INITIATOR_ENCODE_KEY, INITIATOR_DECODE_KEY, 1, 1);
+    responderCtx = new D2DConnectionContextV0(INITIATOR_DECODE_KEY, 1);
+
+    // Decoding the responder's message should succeed, because the decode key and sequence numbers
+    // match.
+    initiatorCtx.decodeMessageFromPeer(responderCtx.encodeMessageToPeer(PING));
+
+    // Responder fails to decodes initiator's encoded message because keys do not match.
+    try {
+      responderCtx.decodeMessageFromPeer(initiatorCtx.encodeMessageToPeer(PONG));
+      fail("Expected verification to fail.");
+    } catch (SignatureException e) {
+      // Exception expected.
+    }
+  }
+
+  @Test
+  public void testErrorWithV0InitiatorV1Responder() throws SignatureException {
+    if (KeyEncoding.isLegacyCryptoRequired()) {
+      // this means we're running on an old SDK, which doesn't support the
+      // necessary crypto. Let's not test anything in this case.
+      return;
+    }
+
+    initiatorCtx = new D2DConnectionContextV0(INITIATOR_ENCODE_KEY, 1);
+    responderCtx = new D2DConnectionContextV1(INITIATOR_DECODE_KEY, INITIATOR_ENCODE_KEY, 1, 1);
+
+    // Decoding the initiator's message should succeed, because the decode key and sequence numbers
+    // match.
+    responderCtx.decodeMessageFromPeer(initiatorCtx.encodeMessageToPeer(PING));
+
+    // Initiator fails to decodes responder's encoded message because keys do not match.
+    try {
+      initiatorCtx.decodeMessageFromPeer(responderCtx.encodeMessageToPeer(PONG));
+      fail("Expected verification to fail.");
+    } catch (SignatureException e) {
+      // Exception expected.
+    }
+  }
+
+  protected void testSessionUnique(int protocolVersion) throws Exception {
+    // Should be the same (we set them up with the same key and sequence number)
+    initiatorCtx = createConnectionContext(protocolVersion, true /** isInitiator */);
+    responderCtx = createConnectionContext(protocolVersion, false /** isInitiator */);
+    Assert.assertArrayEquals(initiatorCtx.getSessionUnique(), responderCtx.getSessionUnique());
+
+    // Change just the key (should not match)
+    SecretKey wrongKey = new SecretKeySpec("wrong".getBytes("UTF8"), "AES");
+    responderCtx = createConnectionContext(protocolVersion, false, wrongKey, wrongKey, 0, 1);
+    assertFalse(Arrays.equals(initiatorCtx.getSessionUnique(), responderCtx.getSessionUnique()));
+
+    // Change just the sequence number (should still match)
+    responderCtx = createConnectionContext(
+        protocolVersion, false, INITIATOR_ENCODE_KEY, INITIATOR_DECODE_KEY, 2, 2);
+    Assert.assertArrayEquals(initiatorCtx.getSessionUnique(), responderCtx.getSessionUnique());
+  }
+
+  @Test
+  public void testSessionUnique_V0() throws Exception {
+    testSessionUnique(D2DConnectionContextV0.PROTOCOL_VERSION);
+  }
+
+  @Test
+  public void testSessionUnique_V1() throws Exception {
+    testSessionUnique(D2DConnectionContextV1.PROTOCOL_VERSION);
+  }
+
+  @Test
+  public void testSessionUniqueValues_V0() throws Exception {
+    // The key and the session unique value should match ones in the equivalent test in
+    // @link {cs/Nearby/D2DCrypto/Tests/D2DConnectionContextTest.m}
+    byte[] key =
+        new byte[] {
+          (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04, (byte) 0x05, (byte) 0x06, (byte) 0x07,
+          (byte) 0x08, (byte) 0x09, (byte) 0x0a, (byte) 0x0b, (byte) 0x0c, (byte) 0x0d, (byte) 0x0e,
+          (byte) 0x0f, (byte) 0x10, (byte) 0x11, (byte) 0x12, (byte) 0x13, (byte) 0x14, (byte) 0x15,
+          (byte) 0x16, (byte) 0x17, (byte) 0x18, (byte) 0x19, (byte) 0x1a, (byte) 0x1b, (byte) 0x1c,
+          (byte) 0x1d, (byte) 0x1e, (byte) 0x1f, (byte) 0x20
+        };
+    byte[] sessionUnique =
+        new byte[] {
+          (byte) 0x70, (byte) 0x7a, (byte) 0x17, (byte) 0x27, (byte) 0xa3, (byte) 0x0e, (byte) 0x68,
+          (byte) 0x63, (byte) 0x38, (byte) 0xdf, (byte) 0x72, (byte) 0x62, (byte) 0xf4, (byte) 0xb0,
+          (byte) 0x41, (byte) 0xac, (byte) 0x75, (byte) 0x8b, (byte) 0xca, (byte) 0x3b, (byte) 0x11,
+          (byte) 0xd4, (byte) 0x09, (byte) 0x64, (byte) 0x96, (byte) 0x54, (byte) 0xb4, (byte) 0x9b,
+          (byte) 0x43, (byte) 0xe6, (byte) 0x9b, (byte) 0xce
+        };
+
+    SecretKey secretKey = new SecretKeySpec(key, "AES");
+    D2DConnectionContext context = new D2DConnectionContextV0(secretKey, 1);
+
+    Assert.assertArrayEquals(context.getSessionUnique(), sessionUnique);
+  }
+
+  @Test
+  public void testSessionUniqueValues_V1_Initiator() throws Exception {
+    // The key and the session unique value should match ones in the equivalent test in
+    // @link {cs/Nearby/D2DCrypto/Tests/D2DConnectionContextTest.m}
+    byte[] sessionUnique =
+        new byte[] {
+          (byte) 0x91, (byte) 0xc7, (byte) 0xc9, (byte) 0x26, (byte) 0x2c, (byte) 0x17, (byte) 0x8a,
+          (byte) 0xa0, (byte) 0x36, (byte) 0x9f, (byte) 0xf2, (byte) 0x05, (byte) 0x20, (byte) 0x98,
+          (byte) 0x38, (byte) 0x53, (byte) 0xa5, (byte) 0x46, (byte) 0xab, (byte) 0x3a, (byte) 0x21,
+          (byte) 0x3b, (byte) 0x76, (byte) 0x58, (byte) 0x59, (byte) 0x4e, (byte) 0xe7, (byte) 0xe3,
+          (byte) 0xc1, (byte) 0x69, (byte) 0x87, (byte) 0xfa
+        };
+
+    D2DConnectionContext initiatorContext = new D2DConnectionContextV1(
+        INITIATOR_ENCODE_KEY, INITIATOR_DECODE_KEY, 0, 1);
+    D2DConnectionContext responderContext = new D2DConnectionContextV1(
+        INITIATOR_DECODE_KEY, INITIATOR_ENCODE_KEY, 1, 0);
+
+    // Both the initiator and responder must be the same.
+    Assert.assertArrayEquals(initiatorContext.getSessionUnique(), sessionUnique);
+    Assert.assertArrayEquals(responderContext.getSessionUnique(), sessionUnique);
+  }
+
+  @Test
+  public void testSaveSessionV0() throws Exception {
+    D2DConnectionContext initiatorCtx = new D2DConnectionContextV0(INITIATOR_ENCODE_KEY, 1);
+    D2DConnectionContext responderCtx = new D2DConnectionContextV0(INITIATOR_ENCODE_KEY, 1);
+
+    // Save the state
+    byte[] initiatorSavedSessionState = initiatorCtx.saveSession();
+    byte[] responderSavedSessionState = responderCtx.saveSession();
+
+    // Try to rebuild the context
+    initiatorCtx = D2DConnectionContext.fromSavedSession(initiatorSavedSessionState);
+    responderCtx = D2DConnectionContext.fromSavedSession(responderSavedSessionState);
+
+    // Sanity check
+    assertEquals(1, initiatorCtx.getSequenceNumberForDecoding());
+    assertEquals(1, responderCtx.getSequenceNumberForDecoding());
+    Assert.assertArrayEquals(initiatorCtx.getSessionUnique(), responderCtx.getSessionUnique());
+
+    // Make sure they can still talk to one another
+    assertEquals(PING,
+        responderCtx.decodeMessageFromPeerAsString(initiatorCtx.encodeMessageToPeer(PING)));
+    assertEquals(PONG,
+        initiatorCtx.decodeMessageFromPeerAsString(responderCtx.encodeMessageToPeer(PONG)));
+  }
+
+  @Test
+  public void testSaveSessionV0_negativeSeqNumber() throws Exception {
+    D2DConnectionContext initiatorCtx = new D2DConnectionContextV0(INITIATOR_ENCODE_KEY, -5);
+
+    // Save the state
+    byte[] initiatorSavedSessionState = initiatorCtx.saveSession();
+
+    // Try to rebuild the context
+    initiatorCtx = D2DConnectionContext.fromSavedSession(initiatorSavedSessionState);
+
+    // Sanity check
+    assertEquals(-5, initiatorCtx.getSequenceNumberForDecoding());
+  }
+
+  @Test
+  public void testSaveSessionV0_shortKey() throws Exception {
+    D2DConnectionContext initiatorCtx = new D2DConnectionContextV0(INITIATOR_ENCODE_KEY, -5);
+
+    // Save the state
+    byte[] initiatorSavedSessionState = initiatorCtx.saveSession();
+
+    // Try to rebuild the context
+    try {
+      D2DConnectionContext.fromSavedSession(Arrays.copyOf(initiatorSavedSessionState,
+          initiatorSavedSessionState.length - 1));
+      fail("Expected failure as key is too short");
+    } catch (IllegalArgumentException e) {
+      // expected
+    }
+  }
+
+  @Test
+  public void testSaveSession_unknownProtocolVersion() throws Exception {
+    D2DConnectionContext initiatorCtx = new D2DConnectionContextV0(INITIATOR_ENCODE_KEY, -5);
+
+    // Save the state
+    byte[] initiatorSavedSessionState = initiatorCtx.saveSession();
+
+    // Mess with the protocol version
+    initiatorSavedSessionState[0] = (byte) 0xff;
+
+    // Try to rebuild the context
+    try {
+      D2DConnectionContext.fromSavedSession(initiatorSavedSessionState);
+      fail("Expected failure as 0xff is not a valid protocol version");
+    } catch (IllegalArgumentException e) {
+      // expected
+    }
+
+    // Mess with the protocol version in the other direction
+    initiatorSavedSessionState[0] = 2;
+
+    // Try to rebuild the context
+    try {
+      D2DConnectionContext.fromSavedSession(initiatorSavedSessionState);
+      fail("Expected failure as 2 is not a valid protocol version");
+    } catch (IllegalArgumentException e) {
+      // expected
+    }
+  }
+
+  @Test
+  public void testSaveSessionV1() throws Exception {
+    D2DConnectionContext initiatorCtx = new D2DConnectionContextV1(INITIATOR_ENCODE_KEY,
+        INITIATOR_DECODE_KEY, 0, 1);
+    D2DConnectionContext responderCtx = new D2DConnectionContextV1(INITIATOR_DECODE_KEY,
+        INITIATOR_ENCODE_KEY, 1, 0);
+
+    // Save the state
+    byte[] initiatorSavedSessionState = initiatorCtx.saveSession();
+    byte[] responderSavedSessionState = responderCtx.saveSession();
+
+    // Try to rebuild the context
+    initiatorCtx = D2DConnectionContext.fromSavedSession(initiatorSavedSessionState);
+    responderCtx = D2DConnectionContext.fromSavedSession(responderSavedSessionState);
+
+    // Sanity check
+    assertEquals(1, initiatorCtx.getSequenceNumberForDecoding());
+    assertEquals(0, initiatorCtx.getSequenceNumberForEncoding());
+    assertEquals(0, responderCtx.getSequenceNumberForDecoding());
+    assertEquals(1, responderCtx.getSequenceNumberForEncoding());
+    Assert.assertArrayEquals(initiatorCtx.getSessionUnique(), responderCtx.getSessionUnique());
+
+    // Make sure they can still talk to one another
+    assertEquals(PING,
+        responderCtx.decodeMessageFromPeerAsString(initiatorCtx.encodeMessageToPeer(PING)));
+    assertEquals(PONG,
+        initiatorCtx.decodeMessageFromPeerAsString(responderCtx.encodeMessageToPeer(PONG)));
+  }
+
+  @Test
+  public void testSaveSessionV1_negativeSeqNumbers() throws Exception {
+    D2DConnectionContext initiatorCtx = new D2DConnectionContextV1(INITIATOR_ENCODE_KEY,
+        INITIATOR_DECODE_KEY, -8, -10);
+
+    // Save the state
+    byte[] initiatorSavedSessionState = initiatorCtx.saveSession();
+
+    // Try to rebuild the context
+    initiatorCtx = D2DConnectionContext.fromSavedSession(initiatorSavedSessionState);
+
+    // Sanity check
+    assertEquals(-10, initiatorCtx.getSequenceNumberForDecoding());
+    assertEquals(-8, initiatorCtx.getSequenceNumberForEncoding());
+  }
+
+  @Test
+  public void testSaveSessionV1_tooShort() throws Exception {
+    D2DConnectionContext initiatorCtx = new D2DConnectionContextV1(INITIATOR_ENCODE_KEY,
+        INITIATOR_DECODE_KEY, -8, -10);
+
+    // Save the state
+    byte[] initiatorSavedSessionState = initiatorCtx.saveSession();
+
+    // Try to rebuild the context
+    try {
+      D2DConnectionContext.fromSavedSession(
+          Arrays.copyOf(initiatorSavedSessionState, initiatorSavedSessionState.length - 1));
+      fail("Expected error as saved session is too short");
+    } catch (IllegalArgumentException e) {
+      // expected
+    }
+
+    // Sanity check
+    assertEquals(-10, initiatorCtx.getSequenceNumberForDecoding());
+    assertEquals(-8, initiatorCtx.getSequenceNumberForEncoding());
+  }
+
+  D2DConnectionContext createConnectionContext(int protocolVersion, boolean isInitiator) {
+    return createConnectionContext(
+        protocolVersion, isInitiator, INITIATOR_ENCODE_KEY, INITIATOR_DECODE_KEY, 0, 1);
+  }
+
+  D2DConnectionContext createConnectionContext(
+      int protocolVersion, boolean isInitiator,
+      SecretKey initiatorEncodeKey, SecretKey initiatorDecodeKey,
+      int initiatorSequenceNumber, int responderSequenceNumber) {
+    if (protocolVersion == D2DConnectionContextV0.PROTOCOL_VERSION) {
+      return new D2DConnectionContextV0(initiatorEncodeKey, responderSequenceNumber);
+    } else if (protocolVersion == D2DConnectionContextV1.PROTOCOL_VERSION) {
+      return isInitiator
+          ? new D2DConnectionContextV1(
+              initiatorEncodeKey, initiatorDecodeKey,
+              initiatorSequenceNumber, responderSequenceNumber)
+          : new D2DConnectionContextV1(
+              initiatorDecodeKey, initiatorEncodeKey,
+              responderSequenceNumber, initiatorSequenceNumber);
+    } else {
+      throw new IllegalArgumentException("Unknown version: " + protocolVersion);
+    }
+  }
+}
diff --git a/src/main/javatest/com/google/security/cryptauth/lib/securegcm/D2DDiffieHellmanKeyExchangeHandshakeTest.java b/src/main/javatest/com/google/security/cryptauth/lib/securegcm/D2DDiffieHellmanKeyExchangeHandshakeTest.java
new file mode 100644
index 0000000..4de794a
--- /dev/null
+++ b/src/main/javatest/com/google/security/cryptauth/lib/securegcm/D2DDiffieHellmanKeyExchangeHandshakeTest.java
@@ -0,0 +1,432 @@
+// Copyright 2020 Google LLC
+//
+// 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
+//
+//     https://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.google.security.cryptauth.lib.securegcm;
+
+import com.google.security.cryptauth.lib.securegcm.DeviceToDeviceMessagesProto.InitiatorHello;
+import com.google.security.cryptauth.lib.securegcm.DeviceToDeviceMessagesProto.ResponderHello;
+import com.google.security.cryptauth.lib.securegcm.TransportCryptoOps.Payload;
+import com.google.security.cryptauth.lib.securegcm.TransportCryptoOps.PayloadType;
+import com.google.security.cryptauth.lib.securemessage.PublicKeyProtoUtil;
+import java.nio.charset.Charset;
+import java.security.KeyPair;
+import java.security.PublicKey;
+import java.security.SignatureException;
+import javax.crypto.SecretKey;
+import junit.framework.TestCase;
+import org.junit.Assert;
+
+/**
+ * Android compatible tests for the {@link D2DDiffieHellmanKeyExchangeHandshake} class.
+ */
+public class D2DDiffieHellmanKeyExchangeHandshakeTest extends TestCase {
+
+  private static final byte[] RESPONDER_HELLO_MESSAGE =
+      "first payload".getBytes(Charset.forName("UTF-8"));
+
+  private static final String PING = "ping";
+
+  @Override
+  protected void setUp() throws Exception {
+    KeyEncodingTest.installSunEcSecurityProviderIfNecessary();
+    super.setUp();
+  }
+
+  public void testHandshakeWithPayload() throws Exception {
+
+    if (KeyEncoding.isLegacyCryptoRequired()) {
+      // this means we're running on an old SDK, which doesn't support the
+      // necessary crypto. Let's not test anything in this case.
+      return;
+    }
+
+    // initiator:
+    D2DHandshakeContext initiatorHandshakeContext =
+        D2DDiffieHellmanKeyExchangeHandshake.forInitiator();
+    assertFalse(initiatorHandshakeContext.canSendPayloadInHandshakeMessage());
+    assertFalse(initiatorHandshakeContext.isHandshakeComplete());
+    byte[] initiatorHello = initiatorHandshakeContext.getNextHandshakeMessage();
+    assertFalse(initiatorHandshakeContext.isHandshakeComplete());
+    // (send initiatorHello to responder)
+
+    // responder:
+    D2DHandshakeContext responderHandshakeContext =
+        D2DDiffieHellmanKeyExchangeHandshake.forResponder();
+    byte[] payload = responderHandshakeContext.parseHandshakeMessage(initiatorHello);
+    assertEquals(0, payload.length);
+    assertTrue(responderHandshakeContext.canSendPayloadInHandshakeMessage());
+    assertFalse(responderHandshakeContext.isHandshakeComplete());
+    byte[] responderHelloAndPayload = responderHandshakeContext.getNextHandshakeMessage(
+        RESPONDER_HELLO_MESSAGE);
+    assertTrue(responderHandshakeContext.isHandshakeComplete());
+    D2DConnectionContext responderCtx = responderHandshakeContext.toConnectionContext();
+    // (send responderHelloAndPayload to initiator)
+
+    // initiator
+    byte[] messageFromPayload =
+        initiatorHandshakeContext.parseHandshakeMessage(responderHelloAndPayload);
+    Assert.assertArrayEquals(RESPONDER_HELLO_MESSAGE, messageFromPayload);
+    assertTrue(initiatorHandshakeContext.isHandshakeComplete());
+    D2DConnectionContextV1 initiatorCtx =
+        (D2DConnectionContextV1) initiatorHandshakeContext.toConnectionContext();
+
+    // Test that that initiator and responder contexts are initialized correctly.
+    checkInitializedConnectionContexts(initiatorCtx, responderCtx);
+  }
+
+  public void testHandshakeWithoutPayload() throws Exception {
+
+    if (KeyEncoding.isLegacyCryptoRequired()) {
+      // this means we're running on an old SDK, which doesn't support the
+      // necessary crypto. Let's not test anything in this case.
+      return;
+    }
+
+    // initiator:
+    D2DHandshakeContext initiatorHandshakeContext =
+        D2DDiffieHellmanKeyExchangeHandshake.forInitiator();
+    byte[] initiatorHello = initiatorHandshakeContext.getNextHandshakeMessage();
+    // (send initiatorHello to responder)
+
+    // responder:
+    D2DHandshakeContext responderHandshakeContext =
+        D2DDiffieHellmanKeyExchangeHandshake.forResponder();
+    responderHandshakeContext.parseHandshakeMessage(initiatorHello);
+    byte[] responderHelloAndPayload = responderHandshakeContext.getNextHandshakeMessage();
+    assertTrue(responderHandshakeContext.isHandshakeComplete());
+    D2DConnectionContext responderCtx = responderHandshakeContext.toConnectionContext();
+    // (send responderHelloAndPayload to initiator)
+
+    // initiator
+    byte[] messageFromPayload =
+        initiatorHandshakeContext.parseHandshakeMessage(responderHelloAndPayload);
+    assertEquals(0, messageFromPayload.length);
+    assertTrue(initiatorHandshakeContext.isHandshakeComplete());
+    D2DConnectionContext initiatorCtx = initiatorHandshakeContext.toConnectionContext();
+
+    // Test that that initiator and responder contexts are initialized correctly.
+    checkInitializedConnectionContexts(initiatorCtx, responderCtx);
+  }
+
+  public void testErrorWhenInitiatorOrResponderSendTwice() throws Exception {
+
+    if (KeyEncoding.isLegacyCryptoRequired()) {
+      // this means we're running on an old SDK, which doesn't support the
+      // necessary crypto. Let's not test anything in this case.
+      return;
+    }
+
+    // initiator:
+    D2DHandshakeContext initiatorHandshakeContext =
+        D2DDiffieHellmanKeyExchangeHandshake.forInitiator();
+    byte[] initiatorHello = initiatorHandshakeContext.getNextHandshakeMessage();
+    try {
+      initiatorHandshakeContext.getNextHandshakeMessage();
+      fail("Expected error as initiator has no more initiator messages to send");
+    } catch (HandshakeException expected) {
+      assertTrue(expected.getMessage().contains("Cannot get next message"));
+    }
+    // (send initiatorHello to responder)
+
+    // responder:
+    D2DHandshakeContext responderHandshakeContext =
+        D2DDiffieHellmanKeyExchangeHandshake.forResponder();
+    responderHandshakeContext.parseHandshakeMessage(initiatorHello);
+    responderHandshakeContext.getNextHandshakeMessage();
+    try {
+      responderHandshakeContext.getNextHandshakeMessage();
+      fail("Expected error as initiator has no more responder messages to send");
+    } catch (HandshakeException expected) {
+      assertTrue(expected.getMessage().contains("Cannot get"));
+    }
+  }
+
+  public void testInitiatorOrResponderFailOnEmptyMessage() throws Exception {
+
+    if (KeyEncoding.isLegacyCryptoRequired()) {
+      // this means we're running on an old SDK, which doesn't support the
+      // necessary crypto. Let's not test anything in this case.
+      return;
+    }
+
+    D2DHandshakeContext handshakeContext =
+        D2DDiffieHellmanKeyExchangeHandshake.forInitiator();
+    try {
+      handshakeContext.parseHandshakeMessage(null);
+      fail("Expected to crash on null message");
+    } catch (HandshakeException expected) {
+      assertTrue(expected.getMessage().contains("short"));
+    }
+    try {
+      handshakeContext.parseHandshakeMessage(new byte[0]);
+      fail("Expected to crash on empty message");
+    } catch (HandshakeException expected) {
+      assertTrue(expected.getMessage().contains("short"));
+    }
+  }
+
+  public void testPrematureConversionToConnection() throws Exception {
+    if (KeyEncoding.isLegacyCryptoRequired()) {
+      // this means we're running on an old SDK, which doesn't support the
+      // necessary crypto. Let's not test anything in this case.
+      return;
+    }
+
+    // initiator:
+    D2DHandshakeContext initiatorHandshakeContext =
+        D2DDiffieHellmanKeyExchangeHandshake.forInitiator();
+    try {
+      initiatorHandshakeContext.toConnectionContext();
+      fail("Expected to crash: initiator hasn't done anything to deserve full connection");
+    } catch (HandshakeException expected) {
+      assertTrue(expected.getMessage().contains("not complete"));
+    }
+
+    byte[] initiatorHello = initiatorHandshakeContext.getNextHandshakeMessage();
+    try {
+      initiatorHandshakeContext.toConnectionContext();
+      fail("Expected to crash: initiator hasn't yet received responder's key");
+    } catch (HandshakeException expected) {
+      assertTrue(expected.getMessage().contains("not complete"));
+    }
+    // (send initiatorHello to responder)
+
+    // responder:
+    D2DHandshakeContext responderHandshakeContext =
+        D2DDiffieHellmanKeyExchangeHandshake.forResponder();
+    responderHandshakeContext.parseHandshakeMessage(initiatorHello);
+    try {
+      initiatorHandshakeContext.toConnectionContext();
+      fail("Expected to crash: responder hasn't yet send their key");
+    } catch (HandshakeException expected) {
+      assertTrue(expected.getMessage().contains("not complete"));
+    }
+  }
+
+  public void testCannotReuseHandshakeContext() throws Exception {
+
+    if (KeyEncoding.isLegacyCryptoRequired()) {
+      // this means we're running on an old SDK, which doesn't support the
+      // necessary crypto. Let's not test anything in this case.
+      return;
+    }
+
+    // initiator:
+    D2DHandshakeContext initiatorHandshakeContext =
+        D2DDiffieHellmanKeyExchangeHandshake.forInitiator();
+    byte[] initiatorHello = initiatorHandshakeContext.getNextHandshakeMessage();
+    // (send initiatorHello to responder)
+
+    // responder:
+    D2DHandshakeContext responderHandshakeContext =
+        D2DDiffieHellmanKeyExchangeHandshake.forResponder();
+    responderHandshakeContext.parseHandshakeMessage(initiatorHello);
+    byte[] responderHelloAndPayload = responderHandshakeContext.getNextHandshakeMessage();
+    D2DConnectionContext responderCtx = responderHandshakeContext.toConnectionContext();
+    // (send responderHelloAndPayload to initiator)
+
+    // initiator
+    initiatorHandshakeContext.parseHandshakeMessage(responderHelloAndPayload);
+    D2DConnectionContext initiatorCtx = initiatorHandshakeContext.toConnectionContext();
+
+    // Test that that initiator and responder contexts are initialized correctly.
+    checkInitializedConnectionContexts(initiatorCtx, responderCtx);
+
+    // Try to get another full context
+    try {
+      initiatorHandshakeContext.toConnectionContext();
+      fail("Expected crash: initiator context has already been used");
+    } catch (HandshakeException expected) {
+      assertTrue(expected.getMessage().contains("used"));
+    }
+    try {
+      responderHandshakeContext.toConnectionContext();
+      fail("Expected crash: responder context has already been used");
+    } catch (HandshakeException expected) {
+      assertTrue(expected.getMessage().contains("used"));
+    }
+  }
+
+  public void testErrorWhenInitiatorEchosResponderHello() throws Exception {
+    if (KeyEncoding.isLegacyCryptoRequired()) {
+      // this means we're running on an old SDK, which doesn't support the
+      // necessary crypto. Let's not test anything in this case.
+      return;
+    }
+
+    // Initiator echoing back responder's first packet:
+    D2DDiffieHellmanKeyExchangeHandshake partialInitiatorContext =
+        D2DDiffieHellmanKeyExchangeHandshake.forInitiator();
+    byte[] initiatorHello = partialInitiatorContext.getNextHandshakeMessage();
+
+    D2DDiffieHellmanKeyExchangeHandshake partialResponderCtx =
+        D2DDiffieHellmanKeyExchangeHandshake.forResponder();
+    partialResponderCtx.parseHandshakeMessage(initiatorHello);
+    byte[] responderHelloAndPayload =
+        partialResponderCtx.getNextHandshakeMessage(RESPONDER_HELLO_MESSAGE);
+    D2DConnectionContext responderCtx = partialResponderCtx.toConnectionContext();
+
+    try {
+      // initiator sends responderHelloAndPayload to responder
+      responderCtx.decodeMessageFromPeerAsString(responderHelloAndPayload);
+      fail("expected exception, but didn't get it");
+    } catch (SignatureException expected) {
+      assertTrue(expected.getMessage().contains("Signature failed verification"));
+    }
+  }
+
+  public void testErrorWhenInitiatorResendsMessage() throws Exception {
+    if (KeyEncoding.isLegacyCryptoRequired()) {
+      // this means we're running on an old SDK, which doesn't support the
+      // necessary crypto. Let's not test anything in this case.
+      return;
+    }
+
+    // Initiator repeating the same packet twice
+    D2DDiffieHellmanKeyExchangeHandshake partialInitiatorContext =
+        D2DDiffieHellmanKeyExchangeHandshake.forInitiator();
+    byte[] initiatorHello = partialInitiatorContext.getNextHandshakeMessage();
+
+    D2DDiffieHellmanKeyExchangeHandshake partialResponderCtx =
+        D2DDiffieHellmanKeyExchangeHandshake.forResponder();
+    partialResponderCtx.parseHandshakeMessage(initiatorHello);
+    byte[] responderHelloAndPayload =
+        partialResponderCtx.getNextHandshakeMessage(RESPONDER_HELLO_MESSAGE);
+    D2DConnectionContext responderCtx = partialResponderCtx.toConnectionContext();
+
+    partialInitiatorContext.parseHandshakeMessage(responderHelloAndPayload);
+    D2DConnectionContext initiatorCtx = partialInitiatorContext.toConnectionContext();
+
+    byte[] pingMessage = initiatorCtx.encodeMessageToPeer(PING);
+    assertEquals(PING, responderCtx.decodeMessageFromPeerAsString(pingMessage));
+
+    try {
+      // send pingMessage to responder again
+      responderCtx.decodeMessageFromPeerAsString(pingMessage);
+      fail("expected exception, but didn't get it");
+    } catch (SignatureException expected) {
+      assertTrue(expected.getMessage().contains("sequence"));
+    }
+  }
+
+  public void testErrorWhenResponderResendsFirstMessage() throws Exception {
+    if (KeyEncoding.isLegacyCryptoRequired()) {
+      // this means we're running on an old SDK, which doesn't support the
+      // necessary crypto. Let's not test anything in this case.
+      return;
+    }
+
+    D2DDiffieHellmanKeyExchangeHandshake partialInitiatorContext =
+        D2DDiffieHellmanKeyExchangeHandshake.forInitiator();
+    byte[] initiatorHello = partialInitiatorContext.getNextHandshakeMessage();
+
+    D2DDiffieHellmanKeyExchangeHandshake partialResponderCtx =
+        D2DDiffieHellmanKeyExchangeHandshake.forResponder();
+    partialResponderCtx.parseHandshakeMessage(initiatorHello);
+    byte[] responderHelloAndPayload =
+        partialResponderCtx.getNextHandshakeMessage(RESPONDER_HELLO_MESSAGE);
+
+    partialInitiatorContext.parseHandshakeMessage(responderHelloAndPayload);
+    D2DConnectionContext initiatorCtx = partialInitiatorContext.toConnectionContext();
+
+    try {
+      // Send the responderHelloAndPayload again. This time, the initiator will
+      // process it as a normal message.
+      initiatorCtx.decodeMessageFromPeerAsString(responderHelloAndPayload);
+      fail("expected exception, but didn't get it");
+    } catch (SignatureException expected) {
+      assertTrue(expected.getMessage().contains("wrong message type"));
+    }
+  }
+
+  public void testHandshakeWithInitiatorV1AndResponderV0() throws Exception {
+    if (KeyEncoding.isLegacyCryptoRequired()) {
+      // this means we're running on an old SDK, which doesn't support the
+      // necessary crypto. Let's not test anything in this case.
+      return;
+    }
+
+    // Initialize initiator side.
+    D2DHandshakeContext initiatorHandshakeContext =
+        D2DDiffieHellmanKeyExchangeHandshake.forInitiator();
+    byte[] initiatorHello = initiatorHandshakeContext.getNextHandshakeMessage();
+
+    // Set up keys used by the responder.
+    PublicKey initiatorPublicKey = PublicKeyProtoUtil.parsePublicKey(
+        InitiatorHello.parseFrom(initiatorHello).getPublicDhKey());
+    KeyPair responderKeyPair = PublicKeyProtoUtil.generateEcP256KeyPair();
+    SecretKey sharedKey =
+        EnrollmentCryptoOps.doKeyAgreement(responderKeyPair.getPrivate(), initiatorPublicKey);
+
+    // Construct a responder hello message without the version field, whose payload is encrypted
+    // with the shared key.
+    byte[] responderHello = D2DCryptoOps.signcryptPayload(
+          new Payload(
+              PayloadType.DEVICE_TO_DEVICE_RESPONDER_HELLO_PAYLOAD,
+              D2DConnectionContext.createDeviceToDeviceMessage(new byte[] {}, 1).toByteArray()),
+          sharedKey,
+          ResponderHello.newBuilder()
+              .setPublicDhKey(
+                  PublicKeyProtoUtil.encodePublicKey(responderKeyPair.getPublic()))
+              .build().toByteArray());
+
+    // Handle V0 responder hello message.
+    initiatorHandshakeContext.parseHandshakeMessage(responderHello);
+    D2DConnectionContext initiatorCtx = initiatorHandshakeContext.toConnectionContext();
+
+    assertEquals(D2DConnectionContextV0.PROTOCOL_VERSION, initiatorCtx.getProtocolVersion());
+    assertEquals(1, initiatorCtx.getSequenceNumberForEncoding());
+    assertEquals(1, initiatorCtx.getSequenceNumberForDecoding());
+  }
+
+  public void testHandshakeWithInitiatorV0AndResponderV1() throws Exception {
+    if (KeyEncoding.isLegacyCryptoRequired()) {
+      // this means we're running on an old SDK, which doesn't support the
+      // necessary crypto. Let's not test anything in this case.
+      return;
+    }
+
+    // Construct an initiator hello message without the version field.
+    byte[] initiatorHello = InitiatorHello.newBuilder()
+        .setPublicDhKey(PublicKeyProtoUtil.encodePublicKey(
+            PublicKeyProtoUtil.generateEcP256KeyPair().getPublic()))
+        .build()
+        .toByteArray();
+
+    // Handle V0 initiator hello message.
+    D2DHandshakeContext responderHandshakeContext =
+        D2DDiffieHellmanKeyExchangeHandshake.forResponder();
+    responderHandshakeContext.parseHandshakeMessage(initiatorHello);
+    responderHandshakeContext.getNextHandshakeMessage();
+    D2DConnectionContext responderCtx = responderHandshakeContext.toConnectionContext();
+
+    assertEquals(D2DConnectionContextV0.PROTOCOL_VERSION, responderCtx.getProtocolVersion());
+    assertEquals(1, responderCtx.getSequenceNumberForEncoding());
+    assertEquals(1, responderCtx.getSequenceNumberForDecoding());
+  }
+
+  private void checkInitializedConnectionContexts(
+      D2DConnectionContext initiatorCtx, D2DConnectionContext responderCtx) {
+    assertNotNull(initiatorCtx);
+    assertNotNull(responderCtx);
+    assertEquals(D2DConnectionContextV1.PROTOCOL_VERSION, initiatorCtx.getProtocolVersion());
+    assertEquals(D2DConnectionContextV1.PROTOCOL_VERSION, responderCtx.getProtocolVersion());
+    assertEquals(initiatorCtx.getEncodeKey(), responderCtx.getDecodeKey());
+    assertEquals(initiatorCtx.getDecodeKey(), responderCtx.getEncodeKey());
+    assertEquals(0, initiatorCtx.getSequenceNumberForEncoding());
+    assertEquals(1, initiatorCtx.getSequenceNumberForDecoding());
+    assertEquals(1, responderCtx.getSequenceNumberForEncoding());
+    assertEquals(0, responderCtx.getSequenceNumberForDecoding());
+  }
+}
diff --git a/src/main/javatest/com/google/security/cryptauth/lib/securegcm/Ed25519Test.java b/src/main/javatest/com/google/security/cryptauth/lib/securegcm/Ed25519Test.java
new file mode 100644
index 0000000..6ae95d8
--- /dev/null
+++ b/src/main/javatest/com/google/security/cryptauth/lib/securegcm/Ed25519Test.java
@@ -0,0 +1,195 @@
+// Copyright 2020 Google LLC
+//
+// 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
+//
+//     https://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.google.security.cryptauth.lib.securegcm;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertThat;
+
+import com.google.security.cryptauth.lib.securegcm.Ed25519.Ed25519Exception;
+import java.math.BigInteger;
+import junit.framework.TestCase;
+
+/**
+ * Android compatible tests for the {@link Ed25519} class.
+ */
+public class Ed25519Test extends TestCase {
+
+  // Points on the curve
+  private static final int HEX_RADIX = 16;
+  private static final BigInteger[] KM = new BigInteger[] {
+    new BigInteger("1981FB43F103290ECF9772022DB8B19BFAF389057ED91E8486EB368763435925", HEX_RADIX),
+    new BigInteger("A714C34F3B588AAC92FD2587884A20964FD351A1F147D5C4BBF5C2F37A77C36", HEX_RADIX)};
+  private static final BigInteger[] KN = new BigInteger[] {
+    new BigInteger("201A184F47D9A7973891D148E3D1C864D8084547131C2C1CEFB7EEBD26C63567", HEX_RADIX),
+    new BigInteger("6DA2D3B18EC4F9AA3B08E39C997CD8BF6E9948FFD4FEFFECAF8DD0B3D648B7E8", HEX_RADIX)};
+
+  // Curve prime P
+  private static final BigInteger P =
+      new BigInteger("7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED", HEX_RADIX);
+
+  // Test vectors obtain by multiplying KM by k by manually using the official implementation
+  // see: http://ed25519.cr.yp.to/python/ed25519.py
+  // k = 2
+  private static final BigInteger[] KM_2 = new BigInteger[] {
+    new BigInteger("718079972e63c2d62caf0ee93ec6f00337ceaff4e283181c04c4082b1d5e1ecf", HEX_RADIX),
+    new BigInteger("143d18d393a8058c8614335bf36bf59364cc7c451db74726b322ce9d0b826d51", HEX_RADIX)
+  };
+  // k = 3
+  private static final BigInteger[] KM_3 = new BigInteger[] {
+    new BigInteger("39DA3C92EFC0577586B4D58F4A5C0BF65A6CC8F6BF358F38D70B2E6C28A31E8E", HEX_RADIX),
+    new BigInteger("6D194F054B3FC2BE217F6A360BBEC747D2937FCEBD74B67FC3B20ED638ADD670", HEX_RADIX)
+  };
+  // k = 317698
+  private static final BigInteger[] KM_317698 = new BigInteger[] {
+    new BigInteger("7945D0ADEB568B16495476E81ADF281F4515439AE835914FBF6CEEAFEB9CD7E8", HEX_RADIX),
+    new BigInteger("3631503DCDEBC0BF9BB1FFC3984A8CB52A34FFC2E77E9C19FD896DC6EE64A530", HEX_RADIX)
+  };
+  // k = P
+  private static final BigInteger[] KM_HUGE = new BigInteger[] {
+    new BigInteger("530162B05F440E00E219DFD3188524821C860C41FD87B9AC6AF2A283FDD585A1", HEX_RADIX),
+    new BigInteger("48385A7D2BB858F3DB7F72E7CDFE218B9CA84DDA8BD64C3775AA43551D974F60", HEX_RADIX)
+  };
+  // k = P + 10000
+  private static final BigInteger[] KM_XRAHUGE = new BigInteger[] {
+    new BigInteger("16377E9F5EE2C0F4C70E17AC298EF670700A7CB186EEB0DA10CDD59635000AF8", HEX_RADIX),
+    new BigInteger("5BD7921EEE662ACBAC3A96D8B6039D2356F154859FAF41FD2F0D99DF06CD2EAE", HEX_RADIX)
+  };
+
+  // Helpful constants
+  private static final BigInteger ONE = BigInteger.ONE;
+  private static final BigInteger ZERO = BigInteger.ZERO;
+
+  // Identity element of the group (the zero) in affine and extended representations
+  private static final BigInteger[] ID = new BigInteger[] {ZERO, ONE};
+  private static final BigInteger[] ID_EX = new BigInteger[] {ZERO, ONE, ONE, ZERO};
+
+  public void testValidPoints() throws Exception {
+    if (KeyEncoding.isLegacyCryptoRequired()) {
+      // this means we're running on an old SDK, which doesn't support the
+      // necessary crypto. Let's not test anything in this case.
+      return;
+    }
+
+    // We've got a couple of valid points
+    Ed25519.validateAffinePoint(KM);
+    Ed25519.validateAffinePoint(KN);
+    
+    // And a bunch of invalid ones
+    try {
+      Ed25519.validateAffinePoint(new BigInteger[] {ZERO, ONE});
+      fail("Validate point not catching zero x coordinates");
+    } catch (Ed25519Exception e) {
+      assertThat(e.getMessage(), containsString("positive"));
+    }
+    
+    try {
+      Ed25519.validateAffinePoint(new BigInteger[] {ONE, ZERO});
+      fail("Validate point not catching zero y coordinates");
+    } catch (Ed25519Exception e) {
+      assertThat(e.getMessage(), containsString("positive"));
+    }
+    
+    try {
+      Ed25519.validateAffinePoint(new BigInteger[] {new BigInteger("-1"), ONE});
+      fail("Validate point not catching negative x coordinates");
+    } catch (Ed25519Exception e) {
+      assertThat(e.getMessage(), containsString("positive"));
+    }
+    
+    try {
+      Ed25519.validateAffinePoint(new BigInteger[] {ONE, new BigInteger("-1")});
+      fail("Validate point not catching negative y coordinates");
+    } catch (Ed25519Exception e) {
+      assertThat(e.getMessage(), containsString("positive"));
+    }
+    
+    try {
+      Ed25519.validateAffinePoint(new BigInteger[] {ONE, ONE});
+      fail("Validate point not catching points that are not on curve");
+    } catch (Ed25519Exception e) {
+      assertThat(e.getMessage(), containsString("expected curve"));
+    }
+  }
+
+  public void testAffineExtendedConversion() throws Exception {
+    BigInteger[] km1 = Ed25519.toAffine(Ed25519.toExtended(KM));
+    BigInteger[] kn1 = Ed25519.toAffine(Ed25519.toExtended(KN));
+
+    assertArrayEquals(KM, km1);
+    assertArrayEquals(KN, kn1);
+
+    assertArrayEquals(ID, Ed25519.toAffine(ID_EX));
+    assertArrayEquals(ID_EX, Ed25519.toExtended(ID));
+  }
+
+  public void testRepresentationCheck() throws Exception {
+    Ed25519.checkPointIsInAffineRepresentation(KM);
+    Ed25519.checkPointIsInExtendedRepresentation(ID_EX);
+
+    try {
+      Ed25519.checkPointIsInExtendedRepresentation(KM);
+      fail("Point is not really in extended representation, expected failure");
+    } catch (Ed25519Exception e) {      
+      assertThat(e.getMessage(), containsString("not in extended"));
+    }
+
+    try {
+      Ed25519.checkPointIsInAffineRepresentation(Ed25519.toExtended(KM));
+      fail("Point is not really in affine representation, expected failure");
+    } catch (Ed25519Exception e) {     
+      assertThat(e.getMessage(), containsString("not in affine"));
+    }
+  }
+
+  public void testAddSubtractExtendedPoints() throws Exception {
+    // Adding/subtracting identity to/from itself should yield the identity point
+    assertArrayEquals(ID, Ed25519.addAffinePoints(ID, ID));
+    assertArrayEquals(ID, Ed25519.subtractAffinePoints(ID, ID));
+
+    // In fact adding/subtracting the identity point to/from any point should yield that point
+    assertArrayEquals(KM, Ed25519.addAffinePoints(KM, ID));
+    assertArrayEquals(KM, Ed25519.subtractAffinePoints(KM, ID));
+
+    // Subtracting a point from itself should yield the identity element
+    assertArrayEquals(ID, Ed25519.subtractAffinePoints(KM, KM));
+    assertArrayEquals(ID, Ed25519.subtractAffinePoints(KN, KN));
+
+    // Adding and subtracting should yield the same point
+    assertArrayEquals(KM, Ed25519.subtractAffinePoints(Ed25519.addAffinePoints(KM, KN), KN));
+    assertArrayEquals(KN, Ed25519.subtractAffinePoints(Ed25519.addAffinePoints(KN, KM), KM));
+  }
+
+  public void testScalarMultiplyExtendedPoints() throws Exception {
+    // A point times one is the point itself
+    assertArrayEquals(KM, Ed25519.scalarMultiplyAffinePoint(KM, ONE));
+    assertArrayEquals(KN, Ed25519.scalarMultiplyAffinePoint(KN, ONE));
+
+    // A point times zero is the identity point
+    assertArrayEquals(ID, Ed25519.scalarMultiplyAffinePoint(KM, ZERO));
+    assertArrayEquals(ID, Ed25519.scalarMultiplyAffinePoint(KN, ZERO));
+
+    // The identity times a scalar is the identity
+    assertArrayEquals(ID, Ed25519.scalarMultiplyAffinePoint(ID, BigInteger.valueOf(317698)));
+
+    // Use test vectors
+    assertArrayEquals(KM_2, Ed25519.scalarMultiplyAffinePoint(KM, BigInteger.valueOf(2)));
+    assertArrayEquals(KM_3, Ed25519.scalarMultiplyAffinePoint(KM, BigInteger.valueOf(3)));
+    assertArrayEquals(KM_317698, Ed25519.scalarMultiplyAffinePoint(KM, BigInteger.valueOf(317698)));
+    assertArrayEquals(KM_HUGE, Ed25519.scalarMultiplyAffinePoint(KM, P));
+    assertArrayEquals(KM_XRAHUGE,
+        Ed25519.scalarMultiplyAffinePoint(KM, P.add(BigInteger.valueOf(10000))));
+  }
+}
diff --git a/src/main/javatest/com/google/security/cryptauth/lib/securegcm/EnrollmentCryptoOpsTest.java b/src/main/javatest/com/google/security/cryptauth/lib/securegcm/EnrollmentCryptoOpsTest.java
new file mode 100644
index 0000000..4437045
--- /dev/null
+++ b/src/main/javatest/com/google/security/cryptauth/lib/securegcm/EnrollmentCryptoOpsTest.java
@@ -0,0 +1,134 @@
+// Copyright 2020 Google LLC
+//
+// 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
+//
+//     https://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.google.security.cryptauth.lib.securegcm;
+
+import com.google.protobuf.ByteString;
+import com.google.security.cryptauth.lib.securegcm.SecureGcmProto.GcmDeviceInfo;
+import com.google.security.cryptauth.lib.securemessage.PublicKeyProtoUtil;
+import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.GenericPublicKey;
+import java.security.KeyPair;
+import java.security.PublicKey;
+import java.util.Arrays;
+import javax.crypto.SecretKey;
+import junit.framework.TestCase;
+
+/**
+ * Android compatible tests for the {@link EnrollmentCryptoOps} class.
+ */
+public class EnrollmentCryptoOpsTest extends TestCase {
+
+  private static final long DEVICE_ID = 1234567890L;
+  private static final byte[] GCM_REGISTRATION_ID = { -0x80, 0, -0x80, 0, -0x80, 0 };
+  private static final String DEVICE_MODEL = "TEST DEVICE";
+  private static final String LOCALE = "en";
+  private static final byte[] SESSION_ID = { 5, 5, 4, 4, 3, 3, 2, 2, 1, 1 };
+  private static final String OAUTH_TOKEN = "1/23456etc";
+
+  @Override
+  protected void setUp() throws Exception {
+    KeyEncodingTest.installSunEcSecurityProviderIfNecessary();
+    assertEquals(
+        PublicKeyProtoUtil.isLegacyCryptoRequired(), KeyEncoding.isLegacyCryptoRequired());
+    super.setUp();
+  }
+
+  @Override
+  protected void tearDown() throws Exception {
+    KeyEncoding.setSimulateLegacyCrypto(false);
+    super.tearDown();
+  }
+
+
+  public void testSimulatedEnrollment() throws Exception {
+    boolean isLegacy = KeyEncoding.isLegacyCryptoRequired();
+    // Step 1: Server generates an ephemeral DH key pair, saves the private key, and sends
+    //         the public key to the client as server_ephemeral_key.
+    KeyPair serverEphemeralKeyPair =
+        EnrollmentCryptoOps.generateEnrollmentKeyAgreementKeyPair(isLegacy);
+    byte[] savedServerPrivateKey =
+        KeyEncoding.encodeKeyAgreementPrivateKey(serverEphemeralKeyPair.getPrivate());
+    byte[] serverEphemeralKey = KeyEncoding.encodeKeyAgreementPublicKey(
+        serverEphemeralKeyPair.getPublic());
+
+    // Step 2a: Client generates an ephemeral DH key pair, and completes the DH key exchange
+    //          to derive the master key.
+    KeyPair clientEphemeralKeyPair =
+        EnrollmentCryptoOps.generateEnrollmentKeyAgreementKeyPair(isLegacy);
+    byte[] clientEphemeralKey = KeyEncoding.encodeKeyAgreementPublicKey(
+        clientEphemeralKeyPair.getPublic());
+    SecretKey clientMasterKey = EnrollmentCryptoOps.doKeyAgreement(
+        clientEphemeralKeyPair.getPrivate(),
+        KeyEncoding.parseKeyAgreementPublicKey(serverEphemeralKey));
+
+    // Step 2b: Client generates its user key pair, and fills in a GcmDeviceInfo message containing
+    //          the enrollment request (which includes the user public key).
+    KeyPair userKeyPair = isLegacy ? PublicKeyProtoUtil.generateRSA2048KeyPair()
+        : PublicKeyProtoUtil.generateEcP256KeyPair();
+    GcmDeviceInfo clientInfo = createGcmDeviceInfo(userKeyPair.getPublic(), clientMasterKey);
+
+    // Step 2c: Client signcrypts the enrollment request to the server, using a combination of the
+    //          master key and its user signing key.
+    byte[] enrollmentMessage = EnrollmentCryptoOps.encryptEnrollmentMessage(
+        clientInfo, clientMasterKey, userKeyPair.getPrivate());
+
+
+    // Step 3a: Server receives the client's DH public key and completes the key exchange using
+    //          the saved DH private key.
+    SecretKey serverMasterKey = EnrollmentCryptoOps.doKeyAgreement(
+        KeyEncoding.parseKeyAgreementPrivateKey(savedServerPrivateKey, isLegacy),
+        KeyEncoding.parseKeyAgreementPublicKey(clientEphemeralKey));
+
+    // Step 3b: Server uses the exchanged master key to de-signcrypt the enrollment request
+    //          (which also provides the user public key in the clear).
+    GcmDeviceInfo serverInfo = EnrollmentCryptoOps.decryptEnrollmentMessage(
+        enrollmentMessage, serverMasterKey, isLegacy);
+
+    // Verify that the server sees the client's original enrollment request
+    assertTrue(Arrays.equals(clientInfo.toByteArray(), serverInfo.toByteArray()));
+
+    // Confirm that the server can recover a valid user PublicKey from the enrollment
+    PublicKey serverUserPublicKey = KeyEncoding.parseUserPublicKey(
+        serverInfo.getUserPublicKey().toByteArray());
+    assertTrue(serverUserPublicKey.equals(userKeyPair.getPublic()));
+  }
+
+  public void testSimulatedEnrollmentWithForcedLegacy() throws Exception {
+    if (PublicKeyProtoUtil.isLegacyCryptoRequired()) {
+      // We already test with legacy in this case
+      return;
+    }
+    KeyEncoding.setSimulateLegacyCrypto(true);
+    testSimulatedEnrollment();
+  }
+
+  private GcmDeviceInfo createGcmDeviceInfo(PublicKey userPublicKey, SecretKey masterKey) {
+    // One possible method of generating a key handle:
+    GenericPublicKey encodedUserPublicKey = PublicKeyProtoUtil.encodePublicKey(userPublicKey);
+    byte[] keyHandle = EnrollmentCryptoOps.sha256(encodedUserPublicKey.toByteArray());
+
+    return GcmDeviceInfo.newBuilder()
+        .setAndroidDeviceId(DEVICE_ID)
+        .setGcmRegistrationId(ByteString.copyFrom(GCM_REGISTRATION_ID))
+        .setDeviceMasterKeyHash(
+            ByteString.copyFrom(EnrollmentCryptoOps.getMasterKeyHash(masterKey)))
+        .setUserPublicKey(ByteString.copyFrom(KeyEncoding.encodeUserPublicKey(userPublicKey)))
+        .setDeviceModel(DEVICE_MODEL)
+        .setLocale(LOCALE)
+        .setKeyHandle(ByteString.copyFrom(keyHandle))
+        .setEnrollmentSessionId(ByteString.copyFrom(SESSION_ID))
+        .setOauthToken(OAUTH_TOKEN)
+        .build();
+  }
+}
diff --git a/src/main/javatest/com/google/security/cryptauth/lib/securegcm/KeyEncodingTest.java b/src/main/javatest/com/google/security/cryptauth/lib/securegcm/KeyEncodingTest.java
new file mode 100644
index 0000000..7012eae
--- /dev/null
+++ b/src/main/javatest/com/google/security/cryptauth/lib/securegcm/KeyEncodingTest.java
@@ -0,0 +1,189 @@
+// Copyright 2020 Google LLC
+//
+// 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
+//
+//     https://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.google.security.cryptauth.lib.securegcm;
+
+import com.google.security.cryptauth.lib.securemessage.PublicKeyProtoUtil;
+import java.security.Key;
+import java.security.KeyPair;
+import java.security.PrivateKey;
+import java.security.Provider;
+import java.security.PublicKey;
+import java.security.Security;
+import java.security.interfaces.ECPrivateKey;
+import java.security.interfaces.ECPublicKey;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.InvalidKeySpecException;
+import java.util.Arrays;
+import javax.crypto.interfaces.DHPrivateKey;
+import javax.crypto.interfaces.DHPublicKey;
+import junit.framework.TestCase;
+
+/**
+ * Android compatible tests for the {@link KeyEncoding} class.
+ */
+public class KeyEncodingTest extends TestCase {
+  private static final byte[] RAW_KEY_BYTES = {
+      1, 2, 3, 4, 5, 6, 7, 8, 9, 0,
+      1, 2, 3, 4, 5, 6, 7, 8, 9, 0,
+      1, 2, 3, 4, 5, 6, 7, 8, 9, 0,
+      1, 2};
+
+  private Boolean isLegacy;
+  private KeyPair userKeyPair;
+
+  @Override
+  protected void setUp() throws Exception {
+    installSunEcSecurityProviderIfNecessary();
+    isLegacy = PublicKeyProtoUtil.isLegacyCryptoRequired();
+    setUserKeyPair();
+    super.setUp();
+  }
+
+  @Override
+  protected void tearDown() throws Exception {
+    KeyEncoding.setSimulateLegacyCrypto(false);
+    isLegacy = PublicKeyProtoUtil.isLegacyCryptoRequired();
+    super.tearDown();
+  }
+
+  private void setUserKeyPair() {
+    userKeyPair = isLegacy ? PublicKeyProtoUtil.generateRSA2048KeyPair()
+        : PublicKeyProtoUtil.generateEcP256KeyPair();
+  }
+
+  public void testSimulateLegacyCrypto() {
+    if (isLegacy) {
+      return;  // Nothing to test if we are already stuck in a legacy platform
+    }
+    assertFalse(KeyEncoding.isLegacyCryptoRequired());
+    KeyEncoding.setSimulateLegacyCrypto(true);
+    assertTrue(KeyEncoding.isLegacyCryptoRequired());
+  }
+
+  public void testMasterKeyEncoding() {
+    // Require that master keys are encoded/decoded as raw byte arrays
+    assertTrue(Arrays.equals(
+        RAW_KEY_BYTES,
+        KeyEncoding.encodeMasterKey(KeyEncoding.parseMasterKey(RAW_KEY_BYTES))));
+  }
+
+  public void testUserPublicKeyEncoding() throws InvalidKeySpecException {
+    PublicKey pk = userKeyPair.getPublic();
+    byte[] encodedPk = KeyEncoding.encodeUserPublicKey(pk);
+    PublicKey decodedPk = KeyEncoding.parseUserPublicKey(encodedPk);
+    assertKeysEqual(pk, decodedPk);
+  }
+
+  public void testUserPrivateKeyEncoding() throws InvalidKeySpecException {
+    PrivateKey sk = userKeyPair.getPrivate();
+    byte[] encodedSk = KeyEncoding.encodeUserPrivateKey(sk);
+    PrivateKey decodedSk = KeyEncoding.parseUserPrivateKey(encodedSk, isLegacy);
+    assertKeysEqual(sk, decodedSk);
+  }
+
+  public void testKeyAgreementPublicKeyEncoding() throws InvalidKeySpecException {
+    KeyPair clientKeyPair = EnrollmentCryptoOps.generateEnrollmentKeyAgreementKeyPair(isLegacy);
+    PublicKey pk = clientKeyPair.getPublic();
+    byte[] encodedPk = KeyEncoding.encodeKeyAgreementPublicKey(pk);
+    PublicKey decodedPk = KeyEncoding.parseKeyAgreementPublicKey(encodedPk);
+    assertKeysEqual(pk, decodedPk);
+  }
+
+  public void testKeyAgreementPrivateKeyEncoding() throws InvalidKeySpecException {
+    KeyPair clientKeyPair = EnrollmentCryptoOps.generateEnrollmentKeyAgreementKeyPair(isLegacy);
+    PrivateKey sk = clientKeyPair.getPrivate();
+    byte[] encodedSk = KeyEncoding.encodeKeyAgreementPrivateKey(sk);
+    PrivateKey decodedSk = KeyEncoding.parseKeyAgreementPrivateKey(encodedSk, isLegacy);
+    assertKeysEqual(sk, decodedSk);
+  }
+
+  public void testEncodingsWithForcedLegacy() throws InvalidKeySpecException {
+    if (PublicKeyProtoUtil.isLegacyCryptoRequired()) {
+      // We already test with legacy in this case
+      return;
+    }
+    KeyEncoding.setSimulateLegacyCrypto(true);
+    isLegacy = true;
+    setUserKeyPair();
+    testUserPublicKeyEncoding();
+    testUserPrivateKeyEncoding();
+    testKeyAgreementPublicKeyEncoding();
+    testKeyAgreementPrivateKeyEncoding();
+  }
+
+  public void testSigningPublicKeyEncoding() throws InvalidKeySpecException {
+    KeyPair keyPair = PublicKeyProtoUtil.generateEcP256KeyPair();
+    PublicKey pk = keyPair.getPublic();
+    byte[] encodedPk = KeyEncoding.encodeSigningPublicKey(pk);
+    PublicKey decodedPk = KeyEncoding.parseSigningPublicKey(encodedPk);
+    assertKeysEqual(pk, decodedPk);
+  }
+
+  public void testSigningPrivateKeyEncoding() throws InvalidKeySpecException {
+    KeyPair keyPair = PublicKeyProtoUtil.generateEcP256KeyPair();
+    PrivateKey sk = keyPair.getPrivate();
+    byte[] encodedSk = KeyEncoding.encodeSigningPrivateKey(sk);
+    PrivateKey decodedSk = KeyEncoding.parseSigningPrivateKey(encodedSk);
+    assertKeysEqual(sk, decodedSk);
+  }
+
+  public void testDeviceSyncPublicKeyEncoding() throws InvalidKeySpecException {
+    KeyPair keyPair = PublicKeyProtoUtil.generateEcP256KeyPair();
+    PublicKey pk = keyPair.getPublic();
+    byte[] encodedPk = KeyEncoding.encodeDeviceSyncGroupPublicKey(pk);
+    PublicKey decodedPk = KeyEncoding.parseDeviceSyncGroupPublicKey(encodedPk);
+    assertKeysEqual(pk, decodedPk);
+  }
+
+  void assertKeysEqual(Key a, Key b) {
+    if ((a instanceof ECPublicKey)
+        || (a instanceof ECPrivateKey)
+        || (a instanceof RSAPublicKey)
+        || (a instanceof RSAPrivateKey)) {
+      assertNotNull(a.getEncoded());
+      assertTrue(Arrays.equals(a.getEncoded(), b.getEncoded()));
+    }
+    if (a instanceof DHPublicKey) {
+      DHPublicKey ya = (DHPublicKey) a;
+      DHPublicKey yb = (DHPublicKey) b;
+      assertEquals(ya.getY(), yb.getY());
+      assertEquals(ya.getParams().getG(), yb.getParams().getG());
+      assertEquals(ya.getParams().getP(), yb.getParams().getP());
+    }
+    if (a instanceof DHPrivateKey) {
+      DHPrivateKey xa = (DHPrivateKey) a;
+      DHPrivateKey xb = (DHPrivateKey) b;
+      assertEquals(xa.getX(), xb.getX());
+      assertEquals(xa.getParams().getG(), xb.getParams().getG());
+      assertEquals(xa.getParams().getP(), xb.getParams().getP());
+    }
+  }
+
+  /**
+   * Registers the SunEC security provider if no EC security providers are currently registered.
+   */
+  // TODO(shabsi): Remove this method when b/7891565 is fixed
+  static void installSunEcSecurityProviderIfNecessary() {
+    if (Security.getProviders("KeyPairGenerator.EC") == null) {
+      try {
+        Class<?> providerClass = Class.forName("sun.security.ec.SunEC");
+        Security.addProvider((Provider) providerClass.newInstance());
+      } catch (Exception e) {
+        // SunEC is not available, nothing we can do
+      }
+    }
+  }
+}
diff --git a/src/main/javatest/com/google/security/cryptauth/lib/securegcm/TransportCryptoOpsTest.java b/src/main/javatest/com/google/security/cryptauth/lib/securegcm/TransportCryptoOpsTest.java
new file mode 100644
index 0000000..9e45c0a
--- /dev/null
+++ b/src/main/javatest/com/google/security/cryptauth/lib/securegcm/TransportCryptoOpsTest.java
@@ -0,0 +1,110 @@
+// Copyright 2020 Google LLC
+//
+// 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
+//
+//     https://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.google.security.cryptauth.lib.securegcm;
+
+import com.google.security.cryptauth.lib.securegcm.SecureGcmProto.Tickle;
+import com.google.security.cryptauth.lib.securegcm.TransportCryptoOps.Payload;
+import com.google.security.cryptauth.lib.securegcm.TransportCryptoOps.PayloadType;
+import com.google.security.cryptauth.lib.securemessage.PublicKeyProtoUtil;
+import java.security.KeyPair;
+import java.security.PublicKey;
+import java.util.Arrays;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+import junit.framework.TestCase;
+
+/**
+ * Android compatible tests for the {@link TransportCryptoOps} class.
+ */
+public class TransportCryptoOpsTest extends TestCase {
+  private static final byte[] KEY_BYTES = {
+      1, 2, 3, 4, 5, 6, 7, 8, 9, 0,
+      1, 2, 3, 4, 5, 6, 7, 8, 9, 0,
+      1, 2, 3, 4, 5, 6, 7, 8, 9, 0,
+      1, 2
+  };
+  private static final byte[] KEY_HANDLE = { 9 };
+
+  private SecretKey masterKey;
+
+  @Override
+  protected void setUp() throws Exception {
+    KeyEncodingTest.installSunEcSecurityProviderIfNecessary();
+    masterKey = new SecretKeySpec(KEY_BYTES, "AES");
+    super.setUp();
+  }
+
+  public void testServerMessage() throws Exception {
+    long tickleExpiry = 12345L;
+    Tickle tickle = Tickle.newBuilder()
+        .setExpiryTime(tickleExpiry)
+        .build();
+
+    // Simulate sending a message
+    byte[] signcryptedMessage = TransportCryptoOps.signcryptServerMessage(
+        new Payload(PayloadType.TICKLE, tickle.toByteArray()),
+        masterKey,
+        KEY_HANDLE);
+
+    // Simulate the process of receiving the message
+    assertTrue(Arrays.equals(KEY_HANDLE, TransportCryptoOps.getKeyHandleFor(signcryptedMessage)));
+    Payload received = TransportCryptoOps.verifydecryptServerMessage(signcryptedMessage, masterKey);
+    assertEquals(PayloadType.TICKLE, received.getPayloadType());
+    Tickle receivedTickle = Tickle.parseFrom(received.getMessage());
+    assertEquals(tickleExpiry, receivedTickle.getExpiryTime());
+  }
+
+  public void testClientMessage() throws Exception {
+    if (PublicKeyProtoUtil.isLegacyCryptoRequired()) {
+      return;  // This test isn't for legacy crypto
+    }
+    KeyPair userKeyPair = PublicKeyProtoUtil.generateEcP256KeyPair();
+    doTestClientMessageWith(userKeyPair);
+  }
+
+  public void testClientMessageWithLegacyCrypto() throws Exception {
+    KeyPair userKeyPair = PublicKeyProtoUtil.generateRSA2048KeyPair();
+    doTestClientMessageWith(userKeyPair);
+  }
+
+  private void doTestClientMessageWith(KeyPair userKeyPair) throws Exception {
+    PublicKey userPublicKey = userKeyPair.getPublic();
+    // Will use a Tickle for the test message, even though that would normally
+    // only be sent from the server to the client
+    long tickleExpiry = 12345L;
+    Tickle tickle = Tickle.newBuilder()
+        .setExpiryTime(tickleExpiry)
+        .build();
+
+    // Simulate sending a message
+    byte[] signcryptedMessage = TransportCryptoOps.signcryptClientMessage(
+        new Payload(PayloadType.TICKLE, tickle.toByteArray()),
+        userKeyPair,
+        masterKey);
+
+    // Simulate the process of receiving the message
+    byte[] encodedUserPublicKey = TransportCryptoOps.getEncodedUserPublicKeyFor(signcryptedMessage);
+    assertTrue(Arrays.equals(KeyEncoding.encodeUserPublicKey(userPublicKey), encodedUserPublicKey));
+    userPublicKey = KeyEncoding.parseUserPublicKey(encodedUserPublicKey);
+    // At this point the server would have looked up the masterKey for this userPublicKey
+
+    Payload received = TransportCryptoOps.verifydecryptClientMessage(
+        signcryptedMessage, userPublicKey, masterKey);
+
+    assertEquals(PayloadType.TICKLE, received.getPayloadType());
+    Tickle receivedTickle = Tickle.parseFrom(received.getMessage());
+    assertEquals(tickleExpiry, receivedTickle.getExpiryTime());
+  }
+}
diff --git a/src/main/javatest/com/google/security/cryptauth/lib/securegcm/Ukey2CppCompatibilityTest.java b/src/main/javatest/com/google/security/cryptauth/lib/securegcm/Ukey2CppCompatibilityTest.java
new file mode 100644
index 0000000..db319e0
--- /dev/null
+++ b/src/main/javatest/com/google/security/cryptauth/lib/securegcm/Ukey2CppCompatibilityTest.java
@@ -0,0 +1,124 @@
+// Copyright 2020 Google LLC
+//
+// 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
+//
+//     https://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.google.security.cryptauth.lib.securegcm;
+
+import com.google.security.cryptauth.lib.securegcm.Ukey2Handshake.HandshakeCipher;
+import com.google.security.cryptauth.lib.securegcm.Ukey2ShellCppWrapper.Mode;
+import java.util.Arrays;
+import junit.framework.TestCase;
+
+/**
+ * Tests the compatibility between the Java and C++ implementations of the UKEY2 protocol. This
+ * integration test executes and talks to a compiled binary exposing the C++ implementation (wrapped
+ * by {@link Ukey2ShellCppWrapper}).
+ *
+ * <p>The C++ implementation is located in //security/cryptauth/lib/securegcm.
+ */
+public class Ukey2CppCompatibilityTest extends TestCase {
+  private static final int VERIFICATION_STRING_LENGTH = 32;
+
+  private static final byte[] sPayload1 = "payload to encrypt1".getBytes();
+  private static final byte[] sPayload2 = "payload to encrypt2".getBytes();
+
+  /** Tests full handshake with C++ client and Java server. */
+  public void testCppClientJavaServer() throws Exception {
+    Ukey2ShellCppWrapper cppUkey2Shell =
+        new Ukey2ShellCppWrapper(Mode.INITIATOR, VERIFICATION_STRING_LENGTH);
+    cppUkey2Shell.startShell();
+    Ukey2Handshake javaUkey2Handshake = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512);
+
+    // ClientInit:
+    byte[] clientInit = cppUkey2Shell.readHandshakeMessage();
+    javaUkey2Handshake.parseHandshakeMessage(clientInit);
+
+    // ServerInit:
+    byte[] serverInit = javaUkey2Handshake.getNextHandshakeMessage();
+    cppUkey2Shell.writeHandshakeMessage(serverInit);
+
+    // ClientFinished:
+    byte[] clientFinished = cppUkey2Shell.readHandshakeMessage();
+    javaUkey2Handshake.parseHandshakeMessage(clientFinished);
+
+    // Verification String:
+    cppUkey2Shell.confirmAuthString(
+        javaUkey2Handshake.getVerificationString(VERIFICATION_STRING_LENGTH));
+    javaUkey2Handshake.verifyHandshake();
+
+    // Secure channel:
+    D2DConnectionContext javaSecureContext = javaUkey2Handshake.toConnectionContext();
+
+    // ukey2_shell encodes data:
+    byte[] encodedData = cppUkey2Shell.sendEncryptCommand(sPayload1);
+    byte[] decodedData = javaSecureContext.decodeMessageFromPeer(encodedData);
+    assertTrue(Arrays.equals(sPayload1, decodedData));
+
+    // ukey2_shell decodes data:
+    encodedData = javaSecureContext.encodeMessageToPeer(sPayload2);
+    decodedData = cppUkey2Shell.sendDecryptCommand(encodedData);
+    assertTrue(Arrays.equals(sPayload2, decodedData));
+
+    // ukey2_shell session unique:
+    byte[] localSessionUnique = javaSecureContext.getSessionUnique();
+    byte[] remoteSessionUnique = cppUkey2Shell.sendSessionUniqueCommand();
+    assertTrue(Arrays.equals(localSessionUnique, remoteSessionUnique));
+
+    cppUkey2Shell.stopShell();
+  }
+
+  /** Tests full handshake with C++ server and Java client. */
+  public void testCppServerJavaClient() throws Exception {
+    Ukey2ShellCppWrapper cppUkey2Shell =
+        new Ukey2ShellCppWrapper(Mode.RESPONDER, VERIFICATION_STRING_LENGTH);
+    cppUkey2Shell.startShell();
+    Ukey2Handshake javaUkey2Handshake = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512);
+
+    // ClientInit:
+    byte[] clientInit = javaUkey2Handshake.getNextHandshakeMessage();
+    cppUkey2Shell.writeHandshakeMessage(clientInit);
+
+    // ServerInit:
+    byte[] serverInit = cppUkey2Shell.readHandshakeMessage();
+    javaUkey2Handshake.parseHandshakeMessage(serverInit);
+
+    // ClientFinished:
+    byte[] clientFinished = javaUkey2Handshake.getNextHandshakeMessage();
+    cppUkey2Shell.writeHandshakeMessage(clientFinished);
+
+    // Verification String:
+    cppUkey2Shell.confirmAuthString(
+        javaUkey2Handshake.getVerificationString(VERIFICATION_STRING_LENGTH));
+    javaUkey2Handshake.verifyHandshake();
+
+    // Secure channel:
+    D2DConnectionContext javaSecureContext = javaUkey2Handshake.toConnectionContext();
+
+    // ukey2_shell encodes data:
+    byte[] encodedData = cppUkey2Shell.sendEncryptCommand(sPayload1);
+    byte[] decodedData = javaSecureContext.decodeMessageFromPeer(encodedData);
+    assertTrue(Arrays.equals(sPayload1, decodedData));
+
+    // ukey2_shell decodes data:
+    encodedData = javaSecureContext.encodeMessageToPeer(sPayload2);
+    decodedData = cppUkey2Shell.sendDecryptCommand(encodedData);
+    assertTrue(Arrays.equals(sPayload2, decodedData));
+
+    // ukey2_shell session unique:
+    byte[] localSessionUnique = javaSecureContext.getSessionUnique();
+    byte[] remoteSessionUnique = cppUkey2Shell.sendSessionUniqueCommand();
+    assertTrue(Arrays.equals(localSessionUnique, remoteSessionUnique));
+
+    cppUkey2Shell.stopShell();
+  }
+}
diff --git a/src/main/javatest/com/google/security/cryptauth/lib/securegcm/Ukey2HandshakeTest.java b/src/main/javatest/com/google/security/cryptauth/lib/securegcm/Ukey2HandshakeTest.java
new file mode 100644
index 0000000..f5d0e1a
--- /dev/null
+++ b/src/main/javatest/com/google/security/cryptauth/lib/securegcm/Ukey2HandshakeTest.java
@@ -0,0 +1,818 @@
+// Copyright 2020 Google LLC
+//
+// 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
+//
+//     https://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.google.security.cryptauth.lib.securegcm;
+
+import com.google.protobuf.ByteString;
+import com.google.security.cryptauth.lib.securegcm.Ukey2Handshake.AlertException;
+import com.google.security.cryptauth.lib.securegcm.Ukey2Handshake.HandshakeCipher;
+import com.google.security.cryptauth.lib.securegcm.Ukey2Handshake.State;
+import com.google.security.cryptauth.lib.securegcm.UkeyProto.Ukey2ClientFinished;
+import com.google.security.cryptauth.lib.securegcm.UkeyProto.Ukey2ClientInit;
+import com.google.security.cryptauth.lib.securegcm.UkeyProto.Ukey2ClientInit.CipherCommitment;
+import com.google.security.cryptauth.lib.securegcm.UkeyProto.Ukey2Message;
+import com.google.security.cryptauth.lib.securegcm.UkeyProto.Ukey2ServerInit;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import junit.framework.TestCase;
+import org.junit.Assert;
+
+/**
+ * Android compatible tests for the {@link Ukey2Handshake} class.
+ */
+public class Ukey2HandshakeTest extends TestCase {
+
+  private static final int MAX_AUTH_STRING_LENGTH = 32;
+
+  @Override
+  protected void setUp() throws Exception {
+    KeyEncodingTest.installSunEcSecurityProviderIfNecessary();
+    super.setUp();
+  }
+
+  /**
+   * Tests correct use
+   */
+  public void testHandshake() throws Exception {
+    if (KeyEncoding.isLegacyCryptoRequired()) {
+      // this means we're running on an old SDK, which doesn't support the
+      // necessary crypto. Let's not test anything in this case.
+      return;
+    }
+
+    Ukey2Handshake client = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512);
+    Ukey2Handshake server = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512);
+    byte[] handshakeMessage;
+
+    assertEquals(State.IN_PROGRESS, client.getHandshakeState());
+    assertEquals(State.IN_PROGRESS, server.getHandshakeState());
+
+    // Message 1 (Client Init)
+    handshakeMessage = client.getNextHandshakeMessage();
+    server.parseHandshakeMessage(handshakeMessage);
+    assertEquals(State.IN_PROGRESS, client.getHandshakeState());
+    assertEquals(State.IN_PROGRESS, server.getHandshakeState());
+
+    // Message 2 (Server Init)
+    handshakeMessage = server.getNextHandshakeMessage();
+    client.parseHandshakeMessage(handshakeMessage);
+    assertEquals(State.IN_PROGRESS, client.getHandshakeState());
+    assertEquals(State.IN_PROGRESS, server.getHandshakeState());
+
+    // Message 3 (Client Finish)
+    handshakeMessage = client.getNextHandshakeMessage();
+    server.parseHandshakeMessage(handshakeMessage);
+    assertEquals(State.VERIFICATION_NEEDED, client.getHandshakeState());
+    assertEquals(State.VERIFICATION_NEEDED, server.getHandshakeState());
+
+    // Get the auth string
+    byte[] clientAuthString = client.getVerificationString(MAX_AUTH_STRING_LENGTH);
+    byte[] serverAuthString = server.getVerificationString(MAX_AUTH_STRING_LENGTH);
+    Assert.assertArrayEquals(clientAuthString, serverAuthString);
+    assertEquals(State.VERIFICATION_IN_PROGRESS, client.getHandshakeState());
+    assertEquals(State.VERIFICATION_IN_PROGRESS, server.getHandshakeState());
+
+    // Verify the auth string
+    client.verifyHandshake();
+    server.verifyHandshake();
+    assertEquals(State.FINISHED, client.getHandshakeState());
+    assertEquals(State.FINISHED, server.getHandshakeState());
+
+    // Make a context
+    D2DConnectionContext clientContext = client.toConnectionContext();
+    D2DConnectionContext serverContext = server.toConnectionContext();
+    assertContextsCompatible(clientContext, serverContext);
+    assertEquals(State.ALREADY_USED, client.getHandshakeState());
+    assertEquals(State.ALREADY_USED, server.getHandshakeState());
+  }
+
+  /**
+   * Verify enums for ciphers match the proto values
+   */
+  public void testCipherEnumValuesCorrect() {
+    assertEquals(
+        "You added a cipher, but forgot to change the test", 1, HandshakeCipher.values().length);
+
+    assertEquals(UkeyProto.Ukey2HandshakeCipher.P256_SHA512,
+        HandshakeCipher.P256_SHA512.getValue());
+  }
+
+  /**
+   * Tests incorrect use by callers (client and servers accidentally sending the wrong message at
+   * the wrong time)
+   */
+  public void testHandshakeClientAndServerSendRepeatedOutOfOrderMessages() throws Exception {
+    if (KeyEncoding.isLegacyCryptoRequired()) {
+      // this means we're running on an old SDK, which doesn't support the
+      // necessary crypto. Let's not test anything in this case.
+      return;
+    }
+
+    // Client sends ClientInit (again) instead of ClientFinished
+    Ukey2Handshake client = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512);
+    Ukey2Handshake server = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512);
+    byte[] handshakeMessage = client.getNextHandshakeMessage();
+    server.parseHandshakeMessage(handshakeMessage);
+    server.getNextHandshakeMessage(); // do this to avoid illegal state
+    try {
+      server.parseHandshakeMessage(handshakeMessage);
+      fail("Expected Alert for client sending ClientInit twice");
+    } catch (HandshakeException e) {
+      // success
+    }
+    assertEquals(State.ERROR, server.getHandshakeState());
+
+    // Server sends ClientInit back to client instead of ServerInit
+    client = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512);
+    server = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512);
+    handshakeMessage = client.getNextHandshakeMessage();
+    try {
+      client.parseHandshakeMessage(handshakeMessage);
+      fail("Expected Alert for server sending ClientInit back to client");
+    } catch (AlertException e) {
+      // success
+    }
+    assertEquals(State.ERROR, client.getHandshakeState());
+
+    // Clients sends ServerInit back to client instead of ClientFinished
+    client = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512);
+    server = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512);
+    handshakeMessage = client.getNextHandshakeMessage();
+    server.parseHandshakeMessage(handshakeMessage);
+    handshakeMessage = server.getNextHandshakeMessage();
+    try {
+      server.parseHandshakeMessage(handshakeMessage);
+      fail("Expected Alert for client sending ServerInit back to server");
+    } catch (HandshakeException e) {
+      // success
+    }
+    assertEquals(State.ERROR, server.getHandshakeState());
+  }
+
+  /**
+   * Tests that verification codes are different for different handshake runs. Also tests a full
+   * man-in-the-middle attack.
+   */
+  public void testVerificationCodeUniqueToSession() throws Exception {
+    if (KeyEncoding.isLegacyCryptoRequired()) {
+      // this means we're running on an old SDK, which doesn't support the
+      // necessary crypto. Let's not test anything in this case.
+      return;
+    }
+
+    // Client 1 and Server 1
+    Ukey2Handshake client1 = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512);
+    Ukey2Handshake server1 = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512);
+    byte[] handshakeMessage = client1.getNextHandshakeMessage();
+    server1.parseHandshakeMessage(handshakeMessage);
+    handshakeMessage = server1.getNextHandshakeMessage();
+    client1.parseHandshakeMessage(handshakeMessage);
+    handshakeMessage = client1.getNextHandshakeMessage();
+    server1.parseHandshakeMessage(handshakeMessage);
+    byte[] client1AuthString = client1.getVerificationString(MAX_AUTH_STRING_LENGTH);
+    byte[] server1AuthString = server1.getVerificationString(MAX_AUTH_STRING_LENGTH);
+    Assert.assertArrayEquals(client1AuthString, server1AuthString);
+
+    // Client 2 and Server 2
+    Ukey2Handshake client2 = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512);
+    Ukey2Handshake server2 = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512);
+    handshakeMessage = client2.getNextHandshakeMessage();
+    server2.parseHandshakeMessage(handshakeMessage);
+    handshakeMessage = server2.getNextHandshakeMessage();
+    client2.parseHandshakeMessage(handshakeMessage);
+    handshakeMessage = client2.getNextHandshakeMessage();
+    server2.parseHandshakeMessage(handshakeMessage);
+    byte[] client2AuthString = client2.getVerificationString(MAX_AUTH_STRING_LENGTH);
+    byte[] server2AuthString = server2.getVerificationString(MAX_AUTH_STRING_LENGTH);
+    Assert.assertArrayEquals(client2AuthString, server2AuthString);
+
+    // Make sure the verification strings differ
+    assertFalse(Arrays.equals(client1AuthString, client2AuthString));
+  }
+
+  /**
+   * Test an attack where the adversary swaps out the public key in the final message (i.e.,
+   * commitment doesn't match public key)
+   */
+  public void testPublicKeyDoesntMatchCommitment() throws Exception {
+    if (KeyEncoding.isLegacyCryptoRequired()) {
+      // this means we're running on an old SDK, which doesn't support the
+      // necessary crypto. Let's not test anything in this case.
+      return;
+    }
+
+    // Run handshake as usual, but stop before sending client finished
+    Ukey2Handshake client1 = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512);
+    Ukey2Handshake server1 = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512);
+    byte[] handshakeMessage = client1.getNextHandshakeMessage();
+    server1.parseHandshakeMessage(handshakeMessage);
+    handshakeMessage = server1.getNextHandshakeMessage();
+
+    // Run another handshake and get the final client finished
+    Ukey2Handshake client2 = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512);
+    Ukey2Handshake server2 = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512);
+    handshakeMessage = client2.getNextHandshakeMessage();
+    server2.parseHandshakeMessage(handshakeMessage);
+    handshakeMessage = server2.getNextHandshakeMessage();
+    client2.parseHandshakeMessage(handshakeMessage);
+    handshakeMessage = client2.getNextHandshakeMessage();
+
+    // Now use the client finished from second handshake in first handshake (simulates where an
+    // attacker switches out the last message).
+    try {
+      server1.parseHandshakeMessage(handshakeMessage);
+      fail("Expected server to catch mismatched ClientFinished");
+    } catch (HandshakeException e) {
+      // success
+    }
+    assertEquals(State.ERROR, server1.getHandshakeState());
+
+    // Make sure caller can't actually do anything with the server now that an error has occurred
+    try {
+      server1.getVerificationString(MAX_AUTH_STRING_LENGTH);
+      fail("Server allows operations post error");
+    } catch (IllegalStateException e) {
+      // success
+    }
+    try {
+      server1.verifyHandshake();
+      fail("Server allows operations post error");
+    } catch (IllegalStateException e) {
+      // success
+    }
+    try {
+      server1.toConnectionContext();
+      fail("Server allows operations post error");
+    } catch (IllegalStateException e) {
+      // success
+    }
+  }
+
+  /**
+   * Test commitment having unsupported version
+   */
+  public void testClientInitUnsupportedVersion() throws Exception {
+    if (KeyEncoding.isLegacyCryptoRequired()) {
+      // this means we're running on an old SDK, which doesn't support the
+      // necessary crypto. Let's not test anything in this case.
+      return;
+    }
+
+    // Get ClientInit and modify the version to be too big
+    Ukey2Handshake client = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512);
+    Ukey2Handshake server = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512);
+    byte[] handshakeMessage = client.getNextHandshakeMessage();
+
+    Ukey2Message.Builder message = Ukey2Message.newBuilder(
+        Ukey2Message.parseFrom(handshakeMessage));
+    Ukey2ClientInit.Builder clientInit =
+        Ukey2ClientInit.newBuilder(Ukey2ClientInit.parseFrom(message.getMessageData()));
+    clientInit.setVersion(Ukey2Handshake.VERSION + 1);
+    message.setMessageData(ByteString.copyFrom(clientInit.build().toByteArray()));
+    handshakeMessage = message.build().toByteArray();
+
+    try {
+      server.parseHandshakeMessage(handshakeMessage);
+      fail("Server did not catch unsupported version (too big) in ClientInit");
+    } catch (AlertException e) {
+      // success
+    }
+    assertEquals(State.ERROR, server.getHandshakeState());
+
+    // Get ClientInit and modify the version to be too big
+    client = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512);
+    server = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512);
+    handshakeMessage = client.getNextHandshakeMessage();
+
+    message = Ukey2Message.newBuilder(
+        Ukey2Message.parseFrom(handshakeMessage));
+    clientInit = Ukey2ClientInit.newBuilder(Ukey2ClientInit.parseFrom(message.getMessageData()));
+    clientInit.setVersion(0 /* minimum version is 1 */);
+    message.setMessageData(ByteString.copyFrom(clientInit.build().toByteArray()));
+    handshakeMessage = message.build().toByteArray();
+
+    try {
+      server.parseHandshakeMessage(handshakeMessage);
+      fail("Server did not catch unsupported version (too small) in ClientInit");
+    } catch (AlertException e) {
+      // success
+    }
+    assertEquals(State.ERROR, server.getHandshakeState());
+  }
+
+  /**
+   * Tests that server catches wrong number of random bytes in ClientInit
+   */
+  public void testWrongNonceLengthInClientInit() throws Exception {
+    if (KeyEncoding.isLegacyCryptoRequired()) {
+      // this means we're running on an old SDK, which doesn't support the
+      // necessary crypto. Let's not test anything in this case.
+      return;
+    }
+
+    // Get ClientInit and modify the nonce
+    Ukey2Handshake client = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512);
+    Ukey2Handshake server = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512);
+    byte[] handshakeMessage = client.getNextHandshakeMessage();
+
+    Ukey2Message.Builder message = Ukey2Message.newBuilder(
+        Ukey2Message.parseFrom(handshakeMessage));
+    Ukey2ClientInit.Builder clientInit =
+        Ukey2ClientInit.newBuilder(Ukey2ClientInit.parseFrom(message.getMessageData()));
+    clientInit.setRandom(
+        ByteString.copyFrom(
+            Arrays.copyOf(
+                clientInit.getRandom().toByteArray(),
+                31 /* as per go/ukey2, nonces must be 32 bytes long */)));
+    message.setMessageData(ByteString.copyFrom(clientInit.build().toByteArray()));
+    handshakeMessage = message.build().toByteArray();
+
+    try {
+      server.parseHandshakeMessage(handshakeMessage);
+      fail("Server did not catch nonce being too short in ClientInit");
+    } catch (AlertException e) {
+      // success
+    }
+    assertEquals(State.ERROR, server.getHandshakeState());
+  }
+
+  /**
+   * Test that server catches missing commitment in ClientInit message
+   */
+  public void testServerCatchesMissingCommitmentInClientInit() throws Exception {
+    if (KeyEncoding.isLegacyCryptoRequired()) {
+      // this means we're running on an old SDK, which doesn't support the
+      // necessary crypto. Let's not test anything in this case.
+      return;
+    }
+
+    // Get ClientInit and modify the commitment
+    Ukey2Handshake client = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512);
+    Ukey2Handshake server = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512);
+    byte[] handshakeMessage = client.getNextHandshakeMessage();
+
+    Ukey2Message.Builder message = Ukey2Message.newBuilder(
+        Ukey2Message.parseFrom(handshakeMessage));
+    Ukey2ClientInit clientInit =
+        Ukey2ClientInit.newBuilder(Ukey2ClientInit.parseFrom(message.getMessageData()))
+        .build();
+    Ukey2ClientInit.Builder badClientInit = Ukey2ClientInit.newBuilder()
+        .setVersion(clientInit.getVersion())
+        .setRandom(clientInit.getRandom());
+    for (CipherCommitment commitment : clientInit.getCipherCommitmentsList()) {
+      badClientInit.addCipherCommitments(commitment);
+    }
+
+    message.setMessageData(ByteString.copyFrom(badClientInit.build().toByteArray()));
+    handshakeMessage = message.build().toByteArray();
+
+    try {
+      server.parseHandshakeMessage(handshakeMessage);
+      fail("Server did not catch missing commitment in ClientInit");
+    } catch (AlertException e) {
+      // success
+    }
+  }
+
+  /**
+   * Test that client catches invalid version in ServerInit
+   */
+  public void testServerInitUnsupportedVersion() throws Exception {
+    if (KeyEncoding.isLegacyCryptoRequired()) {
+      // this means we're running on an old SDK, which doesn't support the
+      // necessary crypto. Let's not test anything in this case.
+      return;
+    }
+
+    // Get ServerInit and modify the version to be too big
+    Ukey2Handshake client = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512);
+    Ukey2Handshake server = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512);
+    byte[] handshakeMessage = client.getNextHandshakeMessage();
+    server.parseHandshakeMessage(handshakeMessage);
+    handshakeMessage = server.getNextHandshakeMessage();
+
+    Ukey2Message.Builder message = Ukey2Message.newBuilder(
+        Ukey2Message.parseFrom(handshakeMessage));
+    Ukey2ServerInit serverInit =
+        Ukey2ServerInit.newBuilder(Ukey2ServerInit.parseFrom(message.getMessageData()))
+            .setVersion(Ukey2Handshake.VERSION + 1)
+            .build();
+    message.setMessageData(ByteString.copyFrom(serverInit.toByteArray()));
+    handshakeMessage = message.build().toByteArray();
+
+    try {
+      client.parseHandshakeMessage(handshakeMessage);
+      fail("Client did not catch unsupported version (too big) in ServerInit");
+    } catch (AlertException e) {
+      // success
+    }
+    assertEquals(State.ERROR, client.getHandshakeState());
+
+    // Get ServerInit and modify the version to be too big
+    client = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512);
+    server = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512);
+    handshakeMessage = client.getNextHandshakeMessage();
+    server.parseHandshakeMessage(handshakeMessage);
+    handshakeMessage = server.getNextHandshakeMessage();
+
+    message = Ukey2Message.newBuilder(Ukey2Message.parseFrom(handshakeMessage));
+    serverInit =
+        Ukey2ServerInit.newBuilder(Ukey2ServerInit.parseFrom(message.getMessageData()))
+            .setVersion(0 /* minimum version is 1 */)
+            .build();
+    message.setMessageData(ByteString.copyFrom(serverInit.toByteArray()));
+    handshakeMessage = message.build().toByteArray();
+
+    try {
+      client.parseHandshakeMessage(handshakeMessage);
+      fail("Client did not catch unsupported version (too small) in ServerInit");
+    } catch (AlertException e) {
+      // success
+    }
+    assertEquals(State.ERROR, client.getHandshakeState());
+  }
+
+  /**
+   * Tests that client catches wrong number of random bytes in ServerInit
+   */
+  public void testWrongNonceLengthInServerInit() throws Exception {
+    if (KeyEncoding.isLegacyCryptoRequired()) {
+      // this means we're running on an old SDK, which doesn't support the
+      // necessary crypto. Let's not test anything in this case.
+      return;
+    }
+
+    // Get ServerInit and modify the nonce
+    Ukey2Handshake client = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512);
+    Ukey2Handshake server = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512);
+    byte[] handshakeMessage = client.getNextHandshakeMessage();
+    server.parseHandshakeMessage(handshakeMessage);
+    handshakeMessage = server.getNextHandshakeMessage();
+
+    Ukey2Message.Builder message = Ukey2Message.newBuilder(
+        Ukey2Message.parseFrom(handshakeMessage));
+    Ukey2ServerInit.Builder serverInitBuilder =
+        Ukey2ServerInit.newBuilder(Ukey2ServerInit.parseFrom(message.getMessageData()));
+    Ukey2ServerInit serverInit = serverInitBuilder.setRandom(ByteString.copyFrom(Arrays.copyOf(
+            serverInitBuilder.getRandom().toByteArray(),
+            31 /* as per go/ukey2, nonces must be 32 bytes long */)))
+        .build();
+    message.setMessageData(ByteString.copyFrom(serverInit.toByteArray()));
+    handshakeMessage = message.build().toByteArray();
+
+    try {
+      client.parseHandshakeMessage(handshakeMessage);
+      fail("Client did not catch nonce being too short in ServerInit");
+    } catch (AlertException e) {
+      // success
+    }
+    assertEquals(State.ERROR, client.getHandshakeState());
+  }
+
+  /**
+   * Test that client catches missing or incorrect handshake cipher in serverInit
+   */
+  public void testMissingOrIncorrectHandshakeCipherInServerInit() throws Exception {
+    if (KeyEncoding.isLegacyCryptoRequired()) {
+      // this means we're running on an old SDK, which doesn't support the
+      // necessary crypto. Let's not test anything in this case.
+      return;
+    }
+
+    // Get ServerInit
+    Ukey2Handshake client = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512);
+    Ukey2Handshake server = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512);
+    byte[] handshakeMessage = client.getNextHandshakeMessage();
+    server.parseHandshakeMessage(handshakeMessage);
+    handshakeMessage = server.getNextHandshakeMessage();
+    Ukey2Message.Builder message = Ukey2Message.newBuilder(
+        Ukey2Message.parseFrom(handshakeMessage));
+    Ukey2ServerInit serverInit = Ukey2ServerInit.parseFrom(message.getMessageData());
+
+    // remove handshake cipher
+    Ukey2ServerInit badServerInit = Ukey2ServerInit.newBuilder()
+        .setPublicKey(serverInit.getPublicKey())
+        .setRandom(serverInit.getRandom())
+        .setVersion(serverInit.getVersion())
+        .build();
+
+    message.setMessageData(ByteString.copyFrom(badServerInit.toByteArray()));
+    handshakeMessage = message.build().toByteArray();
+
+    try {
+      client.parseHandshakeMessage(handshakeMessage);
+      fail("Client did not catch missing handshake cipher in ServerInit");
+    } catch (AlertException e) {
+      // success
+    }
+    assertEquals(State.ERROR, client.getHandshakeState());
+
+    // Get ServerInit
+    client = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512);
+    server = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512);
+    handshakeMessage = client.getNextHandshakeMessage();
+    server.parseHandshakeMessage(handshakeMessage);
+    handshakeMessage = server.getNextHandshakeMessage();
+    message = Ukey2Message.newBuilder(Ukey2Message.parseFrom(handshakeMessage));
+    serverInit = Ukey2ServerInit.parseFrom(message.getMessageData());
+
+    // put in a bad handshake cipher
+    badServerInit = Ukey2ServerInit.newBuilder()
+        .setPublicKey(serverInit.getPublicKey())
+        .setRandom(serverInit.getRandom())
+        .setVersion(serverInit.getVersion())
+        .setHandshakeCipher(UkeyProto.Ukey2HandshakeCipher.RESERVED)
+        .build();
+
+    message.setMessageData(ByteString.copyFrom(badServerInit.toByteArray()));
+    handshakeMessage = message.build().toByteArray();
+
+    try {
+      client.parseHandshakeMessage(handshakeMessage);
+      fail("Client did not catch bad handshake cipher in ServerInit");
+    } catch (AlertException e) {
+      // success
+    }
+    assertEquals(State.ERROR, client.getHandshakeState());
+  }
+
+  /**
+   * Test that client catches missing or incorrect public key in serverInit
+   */
+  public void testMissingOrIncorrectPublicKeyInServerInit() throws Exception {
+    if (KeyEncoding.isLegacyCryptoRequired()) {
+      // this means we're running on an old SDK, which doesn't support the
+      // necessary crypto. Let's not test anything in this case.
+      return;
+    }
+
+    // Get ServerInit
+    Ukey2Handshake client = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512);
+    Ukey2Handshake server = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512);
+    byte[] handshakeMessage = client.getNextHandshakeMessage();
+    server.parseHandshakeMessage(handshakeMessage);
+    handshakeMessage = server.getNextHandshakeMessage();
+    Ukey2Message.Builder message = Ukey2Message.newBuilder(
+        Ukey2Message.parseFrom(handshakeMessage));
+    Ukey2ServerInit serverInit = Ukey2ServerInit.parseFrom(message.getMessageData());
+
+    // remove public key
+    Ukey2ServerInit badServerInit = Ukey2ServerInit.newBuilder()
+        .setRandom(serverInit.getRandom())
+        .setVersion(serverInit.getVersion())
+        .setHandshakeCipher(serverInit.getHandshakeCipher())
+        .build();
+
+    message.setMessageData(ByteString.copyFrom(badServerInit.toByteArray()));
+    handshakeMessage = message.build().toByteArray();
+
+    try {
+      client.parseHandshakeMessage(handshakeMessage);
+      fail("Client did not catch missing public key in ServerInit");
+    } catch (AlertException e) {
+      // success
+    }
+    assertEquals(State.ERROR, client.getHandshakeState());
+
+    // Get ServerInit
+    client = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512);
+    server = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512);
+    handshakeMessage = client.getNextHandshakeMessage();
+    server.parseHandshakeMessage(handshakeMessage);
+    handshakeMessage = server.getNextHandshakeMessage();
+    message = Ukey2Message.newBuilder(
+        Ukey2Message.parseFrom(handshakeMessage));
+    serverInit = Ukey2ServerInit.parseFrom(message.getMessageData());
+
+    // put in a bad public key
+    badServerInit = Ukey2ServerInit.newBuilder()
+        .setPublicKey(ByteString.copyFrom(new byte[] {42, 12, 1}))
+        .setRandom(serverInit.getRandom())
+        .setVersion(serverInit.getVersion())
+        .setHandshakeCipher(serverInit.getHandshakeCipher())
+        .build();
+
+    message.setMessageData(ByteString.copyFrom(badServerInit.toByteArray()));
+    handshakeMessage = message.build().toByteArray();
+
+    try {
+      client.parseHandshakeMessage(handshakeMessage);
+      fail("Client did not catch bad public key in ServerInit");
+    } catch (AlertException e) {
+      // success
+    }
+    assertEquals(State.ERROR, client.getHandshakeState());
+  }
+
+  /**
+   * Test that client catches missing or incorrect public key in clientFinished
+   */
+  public void testMissingOrIncorrectPublicKeyInClientFinished() throws Exception {
+    if (KeyEncoding.isLegacyCryptoRequired()) {
+      // this means we're running on an old SDK, which doesn't support the
+      // necessary crypto. Let's not test anything in this case.
+      return;
+    }
+
+    // Get ClientFinished
+    Ukey2Handshake client = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512);
+    Ukey2Handshake server = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512);
+    byte[] handshakeMessage = client.getNextHandshakeMessage();
+    server.parseHandshakeMessage(handshakeMessage);
+    handshakeMessage = server.getNextHandshakeMessage();
+    client.parseHandshakeMessage(handshakeMessage);
+    handshakeMessage = client.getNextHandshakeMessage();
+    Ukey2Message.Builder message = Ukey2Message.newBuilder(
+        Ukey2Message.parseFrom(handshakeMessage));
+
+    // remove public key
+    Ukey2ClientFinished.Builder badClientFinished = Ukey2ClientFinished.newBuilder();
+
+    message.setMessageData(ByteString.copyFrom(badClientFinished.build().toByteArray()));
+    handshakeMessage = message.build().toByteArray();
+
+    try {
+      server.parseHandshakeMessage(handshakeMessage);
+      fail("Server did not catch missing public key in ClientFinished");
+    } catch (HandshakeException e) {
+      // success
+    }
+    assertEquals(State.ERROR, server.getHandshakeState());
+
+    // Get ClientFinished
+    client = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512);
+    server = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512);
+    handshakeMessage = client.getNextHandshakeMessage();
+    server.parseHandshakeMessage(handshakeMessage);
+    handshakeMessage = server.getNextHandshakeMessage();
+    client.parseHandshakeMessage(handshakeMessage);
+    handshakeMessage = client.getNextHandshakeMessage();
+    message = Ukey2Message.newBuilder(Ukey2Message.parseFrom(handshakeMessage));
+
+    // remove public key
+    badClientFinished = Ukey2ClientFinished.newBuilder()
+        .setPublicKey(ByteString.copyFrom(new byte[] {42, 12, 1}));
+
+    message.setMessageData(ByteString.copyFrom(badClientFinished.build().toByteArray()));
+    handshakeMessage = message.build().toByteArray();
+
+    try {
+      server.parseHandshakeMessage(handshakeMessage);
+      fail("Server did not catch bad public key in ClientFinished");
+    } catch (HandshakeException e) {
+      // success
+    }
+    assertEquals(State.ERROR, server.getHandshakeState());
+  }
+
+  /**
+   * Tests that items (nonces, commitments, public keys) that should be random are at least
+   * different on every run.
+   */
+  public void testRandomItemsDifferentOnEveryRun() throws Exception {
+    if (KeyEncoding.isLegacyCryptoRequired()) {
+      // this means we're running on an old SDK, which doesn't support the
+      // necessary crypto. Let's not test anything in this case.
+      return;
+    }
+
+    int numberOfRuns = 50;
+
+    // Search for collisions
+    Set<Integer> commitments = new HashSet<>(numberOfRuns);
+    Set<Integer> clientNonces = new HashSet<>(numberOfRuns);
+    Set<Integer> serverNonces = new HashSet<>(numberOfRuns);
+    Set<Integer> serverPublicKeys = new HashSet<>(numberOfRuns);
+    Set<Integer> clientPublicKeys = new HashSet<>(numberOfRuns);
+
+    for (int i = 0; i < numberOfRuns; i++) {
+      Ukey2Handshake client = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512);
+      Ukey2Handshake server = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512);
+      byte[] handshakeMessage = client.getNextHandshakeMessage();
+      Ukey2Message message = Ukey2Message.parseFrom(handshakeMessage);
+      Ukey2ClientInit clientInit = Ukey2ClientInit.parseFrom(message.getMessageData());
+
+      server.parseHandshakeMessage(handshakeMessage);
+      handshakeMessage = server.getNextHandshakeMessage();
+      message = Ukey2Message.parseFrom(handshakeMessage);
+      Ukey2ServerInit serverInit = Ukey2ServerInit.parseFrom(message.getMessageData());
+
+      client.parseHandshakeMessage(handshakeMessage);
+      handshakeMessage = client.getNextHandshakeMessage();
+      message = Ukey2Message.parseFrom(handshakeMessage);
+      Ukey2ClientFinished clientFinished = Ukey2ClientFinished.parseFrom(message.getMessageData());
+
+      // Clean up to save some memory (b/32054837)
+      client = null;
+      server = null;
+      handshakeMessage = null;
+      message = null;
+      System.gc();
+
+      // ClientInit randomness
+      Integer nonceHash = Integer.valueOf(Arrays.hashCode(clientInit.getRandom().toByteArray()));
+      if (clientNonces.contains(nonceHash) || serverNonces.contains(nonceHash)) {
+        fail("Nonce in ClientINit has repeated!");
+      }
+      clientNonces.add(nonceHash);
+
+      Integer commitmentHash = 0;
+      for (CipherCommitment commitement : clientInit.getCipherCommitmentsList()) {
+        commitmentHash += Arrays.hashCode(commitement.toByteArray());
+      }
+      if (commitments.contains(nonceHash)) {
+        fail("Commitment has repeated!");
+      }
+      commitments.add(commitmentHash);
+
+      // ServerInit randomness
+      nonceHash = Integer.valueOf(Arrays.hashCode(serverInit.getRandom().toByteArray()));
+      if (serverNonces.contains(nonceHash) || clientNonces.contains(nonceHash)) {
+        fail("Nonce in ServerInit repeated!");
+      }
+      serverNonces.add(nonceHash);
+
+      Integer publicKeyHash =
+          Integer.valueOf(Arrays.hashCode(serverInit.getPublicKey().toByteArray()));
+      if (serverPublicKeys.contains(publicKeyHash) || clientPublicKeys.contains(publicKeyHash)) {
+        fail("Public Key in ServerInit repeated!");
+      }
+      serverPublicKeys.add(publicKeyHash);
+
+      // Client Finished randomness
+      publicKeyHash = Integer.valueOf(Arrays.hashCode(clientFinished.getPublicKey().toByteArray()));
+      if (serverPublicKeys.contains(publicKeyHash) || clientPublicKeys.contains(publicKeyHash)) {
+        fail("Public Key in ClientFinished repeated!");
+      }
+      clientPublicKeys.add(publicKeyHash);
+    }
+  }
+
+  /**
+   * Tests that {@link Ukey2Handshake#getVerificationString(int)} enforces sane verification string
+   * lengths.
+   */
+  public void testGetVerificationEnforcesSaneLengths() throws Exception {
+    if (KeyEncoding.isLegacyCryptoRequired()) {
+      // this means we're running on an old SDK, which doesn't support the
+      // necessary crypto. Let's not test anything in this case.
+      return;
+    }
+
+    // Run the protocol
+    Ukey2Handshake client = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512);
+    Ukey2Handshake server = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512);
+    byte[] handshakeMessage = client.getNextHandshakeMessage();
+    server.parseHandshakeMessage(handshakeMessage);
+    handshakeMessage = server.getNextHandshakeMessage();
+    client.parseHandshakeMessage(handshakeMessage);
+    handshakeMessage = client.getNextHandshakeMessage();
+    server.parseHandshakeMessage(handshakeMessage);
+
+    // Try to get too short verification string
+    try {
+      client.getVerificationString(0);
+      fail("Too short verification string allowed");
+    } catch (IllegalArgumentException e) {
+      // success
+    }
+
+    // Try to get too long verification string
+    try {
+      server.getVerificationString(MAX_AUTH_STRING_LENGTH + 1);
+      fail("Too long verification string allowed");
+    } catch (IllegalArgumentException e) {
+      // success
+    }
+  }
+
+  /**
+   * Asserts that the given client and server contexts are compatible
+   */
+  private void assertContextsCompatible(
+      D2DConnectionContext clientContext, D2DConnectionContext serverContext) {
+    assertNotNull(clientContext);
+    assertNotNull(serverContext);
+    assertEquals(D2DConnectionContextV1.PROTOCOL_VERSION, clientContext.getProtocolVersion());
+    assertEquals(D2DConnectionContextV1.PROTOCOL_VERSION, serverContext.getProtocolVersion());
+    assertEquals(clientContext.getEncodeKey(), serverContext.getDecodeKey());
+    assertEquals(clientContext.getDecodeKey(), serverContext.getEncodeKey());
+    assertFalse(clientContext.getEncodeKey().equals(clientContext.getDecodeKey()));
+    assertEquals(0, clientContext.getSequenceNumberForEncoding());
+    assertEquals(0, clientContext.getSequenceNumberForDecoding());
+    assertEquals(0, serverContext.getSequenceNumberForEncoding());
+    assertEquals(0, serverContext.getSequenceNumberForDecoding());
+  }
+}
diff --git a/src/main/javatest/com/google/security/cryptauth/lib/securegcm/Ukey2ShellCppWrapper.java b/src/main/javatest/com/google/security/cryptauth/lib/securegcm/Ukey2ShellCppWrapper.java
new file mode 100644
index 0000000..2b73653
--- /dev/null
+++ b/src/main/javatest/com/google/security/cryptauth/lib/securegcm/Ukey2ShellCppWrapper.java
@@ -0,0 +1,342 @@
+// Copyright 2020 Google LLC
+//
+// 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
+//
+//     https://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.google.security.cryptauth.lib.securegcm;
+
+import com.google.common.io.BaseEncoding;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.ProcessBuilder.Redirect;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import javax.annotation.Nullable;
+
+/**
+ * A wrapper to execute and interact with the //security/cryptauth/lib/securegcm:ukey2_shell binary.
+ *
+ * <p>This binary is a shell over the C++ implementation of the UKEY2 protocol, so this wrapper is
+ * used to test compatibility between the C++ and Java implementations.
+ *
+ * <p>The ukey2_shell is invoked as follows:
+ *
+ * <pre>{@code
+ * ukey2_shell --mode=<mode> --verification_string_length=<length>
+ * }</pre>
+ *
+ * where {@code mode={initiator, responder}} and {@code verification_string_length} is a positive
+ * integer.
+ */
+public class Ukey2ShellCppWrapper {
+  // The path the the ukey2_shell binary.
+  private static final String BINARY_PATH = "build/src/main/cpp/src/securegcm/ukey2_shell";
+
+  // The time to wait before timing out a read or write operation to the shell.
+  @SuppressWarnings("GoodTime") // TODO(b/147378611): store a java.time.Duration instead
+  private static final long IO_TIMEOUT_MILLIS = 5000;
+
+  public enum Mode {
+    INITIATOR,
+    RESPONDER
+  }
+
+  private final Mode mode;
+  private final int verificationStringLength;
+  private final ExecutorService executorService;
+
+  @Nullable private Process shellProcess;
+  private boolean secureContextEstablished;
+
+  /**
+   * @param mode The mode to run the shell in (initiator or responder).
+   * @param verificationStringLength The length of the verification string used in the handshake.
+   */
+  public Ukey2ShellCppWrapper(Mode mode, int verificationStringLength) {
+    this.mode = mode;
+    this.verificationStringLength = verificationStringLength;
+    this.executorService = Executors.newSingleThreadExecutor();
+  }
+
+  /**
+   * Begins execution of the ukey2_shell binary.
+   *
+   * @throws IOException
+   */
+  public void startShell() throws IOException {
+    if (shellProcess != null) {
+      throw new IllegalStateException("Shell already started.");
+    }
+
+    String modeArg = "--mode=" + getModeString();
+    String verificationStringLengthArg = "--verification_string_length=" + verificationStringLength;
+
+    final ProcessBuilder builder =
+        new ProcessBuilder(BINARY_PATH, modeArg, verificationStringLengthArg);
+
+    // Merge the shell's stderr with the stderr of the current process.
+    builder.redirectError(Redirect.INHERIT);
+
+    shellProcess = builder.start();
+  }
+
+  /**
+   * Stops execution of the ukey2_shell binary.
+   *
+   * @throws IOException
+   */
+  public void stopShell() {
+    if (shellProcess == null) {
+      throw new IllegalStateException("Shell not started.");
+    }
+    shellProcess.destroy();
+  }
+
+  /**
+   * @return the handshake message read from the shell.
+   * @throws IOException
+   */
+  public byte[] readHandshakeMessage() throws IOException {
+    return readFrameWithTimeout();
+  }
+
+  /**
+   * Sends the handshake message to the shell.
+   *
+   * @param message
+   * @throws IOException
+   */
+  public void writeHandshakeMessage(byte[] message) throws IOException {
+    writeFrameWithTimeout(message);
+  }
+
+  /**
+   * Reads the auth string from the shell and compares it with {@code authString}. If verification
+   * succeeds, then write "ok" back as a confirmation.
+   *
+   * @param authString the auth string to compare to.
+   * @throws IOException
+   */
+  public void confirmAuthString(byte[] authString) throws IOException {
+    byte[] shellAuthString = readFrameWithTimeout();
+    if (!Arrays.equals(authString, shellAuthString)) {
+      throw new IOException(
+          String.format(
+              "Unable to verify auth string: 0x%s != 0x%s",
+              BaseEncoding.base16().encode(authString),
+              BaseEncoding.base16().encode(shellAuthString)));
+    }
+    writeFrameWithTimeout("ok".getBytes());
+    secureContextEstablished = true;
+  }
+
+  /**
+   * Sends {@code payload} to be encrypted by the shell. This function can only be called after a
+   * handshake is performed and a secure context established.
+   *
+   * @param payload the data to be encrypted.
+   * @return the encrypted message returned by the shell.
+   * @throws IOException
+   */
+  public byte[] sendEncryptCommand(byte[] payload) throws IOException {
+    writeFrameWithTimeout(createExpression("encrypt", payload));
+    return readFrameWithTimeout();
+  }
+
+  /**
+   * Sends {@code message} to be decrypted by the shell. This function can only be called after a
+   * handshake is performed and a secure context established.
+   *
+   * @param message the data to be decrypted.
+   * @return the decrypted payload returned by the shell.
+   * @throws IOException
+   */
+  public byte[] sendDecryptCommand(byte[] message) throws IOException {
+    writeFrameWithTimeout(createExpression("decrypt", message));
+    return readFrameWithTimeout();
+  }
+
+  /**
+   * Requests the session unique value from the shell. This function can only be called after a
+   * handshake is performed and a secure context established.
+   *
+   * @return the session unique value returned by the shell.
+   * @throws IOException
+   */
+  public byte[] sendSessionUniqueCommand() throws IOException {
+    writeFrameWithTimeout(createExpression("session_unique", null));
+    return readFrameWithTimeout();
+  }
+
+  /**
+   * Reads a frame from the shell's stdout with a timeout.
+   *
+   * @return The contents of the frame.
+   * @throws IOException
+   */
+  private byte[] readFrameWithTimeout() throws IOException {
+    Future<byte[]> future =
+        executorService.submit(
+            new Callable<byte[]>() {
+              @Override
+              public byte[] call() throws Exception {
+                return readFrame();
+              }
+            });
+
+    try {
+      return future.get(IO_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+    } catch (InterruptedException | ExecutionException | TimeoutException e) {
+      throw new IOException(e);
+    }
+  }
+
+  /**
+   * Writes a frame to the shell's stdin with a timeout.
+   *
+   * @param contents the contents of the frame.
+   * @throws IOException
+   */
+  private void writeFrameWithTimeout(final byte[] contents) throws IOException {
+    Future<?> future =
+        executorService.submit(
+            new Runnable() {
+              @Override
+              public void run() {
+                try {
+                  writeFrame(contents);
+                } catch (IOException e) {
+                  throw new RuntimeException(e);
+                }
+              }
+            });
+
+    try {
+      future.get(IO_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+    } catch (InterruptedException | ExecutionException | TimeoutException e) {
+      throw new IOException(e);
+    }
+  }
+
+  /**
+   * Reads a frame from the shell's stdout, which has the format:
+   *
+   * <pre>{@code
+   * +---------------------+-----------------+
+   * | 4-bytes             | |length| bytes  |
+   * +---------------------+-----------------+
+   * | (unsigned) length   |     contents    |
+   * +---------------------+-----------------+
+   * }</pre>
+   *
+   * @return the contents that were read
+   * @throws IOException
+   */
+  private byte[] readFrame() throws IOException {
+    if (shellProcess == null) {
+      throw new IllegalStateException("Shell not started.");
+    }
+
+    InputStream inputStream = shellProcess.getInputStream();
+    byte[] lengthBytes = new byte[4];
+    if (inputStream.read(lengthBytes) != lengthBytes.length) {
+      throw new IOException("Failed to read length.");
+    }
+
+    int length = ByteBuffer.wrap(lengthBytes).order(ByteOrder.BIG_ENDIAN).getInt();
+    if (length < 0) {
+      throw new IOException("Length too large: " + Arrays.toString(lengthBytes));
+    }
+
+    byte[] contents = new byte[length];
+    int bytesRead = inputStream.read(contents);
+    if (bytesRead != length) {
+      throw new IOException("Failed to read entire contents: " + bytesRead + " != " + length);
+    }
+
+    return contents;
+  }
+
+  /**
+   * Writes a frame to the shell's stdin, which has the format:
+   *
+   * <pre>{@code
+   * +---------------------+-----------------+
+   * | 4-bytes             | |length| bytes  |
+   * +---------------------+-----------------+
+   * | (unsigned) length   |     contents    |
+   * +---------------------+-----------------+
+   * }</pre>
+   *
+   * @param contents the contents to send.
+   * @throws IOException
+   */
+  private void writeFrame(byte[] contents) throws IOException {
+    if (shellProcess == null) {
+      throw new IllegalStateException("Shell not started.");
+    }
+
+    // The length is big-endian encoded, network byte order.
+    long length = contents.length;
+    byte[] lengthBytes = new byte[4];
+    lengthBytes[0] = (byte) (length >> 32 & 0xFF);
+    lengthBytes[1] = (byte) (length >> 16 & 0xFF);
+    lengthBytes[2] = (byte) (length >> 8 & 0xFF);
+    lengthBytes[3] = (byte) (length >> 0 & 0xFF);
+
+    OutputStream outputStream = shellProcess.getOutputStream();
+    outputStream.write(lengthBytes);
+    outputStream.write(contents);
+    outputStream.flush();
+  }
+
+  /**
+   * Creates an expression to be processed when a secure connection is established, after the
+   * handshake is done.
+   *
+   * @param command The command to send.
+   * @param argument The argument of the command. Can be null.
+   * @return the expression that can be sent to the shell.
+   * @throws IOException.
+   */
+  private byte[] createExpression(String command, @Nullable byte[] argument) throws IOException {
+    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+    outputStream.write(command.getBytes());
+    outputStream.write(" ".getBytes());
+    if (argument != null) {
+      outputStream.write(argument);
+    }
+    return outputStream.toByteArray();
+  }
+
+  /** @return the mode string to use in the argument to start the ukey2_shell process. */
+  private String getModeString() {
+    switch (mode) {
+      case INITIATOR:
+        return "initiator";
+      case RESPONDER:
+        return "responder";
+      default:
+        throw new IllegalArgumentException("Uknown mode " + mode);
+    }
+  }
+}
diff --git a/src/main/javatest/com/google/security/cryptauth/lib/securemessage/CryptoOpsTest.java b/src/main/javatest/com/google/security/cryptauth/lib/securemessage/CryptoOpsTest.java
new file mode 100644
index 0000000..65fa094
--- /dev/null
+++ b/src/main/javatest/com/google/security/cryptauth/lib/securemessage/CryptoOpsTest.java
@@ -0,0 +1,172 @@
+// Copyright 2020 Google LLC
+//
+// 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
+//
+//     https://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.google.security.cryptauth.lib.securemessage;
+
+import static org.junit.Assert.assertThrows;
+
+import com.google.security.cryptauth.lib.securemessage.CryptoOps.EncType;
+import com.google.security.cryptauth.lib.securemessage.CryptoOps.SigType;
+import java.util.Arrays;
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+import junit.framework.TestCase;
+
+/**
+ * Unit tests for the CryptoOps class
+ */
+public class CryptoOpsTest extends TestCase {
+
+  /** HKDF Test Case 1 IKM from RFC 5869 */
+  private static final byte[] HKDF_CASE1_IKM = {
+    0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
+    0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
+    0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
+    0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
+    0x0b, 0x0b
+  };
+
+  /** HKDF Test Case 1 salt from RFC 5869 */
+  private static final byte[] HKDF_CASE1_SALT = {
+    0x00, 0x01, 0x02, 0x03, 0x04,
+    0x05, 0x06, 0x07, 0x08, 0x09,
+    0x0a, 0x0b, 0x0c
+  };
+
+  /** HKDF Test Case 1 info from RFC 5869 */
+  private static final byte[] HKDF_CASE1_INFO = {
+    (byte) 0xf0, (byte) 0xf1, (byte) 0xf2, (byte) 0xf3, (byte) 0xf4,
+    (byte) 0xf5, (byte) 0xf6, (byte) 0xf7, (byte) 0xf8, (byte) 0xf9
+  };
+
+  /** First 32 bytes of HKDF Test Case 1 OKM (output) from RFC 5869 */
+  private static final byte[] HKDF_CASE1_OKM = {
+    (byte) 0x3c, (byte) 0xb2, (byte) 0x5f, (byte) 0x25, (byte) 0xfa,
+    (byte) 0xac, (byte) 0xd5, (byte) 0x7a, (byte) 0x90, (byte) 0x43,
+    (byte) 0x4f, (byte) 0x64, (byte) 0xd0, (byte) 0x36, (byte) 0x2f,
+    (byte) 0x2a, (byte) 0x2d, (byte) 0x2d, (byte) 0x0a, (byte) 0x90,
+    (byte) 0xcf, (byte) 0x1a, (byte) 0x5a, (byte) 0x4c, (byte) 0x5d,
+    (byte) 0xb0, (byte) 0x2d, (byte) 0x56, (byte) 0xec, (byte) 0xc4,
+    (byte) 0xc5, (byte) 0xbf, (byte) 0x34, (byte) 0x00, (byte) 0x72,
+    (byte) 0x08, (byte) 0xd5, (byte) 0xb8, (byte) 0x87, (byte) 0x18,
+    (byte) 0x58, (byte) 0x65
+  };
+
+  private SecretKey aesKey1;
+  private SecretKey aesKey2;
+
+  @Override
+  protected void setUp() throws Exception {
+    KeyGenerator aesKeygen = KeyGenerator.getInstance("AES");
+    aesKeygen.init(256);
+    aesKey1 = aesKeygen.generateKey();
+    aesKey2 = aesKeygen.generateKey();
+    super.setUp();
+  }
+
+  public void testNoPurposeConflicts() {
+    // Ensure that signature algorithms and encryption algorithms are not given identical purposes
+    // (this prevents confusion of derived keys).
+    for (SigType sigType : SigType.values()) {
+      for (EncType encType : EncType.values()) {
+        assertFalse(CryptoOps.getPurpose(sigType).equals(CryptoOps.getPurpose(encType)));
+      }
+    }
+  }
+
+  public void testDeriveAes256KeyFor() throws Exception {
+    // Test that deriving with the same key and purpose twice is deterministic
+    assertTrue(Arrays.equals(CryptoOps.deriveAes256KeyFor(aesKey1, "A").getEncoded(),
+                             CryptoOps.deriveAes256KeyFor(aesKey1, "A").getEncoded()));
+    // Test that derived keys with different purposes differ
+    assertFalse(Arrays.equals(CryptoOps.deriveAes256KeyFor(aesKey1, "A").getEncoded(),
+                              CryptoOps.deriveAes256KeyFor(aesKey1, "B").getEncoded()));
+    // Test that derived keys with the same purpose but different master keys differ
+    assertFalse(Arrays.equals(CryptoOps.deriveAes256KeyFor(aesKey1, "A").getEncoded(),
+                              CryptoOps.deriveAes256KeyFor(aesKey2, "A").getEncoded()));
+  }
+
+  public void testHkdf() throws Exception {
+    SecretKey inputKey = new SecretKeySpec(HKDF_CASE1_IKM, "AES");
+    byte[] result = CryptoOps.hkdf(inputKey, HKDF_CASE1_SALT, HKDF_CASE1_INFO);
+    byte[] expectedResult = Arrays.copyOf(HKDF_CASE1_OKM, 32);
+    assertTrue(Arrays.equals(result, expectedResult));
+  }
+
+  public void testHkdfLongOutput() throws Exception {
+    SecretKey inputKey = new SecretKeySpec(HKDF_CASE1_IKM, "AES");
+    byte[] result = CryptoOps.hkdf(inputKey, HKDF_CASE1_SALT, HKDF_CASE1_INFO, 42);
+    byte[] expectedResult = Arrays.copyOf(HKDF_CASE1_OKM, 42);
+    assertTrue(Arrays.equals(result, expectedResult));
+  }
+
+  public void testHkdfShortOutput() throws Exception {
+    SecretKey inputKey = new SecretKeySpec(HKDF_CASE1_IKM, "AES");
+    byte[] result = CryptoOps.hkdf(inputKey, HKDF_CASE1_SALT, HKDF_CASE1_INFO, 12);
+    byte[] expectedResult = Arrays.copyOf(HKDF_CASE1_OKM, 12);
+    assertTrue(Arrays.equals(result, expectedResult));
+  }
+
+  public void testHkdfInvalidLengths() throws Exception {
+    SecretKey inputKey = new SecretKeySpec(HKDF_CASE1_IKM, "AES");
+
+    // Negative length
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> CryptoOps.hkdf(inputKey, HKDF_CASE1_SALT, HKDF_CASE1_INFO, -5));
+
+    // Too long, would be more than 256 blocks
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> CryptoOps.hkdf(inputKey, HKDF_CASE1_SALT, HKDF_CASE1_INFO, 32 * 256 + 1));
+  }
+
+  public void testConcat() {
+    byte[] a = { 1, 2, 3, 4};
+    byte[] b = { 5 , 6 };
+    byte[] expectedResult = { 1, 2, 3, 4, 5, 6 };
+    byte[] result = CryptoOps.concat(a, b);
+    assertEquals(a.length + b.length, result.length);
+    assertTrue(Arrays.equals(expectedResult, result));
+
+    byte[] empty =  { };
+    assertEquals(0, CryptoOps.concat(empty, empty).length);
+    assertTrue(Arrays.equals(a, CryptoOps.concat(a, empty)));
+    assertTrue(Arrays.equals(a, CryptoOps.concat(empty, a)));
+
+    assertEquals(0, CryptoOps.concat(null, null).length);
+    assertTrue(Arrays.equals(a, CryptoOps.concat(a, null)));
+    assertTrue(Arrays.equals(a, CryptoOps.concat(null, a)));
+  }
+
+  public void testSubarray() {
+    byte[] in = { 1, 2, 3, 4, 5, 6, 7 };
+    assertTrue(Arrays.equals(in, CryptoOps.subarray(in, 0, in.length)));
+    assertEquals(0, CryptoOps.subarray(in, 0, 0).length);
+    byte[] expectedResult1 = { 1 };
+    assertTrue(Arrays.equals(expectedResult1, CryptoOps.subarray(in, 0, 1)));
+    byte[] expectedResult34 = { 3, 4 };
+    assertTrue(Arrays.equals(expectedResult34, CryptoOps.subarray(in, 2, 4)));
+    assertThrows(IndexOutOfBoundsException.class, () -> CryptoOps.subarray(in, 0, in.length + 1));
+    assertThrows(IndexOutOfBoundsException.class, () -> CryptoOps.subarray(in, -1, in.length));
+    assertThrows(
+        IndexOutOfBoundsException.class, () -> CryptoOps.subarray(in, in.length, in.length));
+    assertThrows(
+        IndexOutOfBoundsException.class,
+        () -> CryptoOps.subarray(in, Integer.MIN_VALUE, in.length));
+    assertThrows(
+        IndexOutOfBoundsException.class, () -> CryptoOps.subarray(in, 1, Integer.MIN_VALUE));
+  }
+}
diff --git a/src/main/javatest/com/google/security/cryptauth/lib/securemessage/NullsGoogle3Test.java b/src/main/javatest/com/google/security/cryptauth/lib/securemessage/NullsGoogle3Test.java
new file mode 100644
index 0000000..c28d2f9
--- /dev/null
+++ b/src/main/javatest/com/google/security/cryptauth/lib/securemessage/NullsGoogle3Test.java
@@ -0,0 +1,42 @@
+// Copyright 2020 Google LLC
+//
+// 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
+//
+//     https://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.google.security.cryptauth.lib.securemessage;
+
+import com.google.common.testing.NullPointerTester;
+import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.SecureMessage;
+import junit.framework.TestCase;
+
+/**
+ * Non-portable Google3-based test to check null pointer behavior.
+ */
+public class NullsGoogle3Test extends TestCase {
+
+  /**
+   *  We test all of the classes in one place to avoid a proliferation of similar test cases,
+   * noting that {@link NullPointerTester} emits the name of the class where the breakge occurs.
+   */
+  public void testNulls() {
+    final NullPointerTester tester = new NullPointerTester();
+    tester.testAllPublicStaticMethods(CryptoOps.class);
+    tester.testAllPublicStaticMethods(PublicKeyProtoUtil.class);
+
+    tester.setDefault(SecureMessage.class, SecureMessage.getDefaultInstance());
+    tester.testAllPublicStaticMethods(SecureMessageParser.class);
+
+    tester.testAllPublicStaticMethods(SecureMessageBuilder.class);
+    tester.testAllPublicConstructors(SecureMessageBuilder.class);
+    tester.testAllPublicInstanceMethods(new SecureMessageBuilder());
+  }
+}
diff --git a/src/main/javatest/com/google/security/cryptauth/lib/securemessage/PublicKeyProtoUtilTest.java b/src/main/javatest/com/google/security/cryptauth/lib/securemessage/PublicKeyProtoUtilTest.java
new file mode 100644
index 0000000..8581622
--- /dev/null
+++ b/src/main/javatest/com/google/security/cryptauth/lib/securemessage/PublicKeyProtoUtilTest.java
@@ -0,0 +1,412 @@
+// Copyright 2020 Google LLC
+//
+// 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
+//
+//     https://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.google.security.cryptauth.lib.securemessage;
+
+import com.google.common.io.BaseEncoding;
+import com.google.protobuf.ByteString;
+import com.google.security.annotations.SuppressInsecureCipherModeCheckerNoReview;
+import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.DhPublicKey;
+import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.EcP256PublicKey;
+import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.GenericPublicKey;
+import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.SimpleRsaPublicKey;
+import java.math.BigInteger;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.interfaces.ECPublicKey;
+import java.security.spec.ECPoint;
+import java.security.spec.ECPublicKeySpec;
+import java.security.spec.InvalidKeySpecException;
+import java.util.Arrays;
+import javax.crypto.KeyAgreement;
+import javax.crypto.interfaces.DHPrivateKey;
+import javax.crypto.interfaces.DHPublicKey;
+import junit.framework.TestCase;
+
+/** Tests for the PublicKeyProtoUtil class. */
+public class PublicKeyProtoUtilTest extends TestCase {
+
+  private static final byte[] ZERO_BYTE = {0};
+  private PublicKey ecPublicKey;
+  private PublicKey rsaPublicKey;
+
+  /**
+   * Diffie Hellman {@link PublicKey}s require special treatment, so we store them specifically as a
+   * {@link DHPublicKey} to minimize casting.
+   */
+  private DHPublicKey dhPublicKey;
+
+  @Override
+  public void setUp() {
+    if (!isAndroidOsWithoutEcSupport()) {
+      ecPublicKey = PublicKeyProtoUtil.generateEcP256KeyPair().getPublic();
+    }
+    rsaPublicKey = PublicKeyProtoUtil.generateRSA2048KeyPair().getPublic();
+    dhPublicKey = (DHPublicKey) PublicKeyProtoUtil.generateDh2048KeyPair().getPublic();
+  }
+
+  public void testPublicKeyProtoSpecificEncodeParse() throws Exception {
+    if (!isAndroidOsWithoutEcSupport()) {
+      assertEquals(
+          ecPublicKey,
+          PublicKeyProtoUtil.parseEcPublicKey(PublicKeyProtoUtil.encodeEcPublicKey(ecPublicKey)));
+    }
+
+    assertEquals(
+        rsaPublicKey,
+        PublicKeyProtoUtil.parseRsa2048PublicKey(
+            PublicKeyProtoUtil.encodeRsa2048PublicKey(rsaPublicKey)));
+
+    // DHPublicKey objects don't seem to properly implement equals(), so we have to test that
+    // the individual y and p values match (it is safe to assume g = 2 is used if p is correct).
+    DHPublicKey parsedDHPublicKey =
+        PublicKeyProtoUtil.parseDh2048PublicKey(
+            PublicKeyProtoUtil.encodeDh2048PublicKey(dhPublicKey));
+    assertEquals(dhPublicKey.getY(), parsedDHPublicKey.getY());
+    assertEquals(dhPublicKey.getParams().getP(), parsedDHPublicKey.getParams().getP());
+    assertEquals(dhPublicKey.getParams().getG(), parsedDHPublicKey.getParams().getG());
+  }
+
+  public void testPublicKeyProtoGenericEncodeParse() throws Exception {
+    if (!isAndroidOsWithoutEcSupport()) {
+      assertEquals(
+          ecPublicKey,
+          PublicKeyProtoUtil.parsePublicKey(
+              PublicKeyProtoUtil.encodePaddedEcPublicKey(ecPublicKey)));
+      assertEquals(
+          ecPublicKey,
+          PublicKeyProtoUtil.parsePublicKey(PublicKeyProtoUtil.encodePublicKey(ecPublicKey)));
+    }
+
+    assertEquals(
+        rsaPublicKey,
+        PublicKeyProtoUtil.parsePublicKey(PublicKeyProtoUtil.encodePublicKey(rsaPublicKey)));
+
+    // See above explanation for why we treat DHPublicKey objects differently.
+    DHPublicKey parsedDHPublicKey =
+        PublicKeyProtoUtil.parseDh2048PublicKey(
+            PublicKeyProtoUtil.encodeDh2048PublicKey(dhPublicKey));
+    assertEquals(dhPublicKey.getY(), parsedDHPublicKey.getY());
+    assertEquals(dhPublicKey.getParams().getP(), parsedDHPublicKey.getParams().getP());
+    assertEquals(dhPublicKey.getParams().getG(), parsedDHPublicKey.getParams().getG());
+  }
+
+  public void testPaddedECPublicKeyEncodeHasPaddedNullByte() throws Exception {
+    if (isAndroidOsWithoutEcSupport()) {
+      return;
+    }
+
+    // Key where the x coordinate is 33 bytes, y coordinate is 32 bytes
+    ECPublicKey maxXByteLengthKey =
+        buildEcPublicKey(
+            BaseEncoding.base64().decode("AM730WQL7ZAmvyAJX4euNdr3+nAIueGlYYGXE6p732h6"),
+            BaseEncoding.base64().decode("JEnmaDpKn0fH4/0kKGb97qUSwI2uT+ta0GLe3V7REfk="));
+    // Key where both coordinates are 33 bytes
+    ECPublicKey maxByteLengthKey =
+        buildEcPublicKey(
+            BaseEncoding.base64().decode("AOg9TQCxFfVdXv7lO/6UVDyiPsu8XDkEWQIPUfqX6UHP"),
+            BaseEncoding.base64().decode("AP/RW8uVyu6QImpbza51CqG1mtBTh5c9pjv9CUwOuB7E"));
+    // Key where both coordinates are 32 bytes
+    ECPublicKey notMaxByteLengthKey =
+        buildEcPublicKey(
+            BaseEncoding.base64().decode("M35bxV8HKr0e8v7f4zuXgw6TYFawvikFdI71u9S1ONI="),
+            BaseEncoding.base64().decode("OXR+xCpD8AR0VR8TeBXA00eIr3rWE6sV6KrOM6MoWsc="));
+    GenericPublicKey encodedMaxXByteLengthKey =
+        PublicKeyProtoUtil.encodePublicKey(maxXByteLengthKey);
+    GenericPublicKey paddedEncodedMaxXByteLengthKey =
+        PublicKeyProtoUtil.encodePaddedEcPublicKey(maxXByteLengthKey);
+    GenericPublicKey encodedMaxByteLengthKey = PublicKeyProtoUtil.encodePublicKey(maxByteLengthKey);
+    GenericPublicKey paddedEncodedMaxByteLengthKey =
+        PublicKeyProtoUtil.encodePaddedEcPublicKey(maxByteLengthKey);
+    GenericPublicKey encodedNotMaxByteLengthKey =
+        PublicKeyProtoUtil.encodePublicKey(notMaxByteLengthKey);
+    GenericPublicKey paddedEncodedNotMaxByteLengthKey =
+        PublicKeyProtoUtil.encodePaddedEcPublicKey(notMaxByteLengthKey);
+
+    assertEquals(maxXByteLengthKey, PublicKeyProtoUtil.parsePublicKey(encodedMaxXByteLengthKey));
+    assertEquals(
+        maxXByteLengthKey, PublicKeyProtoUtil.parsePublicKey(paddedEncodedMaxXByteLengthKey));
+    assertEquals(maxByteLengthKey, PublicKeyProtoUtil.parsePublicKey(encodedMaxByteLengthKey));
+    assertEquals(
+        maxByteLengthKey, PublicKeyProtoUtil.parsePublicKey(paddedEncodedMaxByteLengthKey));
+    assertEquals(
+        notMaxByteLengthKey, PublicKeyProtoUtil.parsePublicKey(paddedEncodedNotMaxByteLengthKey));
+    assertEquals(
+        notMaxByteLengthKey, PublicKeyProtoUtil.parsePublicKey(encodedNotMaxByteLengthKey));
+
+    assertEquals(33, paddedEncodedMaxXByteLengthKey.getEcP256PublicKey().getX().size());
+    assertEquals(33, paddedEncodedMaxXByteLengthKey.getEcP256PublicKey().getY().size());
+    assertEquals(0, paddedEncodedMaxXByteLengthKey.getEcP256PublicKey().getX().byteAt(0));
+    assertEquals(0, paddedEncodedMaxXByteLengthKey.getEcP256PublicKey().getY().byteAt(0));
+    assertEquals(33, encodedMaxXByteLengthKey.getEcP256PublicKey().getX().size());
+    assertEquals(32, encodedMaxXByteLengthKey.getEcP256PublicKey().getY().size());
+
+    assertEquals(33, paddedEncodedMaxByteLengthKey.getEcP256PublicKey().getX().size());
+    assertEquals(33, paddedEncodedMaxByteLengthKey.getEcP256PublicKey().getY().size());
+    assertEquals(0, paddedEncodedMaxByteLengthKey.getEcP256PublicKey().getX().byteAt(0));
+    assertEquals(0, paddedEncodedMaxByteLengthKey.getEcP256PublicKey().getY().byteAt(0));
+    assertEquals(33, encodedMaxByteLengthKey.getEcP256PublicKey().getX().size());
+    assertEquals(33, encodedMaxByteLengthKey.getEcP256PublicKey().getY().size());
+
+    assertEquals(32, encodedNotMaxByteLengthKey.getEcP256PublicKey().getX().size());
+    assertEquals(32, encodedNotMaxByteLengthKey.getEcP256PublicKey().getY().size());
+    assertEquals(0, paddedEncodedNotMaxByteLengthKey.getEcP256PublicKey().getX().byteAt(0));
+    assertEquals(0, paddedEncodedNotMaxByteLengthKey.getEcP256PublicKey().getY().byteAt(0));
+    assertEquals(33, paddedEncodedNotMaxByteLengthKey.getEcP256PublicKey().getX().size());
+    assertEquals(33, paddedEncodedNotMaxByteLengthKey.getEcP256PublicKey().getY().size());
+  }
+
+  @SuppressInsecureCipherModeCheckerNoReview
+  public void testWrongPublicKeyType() throws Exception {
+    KeyPairGenerator dsaGen = KeyPairGenerator.getInstance("DSA");
+    dsaGen.initialize(512);
+    PublicKey pk = dsaGen.generateKeyPair().getPublic();
+
+    if (!isAndroidOsWithoutEcSupport()) {
+      // Try to encode it as EC
+      try {
+        PublicKeyProtoUtil.encodeEcPublicKey(pk);
+        fail();
+      } catch (IllegalArgumentException expected) {
+      }
+
+      try {
+        PublicKeyProtoUtil.encodePaddedEcPublicKey(pk);
+        fail();
+      } catch (IllegalArgumentException expected) {
+      }
+    }
+
+    // Try to encode it as RSA
+    try {
+      PublicKeyProtoUtil.encodeRsa2048PublicKey(pk);
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+
+    // Try to encode it as DH
+    try {
+      PublicKeyProtoUtil.encodeDh2048PublicKey(pk);
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+
+    // Try to encode it as Generic
+    try {
+      PublicKeyProtoUtil.encodePublicKey(pk);
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  public void testEcPublicKeyProtoInvalidEncoding() throws Exception {
+    if (isAndroidOsWithoutEcSupport()) {
+      return;
+    }
+
+    EcP256PublicKey validProto = PublicKeyProtoUtil.encodeEcPublicKey(ecPublicKey);
+    EcP256PublicKey.Builder invalidProto = EcP256PublicKey.newBuilder(validProto);
+
+    // Mess up the X coordinate by repeating it twice
+    byte[] newX =
+        CryptoOps.concat(validProto.getX().toByteArray(), validProto.getX().toByteArray());
+    checkParsingFailsFor(invalidProto.setX(ByteString.copyFrom(newX)).build());
+
+    // Mess up the Y coordinate by erasing it
+    invalidProto = EcP256PublicKey.newBuilder(validProto);
+    checkParsingFailsFor(invalidProto.setY(ByteString.EMPTY).build());
+
+    // Pick a point that is likely not on the curve by copying X over Y
+    invalidProto = EcP256PublicKey.newBuilder(validProto);
+    checkParsingFailsFor(invalidProto.setY(validProto.getX()).build());
+
+    // Try the point (0, 0)
+    invalidProto = EcP256PublicKey.newBuilder(validProto);
+    checkParsingFailsFor(
+        invalidProto
+            .setX(ByteString.copyFrom(ZERO_BYTE))
+            .setY(ByteString.copyFrom(ZERO_BYTE))
+            .build());
+  }
+
+  private void checkParsingFailsFor(EcP256PublicKey invalid) {
+    try {
+      // Should fail to decode
+      PublicKeyProtoUtil.parseEcPublicKey(invalid);
+      fail();
+    } catch (InvalidKeySpecException expected) {
+    }
+  }
+
+  public void testSimpleRsaPublicKeyProtoInvalidEncoding() throws Exception {
+    SimpleRsaPublicKey validProto = PublicKeyProtoUtil.encodeRsa2048PublicKey(rsaPublicKey);
+    SimpleRsaPublicKey.Builder invalidProto;
+
+    // Double the number of bits in the modulus
+    invalidProto = SimpleRsaPublicKey.newBuilder(validProto);
+    byte[] newN =
+        CryptoOps.concat(validProto.getN().toByteArray(), validProto.getN().toByteArray());
+    checkParsingFailsFor(invalidProto.setN(ByteString.copyFrom(newN)).build());
+
+    // Set the modulus to 0
+    invalidProto = SimpleRsaPublicKey.newBuilder(validProto);
+    checkParsingFailsFor(invalidProto.setN(ByteString.copyFrom(ZERO_BYTE)).build());
+
+    // Set the modulus to 65537 (way too small)
+    invalidProto = SimpleRsaPublicKey.newBuilder(validProto);
+    checkParsingFailsFor(
+        invalidProto.setN(ByteString.copyFrom(BigInteger.valueOf(65537).toByteArray())).build());
+  }
+
+  private static void checkParsingFailsFor(SimpleRsaPublicKey invalid) {
+    try {
+      // Should fail to decode
+      PublicKeyProtoUtil.parseRsa2048PublicKey(invalid);
+      fail();
+    } catch (InvalidKeySpecException expected) {
+    }
+  }
+
+  public void testSimpleDhPublicKeyProtoInvalidEncoding() throws Exception {
+    DhPublicKey validProto = PublicKeyProtoUtil.encodeDh2048PublicKey(dhPublicKey);
+    DhPublicKey.Builder invalidProto;
+
+    // Double the number of bits in the public element encoding
+    invalidProto = DhPublicKey.newBuilder(validProto);
+    byte[] newY =
+        CryptoOps.concat(validProto.getY().toByteArray(), validProto.getY().toByteArray());
+    checkParsingFailsFor(invalidProto.setY(ByteString.copyFrom(newY)).build());
+
+    // Set the public element to 0
+    invalidProto = DhPublicKey.newBuilder(validProto);
+    checkParsingFailsFor(invalidProto.setY(ByteString.copyFrom(ZERO_BYTE)).build());
+  }
+
+  private static void checkParsingFailsFor(DhPublicKey invalid) {
+    try {
+      // Should fail to decode
+      PublicKeyProtoUtil.parseDh2048PublicKey(invalid);
+      fail();
+    } catch (InvalidKeySpecException expected) {
+    }
+  }
+
+  public void testDhKeyAgreementWorks() throws Exception {
+    int minExpectedSecretLength = (PublicKeyProtoUtil.DH_P.bitLength() / 8) - 4;
+
+    KeyPair clientKeyPair = PublicKeyProtoUtil.generateDh2048KeyPair();
+    KeyPair serverKeyPair = PublicKeyProtoUtil.generateDh2048KeyPair();
+    BigInteger clientY = ((DHPublicKey) clientKeyPair.getPublic()).getY();
+    BigInteger serverY = ((DHPublicKey) serverKeyPair.getPublic()).getY();
+    assertFalse(clientY.equals(serverY)); // DHPublicKeys should not be equal
+
+    // Run client side of the key exchange
+    byte[] clientSecret = doDhAgreement(clientKeyPair.getPrivate(), serverKeyPair.getPublic());
+    assert (clientSecret.length >= minExpectedSecretLength);
+
+    // Run the server side of the key exchange
+    byte[] serverSecret = doDhAgreement(serverKeyPair.getPrivate(), clientKeyPair.getPublic());
+    assert (serverSecret.length >= minExpectedSecretLength);
+
+    assertTrue(Arrays.equals(clientSecret, serverSecret));
+  }
+
+  public void testDh2048PrivateKeyEncoding() throws Exception {
+    KeyPair testPair = PublicKeyProtoUtil.generateDh2048KeyPair();
+    DHPrivateKey sk = (DHPrivateKey) testPair.getPrivate();
+    DHPrivateKey skParsed =
+        PublicKeyProtoUtil.parseDh2048PrivateKey(PublicKeyProtoUtil.encodeDh2048PrivateKey(sk));
+    assertEquals(sk.getX(), skParsed.getX());
+    assertEquals(sk.getParams().getP(), skParsed.getParams().getP());
+    assertEquals(sk.getParams().getG(), skParsed.getParams().getG());
+  }
+
+  public void testParseEcPublicKeyOnLegacyPlatform() {
+    if (!PublicKeyProtoUtil.isLegacyCryptoRequired()) {
+      return; // This test only runs on legacy platforms
+    }
+    byte[] pointBytes = {
+      1, 2, 3, 4, 5, 6, 7, 8, 9, 0,
+      1, 2, 3, 4, 5, 6, 7, 8, 9, 0,
+      1, 2, 3, 4, 5, 6, 7, 8, 9, 0,
+      1, 2
+    };
+
+    try {
+      PublicKeyProtoUtil.parseEcPublicKey(
+          EcP256PublicKey.newBuilder()
+              .setX(ByteString.copyFrom(pointBytes))
+              .setY(ByteString.copyFrom(pointBytes))
+              .build());
+      fail();
+    } catch (InvalidKeySpecException expected) {
+      // Should get this specific exception when EC doesn't work
+    }
+  }
+
+  public void testIsLegacyCryptoRequired() {
+    assertEquals(isAndroidOsWithoutEcSupport(), PublicKeyProtoUtil.isLegacyCryptoRequired());
+  }
+
+  /** @return true if running on an Android OS that doesn't support Elliptic Curve algorithms */
+  public static boolean isAndroidOsWithoutEcSupport() {
+    try {
+      Class<?> clazz = ClassLoader.getSystemClassLoader().loadClass("android.os.Build$VERSION");
+      int sdkVersion = clazz.getField("SDK_INT").getInt(null);
+      if (sdkVersion < PublicKeyProtoUtil.ANDROID_HONEYCOMB_SDK_INT) {
+        return true;
+      }
+    } catch (ClassNotFoundException e) {
+      // Not running on Android
+      return false;
+    } catch (SecurityException e) {
+      throw new AssertionError(e);
+    } catch (NoSuchFieldException e) {
+      throw new AssertionError(e);
+    } catch (IllegalArgumentException e) {
+      throw new AssertionError(e);
+    } catch (IllegalAccessException e) {
+      throw new AssertionError(e);
+    }
+    return false;
+  }
+
+  @SuppressInsecureCipherModeCheckerNoReview
+  private static byte[] doDhAgreement(PrivateKey secretKey, PublicKey peerKey) throws Exception {
+    KeyAgreement agreement = KeyAgreement.getInstance("DH");
+    agreement.init(secretKey);
+    agreement.doPhase(peerKey, true);
+    return agreement.generateSecret();
+  }
+
+  private static ECPublicKey buildEcPublicKey(byte[] encodedX, byte[] encodedY) throws Exception {
+    try {
+      BigInteger wX = new BigInteger(encodedX);
+      BigInteger wY = new BigInteger(encodedY);
+      return (ECPublicKey)
+          KeyFactory.getInstance("EC")
+              .generatePublic(
+                  new ECPublicKeySpec(
+                      new ECPoint(wX, wY),
+                      ((ECPublicKey) PublicKeyProtoUtil.generateEcP256KeyPair().getPublic())
+                          .getParams()));
+    } catch (NoSuchAlgorithmException e) {
+      throw new RuntimeException(e);
+    }
+  }
+}
diff --git a/src/main/javatest/com/google/security/cryptauth/lib/securemessage/SecureMessageSimpleTestVectorTest.java b/src/main/javatest/com/google/security/cryptauth/lib/securemessage/SecureMessageSimpleTestVectorTest.java
new file mode 100644
index 0000000..285b259
--- /dev/null
+++ b/src/main/javatest/com/google/security/cryptauth/lib/securemessage/SecureMessageSimpleTestVectorTest.java
@@ -0,0 +1,403 @@
+// Copyright 2020 Google LLC
+//
+// 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
+//
+//     https://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.google.security.cryptauth.lib.securemessage;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.security.cryptauth.lib.securemessage.CryptoOps.EncType;
+import com.google.security.cryptauth.lib.securemessage.CryptoOps.SigType;
+import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.GenericPublicKey;
+import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.Header;
+import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.HeaderAndBody;
+import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.SecureMessage;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.Arrays;
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+import junit.framework.TestCase;
+
+/**
+ * Tests the library against some very basic test vectors, to help ensure wire-format
+ * compatibility is not broken.
+ */
+public class SecureMessageSimpleTestVectorTest extends TestCase {
+
+  private static final KeyFactory EC_KEY_FACTORY;
+  static {
+    try {
+      if (PublicKeyProtoUtil.isLegacyCryptoRequired()) {
+        EC_KEY_FACTORY = null;
+      } else {
+        EC_KEY_FACTORY = KeyFactory.getInstance("EC");
+      }
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  private static final byte[] TEST_ASSOCIATED_DATA = {
+    11, 22, 33, 44, 55
+  };
+  private static final byte[] TEST_METADATA = {
+    10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28
+  };
+  private static final byte[] TEST_VKID  = {
+    0, 0, 1
+  };
+  private static final byte[] TEST_DKID = {
+    -1, -1, 0,
+  };
+  private static final byte[] TEST_MESSAGE = {
+    0, 99, 1, 98, 2, 97, 3, 96, 4, 95, 5, 94, 6, 93, 7, 92, 8, 91, 9, 90
+  };
+
+  // The following fields are initialized below, in a static block that contains auto-generated test
+  // vectors. Initialization can't just be done inline due to code that throws checked exceptions.
+  private static final PublicKey TEST_EC_PUBLIC_KEY;
+  private static final PrivateKey TEST_EC_PRIVATE_KEY;
+  private static final SecretKey TEST_KEY1;
+  private static final SecretKey TEST_KEY2;
+  private static final byte[] TEST_VECTOR_ECDSA_ONLY;
+  private static final byte[] TEST_VECTOR_ECDSA_AND_AES;
+  private static final byte[] TEST_VECTOR_HMAC_AND_AES_SAME_KEYS;
+  private static final byte[] TEST_VECTOR_HMAC_AND_AES_DIFFERENT_KEYS;
+
+  public void testEcdsaOnly() throws Exception {
+   if (PublicKeyProtoUtil.isLegacyCryptoRequired()) {
+      // On older Android platforms we can't run this test.
+      return;
+    }
+    SecureMessage testVector = SecureMessage.parseFrom(TEST_VECTOR_ECDSA_ONLY);
+    Header unverifiedHeader = SecureMessageParser.getUnverifiedHeader(testVector);
+    HeaderAndBody headerAndBody = SecureMessageParser.parseSignedCleartextMessage(
+        testVector, TEST_EC_PUBLIC_KEY, SigType.ECDSA_P256_SHA256, TEST_ASSOCIATED_DATA);
+    assertTrue(Arrays.equals(
+        unverifiedHeader.toByteArray(),
+        headerAndBody.getHeader().toByteArray()));
+    assertTrue(Arrays.equals(TEST_MESSAGE, headerAndBody.getBody().toByteArray()));
+    assertEquals(TEST_ASSOCIATED_DATA.length, unverifiedHeader.getAssociatedDataLength());
+    assertTrue(Arrays.equals(TEST_METADATA, unverifiedHeader.getPublicMetadata().toByteArray()));
+    assertTrue(Arrays.equals(TEST_VKID, unverifiedHeader.getVerificationKeyId().toByteArray()));
+    assertFalse(unverifiedHeader.hasDecryptionKeyId());
+  }
+
+  public void testEcdsaAndAes() throws Exception {
+   if (PublicKeyProtoUtil.isLegacyCryptoRequired()) {
+      // On older Android platforms we can't run this test.
+      return;
+    }
+    SecureMessage testVector = SecureMessage.parseFrom(TEST_VECTOR_ECDSA_AND_AES);
+    Header unverifiedHeader = SecureMessageParser.getUnverifiedHeader(testVector);
+    HeaderAndBody headerAndBody = SecureMessageParser.parseSignCryptedMessage(
+        testVector,
+        TEST_EC_PUBLIC_KEY,
+        SigType.ECDSA_P256_SHA256,
+        TEST_KEY1,
+        EncType.AES_256_CBC,
+        TEST_ASSOCIATED_DATA);
+    assertTrue(Arrays.equals(
+        unverifiedHeader.toByteArray(),
+        headerAndBody.getHeader().toByteArray()));
+    assertTrue(Arrays.equals(TEST_MESSAGE, headerAndBody.getBody().toByteArray()));
+    assertEquals(TEST_ASSOCIATED_DATA.length, unverifiedHeader.getAssociatedDataLength());
+    assertTrue(Arrays.equals(TEST_METADATA, unverifiedHeader.getPublicMetadata().toByteArray()));
+    assertTrue(Arrays.equals(TEST_VKID, unverifiedHeader.getVerificationKeyId().toByteArray()));
+    assertTrue(Arrays.equals(TEST_DKID, unverifiedHeader.getDecryptionKeyId().toByteArray()));
+  }
+
+  public void testHmacAndAesSameKeys() throws Exception {
+    SecureMessage testVector = SecureMessage.parseFrom(TEST_VECTOR_HMAC_AND_AES_SAME_KEYS);
+    Header unverifiedHeader = SecureMessageParser.getUnverifiedHeader(testVector);
+
+    HeaderAndBody headerAndBody = SecureMessageParser.parseSignCryptedMessage(
+        testVector,
+        TEST_KEY1,
+        SigType.HMAC_SHA256,
+        TEST_KEY1,
+        EncType.AES_256_CBC,
+        TEST_ASSOCIATED_DATA);
+    assertTrue(Arrays.equals(
+        unverifiedHeader.toByteArray(),
+        headerAndBody.getHeader().toByteArray()));
+    assertTrue(Arrays.equals(TEST_MESSAGE, headerAndBody.getBody().toByteArray()));
+    assertEquals(TEST_ASSOCIATED_DATA.length, unverifiedHeader.getAssociatedDataLength());
+    assertTrue(Arrays.equals(TEST_METADATA, unverifiedHeader.getPublicMetadata().toByteArray()));
+    assertTrue(Arrays.equals(TEST_VKID, unverifiedHeader.getVerificationKeyId().toByteArray()));
+    assertTrue(Arrays.equals(TEST_DKID, unverifiedHeader.getDecryptionKeyId().toByteArray()));
+  }
+
+  public void testHmacAndAesDifferentKeys() throws Exception {
+    SecureMessage testVector = SecureMessage.parseFrom(TEST_VECTOR_HMAC_AND_AES_DIFFERENT_KEYS);
+    Header unverifiedHeader = SecureMessageParser.getUnverifiedHeader(testVector);
+    HeaderAndBody headerAndBody = SecureMessageParser.parseSignCryptedMessage(
+        testVector,
+        TEST_KEY1,
+        SigType.HMAC_SHA256,
+        TEST_KEY2,
+        EncType.AES_256_CBC,
+        TEST_ASSOCIATED_DATA);
+    assertTrue(Arrays.equals(
+        unverifiedHeader.toByteArray(),
+        headerAndBody.getHeader().toByteArray()));
+    assertTrue(Arrays.equals(TEST_MESSAGE, headerAndBody.getBody().toByteArray()));
+    assertEquals(TEST_ASSOCIATED_DATA.length, unverifiedHeader.getAssociatedDataLength());
+    assertTrue(Arrays.equals(TEST_METADATA, unverifiedHeader.getPublicMetadata().toByteArray()));
+    assertTrue(Arrays.equals(TEST_VKID, unverifiedHeader.getVerificationKeyId().toByteArray()));
+    assertTrue(Arrays.equals(TEST_DKID, unverifiedHeader.getDecryptionKeyId().toByteArray()));
+  }
+
+  /**
+   * This code emits the test vectors to {@code System.out}. It will not generate fresh test
+   * vectors unless an existing test vector is set to {@code null}, but it contains all of the code
+   * used to the generate the test vector values. Ideally, existing test vectors should never be
+   * regenerated, but having this code available should make it easier to add new test vectors.
+   */
+  public void testGenerateTestVectorsPseudoTest() throws Exception {
+   if (PublicKeyProtoUtil.isLegacyCryptoRequired()) {
+      // On older Android platforms we can't run this test.
+      return;
+    }
+    System.out.printf("  static {\n    try {\n");
+    String indent = "      ";
+    PublicKey testEcPublicKey = TEST_EC_PUBLIC_KEY;
+    PrivateKey testEcPrivateKey = TEST_EC_PRIVATE_KEY;
+    if (testEcPublicKey == null) {
+      KeyPair testEcKeyPair = PublicKeyProtoUtil.generateEcP256KeyPair();
+      testEcPublicKey = testEcKeyPair.getPublic();
+      testEcPrivateKey = testEcKeyPair.getPrivate();
+    }
+    System.out.printf("%s%s = parsePublicKey(new byte[] %s);\n",
+        indent,
+        "TEST_EC_PUBLIC_KEY",
+        byteArrayToJavaCode(indent, encodePublicKey(testEcPublicKey)));
+    System.out.printf("%s%s = parseEcPrivateKey(new byte[] %s);\n",
+        indent,
+        "TEST_EC_PRIVATE_KEY",
+        byteArrayToJavaCode(indent, encodeEcPrivateKey(testEcPrivateKey)));
+
+    SecretKey testKey1 = TEST_KEY1;
+    if (testKey1 == null) {
+      testKey1 = makeAesKey();
+    }
+    System.out.printf("%s%s = new SecretKeySpec(new byte[] %s, \"AES\");\n",
+        indent,
+        "TEST_KEY1",
+        byteArrayToJavaCode(indent, testKey1.getEncoded()));
+
+    SecretKey testKey2 = TEST_KEY2;
+    if (testKey2 == null) {
+      testKey2 = makeAesKey();
+    }
+    System.out.printf("%s%s = new SecretKeySpec(new byte[] %s, \"AES\");\n",
+        indent,
+        "TEST_KEY2",
+        byteArrayToJavaCode(indent, testKey2.getEncoded()));
+
+    byte[] testVectorEcdsaOnly = TEST_VECTOR_ECDSA_ONLY;
+    if (testVectorEcdsaOnly == null) {
+      testVectorEcdsaOnly = new SecureMessageBuilder()
+          .setAssociatedData(TEST_ASSOCIATED_DATA)
+          .setPublicMetadata(TEST_METADATA)
+          .setVerificationKeyId(TEST_VKID)
+          .buildSignedCleartextMessage(
+              testEcPrivateKey, SigType.ECDSA_P256_SHA256, TEST_MESSAGE).toByteArray();
+    }
+    printInitializerFor(indent, "TEST_VECTOR_ECDSA_ONLY", testVectorEcdsaOnly);
+
+    byte[] testVectorEcdsaAndAes = TEST_VECTOR_ECDSA_AND_AES;
+    if (testVectorEcdsaAndAes == null) {
+      testVectorEcdsaAndAes = new SecureMessageBuilder()
+          .setAssociatedData(TEST_ASSOCIATED_DATA)
+          .setDecryptionKeyId(TEST_DKID)
+          .setPublicMetadata(TEST_METADATA)
+          .setVerificationKeyId(TEST_VKID)
+          .buildSignCryptedMessage(
+              testEcPrivateKey,
+              SigType.ECDSA_P256_SHA256,
+              testKey1,
+              EncType.AES_256_CBC,
+              TEST_MESSAGE).toByteArray();
+    }
+    printInitializerFor(indent, "TEST_VECTOR_ECDSA_AND_AES", testVectorEcdsaAndAes);
+
+    byte[] testVectorHmacAndAesSameKeys = TEST_VECTOR_HMAC_AND_AES_SAME_KEYS;
+    if (testVectorHmacAndAesSameKeys == null) {
+      testVectorHmacAndAesSameKeys = new SecureMessageBuilder()
+          .setAssociatedData(TEST_ASSOCIATED_DATA)
+          .setDecryptionKeyId(TEST_DKID)
+          .setPublicMetadata(TEST_METADATA)
+          .setVerificationKeyId(TEST_VKID)
+          .buildSignCryptedMessage(
+              testKey1,
+              SigType.HMAC_SHA256,
+              testKey1,
+              EncType.AES_256_CBC,
+              TEST_MESSAGE).toByteArray();
+    }
+    printInitializerFor(indent, "TEST_VECTOR_HMAC_AND_AES_SAME_KEYS", testVectorHmacAndAesSameKeys);
+
+    byte[] testVectorHmacAndAesDifferentKeys = TEST_VECTOR_HMAC_AND_AES_DIFFERENT_KEYS;
+    if (testVectorHmacAndAesDifferentKeys == null) {
+      testVectorHmacAndAesDifferentKeys = new SecureMessageBuilder()
+          .setAssociatedData(TEST_ASSOCIATED_DATA)
+          .setDecryptionKeyId(TEST_DKID)
+          .setPublicMetadata(TEST_METADATA)
+          .setVerificationKeyId(TEST_VKID)
+          .buildSignCryptedMessage(
+              testKey1,
+              SigType.HMAC_SHA256,
+              testKey2,
+              EncType.AES_256_CBC,
+              TEST_MESSAGE).toByteArray();
+    }
+    printInitializerFor(
+        indent, "TEST_VECTOR_HMAC_AND_AES_DIFFERENT_KEYS", testVectorHmacAndAesDifferentKeys);
+
+    System.out.printf(
+        "    } catch (Exception e) {\n      throw new RuntimeException(e);\n    }\n  }\n");
+  }
+
+  private SecretKey makeAesKey() throws NoSuchAlgorithmException {
+    KeyGenerator aesKeygen = KeyGenerator.getInstance("AES");
+    aesKeygen.init(256);
+    return aesKeygen.generateKey();
+  }
+
+  private void printInitializerFor(String indent, String name, byte[] value) {
+    System.out.printf("%s%s = new byte[] %s;\n",
+        indent,
+        name,
+        byteArrayToJavaCode(indent, value));
+  }
+
+  private static String byteArrayToJavaCode(String lineIndent, byte[] array) {
+    String newline = "\n" + lineIndent + "    ";
+    String unwrappedArray = Arrays.toString(array).replace("[", "").replace("]", "");
+    int wrapAfter = 16;
+    int count = wrapAfter;
+    StringBuilder result = new StringBuilder("{");
+    for (String entry : unwrappedArray.split(" ")) {
+      if (++count > wrapAfter) {
+        result.append(newline);
+        count = 0;
+      } else {
+        result.append(" ");
+      }
+      result.append(entry);
+    }
+    result.append(" }");
+    return result.toString();
+  }
+
+  private static byte[] encodePublicKey(PublicKey pk) {
+    return PublicKeyProtoUtil.encodePublicKey(pk).toByteArray();
+  }
+
+  private static PublicKey parsePublicKey(byte[] encodedPk)
+      throws InvalidKeySpecException, InvalidProtocolBufferException {
+    GenericPublicKey gpk = GenericPublicKey.parseFrom(encodedPk);
+    if (PublicKeyProtoUtil.isLegacyCryptoRequired()
+        && gpk.getType() == SecureMessageProto.PublicKeyType.EC_P256) {
+      return null;
+    }
+    return PublicKeyProtoUtil.parsePublicKey(gpk);
+  }
+
+  private static byte[] encodeEcPrivateKey(PrivateKey sk) {
+    return sk.getEncoded();
+  }
+
+  private static PrivateKey parseEcPrivateKey(byte[] sk) throws InvalidKeySpecException {
+    if (PublicKeyProtoUtil.isLegacyCryptoRequired()) {
+      return null;
+    }
+    return EC_KEY_FACTORY.generatePrivate(new PKCS8EncodedKeySpec(sk));
+  }
+
+  // The following block of code was automatically generated by cut and pasting the output of the
+  // generateTestVectorsPseudoTest, which should reliably emit this same test vectors. Please
+  // DO NOT DELETE any of these existing test vectors unless you _really_ know what you are doing.
+  //
+  // --- AUTO GENERATED CODE BEGINS HERE ---
+  static {
+    try {
+      TEST_EC_PUBLIC_KEY = parsePublicKey(new byte[] {
+          8, 1, 18, 70, 10, 33, 0, -109, 9, 5, 8, -89, -3, -68, -86, -19, 17,
+          -126, -11, -95, 35, 101, 102, -57, -84, -118, 73, 83, 66, -62, -49, -91, 71, -19,
+          52, 123, 113, 119, 45, 18, 33, 0, -65, -19, 83, -66, -12, 62, 102, -67, 116,
+          64, 42, 55, -84, -101, 90, -106, 113, -89, -30, 57, -112, 96, -99, -126, 14, 83,
+          41, 95, -24, -114, 23, -5 });
+      TEST_EC_PRIVATE_KEY = parseEcPrivateKey(new byte[] {
+          48, 65, 2, 1, 0, 48, 19, 6, 7, 42, -122, 72, -50, 61, 2, 1, 6,
+          8, 42, -122, 72, -50, 61, 3, 1, 7, 4, 39, 48, 37, 2, 1, 1, 4,
+          32, 26, -82, -61, -86, -59, -8, 2, -62, -17, -20, 122, 3, 85, -102, -76, 81,
+          51, 39, -9, 12, 99, -117, 127, 19, 121, 109, -31, -49, 110, 121, 76, -107 });
+      TEST_KEY1 = new SecretKeySpec(new byte[] {
+          -89, 105, 62, -41, -75, 78, 70, 110, -62, -58, -80, -81, -99, -62, 39, 38, 37,
+          -7, -112, -83, 81, 23, 125, -72, -100, 103, -34, -23, -68, 21, -46, -104 }, "AES");
+      TEST_KEY2 = new SecretKeySpec(new byte[] {
+          -6, 48, 107, 61, -99, -89, 111, 33, 70, 54, -13, 111, 81, -120, 50, 89, -119,
+          -113, -114, 63, 12, -68, 40, 42, -77, -58, -49, 18, 69, 91, -20, -65 }, "AES");
+      TEST_VECTOR_ECDSA_ONLY = new byte[] {
+          10, 56, 10, 32, 8, 2, 16, 1, 26, 3, 0, 0, 1, 50, 19, 10, 11,
+          12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28,
+          56, 5, 18, 20, 0, 99, 1, 98, 2, 97, 3, 96, 4, 95, 5, 94, 6,
+          93, 7, 92, 8, 91, 9, 90, 18, 72, 48, 70, 2, 33, 0, -79, 59, 50,
+          21, 54, 61, -92, 77, -34, -77, -45, -105, 107, -28, -19, 91, -78, 120, 68, 33,
+          11, -76, -1, 50, 64, -127, -78, 6, 108, 115, -13, 126, 2, 33, 0, -72, -44,
+          52, 93, 105, 109, -127, -111, 11, 33, -111, 97, -114, 9, 117, -68, -45, 64, 63,
+          43, 60, -44, -89, -107, -59, -45, 56, 100, -66, -40, 46, -60 };
+      TEST_VECTOR_ECDSA_AND_AES = new byte[] {
+          10, 107, 10, 55, 8, 2, 16, 2, 26, 3, 0, 0, 1, 34, 3, -1, -1,
+          0, 42, 16, -86, 16, 55, -8, -85, -47, -77, -36, -127, 44, -10, -44, -63, 115,
+          -111, 26, 50, 19, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,
+          23, 24, 25, 26, 27, 28, 56, 5, 18, 48, -110, 23, -67, 122, -118, 96, -4,
+          32, -113, -104, -107, -16, 76, 37, -61, -67, -63, 90, 38, 96, -47, -105, 56, -34,
+          50, -30, 82, 25, 100, 36, 69, 50, 68, 60, 38, 96, -108, -49, -73, -10, -62,
+          -76, -45, -105, -86, 93, 28, 34, 18, 70, 48, 68, 2, 33, 0, -87, -103, 11,
+          -70, 34, 33, -41, 90, -83, -74, 19, -13, 127, -43, -116, -32, 88, -13, 125, -122,
+          56, -21, 79, 47, 101, 89, -80, -43, 102, 92, 4, -15, 2, 31, 109, -69, 35,
+          21, 44, -27, -77, 32, 17, -90, -68, 113, 55, -24, -122, 40, 81, 51, 0, -84,
+          -29, -12, -26, 73, 105, -32, 116, -28, 84, -116, -117 };
+      TEST_VECTOR_HMAC_AND_AES_SAME_KEYS = new byte[] {
+          10, 91, 10, 55, 8, 1, 16, 2, 26, 3, 0, 0, 1, 34, 3, -1, -1,
+          0, 42, 16, -110, 48, 67, 67, -31, 24, -42, 13, -44, -109, 6, 113, 34, -70,
+          121, 6, 50, 19, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,
+          23, 24, 25, 26, 27, 28, 56, 5, 18, 32, -44, -102, -16, 123, 113, -75, 88,
+          -33, 118, 25, 60, -65, 109, 26, -70, -123, 58, -114, 126, 8, 106, -28, 65, -38,
+          -4, 68, -78, -91, 49, -13, 22, -122, 18, 32, 20, -120, -113, -76, 85, -35, -53,
+          37, -18, 66, -38, 32, 10, 30, 89, 112, -39, -27, 24, 93, -36, -100, -127, -79,
+          94, -7, -19, -41, -47, -29, 1, 12 };
+      TEST_VECTOR_HMAC_AND_AES_DIFFERENT_KEYS = new byte[] {
+          10, 107, 10, 55, 8, 1, 16, 2, 26, 3, 0, 0, 1, 34, 3, -1, -1,
+          0, 42, 16, -96, -7, 39, 79, -37, 40, 1, -30, 97, 0, 123, -7, -124, -75,
+          -127, -18, 50, 19, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,
+          23, 24, 25, 26, 27, 28, 56, 5, 18, 48, 90, 40, -48, -113, 84, -32, 47,
+          98, 54, -128, 127, 115, 32, 87, -86, 4, -26, 99, 9, -88, 13, 77, 127, 114,
+          -48, -117, -94, 96, -86, -105, -123, 11, 116, -69, -83, -110, 3, -10, 0, -34, 72,
+          10, -58, 3, -119, -94, 23, -114, 18, 32, -25, -126, 95, 125, -110, -62, -36, -78,
+          97, 72, -54, -114, 97, -68, -46, 107, 53, 55, -57, 88, 127, -20, -23, 80, -9,
+          -91, 115, 42, 24, 49, -76, -111 };
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+}
diff --git a/src/main/javatest/com/google/security/cryptauth/lib/securemessage/SecureMessageTest.java b/src/main/javatest/com/google/security/cryptauth/lib/securemessage/SecureMessageTest.java
new file mode 100644
index 0000000..40e5091
--- /dev/null
+++ b/src/main/javatest/com/google/security/cryptauth/lib/securemessage/SecureMessageTest.java
@@ -0,0 +1,766 @@
+// Copyright 2020 Google LLC
+//
+// 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
+//
+//     https://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.google.security.cryptauth.lib.securemessage;
+
+import com.google.protobuf.ByteString;
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.UninitializedMessageException;
+import com.google.security.cryptauth.lib.securemessage.CryptoOps.EncType;
+import com.google.security.cryptauth.lib.securemessage.CryptoOps.SigType;
+import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.EcP256PublicKey;
+import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.GenericPublicKey;
+import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.Header;
+import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.HeaderAndBody;
+import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.SecureMessage;
+import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.SimpleRsaPublicKey;
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.security.KeyPair;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+import java.security.SignatureException;
+import java.security.spec.InvalidKeySpecException;
+import java.util.Arrays;
+import java.util.List;
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+import junit.framework.TestCase;
+
+/**
+ * Tests for the SecureMessageBuilder and SecureMessageParser classes.
+ */
+public class SecureMessageTest extends TestCase {
+  // Not to be used when generating cross-platform test vectors (due to default charset encoding)
+  public static final byte[] TEST_MESSAGE =
+      "Testing 1 2 3... Testing 1 2 3... Testing 1 2 3...".getBytes();
+
+  private static final byte[] TEST_KEY_ID =
+    { 0, 1, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
+  // Not to be used when generating cross-platform test vectors (due to default charset encoding)
+  private static final byte[] TEST_METADATA = "Some protocol metadata string goes here".getBytes();
+  private static final byte[] TEST_ASSOCIATED_DATA = {
+    1, 0, 2, 0, 3, 0, 4, 0, 5, 0, 6, 0, 7, 0, 8, 0, 9, 0, 10, 0, 11 };
+  private static final byte[] ZERO_BYTE = { 0 };
+  private static final byte[] EMPTY_BYTES = { };
+
+  private static final List<byte[]> MESSAGE_VALUES = Arrays.asList(
+      EMPTY_BYTES,
+      TEST_MESSAGE
+      );
+
+  private static final List<byte[]> KEY_ID_VALUES = Arrays.asList(
+      null,
+      EMPTY_BYTES,
+      TEST_KEY_ID);
+
+  private static final List<byte[]> METADATA_VALUES = Arrays.asList(
+      null,
+      EMPTY_BYTES,
+      TEST_METADATA
+      );
+
+  private static final List<byte[]> ASSOCIATED_DATA_VALUES = Arrays.asList(
+      null,
+      ZERO_BYTE,
+      TEST_ASSOCIATED_DATA);
+
+  private byte[] message;
+  private byte[] metadata;
+  private byte[] verificationKeyId;
+  private byte[] decryptionKeyId;
+  private byte[] associatedData;
+  private PublicKey ecPublicKey;
+  private PrivateKey ecPrivateKey;
+  private PublicKey rsaPublicKey;
+  private PrivateKey rsaPrivateKey;
+  private SecretKey aesEncryptionKey;
+  private SecretKey hmacKey;
+  private SecureMessageBuilder secureMessageBuilder = new SecureMessageBuilder();
+  private SecureRandom rng = new SecureRandom();
+
+  @Override
+  public void setUp() {
+    message = TEST_MESSAGE;
+    metadata = null;
+    verificationKeyId = null;
+    decryptionKeyId = null;
+    associatedData = null;
+    if (!PublicKeyProtoUtil.isLegacyCryptoRequired()) {
+      KeyPair ecKeyPair = PublicKeyProtoUtil.generateEcP256KeyPair();
+      ecPublicKey = ecKeyPair.getPublic();
+      ecPrivateKey = ecKeyPair.getPrivate();
+    }
+    KeyPair rsaKeyPair = PublicKeyProtoUtil.generateRSA2048KeyPair();
+    rsaPublicKey = rsaKeyPair.getPublic();
+    rsaPrivateKey = rsaKeyPair.getPrivate();
+    try {
+      aesEncryptionKey = makeAesKey();
+      hmacKey = makeAesKey();
+    } catch (NoSuchAlgorithmException e) {
+      e.printStackTrace();
+      fail();
+    }
+    secureMessageBuilder.reset();
+  }
+
+  private SecureMessage sign(SigType sigType) throws NoSuchAlgorithmException, InvalidKeyException {
+    return getPreconfiguredBuilder().buildSignedCleartextMessage(
+        getSigningKeyFor(sigType), sigType, message);
+  }
+
+  private SecureMessage signCrypt(SigType sigType, EncType encType)
+      throws NoSuchAlgorithmException, InvalidKeyException {
+    return getPreconfiguredBuilder().buildSignCryptedMessage(
+        getSigningKeyFor(sigType), sigType, aesEncryptionKey, encType, message);
+  }
+
+  private void verify(SecureMessage signed, SigType sigType)
+      throws NoSuchAlgorithmException, InvalidKeyException, SignatureException,
+      InvalidProtocolBufferException {
+    HeaderAndBody headerAndBody = SecureMessageParser.parseSignedCleartextMessage(
+        signed,
+        getVerificationKeyFor(sigType),
+        sigType,
+        associatedData);
+    consistencyCheck(signed, headerAndBody, sigType, EncType.NONE);
+  }
+
+  private void verifyDecrypt(SecureMessage encryptedAndSigned, SigType sigType, EncType encType)
+      throws InvalidProtocolBufferException, InvalidKeyException, NoSuchAlgorithmException,
+      SignatureException {
+    HeaderAndBody headerAndBody = SecureMessageParser.parseSignCryptedMessage(
+        encryptedAndSigned,
+        getVerificationKeyFor(sigType),
+        sigType,
+        aesEncryptionKey,
+        encType,
+        associatedData);
+    consistencyCheck(encryptedAndSigned, headerAndBody, sigType, encType);
+  }
+
+  // A collection of different kinds of "alterations" that can be made to SecureMessage protos.
+  enum Alteration {
+    DECRYPTION_KEY_ID,
+    ENCTYPE,
+    HEADER_AND_BODY_PROTO,
+    MESSAGE,
+    METADATA,
+    RESIGNCRYPTION_ATTACK,
+    SIGTYPE,
+    VERIFICATION_KEY_ID,
+    ASSOCIATED_DATA_LENGTH,
+  }
+
+  private void doSignAndVerify(SigType sigType) throws Exception {
+    System.out.println("BEGIN_TEST -- Testing SigType: " + sigType + " with:");
+    System.out.println("VerificationKeyId: " + Arrays.toString(verificationKeyId));
+    System.out.println("Metadata: " + Arrays.toString(metadata));
+    System.out.println("AssociatedData: " + Arrays.toString(associatedData));
+    System.out.println("Message: " + Arrays.toString(message));
+    // Positive test cases
+    SecureMessage signed = sign(sigType);
+    verify(signed, sigType);
+
+    // Negative test cases
+    for (Alteration altType : getAlterationsToTest()) {
+      System.out.println("Testing alteration: " + altType.toString());
+      SecureMessage modified = modifyMessage(signed, altType);
+      try {
+        verify(modified, sigType);
+        fail(altType.toString());
+      } catch (SignatureException e) {
+        // We expect this
+      }
+    }
+
+    // Try verifying with the wrong associated data
+    if ((associatedData == null) || (associatedData.length == 0)) {
+      associatedData = ZERO_BYTE;
+    } else {
+      associatedData = null;
+    }
+    try {
+      verify(signed, sigType);
+      fail("Expected verification to fail due to incorrect associatedData");
+    } catch (SignatureException e) {
+      // We expect this
+    }
+
+    System.out.println("PASS_TEST -- Testing SigType: " + sigType);
+  }
+
+  private List<Alteration> getAlterationsToTest() {
+    if (isRunningInAndroid()) {
+      // Android is very slow. Only try one alteration attack, intead of all of them.
+      int randomAlteration = Math.abs(rng.nextInt()) % Alteration.values().length;
+      return Arrays.asList(Alteration.values()[randomAlteration]);
+    } else {
+      // Just try all of them
+      return Arrays.asList(Alteration.values());
+    }
+  }
+
+  private void doSignCryptAndVerifyDecrypt(SigType sigType) throws Exception {
+    // For now, EncType is always AES_256_CBC
+    EncType encType = EncType.AES_256_CBC;
+    System.out.println("BEGIN_TEST -- Testing SigType: " + sigType
+        + " EncType: " + encType + " with:");
+    System.out.println("DecryptionKeyId: " + Arrays.toString(decryptionKeyId));
+    System.out.println("VerificationKeyId: " + Arrays.toString(verificationKeyId));
+    System.out.println("Metadata: " + Arrays.toString(metadata));
+    System.out.println("AssociatedData: " + Arrays.toString(associatedData));
+    System.out.println("Message: " + Arrays.toString(message));
+    SecureMessage encryptedAndSigned = null;
+    encryptedAndSigned = signCrypt(sigType, encType);
+    verifyDecrypt(encryptedAndSigned, sigType, encType);
+
+    // Negative test cases
+    for (Alteration altType : getAlterationsToTest()) {
+      if (skipAlterationTestFor(altType, sigType)) {
+        System.out.println("Skipping alteration test: " + altType.toString());
+        continue;
+      }
+
+      System.out.println("Testing alteration: " + altType.toString());
+      SecureMessage modified = modifyMessage(encryptedAndSigned, altType);
+      try {
+        verifyDecrypt(modified, sigType, encType);
+        fail();
+      } catch (SignatureException e) {
+        // We expect this
+      }
+    }
+    System.out.println("PASS_TEST -- Testing SigType: " + sigType + " EncType: " + encType);
+  }
+
+  private boolean skipAlterationTestFor(Alteration altType, SigType sigType) {
+    // The RESIGNCRYPTION_ATTACK may be allowed to succeed iff the same symmetric key
+    // is being reused for both signature and encryption.
+    return (altType == Alteration.RESIGNCRYPTION_ATTACK)
+        // Intentionally testing equality of object address here
+        && (getVerificationKeyFor(sigType) == aesEncryptionKey);
+  }
+
+  private SecureMessage modifyMessage(SecureMessage original, Alteration altType) throws Exception {
+    ByteString bogus = ByteString.copyFromUtf8("BOGUS");
+    HeaderAndBody origHAB = HeaderAndBody.parseFrom(original.getHeaderAndBody());
+    HeaderAndBody.Builder newHAB = HeaderAndBody.newBuilder(origHAB);
+    Header.Builder newHeader = Header.newBuilder(origHAB.getHeader());
+    Header origHeader = origHAB.getHeader();
+    SecureMessage.Builder result = SecureMessage.newBuilder(original);
+    switch (altType) {
+      case DECRYPTION_KEY_ID:
+        if (origHeader.hasDecryptionKeyId()) {
+          newHeader.clearDecryptionKeyId();
+        } else {
+          newHeader.setDecryptionKeyId(ByteString.copyFrom(TEST_KEY_ID));
+        }
+        break;
+      case ENCTYPE:
+        if (origHeader.getEncryptionScheme() == SecureMessageProto.EncScheme.NONE) {
+          newHeader.setEncryptionScheme(SecureMessageProto.EncScheme.AES_256_CBC);
+        } else {
+          newHeader.setEncryptionScheme(SecureMessageProto.EncScheme.NONE);
+        }
+        break;
+      case HEADER_AND_BODY_PROTO:
+        // Substitute a junk byte string instead of the HeeaderAndBody proto message
+        return result.setHeaderAndBody(bogus).build();
+      case MESSAGE:
+        byte[] origBody = origHAB.getBody().toByteArray();
+        if (origBody.length > 0) {
+          // Lop off trailing byte of the body
+          byte[] truncatedBody = CryptoOps.subarray(origBody, 0, origBody.length - 1);
+          newHAB.setBody(ByteString.copyFrom(truncatedBody));
+        } else {
+          newHAB.setBody(bogus);
+        }
+        break;
+      case METADATA:
+        if (origHeader.hasPublicMetadata()) {
+          newHeader.clearPublicMetadata();
+        } else {
+          newHeader.setPublicMetadata(bogus);
+        }
+        break;
+      case RESIGNCRYPTION_ATTACK:
+        // Simulate stripping a signature, and re-signing a message to see if it will be decrypted.
+        newHeader
+            .setVerificationKeyId(bogus)
+            // In case original was cleartext
+            .setEncryptionScheme(SecureMessageProto.EncScheme.AES_256_CBC);
+
+        // Now that we've mildly changed the header, compute a new signature for it.
+        newHAB.setHeader(newHeader.build());
+        byte[] headerAndBodyBytes = newHAB.build().toByteArray();
+        result.setHeaderAndBody(ByteString.copyFrom(headerAndBodyBytes));
+        SigType sigType = SigType.valueOf(origHeader.getSignatureScheme());
+        // Note that in all cases where this attack applies, the associatedData is not normally
+        // used directly inside the signature (but rather inside the inner ciphertext).
+        result.setSignature(ByteString.copyFrom(CryptoOps.sign(
+            sigType, getSigningKeyFor(sigType), rng, headerAndBodyBytes)));
+        return result.build();
+      case SIGTYPE:
+        if (origHeader.getSignatureScheme() == SecureMessageProto.SigScheme.ECDSA_P256_SHA256) {
+          newHeader
+              .setSignatureScheme(SecureMessageProto.SigScheme.HMAC_SHA256);
+        } else {
+          newHeader
+              .setSignatureScheme(SecureMessageProto.SigScheme.ECDSA_P256_SHA256);
+        }
+        break;
+      case VERIFICATION_KEY_ID:
+        if (origHeader.hasVerificationKeyId()) {
+          newHeader.clearVerificationKeyId();
+        } else {
+          newHeader.setVerificationKeyId(
+              ByteString.copyFrom(TEST_KEY_ID));
+        }
+        break;
+      case ASSOCIATED_DATA_LENGTH:
+        int adLength = origHeader.getAssociatedDataLength();
+        switch (adLength) {
+          case 0:
+            newHeader.setAssociatedDataLength(1);
+            break;
+          case 1:
+            newHeader.setAssociatedDataLength(0);
+            break;
+          default:
+            newHeader.setAssociatedDataLength(adLength - 1);
+        }
+        break;
+      default:
+        fail("Forgot to implement an alteration attack: " + altType);
+        break;
+    }
+    // Set the header.
+    newHAB.setHeader(newHeader.build());
+
+    return result.setHeaderAndBody(ByteString.copyFrom(newHAB.build().toByteArray()))
+        .build();
+  }
+
+  public void testEcDsaSignedOnly() throws Exception {
+    doTestSignedOnly(SigType.ECDSA_P256_SHA256);
+  }
+
+  public void testRsaSignedOnly() throws Exception {
+    doTestSignedOnly(SigType.RSA2048_SHA256);
+  }
+
+  public void testHmacSignedOnly() throws Exception {
+    doTestSignedOnly(SigType.HMAC_SHA256);
+  }
+
+  private void doTestSignedOnly(SigType sigType) throws Exception {
+    if (isUnsupported(sigType)) {
+      return;
+    }
+
+    // decryptionKeyId must be left null for signature-only operation
+    for (byte[] vkId : KEY_ID_VALUES) {
+      verificationKeyId = vkId;
+      for (byte[] md : METADATA_VALUES) {
+        metadata = md;
+        for (byte[] ad : ASSOCIATED_DATA_VALUES) {
+          associatedData = ad;
+          for (byte[] msg : MESSAGE_VALUES) {
+            message = msg;
+            doSignAndVerify(sigType);
+          }
+        }
+      }
+    }
+
+    // Test that use of a DecryptionKeyId is not allowed for signature-only
+    try {
+      decryptionKeyId = TEST_KEY_ID;  // Should trigger a failure
+      doSignAndVerify(sigType);
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+}
+
+  public void testEncryptedAndMACed() throws Exception {
+    for (byte[] dkId : KEY_ID_VALUES) {
+      decryptionKeyId = dkId;
+      for (byte[] vkId : KEY_ID_VALUES) {
+        verificationKeyId = vkId;
+        for (byte[] md : METADATA_VALUES) {
+          metadata = md;
+          for (byte[] ad : ASSOCIATED_DATA_VALUES) {
+            associatedData = ad;
+            for (byte[] msg : MESSAGE_VALUES) {
+              message = msg;
+              doSignCryptAndVerifyDecrypt(SigType.HMAC_SHA256);
+            }
+          }
+        }
+      }
+    }
+  }
+
+  public void testEncryptedAndMACedWithSameKey() throws Exception {
+    hmacKey = aesEncryptionKey; // Re-use the same key for both
+    testEncryptedAndMACed();
+  }
+
+  public void testEncryptedAndEcdsaSigned() throws Exception {
+    doTestEncryptedAndSigned(SigType.ECDSA_P256_SHA256);
+  }
+
+  public void testEncryptedAndRsaSigned() throws Exception {
+    doTestEncryptedAndSigned(SigType.RSA2048_SHA256);
+  }
+
+  public void doTestEncryptedAndSigned(SigType sigType) throws Exception {
+    if (isUnsupported(sigType)) {
+      return;  // EC operations aren't supported on older Android releases
+    }
+
+    for (byte[] dkId : KEY_ID_VALUES) {
+      decryptionKeyId = dkId;
+      for (byte[] vkId : KEY_ID_VALUES) {
+        verificationKeyId = vkId;
+        if ((verificationKeyId == null) && sigType.isPublicKeyScheme()) {
+          continue;  // Null verificationKeyId is not allowed with public key signcryption
+        }
+        for (byte[] md : METADATA_VALUES) {
+          metadata = md;
+          for (byte[] ad : ASSOCIATED_DATA_VALUES) {
+            associatedData = ad;
+            for (byte[] msg : MESSAGE_VALUES) {
+              message = msg;
+              doSignCryptAndVerifyDecrypt(sigType);
+            }
+          }
+        }
+      }
+    }
+
+    // Verify that a missing verificationKeyId is not allowed here
+    try {
+      verificationKeyId = null;  // Should trigger a failure
+      signCrypt(sigType, EncType.AES_256_CBC);
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+  }
+
+  public void testSignCryptionRequiresEncryption() throws Exception {
+    try {
+      signCrypt(SigType.RSA2048_SHA256, EncType.NONE);
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  public void testAssociatedData() throws Exception {
+    // How much extra room might the encoding of AssociatedDataLength take up?
+    int maxAssociatedDataOverheadBytes = 4;
+    // How many bytes might normally vary in the encoding length for SecureMessages generated with
+    // fresh randomness but identical contents (e.g., due to MSBs being 0)
+    int maxJitter = 2;
+    verificationKeyId = TEST_KEY_ID;  // So that public key signcryption will work
+    message = TEST_MESSAGE;
+
+    for (SigType sigType : SigType.values()) {
+      if (isUnsupported(sigType)) {
+        continue;
+      }
+      associatedData = null;
+      SecureMessage signed = sign(sigType);
+      int signedLength = signed.toByteArray().length;
+      associatedData = EMPTY_BYTES;
+      // Check that EMPTY_BYTES is equivalent to null associated data under verification
+      verify(signed, sigType);
+      // We already tested that incorrect associated data fails elsewhere in negative test cases
+      associatedData = TEST_ASSOCIATED_DATA;
+      SecureMessage signedWithAssociatedData = sign(sigType);
+      int signedWithAssociatedDataLength = signedWithAssociatedData.toByteArray().length;
+      String logInfo = "Testing associated data overhead for signature using: " + sigType
+          + " signedLength=" + signedLength
+          + " signedWithAssociatedDataLength=" + signedWithAssociatedDataLength;
+      System.out.println(logInfo);
+      assertTrue(logInfo,
+          signedWithAssociatedData.toByteArray().length
+          <= signed.toByteArray().length + maxAssociatedDataOverheadBytes + maxJitter);
+    }
+
+    for (SigType sigType : SigType.values()) {
+      if (isUnsupported(sigType)) {
+        continue;
+      }
+      associatedData = null;
+      SecureMessage signCrypted = signCrypt(sigType, EncType.AES_256_CBC);
+      int signCryptedLength = signCrypted.toByteArray().length;
+      // Check that EMPTY_BYTES is equivalent to null associated data under verification
+      associatedData = EMPTY_BYTES;
+      verifyDecrypt(signCrypted, sigType, EncType.AES_256_CBC);
+      // We already tested that incorrect associated data fails elsewhere in negative test cases
+      associatedData = TEST_ASSOCIATED_DATA;
+      SecureMessage signCryptedWithAssociatedData = signCrypt(sigType, EncType.AES_256_CBC);
+      int signCryptedWithAssociatedDataLength = signCryptedWithAssociatedData.toByteArray().length;
+      String logInfo = "Testing associated data overhead for signcryption using: " + sigType
+          + " signCryptedLength=" + signCryptedLength
+          + " signCryptedWithAssociatedDataLength=" + signCryptedWithAssociatedDataLength;
+      System.out.println(logInfo);
+      assertTrue(logInfo,
+          signCryptedWithAssociatedData.toByteArray().length
+          <= signCrypted.toByteArray().length + maxAssociatedDataOverheadBytes + maxJitter);
+    }
+  }
+
+  public void testEncryptedAndEcdsaSignedUsingPublicKeyProto() throws Exception {
+    if (isUnsupported(SigType.ECDSA_P256_SHA256)) {
+      return;
+    }
+
+    // Safest usage of SignCryption is to set the VerificationKeyId to an actual representation of
+    // the verification key.
+    verificationKeyId = PublicKeyProtoUtil.encodeEcPublicKey(ecPublicKey).toByteArray();
+    SecureMessage encryptedAndSigned = signCrypt(SigType.ECDSA_P256_SHA256, EncType.AES_256_CBC);
+
+    // Simulate extracting the verification key ID from the SecureMessage (non-standard usage)
+    ecPublicKey =
+        PublicKeyProtoUtil.parseEcPublicKey(
+            EcP256PublicKey.parseFrom(
+                SecureMessageParser.getUnverifiedHeader(encryptedAndSigned)
+                    .getVerificationKeyId()));
+
+    // Note that this verification uses the encoded/decoded ecPublicKey value
+    verifyDecrypt(encryptedAndSigned, SigType.ECDSA_P256_SHA256, EncType.AES_256_CBC);
+  }
+
+  public void testEncryptedAndRsaSignedUsingPublicKeyProto() throws Exception {
+    // Safest usage of SignCryption is to set the VerificationKeyId to an actual representation of
+    // the verification key.
+    verificationKeyId = PublicKeyProtoUtil.encodeRsa2048PublicKey(rsaPublicKey).toByteArray();
+    SecureMessage encryptedAndSigned = signCrypt(SigType.RSA2048_SHA256, EncType.AES_256_CBC);
+
+    // Simulate extracting the verification key ID from the SecureMessage (non-standard usage)
+    rsaPublicKey =
+        PublicKeyProtoUtil.parseRsa2048PublicKey(
+            SimpleRsaPublicKey.parseFrom(
+                SecureMessageParser.getUnverifiedHeader(encryptedAndSigned)
+                    .getVerificationKeyId()));
+
+    // Note that this verification uses the encoded/decoded SimpleRsaPublicKey value
+    verifyDecrypt(encryptedAndSigned, SigType.RSA2048_SHA256, EncType.AES_256_CBC);
+  }
+
+  // TODO(shabsi): The test was only corrupting header but wasn't setting the body. With protolite,
+  // not setting a required field causes problems. Modify the SecureMessageParser test and
+  // enable/remove this test.
+  /*
+  public void testCorruptUnverifiedHeader() throws Exception {
+    // Create a sample message
+    SecureMessage original = signCrypt(SigType.HMAC_SHA256, EncType.AES_256_CBC);
+    HeaderAndBody originalHAB = HeaderAndBody.parseFrom(original.getHeaderAndBody().toByteArray());
+    for (CorruptHeaderType corruptionType : CorruptHeaderType.values()) {
+      // Mess with the HeaderAndBody field
+      HeaderAndBody.Builder corruptHAB = HeaderAndBody.newBuilder(originalHAB);
+      try {
+        corruptHeaderWith(corruptionType, corruptHAB);
+        // Construct the corrupted message using the modified HeaderAndBody
+        SecureMessage.Builder corrupt = SecureMessage.newBuilder(original);
+          corrupt.setHeaderAndBody(ByteString.copyFrom(corruptHAB.build().toByteArray())).build();
+          SecureMessageParser.getUnverifiedHeader(corrupt.build());
+          fail("Corrupt header type " + corruptionType + " parsed without error");
+      } catch (InvalidProtocolBufferException expected) {
+      }
+    }
+  }
+  */
+
+  public void testParseEmptyMessage() throws Exception {
+    byte[] bogusData = new byte[0];
+
+    try {
+      SecureMessageParser.parseSignedCleartextMessage(
+          SecureMessage.parseFrom(bogusData),
+          aesEncryptionKey,
+          SigType.HMAC_SHA256);
+      fail("Empty message verified without error");
+    } catch (SignatureException | UninitializedMessageException
+        | InvalidProtocolBufferException expected) {
+    }
+  }
+
+  public void testParseKeyInvalidInputs() throws Exception {
+    GenericPublicKey[] badKeys = new GenericPublicKey[] {
+        GenericPublicKey.newBuilder().setType(SecureMessageProto.PublicKeyType.EC_P256).build(),
+        GenericPublicKey.newBuilder().setType(SecureMessageProto.PublicKeyType.RSA2048).build(),
+        GenericPublicKey.newBuilder().setType(SecureMessageProto.PublicKeyType.DH2048_MODP).build(),
+    };
+    for (int i = 0; i < badKeys.length; i++) {
+      GenericPublicKey key = badKeys[i];
+      try {
+        PublicKeyProtoUtil.parsePublicKey(key);
+        fail(String.format("%sth key was parsed without exceptions", i));
+      } catch (InvalidKeySpecException expected) {
+      }
+    }
+  }
+
+  enum CorruptHeaderType {
+    EMPTY,
+    // TODO(shabsi): Remove these test cases and modify code in SecureMessageParser appropriately.
+    // UNSET,
+    // JUNK,
+  }
+
+  private void corruptHeaderWith(CorruptHeaderType corruptionType,
+      HeaderAndBody.Builder protoToModify) {
+    switch (corruptionType) {
+      case EMPTY:
+        protoToModify.setHeader(Header.getDefaultInstance());
+        break;
+      /*
+      case JUNK:
+        Header.Builder junk = Header.newBuilder();
+        junk.setDecryptionKeyId(ByteString.copyFromUtf8("fooooo"));
+        junk.setIv(ByteString.copyFromUtf8("bar"));
+        // Don't set signature scheme.
+        junk.setVerificationKeyId(ByteString.copyFromUtf8("bazzzzz"));
+        protoToModify.setHeader(junk.build());
+        break;
+      case UNSET:
+        protoToModify.clearHeader();
+        break;
+      */
+      default:
+        throw new RuntimeException("Broken test code");
+    }
+  }
+
+  private void consistencyCheck(
+      SecureMessage secmsg, HeaderAndBody headerAndBody, SigType sigType, EncType encType)
+      throws InvalidProtocolBufferException {
+    Header header = SecureMessageParser.getUnverifiedHeader(secmsg);
+    checkHeader(header, sigType, encType);  // Checks that the "unverified header" looks right
+    checkHeaderAndBody(header, headerAndBody);  // Matches header vs. the "verified" headerAndBody
+  }
+
+  private Header checkHeader(Header header, SigType sigType, EncType encType) {
+    assertEquals(sigType.getSigScheme(), header.getSignatureScheme());
+    assertEquals(encType.getEncScheme(), header.getEncryptionScheme());
+    checkKeyIdsAndMetadata(verificationKeyId, decryptionKeyId, metadata, associatedData, header);
+    return header;
+  }
+
+  private void checkHeaderAndBody(Header header, HeaderAndBody headerAndBody) {
+    assertTrue(header.equals(headerAndBody.getHeader()));
+    assertTrue(Arrays.equals(message, headerAndBody.getBody().toByteArray()));
+  }
+
+  private void checkKeyIdsAndMetadata(byte[] verificationKeyId, byte[] decryptionKeyId,
+      byte[] metadata, byte[] associatedData, Header header) {
+    if (verificationKeyId == null) {
+      assertFalse(header.hasVerificationKeyId());
+    } else {
+      assertTrue(Arrays.equals(verificationKeyId, header.getVerificationKeyId().toByteArray()));
+    }
+    if (decryptionKeyId == null) {
+      assertFalse(header.hasDecryptionKeyId());
+    } else {
+      assertTrue(Arrays.equals(decryptionKeyId, header.getDecryptionKeyId().toByteArray()));
+    }
+    if (metadata == null) {
+      assertFalse(header.hasPublicMetadata());
+    } else {
+      assertTrue(Arrays.equals(metadata, header.getPublicMetadata().toByteArray()));
+    }
+    if (associatedData == null) {
+      assertFalse(header.hasAssociatedDataLength());
+    } else {
+      assertEquals(associatedData.length, header.getAssociatedDataLength());
+    }
+  }
+
+  private SecretKey makeAesKey() throws NoSuchAlgorithmException {
+    KeyGenerator aesKeygen = KeyGenerator.getInstance("AES");
+    aesKeygen.init(256);
+    return aesKeygen.generateKey();
+  }
+
+  private Key getSigningKeyFor(SigType sigType) {
+    if (sigType == SigType.ECDSA_P256_SHA256) {
+      return ecPrivateKey;
+    }
+    if (sigType == SigType.RSA2048_SHA256) {
+      return rsaPrivateKey;
+    }
+    if (sigType == SigType.HMAC_SHA256) {
+      return hmacKey;
+    }
+    return null;  // This should not happen
+  }
+
+  private Key getVerificationKeyFor(SigType sigType) {
+    try {
+      if (sigType == SigType.ECDSA_P256_SHA256) {
+        return PublicKeyProtoUtil.parseEcPublicKey(
+            PublicKeyProtoUtil.encodeEcPublicKey(ecPublicKey));
+      }
+      if (sigType == SigType.RSA2048_SHA256) {
+        return PublicKeyProtoUtil.parseRsa2048PublicKey(
+            PublicKeyProtoUtil.encodeRsa2048PublicKey(rsaPublicKey));
+      }
+    } catch (InvalidKeySpecException e) {
+      throw new AssertionError(e);
+    }
+
+    assertFalse(sigType.isPublicKeyScheme());
+    // For symmetric key schemes
+    return getSigningKeyFor(sigType);
+  }
+
+  private SecureMessageBuilder getPreconfiguredBuilder() {
+    // Re-use a single instance of SecureMessageBuilder for efficiency.
+    SecureMessageBuilder builder = secureMessageBuilder.reset();
+    if (verificationKeyId != null) {
+      builder.setVerificationKeyId(verificationKeyId);
+    }
+    if (decryptionKeyId != null) {
+      builder.setDecryptionKeyId(decryptionKeyId);
+    }
+    if (metadata != null) {
+      builder.setPublicMetadata(metadata);
+    }
+    if (associatedData != null) {
+      builder.setAssociatedData(associatedData);
+    }
+    return builder;
+  }
+
+  private static boolean isUnsupported(SigType sigType) {
+    // EC operations aren't supported on older Android releases
+    return PublicKeyProtoUtil.isLegacyCryptoRequired()
+        && (sigType == SigType.ECDSA_P256_SHA256);
+  }
+
+  private static boolean isRunningInAndroid() {
+    try {
+      ClassLoader.getSystemClassLoader().loadClass("android.os.Build$VERSION");
+      return true;
+    } catch (ClassNotFoundException e) {
+      // Not running on Android
+      return false;
+    }
+  }
+}
diff --git a/src/main/proto/CMakeLists.txt b/src/main/proto/CMakeLists.txt
new file mode 100644
index 0000000..cd94f3f
--- /dev/null
+++ b/src/main/proto/CMakeLists.txt
@@ -0,0 +1,32 @@
+# Copyright 2020 Google LLC
+#
+# 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
+#
+#     https://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.
+
+add_cc_proto_library(
+  proto_device_to_device_messages_cc_proto
+  PROTOS device_to_device_messages.proto
+  DEPS proto_securemessage_cc_proto
+  INCS ${CMAKE_CURRENT_BINARY_DIR}/..
+)
+
+add_cc_proto_library(
+  proto_securegcm_cc_proto
+  PROTOS securegcm.proto
+  INCS ${CMAKE_CURRENT_BINARY_DIR}/..
+)
+
+add_cc_proto_library(
+  proto_ukey_cc_proto
+  PROTOS ukey.proto
+  INCS ${CMAKE_CURRENT_BINARY_DIR}/..
+)
diff --git a/third_party/absl b/third_party/absl
new file mode 160000
index 0000000..62f05b1
--- /dev/null
+++ b/third_party/absl
@@ -0,0 +1 @@
+Subproject commit 62f05b1f57ad660e9c09e02ce7d591dcc4d0ca08
diff --git a/third_party/gtest b/third_party/gtest
new file mode 160000
index 0000000..703bd9c
--- /dev/null
+++ b/third_party/gtest
@@ -0,0 +1 @@
+Subproject commit 703bd9caab50b139428cea1aaff9974ebee5742e
diff --git a/third_party/protobuf b/third_party/protobuf
new file mode 160000
index 0000000..d0bfd52
--- /dev/null
+++ b/third_party/protobuf
@@ -0,0 +1 @@
+Subproject commit d0bfd5221182da1a7cc280f3337b5e41a89539cf
diff --git a/third_party/secure_message b/third_party/secure_message
new file mode 160000
index 0000000..e7b6988
--- /dev/null
+++ b/third_party/secure_message
@@ -0,0 +1 @@
+Subproject commit e7b6988454bc94601616fbbf0db3559f73a1ebdf