diff options
author | Alexei Czeskis <aczeskis@google.com> | 2020-06-22 16:34:57 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-06-22 16:34:57 -0700 |
commit | 4550c84830286c24f8c189cbd54edbbd986a2dd1 (patch) | |
tree | 51dd4bc8823d45453df3afec57ebbde2d8dee692 | |
parent | 8ed3a3675132bdebe8070a0744e574facc603de5 (diff) | |
parent | 65a1750598c4503cb22dd9dd6938f932fa4969c3 (diff) | |
download | ukey2-4550c84830286c24f8c189cbd54edbbd986a2dd1.tar.gz |
Merge pull request #4 from apolyudov/master
Bring ukey2 up-to-date with internal code
34 files changed, 2680 insertions, 17 deletions
@@ -1,3 +1,3 @@ build/** .gradle/** - +.idea/** diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..2624f19 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,16 @@ +[submodule "third_party/secure_message"] + path = third_party/secure_message + url = https://github.com/google/securemessage + branch = master +[submodule "third_party/gtest"] + path = third_party/gtest + url = https://github.com/google/googletest + branch = master +[submodule "third_party/protobuf"] + path = third_party/protobuf + url = https://github.com/protocolbuffers/protobuf + branch = master +[submodule "third_party/absl"] + path = third_party/absl + url = https://github.com/abseil/abseil-cpp + branch = master diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..02ddda2 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,50 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +cmake_minimum_required(VERSION 3.0.2) + +project(ukey2) + +option(ukey2_USE_LOCAL_PROTOBUF + "Use local copy of protobuf library and compiler" OFF) + +option(ukey2_USE_LOCAL_ABSL + "Use local copy of abseil-cpp library" OFF) + +include(cmake/proto_defs.cmake) +include(cmake/local_build_setup.cmake) + +if (ukey2_USE_LOCAL_PROTOBUF) + include(cmake/local_build_protobuf.cmake) +endif() + +if (ukey2_USE_LOCAL_ABSL) + if (NOT TARGET absl::base) + add_subdirectory(third_party/absl) + endif() +else() + find_package(absl REQUIRED) +endif() + +find_package(Protobuf REQUIRED) + +enable_testing() + +add_subdirectory(src/main) +if (NOT TARGET securemessage) +add_subdirectory(third_party/secure_message) +endif() +if (NOT TARGET gtest) +add_subdirectory(third_party/gtest) +endif() diff --git a/METADATA b/METADATA deleted file mode 100644 index 1548a33..0000000 --- a/METADATA +++ /dev/null @@ -1,12 +0,0 @@ -name: "Ukey2" -description: - "UKEY2 is a Diffie-Hellman based authenticated key exchange protocol." - -third_party { - url { - type: ARCHIVE - value: "https://user.git.corp.google.com/michalp/ukey2/" - } - version: "1.0" - last_upgrade_date { year: 2018 month: 12 day: 28 } -} @@ -3,7 +3,9 @@ This is not an officially supported Google product **Coathored by:** Alexei Czeskis, Thai Duong, Eduardo' Vela'' \<Nava\>, and Adam Stubblefield. -**Status:** Implemented in Java (aczeskis@google.com) +**Status:** +Implemented in Java by Alexei Czeskis (aczeskis@google.com) +Ported from Java to C++ by Tim Song (tengs@google.com) **Design reviewers:** Thai Duong, Bruno Blanchet, Martin Abadi, and Bo Wang @@ -335,7 +337,29 @@ cd ukey2 git submodule update --init --recursive ``` -## Buillding Java library and run Java Tests +# Building and tesging C++ code + +## Build +``` +cd <source root> +mkdir build; cd build +cmake -Dukey2_USE_LOCAL_PROTOBUF=ON -Dukey2_USE_LOCAL_ABSL=ON .. +make +``` +## Running C++ tests +``` +cd <source root>/build +ctest -V +``` + +# Buillding Java library and running Java Tests + +NOTE: c++ build must be completed as described above, before running java tests. +This requirement exists because Java build runs a c++/java compatibility test, and +this test depends on c++ test helper binary (found in build/src/main/cpp/test/securegcm/ukey2_test). +Gradle build does not know how to build this artifact. Java test uses a relative +path to the artifact, and expects tests to be run from <source root> as follows: + Pre-reqs: gradle 1. Create gradle wrapper for a specific gradle version. @@ -366,5 +390,3 @@ cd <source root> ``` This will build and execute all the tests. -They are expected to pass, except for the c++ interworking tests which are expected to fail because -c++ library is missing in this PR. diff --git a/build.gradle b/build.gradle index 2564533..e319422 100644 --- a/build.gradle +++ b/build.gradle @@ -1,3 +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. + apply plugin: 'java' apply plugin: 'com.google.protobuf' 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/.gradle/3.2.1/taskArtifacts/fileSnapshots.bin b/src/.gradle/3.2.1/taskArtifacts/fileSnapshots.bin Binary files differdeleted file mode 100644 index f0cc1a8..0000000 --- a/src/.gradle/3.2.1/taskArtifacts/fileSnapshots.bin +++ /dev/null diff --git a/src/.gradle/3.2.1/taskArtifacts/taskArtifacts.bin b/src/.gradle/3.2.1/taskArtifacts/taskArtifacts.bin Binary files differdeleted file mode 100644 index b1f5426..0000000 --- a/src/.gradle/3.2.1/taskArtifacts/taskArtifacts.bin +++ /dev/null diff --git a/src/.gradle/3.2.1/taskArtifacts/taskArtifacts.lock b/src/.gradle/3.2.1/taskArtifacts/taskArtifacts.lock Binary files differdeleted file mode 100644 index 6fa2a90..0000000 --- a/src/.gradle/3.2.1/taskArtifacts/taskArtifacts.lock +++ /dev/null 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..7e5973f --- /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: + // IN_PROGRESS: + // The handshake is in progress, caller should use + // |GetNextHandshakeMessage()| and |ParseHandshakeMessage()| to continue + // the handshake. + // + // VERIFICATION_NEEDED: + // 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. + // + // VERIFICATION_IN_PROGRESS: + // 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. + // + // FINISHED: + // The handshake is finished, and the caller can use + // |ToConnectionContext()| to produce a |D2DConnectionContextV1|. + // + // ALREADY_USED: + // The hanshake has already been used and should be destroyed. + // + // ERROR: + // The handshake produced an error and should be destroyed. + enum class State { + IN_PROGRESS, + VERIFICATION_NEEDED, + VERIFICATION_IN_PROGRESS, + FINISHED, + ALREADY_USED, + ERROR, + }; + + // 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..295331e --- /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::IN_PROGRESS; + case InternalState::HANDSHAKE_VERIFICATION_NEEDED: + return State::VERIFICATION_NEEDED; + case InternalState::HANDSHAKE_VERIFICATION_IN_PROGRESS: + return State::VERIFICATION_IN_PROGRESS; + case InternalState::HANDSHAKE_FINISHED: + return State::FINISHED; + case InternalState::HANDSHAKE_ALREADY_USED: + return State::ALREADY_USED; + case InternalState::HANDSHAKE_ERROR: + return State::ERROR; + default: + // Unreachable. + return State::ERROR; + } +} + +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/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/src/main/proto/securegcm.proto b/src/main/proto/securegcm.proto index 8b45755..0325f06 100644 --- a/src/main/proto/securegcm.proto +++ b/src/main/proto/securegcm.proto @@ -1,3 +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. + syntax = "proto2"; package securegcm; 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 |