Commit 1ea3015a authored by Kyle Horimoto's avatar Kyle Horimoto Committed by Commit Bot

[CrOS PhoneHub] Implement SecureChannel protocol v4

WireMessage protocol v3 used an unsigned 16-bit integer to represent
the message length. Unfortunately, when big messages are sent (e.g.,
images sent through Phone Hub), the size can exceed the maximum value of
a 16-bit int, so the protocol breaks down.

This CL introduces v4, which uses an unsigned 32-bit integer to
represent the message size. For backward compatibility, we still use v3
to send messages used for authentication, Smart Lock, and Instant
Tethering. If a message is sent for any other feature type, we use v4.

Bug: 1150565, 1106937
Change-Id: I2e074505f8efa8a6a7ba355229e8cd0adf7e7c19
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2552250
Commit-Queue: Kyle Horimoto <khorimoto@chromium.org>
Reviewed-by: default avatarJames Vecore <vecore@google.com>
Cr-Commit-Position: refs/heads/master@{#829896}
parent f5211095
......@@ -10,36 +10,36 @@
#include <limits>
#include "base/base64url.h"
#include "base/big_endian.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/macros.h"
#include "base/memory/ptr_util.h"
#include "base/notreached.h"
#include "base/values.h"
#include "chromeos/components/multidevice/logging/logging.h"
// The wire messages have a simple format:
// [ message version ] [ body length ] [ JSON body ]
// 1 byte 2 bytes body length
// When sending encrypted messages, the JSON body contains two fields: an
// optional |permit_id| field and a required |payload| field.
//
// For non-encrypted messages, the message itself is the JSON body, and it
// doesn't have a |payload| field.
namespace chromeos {
namespace secure_channel {
namespace {
// The length of the message header, in bytes.
const size_t kHeaderLength = 3;
// The number of bytes used to represent the protocol version in the message
// header.
const size_t kNumBytesInHeaderProtocolVersion = 1u;
// The protocol version of the message format.
const int kMessageFormatVersionThree = 3;
// The number of bytes used to represent the message body size.
const size_t kV3NumBytesInHeaderSize = 2u; // 16-bit int
const size_t kV4NumBytesInHeaderSize = 4u; // 32-bit int
const char kPayloadKey[] = "payload";
// Supported Protocol versions.
const uint8_t kV3HeaderVersion = 0x03;
const uint8_t kV4HeaderVersion = 0x04;
// JSON keys for feature and payload.
const char kFeatureKey[] = "feature";
const char kPayloadKey[] = "payload";
// The default feature value. This is the default for backward compatibility
// reasons; previously, the protocol did not transmit the feature in the
......@@ -48,77 +48,41 @@ const char kFeatureKey[] = "feature";
// EasyUnlock by default.
const char kDefaultFeature[] = "easy_unlock";
// Parses the |serialized_message|'s header. Returns |true| iff the message has
// a valid header, is complete, and is well-formed according to the header. Sets
// |is_incomplete_message| to true iff the message does not have enough data to
// parse the header, or if the message length encoded in the message header
// exceeds the size of the |serialized_message|.
bool ParseHeader(const std::string& serialized_message,
bool* is_incomplete_message) {
*is_incomplete_message = false;
if (serialized_message.size() < kHeaderLength) {
*is_incomplete_message = true;
return false;
}
static_assert(kHeaderLength > 2, "kHeaderLength too small");
size_t version = serialized_message[0];
if (version != kMessageFormatVersionThree) {
PA_LOG(WARNING) << "Error: Invalid message version. Got " << version
<< ", expected " << kMessageFormatVersionThree;
return false;
}
uint16_t expected_body_length =
(static_cast<uint8_t>(serialized_message[1]) << 8) |
(static_cast<uint8_t>(serialized_message[2]) << 0);
size_t expected_message_length = kHeaderLength + expected_body_length;
if (serialized_message.size() < expected_message_length) {
*is_incomplete_message = true;
return false;
}
if (serialized_message.size() != expected_message_length) {
PA_LOG(WARNING) << "Error: Invalid message length. Got "
<< serialized_message.size() << ", expected "
<< expected_message_length;
return false;
}
return true;
}
} // namespace
// Features which were launched before protocol v4 was introduced. If we try to
// serialize a WireMessage whose feature is one of these, we should use the v3
// protocol.
const char* const kV3Features[] = {
// Authentication protocol (used for all features).
"auth"
WireMessage::~WireMessage() {}
// Smart Lock.
"easy_unlock",
// static
std::unique_ptr<WireMessage> WireMessage::Deserialize(
const std::string& serialized_message,
bool* is_incomplete_message) {
if (!ParseHeader(serialized_message, is_incomplete_message))
return nullptr;
// Instant Tethering.
"magic_tether",
};
std::unique_ptr<base::Value> body_value = base::JSONReader::ReadDeprecated(
serialized_message.substr(kHeaderLength));
std::unique_ptr<WireMessage> DeserializeJsonMessageBody(
const std::string& serialized_message_body) {
std::unique_ptr<base::Value> body_value =
base::JSONReader::ReadDeprecated(serialized_message_body);
if (!body_value || !body_value->is_dict()) {
PA_LOG(WARNING) << "Error: Unable to parse message as JSON.";
PA_LOG(WARNING) << "Unable to parse message as JSON.";
return nullptr;
}
base::DictionaryValue* body;
bool success = body_value->GetAsDictionary(&body);
DCHECK(success);
if (!body_value->GetAsDictionary(&body))
NOTREACHED();
std::string payload_base64;
if (!body->GetString(kPayloadKey, &payload_base64)) {
// The body is a valid JSON, but it doesn't contain a |payload| field. It
// must be a non-encrypted message.
return base::WrapUnique(
new WireMessage(serialized_message.substr(kHeaderLength)));
// Legacy case: Message without a payload.
return base::WrapUnique(new WireMessage(serialized_message_body));
}
if (payload_base64.empty()) {
PA_LOG(WARNING) << "Error: Missing payload.";
PA_LOG(WARNING) << "Message contains empty payload.";
return nullptr;
}
......@@ -126,18 +90,103 @@ std::unique_ptr<WireMessage> WireMessage::Deserialize(
if (!base::Base64UrlDecode(payload_base64,
base::Base64UrlDecodePolicy::REQUIRE_PADDING,
&payload)) {
PA_LOG(WARNING) << "Error: Invalid base64 encoding for payload.";
PA_LOG(WARNING) << "Payload contains invalid base64 encoding.";
return nullptr;
}
std::string feature;
if (!body->GetString(kFeatureKey, &feature) || feature.empty()) {
feature = std::string(kDefaultFeature);
feature = kDefaultFeature;
}
return base::WrapUnique(new WireMessage(payload, feature));
}
std::unique_ptr<WireMessage> DeserializeV3OrV4Message(
const std::string& serialized_message,
bool* is_incomplete_message,
bool is_v3) {
const size_t kHeaderSize =
kNumBytesInHeaderProtocolVersion +
(is_v3 ? kV3NumBytesInHeaderSize : kV4NumBytesInHeaderSize);
if (serialized_message.size() < kHeaderSize) {
PA_LOG(ERROR) << "Message was shorter than expected. "
<< "Size: " << serialized_message.size() << ", "
<< "Version: " << (is_v3 ? 3 : 4);
*is_incomplete_message = true;
return nullptr;
}
// Reads the expected body size, starting after the protocol message portion
// of the header. Because this value is received over the network, we must
// convert from big endian to host byte order.
base::BigEndianReader reader(
serialized_message.data() + kNumBytesInHeaderProtocolVersion,
serialized_message.size() - kNumBytesInHeaderProtocolVersion);
size_t expected_message_length;
if (is_v3) {
uint16_t body_length;
if (!reader.ReadU16(&body_length)) {
PA_LOG(ERROR) << "Failed to read v3 message length.";
*is_incomplete_message = true;
return nullptr;
}
expected_message_length = kHeaderSize + body_length;
} else {
uint32_t body_length;
if (!reader.ReadU32(&body_length)) {
PA_LOG(ERROR) << "Failed to read v4 message length.";
*is_incomplete_message = true;
return nullptr;
}
expected_message_length = kHeaderSize + body_length;
}
size_t message_length = serialized_message.size();
if (message_length != expected_message_length) {
PA_LOG(ERROR) << "Message length does not match expectation. "
<< "Size: " << serialized_message.size() << ", "
<< "Expected size: " << expected_message_length << ", "
<< "Version: " << (is_v3 ? 3 : 4);
*is_incomplete_message = message_length < expected_message_length;
return nullptr;
}
*is_incomplete_message = false;
return DeserializeJsonMessageBody(serialized_message.substr(kHeaderSize));
}
} // namespace
WireMessage::~WireMessage() = default;
// static
std::unique_ptr<WireMessage> WireMessage::Deserialize(
const std::string& serialized_message,
bool* is_incomplete_message) {
if (serialized_message.empty()) {
PA_LOG(ERROR) << "Attempted to deserialize empty message.";
*is_incomplete_message = true;
return nullptr;
}
// The first byte of the message is the protocol version as an unsigned 8-bit
// integer.
uint8_t protocol_version = serialized_message[0];
if (protocol_version == kV3HeaderVersion ||
protocol_version == kV4HeaderVersion) {
return DeserializeV3OrV4Message(serialized_message, is_incomplete_message,
protocol_version == kV3HeaderVersion);
}
PA_LOG(ERROR) << "Received message with unknown version " << protocol_version;
*is_incomplete_message = false;
return nullptr;
}
std::string WireMessage::Serialize() const {
std::string json_body;
if (body_.empty()) {
......@@ -146,7 +195,7 @@ std::string WireMessage::Serialize() const {
return std::string();
}
// Create JSON body containing permit id and payload.
// Create JSON body containing feature and payload.
base::DictionaryValue body;
std::string base64_payload;
......@@ -164,23 +213,30 @@ std::string WireMessage::Serialize() const {
json_body = body_;
}
// Create header containing version and payload size.
bool use_v3_encoding = body_.empty() || feature_.empty() ||
base::Contains(kV3Features, feature_);
size_t body_size = json_body.size();
if (body_size > std::numeric_limits<uint16_t>::max()) {
if (use_v3_encoding && body_size > std::numeric_limits<uint16_t>::max()) {
PA_LOG(ERROR) << "Can not create WireMessage because body size exceeds "
<< "16-bit unsigned integer: " << body_size;
return std::string();
}
uint8_t header[] = {
static_cast<uint8_t>(kMessageFormatVersionThree),
static_cast<uint8_t>((body_size >> 8) & 0xFF),
static_cast<uint8_t>(body_size & 0xFF),
};
static_assert(sizeof(header) == kHeaderLength, "Malformed header.");
const size_t kHeaderSize =
kNumBytesInHeaderProtocolVersion +
(use_v3_encoding ? kV3NumBytesInHeaderSize : kV4NumBytesInHeaderSize);
std::string header_string(kHeaderSize, 0);
base::BigEndianWriter writer(&header_string[0], kHeaderSize);
if (use_v3_encoding) {
writer.WriteU8(kV3HeaderVersion);
writer.WriteU16(static_cast<uint16_t>(body_size));
} else {
writer.WriteU8(kV4HeaderVersion);
writer.WriteU32(static_cast<uint32_t>(body_size));
}
std::string header_string(kHeaderLength, 0);
std::memcpy(&header_string[0], header, kHeaderLength);
return header_string + json_body;
}
......
......@@ -14,6 +14,33 @@ namespace chromeos {
namespace secure_channel {
// Message sent over the wire via SecureChannel. Each message sent using the
// SecureChannel protocol has a feature name (a unique identifier for the client
// sending/receiving a message) and a payload.
//
// A wire message in SecureChannel is serialized to a byte array (in the format
// of a std::string). The first byte is always the protocol version expressed as
// an unsigned 8-bit int. This class supports protocol versions 3 and 4.
//
// v3: One byte version (0x03), followed by 2 bytes of the message size as an
// unsigned 16-bit int, followed by a stringified JSON object containing a
// "feature" key whose value is the feature and a "payload" key whose value
// is the payload.
//
// [ message version ] [ body length ] [ JSON body ]
// 1 byte 2 bytes body length
//
// v4: One byte version (0x04), followed by 4 bytes of the message size as an
// unsigned 32-bit int, followed by a stringified JSON object containing a
// "feature" key whose value is the feature and a "payload" key whose value
// is the payload.
//
// [ message version ] [ body length ] [ JSON body ]
// 1 byte 4 bytes body length
//
// v3 is deprecated and all new features use v4, but we special-case features
// which were released before v4 and send them via v3 for backward
// compatibility.
class WireMessage {
public:
// Creates a WireMessage containing |payload| for feature |feature| and
......
......@@ -30,7 +30,7 @@ TEST(SecureChannelWireMessageTest, Deserialize_EmptyMessage) {
EXPECT_FALSE(message);
}
TEST(SecureChannelWireMessageTest, Deserialize_IncompleteHeader) {
TEST(SecureChannelWireMessageTest, Deserialize_IncompleteHeader_V3) {
bool is_incomplete;
std::unique_ptr<WireMessage> message =
WireMessage::Deserialize("\3", &is_incomplete);
......@@ -38,6 +38,14 @@ TEST(SecureChannelWireMessageTest, Deserialize_IncompleteHeader) {
EXPECT_FALSE(message);
}
TEST(SecureChannelWireMessageTest, Deserialize_IncompleteHeader_V4) {
bool is_incomplete;
std::unique_ptr<WireMessage> message =
WireMessage::Deserialize("\4", &is_incomplete);
EXPECT_TRUE(is_incomplete);
EXPECT_FALSE(message);
}
TEST(SecureChannelWireMessageTest, Deserialize_UnexpectedMessageFormatVersion) {
bool is_incomplete;
// Version 2 is below the minimum supported version.
......@@ -47,7 +55,7 @@ TEST(SecureChannelWireMessageTest, Deserialize_UnexpectedMessageFormatVersion) {
EXPECT_FALSE(message);
}
TEST(SecureChannelWireMessageTest, Deserialize_BodyOfSizeZero) {
TEST(SecureChannelWireMessageTest, Deserialize_BodyOfSizeZero_V3) {
bool is_incomplete;
std::unique_ptr<WireMessage> message =
WireMessage::Deserialize(std::string("\3\0\0", 3), &is_incomplete);
......@@ -55,7 +63,15 @@ TEST(SecureChannelWireMessageTest, Deserialize_BodyOfSizeZero) {
EXPECT_FALSE(message);
}
TEST(SecureChannelWireMessageTest, Deserialize_IncompleteBody) {
TEST(SecureChannelWireMessageTest, Deserialize_BodyOfSizeZero_V4) {
bool is_incomplete;
std::unique_ptr<WireMessage> message =
WireMessage::Deserialize(std::string("\4\0\0\0\0", 5), &is_incomplete);
EXPECT_FALSE(is_incomplete);
EXPECT_FALSE(message);
}
TEST(SecureChannelWireMessageTest, Deserialize_IncompleteBody_V3) {
bool is_incomplete;
std::unique_ptr<WireMessage> message =
WireMessage::Deserialize(std::string("\3\0\5", 3), &is_incomplete);
......@@ -63,8 +79,16 @@ TEST(SecureChannelWireMessageTest, Deserialize_IncompleteBody) {
EXPECT_FALSE(message);
}
TEST(SecureChannelWireMessageTest, Deserialize_IncompleteBody_V4) {
bool is_incomplete;
std::unique_ptr<WireMessage> message =
WireMessage::Deserialize(std::string("\4\0\0\0\5", 5), &is_incomplete);
EXPECT_TRUE(is_incomplete);
EXPECT_FALSE(message);
}
TEST(SecureChannelWireMessageTest,
Deserialize_BodyLongerThanSpecifiedInHeader) {
Deserialize_BodyLongerThanSpecifiedInHeader_V3) {
bool is_incomplete;
std::unique_ptr<WireMessage> message = WireMessage::Deserialize(
std::string("\3\0\5", 3) + "123456", &is_incomplete);
......@@ -72,7 +96,16 @@ TEST(SecureChannelWireMessageTest,
EXPECT_FALSE(message);
}
TEST(SecureChannelWireMessageTest, Deserialize_BodyIsNotValidJSON) {
TEST(SecureChannelWireMessageTest,
Deserialize_BodyLongerThanSpecifiedInHeader_V4) {
bool is_incomplete;
std::unique_ptr<WireMessage> message = WireMessage::Deserialize(
std::string("\4\0\0\0\5", 5) + "123456", &is_incomplete);
EXPECT_FALSE(is_incomplete);
EXPECT_FALSE(message);
}
TEST(SecureChannelWireMessageTest, Deserialize_BodyIsNotValidJSON_V3) {
bool is_incomplete;
std::unique_ptr<WireMessage> message = WireMessage::Deserialize(
std::string("\3\0\5", 3) + "12345", &is_incomplete);
......@@ -80,7 +113,15 @@ TEST(SecureChannelWireMessageTest, Deserialize_BodyIsNotValidJSON) {
EXPECT_FALSE(message);
}
TEST(SecureChannelWireMessageTest, Deserialize_BodyIsNotADictionary) {
TEST(SecureChannelWireMessageTest, Deserialize_BodyIsNotValidJSON_V4) {
bool is_incomplete;
std::unique_ptr<WireMessage> message = WireMessage::Deserialize(
std::string("\4\0\0\0\5", 5) + "12345", &is_incomplete);
EXPECT_FALSE(is_incomplete);
EXPECT_FALSE(message);
}
TEST(SecureChannelWireMessageTest, Deserialize_BodyIsNotADictionary_V3) {
bool is_incomplete;
std::string header("\3\0\x15", 3);
std::string bytes = header + "[{\"payload\": \"YQ==\"}]";
......@@ -90,7 +131,17 @@ TEST(SecureChannelWireMessageTest, Deserialize_BodyIsNotADictionary) {
EXPECT_FALSE(message);
}
TEST(SecureChannelWireMessageTest, Deserialize_NonEncryptedMessage) {
TEST(SecureChannelWireMessageTest, Deserialize_BodyIsNotADictionary_V4) {
bool is_incomplete;
std::string header("\4\0\0\0\x15", 5);
std::string bytes = header + "[{\"payload\": \"YQ==\"}]";
std::unique_ptr<WireMessage> message =
WireMessage::Deserialize(bytes, &is_incomplete);
EXPECT_FALSE(is_incomplete);
EXPECT_FALSE(message);
}
TEST(SecureChannelWireMessageTest, Deserialize_NonEncryptedMessage_V3) {
bool is_incomplete;
std::string header("\3\0\x02", 3);
std::string bytes = header + "{}";
......@@ -101,7 +152,18 @@ TEST(SecureChannelWireMessageTest, Deserialize_NonEncryptedMessage) {
EXPECT_EQ("{}", message->body());
}
TEST(SecureChannelWireMessageTest, Deserialize_BodyHasEmptyPayload) {
TEST(SecureChannelWireMessageTest, Deserialize_NonEncryptedMessage_V4) {
bool is_incomplete;
std::string header("\4\0\0\0\x02", 5);
std::string bytes = header + "{}";
std::unique_ptr<WireMessage> message =
WireMessage::Deserialize(bytes, &is_incomplete);
EXPECT_FALSE(is_incomplete);
ASSERT_TRUE(message);
EXPECT_EQ("{}", message->body());
}
TEST(SecureChannelWireMessageTest, Deserialize_BodyHasEmptyPayload_V3) {
bool is_incomplete;
std::string header("\3\0\x29", 3);
std::string bytes =
......@@ -112,7 +174,18 @@ TEST(SecureChannelWireMessageTest, Deserialize_BodyHasEmptyPayload) {
EXPECT_FALSE(message);
}
TEST(SecureChannelWireMessageTest, Deserialize_PayloadIsNotBase64Encoded) {
TEST(SecureChannelWireMessageTest, Deserialize_BodyHasEmptyPayload_V4) {
bool is_incomplete;
std::string header("\4\0\0\0\x29", 5);
std::string bytes =
header + "{\"payload\": \"\", \"feature\": \"testFeature\"}";
std::unique_ptr<WireMessage> message =
WireMessage::Deserialize(bytes, &is_incomplete);
EXPECT_FALSE(is_incomplete);
EXPECT_FALSE(message);
}
TEST(SecureChannelWireMessageTest, Deserialize_PayloadIsNotBase64Encoded_V3) {
bool is_incomplete;
std::string header("\3\0\x30", 3);
std::string bytes =
......@@ -123,7 +196,18 @@ TEST(SecureChannelWireMessageTest, Deserialize_PayloadIsNotBase64Encoded) {
EXPECT_FALSE(message);
}
TEST(SecureChannelWireMessageTest, Deserialize_ValidMessage) {
TEST(SecureChannelWireMessageTest, Deserialize_PayloadIsNotBase64Encoded_V4) {
bool is_incomplete;
std::string header("\4\0\0\0\x30", 5);
std::string bytes =
header + "{\"payload\": \"garbage\", \"feature\": \"testFeature\"}";
std::unique_ptr<WireMessage> message =
WireMessage::Deserialize(bytes, &is_incomplete);
EXPECT_FALSE(is_incomplete);
EXPECT_FALSE(message);
}
TEST(SecureChannelWireMessageTest, Deserialize_ValidMessage_V3) {
bool is_incomplete;
std::string header("\3\0\x2d", 3);
std::string bytes =
......@@ -136,8 +220,21 @@ TEST(SecureChannelWireMessageTest, Deserialize_ValidMessage) {
EXPECT_EQ("testFeature", message->feature());
}
TEST(SecureChannelWireMessageTest, Deserialize_ValidMessage_V4) {
bool is_incomplete;
std::string header("\4\0\0\0\x2d", 5);
std::string bytes =
header + "{\"payload\": \"YQ==\", \"feature\": \"testFeature\"}";
std::unique_ptr<WireMessage> message =
WireMessage::Deserialize(bytes, &is_incomplete);
EXPECT_FALSE(is_incomplete);
ASSERT_TRUE(message);
EXPECT_EQ("a", message->payload());
EXPECT_EQ("testFeature", message->feature());
}
TEST(SecureChannelWireMessageTest,
Deserialize_ValidMessageWithBase64UrlEncoding) {
Deserialize_ValidMessageWithBase64UrlEncoding_V3) {
bool is_incomplete;
std::string header("\3\0\x2d", 3);
std::string bytes =
......@@ -151,7 +248,21 @@ TEST(SecureChannelWireMessageTest,
}
TEST(SecureChannelWireMessageTest,
Deserialize_ValidMessageWithExtraUnknownFields) {
Deserialize_ValidMessageWithBase64UrlEncoding_V4) {
bool is_incomplete;
std::string header("\4\0\0\0\x2d", 5);
std::string bytes =
header + "{\"payload\": \"_-Y=\", \"feature\": \"testFeature\"}";
std::unique_ptr<WireMessage> message =
WireMessage::Deserialize(bytes, &is_incomplete);
EXPECT_FALSE(is_incomplete);
ASSERT_TRUE(message);
EXPECT_EQ("\xFF\xE6", message->payload());
EXPECT_EQ("testFeature", message->feature());
}
TEST(SecureChannelWireMessageTest,
Deserialize_ValidMessageWithExtraUnknownFields_V3) {
bool is_incomplete;
std::string header("\3\0\x4c", 3);
std::string bytes = header +
......@@ -168,6 +279,24 @@ TEST(SecureChannelWireMessageTest,
EXPECT_EQ("testFeature", message->feature());
}
TEST(SecureChannelWireMessageTest,
Deserialize_ValidMessageWithExtraUnknownFields_V4) {
bool is_incomplete;
std::string header("\4\0\0\0\x4c", 5);
std::string bytes = header +
"{"
" \"payload\": \"YQ==\","
" \"feature\": \"testFeature\","
" \"unexpected\": \"surprise!\""
"}";
std::unique_ptr<WireMessage> message =
WireMessage::Deserialize(bytes, &is_incomplete);
EXPECT_FALSE(is_incomplete);
ASSERT_TRUE(message);
EXPECT_EQ("a", message->payload());
EXPECT_EQ("testFeature", message->feature());
}
TEST(SecureChannelWireMessageTest, Deserialize_SizeEquals0x01FF) {
// Create a message with a body of 0x01FF bytes to test the size contained in
// the header is parsed correctly.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment