| Index: components/gcm_driver/crypto/gcm_message_cryptographer.cc
|
| diff --git a/components/gcm_driver/crypto/gcm_message_cryptographer.cc b/components/gcm_driver/crypto/gcm_message_cryptographer.cc
|
| index 73a708a059cb86c3679bf1a72f77b9498b587d9f..d11989174215f04dea77c29f528ac6be758a666f 100644
|
| --- a/components/gcm_driver/crypto/gcm_message_cryptographer.cc
|
| +++ b/components/gcm_driver/crypto/gcm_message_cryptographer.cc
|
| @@ -11,6 +11,8 @@
|
| #include <sstream>
|
|
|
| #include "base/logging.h"
|
| +#include "base/macros.h"
|
| +#include "base/memory/ptr_util.h"
|
| #include "base/numerics/safe_math.h"
|
| #include "base/strings/string_util.h"
|
| #include "base/sys_byteorder.h"
|
| @@ -18,6 +20,7 @@
|
| #include "third_party/boringssl/src/include/openssl/aead.h"
|
|
|
| namespace gcm {
|
| +
|
| namespace {
|
|
|
| // Size, in bytes, of the nonce for a record. This must be at least the size
|
| @@ -37,91 +40,173 @@ using EVP_AEAD_CTX_TransformFunction =
|
| size_t max_out_len, const uint8_t *nonce, size_t nonce_len,
|
| const uint8_t *in, size_t in_len, const uint8_t *ad, size_t ad_len);
|
|
|
| -// Creates the info parameter for an HKDF value for the given |content_encoding|
|
| -// in accordance with draft-thomson-http-encryption.
|
| -//
|
| -// cek_info = "Content-Encoding: aesgcm" || 0x00 || context
|
| -// nonce_info = "Content-Encoding: nonce" || 0x00 || context
|
| -//
|
| -// context = "P-256" || 0x00 ||
|
| -// length(recipient_public) || recipient_public ||
|
| -// length(sender_public) || sender_public
|
| -//
|
| -// The length of the public keys must be written as a two octet unsigned integer
|
| -// in network byte order (big endian).
|
| -std::string InfoForContentEncoding(
|
| - const char* content_encoding,
|
| - const base::StringPiece& recipient_public_key,
|
| - const base::StringPiece& sender_public_key) {
|
| - DCHECK_EQ(recipient_public_key.size(), 65u);
|
| - DCHECK_EQ(sender_public_key.size(), 65u);
|
| +// Implementation of draft 03 of the Web Push Encryption standard:
|
| +// https://tools.ietf.org/html/draft-ietf-webpush-encryption-03
|
| +// https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-02
|
| +class WebPushEncryptionDraft03
|
| + : public GCMMessageCryptographer::EncryptionScheme {
|
| + public:
|
| + WebPushEncryptionDraft03() = default;
|
| + ~WebPushEncryptionDraft03() override = default;
|
| +
|
| + // GCMMessageCryptographer::EncryptionScheme implementation.
|
| + std::string DerivePseudoRandomKey(
|
| + const base::StringPiece& ecdh_shared_secret,
|
| + const base::StringPiece& auth_secret) override {
|
| + std::stringstream info_stream;
|
| + info_stream << "Content-Encoding: auth" << '\x00';
|
| +
|
| + crypto::HKDF hkdf(ecdh_shared_secret, auth_secret, info_stream.str(),
|
| + 32, /* key_bytes_to_generate */
|
| + 0, /* iv_bytes_to_generate */
|
| + 0 /* subkey_secret_bytes_to_generate */);
|
| +
|
| + return hkdf.client_write_key().as_string();
|
| + }
|
|
|
| - std::stringstream info_stream;
|
| - info_stream << "Content-Encoding: " << content_encoding << '\x00';
|
| - info_stream << "P-256" << '\x00';
|
| + // Creates the info parameter for an HKDF value for the given
|
| + // |content_encoding| in accordance with draft-ietf-webpush-encryption-03.
|
| + //
|
| + // cek_info = "Content-Encoding: aesgcm" || 0x00 || context
|
| + // nonce_info = "Content-Encoding: nonce" || 0x00 || context
|
| + //
|
| + // context = "P-256" || 0x00 ||
|
| + // length(recipient_public) || recipient_public ||
|
| + // length(sender_public) || sender_public
|
| + //
|
| + // The length of the public keys must be written as a two octet unsigned
|
| + // integer in network byte order (big endian).
|
| + std::string GenerateInfoForContentEncoding(
|
| + EncodingType type,
|
| + const base::StringPiece& recipient_public_key,
|
| + const base::StringPiece& sender_public_key) override {
|
| + std::stringstream info_stream;
|
| + info_stream << "Content-Encoding: ";
|
| +
|
| + switch (type) {
|
| + case EncodingType::CONTENT_ENCRYPTION_KEY:
|
| + info_stream << "aesgcm";
|
| + break;
|
| + case EncodingType::NONCE:
|
| + info_stream << "nonce";
|
| + break;
|
| + }
|
| +
|
| + info_stream << '\x00' << "P-256" << '\x00';
|
| +
|
| + uint16_t local_len =
|
| + base::HostToNet16(static_cast<uint16_t>(recipient_public_key.size()));
|
| + info_stream.write(reinterpret_cast<char*>(&local_len), sizeof(local_len));
|
| + info_stream << recipient_public_key;
|
| +
|
| + uint16_t peer_len =
|
| + base::HostToNet16(static_cast<uint16_t>(sender_public_key.size()));
|
| + info_stream.write(reinterpret_cast<char*>(&peer_len), sizeof(peer_len));
|
| + info_stream << sender_public_key;
|
| +
|
| + return info_stream.str();
|
| + }
|
|
|
| - uint16_t local_len =
|
| - base::HostToNet16(static_cast<uint16_t>(recipient_public_key.size()));
|
| - info_stream.write(reinterpret_cast<char*>(&local_len), sizeof(local_len));
|
| - info_stream << recipient_public_key;
|
| + // draft-ietf-webpush-encryption-03 defines that the padding is included at
|
| + // the beginning of the message. The first two bytes, in network byte order,
|
| + // contain the length of the included padding. Then that exact number of bytes
|
| + // must follow as padding, all of which must have a zero value.
|
| + //
|
| + // TODO(peter): Add support for message padding if the GCMMessageCryptographer
|
| + // starts encrypting payloads for reasons other than testing.
|
| + std::string CreateRecord(const base::StringPiece& plaintext) override {
|
| + std::string record;
|
| + record.reserve(sizeof(uint16_t) + plaintext.size());
|
| + record.append(sizeof(uint16_t), '\x00');
|
| +
|
| + plaintext.AppendToString(&record);
|
| + return record;
|
| + }
|
|
|
| - uint16_t peer_len =
|
| - base::HostToNet16(static_cast<uint16_t>(sender_public_key.size()));
|
| - info_stream.write(reinterpret_cast<char*>(&peer_len), sizeof(peer_len));
|
| - info_stream << sender_public_key;
|
| + // The record padding in draft-ietf-webpush-encryption-03 is included at the
|
| + // beginning of the record. The first two bytes indicate the length of the
|
| + // padding. All padding bytes immediately follow, and must be set to zero.
|
| + bool ValidateAndRemovePadding(base::StringPiece& record) override {
|
| + // Records must be at least two octets in size (to hold the padding).
|
| + // Records that are smaller, i.e. a single octet, are invalid.
|
| + if (record.size() < sizeof(uint16_t))
|
| + return false;
|
|
|
| - return info_stream.str();
|
| -}
|
| + // Records contain a two-byte, big-endian padding length followed by zero to
|
| + // 65535 bytes of padding. Padding bytes must be zero but, since AES-GCM
|
| + // authenticates the plaintext, checking and removing padding need not be
|
| + // done in constant-time.
|
| + uint16_t padding_length = (static_cast<uint8_t>(record[0]) << 8) |
|
| + static_cast<uint8_t>(record[1]);
|
| + record.remove_prefix(sizeof(uint16_t));
|
| +
|
| + if (padding_length > record.size()) {
|
| + return false;
|
| + }
|
| +
|
| + for (size_t i = 0; i < padding_length; ++i) {
|
| + if (record[i] != 0)
|
| + return false;
|
| + }
|
| +
|
| + record.remove_prefix(padding_length);
|
| + return true;
|
| + }
|
| +
|
| + private:
|
| + DISALLOW_COPY_AND_ASSIGN(WebPushEncryptionDraft03);
|
| +};
|
|
|
| } // namespace
|
|
|
| const size_t GCMMessageCryptographer::kAuthenticationTagBytes = 16;
|
| const size_t GCMMessageCryptographer::kSaltSize = 16;
|
|
|
| -GCMMessageCryptographer::GCMMessageCryptographer(
|
| - const base::StringPiece& recipient_public_key,
|
| - const base::StringPiece& sender_public_key,
|
| - const std::string& auth_secret)
|
| - : content_encryption_key_info_(
|
| - InfoForContentEncoding("aesgcm", recipient_public_key,
|
| - sender_public_key)),
|
| - nonce_info_(
|
| - InfoForContentEncoding("nonce", recipient_public_key,
|
| - sender_public_key)),
|
| - auth_secret_(auth_secret) {
|
| +GCMMessageCryptographer::GCMMessageCryptographer(Version version) {
|
| + switch (version) {
|
| + case Version::DRAFT_03:
|
| + encryption_scheme_ = base::MakeUnique<WebPushEncryptionDraft03>();
|
| + return;
|
| + }
|
| +
|
| + NOTREACHED();
|
| }
|
|
|
| -GCMMessageCryptographer::~GCMMessageCryptographer() {}
|
| +GCMMessageCryptographer::~GCMMessageCryptographer() = default;
|
|
|
| -bool GCMMessageCryptographer::Encrypt(const base::StringPiece& plaintext,
|
| - const base::StringPiece& ikm,
|
| - const base::StringPiece& salt,
|
| - size_t* record_size,
|
| - std::string* ciphertext) const {
|
| - DCHECK(ciphertext);
|
| +bool GCMMessageCryptographer::Encrypt(
|
| + const base::StringPiece& recipient_public_key,
|
| + const base::StringPiece& sender_public_key,
|
| + const base::StringPiece& ecdh_shared_secret,
|
| + const base::StringPiece& auth_secret,
|
| + const base::StringPiece& salt,
|
| + const base::StringPiece& plaintext,
|
| + size_t* record_size,
|
| + std::string* ciphertext) const {
|
| + DCHECK_EQ(recipient_public_key.size(), 65u);
|
| + DCHECK_EQ(sender_public_key.size(), 65u);
|
| DCHECK(record_size);
|
| + DCHECK(ciphertext);
|
| +
|
| + // TODO(peter): DCHECK the lengths of |ecdh_shared_secret|, |auth_secret| and
|
| + // |salt|.
|
|
|
| if (salt.size() != kSaltSize)
|
| return false;
|
|
|
| - std::string prk = DerivePseudoRandomKey(ikm);
|
| -
|
| - std::string content_encryption_key = DeriveContentEncryptionKey(prk, salt);
|
| - std::string nonce = DeriveNonce(prk, salt);
|
| + std::string prk = encryption_scheme_->DerivePseudoRandomKey(
|
| + ecdh_shared_secret, auth_secret);
|
|
|
| - // Prior to the plaintext, draft-thomson-http-encryption has a two-byte
|
| - // padding length followed by zero to 65535 bytes of padding. There is no need
|
| - // for payloads created by Chrome to be padded so the padding length is set to
|
| - // zero.
|
| - std::string record;
|
| - record.reserve(sizeof(uint16_t) + plaintext.size());
|
| - record.append(sizeof(uint16_t), '\0');
|
| -
|
| - plaintext.AppendToString(&record);
|
| + std::string content_encryption_key = DeriveContentEncryptionKey(
|
| + recipient_public_key, sender_public_key, prk, salt);
|
| + std::string nonce =
|
| + DeriveNonce(recipient_public_key, sender_public_key, prk, salt);
|
|
|
| + std::string record = encryption_scheme_->CreateRecord(plaintext);
|
| std::string encrypted_record;
|
| - if (!EncryptDecryptRecordInternal(ENCRYPT, record, content_encryption_key,
|
| - nonce, &encrypted_record)) {
|
| +
|
| + if (!TransformRecord(Direction::ENCRYPT, record, content_encryption_key,
|
| + nonce, &encrypted_record)) {
|
| return false;
|
| }
|
|
|
| @@ -133,74 +218,64 @@ bool GCMMessageCryptographer::Encrypt(const base::StringPiece& plaintext,
|
| return true;
|
| }
|
|
|
| -bool GCMMessageCryptographer::Decrypt(const base::StringPiece& ciphertext,
|
| - const base::StringPiece& ikm,
|
| - const base::StringPiece& salt,
|
| - size_t record_size,
|
| - std::string* plaintext) const {
|
| +bool GCMMessageCryptographer::Decrypt(
|
| + const base::StringPiece& recipient_public_key,
|
| + const base::StringPiece& sender_public_key,
|
| + const base::StringPiece& ecdh_shared_secret,
|
| + const base::StringPiece& auth_secret,
|
| + const base::StringPiece& salt,
|
| + const base::StringPiece& ciphertext,
|
| + size_t record_size,
|
| + std::string* plaintext) const {
|
| + DCHECK_EQ(recipient_public_key.size(), 65u);
|
| + DCHECK_EQ(sender_public_key.size(), 65u);
|
| DCHECK(plaintext);
|
|
|
| - if (salt.size() != kSaltSize || record_size <= 1)
|
| + // TODO(peter): DCHECK the lengths of |ecdh_shared_secret|, |auth_secret| and
|
| + // |salt|.
|
| +
|
| + if (record_size <= 1)
|
| return false;
|
|
|
| - // The |ciphertext| must be at least of size kAuthenticationTagBytes plus
|
| - // len(uint16) to hold the message's padding length, which is the case when an
|
| - // empty message with a zero padding length has been received. Per
|
| - // https://tools.ietf.org/html/draft-thomson-http-encryption-02#section-3, the
|
| - // |record_size| parameter must be large enough to use only one record.
|
| + std::string prk = encryption_scheme_->DerivePseudoRandomKey(
|
| + ecdh_shared_secret, auth_secret);
|
| +
|
| + std::string content_encryption_key = DeriveContentEncryptionKey(
|
| + recipient_public_key, sender_public_key, prk, salt);
|
| +
|
| + std::string nonce =
|
| + DeriveNonce(recipient_public_key, sender_public_key, prk, salt);
|
| +
|
| + // The |ciphertext| must be at least of size kAuthenticationTagBytes, which
|
| + // is the case when an empty message with a zero padding length has been
|
| + // received. The |record_size| must be large enough to use only one record.
|
| + // https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-03#section-2
|
| if (ciphertext.size() < sizeof(uint16_t) + kAuthenticationTagBytes ||
|
| ciphertext.size() > record_size + kAuthenticationTagBytes) {
|
| return false;
|
| }
|
|
|
| - std::string prk = DerivePseudoRandomKey(ikm);
|
| -
|
| - std::string content_encryption_key = DeriveContentEncryptionKey(prk, salt);
|
| - std::string nonce = DeriveNonce(prk, salt);
|
| -
|
| std::string decrypted_record_string;
|
| - if (!EncryptDecryptRecordInternal(DECRYPT, ciphertext, content_encryption_key,
|
| - nonce, &decrypted_record_string)) {
|
| + if (!TransformRecord(Direction::DECRYPT, ciphertext, content_encryption_key,
|
| + nonce, &decrypted_record_string)) {
|
| return false;
|
| }
|
|
|
| DCHECK(!decrypted_record_string.empty());
|
|
|
| base::StringPiece decrypted_record(decrypted_record_string);
|
| -
|
| - // Records must be at least two octets in size (to hold the padding). Records
|
| - // that are smaller, i.e. a single octet, are invalid.
|
| - if (decrypted_record.size() < sizeof(uint16_t))
|
| + if (!encryption_scheme_->ValidateAndRemovePadding(decrypted_record))
|
| return false;
|
|
|
| - // Records contain a two-byte, big-endian padding length followed by zero to
|
| - // 65535 bytes of padding. Padding bytes must be zero but, since AES-GCM
|
| - // authenticates the plaintext, checking and removing padding need not be done
|
| - // in constant-time.
|
| - uint16_t padding_length = (static_cast<uint8_t>(decrypted_record[0]) << 8) |
|
| - static_cast<uint8_t>(decrypted_record[1]);
|
| - decrypted_record.remove_prefix(sizeof(uint16_t));
|
| -
|
| - if (padding_length > decrypted_record.size()) {
|
| - return false;
|
| - }
|
| -
|
| - for (size_t i = 0; i < padding_length; ++i) {
|
| - if (decrypted_record[i] != 0)
|
| - return false;
|
| - }
|
| -
|
| - decrypted_record.remove_prefix(padding_length);
|
| decrypted_record.CopyToString(plaintext);
|
| return true;
|
| }
|
|
|
| -bool GCMMessageCryptographer::EncryptDecryptRecordInternal(
|
| - Mode mode,
|
| - const base::StringPiece& input,
|
| - const base::StringPiece& key,
|
| - const base::StringPiece& nonce,
|
| - std::string* output) const {
|
| +bool GCMMessageCryptographer::TransformRecord(Direction direction,
|
| + const base::StringPiece& input,
|
| + const base::StringPiece& key,
|
| + const base::StringPiece& nonce,
|
| + std::string* output) const {
|
| DCHECK(output);
|
|
|
| const EVP_AEAD* aead = EVP_aead_aes_128_gcm();
|
| @@ -213,7 +288,7 @@ bool GCMMessageCryptographer::EncryptDecryptRecordInternal(
|
| }
|
|
|
| base::CheckedNumeric<size_t> maximum_output_length(input.size());
|
| - if (mode == ENCRYPT)
|
| + if (direction == Direction::ENCRYPT)
|
| maximum_output_length += kAuthenticationTagBytes;
|
|
|
| // WriteInto requires the buffer to finish with a NULL-byte.
|
| @@ -224,7 +299,7 @@ bool GCMMessageCryptographer::EncryptDecryptRecordInternal(
|
| base::WriteInto(output, maximum_output_length.ValueOrDie()));
|
|
|
| EVP_AEAD_CTX_TransformFunction* transform_function =
|
| - mode == ENCRYPT ? EVP_AEAD_CTX_seal : EVP_AEAD_CTX_open;
|
| + direction == Direction::ENCRYPT ? EVP_AEAD_CTX_seal : EVP_AEAD_CTX_open;
|
|
|
| if (!transform_function(
|
| &context, raw_output, &output_length, output->size(),
|
| @@ -238,7 +313,7 @@ bool GCMMessageCryptographer::EncryptDecryptRecordInternal(
|
| EVP_AEAD_CTX_cleanup(&context);
|
|
|
| base::CheckedNumeric<size_t> expected_output_length(input.size());
|
| - if (mode == ENCRYPT)
|
| + if (direction == Direction::ENCRYPT)
|
| expected_output_length += kAuthenticationTagBytes;
|
| else
|
| expected_output_length -= kAuthenticationTagBytes;
|
| @@ -249,49 +324,40 @@ bool GCMMessageCryptographer::EncryptDecryptRecordInternal(
|
| return true;
|
| }
|
|
|
| -std::string GCMMessageCryptographer::DerivePseudoRandomKey(
|
| - const base::StringPiece& ikm) const {
|
| - if (allow_empty_auth_secret_for_tests_ && auth_secret_.empty())
|
| - return ikm.as_string();
|
| -
|
| - CHECK(!auth_secret_.empty());
|
| -
|
| - std::stringstream info_stream;
|
| - info_stream << "Content-Encoding: auth" << '\x00';
|
| -
|
| - crypto::HKDF hkdf(ikm, auth_secret_,
|
| - info_stream.str(),
|
| - 32, /* key_bytes_to_generate */
|
| - 0, /* iv_bytes_to_generate */
|
| - 0 /* subkey_secret_bytes_to_generate */);
|
| -
|
| - return hkdf.client_write_key().as_string();
|
| -}
|
| -
|
| std::string GCMMessageCryptographer::DeriveContentEncryptionKey(
|
| - const base::StringPiece& prk,
|
| + const base::StringPiece& recipient_public_key,
|
| + const base::StringPiece& sender_public_key,
|
| + const base::StringPiece& ecdh_shared_secret,
|
| const base::StringPiece& salt) const {
|
| - crypto::HKDF hkdf(prk, salt,
|
| - content_encryption_key_info_,
|
| - kContentEncryptionKeySize,
|
| - 0, /* iv_bytes_to_generate */
|
| - 0 /* subkey_secret_bytes_to_generate */);
|
| + std::string content_encryption_key_info =
|
| + encryption_scheme_->GenerateInfoForContentEncoding(
|
| + EncryptionScheme::EncodingType::CONTENT_ENCRYPTION_KEY,
|
| + recipient_public_key, sender_public_key);
|
| +
|
| + crypto::HKDF hkdf(ecdh_shared_secret, salt, content_encryption_key_info,
|
| + kContentEncryptionKeySize, 0, /* iv_bytes_to_generate */
|
| + 0 /* subkey_secret_bytes_to_generate */);
|
|
|
| return hkdf.client_write_key().as_string();
|
| }
|
|
|
| std::string GCMMessageCryptographer::DeriveNonce(
|
| - const base::StringPiece& prk,
|
| + const base::StringPiece& recipient_public_key,
|
| + const base::StringPiece& sender_public_key,
|
| + const base::StringPiece& ecdh_shared_secret,
|
| const base::StringPiece& salt) const {
|
| - crypto::HKDF hkdf(prk, salt,
|
| - nonce_info_,
|
| - kNonceSize,
|
| - 0, /* iv_bytes_to_generate */
|
| - 0 /* subkey_secret_bytes_to_generate */);
|
| -
|
| - // draft-thomson-http-encryption defines that the result should be XOR'ed with
|
| - // the record's sequence number, however, Web Push encryption is limited to a
|
| - // single record per draft-ietf-webpush-encryption.
|
| + std::string nonce_info = encryption_scheme_->GenerateInfoForContentEncoding(
|
| + EncryptionScheme::EncodingType::NONCE, recipient_public_key,
|
| + sender_public_key);
|
| +
|
| + crypto::HKDF hkdf(ecdh_shared_secret, salt, nonce_info, kNonceSize,
|
| + 0, /* iv_bytes_to_generate */
|
| + 0 /* subkey_secret_bytes_to_generate */);
|
| +
|
| + // https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-02
|
| + // defines that the result should be XOR'ed with the record's sequence number,
|
| + // however, Web Push encryption is limited to a single record per
|
| + // https://tools.ietf.org/html/draft-ietf-webpush-encryption-03.
|
|
|
| return hkdf.client_write_key().as_string();
|
| }
|
|
|