summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorElliott Hughes <enh@google.com>2021-02-08 23:33:02 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2021-02-08 23:33:02 +0000
commitd1efa50145c59b88b49b61a6558ae72577d22718 (patch)
treedf40fdfaf3c7f073f2b61cae080cba5d5a906141
parent60ff835a1231f4ca2bc97e6dca819cac9c2b5aa4 (diff)
parent5c396b21a4bdb3bc12f5c5cdeecacbc5f1acb8c7 (diff)
downloadukey2-d1efa50145c59b88b49b61a6558ae72577d22718.tar.gz
Merge remote-tracking branch 'aosp/upstream-master' into mymerge am: 77f068fc5a am: bd0ecf8534 am: 974bcc2830 am: 5c396b21a4
Original change: https://android-review.googlesource.com/c/platform/external/ukey2/+/1579488 MUST ONLY BE SUBMITTED BY AUTOMERGER Change-Id: I3400b7fcae3272905dcb01e1eeb38e867b65af71
-rw-r--r--.gitignore3
-rw-r--r--.gitmodules16
-rw-r--r--Android.bp2
-rw-r--r--CMakeLists.txt50
-rw-r--r--METADATA6
-rw-r--r--build.gradle30
-rw-r--r--cmake/local_build_protobuf.cmake42
-rw-r--r--cmake/local_build_setup.cmake26
-rw-r--r--cmake/proto_defs.cmake42
-rw-r--r--src/main/CMakeLists.txt18
-rw-r--r--src/main/cpp/CMakeLists.txt17
-rw-r--r--src/main/cpp/include/securegcm/d2d_connection_context_v1.h89
-rw-r--r--src/main/cpp/include/securegcm/d2d_crypto_ops.h78
-rw-r--r--src/main/cpp/include/securegcm/java_util.h57
-rw-r--r--src/main/cpp/include/securegcm/ukey2_handshake.h263
-rw-r--r--src/main/cpp/src/securegcm/CMakeLists.txt47
-rw-r--r--src/main/cpp/src/securegcm/d2d_connection_context_v1.cc228
-rw-r--r--src/main/cpp/src/securegcm/d2d_crypto_ops.cc151
-rw-r--r--src/main/cpp/src/securegcm/java_util.cc60
-rw-r--r--src/main/cpp/src/securegcm/ukey2_handshake.cc715
-rw-r--r--src/main/cpp/src/securegcm/ukey2_shell.cc297
-rw-r--r--src/main/cpp/test/securegcm/CMakeLists.txt31
-rw-r--r--src/main/cpp/test/securegcm/d2d_connection_context_v1_test.cc124
-rw-r--r--src/main/cpp/test/securegcm/d2d_crypto_ops_test.cc158
-rw-r--r--src/main/cpp/test/securegcm/java_util_test.cc84
-rw-r--r--src/main/javatest/com/google/security/cryptauth/lib/securegcm/D2DConnectionContextTest.java568
-rw-r--r--src/main/javatest/com/google/security/cryptauth/lib/securegcm/D2DDiffieHellmanKeyExchangeHandshakeTest.java432
-rw-r--r--src/main/javatest/com/google/security/cryptauth/lib/securegcm/Ed25519Test.java195
-rw-r--r--src/main/javatest/com/google/security/cryptauth/lib/securegcm/EnrollmentCryptoOpsTest.java134
-rw-r--r--src/main/javatest/com/google/security/cryptauth/lib/securegcm/KeyEncodingTest.java189
-rw-r--r--src/main/javatest/com/google/security/cryptauth/lib/securegcm/TransportCryptoOpsTest.java110
-rw-r--r--src/main/javatest/com/google/security/cryptauth/lib/securegcm/Ukey2CppCompatibilityTest.java124
-rw-r--r--src/main/javatest/com/google/security/cryptauth/lib/securegcm/Ukey2HandshakeTest.java818
-rw-r--r--src/main/javatest/com/google/security/cryptauth/lib/securegcm/Ukey2ShellCppWrapper.java342
-rw-r--r--src/main/javatest/com/google/security/cryptauth/lib/securemessage/CryptoOpsTest.java172
-rw-r--r--src/main/javatest/com/google/security/cryptauth/lib/securemessage/NullsGoogle3Test.java42
-rw-r--r--src/main/javatest/com/google/security/cryptauth/lib/securemessage/PublicKeyProtoUtilTest.java412
-rw-r--r--src/main/javatest/com/google/security/cryptauth/lib/securemessage/SecureMessageSimpleTestVectorTest.java403
-rw-r--r--src/main/javatest/com/google/security/cryptauth/lib/securemessage/SecureMessageTest.java766
-rw-r--r--src/main/proto/CMakeLists.txt32
m---------third_party/absl0
m---------third_party/gtest0
m---------third_party/protobuf0
m---------third_party/secure_message0
44 files changed, 7363 insertions, 10 deletions
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 074b21a..1376405 100644
--- a/Android.bp
+++ b/Android.bp
@@ -40,7 +40,7 @@ java_library {
},
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 @@ description:
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 @@ buildscript {
}
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
+Subproject 62f05b1f57ad660e9c09e02ce7d591dcc4d0ca0
diff --git a/third_party/gtest b/third_party/gtest
new file mode 160000
+Subproject 703bd9caab50b139428cea1aaff9974ebee5742
diff --git a/third_party/protobuf b/third_party/protobuf
new file mode 160000
+Subproject d0bfd5221182da1a7cc280f3337b5e41a89539c
diff --git a/third_party/secure_message b/third_party/secure_message
new file mode 160000
+Subproject e7b6988454bc94601616fbbf0db3559f73a1ebd