diff options
author | Elliott Hughes <enh@google.com> | 2021-02-08 22:35:05 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2021-02-08 22:35:05 +0000 |
commit | 5c396b21a4bdb3bc12f5c5cdeecacbc5f1acb8c7 (patch) | |
tree | df40fdfaf3c7f073f2b61cae080cba5d5a906141 | |
parent | 66b58ac3f2e612a260d8c4167b33aed6de5b7556 (diff) | |
parent | 974bcc28302a760fc15f3df1de04ab7bfeb83e85 (diff) | |
download | ukey2-5c396b21a4bdb3bc12f5c5cdeecacbc5f1acb8c7.tar.gz |
Merge remote-tracking branch 'aosp/upstream-master' into mymerge am: 77f068fc5a am: bd0ecf8534 am: 974bcc2830
Original change: https://android-review.googlesource.com/c/platform/external/ukey2/+/1579488
MUST ONLY BE SUBMITTED BY AUTOMERGER
Change-Id: I91cc174d53d83378b55c71c0e3c49a195eabc967
44 files changed, 7363 insertions, 10 deletions
@@ -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 @@ -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() @@ -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 |