diff options
author | albertb@chromium.org <albertb@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-03-31 16:18:30 +0000 |
---|---|---|
committer | albertb@chromium.org <albertb@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-03-31 16:18:30 +0000 |
commit | 1b47ce2e8d25023f531f4afa8f05b044c4cef111 (patch) | |
tree | 7b4875711b3b1aea46b06ad0d2bb84194d0301c7 /base | |
parent | 61ee6287a14aed0235a40488394fb700e9c5c43c (diff) | |
download | chromium_src-1b47ce2e8d25023f531f4afa8f05b044c4cef111.zip chromium_src-1b47ce2e8d25023f531f4afa8f05b044c4cef111.tar.gz chromium_src-1b47ce2e8d25023f531f4afa8f05b044c4cef111.tar.bz2 |
First pass of a Nigori implementation for Chrome. Only unassisted key
derivation is supported and there is no support for server authentication.
BUG=37363
TEST=unit tests
Review URL: http://codereview.chromium.org/1357003
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@43220 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'base')
-rw-r--r-- | base/base.gyp | 1 | ||||
-rw-r--r-- | base/base.gypi | 2 | ||||
-rw-r--r-- | base/crypto/encryptor.h | 11 | ||||
-rw-r--r-- | base/crypto/encryptor_mac.cc | 8 | ||||
-rw-r--r-- | base/crypto/encryptor_nss.cc | 3 | ||||
-rw-r--r-- | base/crypto/symmetric_key_nss.cc | 1 | ||||
-rw-r--r-- | base/hmac.h | 5 | ||||
-rw-r--r-- | base/hmac_nss.cc | 25 | ||||
-rw-r--r-- | base/nigori.cc | 221 | ||||
-rw-r--r-- | base/nigori.h | 72 | ||||
-rw-r--r-- | base/nigori_unittest.cc | 101 |
11 files changed, 427 insertions, 23 deletions
diff --git a/base/base.gyp b/base/base.gyp index 72255de..37c21ff 100644 --- a/base/base.gyp +++ b/base/base.gyp @@ -100,6 +100,7 @@ 'mac_util_unittest.mm', 'message_loop_unittest.cc', 'message_pump_glib_unittest.cc', + 'nigori_unittest.cc', 'object_watcher_unittest.cc', 'observer_list_unittest.cc', 'path_service_unittest.cc', diff --git a/base/base.gypi b/base/base.gypi index 907f490..484b7bf 100644 --- a/base/base.gypi +++ b/base/base.gypi @@ -535,6 +535,8 @@ 'message_pump_libevent.h', 'message_pump_mac.h', 'message_pump_mac.mm', + 'nigori.h', + 'nigori.cc', 'nsimage_cache_mac.h', 'nsimage_cache_mac.mm', 'nss_util.cc', diff --git a/base/crypto/encryptor.h b/base/crypto/encryptor.h index a09c7cd..96a6d6a 100644 --- a/base/crypto/encryptor.h +++ b/base/crypto/encryptor.h @@ -17,12 +17,11 @@ class Encryptor { enum Mode { CBC }; - explicit Encryptor(); - ~Encryptor(); + Encryptor(); + virtual ~Encryptor(); - // Initializes the encryptor using |key| and |iv|. Takes ownership of |key| if - // successful. Returns false if either the key or the initialization vector - // cannot be used. + // Initializes the encryptor using |key| and |iv|. Returns false if either the + // key or the initialization vector cannot be used. bool Init(SymmetricKey* key, Mode mode, const std::string& iv); // Encrypts |plaintext| into |ciphertext|. @@ -34,8 +33,8 @@ class Encryptor { // TODO(albertb): Support streaming encryption. private: + SymmetricKey* key_; Mode mode_; - scoped_ptr<SymmetricKey> key_; #if defined(USE_NSS) ScopedPK11Slot slot_; diff --git a/base/crypto/encryptor_mac.cc b/base/crypto/encryptor_mac.cc index 4e8984a..e892c12 100644 --- a/base/crypto/encryptor_mac.cc +++ b/base/crypto/encryptor_mac.cc @@ -28,7 +28,7 @@ bool Encryptor::Init(SymmetricKey* key, Mode mode, const std::string& iv) { if (iv.size() != kCCBlockSizeAES128) return false; - key_.reset(key); + key_ = key; mode_ = mode; iv_ = iv; return true; @@ -37,12 +37,12 @@ bool Encryptor::Init(SymmetricKey* key, Mode mode, const std::string& iv) { bool Encryptor::Crypt(int /*CCOperation*/ op, const std::string& input, std::string* output) { - DCHECK(key_.get()); + DCHECK(key_); CSSM_DATA raw_key = key_->cssm_data(); // CommonCryptor.h: "A general rule for the size of the output buffer which - // must be provided by the caller is that for block ciphers, the output + // must be provided by the caller is that for block ciphers, the output // length is never larger than the input length plus the block size." - + size_t output_size = input.size() + iv_.size(); CCCryptorStatus err = CCCrypt(op, kCCAlgorithmAES128, diff --git a/base/crypto/encryptor_nss.cc b/base/crypto/encryptor_nss.cc index 78ddb64..eac6779 100644 --- a/base/crypto/encryptor_nss.cc +++ b/base/crypto/encryptor_nss.cc @@ -23,7 +23,9 @@ bool Encryptor::Init(SymmetricKey* key, Mode mode, const std::string& iv) { DCHECK(key); DCHECK_EQ(CBC, mode); + key_ = key; mode_ = mode; + if (iv.size() != AES_BLOCK_SIZE) return false; @@ -41,7 +43,6 @@ bool Encryptor::Init(SymmetricKey* key, Mode mode, const std::string& iv) { if (!param_.get()) return false; - key_.reset(key); return true; } diff --git a/base/crypto/symmetric_key_nss.cc b/base/crypto/symmetric_key_nss.cc index 0fb8cfa..5af7cde 100644 --- a/base/crypto/symmetric_key_nss.cc +++ b/base/crypto/symmetric_key_nss.cc @@ -55,7 +55,6 @@ SymmetricKey* SymmetricKey::DeriveKeyFromPassword(Algorithm algorithm, const_cast<char *>(salt.data())); salt_item.len = salt.size(); - SECOidTag cipher_algorithm = algorithm == AES ? SEC_OID_AES_256_CBC : SEC_OID_HMAC_SHA1; ScopedSECAlgorithmID alg_id(PK11_CreatePBEV2AlgorithmID(SEC_OID_PKCS5_PBKDF2, diff --git a/base/hmac.h b/base/hmac.h index bdd23a9..b915124 100644 --- a/base/hmac.h +++ b/base/hmac.h @@ -22,7 +22,8 @@ class HMAC { public: // The set of supported hash functions. Extend as required. enum HashAlgorithm { - SHA1 + SHA1, + SHA256, }; explicit HMAC(HashAlgorithm hash_alg); @@ -44,6 +45,8 @@ class HMAC { // returned in |digest|, which has |digest_length| bytes of storage available. bool Sign(const std::string& data, unsigned char* digest, int digest_length); + // TODO(albertb): Add a Verify method. + private: HashAlgorithm hash_alg_; scoped_ptr<HMACPlatformData> plat_; diff --git a/base/hmac_nss.cc b/base/hmac_nss.cc index 8f63d04..b3c0c67 100644 --- a/base/hmac_nss.cc +++ b/base/hmac_nss.cc @@ -15,31 +15,36 @@ namespace base { struct HMACPlatformData { + CK_MECHANISM_TYPE mechanism_; ScopedPK11Slot slot_; ScopedPK11SymKey sym_key_; }; HMAC::HMAC(HashAlgorithm hash_alg) : hash_alg_(hash_alg), plat_(new HMACPlatformData()) { - // Only SHA-1 digest is supported now. - DCHECK(hash_alg_ == SHA1); + // Only SHA-1 and SHA-256 hash algorithms are supported. + switch (hash_alg_) { + case SHA1: + plat_->mechanism_ = CKM_SHA_1_HMAC; + break; + case SHA256: + plat_->mechanism_ = CKM_SHA256_HMAC; + break; + default: + NOTREACHED() << "Unsupported hash algorithm"; + } } bool HMAC::Init(const unsigned char *key, int key_length) { base::EnsureNSSInit(); - if (hash_alg_ != SHA1) { - NOTREACHED(); - return false; - } - if (plat_->slot_.get()) { // Init must not be called more than twice on the same HMAC object. NOTREACHED(); return false; } - plat_->slot_.reset(PK11_GetBestSlot(CKM_SHA_1_HMAC, NULL)); + plat_->slot_.reset(PK11_GetBestSlot(plat_->mechanism_, NULL)); if (!plat_->slot_.get()) { NOTREACHED(); return false; @@ -51,7 +56,7 @@ bool HMAC::Init(const unsigned char *key, int key_length) { key_item.len = key_length; plat_->sym_key_.reset(PK11_ImportSymKey(plat_->slot_.get(), - CKM_SHA_1_HMAC, + plat_->mechanism_, PK11_OriginUnwrap, CKA_SIGN, &key_item, @@ -77,7 +82,7 @@ bool HMAC::Sign(const std::string& data, } SECItem param = { siBuffer, NULL, 0 }; - ScopedPK11Context context(PK11_CreateContextBySymKey(CKM_SHA_1_HMAC, + ScopedPK11Context context(PK11_CreateContextBySymKey(plat_->mechanism_, CKA_SIGN, plat_->sym_key_.get(), ¶m)); diff --git a/base/nigori.cc b/base/nigori.cc new file mode 100644 index 0000000..753417d --- /dev/null +++ b/base/nigori.cc @@ -0,0 +1,221 @@ +// Copyright (c) 2010 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 "base/nigori.h" + +#if defined(OS_WIN) +#include <winsock2.h> // for htonl +#endif + +#include <sstream> +#include <vector> + +#include "base/base64.h" +#include "base/crypto/encryptor.h" +#include "base/hmac.h" +#include "base/logging.h" +#include "base/rand_util.h" +#include "base/string_util.h" + +namespace base { + +// NigoriStream simplifies the concatenation operation of the Nigori protocol. +class NigoriStream { + public: + // Append the big-endian representation of the length of |value| with 32 bits, + // followed by |value| itself to the stream. + NigoriStream& operator<<(const std::string& value) { + uint32 size = htonl(value.size()); + stream_.write((char *) &size, sizeof(uint32)); + stream_ << value; + return *this; + } + + // Append the big-endian representation of the length of |type| with 32 bits, + // followed by the big-endian representation of the value of |type|, with 32 + // bits, to the stream. + NigoriStream& operator<<(const Nigori::Type type) { + uint32 size = htonl(sizeof(uint32)); + stream_.write((char *) &size, sizeof(uint32)); + uint32 value = htonl(type); + stream_.write((char *) &value, sizeof(uint32)); + return *this; + } + + std::string str() { + return stream_.str(); + } + + private: + std::ostringstream stream_; +}; + +// static +const char Nigori::kSaltSalt[] = "saltsalt"; + +Nigori::Nigori(const std::string& hostname) + : hostname_(hostname) { +} + +Nigori::~Nigori() { +} + +bool Nigori::Init(const std::string& username, const std::string& password) { + NigoriStream salt_password; + salt_password << username << hostname_; + + // Suser = PBKDF2(Username || Servername, "saltsalt", Nsalt, 8) + scoped_ptr<SymmetricKey> user_salt(SymmetricKey::DeriveKeyFromPassword( + SymmetricKey::HMAC_SHA1, salt_password.str(), + kSaltSalt, + kSaltIterations, + kSaltKeySize)); + DCHECK(user_salt.get()); + + std::string raw_user_salt; + user_salt->GetRawKey(&raw_user_salt); + + // Kuser = PBKDF2(P, Suser, Nuser, 16) + user_key_.reset(SymmetricKey::DeriveKeyFromPassword(SymmetricKey::AES, + password, raw_user_salt, kUserIterations, kDerivedKeySizeInBits)); + DCHECK(user_key_.get()); + + // Kenc = PBKDF2(P, Suser, Nenc, 16) + encryption_key_.reset(SymmetricKey::DeriveKeyFromPassword(SymmetricKey::AES, + password, raw_user_salt, kEncryptionIterations, kDerivedKeySizeInBits)); + DCHECK(encryption_key_.get()); + + // Kmac = PBKDF2(P, Suser, Nmac, 16) + mac_key_.reset(SymmetricKey::DeriveKeyFromPassword( + SymmetricKey::HMAC_SHA1, password, raw_user_salt, kSigningIterations, + kDerivedKeySizeInBits)); + DCHECK(mac_key_.get()); + + return true; +} + +// Permute[Kenc,Kmac](type || name) +bool Nigori::Permute(Type type, const std::string& name, + std::string* permuted) { + DCHECK_LT(0U, name.size()); + + NigoriStream plaintext; + plaintext << type << name; + + Encryptor encryptor; + if (!encryptor.Init(encryption_key_.get(), Encryptor::CBC, + std::string(kIvSize, 0))) + return false; + + std::string ciphertext; + if (!encryptor.Encrypt(plaintext.str(), &ciphertext)) + return false; + + std::string raw_mac_key; + if (!mac_key_->GetRawKey(&raw_mac_key)) + return false; + + HMAC hmac(HMAC::SHA256); + if (!hmac.Init(raw_mac_key)) + return false; + + std::vector<unsigned char> hash(kHashSize); + if (!hmac.Sign(ciphertext, &hash[0], hash.size())) + return false; + + std::string output; + output.assign(ciphertext); + output.append(hash.begin(), hash.end()); + + return Base64Encode(output, permuted); +} + +std::string GenerateRandomString(size_t size) { + // TODO(albertb): Use a secure random function. + std::string random(size, 0); + for (size_t i = 0; i < size; ++i) + random[i] = RandInt(0, 0xff); + return random; +} + +// Enc[Kenc,Kmac](value) +bool Nigori::Encrypt(const std::string& value, std::string* encrypted) { + DCHECK_LT(0U, value.size()); + + std::string iv = GenerateRandomString(kIvSize); + + Encryptor encryptor; + if (!encryptor.Init(encryption_key_.get(), Encryptor::CBC, iv)) + return false; + + std::string ciphertext; + if (!encryptor.Encrypt(value, &ciphertext)) + return false; + + std::string raw_mac_key; + if (!mac_key_->GetRawKey(&raw_mac_key)) + return false; + + HMAC hmac(HMAC::SHA256); + if (!hmac.Init(raw_mac_key)) + return false; + + std::vector<unsigned char> hash(kHashSize); + if (!hmac.Sign(ciphertext, &hash[0], hash.size())) + return false; + + std::string output; + output.assign(iv); + output.append(ciphertext); + output.append(hash.begin(), hash.end()); + + return Base64Encode(output, encrypted); +} + +bool Nigori::Decrypt(const std::string& encrypted, std::string* value) { + std::string input; + if (!Base64Decode(encrypted, &input)) + return false; + + if (input.size() < kIvSize * 2 + kHashSize) + return false; + + // The input is: + // * iv (16 bytes) + // * ciphertext (multiple of 16 bytes) + // * hash (32 bytes) + std::string iv(input.substr(0, kIvSize)); + std::string ciphertext(input.substr(kIvSize, + input.size() - (kIvSize + kHashSize))); + std::string hash(input.substr(input.size() - kHashSize, kHashSize)); + + std::string raw_mac_key; + if (!mac_key_->GetRawKey(&raw_mac_key)) + return false; + + HMAC hmac(HMAC::SHA256); + if (!hmac.Init(raw_mac_key)) + return false; + + std::vector<unsigned char> expected(kHashSize); + if (!hmac.Sign(ciphertext, &expected[0], expected.size())) + return false; + + if (hash.compare(0, hash.size(), + reinterpret_cast<char *>(&expected[0]), + expected.size())) + return false; + + Encryptor encryptor; + if (!encryptor.Init(encryption_key_.get(), Encryptor::CBC, iv)) + return false; + + std::string plaintext; + if (!encryptor.Decrypt(ciphertext, value)) + return false; + + return true; +} + +} // namespace base diff --git a/base/nigori.h b/base/nigori.h new file mode 100644 index 0000000..df6bf9a --- /dev/null +++ b/base/nigori.h @@ -0,0 +1,72 @@ +// Copyright (c) 2010 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 BASE_CRYPTO_NIGORI_H_ +#define BASE_CRYPTO_NIGORI_H_ + +#include <string> + +#include "base/crypto/symmetric_key.h" +#include "base/scoped_ptr.h" + +namespace base { + +// A (partial) implementation of Nigori, a protocol to securely store secrets in +// the cloud. This implementation does not support server authentication or +// assisted key derivation. +// +// To store secrets securely, use the |Permute| method to derive a lookup name +// for your secret (basically a map key), and |Encrypt| and |Decrypt| to store +// and retrieve the secret. +// +// TODO: Link to doc. +class Nigori { + public: + enum Type { + Password = 1, + }; + + // Creates a Nigori client for communicating with |hostname|. Note that + // |hostname| is used to derive the keys used to encrypt and decrypt data. + explicit Nigori(const std::string& hostname); + virtual ~Nigori(); + + // Initialize the client with the supplied |username| and |password|. + bool Init(const std::string& username, const std::string& password); + + // Derives a secure lookup name from |type| and |name|. If |hostname|, + // |username| and |password| are kept constant, a given |type| and |name| pair + // always yields the same |permuted| value. Note that |permuted| will be + // Base64 encoded. + bool Permute(Type type, const std::string& name, std::string* permuted); + + // Encrypts |value|. Note that on success, |encrypted| will be Base64 + // encoded. + bool Encrypt(const std::string& value, std::string* encrypted); + + // Decrypts |value| into |decrypted|. It is assumed that |value| is Base64 + // encoded. + bool Decrypt(const std::string& value, std::string* decrypted); + + static const char kSaltSalt[]; // The salt used to derive the user salt. + static const size_t kSaltKeySize = 8; + static const size_t kDerivedKeySizeInBits = 128; + static const size_t kIvSize = 16; + static const size_t kHashSize = 32; + + static const size_t kSaltIterations = 1001; + static const size_t kUserIterations = 1002; + static const size_t kEncryptionIterations = 1003; + static const size_t kSigningIterations = 1004; + + private: + const std::string hostname_; + scoped_ptr<SymmetricKey> user_key_; + scoped_ptr<SymmetricKey> encryption_key_; + scoped_ptr<SymmetricKey> mac_key_; +}; + +} // namespace base + +#endif // BASE_CRYPTO_NIGORI_H_ diff --git a/base/nigori_unittest.cc b/base/nigori_unittest.cc new file mode 100644 index 0000000..6ebbd00 --- /dev/null +++ b/base/nigori_unittest.cc @@ -0,0 +1,101 @@ +// Copyright (c) 2010 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 "base/nigori.h" + +#include <string> + +#include "base/scoped_ptr.h" +#include "base/string_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +#if defined(USE_NSS) +#define MAYBE(name) name +#else +#define MAYBE(name) DISABLED_ ## name +#endif + +TEST(NigoriTest, MAYBE(PermuteIsConstant)) { + base::Nigori nigori1("example.com"); + EXPECT_TRUE(nigori1.Init("username", "password")); + + std::string permuted1; + EXPECT_TRUE(nigori1.Permute(base::Nigori::Password, "name", &permuted1)); + + base::Nigori nigori2("example.com"); + EXPECT_TRUE(nigori2.Init("username", "password")); + + std::string permuted2; + EXPECT_TRUE(nigori2.Permute(base::Nigori::Password, "name", &permuted2)); + + EXPECT_LT(0U, permuted1.size()); + EXPECT_EQ(permuted1, permuted2); +} + +TEST(NigoriTest, MAYBE(EncryptDifferentIv)) { + base::Nigori nigori("example.com"); + EXPECT_TRUE(nigori.Init("username", "password")); + + std::string plaintext("value"); + + std::string encrypted1; + EXPECT_TRUE(nigori.Encrypt(plaintext, &encrypted1)); + + std::string encrypted2; + EXPECT_TRUE(nigori.Encrypt(plaintext, &encrypted2)); + + EXPECT_NE(encrypted1, encrypted2); +} + +TEST(NigoriTest, MAYBE(EncryptDecrypt)) { + base::Nigori nigori("example.com"); + EXPECT_TRUE(nigori.Init("username", "password")); + + std::string plaintext("value"); + + std::string encrypted; + EXPECT_TRUE(nigori.Encrypt(plaintext, &encrypted)); + + std::string decrypted; + EXPECT_TRUE(nigori.Decrypt(encrypted, &decrypted)); + + EXPECT_EQ(plaintext, decrypted); +} + +TEST(NigoriTest, MAYBE(CorruptedIv)) { + base::Nigori nigori("example.com"); + EXPECT_TRUE(nigori.Init("username", "password")); + + std::string plaintext("test"); + + std::string encrypted; + EXPECT_TRUE(nigori.Encrypt(plaintext, &encrypted)); + + // Corrupt the IV by changing one of its byte. + encrypted[0] = (encrypted[0] == 'a' ? 'b' : 'a'); + + std::string decrypted; + EXPECT_TRUE(nigori.Decrypt(encrypted, &decrypted)); + + EXPECT_NE(plaintext, decrypted); +} + +TEST(NigoriTest, MAYBE(CorruptedCiphertext)) { + base::Nigori nigori("example.com"); + EXPECT_TRUE(nigori.Init("username", "password")); + + std::string plaintext("test"); + + std::string encrypted; + EXPECT_TRUE(nigori.Encrypt(plaintext, &encrypted)); + + // Corrput the ciphertext by changing one of its bytes. + encrypted[base::Nigori::kIvSize + 10] = + (encrypted[base::Nigori::kIvSize + 10] == 'a' ? 'b' : 'a'); + + std::string decrypted; + EXPECT_FALSE(nigori.Decrypt(encrypted, &decrypted)); + + EXPECT_NE(plaintext, decrypted); +} |