summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorpeter <peter@chromium.org>2015-07-17 07:30:02 -0700
committerCommit bot <commit-bot@chromium.org>2015-07-17 14:30:33 +0000
commit40810248c4f2a3028e3d21797d0f62e8d866c9e8 (patch)
tree0796146fe89c3feafb4732ea3309507aac59be64
parent952488b2b845ed8f4843286e4348a3b97d66b622 (diff)
downloadchromium_src-40810248c4f2a3028e3d21797d0f62e8d866c9e8.zip
chromium_src-40810248c4f2a3028e3d21797d0f62e8d866c9e8.tar.gz
chromium_src-40810248c4f2a3028e3d21797d0f62e8d866c9e8.tar.bz2
Introduce the GCMMessageCryptographer class.
This class implements the encryption guts of the following drafts: https://tools.ietf.org/html/draft-thomson-webpush-encryption-01 https://tools.ietf.org/html/draft-thomson-http-encryption-01 In short, given an input, key, salt and record size, this class will calculate a content encryption key using the HKDF and apply AEAD_AES_128_GCM over it, using an authentication tag size of 16 octets. Both encrypting and decrypting routines have been implemented. Following draft-thomson-webpush-encryption-01, only messages existing of a single record are supported. The record may be prepended by some padding, a maximum of 255 bytes, to hide the length of the message. The first and primary customer of this will be the Push Messaging feature, which will mandate encryption for payloads associated with received push messages. Chrome will decrypt incoming messages. This patch does not introduce the additional functionalities required for the entire feature - generation and storage of the keys, plumbing required to expose this to JavaScript and infrastructure within the rest of the GCM Driver to discover and use the necessary keys. This CL is dependent on the following ones: https://codereview.chromium.org/1199033006/ https://codereview.chromium.org/1226033002/ BUG=486040 Review URL: https://codereview.chromium.org/1198533003 Cr-Commit-Position: refs/heads/master@{#339250}
-rw-r--r--components/components_tests.gyp1
-rw-r--r--components/gcm_driver.gypi18
-rw-r--r--components/gcm_driver/crypto/BUILD.gn15
-rw-r--r--components/gcm_driver/crypto/gcm_message_cryptographer.cc155
-rw-r--r--components/gcm_driver/crypto/gcm_message_cryptographer.h86
-rw-r--r--components/gcm_driver/crypto/gcm_message_cryptographer_nss.cc101
-rw-r--r--components/gcm_driver/crypto/gcm_message_cryptographer_openssl.cc80
-rw-r--r--components/gcm_driver/crypto/gcm_message_cryptographer_unittest.cc301
8 files changed, 757 insertions, 0 deletions
diff --git a/components/components_tests.gyp b/components/components_tests.gyp
index 2e7bb3c..509ef5e 100644
--- a/components/components_tests.gyp
+++ b/components/components_tests.gyp
@@ -232,6 +232,7 @@
],
'gcm_driver_crypto_unittest_sources': [
'gcm_driver/crypto/gcm_key_store_unittest.cc',
+ 'gcm_driver/crypto/gcm_message_cryptographer_unittest.cc',
],
'google_unittest_sources': [
'google/core/browser/google_url_tracker_unittest.cc',
diff --git a/components/gcm_driver.gypi b/components/gcm_driver.gypi
index c927e9a..699366d 100644
--- a/components/gcm_driver.gypi
+++ b/components/gcm_driver.gypi
@@ -209,6 +209,24 @@
# Note: file list duplicated in GN build.
'gcm_driver/crypto/gcm_key_store.cc',
'gcm_driver/crypto/gcm_key_store.h',
+ 'gcm_driver/crypto/gcm_message_cryptographer.cc',
+ 'gcm_driver/crypto/gcm_message_cryptographer.h',
+ 'gcm_driver/crypto/gcm_message_cryptographer_nss.cc',
+ 'gcm_driver/crypto/gcm_message_cryptographer_openssl.cc',
+ ],
+ 'conditions': [
+ ['use_openssl==1', {
+ 'sources!': [
+ 'gcm_driver/crypto/gcm_message_cryptographer_nss.cc',
+ ],
+ 'dependencies': [
+ '../third_party/boringssl/boringssl.gyp:boringssl',
+ ],
+ }, {
+ 'sources!': [
+ 'gcm_driver/crypto/gcm_message_cryptographer_openssl.cc',
+ ],
+ }],
],
},
{
diff --git a/components/gcm_driver/crypto/BUILD.gn b/components/gcm_driver/crypto/BUILD.gn
index d3aef29..f7ceece 100644
--- a/components/gcm_driver/crypto/BUILD.gn
+++ b/components/gcm_driver/crypto/BUILD.gn
@@ -2,32 +2,47 @@
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
+import("//build/config/crypto.gni")
+
# GYP version: components/gcm_driver.gypi:gcm_driver_crypto
source_set("crypto") {
sources = [
"gcm_key_store.cc",
"gcm_key_store.h",
+ "gcm_message_cryptographer.cc",
+ "gcm_message_cryptographer.h",
+ "gcm_message_cryptographer_nss.cc",
+ "gcm_message_cryptographer_openssl.cc",
]
deps = [
"//base",
"//crypto",
+ "//crypto:platform",
"//components/gcm_driver",
"//components/gcm_driver/crypto/proto",
"//components/leveldb_proto",
"//third_party/protobuf:protobuf_lite",
]
+
+ if (use_openssl) {
+ sources -= [ "gcm_message_cryptographer_nss.cc" ]
+ } else {
+ sources -= [ "gcm_message_cryptographer_openssl.cc" ]
+ }
}
source_set("unit_tests") {
testonly = true
sources = [
"gcm_key_store_unittest.cc",
+ "gcm_message_cryptographer_unittest.cc",
]
deps = [
":crypto",
"//base",
+ "//crypto:platform",
"//testing/gtest",
"//third_party/protobuf:protobuf_lite",
]
diff --git a/components/gcm_driver/crypto/gcm_message_cryptographer.cc b/components/gcm_driver/crypto/gcm_message_cryptographer.cc
new file mode 100644
index 0000000..62b7247d
--- /dev/null
+++ b/components/gcm_driver/crypto/gcm_message_cryptographer.cc
@@ -0,0 +1,155 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/gcm_driver/crypto/gcm_message_cryptographer.h"
+
+#include <algorithm>
+
+#include "base/big_endian.h"
+#include "base/logging.h"
+#include "crypto/hkdf.h"
+
+namespace gcm {
+namespace {
+
+// Size, in bytes, of the nonce for a record. This must be at least the size
+// of a uint64_t, which is used to indicate the record sequence number.
+const uint64_t kNonceSize = 12;
+
+// The default record size as defined by draft-thomson-http-encryption-01.
+const size_t kDefaultRecordSize = 4096;
+
+// Key size, in bytes, of a valid AEAD_AES_128_GCM key.
+const size_t kContentEncryptionKeySize = 16;
+
+// Salt size, in bytes, that will be used together with the key to create a
+// unique content encryption key for a given message.
+const size_t kSaltSize = 16;
+
+} // namespace
+
+const size_t GCMMessageCryptographer::kAuthenticationTagBytes = 16;
+
+GCMMessageCryptographer::GCMMessageCryptographer() {}
+
+GCMMessageCryptographer::~GCMMessageCryptographer() {}
+
+bool GCMMessageCryptographer::Encrypt(const base::StringPiece& plaintext,
+ const base::StringPiece& key,
+ const base::StringPiece& salt,
+ size_t* record_size,
+ std::string* ciphertext) const {
+ DCHECK(ciphertext);
+ DCHECK(record_size);
+
+ if (salt.size() != kSaltSize)
+ return false;
+
+ std::string content_encryption_key = DeriveContentEncryptionKey(key, salt);
+ std::string nonce = DeriveNonce(key, salt);
+
+ // draft-nottingham-http-encryption-encoding-00 allows between 0 and 255
+ // octets of padding to be inserted before the enciphered content, with the
+ // length of the padding stored in the first octet of the payload. Since
+ // there is no necessity for payloads to contain padding, don't add any.
+ std::string record;
+ record.reserve(plaintext.size() + 1);
+ record.append(1, '\0');
+ plaintext.AppendToString(&record);
+
+ std::string encrypted_record;
+ if (!EncryptDecryptRecordInternal(ENCRYPT, record, content_encryption_key,
+ nonce, &encrypted_record)) {
+ return false;
+ }
+
+ // The advertised record size must be at least one more than the padded
+ // plaintext to ensure only one record.
+ *record_size = std::max(kDefaultRecordSize, record.size() + 1);
+
+ ciphertext->swap(encrypted_record);
+ return true;
+}
+
+bool GCMMessageCryptographer::Decrypt(
+ const base::StringPiece& ciphertext,
+ const base::StringPiece& key,
+ const base::StringPiece& salt,
+ size_t record_size,
+ std::string* plaintext) const {
+ DCHECK(plaintext);
+
+ if (salt.size() != kSaltSize || record_size <= 1)
+ return false;
+
+ // The |ciphertext| must be at least kAuthenticationTagBytes + 1 bytes, which
+ // would be used for an empty message. Per
+ // https://tools.ietf.org/html/draft-thomson-webpush-encryption-01#section-3,
+ // the |record_size| parameter must be large enough to use only one record.
+ if (ciphertext.size() < kAuthenticationTagBytes + 1 ||
+ ciphertext.size() >= record_size + kAuthenticationTagBytes + 1) {
+ return false;
+ }
+
+ std::string content_encryption_key = DeriveContentEncryptionKey(key, salt);
+ std::string nonce = DeriveNonce(key, salt);
+
+ std::string decrypted_record;
+ if (!EncryptDecryptRecordInternal(DECRYPT, ciphertext, content_encryption_key,
+ nonce, &decrypted_record)) {
+ return false;
+ }
+
+ DCHECK(!decrypted_record.empty());
+
+ // Records can contain between 0 and 255 octets of padding, indicated by the
+ // first octet of the decrypted message. Padding bytes that are not set to
+ // zero are considered a fatal decryption failure as well. Since AES-GCM
+ // includes an authentication check, neither verification nor removing the
+ // padding have to be done in constant time.
+ size_t padding_length = static_cast<size_t>(decrypted_record[0]);
+ if (padding_length >= decrypted_record.size())
+ return false;
+
+ for (size_t i = 1; i <= padding_length; ++i) {
+ if (decrypted_record[i] != 0)
+ return false;
+ }
+
+ base::StringPiece decoded_record_string_piece(decrypted_record);
+ decoded_record_string_piece.remove_prefix(1 + padding_length);
+ decoded_record_string_piece.CopyToString(plaintext);
+
+ return true;
+}
+
+std::string GCMMessageCryptographer::DeriveContentEncryptionKey(
+ const base::StringPiece& key,
+ const base::StringPiece& salt) const {
+ crypto::HKDF hkdf(key, salt,
+ "Content-Encoding: aesgcm128",
+ 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& key,
+ const base::StringPiece& salt) const {
+ crypto::HKDF hkdf(key, salt,
+ "Content-Encoding: nonce",
+ kNonceSize,
+ 0, /* iv_bytes_to_generate */
+ 0 /* subkey_secret_bytes_to_generate */);
+
+ // draft-thomson-http-encryption-01 defines that the result should be XOR'ed
+ // with the record's sequence number, but because Web Push encryption is
+ // limited to a single record we do not have to do that.
+
+ return hkdf.client_write_key().as_string();
+}
+
+} // namespace gcm
diff --git a/components/gcm_driver/crypto/gcm_message_cryptographer.h b/components/gcm_driver/crypto/gcm_message_cryptographer.h
new file mode 100644
index 0000000..58c44a8
--- /dev/null
+++ b/components/gcm_driver/crypto/gcm_message_cryptographer.h
@@ -0,0 +1,86 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPONENTS_GCM_DRIVER_CRYPTO_GCM_MESSAGE_CRYPTOGRAPHER_H_
+#define COMPONENTS_GCM_DRIVER_CRYPTO_GCM_MESSAGE_CRYPTOGRAPHER_H_
+
+#include <stdint.h>
+#include <string>
+
+#include "base/compiler_specific.h"
+#include "base/gtest_prod_util.h"
+#include "base/strings/string_piece.h"
+
+namespace gcm {
+
+// Messages delivered through GCM may be encrypted according to the IETF Web
+// Push protocol, as described in draft-thomson-webpush-encryption-01:
+//
+// https://tools.ietf.org/html/draft-thomson-webpush-encryption-01
+//
+// This class implements the ability to encrypt or decrypt such messages using
+// AEAD_AES_128_GCM with a 16-octet authentication tag. The encrypted payload
+// will be stored in a single record as described in
+// draft-thomson-http-encryption-01:
+//
+// https://tools.ietf.org/html/draft-thomson-http-encryption-01
+//
+// Note that while this class is not responsible for creating or storing the
+// actual keys, it uses a key derivation function for the actual message
+// encryption/decryption, thus allowing for the safe re-use of keys in multiple
+// messages provided that a cryptographically-strong random salt is used.
+class GCMMessageCryptographer {
+ public:
+ GCMMessageCryptographer();
+ ~GCMMessageCryptographer();
+
+ // Encrypts |plaintext| using the |key| and the |salt|, both of which must be
+ // 16 octets in length. The |plaintext| will be written to a single record,
+ // and will include a 16 octet authentication tag. The encrypted result will
+ // be written to |ciphertext|, the record size to |record_size|. This
+ // implementation does not support prepending padding to the |plaintext|.
+ bool Encrypt(const base::StringPiece& plaintext,
+ const base::StringPiece& key,
+ const base::StringPiece& salt,
+ size_t* record_size,
+ std::string* ciphertext) const WARN_UNUSED_RESULT;
+
+ // Decrypts |ciphertext| using the |key| and the |salt|, both of which must be
+ // 16 octets in length. The result will be stored in |plaintext|. Note that
+ // there must only be a single record, per draft-thomson-http-encryption-01.
+ bool Decrypt(const base::StringPiece& ciphertext,
+ const base::StringPiece& key,
+ const base::StringPiece& salt,
+ size_t record_size,
+ std::string* plaintext) const WARN_UNUSED_RESULT;
+
+ private:
+ FRIEND_TEST_ALL_PREFIXES(GCMMessageCryptographerTest, InvalidRecordPadding);
+ FRIEND_TEST_ALL_PREFIXES(GCMMessageCryptographerTest, NonceGeneration);
+
+ // Size, in bytes, of the authentication tag included in the messages.
+ static const size_t kAuthenticationTagBytes;
+
+ enum Mode { ENCRYPT, DECRYPT };
+
+ // Private implementation of the encryption and decryption routines, provided
+ // by either NSS or BoringSSL depending on the platform.
+ bool EncryptDecryptRecordInternal(Mode mode,
+ const base::StringPiece& input,
+ const base::StringPiece& key,
+ const base::StringPiece& nonce,
+ std::string* output) const;
+
+ // Derives the content encryption key from |key| and |salt|.
+ std::string DeriveContentEncryptionKey(const base::StringPiece& key,
+ const base::StringPiece& salt) const;
+
+ // Derives the nonce from |key| and |salt|.
+ std::string DeriveNonce(const base::StringPiece& key,
+ const base::StringPiece& salt) const;
+};
+
+} // namespace gcm
+
+#endif // COMPONENTS_GCM_DRIVER_CRYPTO_GCM_MESSAGE_CRYPTOGRAPHER_H_
diff --git a/components/gcm_driver/crypto/gcm_message_cryptographer_nss.cc b/components/gcm_driver/crypto/gcm_message_cryptographer_nss.cc
new file mode 100644
index 0000000..bfe85df
--- /dev/null
+++ b/components/gcm_driver/crypto/gcm_message_cryptographer_nss.cc
@@ -0,0 +1,101 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/gcm_driver/crypto/gcm_message_cryptographer.h"
+
+#include <pk11pub.h>
+#include <secerr.h>
+
+#include "base/logging.h"
+#include "base/numerics/safe_math.h"
+#include "base/strings/string_util.h"
+#include "crypto/aes_128_gcm_helpers_nss.h"
+#include "crypto/scoped_nss_types.h"
+
+namespace gcm {
+
+bool GCMMessageCryptographer::EncryptDecryptRecordInternal(
+ Mode mode,
+ const base::StringPiece& input,
+ const base::StringPiece& key,
+ const base::StringPiece& nonce,
+ std::string* output) const {
+ DCHECK(output);
+
+ SECItem key_item;
+ key_item.type = siBuffer;
+ key_item.data = const_cast<unsigned char*>(
+ reinterpret_cast<const unsigned char*>(key.data()));
+ key_item.len = key.size();
+
+ const CK_ATTRIBUTE_TYPE cka_mode = mode == ENCRYPT ? CKA_ENCRYPT
+ : CKA_DECRYPT;
+
+ // TODO(peter): For AES-GCM we should be using CKM_AES_GCM as the mechanism,
+ // but because of an NSS bug we need to use CKM_AES_ECB as a work-around until
+ // we require NSS 3.15+. https://bugzilla.mozilla.org/show_bug.cgi?id=853285
+ // This does not affect the call to PK11{Decrypt,Encrypt}Helper below.
+ const CK_MECHANISM_TYPE key_mechanism = CKM_AES_ECB;
+
+ crypto::ScopedPK11Slot slot(PK11_GetInternalSlot());
+ crypto::ScopedPK11SymKey aead_key(PK11_ImportSymKey(slot.get(), key_mechanism,
+ PK11_OriginUnwrap, cka_mode, &key_item, nullptr));
+
+ CK_GCM_PARAMS gcm_params;
+ gcm_params.pIv = const_cast<unsigned char*>(
+ reinterpret_cast<const unsigned char*>(nonce.data()));
+ gcm_params.ulIvLen = nonce.size();
+
+ gcm_params.pAAD = nullptr;
+ gcm_params.ulAADLen = 0;
+
+ gcm_params.ulTagBits = kAuthenticationTagBytes * 8;
+
+ SECItem param;
+ param.type = siBuffer;
+ param.data = reinterpret_cast<unsigned char*>(&gcm_params);
+ param.len = sizeof(gcm_params);
+
+ base::CheckedNumeric<size_t> maximum_output_length(input.size());
+ if (mode == ENCRYPT)
+ maximum_output_length += kAuthenticationTagBytes;
+
+ // WriteInto requires the buffer to finish with a NULL-byte.
+ maximum_output_length += 1;
+
+ unsigned int output_length = 0;
+ unsigned char* raw_input = const_cast<unsigned char*>(
+ reinterpret_cast<const unsigned char*>(input.data()));
+ unsigned char* raw_output = reinterpret_cast<unsigned char*>(
+ base::WriteInto(output, maximum_output_length.ValueOrDie()));
+
+ if (mode == ENCRYPT) {
+ if (crypto::PK11EncryptHelper(aead_key.get(), CKM_AES_GCM, &param,
+ raw_output, &output_length, output->size(),
+ raw_input, input.size())
+ != SECSuccess) {
+ return false;
+ }
+ } else {
+ if (crypto::PK11DecryptHelper(aead_key.get(), CKM_AES_GCM, &param,
+ raw_output, &output_length, output->size(),
+ raw_input, input.size())
+ != SECSuccess) {
+ return false;
+ }
+ }
+
+ base::CheckedNumeric<size_t> expected_output_length(input.size());
+ if (mode == ENCRYPT)
+ expected_output_length += kAuthenticationTagBytes;
+ else
+ expected_output_length -= kAuthenticationTagBytes;
+
+ DCHECK_EQ(expected_output_length.ValueOrDie(), output_length);
+
+ output->resize(output_length);
+ return true;
+}
+
+} // namespace gcm
diff --git a/components/gcm_driver/crypto/gcm_message_cryptographer_openssl.cc b/components/gcm_driver/crypto/gcm_message_cryptographer_openssl.cc
new file mode 100644
index 0000000..46c13f7
--- /dev/null
+++ b/components/gcm_driver/crypto/gcm_message_cryptographer_openssl.cc
@@ -0,0 +1,80 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/gcm_driver/crypto/gcm_message_cryptographer.h"
+
+#include <openssl/aead.h>
+
+#include "base/logging.h"
+#include "base/numerics/safe_math.h"
+#include "base/strings/string_util.h"
+
+namespace gcm {
+
+namespace {
+
+// The BoringSSL functions used to seal (encrypt) and open (decrypt) a payload
+// follow the same prototype, declared as follows.
+using EVP_AEAD_CTX_TransformFunction =
+ int(const EVP_AEAD_CTX *ctx, uint8_t *out, size_t *out_len,
+ 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);
+
+} // namespace
+
+bool GCMMessageCryptographer::EncryptDecryptRecordInternal(
+ Mode mode,
+ 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();
+
+ EVP_AEAD_CTX context;
+ if (!EVP_AEAD_CTX_init(&context, aead,
+ reinterpret_cast<const uint8_t*>(key.data()),
+ key.size(), EVP_AEAD_DEFAULT_TAG_LENGTH, nullptr)) {
+ return false;
+ }
+
+ base::CheckedNumeric<size_t> maximum_output_length(input.size());
+ if (mode == ENCRYPT)
+ maximum_output_length += kAuthenticationTagBytes;
+
+ // WriteInto requires the buffer to finish with a NULL-byte.
+ maximum_output_length += 1;
+
+ size_t output_length = 0;
+ uint8_t* raw_output = reinterpret_cast<uint8_t*>(
+ base::WriteInto(output, maximum_output_length.ValueOrDie()));
+
+ EVP_AEAD_CTX_TransformFunction* transform_function =
+ mode == ENCRYPT ? EVP_AEAD_CTX_seal : EVP_AEAD_CTX_open;
+
+ if (!transform_function(
+ &context, raw_output, &output_length, output->size(),
+ reinterpret_cast<const uint8_t*>(nonce.data()), nonce.size(),
+ reinterpret_cast<const uint8_t*>(input.data()), input.size(),
+ nullptr, 0)) {
+ EVP_AEAD_CTX_cleanup(&context);
+ return false;
+ }
+
+ EVP_AEAD_CTX_cleanup(&context);
+
+ base::CheckedNumeric<size_t> expected_output_length(input.size());
+ if (mode == ENCRYPT)
+ expected_output_length += kAuthenticationTagBytes;
+ else
+ expected_output_length -= kAuthenticationTagBytes;
+
+ DCHECK_EQ(expected_output_length.ValueOrDie(), output_length);
+
+ output->resize(output_length);
+ return true;
+}
+
+} // namespace gcm
diff --git a/components/gcm_driver/crypto/gcm_message_cryptographer_unittest.cc b/components/gcm_driver/crypto/gcm_message_cryptographer_unittest.cc
new file mode 100644
index 0000000..a1a0280
--- /dev/null
+++ b/components/gcm_driver/crypto/gcm_message_cryptographer_unittest.cc
@@ -0,0 +1,301 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/gcm_driver/crypto/gcm_message_cryptographer.h"
+
+#include "base/base64.h"
+#include "base/memory/scoped_ptr.h"
+#include "base/strings/string_util.h"
+#include "crypto/random.h"
+#include "crypto/symmetric_key.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace gcm {
+
+namespace {
+
+// The number of bits of the key in AEAD_AES_128_GCM.
+const size_t kKeySizeBits = 128;
+
+// Example plaintext data to use in the tests.
+const char kExamplePlaintext[] = "Example plaintext";
+
+// A test vector contains the information necessary to either encrypt or decrypt
+// a message. These vectors were created using a JavaScript implementation of
+// the same RFCs that the GCMMessageCryptographer implements.
+struct TestVector {
+ const char* const input;
+ const char* const key;
+ const char* const salt;
+ size_t record_size;
+ const char* const output;
+};
+
+const TestVector kEncryptionTestVectors[] = {
+ // Simple message.
+ { "Hello, world!",
+ "AhA6n2oFYPWIh+cXwyv1m2C0JvmjHB4ZkXj8QylESXU=",
+ "tsJYqAGvFDk6lDEv7daecw==",
+ 4096,
+ "KNXRqBR9VKhtajeMaeKR/rHYIiORcyeFpUwWFGyS"
+ },
+ // Empty message.
+ { "",
+ "lMyvTong4VR053jfCpWmMDGW5dEDAqiTZUIU+inhTjU=",
+ "wH3uvZqcN6oey9whiGpn1A==",
+ 4096,
+ "Mnfr+AU5o7D30gjFdVOTFtw="
+ },
+ // Message with an invalid salt size.
+ { "Hello, world!",
+ "CcdxzkR6z1EY9vSrM7/IxYVxDxu46hV638EZQTPd7XI=",
+ "aRr1fI1YSGVi5XU=",
+ 4096,
+ nullptr // expected to fail
+ }
+};
+
+const TestVector kDecryptionTestVectors[] = {
+ // Simple message.
+ { "avAFNhdbQohzizu+ORbU4XHhHSaXzw9lTN7UzB/j",
+ "47ZytAw9qHlm+Q8g+7rH81rUPzaCgGcoFvlS1qxQtQk=",
+ "EuR7EVetcaWpndXd/dKeyA==",
+ 4096,
+ "Hello, world!"
+ },
+ // Simple message with 16 bytes of padding.
+ { "0198n7ZJ/ZPMnl4ZU2l9Lma5ktKbuzXCiJEXyYtROmWTP8RSiZd8sUd48xpk6Q==",
+ "MYSsNybwrTzRIzQYUq/yFPc6ugcTrJdEZJDM4NswvUg=",
+ "8sEAMQYnufo2UkKl80cUGQ==",
+ 4096,
+ "Hello, world!"
+ },
+ // Empty message.
+ { "g+ACk32a4gK2dS2xllKXn4c=",
+ "S3+Ki/+XtzR66gUp/zR75CC5JXO62pyr5fWfneTYwFE=",
+ "4RM6s19jJHdmqiVEJDp9jg==",
+ 4096,
+ ""
+ },
+ // Message with an invalid salt size.
+ { "rt4OiodS087DAQo6e24wA55k0hRPAHgz7OX7m+nj",
+ "wW3Iy5ma803lLd+ysPdHUe2NB3HqXbY0XhCCdG5Y1Gw=",
+ "N7oMH/xohAhMhOY=",
+ 4096,
+ nullptr // expected to fail
+ },
+ // Message with an invalid record size.
+ { "AsuoRkFtqLE1c0mGCae4OvgZSCSHWCoeRL9mXKjY",
+ "omxWz7tse3lgDpxUP+e7u14Dp1irvV3BdzXTcZOtsHs=",
+ "vKJD3bexto1hY64KVzS7ug==",
+ 3,
+ nullptr // expected to fail
+ },
+ // Truncated message.
+ { "AGr4BfZSXW9txWkAG8pjg7IuRWWm1Mo8bDli/PSv",
+ "kR5BMfqMKOD1yrLKE2giObXHI7merrMtnoO2oqneqXA=",
+ "SQeJSPrqHvTdSfAMF8bBzQ==",
+ 13,
+ nullptr // expected to fail
+ },
+ // Message with multiple (2) records.
+ { "H2ujfPbpRbVSy+adIG2NRe4VxkX4V0r/zl6e9xnMSF6LSutblGdWLrwQc82Xh7DXAQlihW0q3"
+ "IQzHP+LIxuAiA==",
+ "W3W4gx7sqcfmBnvNNdO9d4MBCC1bvJkvsNjZOGD+CCg=",
+ "xG0TPGi9aIcxjpXKmaYBBQ==",
+ 7,
+ nullptr // expected to fail
+ }
+};
+
+} // namespace
+
+class GCMMessageCryptographerTest : public ::testing::Test {
+ public:
+ void SetUp() override {
+ scoped_ptr<crypto::SymmetricKey> random_key(
+ crypto::SymmetricKey::GenerateRandomKey(crypto::SymmetricKey::AES,
+ kKeySizeBits));
+
+ ASSERT_TRUE(random_key->GetRawKey(&key_));
+ }
+
+ protected:
+ // Generates a cryptographically secure random salt of 16-octets in size, the
+ // required length as expected by the HKDF.
+ std::string GenerateRandomSalt() {
+ const size_t kSaltSize = 16;
+
+ std::string salt;
+
+ crypto::RandBytes(base::WriteInto(&salt, kSaltSize + 1), kSaltSize);
+ return salt;
+ }
+
+ GCMMessageCryptographer* cryptographer() { return &cryptographer_; }
+
+ base::StringPiece key() const { return key_; }
+
+ private:
+ GCMMessageCryptographer cryptographer_;
+
+ std::string key_;
+};
+
+TEST_F(GCMMessageCryptographerTest, RoundTrip) {
+ const std::string salt = GenerateRandomSalt();
+
+ size_t record_size = 0;
+
+ std::string ciphertext, plaintext;
+ ASSERT_TRUE(cryptographer()->Encrypt(kExamplePlaintext, key(), salt,
+ &record_size, &ciphertext));
+
+ EXPECT_GT(record_size, ciphertext.size() - 16);
+ EXPECT_GT(ciphertext.size(), 0u);
+
+ ASSERT_TRUE(cryptographer()->Decrypt(ciphertext, key(), salt, record_size,
+ &plaintext));
+
+ EXPECT_EQ(kExamplePlaintext, plaintext);
+}
+
+TEST_F(GCMMessageCryptographerTest, RoundTripEmptyMessage) {
+ const std::string salt = GenerateRandomSalt();
+ const std::string message = "";
+
+ size_t record_size = 0;
+
+ std::string ciphertext, plaintext;
+ ASSERT_TRUE(cryptographer()->Encrypt(message, key(), salt, &record_size,
+ &ciphertext));
+
+ EXPECT_GT(record_size, ciphertext.size() - 16);
+ EXPECT_GT(ciphertext.size(), 0u);
+
+ ASSERT_TRUE(cryptographer()->Decrypt(ciphertext, key(), salt, record_size,
+ &plaintext));
+
+ EXPECT_EQ(message, plaintext);
+}
+
+TEST_F(GCMMessageCryptographerTest, InvalidRecordSize) {
+ const std::string salt = GenerateRandomSalt();
+
+ size_t record_size = 0;
+
+ std::string ciphertext, plaintext;
+ EXPECT_TRUE(cryptographer()->Encrypt(kExamplePlaintext, key(), salt,
+ &record_size, &ciphertext));
+
+ EXPECT_GT(record_size, ciphertext.size() - 16);
+ EXPECT_FALSE(cryptographer()->Decrypt(ciphertext, key(), salt,
+ 0 /* record_size */, &plaintext));
+
+ EXPECT_FALSE(cryptographer()->Decrypt(ciphertext, key(), salt,
+ ciphertext.size() - 17, &plaintext));
+
+ EXPECT_TRUE(cryptographer()->Decrypt(ciphertext, key(), salt,
+ ciphertext.size() - 16, &plaintext));
+}
+
+TEST_F(GCMMessageCryptographerTest, InvalidRecordPadding) {
+ std::string message = std::string(1, '\0') + kExamplePlaintext;
+
+ const std::string salt = GenerateRandomSalt();
+
+ const std::string nonce = cryptographer()->DeriveNonce(key(), salt);
+ const std::string content_encryption_key =
+ cryptographer()->DeriveContentEncryptionKey(key(), salt);
+
+ ASSERT_GT(message.size(), 1u);
+ const size_t record_size = message.size() + 1;
+
+ std::string ciphertext, plaintext;
+ ASSERT_TRUE(cryptographer()->EncryptDecryptRecordInternal(
+ GCMMessageCryptographer::ENCRYPT, message, content_encryption_key, nonce,
+ &ciphertext));
+
+ ASSERT_TRUE(cryptographer()->Decrypt(ciphertext, key(), salt, record_size,
+ &plaintext));
+
+ // Note that GCMMessageCryptographer::Decrypt removes the padding.
+ EXPECT_EQ(kExamplePlaintext, plaintext);
+
+ // Now run the same steps again, but say that there are four padding octets.
+ // This should be rejected because the padding will not be all zeros.
+ message[0] = 4;
+
+ ASSERT_TRUE(cryptographer()->EncryptDecryptRecordInternal(
+ GCMMessageCryptographer::ENCRYPT, message, content_encryption_key, nonce,
+ &ciphertext));
+
+ ASSERT_FALSE(cryptographer()->Decrypt(ciphertext, key(), salt, record_size,
+ &plaintext));
+
+ // Run the same steps again, but say that there are more padding octets than
+ // the length of the message.
+ message[0] = 64;
+
+ EXPECT_GT(static_cast<size_t>(message[0]), message.size());
+ ASSERT_TRUE(cryptographer()->EncryptDecryptRecordInternal(
+ GCMMessageCryptographer::ENCRYPT, message, content_encryption_key, nonce,
+ &ciphertext));
+
+ ASSERT_FALSE(cryptographer()->Decrypt(ciphertext, key(), salt, record_size,
+ &plaintext));
+}
+
+TEST_F(GCMMessageCryptographerTest, EncryptionTestVectors) {
+ std::string key, salt, output, ciphertext;
+ size_t record_size = 0;
+
+ for (size_t i = 0; i < arraysize(kEncryptionTestVectors); ++i) {
+ SCOPED_TRACE(i);
+
+ ASSERT_TRUE(base::Base64Decode(kEncryptionTestVectors[i].key, &key));
+ ASSERT_TRUE(base::Base64Decode(kEncryptionTestVectors[i].salt, &salt));
+
+ const bool has_output = kEncryptionTestVectors[i].output;
+ const bool result = cryptographer()->Encrypt(
+ kEncryptionTestVectors[i].input, key, salt, &record_size, &ciphertext);
+
+ if (!has_output) {
+ EXPECT_FALSE(result);
+ continue;
+ }
+
+ EXPECT_TRUE(result);
+ ASSERT_TRUE(base::Base64Decode(kEncryptionTestVectors[i].output,
+ &output));
+
+ EXPECT_EQ(kEncryptionTestVectors[i].record_size, record_size);
+ EXPECT_EQ(output, ciphertext);
+ }
+}
+
+TEST_F(GCMMessageCryptographerTest, DecryptionTestVectors) {
+ std::string input, key, salt, plaintext;
+ for (size_t i = 0; i < arraysize(kDecryptionTestVectors); ++i) {
+ SCOPED_TRACE(i);
+
+ ASSERT_TRUE(base::Base64Decode(kDecryptionTestVectors[i].input, &input));
+ ASSERT_TRUE(base::Base64Decode(kDecryptionTestVectors[i].key, &key));
+ ASSERT_TRUE(base::Base64Decode(kDecryptionTestVectors[i].salt, &salt));
+
+ const bool has_output = kDecryptionTestVectors[i].output;
+ const bool result = cryptographer()->Decrypt(
+ input, key, salt, kDecryptionTestVectors[i].record_size, &plaintext);
+
+ if (!has_output) {
+ EXPECT_FALSE(result);
+ continue;
+ }
+
+ EXPECT_TRUE(result);
+ EXPECT_EQ(kDecryptionTestVectors[i].output, plaintext);
+ }
+}
+
+} // namespace gcm