diff options
Diffstat (limited to 'src/main')
60 files changed, 0 insertions, 13222 deletions
diff --git a/src/main/CMakeLists.txt b/src/main/CMakeLists.txt deleted file mode 100644 index 776826c..0000000 --- a/src/main/CMakeLists.txt +++ /dev/null @@ -1,18 +0,0 @@ -# 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 deleted file mode 100644 index 919e096..0000000 --- a/src/main/cpp/CMakeLists.txt +++ /dev/null @@ -1,17 +0,0 @@ -# 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 deleted file mode 100644 index 098e654..0000000 --- a/src/main/cpp/include/securegcm/d2d_connection_context_v1.h +++ /dev/null @@ -1,89 +0,0 @@ -// 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 deleted file mode 100644 index eeeeb20..0000000 --- a/src/main/cpp/include/securegcm/d2d_crypto_ops.h +++ /dev/null @@ -1,78 +0,0 @@ -// 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 deleted file mode 100644 index 8783af4..0000000 --- a/src/main/cpp/include/securegcm/java_util.h +++ /dev/null @@ -1,57 +0,0 @@ -// 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 deleted file mode 100644 index 8455fd7..0000000 --- a/src/main/cpp/include/securegcm/ukey2_handshake.h +++ /dev/null @@ -1,263 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#ifndef SECURITY_CRYPTAUTH_LIB_SECUREGCM_UKEY2_HANDSHAKE_H_ -#define SECURITY_CRYPTAUTH_LIB_SECUREGCM_UKEY2_HANDSHAKE_H_ - -#include <map> -#include <memory> - -#include "proto/ukey.pb.h" -#include "securegcm/d2d_connection_context_v1.h" -#include "securemessage/crypto_ops.h" - -namespace securegcm { - -// Implements UKEY2 and produces a |D2DConnectionContextV1|. -// This class should be kept compatible with the Java implementation in -// //java/com/google/security/cryptauth/lib/securegcm/Ukey2Handshake.java -// -// For usage examples, see ukey2_shell.cc. This file contains a shell exercising -// both the initiator and responder handshake roles. -class UKey2Handshake { - public: - // Handshake states: - // kInProgress: - // The handshake is in progress, caller should use - // |GetNextHandshakeMessage()| and |ParseHandshakeMessage()| to continue - // the handshake. - // - // kVerificationNeeded: - // The handshake is complete, but pending verification of the - // authentication string. Clients should use |GetVerificationString()| - // to get the verification string and use out-of-band methods to - // authenticate the handshake. - // - // kVerificationInProgress: - // The handshake is complete, verification string has been generated, - // but has not been confirmed. After authenticating the handshake - // out-of-band, use |VerifyHandshake()| to mark the handshake as - // verified. - // - // kFinished: - // The handshake is finished, and the caller can use - // |ToConnectionContext()| to produce a |D2DConnectionContextV1|. - // - // kAlreadyUsed: - // The hanshake has already been used and should be destroyed. - // - // kError: - // The handshake produced an error and should be destroyed. - enum class State { - kInProgress, - kVerificationNeeded, - kVerificationInProgress, - kFinished, - kAlreadyUsed, - kError, - }; - - // Currently implemented UKEY2 handshake ciphers. Each cipher is a tuple - // consisting of a key negotiation cipher and a hash function used for a - // commitment. Currently the ciphers are: - // +-----------------------------------------------------+ - // | Enum | Key negotiation | Hash function | - // +-------------+-----------------------+---------------+ - // | P256_SHA512 | ECDH using NIST P-256 | SHA512 | - // +-----------------------------------------------------+ - // - // Note that these should correspond to values in - // device_to_device_messages.proto. - enum class HandshakeCipher : int { - // TODO(aczeskis): add CURVE25519_SHA512 - - P256_SHA512 = securegcm::P256_SHA512, - }; - - // Creates a |UKey2Handshake| with a particular |cipher| that can be used by - // an initiator / client. - static std::unique_ptr<UKey2Handshake> ForInitiator(HandshakeCipher cipher); - - // Creates a |UKey2Handshake| with a particular |cipher| that can be used by - // a responder / server. - static std::unique_ptr<UKey2Handshake> ForResponder(HandshakeCipher cipher); - - // Returns the current state of the handshake. - State GetHandshakeState() const; - - // Returns the last error message. Empty string if there was no error. - const string& GetLastError() const; - - // Gets the next handshake message suitable for sending on the wire. - // If |nullptr| is returned, check |GetLastError()| for the error message. - std::unique_ptr<string> GetNextHandshakeMessage(); - - // Parses the given |handshake_message|, updating the internal state. - struct ParseResult { - // True if |handshake_message| is parsed successfully. If |false|, call - // |GetLastError()| for the error message. - bool success; - - // May be set if parsing fails. This value should be sent to the remote - // device before disconnecting. - std::unique_ptr<string> alert_to_send; - }; - ParseResult ParseHandshakeMessage(const string& handshake_message); - - // Returns an authentication string suitable for authenticating the handshake - // out-of-band. Note that the authentication string can be short (e.g., a 6 - // digit visual confirmation code). - // - // Note: This should only be called when the state returned from - // |GetHandshakeState()| is |State::VERIFICATION_NEEDED|, which means this can - // only be called once. - // - // |byte_length|: The length of the output. Min length is 1; max length is 32. - // If |nullptr| is returned, check |GetLastError()| for the error message. - std::unique_ptr<string> GetVerificationString(int byte_length); - - // Invoked to let the handshake state machine know that caller has validated - // the authentication string obtained via |GetVerificationString()|. - // Note: This should only be called when the state returned by - // |GetHandshakeState()| is |State::VERIFICATION_IN_PROGRESS|. - // - // If |false| is returned, check |GetLastError()| for the error message. - bool VerifyHandshake(); - - // Can be called to generate a |D2DConnectionContextV1|. Returns nullptr on - // failure. - // Note: This should only be called when the state returned by - // |GetHandshakeState()| is |State::FINISHED|. - // - // If |nullptr| is returned, check |GetLastError()| for the error message. - std::unique_ptr<D2DConnectionContextV1> ToConnectionContext(); - - private: - // Enums for internal state machinery. - enum class InternalState : int { - CLIENT_START, - CLIENT_WAITING_FOR_SERVER_INIT, - CLIENT_AFTER_SERVER_INIT, - - // Responder/server state - SERVER_START, - SERVER_AFTER_CLIENT_INIT, - SERVER_WAITING_FOR_CLIENT_FINISHED, - - // Common completion state - HANDSHAKE_VERIFICATION_NEEDED, - HANDSHAKE_VERIFICATION_IN_PROGRESS, - HANDSHAKE_FINISHED, - HANDSHAKE_ALREADY_USED, - HANDSHAKE_ERROR, - }; - - // Helps us remember our role in the handshake. - enum class HandshakeRole { - CLIENT, - SERVER - }; - - // Prevent public instantiation. Callers should use |ForInitiator()| or - // |ForResponder()|. - UKey2Handshake(InternalState state, HandshakeCipher cipher); - - // Attempts to parse Ukey2ClientInit, wrapped inside a Ukey2Message. - // See go/ukey2 for details. - ParseResult ParseClientInitUkey2Message(const string& handshake_message); - - // Attempts to parse Ukey2ServerInit, wrapped inside a Ukey2Message. - // See go/ukey2 for details. - ParseResult ParseServerInitUkey2Message(const string& handshake_message); - - // Attempts to parse Ukey2ClientFinish, wrapped inside a Ukey2Message. - // See go/ukey2 for details. - ParseResult ParseClientFinishUkey2Message(const string& handshake_message); - - // Convenience function to set |last_error_| and create a ParseResult with a - // given alert. - ParseResult CreateFailedResultWithAlert(Ukey2Alert::AlertType alert_type, - const string& error_message); - - // Convenience function to set |last_error_| and create a failed ParseResult - // without an alert. - ParseResult CreateFailedResultWithoutAlert(const string& error_message); - - // Convenience function to create a successful ParseResult. - ParseResult CreateSuccessResult(); - - // Verifies that the peer's commitment stored in |peer_commitment_| is the - // same as that obtained from |handshake_message|. - bool VerifyCommitment(const string& handshake_message); - - // Generates a commitment for the P256_SHA512 cipher. - std::unique_ptr<Ukey2ClientInit::CipherCommitment> - GenerateP256Sha512Commitment(); - - // Creates a serialized Ukey2Message, wrapping an inner ClientInit message. - std::unique_ptr<string> MakeClientInitUkey2Message(); - - // Creates a serialized Ukey2Message, wrapping an inner ServerInit message. - std::unique_ptr<string> MakeServerInitUkey2Message(); - - // Creates a serialized Ukey2Message of a given |type|, wrapping |data|. - std::unique_ptr<string> MakeUkey2Message(Ukey2Message::Type type, - const string& data); - - // Called when an error occurs to set |handshake_state_| and |last_error_|. - void SetError(const string& error_message); - - // The current state of the handshake. - InternalState handshake_state_; - - // The cipher to use for the handshake. - const HandshakeCipher handshake_cipher_; - - // The role to perform, i.e. client or server. - const HandshakeRole handshake_role_; - - // A newly generated key-pair for this handshake. - std::unique_ptr<securemessage::CryptoOps::KeyPair> our_key_pair_; - - // The peer's public key retrieved from a handshake message. - std::unique_ptr<securemessage::CryptoOps::PublicKey> their_public_key_; - - // The secret key derived from |our_key_pair_| and |their_public_key_|. - std::unique_ptr<securemessage::CryptoOps::SecretKey> derived_secret_key_; - - // The raw bytes of the Ukey2ClientInit, wrapped inside a Ukey2Message. - // Empty string if not initialized. - string wrapped_client_init_; - - // The raw bytes of the Ukey2ServerInit, wrapped inside a Ukey2Message. - // Empty string if not initialized. - string wrapped_server_init_; - - // The commitment of the peer retrieved from a handshake message. Empty string - // if not initialized. - string peer_commitment_; - - // Map from ciphers to the raw bytes of message 3 (which is a wrapped - // Ukey2ClientFinished message). - // Note: Currently only one cipher is supported, so at most one entry exists - // in this map. - std::map<HandshakeCipher, string> raw_message3_map_; - - // Contains the last error message. - string last_error_; -}; - -} // namespace securegcm - -#endif // SECURITY_CRYPTAUTH_LIB_SECUREGCM_UKEY2_HANDSHAKE_H_ diff --git a/src/main/cpp/src/securegcm/CMakeLists.txt b/src/main/cpp/src/securegcm/CMakeLists.txt deleted file mode 100644 index b562756..0000000 --- a/src/main/cpp/src/securegcm/CMakeLists.txt +++ /dev/null @@ -1,47 +0,0 @@ -# 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 deleted file mode 100644 index 8a9a612..0000000 --- a/src/main/cpp/src/securegcm/d2d_connection_context_v1.cc +++ /dev/null @@ -1,228 +0,0 @@ -// 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 deleted file mode 100644 index 49f0b85..0000000 --- a/src/main/cpp/src/securegcm/d2d_crypto_ops.cc +++ /dev/null @@ -1,151 +0,0 @@ -// 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 deleted file mode 100644 index 1ce4d7b..0000000 --- a/src/main/cpp/src/securegcm/java_util.cc +++ /dev/null @@ -1,60 +0,0 @@ -// 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 deleted file mode 100644 index dc5c131..0000000 --- a/src/main/cpp/src/securegcm/ukey2_handshake.cc +++ /dev/null @@ -1,715 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#include "securegcm/ukey2_handshake.h" - -#include <sstream> - -#include "securegcm/d2d_crypto_ops.h" -#include "securemessage/public_key_proto_util.h" - -namespace securegcm { - -using securemessage::ByteBuffer; -using securemessage::CryptoOps; -using securemessage::GenericPublicKey; -using securemessage::PublicKeyProtoUtil; - -namespace { - -// Salt value used to derive client and server keys for next protocol. -const char kUkey2HkdfSalt[] = "UKEY2 v1 next"; - -// Salt value used to derive verification string. -const char kUkey2VerificationStringSalt[] = "UKEY2 v1 auth"; - -// Maximum version of the handshake supported by this class. -const uint32_t kVersion = 1; - -// Random nonce is fixed at 32 bytes (as per go/ukey2). -const uint32_t kNonceLengthInBytes = 32; - -// Currently, we only support one next protocol. -const char kNextProtocol[] = "AES_256_CBC-HMAC_SHA256"; - -// Creates the appropriate KeyPair for |cipher|. -std::unique_ptr<CryptoOps::KeyPair> GenerateKeyPair( - UKey2Handshake::HandshakeCipher cipher) { - switch (cipher) { - case UKey2Handshake::HandshakeCipher::P256_SHA512: - return CryptoOps::GenerateEcP256KeyPair(); - default: - return nullptr; - } -} - -// Parses a CryptoOps::PublicKey from a serialized GenericPublicKey. -std::unique_ptr<securemessage::CryptoOps::PublicKey> ParsePublicKey( - const string& serialized_generic_public_key) { - GenericPublicKey generic_public_key; - if (!generic_public_key.ParseFromString(serialized_generic_public_key)) { - return nullptr; - } - return PublicKeyProtoUtil::ParsePublicKey(generic_public_key); -} - -} // namespace - -// static. -std::unique_ptr<UKey2Handshake> UKey2Handshake::ForInitiator( - HandshakeCipher cipher) { - return std::unique_ptr<UKey2Handshake>( - new UKey2Handshake(InternalState::CLIENT_START, cipher)); -} - -// static. -std::unique_ptr<UKey2Handshake> UKey2Handshake::ForResponder( - HandshakeCipher cipher) { - return std::unique_ptr<UKey2Handshake>( - new UKey2Handshake(InternalState::SERVER_START, cipher)); -} - -UKey2Handshake::UKey2Handshake(InternalState state, HandshakeCipher cipher) - : handshake_state_(state), - handshake_cipher_(cipher), - handshake_role_(state == InternalState::CLIENT_START - ? HandshakeRole::CLIENT - : HandshakeRole::SERVER), - our_key_pair_(GenerateKeyPair(cipher)) {} - -UKey2Handshake::State UKey2Handshake::GetHandshakeState() const { - switch (handshake_state_) { - case InternalState::CLIENT_START: - case InternalState::CLIENT_WAITING_FOR_SERVER_INIT: - case InternalState::CLIENT_AFTER_SERVER_INIT: - case InternalState::SERVER_START: - case InternalState::SERVER_AFTER_CLIENT_INIT: - case InternalState::SERVER_WAITING_FOR_CLIENT_FINISHED: - // Fallthrough intended -- these are all in-progress states. - return State::kInProgress; - case InternalState::HANDSHAKE_VERIFICATION_NEEDED: - return State::kVerificationNeeded; - case InternalState::HANDSHAKE_VERIFICATION_IN_PROGRESS: - return State::kVerificationInProgress; - case InternalState::HANDSHAKE_FINISHED: - return State::kFinished; - case InternalState::HANDSHAKE_ALREADY_USED: - return State::kAlreadyUsed; - case InternalState::HANDSHAKE_ERROR: - return State::kError; - default: - // Unreachable. - return State::kError; - } -} - -const string& UKey2Handshake::GetLastError() const { - return last_error_; -} - -std::unique_ptr<string> UKey2Handshake::GetNextHandshakeMessage() { - switch (handshake_state_) { - case InternalState::CLIENT_START: { - std::unique_ptr<string> client_init = MakeClientInitUkey2Message(); - if (!client_init) { - // |last_error_| is already set. - return nullptr; - } - - wrapped_client_init_ = *client_init; - handshake_state_ = InternalState::CLIENT_WAITING_FOR_SERVER_INIT; - return client_init; - } - - case InternalState::SERVER_AFTER_CLIENT_INIT: { - std::unique_ptr<string> server_init = MakeServerInitUkey2Message(); - if (!server_init) { - // |last_error_| is already set. - return nullptr; - } - - wrapped_server_init_ = *server_init; - handshake_state_ = InternalState::SERVER_WAITING_FOR_CLIENT_FINISHED; - return server_init; - } - - case InternalState::CLIENT_AFTER_SERVER_INIT: { - // Make sure we have a message 3 for the chosen cipher. - if (raw_message3_map_.count(handshake_cipher_) == 0) { - std::ostringstream stream; - stream << "Client state is CLIENT_AFTER_SERVER_INIT, and cipher is " - << static_cast<int>(handshake_cipher_) - << ", but no corresponding raw " - << "[Client Finished] message has been generated."; - SetError(stream.str()); - return nullptr; - } - handshake_state_ = InternalState::HANDSHAKE_VERIFICATION_NEEDED; - return std::unique_ptr<string>( - new string(raw_message3_map_[handshake_cipher_])); - } - - default: { - std::ostringstream stream; - stream << "Cannot get next message in state " - << static_cast<int>(handshake_state_); - SetError(stream.str()); - return nullptr; - } - } -} - -UKey2Handshake::ParseResult -UKey2Handshake::ParseHandshakeMessage(const string& handshake_message) { - switch (handshake_state_) { - case InternalState::SERVER_START: - return ParseClientInitUkey2Message(handshake_message); - case InternalState::CLIENT_WAITING_FOR_SERVER_INIT: - return ParseServerInitUkey2Message(handshake_message); - case InternalState::SERVER_WAITING_FOR_CLIENT_FINISHED: - return ParseClientFinishUkey2Message(handshake_message); - default: - std::ostringstream stream; - stream << "Cannot parse message in state " - << static_cast<int>(handshake_state_); - SetError(stream.str()); - return {false, nullptr}; - } -} - -std::unique_ptr<string> UKey2Handshake::GetVerificationString(int byte_length) { - if (byte_length < 1 || byte_length > 32) { - SetError("Minimum length is 1 byte, max is 32 bytes."); - return nullptr; - } - - if (handshake_state_ != InternalState::HANDSHAKE_VERIFICATION_NEEDED) { - std::ostringstream stream; - stream << "Unexpected state: " << static_cast<int>(handshake_state_); - SetError(stream.str()); - return nullptr; - } - - if (!our_key_pair_ || !our_key_pair_->private_key || !their_public_key_) { - SetError("One of our private key or their public key is null."); - return nullptr; - } - - switch (handshake_cipher_) { - case HandshakeCipher::P256_SHA512: - derived_secret_key_ = CryptoOps::KeyAgreementSha256( - *(our_key_pair_->private_key), *their_public_key_); - break; - default: - // Unreachable. - return nullptr; - } - - if (!derived_secret_key_) { - SetError("Failed to derive shared secret key."); - return nullptr; - } - - std::unique_ptr<string> auth_string = CryptoOps::Hkdf( - derived_secret_key_->data().String(), - string(kUkey2VerificationStringSalt, sizeof(kUkey2VerificationStringSalt)), - wrapped_client_init_ + wrapped_server_init_); - - handshake_state_ = InternalState::HANDSHAKE_VERIFICATION_IN_PROGRESS; - return auth_string; -} - -bool UKey2Handshake::VerifyHandshake() { - if (handshake_state_ != InternalState::HANDSHAKE_VERIFICATION_IN_PROGRESS) { - std::ostringstream stream; - stream << "Unexpected state: " << static_cast<int>(handshake_state_); - SetError(stream.str()); - return false; - } - - handshake_state_ = InternalState::HANDSHAKE_FINISHED; - return true; -} - -std::unique_ptr<D2DConnectionContextV1> UKey2Handshake::ToConnectionContext() { - if (InternalState::HANDSHAKE_FINISHED != handshake_state_) { - std::ostringstream stream; - stream << "ToConnectionContext can only be called when handshake is " - << "completed, but current state is " - << static_cast<int>(handshake_state_); - SetError(stream.str()); - return nullptr; - } - - if (!derived_secret_key_) { - SetError("Derived key is null."); - return nullptr; - } - - string info = wrapped_client_init_ + wrapped_server_init_; - std::unique_ptr<string> master_key_data = CryptoOps::Hkdf( - derived_secret_key_->data().String(), kUkey2HkdfSalt, info); - - if (!master_key_data) { - SetError("Failed to create master key."); - return nullptr; - } - - // Derive separate encode keys for both client and server. - CryptoOps::SecretKey master_key(*master_key_data, CryptoOps::AES_256_KEY); - std::unique_ptr<CryptoOps::SecretKey> client_key = - D2DCryptoOps::DeriveNewKeyForPurpose(master_key, "client"); - std::unique_ptr<CryptoOps::SecretKey> server_key = - D2DCryptoOps::DeriveNewKeyForPurpose(master_key, "server"); - if (!client_key || !server_key) { - SetError("Failed to derive client or server key."); - return nullptr; - } - - handshake_state_ = InternalState::HANDSHAKE_ALREADY_USED; - - return std::unique_ptr<D2DConnectionContextV1>(new D2DConnectionContextV1( - handshake_role_ == HandshakeRole::CLIENT ? *client_key : *server_key, - handshake_role_ == HandshakeRole::CLIENT ? *server_key : *client_key, - 0 /* initial encode sequence number */, - 0 /* initial decode sequence number */)); -} - -UKey2Handshake::ParseResult UKey2Handshake::ParseClientInitUkey2Message( - const string& handshake_message) { - // Deserialize the protobuf. - Ukey2Message message; - if (!message.ParseFromString(handshake_message)) { - return CreateFailedResultWithAlert(Ukey2Alert::BAD_MESSAGE, - "Can't parse message 1."); - } - - // Verify that message_type == CLIENT_INIT. - if (!message.has_message_type() || - message.message_type() != Ukey2Message::CLIENT_INIT) { - return CreateFailedResultWithAlert( - Ukey2Alert::BAD_MESSAGE, - "Expected, but did not find ClientInit message type."); - } - - // Derserialize message_data as a ClientInit message. - if (!message.has_message_data()) { - return CreateFailedResultWithAlert( - Ukey2Alert::BAD_MESSAGE_DATA, - "Expected message data, but did not find it."); - } - - Ukey2ClientInit client_init; - if (!client_init.ParseFromString(message.message_data())) { - return CreateFailedResultWithAlert( - Ukey2Alert::BAD_MESSAGE_DATA, - "Can't parse message data into ClientInit."); - } - - // Check that version == VERSION. - if (!client_init.has_version()) { - return CreateFailedResultWithAlert(Ukey2Alert::BAD_VERSION, - "ClientInit missing version."); - } - if (client_init.version() != kVersion) { - return CreateFailedResultWithAlert(Ukey2Alert::BAD_VERSION, - "ClientInit version mismatch."); - } - - // Check that random is exactly kNonceLengthInBytes. - if (!client_init.has_random()) { - return CreateFailedResultWithAlert(Ukey2Alert::BAD_RANDOM, - "ClientInit missing random."); - } - if (client_init.random().length() != kNonceLengthInBytes) { - return CreateFailedResultWithAlert( - Ukey2Alert::BAD_RANDOM, "ClientInit has incorrect nonce length."); - } - - // Check to see if any of the handshake_cipher in handshake_cipher_commitment - // are acceptable. Servers should select the first ahdnshake_cipher that it - // finds acceptable to support clients signalling deprecated but supported - // HandshakeCiphers. If no handshake_cipher is acceptable (or there are no - // HandshakeCiphers in the message), the server sends a BAD_HANDSHAKE_CIPHER - // alert message. - if (client_init.cipher_commitments_size() == 0) { - return CreateFailedResultWithAlert( - Ukey2Alert::BAD_HANDSHAKE_CIPHER, - "ClientInit is missing cipher commitments."); - } - - for (const Ukey2ClientInit::CipherCommitment& commitment : - client_init.cipher_commitments()) { - if (!commitment.has_handshake_cipher() || !commitment.has_commitment() || - commitment.commitment().empty()) { - return CreateFailedResultWithAlert( - Ukey2Alert::BAD_HANDSHAKE_CIPHER, - "ClientInit has improperly formatted cipher commitment."); - } - - // TODO(aczeskis): for now we only support one cipher, eventually support - // more. - if (commitment.handshake_cipher() == static_cast<int>(handshake_cipher_)) { - peer_commitment_ = commitment.commitment(); - } - } - - if (peer_commitment_.empty()) { - return CreateFailedResultWithAlert(Ukey2Alert::BAD_HANDSHAKE_CIPHER, - "No acceptable commitments found"); - } - - // Checks that next_protocol contains a protocol that the server supports. We - // currently only support one protocol. - if (!client_init.has_next_protocol() || - client_init.next_protocol() != kNextProtocol) { - return CreateFailedResultWithAlert(Ukey2Alert::BAD_NEXT_PROTOCOL, - "Incorrect next protocol."); - } - - // Store raw message for AUTH_STRING computation. - wrapped_client_init_ = handshake_message; - handshake_state_ = InternalState::SERVER_AFTER_CLIENT_INIT; - return CreateSuccessResult(); -} - -UKey2Handshake::ParseResult UKey2Handshake::ParseServerInitUkey2Message( - const string& handshake_message) { - // Deserialize the protobuf. - Ukey2Message message; - if (!message.ParseFromString(handshake_message)) { - return CreateFailedResultWithAlert(Ukey2Alert::BAD_MESSAGE, - "Can't parse message 2."); - } - - // Verify that message_type == SERVER_INIT. - if (!message.has_message_type() || - message.message_type() != Ukey2Message::SERVER_INIT) { - return CreateFailedResultWithAlert( - Ukey2Alert::BAD_MESSAGE, - "Expected, but did not find SERVER_INIT message type."); - } - - // Derserialize message_data as a ServerInit message. - if (!message.has_message_data()) { - return CreateFailedResultWithAlert( - Ukey2Alert::BAD_MESSAGE_DATA, - "Expected message data, but did not find it."); - } - - Ukey2ServerInit server_init; - if (!server_init.ParseFromString(message.message_data())) { - return CreateFailedResultWithAlert( - Ukey2Alert::BAD_MESSAGE_DATA, - "Can't parse message data into ServerInit."); - } - - // Check that version == VERSION. - if (!server_init.has_version()) { - return CreateFailedResultWithAlert(Ukey2Alert::BAD_VERSION, - "ServerInit missing version."); - } - if (server_init.version() != kVersion) { - return CreateFailedResultWithAlert(Ukey2Alert::BAD_VERSION, - "ServerInit version mismatch."); - } - - // Check that random is exactly kNonceLengthInBytes. - if (!server_init.has_random()) { - return CreateFailedResultWithAlert(Ukey2Alert::BAD_RANDOM, - "ServerInit missing random."); - } - if (server_init.random().length() != kNonceLengthInBytes) { - return CreateFailedResultWithAlert( - Ukey2Alert::BAD_RANDOM, "ServerInit has incorrect nonce length."); - } - - // Check that the handshake_cipher matches a handshake cipher that was sent in - // ClientInit::cipher_commitments(). - if (!server_init.has_handshake_cipher()) { - return CreateFailedResultWithAlert(Ukey2Alert::BAD_HANDSHAKE_CIPHER, - "No handshake cipher found."); - } - - Ukey2HandshakeCipher cipher = server_init.handshake_cipher(); - HandshakeCipher server_cipher; - switch (static_cast<HandshakeCipher>(cipher)) { - case HandshakeCipher::P256_SHA512: - server_cipher = static_cast<HandshakeCipher>(cipher); - break; - default: - return CreateFailedResultWithAlert(Ukey2Alert::BAD_HANDSHAKE_CIPHER, - "No acceptable handshake found."); - } - - // Check that public_key parses into a correct public key structure. - if (!server_init.has_public_key()) { - return CreateFailedResultWithAlert(Ukey2Alert::BAD_PUBLIC_KEY, - "No public key found in ServerInit."); - } - - their_public_key_ = ParsePublicKey(server_init.public_key()); - if (!their_public_key_) { - return CreateFailedResultWithAlert(Ukey2Alert::BAD_PUBLIC_KEY, - "Failed to parse public key."); - } - - // Store raw message for AUTH_STRING computation. - wrapped_server_init_ = handshake_message; - handshake_state_ = InternalState::CLIENT_AFTER_SERVER_INIT; - return CreateSuccessResult(); -} - -UKey2Handshake::ParseResult UKey2Handshake::ParseClientFinishUkey2Message( - const string& handshake_message) { - // Deserialize the protobuf. - Ukey2Message message; - if (!message.ParseFromString(handshake_message)) { - return CreateFailedResultWithoutAlert("Can't parse message 3."); - } - - // Verify that message_type == CLIENT_FINISH. - if (!message.has_message_type() || - message.message_type() != Ukey2Message::CLIENT_FINISH) { - return CreateFailedResultWithoutAlert( - "Expected, but did not find CLIENT_FINISH message type."); - } - - // Verify that the hash of the CLientFinished message matches the expected - // commitment from ClientInit. - if (!VerifyCommitment(handshake_message)) { - return CreateFailedResultWithoutAlert(last_error_); - } - - // Deserialize message_data as a ClientFinished message. - if (!message.has_message_data()) { - return CreateFailedResultWithoutAlert( - "Expected message data, but didn't find it."); - } - - Ukey2ClientFinished client_finished; - if (!client_finished.ParseFromString(message.message_data())) { - return CreateFailedResultWithoutAlert("Failed to parse ClientFinished."); - } - - // Check that public_key parses into a correct public key structure. - if (!client_finished.has_public_key()) { - return CreateFailedResultWithoutAlert( - "No public key found in ClientFinished."); - } - - their_public_key_ = ParsePublicKey(client_finished.public_key()); - if (!their_public_key_) { - return CreateFailedResultWithoutAlert("Failed to parse public key."); - } - - handshake_state_ = InternalState::HANDSHAKE_VERIFICATION_NEEDED; - return CreateSuccessResult(); -} - -UKey2Handshake::ParseResult UKey2Handshake::CreateFailedResultWithAlert( - Ukey2Alert::AlertType alert_type, const string& error_message) { - if (!Ukey2Alert_AlertType_IsValid(alert_type)) { - std::ostringstream stream; - stream << "Unknown alert type: " << static_cast<int>(alert_type); - SetError(stream.str()); - return {false, nullptr}; - } - - Ukey2Alert alert; - alert.set_type(alert_type); - if (!error_message.empty()) { - alert.set_error_message(error_message); - } - - std::unique_ptr<string> alert_message = - MakeUkey2Message(Ukey2Message::ALERT, alert.SerializeAsString()); - - SetError(error_message); - ParseResult result{false, std::move(alert_message)}; - return result; -} - -UKey2Handshake::ParseResult -UKey2Handshake::CreateFailedResultWithoutAlert(const string& error_message) { - SetError(error_message); - return {false, nullptr}; -} - -UKey2Handshake::ParseResult UKey2Handshake::CreateSuccessResult() { - return {true, nullptr}; -} - -bool UKey2Handshake::VerifyCommitment(const string& handshake_message) { - std::unique_ptr<ByteBuffer> actual_client_finish_hash; - switch (handshake_cipher_) { - case HandshakeCipher::P256_SHA512: - actual_client_finish_hash = - CryptoOps::Sha512(ByteBuffer(handshake_message)); - break; - default: - // Unreachable. - return false; - } - - if (!actual_client_finish_hash) { - SetError("Failed to hash ClientFinish message."); - return false; - } - - // Note: Equals() is a time constant comparison operation. - if (!actual_client_finish_hash->Equals(peer_commitment_)) { - SetError("Failed to verify commitment."); - return false; - } - - return true; -} - -std::unique_ptr<Ukey2ClientInit::CipherCommitment> -UKey2Handshake::GenerateP256Sha512Commitment() { - // Generate the corresponding ClientFinished message if it's not done yet. - if (raw_message3_map_.count(HandshakeCipher::P256_SHA512) == 0) { - if (!our_key_pair_ || !our_key_pair_->public_key) { - SetError("Invalid public key."); - return nullptr; - } - - std::unique_ptr<GenericPublicKey> generic_public_key = - PublicKeyProtoUtil::EncodePublicKey(*(our_key_pair_->public_key)); - if (!generic_public_key) { - SetError("Failed to encode generic public key."); - return nullptr; - } - - Ukey2ClientFinished client_finished; - client_finished.set_public_key(generic_public_key->SerializeAsString()); - std::unique_ptr<string> serialized_ukey2_message = MakeUkey2Message( - Ukey2Message::CLIENT_FINISH, client_finished.SerializeAsString()); - if (!serialized_ukey2_message) { - SetError("Failed to serialized Ukey2Message."); - return nullptr; - } - - raw_message3_map_[HandshakeCipher::P256_SHA512] = *serialized_ukey2_message; - } - - // Create the SHA512 commitment from raw message 3. - std::unique_ptr<ByteBuffer> commitment = CryptoOps::Sha512( - ByteBuffer(raw_message3_map_[HandshakeCipher::P256_SHA512])); - if (!commitment) { - SetError("Failed to hash message for commitment."); - return nullptr; - } - - // Wrap the commitment in a proto. - std::unique_ptr<Ukey2ClientInit::CipherCommitment> - handshake_cipher_commitment(new Ukey2ClientInit::CipherCommitment()); - handshake_cipher_commitment->set_handshake_cipher(P256_SHA512); - handshake_cipher_commitment->set_commitment(commitment->String()); - - return handshake_cipher_commitment; -} - -std::unique_ptr<string> UKey2Handshake::MakeClientInitUkey2Message() { - std::unique_ptr<ByteBuffer> nonce = - CryptoOps::SecureRandom(kNonceLengthInBytes); - if (!nonce) { - SetError("Failed to generate nonce."); - return nullptr; - } - - Ukey2ClientInit client_init; - client_init.set_version(kVersion); - client_init.set_random(nonce->String()); - client_init.set_next_protocol(kNextProtocol); - - // At the moment, we only support one cipher. - std::unique_ptr<Ukey2ClientInit::CipherCommitment> - handshake_cipher_commitment = GenerateP256Sha512Commitment(); - if (!handshake_cipher_commitment) { - // |last_error_| already set. - return nullptr; - } - *(client_init.add_cipher_commitments()) = *handshake_cipher_commitment; - - return MakeUkey2Message(Ukey2Message::CLIENT_INIT, - client_init.SerializeAsString()); -} - -std::unique_ptr<string> UKey2Handshake::MakeServerInitUkey2Message() { - std::unique_ptr<ByteBuffer> nonce = - CryptoOps::SecureRandom(kNonceLengthInBytes); - if (!nonce) { - SetError("Failed to generate nonce."); - return nullptr; - } - - if (!our_key_pair_ || !our_key_pair_->public_key) { - SetError("Invalid key pair."); - return nullptr; - } - - std::unique_ptr<GenericPublicKey> public_key = - PublicKeyProtoUtil::EncodePublicKey(*(our_key_pair_->public_key)); - if (!public_key) { - SetError("Failed to encode public key."); - return nullptr; - } - - Ukey2ServerInit server_init; - server_init.set_version(kVersion); - server_init.set_random(nonce->String()); - server_init.set_handshake_cipher( - static_cast<Ukey2HandshakeCipher>(handshake_cipher_)); - server_init.set_public_key(public_key->SerializeAsString()); - - return MakeUkey2Message(Ukey2Message::SERVER_INIT, - server_init.SerializeAsString()); -} - -// Generates the serialized representation of a Ukey2Message based on the -// provided |type| and |data|. On error, returns nullptr and writes error -// message to |out_error|. -std::unique_ptr<string> UKey2Handshake::MakeUkey2Message( - Ukey2Message::Type type, const string& data) { - Ukey2Message message; - if (!Ukey2Message::Type_IsValid(type)) { - std::ostringstream stream; - stream << "Invalid message type: " << type; - SetError(stream.str()); - return nullptr; - } - message.set_message_type(type); - - // Only ALERT messages can have a blank data field. - if (type != Ukey2Message::ALERT) { - if (data.length() == 0) { - SetError("Cannot send empty message data for non-alert messages"); - return nullptr; - } - } - message.set_message_data(data); - - std::unique_ptr<string> serialized(new string()); - message.SerializeToString(serialized.get()); - return serialized; -} - -void UKey2Handshake::SetError(const string& error_message) { - handshake_state_ = InternalState::HANDSHAKE_ERROR; - last_error_ = error_message; -} - -} // namespace securegcm diff --git a/src/main/cpp/src/securegcm/ukey2_shell.cc b/src/main/cpp/src/securegcm/ukey2_shell.cc deleted file mode 100644 index 99a35a8..0000000 --- a/src/main/cpp/src/securegcm/ukey2_shell.cc +++ /dev/null @@ -1,297 +0,0 @@ -// 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 deleted file mode 100644 index 272f919..0000000 --- a/src/main/cpp/test/securegcm/CMakeLists.txt +++ /dev/null @@ -1,31 +0,0 @@ -# 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 deleted file mode 100644 index daf69d1..0000000 --- a/src/main/cpp/test/securegcm/d2d_connection_context_v1_test.cc +++ /dev/null @@ -1,124 +0,0 @@ -// 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 deleted file mode 100644 index 5acbb89..0000000 --- a/src/main/cpp/test/securegcm/d2d_crypto_ops_test.cc +++ /dev/null @@ -1,158 +0,0 @@ -// 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 deleted file mode 100644 index 20928fa..0000000 --- a/src/main/cpp/test/securegcm/java_util_test.cc +++ /dev/null @@ -1,84 +0,0 @@ -// 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/java/com/google/security/annotations/CryptoAnnotation.java b/src/main/java/com/google/security/annotations/CryptoAnnotation.java deleted file mode 100644 index 7f4230d..0000000 --- a/src/main/java/com/google/security/annotations/CryptoAnnotation.java +++ /dev/null @@ -1,68 +0,0 @@ -/* Copyright 2018 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. - */ -// Copyright 2007 Google Inc. All Rights Reserved - -package com.google.security.annotations; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Crypto Key Annotation: Label any cryptographic keys in code with this - * annotation. This will help identify cryptographic keys that are exposed in - * source code. Keys in source code should be annotated with an owner, purpose, - * removal priority, and leak severity. - * - * Example of usage: - * @CryptoAnnotation( - * purpose = CryptoAnnotation.Purpose.AUTHENTICATION, - * owner = "sweis", - * bugId = 7041243, - * leakSeverity = CryptoAnnotation.LeakSeverity.S2, - * removalPriority = CryptoAnnotation.RemovalPriority.P1, - * description = "This key is used to sign blah blah blah." - * removalDate = "9/2007 - * ) - * byte[] keyBytes = {0xDE, 0xAD, 0xBE, 0xEF}; - * - * @author sweis@google.com (Steve Weis) - */ -@Retention(RetentionPolicy.SOURCE) -@Target({ElementType.FIELD, ElementType.LOCAL_VARIABLE}) -public @interface CryptoAnnotation { - /* - * Keys with "encryption" and "authentication" purposes should be removed - * from source code. - * - * Keys with "obfuscation" and "integrity check" purposes do not necessarily - * need to be cryptographically strong. They may or may not be removed from - * code at the discretion of the code owner. - */ - public enum Purpose {ENCRYPTION, AUTHENTICATION, OBFUSCATION, - INTEGRITY_CHECK, PASSWORD, OTHER} - public enum LeakSeverity {S0, S1, S2, S3, S4, NoRisk} - public enum RemovalPriority {P0, P1, P2, P3, P4, WillNotFix} - - LeakSeverity leakSeverity(); - RemovalPriority removalPriority(); - int bugId() default 0; - String owner(); // Will be contacted in the event a key is leaked - Purpose purpose(); - String description() default ""; - String removalDate() default ""; -} - diff --git a/src/main/java/com/google/security/annotations/SuppressInsecureCipherModeCheckerNoReview.java b/src/main/java/com/google/security/annotations/SuppressInsecureCipherModeCheckerNoReview.java deleted file mode 100644 index 93dc716..0000000 --- a/src/main/java/com/google/security/annotations/SuppressInsecureCipherModeCheckerNoReview.java +++ /dev/null @@ -1,57 +0,0 @@ -/* Copyright 2018 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.security.annotations; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -import javax.crypto.Cipher; - -/** - * This annotation is used to disable the InsecureCipherMode Error Prone checker for legacy code - * that didn't undergo a security review by ISE. - * - * <p>A {@link Cipher} object is created using one of the overloads of the - * {@link Cipher#getInstance()} method. This method takes a specification of the transformer either - * as a triple "Algorithm/Mode/Padding" or just "Algorithm", using the provider's default settings. - * The InsecureCipherMode checker implemented in Error Prone flags all call sites of - * {@link Cipher#getInstance()}, where either the insecure ECB mode or the provider's default mode - * is used. This method annotation is used to suppress the Error Prone checker for legacy code - * without review by ISE. The annotation is BUILD-visibility restricted and every use must be vetted - * by the ISE team. - * - * <p>Example of usage: - * <pre> - * {@code - * @SuppressInsecureCipherModeCheckerNoReview - * private String decrypt(String[] input) { - * Cipher aesCipher = Cipher.getInstance("AES"); - * aesCipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(rawKeyMaterial, "AES")); - * // ... - * } - * } - * </pre> - * - * @author avenet@google.com (Arnaud J. Venet) - * - */ -@Documented -@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.CONSTRUCTOR, - ElementType.LOCAL_VARIABLE}) -@Retention(RetentionPolicy.SOURCE) -public @interface SuppressInsecureCipherModeCheckerNoReview {} diff --git a/src/main/java/com/google/security/annotations/SuppressInsecureCipherModeCheckerPendingReview.java b/src/main/java/com/google/security/annotations/SuppressInsecureCipherModeCheckerPendingReview.java deleted file mode 100644 index a7957c1..0000000 --- a/src/main/java/com/google/security/annotations/SuppressInsecureCipherModeCheckerPendingReview.java +++ /dev/null @@ -1,59 +0,0 @@ -/* Copyright 2018 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.security.annotations; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -import javax.crypto.Cipher; - -/** - * This annotation is used to temporarily disable the InsecureCipherMode Error Prone checker while - * the violation is being reviewed by ISE. A comment including a tracking bug for the ongoing - * security review should accompany the annotation. If the specific use is deemed a valid exception - * after review, the annotation should be changed to @SuppressInsecureCipherModeCheckerReviewed. - * - * <p>A {@link Cipher} object is created using one of the overloads of the - * {@link Cipher#getInstance()} method. This method takes a specification of the transformer either - * as a triple "Algorithm/Mode/Padding" or just "Algorithm", using the provider's default settings. - * The InsecureCipherMode checker implemented in Error Prone flags all call sites of - * {@link Cipher#getInstance()}, where either the insecure ECB mode or the provider's default mode - * is used. This method annotation is used to suppress the Error Prone checker in use cases where an - * exception has been granted by ISE after proper review. The annotation is BUILD-visibility - * restricted and every use must be vetted by the ISE team. - * - * <p>Example of usage: - * <pre> - * {@code - * @SuppressInsecureCipherModeCheckerPendingReview // Tracking bug for the review: b/... - * private String decrypt(String[] input) { - * Cipher aesCipher = Cipher.getInstance("AES"); - * aesCipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(rawKeyMaterial, "AES")); - * // ... - * } - * } - * </pre> - * - * @author avenet@google.com (Arnaud J. Venet) - * - */ -@Documented -@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.CONSTRUCTOR, - ElementType.LOCAL_VARIABLE}) -@Retention(RetentionPolicy.SOURCE) -public @interface SuppressInsecureCipherModeCheckerPendingReview {} diff --git a/src/main/java/com/google/security/annotations/SuppressInsecureCipherModeCheckerReviewed.java b/src/main/java/com/google/security/annotations/SuppressInsecureCipherModeCheckerReviewed.java deleted file mode 100644 index d1780c5..0000000 --- a/src/main/java/com/google/security/annotations/SuppressInsecureCipherModeCheckerReviewed.java +++ /dev/null @@ -1,58 +0,0 @@ -/* Copyright 2018 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.security.annotations; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -import javax.crypto.Cipher; - -/** - * This annotation is used to disable the InsecureCipherMode Error Prone checker after a proper - * review by ISE. A comment including a tracking bug for the security review should accompany the - * annotation. - * - * <p>A {@link Cipher} object is created using one of the overloads of the - * {@link Cipher#getInstance()} method. This method takes a specification of the transformer either - * as a triple "Algorithm/Mode/Padding" or just "Algorithm", using the provider's default settings. - * The InsecureCipherMode checker implemented in Error Prone flags all call sites of - * {@link Cipher#getInstance()}, where either the insecure ECB mode or the provider's default mode - * is used. This method annotation is used to suppress the Error Prone checker in use cases where an - * exception has been granted by ISE after proper review. The annotation is BUILD-visibility - * restricted and every use must be vetted by the ISE team. - * - * <p>Example of usage: - * <pre> - * {@code - * @SuppressInsecureCipherModeCheckerReviewed // Tracking bug for the review: b/... - * private String decrypt(String[] input) { - * Cipher aesCipher = Cipher.getInstance("AES"); - * aesCipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(rawKeyMaterial, "AES")); - * // ... - * } - * } - * </pre> - * - * @author avenet@google.com (Arnaud J. Venet) - * - */ -@Documented -@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.CONSTRUCTOR, - ElementType.LOCAL_VARIABLE}) -@Retention(RetentionPolicy.SOURCE) -public @interface SuppressInsecureCipherModeCheckerReviewed {} diff --git a/src/main/java/com/google/security/cryptauth/lib/securegcm/D2DConnectionContext.java b/src/main/java/com/google/security/cryptauth/lib/securegcm/D2DConnectionContext.java deleted file mode 100644 index fb4af63..0000000 --- a/src/main/java/com/google/security/cryptauth/lib/securegcm/D2DConnectionContext.java +++ /dev/null @@ -1,274 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.security.cryptauth.lib.securegcm; - -import com.google.common.annotations.VisibleForTesting; -import com.google.protobuf.ByteString; -import com.google.protobuf.InvalidProtocolBufferException; -import com.google.security.cryptauth.lib.securegcm.DeviceToDeviceMessagesProto.DeviceToDeviceMessage; -import com.google.security.cryptauth.lib.securegcm.TransportCryptoOps.Payload; -import com.google.security.cryptauth.lib.securegcm.TransportCryptoOps.PayloadType; -import java.io.UnsupportedEncodingException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.SignatureException; -import java.util.Arrays; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; - -/** - * The full context of a secure connection. This object has methods to encode and decode messages - * that are to be sent to another device. - * - * Subclasses keep track of the keys shared with the other device, and of the sequence in which the - * messages are expected. - */ -public abstract class D2DConnectionContext { - private static final String UTF8 = "UTF-8"; - private final int protocolVersion; - - protected D2DConnectionContext(int protocolVersion) { - this.protocolVersion = protocolVersion; - } - - /** - * @return the version of the D2D protocol. - */ - public int getProtocolVersion() { - return protocolVersion; - } - - /** - * Once initiator and responder have exchanged public keys, use this method to encrypt and - * sign a payload. Both initiator and responder devices can use this message. - * - * @param payload the payload that should be encrypted. - */ - public byte[] encodeMessageToPeer(byte[] payload) { - incrementSequenceNumberForEncoding(); - DeviceToDeviceMessage message = createDeviceToDeviceMessage( - payload, getSequenceNumberForEncoding()); - try { - return D2DCryptoOps.signcryptPayload( - new Payload(PayloadType.DEVICE_TO_DEVICE_MESSAGE, - message.toByteArray()), - getEncodeKey()); - } catch (InvalidKeyException e) { - // should never happen, since we agreed on the key earlier - throw new RuntimeException(e); - } catch (NoSuchAlgorithmException e) { - // should never happen - throw new RuntimeException(e); - } - } - - /** - * Encrypting/signing a string for transmission to another device. - * - * @see #encodeMessageToPeer(byte[]) - * - * @param payload the payload that should be encrypted. - */ - public byte[] encodeMessageToPeer(String payload) { - try { - return encodeMessageToPeer(payload.getBytes(UTF8)); - } catch (UnsupportedEncodingException e) { - // Should never happen - we should always be able to UTF-8-encode a string - throw new RuntimeException(e); - } - } - - /** - * Once InitiatorHello and ResponderHello(AndPayload) are exchanged, use this method - * to decrypt and verify a message received from the other device. Both initiator and - * responder device can use this message. - * - * @param message the message that should be encrypted. - * @throws SignatureException if the message from the remote peer did not pass verification - */ - public byte[] decodeMessageFromPeer(byte[] message) throws SignatureException { - try { - Payload payload = D2DCryptoOps.verifydecryptPayload(message, getDecodeKey()); - if (!PayloadType.DEVICE_TO_DEVICE_MESSAGE.equals(payload.getPayloadType())) { - throw new SignatureException("wrong message type in device-to-device message"); - } - - DeviceToDeviceMessage messageProto = DeviceToDeviceMessage.parseFrom(payload.getMessage()); - incrementSequenceNumberForDecoding(); - if (messageProto.getSequenceNumber() != getSequenceNumberForDecoding()) { - throw new SignatureException("Incorrect sequence number"); - } - - return messageProto.getMessage().toByteArray(); - } catch (InvalidKeyException e) { - throw new SignatureException(e); - } catch (NoSuchAlgorithmException e) { - // this shouldn't happen - the algorithms are hard-coded. - throw new RuntimeException(e); - } catch (InvalidProtocolBufferException e) { - throw new SignatureException(e); - } - } - - /** - * Once InitiatorHello and ResponderHello(AndPayload) are exchanged, use this method - * to decrypt and verify a message received from the other device. Both initiator and - * responder device can use this message. - * - * @param message the message that should be encrypted. - */ - public String decodeMessageFromPeerAsString(byte[] message) throws SignatureException { - try { - return new String(decodeMessageFromPeer(message), UTF8); - } catch (UnsupportedEncodingException e) { - // Should never happen - we should always be able to UTF-8-encode a string - throw new RuntimeException(e); - } - } - - // package-private - static DeviceToDeviceMessage createDeviceToDeviceMessage(byte[] message, int sequenceNumber) { - DeviceToDeviceMessage.Builder deviceToDeviceMessage = DeviceToDeviceMessage.newBuilder(); - deviceToDeviceMessage.setSequenceNumber(sequenceNumber); - deviceToDeviceMessage.setMessage(ByteString.copyFrom(message)); - return deviceToDeviceMessage.build(); - } - - /** - * Returns a cryptographic digest (SHA256) of the session keys prepended by the SHA256 hash - * of the ASCII string "D2D" - * @throws NoSuchAlgorithmException if SHA 256 doesn't exist on this platform - */ - public abstract byte[] getSessionUnique() throws NoSuchAlgorithmException; - - /** - * Increments the sequence number used for encoding messages. - */ - protected abstract void incrementSequenceNumberForEncoding(); - - /** - * Increments the sequence number used for decoding messages. - */ - protected abstract void incrementSequenceNumberForDecoding(); - - /** - * @return the last sequence number used to encode a message. - */ - @VisibleForTesting - abstract int getSequenceNumberForEncoding(); - - /** - * @return the last sequence number used to decode a message. - */ - @VisibleForTesting - abstract int getSequenceNumberForDecoding(); - - /** - * @return the {@link SecretKey} used for encoding messages. - */ - @VisibleForTesting - abstract SecretKey getEncodeKey(); - - /** - * @return the {@link SecretKey} used for decoding messages. - */ - @VisibleForTesting - abstract SecretKey getDecodeKey(); - - /** - * Creates a saved session that can later be used for resumption. Note, this must be stored in a - * secure location. - * - * @return the saved session, suitable for resumption. - */ - public abstract byte[] saveSession(); - - /** - * Parse a saved session info and attempt to construct a resumed context. - * The first byte in a saved session info must always be the protocol version. - * Note that an {@link IllegalArgumentException} will be thrown if the savedSessionInfo is not - * properly formatted. - * - * @return a resumed context from a saved session. - */ - public static D2DConnectionContext fromSavedSession(byte[] savedSessionInfo) { - if (savedSessionInfo == null || savedSessionInfo.length == 0) { - throw new IllegalArgumentException("savedSessionInfo null or too short"); - } - - int protocolVersion = savedSessionInfo[0] & 0xff; - - switch (protocolVersion) { - case 0: - // Version 0 has a 1 byte protocol version, a 4 byte sequence number, - // and 32 bytes of AES key (1 + 4 + 32 = 37) - if (savedSessionInfo.length != 37) { - throw new IllegalArgumentException("Incorrect data length (" + savedSessionInfo.length - + ") for v0 protocol"); - } - int sequenceNumber = bytesToSignedInt(Arrays.copyOfRange(savedSessionInfo, 1, 5)); - SecretKey sharedKey = new SecretKeySpec(Arrays.copyOfRange(savedSessionInfo, 5, 37), "AES"); - return new D2DConnectionContextV0(sharedKey, sequenceNumber); - - case 1: - // Version 1 has a 1 byte protocol version, two 4 byte sequence numbers, - // and two 32 byte AES keys (1 + 4 + 4 + 32 + 32 = 73) - if (savedSessionInfo.length != 73) { - throw new IllegalArgumentException("Incorrect data length for v1 protocol"); - } - int encodeSequenceNumber = bytesToSignedInt(Arrays.copyOfRange(savedSessionInfo, 1, 5)); - int decodeSequenceNumber = bytesToSignedInt(Arrays.copyOfRange(savedSessionInfo, 5, 9)); - SecretKey encodeKey = - new SecretKeySpec(Arrays.copyOfRange(savedSessionInfo, 9, 41), "AES"); - SecretKey decodeKey = - new SecretKeySpec(Arrays.copyOfRange(savedSessionInfo, 41, 73), "AES"); - return new D2DConnectionContextV1(encodeKey, decodeKey, encodeSequenceNumber, - decodeSequenceNumber); - - default: - throw new IllegalArgumentException("Cannot rebuild context, unkown protocol version: " - + protocolVersion); - } - } - - /** - * Convert 4 bytes in big-endian representation into a signed int. - */ - static int bytesToSignedInt(byte[] bytes) { - if (bytes.length != 4) { - throw new IllegalArgumentException("Expected 4 bytes to encode int, but got: " - + bytes.length + " bytes"); - } - - return ((bytes[0] << 24) & 0xff000000) - | ((bytes[1] << 16) & 0x00ff0000) - | ((bytes[2] << 8) & 0x0000ff00) - | (bytes[3] & 0x000000ff); - } - - /** - * Convert a signed int into a 4 byte big-endian representation - */ - static byte[] signedIntToBytes(int val) { - byte[] bytes = new byte[4]; - - bytes[0] = (byte) ((val >> 24) & 0xff); - bytes[1] = (byte) ((val >> 16) & 0xff); - bytes[2] = (byte) ((val >> 8) & 0xff); - bytes[3] = (byte) (val & 0xff); - - return bytes; - } -} diff --git a/src/main/java/com/google/security/cryptauth/lib/securegcm/D2DConnectionContextV0.java b/src/main/java/com/google/security/cryptauth/lib/securegcm/D2DConnectionContextV0.java deleted file mode 100644 index d0efa44..0000000 --- a/src/main/java/com/google/security/cryptauth/lib/securegcm/D2DConnectionContextV0.java +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.security.cryptauth.lib.securegcm; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import javax.crypto.SecretKey; - -/** - * Implementation of {@link D2DConnectionContext} for version 0 of the D2D protocol. In this - * version, communication is half-duplex, as there is a shared key and a shared sequence number - * between the two sides. - */ -public class D2DConnectionContextV0 extends D2DConnectionContext { - public static final int PROTOCOL_VERSION = 0; - - private final SecretKey sharedKey; - private int sequenceNumber; - - /** - * Package private constructor. Should never be called directly except by the - * {@link D2DHandshakeContext} - * - * @param sharedKey - * @param initialSequenceNumber - */ - D2DConnectionContextV0(SecretKey sharedKey, int initialSequenceNumber) { - super(PROTOCOL_VERSION); - this.sharedKey = sharedKey; - this.sequenceNumber = initialSequenceNumber; - } - - @Override - public byte[] getSessionUnique() throws NoSuchAlgorithmException { - if (sharedKey == null) { - throw new IllegalStateException( - "Connection has not been correctly initialized; shared key is null"); - } - - MessageDigest md = MessageDigest.getInstance("SHA-256"); - md.update(D2DCryptoOps.SALT); - return md.digest(sharedKey.getEncoded()); - } - - @Override - protected void incrementSequenceNumberForEncoding() { - sequenceNumber++; - } - - @Override - protected void incrementSequenceNumberForDecoding() { - sequenceNumber++; - } - - @Override - int getSequenceNumberForEncoding() { - return sequenceNumber; - } - - @Override - int getSequenceNumberForDecoding() { - return sequenceNumber; - } - - @Override - SecretKey getEncodeKey() { - return sharedKey; - } - - @Override - SecretKey getDecodeKey() { - return sharedKey; - } - - /** - * Structure of saved session is: - * +-----------------------------------------------------+ - * | 1 Byte | 4 Bytes (big endian) | 32 Bytes | - * +-----------------------------------------------------+ - * | Protocol Version | sequence number | key | - * +-----------------------------------------------------+ - */ - @Override - public byte[] saveSession() { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - - try { - // Protocol version - bytes.write(0); - - // Sequence number - bytes.write(signedIntToBytes(sequenceNumber)); - - // Key - bytes.write(sharedKey.getEncoded()); - } catch (IOException e) { - // should not happen - e.printStackTrace(); - return null; - } - - return bytes.toByteArray(); - } -} diff --git a/src/main/java/com/google/security/cryptauth/lib/securegcm/D2DConnectionContextV1.java b/src/main/java/com/google/security/cryptauth/lib/securegcm/D2DConnectionContextV1.java deleted file mode 100644 index 1566849..0000000 --- a/src/main/java/com/google/security/cryptauth/lib/securegcm/D2DConnectionContextV1.java +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.security.cryptauth.lib.securegcm; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Arrays; -import javax.crypto.SecretKey; - -/** - * Implementation of {@link D2DConnectionContext} for version 1 of the D2D protocol. In this - * version, communication is fully duplex, as separate keys and sequence nubmers are used for - * encoding and decoding. - */ -public class D2DConnectionContextV1 extends D2DConnectionContext { - public static final int PROTOCOL_VERSION = 1; - - private final SecretKey encodeKey; - private final SecretKey decodeKey; - private int encodeSequenceNumber; - private int decodeSequenceNumber; - - /** - * Package private constructor. Should never be called directly except by the - * {@link D2DHandshakeContext} - * - * @param encodeKey - * @param decodeKey - * @param initialEncodeSequenceNumber - * @param initialDecodeSequenceNumber - */ - D2DConnectionContextV1( - SecretKey encodeKey, - SecretKey decodeKey, - int initialEncodeSequenceNumber, - int initialDecodeSequenceNumber) { - super(PROTOCOL_VERSION); - this.encodeKey = encodeKey; - this.decodeKey = decodeKey; - this.encodeSequenceNumber = initialEncodeSequenceNumber; - this.decodeSequenceNumber = initialDecodeSequenceNumber; - } - - @Override - public byte[] getSessionUnique() throws NoSuchAlgorithmException { - if (encodeKey == null || decodeKey == null) { - throw new IllegalStateException( - "Connection has not been correctly initialized; encode key or decode key is null"); - } - - // Ensure that the initator and responder keys are hashed in a deterministic order, so they have - // the same session unique code. - byte[] encodeKeyBytes = encodeKey.getEncoded(); - byte[] decodeKeyBytes = decodeKey.getEncoded(); - int encodeKeyHash = Arrays.hashCode(encodeKeyBytes); - int decodeKeyHash = Arrays.hashCode(decodeKeyBytes); - byte[] firstKeyBytes = encodeKeyHash < decodeKeyHash ? encodeKeyBytes : decodeKeyBytes; - byte[] secondKeyBytes = firstKeyBytes == encodeKeyBytes ? decodeKeyBytes : encodeKeyBytes; - - MessageDigest md = MessageDigest.getInstance("SHA-256"); - md.update(D2DCryptoOps.SALT); - md.update(firstKeyBytes); - md.update(secondKeyBytes); - return md.digest(); - } - - @Override - protected void incrementSequenceNumberForEncoding() { - encodeSequenceNumber++; - } - - @Override - protected void incrementSequenceNumberForDecoding() { - decodeSequenceNumber++; - } - - @Override - int getSequenceNumberForEncoding() { - return encodeSequenceNumber; - } - - @Override - int getSequenceNumberForDecoding() { - return decodeSequenceNumber; - } - - @Override - SecretKey getEncodeKey() { - return encodeKey; - } - - @Override - SecretKey getDecodeKey() { - return decodeKey; - } - - /** - * Structure of saved session is: - * +------------------------------------------------------------------------------------------+ - * | 1 Byte | 4 Bytes (big endian) | 4 Bytes (big endian) | 32 Bytes | 32 Bytes | - * +------------------------------------------------------------------------------------------+ - * | Protocol Version | encode seq number | decode seq number | encode key | decode key | - * +------------------------------------------------------------------------------------------+ - */ - @Override - public byte[] saveSession() { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - - try { - // Protocol version - bytes.write(1); - - // Encode sequence number - bytes.write(signedIntToBytes(encodeSequenceNumber)); - - // Decode sequence number - bytes.write(signedIntToBytes(decodeSequenceNumber)); - - // Encode Key - bytes.write(encodeKey.getEncoded()); - - // Decode Key - bytes.write(decodeKey.getEncoded()); - } catch (IOException e) { - // should not happen - e.printStackTrace(); - return null; - } - - return bytes.toByteArray(); - } -} diff --git a/src/main/java/com/google/security/cryptauth/lib/securegcm/D2DCryptoOps.java b/src/main/java/com/google/security/cryptauth/lib/securegcm/D2DCryptoOps.java deleted file mode 100644 index a7203d1..0000000 --- a/src/main/java/com/google/security/cryptauth/lib/securegcm/D2DCryptoOps.java +++ /dev/null @@ -1,239 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.security.cryptauth.lib.securegcm; - -import com.google.common.annotations.VisibleForTesting; -import com.google.protobuf.InvalidProtocolBufferException; -import com.google.security.cryptauth.lib.securegcm.DeviceToDeviceMessagesProto.DeviceToDeviceMessage; -import com.google.security.cryptauth.lib.securegcm.DeviceToDeviceMessagesProto.ResponderHello; -import com.google.security.cryptauth.lib.securegcm.SecureGcmProto.GcmMetadata; -import com.google.security.cryptauth.lib.securegcm.TransportCryptoOps.Payload; -import com.google.security.cryptauth.lib.securegcm.TransportCryptoOps.PayloadType; -import com.google.security.cryptauth.lib.securemessage.CryptoOps; -import com.google.security.cryptauth.lib.securemessage.CryptoOps.EncType; -import com.google.security.cryptauth.lib.securemessage.CryptoOps.SigType; -import com.google.security.cryptauth.lib.securemessage.PublicKeyProtoUtil; -import com.google.security.cryptauth.lib.securemessage.SecureMessageBuilder; -import com.google.security.cryptauth.lib.securemessage.SecureMessageParser; -import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.GenericPublicKey; -import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.Header; -import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.HeaderAndBody; -import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.SecureMessage; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.SignatureException; -import java.security.spec.InvalidKeySpecException; -import javax.annotation.Nullable; -import javax.crypto.SecretKey; - -/** - * A collection of static utility methods used by {@link D2DHandshakeContext} for the Device to - * Device communication (D2D) library. - */ -class D2DCryptoOps { - // SHA256 of "D2D" - // package-private - static final byte[] SALT = new byte[] { - (byte) 0x82, (byte) 0xAA, (byte) 0x55, (byte) 0xA0, (byte) 0xD3, (byte) 0x97, (byte) 0xF8, - (byte) 0x83, (byte) 0x46, (byte) 0xCA, (byte) 0x1C, (byte) 0xEE, (byte) 0x8D, (byte) 0x39, - (byte) 0x09, (byte) 0xB9, (byte) 0x5F, (byte) 0x13, (byte) 0xFA, (byte) 0x7D, (byte) 0xEB, - (byte) 0x1D, (byte) 0x4A, (byte) 0xB3, (byte) 0x83, (byte) 0x76, (byte) 0xB8, (byte) 0x25, - (byte) 0x6D, (byte) 0xA8, (byte) 0x55, (byte) 0x10 - }; - - // Data passed to hkdf to create the key used by the initiator to encode messages. - static final String INITIATOR_PURPOSE = "initiator"; - // Data passed to hkdf to create the key used by the responder to encode messages. - static final String RESPONDER_PURPOSE = "responder"; - - // Don't instantiate - private D2DCryptoOps() { } - - /** - * Used by the responder device to create a signcrypted message that contains - * a payload and a {@link ResponderHello}. - * - * @param sharedKey used to signcrypt the {@link Payload} - * @param publicDhKey the key the recipient will need to derive the shared DH secret. - * This key will be added to the {@link ResponderHello} in the header. - * @param protocolVersion the protocol version to include in the proto - */ - static byte[] signcryptMessageAndResponderHello( - Payload payload, SecretKey sharedKey, PublicKey publicDhKey, int protocolVersion) - throws InvalidKeyException, NoSuchAlgorithmException { - ResponderHello.Builder responderHello = ResponderHello.newBuilder(); - responderHello.setPublicDhKey(PublicKeyProtoUtil.encodePublicKey(publicDhKey)); - responderHello.setProtocolVersion(protocolVersion); - return signcryptPayload(payload, sharedKey, responderHello.build().toByteArray()); - } - - /** - * Used by a device to send a secure {@link Payload} to another device. - */ - static byte[] signcryptPayload( - Payload payload, SecretKey masterKey) - throws InvalidKeyException, NoSuchAlgorithmException { - return signcryptPayload(payload, masterKey, null); - } - - /** - * Used by a device to send a secure {@link Payload} to another device. - * - * @param responderHello is an optional public value to attach in the header of - * the {@link SecureMessage} (in the DecryptionKeyId). - */ - @VisibleForTesting - static byte[] signcryptPayload( - Payload payload, SecretKey masterKey, @Nullable byte[] responderHello) - throws InvalidKeyException, NoSuchAlgorithmException { - if ((payload == null) || (masterKey == null)) { - throw new NullPointerException(); - } - - SecureMessageBuilder secureMessageBuilder = new SecureMessageBuilder() - .setPublicMetadata(GcmMetadata.newBuilder() - .setType(payload.getPayloadType().getType()) - .setVersion(SecureGcmConstants.SECURE_GCM_VERSION) - .build() - .toByteArray()); - - if (responderHello != null) { - secureMessageBuilder.setDecryptionKeyId(responderHello); - } - - return secureMessageBuilder.buildSignCryptedMessage( - masterKey, - SigType.HMAC_SHA256, - masterKey, - EncType.AES_256_CBC, - payload.getMessage()) - .toByteArray(); - } - - /** - * Extracts a ResponderHello proto from the header of a signcrypted message so that we - * can derive the shared secret that was used to sign/encrypt the message. - * - * @return the {@link ResponderHello} embedded in the signcrypted message. - */ - static ResponderHello parseAndValidateResponderHello( - byte[] signcryptedMessageFromResponder) throws InvalidProtocolBufferException { - if (signcryptedMessageFromResponder == null) { - throw new NullPointerException(); - } - SecureMessage secmsg = SecureMessage.parseFrom(signcryptedMessageFromResponder); - Header messageHeader = SecureMessageParser.getUnverifiedHeader(secmsg); - if (!messageHeader.hasDecryptionKeyId()) { - // Maybe this should be a different exception type, because in general, it's legal for the - // SecureMessage proto to not have the decryption key id, but it's illegal in this protocol. - throw new InvalidProtocolBufferException("Missing decryption key id"); - } - byte[] encodedResponderHello = messageHeader.getDecryptionKeyId().toByteArray(); - ResponderHello responderHello = ResponderHello.parseFrom(encodedResponderHello); - if (!responderHello.hasPublicDhKey()) { - throw new InvalidProtocolBufferException("Missing public key in responder hello"); - } - return responderHello; - } - - /** - * Used by a device to recover a secure {@link Payload} sent by another device. - */ - static Payload verifydecryptPayload( - byte[] signcryptedMessage, SecretKey masterKey) - throws SignatureException, InvalidKeyException, NoSuchAlgorithmException { - if ((signcryptedMessage == null) || (masterKey == null)) { - throw new NullPointerException(); - } - try { - SecureMessage secmsg = SecureMessage.parseFrom(signcryptedMessage); - HeaderAndBody parsed = SecureMessageParser.parseSignCryptedMessage( - secmsg, - masterKey, - SigType.HMAC_SHA256, - masterKey, - EncType.AES_256_CBC); - if (!parsed.getHeader().hasPublicMetadata()) { - throw new SignatureException("missing metadata"); - } - GcmMetadata metadata = GcmMetadata.parseFrom(parsed.getHeader().getPublicMetadata()); - if (metadata.getVersion() > SecureGcmConstants.SECURE_GCM_VERSION) { - throw new SignatureException("Unsupported protocol version"); - } - return new Payload(PayloadType.valueOf(metadata.getType()), parsed.getBody().toByteArray()); - } catch (InvalidProtocolBufferException e) { - throw new SignatureException(e); - } catch (IllegalArgumentException e) { - throw new SignatureException(e); - } - } - - /** - * Used by the initiator device to derive the shared key from the {@link PrivateKey} in the - * {@link D2DHandshakeContext} and the responder's {@link GenericPublicKey} (contained in the - * {@link ResponderHello} proto). - */ - static SecretKey deriveSharedKeyFromGenericPublicKey( - PrivateKey ourPrivateKey, GenericPublicKey theirGenericPublicKey) throws SignatureException { - try { - PublicKey theirPublicKey = PublicKeyProtoUtil.parsePublicKey(theirGenericPublicKey); - return EnrollmentCryptoOps.doKeyAgreement(ourPrivateKey, theirPublicKey); - } catch (InvalidKeySpecException e) { - throw new SignatureException(e); - } catch (InvalidKeyException e) { - throw new SignatureException(e); - } - } - - /** - * Used to derive a distinct key for each initiator and responder. - * - * @param masterKey the source key used to derive the new key. - * @param purpose a string to make the new key different for each purpose. - * @return the derived {@link SecretKey}. - */ - static SecretKey deriveNewKeyForPurpose(SecretKey masterKey, String purpose) - throws NoSuchAlgorithmException, InvalidKeyException { - byte[] info = purpose.getBytes(); - return KeyEncoding.parseMasterKey(CryptoOps.hkdf(masterKey, SALT, info)); - } - - /** - * Used by the initiator device to decrypt the first payload portion that was sent in the - * {@code responderHelloAndPayload}, and extract the {@link DeviceToDeviceMessage} contained - * within it. In order to decrypt, the {@code sharedKey} must first be derived. - * - * @see #deriveSharedKeyFromGenericPublicKey(PrivateKey, GenericPublicKey) - */ - static DeviceToDeviceMessage decryptResponderHelloMessage( - SecretKey sharedKey, byte[] responderHelloAndPayload) throws SignatureException { - try { - Payload payload = verifydecryptPayload(responderHelloAndPayload, sharedKey); - if (!PayloadType.DEVICE_TO_DEVICE_RESPONDER_HELLO_PAYLOAD.equals( - payload.getPayloadType())) { - throw new SignatureException("wrong message type in responder hello"); - } - return DeviceToDeviceMessage.parseFrom(payload.getMessage()); - } catch (InvalidProtocolBufferException e) { - throw new SignatureException(e); - } catch (InvalidKeyException e) { - throw new SignatureException(e); - } catch (NoSuchAlgorithmException e) { - throw new SignatureException(e); - } - } -} diff --git a/src/main/java/com/google/security/cryptauth/lib/securegcm/D2DDiffieHellmanKeyExchangeHandshake.java b/src/main/java/com/google/security/cryptauth/lib/securegcm/D2DDiffieHellmanKeyExchangeHandshake.java deleted file mode 100644 index f929a3a..0000000 --- a/src/main/java/com/google/security/cryptauth/lib/securegcm/D2DDiffieHellmanKeyExchangeHandshake.java +++ /dev/null @@ -1,307 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.security.cryptauth.lib.securegcm; - -import com.google.protobuf.InvalidProtocolBufferException; -import com.google.security.cryptauth.lib.securegcm.DeviceToDeviceMessagesProto.DeviceToDeviceMessage; -import com.google.security.cryptauth.lib.securegcm.DeviceToDeviceMessagesProto.InitiatorHello; -import com.google.security.cryptauth.lib.securegcm.DeviceToDeviceMessagesProto.ResponderHello; -import com.google.security.cryptauth.lib.securegcm.TransportCryptoOps.Payload; -import com.google.security.cryptauth.lib.securegcm.TransportCryptoOps.PayloadType; -import com.google.security.cryptauth.lib.securemessage.PublicKeyProtoUtil; -import java.security.InvalidKeyException; -import java.security.KeyPair; -import java.security.NoSuchAlgorithmException; -import java.security.PublicKey; -import java.security.SignatureException; -import java.security.spec.InvalidKeySpecException; -import javax.crypto.SecretKey; - -/** - * Implements an unauthenticated EC Diffie Hellman Key Exchange Handshake - * <p> - * Initiator sends an InitiatorHello, which is a protobuf that contains a public key. Responder - * sends a responder hello, which a signed and encrypted message containing a payload, and a public - * key in the unencrypted header (payload is encrypted with the derived DH key). - * <p> - * Example Usage: - * <pre> - * // initiator: - * D2DHandshakeContext initiatorHandshakeContext = - * D2DDiffieHellmanKeyExchangeHandshake.forInitiator(); - * byte[] initiatorHello = initiatorHandshakeContext.getNextHandshakeMessage(); - * // (send initiatorHello to responder) - * - * // responder: - * D2DHandshakeContext responderHandshakeContext = - * D2DDiffieHellmanKeyExchangeHandshake.forResponder(); - * responderHandshakeContext.parseHandshakeMessage(initiatorHello); - * byte[] responderHelloAndPayload = responderHandshakeContext.getNextHandshakeMessage( - * toBytes(RESPONDER_HELLO_MESSAGE)); - * D2DConnectionContext responderCtx = responderHandshakeContext.toConnectionContext(); - * // (send responderHelloAndPayload to initiator) - * - * // initiator - * byte[] messageFromPayload = - * initiatorHandshakeContext.parseHandshakeMessage(responderHelloAndPayload); - * if (messageFromPayload.length > 0) { - * handle(messageFromPayload); - * } - * - * D2DConnectionContext initiatorCtx = initiatorHandshakeContext.toConnectionContext(); - * </pre> - */ -public class D2DDiffieHellmanKeyExchangeHandshake implements D2DHandshakeContext { - private KeyPair ourKeyPair; - private PublicKey theirPublicKey; - private SecretKey initiatorEncodeKey; - private SecretKey responderEncodeKey; - private State handshakeState; - private boolean isInitiator; - private int protocolVersionToUse; - - private enum State { - // Initiator state - INITIATOR_START, - INITIATOR_WAITING_FOR_RESPONDER_HELLO, - - // Responder state - RESPONDER_START, - RESPONDER_AFTER_INITIATOR_HELLO, - - // Common completion state - HANDSHAKE_FINISHED, - HANDSHAKE_ALREADY_USED - } - - private D2DDiffieHellmanKeyExchangeHandshake(State state) { - ourKeyPair = PublicKeyProtoUtil.generateEcP256KeyPair(); - theirPublicKey = null; - initiatorEncodeKey = null; - responderEncodeKey = null; - handshakeState = state; - isInitiator = state == State.INITIATOR_START; - protocolVersionToUse = D2DConnectionContextV1.PROTOCOL_VERSION; - } - - /** - * Creates a new Diffie Hellman handshake context for the handshake initiator - */ - public static D2DDiffieHellmanKeyExchangeHandshake forInitiator() { - return new D2DDiffieHellmanKeyExchangeHandshake(State.INITIATOR_START); - } - - /** - * Creates a new Diffie Hellman handshake context for the handshake responder - */ - public static D2DDiffieHellmanKeyExchangeHandshake forResponder() { - return new D2DDiffieHellmanKeyExchangeHandshake(State.RESPONDER_START); - } - - @Override - public boolean isHandshakeComplete() { - return handshakeState == State.HANDSHAKE_FINISHED - || handshakeState == State.HANDSHAKE_ALREADY_USED; - } - - @Override - public byte[] getNextHandshakeMessage() throws HandshakeException { - switch(handshakeState) { - case INITIATOR_START: - handshakeState = State.INITIATOR_WAITING_FOR_RESPONDER_HELLO; - return InitiatorHello.newBuilder() - .setPublicDhKey(PublicKeyProtoUtil.encodePublicKey(ourKeyPair.getPublic())) - .setProtocolVersion(protocolVersionToUse) - .build() - .toByteArray(); - - case RESPONDER_AFTER_INITIATOR_HELLO: - byte[] responderHello = makeResponderHelloWithPayload(new byte[0]); - handshakeState = State.HANDSHAKE_FINISHED; - return responderHello; - - default: - throw new HandshakeException("Cannot get next message in state: " + handshakeState); - } - } - - @Override - public boolean canSendPayloadInHandshakeMessage() { - return handshakeState == State.RESPONDER_AFTER_INITIATOR_HELLO; - } - - @Override - public byte[] getNextHandshakeMessage(byte[] payload) throws HandshakeException { - if (handshakeState != State.RESPONDER_AFTER_INITIATOR_HELLO) { - throw new HandshakeException( - "Cannot get next message with payload in state: " + handshakeState); - } - - byte[] responderHello = makeResponderHelloWithPayload(payload); - handshakeState = State.HANDSHAKE_FINISHED; - - return responderHello; - } - - private byte[] makeResponderHelloWithPayload(byte[] payload) throws HandshakeException { - if (payload == null) { - throw new HandshakeException("Not expecting null payload"); - } - - try { - SecretKey masterKey = - EnrollmentCryptoOps.doKeyAgreement(ourKeyPair.getPrivate(), theirPublicKey); - - // V0 uses the same key for encoding and decoding, but V1 uses separate keys. - switch (protocolVersionToUse) { - case D2DConnectionContextV0.PROTOCOL_VERSION: - initiatorEncodeKey = masterKey; - responderEncodeKey = masterKey; - break; - case D2DConnectionContextV1.PROTOCOL_VERSION: - initiatorEncodeKey = D2DCryptoOps.deriveNewKeyForPurpose(masterKey, - D2DCryptoOps.INITIATOR_PURPOSE); - responderEncodeKey = D2DCryptoOps.deriveNewKeyForPurpose(masterKey, - D2DCryptoOps.RESPONDER_PURPOSE); - break; - default: - throw new IllegalStateException("Unexpected protocol version: " + protocolVersionToUse); - } - - DeviceToDeviceMessage deviceToDeviceMessage = - D2DConnectionContext.createDeviceToDeviceMessage(payload, 1 /* sequence number */); - - return D2DCryptoOps.signcryptMessageAndResponderHello( - new Payload(PayloadType.DEVICE_TO_DEVICE_RESPONDER_HELLO_PAYLOAD, - deviceToDeviceMessage.toByteArray()), - responderEncodeKey, - ourKeyPair.getPublic(), - protocolVersionToUse); - } catch (InvalidKeyException|NoSuchAlgorithmException e) { - throw new HandshakeException(e); - } - } - - @Override - public byte[] parseHandshakeMessage(byte[] handshakeMessage) throws HandshakeException { - if (handshakeMessage == null || handshakeMessage.length == 0) { - throw new HandshakeException("Handshake message too short"); - } - - switch(handshakeState) { - case INITIATOR_WAITING_FOR_RESPONDER_HELLO: - byte[] payload = parseResponderHello(handshakeMessage); - handshakeState = State.HANDSHAKE_FINISHED; - return payload; - - case RESPONDER_START: - parseInitiatorHello(handshakeMessage); - handshakeState = State.RESPONDER_AFTER_INITIATOR_HELLO; - return new byte[0]; - - default: - throw new HandshakeException("Cannot parse message in state: " + handshakeState); - } - } - - private byte[] parseResponderHello(byte[] responderHello) throws HandshakeException { - try { - ResponderHello responderHelloProto = - D2DCryptoOps.parseAndValidateResponderHello(responderHello); - - // Downgrade to protocol version 0 if needed for backwards compatibility. - int protocolVersion = responderHelloProto.getProtocolVersion(); - if (protocolVersion == D2DConnectionContextV0.PROTOCOL_VERSION) { - protocolVersionToUse = D2DConnectionContextV0.PROTOCOL_VERSION; - } - - SecretKey masterKey = D2DCryptoOps.deriveSharedKeyFromGenericPublicKey( - ourKeyPair.getPrivate(), responderHelloProto.getPublicDhKey()); - - // V0 uses the same key for encoding and decoding, but V1 uses separate keys. - if (protocolVersionToUse == D2DConnectionContextV0.PROTOCOL_VERSION) { - initiatorEncodeKey = masterKey; - responderEncodeKey = masterKey; - } else { - initiatorEncodeKey = D2DCryptoOps.deriveNewKeyForPurpose(masterKey, - D2DCryptoOps.INITIATOR_PURPOSE); - responderEncodeKey = D2DCryptoOps.deriveNewKeyForPurpose(masterKey, - D2DCryptoOps.RESPONDER_PURPOSE); - } - - DeviceToDeviceMessage message = - D2DCryptoOps.decryptResponderHelloMessage(responderEncodeKey, responderHello); - - if (message.getSequenceNumber() != 1) { - throw new HandshakeException("Incorrect sequence number in responder hello"); - } - - return message.getMessage().toByteArray(); - } catch (SignatureException | InvalidProtocolBufferException - | NoSuchAlgorithmException | InvalidKeyException e) { - throw new HandshakeException(e); - } - } - - private void parseInitiatorHello(byte[] initiatorHello) throws HandshakeException { - try { - InitiatorHello initiatorHelloProto = InitiatorHello.parseFrom(initiatorHello); - - if (!initiatorHelloProto.hasPublicDhKey()) { - throw new HandshakeException("Missing public key in initiator hello"); - } - - theirPublicKey = PublicKeyProtoUtil.parsePublicKey(initiatorHelloProto.getPublicDhKey()); - - // Downgrade to protocol version 0 if needed for backwards compatibility. - int protocolVersion = initiatorHelloProto.getProtocolVersion(); - if (protocolVersion == D2DConnectionContextV0.PROTOCOL_VERSION) { - protocolVersionToUse = D2DConnectionContextV0.PROTOCOL_VERSION; - } - } catch (InvalidKeySpecException | InvalidProtocolBufferException e) { - throw new HandshakeException(e); - } - } - - @Override - public D2DConnectionContext toConnectionContext() throws HandshakeException { - if (handshakeState == State.HANDSHAKE_ALREADY_USED) { - throw new HandshakeException("Cannot reuse handshake context; is has already been used"); - } - - if (!isHandshakeComplete()) { - throw new HandshakeException("Handshake is not complete; cannot create connection context"); - } - - handshakeState = State.HANDSHAKE_ALREADY_USED; - - if (protocolVersionToUse == D2DConnectionContextV0.PROTOCOL_VERSION) { - // Both sides start with an initial sequence number of 1 because the last message of the - // handshake had an optional payload with sequence number 1. D2DConnectionContext remembers - // the last sequence number used by each side. - // Note: initiatorEncodeKey == responderEncodeKey - return new D2DConnectionContextV0(initiatorEncodeKey, 1 /** initialSequenceNumber */); - } else { - SecretKey encodeKey = isInitiator ? initiatorEncodeKey : responderEncodeKey; - SecretKey decodeKey = isInitiator ? responderEncodeKey : initiatorEncodeKey; - // Only the responder sends a DeviceToDeviceMessage during the handshake, so it has an initial - // sequence number of 1. The initiator will therefore have an initial sequence number of 0. - int initialEncodeSequenceNumber = isInitiator ? 0 : 1; - int initialDecodeSequenceNumber = isInitiator ? 1 : 0; - return new D2DConnectionContextV1( - encodeKey, decodeKey, initialEncodeSequenceNumber, initialDecodeSequenceNumber); - } - } -} diff --git a/src/main/java/com/google/security/cryptauth/lib/securegcm/D2DHandshakeContext.java b/src/main/java/com/google/security/cryptauth/lib/securegcm/D2DHandshakeContext.java deleted file mode 100644 index 5fc1d7b..0000000 --- a/src/main/java/com/google/security/cryptauth/lib/securegcm/D2DHandshakeContext.java +++ /dev/null @@ -1,158 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.security.cryptauth.lib.securegcm; - -/** - * Describes a cryptographic handshake with arbitrary number of round trips. Some handshake - * messages may also send a payload. - * - * <p>Generic usage for handshake Initiator: - * {@code - * // Handshake Initiator - * D2DHandshakeContext handshake = <specific handshake>.forInitiator(); - * while (!handshake.isHandshakeComplete()) { - * try { - * // Get the next handshake message to send - * byte[] initiatorMessage = handshake.getNextHandshakeMessage(); - * - * // Send the message out and get the response - * socket.send(initiatorMessage); - * byte[] responderMessage = socket.read(); - * - * // Handle the response and obtain the optional payload - * byte[] payload = handshake.parseHandshakeMessage(responderMessage); - * - * // Handle the payload if one was sent - * if (payload.length > 0) { - * handlePayload(payload); - * } - * } catch (HandshakeException e) { - * // Handshake has failed, bail - * Log("Handshake failed!", e); - * return; - * } - * } - * - * ConnectionContext connectionContext; - * try { - * // Upgrade handshake context to a full connection context - * connectionContext = handshake.toConnectionContext(); - * } catch (HandshakeException e) { - * Log("Cannot convert handshake to connection context", e); - * } - * } - * - * <p>Generic usage for handshake Responder: - * {@code - * // Handshake Responder - * D2DHandshakeContext handshake = <specific handshake>.forResponder(); - * - * while (!handshake.isHandshakeComplete()) { - * try { - * // Get the message from the initiator - * byte[] initiatorMessage = socket.read(); - * - * // Handle the message and get the payload if it exists - * byte[] payload = handshake.parseHandshakeMessage(initiatorMessage); - * - * // Handle the payload if one was sent - * if (payload.length > 0) { - * handlePayload(payload); - * } - * - * // Make sure that wasn't the last message - * if (handshake.isHandshakeComplete()) { - * break; - * } - * - * // Get next message to send and send it - * byte[] responderMessage = handshake.getNextHandshakeMessage(); - * socket.send(responderMessage); - * } catch (HandshakeException e) { - * // Handshake has failed, bail - * Log("Handshake failed!", e); - * return; - * } - * } - * - * ConnectionContext connectionContext; - * try { - * // Upgrade handshake context to a full connection context - * connectionContext = handshake.toConnectionContext(); - * } catch (HandshakeException e) { - * Log("Cannot convert handshake to connection context", e); - * } - * } - */ -public interface D2DHandshakeContext { - - /** - * Tells the caller whether the handshake has completed or not. If the handshake is complete, the - * caller may call {@link #toConnectionContext()} to obtain a connection context. - * - * @return true if the handshake is complete, false otherwise - */ - boolean isHandshakeComplete(); - - /** - * Constructs the next message that should be sent in the handshake. - * - * @return the next message - * @throws HandshakeException if the handshake is over or if the next handshake message can't be - * obtained (e.g., there is an internal error) - */ - byte[] getNextHandshakeMessage() throws HandshakeException; - - /** - * Tells the caller whether the next handshake message may carry a payload. If true, caller may - * call {@link #getNextHandshakeMessage(byte[])} instead of the regular - * {@link #getNextHandshakeMessage()}. If false, calling {@link #getNextHandshakeMessage(byte[])} - * will result in a {@link HandshakeException}. - * - * @return true if the next handshake message can carry a payload, false otherwise - */ - boolean canSendPayloadInHandshakeMessage(); - - /** - * Constructs the next message that should be sent in the handshake along with a payload. Caller - * should verify that this method can be called by calling - * {@link #canSendPayloadInHandshakeMessage()}. - * - * @param payload the payload to include in the handshake message - * @return the next message - * @throws HandshakeException if the handshake is over or if the next handshake message can't be - * obtained (e.g., there is an internal error) or if the payload may not be included in this - * message - */ - byte[] getNextHandshakeMessage(byte[] payload) throws HandshakeException; - - /** - * Parses a handshake message and returns the included payload (if any). - * - * @param handshakeMessage message received in the handshake - * @return payload or empty byte[] if no payload was in the handshake message - * @throws HandshakeException if an error occurs in parsing the handshake message - */ - byte[] parseHandshakeMessage(byte[] handshakeMessage) throws HandshakeException; - - /** - * Creates a full {@link D2DConnectionContext}. May only be called if - * {@link #isHandshakeComplete()} returns true. - * - * @return a full {@link D2DConnectionContext} - * @throws HandshakeException if a connection context cannot be created - */ - D2DConnectionContext toConnectionContext() throws HandshakeException; -} diff --git a/src/main/java/com/google/security/cryptauth/lib/securegcm/Ed25519.java b/src/main/java/com/google/security/cryptauth/lib/securegcm/Ed25519.java deleted file mode 100644 index 454b942..0000000 --- a/src/main/java/com/google/security/cryptauth/lib/securegcm/Ed25519.java +++ /dev/null @@ -1,270 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.security.cryptauth.lib.securegcm; - -import static java.math.BigInteger.ONE; -import static java.math.BigInteger.ZERO; - -import com.google.common.annotations.VisibleForTesting; -import java.math.BigInteger; - -/** - * Implements the Ed25519 twisted Edwards curve. See http://ed25519.cr.yp.to/ for more details. - */ -public class Ed25519 { - - // Don't instantiate - private Ed25519() { } - - // Curve parameters (http://ed25519.cr.yp.to/) - private static final int HEX_RADIX = 16; - private static final BigInteger Ed25519_P = - new BigInteger("7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED", HEX_RADIX); - private static final BigInteger Ed25519_D = - new BigInteger("52036CEE2B6FFE738CC740797779E89800700A4D4141D8AB75EB4DCA135978A3", HEX_RADIX); - - // Helps to do fast addition k = 2*d - private static final BigInteger Ed25519_K = - new BigInteger("2406D9DC56DFFCE7198E80F2EEF3D13000E0149A8283B156EBD69B9426B2F159", HEX_RADIX); - - // Identity point in extended representation (0, 1, 1, 0) - static final BigInteger[] IDENTITY_POINT = new BigInteger[] {ZERO, ONE, ONE, ZERO}; - - // Helps for reading coordinate type in point representation - private static final int X = 0; - private static final int Y = 1; - private static final int Z = 2; - private static final int T = 3; - - // Number of bits that we need to represent a point. Realistically, we only need 255, but using - // 256 doesn't hurt. - private static final int POINT_SIZE_BITS = 256; - - /** - * Returns the result of multiplying point p by scalar k. A point is represented as a BigInteger - * array of length 2 where the first element (at index 0) is the X coordinate and the second - * element (at index 1) is the Y coordinate. - */ - public static BigInteger[] scalarMultiplyAffinePoint(BigInteger[] p, BigInteger k) - throws Ed25519Exception { - return toAffine(scalarMultiplyExtendedPoint(toExtended(p), k)); - } - - /** - * Returns the sum of two points in affine representation. A point is represented as a BigInteger - * array of length 2 where the first element (at index 0) is the X coordinate and the second - * element (at index 1) is the Y coordinate. - */ - public static BigInteger[] addAffinePoints(BigInteger[] p1, BigInteger[] p2) - throws Ed25519Exception { - return toAffine(addExtendedPoints(toExtended(p1), toExtended(p2))); - } - - /** - * Returns the result of subtracting p2 from p1 (i.e., p1 - p2) in affine representation. A point - * is represented as a BigInteger array of length 2 where the first element (at index 0) is the X - * coordinate and the second element (at index 1) is the Y coordinate. - */ - public static BigInteger[] subtractAffinePoints(BigInteger[] p1, BigInteger[] p2) - throws Ed25519Exception { - return toAffine(subtractExtendedPoints(toExtended(p1), toExtended(p2))); - } - - /** - * Validates that a given point in affine representation is on the curve and is positive. - * @throws Ed25519Exception if the point does not validate - */ - public static void validateAffinePoint(BigInteger[] p) throws Ed25519Exception { - checkPointIsInAffineRepresentation(p); - - BigInteger x = p[X]; - BigInteger y = p[Y]; - - if (x.signum() != 1 || y.signum() != 1) { - throw new Ed25519Exception("Point encoding must use only positive integers"); - } - - if ((x.compareTo(Ed25519_P) >= 0) || (y.compareTo(Ed25519_P) >= 0)) { - throw new Ed25519Exception("Point lies outside of the expected field"); - } - - BigInteger xx = x.multiply(x); - BigInteger yy = y.multiply(y); - BigInteger lhs = xx.negate().add(yy).mod(Ed25519_P); // -x*x + y*y - BigInteger rhs = ONE.add(Ed25519_D.multiply(xx).multiply(yy)).mod(Ed25519_P); // 1 + d*x*x*y*y - - if (!lhs.equals(rhs)) { - throw new Ed25519Exception("Point does not lie on the expected curve"); - } - } - - /** - * Returns the result of multiplying point p by scalar k - */ - static BigInteger[] scalarMultiplyExtendedPoint(BigInteger[] p, BigInteger k) - throws Ed25519Exception { - checkPointIsInExtendedRepresentation(p); - if (k == null) { - throw new Ed25519Exception("Can't multiply point by null"); - } - - if (k.bitLength() > POINT_SIZE_BITS) { - throw new Ed25519Exception( - "Refuse to multiply point by scalar with more than " + POINT_SIZE_BITS + " bits"); - } - - // Perform best effort time-constant accumulation - BigInteger[] q = IDENTITY_POINT; - BigInteger[] r = IDENTITY_POINT; - BigInteger[] doubleAccumulator = p; - for (int i = 0; i < POINT_SIZE_BITS; i++) { - if (k.testBit(i)) { - q = addExtendedPoints(q, doubleAccumulator); - } else { - r = addExtendedPoints(q, doubleAccumulator); - } - if (i < POINT_SIZE_BITS - 1) { - doubleAccumulator = doubleExtendedPoint(doubleAccumulator); - } - } - - // Not needed, but we're just trying to fool the compiler into not optimizing away r - r = subtractExtendedPoints(r, r); - q = addExtendedPoints(q, r); - return q; - } - - /** - * Returns the doubling of a point in extended representation - */ - private static BigInteger[] doubleExtendedPoint(BigInteger[] p) throws Ed25519Exception { - // The Edwards curve is complete, so we can just add a point to itself. - // Note that the currently best known algorithms for doubling have the same order as addition. - // https://hyperelliptic.org/EFD/g1p/auto-twisted-extended-1.html - checkPointIsInExtendedRepresentation(p); - - BigInteger c = p[T].pow(2).multiply(Ed25519_K); - BigInteger d = p[Z].pow(2).shiftLeft(1); - BigInteger e = p[Y].multiply(p[X]).shiftLeft(2); - BigInteger f = d.subtract(c); - BigInteger g = d.add(c); - BigInteger h = p[Y].pow(2).add(p[X].pow(2)).shiftLeft(1); - - return new BigInteger[] { - e.multiply(f).mod(Ed25519_P), - g.multiply(h).mod(Ed25519_P), - f.multiply(g).mod(Ed25519_P), - e.multiply(h).mod(Ed25519_P) - }; - } - - /** - * Returns the result of subtracting p2 from p1 (p1 - p2) - */ - static BigInteger[] subtractExtendedPoints(BigInteger[] p1, BigInteger[] p2) - throws Ed25519Exception { - checkPointIsInExtendedRepresentation(p1); - checkPointIsInExtendedRepresentation(p2); - - return addExtendedPoints(p1, new BigInteger[] {p2[X].negate(), p2[Y], p2[Z], p2[T].negate()}); - } - - /** - * Returns the sum of two points in extended representation - * Uses: https://hyperelliptic.org/EFD/g1p/auto-twisted-extended-1.html#addition-add-2008-hwcd-3 - */ - static BigInteger[] addExtendedPoints(BigInteger[] p1, BigInteger[] p2) - throws Ed25519Exception { - checkPointIsInExtendedRepresentation(p1); - checkPointIsInExtendedRepresentation(p2); - - BigInteger a = p1[Y].subtract(p1[X]).multiply(p2[Y].subtract(p2[X])); - BigInteger b = p1[Y].add(p1[X]).multiply(p2[Y].add(p2[X])); - BigInteger c = p1[T].multiply(Ed25519_K).multiply(p2[T]); - BigInteger d = p1[Z].add(p1[Z]).multiply(p2[Z]); - BigInteger e = b.subtract(a); - BigInteger f = d.subtract(c); - BigInteger g = d.add(c); - BigInteger h = b.add(a); - - return new BigInteger[] { - e.multiply(f).mod(Ed25519_P), - g.multiply(h).mod(Ed25519_P), - f.multiply(g).mod(Ed25519_P), - e.multiply(h).mod(Ed25519_P) - }; - } - - /** Converts a point in affine representation to extended representation */ - // TODO(b/120887495): This @VisibleForTesting annotation was being ignored by prod code. - // Please check that removing it is correct, and remove this comment along with it. - // @VisibleForTesting - static BigInteger[] toExtended(BigInteger[] p) throws Ed25519Exception { - checkPointIsInAffineRepresentation(p); - - return new BigInteger[] {p[X], p[Y], ONE, p[X].multiply(p[Y]).mod(Ed25519_P)}; // x, y, 1, x*y - } - - /** Converts a point in extended representation to affine representation */ - // TODO(b/120887495): This @VisibleForTesting annotation was being ignored by prod code. - // Please check that removing it is correct, and remove this comment along with it. - // @VisibleForTesting - static BigInteger[] toAffine(BigInteger[] p) throws Ed25519Exception { - checkPointIsInExtendedRepresentation(p); - - return new BigInteger[] {p[X].multiply(p[Z].modInverse(Ed25519_P)).mod(Ed25519_P), // x = X / Z - p[Y].multiply(p[Z].modInverse(Ed25519_P)).mod(Ed25519_P)}; // y = Y / Z - } - - /** - * Checks that a given point is in the extended representation - * @throws Ed25519Exception if the point is not in the extended representation - */ - @VisibleForTesting - static void checkPointIsInExtendedRepresentation(BigInteger[] p) throws Ed25519Exception { - if (p == null || p.length != 4 || p[X] == null || p[Y] == null || p[Z] == null - || p[T] == null) { - throw new Ed25519Exception("Point is not in extended representation"); - } - } - - /** - * Checks that a given point is in the affine representation - * @throws Ed25519Exception if the point is not in the affine representation - */ - @VisibleForTesting - static void checkPointIsInAffineRepresentation(BigInteger[] p) throws Ed25519Exception { - if (p == null || p.length != 2 || p[X] == null || p[Y] == null) { - throw new Ed25519Exception("Point is not in affine representation"); - } - } - - /** - * Represents an unrecoverable error that has occurred while performing a curve operation. - */ - public static class Ed25519Exception extends Exception { - public Ed25519Exception(String message) { - super(message); - } - - public Ed25519Exception(Exception e) { - super(e); - } - - public Ed25519Exception(String message, Exception e) { - super(message, e); - } - } -} diff --git a/src/main/java/com/google/security/cryptauth/lib/securegcm/EnrollmentCryptoOps.java b/src/main/java/com/google/security/cryptauth/lib/securegcm/EnrollmentCryptoOps.java deleted file mode 100644 index 450c806..0000000 --- a/src/main/java/com/google/security/cryptauth/lib/securegcm/EnrollmentCryptoOps.java +++ /dev/null @@ -1,233 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.security.cryptauth.lib.securegcm; - -import com.google.protobuf.InvalidProtocolBufferException; -import com.google.security.annotations.SuppressInsecureCipherModeCheckerPendingReview; -import com.google.security.cryptauth.lib.securegcm.SecureGcmProto.GcmDeviceInfo; -import com.google.security.cryptauth.lib.securegcm.SecureGcmProto.GcmMetadata; -import com.google.security.cryptauth.lib.securegcm.TransportCryptoOps.PayloadType; -import com.google.security.cryptauth.lib.securemessage.CryptoOps.EncType; -import com.google.security.cryptauth.lib.securemessage.CryptoOps.SigType; -import com.google.security.cryptauth.lib.securemessage.PublicKeyProtoUtil; -import com.google.security.cryptauth.lib.securemessage.SecureMessageBuilder; -import com.google.security.cryptauth.lib.securemessage.SecureMessageParser; -import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.HeaderAndBody; -import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.SecureMessage; -import java.security.InvalidKeyException; -import java.security.KeyPair; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.SignatureException; -import java.security.spec.InvalidKeySpecException; -import java.util.Arrays; -import javax.crypto.KeyAgreement; -import javax.crypto.SecretKey; - -/** - * Utility class for implementing Secure GCM enrollment flows. - */ -public class EnrollmentCryptoOps { - - private EnrollmentCryptoOps() { } // Do not instantiate - - /** - * Type of symmetric key signature to use for the signcrypted "outer layer" message. - */ - private static final SigType OUTER_SIG_TYPE = SigType.HMAC_SHA256; - - /** - * Type of symmetric key encryption to use for the signcrypted "outer layer" message. - */ - private static final EncType OUTER_ENC_TYPE = EncType.AES_256_CBC; - - /** - * Type of public key signature to use for the (cleartext) "inner layer" message. - */ - private static final SigType INNER_SIG_TYPE = SigType.ECDSA_P256_SHA256; - - /** - * Type of public key signature to use for the (cleartext) "inner layer" message on platforms that - * don't support Elliptic Curve operations (such as old Android versions). - */ - private static final SigType LEGACY_INNER_SIG_TYPE = SigType.RSA2048_SHA256; - - /** - * Which {@link KeyAgreement} algorithm to use. - */ - private static final String KA_ALG = "ECDH"; - - /** - * Which {@link KeyAgreement} algorithm to use on platforms that don't support Elliptic Curve. - */ - private static final String LEGACY_KA_ALG = "DH"; - - /** - * Used by both the client and server to perform a key exchange. - * - * @return a {@link SecretKey} derived from the key exchange - * @throws InvalidKeyException if either of the input keys is of the wrong type - */ - @SuppressInsecureCipherModeCheckerPendingReview // b/32143855 - public static SecretKey doKeyAgreement(PrivateKey myKey, PublicKey peerKey) - throws InvalidKeyException { - String alg = KA_ALG; - if (KeyEncoding.isLegacyPrivateKey(myKey)) { - alg = LEGACY_KA_ALG; - } - KeyAgreement agreement; - try { - agreement = KeyAgreement.getInstance(alg); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } - - agreement.init(myKey); - agreement.doPhase(peerKey, true); - byte[] agreedKey = agreement.generateSecret(); - - // Derive a 256-bit AES key by using sha256 on the Diffie-Hellman output - return KeyEncoding.parseMasterKey(sha256(agreedKey)); - } - - public static KeyPair generateEnrollmentKeyAgreementKeyPair(boolean isLegacy) { - if (isLegacy) { - return PublicKeyProtoUtil.generateDh2048KeyPair(); - } - return PublicKeyProtoUtil.generateEcP256KeyPair(); - } - - /** - * @return SHA-256 hash of {@code masterKey} - */ - public static byte[] getMasterKeyHash(SecretKey masterKey) { - return sha256(masterKey.getEncoded()); - } - - /** - * Used by the client to signcrypt an enrollment request before sending it to the server. - * - * <p>Note: You <em>MUST</em> correctly set the value of the {@code device_master_key_hash} on - * {@code enrollmentInfo} from {@link #getMasterKeyHash(SecretKey)} before calling this method. - * - * @param enrollmentInfo the enrollment request to send to the server. You must correctly set - * the {@code device_master_key_hash} field. - * @param masterKey the shared key derived from the key agreement - * @param signingKey the signing key corresponding to the user's {@link PublicKey} being enrolled - * @return the encrypted enrollment message - * @throws IllegalArgumentException if {@code enrollmentInfo} doesn't have a valid - * {@code device_master_key_hash} - * @throws InvalidKeyException if {@code masterKey} or {@code signingKey} is the wrong type - */ - public static byte[] encryptEnrollmentMessage( - GcmDeviceInfo enrollmentInfo, SecretKey masterKey, PrivateKey signingKey) - throws InvalidKeyException, NoSuchAlgorithmException { - if ((enrollmentInfo == null) || (masterKey == null) || (signingKey == null)) { - throw new NullPointerException(); - } - - if (!Arrays.equals(enrollmentInfo.getDeviceMasterKeyHash().toByteArray(), - getMasterKeyHash(masterKey))) { - throw new IllegalArgumentException("DeviceMasterKeyHash not set correctly"); - } - - // First create the inner message, which is basically a self-signed certificate - SigType sigType = - KeyEncoding.isLegacyPrivateKey(signingKey) ? LEGACY_INNER_SIG_TYPE : INNER_SIG_TYPE; - SecureMessage innerMsg = new SecureMessageBuilder() - .setVerificationKeyId(enrollmentInfo.getUserPublicKey().toByteArray()) - .buildSignedCleartextMessage(signingKey, sigType, enrollmentInfo.toByteArray()); - - // Next create the outer message, which uses the newly exchanged master key to signcrypt - SecureMessage outerMsg = new SecureMessageBuilder() - .setVerificationKeyId(new byte[] {}) // Empty - .setPublicMetadata(GcmMetadata.newBuilder() - .setType(PayloadType.ENROLLMENT.getType()) - .setVersion(SecureGcmConstants.SECURE_GCM_VERSION) - .build() - .toByteArray()) - .buildSignCryptedMessage( - masterKey, OUTER_SIG_TYPE, masterKey, OUTER_ENC_TYPE, innerMsg.toByteArray()); - return outerMsg.toByteArray(); - } - - /** - * Used by the server to decrypt the client's enrollment request. - * @param enrollmentMessage generated by the client's call to - * {@link #encryptEnrollmentMessage(GcmDeviceInfo, SecretKey, PrivateKey)} - * @param masterKey the shared key derived from the key agreement - * @return the client's enrollment request data - * @throws SignatureException if {@code enrollmentMessage} is malformed or has been tampered with - * @throws InvalidKeyException if {@code masterKey} is the wrong type - */ - public static GcmDeviceInfo decryptEnrollmentMessage( - byte[] enrollmentMessage, SecretKey masterKey, boolean isLegacy) - throws SignatureException, InvalidKeyException, NoSuchAlgorithmException { - if ((enrollmentMessage == null) || (masterKey == null)) { - throw new NullPointerException(); - } - - HeaderAndBody outerHeaderAndBody; - GcmMetadata outerMetadata; - HeaderAndBody innerHeaderAndBody; - byte[] encodedUserPublicKey; - GcmDeviceInfo enrollmentInfo; - try { - SecureMessage outerMsg = SecureMessage.parseFrom(enrollmentMessage); - outerHeaderAndBody = SecureMessageParser.parseSignCryptedMessage( - outerMsg, masterKey, OUTER_SIG_TYPE, masterKey, OUTER_ENC_TYPE); - outerMetadata = GcmMetadata.parseFrom(outerHeaderAndBody.getHeader().getPublicMetadata()); - - SecureMessage innerMsg = SecureMessage.parseFrom(outerHeaderAndBody.getBody()); - encodedUserPublicKey = SecureMessageParser.getUnverifiedHeader(innerMsg) - .getVerificationKeyId().toByteArray(); - PublicKey userPublicKey = KeyEncoding.parseUserPublicKey(encodedUserPublicKey); - SigType sigType = isLegacy ? LEGACY_INNER_SIG_TYPE : INNER_SIG_TYPE; - innerHeaderAndBody = SecureMessageParser.parseSignedCleartextMessage( - innerMsg, userPublicKey, sigType); - enrollmentInfo = GcmDeviceInfo.parseFrom(innerHeaderAndBody.getBody()); - } catch (InvalidProtocolBufferException e) { - throw new SignatureException(e); - } catch (InvalidKeySpecException e) { - throw new SignatureException(e); - } - - boolean verified = - (outerMetadata.getType() == PayloadType.ENROLLMENT.getType()) - && (outerMetadata.getVersion() <= SecureGcmConstants.SECURE_GCM_VERSION) - && outerHeaderAndBody.getHeader().getVerificationKeyId().isEmpty() - && innerHeaderAndBody.getHeader().getPublicMetadata().isEmpty() - // Verify the encoded public key we used matches the encoded public key key being enrolled - && Arrays.equals(encodedUserPublicKey, enrollmentInfo.getUserPublicKey().toByteArray()) - && Arrays.equals(getMasterKeyHash(masterKey), - enrollmentInfo.getDeviceMasterKeyHash().toByteArray()); - - if (verified) { - return enrollmentInfo; - } - throw new SignatureException(); - } - - static byte[] sha256(byte[] input) { - try { - MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); - return sha256.digest(input); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); // Shouldn't happen - } - } -} diff --git a/src/main/java/com/google/security/cryptauth/lib/securegcm/HandshakeException.java b/src/main/java/com/google/security/cryptauth/lib/securegcm/HandshakeException.java deleted file mode 100644 index b717eb6..0000000 --- a/src/main/java/com/google/security/cryptauth/lib/securegcm/HandshakeException.java +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.security.cryptauth.lib.securegcm; - -/** - * Represents an unrecoverable error that has occurred during the handshake procedure. - */ -public class HandshakeException extends Exception { - public HandshakeException(String message) { - super(message); - } - - public HandshakeException(Exception e) { - super(e); - } - - public HandshakeException(String message, Exception e) { - super(message, e); - } -} diff --git a/src/main/java/com/google/security/cryptauth/lib/securegcm/KeyEncoding.java b/src/main/java/com/google/security/cryptauth/lib/securegcm/KeyEncoding.java deleted file mode 100644 index 67e4ace..0000000 --- a/src/main/java/com/google/security/cryptauth/lib/securegcm/KeyEncoding.java +++ /dev/null @@ -1,180 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.security.cryptauth.lib.securegcm; - -import com.google.protobuf.InvalidProtocolBufferException; -import com.google.security.cryptauth.lib.securemessage.PublicKeyProtoUtil; -import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.GenericPublicKey; -import java.security.KeyFactory; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.interfaces.ECPrivateKey; -import java.security.interfaces.ECPublicKey; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.PKCS8EncodedKeySpec; -import javax.crypto.SecretKey; -import javax.crypto.interfaces.DHPrivateKey; -import javax.crypto.spec.SecretKeySpec; - -/** - * Utility class for encoding and parsing keys used by SecureGcm. - */ -public class KeyEncoding { - private KeyEncoding() {} // Do not instantiate - - private static boolean simulateLegacyCryptoRequired = false; - - /** - * The JCA algorithm name to use when encoding/decoding symmetric keys. - */ - static final String SYMMETRIC_KEY_ENCODING_ALG = "AES"; - - public static byte[] encodeMasterKey(SecretKey masterKey) { - return masterKey.getEncoded(); - } - - public static SecretKey parseMasterKey(byte[] encodedMasterKey) { - return new SecretKeySpec(encodedMasterKey, SYMMETRIC_KEY_ENCODING_ALG); - } - - public static byte[] encodeUserPublicKey(PublicKey pk) { - return encodePublicKey(pk); - } - - public static byte[] encodeUserPrivateKey(PrivateKey sk) { - return sk.getEncoded(); - } - - public static byte[] encodeDeviceSyncGroupPublicKey(PublicKey pk) { - return PublicKeyProtoUtil.encodePaddedEcPublicKey(pk).toByteArray(); - } - - public static PrivateKey parseUserPrivateKey(byte[] encodedPrivateKey, boolean isLegacy) - throws InvalidKeySpecException { - PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encodedPrivateKey); - if (isLegacy) { - return getRsaKeyFactory().generatePrivate(keySpec); - } - return getEcKeyFactory().generatePrivate(keySpec); - } - - public static PublicKey parseUserPublicKey(byte[] keyBytes) throws InvalidKeySpecException { - return parsePublicKey(keyBytes); - } - - public static PublicKey parseDeviceSyncGroupPublicKey(byte[] keyBytes) - throws InvalidKeySpecException { - return parsePublicKey(keyBytes); - } - - public static byte[] encodeKeyAgreementPublicKey(PublicKey pk) { - return encodePublicKey(pk); - } - - public static PublicKey parseKeyAgreementPublicKey(byte[] keyBytes) - throws InvalidKeySpecException { - return parsePublicKey(keyBytes); - } - - public static byte[] encodeKeyAgreementPrivateKey(PrivateKey sk) { - if (isLegacyPrivateKey(sk)) { - return PublicKeyProtoUtil.encodeDh2048PrivateKey((DHPrivateKey) sk); - } - return sk.getEncoded(); - } - - public static PrivateKey parseKeyAgreementPrivateKey(byte[] keyBytes, boolean isLegacy) - throws InvalidKeySpecException { - if (isLegacy) { - return PublicKeyProtoUtil.parseDh2048PrivateKey(keyBytes); - } - PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); - return getEcKeyFactory().generatePrivate(keySpec); - } - - public static byte[] encodeSigningPublicKey(PublicKey pk) { - return encodePublicKey(pk); - } - - public static PublicKey parseSigningPublicKey(byte[] keyBytes) throws InvalidKeySpecException { - return parsePublicKey(keyBytes); - } - - public static byte[] encodeSigningPrivateKey(PrivateKey sk) { - return sk.getEncoded(); - } - - public static PrivateKey parseSigningPrivateKey(byte[] keyBytes) throws InvalidKeySpecException { - PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); - return getEcKeyFactory().generatePrivate(keySpec); - } - - public static boolean isLegacyPublicKey(PublicKey pk) { - if (pk instanceof ECPublicKey) { - return false; - } - return true; - } - - public static boolean isLegacyPrivateKey(PrivateKey sk) { - if (sk instanceof ECPrivateKey) { - return false; - } - return true; - } - - public static boolean isLegacyCryptoRequired() { - return PublicKeyProtoUtil.isLegacyCryptoRequired() || simulateLegacyCryptoRequired; - } - - /** - * When testing, use this to force {@link #isLegacyCryptoRequired()} to return {@code true} - */ - // @VisibleForTesting - public static void setSimulateLegacyCrypto(boolean forceLegacy) { - simulateLegacyCryptoRequired = forceLegacy; - } - - private static byte[] encodePublicKey(PublicKey pk) { - return PublicKeyProtoUtil.encodePublicKey(pk).toByteArray(); - } - - private static PublicKey parsePublicKey(byte[] keyBytes) throws InvalidKeySpecException { - try { - return PublicKeyProtoUtil.parsePublicKey(GenericPublicKey.parseFrom(keyBytes)); - } catch (InvalidProtocolBufferException e) { - throw new InvalidKeySpecException("Unable to parse GenericPublicKey", e); - } catch (IllegalArgumentException e) { - throw new InvalidKeySpecException("Unable to parse GenericPublicKey", e); - } - } - - static KeyFactory getEcKeyFactory() { - try { - return KeyFactory.getInstance("EC"); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); // No ECDH provider available - } - } - - static KeyFactory getRsaKeyFactory() { - try { - return KeyFactory.getInstance("RSA"); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); // No RSA provider available - } - } -} diff --git a/src/main/java/com/google/security/cryptauth/lib/securegcm/SecureGcmConstants.java b/src/main/java/com/google/security/cryptauth/lib/securegcm/SecureGcmConstants.java deleted file mode 100644 index a69431f..0000000 --- a/src/main/java/com/google/security/cryptauth/lib/securegcm/SecureGcmConstants.java +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.security.cryptauth.lib.securegcm; - -/** - * A container for GCM related constants used by SecureGcm channels. - */ -public final class SecureGcmConstants { - private SecureGcmConstants() {} // Do not instantiate - - public static final int SECURE_GCM_VERSION = 1; - - /** - * The GCM sender identity used by this library (GMSCore). - */ - public static final String SENDER_ID = "745476177629"; - - /** - * The key used for indexing the GCM {@link TransportCryptoOps.Payload} within {@code AppData}. - */ - public static final String MESSAGE_KEY = "P"; - - /** - * The origin that should be use for GCM device enrollments. - */ - public static final String GOOGLE_ORIGIN = "google.com"; - - /** - * The origin that should be use for GCM Legacy android device enrollments. - */ - public static final String LEGACY_ANDROID_ORIGIN = "c.g.a.gms"; - - /** - * The name of the protocol this library speaks. - */ - public static final String PROTOCOL_TYPE_NAME = "gcmV1"; -} diff --git a/src/main/java/com/google/security/cryptauth/lib/securegcm/TransportCryptoOps.java b/src/main/java/com/google/security/cryptauth/lib/securegcm/TransportCryptoOps.java deleted file mode 100644 index b053bf4..0000000 --- a/src/main/java/com/google/security/cryptauth/lib/securegcm/TransportCryptoOps.java +++ /dev/null @@ -1,268 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.security.cryptauth.lib.securegcm; - -import com.google.protobuf.InvalidProtocolBufferException; -import com.google.security.cryptauth.lib.securegcm.SecureGcmProto.GcmMetadata; -import com.google.security.cryptauth.lib.securemessage.CryptoOps.EncType; -import com.google.security.cryptauth.lib.securemessage.CryptoOps.SigType; -import com.google.security.cryptauth.lib.securemessage.SecureMessageBuilder; -import com.google.security.cryptauth.lib.securemessage.SecureMessageParser; -import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.HeaderAndBody; -import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.SecureMessage; -import java.security.InvalidKeyException; -import java.security.KeyPair; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.SignatureException; -import java.security.interfaces.ECPublicKey; -import java.security.interfaces.RSAPublicKey; -import javax.crypto.SecretKey; - -/** - * Utility class for implementing a secure transport for GCM messages. - */ -public class TransportCryptoOps { - private TransportCryptoOps() {} // Do not instantiate - - /** - * A type safe version of the {@link SecureGcmProto} {@code Type} codes. - */ - public enum PayloadType { - ENROLLMENT(SecureGcmProto.Type.ENROLLMENT), - TICKLE(SecureGcmProto.Type.TICKLE), - TX_REQUEST(SecureGcmProto.Type.TX_REQUEST), - TX_REPLY(SecureGcmProto.Type.TX_REPLY), - TX_SYNC_REQUEST(SecureGcmProto.Type.TX_SYNC_REQUEST), - TX_SYNC_RESPONSE(SecureGcmProto.Type.TX_SYNC_RESPONSE), - TX_PING(SecureGcmProto.Type.TX_PING), - DEVICE_INFO_UPDATE(SecureGcmProto.Type.DEVICE_INFO_UPDATE), - TX_CANCEL_REQUEST(SecureGcmProto.Type.TX_CANCEL_REQUEST), - LOGIN_NOTIFICATION(SecureGcmProto.Type.LOGIN_NOTIFICATION), - PROXIMITYAUTH_PAIRING(SecureGcmProto.Type.PROXIMITYAUTH_PAIRING), - GCMV1_IDENTITY_ASSERTION(SecureGcmProto.Type.GCMV1_IDENTITY_ASSERTION), - DEVICE_TO_DEVICE_RESPONDER_HELLO_PAYLOAD( - SecureGcmProto.Type.DEVICE_TO_DEVICE_RESPONDER_HELLO_PAYLOAD), - DEVICE_TO_DEVICE_MESSAGE(SecureGcmProto.Type.DEVICE_TO_DEVICE_MESSAGE), - DEVICE_PROXIMITY_CALLBACK(SecureGcmProto.Type.DEVICE_PROXIMITY_CALLBACK), - UNLOCK_KEY_SIGNED_CHALLENGE(SecureGcmProto.Type.UNLOCK_KEY_SIGNED_CHALLENGE); - - private final SecureGcmProto.Type type; - PayloadType(SecureGcmProto.Type type) { - this.type = type; - } - - public SecureGcmProto.Type getType() { - return this.type; - } - - public static PayloadType valueOf(SecureGcmProto.Type type) { - return PayloadType.valueOf(type.getNumber()); - } - - public static PayloadType valueOf(int type) { - for (PayloadType payloadType : PayloadType.values()) { - if (payloadType.getType().getNumber() == type) { - return payloadType; - } - } - throw new IllegalArgumentException("Unsupported payload type: " + type); - } - } - - /** - * Encapsulates a {@link PayloadType} specifier, and a corresponding raw {@code message} payload. - */ - public static class Payload { - private final PayloadType payloadType; - private final byte[] message; - - public Payload(PayloadType payloadType, byte[] message) { - if ((payloadType == null) || (message == null)) { - throw new NullPointerException(); - } - this.payloadType = payloadType; - this.message = message; - } - - public PayloadType getPayloadType() { - return payloadType; - } - - public byte[] getMessage() { - return message; - } - } - - /** - * Used by the the server-side to send a secure {@link Payload} to the client. - * - * @param masterKey used to signcrypt the {@link Payload} - * @param keyHandle the name by which the client refers to the specified {@code masterKey} - */ - public static byte[] signcryptServerMessage( - Payload payload, SecretKey masterKey, byte[] keyHandle) - throws InvalidKeyException, NoSuchAlgorithmException { - if ((payload == null) || (masterKey == null) || (keyHandle == null)) { - throw new NullPointerException(); - } - return new SecureMessageBuilder() - .setVerificationKeyId(keyHandle) - .setPublicMetadata(GcmMetadata.newBuilder() - .setType(payload.getPayloadType().getType()) - .setVersion(SecureGcmConstants.SECURE_GCM_VERSION) - .build() - .toByteArray()) - .buildSignCryptedMessage( - masterKey, - SigType.HMAC_SHA256, - masterKey, - EncType.AES_256_CBC, - payload.getMessage()) - .toByteArray(); - } - - /** - * Extracts the {@code keyHandle} from a {@code signcryptedMessage}. - * - * @see #signcryptServerMessage(Payload, SecretKey, byte[]) - */ - public static byte[] getKeyHandleFor(byte[] signcryptedServerMessage) - throws InvalidProtocolBufferException { - if (signcryptedServerMessage == null) { - throw new NullPointerException(); - } - SecureMessage secmsg = SecureMessage.parseFrom(signcryptedServerMessage); - return SecureMessageParser.getUnverifiedHeader(secmsg).getVerificationKeyId().toByteArray(); - } - - /** - * Used by a client to recover a secure {@link Payload} sent by the server-side. - * - * @see #getKeyHandleFor(byte[]) - * @see #signcryptServerMessage(Payload, SecretKey, byte[]) - */ - public static Payload verifydecryptServerMessage( - byte[] signcryptedServerMessage, SecretKey masterKey) - throws SignatureException, InvalidKeyException, NoSuchAlgorithmException { - if ((signcryptedServerMessage == null) || (masterKey == null)) { - throw new NullPointerException(); - } - try { - SecureMessage secmsg = SecureMessage.parseFrom(signcryptedServerMessage); - HeaderAndBody parsed = SecureMessageParser.parseSignCryptedMessage( - secmsg, - masterKey, - SigType.HMAC_SHA256, - masterKey, - EncType.AES_256_CBC); - GcmMetadata metadata = GcmMetadata.parseFrom(parsed.getHeader().getPublicMetadata()); - if (metadata.getVersion() > SecureGcmConstants.SECURE_GCM_VERSION) { - throw new SignatureException("Unsupported protocol version"); - } - return new Payload(PayloadType.valueOf(metadata.getType()), parsed.getBody().toByteArray()); - } catch (InvalidProtocolBufferException | IllegalArgumentException e) { - throw new SignatureException(e); - } - } - - /** - * Used by the the client-side to send a secure {@link Payload} to the client. - * - * @param userKeyPair used to sign the {@link Payload}. In particular, the {@link PrivateKey} - * portion is used for signing, and (the {@link PublicKey} portion is sent to the server. - * @param masterKey used to encrypt the {@link Payload} - */ - public static byte[] signcryptClientMessage( - Payload payload, KeyPair userKeyPair, SecretKey masterKey) - throws InvalidKeyException, NoSuchAlgorithmException { - if ((payload == null) || (masterKey == null)) { - throw new NullPointerException(); - } - - PublicKey userPublicKey = userKeyPair.getPublic(); - PrivateKey userPrivateKey = userKeyPair.getPrivate(); - - return new SecureMessageBuilder() - .setVerificationKeyId(KeyEncoding.encodeUserPublicKey(userPublicKey)) - .setPublicMetadata(GcmMetadata.newBuilder() - .setType(payload.getPayloadType().getType()) - .setVersion(SecureGcmConstants.SECURE_GCM_VERSION) - .build() - .toByteArray()) - .buildSignCryptedMessage( - userPrivateKey, - getSigTypeFor(userPublicKey), - masterKey, - EncType.AES_256_CBC, - payload.getMessage()) - .toByteArray(); - } - - /** - * Used by the server-side to recover a secure {@link Payload} sent by a client. - * - * @see #getEncodedUserPublicKeyFor(byte[]) - * @see #signcryptClientMessage(Payload, KeyPair, SecretKey) - */ - public static Payload verifydecryptClientMessage( - byte[] signcryptedClientMessage, PublicKey userPublicKey, SecretKey masterKey) - throws SignatureException, InvalidKeyException, NoSuchAlgorithmException { - if ((signcryptedClientMessage == null) || (masterKey == null)) { - throw new NullPointerException(); - } - try { - SecureMessage secmsg = SecureMessage.parseFrom(signcryptedClientMessage); - HeaderAndBody parsed = SecureMessageParser.parseSignCryptedMessage( - secmsg, - userPublicKey, - getSigTypeFor(userPublicKey), - masterKey, - EncType.AES_256_CBC); - GcmMetadata metadata = GcmMetadata.parseFrom(parsed.getHeader().getPublicMetadata()); - if (metadata.getVersion() > SecureGcmConstants.SECURE_GCM_VERSION) { - throw new SignatureException("Unsupported protocol version"); - } - return new Payload(PayloadType.valueOf(metadata.getType()), parsed.getBody().toByteArray()); - } catch (InvalidProtocolBufferException | IllegalArgumentException e) { - throw new SignatureException(e); - } - } - - /** - * Extracts an encoded {@code userPublicKey} from a {@code signcryptedClientMessage}. - * - * @see #signcryptClientMessage(Payload, KeyPair, SecretKey) - */ - public static byte[] getEncodedUserPublicKeyFor(byte[] signcryptedClientMessage) - throws InvalidProtocolBufferException { - if (signcryptedClientMessage == null) { - throw new NullPointerException(); - } - SecureMessage secmsg = SecureMessage.parseFrom(signcryptedClientMessage); - return SecureMessageParser.getUnverifiedHeader(secmsg).getVerificationKeyId().toByteArray(); - } - - private static SigType getSigTypeFor(PublicKey userPublicKey) throws InvalidKeyException { - if (userPublicKey instanceof ECPublicKey) { - return SigType.ECDSA_P256_SHA256; - } else if (userPublicKey instanceof RSAPublicKey) { - return SigType.RSA2048_SHA256; - } else { - throw new InvalidKeyException("Unsupported key type"); - } - } -} diff --git a/src/main/java/com/google/security/cryptauth/lib/securegcm/Ukey2Handshake.java b/src/main/java/com/google/security/cryptauth/lib/securegcm/Ukey2Handshake.java deleted file mode 100644 index 8e00ea9..0000000 --- a/src/main/java/com/google/security/cryptauth/lib/securegcm/Ukey2Handshake.java +++ /dev/null @@ -1,1041 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.security.cryptauth.lib.securegcm; - -import com.google.protobuf.ByteString; -import com.google.protobuf.InvalidProtocolBufferException; -import com.google.security.cryptauth.lib.securegcm.UkeyProto.Ukey2Alert; -import com.google.security.cryptauth.lib.securegcm.UkeyProto.Ukey2ClientFinished; -import com.google.security.cryptauth.lib.securegcm.UkeyProto.Ukey2ClientInit; -import com.google.security.cryptauth.lib.securegcm.UkeyProto.Ukey2ClientInit.CipherCommitment; -import com.google.security.cryptauth.lib.securegcm.UkeyProto.Ukey2Message; -import com.google.security.cryptauth.lib.securegcm.UkeyProto.Ukey2ServerInit; -import com.google.security.cryptauth.lib.securemessage.CryptoOps; -import com.google.security.cryptauth.lib.securemessage.PublicKeyProtoUtil; -import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.GenericPublicKey; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.security.InvalidKeyException; -import java.security.KeyPair; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.PublicKey; -import java.security.SecureRandom; -import java.security.spec.InvalidKeySpecException; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import javax.annotation.Nullable; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; - -/** - * Implements UKEY2 and produces a {@link D2DConnectionContext}. - * - * <p>Client Usage: - * <code> - * try { - * Ukey2Handshake client = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512); - * byte[] handshakeMessage; - * - * // Message 1 (Client Init) - * handshakeMessage = client.getNextHandshakeMessage(); - * sendMessageToServer(handshakeMessage); - * - * // Message 2 (Server Init) - * handshakeMessage = receiveMessageFromServer(); - * client.parseHandshakeMessage(handshakeMessage); - * - * // Message 3 (Client Finish) - * handshakeMessage = client.getNextHandshakeMessage(); - * sendMessageToServer(handshakeMessage); - * - * // Get the auth string - * byte[] clientAuthString = client.getVerificationString(STRING_LENGTH); - * showStringToUser(clientAuthString); - * - * // Using out-of-band channel, verify auth string, then call: - * client.verifyHandshake(); - * - * // Make a connection context - * D2DConnectionContext clientContext = client.toConnectionContext(); - * } catch (AlertException e) { - * log(e.getMessage); - * sendMessageToServer(e.getAlertMessageToSend()); - * } catch (HandshakeException e) { - * log(e); - * // terminate handshake - * } - * </code> - * - * <p>Server Usage: - * <code> - * try { - * Ukey2Handshake server = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512); - * byte[] handshakeMessage; - * - * // Message 1 (Client Init) - * handshakeMessage = receiveMessageFromClient(); - * server.parseHandshakeMessage(handshakeMessage); - * - * // Message 2 (Server Init) - * handshakeMessage = server.getNextHandshakeMessage(); - * sendMessageToServer(handshakeMessage); - * - * // Message 3 (Client Finish) - * handshakeMessage = receiveMessageFromClient(); - * server.parseHandshakeMessage(handshakeMessage); - * - * // Get the auth string - * byte[] serverAuthString = server.getVerificationString(STRING_LENGTH); - * showStringToUser(serverAuthString); - * - * // Using out-of-band channel, verify auth string, then call: - * server.verifyHandshake(); - * - * // Make a connection context - * D2DConnectionContext serverContext = server.toConnectionContext(); - * } catch (AlertException e) { - * log(e.getMessage); - * sendMessageToClient(e.getAlertMessageToSend()); - * } catch (HandshakeException e) { - * log(e); - * // terminate handshake - * } - * </code> - */ -public class Ukey2Handshake { - - /** - * Creates a {@link Ukey2Handshake} with a particular cipher that can be used by an initiator / - * client. - * - * @throws HandshakeException - */ - public static Ukey2Handshake forInitiator(HandshakeCipher cipher) throws HandshakeException { - return new Ukey2Handshake(InternalState.CLIENT_START, cipher); - } - - /** - * Creates a {@link Ukey2Handshake} with a particular cipher that can be used by an responder / - * server. - * - * @throws HandshakeException - */ - public static Ukey2Handshake forResponder(HandshakeCipher cipher) throws HandshakeException { - return new Ukey2Handshake(InternalState.SERVER_START, cipher); - } - - /** - * Handshake States. Meaning of states: - * <ul> - * <li>IN_PROGRESS: The handshake is in progress, caller should use - * {@link Ukey2Handshake#getNextHandshakeMessage()} and - * {@link Ukey2Handshake#parseHandshakeMessage(byte[])} to continue the handshake. - * <li>VERIFICATION_NEEDED: The handshake is complete, but pending verification of the - * authentication string. Clients should use {@link Ukey2Handshake#getVerificationString(int)} to - * get the verification string and use out-of-band methods to authenticate the handshake. - * <li>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 - * {@link Ukey2Handshake#verifyHandshake()} to mark the handshake as verified. - * <li>FINISHED: The handshake is finished, and caller can use - * {@link Ukey2Handshake#toConnectionContext()} to produce a {@link D2DConnectionContext}. - * <li>ALREADY_USED: The handshake has already been used and should be discarded / garbage - * collected. - * <li>ERROR: The handshake produced an error and should be destroyed. - * </ul> - */ - public enum 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: - * <code> - * +-----------------------------------------------------+ - * | Enum | Key negotiation | Hash function | - * +-------------+-----------------------+---------------+ - * | P256_SHA512 | ECDH using NIST P-256 | SHA512 | - * +-----------------------------------------------------+ - * </code> - * - * <p>Note that these should correspond to values in device_to_device_messages.proto. - */ - public enum HandshakeCipher { - P256_SHA512(UkeyProto.Ukey2HandshakeCipher.P256_SHA512); - // TODO(aczeskis): add CURVE25519_SHA512 - - private final UkeyProto.Ukey2HandshakeCipher value; - - HandshakeCipher(UkeyProto.Ukey2HandshakeCipher value) { - // Make sure we only accept values that are valid as per the ukey protobuf. - // NOTE: Don't use switch statement on value, as that will trigger a bug. b/30682989. - if (value == UkeyProto.Ukey2HandshakeCipher.P256_SHA512) { - this.value = value; - } else { - throw new IllegalArgumentException("Unknown cipher value: " + value); - } - } - - public UkeyProto.Ukey2HandshakeCipher getValue() { - return value; - } - } - - /** - * If thrown, this exception contains information that should be sent on the wire. Specifically, - * the {@link #getAlertMessageToSend()} method returns a <code>byte[]</code> that communicates the - * error to the other party in the handshake. Meanwhile, the {@link #getMessage()} method can be - * used to get a log-able error message. - */ - public static class AlertException extends Exception { - private final Ukey2Alert alertMessageToSend; - - public AlertException(String alertMessageToLog, Ukey2Alert alertMessageToSend) { - super(alertMessageToLog); - this.alertMessageToSend = alertMessageToSend; - } - - /** - * @return a message suitable for sending to other member of handshake. - */ - public byte[] getAlertMessageToSend() { - return alertMessageToSend.toByteArray(); - } - } - - // Maximum version of the handshake supported by this class. - public static final int VERSION = 1; - - // Random nonce is fixed at 32 bytes (as per go/ukey2). - private static final int NONCE_LENGTH_IN_BYTES = 32; - - private static final String UTF_8 = "UTF-8"; - - // Currently, we only support one next protocol. - private static final String NEXT_PROTOCOL = "AES_256_CBC-HMAC_SHA256"; - - // Clients need to store a map of message 3's (client finishes) for each commitment. - private final HashMap<HandshakeCipher, byte[]> rawMessage3Map = new HashMap<>(); - - private final HandshakeCipher handshakeCipher; - private final HandshakeRole handshakeRole; - private InternalState handshakeState; - private final KeyPair ourKeyPair; - private PublicKey theirPublicKey; - private SecretKey derivedSecretKey; - - // Servers need to store client commitments. - private byte[] theirCommitment; - - // We store the raw messages sent for computing the authentication strings and next key. - private byte[] rawMessage1; - private byte[] rawMessage2; - - // Enums for internal state machinery - private enum InternalState { - // Initiator/client state - 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 - private enum HandshakeRole { - CLIENT, - SERVER - } - - /** - * Never invoked directly. Caller should use {@link #forInitiator(HandshakeCipher)} or - * {@link #forResponder(HandshakeCipher)} instead. - * - * @throws HandshakeException if an unrecoverable error occurs and the connection should be shut - * down. - */ - private Ukey2Handshake(InternalState state, HandshakeCipher cipher) throws HandshakeException { - if (cipher == null) { - throwIllegalArgumentException("Invalid handshake cipher"); - } - this.handshakeCipher = cipher; - - switch (state) { - case CLIENT_START: - handshakeRole = HandshakeRole.CLIENT; - break; - case SERVER_START: - handshakeRole = HandshakeRole.SERVER; - break; - default: - throwIllegalStateException("Invalid handshake state"); - handshakeRole = null; // unreachable, but makes compiler happy - } - this.handshakeState = state; - - this.ourKeyPair = genKeyPair(cipher); - } - - /** - * Get the next handshake message suitable for sending on the wire. - * - * @throws HandshakeException if an unrecoverable error occurs and the connection should be shut - * down. - */ - public byte[] getNextHandshakeMessage() throws HandshakeException { - switch (handshakeState) { - case CLIENT_START: - rawMessage1 = makeUkey2Message(Ukey2Message.Type.CLIENT_INIT, makeClientInitMessage()); - handshakeState = InternalState.CLIENT_WAITING_FOR_SERVER_INIT; - return rawMessage1; - - case SERVER_AFTER_CLIENT_INIT: - rawMessage2 = makeUkey2Message(Ukey2Message.Type.SERVER_INIT, makeServerInitMessage()); - handshakeState = InternalState.SERVER_WAITING_FOR_CLIENT_FINISHED; - return rawMessage2; - - case CLIENT_AFTER_SERVER_INIT: - // Make sure we have a message 3 for the chosen cipher. - if (!rawMessage3Map.containsKey(handshakeCipher)) { - throwIllegalStateException( - "Client state is CLIENT_AFTER_SERVER_INIT, and cipher is " - + handshakeCipher - + ", but no corresponding raw client finished message has been generated"); - } - handshakeState = InternalState.HANDSHAKE_VERIFICATION_NEEDED; - return rawMessage3Map.get(handshakeCipher); - - default: - throwIllegalStateException("Cannot get next message in state: " + handshakeState); - return null; // unreachable, but makes compiler happy - } - } - - /** - * 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 byte {@link #getHandshakeState()} is - * {@link State#VERIFICATION_NEEDED}, which means this can only be called once. - * - * @param byteLength length of output in bytes. Min length is 1; max length is 32. - */ - public byte[] getVerificationString(int byteLength) throws HandshakeException { - if (byteLength < 1 || byteLength > 32) { - throwIllegalArgumentException("Minimum length is 1 byte, max is 32 bytes"); - } - - if (handshakeState != InternalState.HANDSHAKE_VERIFICATION_NEEDED) { - throwIllegalStateException("Unexpected state: " + handshakeState); - } - - try { - derivedSecretKey = - EnrollmentCryptoOps.doKeyAgreement(ourKeyPair.getPrivate(), theirPublicKey); - } catch (InvalidKeyException e) { - // unreachable in practice - throwHandshakeException(e); - } - - ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); - try { - byteStream.write(rawMessage1); - byteStream.write(rawMessage2); - } catch (IOException e) { - // unreachable in practice - throwHandshakeException(e); - } - byte[] info = byteStream.toByteArray(); - - byte[] salt = null; - - try { - salt = "UKEY2 v1 auth".getBytes(UTF_8); - } catch (UnsupportedEncodingException e) { - // unreachable in practice - throwHandshakeException(e); - } - - byte[] authString = null; - try { - authString = CryptoOps.hkdf(derivedSecretKey, salt, info); - } catch (InvalidKeyException | NoSuchAlgorithmException e) { - // unreachable in practice - throwHandshakeException(e); - } - - handshakeState = InternalState.HANDSHAKE_VERIFICATION_IN_PROGRESS; - return Arrays.copyOf(authString, byteLength); - } - - /** - * Invoked to let handshake state machine know that caller has validated the authentication - * string obtained via {@link #getVerificationString(int)}; Note: this should only be called when - * the state returned byte {@link #getHandshakeState()} is {@link State#VERIFICATION_IN_PROGRESS}. - */ - public void verifyHandshake() { - if (handshakeState != InternalState.HANDSHAKE_VERIFICATION_IN_PROGRESS) { - throwIllegalStateException("Unexpected state: " + handshakeState); - } - handshakeState = InternalState.HANDSHAKE_FINISHED; - } - - /** - * Parses the given handshake message. - * @throws AlertException if an error occurs that should be sent to other party. - * @throws HandshakeException in an error occurs and the connection should be torn down. - */ - public void parseHandshakeMessage(byte[] handshakeMessage) - throws AlertException, HandshakeException { - switch (handshakeState) { - case SERVER_START: - parseMessage1(handshakeMessage); - handshakeState = InternalState.SERVER_AFTER_CLIENT_INIT; - break; - - case CLIENT_WAITING_FOR_SERVER_INIT: - parseMessage2(handshakeMessage); - handshakeState = InternalState.CLIENT_AFTER_SERVER_INIT; - break; - - case SERVER_WAITING_FOR_CLIENT_FINISHED: - parseMessage3(handshakeMessage); - handshakeState = InternalState.HANDSHAKE_VERIFICATION_NEEDED; - break; - - default: - throwIllegalStateException("Cannot parse message in state " + handshakeState); - } - } - - /** - * Returns the current state of the handshake. See {@link State}. - */ - public State getHandshakeState() { - switch (handshakeState) { - case CLIENT_START: - case CLIENT_WAITING_FOR_SERVER_INIT: - case CLIENT_AFTER_SERVER_INIT: - case SERVER_START: - case SERVER_WAITING_FOR_CLIENT_FINISHED: - case SERVER_AFTER_CLIENT_INIT: - // fallback intended -- these are all in-progress states - return State.IN_PROGRESS; - - case HANDSHAKE_ERROR: - return State.ERROR; - - case HANDSHAKE_VERIFICATION_NEEDED: - return State.VERIFICATION_NEEDED; - - case HANDSHAKE_VERIFICATION_IN_PROGRESS: - return State.VERIFICATION_IN_PROGRESS; - - case HANDSHAKE_FINISHED: - return State.FINISHED; - - case HANDSHAKE_ALREADY_USED: - return State.ALREADY_USED; - - default: - // unreachable in practice - throwIllegalStateException("Unknown state"); - return null; // really unreachable, but makes compiler happy - } - } - - /** - * Can be called to generate a {@link D2DConnectionContext}. Note: this should only be called - * when the state returned byte {@link #getHandshakeState()} is {@link State#FINISHED}. - * - * @throws HandshakeException - */ - public D2DConnectionContext toConnectionContext() throws HandshakeException { - switch (handshakeState) { - case HANDSHAKE_ERROR: - throwIllegalStateException("Cannot make context; handshake had error"); - return null; // makes linter happy - case HANDSHAKE_ALREADY_USED: - throwIllegalStateException("Cannot reuse handshake context; is has already been used"); - return null; // makes linter happy - case HANDSHAKE_VERIFICATION_NEEDED: - throwIllegalStateException("Handshake not verified, cannot create context"); - return null; // makes linter happy - case HANDSHAKE_FINISHED: - // We're done, okay to return a context - break; - default: - // unreachable in practice - throwIllegalStateException("Handshake is not complete; cannot create connection context"); - } - - if (derivedSecretKey == null) { - throwIllegalStateException("Unexpected state error: derived key is null"); - } - - ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); - try { - byteStream.write(rawMessage1); - byteStream.write(rawMessage2); - } catch (IOException e) { - // unreachable in practice - throwHandshakeException(e); - } - byte[] info = byteStream.toByteArray(); - - byte[] salt = null; - try { - salt = "UKEY2 v1 next".getBytes(UTF_8); - } catch (UnsupportedEncodingException e) { - // unreachable - throwHandshakeException(e); - } - - SecretKey nextProtocolKey = null; - try { - nextProtocolKey = new SecretKeySpec(CryptoOps.hkdf(derivedSecretKey, salt, info), "AES"); - } catch (InvalidKeyException | NoSuchAlgorithmException e) { - // unreachable in practice - throwHandshakeException(e); - } - - SecretKey clientKey = null; - SecretKey serverKey = null; - try { - clientKey = D2DCryptoOps.deriveNewKeyForPurpose(nextProtocolKey, "client"); - serverKey = D2DCryptoOps.deriveNewKeyForPurpose(nextProtocolKey, "server"); - } catch (InvalidKeyException | NoSuchAlgorithmException e) { - // unreachable in practice - throwHandshakeException(e); - } - - handshakeState = InternalState.HANDSHAKE_ALREADY_USED; - - return new D2DConnectionContextV1( - handshakeRole == HandshakeRole.CLIENT ? clientKey : serverKey, - handshakeRole == HandshakeRole.CLIENT ? serverKey : clientKey, - 0 /* initial encode sequence number */, - 0 /* initial decode sequence number */); - } - - /** - * Generates the byte[] encoding of a {@link Ukey2ClientInit} message. - * - * @throws HandshakeException - */ - private byte[] makeClientInitMessage() throws HandshakeException { - Ukey2ClientInit.Builder clientInit = Ukey2ClientInit.newBuilder(); - clientInit.setVersion(VERSION); - clientInit.setRandom(ByteString.copyFrom(generateRandomNonce())); - clientInit.setNextProtocol(NEXT_PROTOCOL); - - // At the moment, we only support one cipher - clientInit.addCipherCommitments(generateP256SHA512Commitment()); - - return clientInit.build().toByteArray(); - } - - /** - * Generates the byte[] encoding of a {@link Ukey2ServerInit} message. - */ - private byte[] makeServerInitMessage() { - Ukey2ServerInit.Builder serverInit = Ukey2ServerInit.newBuilder(); - serverInit.setVersion(VERSION); - serverInit.setRandom(ByteString.copyFrom(generateRandomNonce())); - serverInit.setHandshakeCipher(handshakeCipher.getValue()); - serverInit.setPublicKey( - PublicKeyProtoUtil.encodePublicKey(ourKeyPair.getPublic()).toByteString()); - - return serverInit.build().toByteArray(); - } - - /** - * Generates a keypair for the provided handshake cipher. Currently only P256_SHA512 is - * supported. - * - * @throws HandshakeException - */ - private KeyPair genKeyPair(HandshakeCipher cipher) throws HandshakeException { - switch (cipher) { - case P256_SHA512: - return PublicKeyProtoUtil.generateEcP256KeyPair(); - default: - // Should never happen - throwHandshakeException("unknown cipher: " + cipher); - } - return null; // unreachable, but makes compiler happy - } - - /** - * Attempts to parse message 1 (which is a wrapped {@link Ukey2ClientInit}). See go/ukey2 for - * details. - * - * @throws AlertException if an error occurs - */ - private void parseMessage1(byte[] handshakeMessage) throws AlertException, HandshakeException { - // Deserialize the protobuf; send a BAD_MESSAGE message if deserialization fails - Ukey2Message message = null; - try { - message = Ukey2Message.parseFrom(handshakeMessage); - } catch (InvalidProtocolBufferException e) { - throwAlertException(Ukey2Alert.AlertType.BAD_MESSAGE, - "Can't parse message 1 " + e.getMessage()); - } - - // Verify that message_type == Type.CLIENT_INIT; send a BAD_MESSAGE_TYPE message if mismatch - if (!message.hasMessageType() || message.getMessageType() != Ukey2Message.Type.CLIENT_INIT) { - throwAlertException( - Ukey2Alert.AlertType.BAD_MESSAGE_TYPE, - "Expected, but did not find ClientInit message type"); - } - - // Deserialize message_data as a ClientInit message; send a BAD_MESSAGE_DATA message if - // deserialization fails - if (!message.hasMessageData()) { - throwAlertException(Ukey2Alert.AlertType.BAD_MESSAGE_DATA, - "Expected message data, but didn't find it"); - } - Ukey2ClientInit clientInit = null; - try { - clientInit = Ukey2ClientInit.parseFrom(message.getMessageData()); - } catch (InvalidProtocolBufferException e) { - throwAlertException(Ukey2Alert.AlertType.BAD_MESSAGE_DATA, - "Can't parse message data into ClientInit"); - } - - // Check that version == VERSION; send BAD_VERSION message if mismatch - if (!clientInit.hasVersion()) { - throwAlertException(Ukey2Alert.AlertType.BAD_VERSION, "ClientInit missing version"); - } - if (clientInit.getVersion() != VERSION) { - throwAlertException(Ukey2Alert.AlertType.BAD_VERSION, "ClientInit version mismatch"); - } - - // Check that random is exactly NONCE_LENGTH_IN_BYTES bytes; send Alert.BAD_RANDOM message if - // not. - if (!clientInit.hasRandom()) { - throwAlertException(Ukey2Alert.AlertType.BAD_RANDOM, "ClientInit missing random"); - } - if (clientInit.getRandom().toByteArray().length != NONCE_LENGTH_IN_BYTES) { - throwAlertException(Ukey2Alert.AlertType.BAD_RANDOM, "ClientInit has incorrect nonce length"); - } - - // Check to see if any of the handshake_cipher in cipher_commitment are acceptable. Servers - // should select the first handshake_cipher that it finds acceptable to support clients - // signaling 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 - // message - List<Ukey2ClientInit.CipherCommitment> commitments = clientInit.getCipherCommitmentsList(); - if (commitments.isEmpty()) { - throwAlertException( - Ukey2Alert.AlertType.BAD_HANDSHAKE_CIPHER, "ClientInit is missing cipher commitments"); - } - for (Ukey2ClientInit.CipherCommitment commitment : commitments) { - if (!commitment.hasHandshakeCipher() - || !commitment.hasCommitment()) { - throwAlertException( - Ukey2Alert.AlertType.BAD_HANDSHAKE_CIPHER, - "ClientInit has improperly formatted cipher commitment"); - } - - // TODO(aczeskis): for now we only support one cipher, eventually support more - if (commitment.getHandshakeCipher() == handshakeCipher.getValue()) { - theirCommitment = commitment.getCommitment().toByteArray(); - } - } - if (theirCommitment == null) { - throwAlertException(Ukey2Alert.AlertType.BAD_HANDSHAKE_CIPHER, - "No acceptable commitments found"); - } - - // Checks that next_protocol contains a protocol that the server supports. Send a - // BAD_NEXT_PROTOCOL message if not. We currently only support one protocol - if (!clientInit.hasNextProtocol() || !NEXT_PROTOCOL.equals(clientInit.getNextProtocol())) { - throwAlertException(Ukey2Alert.AlertType.BAD_NEXT_PROTOCOL, "Incorrect next protocol"); - } - - // Store raw message for AUTH_STRING computation - rawMessage1 = handshakeMessage; - } - - /** - * Attempts to parse message 2 (which is a wrapped {@link Ukey2ServerInit}). See go/ukey2 for - * details. - */ - private void parseMessage2(final byte[] handshakeMessage) - throws AlertException, HandshakeException { - // Deserialize the protobuf; send a BAD_MESSAGE message if deserialization fails - Ukey2Message message = null; - try { - message = Ukey2Message.parseFrom(handshakeMessage); - } catch (InvalidProtocolBufferException e) { - throwAlertException(Ukey2Alert.AlertType.BAD_MESSAGE, - "Can't parse message 2 " + e.getMessage()); - } - - // Verify that message_type == Type.SERVER_INIT; send a BAD_MESSAGE_TYPE message if mismatch - if (!message.hasMessageType()) { - throwAlertException(Ukey2Alert.AlertType.BAD_MESSAGE_TYPE, - "Expected, but did not find message type"); - } - if (message.getMessageType() == Ukey2Message.Type.ALERT) { - handshakeState = InternalState.HANDSHAKE_ERROR; - throwHandshakeMessageFromAlertMessage(message); - } - if (message.getMessageType() != Ukey2Message.Type.SERVER_INIT) { - throwAlertException( - Ukey2Alert.AlertType.BAD_MESSAGE_TYPE, - "Expected, but did not find SERVER_INIT message type"); - } - - // Deserialize message_data as a ServerInit message; send a BAD_MESSAGE_DATA message if - // deserialization fails - if (!message.hasMessageData()) { - - throwAlertException(Ukey2Alert.AlertType.BAD_MESSAGE_DATA, - "Expected message data, but didn't find it"); - } - Ukey2ServerInit serverInit = null; - try { - serverInit = Ukey2ServerInit.parseFrom(message.getMessageData()); - } catch (InvalidProtocolBufferException e) { - throwAlertException(Ukey2Alert.AlertType.BAD_MESSAGE_DATA, - "Can't parse message data into ServerInit"); - } - - // Check that version == VERSION; send BAD_VERSION message if mismatch - if (!serverInit.hasVersion()) { - throwAlertException(Ukey2Alert.AlertType.BAD_VERSION, "ServerInit missing version"); - } - if (serverInit.getVersion() != VERSION) { - throwAlertException(Ukey2Alert.AlertType.BAD_VERSION, "ServerInit version mismatch"); - } - - // Check that random is exactly NONCE_LENGTH_IN_BYTES bytes; send Alert.BAD_RANDOM message if - // not. - if (!serverInit.hasRandom()) { - throwAlertException(Ukey2Alert.AlertType.BAD_RANDOM, "ServerInit missing random"); - } - if (serverInit.getRandom().toByteArray().length != NONCE_LENGTH_IN_BYTES) { - throwAlertException(Ukey2Alert.AlertType.BAD_RANDOM, "ServerInit has incorrect nonce length"); - } - - // Check that handshake_cipher matches a handshake cipher that was sent in - // ClientInit.cipher_commitments. If not, send a BAD_HANDSHAKECIPHER message - if (!serverInit.hasHandshakeCipher()) { - throwAlertException(Ukey2Alert.AlertType.BAD_HANDSHAKE_CIPHER, "No handshake cipher found"); - } - HandshakeCipher serverCipher = null; - for (HandshakeCipher cipher : HandshakeCipher.values()) { - if (cipher.getValue() == serverInit.getHandshakeCipher()) { - serverCipher = cipher; - break; - } - } - if (serverCipher == null || serverCipher != handshakeCipher) { - throwAlertException(Ukey2Alert.AlertType.BAD_HANDSHAKE_CIPHER, - "No acceptable handshake cipher found"); - } - - // Check that public_key parses into a correct public key structure. If not, send a - // BAD_PUBLIC_KEY message. - if (!serverInit.hasPublicKey()) { - throwAlertException(Ukey2Alert.AlertType.BAD_PUBLIC_KEY, "No public key found in ServerInit"); - } - theirPublicKey = parseP256PublicKey(serverInit.getPublicKey().toByteArray()); - - // Store raw message for AUTH_STRING computation - rawMessage2 = handshakeMessage; - } - - /** - * Attempts to parse message 3 (which is a wrapped {@link Ukey2ClientFinished}). See go/ukey2 for - * details. - */ - private void parseMessage3(final byte[] handshakeMessage) throws HandshakeException { - // Deserialize the protobuf; terminate the connection if deserialization fails. - Ukey2Message message = null; - try { - message = Ukey2Message.parseFrom(handshakeMessage); - } catch (InvalidProtocolBufferException e) { - throwHandshakeException("Can't parse message 3", e); - } - - // Verify that message_type == Type.CLIENT_FINISH; terminate connection if mismatch occurs - if (!message.hasMessageType()) { - throw new HandshakeException("Expected, but did not find message type"); - } - if (message.getMessageType() == Ukey2Message.Type.ALERT) { - throwHandshakeMessageFromAlertMessage(message); - } - if (message.getMessageType() != Ukey2Message.Type.CLIENT_FINISH) { - throwHandshakeException("Expected, but did not find CLIENT_FINISH message type"); - } - - // Verify that the hash of the ClientFinished matches the expected commitment from ClientInit. - // Terminate the connection if the expected match fails. - verifyCommitment(handshakeMessage); - - // Deserialize message_data as a ClientFinished message; terminate the connection if - // deserialization fails. - if (!message.hasMessageData()) { - throwHandshakeException("Expected message data, but didn't find it"); - } - Ukey2ClientFinished clientFinished = null; - try { - clientFinished = Ukey2ClientFinished.parseFrom(message.getMessageData()); - } catch (InvalidProtocolBufferException e) { - throwHandshakeException(e); - } - - // Check that public_key parses into a correct public key structure. If not, terminate the - // connection. - if (!clientFinished.hasPublicKey()) { - throwHandshakeException("No public key found in ClientFinished"); - } - try { - theirPublicKey = parseP256PublicKey(clientFinished.getPublicKey().toByteArray()); - } catch (AlertException e) { - // Wrap in a HandshakeException because error should not be sent on the wire. - throwHandshakeException(e); - } - } - - private void verifyCommitment(byte[] handshakeMessage) throws HandshakeException { - byte[] actualClientFinishHash = null; - switch (handshakeCipher) { - case P256_SHA512: - actualClientFinishHash = sha512(handshakeMessage); - break; - default: - // should be unreachable - throwIllegalStateException("Unexpected handshakeCipher"); - } - - // Time constant after Java SE 6 Update 17 - // See http://www.oracle.com/technetwork/java/javase/6u17-141447.html - if (!MessageDigest.isEqual(actualClientFinishHash, theirCommitment)) { - throwHandshakeException("Commitment does not match"); - } - } - - private void throwHandshakeMessageFromAlertMessage(Ukey2Message message) - throws HandshakeException { - if (message.hasMessageData()) { - Ukey2Alert alert = null; - try { - alert = Ukey2Alert.parseFrom(message.getMessageData()); - } catch (InvalidProtocolBufferException e) { - throwHandshakeException("Cannot parse alert message", e); - } - - if (alert.hasType() && alert.hasErrorMessage()) { - throwHandshakeException( - "Received Alert message. Type: " - + alert.getType() - + " Error Message: " - + alert.getErrorMessage()); - } else if (alert.hasType()) { - throwHandshakeException("Received Alert message. Type: " + alert.getType()); - } - } - - throwHandshakeException("Received empty Alert Message"); - } - - /** - * Parses an encoded public P256 key. - */ - private PublicKey parseP256PublicKey(byte[] encodedPublicKey) - throws AlertException, HandshakeException { - try { - return PublicKeyProtoUtil.parsePublicKey(GenericPublicKey.parseFrom(encodedPublicKey)); - } catch (InvalidProtocolBufferException | InvalidKeySpecException e) { - throwAlertException(Ukey2Alert.AlertType.BAD_PUBLIC_KEY, - "Cannot parse public key: " + e.getMessage()); - return null; // unreachable, but makes compiler happy - } - } - - /** - * Generates a {@link CipherCommitment} for the P256_SHA512 cipher. - */ - private CipherCommitment generateP256SHA512Commitment() throws HandshakeException { - // Generate the corresponding finished message if it's not done yet - if (!rawMessage3Map.containsKey(HandshakeCipher.P256_SHA512)) { - generateP256SHA512ClientFinished(ourKeyPair); - } - - CipherCommitment.Builder cipherCommitment = CipherCommitment.newBuilder(); - cipherCommitment.setHandshakeCipher(UkeyProto.Ukey2HandshakeCipher.P256_SHA512); - cipherCommitment.setCommitment( - ByteString.copyFrom(sha512(rawMessage3Map.get(HandshakeCipher.P256_SHA512)))); - - return cipherCommitment.build(); - } - - /** - * Generates and records a {@link Ukey2ClientFinished} message for the P256_SHA512 cipher. - */ - private Ukey2ClientFinished generateP256SHA512ClientFinished(KeyPair p256KeyPair) { - byte[] encodedKey = PublicKeyProtoUtil.encodePublicKey(p256KeyPair.getPublic()).toByteArray(); - - Ukey2ClientFinished.Builder clientFinished = Ukey2ClientFinished.newBuilder(); - clientFinished.setPublicKey(ByteString.copyFrom(encodedKey)); - - rawMessage3Map.put( - HandshakeCipher.P256_SHA512, - makeUkey2Message(Ukey2Message.Type.CLIENT_FINISH, clientFinished.build().toByteArray())); - - return clientFinished.build(); - } - - /** - * Generates the serialized representation of a {@link Ukey2Message} based on the provided type - * and data. - */ - private byte[] makeUkey2Message(Ukey2Message.Type messageType, byte[] messageData) { - Ukey2Message.Builder message = Ukey2Message.newBuilder(); - - switch (messageType) { - case ALERT: - case CLIENT_INIT: - case SERVER_INIT: - case CLIENT_FINISH: - // fall through intentional; valid message types - break; - default: - throwIllegalArgumentException("Invalid message type: " + messageType); - } - message.setMessageType(messageType); - - // Alerts a blank message data field - if (messageType != Ukey2Message.Type.ALERT) { - if (messageData == null || messageData.length == 0) { - throwIllegalArgumentException("Cannot send empty message data for non-alert messages"); - } - message.setMessageData(ByteString.copyFrom(messageData)); - } - - return message.build().toByteArray(); - } - - /** - * Returns a {@link Ukey2Alert} message of given type and having the loggable additional data if - * present. - */ - private Ukey2Alert makeAlertMessage(Ukey2Alert.AlertType alertType, - @Nullable String loggableAdditionalData) throws HandshakeException { - switch (alertType) { - case BAD_MESSAGE: - case BAD_MESSAGE_TYPE: - case INCORRECT_MESSAGE: - case BAD_MESSAGE_DATA: - case BAD_VERSION: - case BAD_RANDOM: - case BAD_HANDSHAKE_CIPHER: - case BAD_NEXT_PROTOCOL: - case BAD_PUBLIC_KEY: - case INTERNAL_ERROR: - // fall through intentional; valid alert types - break; - default: - throwHandshakeException("Unknown alert type: " + alertType); - } - - Ukey2Alert.Builder alert = Ukey2Alert.newBuilder(); - alert.setType(alertType); - - if (loggableAdditionalData != null) { - alert.setErrorMessage(loggableAdditionalData); - } - - return alert.build(); - } - - /** - * Generates a cryptoraphically random nonce of NONCE_LENGTH_IN_BYTES bytes. - */ - private static byte[] generateRandomNonce() { - SecureRandom rng = new SecureRandom(); - byte[] randomNonce = new byte[NONCE_LENGTH_IN_BYTES]; - rng.nextBytes(randomNonce); - return randomNonce; - } - - /** - * Handy wrapper to do SHA512. - */ - private byte[] sha512(byte[] input) throws HandshakeException { - MessageDigest sha512; - try { - sha512 = MessageDigest.getInstance("SHA-512"); - return sha512.digest(input); - } catch (NoSuchAlgorithmException e) { - throwHandshakeException("No security provider initialized yet?", e); - return null; // unreachable in practice, but makes compiler happy - } - } - - // Exception wrappers that remember to set the handshake state to ERROR - - private void throwAlertException(Ukey2Alert.AlertType alertType, String alertLogStatement) - throws AlertException, HandshakeException { - handshakeState = InternalState.HANDSHAKE_ERROR; - throw new AlertException(alertLogStatement, makeAlertMessage(alertType, alertLogStatement)); - } - - private void throwHandshakeException(String logMessage) throws HandshakeException { - handshakeState = InternalState.HANDSHAKE_ERROR; - throw new HandshakeException(logMessage); - } - - private void throwHandshakeException(Exception e) throws HandshakeException { - handshakeState = InternalState.HANDSHAKE_ERROR; - throw new HandshakeException(e); - } - - private void throwHandshakeException(String logMessage, Exception e) throws HandshakeException { - handshakeState = InternalState.HANDSHAKE_ERROR; - throw new HandshakeException(logMessage, e); - } - - private void throwIllegalStateException(String logMessage) { - handshakeState = InternalState.HANDSHAKE_ERROR; - throw new IllegalStateException(logMessage); - } - - private void throwIllegalArgumentException(String logMessage) { - handshakeState = InternalState.HANDSHAKE_ERROR; - throw new IllegalArgumentException(logMessage); - } -} diff --git a/src/main/java/com/google/security/cryptauth/lib/securegcm/device_to_device_messages_config.asciipb b/src/main/java/com/google/security/cryptauth/lib/securegcm/device_to_device_messages_config.asciipb deleted file mode 100644 index 0e2952c..0000000 --- a/src/main/java/com/google/security/cryptauth/lib/securegcm/device_to_device_messages_config.asciipb +++ /dev/null @@ -1,3 +0,0 @@ -optimize_mode: LITE_RUNTIME - -allowed_message: "securegcm.DeviceToDeviceMessage" diff --git a/src/main/java/com/google/security/cryptauth/lib/securegcm/securegcm_config.asciipb b/src/main/java/com/google/security/cryptauth/lib/securegcm/securegcm_config.asciipb deleted file mode 100644 index d838bd3..0000000 --- a/src/main/java/com/google/security/cryptauth/lib/securegcm/securegcm_config.asciipb +++ /dev/null @@ -1,4 +0,0 @@ -optimize_mode: LITE_RUNTIME - -allowed_enum: "securegcm.Type" -allowed_message: "securegcm.GcmMetadata" diff --git a/src/main/java/com/google/security/cryptauth/lib/securemessage/CryptoOps.java b/src/main/java/com/google/security/cryptauth/lib/securemessage/CryptoOps.java deleted file mode 100644 index 876bd93..0000000 --- a/src/main/java/com/google/security/cryptauth/lib/securemessage/CryptoOps.java +++ /dev/null @@ -1,564 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.security.cryptauth.lib.securemessage; - -import com.google.security.annotations.SuppressInsecureCipherModeCheckerReviewed; -import java.io.UnsupportedEncodingException; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.Key; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.SecureRandom; -import java.security.Signature; -import java.security.SignatureException; -import javax.annotation.Nullable; -import javax.crypto.BadPaddingException; -import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.Mac; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.SecretKey; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.SecretKeySpec; - -/** - * Encapsulates the cryptographic operations used by the {@code SecureMessage*} classes. - */ -public class CryptoOps { - - private CryptoOps() {} // Do not instantiate - - /** - * Enum of supported signature types, with additional mappings to indicate the name of the - * underlying JCA algorithm used to create the signature. - * @see <a href= - * "http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html"> - * Java Cryptography Architecture, Standard Algorithm Name Documentation</a> - */ - public enum SigType { - HMAC_SHA256(SecureMessageProto.SigScheme.HMAC_SHA256, "HmacSHA256", false), - ECDSA_P256_SHA256(SecureMessageProto.SigScheme.ECDSA_P256_SHA256, "SHA256withECDSA", true), - RSA2048_SHA256(SecureMessageProto.SigScheme.RSA2048_SHA256, "SHA256withRSA", true); - - public SecureMessageProto.SigScheme getSigScheme() { - return sigScheme; - } - - public String getJcaName() { - return jcaName; - } - - public boolean isPublicKeyScheme() { - return publicKeyScheme; - } - - public static SigType valueOf(SecureMessageProto.SigScheme sigScheme) { - for (SigType value : values()) { - if (value.sigScheme.equals(sigScheme)) { - return value; - } - } - throw new IllegalArgumentException("Unsupported SigType: " + sigScheme); - } - - private final SecureMessageProto.SigScheme sigScheme; - private final String jcaName; - private final boolean publicKeyScheme; - - SigType(SecureMessageProto.SigScheme sigType, String jcaName, boolean publicKeyScheme) { - this.sigScheme = sigType; - this.jcaName = jcaName; - this.publicKeyScheme = publicKeyScheme; - } - } - - /** - * Enum of supported encryption types, with additional mappings to indicate the name of the - * underlying JCA algorithm used to perform the encryption. - * @see <a href= - * "http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html"> - * Java Cryptography Architecture, Standard Algorithm Name Documentation</a> - */ - public enum EncType { - NONE(SecureMessageProto.EncScheme.NONE, "InvalidDoNotUseForJCA"), - AES_256_CBC(SecureMessageProto.EncScheme.AES_256_CBC, "AES/CBC/PKCS5Padding"); - - public SecureMessageProto.EncScheme getEncScheme() { - return encScheme; - } - - public String getJcaName() { - return jcaName; - } - - public static EncType valueOf(SecureMessageProto.EncScheme encScheme) { - for (EncType value : values()) { - if (value.encScheme.equals(encScheme)) { - return value; - } - } - throw new IllegalArgumentException("Unsupported EncType: " + encScheme); - } - - private final SecureMessageProto.EncScheme encScheme; - private final String jcaName; - - EncType(SecureMessageProto.EncScheme encScheme, String jcaName) { - this.encScheme = encScheme; - this.jcaName = jcaName; - } - } - - /** - * Truncated hash output length, in bytes. - */ - static final int DIGEST_LENGTH = 20; - /** - * A salt value specific to this library, generated as SHA-256("SecureMessage") - */ - private static final byte[] SALT = sha256("SecureMessage"); - - /** - * Signs {@code data} using the algorithm specified by {@code sigType} with {@code signingKey}. - * - * @param rng is required for public key signature schemes - * @return raw signature - * @throws InvalidKeyException if {@code signingKey} is incompatible with {@code sigType} - * @throws NoSuchAlgorithmException if the security provider is inadequate for {@code sigType} - */ - static byte[] sign( - SigType sigType, Key signingKey, @Nullable SecureRandom rng, byte[] data) - throws InvalidKeyException, NoSuchAlgorithmException { - if ((signingKey == null) || (data == null)) { - throw new NullPointerException(); - } - if (sigType.isPublicKeyScheme()) { - if (rng == null) { - throw new NullPointerException(); - } - if (!(signingKey instanceof PrivateKey)) { - throw new InvalidKeyException("Expected a PrivateKey"); - } - Signature sigScheme = Signature.getInstance(sigType.getJcaName()); - sigScheme.initSign((PrivateKey) signingKey, rng); - try { - // We include a fixed magic value (salt) in the signature so that if the signing key is - // reused in another context we can't be confused -- provided that the other user of the - // signing key only signs statements that do not begin with this salt. - sigScheme.update(SALT); - sigScheme.update(data); - return sigScheme.sign(); - } catch (SignatureException e) { - throw new IllegalStateException(e); // Consistent with failures in Mac.doFinal - } - } else { - Mac macScheme = Mac.getInstance(sigType.getJcaName()); - // Note that an AES-256 SecretKey should work with most Mac schemes - SecretKey derivedKey = deriveAes256KeyFor(getSecretKey(signingKey), getPurpose(sigType)); - macScheme.init(derivedKey); - return macScheme.doFinal(data); - } - } - - /** - * Verifies the {@code signature} on {@code data} using the algorithm specified by - * {@code sigType} with {@code verificationKey}. - * - * @return true iff the signature is verified - * @throws NoSuchAlgorithmException if the security provider is inadequate for {@code sigType} - * @throws InvalidKeyException if {@code verificationKey} is incompatible with {@code sigType} - * @throws SignatureException - */ - static boolean verify(Key verificationKey, SigType sigType, byte[] signature, byte[] data) - throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { - if ((verificationKey == null) || (signature == null) || (data == null)) { - throw new NullPointerException(); - } - if (sigType.isPublicKeyScheme()) { - if (!(verificationKey instanceof PublicKey)) { - throw new InvalidKeyException("Expected a PublicKey"); - } - Signature sigScheme = Signature.getInstance(sigType.getJcaName()); - sigScheme.initVerify((PublicKey) verificationKey); - sigScheme.update(SALT); // See the comments in sign() for more on this - sigScheme.update(data); - return sigScheme.verify(signature); - } else { - Mac macScheme = Mac.getInstance(sigType.getJcaName()); - SecretKey derivedKey = - deriveAes256KeyFor(getSecretKey(verificationKey), getPurpose(sigType)); - macScheme.init(derivedKey); - return constantTimeArrayEquals(signature, macScheme.doFinal(data)); - } - } - - /** - * Generate a random IV appropriate for use with the algorithm specified in {@code encType}. - * - * @return a freshly generated IV (a random byte sequence of appropriate length) - * @throws NoSuchAlgorithmException if the security provider is inadequate for {@code encType} - */ - @SuppressInsecureCipherModeCheckerReviewed - // See b/26525455 for security review. - static byte[] generateIv(EncType encType, SecureRandom rng) throws NoSuchAlgorithmException { - if (rng == null) { - throw new NullPointerException(); - } - try { - Cipher encrypter = Cipher.getInstance(encType.getJcaName()); - byte[] iv = new byte[encrypter.getBlockSize()]; - rng.nextBytes(iv); - return iv; - } catch (NoSuchPaddingException e) { - throw new NoSuchAlgorithmException(e); // Consolidate into NoSuchAlgorithmException - } - } - - /** - * Encrypts {@code plaintext} using the algorithm specified in {@code encType}, with the specified - * {@code iv} and {@code encryptionKey}. - * - * @param rng source of randomness to be used with the specified cipher, if necessary - * @return encrypted data - * @throws NoSuchAlgorithmException if the security provider is inadequate for {@code encType} - * @throws InvalidKeyException if {@code encryptionKey} is incompatible with {@code encType} - */ - @SuppressInsecureCipherModeCheckerReviewed - // See b/26525455 for security review. - static byte[] encrypt( - Key encryptionKey, EncType encType, @Nullable SecureRandom rng, byte[] iv, byte[] plaintext) - throws NoSuchAlgorithmException, InvalidKeyException { - if ((encryptionKey == null) || (iv == null) || (plaintext == null)) { - throw new NullPointerException(); - } - if (encType == EncType.NONE) { - throw new NoSuchAlgorithmException("Cannot use NONE type here"); - } - try { - Cipher encrypter = Cipher.getInstance(encType.getJcaName()); - SecretKey derivedKey = - deriveAes256KeyFor(getSecretKey(encryptionKey), getPurpose(encType)); - encrypter.init(Cipher.ENCRYPT_MODE, derivedKey, new IvParameterSpec(iv), rng); - return encrypter.doFinal(plaintext); - } catch (InvalidAlgorithmParameterException e) { - throw new AssertionError(e); // Should never happen - } catch (IllegalBlockSizeException e) { - throw new AssertionError(e); // Should never happen - } catch (BadPaddingException e) { - throw new AssertionError(e); // Should never happen - } catch (NoSuchPaddingException e) { - throw new NoSuchAlgorithmException(e); // Consolidate into NoSuchAlgorithmException - } - } - - /** - * Decrypts {@code ciphertext} using the algorithm specified in {@code encType}, with the - * specified {@code iv} and {@code decryptionKey}. - * - * @return the plaintext (decrypted) data - * @throws NoSuchAlgorithmException if the security provider is inadequate for {@code encType} - * @throws InvalidKeyException if {@code decryptionKey} is incompatible with {@code encType} - * @throws InvalidAlgorithmParameterException if {@code encType} exceeds legal cryptographic - * strength limits in this jurisdiction - * @throws IllegalBlockSizeException if {@code ciphertext} contains an illegal block - * @throws BadPaddingException if {@code ciphertext} contains an illegal padding - */ - @SuppressInsecureCipherModeCheckerReviewed - // See b/26525455 for security review - static byte[] decrypt(Key decryptionKey, EncType encType, byte[] iv, byte[] ciphertext) - throws NoSuchAlgorithmException, InvalidKeyException, InvalidAlgorithmParameterException, - IllegalBlockSizeException, BadPaddingException { - if ((decryptionKey == null) || (iv == null) || (ciphertext == null)) { - throw new NullPointerException(); - } - if (encType == EncType.NONE) { - throw new NoSuchAlgorithmException("Cannot use NONE type here"); - } - try { - Cipher decrypter = Cipher.getInstance(encType.getJcaName()); - SecretKey derivedKey = - deriveAes256KeyFor(getSecretKey(decryptionKey), getPurpose(encType)); - decrypter.init(Cipher.DECRYPT_MODE, derivedKey, new IvParameterSpec(iv)); - return decrypter.doFinal(ciphertext); - } catch (NoSuchPaddingException e) { - throw new AssertionError(e); // Should never happen - } - } - - /** - * Computes a collision-resistant hash of {@link #DIGEST_LENGTH} bytes - * (using a truncated SHA-256 output). - */ - static byte[] digest(byte[] data) throws NoSuchAlgorithmException { - MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); - byte[] truncatedHash = new byte[DIGEST_LENGTH]; - System.arraycopy(sha256.digest(data), 0, truncatedHash, 0, DIGEST_LENGTH); - return truncatedHash; - } - - /** - * Returns {@code true} if the two arrays are equal to one another. - * When the two arrays differ in length, trivially returns {@code false}. - * When the two arrays are equal in length, does a constant-time comparison - * of the two, i.e. does not abort the comparison when the first differing - * element is found. - * - * <p>NOTE: This is a copy of {@code java/com/google/math/crypto/ConstantTime#arrayEquals}. - * - * @param a An array to compare - * @param b Another array to compare - * @return {@code true} if these arrays are both null or if they have equal - * length and equal bytes in all elements - */ - static boolean constantTimeArrayEquals(@Nullable byte[] a, @Nullable byte[] b) { - if (a == null || b == null) { - return (a == b); - } - if (a.length != b.length) { - return false; - } - byte result = 0; - for (int i = 0; i < b.length; i++) { - result = (byte) (result | a[i] ^ b[i]); - } - return (result == 0); - } - - // @VisibleForTesting - static String getPurpose(SigType sigType) { - return "SIG:" + sigType.getSigScheme().getNumber(); - } - - // @VisibleForTesting - static String getPurpose(EncType encType) { - return "ENC:" + encType.getEncScheme().getNumber(); - } - - private static SecretKey getSecretKey(Key key) throws InvalidKeyException { - if (!(key instanceof SecretKey)) { - throw new InvalidKeyException("Expected a SecretKey"); - } - return (SecretKey) key; - } - - /** - * @return the UTF-8 encoding of the given string - * @throws RuntimeException if the UTF-8 charset is not present. - */ - public static byte[] utf8StringToBytes(String input) { - try { - return input.getBytes("UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); // Shouldn't happen, UTF-8 is universal - } - } - - /** - * @return SHA-256(UTF-8 encoded input) - */ - public static byte[] sha256(String input) { - MessageDigest sha256; - try { - sha256 = MessageDigest.getInstance("SHA-256"); - return sha256.digest(utf8StringToBytes(input)); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException("No security provider initialized yet?", e); - } - } - - /** - * A key derivation function specific to this library, which accepts a {@code masterKey} and an - * arbitrary {@code purpose} describing the intended application of the derived sub-key, - * and produces a derived AES-256 key safe to use as if it were independent of any other - * derived key which used a different {@code purpose}. - * - * @param masterKey any key suitable for use with HmacSHA256 - * @param purpose a UTF-8 encoded string describing the intended purpose of derived key - * @return a derived SecretKey suitable for use with AES-256 - * @throws InvalidKeyException if the encoded form of {@code masterKey} cannot be accessed - */ - static SecretKey deriveAes256KeyFor(SecretKey masterKey, String purpose) - throws NoSuchAlgorithmException, InvalidKeyException { - return new SecretKeySpec(hkdf(masterKey, SALT, utf8StringToBytes(purpose)), "AES"); - } - - /** - * Implements HKDF (RFC 5869) with the SHA-256 hash and a 256-bit output key length. - * - * Please make sure to select a salt that is fixed and unique for your codebase, and use the - * {@code info} parameter to specify any additional bits that should influence the derived key. - * - * @param inputKeyMaterial master key from which to derive sub-keys - * @param salt a (public) randomly generated 256-bit input that can be re-used - * @param info arbitrary information that is bound to the derived key (i.e., used in its creation) - * @return raw derived key bytes = HKDF-SHA256(inputKeyMaterial, salt, info) - * @throws InvalidKeyException if the encoded form of {@code inputKeyMaterial} cannot be accessed - */ - public static byte[] hkdf(SecretKey inputKeyMaterial, byte[] salt, byte[] info) - throws NoSuchAlgorithmException, InvalidKeyException { - return hkdf(inputKeyMaterial, salt, info, /* length= */ 32); - } - - /** - * Implements HKDF (RFC 5869) with the SHA-256 hash. - * - * <p>Please make sure to select a salt that is fixed and unique for your codebase, and use the - * {@code info} parameter to specify any additional bits that should influence the derived key. - * - * @param inputKeyMaterial master key from which to derive sub-keys - * @param salt a (public) randomly generated 256-bit input that can be re-used - * @param info arbitrary information that is bound to the derived key (i.e., used in its creation) - * @param length length of returned key material - * @return raw derived key bytes = HKDF-SHA256(inputKeyMaterial, salt, info) - * @throws InvalidKeyException if the encoded form of {@code inputKeyMaterial} cannot be accessed - */ - public static byte[] hkdf(SecretKey inputKeyMaterial, byte[] salt, byte[] info, int length) - throws NoSuchAlgorithmException, InvalidKeyException { - if ((inputKeyMaterial == null) || (salt == null) || (info == null)) { - throw new NullPointerException(); - } - if (length < 0) { - throw new IllegalArgumentException("Length must be positive"); - } - return hkdfSha256Expand(hkdfSha256Extract(inputKeyMaterial, salt), info, length); - } - - /** - * @return the concatenation of {@code a} and {@code b}, treating {@code null} as the empty array. - */ - static byte[] concat(@Nullable byte[] a, @Nullable byte[] b) { - if ((a == null) && (b == null)) { - return new byte[] { }; - } - if (a == null) { - return b; - } - if (b == null) { - return a; - } - byte[] result = new byte[a.length + b.length]; - System.arraycopy(a, 0, result, 0, a.length); - System.arraycopy(b, 0, result, a.length, b.length); - return result; - } - - /** - * Since {@code Arrays.copyOfRange(...)} is not available on older Android platforms, - * a custom method for computing a subarray is provided here. - * - * @return the substring of {@code in} from {@code beginIndex} (inclusive) - * up to {@code endIndex} (exclusive) - */ - static byte[] subarray(byte[] in, int beginIndex, int endIndex) { - if (in == null) { - throw new NullPointerException(); - } - int length = endIndex - beginIndex; - if ((length < 0) - || (beginIndex < 0) - || (endIndex < 0) - || (beginIndex >= in.length) - || (endIndex > in.length)) { - throw new IndexOutOfBoundsException(); - } - byte[] result = new byte[length]; - if (length > 0) { - System.arraycopy(in, beginIndex, result, 0, length); - } - return result; - } - - /** - * The HKDF (RFC 5869) extraction function, using the SHA-256 hash function. This function is used - * to pre-process the inputKeyMaterial and mix it with the salt, producing output suitable for use - * with HKDF expansion function (which produces the actual derived key). - * - * @see #hkdfSha256Expand(byte[], byte[], int) - * @return HMAC-SHA256(salt, inputKeyMaterial) (salt is the "key" for the HMAC) - * @throws InvalidKeyException if the encoded form of {@code inputKeyMaterial} cannot be accessed - * @throws NoSuchAlgorithmException if the HmacSHA256 or AES algorithms are unavailable - */ - private static byte[] hkdfSha256Extract(SecretKey inputKeyMaterial, byte[] salt) - throws NoSuchAlgorithmException, InvalidKeyException { - Mac macScheme = Mac.getInstance("HmacSHA256"); - try { - macScheme.init(new SecretKeySpec(salt, "AES")); - } catch (InvalidKeyException e) { - throw new AssertionError(e); // This should never happen - } - // Note that the SecretKey encoding format is defined to be RAW, so the encoded form should be - // consistent across implementations. - byte[] encodedKeyMaterial = inputKeyMaterial.getEncoded(); - if (encodedKeyMaterial == null) { - throw new InvalidKeyException("Cannot get encoded form of SecretKey"); - } - return macScheme.doFinal(encodedKeyMaterial); - } - - /** - * HKDF (RFC 5869) expansion function, using the SHA-256 hash function. - * - * @param pseudoRandomKey should be generated by {@link #hkdfSha256Extract(SecretKey, byte[])} - * @param info arbitrary information the derived key should be bound to - * @param length length of the output key material in bytes - * @return raw derived key bytes = HMAC-SHA256(pseudoRandomKey, info | 0x01) - * @throws NoSuchAlgorithmException if the HmacSHA256 or AES algorithms are unavailable - */ - private static byte[] hkdfSha256Expand(byte[] pseudoRandomKey, byte[] info, int length) - throws NoSuchAlgorithmException { - Mac macScheme = Mac.getInstance("HmacSHA256"); - try { - macScheme.init(new SecretKeySpec(pseudoRandomKey, "AES")); - } catch (InvalidKeyException e) { - throw new AssertionError(e); // This should never happen - } - - // Number of blocks N = ceil(hash length / output length). - int blocks = length / 32; - if (length % 32 > 0) { - blocks += 1; - } - - // The counter used to generate the blocks according to the RFC is only one byte long, - // which puts a limit on the number of blocks possible. - if (blocks > 0xFF) { - throw new IllegalArgumentException("Maximum HKDF output length exceeded."); - } - - byte[] outputBlock = new byte[32]; - byte[] counter = new byte[1]; - byte[] output = new byte[32 * blocks]; - for (int i = 0; i < blocks; ++i) { - macScheme.reset(); - if (i > 0) { - // Previous block - macScheme.update(outputBlock); - } - // Arbitrary info - macScheme.update(info); - // Counter - counter[0] = (byte) (i + 1); - outputBlock = macScheme.doFinal(counter); - - System.arraycopy(outputBlock, 0, output, 32 * i, 32); - } - - return subarray(output, 0, length); - } - -} diff --git a/src/main/java/com/google/security/cryptauth/lib/securemessage/PublicKeyProtoUtil.java b/src/main/java/com/google/security/cryptauth/lib/securemessage/PublicKeyProtoUtil.java deleted file mode 100644 index 0c593fe..0000000 --- a/src/main/java/com/google/security/cryptauth/lib/securemessage/PublicKeyProtoUtil.java +++ /dev/null @@ -1,675 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.security.cryptauth.lib.securemessage; - -import com.google.common.collect.Lists; -import com.google.protobuf.ByteString; -import com.google.security.annotations.SuppressInsecureCipherModeCheckerPendingReview; -import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.DhPublicKey; -import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.EcP256PublicKey; -import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.GenericPublicKey; -import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.SimpleRsaPublicKey; -import java.math.BigInteger; -import java.security.InvalidAlgorithmParameterException; -import java.security.KeyFactory; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.NoSuchAlgorithmException; -import java.security.PublicKey; -import java.security.SecureRandom; -import java.security.interfaces.ECPublicKey; -import java.security.interfaces.RSAPublicKey; -import java.security.spec.ECFieldFp; -import java.security.spec.ECGenParameterSpec; -import java.security.spec.ECParameterSpec; -import java.security.spec.ECPoint; -import java.security.spec.ECPublicKeySpec; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.RSAPublicKeySpec; -import javax.crypto.interfaces.DHPrivateKey; -import javax.crypto.interfaces.DHPublicKey; -import javax.crypto.spec.DHParameterSpec; -import javax.crypto.spec.DHPublicKeySpec; - -/** - * Utility class containing static factory methods for a simple protobuf based representation of - * EC public keys that is intended for use with the SecureMessage library. - * - * N.B.: Requires the availability of an EC security provider supporting the NIST P-256 curve. - * - */ -public class PublicKeyProtoUtil { - - private PublicKeyProtoUtil() {} // Do not instantiate - - /** - * Caches state about whether the current platform supports Elliptic Curve algorithms. - */ - private static final Boolean IS_LEGACY_CRYPTO_REQUIRED = determineIfLegacyCryptoRequired(); - - private static final BigInteger ONE = new BigInteger("1"); - private static final BigInteger TWO = new BigInteger("2"); - - /** - * Name for Elliptic Curve cryptography algorithm suite, used by the security provider. If the - * security provider does not implement the specified algorithm, runtime errors will ensue. - */ - private static final String EC_ALG = "EC"; - - /** - * A common name for the NIST P-256 curve, used by most Java security providers. - */ - private static final String EC_P256_COMMON_NAME = "secp256r1"; - - /** - * A name the NIST P-256 curve, used by the OpenSSL Java security provider (e.g,. on Android). - */ - private static final String EC_P256_OPENSSL_NAME = "prime256v1"; - - /** - * The {@link ECParameterSpec} for the NIST P-256 Elliptic Curve. - */ - private static final ECParameterSpec EC_P256_PARAMS = isLegacyCryptoRequired() ? null : - ((ECPublicKey) generateEcP256KeyPair().getPublic()).getParams(); - - /** - * The prime {@code p} describing the field for the NIST P-256 curve. - */ - private static final BigInteger EC_P256_P = isLegacyCryptoRequired() ? null : - ((ECFieldFp) EC_P256_PARAMS.getCurve().getField()).getP(); - - /** - * The coefficient {@code a} for the NIST P-256 curve. - */ - private static final BigInteger EC_P256_A = isLegacyCryptoRequired() ? null : - EC_P256_PARAMS.getCurve().getA(); - - /** - * The coefficient {@code b} for the NIST P-256 curve. - */ - private static final BigInteger EC_P256_B = isLegacyCryptoRequired() ? null : - EC_P256_PARAMS.getCurve().getB(); - - /** - * Maximum number of bytes in a 2's complement encoding of a NIST P-256 elliptic curve point. - */ - private static final int MAX_P256_ENCODING_BYTES = 33; - - /** - * The JCA name for the RSA cryptography suite. - */ - private static final String RSA_ALG = "RSA"; - - private static final int RSA2048_MODULUS_BITS = 2048; - - /** - * Maximum number of bytes in a 2's complement encoding of a 2048-bit RSA key. - */ - private static final int MAX_RSA2048_ENCODING_BYTES = 257; - - /** - * The JCA name for the Diffie-Hellman cryptography suite. - */ - private static final String DH_ALG = "DH"; - - /** - * The prime from the 2048-bit MODP Group (group 14) described in RFC 3526, to be used for - * Diffie-Hellman computations. Use only if Elliptic Curve ciphers are unavailable. - */ - public static final BigInteger DH_P = new BigInteger( - "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1" + - "29024E088A67CC74020BBEA63B139B22514A08798E3404DD" + - "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245" + - "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" + - "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D" + - "C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F" + - "83655D23DCA3AD961C62F356208552BB9ED529077096966D" + - "670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B" + - "E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9" + - "DE2BCBF6955817183995497CEA956AE515D2261898FA0510" + - "15728E5A8AACAA68FFFFFFFFFFFFFFFF", 16); - - /** - * The generator for the 2048-bit MODP Group (group 14) described in RFC 3526, to be used for - * Diffie-Hellman computations. Use only if Elliptic Curve ciphers are unavailable. - */ - public static final BigInteger DH_G = TWO; - - /** - * The size of the Diffie-Hellman exponent to use, in bits. - */ - public static final int DH_LEN = 512; - - /** - * Maximum number of bytes in a 2's complement encoding of a - * Diffie-Hellman key using {@link #DH_G}. - */ - private static final int MAX_DH2048_ENCODING_BYTES = 257; - - /** - * Version code for the Honeycomb release of Android, which is the first release supporting - * Elliptic Curve. - */ - public static final int ANDROID_HONEYCOMB_SDK_INT = 11; - - /** - * Encodes any supported {@link PublicKey} type as a {@link GenericPublicKey} proto message. - * - * @see SecureMessageProto constants (defined in the .proto file) for supported types - */ - public static GenericPublicKey encodePublicKey(PublicKey pk) { - if (pk == null) { - throw new NullPointerException(); - } - if (pk instanceof ECPublicKey) { - return GenericPublicKey.newBuilder() - .setType(SecureMessageProto.PublicKeyType.EC_P256) - .setEcP256PublicKey(encodeEcPublicKey(pk)) - .build(); - } - if (pk instanceof RSAPublicKey) { - return GenericPublicKey.newBuilder() - .setType(SecureMessageProto.PublicKeyType.RSA2048) - .setRsa2048PublicKey(encodeRsa2048PublicKey(pk)) - .build(); - } - if (pk instanceof DHPublicKey) { - return GenericPublicKey.newBuilder() - .setType(SecureMessageProto.PublicKeyType.DH2048_MODP) - .setDh2048PublicKey(encodeDh2048PublicKey(pk)) - .build(); - } - throw new IllegalArgumentException("Unsupported PublicKey type"); - } - - /** - * Encodes an {@link ECPublicKey} to an {@link GenericPublicKey} proto message. The returned key - * has a null-byte padded to the front in order to match the C++ implementation. - */ - public static GenericPublicKey encodePaddedEcPublicKey(PublicKey pk) { - if (pk == null) { - throw new NullPointerException(); - } - if (!(pk instanceof ECPublicKey)) { - throw new IllegalArgumentException("Expected ECPublicKey PublicKey type"); - } - - ECPublicKey epk = pkToECPublicKey(pk); - ByteString nullByteString = ByteString.copyFrom(new byte[] {0}); - ByteString xByteString = extractX(epk); - if (xByteString.size() < MAX_P256_ENCODING_BYTES) { - xByteString = ByteString.copyFrom(Lists.newArrayList(nullByteString, xByteString)); - } - ByteString yByteString = extractY(epk); - if (yByteString.size() < MAX_P256_ENCODING_BYTES) { - yByteString = ByteString.copyFrom(Lists.newArrayList(nullByteString, yByteString)); - } - EcP256PublicKey newKey = - EcP256PublicKey.newBuilder().setX(xByteString).setY(yByteString).build(); - - return GenericPublicKey.newBuilder() - .setType(SecureMessageProto.PublicKeyType.EC_P256) - .setEcP256PublicKey(newKey) - .build(); - } - - /** - * Encodes an {@link ECPublicKey} to an {@link EcP256PublicKey} proto message. - */ - public static EcP256PublicKey encodeEcPublicKey(PublicKey pk) { - ECPublicKey epk = pkToECPublicKey(pk); - return EcP256PublicKey.newBuilder() - .setX(extractX(epk)) - .setY(extractY(epk)) - .build(); - } - - /** - * Encodes a 2048-bit {@link RSAPublicKey} to an {@link SimpleRsaPublicKey} proto message. - */ - public static SimpleRsaPublicKey encodeRsa2048PublicKey(PublicKey pk) { - RSAPublicKey rpk = pkToRSAPublicKey(pk); - return SimpleRsaPublicKey.newBuilder() - .setN(ByteString.copyFrom(rpk.getModulus().toByteArray())) - .setE(rpk.getPublicExponent().intValue()) - .build(); - } - - /** - * Encodes a 2048-bit {@link DhPublicKey} using the {@link #DH_G} group to a - * {@link DhPublicKey} proto message. - */ - public static DhPublicKey encodeDh2048PublicKey(PublicKey pk) { - DHPublicKey dhpk = pkToDHPublicKey(pk); - return DhPublicKey.newBuilder() - .setY(ByteString.copyFrom(dhpk.getY().toByteArray())) - .build(); - } - - /** - * Extracts a {@link PublicKey} from an {@link GenericPublicKey} proto message. - * - * @throws InvalidKeySpecException if the input is not a valid and/or supported public key type - */ - public static PublicKey parsePublicKey(GenericPublicKey gpk) throws InvalidKeySpecException { - if (!gpk.hasType()) { - // "required" means nothing in micro proto land. We have to check this ourselves. - throw new InvalidKeySpecException("GenericPublicKey.type is a required field"); - } - switch (gpk.getType()) { - case EC_P256: - if (!gpk.hasEcP256PublicKey()) { - break; - } - return parseEcPublicKey(gpk.getEcP256PublicKey()); - case RSA2048: - if (!gpk.hasRsa2048PublicKey()) { - break; - } - return parseRsa2048PublicKey(gpk.getRsa2048PublicKey()); - case DH2048_MODP: - if (!gpk.hasDh2048PublicKey()) { - break; - } - return parseDh2048PublicKey(gpk.getDh2048PublicKey()); - default: - throw new InvalidKeySpecException("Unsupported GenericPublicKey type: " + gpk.getType()); - } - throw new InvalidKeySpecException("key object is missing for key type: " + gpk.getType()); - } - - /** - * Extracts a {@link ECPublicKey} from an {@link EcP256PublicKey} proto message. - * - * @throws InvalidKeySpecException if the input is not a valid NIST P-256 public key or if - * this platform does not support Elliptic Curve keys - */ - public static ECPublicKey parseEcPublicKey(EcP256PublicKey p256pk) - throws InvalidKeySpecException { - if (!p256pk.hasX() || !p256pk.hasY()) { - throw new InvalidKeySpecException("Key is missing a required coordinate"); - } - if (isLegacyCryptoRequired()) { - throw new InvalidKeySpecException("Elliptic Curve keys not supported on this platform"); - } - byte[] encodedX = p256pk.getX().toByteArray(); - byte[] encodedY = p256pk.getY().toByteArray(); - try { - validateEcP256CoordinateEncoding(encodedX); - validateEcP256CoordinateEncoding(encodedY); - BigInteger wX = new BigInteger(encodedX); - BigInteger wY = new BigInteger(encodedY); - validateEcP256CurvePoint(wX, wY); - return (ECPublicKey) KeyFactory.getInstance(EC_ALG).generatePublic( - new ECPublicKeySpec(new ECPoint(wX, wY), EC_P256_PARAMS)); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } - } - - /** - * Extracts a {@link RSAPublicKey} from an {@link SimpleRsaPublicKey} proto message. - * - * @throws InvalidKeySpecException when the input RSA public key is invalid - */ - public static RSAPublicKey parseRsa2048PublicKey(SimpleRsaPublicKey pk) - throws InvalidKeySpecException { - if (!pk.hasN()) { - throw new InvalidKeySpecException("required field is missing"); - } - byte[] encodedN = pk.getN().toByteArray(); - validateSimpleRsaEncoding(encodedN); - BigInteger n = new BigInteger(encodedN); - if (n.bitLength() != RSA2048_MODULUS_BITS) { - throw new InvalidKeySpecException(); - } - BigInteger e = BigInteger.valueOf(pk.getE()); - try { - return (RSAPublicKey) KeyFactory.getInstance(RSA_ALG).generatePublic( - new RSAPublicKeySpec(n, e)); - } catch (NoSuchAlgorithmException e1) { - throw new AssertionError(e1); // Should never happen - } - } - - /** - * Extracts a {@link DHPublicKey} from an {@link DhPublicKey} proto message. - * - * @throws InvalidKeySpecException when the input DH public key is invalid - */ - @SuppressInsecureCipherModeCheckerPendingReview // b/32143855 - public static DHPublicKey parseDh2048PublicKey(DhPublicKey pk) throws InvalidKeySpecException { - if (!pk.hasY()) { - throw new InvalidKeySpecException("required field is missing"); - } - byte[] encodedY = pk.getY().toByteArray(); - validateDhEncoding(encodedY); - BigInteger y; - try { - y = new BigInteger(encodedY); - } catch (NumberFormatException e) { - throw new InvalidKeySpecException(); - } - validateDhGroupElement(y); - try { - return (DHPublicKey) KeyFactory.getInstance(DH_ALG).generatePublic( - new DHPublicKeySpec(y, DH_P, DH_G)); - } catch (NoSuchAlgorithmException e) { - throw new AssertionError(e); // Should never happen - } - } - - /** - * @return a freshly generated NIST P-256 Elliptic Curve key pair. - */ - public static KeyPair generateEcP256KeyPair() { - return getEcKeyGen().generateKeyPair(); - } - - /** - * @return a freshly generated 2048-bit RSA key pair. - */ - public static KeyPair generateRSA2048KeyPair() { - return getRsaKeyGen().generateKeyPair(); - } - - /** - * @return a freshly generated Diffie-Hellman key pair for the 2048-bit group - * described by {@link #DH_G} - */ - public static KeyPair generateDh2048KeyPair() { - try { - return getDhKeyGen().generateKeyPair(); - } catch (InvalidAlgorithmParameterException e) { - // Construct an appropriate KeyPair manually, since this platform refuses to do it for us - DHParameterSpec spec = new DHParameterSpec(DH_P, DH_G); - BigInteger x = new BigInteger(DH_LEN, new SecureRandom()); - DHPrivateKey privateKey = new DHPrivateKeyShim(x, spec); - DHPublicKey publicKey = new DHPublicKeyShim(DH_G.modPow(x, DH_P), spec); - return new KeyPair(publicKey, privateKey); - } - } - - /** - * A lightweight encoding for a {@link DHPrivateKey}. Strongly recommended over attempting to use - * {@link DHPrivateKey#getEncoded()}, but not compatible with the standard encoding. - * - * @see #parseDh2048PrivateKey(byte[]) - */ - public static byte[] encodeDh2048PrivateKey(DHPrivateKey sk) { - return sk.getX().toByteArray(); - } - - /** - * Parses a {@link DHPrivateKey} encoded with {@link #encodeDh2048PrivateKey(DHPrivateKey)}. - */ - public static DHPrivateKey parseDh2048PrivateKey(byte[] encodedX) - throws InvalidKeySpecException { - validateDhEncoding(encodedX); // Could be stricter for x, but should be fine to use this - BigInteger x; - try { - x = new BigInteger(encodedX); - } catch (NumberFormatException e) { - throw new InvalidKeySpecException(); - } - validateDhGroupElement(x); // Again, this validation should be good enough - return new DHPrivateKeyShim(x, new DHParameterSpec(DH_P, DH_G)); - } - - /** - * @throws InvalidKeySpecException if point ({@code x},{@code y}) isn't on the NIST P-256 curve - */ - private static void validateEcP256CurvePoint(BigInteger x, BigInteger y) - throws InvalidKeySpecException { - if ((x.signum() == -1) || (y.signum() == -1)) { - throw new InvalidKeySpecException("Point encoding must use only non-negative integers"); - } - - BigInteger p = EC_P256_P; - if ((x.compareTo(p) >= 0) || (y.compareTo(p) >= 0)) { - throw new InvalidKeySpecException("Point lies outside of the expected field"); - } - - // Points on the curve satisfy y^2 = x^3 + ax + b (mod p) - BigInteger lhs = squareMod(y, p); - BigInteger rhs = squareMod(x, p).add(EC_P256_A) // = (x^2 + a) - .multiply(x).mod(p) // = x(x^2 + a) = x^3 + ax - .add(EC_P256_B) // = x^3 + ax + b - .mod(p); - if (!lhs.equals(rhs)) { - throw new InvalidKeySpecException("Point does not lie on the expected curve"); - } - } - - /** - * @return value of {@code x}^2 (mod {@code p}) - */ - private static BigInteger squareMod(BigInteger x, BigInteger p) { - return x.multiply(x).mod(p); - } - - /** - * @throws InvalidKeySpecException if the coordinate is too large for a 256-bit curve - */ - private static void validateEcP256CoordinateEncoding(byte[] p) throws InvalidKeySpecException { - if ((p.length == 0) - || (p.length > MAX_P256_ENCODING_BYTES) - || (p.length == MAX_P256_ENCODING_BYTES && p[0] != 0)) { - throw new InvalidKeySpecException(); // Intentionally vague for security reasons - } - } - - /** - * @throws InvalidKeySpecException if the input is too large for a 2048-bit RSA modulus - */ - private static void validateSimpleRsaEncoding(byte[] n) throws InvalidKeySpecException { - if (n.length == 0 || n.length > MAX_RSA2048_ENCODING_BYTES) { - throw new InvalidKeySpecException(); - } - } - - /** - * @throws InvalidKeySpecException if the public key is too large for a 2048-bit DH group - */ - private static void validateDhEncoding(byte[] y) throws InvalidKeySpecException { - if (y.length == 0 || y.length > MAX_DH2048_ENCODING_BYTES) { - throw new InvalidKeySpecException(); - } - } - - /** - * @throws InvalidKeySpecException if {@code y} is not a valid Diffie-Hellman public key - */ - private static void validateDhGroupElement(BigInteger y) throws InvalidKeySpecException { - // Check that 1 < y < p -1 - if ((y.compareTo(ONE) < 1) || (y.compareTo(DH_P.subtract(ONE)) > -1)) { - throw new InvalidKeySpecException(); - } - } - - private static ByteString extractY(ECPublicKey epk) { - return ByteString.copyFrom(epk.getW().getAffineY().toByteArray()); - } - - private static ByteString extractX(ECPublicKey epk) { - return ByteString.copyFrom(epk.getW().getAffineX().toByteArray()); - } - - private static ECPublicKey pkToECPublicKey(PublicKey pk) { - if (pk == null) { - throw new NullPointerException(); - } - if (!(pk instanceof ECPublicKey)) { - throw new IllegalArgumentException("Not an EC Public Key"); - } - return (ECPublicKey) pk; - } - - private static RSAPublicKey pkToRSAPublicKey(PublicKey pk) { - if (pk == null) { - throw new NullPointerException(); - } - if (!(pk instanceof RSAPublicKey)) { - throw new IllegalArgumentException("Not an RSA Public Key"); - } - return (RSAPublicKey) pk; - } - - private static DHPublicKey pkToDHPublicKey(PublicKey pk) { - if (pk == null) { - throw new NullPointerException(); - } - if (!(pk instanceof DHPublicKey)) { - throw new IllegalArgumentException("Not a DH Public Key"); - } - return (DHPublicKey) pk; - } - - /** - * @return an EC {@link KeyPairGenerator} object initialized for NIST P-256. - */ - private static KeyPairGenerator getEcKeyGen() { - KeyPairGenerator keygen; - try { - keygen = KeyPairGenerator.getInstance(EC_ALG); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } - try { - // Try using the OpenSSL provider first, since we prefer it over BouncyCastle - keygen.initialize(new ECGenParameterSpec(EC_P256_OPENSSL_NAME)); - return keygen; - } catch (InvalidAlgorithmParameterException e) { - // Try another name for NIST P-256 - } - try { - keygen.initialize(new ECGenParameterSpec(EC_P256_COMMON_NAME)); - return keygen; - } catch (InvalidAlgorithmParameterException e) { - throw new RuntimeException("Unable to find the NIST P-256 curve"); - } - } - - /** - * @return an RSA {@link KeyPairGenerator} object initialized for 2048-bit keys. - */ - private static KeyPairGenerator getRsaKeyGen() { - try { - KeyPairGenerator keygen = KeyPairGenerator.getInstance(RSA_ALG); - keygen.initialize(RSA2048_MODULUS_BITS); - return keygen; - } catch (NoSuchAlgorithmException e) { - throw new AssertionError(e); // This should never happen - } - } - - /** - * @return a DH {@link KeyPairGenerator} object initialized for the group described by {@link - * #DH_G}. - * @throws InvalidAlgorithmParameterException on some platforms that don't support large DH groups - */ - @SuppressInsecureCipherModeCheckerPendingReview // b/32143855 - private static KeyPairGenerator getDhKeyGen() throws InvalidAlgorithmParameterException { - try { - KeyPairGenerator keygen = KeyPairGenerator.getInstance(DH_ALG); - keygen.initialize(new DHParameterSpec(DH_P, DH_G, DH_LEN)); - return keygen; - } catch (NoSuchAlgorithmException e) { - throw new AssertionError(e); // This should never happen - } - } - - /** - * A lightweight shim class to enable the creation of {@link DHPublicKey} and {@link DHPrivateKey} - * objects that accept arbitrary {@link DHParameterSpec}s -- unfortunately, many platforms do - * not support using reasonably sized Diffie-Hellman groups any other way. For instance, see - * <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6521495">Java bug 6521495</a>. - */ - public abstract static class DHKeyShim { - - private BigInteger eitherXorY; - private DHParameterSpec params; - - public DHKeyShim(BigInteger eitherXorY, DHParameterSpec params) { - this.eitherXorY = eitherXorY; - this.params = params; - } - - public DHParameterSpec getParams() { - return params; - } - - public String getAlgorithm() { - return "DH"; - } - - public String getFormat() { - return null; - } - - public byte[] getEncoded() { - return null; - } - - public BigInteger getX() { - return eitherXorY; - } - - public BigInteger getY() { - return eitherXorY; - } - } - - /** - * A simple {@link DHPublicKey} implementation. - * - * @see DHKeyShim - */ - public static class DHPublicKeyShim extends DHKeyShim implements DHPublicKey { - public DHPublicKeyShim(BigInteger y, DHParameterSpec params) { - super(y, params); - } - } - - /** - * A simple {@link DHPrivateKey} implementation. - * - * @see DHKeyShim - */ - public static class DHPrivateKeyShim extends DHKeyShim implements DHPrivateKey { - public DHPrivateKeyShim(BigInteger x, DHParameterSpec params) { - super(x, params); - } - } - - /** - * @return true if this platform does not support Elliptic Curve algorithms - */ - public static boolean isLegacyCryptoRequired() { - return IS_LEGACY_CRYPTO_REQUIRED; - } - - /** - * @return true if using the Elliptic Curve key generator fails on this platform - */ - private static boolean determineIfLegacyCryptoRequired() { - try { - getEcKeyGen(); - } catch (Exception e) { - return true; - } - return false; - } -} diff --git a/src/main/java/com/google/security/cryptauth/lib/securemessage/SecureMessageBuilder.java b/src/main/java/com/google/security/cryptauth/lib/securemessage/SecureMessageBuilder.java deleted file mode 100644 index f1a9464..0000000 --- a/src/main/java/com/google/security/cryptauth/lib/securemessage/SecureMessageBuilder.java +++ /dev/null @@ -1,277 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.security.cryptauth.lib.securemessage; - -import com.google.protobuf.ByteString; -import com.google.security.cryptauth.lib.securemessage.CryptoOps.EncType; -import com.google.security.cryptauth.lib.securemessage.CryptoOps.SigType; -import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.Header; -import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.HeaderAndBodyInternal; -import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.SecureMessage; -import java.security.InvalidKeyException; -import java.security.Key; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.util.Arrays; -import javax.annotation.Nullable; - -/** - * Builder for {@link SecureMessage} protos. Can be used to create either signed messages, - * or "signcrypted" (encrypted then signed) messages that include a tight binding between the - * ciphertext portion and a verification key identity. - * - * @see SecureMessageParser - */ -public class SecureMessageBuilder { - private ByteString publicMetadata; - private ByteString verificationKeyId; - private ByteString decryptionKeyId; - /** - * This data is never sent inside the protobufs, so the builder just saves it as a byte[]. - */ - private byte[] associatedData; - - private SecureRandom rng; - - public SecureMessageBuilder() { - reset(); - this.rng = new SecureRandom(); - } - - /** - * Resets this {@link SecureMessageBuilder} instance to a blank configuration (and returns it). - */ - public SecureMessageBuilder reset() { - this.publicMetadata = null; - this.verificationKeyId = null; - this.decryptionKeyId = null; - this.associatedData = null; - return this; - } - - /** - * Optional metadata to be sent along with the header information in this {@link SecureMessage}. - * <p> - * Note that this value will be sent <em>UNENCRYPTED</em> in all cases. - * <p> - * Can be used with either cleartext or signcrypted messages, but is intended primarily for use - * with signcrypted messages. - */ - public SecureMessageBuilder setPublicMetadata(byte[] publicMetadata) { - this.publicMetadata = ByteString.copyFrom(publicMetadata); - return this; - } - - /** - * The recipient of the {@link SecureMessage} should be able to uniquely determine the correct - * verification key, given only this value. - * <p> - * Can be used with either cleartext or signcrypted messages. Setting this is mandatory for - * signcrypted messages using a public key {@link SigType}, in order to bind the encrypted - * body to a specific verification key. - * <p> - * Note that this value is sent <em>UNENCRYPTED</em> in all cases. - */ - public SecureMessageBuilder setVerificationKeyId(byte[] verificationKeyId) { - this.verificationKeyId = ByteString.copyFrom(verificationKeyId); - return this; - } - - /** - * To be used only with {@link #buildSignCryptedMessage(Key, SigType, Key, EncType, byte[])}, - * this value is sent <em>UNENCRYPTED</em> as part of the header. It should be used by the - * recipient of the {@link SecureMessage} to identify an appropriate key to use for decrypting - * the message body. - */ - public SecureMessageBuilder setDecryptionKeyId(byte[] decryptionKeyId) { - this.decryptionKeyId = ByteString.copyFrom(decryptionKeyId); - return this; - } - - /** - * Additional data is "associated" with this {@link SecureMessage}, but will not be sent as - * part of it. The recipient of the {@link SecureMessage} will need to provide the same data in - * order to verify the message body. Setting this to {@code null} is equivalent to using an - * empty array (unlike the behavior of {@code VerificationKeyId} and {@code DecryptionKeyId}). - * <p> - * Note that the <em>size</em> (length in bytes) of the associated data will be sent in the - * <em>UNENCRYPTED</em> header information, even if you are using encryption. - * <p> - * If you will be using {@link #buildSignedCleartextMessage(Key, SigType, byte[])}, then anyone - * observing the {@link SecureMessage} may be able to infer this associated data via an - * "offline dictionary attack". That is, when no encryption is used, you will not be hiding this - * data simply because it is not being sent over the wire. - */ - public SecureMessageBuilder setAssociatedData(@Nullable byte[] associatedData) { - this.associatedData = associatedData; - return this; - } - - // @VisibleForTesting - SecureMessageBuilder setRng(SecureRandom rng) { - this.rng = rng; - return this; - } - - /** - * Generates a signed {@link SecureMessage} with the payload {@code body} left - * <em>UNENCRYPTED</em>. - * - * <p>Note that if you have used {@link #setAssociatedData(byte[])}, the associated data will - * be subject to offline dictionary attacks if you use a public key {@link SigType}. - * - * <p>Doesn't currently support symmetric keys stored in a TPM (since we access the raw key). - * - * @see SecureMessageParser#parseSignedCleartextMessage(SecureMessage, Key, SigType) - */ - public SecureMessage buildSignedCleartextMessage(Key signingKey, SigType sigType, byte[] body) - throws NoSuchAlgorithmException, InvalidKeyException { - if ((signingKey == null) || (sigType == null) || (body == null)) { - throw new NullPointerException(); - } - if (decryptionKeyId != null) { - throw new IllegalStateException("Cannot set decryptionKeyId for a cleartext message"); - } - - byte[] headerAndBody = serializeHeaderAndBody( - buildHeader(sigType, EncType.NONE, null).toByteArray(), body); - return createSignedResult(signingKey, sigType, headerAndBody, associatedData); - } - - /** - * Generates a signed and encrypted {@link SecureMessage}. If the signature type requires a public - * key, such as with ECDSA_P256_SHA256, then the caller <em>must</em> set a verification id using - * the {@link #setVerificationKeyId(byte[])} method. The verification key id will be bound to the - * encrypted {@code body}, preventing attacks that involve stripping the signature and then - * re-signing the encrypted {@code body} as if it was originally sent by the attacker. - * - * <p> - * It is safe to re-use one {@link javax.crypto.SecretKey} as both {@code signingKey} and - * {@code encryptionKey}, even if that key is also used for - * {@link #buildSignedCleartextMessage(Key, SigType, byte[])}. In fact, the resulting output - * encoding will be more compact when the same symmetric key is used for both. - * - * <p> - * Note that PublicMetadata and other header fields are left <em>UNENCRYPTED</em>. - * - * <p> - * Doesn't currently support symmetric keys stored in a TPM (since we access the raw key). - * - * @param encType <em>must not</em> be set to {@link EncType#NONE} - * @see SecureMessageParser#parseSignCryptedMessage(SecureMessage, Key, SigType, Key, EncType) - */ - public SecureMessage buildSignCryptedMessage( - Key signingKey, SigType sigType, Key encryptionKey, EncType encType, byte[] body) - throws NoSuchAlgorithmException, InvalidKeyException { - if ((signingKey == null) - || (sigType == null) - || (encryptionKey == null) - || (encType == null) - || (body == null)) { - throw new NullPointerException(); - } - if (encType == EncType.NONE) { - throw new IllegalArgumentException(encType + " not supported for encrypted messages"); - } - if (sigType.isPublicKeyScheme() && (verificationKeyId == null)) { - throw new IllegalStateException( - "Must set a verificationKeyId when using public key signature with encryption"); - } - - byte[] iv = CryptoOps.generateIv(encType, rng); - byte[] header = buildHeader(sigType, encType, iv).toByteArray(); - - // We may or may not need an extra tag in front of the plaintext body - byte[] taggedBody; - // We will only sign the associated data when we don't tag the plaintext body - byte[] associatedDataToBeSigned; - if (taggedPlaintextRequired(signingKey, sigType, encryptionKey)) { - // Place a "tag" in front of the the plaintext message containing a digest of the header - taggedBody = CryptoOps.concat( - // Digest the header + any associated data, yielding a tag to be encrypted with the body. - CryptoOps.digest(CryptoOps.concat(header, associatedData)), - body); - associatedDataToBeSigned = null; // We already handled any associatedData via the tag - } else { - taggedBody = body; - associatedDataToBeSigned = associatedData; - } - - // Compute the encrypted body, which binds the tag to the message inside the ciphertext - byte[] encryptedBody = CryptoOps.encrypt(encryptionKey, encType, rng, iv, taggedBody); - - byte[] headerAndBody = serializeHeaderAndBody(header, encryptedBody); - return createSignedResult(signingKey, sigType, headerAndBody, associatedDataToBeSigned); - } - - /** - * Indicates whether a "tag" is needed next to the plaintext body inside the ciphertext, to - * prevent the same ciphertext from being reused with someone else's signature on it. - */ - static boolean taggedPlaintextRequired(Key signingKey, SigType sigType, Key encryptionKey) { - // We need a tag if different keys are being used to "sign" vs. encrypt - return sigType.isPublicKeyScheme() - || !Arrays.equals(signingKey.getEncoded(), encryptionKey.getEncoded()); - } - - /** - * @param iv IV or {@code null} if IV to be left unset in the Header - */ - private Header buildHeader(SigType sigType, EncType encType, byte[] iv) { - Header.Builder result = Header.newBuilder() - .setSignatureScheme(sigType.getSigScheme()) - .setEncryptionScheme(encType.getEncScheme()); - if (verificationKeyId != null) { - result.setVerificationKeyId(verificationKeyId); - } - if (decryptionKeyId != null) { - result.setDecryptionKeyId(decryptionKeyId); - } - if (publicMetadata != null) { - result.setPublicMetadata(publicMetadata); - } - if (associatedData != null) { - result.setAssociatedDataLength(associatedData.length); - } - if (iv != null) { - result.setIv(ByteString.copyFrom(iv)); - } - return result.build(); - } - - /** - * @param header a serialized representation of a {@link Header} - * @param body arbitrary payload data - * @return a serialized representation of a {@link SecureMessageProto.HeaderAndBody} - */ - private byte[] serializeHeaderAndBody(byte[] header, byte[] body) { - return HeaderAndBodyInternal.newBuilder() - .setHeader(ByteString.copyFrom(header)) - .setBody(ByteString.copyFrom(body)) - .build() - .toByteArray(); - } - - private SecureMessage createSignedResult( - Key signingKey, SigType sigType, byte[] headerAndBody, @Nullable byte[] associatedData) - throws NoSuchAlgorithmException, InvalidKeyException { - byte[] sig = - CryptoOps.sign(sigType, signingKey, rng, CryptoOps.concat(headerAndBody, associatedData)); - return SecureMessage.newBuilder() - .setHeaderAndBody(ByteString.copyFrom(headerAndBody)) - .setSignature(ByteString.copyFrom(sig)) - .build(); - } -} diff --git a/src/main/java/com/google/security/cryptauth/lib/securemessage/SecureMessageParser.java b/src/main/java/com/google/security/cryptauth/lib/securemessage/SecureMessageParser.java deleted file mode 100644 index d634d40..0000000 --- a/src/main/java/com/google/security/cryptauth/lib/securemessage/SecureMessageParser.java +++ /dev/null @@ -1,270 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.security.cryptauth.lib.securemessage; - -import com.google.protobuf.ByteString; -import com.google.protobuf.InvalidProtocolBufferException; -import com.google.security.cryptauth.lib.securemessage.CryptoOps.EncType; -import com.google.security.cryptauth.lib.securemessage.CryptoOps.SigType; -import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.Header; -import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.HeaderAndBody; -import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.HeaderAndBodyInternal; -import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.SecureMessage; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.Key; -import java.security.NoSuchAlgorithmException; -import java.security.SignatureException; -import javax.annotation.Nullable; -import javax.crypto.BadPaddingException; -import javax.crypto.IllegalBlockSizeException; - -/** - * Utility class to parse and verify {@link SecureMessage} protos. Verifies the signature on the - * message, and decrypts "signcrypted" messages (while simultaneously verifying the signature). - * - * @see SecureMessageBuilder - */ -public class SecureMessageParser { - - private SecureMessageParser() {} // Do not instantiate - - /** - * Extracts the {@link Header} component from a {@link SecureMessage} but <em>DOES NOT VERIFY</em> - * the signature when doing so. Callers should not trust the resulting output until after a - * subsequent {@code parse*()} call has succeeded. - * - * <p>The intention is to allow the caller to determine the type of the protocol message and which - * keys are in use, prior to attempting to verify (and possibly decrypt) the payload body. - */ - public static Header getUnverifiedHeader(SecureMessage secmsg) - throws InvalidProtocolBufferException { - if (!secmsg.hasHeaderAndBody()) { - throw new InvalidProtocolBufferException("Missing header and body"); - } - if (!HeaderAndBody.parseFrom(secmsg.getHeaderAndBody()).hasHeader()) { - throw new InvalidProtocolBufferException("Missing header"); - } - Header result = HeaderAndBody.parseFrom(secmsg.getHeaderAndBody()).getHeader(); - // Check that at least a signature scheme was set - if (!result.hasSignatureScheme()) { - throw new InvalidProtocolBufferException("Missing header field(s)"); - } - // Check signature scheme is legal - try { - SigType.valueOf(result.getSignatureScheme()); - } catch (IllegalArgumentException e) { - throw new InvalidProtocolBufferException("Corrupt/unsupported SignatureScheme"); - } - // Check encryption scheme is legal - if (result.hasEncryptionScheme()) { - try { - EncType.valueOf(result.getEncryptionScheme()); - } catch (IllegalArgumentException e) { - throw new InvalidProtocolBufferException("Corrupt/unsupported EncryptionScheme"); - } - } - return result; - } - - /** - * Parses a {@link SecureMessage} containing a cleartext payload body, and verifies the signature. - * - * @return the parsed {@link HeaderAndBody} pair (which is fully verified) - * @throws SignatureException if signature verification fails - * @see SecureMessageBuilder#buildSignedCleartextMessage(Key, SigType, byte[]) - */ - public static HeaderAndBody parseSignedCleartextMessage( - SecureMessage secmsg, Key verificationKey, SigType sigType) - throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { - return parseSignedCleartextMessage(secmsg, verificationKey, sigType, null); - } - - /** - * Parses a {@link SecureMessage} containing a cleartext payload body, and verifies the signature. - * - * @param associatedData optional associated data bound to the signature (but not in the message) - * @return the parsed {@link HeaderAndBody} pair (which is fully verified) - * @throws SignatureException if signature verification fails - * @see SecureMessageBuilder#buildSignedCleartextMessage(Key, SigType, byte[]) - */ - public static HeaderAndBody parseSignedCleartextMessage( - SecureMessage secmsg, Key verificationKey, SigType sigType, @Nullable byte[] associatedData) - throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { - if ((secmsg == null) || (verificationKey == null) || (sigType == null)) { - throw new NullPointerException(); - } - return verifyHeaderAndBody( - secmsg, - verificationKey, - sigType, - EncType.NONE, - associatedData, - false /* suppressAssociatedData is always false for signed cleartext */); - } - - /** - * Parses a {@link SecureMessage} containing an encrypted payload body, extracting a decryption of - * the payload body and verifying the signature. - * - * @return the parsed {@link HeaderAndBody} pair (which is fully verified and decrypted) - * @throws SignatureException if signature verification fails - * @see SecureMessageBuilder#buildSignCryptedMessage(Key, SigType, Key, EncType, byte[]) - */ - public static HeaderAndBody parseSignCryptedMessage( - SecureMessage secmsg, - Key verificationKey, - SigType sigType, - Key decryptionKey, - EncType encType) - throws InvalidKeyException, NoSuchAlgorithmException, SignatureException { - return parseSignCryptedMessage(secmsg, verificationKey, sigType, decryptionKey, encType, null); - } - - /** - * Parses a {@link SecureMessage} containing an encrypted payload body, extracting a decryption of - * the payload body and verifying the signature. - * - * @param associatedData optional associated data bound to the signature (but not in the message) - * @return the parsed {@link HeaderAndBody} pair (which is fully verified and decrypted) - * @throws SignatureException if signature verification fails - * @see SecureMessageBuilder#buildSignCryptedMessage(Key, SigType, Key, EncType, byte[]) - */ - public static HeaderAndBody parseSignCryptedMessage( - SecureMessage secmsg, - Key verificationKey, - SigType sigType, - Key decryptionKey, - EncType encType, - @Nullable byte[] associatedData) - throws InvalidKeyException, NoSuchAlgorithmException, SignatureException { - if ((secmsg == null) - || (verificationKey == null) - || (sigType == null) - || (decryptionKey == null) - || (encType == null)) { - throw new NullPointerException(); - } - if (encType == EncType.NONE) { - throw new SignatureException("Not a signcrypted message"); - } - - boolean tagRequired = - SecureMessageBuilder.taggedPlaintextRequired(verificationKey, sigType, decryptionKey); - HeaderAndBody headerAndEncryptedBody; - headerAndEncryptedBody = verifyHeaderAndBody( - secmsg, - verificationKey, - sigType, - encType, - associatedData, - tagRequired /* suppressAssociatedData if it is handled by the tag instead */); - - byte[] rawDecryptedBody; - Header header = headerAndEncryptedBody.getHeader(); - if (!header.hasIv()) { - throw new SignatureException(); - } - try { - rawDecryptedBody = CryptoOps.decrypt( - decryptionKey, encType, header.getIv().toByteArray(), - headerAndEncryptedBody.getBody().toByteArray()); - } catch (InvalidAlgorithmParameterException e) { - throw new SignatureException(); - } catch (IllegalBlockSizeException e) { - throw new SignatureException(); - } catch (BadPaddingException e) { - throw new SignatureException(); - } - - if (!tagRequired) { - // No tag expected, so we're all done - return HeaderAndBody.newBuilder(headerAndEncryptedBody) - .setBody(ByteString.copyFrom(rawDecryptedBody)) - .build(); - } - - // Verify the tag that binds the ciphertext to the header, and remove it - byte[] headerBytes; - try { - headerBytes = - HeaderAndBodyInternal.parseFrom(secmsg.getHeaderAndBody()).getHeader().toByteArray(); - } catch (InvalidProtocolBufferException e) { - // This shouldn't happen, but throw it up just in case - throw new SignatureException(e); - } - boolean verifiedBinding = false; - byte[] expectedTag = CryptoOps.digest(CryptoOps.concat(headerBytes, associatedData)); - if (rawDecryptedBody.length >= CryptoOps.DIGEST_LENGTH) { - byte[] actualTag = CryptoOps.subarray(rawDecryptedBody, 0, CryptoOps.DIGEST_LENGTH); - if (CryptoOps.constantTimeArrayEquals(actualTag, expectedTag)) { - verifiedBinding = true; - } - } - if (!verifiedBinding) { - throw new SignatureException(); - } - - int bodyLen = rawDecryptedBody.length - CryptoOps.DIGEST_LENGTH; - return HeaderAndBody.newBuilder(headerAndEncryptedBody) - // Remove the tag and set the plaintext body - .setBody(ByteString.copyFrom(rawDecryptedBody, CryptoOps.DIGEST_LENGTH, bodyLen)) - .build(); - } - - private static HeaderAndBody verifyHeaderAndBody( - SecureMessage secmsg, - Key verificationKey, - SigType sigType, - EncType encType, - @Nullable byte[] associatedData, - boolean suppressAssociatedData /* in case it is in the tag instead */) - throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { - if (!secmsg.hasHeaderAndBody() || !secmsg.hasSignature()) { - throw new SignatureException("Signature failed verification"); - } - byte[] signature = secmsg.getSignature().toByteArray(); - byte[] data = secmsg.getHeaderAndBody().toByteArray(); - byte[] signedData = suppressAssociatedData ? data : CryptoOps.concat(data, associatedData); - - // Try not to leak the specific reason for verification failures, due to security concerns. - boolean verified = CryptoOps.verify(verificationKey, sigType, signature, signedData); - HeaderAndBody result = null; - try { - result = HeaderAndBody.parseFrom(secmsg.getHeaderAndBody()); - // Even if declared required, micro proto doesn't throw an exception if fields are not present - if (!result.hasHeader() || !result.hasBody()) { - throw new SignatureException("Signature failed verification"); - } - verified &= (result.getHeader().getSignatureScheme() == sigType.getSigScheme()); - verified &= (result.getHeader().getEncryptionScheme() == encType.getEncScheme()); - // Check that either a decryption operation is expected, or no DecryptionKeyId is set. - verified &= (encType != EncType.NONE) || !result.getHeader().hasDecryptionKeyId(); - // If encryption was used, check that either we are not using a public key signature or a - // VerificationKeyId was set (as is required for public key based signature + encryption). - verified &= (encType == EncType.NONE) || !sigType.isPublicKeyScheme() || - result.getHeader().hasVerificationKeyId(); - int associatedDataLength = associatedData == null ? 0 : associatedData.length; - verified &= (result.getHeader().getAssociatedDataLength() == associatedDataLength); - } catch (InvalidProtocolBufferException e) { - verified = false; - } - - if (verified) { - return result; - } - throw new SignatureException("Signature failed verification"); - } -} diff --git a/src/main/javatest/com/google/security/cryptauth/lib/securegcm/D2DConnectionContextTest.java b/src/main/javatest/com/google/security/cryptauth/lib/securegcm/D2DConnectionContextTest.java deleted file mode 100644 index e671e8c..0000000 --- a/src/main/javatest/com/google/security/cryptauth/lib/securegcm/D2DConnectionContextTest.java +++ /dev/null @@ -1,568 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.security.cryptauth.lib.securegcm; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -import java.security.SignatureException; -import java.util.Arrays; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -/** - * Base class for Android compatible tests for {@link D2DConnectionContext} subclasses. - * Note: We would use a Parameterized test runner to test different versions, but this - * functionality is not supported by Android tests. - */ -@RunWith(JUnit4.class) -public class D2DConnectionContextTest { - private static final String PING = "ping"; - private static final String PONG = "pong"; - - // Key is: "initiator_encode_key_for_aes_256" - private static final SecretKey INITIATOR_ENCODE_KEY = new SecretKeySpec( - new byte[] { - (byte) 0x69, (byte) 0x6e, (byte) 0x69, (byte) 0x74, (byte) 0x69, (byte) 0x61, (byte) 0x74, - (byte) 0x6f, (byte) 0x72, (byte) 0x5f, (byte) 0x65, (byte) 0x6e, (byte) 0x63, (byte) 0x6f, - (byte) 0x64, (byte) 0x65, (byte) 0x5f, (byte) 0x6b, (byte) 0x65, (byte) 0x79, (byte) 0x5f, - (byte) 0x66, (byte) 0x6f, (byte) 0x72, (byte) 0x5f, (byte) 0x61, (byte) 0x65, (byte) 0x73, - (byte) 0x5f, (byte) 0x32, (byte) 0x35, (byte) 0x36 - }, - "AES"); - - // Key is: "initiator_decode_key_for_aes_256" - private static final SecretKey INITIATOR_DECODE_KEY = new SecretKeySpec( - new byte[] { - (byte) 0x69, (byte) 0x6e, (byte) 0x69, (byte) 0x74, (byte) 0x69, (byte) 0x61, (byte) 0x74, - (byte) 0x6f, (byte) 0x72, (byte) 0x5f, (byte) 0x64, (byte) 0x65, (byte) 0x63, (byte) 0x6f, - (byte) 0x64, (byte) 0x65, (byte) 0x5f, (byte) 0x6b, (byte) 0x65, (byte) 0x79, (byte) 0x5f, - (byte) 0x66, (byte) 0x6f, (byte) 0x72, (byte) 0x5f, (byte) 0x61, (byte) 0x65, (byte) 0x73, - (byte) 0x5f, (byte) 0x32, (byte) 0x35, (byte) 0x36 - }, - "AES"); - - private D2DConnectionContext initiatorCtx; - private D2DConnectionContext responderCtx; - - @Before - public void setUp() throws Exception { - KeyEncodingTest.installSunEcSecurityProviderIfNecessary(); - } - - protected void testPeerToPeerProtocol(int protocolVersion) throws Exception { - - if (KeyEncoding.isLegacyCryptoRequired()) { - // this means we're running on an old SDK, which doesn't support the - // necessary crypto. Let's not test anything in this case. - return; - } - - initiatorCtx = createConnectionContext(protocolVersion, true /** isInitiator */); - responderCtx = createConnectionContext(protocolVersion, false /** isInitiator */); - - byte[] pingMessage = initiatorCtx.encodeMessageToPeer(PING); - // (send message to responder) - - // responder - String messageStr = responderCtx.decodeMessageFromPeerAsString(pingMessage); - assertEquals(PING, messageStr); - - byte[] pongMessage = responderCtx.encodeMessageToPeer(PONG); - // (send message to initiator) - - // initiator - messageStr = initiatorCtx.decodeMessageFromPeerAsString(pongMessage); - assertEquals(PONG, messageStr); - - // let's make sure there is actually some crypto involved. - pingMessage = initiatorCtx.encodeMessageToPeer("can you see this?"); - pingMessage[2] = (byte) (pingMessage[2] + 1); // twiddle with the message - try { - responderCtx.decodeMessageFromPeerAsString(pingMessage); - fail("expected exception, but didn't get it"); - } catch (SignatureException expected) { - assertTrue(expected.getMessage().contains("failed verification")); - } - - // Try and replay the previous encoded message to the initiator (replays should not work). - try { - initiatorCtx.decodeMessageFromPeerAsString(pongMessage); - fail("expected exception, but didn't get it"); - } catch (SignatureException expected) { - assertTrue(expected.getMessage().contains("sequence")); - } - - assertEquals(protocolVersion, initiatorCtx.getProtocolVersion()); - assertEquals(protocolVersion, responderCtx.getProtocolVersion()); - } - - @Test - public void testPeerToPeerProtocol_V0() throws Exception { - testPeerToPeerProtocol(D2DConnectionContextV0.PROTOCOL_VERSION); - } - - @Test - public void testPeerToPeerProtocol_V1() throws Exception { - testPeerToPeerProtocol(D2DConnectionContextV1.PROTOCOL_VERSION); - } - - protected void testResponderSendsFirst(int protocolVersion) throws Exception { - - if (KeyEncoding.isLegacyCryptoRequired()) { - // this means we're running on an old SDK, which doesn't support the - // necessary crypto. Let's not test anything in this case. - return; - } - - initiatorCtx = createConnectionContext(protocolVersion, true /** isInitiator */); - responderCtx = createConnectionContext(protocolVersion, false /** isInitiator */); - - byte[] pongMessage = responderCtx.encodeMessageToPeer(PONG); - assertEquals(PONG, initiatorCtx.decodeMessageFromPeerAsString(pongMessage)); - - pongMessage = responderCtx.encodeMessageToPeer(PONG); - assertEquals(PONG, initiatorCtx.decodeMessageFromPeerAsString(pongMessage)); - - // for good measure, if the initiator now responds, it should also work: - byte[] pingMessage = initiatorCtx.encodeMessageToPeer(PING); - assertEquals(PING, responderCtx.decodeMessageFromPeerAsString(pingMessage)); - - pingMessage = initiatorCtx.encodeMessageToPeer(PING); - assertEquals(PING, responderCtx.decodeMessageFromPeerAsString(pingMessage)); - - pingMessage = initiatorCtx.encodeMessageToPeer(PING); - assertEquals(PING, responderCtx.decodeMessageFromPeerAsString(pingMessage)); - } - - @Test - public void testResponderSendsFirst_V0() throws Exception { - testResponderSendsFirst(D2DConnectionContextV0.PROTOCOL_VERSION); - } - - @Test - public void testResponderSendsFirst_V1() throws Exception { - testResponderSendsFirst(D2DConnectionContextV1.PROTOCOL_VERSION); - } - - protected void testAssymmetricFlows(int protocolVersion) throws Exception { - - if (KeyEncoding.isLegacyCryptoRequired()) { - // this means we're running on an old SDK, which doesn't support the - // necessary crypto. Let's not test anything in this case. - return; - } - - initiatorCtx = createConnectionContext(protocolVersion, true /** isInitiator */); - responderCtx = createConnectionContext(protocolVersion, false /** isInitiator */); - - // Let's test that this still works if one side sends a few messages in a row. - byte[] pingMessage = initiatorCtx.encodeMessageToPeer(PING); - assertEquals(PING, responderCtx.decodeMessageFromPeerAsString(pingMessage)); - - pingMessage = initiatorCtx.encodeMessageToPeer(PING); - assertEquals(PING, responderCtx.decodeMessageFromPeerAsString(pingMessage)); - - pingMessage = initiatorCtx.encodeMessageToPeer(PING); - assertEquals(PING, responderCtx.decodeMessageFromPeerAsString(pingMessage)); - - - byte[] pongMessage = responderCtx.encodeMessageToPeer(PONG); - assertEquals(PONG, initiatorCtx.decodeMessageFromPeerAsString(pongMessage)); - - pongMessage = responderCtx.encodeMessageToPeer(PONG); - assertEquals(PONG, initiatorCtx.decodeMessageFromPeerAsString(pongMessage)); - } - - @Test - public void testAssymmetricFlows_V0() throws Exception { - testAssymmetricFlows(D2DConnectionContextV0.PROTOCOL_VERSION); - } - - @Test - public void testAssymmetricFlows_V1() throws Exception { - testAssymmetricFlows(D2DConnectionContextV1.PROTOCOL_VERSION); - } - - public void testErrorWhenResponderResendsMessage(int protocolVersion) throws Exception { - if (KeyEncoding.isLegacyCryptoRequired()) { - // this means we're running on an old SDK, which doesn't support the - // necessary crypto. Let's not test anything in this case. - return; - } - - initiatorCtx = createConnectionContext(protocolVersion, true /** isInitiator */); - responderCtx = createConnectionContext(protocolVersion, false /** isInitiator */); - - byte[] pongMessage = responderCtx.encodeMessageToPeer(PONG); - assertEquals(PONG, initiatorCtx.decodeMessageFromPeerAsString(pongMessage)); - - try { - // send pongMessage again to the initiator - initiatorCtx.decodeMessageFromPeerAsString(pongMessage); - fail("expected exception, but didn't get it"); - } catch (SignatureException expected) { - assertTrue(expected.getMessage().contains("sequence")); - } - } - - @Test - public void testErrorWhenResponderResendsMessage_V0() throws Exception { - testErrorWhenResponderResendsMessage(D2DConnectionContextV0.PROTOCOL_VERSION); - } - - @Test - public void testErrorWhenResponderResendsMessage_V1() throws Exception { - testErrorWhenResponderResendsMessage(D2DConnectionContextV1.PROTOCOL_VERSION); - } - - protected void testErrorWhenResponderEchoesInitiatorMessage( - int protocolVersion) throws Exception { - if (KeyEncoding.isLegacyCryptoRequired()) { - return; - } - - initiatorCtx = createConnectionContext(protocolVersion, true /** isInitiator */); - responderCtx = createConnectionContext(protocolVersion, false /** isInitiator */); - - byte[] pingMessage = initiatorCtx.encodeMessageToPeer(PING); - assertEquals(PING, responderCtx.decodeMessageFromPeerAsString(pingMessage)); - - try { - initiatorCtx.decodeMessageFromPeerAsString(pingMessage); - fail("expected exception, but didn't get it"); - } catch (SignatureException expected) { - } - } - - @Test - public void testErrorWhenResponderEchoesInitiatorMessage_V0() throws Exception { - testErrorWhenResponderEchoesInitiatorMessage(D2DConnectionContextV0.PROTOCOL_VERSION); - } - - @Test - public void testErrorWhenResponderEchoesInitiatorMessage_V1() throws Exception { - testErrorWhenResponderEchoesInitiatorMessage(D2DConnectionContextV1.PROTOCOL_VERSION); - } - - @Test - public void testErrorUsingV1InitiatorWithV0Responder() throws SignatureException { - if (KeyEncoding.isLegacyCryptoRequired()) { - // this means we're running on an old SDK, which doesn't support the - // necessary crypto. Let's not test anything in this case. - return; - } - - initiatorCtx = new D2DConnectionContextV1(INITIATOR_ENCODE_KEY, INITIATOR_DECODE_KEY, 1, 1); - responderCtx = new D2DConnectionContextV0(INITIATOR_DECODE_KEY, 1); - - // Decoding the responder's message should succeed, because the decode key and sequence numbers - // match. - initiatorCtx.decodeMessageFromPeer(responderCtx.encodeMessageToPeer(PING)); - - // Responder fails to decodes initiator's encoded message because keys do not match. - try { - responderCtx.decodeMessageFromPeer(initiatorCtx.encodeMessageToPeer(PONG)); - fail("Expected verification to fail."); - } catch (SignatureException e) { - // Exception expected. - } - } - - @Test - public void testErrorWithV0InitiatorV1Responder() throws SignatureException { - if (KeyEncoding.isLegacyCryptoRequired()) { - // this means we're running on an old SDK, which doesn't support the - // necessary crypto. Let's not test anything in this case. - return; - } - - initiatorCtx = new D2DConnectionContextV0(INITIATOR_ENCODE_KEY, 1); - responderCtx = new D2DConnectionContextV1(INITIATOR_DECODE_KEY, INITIATOR_ENCODE_KEY, 1, 1); - - // Decoding the initiator's message should succeed, because the decode key and sequence numbers - // match. - responderCtx.decodeMessageFromPeer(initiatorCtx.encodeMessageToPeer(PING)); - - // Initiator fails to decodes responder's encoded message because keys do not match. - try { - initiatorCtx.decodeMessageFromPeer(responderCtx.encodeMessageToPeer(PONG)); - fail("Expected verification to fail."); - } catch (SignatureException e) { - // Exception expected. - } - } - - protected void testSessionUnique(int protocolVersion) throws Exception { - // Should be the same (we set them up with the same key and sequence number) - initiatorCtx = createConnectionContext(protocolVersion, true /** isInitiator */); - responderCtx = createConnectionContext(protocolVersion, false /** isInitiator */); - Assert.assertArrayEquals(initiatorCtx.getSessionUnique(), responderCtx.getSessionUnique()); - - // Change just the key (should not match) - SecretKey wrongKey = new SecretKeySpec("wrong".getBytes("UTF8"), "AES"); - responderCtx = createConnectionContext(protocolVersion, false, wrongKey, wrongKey, 0, 1); - assertFalse(Arrays.equals(initiatorCtx.getSessionUnique(), responderCtx.getSessionUnique())); - - // Change just the sequence number (should still match) - responderCtx = createConnectionContext( - protocolVersion, false, INITIATOR_ENCODE_KEY, INITIATOR_DECODE_KEY, 2, 2); - Assert.assertArrayEquals(initiatorCtx.getSessionUnique(), responderCtx.getSessionUnique()); - } - - @Test - public void testSessionUnique_V0() throws Exception { - testSessionUnique(D2DConnectionContextV0.PROTOCOL_VERSION); - } - - @Test - public void testSessionUnique_V1() throws Exception { - testSessionUnique(D2DConnectionContextV1.PROTOCOL_VERSION); - } - - @Test - public void testSessionUniqueValues_V0() throws Exception { - // The key and the session unique value should match ones in the equivalent test in - // @link {cs/Nearby/D2DCrypto/Tests/D2DConnectionContextTest.m} - byte[] key = - new byte[] { - (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04, (byte) 0x05, (byte) 0x06, (byte) 0x07, - (byte) 0x08, (byte) 0x09, (byte) 0x0a, (byte) 0x0b, (byte) 0x0c, (byte) 0x0d, (byte) 0x0e, - (byte) 0x0f, (byte) 0x10, (byte) 0x11, (byte) 0x12, (byte) 0x13, (byte) 0x14, (byte) 0x15, - (byte) 0x16, (byte) 0x17, (byte) 0x18, (byte) 0x19, (byte) 0x1a, (byte) 0x1b, (byte) 0x1c, - (byte) 0x1d, (byte) 0x1e, (byte) 0x1f, (byte) 0x20 - }; - byte[] sessionUnique = - new byte[] { - (byte) 0x70, (byte) 0x7a, (byte) 0x17, (byte) 0x27, (byte) 0xa3, (byte) 0x0e, (byte) 0x68, - (byte) 0x63, (byte) 0x38, (byte) 0xdf, (byte) 0x72, (byte) 0x62, (byte) 0xf4, (byte) 0xb0, - (byte) 0x41, (byte) 0xac, (byte) 0x75, (byte) 0x8b, (byte) 0xca, (byte) 0x3b, (byte) 0x11, - (byte) 0xd4, (byte) 0x09, (byte) 0x64, (byte) 0x96, (byte) 0x54, (byte) 0xb4, (byte) 0x9b, - (byte) 0x43, (byte) 0xe6, (byte) 0x9b, (byte) 0xce - }; - - SecretKey secretKey = new SecretKeySpec(key, "AES"); - D2DConnectionContext context = new D2DConnectionContextV0(secretKey, 1); - - Assert.assertArrayEquals(context.getSessionUnique(), sessionUnique); - } - - @Test - public void testSessionUniqueValues_V1_Initiator() throws Exception { - // The key and the session unique value should match ones in the equivalent test in - // @link {cs/Nearby/D2DCrypto/Tests/D2DConnectionContextTest.m} - byte[] sessionUnique = - new byte[] { - (byte) 0x91, (byte) 0xc7, (byte) 0xc9, (byte) 0x26, (byte) 0x2c, (byte) 0x17, (byte) 0x8a, - (byte) 0xa0, (byte) 0x36, (byte) 0x9f, (byte) 0xf2, (byte) 0x05, (byte) 0x20, (byte) 0x98, - (byte) 0x38, (byte) 0x53, (byte) 0xa5, (byte) 0x46, (byte) 0xab, (byte) 0x3a, (byte) 0x21, - (byte) 0x3b, (byte) 0x76, (byte) 0x58, (byte) 0x59, (byte) 0x4e, (byte) 0xe7, (byte) 0xe3, - (byte) 0xc1, (byte) 0x69, (byte) 0x87, (byte) 0xfa - }; - - D2DConnectionContext initiatorContext = new D2DConnectionContextV1( - INITIATOR_ENCODE_KEY, INITIATOR_DECODE_KEY, 0, 1); - D2DConnectionContext responderContext = new D2DConnectionContextV1( - INITIATOR_DECODE_KEY, INITIATOR_ENCODE_KEY, 1, 0); - - // Both the initiator and responder must be the same. - Assert.assertArrayEquals(initiatorContext.getSessionUnique(), sessionUnique); - Assert.assertArrayEquals(responderContext.getSessionUnique(), sessionUnique); - } - - @Test - public void testSaveSessionV0() throws Exception { - D2DConnectionContext initiatorCtx = new D2DConnectionContextV0(INITIATOR_ENCODE_KEY, 1); - D2DConnectionContext responderCtx = new D2DConnectionContextV0(INITIATOR_ENCODE_KEY, 1); - - // Save the state - byte[] initiatorSavedSessionState = initiatorCtx.saveSession(); - byte[] responderSavedSessionState = responderCtx.saveSession(); - - // Try to rebuild the context - initiatorCtx = D2DConnectionContext.fromSavedSession(initiatorSavedSessionState); - responderCtx = D2DConnectionContext.fromSavedSession(responderSavedSessionState); - - // Sanity check - assertEquals(1, initiatorCtx.getSequenceNumberForDecoding()); - assertEquals(1, responderCtx.getSequenceNumberForDecoding()); - Assert.assertArrayEquals(initiatorCtx.getSessionUnique(), responderCtx.getSessionUnique()); - - // Make sure they can still talk to one another - assertEquals(PING, - responderCtx.decodeMessageFromPeerAsString(initiatorCtx.encodeMessageToPeer(PING))); - assertEquals(PONG, - initiatorCtx.decodeMessageFromPeerAsString(responderCtx.encodeMessageToPeer(PONG))); - } - - @Test - public void testSaveSessionV0_negativeSeqNumber() throws Exception { - D2DConnectionContext initiatorCtx = new D2DConnectionContextV0(INITIATOR_ENCODE_KEY, -5); - - // Save the state - byte[] initiatorSavedSessionState = initiatorCtx.saveSession(); - - // Try to rebuild the context - initiatorCtx = D2DConnectionContext.fromSavedSession(initiatorSavedSessionState); - - // Sanity check - assertEquals(-5, initiatorCtx.getSequenceNumberForDecoding()); - } - - @Test - public void testSaveSessionV0_shortKey() throws Exception { - D2DConnectionContext initiatorCtx = new D2DConnectionContextV0(INITIATOR_ENCODE_KEY, -5); - - // Save the state - byte[] initiatorSavedSessionState = initiatorCtx.saveSession(); - - // Try to rebuild the context - try { - D2DConnectionContext.fromSavedSession(Arrays.copyOf(initiatorSavedSessionState, - initiatorSavedSessionState.length - 1)); - fail("Expected failure as key is too short"); - } catch (IllegalArgumentException e) { - // expected - } - } - - @Test - public void testSaveSession_unknownProtocolVersion() throws Exception { - D2DConnectionContext initiatorCtx = new D2DConnectionContextV0(INITIATOR_ENCODE_KEY, -5); - - // Save the state - byte[] initiatorSavedSessionState = initiatorCtx.saveSession(); - - // Mess with the protocol version - initiatorSavedSessionState[0] = (byte) 0xff; - - // Try to rebuild the context - try { - D2DConnectionContext.fromSavedSession(initiatorSavedSessionState); - fail("Expected failure as 0xff is not a valid protocol version"); - } catch (IllegalArgumentException e) { - // expected - } - - // Mess with the protocol version in the other direction - initiatorSavedSessionState[0] = 2; - - // Try to rebuild the context - try { - D2DConnectionContext.fromSavedSession(initiatorSavedSessionState); - fail("Expected failure as 2 is not a valid protocol version"); - } catch (IllegalArgumentException e) { - // expected - } - } - - @Test - public void testSaveSessionV1() throws Exception { - D2DConnectionContext initiatorCtx = new D2DConnectionContextV1(INITIATOR_ENCODE_KEY, - INITIATOR_DECODE_KEY, 0, 1); - D2DConnectionContext responderCtx = new D2DConnectionContextV1(INITIATOR_DECODE_KEY, - INITIATOR_ENCODE_KEY, 1, 0); - - // Save the state - byte[] initiatorSavedSessionState = initiatorCtx.saveSession(); - byte[] responderSavedSessionState = responderCtx.saveSession(); - - // Try to rebuild the context - initiatorCtx = D2DConnectionContext.fromSavedSession(initiatorSavedSessionState); - responderCtx = D2DConnectionContext.fromSavedSession(responderSavedSessionState); - - // Sanity check - assertEquals(1, initiatorCtx.getSequenceNumberForDecoding()); - assertEquals(0, initiatorCtx.getSequenceNumberForEncoding()); - assertEquals(0, responderCtx.getSequenceNumberForDecoding()); - assertEquals(1, responderCtx.getSequenceNumberForEncoding()); - Assert.assertArrayEquals(initiatorCtx.getSessionUnique(), responderCtx.getSessionUnique()); - - // Make sure they can still talk to one another - assertEquals(PING, - responderCtx.decodeMessageFromPeerAsString(initiatorCtx.encodeMessageToPeer(PING))); - assertEquals(PONG, - initiatorCtx.decodeMessageFromPeerAsString(responderCtx.encodeMessageToPeer(PONG))); - } - - @Test - public void testSaveSessionV1_negativeSeqNumbers() throws Exception { - D2DConnectionContext initiatorCtx = new D2DConnectionContextV1(INITIATOR_ENCODE_KEY, - INITIATOR_DECODE_KEY, -8, -10); - - // Save the state - byte[] initiatorSavedSessionState = initiatorCtx.saveSession(); - - // Try to rebuild the context - initiatorCtx = D2DConnectionContext.fromSavedSession(initiatorSavedSessionState); - - // Sanity check - assertEquals(-10, initiatorCtx.getSequenceNumberForDecoding()); - assertEquals(-8, initiatorCtx.getSequenceNumberForEncoding()); - } - - @Test - public void testSaveSessionV1_tooShort() throws Exception { - D2DConnectionContext initiatorCtx = new D2DConnectionContextV1(INITIATOR_ENCODE_KEY, - INITIATOR_DECODE_KEY, -8, -10); - - // Save the state - byte[] initiatorSavedSessionState = initiatorCtx.saveSession(); - - // Try to rebuild the context - try { - D2DConnectionContext.fromSavedSession( - Arrays.copyOf(initiatorSavedSessionState, initiatorSavedSessionState.length - 1)); - fail("Expected error as saved session is too short"); - } catch (IllegalArgumentException e) { - // expected - } - - // Sanity check - assertEquals(-10, initiatorCtx.getSequenceNumberForDecoding()); - assertEquals(-8, initiatorCtx.getSequenceNumberForEncoding()); - } - - D2DConnectionContext createConnectionContext(int protocolVersion, boolean isInitiator) { - return createConnectionContext( - protocolVersion, isInitiator, INITIATOR_ENCODE_KEY, INITIATOR_DECODE_KEY, 0, 1); - } - - D2DConnectionContext createConnectionContext( - int protocolVersion, boolean isInitiator, - SecretKey initiatorEncodeKey, SecretKey initiatorDecodeKey, - int initiatorSequenceNumber, int responderSequenceNumber) { - if (protocolVersion == D2DConnectionContextV0.PROTOCOL_VERSION) { - return new D2DConnectionContextV0(initiatorEncodeKey, responderSequenceNumber); - } else if (protocolVersion == D2DConnectionContextV1.PROTOCOL_VERSION) { - return isInitiator - ? new D2DConnectionContextV1( - initiatorEncodeKey, initiatorDecodeKey, - initiatorSequenceNumber, responderSequenceNumber) - : new D2DConnectionContextV1( - initiatorDecodeKey, initiatorEncodeKey, - responderSequenceNumber, initiatorSequenceNumber); - } else { - throw new IllegalArgumentException("Unknown version: " + protocolVersion); - } - } -} diff --git a/src/main/javatest/com/google/security/cryptauth/lib/securegcm/D2DDiffieHellmanKeyExchangeHandshakeTest.java b/src/main/javatest/com/google/security/cryptauth/lib/securegcm/D2DDiffieHellmanKeyExchangeHandshakeTest.java deleted file mode 100644 index 4de794a..0000000 --- a/src/main/javatest/com/google/security/cryptauth/lib/securegcm/D2DDiffieHellmanKeyExchangeHandshakeTest.java +++ /dev/null @@ -1,432 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.security.cryptauth.lib.securegcm; - -import com.google.security.cryptauth.lib.securegcm.DeviceToDeviceMessagesProto.InitiatorHello; -import com.google.security.cryptauth.lib.securegcm.DeviceToDeviceMessagesProto.ResponderHello; -import com.google.security.cryptauth.lib.securegcm.TransportCryptoOps.Payload; -import com.google.security.cryptauth.lib.securegcm.TransportCryptoOps.PayloadType; -import com.google.security.cryptauth.lib.securemessage.PublicKeyProtoUtil; -import java.nio.charset.Charset; -import java.security.KeyPair; -import java.security.PublicKey; -import java.security.SignatureException; -import javax.crypto.SecretKey; -import junit.framework.TestCase; -import org.junit.Assert; - -/** - * Android compatible tests for the {@link D2DDiffieHellmanKeyExchangeHandshake} class. - */ -public class D2DDiffieHellmanKeyExchangeHandshakeTest extends TestCase { - - private static final byte[] RESPONDER_HELLO_MESSAGE = - "first payload".getBytes(Charset.forName("UTF-8")); - - private static final String PING = "ping"; - - @Override - protected void setUp() throws Exception { - KeyEncodingTest.installSunEcSecurityProviderIfNecessary(); - super.setUp(); - } - - public void testHandshakeWithPayload() throws Exception { - - if (KeyEncoding.isLegacyCryptoRequired()) { - // this means we're running on an old SDK, which doesn't support the - // necessary crypto. Let's not test anything in this case. - return; - } - - // initiator: - D2DHandshakeContext initiatorHandshakeContext = - D2DDiffieHellmanKeyExchangeHandshake.forInitiator(); - assertFalse(initiatorHandshakeContext.canSendPayloadInHandshakeMessage()); - assertFalse(initiatorHandshakeContext.isHandshakeComplete()); - byte[] initiatorHello = initiatorHandshakeContext.getNextHandshakeMessage(); - assertFalse(initiatorHandshakeContext.isHandshakeComplete()); - // (send initiatorHello to responder) - - // responder: - D2DHandshakeContext responderHandshakeContext = - D2DDiffieHellmanKeyExchangeHandshake.forResponder(); - byte[] payload = responderHandshakeContext.parseHandshakeMessage(initiatorHello); - assertEquals(0, payload.length); - assertTrue(responderHandshakeContext.canSendPayloadInHandshakeMessage()); - assertFalse(responderHandshakeContext.isHandshakeComplete()); - byte[] responderHelloAndPayload = responderHandshakeContext.getNextHandshakeMessage( - RESPONDER_HELLO_MESSAGE); - assertTrue(responderHandshakeContext.isHandshakeComplete()); - D2DConnectionContext responderCtx = responderHandshakeContext.toConnectionContext(); - // (send responderHelloAndPayload to initiator) - - // initiator - byte[] messageFromPayload = - initiatorHandshakeContext.parseHandshakeMessage(responderHelloAndPayload); - Assert.assertArrayEquals(RESPONDER_HELLO_MESSAGE, messageFromPayload); - assertTrue(initiatorHandshakeContext.isHandshakeComplete()); - D2DConnectionContextV1 initiatorCtx = - (D2DConnectionContextV1) initiatorHandshakeContext.toConnectionContext(); - - // Test that that initiator and responder contexts are initialized correctly. - checkInitializedConnectionContexts(initiatorCtx, responderCtx); - } - - public void testHandshakeWithoutPayload() throws Exception { - - if (KeyEncoding.isLegacyCryptoRequired()) { - // this means we're running on an old SDK, which doesn't support the - // necessary crypto. Let's not test anything in this case. - return; - } - - // initiator: - D2DHandshakeContext initiatorHandshakeContext = - D2DDiffieHellmanKeyExchangeHandshake.forInitiator(); - byte[] initiatorHello = initiatorHandshakeContext.getNextHandshakeMessage(); - // (send initiatorHello to responder) - - // responder: - D2DHandshakeContext responderHandshakeContext = - D2DDiffieHellmanKeyExchangeHandshake.forResponder(); - responderHandshakeContext.parseHandshakeMessage(initiatorHello); - byte[] responderHelloAndPayload = responderHandshakeContext.getNextHandshakeMessage(); - assertTrue(responderHandshakeContext.isHandshakeComplete()); - D2DConnectionContext responderCtx = responderHandshakeContext.toConnectionContext(); - // (send responderHelloAndPayload to initiator) - - // initiator - byte[] messageFromPayload = - initiatorHandshakeContext.parseHandshakeMessage(responderHelloAndPayload); - assertEquals(0, messageFromPayload.length); - assertTrue(initiatorHandshakeContext.isHandshakeComplete()); - D2DConnectionContext initiatorCtx = initiatorHandshakeContext.toConnectionContext(); - - // Test that that initiator and responder contexts are initialized correctly. - checkInitializedConnectionContexts(initiatorCtx, responderCtx); - } - - public void testErrorWhenInitiatorOrResponderSendTwice() throws Exception { - - if (KeyEncoding.isLegacyCryptoRequired()) { - // this means we're running on an old SDK, which doesn't support the - // necessary crypto. Let's not test anything in this case. - return; - } - - // initiator: - D2DHandshakeContext initiatorHandshakeContext = - D2DDiffieHellmanKeyExchangeHandshake.forInitiator(); - byte[] initiatorHello = initiatorHandshakeContext.getNextHandshakeMessage(); - try { - initiatorHandshakeContext.getNextHandshakeMessage(); - fail("Expected error as initiator has no more initiator messages to send"); - } catch (HandshakeException expected) { - assertTrue(expected.getMessage().contains("Cannot get next message")); - } - // (send initiatorHello to responder) - - // responder: - D2DHandshakeContext responderHandshakeContext = - D2DDiffieHellmanKeyExchangeHandshake.forResponder(); - responderHandshakeContext.parseHandshakeMessage(initiatorHello); - responderHandshakeContext.getNextHandshakeMessage(); - try { - responderHandshakeContext.getNextHandshakeMessage(); - fail("Expected error as initiator has no more responder messages to send"); - } catch (HandshakeException expected) { - assertTrue(expected.getMessage().contains("Cannot get")); - } - } - - public void testInitiatorOrResponderFailOnEmptyMessage() throws Exception { - - if (KeyEncoding.isLegacyCryptoRequired()) { - // this means we're running on an old SDK, which doesn't support the - // necessary crypto. Let's not test anything in this case. - return; - } - - D2DHandshakeContext handshakeContext = - D2DDiffieHellmanKeyExchangeHandshake.forInitiator(); - try { - handshakeContext.parseHandshakeMessage(null); - fail("Expected to crash on null message"); - } catch (HandshakeException expected) { - assertTrue(expected.getMessage().contains("short")); - } - try { - handshakeContext.parseHandshakeMessage(new byte[0]); - fail("Expected to crash on empty message"); - } catch (HandshakeException expected) { - assertTrue(expected.getMessage().contains("short")); - } - } - - public void testPrematureConversionToConnection() throws Exception { - if (KeyEncoding.isLegacyCryptoRequired()) { - // this means we're running on an old SDK, which doesn't support the - // necessary crypto. Let's not test anything in this case. - return; - } - - // initiator: - D2DHandshakeContext initiatorHandshakeContext = - D2DDiffieHellmanKeyExchangeHandshake.forInitiator(); - try { - initiatorHandshakeContext.toConnectionContext(); - fail("Expected to crash: initiator hasn't done anything to deserve full connection"); - } catch (HandshakeException expected) { - assertTrue(expected.getMessage().contains("not complete")); - } - - byte[] initiatorHello = initiatorHandshakeContext.getNextHandshakeMessage(); - try { - initiatorHandshakeContext.toConnectionContext(); - fail("Expected to crash: initiator hasn't yet received responder's key"); - } catch (HandshakeException expected) { - assertTrue(expected.getMessage().contains("not complete")); - } - // (send initiatorHello to responder) - - // responder: - D2DHandshakeContext responderHandshakeContext = - D2DDiffieHellmanKeyExchangeHandshake.forResponder(); - responderHandshakeContext.parseHandshakeMessage(initiatorHello); - try { - initiatorHandshakeContext.toConnectionContext(); - fail("Expected to crash: responder hasn't yet send their key"); - } catch (HandshakeException expected) { - assertTrue(expected.getMessage().contains("not complete")); - } - } - - public void testCannotReuseHandshakeContext() throws Exception { - - if (KeyEncoding.isLegacyCryptoRequired()) { - // this means we're running on an old SDK, which doesn't support the - // necessary crypto. Let's not test anything in this case. - return; - } - - // initiator: - D2DHandshakeContext initiatorHandshakeContext = - D2DDiffieHellmanKeyExchangeHandshake.forInitiator(); - byte[] initiatorHello = initiatorHandshakeContext.getNextHandshakeMessage(); - // (send initiatorHello to responder) - - // responder: - D2DHandshakeContext responderHandshakeContext = - D2DDiffieHellmanKeyExchangeHandshake.forResponder(); - responderHandshakeContext.parseHandshakeMessage(initiatorHello); - byte[] responderHelloAndPayload = responderHandshakeContext.getNextHandshakeMessage(); - D2DConnectionContext responderCtx = responderHandshakeContext.toConnectionContext(); - // (send responderHelloAndPayload to initiator) - - // initiator - initiatorHandshakeContext.parseHandshakeMessage(responderHelloAndPayload); - D2DConnectionContext initiatorCtx = initiatorHandshakeContext.toConnectionContext(); - - // Test that that initiator and responder contexts are initialized correctly. - checkInitializedConnectionContexts(initiatorCtx, responderCtx); - - // Try to get another full context - try { - initiatorHandshakeContext.toConnectionContext(); - fail("Expected crash: initiator context has already been used"); - } catch (HandshakeException expected) { - assertTrue(expected.getMessage().contains("used")); - } - try { - responderHandshakeContext.toConnectionContext(); - fail("Expected crash: responder context has already been used"); - } catch (HandshakeException expected) { - assertTrue(expected.getMessage().contains("used")); - } - } - - public void testErrorWhenInitiatorEchosResponderHello() throws Exception { - if (KeyEncoding.isLegacyCryptoRequired()) { - // this means we're running on an old SDK, which doesn't support the - // necessary crypto. Let's not test anything in this case. - return; - } - - // Initiator echoing back responder's first packet: - D2DDiffieHellmanKeyExchangeHandshake partialInitiatorContext = - D2DDiffieHellmanKeyExchangeHandshake.forInitiator(); - byte[] initiatorHello = partialInitiatorContext.getNextHandshakeMessage(); - - D2DDiffieHellmanKeyExchangeHandshake partialResponderCtx = - D2DDiffieHellmanKeyExchangeHandshake.forResponder(); - partialResponderCtx.parseHandshakeMessage(initiatorHello); - byte[] responderHelloAndPayload = - partialResponderCtx.getNextHandshakeMessage(RESPONDER_HELLO_MESSAGE); - D2DConnectionContext responderCtx = partialResponderCtx.toConnectionContext(); - - try { - // initiator sends responderHelloAndPayload to responder - responderCtx.decodeMessageFromPeerAsString(responderHelloAndPayload); - fail("expected exception, but didn't get it"); - } catch (SignatureException expected) { - assertTrue(expected.getMessage().contains("Signature failed verification")); - } - } - - public void testErrorWhenInitiatorResendsMessage() throws Exception { - if (KeyEncoding.isLegacyCryptoRequired()) { - // this means we're running on an old SDK, which doesn't support the - // necessary crypto. Let's not test anything in this case. - return; - } - - // Initiator repeating the same packet twice - D2DDiffieHellmanKeyExchangeHandshake partialInitiatorContext = - D2DDiffieHellmanKeyExchangeHandshake.forInitiator(); - byte[] initiatorHello = partialInitiatorContext.getNextHandshakeMessage(); - - D2DDiffieHellmanKeyExchangeHandshake partialResponderCtx = - D2DDiffieHellmanKeyExchangeHandshake.forResponder(); - partialResponderCtx.parseHandshakeMessage(initiatorHello); - byte[] responderHelloAndPayload = - partialResponderCtx.getNextHandshakeMessage(RESPONDER_HELLO_MESSAGE); - D2DConnectionContext responderCtx = partialResponderCtx.toConnectionContext(); - - partialInitiatorContext.parseHandshakeMessage(responderHelloAndPayload); - D2DConnectionContext initiatorCtx = partialInitiatorContext.toConnectionContext(); - - byte[] pingMessage = initiatorCtx.encodeMessageToPeer(PING); - assertEquals(PING, responderCtx.decodeMessageFromPeerAsString(pingMessage)); - - try { - // send pingMessage to responder again - responderCtx.decodeMessageFromPeerAsString(pingMessage); - fail("expected exception, but didn't get it"); - } catch (SignatureException expected) { - assertTrue(expected.getMessage().contains("sequence")); - } - } - - public void testErrorWhenResponderResendsFirstMessage() throws Exception { - if (KeyEncoding.isLegacyCryptoRequired()) { - // this means we're running on an old SDK, which doesn't support the - // necessary crypto. Let's not test anything in this case. - return; - } - - D2DDiffieHellmanKeyExchangeHandshake partialInitiatorContext = - D2DDiffieHellmanKeyExchangeHandshake.forInitiator(); - byte[] initiatorHello = partialInitiatorContext.getNextHandshakeMessage(); - - D2DDiffieHellmanKeyExchangeHandshake partialResponderCtx = - D2DDiffieHellmanKeyExchangeHandshake.forResponder(); - partialResponderCtx.parseHandshakeMessage(initiatorHello); - byte[] responderHelloAndPayload = - partialResponderCtx.getNextHandshakeMessage(RESPONDER_HELLO_MESSAGE); - - partialInitiatorContext.parseHandshakeMessage(responderHelloAndPayload); - D2DConnectionContext initiatorCtx = partialInitiatorContext.toConnectionContext(); - - try { - // Send the responderHelloAndPayload again. This time, the initiator will - // process it as a normal message. - initiatorCtx.decodeMessageFromPeerAsString(responderHelloAndPayload); - fail("expected exception, but didn't get it"); - } catch (SignatureException expected) { - assertTrue(expected.getMessage().contains("wrong message type")); - } - } - - public void testHandshakeWithInitiatorV1AndResponderV0() throws Exception { - if (KeyEncoding.isLegacyCryptoRequired()) { - // this means we're running on an old SDK, which doesn't support the - // necessary crypto. Let's not test anything in this case. - return; - } - - // Initialize initiator side. - D2DHandshakeContext initiatorHandshakeContext = - D2DDiffieHellmanKeyExchangeHandshake.forInitiator(); - byte[] initiatorHello = initiatorHandshakeContext.getNextHandshakeMessage(); - - // Set up keys used by the responder. - PublicKey initiatorPublicKey = PublicKeyProtoUtil.parsePublicKey( - InitiatorHello.parseFrom(initiatorHello).getPublicDhKey()); - KeyPair responderKeyPair = PublicKeyProtoUtil.generateEcP256KeyPair(); - SecretKey sharedKey = - EnrollmentCryptoOps.doKeyAgreement(responderKeyPair.getPrivate(), initiatorPublicKey); - - // Construct a responder hello message without the version field, whose payload is encrypted - // with the shared key. - byte[] responderHello = D2DCryptoOps.signcryptPayload( - new Payload( - PayloadType.DEVICE_TO_DEVICE_RESPONDER_HELLO_PAYLOAD, - D2DConnectionContext.createDeviceToDeviceMessage(new byte[] {}, 1).toByteArray()), - sharedKey, - ResponderHello.newBuilder() - .setPublicDhKey( - PublicKeyProtoUtil.encodePublicKey(responderKeyPair.getPublic())) - .build().toByteArray()); - - // Handle V0 responder hello message. - initiatorHandshakeContext.parseHandshakeMessage(responderHello); - D2DConnectionContext initiatorCtx = initiatorHandshakeContext.toConnectionContext(); - - assertEquals(D2DConnectionContextV0.PROTOCOL_VERSION, initiatorCtx.getProtocolVersion()); - assertEquals(1, initiatorCtx.getSequenceNumberForEncoding()); - assertEquals(1, initiatorCtx.getSequenceNumberForDecoding()); - } - - public void testHandshakeWithInitiatorV0AndResponderV1() throws Exception { - if (KeyEncoding.isLegacyCryptoRequired()) { - // this means we're running on an old SDK, which doesn't support the - // necessary crypto. Let's not test anything in this case. - return; - } - - // Construct an initiator hello message without the version field. - byte[] initiatorHello = InitiatorHello.newBuilder() - .setPublicDhKey(PublicKeyProtoUtil.encodePublicKey( - PublicKeyProtoUtil.generateEcP256KeyPair().getPublic())) - .build() - .toByteArray(); - - // Handle V0 initiator hello message. - D2DHandshakeContext responderHandshakeContext = - D2DDiffieHellmanKeyExchangeHandshake.forResponder(); - responderHandshakeContext.parseHandshakeMessage(initiatorHello); - responderHandshakeContext.getNextHandshakeMessage(); - D2DConnectionContext responderCtx = responderHandshakeContext.toConnectionContext(); - - assertEquals(D2DConnectionContextV0.PROTOCOL_VERSION, responderCtx.getProtocolVersion()); - assertEquals(1, responderCtx.getSequenceNumberForEncoding()); - assertEquals(1, responderCtx.getSequenceNumberForDecoding()); - } - - private void checkInitializedConnectionContexts( - D2DConnectionContext initiatorCtx, D2DConnectionContext responderCtx) { - assertNotNull(initiatorCtx); - assertNotNull(responderCtx); - assertEquals(D2DConnectionContextV1.PROTOCOL_VERSION, initiatorCtx.getProtocolVersion()); - assertEquals(D2DConnectionContextV1.PROTOCOL_VERSION, responderCtx.getProtocolVersion()); - assertEquals(initiatorCtx.getEncodeKey(), responderCtx.getDecodeKey()); - assertEquals(initiatorCtx.getDecodeKey(), responderCtx.getEncodeKey()); - assertEquals(0, initiatorCtx.getSequenceNumberForEncoding()); - assertEquals(1, initiatorCtx.getSequenceNumberForDecoding()); - assertEquals(1, responderCtx.getSequenceNumberForEncoding()); - assertEquals(0, responderCtx.getSequenceNumberForDecoding()); - } -} diff --git a/src/main/javatest/com/google/security/cryptauth/lib/securegcm/Ed25519Test.java b/src/main/javatest/com/google/security/cryptauth/lib/securegcm/Ed25519Test.java deleted file mode 100644 index 6ae95d8..0000000 --- a/src/main/javatest/com/google/security/cryptauth/lib/securegcm/Ed25519Test.java +++ /dev/null @@ -1,195 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.security.cryptauth.lib.securegcm; - -import static org.hamcrest.CoreMatchers.containsString; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertThat; - -import com.google.security.cryptauth.lib.securegcm.Ed25519.Ed25519Exception; -import java.math.BigInteger; -import junit.framework.TestCase; - -/** - * Android compatible tests for the {@link Ed25519} class. - */ -public class Ed25519Test extends TestCase { - - // Points on the curve - private static final int HEX_RADIX = 16; - private static final BigInteger[] KM = new BigInteger[] { - new BigInteger("1981FB43F103290ECF9772022DB8B19BFAF389057ED91E8486EB368763435925", HEX_RADIX), - new BigInteger("A714C34F3B588AAC92FD2587884A20964FD351A1F147D5C4BBF5C2F37A77C36", HEX_RADIX)}; - private static final BigInteger[] KN = new BigInteger[] { - new BigInteger("201A184F47D9A7973891D148E3D1C864D8084547131C2C1CEFB7EEBD26C63567", HEX_RADIX), - new BigInteger("6DA2D3B18EC4F9AA3B08E39C997CD8BF6E9948FFD4FEFFECAF8DD0B3D648B7E8", HEX_RADIX)}; - - // Curve prime P - private static final BigInteger P = - new BigInteger("7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED", HEX_RADIX); - - // Test vectors obtain by multiplying KM by k by manually using the official implementation - // see: http://ed25519.cr.yp.to/python/ed25519.py - // k = 2 - private static final BigInteger[] KM_2 = new BigInteger[] { - new BigInteger("718079972e63c2d62caf0ee93ec6f00337ceaff4e283181c04c4082b1d5e1ecf", HEX_RADIX), - new BigInteger("143d18d393a8058c8614335bf36bf59364cc7c451db74726b322ce9d0b826d51", HEX_RADIX) - }; - // k = 3 - private static final BigInteger[] KM_3 = new BigInteger[] { - new BigInteger("39DA3C92EFC0577586B4D58F4A5C0BF65A6CC8F6BF358F38D70B2E6C28A31E8E", HEX_RADIX), - new BigInteger("6D194F054B3FC2BE217F6A360BBEC747D2937FCEBD74B67FC3B20ED638ADD670", HEX_RADIX) - }; - // k = 317698 - private static final BigInteger[] KM_317698 = new BigInteger[] { - new BigInteger("7945D0ADEB568B16495476E81ADF281F4515439AE835914FBF6CEEAFEB9CD7E8", HEX_RADIX), - new BigInteger("3631503DCDEBC0BF9BB1FFC3984A8CB52A34FFC2E77E9C19FD896DC6EE64A530", HEX_RADIX) - }; - // k = P - private static final BigInteger[] KM_HUGE = new BigInteger[] { - new BigInteger("530162B05F440E00E219DFD3188524821C860C41FD87B9AC6AF2A283FDD585A1", HEX_RADIX), - new BigInteger("48385A7D2BB858F3DB7F72E7CDFE218B9CA84DDA8BD64C3775AA43551D974F60", HEX_RADIX) - }; - // k = P + 10000 - private static final BigInteger[] KM_XRAHUGE = new BigInteger[] { - new BigInteger("16377E9F5EE2C0F4C70E17AC298EF670700A7CB186EEB0DA10CDD59635000AF8", HEX_RADIX), - new BigInteger("5BD7921EEE662ACBAC3A96D8B6039D2356F154859FAF41FD2F0D99DF06CD2EAE", HEX_RADIX) - }; - - // Helpful constants - private static final BigInteger ONE = BigInteger.ONE; - private static final BigInteger ZERO = BigInteger.ZERO; - - // Identity element of the group (the zero) in affine and extended representations - private static final BigInteger[] ID = new BigInteger[] {ZERO, ONE}; - private static final BigInteger[] ID_EX = new BigInteger[] {ZERO, ONE, ONE, ZERO}; - - public void testValidPoints() throws Exception { - if (KeyEncoding.isLegacyCryptoRequired()) { - // this means we're running on an old SDK, which doesn't support the - // necessary crypto. Let's not test anything in this case. - return; - } - - // We've got a couple of valid points - Ed25519.validateAffinePoint(KM); - Ed25519.validateAffinePoint(KN); - - // And a bunch of invalid ones - try { - Ed25519.validateAffinePoint(new BigInteger[] {ZERO, ONE}); - fail("Validate point not catching zero x coordinates"); - } catch (Ed25519Exception e) { - assertThat(e.getMessage(), containsString("positive")); - } - - try { - Ed25519.validateAffinePoint(new BigInteger[] {ONE, ZERO}); - fail("Validate point not catching zero y coordinates"); - } catch (Ed25519Exception e) { - assertThat(e.getMessage(), containsString("positive")); - } - - try { - Ed25519.validateAffinePoint(new BigInteger[] {new BigInteger("-1"), ONE}); - fail("Validate point not catching negative x coordinates"); - } catch (Ed25519Exception e) { - assertThat(e.getMessage(), containsString("positive")); - } - - try { - Ed25519.validateAffinePoint(new BigInteger[] {ONE, new BigInteger("-1")}); - fail("Validate point not catching negative y coordinates"); - } catch (Ed25519Exception e) { - assertThat(e.getMessage(), containsString("positive")); - } - - try { - Ed25519.validateAffinePoint(new BigInteger[] {ONE, ONE}); - fail("Validate point not catching points that are not on curve"); - } catch (Ed25519Exception e) { - assertThat(e.getMessage(), containsString("expected curve")); - } - } - - public void testAffineExtendedConversion() throws Exception { - BigInteger[] km1 = Ed25519.toAffine(Ed25519.toExtended(KM)); - BigInteger[] kn1 = Ed25519.toAffine(Ed25519.toExtended(KN)); - - assertArrayEquals(KM, km1); - assertArrayEquals(KN, kn1); - - assertArrayEquals(ID, Ed25519.toAffine(ID_EX)); - assertArrayEquals(ID_EX, Ed25519.toExtended(ID)); - } - - public void testRepresentationCheck() throws Exception { - Ed25519.checkPointIsInAffineRepresentation(KM); - Ed25519.checkPointIsInExtendedRepresentation(ID_EX); - - try { - Ed25519.checkPointIsInExtendedRepresentation(KM); - fail("Point is not really in extended representation, expected failure"); - } catch (Ed25519Exception e) { - assertThat(e.getMessage(), containsString("not in extended")); - } - - try { - Ed25519.checkPointIsInAffineRepresentation(Ed25519.toExtended(KM)); - fail("Point is not really in affine representation, expected failure"); - } catch (Ed25519Exception e) { - assertThat(e.getMessage(), containsString("not in affine")); - } - } - - public void testAddSubtractExtendedPoints() throws Exception { - // Adding/subtracting identity to/from itself should yield the identity point - assertArrayEquals(ID, Ed25519.addAffinePoints(ID, ID)); - assertArrayEquals(ID, Ed25519.subtractAffinePoints(ID, ID)); - - // In fact adding/subtracting the identity point to/from any point should yield that point - assertArrayEquals(KM, Ed25519.addAffinePoints(KM, ID)); - assertArrayEquals(KM, Ed25519.subtractAffinePoints(KM, ID)); - - // Subtracting a point from itself should yield the identity element - assertArrayEquals(ID, Ed25519.subtractAffinePoints(KM, KM)); - assertArrayEquals(ID, Ed25519.subtractAffinePoints(KN, KN)); - - // Adding and subtracting should yield the same point - assertArrayEquals(KM, Ed25519.subtractAffinePoints(Ed25519.addAffinePoints(KM, KN), KN)); - assertArrayEquals(KN, Ed25519.subtractAffinePoints(Ed25519.addAffinePoints(KN, KM), KM)); - } - - public void testScalarMultiplyExtendedPoints() throws Exception { - // A point times one is the point itself - assertArrayEquals(KM, Ed25519.scalarMultiplyAffinePoint(KM, ONE)); - assertArrayEquals(KN, Ed25519.scalarMultiplyAffinePoint(KN, ONE)); - - // A point times zero is the identity point - assertArrayEquals(ID, Ed25519.scalarMultiplyAffinePoint(KM, ZERO)); - assertArrayEquals(ID, Ed25519.scalarMultiplyAffinePoint(KN, ZERO)); - - // The identity times a scalar is the identity - assertArrayEquals(ID, Ed25519.scalarMultiplyAffinePoint(ID, BigInteger.valueOf(317698))); - - // Use test vectors - assertArrayEquals(KM_2, Ed25519.scalarMultiplyAffinePoint(KM, BigInteger.valueOf(2))); - assertArrayEquals(KM_3, Ed25519.scalarMultiplyAffinePoint(KM, BigInteger.valueOf(3))); - assertArrayEquals(KM_317698, Ed25519.scalarMultiplyAffinePoint(KM, BigInteger.valueOf(317698))); - assertArrayEquals(KM_HUGE, Ed25519.scalarMultiplyAffinePoint(KM, P)); - assertArrayEquals(KM_XRAHUGE, - Ed25519.scalarMultiplyAffinePoint(KM, P.add(BigInteger.valueOf(10000)))); - } -} diff --git a/src/main/javatest/com/google/security/cryptauth/lib/securegcm/EnrollmentCryptoOpsTest.java b/src/main/javatest/com/google/security/cryptauth/lib/securegcm/EnrollmentCryptoOpsTest.java deleted file mode 100644 index 4437045..0000000 --- a/src/main/javatest/com/google/security/cryptauth/lib/securegcm/EnrollmentCryptoOpsTest.java +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.security.cryptauth.lib.securegcm; - -import com.google.protobuf.ByteString; -import com.google.security.cryptauth.lib.securegcm.SecureGcmProto.GcmDeviceInfo; -import com.google.security.cryptauth.lib.securemessage.PublicKeyProtoUtil; -import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.GenericPublicKey; -import java.security.KeyPair; -import java.security.PublicKey; -import java.util.Arrays; -import javax.crypto.SecretKey; -import junit.framework.TestCase; - -/** - * Android compatible tests for the {@link EnrollmentCryptoOps} class. - */ -public class EnrollmentCryptoOpsTest extends TestCase { - - private static final long DEVICE_ID = 1234567890L; - private static final byte[] GCM_REGISTRATION_ID = { -0x80, 0, -0x80, 0, -0x80, 0 }; - private static final String DEVICE_MODEL = "TEST DEVICE"; - private static final String LOCALE = "en"; - private static final byte[] SESSION_ID = { 5, 5, 4, 4, 3, 3, 2, 2, 1, 1 }; - private static final String OAUTH_TOKEN = "1/23456etc"; - - @Override - protected void setUp() throws Exception { - KeyEncodingTest.installSunEcSecurityProviderIfNecessary(); - assertEquals( - PublicKeyProtoUtil.isLegacyCryptoRequired(), KeyEncoding.isLegacyCryptoRequired()); - super.setUp(); - } - - @Override - protected void tearDown() throws Exception { - KeyEncoding.setSimulateLegacyCrypto(false); - super.tearDown(); - } - - - public void testSimulatedEnrollment() throws Exception { - boolean isLegacy = KeyEncoding.isLegacyCryptoRequired(); - // Step 1: Server generates an ephemeral DH key pair, saves the private key, and sends - // the public key to the client as server_ephemeral_key. - KeyPair serverEphemeralKeyPair = - EnrollmentCryptoOps.generateEnrollmentKeyAgreementKeyPair(isLegacy); - byte[] savedServerPrivateKey = - KeyEncoding.encodeKeyAgreementPrivateKey(serverEphemeralKeyPair.getPrivate()); - byte[] serverEphemeralKey = KeyEncoding.encodeKeyAgreementPublicKey( - serverEphemeralKeyPair.getPublic()); - - // Step 2a: Client generates an ephemeral DH key pair, and completes the DH key exchange - // to derive the master key. - KeyPair clientEphemeralKeyPair = - EnrollmentCryptoOps.generateEnrollmentKeyAgreementKeyPair(isLegacy); - byte[] clientEphemeralKey = KeyEncoding.encodeKeyAgreementPublicKey( - clientEphemeralKeyPair.getPublic()); - SecretKey clientMasterKey = EnrollmentCryptoOps.doKeyAgreement( - clientEphemeralKeyPair.getPrivate(), - KeyEncoding.parseKeyAgreementPublicKey(serverEphemeralKey)); - - // Step 2b: Client generates its user key pair, and fills in a GcmDeviceInfo message containing - // the enrollment request (which includes the user public key). - KeyPair userKeyPair = isLegacy ? PublicKeyProtoUtil.generateRSA2048KeyPair() - : PublicKeyProtoUtil.generateEcP256KeyPair(); - GcmDeviceInfo clientInfo = createGcmDeviceInfo(userKeyPair.getPublic(), clientMasterKey); - - // Step 2c: Client signcrypts the enrollment request to the server, using a combination of the - // master key and its user signing key. - byte[] enrollmentMessage = EnrollmentCryptoOps.encryptEnrollmentMessage( - clientInfo, clientMasterKey, userKeyPair.getPrivate()); - - - // Step 3a: Server receives the client's DH public key and completes the key exchange using - // the saved DH private key. - SecretKey serverMasterKey = EnrollmentCryptoOps.doKeyAgreement( - KeyEncoding.parseKeyAgreementPrivateKey(savedServerPrivateKey, isLegacy), - KeyEncoding.parseKeyAgreementPublicKey(clientEphemeralKey)); - - // Step 3b: Server uses the exchanged master key to de-signcrypt the enrollment request - // (which also provides the user public key in the clear). - GcmDeviceInfo serverInfo = EnrollmentCryptoOps.decryptEnrollmentMessage( - enrollmentMessage, serverMasterKey, isLegacy); - - // Verify that the server sees the client's original enrollment request - assertTrue(Arrays.equals(clientInfo.toByteArray(), serverInfo.toByteArray())); - - // Confirm that the server can recover a valid user PublicKey from the enrollment - PublicKey serverUserPublicKey = KeyEncoding.parseUserPublicKey( - serverInfo.getUserPublicKey().toByteArray()); - assertTrue(serverUserPublicKey.equals(userKeyPair.getPublic())); - } - - public void testSimulatedEnrollmentWithForcedLegacy() throws Exception { - if (PublicKeyProtoUtil.isLegacyCryptoRequired()) { - // We already test with legacy in this case - return; - } - KeyEncoding.setSimulateLegacyCrypto(true); - testSimulatedEnrollment(); - } - - private GcmDeviceInfo createGcmDeviceInfo(PublicKey userPublicKey, SecretKey masterKey) { - // One possible method of generating a key handle: - GenericPublicKey encodedUserPublicKey = PublicKeyProtoUtil.encodePublicKey(userPublicKey); - byte[] keyHandle = EnrollmentCryptoOps.sha256(encodedUserPublicKey.toByteArray()); - - return GcmDeviceInfo.newBuilder() - .setAndroidDeviceId(DEVICE_ID) - .setGcmRegistrationId(ByteString.copyFrom(GCM_REGISTRATION_ID)) - .setDeviceMasterKeyHash( - ByteString.copyFrom(EnrollmentCryptoOps.getMasterKeyHash(masterKey))) - .setUserPublicKey(ByteString.copyFrom(KeyEncoding.encodeUserPublicKey(userPublicKey))) - .setDeviceModel(DEVICE_MODEL) - .setLocale(LOCALE) - .setKeyHandle(ByteString.copyFrom(keyHandle)) - .setEnrollmentSessionId(ByteString.copyFrom(SESSION_ID)) - .setOauthToken(OAUTH_TOKEN) - .build(); - } -} diff --git a/src/main/javatest/com/google/security/cryptauth/lib/securegcm/KeyEncodingTest.java b/src/main/javatest/com/google/security/cryptauth/lib/securegcm/KeyEncodingTest.java deleted file mode 100644 index 7012eae..0000000 --- a/src/main/javatest/com/google/security/cryptauth/lib/securegcm/KeyEncodingTest.java +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.security.cryptauth.lib.securegcm; - -import com.google.security.cryptauth.lib.securemessage.PublicKeyProtoUtil; -import java.security.Key; -import java.security.KeyPair; -import java.security.PrivateKey; -import java.security.Provider; -import java.security.PublicKey; -import java.security.Security; -import java.security.interfaces.ECPrivateKey; -import java.security.interfaces.ECPublicKey; -import java.security.interfaces.RSAPrivateKey; -import java.security.interfaces.RSAPublicKey; -import java.security.spec.InvalidKeySpecException; -import java.util.Arrays; -import javax.crypto.interfaces.DHPrivateKey; -import javax.crypto.interfaces.DHPublicKey; -import junit.framework.TestCase; - -/** - * Android compatible tests for the {@link KeyEncoding} class. - */ -public class KeyEncodingTest extends TestCase { - private static final byte[] RAW_KEY_BYTES = { - 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, - 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, - 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, - 1, 2}; - - private Boolean isLegacy; - private KeyPair userKeyPair; - - @Override - protected void setUp() throws Exception { - installSunEcSecurityProviderIfNecessary(); - isLegacy = PublicKeyProtoUtil.isLegacyCryptoRequired(); - setUserKeyPair(); - super.setUp(); - } - - @Override - protected void tearDown() throws Exception { - KeyEncoding.setSimulateLegacyCrypto(false); - isLegacy = PublicKeyProtoUtil.isLegacyCryptoRequired(); - super.tearDown(); - } - - private void setUserKeyPair() { - userKeyPair = isLegacy ? PublicKeyProtoUtil.generateRSA2048KeyPair() - : PublicKeyProtoUtil.generateEcP256KeyPair(); - } - - public void testSimulateLegacyCrypto() { - if (isLegacy) { - return; // Nothing to test if we are already stuck in a legacy platform - } - assertFalse(KeyEncoding.isLegacyCryptoRequired()); - KeyEncoding.setSimulateLegacyCrypto(true); - assertTrue(KeyEncoding.isLegacyCryptoRequired()); - } - - public void testMasterKeyEncoding() { - // Require that master keys are encoded/decoded as raw byte arrays - assertTrue(Arrays.equals( - RAW_KEY_BYTES, - KeyEncoding.encodeMasterKey(KeyEncoding.parseMasterKey(RAW_KEY_BYTES)))); - } - - public void testUserPublicKeyEncoding() throws InvalidKeySpecException { - PublicKey pk = userKeyPair.getPublic(); - byte[] encodedPk = KeyEncoding.encodeUserPublicKey(pk); - PublicKey decodedPk = KeyEncoding.parseUserPublicKey(encodedPk); - assertKeysEqual(pk, decodedPk); - } - - public void testUserPrivateKeyEncoding() throws InvalidKeySpecException { - PrivateKey sk = userKeyPair.getPrivate(); - byte[] encodedSk = KeyEncoding.encodeUserPrivateKey(sk); - PrivateKey decodedSk = KeyEncoding.parseUserPrivateKey(encodedSk, isLegacy); - assertKeysEqual(sk, decodedSk); - } - - public void testKeyAgreementPublicKeyEncoding() throws InvalidKeySpecException { - KeyPair clientKeyPair = EnrollmentCryptoOps.generateEnrollmentKeyAgreementKeyPair(isLegacy); - PublicKey pk = clientKeyPair.getPublic(); - byte[] encodedPk = KeyEncoding.encodeKeyAgreementPublicKey(pk); - PublicKey decodedPk = KeyEncoding.parseKeyAgreementPublicKey(encodedPk); - assertKeysEqual(pk, decodedPk); - } - - public void testKeyAgreementPrivateKeyEncoding() throws InvalidKeySpecException { - KeyPair clientKeyPair = EnrollmentCryptoOps.generateEnrollmentKeyAgreementKeyPair(isLegacy); - PrivateKey sk = clientKeyPair.getPrivate(); - byte[] encodedSk = KeyEncoding.encodeKeyAgreementPrivateKey(sk); - PrivateKey decodedSk = KeyEncoding.parseKeyAgreementPrivateKey(encodedSk, isLegacy); - assertKeysEqual(sk, decodedSk); - } - - public void testEncodingsWithForcedLegacy() throws InvalidKeySpecException { - if (PublicKeyProtoUtil.isLegacyCryptoRequired()) { - // We already test with legacy in this case - return; - } - KeyEncoding.setSimulateLegacyCrypto(true); - isLegacy = true; - setUserKeyPair(); - testUserPublicKeyEncoding(); - testUserPrivateKeyEncoding(); - testKeyAgreementPublicKeyEncoding(); - testKeyAgreementPrivateKeyEncoding(); - } - - public void testSigningPublicKeyEncoding() throws InvalidKeySpecException { - KeyPair keyPair = PublicKeyProtoUtil.generateEcP256KeyPair(); - PublicKey pk = keyPair.getPublic(); - byte[] encodedPk = KeyEncoding.encodeSigningPublicKey(pk); - PublicKey decodedPk = KeyEncoding.parseSigningPublicKey(encodedPk); - assertKeysEqual(pk, decodedPk); - } - - public void testSigningPrivateKeyEncoding() throws InvalidKeySpecException { - KeyPair keyPair = PublicKeyProtoUtil.generateEcP256KeyPair(); - PrivateKey sk = keyPair.getPrivate(); - byte[] encodedSk = KeyEncoding.encodeSigningPrivateKey(sk); - PrivateKey decodedSk = KeyEncoding.parseSigningPrivateKey(encodedSk); - assertKeysEqual(sk, decodedSk); - } - - public void testDeviceSyncPublicKeyEncoding() throws InvalidKeySpecException { - KeyPair keyPair = PublicKeyProtoUtil.generateEcP256KeyPair(); - PublicKey pk = keyPair.getPublic(); - byte[] encodedPk = KeyEncoding.encodeDeviceSyncGroupPublicKey(pk); - PublicKey decodedPk = KeyEncoding.parseDeviceSyncGroupPublicKey(encodedPk); - assertKeysEqual(pk, decodedPk); - } - - void assertKeysEqual(Key a, Key b) { - if ((a instanceof ECPublicKey) - || (a instanceof ECPrivateKey) - || (a instanceof RSAPublicKey) - || (a instanceof RSAPrivateKey)) { - assertNotNull(a.getEncoded()); - assertTrue(Arrays.equals(a.getEncoded(), b.getEncoded())); - } - if (a instanceof DHPublicKey) { - DHPublicKey ya = (DHPublicKey) a; - DHPublicKey yb = (DHPublicKey) b; - assertEquals(ya.getY(), yb.getY()); - assertEquals(ya.getParams().getG(), yb.getParams().getG()); - assertEquals(ya.getParams().getP(), yb.getParams().getP()); - } - if (a instanceof DHPrivateKey) { - DHPrivateKey xa = (DHPrivateKey) a; - DHPrivateKey xb = (DHPrivateKey) b; - assertEquals(xa.getX(), xb.getX()); - assertEquals(xa.getParams().getG(), xb.getParams().getG()); - assertEquals(xa.getParams().getP(), xb.getParams().getP()); - } - } - - /** - * Registers the SunEC security provider if no EC security providers are currently registered. - */ - // TODO(shabsi): Remove this method when b/7891565 is fixed - static void installSunEcSecurityProviderIfNecessary() { - if (Security.getProviders("KeyPairGenerator.EC") == null) { - try { - Class<?> providerClass = Class.forName("sun.security.ec.SunEC"); - Security.addProvider((Provider) providerClass.newInstance()); - } catch (Exception e) { - // SunEC is not available, nothing we can do - } - } - } -} diff --git a/src/main/javatest/com/google/security/cryptauth/lib/securegcm/TransportCryptoOpsTest.java b/src/main/javatest/com/google/security/cryptauth/lib/securegcm/TransportCryptoOpsTest.java deleted file mode 100644 index 9e45c0a..0000000 --- a/src/main/javatest/com/google/security/cryptauth/lib/securegcm/TransportCryptoOpsTest.java +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.security.cryptauth.lib.securegcm; - -import com.google.security.cryptauth.lib.securegcm.SecureGcmProto.Tickle; -import com.google.security.cryptauth.lib.securegcm.TransportCryptoOps.Payload; -import com.google.security.cryptauth.lib.securegcm.TransportCryptoOps.PayloadType; -import com.google.security.cryptauth.lib.securemessage.PublicKeyProtoUtil; -import java.security.KeyPair; -import java.security.PublicKey; -import java.util.Arrays; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; -import junit.framework.TestCase; - -/** - * Android compatible tests for the {@link TransportCryptoOps} class. - */ -public class TransportCryptoOpsTest extends TestCase { - private static final byte[] KEY_BYTES = { - 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, - 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, - 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, - 1, 2 - }; - private static final byte[] KEY_HANDLE = { 9 }; - - private SecretKey masterKey; - - @Override - protected void setUp() throws Exception { - KeyEncodingTest.installSunEcSecurityProviderIfNecessary(); - masterKey = new SecretKeySpec(KEY_BYTES, "AES"); - super.setUp(); - } - - public void testServerMessage() throws Exception { - long tickleExpiry = 12345L; - Tickle tickle = Tickle.newBuilder() - .setExpiryTime(tickleExpiry) - .build(); - - // Simulate sending a message - byte[] signcryptedMessage = TransportCryptoOps.signcryptServerMessage( - new Payload(PayloadType.TICKLE, tickle.toByteArray()), - masterKey, - KEY_HANDLE); - - // Simulate the process of receiving the message - assertTrue(Arrays.equals(KEY_HANDLE, TransportCryptoOps.getKeyHandleFor(signcryptedMessage))); - Payload received = TransportCryptoOps.verifydecryptServerMessage(signcryptedMessage, masterKey); - assertEquals(PayloadType.TICKLE, received.getPayloadType()); - Tickle receivedTickle = Tickle.parseFrom(received.getMessage()); - assertEquals(tickleExpiry, receivedTickle.getExpiryTime()); - } - - public void testClientMessage() throws Exception { - if (PublicKeyProtoUtil.isLegacyCryptoRequired()) { - return; // This test isn't for legacy crypto - } - KeyPair userKeyPair = PublicKeyProtoUtil.generateEcP256KeyPair(); - doTestClientMessageWith(userKeyPair); - } - - public void testClientMessageWithLegacyCrypto() throws Exception { - KeyPair userKeyPair = PublicKeyProtoUtil.generateRSA2048KeyPair(); - doTestClientMessageWith(userKeyPair); - } - - private void doTestClientMessageWith(KeyPair userKeyPair) throws Exception { - PublicKey userPublicKey = userKeyPair.getPublic(); - // Will use a Tickle for the test message, even though that would normally - // only be sent from the server to the client - long tickleExpiry = 12345L; - Tickle tickle = Tickle.newBuilder() - .setExpiryTime(tickleExpiry) - .build(); - - // Simulate sending a message - byte[] signcryptedMessage = TransportCryptoOps.signcryptClientMessage( - new Payload(PayloadType.TICKLE, tickle.toByteArray()), - userKeyPair, - masterKey); - - // Simulate the process of receiving the message - byte[] encodedUserPublicKey = TransportCryptoOps.getEncodedUserPublicKeyFor(signcryptedMessage); - assertTrue(Arrays.equals(KeyEncoding.encodeUserPublicKey(userPublicKey), encodedUserPublicKey)); - userPublicKey = KeyEncoding.parseUserPublicKey(encodedUserPublicKey); - // At this point the server would have looked up the masterKey for this userPublicKey - - Payload received = TransportCryptoOps.verifydecryptClientMessage( - signcryptedMessage, userPublicKey, masterKey); - - assertEquals(PayloadType.TICKLE, received.getPayloadType()); - Tickle receivedTickle = Tickle.parseFrom(received.getMessage()); - assertEquals(tickleExpiry, receivedTickle.getExpiryTime()); - } -} diff --git a/src/main/javatest/com/google/security/cryptauth/lib/securegcm/Ukey2CppCompatibilityTest.java b/src/main/javatest/com/google/security/cryptauth/lib/securegcm/Ukey2CppCompatibilityTest.java deleted file mode 100644 index db319e0..0000000 --- a/src/main/javatest/com/google/security/cryptauth/lib/securegcm/Ukey2CppCompatibilityTest.java +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.security.cryptauth.lib.securegcm; - -import com.google.security.cryptauth.lib.securegcm.Ukey2Handshake.HandshakeCipher; -import com.google.security.cryptauth.lib.securegcm.Ukey2ShellCppWrapper.Mode; -import java.util.Arrays; -import junit.framework.TestCase; - -/** - * Tests the compatibility between the Java and C++ implementations of the UKEY2 protocol. This - * integration test executes and talks to a compiled binary exposing the C++ implementation (wrapped - * by {@link Ukey2ShellCppWrapper}). - * - * <p>The C++ implementation is located in //security/cryptauth/lib/securegcm. - */ -public class Ukey2CppCompatibilityTest extends TestCase { - private static final int VERIFICATION_STRING_LENGTH = 32; - - private static final byte[] sPayload1 = "payload to encrypt1".getBytes(); - private static final byte[] sPayload2 = "payload to encrypt2".getBytes(); - - /** Tests full handshake with C++ client and Java server. */ - public void testCppClientJavaServer() throws Exception { - Ukey2ShellCppWrapper cppUkey2Shell = - new Ukey2ShellCppWrapper(Mode.INITIATOR, VERIFICATION_STRING_LENGTH); - cppUkey2Shell.startShell(); - Ukey2Handshake javaUkey2Handshake = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512); - - // ClientInit: - byte[] clientInit = cppUkey2Shell.readHandshakeMessage(); - javaUkey2Handshake.parseHandshakeMessage(clientInit); - - // ServerInit: - byte[] serverInit = javaUkey2Handshake.getNextHandshakeMessage(); - cppUkey2Shell.writeHandshakeMessage(serverInit); - - // ClientFinished: - byte[] clientFinished = cppUkey2Shell.readHandshakeMessage(); - javaUkey2Handshake.parseHandshakeMessage(clientFinished); - - // Verification String: - cppUkey2Shell.confirmAuthString( - javaUkey2Handshake.getVerificationString(VERIFICATION_STRING_LENGTH)); - javaUkey2Handshake.verifyHandshake(); - - // Secure channel: - D2DConnectionContext javaSecureContext = javaUkey2Handshake.toConnectionContext(); - - // ukey2_shell encodes data: - byte[] encodedData = cppUkey2Shell.sendEncryptCommand(sPayload1); - byte[] decodedData = javaSecureContext.decodeMessageFromPeer(encodedData); - assertTrue(Arrays.equals(sPayload1, decodedData)); - - // ukey2_shell decodes data: - encodedData = javaSecureContext.encodeMessageToPeer(sPayload2); - decodedData = cppUkey2Shell.sendDecryptCommand(encodedData); - assertTrue(Arrays.equals(sPayload2, decodedData)); - - // ukey2_shell session unique: - byte[] localSessionUnique = javaSecureContext.getSessionUnique(); - byte[] remoteSessionUnique = cppUkey2Shell.sendSessionUniqueCommand(); - assertTrue(Arrays.equals(localSessionUnique, remoteSessionUnique)); - - cppUkey2Shell.stopShell(); - } - - /** Tests full handshake with C++ server and Java client. */ - public void testCppServerJavaClient() throws Exception { - Ukey2ShellCppWrapper cppUkey2Shell = - new Ukey2ShellCppWrapper(Mode.RESPONDER, VERIFICATION_STRING_LENGTH); - cppUkey2Shell.startShell(); - Ukey2Handshake javaUkey2Handshake = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512); - - // ClientInit: - byte[] clientInit = javaUkey2Handshake.getNextHandshakeMessage(); - cppUkey2Shell.writeHandshakeMessage(clientInit); - - // ServerInit: - byte[] serverInit = cppUkey2Shell.readHandshakeMessage(); - javaUkey2Handshake.parseHandshakeMessage(serverInit); - - // ClientFinished: - byte[] clientFinished = javaUkey2Handshake.getNextHandshakeMessage(); - cppUkey2Shell.writeHandshakeMessage(clientFinished); - - // Verification String: - cppUkey2Shell.confirmAuthString( - javaUkey2Handshake.getVerificationString(VERIFICATION_STRING_LENGTH)); - javaUkey2Handshake.verifyHandshake(); - - // Secure channel: - D2DConnectionContext javaSecureContext = javaUkey2Handshake.toConnectionContext(); - - // ukey2_shell encodes data: - byte[] encodedData = cppUkey2Shell.sendEncryptCommand(sPayload1); - byte[] decodedData = javaSecureContext.decodeMessageFromPeer(encodedData); - assertTrue(Arrays.equals(sPayload1, decodedData)); - - // ukey2_shell decodes data: - encodedData = javaSecureContext.encodeMessageToPeer(sPayload2); - decodedData = cppUkey2Shell.sendDecryptCommand(encodedData); - assertTrue(Arrays.equals(sPayload2, decodedData)); - - // ukey2_shell session unique: - byte[] localSessionUnique = javaSecureContext.getSessionUnique(); - byte[] remoteSessionUnique = cppUkey2Shell.sendSessionUniqueCommand(); - assertTrue(Arrays.equals(localSessionUnique, remoteSessionUnique)); - - cppUkey2Shell.stopShell(); - } -} diff --git a/src/main/javatest/com/google/security/cryptauth/lib/securegcm/Ukey2HandshakeTest.java b/src/main/javatest/com/google/security/cryptauth/lib/securegcm/Ukey2HandshakeTest.java deleted file mode 100644 index 49e6f30..0000000 --- a/src/main/javatest/com/google/security/cryptauth/lib/securegcm/Ukey2HandshakeTest.java +++ /dev/null @@ -1,818 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.security.cryptauth.lib.securegcm; - -import com.google.protobuf.ByteString; -import com.google.security.cryptauth.lib.securegcm.Ukey2Handshake.AlertException; -import com.google.security.cryptauth.lib.securegcm.Ukey2Handshake.HandshakeCipher; -import com.google.security.cryptauth.lib.securegcm.Ukey2Handshake.State; -import com.google.security.cryptauth.lib.securegcm.UkeyProto.Ukey2ClientFinished; -import com.google.security.cryptauth.lib.securegcm.UkeyProto.Ukey2ClientInit; -import com.google.security.cryptauth.lib.securegcm.UkeyProto.Ukey2ClientInit.CipherCommitment; -import com.google.security.cryptauth.lib.securegcm.UkeyProto.Ukey2Message; -import com.google.security.cryptauth.lib.securegcm.UkeyProto.Ukey2ServerInit; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; -import junit.framework.TestCase; -import org.junit.Assert; - -/** - * Android compatible tests for the {@link Ukey2Handshake} class. - */ -public class Ukey2HandshakeTest extends TestCase { - - private static final int MAX_AUTH_STRING_LENGTH = 32; - - @Override - protected void setUp() throws Exception { - KeyEncodingTest.installSunEcSecurityProviderIfNecessary(); - super.setUp(); - } - - /** - * Tests correct use - */ - public void testHandshake() throws Exception { - if (KeyEncoding.isLegacyCryptoRequired()) { - // this means we're running on an old SDK, which doesn't support the - // necessary crypto. Let's not test anything in this case. - return; - } - - Ukey2Handshake client = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512); - Ukey2Handshake server = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512); - byte[] handshakeMessage; - - assertEquals(State.IN_PROGRESS, client.getHandshakeState()); - assertEquals(State.IN_PROGRESS, server.getHandshakeState()); - - // Message 1 (Client Init) - handshakeMessage = client.getNextHandshakeMessage(); - server.parseHandshakeMessage(handshakeMessage); - assertEquals(State.IN_PROGRESS, client.getHandshakeState()); - assertEquals(State.IN_PROGRESS, server.getHandshakeState()); - - // Message 2 (Server Init) - handshakeMessage = server.getNextHandshakeMessage(); - client.parseHandshakeMessage(handshakeMessage); - assertEquals(State.IN_PROGRESS, client.getHandshakeState()); - assertEquals(State.IN_PROGRESS, server.getHandshakeState()); - - // Message 3 (Client Finish) - handshakeMessage = client.getNextHandshakeMessage(); - server.parseHandshakeMessage(handshakeMessage); - assertEquals(State.VERIFICATION_NEEDED, client.getHandshakeState()); - assertEquals(State.VERIFICATION_NEEDED, server.getHandshakeState()); - - // Get the auth string - byte[] clientAuthString = client.getVerificationString(MAX_AUTH_STRING_LENGTH); - byte[] serverAuthString = server.getVerificationString(MAX_AUTH_STRING_LENGTH); - Assert.assertArrayEquals(clientAuthString, serverAuthString); - assertEquals(State.VERIFICATION_IN_PROGRESS, client.getHandshakeState()); - assertEquals(State.VERIFICATION_IN_PROGRESS, server.getHandshakeState()); - - // Verify the auth string - client.verifyHandshake(); - server.verifyHandshake(); - assertEquals(State.FINISHED, client.getHandshakeState()); - assertEquals(State.FINISHED, server.getHandshakeState()); - - // Make a context - D2DConnectionContext clientContext = client.toConnectionContext(); - D2DConnectionContext serverContext = server.toConnectionContext(); - assertContextsCompatible(clientContext, serverContext); - assertEquals(State.ALREADY_USED, client.getHandshakeState()); - assertEquals(State.ALREADY_USED, server.getHandshakeState()); - } - - /** - * Verify enums for ciphers match the proto values - */ - public void testCipherEnumValuesCorrect() { - assertEquals( - "You added a cipher, but forgot to change the test", 1, HandshakeCipher.values().length); - - assertEquals(UkeyProto.Ukey2HandshakeCipher.P256_SHA512, - HandshakeCipher.P256_SHA512.getValue()); - } - - /** - * Tests incorrect use by callers (client and servers accidentally sending the wrong message at - * the wrong time) - */ - public void testHandshakeClientAndServerSendRepeatedOutOfOrderMessages() throws Exception { - if (KeyEncoding.isLegacyCryptoRequired()) { - // this means we're running on an old SDK, which doesn't support the - // necessary crypto. Let's not test anything in this case. - return; - } - - // Client sends ClientInit (again) instead of ClientFinished - Ukey2Handshake client = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512); - Ukey2Handshake server = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512); - byte[] handshakeMessage = client.getNextHandshakeMessage(); - server.parseHandshakeMessage(handshakeMessage); - server.getNextHandshakeMessage(); // do this to avoid illegal state - try { - server.parseHandshakeMessage(handshakeMessage); - fail("Expected Alert for client sending ClientInit twice"); - } catch (HandshakeException e) { - // success - } - assertEquals(State.ERROR, server.getHandshakeState()); - - // Server sends ClientInit back to client instead of ServerInit - client = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512); - server = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512); - handshakeMessage = client.getNextHandshakeMessage(); - try { - client.parseHandshakeMessage(handshakeMessage); - fail("Expected Alert for server sending ClientInit back to client"); - } catch (AlertException e) { - // success - } - assertEquals(State.ERROR, client.getHandshakeState()); - - // Clients sends ServerInit back to client instead of ClientFinished - client = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512); - server = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512); - handshakeMessage = client.getNextHandshakeMessage(); - server.parseHandshakeMessage(handshakeMessage); - handshakeMessage = server.getNextHandshakeMessage(); - try { - server.parseHandshakeMessage(handshakeMessage); - fail("Expected Alert for client sending ServerInit back to server"); - } catch (HandshakeException e) { - // success - } - assertEquals(State.ERROR, server.getHandshakeState()); - } - - /** - * Tests that verification codes are different for different handshake runs. Also tests a full - * on-path attack. - */ - public void testVerificationCodeUniqueToSession() throws Exception { - if (KeyEncoding.isLegacyCryptoRequired()) { - // this means we're running on an old SDK, which doesn't support the - // necessary crypto. Let's not test anything in this case. - return; - } - - // Client 1 and Server 1 - Ukey2Handshake client1 = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512); - Ukey2Handshake server1 = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512); - byte[] handshakeMessage = client1.getNextHandshakeMessage(); - server1.parseHandshakeMessage(handshakeMessage); - handshakeMessage = server1.getNextHandshakeMessage(); - client1.parseHandshakeMessage(handshakeMessage); - handshakeMessage = client1.getNextHandshakeMessage(); - server1.parseHandshakeMessage(handshakeMessage); - byte[] client1AuthString = client1.getVerificationString(MAX_AUTH_STRING_LENGTH); - byte[] server1AuthString = server1.getVerificationString(MAX_AUTH_STRING_LENGTH); - Assert.assertArrayEquals(client1AuthString, server1AuthString); - - // Client 2 and Server 2 - Ukey2Handshake client2 = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512); - Ukey2Handshake server2 = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512); - handshakeMessage = client2.getNextHandshakeMessage(); - server2.parseHandshakeMessage(handshakeMessage); - handshakeMessage = server2.getNextHandshakeMessage(); - client2.parseHandshakeMessage(handshakeMessage); - handshakeMessage = client2.getNextHandshakeMessage(); - server2.parseHandshakeMessage(handshakeMessage); - byte[] client2AuthString = client2.getVerificationString(MAX_AUTH_STRING_LENGTH); - byte[] server2AuthString = server2.getVerificationString(MAX_AUTH_STRING_LENGTH); - Assert.assertArrayEquals(client2AuthString, server2AuthString); - - // Make sure the verification strings differ - assertFalse(Arrays.equals(client1AuthString, client2AuthString)); - } - - /** - * Test an attack where the adversary swaps out the public key in the final message (i.e., - * commitment doesn't match public key) - */ - public void testPublicKeyDoesntMatchCommitment() throws Exception { - if (KeyEncoding.isLegacyCryptoRequired()) { - // this means we're running on an old SDK, which doesn't support the - // necessary crypto. Let's not test anything in this case. - return; - } - - // Run handshake as usual, but stop before sending client finished - Ukey2Handshake client1 = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512); - Ukey2Handshake server1 = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512); - byte[] handshakeMessage = client1.getNextHandshakeMessage(); - server1.parseHandshakeMessage(handshakeMessage); - handshakeMessage = server1.getNextHandshakeMessage(); - - // Run another handshake and get the final client finished - Ukey2Handshake client2 = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512); - Ukey2Handshake server2 = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512); - handshakeMessage = client2.getNextHandshakeMessage(); - server2.parseHandshakeMessage(handshakeMessage); - handshakeMessage = server2.getNextHandshakeMessage(); - client2.parseHandshakeMessage(handshakeMessage); - handshakeMessage = client2.getNextHandshakeMessage(); - - // Now use the client finished from second handshake in first handshake (simulates where an - // attacker switches out the last message). - try { - server1.parseHandshakeMessage(handshakeMessage); - fail("Expected server to catch mismatched ClientFinished"); - } catch (HandshakeException e) { - // success - } - assertEquals(State.ERROR, server1.getHandshakeState()); - - // Make sure caller can't actually do anything with the server now that an error has occurred - try { - server1.getVerificationString(MAX_AUTH_STRING_LENGTH); - fail("Server allows operations post error"); - } catch (IllegalStateException e) { - // success - } - try { - server1.verifyHandshake(); - fail("Server allows operations post error"); - } catch (IllegalStateException e) { - // success - } - try { - server1.toConnectionContext(); - fail("Server allows operations post error"); - } catch (IllegalStateException e) { - // success - } - } - - /** - * Test commitment having unsupported version - */ - public void testClientInitUnsupportedVersion() throws Exception { - if (KeyEncoding.isLegacyCryptoRequired()) { - // this means we're running on an old SDK, which doesn't support the - // necessary crypto. Let's not test anything in this case. - return; - } - - // Get ClientInit and modify the version to be too big - Ukey2Handshake client = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512); - Ukey2Handshake server = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512); - byte[] handshakeMessage = client.getNextHandshakeMessage(); - - Ukey2Message.Builder message = Ukey2Message.newBuilder( - Ukey2Message.parseFrom(handshakeMessage)); - Ukey2ClientInit.Builder clientInit = - Ukey2ClientInit.newBuilder(Ukey2ClientInit.parseFrom(message.getMessageData())); - clientInit.setVersion(Ukey2Handshake.VERSION + 1); - message.setMessageData(ByteString.copyFrom(clientInit.build().toByteArray())); - handshakeMessage = message.build().toByteArray(); - - try { - server.parseHandshakeMessage(handshakeMessage); - fail("Server did not catch unsupported version (too big) in ClientInit"); - } catch (AlertException e) { - // success - } - assertEquals(State.ERROR, server.getHandshakeState()); - - // Get ClientInit and modify the version to be too big - client = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512); - server = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512); - handshakeMessage = client.getNextHandshakeMessage(); - - message = Ukey2Message.newBuilder( - Ukey2Message.parseFrom(handshakeMessage)); - clientInit = Ukey2ClientInit.newBuilder(Ukey2ClientInit.parseFrom(message.getMessageData())); - clientInit.setVersion(0 /* minimum version is 1 */); - message.setMessageData(ByteString.copyFrom(clientInit.build().toByteArray())); - handshakeMessage = message.build().toByteArray(); - - try { - server.parseHandshakeMessage(handshakeMessage); - fail("Server did not catch unsupported version (too small) in ClientInit"); - } catch (AlertException e) { - // success - } - assertEquals(State.ERROR, server.getHandshakeState()); - } - - /** - * Tests that server catches wrong number of random bytes in ClientInit - */ - public void testWrongNonceLengthInClientInit() throws Exception { - if (KeyEncoding.isLegacyCryptoRequired()) { - // this means we're running on an old SDK, which doesn't support the - // necessary crypto. Let's not test anything in this case. - return; - } - - // Get ClientInit and modify the nonce - Ukey2Handshake client = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512); - Ukey2Handshake server = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512); - byte[] handshakeMessage = client.getNextHandshakeMessage(); - - Ukey2Message.Builder message = Ukey2Message.newBuilder( - Ukey2Message.parseFrom(handshakeMessage)); - Ukey2ClientInit.Builder clientInit = - Ukey2ClientInit.newBuilder(Ukey2ClientInit.parseFrom(message.getMessageData())); - clientInit.setRandom( - ByteString.copyFrom( - Arrays.copyOf( - clientInit.getRandom().toByteArray(), - 31 /* as per go/ukey2, nonces must be 32 bytes long */))); - message.setMessageData(ByteString.copyFrom(clientInit.build().toByteArray())); - handshakeMessage = message.build().toByteArray(); - - try { - server.parseHandshakeMessage(handshakeMessage); - fail("Server did not catch nonce being too short in ClientInit"); - } catch (AlertException e) { - // success - } - assertEquals(State.ERROR, server.getHandshakeState()); - } - - /** - * Test that server catches missing commitment in ClientInit message - */ - public void testServerCatchesMissingCommitmentInClientInit() throws Exception { - if (KeyEncoding.isLegacyCryptoRequired()) { - // this means we're running on an old SDK, which doesn't support the - // necessary crypto. Let's not test anything in this case. - return; - } - - // Get ClientInit and modify the commitment - Ukey2Handshake client = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512); - Ukey2Handshake server = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512); - byte[] handshakeMessage = client.getNextHandshakeMessage(); - - Ukey2Message.Builder message = Ukey2Message.newBuilder( - Ukey2Message.parseFrom(handshakeMessage)); - Ukey2ClientInit clientInit = - Ukey2ClientInit.newBuilder(Ukey2ClientInit.parseFrom(message.getMessageData())) - .build(); - Ukey2ClientInit.Builder badClientInit = Ukey2ClientInit.newBuilder() - .setVersion(clientInit.getVersion()) - .setRandom(clientInit.getRandom()); - for (CipherCommitment commitment : clientInit.getCipherCommitmentsList()) { - badClientInit.addCipherCommitments(commitment); - } - - message.setMessageData(ByteString.copyFrom(badClientInit.build().toByteArray())); - handshakeMessage = message.build().toByteArray(); - - try { - server.parseHandshakeMessage(handshakeMessage); - fail("Server did not catch missing commitment in ClientInit"); - } catch (AlertException e) { - // success - } - } - - /** - * Test that client catches invalid version in ServerInit - */ - public void testServerInitUnsupportedVersion() throws Exception { - if (KeyEncoding.isLegacyCryptoRequired()) { - // this means we're running on an old SDK, which doesn't support the - // necessary crypto. Let's not test anything in this case. - return; - } - - // Get ServerInit and modify the version to be too big - Ukey2Handshake client = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512); - Ukey2Handshake server = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512); - byte[] handshakeMessage = client.getNextHandshakeMessage(); - server.parseHandshakeMessage(handshakeMessage); - handshakeMessage = server.getNextHandshakeMessage(); - - Ukey2Message.Builder message = Ukey2Message.newBuilder( - Ukey2Message.parseFrom(handshakeMessage)); - Ukey2ServerInit serverInit = - Ukey2ServerInit.newBuilder(Ukey2ServerInit.parseFrom(message.getMessageData())) - .setVersion(Ukey2Handshake.VERSION + 1) - .build(); - message.setMessageData(ByteString.copyFrom(serverInit.toByteArray())); - handshakeMessage = message.build().toByteArray(); - - try { - client.parseHandshakeMessage(handshakeMessage); - fail("Client did not catch unsupported version (too big) in ServerInit"); - } catch (AlertException e) { - // success - } - assertEquals(State.ERROR, client.getHandshakeState()); - - // Get ServerInit and modify the version to be too big - client = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512); - server = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512); - handshakeMessage = client.getNextHandshakeMessage(); - server.parseHandshakeMessage(handshakeMessage); - handshakeMessage = server.getNextHandshakeMessage(); - - message = Ukey2Message.newBuilder(Ukey2Message.parseFrom(handshakeMessage)); - serverInit = - Ukey2ServerInit.newBuilder(Ukey2ServerInit.parseFrom(message.getMessageData())) - .setVersion(0 /* minimum version is 1 */) - .build(); - message.setMessageData(ByteString.copyFrom(serverInit.toByteArray())); - handshakeMessage = message.build().toByteArray(); - - try { - client.parseHandshakeMessage(handshakeMessage); - fail("Client did not catch unsupported version (too small) in ServerInit"); - } catch (AlertException e) { - // success - } - assertEquals(State.ERROR, client.getHandshakeState()); - } - - /** - * Tests that client catches wrong number of random bytes in ServerInit - */ - public void testWrongNonceLengthInServerInit() throws Exception { - if (KeyEncoding.isLegacyCryptoRequired()) { - // this means we're running on an old SDK, which doesn't support the - // necessary crypto. Let's not test anything in this case. - return; - } - - // Get ServerInit and modify the nonce - Ukey2Handshake client = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512); - Ukey2Handshake server = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512); - byte[] handshakeMessage = client.getNextHandshakeMessage(); - server.parseHandshakeMessage(handshakeMessage); - handshakeMessage = server.getNextHandshakeMessage(); - - Ukey2Message.Builder message = Ukey2Message.newBuilder( - Ukey2Message.parseFrom(handshakeMessage)); - Ukey2ServerInit.Builder serverInitBuilder = - Ukey2ServerInit.newBuilder(Ukey2ServerInit.parseFrom(message.getMessageData())); - Ukey2ServerInit serverInit = serverInitBuilder.setRandom(ByteString.copyFrom(Arrays.copyOf( - serverInitBuilder.getRandom().toByteArray(), - 31 /* as per go/ukey2, nonces must be 32 bytes long */))) - .build(); - message.setMessageData(ByteString.copyFrom(serverInit.toByteArray())); - handshakeMessage = message.build().toByteArray(); - - try { - client.parseHandshakeMessage(handshakeMessage); - fail("Client did not catch nonce being too short in ServerInit"); - } catch (AlertException e) { - // success - } - assertEquals(State.ERROR, client.getHandshakeState()); - } - - /** - * Test that client catches missing or incorrect handshake cipher in serverInit - */ - public void testMissingOrIncorrectHandshakeCipherInServerInit() throws Exception { - if (KeyEncoding.isLegacyCryptoRequired()) { - // this means we're running on an old SDK, which doesn't support the - // necessary crypto. Let's not test anything in this case. - return; - } - - // Get ServerInit - Ukey2Handshake client = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512); - Ukey2Handshake server = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512); - byte[] handshakeMessage = client.getNextHandshakeMessage(); - server.parseHandshakeMessage(handshakeMessage); - handshakeMessage = server.getNextHandshakeMessage(); - Ukey2Message.Builder message = Ukey2Message.newBuilder( - Ukey2Message.parseFrom(handshakeMessage)); - Ukey2ServerInit serverInit = Ukey2ServerInit.parseFrom(message.getMessageData()); - - // remove handshake cipher - Ukey2ServerInit badServerInit = Ukey2ServerInit.newBuilder() - .setPublicKey(serverInit.getPublicKey()) - .setRandom(serverInit.getRandom()) - .setVersion(serverInit.getVersion()) - .build(); - - message.setMessageData(ByteString.copyFrom(badServerInit.toByteArray())); - handshakeMessage = message.build().toByteArray(); - - try { - client.parseHandshakeMessage(handshakeMessage); - fail("Client did not catch missing handshake cipher in ServerInit"); - } catch (AlertException e) { - // success - } - assertEquals(State.ERROR, client.getHandshakeState()); - - // Get ServerInit - client = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512); - server = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512); - handshakeMessage = client.getNextHandshakeMessage(); - server.parseHandshakeMessage(handshakeMessage); - handshakeMessage = server.getNextHandshakeMessage(); - message = Ukey2Message.newBuilder(Ukey2Message.parseFrom(handshakeMessage)); - serverInit = Ukey2ServerInit.parseFrom(message.getMessageData()); - - // put in a bad handshake cipher - badServerInit = Ukey2ServerInit.newBuilder() - .setPublicKey(serverInit.getPublicKey()) - .setRandom(serverInit.getRandom()) - .setVersion(serverInit.getVersion()) - .setHandshakeCipher(UkeyProto.Ukey2HandshakeCipher.RESERVED) - .build(); - - message.setMessageData(ByteString.copyFrom(badServerInit.toByteArray())); - handshakeMessage = message.build().toByteArray(); - - try { - client.parseHandshakeMessage(handshakeMessage); - fail("Client did not catch bad handshake cipher in ServerInit"); - } catch (AlertException e) { - // success - } - assertEquals(State.ERROR, client.getHandshakeState()); - } - - /** - * Test that client catches missing or incorrect public key in serverInit - */ - public void testMissingOrIncorrectPublicKeyInServerInit() throws Exception { - if (KeyEncoding.isLegacyCryptoRequired()) { - // this means we're running on an old SDK, which doesn't support the - // necessary crypto. Let's not test anything in this case. - return; - } - - // Get ServerInit - Ukey2Handshake client = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512); - Ukey2Handshake server = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512); - byte[] handshakeMessage = client.getNextHandshakeMessage(); - server.parseHandshakeMessage(handshakeMessage); - handshakeMessage = server.getNextHandshakeMessage(); - Ukey2Message.Builder message = Ukey2Message.newBuilder( - Ukey2Message.parseFrom(handshakeMessage)); - Ukey2ServerInit serverInit = Ukey2ServerInit.parseFrom(message.getMessageData()); - - // remove public key - Ukey2ServerInit badServerInit = Ukey2ServerInit.newBuilder() - .setRandom(serverInit.getRandom()) - .setVersion(serverInit.getVersion()) - .setHandshakeCipher(serverInit.getHandshakeCipher()) - .build(); - - message.setMessageData(ByteString.copyFrom(badServerInit.toByteArray())); - handshakeMessage = message.build().toByteArray(); - - try { - client.parseHandshakeMessage(handshakeMessage); - fail("Client did not catch missing public key in ServerInit"); - } catch (AlertException e) { - // success - } - assertEquals(State.ERROR, client.getHandshakeState()); - - // Get ServerInit - client = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512); - server = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512); - handshakeMessage = client.getNextHandshakeMessage(); - server.parseHandshakeMessage(handshakeMessage); - handshakeMessage = server.getNextHandshakeMessage(); - message = Ukey2Message.newBuilder( - Ukey2Message.parseFrom(handshakeMessage)); - serverInit = Ukey2ServerInit.parseFrom(message.getMessageData()); - - // put in a bad public key - badServerInit = Ukey2ServerInit.newBuilder() - .setPublicKey(ByteString.copyFrom(new byte[] {42, 12, 1})) - .setRandom(serverInit.getRandom()) - .setVersion(serverInit.getVersion()) - .setHandshakeCipher(serverInit.getHandshakeCipher()) - .build(); - - message.setMessageData(ByteString.copyFrom(badServerInit.toByteArray())); - handshakeMessage = message.build().toByteArray(); - - try { - client.parseHandshakeMessage(handshakeMessage); - fail("Client did not catch bad public key in ServerInit"); - } catch (AlertException e) { - // success - } - assertEquals(State.ERROR, client.getHandshakeState()); - } - - /** - * Test that client catches missing or incorrect public key in clientFinished - */ - public void testMissingOrIncorrectPublicKeyInClientFinished() throws Exception { - if (KeyEncoding.isLegacyCryptoRequired()) { - // this means we're running on an old SDK, which doesn't support the - // necessary crypto. Let's not test anything in this case. - return; - } - - // Get ClientFinished - Ukey2Handshake client = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512); - Ukey2Handshake server = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512); - byte[] handshakeMessage = client.getNextHandshakeMessage(); - server.parseHandshakeMessage(handshakeMessage); - handshakeMessage = server.getNextHandshakeMessage(); - client.parseHandshakeMessage(handshakeMessage); - handshakeMessage = client.getNextHandshakeMessage(); - Ukey2Message.Builder message = Ukey2Message.newBuilder( - Ukey2Message.parseFrom(handshakeMessage)); - - // remove public key - Ukey2ClientFinished.Builder badClientFinished = Ukey2ClientFinished.newBuilder(); - - message.setMessageData(ByteString.copyFrom(badClientFinished.build().toByteArray())); - handshakeMessage = message.build().toByteArray(); - - try { - server.parseHandshakeMessage(handshakeMessage); - fail("Server did not catch missing public key in ClientFinished"); - } catch (HandshakeException e) { - // success - } - assertEquals(State.ERROR, server.getHandshakeState()); - - // Get ClientFinished - client = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512); - server = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512); - handshakeMessage = client.getNextHandshakeMessage(); - server.parseHandshakeMessage(handshakeMessage); - handshakeMessage = server.getNextHandshakeMessage(); - client.parseHandshakeMessage(handshakeMessage); - handshakeMessage = client.getNextHandshakeMessage(); - message = Ukey2Message.newBuilder(Ukey2Message.parseFrom(handshakeMessage)); - - // remove public key - badClientFinished = Ukey2ClientFinished.newBuilder() - .setPublicKey(ByteString.copyFrom(new byte[] {42, 12, 1})); - - message.setMessageData(ByteString.copyFrom(badClientFinished.build().toByteArray())); - handshakeMessage = message.build().toByteArray(); - - try { - server.parseHandshakeMessage(handshakeMessage); - fail("Server did not catch bad public key in ClientFinished"); - } catch (HandshakeException e) { - // success - } - assertEquals(State.ERROR, server.getHandshakeState()); - } - - /** - * Tests that items (nonces, commitments, public keys) that should be random are at least - * different on every run. - */ - public void testRandomItemsDifferentOnEveryRun() throws Exception { - if (KeyEncoding.isLegacyCryptoRequired()) { - // this means we're running on an old SDK, which doesn't support the - // necessary crypto. Let's not test anything in this case. - return; - } - - int numberOfRuns = 50; - - // Search for collisions - Set<Integer> commitments = new HashSet<>(numberOfRuns); - Set<Integer> clientNonces = new HashSet<>(numberOfRuns); - Set<Integer> serverNonces = new HashSet<>(numberOfRuns); - Set<Integer> serverPublicKeys = new HashSet<>(numberOfRuns); - Set<Integer> clientPublicKeys = new HashSet<>(numberOfRuns); - - for (int i = 0; i < numberOfRuns; i++) { - Ukey2Handshake client = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512); - Ukey2Handshake server = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512); - byte[] handshakeMessage = client.getNextHandshakeMessage(); - Ukey2Message message = Ukey2Message.parseFrom(handshakeMessage); - Ukey2ClientInit clientInit = Ukey2ClientInit.parseFrom(message.getMessageData()); - - server.parseHandshakeMessage(handshakeMessage); - handshakeMessage = server.getNextHandshakeMessage(); - message = Ukey2Message.parseFrom(handshakeMessage); - Ukey2ServerInit serverInit = Ukey2ServerInit.parseFrom(message.getMessageData()); - - client.parseHandshakeMessage(handshakeMessage); - handshakeMessage = client.getNextHandshakeMessage(); - message = Ukey2Message.parseFrom(handshakeMessage); - Ukey2ClientFinished clientFinished = Ukey2ClientFinished.parseFrom(message.getMessageData()); - - // Clean up to save some memory (b/32054837) - client = null; - server = null; - handshakeMessage = null; - message = null; - System.gc(); - - // ClientInit randomness - Integer nonceHash = Integer.valueOf(Arrays.hashCode(clientInit.getRandom().toByteArray())); - if (clientNonces.contains(nonceHash) || serverNonces.contains(nonceHash)) { - fail("Nonce in ClientINit has repeated!"); - } - clientNonces.add(nonceHash); - - Integer commitmentHash = 0; - for (CipherCommitment commitement : clientInit.getCipherCommitmentsList()) { - commitmentHash += Arrays.hashCode(commitement.toByteArray()); - } - if (commitments.contains(nonceHash)) { - fail("Commitment has repeated!"); - } - commitments.add(commitmentHash); - - // ServerInit randomness - nonceHash = Integer.valueOf(Arrays.hashCode(serverInit.getRandom().toByteArray())); - if (serverNonces.contains(nonceHash) || clientNonces.contains(nonceHash)) { - fail("Nonce in ServerInit repeated!"); - } - serverNonces.add(nonceHash); - - Integer publicKeyHash = - Integer.valueOf(Arrays.hashCode(serverInit.getPublicKey().toByteArray())); - if (serverPublicKeys.contains(publicKeyHash) || clientPublicKeys.contains(publicKeyHash)) { - fail("Public Key in ServerInit repeated!"); - } - serverPublicKeys.add(publicKeyHash); - - // Client Finished randomness - publicKeyHash = Integer.valueOf(Arrays.hashCode(clientFinished.getPublicKey().toByteArray())); - if (serverPublicKeys.contains(publicKeyHash) || clientPublicKeys.contains(publicKeyHash)) { - fail("Public Key in ClientFinished repeated!"); - } - clientPublicKeys.add(publicKeyHash); - } - } - - /** - * Tests that {@link Ukey2Handshake#getVerificationString(int)} enforces sane verification string - * lengths. - */ - public void testGetVerificationEnforcesSaneLengths() throws Exception { - if (KeyEncoding.isLegacyCryptoRequired()) { - // this means we're running on an old SDK, which doesn't support the - // necessary crypto. Let's not test anything in this case. - return; - } - - // Run the protocol - Ukey2Handshake client = Ukey2Handshake.forInitiator(HandshakeCipher.P256_SHA512); - Ukey2Handshake server = Ukey2Handshake.forResponder(HandshakeCipher.P256_SHA512); - byte[] handshakeMessage = client.getNextHandshakeMessage(); - server.parseHandshakeMessage(handshakeMessage); - handshakeMessage = server.getNextHandshakeMessage(); - client.parseHandshakeMessage(handshakeMessage); - handshakeMessage = client.getNextHandshakeMessage(); - server.parseHandshakeMessage(handshakeMessage); - - // Try to get too short verification string - try { - client.getVerificationString(0); - fail("Too short verification string allowed"); - } catch (IllegalArgumentException e) { - // success - } - - // Try to get too long verification string - try { - server.getVerificationString(MAX_AUTH_STRING_LENGTH + 1); - fail("Too long verification string allowed"); - } catch (IllegalArgumentException e) { - // success - } - } - - /** - * Asserts that the given client and server contexts are compatible - */ - private void assertContextsCompatible( - D2DConnectionContext clientContext, D2DConnectionContext serverContext) { - assertNotNull(clientContext); - assertNotNull(serverContext); - assertEquals(D2DConnectionContextV1.PROTOCOL_VERSION, clientContext.getProtocolVersion()); - assertEquals(D2DConnectionContextV1.PROTOCOL_VERSION, serverContext.getProtocolVersion()); - assertEquals(clientContext.getEncodeKey(), serverContext.getDecodeKey()); - assertEquals(clientContext.getDecodeKey(), serverContext.getEncodeKey()); - assertFalse(clientContext.getEncodeKey().equals(clientContext.getDecodeKey())); - assertEquals(0, clientContext.getSequenceNumberForEncoding()); - assertEquals(0, clientContext.getSequenceNumberForDecoding()); - assertEquals(0, serverContext.getSequenceNumberForEncoding()); - assertEquals(0, serverContext.getSequenceNumberForDecoding()); - } -} diff --git a/src/main/javatest/com/google/security/cryptauth/lib/securegcm/Ukey2ShellCppWrapper.java b/src/main/javatest/com/google/security/cryptauth/lib/securegcm/Ukey2ShellCppWrapper.java deleted file mode 100644 index 2b73653..0000000 --- a/src/main/javatest/com/google/security/cryptauth/lib/securegcm/Ukey2ShellCppWrapper.java +++ /dev/null @@ -1,342 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.security.cryptauth.lib.securegcm; - -import com.google.common.io.BaseEncoding; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.lang.ProcessBuilder.Redirect; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.util.Arrays; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import javax.annotation.Nullable; - -/** - * A wrapper to execute and interact with the //security/cryptauth/lib/securegcm:ukey2_shell binary. - * - * <p>This binary is a shell over the C++ implementation of the UKEY2 protocol, so this wrapper is - * used to test compatibility between the C++ and Java implementations. - * - * <p>The ukey2_shell is invoked as follows: - * - * <pre>{@code - * ukey2_shell --mode=<mode> --verification_string_length=<length> - * }</pre> - * - * where {@code mode={initiator, responder}} and {@code verification_string_length} is a positive - * integer. - */ -public class Ukey2ShellCppWrapper { - // The path the the ukey2_shell binary. - private static final String BINARY_PATH = "build/src/main/cpp/src/securegcm/ukey2_shell"; - - // The time to wait before timing out a read or write operation to the shell. - @SuppressWarnings("GoodTime") // TODO(b/147378611): store a java.time.Duration instead - private static final long IO_TIMEOUT_MILLIS = 5000; - - public enum Mode { - INITIATOR, - RESPONDER - } - - private final Mode mode; - private final int verificationStringLength; - private final ExecutorService executorService; - - @Nullable private Process shellProcess; - private boolean secureContextEstablished; - - /** - * @param mode The mode to run the shell in (initiator or responder). - * @param verificationStringLength The length of the verification string used in the handshake. - */ - public Ukey2ShellCppWrapper(Mode mode, int verificationStringLength) { - this.mode = mode; - this.verificationStringLength = verificationStringLength; - this.executorService = Executors.newSingleThreadExecutor(); - } - - /** - * Begins execution of the ukey2_shell binary. - * - * @throws IOException - */ - public void startShell() throws IOException { - if (shellProcess != null) { - throw new IllegalStateException("Shell already started."); - } - - String modeArg = "--mode=" + getModeString(); - String verificationStringLengthArg = "--verification_string_length=" + verificationStringLength; - - final ProcessBuilder builder = - new ProcessBuilder(BINARY_PATH, modeArg, verificationStringLengthArg); - - // Merge the shell's stderr with the stderr of the current process. - builder.redirectError(Redirect.INHERIT); - - shellProcess = builder.start(); - } - - /** - * Stops execution of the ukey2_shell binary. - * - * @throws IOException - */ - public void stopShell() { - if (shellProcess == null) { - throw new IllegalStateException("Shell not started."); - } - shellProcess.destroy(); - } - - /** - * @return the handshake message read from the shell. - * @throws IOException - */ - public byte[] readHandshakeMessage() throws IOException { - return readFrameWithTimeout(); - } - - /** - * Sends the handshake message to the shell. - * - * @param message - * @throws IOException - */ - public void writeHandshakeMessage(byte[] message) throws IOException { - writeFrameWithTimeout(message); - } - - /** - * Reads the auth string from the shell and compares it with {@code authString}. If verification - * succeeds, then write "ok" back as a confirmation. - * - * @param authString the auth string to compare to. - * @throws IOException - */ - public void confirmAuthString(byte[] authString) throws IOException { - byte[] shellAuthString = readFrameWithTimeout(); - if (!Arrays.equals(authString, shellAuthString)) { - throw new IOException( - String.format( - "Unable to verify auth string: 0x%s != 0x%s", - BaseEncoding.base16().encode(authString), - BaseEncoding.base16().encode(shellAuthString))); - } - writeFrameWithTimeout("ok".getBytes()); - secureContextEstablished = true; - } - - /** - * Sends {@code payload} to be encrypted by the shell. This function can only be called after a - * handshake is performed and a secure context established. - * - * @param payload the data to be encrypted. - * @return the encrypted message returned by the shell. - * @throws IOException - */ - public byte[] sendEncryptCommand(byte[] payload) throws IOException { - writeFrameWithTimeout(createExpression("encrypt", payload)); - return readFrameWithTimeout(); - } - - /** - * Sends {@code message} to be decrypted by the shell. This function can only be called after a - * handshake is performed and a secure context established. - * - * @param message the data to be decrypted. - * @return the decrypted payload returned by the shell. - * @throws IOException - */ - public byte[] sendDecryptCommand(byte[] message) throws IOException { - writeFrameWithTimeout(createExpression("decrypt", message)); - return readFrameWithTimeout(); - } - - /** - * Requests the session unique value from the shell. This function can only be called after a - * handshake is performed and a secure context established. - * - * @return the session unique value returned by the shell. - * @throws IOException - */ - public byte[] sendSessionUniqueCommand() throws IOException { - writeFrameWithTimeout(createExpression("session_unique", null)); - return readFrameWithTimeout(); - } - - /** - * Reads a frame from the shell's stdout with a timeout. - * - * @return The contents of the frame. - * @throws IOException - */ - private byte[] readFrameWithTimeout() throws IOException { - Future<byte[]> future = - executorService.submit( - new Callable<byte[]>() { - @Override - public byte[] call() throws Exception { - return readFrame(); - } - }); - - try { - return future.get(IO_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); - } catch (InterruptedException | ExecutionException | TimeoutException e) { - throw new IOException(e); - } - } - - /** - * Writes a frame to the shell's stdin with a timeout. - * - * @param contents the contents of the frame. - * @throws IOException - */ - private void writeFrameWithTimeout(final byte[] contents) throws IOException { - Future<?> future = - executorService.submit( - new Runnable() { - @Override - public void run() { - try { - writeFrame(contents); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - }); - - try { - future.get(IO_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); - } catch (InterruptedException | ExecutionException | TimeoutException e) { - throw new IOException(e); - } - } - - /** - * Reads a frame from the shell's stdout, which has the format: - * - * <pre>{@code - * +---------------------+-----------------+ - * | 4-bytes | |length| bytes | - * +---------------------+-----------------+ - * | (unsigned) length | contents | - * +---------------------+-----------------+ - * }</pre> - * - * @return the contents that were read - * @throws IOException - */ - private byte[] readFrame() throws IOException { - if (shellProcess == null) { - throw new IllegalStateException("Shell not started."); - } - - InputStream inputStream = shellProcess.getInputStream(); - byte[] lengthBytes = new byte[4]; - if (inputStream.read(lengthBytes) != lengthBytes.length) { - throw new IOException("Failed to read length."); - } - - int length = ByteBuffer.wrap(lengthBytes).order(ByteOrder.BIG_ENDIAN).getInt(); - if (length < 0) { - throw new IOException("Length too large: " + Arrays.toString(lengthBytes)); - } - - byte[] contents = new byte[length]; - int bytesRead = inputStream.read(contents); - if (bytesRead != length) { - throw new IOException("Failed to read entire contents: " + bytesRead + " != " + length); - } - - return contents; - } - - /** - * Writes a frame to the shell's stdin, which has the format: - * - * <pre>{@code - * +---------------------+-----------------+ - * | 4-bytes | |length| bytes | - * +---------------------+-----------------+ - * | (unsigned) length | contents | - * +---------------------+-----------------+ - * }</pre> - * - * @param contents the contents to send. - * @throws IOException - */ - private void writeFrame(byte[] contents) throws IOException { - if (shellProcess == null) { - throw new IllegalStateException("Shell not started."); - } - - // The length is big-endian encoded, network byte order. - long length = contents.length; - byte[] lengthBytes = new byte[4]; - lengthBytes[0] = (byte) (length >> 32 & 0xFF); - lengthBytes[1] = (byte) (length >> 16 & 0xFF); - lengthBytes[2] = (byte) (length >> 8 & 0xFF); - lengthBytes[3] = (byte) (length >> 0 & 0xFF); - - OutputStream outputStream = shellProcess.getOutputStream(); - outputStream.write(lengthBytes); - outputStream.write(contents); - outputStream.flush(); - } - - /** - * Creates an expression to be processed when a secure connection is established, after the - * handshake is done. - * - * @param command The command to send. - * @param argument The argument of the command. Can be null. - * @return the expression that can be sent to the shell. - * @throws IOException. - */ - private byte[] createExpression(String command, @Nullable byte[] argument) throws IOException { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - outputStream.write(command.getBytes()); - outputStream.write(" ".getBytes()); - if (argument != null) { - outputStream.write(argument); - } - return outputStream.toByteArray(); - } - - /** @return the mode string to use in the argument to start the ukey2_shell process. */ - private String getModeString() { - switch (mode) { - case INITIATOR: - return "initiator"; - case RESPONDER: - return "responder"; - default: - throw new IllegalArgumentException("Uknown mode " + mode); - } - } -} diff --git a/src/main/javatest/com/google/security/cryptauth/lib/securemessage/CryptoOpsTest.java b/src/main/javatest/com/google/security/cryptauth/lib/securemessage/CryptoOpsTest.java deleted file mode 100644 index 65fa094..0000000 --- a/src/main/javatest/com/google/security/cryptauth/lib/securemessage/CryptoOpsTest.java +++ /dev/null @@ -1,172 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.security.cryptauth.lib.securemessage; - -import static org.junit.Assert.assertThrows; - -import com.google.security.cryptauth.lib.securemessage.CryptoOps.EncType; -import com.google.security.cryptauth.lib.securemessage.CryptoOps.SigType; -import java.util.Arrays; -import javax.crypto.KeyGenerator; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; -import junit.framework.TestCase; - -/** - * Unit tests for the CryptoOps class - */ -public class CryptoOpsTest extends TestCase { - - /** HKDF Test Case 1 IKM from RFC 5869 */ - private static final byte[] HKDF_CASE1_IKM = { - 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, - 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, - 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, - 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, - 0x0b, 0x0b - }; - - /** HKDF Test Case 1 salt from RFC 5869 */ - private static final byte[] HKDF_CASE1_SALT = { - 0x00, 0x01, 0x02, 0x03, 0x04, - 0x05, 0x06, 0x07, 0x08, 0x09, - 0x0a, 0x0b, 0x0c - }; - - /** HKDF Test Case 1 info from RFC 5869 */ - private static final byte[] HKDF_CASE1_INFO = { - (byte) 0xf0, (byte) 0xf1, (byte) 0xf2, (byte) 0xf3, (byte) 0xf4, - (byte) 0xf5, (byte) 0xf6, (byte) 0xf7, (byte) 0xf8, (byte) 0xf9 - }; - - /** First 32 bytes of HKDF Test Case 1 OKM (output) from RFC 5869 */ - private static final byte[] HKDF_CASE1_OKM = { - (byte) 0x3c, (byte) 0xb2, (byte) 0x5f, (byte) 0x25, (byte) 0xfa, - (byte) 0xac, (byte) 0xd5, (byte) 0x7a, (byte) 0x90, (byte) 0x43, - (byte) 0x4f, (byte) 0x64, (byte) 0xd0, (byte) 0x36, (byte) 0x2f, - (byte) 0x2a, (byte) 0x2d, (byte) 0x2d, (byte) 0x0a, (byte) 0x90, - (byte) 0xcf, (byte) 0x1a, (byte) 0x5a, (byte) 0x4c, (byte) 0x5d, - (byte) 0xb0, (byte) 0x2d, (byte) 0x56, (byte) 0xec, (byte) 0xc4, - (byte) 0xc5, (byte) 0xbf, (byte) 0x34, (byte) 0x00, (byte) 0x72, - (byte) 0x08, (byte) 0xd5, (byte) 0xb8, (byte) 0x87, (byte) 0x18, - (byte) 0x58, (byte) 0x65 - }; - - private SecretKey aesKey1; - private SecretKey aesKey2; - - @Override - protected void setUp() throws Exception { - KeyGenerator aesKeygen = KeyGenerator.getInstance("AES"); - aesKeygen.init(256); - aesKey1 = aesKeygen.generateKey(); - aesKey2 = aesKeygen.generateKey(); - super.setUp(); - } - - public void testNoPurposeConflicts() { - // Ensure that signature algorithms and encryption algorithms are not given identical purposes - // (this prevents confusion of derived keys). - for (SigType sigType : SigType.values()) { - for (EncType encType : EncType.values()) { - assertFalse(CryptoOps.getPurpose(sigType).equals(CryptoOps.getPurpose(encType))); - } - } - } - - public void testDeriveAes256KeyFor() throws Exception { - // Test that deriving with the same key and purpose twice is deterministic - assertTrue(Arrays.equals(CryptoOps.deriveAes256KeyFor(aesKey1, "A").getEncoded(), - CryptoOps.deriveAes256KeyFor(aesKey1, "A").getEncoded())); - // Test that derived keys with different purposes differ - assertFalse(Arrays.equals(CryptoOps.deriveAes256KeyFor(aesKey1, "A").getEncoded(), - CryptoOps.deriveAes256KeyFor(aesKey1, "B").getEncoded())); - // Test that derived keys with the same purpose but different master keys differ - assertFalse(Arrays.equals(CryptoOps.deriveAes256KeyFor(aesKey1, "A").getEncoded(), - CryptoOps.deriveAes256KeyFor(aesKey2, "A").getEncoded())); - } - - public void testHkdf() throws Exception { - SecretKey inputKey = new SecretKeySpec(HKDF_CASE1_IKM, "AES"); - byte[] result = CryptoOps.hkdf(inputKey, HKDF_CASE1_SALT, HKDF_CASE1_INFO); - byte[] expectedResult = Arrays.copyOf(HKDF_CASE1_OKM, 32); - assertTrue(Arrays.equals(result, expectedResult)); - } - - public void testHkdfLongOutput() throws Exception { - SecretKey inputKey = new SecretKeySpec(HKDF_CASE1_IKM, "AES"); - byte[] result = CryptoOps.hkdf(inputKey, HKDF_CASE1_SALT, HKDF_CASE1_INFO, 42); - byte[] expectedResult = Arrays.copyOf(HKDF_CASE1_OKM, 42); - assertTrue(Arrays.equals(result, expectedResult)); - } - - public void testHkdfShortOutput() throws Exception { - SecretKey inputKey = new SecretKeySpec(HKDF_CASE1_IKM, "AES"); - byte[] result = CryptoOps.hkdf(inputKey, HKDF_CASE1_SALT, HKDF_CASE1_INFO, 12); - byte[] expectedResult = Arrays.copyOf(HKDF_CASE1_OKM, 12); - assertTrue(Arrays.equals(result, expectedResult)); - } - - public void testHkdfInvalidLengths() throws Exception { - SecretKey inputKey = new SecretKeySpec(HKDF_CASE1_IKM, "AES"); - - // Negative length - assertThrows( - IllegalArgumentException.class, - () -> CryptoOps.hkdf(inputKey, HKDF_CASE1_SALT, HKDF_CASE1_INFO, -5)); - - // Too long, would be more than 256 blocks - assertThrows( - IllegalArgumentException.class, - () -> CryptoOps.hkdf(inputKey, HKDF_CASE1_SALT, HKDF_CASE1_INFO, 32 * 256 + 1)); - } - - public void testConcat() { - byte[] a = { 1, 2, 3, 4}; - byte[] b = { 5 , 6 }; - byte[] expectedResult = { 1, 2, 3, 4, 5, 6 }; - byte[] result = CryptoOps.concat(a, b); - assertEquals(a.length + b.length, result.length); - assertTrue(Arrays.equals(expectedResult, result)); - - byte[] empty = { }; - assertEquals(0, CryptoOps.concat(empty, empty).length); - assertTrue(Arrays.equals(a, CryptoOps.concat(a, empty))); - assertTrue(Arrays.equals(a, CryptoOps.concat(empty, a))); - - assertEquals(0, CryptoOps.concat(null, null).length); - assertTrue(Arrays.equals(a, CryptoOps.concat(a, null))); - assertTrue(Arrays.equals(a, CryptoOps.concat(null, a))); - } - - public void testSubarray() { - byte[] in = { 1, 2, 3, 4, 5, 6, 7 }; - assertTrue(Arrays.equals(in, CryptoOps.subarray(in, 0, in.length))); - assertEquals(0, CryptoOps.subarray(in, 0, 0).length); - byte[] expectedResult1 = { 1 }; - assertTrue(Arrays.equals(expectedResult1, CryptoOps.subarray(in, 0, 1))); - byte[] expectedResult34 = { 3, 4 }; - assertTrue(Arrays.equals(expectedResult34, CryptoOps.subarray(in, 2, 4))); - assertThrows(IndexOutOfBoundsException.class, () -> CryptoOps.subarray(in, 0, in.length + 1)); - assertThrows(IndexOutOfBoundsException.class, () -> CryptoOps.subarray(in, -1, in.length)); - assertThrows( - IndexOutOfBoundsException.class, () -> CryptoOps.subarray(in, in.length, in.length)); - assertThrows( - IndexOutOfBoundsException.class, - () -> CryptoOps.subarray(in, Integer.MIN_VALUE, in.length)); - assertThrows( - IndexOutOfBoundsException.class, () -> CryptoOps.subarray(in, 1, Integer.MIN_VALUE)); - } -} diff --git a/src/main/javatest/com/google/security/cryptauth/lib/securemessage/NullsGoogle3Test.java b/src/main/javatest/com/google/security/cryptauth/lib/securemessage/NullsGoogle3Test.java deleted file mode 100644 index c28d2f9..0000000 --- a/src/main/javatest/com/google/security/cryptauth/lib/securemessage/NullsGoogle3Test.java +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.security.cryptauth.lib.securemessage; - -import com.google.common.testing.NullPointerTester; -import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.SecureMessage; -import junit.framework.TestCase; - -/** - * Non-portable Google3-based test to check null pointer behavior. - */ -public class NullsGoogle3Test extends TestCase { - - /** - * We test all of the classes in one place to avoid a proliferation of similar test cases, - * noting that {@link NullPointerTester} emits the name of the class where the breakge occurs. - */ - public void testNulls() { - final NullPointerTester tester = new NullPointerTester(); - tester.testAllPublicStaticMethods(CryptoOps.class); - tester.testAllPublicStaticMethods(PublicKeyProtoUtil.class); - - tester.setDefault(SecureMessage.class, SecureMessage.getDefaultInstance()); - tester.testAllPublicStaticMethods(SecureMessageParser.class); - - tester.testAllPublicStaticMethods(SecureMessageBuilder.class); - tester.testAllPublicConstructors(SecureMessageBuilder.class); - tester.testAllPublicInstanceMethods(new SecureMessageBuilder()); - } -} diff --git a/src/main/javatest/com/google/security/cryptauth/lib/securemessage/PublicKeyProtoUtilTest.java b/src/main/javatest/com/google/security/cryptauth/lib/securemessage/PublicKeyProtoUtilTest.java deleted file mode 100644 index 8581622..0000000 --- a/src/main/javatest/com/google/security/cryptauth/lib/securemessage/PublicKeyProtoUtilTest.java +++ /dev/null @@ -1,412 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.security.cryptauth.lib.securemessage; - -import com.google.common.io.BaseEncoding; -import com.google.protobuf.ByteString; -import com.google.security.annotations.SuppressInsecureCipherModeCheckerNoReview; -import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.DhPublicKey; -import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.EcP256PublicKey; -import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.GenericPublicKey; -import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.SimpleRsaPublicKey; -import java.math.BigInteger; -import java.security.KeyFactory; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.interfaces.ECPublicKey; -import java.security.spec.ECPoint; -import java.security.spec.ECPublicKeySpec; -import java.security.spec.InvalidKeySpecException; -import java.util.Arrays; -import javax.crypto.KeyAgreement; -import javax.crypto.interfaces.DHPrivateKey; -import javax.crypto.interfaces.DHPublicKey; -import junit.framework.TestCase; - -/** Tests for the PublicKeyProtoUtil class. */ -public class PublicKeyProtoUtilTest extends TestCase { - - private static final byte[] ZERO_BYTE = {0}; - private PublicKey ecPublicKey; - private PublicKey rsaPublicKey; - - /** - * Diffie Hellman {@link PublicKey}s require special treatment, so we store them specifically as a - * {@link DHPublicKey} to minimize casting. - */ - private DHPublicKey dhPublicKey; - - @Override - public void setUp() { - if (!isAndroidOsWithoutEcSupport()) { - ecPublicKey = PublicKeyProtoUtil.generateEcP256KeyPair().getPublic(); - } - rsaPublicKey = PublicKeyProtoUtil.generateRSA2048KeyPair().getPublic(); - dhPublicKey = (DHPublicKey) PublicKeyProtoUtil.generateDh2048KeyPair().getPublic(); - } - - public void testPublicKeyProtoSpecificEncodeParse() throws Exception { - if (!isAndroidOsWithoutEcSupport()) { - assertEquals( - ecPublicKey, - PublicKeyProtoUtil.parseEcPublicKey(PublicKeyProtoUtil.encodeEcPublicKey(ecPublicKey))); - } - - assertEquals( - rsaPublicKey, - PublicKeyProtoUtil.parseRsa2048PublicKey( - PublicKeyProtoUtil.encodeRsa2048PublicKey(rsaPublicKey))); - - // DHPublicKey objects don't seem to properly implement equals(), so we have to test that - // the individual y and p values match (it is safe to assume g = 2 is used if p is correct). - DHPublicKey parsedDHPublicKey = - PublicKeyProtoUtil.parseDh2048PublicKey( - PublicKeyProtoUtil.encodeDh2048PublicKey(dhPublicKey)); - assertEquals(dhPublicKey.getY(), parsedDHPublicKey.getY()); - assertEquals(dhPublicKey.getParams().getP(), parsedDHPublicKey.getParams().getP()); - assertEquals(dhPublicKey.getParams().getG(), parsedDHPublicKey.getParams().getG()); - } - - public void testPublicKeyProtoGenericEncodeParse() throws Exception { - if (!isAndroidOsWithoutEcSupport()) { - assertEquals( - ecPublicKey, - PublicKeyProtoUtil.parsePublicKey( - PublicKeyProtoUtil.encodePaddedEcPublicKey(ecPublicKey))); - assertEquals( - ecPublicKey, - PublicKeyProtoUtil.parsePublicKey(PublicKeyProtoUtil.encodePublicKey(ecPublicKey))); - } - - assertEquals( - rsaPublicKey, - PublicKeyProtoUtil.parsePublicKey(PublicKeyProtoUtil.encodePublicKey(rsaPublicKey))); - - // See above explanation for why we treat DHPublicKey objects differently. - DHPublicKey parsedDHPublicKey = - PublicKeyProtoUtil.parseDh2048PublicKey( - PublicKeyProtoUtil.encodeDh2048PublicKey(dhPublicKey)); - assertEquals(dhPublicKey.getY(), parsedDHPublicKey.getY()); - assertEquals(dhPublicKey.getParams().getP(), parsedDHPublicKey.getParams().getP()); - assertEquals(dhPublicKey.getParams().getG(), parsedDHPublicKey.getParams().getG()); - } - - public void testPaddedECPublicKeyEncodeHasPaddedNullByte() throws Exception { - if (isAndroidOsWithoutEcSupport()) { - return; - } - - // Key where the x coordinate is 33 bytes, y coordinate is 32 bytes - ECPublicKey maxXByteLengthKey = - buildEcPublicKey( - BaseEncoding.base64().decode("AM730WQL7ZAmvyAJX4euNdr3+nAIueGlYYGXE6p732h6"), - BaseEncoding.base64().decode("JEnmaDpKn0fH4/0kKGb97qUSwI2uT+ta0GLe3V7REfk=")); - // Key where both coordinates are 33 bytes - ECPublicKey maxByteLengthKey = - buildEcPublicKey( - BaseEncoding.base64().decode("AOg9TQCxFfVdXv7lO/6UVDyiPsu8XDkEWQIPUfqX6UHP"), - BaseEncoding.base64().decode("AP/RW8uVyu6QImpbza51CqG1mtBTh5c9pjv9CUwOuB7E")); - // Key where both coordinates are 32 bytes - ECPublicKey notMaxByteLengthKey = - buildEcPublicKey( - BaseEncoding.base64().decode("M35bxV8HKr0e8v7f4zuXgw6TYFawvikFdI71u9S1ONI="), - BaseEncoding.base64().decode("OXR+xCpD8AR0VR8TeBXA00eIr3rWE6sV6KrOM6MoWsc=")); - GenericPublicKey encodedMaxXByteLengthKey = - PublicKeyProtoUtil.encodePublicKey(maxXByteLengthKey); - GenericPublicKey paddedEncodedMaxXByteLengthKey = - PublicKeyProtoUtil.encodePaddedEcPublicKey(maxXByteLengthKey); - GenericPublicKey encodedMaxByteLengthKey = PublicKeyProtoUtil.encodePublicKey(maxByteLengthKey); - GenericPublicKey paddedEncodedMaxByteLengthKey = - PublicKeyProtoUtil.encodePaddedEcPublicKey(maxByteLengthKey); - GenericPublicKey encodedNotMaxByteLengthKey = - PublicKeyProtoUtil.encodePublicKey(notMaxByteLengthKey); - GenericPublicKey paddedEncodedNotMaxByteLengthKey = - PublicKeyProtoUtil.encodePaddedEcPublicKey(notMaxByteLengthKey); - - assertEquals(maxXByteLengthKey, PublicKeyProtoUtil.parsePublicKey(encodedMaxXByteLengthKey)); - assertEquals( - maxXByteLengthKey, PublicKeyProtoUtil.parsePublicKey(paddedEncodedMaxXByteLengthKey)); - assertEquals(maxByteLengthKey, PublicKeyProtoUtil.parsePublicKey(encodedMaxByteLengthKey)); - assertEquals( - maxByteLengthKey, PublicKeyProtoUtil.parsePublicKey(paddedEncodedMaxByteLengthKey)); - assertEquals( - notMaxByteLengthKey, PublicKeyProtoUtil.parsePublicKey(paddedEncodedNotMaxByteLengthKey)); - assertEquals( - notMaxByteLengthKey, PublicKeyProtoUtil.parsePublicKey(encodedNotMaxByteLengthKey)); - - assertEquals(33, paddedEncodedMaxXByteLengthKey.getEcP256PublicKey().getX().size()); - assertEquals(33, paddedEncodedMaxXByteLengthKey.getEcP256PublicKey().getY().size()); - assertEquals(0, paddedEncodedMaxXByteLengthKey.getEcP256PublicKey().getX().byteAt(0)); - assertEquals(0, paddedEncodedMaxXByteLengthKey.getEcP256PublicKey().getY().byteAt(0)); - assertEquals(33, encodedMaxXByteLengthKey.getEcP256PublicKey().getX().size()); - assertEquals(32, encodedMaxXByteLengthKey.getEcP256PublicKey().getY().size()); - - assertEquals(33, paddedEncodedMaxByteLengthKey.getEcP256PublicKey().getX().size()); - assertEquals(33, paddedEncodedMaxByteLengthKey.getEcP256PublicKey().getY().size()); - assertEquals(0, paddedEncodedMaxByteLengthKey.getEcP256PublicKey().getX().byteAt(0)); - assertEquals(0, paddedEncodedMaxByteLengthKey.getEcP256PublicKey().getY().byteAt(0)); - assertEquals(33, encodedMaxByteLengthKey.getEcP256PublicKey().getX().size()); - assertEquals(33, encodedMaxByteLengthKey.getEcP256PublicKey().getY().size()); - - assertEquals(32, encodedNotMaxByteLengthKey.getEcP256PublicKey().getX().size()); - assertEquals(32, encodedNotMaxByteLengthKey.getEcP256PublicKey().getY().size()); - assertEquals(0, paddedEncodedNotMaxByteLengthKey.getEcP256PublicKey().getX().byteAt(0)); - assertEquals(0, paddedEncodedNotMaxByteLengthKey.getEcP256PublicKey().getY().byteAt(0)); - assertEquals(33, paddedEncodedNotMaxByteLengthKey.getEcP256PublicKey().getX().size()); - assertEquals(33, paddedEncodedNotMaxByteLengthKey.getEcP256PublicKey().getY().size()); - } - - @SuppressInsecureCipherModeCheckerNoReview - public void testWrongPublicKeyType() throws Exception { - KeyPairGenerator dsaGen = KeyPairGenerator.getInstance("DSA"); - dsaGen.initialize(512); - PublicKey pk = dsaGen.generateKeyPair().getPublic(); - - if (!isAndroidOsWithoutEcSupport()) { - // Try to encode it as EC - try { - PublicKeyProtoUtil.encodeEcPublicKey(pk); - fail(); - } catch (IllegalArgumentException expected) { - } - - try { - PublicKeyProtoUtil.encodePaddedEcPublicKey(pk); - fail(); - } catch (IllegalArgumentException expected) { - } - } - - // Try to encode it as RSA - try { - PublicKeyProtoUtil.encodeRsa2048PublicKey(pk); - fail(); - } catch (IllegalArgumentException expected) { - } - - // Try to encode it as DH - try { - PublicKeyProtoUtil.encodeDh2048PublicKey(pk); - fail(); - } catch (IllegalArgumentException expected) { - } - - // Try to encode it as Generic - try { - PublicKeyProtoUtil.encodePublicKey(pk); - fail(); - } catch (IllegalArgumentException expected) { - } - } - - public void testEcPublicKeyProtoInvalidEncoding() throws Exception { - if (isAndroidOsWithoutEcSupport()) { - return; - } - - EcP256PublicKey validProto = PublicKeyProtoUtil.encodeEcPublicKey(ecPublicKey); - EcP256PublicKey.Builder invalidProto = EcP256PublicKey.newBuilder(validProto); - - // Mess up the X coordinate by repeating it twice - byte[] newX = - CryptoOps.concat(validProto.getX().toByteArray(), validProto.getX().toByteArray()); - checkParsingFailsFor(invalidProto.setX(ByteString.copyFrom(newX)).build()); - - // Mess up the Y coordinate by erasing it - invalidProto = EcP256PublicKey.newBuilder(validProto); - checkParsingFailsFor(invalidProto.setY(ByteString.EMPTY).build()); - - // Pick a point that is likely not on the curve by copying X over Y - invalidProto = EcP256PublicKey.newBuilder(validProto); - checkParsingFailsFor(invalidProto.setY(validProto.getX()).build()); - - // Try the point (0, 0) - invalidProto = EcP256PublicKey.newBuilder(validProto); - checkParsingFailsFor( - invalidProto - .setX(ByteString.copyFrom(ZERO_BYTE)) - .setY(ByteString.copyFrom(ZERO_BYTE)) - .build()); - } - - private void checkParsingFailsFor(EcP256PublicKey invalid) { - try { - // Should fail to decode - PublicKeyProtoUtil.parseEcPublicKey(invalid); - fail(); - } catch (InvalidKeySpecException expected) { - } - } - - public void testSimpleRsaPublicKeyProtoInvalidEncoding() throws Exception { - SimpleRsaPublicKey validProto = PublicKeyProtoUtil.encodeRsa2048PublicKey(rsaPublicKey); - SimpleRsaPublicKey.Builder invalidProto; - - // Double the number of bits in the modulus - invalidProto = SimpleRsaPublicKey.newBuilder(validProto); - byte[] newN = - CryptoOps.concat(validProto.getN().toByteArray(), validProto.getN().toByteArray()); - checkParsingFailsFor(invalidProto.setN(ByteString.copyFrom(newN)).build()); - - // Set the modulus to 0 - invalidProto = SimpleRsaPublicKey.newBuilder(validProto); - checkParsingFailsFor(invalidProto.setN(ByteString.copyFrom(ZERO_BYTE)).build()); - - // Set the modulus to 65537 (way too small) - invalidProto = SimpleRsaPublicKey.newBuilder(validProto); - checkParsingFailsFor( - invalidProto.setN(ByteString.copyFrom(BigInteger.valueOf(65537).toByteArray())).build()); - } - - private static void checkParsingFailsFor(SimpleRsaPublicKey invalid) { - try { - // Should fail to decode - PublicKeyProtoUtil.parseRsa2048PublicKey(invalid); - fail(); - } catch (InvalidKeySpecException expected) { - } - } - - public void testSimpleDhPublicKeyProtoInvalidEncoding() throws Exception { - DhPublicKey validProto = PublicKeyProtoUtil.encodeDh2048PublicKey(dhPublicKey); - DhPublicKey.Builder invalidProto; - - // Double the number of bits in the public element encoding - invalidProto = DhPublicKey.newBuilder(validProto); - byte[] newY = - CryptoOps.concat(validProto.getY().toByteArray(), validProto.getY().toByteArray()); - checkParsingFailsFor(invalidProto.setY(ByteString.copyFrom(newY)).build()); - - // Set the public element to 0 - invalidProto = DhPublicKey.newBuilder(validProto); - checkParsingFailsFor(invalidProto.setY(ByteString.copyFrom(ZERO_BYTE)).build()); - } - - private static void checkParsingFailsFor(DhPublicKey invalid) { - try { - // Should fail to decode - PublicKeyProtoUtil.parseDh2048PublicKey(invalid); - fail(); - } catch (InvalidKeySpecException expected) { - } - } - - public void testDhKeyAgreementWorks() throws Exception { - int minExpectedSecretLength = (PublicKeyProtoUtil.DH_P.bitLength() / 8) - 4; - - KeyPair clientKeyPair = PublicKeyProtoUtil.generateDh2048KeyPair(); - KeyPair serverKeyPair = PublicKeyProtoUtil.generateDh2048KeyPair(); - BigInteger clientY = ((DHPublicKey) clientKeyPair.getPublic()).getY(); - BigInteger serverY = ((DHPublicKey) serverKeyPair.getPublic()).getY(); - assertFalse(clientY.equals(serverY)); // DHPublicKeys should not be equal - - // Run client side of the key exchange - byte[] clientSecret = doDhAgreement(clientKeyPair.getPrivate(), serverKeyPair.getPublic()); - assert (clientSecret.length >= minExpectedSecretLength); - - // Run the server side of the key exchange - byte[] serverSecret = doDhAgreement(serverKeyPair.getPrivate(), clientKeyPair.getPublic()); - assert (serverSecret.length >= minExpectedSecretLength); - - assertTrue(Arrays.equals(clientSecret, serverSecret)); - } - - public void testDh2048PrivateKeyEncoding() throws Exception { - KeyPair testPair = PublicKeyProtoUtil.generateDh2048KeyPair(); - DHPrivateKey sk = (DHPrivateKey) testPair.getPrivate(); - DHPrivateKey skParsed = - PublicKeyProtoUtil.parseDh2048PrivateKey(PublicKeyProtoUtil.encodeDh2048PrivateKey(sk)); - assertEquals(sk.getX(), skParsed.getX()); - assertEquals(sk.getParams().getP(), skParsed.getParams().getP()); - assertEquals(sk.getParams().getG(), skParsed.getParams().getG()); - } - - public void testParseEcPublicKeyOnLegacyPlatform() { - if (!PublicKeyProtoUtil.isLegacyCryptoRequired()) { - return; // This test only runs on legacy platforms - } - byte[] pointBytes = { - 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, - 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, - 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, - 1, 2 - }; - - try { - PublicKeyProtoUtil.parseEcPublicKey( - EcP256PublicKey.newBuilder() - .setX(ByteString.copyFrom(pointBytes)) - .setY(ByteString.copyFrom(pointBytes)) - .build()); - fail(); - } catch (InvalidKeySpecException expected) { - // Should get this specific exception when EC doesn't work - } - } - - public void testIsLegacyCryptoRequired() { - assertEquals(isAndroidOsWithoutEcSupport(), PublicKeyProtoUtil.isLegacyCryptoRequired()); - } - - /** @return true if running on an Android OS that doesn't support Elliptic Curve algorithms */ - public static boolean isAndroidOsWithoutEcSupport() { - try { - Class<?> clazz = ClassLoader.getSystemClassLoader().loadClass("android.os.Build$VERSION"); - int sdkVersion = clazz.getField("SDK_INT").getInt(null); - if (sdkVersion < PublicKeyProtoUtil.ANDROID_HONEYCOMB_SDK_INT) { - return true; - } - } catch (ClassNotFoundException e) { - // Not running on Android - return false; - } catch (SecurityException e) { - throw new AssertionError(e); - } catch (NoSuchFieldException e) { - throw new AssertionError(e); - } catch (IllegalArgumentException e) { - throw new AssertionError(e); - } catch (IllegalAccessException e) { - throw new AssertionError(e); - } - return false; - } - - @SuppressInsecureCipherModeCheckerNoReview - private static byte[] doDhAgreement(PrivateKey secretKey, PublicKey peerKey) throws Exception { - KeyAgreement agreement = KeyAgreement.getInstance("DH"); - agreement.init(secretKey); - agreement.doPhase(peerKey, true); - return agreement.generateSecret(); - } - - private static ECPublicKey buildEcPublicKey(byte[] encodedX, byte[] encodedY) throws Exception { - try { - BigInteger wX = new BigInteger(encodedX); - BigInteger wY = new BigInteger(encodedY); - return (ECPublicKey) - KeyFactory.getInstance("EC") - .generatePublic( - new ECPublicKeySpec( - new ECPoint(wX, wY), - ((ECPublicKey) PublicKeyProtoUtil.generateEcP256KeyPair().getPublic()) - .getParams())); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } - } -} diff --git a/src/main/javatest/com/google/security/cryptauth/lib/securemessage/SecureMessageSimpleTestVectorTest.java b/src/main/javatest/com/google/security/cryptauth/lib/securemessage/SecureMessageSimpleTestVectorTest.java deleted file mode 100644 index 285b259..0000000 --- a/src/main/javatest/com/google/security/cryptauth/lib/securemessage/SecureMessageSimpleTestVectorTest.java +++ /dev/null @@ -1,403 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.security.cryptauth.lib.securemessage; - -import com.google.protobuf.InvalidProtocolBufferException; -import com.google.security.cryptauth.lib.securemessage.CryptoOps.EncType; -import com.google.security.cryptauth.lib.securemessage.CryptoOps.SigType; -import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.GenericPublicKey; -import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.Header; -import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.HeaderAndBody; -import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.SecureMessage; -import java.security.KeyFactory; -import java.security.KeyPair; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.PKCS8EncodedKeySpec; -import java.util.Arrays; -import javax.crypto.KeyGenerator; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; -import junit.framework.TestCase; - -/** - * Tests the library against some very basic test vectors, to help ensure wire-format - * compatibility is not broken. - */ -public class SecureMessageSimpleTestVectorTest extends TestCase { - - private static final KeyFactory EC_KEY_FACTORY; - static { - try { - if (PublicKeyProtoUtil.isLegacyCryptoRequired()) { - EC_KEY_FACTORY = null; - } else { - EC_KEY_FACTORY = KeyFactory.getInstance("EC"); - } - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - private static final byte[] TEST_ASSOCIATED_DATA = { - 11, 22, 33, 44, 55 - }; - private static final byte[] TEST_METADATA = { - 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28 - }; - private static final byte[] TEST_VKID = { - 0, 0, 1 - }; - private static final byte[] TEST_DKID = { - -1, -1, 0, - }; - private static final byte[] TEST_MESSAGE = { - 0, 99, 1, 98, 2, 97, 3, 96, 4, 95, 5, 94, 6, 93, 7, 92, 8, 91, 9, 90 - }; - - // The following fields are initialized below, in a static block that contains auto-generated test - // vectors. Initialization can't just be done inline due to code that throws checked exceptions. - private static final PublicKey TEST_EC_PUBLIC_KEY; - private static final PrivateKey TEST_EC_PRIVATE_KEY; - private static final SecretKey TEST_KEY1; - private static final SecretKey TEST_KEY2; - private static final byte[] TEST_VECTOR_ECDSA_ONLY; - private static final byte[] TEST_VECTOR_ECDSA_AND_AES; - private static final byte[] TEST_VECTOR_HMAC_AND_AES_SAME_KEYS; - private static final byte[] TEST_VECTOR_HMAC_AND_AES_DIFFERENT_KEYS; - - public void testEcdsaOnly() throws Exception { - if (PublicKeyProtoUtil.isLegacyCryptoRequired()) { - // On older Android platforms we can't run this test. - return; - } - SecureMessage testVector = SecureMessage.parseFrom(TEST_VECTOR_ECDSA_ONLY); - Header unverifiedHeader = SecureMessageParser.getUnverifiedHeader(testVector); - HeaderAndBody headerAndBody = SecureMessageParser.parseSignedCleartextMessage( - testVector, TEST_EC_PUBLIC_KEY, SigType.ECDSA_P256_SHA256, TEST_ASSOCIATED_DATA); - assertTrue(Arrays.equals( - unverifiedHeader.toByteArray(), - headerAndBody.getHeader().toByteArray())); - assertTrue(Arrays.equals(TEST_MESSAGE, headerAndBody.getBody().toByteArray())); - assertEquals(TEST_ASSOCIATED_DATA.length, unverifiedHeader.getAssociatedDataLength()); - assertTrue(Arrays.equals(TEST_METADATA, unverifiedHeader.getPublicMetadata().toByteArray())); - assertTrue(Arrays.equals(TEST_VKID, unverifiedHeader.getVerificationKeyId().toByteArray())); - assertFalse(unverifiedHeader.hasDecryptionKeyId()); - } - - public void testEcdsaAndAes() throws Exception { - if (PublicKeyProtoUtil.isLegacyCryptoRequired()) { - // On older Android platforms we can't run this test. - return; - } - SecureMessage testVector = SecureMessage.parseFrom(TEST_VECTOR_ECDSA_AND_AES); - Header unverifiedHeader = SecureMessageParser.getUnverifiedHeader(testVector); - HeaderAndBody headerAndBody = SecureMessageParser.parseSignCryptedMessage( - testVector, - TEST_EC_PUBLIC_KEY, - SigType.ECDSA_P256_SHA256, - TEST_KEY1, - EncType.AES_256_CBC, - TEST_ASSOCIATED_DATA); - assertTrue(Arrays.equals( - unverifiedHeader.toByteArray(), - headerAndBody.getHeader().toByteArray())); - assertTrue(Arrays.equals(TEST_MESSAGE, headerAndBody.getBody().toByteArray())); - assertEquals(TEST_ASSOCIATED_DATA.length, unverifiedHeader.getAssociatedDataLength()); - assertTrue(Arrays.equals(TEST_METADATA, unverifiedHeader.getPublicMetadata().toByteArray())); - assertTrue(Arrays.equals(TEST_VKID, unverifiedHeader.getVerificationKeyId().toByteArray())); - assertTrue(Arrays.equals(TEST_DKID, unverifiedHeader.getDecryptionKeyId().toByteArray())); - } - - public void testHmacAndAesSameKeys() throws Exception { - SecureMessage testVector = SecureMessage.parseFrom(TEST_VECTOR_HMAC_AND_AES_SAME_KEYS); - Header unverifiedHeader = SecureMessageParser.getUnverifiedHeader(testVector); - - HeaderAndBody headerAndBody = SecureMessageParser.parseSignCryptedMessage( - testVector, - TEST_KEY1, - SigType.HMAC_SHA256, - TEST_KEY1, - EncType.AES_256_CBC, - TEST_ASSOCIATED_DATA); - assertTrue(Arrays.equals( - unverifiedHeader.toByteArray(), - headerAndBody.getHeader().toByteArray())); - assertTrue(Arrays.equals(TEST_MESSAGE, headerAndBody.getBody().toByteArray())); - assertEquals(TEST_ASSOCIATED_DATA.length, unverifiedHeader.getAssociatedDataLength()); - assertTrue(Arrays.equals(TEST_METADATA, unverifiedHeader.getPublicMetadata().toByteArray())); - assertTrue(Arrays.equals(TEST_VKID, unverifiedHeader.getVerificationKeyId().toByteArray())); - assertTrue(Arrays.equals(TEST_DKID, unverifiedHeader.getDecryptionKeyId().toByteArray())); - } - - public void testHmacAndAesDifferentKeys() throws Exception { - SecureMessage testVector = SecureMessage.parseFrom(TEST_VECTOR_HMAC_AND_AES_DIFFERENT_KEYS); - Header unverifiedHeader = SecureMessageParser.getUnverifiedHeader(testVector); - HeaderAndBody headerAndBody = SecureMessageParser.parseSignCryptedMessage( - testVector, - TEST_KEY1, - SigType.HMAC_SHA256, - TEST_KEY2, - EncType.AES_256_CBC, - TEST_ASSOCIATED_DATA); - assertTrue(Arrays.equals( - unverifiedHeader.toByteArray(), - headerAndBody.getHeader().toByteArray())); - assertTrue(Arrays.equals(TEST_MESSAGE, headerAndBody.getBody().toByteArray())); - assertEquals(TEST_ASSOCIATED_DATA.length, unverifiedHeader.getAssociatedDataLength()); - assertTrue(Arrays.equals(TEST_METADATA, unverifiedHeader.getPublicMetadata().toByteArray())); - assertTrue(Arrays.equals(TEST_VKID, unverifiedHeader.getVerificationKeyId().toByteArray())); - assertTrue(Arrays.equals(TEST_DKID, unverifiedHeader.getDecryptionKeyId().toByteArray())); - } - - /** - * This code emits the test vectors to {@code System.out}. It will not generate fresh test - * vectors unless an existing test vector is set to {@code null}, but it contains all of the code - * used to the generate the test vector values. Ideally, existing test vectors should never be - * regenerated, but having this code available should make it easier to add new test vectors. - */ - public void testGenerateTestVectorsPseudoTest() throws Exception { - if (PublicKeyProtoUtil.isLegacyCryptoRequired()) { - // On older Android platforms we can't run this test. - return; - } - System.out.printf(" static {\n try {\n"); - String indent = " "; - PublicKey testEcPublicKey = TEST_EC_PUBLIC_KEY; - PrivateKey testEcPrivateKey = TEST_EC_PRIVATE_KEY; - if (testEcPublicKey == null) { - KeyPair testEcKeyPair = PublicKeyProtoUtil.generateEcP256KeyPair(); - testEcPublicKey = testEcKeyPair.getPublic(); - testEcPrivateKey = testEcKeyPair.getPrivate(); - } - System.out.printf("%s%s = parsePublicKey(new byte[] %s);\n", - indent, - "TEST_EC_PUBLIC_KEY", - byteArrayToJavaCode(indent, encodePublicKey(testEcPublicKey))); - System.out.printf("%s%s = parseEcPrivateKey(new byte[] %s);\n", - indent, - "TEST_EC_PRIVATE_KEY", - byteArrayToJavaCode(indent, encodeEcPrivateKey(testEcPrivateKey))); - - SecretKey testKey1 = TEST_KEY1; - if (testKey1 == null) { - testKey1 = makeAesKey(); - } - System.out.printf("%s%s = new SecretKeySpec(new byte[] %s, \"AES\");\n", - indent, - "TEST_KEY1", - byteArrayToJavaCode(indent, testKey1.getEncoded())); - - SecretKey testKey2 = TEST_KEY2; - if (testKey2 == null) { - testKey2 = makeAesKey(); - } - System.out.printf("%s%s = new SecretKeySpec(new byte[] %s, \"AES\");\n", - indent, - "TEST_KEY2", - byteArrayToJavaCode(indent, testKey2.getEncoded())); - - byte[] testVectorEcdsaOnly = TEST_VECTOR_ECDSA_ONLY; - if (testVectorEcdsaOnly == null) { - testVectorEcdsaOnly = new SecureMessageBuilder() - .setAssociatedData(TEST_ASSOCIATED_DATA) - .setPublicMetadata(TEST_METADATA) - .setVerificationKeyId(TEST_VKID) - .buildSignedCleartextMessage( - testEcPrivateKey, SigType.ECDSA_P256_SHA256, TEST_MESSAGE).toByteArray(); - } - printInitializerFor(indent, "TEST_VECTOR_ECDSA_ONLY", testVectorEcdsaOnly); - - byte[] testVectorEcdsaAndAes = TEST_VECTOR_ECDSA_AND_AES; - if (testVectorEcdsaAndAes == null) { - testVectorEcdsaAndAes = new SecureMessageBuilder() - .setAssociatedData(TEST_ASSOCIATED_DATA) - .setDecryptionKeyId(TEST_DKID) - .setPublicMetadata(TEST_METADATA) - .setVerificationKeyId(TEST_VKID) - .buildSignCryptedMessage( - testEcPrivateKey, - SigType.ECDSA_P256_SHA256, - testKey1, - EncType.AES_256_CBC, - TEST_MESSAGE).toByteArray(); - } - printInitializerFor(indent, "TEST_VECTOR_ECDSA_AND_AES", testVectorEcdsaAndAes); - - byte[] testVectorHmacAndAesSameKeys = TEST_VECTOR_HMAC_AND_AES_SAME_KEYS; - if (testVectorHmacAndAesSameKeys == null) { - testVectorHmacAndAesSameKeys = new SecureMessageBuilder() - .setAssociatedData(TEST_ASSOCIATED_DATA) - .setDecryptionKeyId(TEST_DKID) - .setPublicMetadata(TEST_METADATA) - .setVerificationKeyId(TEST_VKID) - .buildSignCryptedMessage( - testKey1, - SigType.HMAC_SHA256, - testKey1, - EncType.AES_256_CBC, - TEST_MESSAGE).toByteArray(); - } - printInitializerFor(indent, "TEST_VECTOR_HMAC_AND_AES_SAME_KEYS", testVectorHmacAndAesSameKeys); - - byte[] testVectorHmacAndAesDifferentKeys = TEST_VECTOR_HMAC_AND_AES_DIFFERENT_KEYS; - if (testVectorHmacAndAesDifferentKeys == null) { - testVectorHmacAndAesDifferentKeys = new SecureMessageBuilder() - .setAssociatedData(TEST_ASSOCIATED_DATA) - .setDecryptionKeyId(TEST_DKID) - .setPublicMetadata(TEST_METADATA) - .setVerificationKeyId(TEST_VKID) - .buildSignCryptedMessage( - testKey1, - SigType.HMAC_SHA256, - testKey2, - EncType.AES_256_CBC, - TEST_MESSAGE).toByteArray(); - } - printInitializerFor( - indent, "TEST_VECTOR_HMAC_AND_AES_DIFFERENT_KEYS", testVectorHmacAndAesDifferentKeys); - - System.out.printf( - " } catch (Exception e) {\n throw new RuntimeException(e);\n }\n }\n"); - } - - private SecretKey makeAesKey() throws NoSuchAlgorithmException { - KeyGenerator aesKeygen = KeyGenerator.getInstance("AES"); - aesKeygen.init(256); - return aesKeygen.generateKey(); - } - - private void printInitializerFor(String indent, String name, byte[] value) { - System.out.printf("%s%s = new byte[] %s;\n", - indent, - name, - byteArrayToJavaCode(indent, value)); - } - - private static String byteArrayToJavaCode(String lineIndent, byte[] array) { - String newline = "\n" + lineIndent + " "; - String unwrappedArray = Arrays.toString(array).replace("[", "").replace("]", ""); - int wrapAfter = 16; - int count = wrapAfter; - StringBuilder result = new StringBuilder("{"); - for (String entry : unwrappedArray.split(" ")) { - if (++count > wrapAfter) { - result.append(newline); - count = 0; - } else { - result.append(" "); - } - result.append(entry); - } - result.append(" }"); - return result.toString(); - } - - private static byte[] encodePublicKey(PublicKey pk) { - return PublicKeyProtoUtil.encodePublicKey(pk).toByteArray(); - } - - private static PublicKey parsePublicKey(byte[] encodedPk) - throws InvalidKeySpecException, InvalidProtocolBufferException { - GenericPublicKey gpk = GenericPublicKey.parseFrom(encodedPk); - if (PublicKeyProtoUtil.isLegacyCryptoRequired() - && gpk.getType() == SecureMessageProto.PublicKeyType.EC_P256) { - return null; - } - return PublicKeyProtoUtil.parsePublicKey(gpk); - } - - private static byte[] encodeEcPrivateKey(PrivateKey sk) { - return sk.getEncoded(); - } - - private static PrivateKey parseEcPrivateKey(byte[] sk) throws InvalidKeySpecException { - if (PublicKeyProtoUtil.isLegacyCryptoRequired()) { - return null; - } - return EC_KEY_FACTORY.generatePrivate(new PKCS8EncodedKeySpec(sk)); - } - - // The following block of code was automatically generated by cut and pasting the output of the - // generateTestVectorsPseudoTest, which should reliably emit this same test vectors. Please - // DO NOT DELETE any of these existing test vectors unless you _really_ know what you are doing. - // - // --- AUTO GENERATED CODE BEGINS HERE --- - static { - try { - TEST_EC_PUBLIC_KEY = parsePublicKey(new byte[] { - 8, 1, 18, 70, 10, 33, 0, -109, 9, 5, 8, -89, -3, -68, -86, -19, 17, - -126, -11, -95, 35, 101, 102, -57, -84, -118, 73, 83, 66, -62, -49, -91, 71, -19, - 52, 123, 113, 119, 45, 18, 33, 0, -65, -19, 83, -66, -12, 62, 102, -67, 116, - 64, 42, 55, -84, -101, 90, -106, 113, -89, -30, 57, -112, 96, -99, -126, 14, 83, - 41, 95, -24, -114, 23, -5 }); - TEST_EC_PRIVATE_KEY = parseEcPrivateKey(new byte[] { - 48, 65, 2, 1, 0, 48, 19, 6, 7, 42, -122, 72, -50, 61, 2, 1, 6, - 8, 42, -122, 72, -50, 61, 3, 1, 7, 4, 39, 48, 37, 2, 1, 1, 4, - 32, 26, -82, -61, -86, -59, -8, 2, -62, -17, -20, 122, 3, 85, -102, -76, 81, - 51, 39, -9, 12, 99, -117, 127, 19, 121, 109, -31, -49, 110, 121, 76, -107 }); - TEST_KEY1 = new SecretKeySpec(new byte[] { - -89, 105, 62, -41, -75, 78, 70, 110, -62, -58, -80, -81, -99, -62, 39, 38, 37, - -7, -112, -83, 81, 23, 125, -72, -100, 103, -34, -23, -68, 21, -46, -104 }, "AES"); - TEST_KEY2 = new SecretKeySpec(new byte[] { - -6, 48, 107, 61, -99, -89, 111, 33, 70, 54, -13, 111, 81, -120, 50, 89, -119, - -113, -114, 63, 12, -68, 40, 42, -77, -58, -49, 18, 69, 91, -20, -65 }, "AES"); - TEST_VECTOR_ECDSA_ONLY = new byte[] { - 10, 56, 10, 32, 8, 2, 16, 1, 26, 3, 0, 0, 1, 50, 19, 10, 11, - 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, - 56, 5, 18, 20, 0, 99, 1, 98, 2, 97, 3, 96, 4, 95, 5, 94, 6, - 93, 7, 92, 8, 91, 9, 90, 18, 72, 48, 70, 2, 33, 0, -79, 59, 50, - 21, 54, 61, -92, 77, -34, -77, -45, -105, 107, -28, -19, 91, -78, 120, 68, 33, - 11, -76, -1, 50, 64, -127, -78, 6, 108, 115, -13, 126, 2, 33, 0, -72, -44, - 52, 93, 105, 109, -127, -111, 11, 33, -111, 97, -114, 9, 117, -68, -45, 64, 63, - 43, 60, -44, -89, -107, -59, -45, 56, 100, -66, -40, 46, -60 }; - TEST_VECTOR_ECDSA_AND_AES = new byte[] { - 10, 107, 10, 55, 8, 2, 16, 2, 26, 3, 0, 0, 1, 34, 3, -1, -1, - 0, 42, 16, -86, 16, 55, -8, -85, -47, -77, -36, -127, 44, -10, -44, -63, 115, - -111, 26, 50, 19, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, - 23, 24, 25, 26, 27, 28, 56, 5, 18, 48, -110, 23, -67, 122, -118, 96, -4, - 32, -113, -104, -107, -16, 76, 37, -61, -67, -63, 90, 38, 96, -47, -105, 56, -34, - 50, -30, 82, 25, 100, 36, 69, 50, 68, 60, 38, 96, -108, -49, -73, -10, -62, - -76, -45, -105, -86, 93, 28, 34, 18, 70, 48, 68, 2, 33, 0, -87, -103, 11, - -70, 34, 33, -41, 90, -83, -74, 19, -13, 127, -43, -116, -32, 88, -13, 125, -122, - 56, -21, 79, 47, 101, 89, -80, -43, 102, 92, 4, -15, 2, 31, 109, -69, 35, - 21, 44, -27, -77, 32, 17, -90, -68, 113, 55, -24, -122, 40, 81, 51, 0, -84, - -29, -12, -26, 73, 105, -32, 116, -28, 84, -116, -117 }; - TEST_VECTOR_HMAC_AND_AES_SAME_KEYS = new byte[] { - 10, 91, 10, 55, 8, 1, 16, 2, 26, 3, 0, 0, 1, 34, 3, -1, -1, - 0, 42, 16, -110, 48, 67, 67, -31, 24, -42, 13, -44, -109, 6, 113, 34, -70, - 121, 6, 50, 19, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, - 23, 24, 25, 26, 27, 28, 56, 5, 18, 32, -44, -102, -16, 123, 113, -75, 88, - -33, 118, 25, 60, -65, 109, 26, -70, -123, 58, -114, 126, 8, 106, -28, 65, -38, - -4, 68, -78, -91, 49, -13, 22, -122, 18, 32, 20, -120, -113, -76, 85, -35, -53, - 37, -18, 66, -38, 32, 10, 30, 89, 112, -39, -27, 24, 93, -36, -100, -127, -79, - 94, -7, -19, -41, -47, -29, 1, 12 }; - TEST_VECTOR_HMAC_AND_AES_DIFFERENT_KEYS = new byte[] { - 10, 107, 10, 55, 8, 1, 16, 2, 26, 3, 0, 0, 1, 34, 3, -1, -1, - 0, 42, 16, -96, -7, 39, 79, -37, 40, 1, -30, 97, 0, 123, -7, -124, -75, - -127, -18, 50, 19, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, - 23, 24, 25, 26, 27, 28, 56, 5, 18, 48, 90, 40, -48, -113, 84, -32, 47, - 98, 54, -128, 127, 115, 32, 87, -86, 4, -26, 99, 9, -88, 13, 77, 127, 114, - -48, -117, -94, 96, -86, -105, -123, 11, 116, -69, -83, -110, 3, -10, 0, -34, 72, - 10, -58, 3, -119, -94, 23, -114, 18, 32, -25, -126, 95, 125, -110, -62, -36, -78, - 97, 72, -54, -114, 97, -68, -46, 107, 53, 55, -57, 88, 127, -20, -23, 80, -9, - -91, 115, 42, 24, 49, -76, -111 }; - } catch (Exception e) { - throw new RuntimeException(e); - } - } -} diff --git a/src/main/javatest/com/google/security/cryptauth/lib/securemessage/SecureMessageTest.java b/src/main/javatest/com/google/security/cryptauth/lib/securemessage/SecureMessageTest.java deleted file mode 100644 index 40e5091..0000000 --- a/src/main/javatest/com/google/security/cryptauth/lib/securemessage/SecureMessageTest.java +++ /dev/null @@ -1,766 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.security.cryptauth.lib.securemessage; - -import com.google.protobuf.ByteString; -import com.google.protobuf.InvalidProtocolBufferException; -import com.google.protobuf.UninitializedMessageException; -import com.google.security.cryptauth.lib.securemessage.CryptoOps.EncType; -import com.google.security.cryptauth.lib.securemessage.CryptoOps.SigType; -import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.EcP256PublicKey; -import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.GenericPublicKey; -import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.Header; -import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.HeaderAndBody; -import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.SecureMessage; -import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.SimpleRsaPublicKey; -import java.security.InvalidKeyException; -import java.security.Key; -import java.security.KeyPair; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.SecureRandom; -import java.security.SignatureException; -import java.security.spec.InvalidKeySpecException; -import java.util.Arrays; -import java.util.List; -import javax.crypto.KeyGenerator; -import javax.crypto.SecretKey; -import junit.framework.TestCase; - -/** - * Tests for the SecureMessageBuilder and SecureMessageParser classes. - */ -public class SecureMessageTest extends TestCase { - // Not to be used when generating cross-platform test vectors (due to default charset encoding) - public static final byte[] TEST_MESSAGE = - "Testing 1 2 3... Testing 1 2 3... Testing 1 2 3...".getBytes(); - - private static final byte[] TEST_KEY_ID = - { 0, 1, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 }; - // Not to be used when generating cross-platform test vectors (due to default charset encoding) - private static final byte[] TEST_METADATA = "Some protocol metadata string goes here".getBytes(); - private static final byte[] TEST_ASSOCIATED_DATA = { - 1, 0, 2, 0, 3, 0, 4, 0, 5, 0, 6, 0, 7, 0, 8, 0, 9, 0, 10, 0, 11 }; - private static final byte[] ZERO_BYTE = { 0 }; - private static final byte[] EMPTY_BYTES = { }; - - private static final List<byte[]> MESSAGE_VALUES = Arrays.asList( - EMPTY_BYTES, - TEST_MESSAGE - ); - - private static final List<byte[]> KEY_ID_VALUES = Arrays.asList( - null, - EMPTY_BYTES, - TEST_KEY_ID); - - private static final List<byte[]> METADATA_VALUES = Arrays.asList( - null, - EMPTY_BYTES, - TEST_METADATA - ); - - private static final List<byte[]> ASSOCIATED_DATA_VALUES = Arrays.asList( - null, - ZERO_BYTE, - TEST_ASSOCIATED_DATA); - - private byte[] message; - private byte[] metadata; - private byte[] verificationKeyId; - private byte[] decryptionKeyId; - private byte[] associatedData; - private PublicKey ecPublicKey; - private PrivateKey ecPrivateKey; - private PublicKey rsaPublicKey; - private PrivateKey rsaPrivateKey; - private SecretKey aesEncryptionKey; - private SecretKey hmacKey; - private SecureMessageBuilder secureMessageBuilder = new SecureMessageBuilder(); - private SecureRandom rng = new SecureRandom(); - - @Override - public void setUp() { - message = TEST_MESSAGE; - metadata = null; - verificationKeyId = null; - decryptionKeyId = null; - associatedData = null; - if (!PublicKeyProtoUtil.isLegacyCryptoRequired()) { - KeyPair ecKeyPair = PublicKeyProtoUtil.generateEcP256KeyPair(); - ecPublicKey = ecKeyPair.getPublic(); - ecPrivateKey = ecKeyPair.getPrivate(); - } - KeyPair rsaKeyPair = PublicKeyProtoUtil.generateRSA2048KeyPair(); - rsaPublicKey = rsaKeyPair.getPublic(); - rsaPrivateKey = rsaKeyPair.getPrivate(); - try { - aesEncryptionKey = makeAesKey(); - hmacKey = makeAesKey(); - } catch (NoSuchAlgorithmException e) { - e.printStackTrace(); - fail(); - } - secureMessageBuilder.reset(); - } - - private SecureMessage sign(SigType sigType) throws NoSuchAlgorithmException, InvalidKeyException { - return getPreconfiguredBuilder().buildSignedCleartextMessage( - getSigningKeyFor(sigType), sigType, message); - } - - private SecureMessage signCrypt(SigType sigType, EncType encType) - throws NoSuchAlgorithmException, InvalidKeyException { - return getPreconfiguredBuilder().buildSignCryptedMessage( - getSigningKeyFor(sigType), sigType, aesEncryptionKey, encType, message); - } - - private void verify(SecureMessage signed, SigType sigType) - throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, - InvalidProtocolBufferException { - HeaderAndBody headerAndBody = SecureMessageParser.parseSignedCleartextMessage( - signed, - getVerificationKeyFor(sigType), - sigType, - associatedData); - consistencyCheck(signed, headerAndBody, sigType, EncType.NONE); - } - - private void verifyDecrypt(SecureMessage encryptedAndSigned, SigType sigType, EncType encType) - throws InvalidProtocolBufferException, InvalidKeyException, NoSuchAlgorithmException, - SignatureException { - HeaderAndBody headerAndBody = SecureMessageParser.parseSignCryptedMessage( - encryptedAndSigned, - getVerificationKeyFor(sigType), - sigType, - aesEncryptionKey, - encType, - associatedData); - consistencyCheck(encryptedAndSigned, headerAndBody, sigType, encType); - } - - // A collection of different kinds of "alterations" that can be made to SecureMessage protos. - enum Alteration { - DECRYPTION_KEY_ID, - ENCTYPE, - HEADER_AND_BODY_PROTO, - MESSAGE, - METADATA, - RESIGNCRYPTION_ATTACK, - SIGTYPE, - VERIFICATION_KEY_ID, - ASSOCIATED_DATA_LENGTH, - } - - private void doSignAndVerify(SigType sigType) throws Exception { - System.out.println("BEGIN_TEST -- Testing SigType: " + sigType + " with:"); - System.out.println("VerificationKeyId: " + Arrays.toString(verificationKeyId)); - System.out.println("Metadata: " + Arrays.toString(metadata)); - System.out.println("AssociatedData: " + Arrays.toString(associatedData)); - System.out.println("Message: " + Arrays.toString(message)); - // Positive test cases - SecureMessage signed = sign(sigType); - verify(signed, sigType); - - // Negative test cases - for (Alteration altType : getAlterationsToTest()) { - System.out.println("Testing alteration: " + altType.toString()); - SecureMessage modified = modifyMessage(signed, altType); - try { - verify(modified, sigType); - fail(altType.toString()); - } catch (SignatureException e) { - // We expect this - } - } - - // Try verifying with the wrong associated data - if ((associatedData == null) || (associatedData.length == 0)) { - associatedData = ZERO_BYTE; - } else { - associatedData = null; - } - try { - verify(signed, sigType); - fail("Expected verification to fail due to incorrect associatedData"); - } catch (SignatureException e) { - // We expect this - } - - System.out.println("PASS_TEST -- Testing SigType: " + sigType); - } - - private List<Alteration> getAlterationsToTest() { - if (isRunningInAndroid()) { - // Android is very slow. Only try one alteration attack, intead of all of them. - int randomAlteration = Math.abs(rng.nextInt()) % Alteration.values().length; - return Arrays.asList(Alteration.values()[randomAlteration]); - } else { - // Just try all of them - return Arrays.asList(Alteration.values()); - } - } - - private void doSignCryptAndVerifyDecrypt(SigType sigType) throws Exception { - // For now, EncType is always AES_256_CBC - EncType encType = EncType.AES_256_CBC; - System.out.println("BEGIN_TEST -- Testing SigType: " + sigType - + " EncType: " + encType + " with:"); - System.out.println("DecryptionKeyId: " + Arrays.toString(decryptionKeyId)); - System.out.println("VerificationKeyId: " + Arrays.toString(verificationKeyId)); - System.out.println("Metadata: " + Arrays.toString(metadata)); - System.out.println("AssociatedData: " + Arrays.toString(associatedData)); - System.out.println("Message: " + Arrays.toString(message)); - SecureMessage encryptedAndSigned = null; - encryptedAndSigned = signCrypt(sigType, encType); - verifyDecrypt(encryptedAndSigned, sigType, encType); - - // Negative test cases - for (Alteration altType : getAlterationsToTest()) { - if (skipAlterationTestFor(altType, sigType)) { - System.out.println("Skipping alteration test: " + altType.toString()); - continue; - } - - System.out.println("Testing alteration: " + altType.toString()); - SecureMessage modified = modifyMessage(encryptedAndSigned, altType); - try { - verifyDecrypt(modified, sigType, encType); - fail(); - } catch (SignatureException e) { - // We expect this - } - } - System.out.println("PASS_TEST -- Testing SigType: " + sigType + " EncType: " + encType); - } - - private boolean skipAlterationTestFor(Alteration altType, SigType sigType) { - // The RESIGNCRYPTION_ATTACK may be allowed to succeed iff the same symmetric key - // is being reused for both signature and encryption. - return (altType == Alteration.RESIGNCRYPTION_ATTACK) - // Intentionally testing equality of object address here - && (getVerificationKeyFor(sigType) == aesEncryptionKey); - } - - private SecureMessage modifyMessage(SecureMessage original, Alteration altType) throws Exception { - ByteString bogus = ByteString.copyFromUtf8("BOGUS"); - HeaderAndBody origHAB = HeaderAndBody.parseFrom(original.getHeaderAndBody()); - HeaderAndBody.Builder newHAB = HeaderAndBody.newBuilder(origHAB); - Header.Builder newHeader = Header.newBuilder(origHAB.getHeader()); - Header origHeader = origHAB.getHeader(); - SecureMessage.Builder result = SecureMessage.newBuilder(original); - switch (altType) { - case DECRYPTION_KEY_ID: - if (origHeader.hasDecryptionKeyId()) { - newHeader.clearDecryptionKeyId(); - } else { - newHeader.setDecryptionKeyId(ByteString.copyFrom(TEST_KEY_ID)); - } - break; - case ENCTYPE: - if (origHeader.getEncryptionScheme() == SecureMessageProto.EncScheme.NONE) { - newHeader.setEncryptionScheme(SecureMessageProto.EncScheme.AES_256_CBC); - } else { - newHeader.setEncryptionScheme(SecureMessageProto.EncScheme.NONE); - } - break; - case HEADER_AND_BODY_PROTO: - // Substitute a junk byte string instead of the HeeaderAndBody proto message - return result.setHeaderAndBody(bogus).build(); - case MESSAGE: - byte[] origBody = origHAB.getBody().toByteArray(); - if (origBody.length > 0) { - // Lop off trailing byte of the body - byte[] truncatedBody = CryptoOps.subarray(origBody, 0, origBody.length - 1); - newHAB.setBody(ByteString.copyFrom(truncatedBody)); - } else { - newHAB.setBody(bogus); - } - break; - case METADATA: - if (origHeader.hasPublicMetadata()) { - newHeader.clearPublicMetadata(); - } else { - newHeader.setPublicMetadata(bogus); - } - break; - case RESIGNCRYPTION_ATTACK: - // Simulate stripping a signature, and re-signing a message to see if it will be decrypted. - newHeader - .setVerificationKeyId(bogus) - // In case original was cleartext - .setEncryptionScheme(SecureMessageProto.EncScheme.AES_256_CBC); - - // Now that we've mildly changed the header, compute a new signature for it. - newHAB.setHeader(newHeader.build()); - byte[] headerAndBodyBytes = newHAB.build().toByteArray(); - result.setHeaderAndBody(ByteString.copyFrom(headerAndBodyBytes)); - SigType sigType = SigType.valueOf(origHeader.getSignatureScheme()); - // Note that in all cases where this attack applies, the associatedData is not normally - // used directly inside the signature (but rather inside the inner ciphertext). - result.setSignature(ByteString.copyFrom(CryptoOps.sign( - sigType, getSigningKeyFor(sigType), rng, headerAndBodyBytes))); - return result.build(); - case SIGTYPE: - if (origHeader.getSignatureScheme() == SecureMessageProto.SigScheme.ECDSA_P256_SHA256) { - newHeader - .setSignatureScheme(SecureMessageProto.SigScheme.HMAC_SHA256); - } else { - newHeader - .setSignatureScheme(SecureMessageProto.SigScheme.ECDSA_P256_SHA256); - } - break; - case VERIFICATION_KEY_ID: - if (origHeader.hasVerificationKeyId()) { - newHeader.clearVerificationKeyId(); - } else { - newHeader.setVerificationKeyId( - ByteString.copyFrom(TEST_KEY_ID)); - } - break; - case ASSOCIATED_DATA_LENGTH: - int adLength = origHeader.getAssociatedDataLength(); - switch (adLength) { - case 0: - newHeader.setAssociatedDataLength(1); - break; - case 1: - newHeader.setAssociatedDataLength(0); - break; - default: - newHeader.setAssociatedDataLength(adLength - 1); - } - break; - default: - fail("Forgot to implement an alteration attack: " + altType); - break; - } - // Set the header. - newHAB.setHeader(newHeader.build()); - - return result.setHeaderAndBody(ByteString.copyFrom(newHAB.build().toByteArray())) - .build(); - } - - public void testEcDsaSignedOnly() throws Exception { - doTestSignedOnly(SigType.ECDSA_P256_SHA256); - } - - public void testRsaSignedOnly() throws Exception { - doTestSignedOnly(SigType.RSA2048_SHA256); - } - - public void testHmacSignedOnly() throws Exception { - doTestSignedOnly(SigType.HMAC_SHA256); - } - - private void doTestSignedOnly(SigType sigType) throws Exception { - if (isUnsupported(sigType)) { - return; - } - - // decryptionKeyId must be left null for signature-only operation - for (byte[] vkId : KEY_ID_VALUES) { - verificationKeyId = vkId; - for (byte[] md : METADATA_VALUES) { - metadata = md; - for (byte[] ad : ASSOCIATED_DATA_VALUES) { - associatedData = ad; - for (byte[] msg : MESSAGE_VALUES) { - message = msg; - doSignAndVerify(sigType); - } - } - } - } - - // Test that use of a DecryptionKeyId is not allowed for signature-only - try { - decryptionKeyId = TEST_KEY_ID; // Should trigger a failure - doSignAndVerify(sigType); - fail(); - } catch (IllegalStateException expected) { - } -} - - public void testEncryptedAndMACed() throws Exception { - for (byte[] dkId : KEY_ID_VALUES) { - decryptionKeyId = dkId; - for (byte[] vkId : KEY_ID_VALUES) { - verificationKeyId = vkId; - for (byte[] md : METADATA_VALUES) { - metadata = md; - for (byte[] ad : ASSOCIATED_DATA_VALUES) { - associatedData = ad; - for (byte[] msg : MESSAGE_VALUES) { - message = msg; - doSignCryptAndVerifyDecrypt(SigType.HMAC_SHA256); - } - } - } - } - } - } - - public void testEncryptedAndMACedWithSameKey() throws Exception { - hmacKey = aesEncryptionKey; // Re-use the same key for both - testEncryptedAndMACed(); - } - - public void testEncryptedAndEcdsaSigned() throws Exception { - doTestEncryptedAndSigned(SigType.ECDSA_P256_SHA256); - } - - public void testEncryptedAndRsaSigned() throws Exception { - doTestEncryptedAndSigned(SigType.RSA2048_SHA256); - } - - public void doTestEncryptedAndSigned(SigType sigType) throws Exception { - if (isUnsupported(sigType)) { - return; // EC operations aren't supported on older Android releases - } - - for (byte[] dkId : KEY_ID_VALUES) { - decryptionKeyId = dkId; - for (byte[] vkId : KEY_ID_VALUES) { - verificationKeyId = vkId; - if ((verificationKeyId == null) && sigType.isPublicKeyScheme()) { - continue; // Null verificationKeyId is not allowed with public key signcryption - } - for (byte[] md : METADATA_VALUES) { - metadata = md; - for (byte[] ad : ASSOCIATED_DATA_VALUES) { - associatedData = ad; - for (byte[] msg : MESSAGE_VALUES) { - message = msg; - doSignCryptAndVerifyDecrypt(sigType); - } - } - } - } - } - - // Verify that a missing verificationKeyId is not allowed here - try { - verificationKeyId = null; // Should trigger a failure - signCrypt(sigType, EncType.AES_256_CBC); - fail(); - } catch (IllegalStateException expected) { - } - } - - public void testSignCryptionRequiresEncryption() throws Exception { - try { - signCrypt(SigType.RSA2048_SHA256, EncType.NONE); - } catch (IllegalArgumentException expected) { - } - } - - public void testAssociatedData() throws Exception { - // How much extra room might the encoding of AssociatedDataLength take up? - int maxAssociatedDataOverheadBytes = 4; - // How many bytes might normally vary in the encoding length for SecureMessages generated with - // fresh randomness but identical contents (e.g., due to MSBs being 0) - int maxJitter = 2; - verificationKeyId = TEST_KEY_ID; // So that public key signcryption will work - message = TEST_MESSAGE; - - for (SigType sigType : SigType.values()) { - if (isUnsupported(sigType)) { - continue; - } - associatedData = null; - SecureMessage signed = sign(sigType); - int signedLength = signed.toByteArray().length; - associatedData = EMPTY_BYTES; - // Check that EMPTY_BYTES is equivalent to null associated data under verification - verify(signed, sigType); - // We already tested that incorrect associated data fails elsewhere in negative test cases - associatedData = TEST_ASSOCIATED_DATA; - SecureMessage signedWithAssociatedData = sign(sigType); - int signedWithAssociatedDataLength = signedWithAssociatedData.toByteArray().length; - String logInfo = "Testing associated data overhead for signature using: " + sigType - + " signedLength=" + signedLength - + " signedWithAssociatedDataLength=" + signedWithAssociatedDataLength; - System.out.println(logInfo); - assertTrue(logInfo, - signedWithAssociatedData.toByteArray().length - <= signed.toByteArray().length + maxAssociatedDataOverheadBytes + maxJitter); - } - - for (SigType sigType : SigType.values()) { - if (isUnsupported(sigType)) { - continue; - } - associatedData = null; - SecureMessage signCrypted = signCrypt(sigType, EncType.AES_256_CBC); - int signCryptedLength = signCrypted.toByteArray().length; - // Check that EMPTY_BYTES is equivalent to null associated data under verification - associatedData = EMPTY_BYTES; - verifyDecrypt(signCrypted, sigType, EncType.AES_256_CBC); - // We already tested that incorrect associated data fails elsewhere in negative test cases - associatedData = TEST_ASSOCIATED_DATA; - SecureMessage signCryptedWithAssociatedData = signCrypt(sigType, EncType.AES_256_CBC); - int signCryptedWithAssociatedDataLength = signCryptedWithAssociatedData.toByteArray().length; - String logInfo = "Testing associated data overhead for signcryption using: " + sigType - + " signCryptedLength=" + signCryptedLength - + " signCryptedWithAssociatedDataLength=" + signCryptedWithAssociatedDataLength; - System.out.println(logInfo); - assertTrue(logInfo, - signCryptedWithAssociatedData.toByteArray().length - <= signCrypted.toByteArray().length + maxAssociatedDataOverheadBytes + maxJitter); - } - } - - public void testEncryptedAndEcdsaSignedUsingPublicKeyProto() throws Exception { - if (isUnsupported(SigType.ECDSA_P256_SHA256)) { - return; - } - - // Safest usage of SignCryption is to set the VerificationKeyId to an actual representation of - // the verification key. - verificationKeyId = PublicKeyProtoUtil.encodeEcPublicKey(ecPublicKey).toByteArray(); - SecureMessage encryptedAndSigned = signCrypt(SigType.ECDSA_P256_SHA256, EncType.AES_256_CBC); - - // Simulate extracting the verification key ID from the SecureMessage (non-standard usage) - ecPublicKey = - PublicKeyProtoUtil.parseEcPublicKey( - EcP256PublicKey.parseFrom( - SecureMessageParser.getUnverifiedHeader(encryptedAndSigned) - .getVerificationKeyId())); - - // Note that this verification uses the encoded/decoded ecPublicKey value - verifyDecrypt(encryptedAndSigned, SigType.ECDSA_P256_SHA256, EncType.AES_256_CBC); - } - - public void testEncryptedAndRsaSignedUsingPublicKeyProto() throws Exception { - // Safest usage of SignCryption is to set the VerificationKeyId to an actual representation of - // the verification key. - verificationKeyId = PublicKeyProtoUtil.encodeRsa2048PublicKey(rsaPublicKey).toByteArray(); - SecureMessage encryptedAndSigned = signCrypt(SigType.RSA2048_SHA256, EncType.AES_256_CBC); - - // Simulate extracting the verification key ID from the SecureMessage (non-standard usage) - rsaPublicKey = - PublicKeyProtoUtil.parseRsa2048PublicKey( - SimpleRsaPublicKey.parseFrom( - SecureMessageParser.getUnverifiedHeader(encryptedAndSigned) - .getVerificationKeyId())); - - // Note that this verification uses the encoded/decoded SimpleRsaPublicKey value - verifyDecrypt(encryptedAndSigned, SigType.RSA2048_SHA256, EncType.AES_256_CBC); - } - - // TODO(shabsi): The test was only corrupting header but wasn't setting the body. With protolite, - // not setting a required field causes problems. Modify the SecureMessageParser test and - // enable/remove this test. - /* - public void testCorruptUnverifiedHeader() throws Exception { - // Create a sample message - SecureMessage original = signCrypt(SigType.HMAC_SHA256, EncType.AES_256_CBC); - HeaderAndBody originalHAB = HeaderAndBody.parseFrom(original.getHeaderAndBody().toByteArray()); - for (CorruptHeaderType corruptionType : CorruptHeaderType.values()) { - // Mess with the HeaderAndBody field - HeaderAndBody.Builder corruptHAB = HeaderAndBody.newBuilder(originalHAB); - try { - corruptHeaderWith(corruptionType, corruptHAB); - // Construct the corrupted message using the modified HeaderAndBody - SecureMessage.Builder corrupt = SecureMessage.newBuilder(original); - corrupt.setHeaderAndBody(ByteString.copyFrom(corruptHAB.build().toByteArray())).build(); - SecureMessageParser.getUnverifiedHeader(corrupt.build()); - fail("Corrupt header type " + corruptionType + " parsed without error"); - } catch (InvalidProtocolBufferException expected) { - } - } - } - */ - - public void testParseEmptyMessage() throws Exception { - byte[] bogusData = new byte[0]; - - try { - SecureMessageParser.parseSignedCleartextMessage( - SecureMessage.parseFrom(bogusData), - aesEncryptionKey, - SigType.HMAC_SHA256); - fail("Empty message verified without error"); - } catch (SignatureException | UninitializedMessageException - | InvalidProtocolBufferException expected) { - } - } - - public void testParseKeyInvalidInputs() throws Exception { - GenericPublicKey[] badKeys = new GenericPublicKey[] { - GenericPublicKey.newBuilder().setType(SecureMessageProto.PublicKeyType.EC_P256).build(), - GenericPublicKey.newBuilder().setType(SecureMessageProto.PublicKeyType.RSA2048).build(), - GenericPublicKey.newBuilder().setType(SecureMessageProto.PublicKeyType.DH2048_MODP).build(), - }; - for (int i = 0; i < badKeys.length; i++) { - GenericPublicKey key = badKeys[i]; - try { - PublicKeyProtoUtil.parsePublicKey(key); - fail(String.format("%sth key was parsed without exceptions", i)); - } catch (InvalidKeySpecException expected) { - } - } - } - - enum CorruptHeaderType { - EMPTY, - // TODO(shabsi): Remove these test cases and modify code in SecureMessageParser appropriately. - // UNSET, - // JUNK, - } - - private void corruptHeaderWith(CorruptHeaderType corruptionType, - HeaderAndBody.Builder protoToModify) { - switch (corruptionType) { - case EMPTY: - protoToModify.setHeader(Header.getDefaultInstance()); - break; - /* - case JUNK: - Header.Builder junk = Header.newBuilder(); - junk.setDecryptionKeyId(ByteString.copyFromUtf8("fooooo")); - junk.setIv(ByteString.copyFromUtf8("bar")); - // Don't set signature scheme. - junk.setVerificationKeyId(ByteString.copyFromUtf8("bazzzzz")); - protoToModify.setHeader(junk.build()); - break; - case UNSET: - protoToModify.clearHeader(); - break; - */ - default: - throw new RuntimeException("Broken test code"); - } - } - - private void consistencyCheck( - SecureMessage secmsg, HeaderAndBody headerAndBody, SigType sigType, EncType encType) - throws InvalidProtocolBufferException { - Header header = SecureMessageParser.getUnverifiedHeader(secmsg); - checkHeader(header, sigType, encType); // Checks that the "unverified header" looks right - checkHeaderAndBody(header, headerAndBody); // Matches header vs. the "verified" headerAndBody - } - - private Header checkHeader(Header header, SigType sigType, EncType encType) { - assertEquals(sigType.getSigScheme(), header.getSignatureScheme()); - assertEquals(encType.getEncScheme(), header.getEncryptionScheme()); - checkKeyIdsAndMetadata(verificationKeyId, decryptionKeyId, metadata, associatedData, header); - return header; - } - - private void checkHeaderAndBody(Header header, HeaderAndBody headerAndBody) { - assertTrue(header.equals(headerAndBody.getHeader())); - assertTrue(Arrays.equals(message, headerAndBody.getBody().toByteArray())); - } - - private void checkKeyIdsAndMetadata(byte[] verificationKeyId, byte[] decryptionKeyId, - byte[] metadata, byte[] associatedData, Header header) { - if (verificationKeyId == null) { - assertFalse(header.hasVerificationKeyId()); - } else { - assertTrue(Arrays.equals(verificationKeyId, header.getVerificationKeyId().toByteArray())); - } - if (decryptionKeyId == null) { - assertFalse(header.hasDecryptionKeyId()); - } else { - assertTrue(Arrays.equals(decryptionKeyId, header.getDecryptionKeyId().toByteArray())); - } - if (metadata == null) { - assertFalse(header.hasPublicMetadata()); - } else { - assertTrue(Arrays.equals(metadata, header.getPublicMetadata().toByteArray())); - } - if (associatedData == null) { - assertFalse(header.hasAssociatedDataLength()); - } else { - assertEquals(associatedData.length, header.getAssociatedDataLength()); - } - } - - private SecretKey makeAesKey() throws NoSuchAlgorithmException { - KeyGenerator aesKeygen = KeyGenerator.getInstance("AES"); - aesKeygen.init(256); - return aesKeygen.generateKey(); - } - - private Key getSigningKeyFor(SigType sigType) { - if (sigType == SigType.ECDSA_P256_SHA256) { - return ecPrivateKey; - } - if (sigType == SigType.RSA2048_SHA256) { - return rsaPrivateKey; - } - if (sigType == SigType.HMAC_SHA256) { - return hmacKey; - } - return null; // This should not happen - } - - private Key getVerificationKeyFor(SigType sigType) { - try { - if (sigType == SigType.ECDSA_P256_SHA256) { - return PublicKeyProtoUtil.parseEcPublicKey( - PublicKeyProtoUtil.encodeEcPublicKey(ecPublicKey)); - } - if (sigType == SigType.RSA2048_SHA256) { - return PublicKeyProtoUtil.parseRsa2048PublicKey( - PublicKeyProtoUtil.encodeRsa2048PublicKey(rsaPublicKey)); - } - } catch (InvalidKeySpecException e) { - throw new AssertionError(e); - } - - assertFalse(sigType.isPublicKeyScheme()); - // For symmetric key schemes - return getSigningKeyFor(sigType); - } - - private SecureMessageBuilder getPreconfiguredBuilder() { - // Re-use a single instance of SecureMessageBuilder for efficiency. - SecureMessageBuilder builder = secureMessageBuilder.reset(); - if (verificationKeyId != null) { - builder.setVerificationKeyId(verificationKeyId); - } - if (decryptionKeyId != null) { - builder.setDecryptionKeyId(decryptionKeyId); - } - if (metadata != null) { - builder.setPublicMetadata(metadata); - } - if (associatedData != null) { - builder.setAssociatedData(associatedData); - } - return builder; - } - - private static boolean isUnsupported(SigType sigType) { - // EC operations aren't supported on older Android releases - return PublicKeyProtoUtil.isLegacyCryptoRequired() - && (sigType == SigType.ECDSA_P256_SHA256); - } - - private static boolean isRunningInAndroid() { - try { - ClassLoader.getSystemClassLoader().loadClass("android.os.Build$VERSION"); - return true; - } catch (ClassNotFoundException e) { - // Not running on Android - return false; - } - } -} diff --git a/src/main/proto/CMakeLists.txt b/src/main/proto/CMakeLists.txt deleted file mode 100644 index cd94f3f..0000000 --- a/src/main/proto/CMakeLists.txt +++ /dev/null @@ -1,32 +0,0 @@ -# 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/device_to_device_messages.proto b/src/main/proto/device_to_device_messages.proto deleted file mode 100644 index 5600373..0000000 --- a/src/main/proto/device_to_device_messages.proto +++ /dev/null @@ -1,81 +0,0 @@ -// 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; - -import "securemessage.proto"; - -option optimize_for = LITE_RUNTIME; -option java_package = "com.google.security.cryptauth.lib.securegcm"; -option java_outer_classname = "DeviceToDeviceMessagesProto"; -option objc_class_prefix = "SGCM"; - -// Used by protocols between devices -message DeviceToDeviceMessage { - // the payload of the message - optional bytes message = 1; - - // the sequence number of the message - must be increasing. - optional int32 sequence_number = 2; -} - -// sent as the first message from initiator to responder -// in an unauthenticated Diffie-Hellman Key Exchange -message InitiatorHello { - // The session public key to send to the responder - optional securemessage.GenericPublicKey public_dh_key = 1; - - // The protocol version - optional int32 protocol_version = 2 [default = 0]; -} - -// sent inside the header of the first message from the responder to the -// initiator in an unauthenticated Diffie-Hellman Key Exchange -message ResponderHello { - // The session public key to send to the initiator - optional securemessage.GenericPublicKey public_dh_key = 1; - - // The protocol version - optional int32 protocol_version = 2 [default = 0]; -} - -// Type of curve -enum Curve { ED_25519 = 1; } - -// A convenience proto for encoding curve points in affine representation -message EcPoint { - required Curve curve = 1; - - // x and y are encoded in big-endian two's complement - // client MUST verify (x,y) is a valid point on the specified curve - required bytes x = 2; - required bytes y = 3; -} - -message SpakeHandshakeMessage { - // Each flow in the protocol bumps this counter - optional int32 flow_number = 1; - - // Some (but not all) SPAKE flows send a point on an elliptic curve - optional EcPoint ec_point = 2; - - // Some (but not all) SPAKE flows send a hash value - optional bytes hash_value = 3; - - // The last flow of a SPAKE protocol can send an optional payload, - // since the key exchange is already complete on the sender's side. - optional bytes payload = 4; -} diff --git a/src/main/proto/passwordless_auth_payloads.proto b/src/main/proto/passwordless_auth_payloads.proto deleted file mode 100644 index 69c2784..0000000 --- a/src/main/proto/passwordless_auth_payloads.proto +++ /dev/null @@ -1,37 +0,0 @@ -// 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; - -option optimize_for = LITE_RUNTIME; -option java_package = "com.google.security.cryptauth.lib.securegcm"; -option java_outer_classname = "SecureGcmPasswordlessAuthProto"; -option objc_class_prefix = "SGCM"; - -message IdentityAssertion { - // Browser data contains the challenge, origin, etc. - optional bytes browser_data_hash = 1; - - // A counter that we expect to increase. - optional int64 counter = 2; - - // An integer encoding whether the user actively approved this assertion, - // or whether the phone auto-issued the assertion. - // Possible values are: - // 1: User explicitly approved the login. - // 0: Phone approved login without consulting the user. - optional int32 user_approval = 3; -} diff --git a/src/main/proto/proximity_payloads.proto b/src/main/proto/proximity_payloads.proto deleted file mode 100644 index d7a4956..0000000 --- a/src/main/proto/proximity_payloads.proto +++ /dev/null @@ -1,59 +0,0 @@ -// 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; - -import "securegcm.proto"; - -option optimize_for = LITE_RUNTIME; -option java_package = "com.google.security.cryptauth.lib.securegcm"; -option java_outer_classname = "SecureGcmProximityAuthProto"; -option objc_class_prefix = "SGCM"; - -// Message used when one device wants to initiate a Proximity Auth pairing with -// another device DEPRECATED. DO NOT USE -message CloudToDeviceProximityAuthPairing { - // The name or description of the device that wants to pair with another - // personal device of the user. This is a string that may be shown to the - // user or may be kept in logs. - optional string initiating_device_name = 1; - - // The original device's Bluetooth address in human readable form - // (e.g., <code>AA:BB:CC:DD:EE:FF</code>) - optional string initiating_device_bt_address = 2; - - // A symmetric key that was generated by the original device. - optional bytes ephemeral_symmetric_key = 3; - - // Optional additional metadata that the initiating device can choose to send. - // Used for quick protocol iteration. - optional bytes additional_metadata = 4; -} - -// Message to push to eligible unlock devices so that they can contact the -// device to be unlocked. Used by FindEligibleUnlockDevicesRequest, with -// PayloadType = DEVICE_PROXIMITY_CALLBACK. -message DeviceProximityCallback { - // Required. The bluetooth MAC address that should be contacted by the unlock - // device. - optional string callback_bluetooth_address = 1; - - // Required. The type of the device that triggered this callback to be sent. - optional DeviceType source_device_type = 2; - - // The version of the setup protocol that the source device expects to use. - optional int32 protocol_version = 3; -} diff --git a/src/main/proto/securegcm.proto b/src/main/proto/securegcm.proto deleted file mode 100644 index 0325f06..0000000 --- a/src/main/proto/securegcm.proto +++ /dev/null @@ -1,308 +0,0 @@ -// 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; - -option optimize_for = LITE_RUNTIME; -option java_package = "com.google.security.cryptauth.lib.securegcm"; -option java_outer_classname = "SecureGcmProto"; -option objc_class_prefix = "SGCM"; - -// Message used only during enrollment -// Field numbers should be kept in sync with DeviceInfo in: -// java/com/google/security/cryptauth/backend/services/common/common.proto -message GcmDeviceInfo { - // This field's name does not match the one in DeviceInfo for legacy reasons. - // Consider using long_device_id and device_type instead when enrolling - // non-android devices. - optional fixed64 android_device_id = 1; - - // Used for device_address of DeviceInfo field 2, but for GCM capable devices. - optional bytes gcm_registration_id = 102; - - // Used for device_address of DeviceInfo field 2, but for iOS devices. - optional bytes apn_registration_id = 202; - - // Does the user have notifications enabled for the given device address. - optional bool notification_enabled = 203 [default = true]; - - // Used for device_address of DeviceInfo field 2, a Bluetooth Mac address for - // the device (e.g., to be used with EasyUnlock) - optional string bluetooth_mac_address = 302; - - // SHA-256 hash of the device master key (from the key exchange). - // Differs from DeviceInfo field 3, which contains the actual master key. - optional bytes device_master_key_hash = 103; - - // A SecureMessage.EcP256PublicKey - required bytes user_public_key = 4; - - // device's model name - // (e.g., an android.os.Build.MODEL or UIDevice.model) - optional string device_model = 7; - - // device's locale - optional string locale = 8; - - // The handle for user_public_key (and implicitly, a master key) - optional bytes key_handle = 9; - - // The initial counter value for the device, sent by the device - optional int64 counter = 12 [default = 0]; - - // The Operating System version on the device - // (e.g., an android.os.Build.DISPLAY or UIDevice.systemVersion) - optional string device_os_version = 13; - - // The Operating System version number on the device - // (e.g., an android.os.Build.VERSION.SDK_INT) - optional int64 device_os_version_code = 14; - - // The Operating System release on the device - // (e.g., an android.os.Build.VERSION.RELEASE) - optional string device_os_release = 15; - - // The Operating System codename on the device - // (e.g., an android.os.Build.VERSION.CODENAME or UIDevice.systemName) - optional string device_os_codename = 16; - - // The software version running on the device - // (e.g., Authenticator app version string) - optional string device_software_version = 17; - - // The software version number running on the device - // (e.g., Authenticator app version code) - optional int64 device_software_version_code = 18; - - // Software package information if applicable - // (e.g., com.google.android.apps.authenticator2) - optional string device_software_package = 19; - - // Size of the display in thousandths of an inch (e.g., 7000 mils = 7 in) - optional int32 device_display_diagonal_mils = 22; - - // For Authzen capable devices, their Authzen protocol version - optional int32 device_authzen_version = 24; - - // Not all devices have device identifiers that fit in 64 bits. - optional bytes long_device_id = 29; - - // The device manufacturer name - // (e.g., android.os.Build.MANUFACTURER) - optional string device_manufacturer = 31; - - // Used to indicate which type of device this is. - optional DeviceType device_type = 32 [default = ANDROID]; - - // Fields corresponding to screenlock type/features and hardware features - // should be numbered in the 400 range. - - // Is this device using a secure screenlock (e.g., pattern or pin unlock) - optional bool using_secure_screenlock = 400 [default = false]; - - // Is auto-unlocking the screenlock (e.g., when at "home") supported? - optional bool auto_unlock_screenlock_supported = 401 [default = false]; - - // Is auto-unlocking the screenlock (e.g., when at "home") enabled? - optional bool auto_unlock_screenlock_enabled = 402 [default = false]; - - // Does the device have a Bluetooth (classic) radio? - optional bool bluetooth_radio_supported = 403 [default = false]; - - // Is the Bluetooth (classic) radio on? - optional bool bluetooth_radio_enabled = 404 [default = false]; - - // Does the device hardware support a mobile data connection? - optional bool mobile_data_supported = 405 [default = false]; - - // Does the device support tethering? - optional bool tethering_supported = 406 [default = false]; - - // Does the device have a BLE radio? - optional bool ble_radio_supported = 407 [default = false]; - - // Is the device a "Pixel Experience" Android device? - optional bool pixel_experience = 408 [default = false]; - - // Is the device running in the ARC++ container on a chromebook? - optional bool arc_plus_plus = 409 [default = false]; - - // Is the value set in |using_secure_screenlock| reliable? On some Android - // devices, the platform API to get the screenlock state is not trustworthy. - // See b/32212161. - optional bool is_screenlock_state_flaky = 410 [default = false]; - - // A list of multi-device software features supported by the device. - repeated SoftwareFeature supported_software_features = 411; - - // A list of multi-device software features currently enabled (active) on the - // device. - repeated SoftwareFeature enabled_software_features = 412; - - // The enrollment session id this is sent with - optional bytes enrollment_session_id = 1000; - - // A copy of the user's OAuth token - optional string oauth_token = 1001; -} - -// This enum is used by iOS devices as values for device_display_diagonal_mils -// in GcmDeviceInfo. There is no good way to calculate it on those devices. -enum AppleDeviceDiagonalMils { - // This is the mils diagonal on an iPhone 5. - APPLE_PHONE = 4000; - // This is the mils diagonal on an iPad mini. - APPLE_PAD = 7900; -} - -// This should be kept in sync with DeviceType in: -// java/com/google/security/cryptauth/backend/services/common/common_enums.proto -enum DeviceType { - UNKNOWN = 0; - ANDROID = 1; - CHROME = 2; - IOS = 3; - BROWSER = 4; - OSX = 5; -} - -// MultiDevice features which may be supported and enabled on a device. See -enum SoftwareFeature { - UNKNOWN_FEATURE = 0; - BETTER_TOGETHER_HOST = 1; - BETTER_TOGETHER_CLIENT = 2; - EASY_UNLOCK_HOST = 3; - EASY_UNLOCK_CLIENT = 4; - MAGIC_TETHER_HOST = 5; - MAGIC_TETHER_CLIENT = 6; - SMS_CONNECT_HOST = 7; - SMS_CONNECT_CLIENT = 8; -} - -// A list of "reasons" that can be provided for calling server-side APIs. -// This is particularly important for calls that can be triggered by different -// kinds of events. Please try to keep reasons as generic as possible, so that -// codes can be re-used by various callers in a sensible fashion. -enum InvocationReason { - REASON_UNKNOWN = 0; - // First run of the software package invoking this call - REASON_INITIALIZATION = 1; - // Ordinary periodic actions (e.g. monthly master key rotation) - REASON_PERIODIC = 2; - // Slow-cycle periodic action (e.g. yearly keypair rotation???) - REASON_SLOW_PERIODIC = 3; - // Fast-cycle periodic action (e.g. daily sync for Smart Lock users) - REASON_FAST_PERIODIC = 4; - // Expired state (e.g. expired credentials, or cached entries) was detected - REASON_EXPIRATION = 5; - // An unexpected protocol failure occurred (so attempting to repair state) - REASON_FAILURE_RECOVERY = 6; - // A new account has been added to the device - REASON_NEW_ACCOUNT = 7; - // An existing account on the device has been changed - REASON_CHANGED_ACCOUNT = 8; - // The user toggled the state of a feature (e.g. Smart Lock enabled via BT) - REASON_FEATURE_TOGGLED = 9; - // A "push" from the server caused this action (e.g. a sync tickle) - REASON_SERVER_INITIATED = 10; - // A local address change triggered this (e.g. GCM registration id changed) - REASON_ADDRESS_CHANGE = 11; - // A software update has triggered this - REASON_SOFTWARE_UPDATE = 12; - // A manual action by the user triggered this (e.g. commands sent via adb) - REASON_MANUAL = 13; - // A custom key has been invalidated on the device (e.g. screen lock is - // disabled). - REASON_CUSTOM_KEY_INVALIDATION = 14; - // Periodic action triggered by auth_proximity - REASON_PROXIMITY_PERIODIC = 15; -} - -enum Type { - ENROLLMENT = 0; - TICKLE = 1; - TX_REQUEST = 2; - TX_REPLY = 3; - TX_SYNC_REQUEST = 4; - TX_SYNC_RESPONSE = 5; - TX_PING = 6; - DEVICE_INFO_UPDATE = 7; - TX_CANCEL_REQUEST = 8; - - // DEPRECATED (can be re-used after Aug 2015) - PROXIMITYAUTH_PAIRING = 10; - - // The kind of identity assertion generated by a "GCM V1" device (i.e., - // an Android phone that has registered with us a public and a symmetric - // key) - GCMV1_IDENTITY_ASSERTION = 11; - - // Device-to-device communications are protected by an unauthenticated - // Diffie-Hellman exchange. The InitiatorHello message is simply the - // initiator's public DH key, and is not encoded as a SecureMessage, so - // it doesn't have a tag. - // The ResponderHello message (which is sent by the responder - // to the initiator), on the other hand, carries a payload that is protected - // by the derived shared key. It also contains the responder's - // public DH key. ResponderHelloAndPayload messages have the - // DEVICE_TO_DEVICE_RESPONDER_HELLO tag. - DEVICE_TO_DEVICE_RESPONDER_HELLO_PAYLOAD = 12; - - // Device-to-device communications are protected by an unauthenticated - // Diffie-Hellman exchange. Once the initiator and responder - // agree on a shared key (through Diffie-Hellman), they will use messages - // tagged with DEVICE_TO_DEVICE_MESSAGE to exchange data. - DEVICE_TO_DEVICE_MESSAGE = 13; - - // Notification to let a device know it should contact a nearby device. - DEVICE_PROXIMITY_CALLBACK = 14; - - // Device-to-device communications are protected by an unauthenticated - // Diffie-Hellman exchange. During device-to-device authentication, the first - // message from initiator (the challenge) is signed and put into the payload - // of the message sent back to the initiator. - UNLOCK_KEY_SIGNED_CHALLENGE = 15; - - // Specialty (corp only) features - LOGIN_NOTIFICATION = 101; -} - -message GcmMetadata { - required Type type = 1; - optional int32 version = 2 [default = 0]; -} - -message Tickle { - // Time after which this tickle should expire - optional fixed64 expiry_time = 1; -} - -message LoginNotificationInfo { - // Time at which the server received the login notification request. - optional fixed64 creation_time = 2; - - // Must correspond to user_id in LoginNotificationRequest, if set. - optional string email = 3; - - // Host where the user's credentials were used to login, if meaningful. - optional string host = 4; - - // Location from where the user's credentials were used, if meaningful. - optional string source = 5; - - // Type of login, e.g. ssh, gnome-screensaver, or web. - optional string event_type = 6; -} diff --git a/src/main/proto/securemessage.proto b/src/main/proto/securemessage.proto deleted file mode 100644 index 5118d35..0000000 --- a/src/main/proto/securemessage.proto +++ /dev/null @@ -1,126 +0,0 @@ -// 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. - -// Proto definitions for SecureMessage format - -syntax = "proto2"; - -package securemessage; - -option optimize_for = LITE_RUNTIME; -option java_package = "com.google.security.cryptauth.lib.securemessage"; -option java_outer_classname = "SecureMessageProto"; -option objc_class_prefix = "SMSG"; - -message SecureMessage { - // Must contain a HeaderAndBody message - required bytes header_and_body = 1; - // Signature of header_and_body - required bytes signature = 2; -} - -// Supported "signature" schemes (both symmetric key and public key based) -enum SigScheme { - HMAC_SHA256 = 1; - ECDSA_P256_SHA256 = 2; - // Not recommended -- use ECDSA_P256_SHA256 instead - RSA2048_SHA256 = 3; -} - -// Supported encryption schemes -enum EncScheme { - // No encryption - NONE = 1; - AES_256_CBC = 2; -} - -message Header { - required SigScheme signature_scheme = 1; - required EncScheme encryption_scheme = 2; - // Identifies the verification key - optional bytes verification_key_id = 3; - // Identifies the decryption key - optional bytes decryption_key_id = 4; - // Encryption may use an IV - optional bytes iv = 5; - // Arbitrary per-protocol public data, to be sent with the plain-text header - optional bytes public_metadata = 6; - // The length of some associated data this is not sent in this SecureMessage, - // but which will be bound to the signature. - optional uint32 associated_data_length = 7 [default = 0]; -} - -message HeaderAndBody { - // Public data about this message (to be bound in the signature) - required Header header = 1; - // Payload data - required bytes body = 2; -} - -// Must be kept wire-format compatible with HeaderAndBody. Provides the -// SecureMessage code with a consistent wire-format representation that -// remains stable irrespective of protobuf implementation choices. This -// low-level representation of a HeaderAndBody should not be used by -// any code outside of the SecureMessage library implementation/tests. -message HeaderAndBodyInternal { - // A raw (wire-format) byte encoding of a Header, suitable for hashing - required bytes header = 1; - // Payload data - required bytes body = 2; -} - -// ------- -// The remainder of the messages defined here are provided only for -// convenience. They are not needed for SecureMessage proper, but are -// commonly useful wherever SecureMessage might be applied. -// ------- - -// A list of supported public key types -enum PublicKeyType { - EC_P256 = 1; - RSA2048 = 2; - // 2048-bit MODP group 14, from RFC 3526 - DH2048_MODP = 3; -} - -// A convenience proto for encoding NIST P-256 elliptic curve public keys -message EcP256PublicKey { - // x and y are encoded in big-endian two's complement (slightly wasteful) - // Client MUST verify (x,y) is a valid point on NIST P256 - required bytes x = 1; - required bytes y = 2; -} - -// A convenience proto for encoding RSA public keys with small exponents -message SimpleRsaPublicKey { - // Encoded in big-endian two's complement - required bytes n = 1; - optional int32 e = 2 [default = 65537]; -} - -// A convenience proto for encoding Diffie-Hellman public keys, -// for use only when Elliptic Curve based key exchanges are not possible. -// (Note that the group parameters must be specified separately) -message DhPublicKey { - // Big-endian two's complement encoded group element - required bytes y = 1; -} - -message GenericPublicKey { - required PublicKeyType type = 1; - optional EcP256PublicKey ec_p256_public_key = 2; - optional SimpleRsaPublicKey rsa2048_public_key = 3; - // Use only as a last resort - optional DhPublicKey dh2048_public_key = 4; -} diff --git a/src/main/proto/ukey.proto b/src/main/proto/ukey.proto deleted file mode 100644 index 327d8d3..0000000 --- a/src/main/proto/ukey.proto +++ /dev/null @@ -1,105 +0,0 @@ -// 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; - -option optimize_for = LITE_RUNTIME; -option java_package = "com.google.security.cryptauth.lib.securegcm"; -option java_outer_classname = "UkeyProto"; - -message Ukey2Message { - enum Type { - UNKNOWN_DO_NOT_USE = 0; - ALERT = 1; - CLIENT_INIT = 2; - SERVER_INIT = 3; - CLIENT_FINISH = 4; - } - - optional Type message_type = 1; // Identifies message type - optional bytes message_data = 2; // Actual message, to be parsed according to - // message_type -} - -message Ukey2Alert { - enum AlertType { - // Framing errors - BAD_MESSAGE = 1; // The message could not be deserialized - BAD_MESSAGE_TYPE = 2; // message_type has an undefined value - INCORRECT_MESSAGE = 3; // message_type received does not correspond to - // expected type at this stage of the protocol - BAD_MESSAGE_DATA = 4; // Could not deserialize message_data as per - // value inmessage_type - - // ClientInit and ServerInit errors - BAD_VERSION = 100; // version is invalid; server cannot find - // suitable version to speak with client. - BAD_RANDOM = 101; // Random data is missing or of incorrect - // length - BAD_HANDSHAKE_CIPHER = 102; // No suitable handshake ciphers were found - BAD_NEXT_PROTOCOL = 103; // The next protocol is missing, unknown, or - // unsupported - BAD_PUBLIC_KEY = 104; // The public key could not be parsed - - // Other errors - INTERNAL_ERROR = 200; // An internal error has occurred. error_message - // may contain additional details for logging - // and debugging. - } - - optional AlertType type = 1; - optional string error_message = 2; -} - -enum Ukey2HandshakeCipher { - RESERVED = 0; - P256_SHA512 = 100; // NIST P-256 used for ECDH, SHA512 used for - // commitment - CURVE25519_SHA512 = 200; // Curve 25519 used for ECDH, SHA512 used for - // commitment -} - -message Ukey2ClientInit { - optional int32 version = 1; // highest supported version for rollback - // protection - optional bytes random = 2; // random bytes for replay/reuse protection - - // One commitment (hash of ClientFinished containing public key) per supported - // cipher - message CipherCommitment { - optional Ukey2HandshakeCipher handshake_cipher = 1; - optional bytes commitment = 2; - } - repeated CipherCommitment cipher_commitments = 3; - - // Next protocol that the client wants to speak. - optional string next_protocol = 4; -} - -message Ukey2ServerInit { - optional int32 version = 1; // highest supported version for rollback - // protection - optional bytes random = 2; // random bytes for replay/reuse protection - - // Selected Cipher and corresponding public key - optional Ukey2HandshakeCipher handshake_cipher = 3; - optional bytes public_key = 4; -} - -message Ukey2ClientFinished { - optional bytes public_key = 1; // public key matching selected handshake - // cipher -} |