From 98ba096951247b7e2119afe6ba4e4ddd7d4a4485 Mon Sep 17 00:00:00 2001 From: Nils Maier Date: Sat, 19 Apr 2014 19:07:38 +0200 Subject: [PATCH 1/2] Add util::security (compare, HMAC, PBKDF2) See #220 --- src/Makefile.am | 3 +- src/util_security.cc | 190 ++++++++++++++++++ src/util_security.h | 313 +++++++++++++++++++++++++++++ test/Makefile.am | 1 + test/UtilSecurityTest.cc | 419 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 925 insertions(+), 1 deletion(-) create mode 100644 src/util_security.cc create mode 100644 src/util_security.h create mode 100644 test/UtilSecurityTest.cc diff --git a/src/Makefile.am b/src/Makefile.am index 4046715a..f33add32 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -262,7 +262,8 @@ SRCS = option_processing.cc\ ChunkChecksum.cc ChunkChecksum.h\ MessageDigest.cc MessageDigest.h\ MessageDigestImpl.h\ - HashFuncEntry.h + HashFuncEntry.h \ + util_security.cc util_security.h if ANDROID SRCS += android/android.c diff --git a/src/util_security.cc b/src/util_security.cc new file mode 100644 index 00000000..9348486b --- /dev/null +++ b/src/util_security.cc @@ -0,0 +1,190 @@ +/* */ + +#include "util_security.h" + +#include "FatalException.h" +#include "util.h" + +namespace { +using namespace aria2; + +static inline size_t getBlockSize(const std::string& algorithm) +{ + std::string canon; + if (!MessageDigest::supports(algorithm)) { + goto err; + } + canon = MessageDigest::getCanonicalHashType(algorithm); + + // Per RFC 6234 + if (canon == "sha-1") { + return 64; + } + if (canon == "sha-224") { + return 64; + } + if (canon == "sha-256") { + return 64; + } + if (canon == "sha-384") { + return 128; + } + if (canon == "sha-512") { + // RFC 4868 + return 128; + } + +err: + throw FATAL_EXCEPTION(fmt("HMAC does not support algorithm %s", algorithm.c_str())); +} + +} // namespace + +namespace aria2 { +namespace util { +namespace security { + +bool compare(const unsigned char a, const unsigned char b) +{ + unsigned char rv = ~(a ^ b); + rv &= rv >> 4; + rv &= rv >> 2; + rv &= rv >> 1; + return rv; +} + +bool compare(const uint8_t *a, const uint8_t *b, size_t length) +{ + unsigned char rv = 0; + for (size_t i = 0; i < length; ++i) { + rv |= a[i] ^ b[i]; + } + return compare(rv, 0); +} + +HMAC::HMAC(const std::string& algorithm, const char* secret, size_t length) + : blockSize_(getBlockSize(algorithm)), md_(MessageDigest::create(algorithm)), + clean_(false) +{ + ipad_.assign(blockSize_, 0x36); + opad_.assign(blockSize_, 0x5c); + + if (length > blockSize_) { + md_->reset(); + md_->update(secret, length); + auto hash = md_->digest(); + for (size_t i = 0uL, e = hash.length(); i < e; ++i) { + ipad_.replace(i, 1, 1, hash[i] ^ 0x36); + opad_.replace(i, 1, 1, hash[i] ^ 0x5c); + } + } + else { + for (size_t i = 0uL, e = length; i < e; ++i) { + ipad_.replace(i, 1, 1, secret[i] ^ 0x36); + opad_.replace(i, 1, 1, secret[i] ^ 0x5c); + } + } + reset(); +} + +std::unique_ptr HMAC::createRandom(const std::string& algorithm) +{ + const auto len = MessageDigest::getDigestLength(algorithm); + if (len == 0) { + return nullptr; + } + auto buf = make_unique(len); + generateRandomData((unsigned char*)buf.get(), len); + return create(algorithm, buf.get(), len); +} + +bool HMAC::supports(const std::string& algorithm) { + if (!MessageDigest::supports(algorithm)) { + return false; + } + const auto canon = MessageDigest::getCanonicalHashType(algorithm); + return canon == "sha-1" || canon == "sha-224" || canon == "sha-256" || + canon == "sha-384" || canon == "sha-512"; +} + +HMACResult PBKDF2(HMAC* hmac, const char* salt, size_t salt_length, + size_t iterations, size_t key_length) +{ + if (!hmac) { + throw FATAL_EXCEPTION("hmac cannot be null"); + } + const size_t hmac_length = hmac->length(); + if (key_length == 0) { + key_length = hmac_length; + } + typedef union { + uint8_t bytes[4]; + uint32_t count; + } counter_t; + counter_t counter, swapped; + counter.count = 1; + + auto work = make_unique(hmac_length); + char* p = work.get(); + std::string rv; + + hmac->reset(); + + while (key_length) { + hmac->update(salt, salt_length); + swapped.count = htonl(counter.count++); + hmac->update((char*)swapped.bytes, sizeof(swapped.bytes)); + + auto bytes = hmac->getResult().getBytes(); + memcpy(p, bytes.data(), bytes.length()); + + for (size_t i = 1uL; i < iterations; ++i) { + hmac->update(bytes); + bytes = hmac->getResult().getBytes(); + for (size_t j = 0uL; j < hmac_length; ++j) { + p[j] ^= bytes[j]; + } + } + auto use = std::min(key_length, hmac_length); + rv.append(p, use); + key_length -= use; + } + return HMACResult(rv); +} + +} // namespace security +} // namespace util +} // namespace aria2 diff --git a/src/util_security.h b/src/util_security.h new file mode 100644 index 00000000..044c535e --- /dev/null +++ b/src/util_security.h @@ -0,0 +1,313 @@ +/* */ + +#ifndef D_UTIL_SECURITY_H +#define D_UTIL_SECURITY_H + +#include + +#include "common.h" +#include "a2functional.h" +#include "MessageDigest.h" + +namespace aria2 { +namespace util { +namespace security { + +/** + * Compare to bytes in constant time. + * + * @param a First byte. + * @param b Second byte. + * @return True, if both match, false otherwise. + */ +bool compare(const uint8_t a, const uint8_t b); + +/** + * Compare two byte arrays in constant time. The arrays must have the same + * length! + * + * @param a First byte array. + * @param b Second byte array. + * @return True, if both match, false otherwise. + */ +bool compare(const uint8_t *a, const uint8_t *b, size_t length); +inline bool compare(const char *a, const char *b, size_t length) +{ + return compare( + reinterpret_cast(a), + reinterpret_cast(b), + length * sizeof(char) + ); +} + +/** + * HMAC Result wrapper. While it is still possible to get the raw result bytes, + * when using this wrapper it is ensured that constant-time comparison is used. + * Also, this wrapper makes it an error to compare results of a different + * length, helping to prevent logic errors either during development, or + * triggering in the wild. Therefore |.getBytes()| use should be avoided. + */ +class HMACResult { +public: + HMACResult(const std::string& result) + : result_(result), len_(result.length()) + {} + + HMACResult(const char* result, size_t length) + : result_(result, length), len_(length) + {} + + HMACResult(const HMACResult& other) : + result_(other.result_), len_(other.len_) + {} + + HMACResult& operator=(const HMACResult& other) { + result_ = other.result_; + len_ = other.len_; + return *this; + } + + bool operator == (const HMACResult& other) const + { + if (len_ != other.len_) { + throw std::domain_error("comparing different hmac is undefined"); + } + return compare(result_.data(), other.result_.data(), len_); + } + + bool operator != (const HMACResult& other) const + { + return !(*this == other); + } + + size_t length() const + { + return len_; + } + + const std::string& getBytes() const + { + return result_; + } + +private: + std::string result_; + size_t len_; +}; + +/** + * Implements HMAC-SHA* per RFC 6234. It supports the same cryptographic hash + * algorithms that MessageDigest supports, but at most the SHA-1, SHA-2 + * algorithms as specified in the RFC. + */ +class HMAC { +public: + /** + * Constructs a new HMAC. It is recommended to use the |create| or + * |createRandom| factory methods instead. + * + * @see create + * @see createRandom + */ + HMAC(const std::string& algorithm, const char* secret, size_t length); + + /** + * Creates a new instance using the specified algorithm and secret. + */ + static std::unique_ptr create( + const std::string& algorithm, const std::string& secret) + { + return create(algorithm, secret.data(), secret.length()); + } + + /** + * Creates a new instance using the specified algorithm and secret. + */ + static std::unique_ptr create( + const std::string& algorithm, const char* secret, size_t length) + { + if (!supports(algorithm)) { + return nullptr; + } + return make_unique(algorithm, secret, length); + } + + /** + * Creates a new instance using sha-1 and the specified secret. + */ + static std::unique_ptr create(const std::string& secret) + { + return create("sha-1", secret.data(), secret.length()); + } + + /** + * Creates a new instance using sha-1 and the specified secret. + */ + static std::unique_ptr create(const char* secret, size_t length) + { + return create("sha-1", secret, length); + } + + /** + * Creates a new instance using the specified algorithm and a random secret. + */ + static std::unique_ptr createRandom(const std::string& algorithm); + + /** + * Creates a new instance using sha-1 and a random secret. + */ + static std::unique_ptr createRandom() + { + return createRandom("sha-1"); + } + + /** + * Tells if this implementation supports a specific hash algorithm. + */ + static bool supports(const std::string& algorithm); + + /** + * Tells the length in bytes of the resulting HMAC. + */ + size_t length() const + { + return md_->getDigestLength(); + } + + /** + * Resets the instance, clearing the internal state. The instance can be + * re-used afterwards. + */ + void reset() + { + if (clean_) { + return; + } + md_->reset(); + md_->update(ipad_.data(), ipad_.length()); + clean_ = true; + } + + /** + * Updates the HMAC with new message data. + */ + void update(const std::string& data) + { + md_->update(data.data(), data.length()); + clean_ = false; + } + + /** + * Updates the HMAC with new message data. + */ + void update(const char* data, size_t length) + { + md_->update(data, length); + clean_ = false; + } + + /** + * Returns the result. This can only be called once. After the call the + * internal state is reset and new HMACs can be computed with the same + * instance. + */ + HMACResult getResult() + { + auto rv = md_->digest(); + md_->reset(); + md_->update(opad_.data(), opad_.length()); + md_->update(rv.data(), rv.length()); + rv = md_->digest(); + clean_ = false; + reset(); + return HMACResult(rv); + } + + /** + * Returns the resulting HMAC of string in one go. You cannot mix call to this + * method with calls to update. + */ + HMACResult getResult(const std::string& str) + { + reset(); + update(str); + return getResult(); + } + + /** + * Returns the resulting HMAC of string in one go. You cannot mix call to this + * method with calls to update. + */ + HMACResult getResult(const char* data, size_t len) + { + reset(); + update(data, len); + return getResult(); + } + +private: + const size_t blockSize_; + std::unique_ptr md_; + std::string ipad_, opad_; + bool clean_; +}; + +/** + * Create A PKBDF2-HMAC. See RFC 2898. + * + * Example: + * result = PBKDF2(HMAC::create("password"), random_salt, salt_len, 1000); + */ +HMACResult PBKDF2(HMAC* hmac, const char* salt, size_t salt_length, + size_t iterations, size_t key_length = 0); + +/** + * Create A PKBDF2-HMAC. See RFC 2898. + * + * Example: + * result = PBKDF2(HMAC::create("password"), random_salt, 1000); + */ +inline HMACResult PBKDF2(HMAC* hmac, const std::string& salt, size_t iterations, + size_t key_length = 0) +{ + return PBKDF2(hmac, salt.data(), salt.length(), iterations, key_length); +} + +} // namespace security +} // namespace util +} // namespace aria2 + +#endif // D_UTIL_SECURITY_H diff --git a/test/Makefile.am b/test/Makefile.am index fc83a204..3c6886a6 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -16,6 +16,7 @@ aria2c_SOURCES = AllTest.cc\ DefaultBtProgressInfoFileTest.cc\ RequestGroupTest.cc\ UtilTest.cc\ + UtilSecurityTest.cc\ UriListParserTest.cc\ HttpHeaderProcessorTest.cc\ RequestTest.cc\ diff --git a/test/UtilSecurityTest.cc b/test/UtilSecurityTest.cc new file mode 100644 index 00000000..ccd42af2 --- /dev/null +++ b/test/UtilSecurityTest.cc @@ -0,0 +1,419 @@ +#include "util.h" +#include "util_security.h" + +#include + +// Test vectors from RFC 6234 +enum { + SHA1HashSize = 20, SHA224HashSize = 28, SHA256HashSize = 32, + SHA384HashSize = 48, SHA512HashSize = 64, +}; + +static struct hmachash { + const char *keyarray[5]; + int keylength[5]; + const char *dataarray[5]; + int datalength[5]; + const char *resultarray[5]; + int resultlength[5]; +} hmachashes[] = { + { /* 1 */ { + "\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b" + "\x0b\x0b\x0b\x0b\x0b" + }, { 20 }, { + "\x48\x69\x20\x54\x68\x65\x72\x65" /* "Hi There" */ + }, { 8 }, { + /* HMAC-SHA-1 */ + "B617318655057264E28BC0B6FB378C8EF146BE00", + /* HMAC-SHA-224 */ + "896FB1128ABBDF196832107CD49DF33F47B4B1169912BA4F53684B22", + /* HMAC-SHA-256 */ + "B0344C61D8DB38535CA8AFCEAF0BF12B881DC200C9833DA726E9376C2E32" + "CFF7", + /* HMAC-SHA-384 */ + "AFD03944D84895626B0825F4AB46907F15F9DADBE4101EC682AA034C7CEB" + "C59CFAEA9EA9076EDE7F4AF152E8B2FA9CB6", + /* HMAC-SHA-512 */ + "87AA7CDEA5EF619D4FF0B4241A1D6CB02379F4E2CE4EC2787AD0B30545E1" + "7CDEDAA833B7D6B8A702038B274EAEA3F4E4BE9D914EEB61F1702E696C20" + "3A126854" + }, { SHA1HashSize, SHA224HashSize, SHA256HashSize, + SHA384HashSize, SHA512HashSize } + }, + { /* 2 */ { + "\x4a\x65\x66\x65" /* "Jefe" */ + }, { 4 }, { + "\x77\x68\x61\x74\x20\x64\x6f\x20\x79\x61\x20\x77\x61\x6e\x74" + "\x20\x66\x6f\x72\x20\x6e\x6f\x74\x68\x69\x6e\x67\x3f" + /* "what do ya want for nothing?" */ + }, { 28 }, { + /* HMAC-SHA-1 */ + "EFFCDF6AE5EB2FA2D27416D5F184DF9C259A7C79", + /* HMAC-SHA-224 */ + "A30E01098BC6DBBF45690F3A7E9E6D0F8BBEA2A39E6148008FD05E44", + /* HMAC-SHA-256 */ + "5BDCC146BF60754E6A042426089575C75A003F089D2739839DEC58B964EC" + "3843", + /* HMAC-SHA-384 */ + "AF45D2E376484031617F78D2B58A6B1B9C7EF464F5A01B47E42EC3736322" + "445E8E2240CA5E69E2C78B3239ECFAB21649", + /* HMAC-SHA-512 */ + "164B7A7BFCF819E2E395FBE73B56E0A387BD64222E831FD610270CD7EA25" + "05549758BF75C05A994A6D034F65F8F0E6FDCAEAB1A34D4A6B4B636E070A" + "38BCE737" + }, { SHA1HashSize, SHA224HashSize, SHA256HashSize, + SHA384HashSize, SHA512HashSize } + }, + { /* 3 */ + { + "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa" + "\xaa\xaa\xaa\xaa\xaa" + }, { 20 }, { + "\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd" + "\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd" + "\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd" + "\xdd\xdd\xdd\xdd\xdd" + }, { 50 }, { + /* HMAC-SHA-1 */ + "125D7342B9AC11CD91A39AF48AA17B4F63F175D3", + /* HMAC-SHA-224 */ + "7FB3CB3588C6C1F6FFA9694D7D6AD2649365B0C1F65D69D1EC8333EA", + /* HMAC-SHA-256 */ + "773EA91E36800E46854DB8EBD09181A72959098B3EF8C122D9635514CED5" + "65FE", + /* HMAC-SHA-384 */ + "88062608D3E6AD8A0AA2ACE014C8A86F0AA635D947AC9FEBE83EF4E55966" + "144B2A5AB39DC13814B94E3AB6E101A34F27", + /* HMAC-SHA-512 */ + "FA73B0089D56A284EFB0F0756C890BE9B1B5DBDD8EE81A3655F83E33B227" + "9D39BF3E848279A722C806B485A47E67C807B946A337BEE8942674278859" + "E13292FB" + }, { SHA1HashSize, SHA224HashSize, SHA256HashSize, + SHA384HashSize, SHA512HashSize } + }, + { /* 4 */ { + "\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" + "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19" + }, { 25 }, { + "\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd" + "\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd" + "\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd" + "\xcd\xcd\xcd\xcd\xcd" + }, { 50 }, { + /* HMAC-SHA-1 */ + "4C9007F4026250C6BC8414F9BF50C86C2D7235DA", + /* HMAC-SHA-224 */ + "6C11506874013CAC6A2ABC1BB382627CEC6A90D86EFC012DE7AFEC5A", + /* HMAC-SHA-256 */ + "82558A389A443C0EA4CC819899F2083A85F0FAA3E578F8077A2E3FF46729" + "665B", + /* HMAC-SHA-384 */ + "3E8A69B7783C25851933AB6290AF6CA77A9981480850009CC5577C6E1F57" + "3B4E6801DD23C4A7D679CCF8A386C674CFFB", + /* HMAC-SHA-512 */ + "B0BA465637458C6990E5A8C5F61D4AF7E576D97FF94B872DE76F8050361E" + "E3DBA91CA5C11AA25EB4D679275CC5788063A5F19741120C4F2DE2ADEBEB" + "10A298DD" + }, { SHA1HashSize, SHA224HashSize, SHA256HashSize, + SHA384HashSize, SHA512HashSize } + }, + { /* 5 */ { + "\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c" + "\x0c\x0c\x0c\x0c\x0c" + }, { 20 }, { + "Test With Truncation" + }, { 20 }, { + /* HMAC-SHA-1 */ + "4C1A03424B55E07FE7F27BE1", + /* HMAC-SHA-224 */ + "0E2AEA68A90C8D37C988BCDB9FCA6FA8", + /* HMAC-SHA-256 */ + "A3B6167473100EE06E0C796C2955552B", + /* HMAC-SHA-384 */ + "3ABF34C3503B2A23A46EFC619BAEF897", + /* HMAC-SHA-512 */ + "415FAD6271580A531D4179BC891D87A6" + }, { 12, 16, 16, 16, 16 } + }, + { /* 6 */ { + "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa" + "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa" + "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa" + "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa" + "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa" + "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa" + "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa" + "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa" + "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa" + }, { 80, 131 }, { + "Test Using Larger Than Block-Size Key - Hash Key First" + }, { 54 }, { + /* HMAC-SHA-1 */ + "AA4AE5E15272D00E95705637CE8A3B55ED402112", + /* HMAC-SHA-224 */ + "95E9A0DB962095ADAEBE9B2D6F0DBCE2D499F112F2D2B7273FA6870E", + /* HMAC-SHA-256 */ + "60E431591EE0B67F0D8A26AACBF5B77F8E0BC6213728C5140546040F0EE3" + "7F54", + /* HMAC-SHA-384 */ + "4ECE084485813E9088D2C63A041BC5B44F9EF1012A2B588F3CD11F05033A" + "C4C60C2EF6AB4030FE8296248DF163F44952", + /* HMAC-SHA-512 */ + "80B24263C7C1A3EBB71493C1DD7BE8B49B46D1F41B4AEEC1121B013783F8" + "F3526B56D037E05F2598BD0FD2215D6A1E5295E64F73F63F0AEC8B915A98" + "5D786598" + }, { SHA1HashSize, SHA224HashSize, SHA256HashSize, + SHA384HashSize, SHA512HashSize } + }, + { /* 7 */ { + "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa" + "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa" + "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa" + "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa" + "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa" + "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa" + "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa" + "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa" + "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa" + }, { 80, 131 }, { + "Test Using Larger Than Block-Size Key and " + "Larger Than One Block-Size Data", + "\x54\x68\x69\x73\x20\x69\x73\x20\x61\x20\x74\x65\x73\x74\x20" + "\x75\x73\x69\x6e\x67\x20\x61\x20\x6c\x61\x72\x67\x65\x72\x20" + "\x74\x68\x61\x6e\x20\x62\x6c\x6f\x63\x6b\x2d\x73\x69\x7a\x65" + "\x20\x6b\x65\x79\x20\x61\x6e\x64\x20\x61\x20\x6c\x61\x72\x67" + "\x65\x72\x20\x74\x68\x61\x6e\x20\x62\x6c\x6f\x63\x6b\x2d\x73" + "\x69\x7a\x65\x20\x64\x61\x74\x61\x2e\x20\x54\x68\x65\x20\x6b" + "\x65\x79\x20\x6e\x65\x65\x64\x73\x20\x74\x6f\x20\x62\x65\x20" + "\x68\x61\x73\x68\x65\x64\x20\x62\x65\x66\x6f\x72\x65\x20\x62" + "\x65\x69\x6e\x67\x20\x75\x73\x65\x64\x20\x62\x79\x20\x74\x68" + "\x65\x20\x48\x4d\x41\x43\x20\x61\x6c\x67\x6f\x72\x69\x74\x68" + "\x6d\x2e" + /* "This is a test using a larger than block-size key and a " + "larger than block-size data. The key needs to be hashed " + "before being used by the HMAC algorithm." */ + }, { 73, 152 }, { + /* HMAC-SHA-1 */ + "E8E99D0F45237D786D6BBAA7965C7808BBFF1A91", + /* HMAC-SHA-224 */ + "3A854166AC5D9F023F54D517D0B39DBD946770DB9C2B95C9F6F565D1", + /* HMAC-SHA-256 */ + "9B09FFA71B942FCB27635FBCD5B0E944BFDC63644F0713938A7F51535C3A" + "35E2", + /* HMAC-SHA-384 */ + "6617178E941F020D351E2F254E8FD32C602420FEB0B8FB9ADCCEBB82461E" + "99C5A678CC31E799176D3860E6110C46523E", + /* HMAC-SHA-512 */ + "E37B6A775DC87DBAA4DFA9F96E5E3FFDDEBD71F8867289865DF5A32D20CD" + "C944B6022CAC3C4982B10D5EEB55C3E4DE15134676FB6DE0446065C97440" + "FA8C6A58" + }, { SHA1HashSize, SHA224HashSize, SHA256HashSize, + SHA384HashSize, SHA512HashSize } + } +}; + +namespace aria2 { + +class SecurityTest: public CppUnit::TestFixture { + + CPPUNIT_TEST_SUITE(SecurityTest); + CPPUNIT_TEST(testCompareByte); + CPPUNIT_TEST(testCompareArray); + CPPUNIT_TEST(testHMAC); + CPPUNIT_TEST(testHMACRandom); + CPPUNIT_TEST(testPBKDF2); + CPPUNIT_TEST_SUITE_END(); + +private: + +public: + void setUp() { } + + void testCompareByte(); + void testCompareArray(); + void testHMAC(); + void testHMACRandom(); + void testPBKDF2(); +}; + + +CPPUNIT_TEST_SUITE_REGISTRATION(SecurityTest); + +void SecurityTest::testCompareByte() +{ + CPPUNIT_ASSERT(util::security::compare('a', 'a')); + CPPUNIT_ASSERT(util::security::compare('\0', '\0')); + CPPUNIT_ASSERT(util::security::compare(0xff, 0xff)); + + CPPUNIT_ASSERT(!util::security::compare('a', 'f')); + CPPUNIT_ASSERT(!util::security::compare('\xfe', 'f')); + CPPUNIT_ASSERT(!util::security::compare(0, 0xff)); +} + +void SecurityTest::testCompareArray() +{ + CPPUNIT_ASSERT(util::security::compare("", "", 0)); + CPPUNIT_ASSERT(util::security::compare("a", "a", 1)); + CPPUNIT_ASSERT(util::security::compare("a", "ab", 1)); + CPPUNIT_ASSERT(util::security::compare("a\0b", "a\0b", 3)); + CPPUNIT_ASSERT(util::security::compare("a\0b", "a\0b", 4)); // implicit null-termination + CPPUNIT_ASSERT(util::security::compare("a\0", "a\0b", 2)); + CPPUNIT_ASSERT(util::security::compare("a\xff", "a\xff", 2)); + + CPPUNIT_ASSERT(!util::security::compare("a", "b", 1)); + CPPUNIT_ASSERT(!util::security::compare("a", "bb", 1)); + CPPUNIT_ASSERT(!util::security::compare("a\1b", "a\0b", 3)); + CPPUNIT_ASSERT(!util::security::compare("a\1b", "a\0b", 4)); // implicit null-termination + CPPUNIT_ASSERT(!util::security::compare("a\4", "a\0b", 2)); + CPPUNIT_ASSERT(!util::security::compare("a\0", "a\xff", 2)); +} + +static struct { + const char* hash; + const size_t idx; +} hmacs[] = { + { "sha-1", 0 }, + { "sha-224", 1 }, + { "sha-256", 2 }, + { "sha-384", 3 }, + { "sha-512", 4 } +}; + +void SecurityTest::testHMAC() +{ + for (const auto& test : hmachashes) { + for (const auto& hmac : hmacs) { + if (!util::security::HMAC::supports(hmac.hash)) { + continue; + } +#define SELECT(v) test.v[hmac.idx] ? test.v[hmac.idx] : test.v[1] ? test.v[1] : test.v[0] + auto key = SELECT(keyarray); + auto keylen = SELECT(keylength); + auto data = SELECT(dataarray); + auto datalen = SELECT(datalength); + auto result = SELECT(resultarray); + auto resultlen = SELECT(resultlength); +#undef SELECT + + auto h = util::security::HMAC::create(hmac.hash, key, keylen); + auto h2 = util::security::HMAC::create(hmac.hash, key, keylen - 1); + auto r = h->getResult(data, datalen); + auto hr = util::toUpper(util::toHex(r.getBytes())); + CPPUNIT_ASSERT(util::security::compare(hr.data(), result, resultlen)); + CPPUNIT_ASSERT(r == r); + auto r2 = r; + CPPUNIT_ASSERT(r == r2); + CPPUNIT_ASSERT(r != h2->getResult(data, datalen)); + } + } +} + +void SecurityTest::testHMACRandom() +{ + auto h = util::security::HMAC::createRandom(); + CPPUNIT_ASSERT(h->getResult("abc") == h->getResult("abc")); + auto r = h->getResult("def"); + CPPUNIT_ASSERT(r == h->getResult("def")); + + // Sanity check. At the very least 3 out of ten times 2 random hmacs should + // be different from each other and the 0-secret hmac. + size_t diff = 0; + for (auto i = 0; i < 10; ++i) { + auto h1 = util::security::HMAC::createRandom("sha-1"); + auto h2 = util::security::HMAC::createRandom("sha-1"); + auto h3 = util::security::HMAC::create("sha-1", ""); + CPPUNIT_ASSERT(h1->getResult("abc") == h1->getResult("abc")); + CPPUNIT_ASSERT(h2->getResult("abc") == h2->getResult("abc")); + if (h1->getResult("abc") != h2->getResult("abc") && + h1->getResult("abc") != h3->getResult("abc") && + h2->getResult("abc") != h3->getResult("abc")) { + ++diff; + } + } + CPPUNIT_ASSERT(diff > 3); +} + +static struct pbkdf2 { + char pass[32]; + size_t pass_len; + char salt[40]; + size_t salt_len; + size_t iterations; + unsigned char key[32]; + size_t key_len; +} pbkdf2s[] = { + { + "password", 8, + "salt", 4, + 1, + { 0x0c, 0x60, 0xc8, 0x0f, 0x96, 0x1f, 0x0e, 0x71, 0xf3, 0xa9, 0xb5, 0x24, + 0xaf, 0x60, 0x12, 0x06, 0x2f, 0xe0, 0x37, 0xa6 }, + 20 + }, { + "password", 8, + "salt", 4, + 1, + { 0x0c, 0x60, 0xc8, 0x0f, 0x96, 0x1f, 0x0e, 0x71, 0xf3, 0xa9, 0xb5, 0x24, + 0xaf, 0x60, 0x12, 0x06, 0x2f, 0xe0, 0x37, 0xa6 }, + 0 + }, { + "password", 8, + "salt", 4, + 2, + { 0xea, 0x6c, 0x01, 0x4d, 0xc7, 0x2d, 0x6f, 0x8c, 0xcd, 0x1e, 0xd9, 0x2a, + 0xce, 0x1d, 0x41, 0xf0, 0xd8, 0xde, 0x89, 0x57 }, + 20 + }, { + "password", 8, + "salt", 4, + 4096, + { 0x4b, 0x00, 0x79, 0x01, 0xb7, 0x65, 0x48, 0x9a, 0xbe, 0xad, 0x49, 0xd9, + 0x26, 0xf7, 0x21, 0xd0, 0x65, 0xa4, 0x29, 0xc1 }, + 20 + }, { + "password", 8, + "salt", 4, + 16777216, + { 0xee, 0xfe, 0x3d, 0x61, 0xcd, 0x4d, 0xa4, 0xe4, 0xe9, 0x94, 0x5b, 0x3d, + 0x6b, 0xa2, 0x15, 0x8c, 0x26, 0x34, 0xe9, 0x84 }, + 20, + }, { + "passwordPASSWORDpassword", 24, + "saltSALTsaltSALTsaltSALTsaltSALTsalt", 36, + 4096, + { 0x3d, 0x2e, 0xec, 0x4f, 0xe4, 0x1c, 0x84, 0x9b, 0x80, 0xc8, 0xd8, 0x36, + 0x62, 0xc0, 0xe4, 0x4a, 0x8b, 0x29, 0x1a, 0x96, 0x4c, 0xf2, 0xf0, 0x70, + 0x38 }, + 25, + }, { + "pass\0word", + 9, + "sa\0lt", + 5, + 4096, + { 0x56, 0xfa, 0x6a, 0xa7, 0x55, 0x48, 0x09, 0x9d, 0xcc, 0x37, 0xd7, 0xf0, + 0x34, 0x25, 0xe0, 0xc3 }, + 16 + } +}; + +void SecurityTest::testPBKDF2() +{ + for (const auto& test : pbkdf2s) { + auto h = util::security::HMAC::create(test.pass, test.pass_len); + auto r = util::security::PBKDF2(h.get(), test.salt, test.salt_len, + test.iterations, test.key_len); + const auto kl = test.key_len ? test.key_len : r.length(); + auto e = util::security::HMACResult( (char*)test.key, kl); +#if 0 + printf("actual [%s]  expected [%s]\n", + util::toHex(r.getBytes()).c_str(), + util::toHex(e.getBytes()).c_str() + ); +#endif + CPPUNIT_ASSERT(r == e); + CPPUNIT_ASSERT(r.getBytes() == std::string((char*)test.key, kl)); + } +} + +} // namespace aria2 From 82dad90ff3fe1438ce5d578dcf50bc82e5c96e9b Mon Sep 17 00:00:00 2001 From: Nils Maier Date: Sat, 19 Apr 2014 19:10:06 +0200 Subject: [PATCH 2/2] Validate token using PBKDF2-HMAC-SHA1. This change should make token validation more resilient to: - timing attacks (constant time array compare) - brute-force/dictionary attacks (PBKDF2) Closes #220 --- src/DownloadEngine.cc | 92 ++++++++++++++++++++++++++++++++++++++++++- src/DownloadEngine.h | 16 ++++++++ src/RpcMethod.cc | 6 +-- test/RpcMethodTest.cc | 2 +- 4 files changed, 110 insertions(+), 6 deletions(-) diff --git a/src/DownloadEngine.cc b/src/DownloadEngine.cc index efa86950..36b67469 100644 --- a/src/DownloadEngine.cc +++ b/src/DownloadEngine.cc @@ -73,6 +73,15 @@ #ifdef ENABLE_WEBSOCKET # include "WebSocketSessionMan.h" #endif // ENABLE_WEBSOCKET +#include "Option.h" +#include "util_security.h" + +// Lower time limit for PBKDF2 operations in validateToken. +static const double kTokenTimeLower = 0.055; +// Upper time limit for PBKDF2 operations in validateToken. +static const double kTokenTimeUpper = 0.120; +// Sweet spot time for PBKDF2 operations in validateToken. +static const double kTokenTimeSweetspot = 0.072; namespace aria2 { @@ -102,7 +111,8 @@ DownloadEngine::DownloadEngine(std::unique_ptr eventPoll) asyncDNSServers_(nullptr), #endif // HAVE_ARES_ADDR_NODE dnsCache_(make_unique()), - option_(nullptr) + option_(nullptr), + tokenIterations_(5000) { unsigned char sessionId[20]; util::generateRandomKey(sessionId); @@ -631,4 +641,84 @@ void DownloadEngine::setWebSocketSessionMan } #endif // ENABLE_WEBSOCKET +bool DownloadEngine::validateToken(const std::string& token) +{ + using namespace util::security; + + if (!option_->defined(PREF_RPC_SECRET)) { + return true; + } + + if (!tokenHMAC_ || tokenAverageDuration_ > kTokenTimeUpper || + tokenAverageDuration_ < kTokenTimeLower) { + + // Setup our stuff. + if (tokenHMAC_) { + A2_LOG_INFO(fmt("Recalculating iterations because avg. duration is %.4f", + tokenAverageDuration_)); + } + + tokenHMAC_ = HMAC::createRandom(); + if (!tokenHMAC_) { + A2_LOG_ERROR("Failed to create HMAC"); + return false; + } + + // This should still be pretty fast on a modern system... Well, too fast + // with the initial 5000 iterations, and that is why we adjust it. + // XXX We should run this setup high priorty, so that other processes on the + // system don't mess up our results and let us underestimate the iterations. + std::deque mm; + for (auto i = 0; i < 10; ++i) { + auto c = std::clock(); + tokenExpected_ = make_unique( + PBKDF2(tokenHMAC_.get(), option_->get(PREF_RPC_SECRET), + tokenIterations_)); + mm.push_back((std::clock() - c) / (double)CLOCKS_PER_SEC); + } + std::sort(mm.begin(), mm.end()); + // Pop outliers. + mm.pop_front(); + mm.pop_back(); + mm.pop_back(); + auto duration = std::accumulate(mm.begin(), mm.end(), 0.0) / mm.size(); + + A2_LOG_INFO(fmt("Took us %.4f secs on average to perform PBKDF2 with %zu " + "iterations during setup", + duration, tokenIterations_)); + + // Adjust iterations so that an op takes about |kTokenTimeSpeetspot| sec, + // which would allow for a couple attempts per second (instead of + // potentially thousands without PBKDF2). + // We might overestimate the performance a bit, but should not perform + // worse than |kTokenTimeUpper| secs per attempt on a normally loaded system + // and no better than |kTokenTimeLower|. If this does not hold true anymore, + // the |tokenAverageDuration_| checks will force a re-calcuation. + tokenIterations_ *= kTokenTimeSweetspot / duration; + + auto c = std::clock(); + tokenExpected_ = make_unique( + PBKDF2(tokenHMAC_.get(), option_->get(PREF_RPC_SECRET), + tokenIterations_)); + duration = (std::clock() - c) / (double)CLOCKS_PER_SEC; + A2_LOG_INFO(fmt("Took us %.4f secs to perform PBKDF2 with %zu iterations", + duration, tokenIterations_)); + + // Seed average duration. + tokenAverageDuration_ = duration; + } + + auto c = std::clock(); + bool rv = *tokenExpected_ == PBKDF2(tokenHMAC_.get(), token, + tokenIterations_); + auto duration = (std::clock() - c) / (double)CLOCKS_PER_SEC; + A2_LOG_DEBUG(fmt("Took us %.4f secs to perform token compare with %zu " + "iterations", + duration, tokenIterations_)); + + // Update rolling hash. + tokenAverageDuration_ = tokenAverageDuration_ * 0.9 + duration * 0.1; + return rv; +} + } // namespace aria2 diff --git a/src/DownloadEngine.h b/src/DownloadEngine.h index eb827b97..eadb2f46 100644 --- a/src/DownloadEngine.h +++ b/src/DownloadEngine.h @@ -74,6 +74,13 @@ class WebSocketSessionMan; } // namespace rpc #endif // ENABLE_WEBSOCKET +namespace util { + namespace security { + class HMAC; + class HMACResult; + } // namespace security +} // namespace util + class DownloadEngine { private: void waitData(); @@ -173,6 +180,13 @@ private: // deleted. std::deque> routineCommands_; std::deque> commands_; + + std::unique_ptr tokenHMAC_; + std::unique_ptr tokenExpected_; + size_t tokenIterations_; + + double tokenAverageDuration_; + public: DownloadEngine(std::unique_ptr eventPoll); @@ -360,6 +374,8 @@ public: return webSocketSessionMan_; } #endif // ENABLE_WEBSOCKET + + bool validateToken(const std::string& token); }; } // namespace aria2 diff --git a/src/RpcMethod.cc b/src/RpcMethod.cc index e7b2750f..1f7ea20b 100644 --- a/src/RpcMethod.cc +++ b/src/RpcMethod.cc @@ -85,10 +85,8 @@ void RpcMethod::authorize(RpcRequest& req, DownloadEngine* e) } } } - if(e && e->getOption()->defined(PREF_RPC_SECRET)) { - if(token != e->getOption()->get(PREF_RPC_SECRET)) { - throw DL_ABORT_EX("Unauthorized"); - } + if (!e || !e->validateToken(token)) { + throw DL_ABORT_EX("Unauthorized"); } } diff --git a/test/RpcMethodTest.cc b/test/RpcMethodTest.cc index deea7ba4..d5614b4e 100644 --- a/test/RpcMethodTest.cc +++ b/test/RpcMethodTest.cc @@ -723,7 +723,7 @@ void RpcMethodTest::testChangeGlobalOption_withNotAllowedOption() void RpcMethodTest::testNoSuchMethod() { NoSuchMethodRpcMethod m; - auto res = m.execute(createReq("make.hamburger"), nullptr); + auto res = m.execute(createReq("make.hamburger"), e_.get()); CPPUNIT_ASSERT_EQUAL(1, res.code); CPPUNIT_ASSERT_EQUAL(std::string("No such method: make.hamburger"), getString(downcast(res.param), "faultString"));