Index: chrome/browser/internal_auth.cc |
diff --git a/chrome/browser/internal_auth.cc b/chrome/browser/internal_auth.cc |
new file mode 100644 |
index 0000000000000000000000000000000000000000..268ee1be3fedb4cb72e662637168df75b19abe44 |
--- /dev/null |
+++ b/chrome/browser/internal_auth.cc |
@@ -0,0 +1,480 @@ |
+// Copyright (c) 2011 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 "chrome/browser/internal_auth.h" |
+ |
+#include <algorithm> |
+#include <deque> |
+ |
+#include "base/base64.h" |
+#include "base/lazy_instance.h" |
+#include "base/rand_util.h" |
+#include "base/synchronization/lock.h" |
+#include "base/string_number_conversions.h" |
+#include "base/string_split.h" |
+#include "base/string_util.h" |
+#include "base/threading/thread_checker.h" |
+#include "base/time.h" |
+#include "base/timer.h" |
+#include "base/values.h" |
+#include "content/browser/browser_thread.h" |
+#include "crypto/hmac.h" |
+ |
+namespace { |
+ |
+typedef std::map<std::string, std::string> VarValueMap; |
+ |
+// Size of a tick in microseconds. This determines upper bound for average |
+// number of passports generated per time unit. This bound equals to |
+// (kMicrosecondsPerSecond / TickUs) calls per second. |
+const int64 kTickUs = 10000; |
+ |
+// Verification window size in ticks; that means any passport expires in |
+// (kVerificationWindowTicks * TickUs / kMicrosecondsPerSecond) seconds. |
+const int kVerificationWindowTicks = 2000; |
+ |
+// Generation window determines how well we are able to cope with bursts of |
+// GeneratePassport calls those exceed upper bound on average speed. |
+const int kGenerationWindowTicks = 20; |
+ |
+// Makes no sense to compare other way round. |
+COMPILE_ASSERT(kGenerationWindowTicks <= kVerificationWindowTicks, |
+ makes_no_sense_to_have_generation_window_larger_than_verification_one); |
+// We are not optimized for high value of kGenerationWindowTicks. |
+COMPILE_ASSERT(kGenerationWindowTicks < 30, too_large_generation_window); |
+ |
+// Regenerate key after this number of ticks. |
+const int kKeyRegenerationSoftTicks = 500000; |
+// Reject passports if key has not been regenerated in that number of ticks. |
+const int kKeyRegenerationHardTicks = kKeyRegenerationSoftTicks * 2; |
+ |
+// Limit for number of accepted var=value pairs. Feel free to bump this limit |
+// higher once needed. |
+const size_t kVarsLimit = 16; |
+ |
+// Limit for length of caller-supplied strings. Feel free to bump this limit |
+// higher once needed. |
+const size_t kStringLengthLimit = 512; |
+ |
+// Character used as a separator for construction of message to take HMAC of. |
+// It is critical to validate all caller-supplied data (used to construct |
+// message) to be clear of this separator because it could allow attacks. |
+const char kItemSeparator = '\n'; |
+ |
+// Character used for var=value separation. |
+const char kVarValueSeparator = '='; |
+ |
+const size_t kKeySizeInBytes = 128 / 8; |
+const int kHMACSizeInBytes = 256 / 8; |
+ |
+// Length of base64 string required to encode given number of raw octets. |
+#define BASE64_PER_RAW(X) (X > 0 ? ((X - 1) / 3 + 1) * 4 : 0) |
+ |
+// Size of decimal string representing 64-bit tick. |
+const size_t kTickStringLength = 20; |
+ |
+// A passport consists of 2 parts: HMAC and tick. |
+const size_t kPassportSize = |
+ BASE64_PER_RAW(kHMACSizeInBytes) + kTickStringLength; |
+ |
+int64 GetCurrentTick() { |
+ int64 tick = base::Time::Now().ToInternalValue() / kTickUs; |
+ if (tick < kVerificationWindowTicks || |
+ tick < kKeyRegenerationHardTicks || |
+ tick > kint64max - kKeyRegenerationHardTicks) { |
+ return 0; |
+ } |
+ return tick; |
+} |
+ |
+bool IsDomainSane(const std::string& domain) { |
+ return !domain.empty() && |
+ domain.size() <= kStringLengthLimit && |
+ IsStringUTF8(domain) && |
+ domain.find_first_of(kItemSeparator) == std::string::npos; |
+} |
+ |
+bool IsVarSane(const std::string& var) { |
+ static const char kAllowedChars[] = |
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ" |
+ "abcdefghijklmnopqrstuvwxyz" |
+ "0123456789" |
+ "_"; |
+ COMPILE_ASSERT( |
+ sizeof(kAllowedChars) == 26 + 26 + 10 + 1 + 1, some_mess_with_chars); |
+ // We must not allow kItemSeparator in anything used as an input to construct |
+ // message to sign. |
+ DCHECK(std::find(kAllowedChars, kAllowedChars + arraysize(kAllowedChars), |
+ kItemSeparator) == kAllowedChars + arraysize(kAllowedChars)); |
+ DCHECK(std::find(kAllowedChars, kAllowedChars + arraysize(kAllowedChars), |
+ kVarValueSeparator) == kAllowedChars + arraysize(kAllowedChars)); |
+ return !var.empty() && |
+ var.size() <= kStringLengthLimit && |
+ IsStringASCII(var) && |
+ var.find_first_not_of(kAllowedChars) == std::string::npos && |
+ !IsAsciiDigit(var[0]); |
+} |
+ |
+bool IsValueSane(const std::string& value) { |
+ return value.size() <= kStringLengthLimit && |
+ IsStringUTF8(value) && |
+ value.find_first_of(kItemSeparator) == std::string::npos; |
+} |
+ |
+bool IsVarValueMapSane(const VarValueMap& map) { |
+ if (map.size() > kVarsLimit) |
+ return false; |
+ for (VarValueMap::const_iterator it = map.begin(); it != map.end(); ++it) { |
+ const std::string& var = it->first; |
+ const std::string& value = it->second; |
+ if (!IsVarSane(var) || !IsValueSane(value)) |
+ return false; |
+ } |
+ return true; |
+} |
+ |
+void ConvertVarValueMapToBlob(const VarValueMap& map, std::string* out) { |
+ out->clear(); |
+ DCHECK(IsVarValueMapSane(map)); |
+ for (VarValueMap::const_iterator it = map.begin(); it != map.end(); ++it) |
+ *out += it->first + kVarValueSeparator + it->second + kItemSeparator; |
+} |
+ |
+void CreatePassport( |
+ const std::string& domain, |
+ const VarValueMap& map, |
+ int64 tick, |
+ const crypto::HMAC* engine, |
+ std::string* out) { |
+ DCHECK(engine); |
+ DCHECK(out); |
+ DCHECK(IsDomainSane(domain)); |
+ DCHECK(IsVarValueMapSane(map)); |
+ |
+ out->clear(); |
+ std::string result(kPassportSize, '0'); |
+ |
+ std::string blob; |
+ blob = domain + kItemSeparator; |
+ std::string tmp; |
+ ConvertVarValueMapToBlob(map, &tmp); |
+ blob += tmp + kItemSeparator + base::Uint64ToString(tick); |
+ |
+ std::string hmac; |
+ unsigned char* hmac_data = reinterpret_cast<unsigned char*>( |
+ WriteInto(&hmac, kHMACSizeInBytes + 1)); |
+ if (!engine->Sign(blob, hmac_data, kHMACSizeInBytes)) { |
+ NOTREACHED(); |
+ return; |
+ } |
+ std::string hmac_base64; |
+ if (!base::Base64Encode(hmac, &hmac_base64)) { |
+ NOTREACHED(); |
+ return; |
+ } |
+ if (hmac_base64.size() != BASE64_PER_RAW(kHMACSizeInBytes)) { |
+ NOTREACHED(); |
+ return; |
+ } |
+ DCHECK(hmac_base64.size() < result.size()); |
+ std::copy(hmac_base64.begin(), hmac_base64.end(), result.begin()); |
+ |
+ std::string tick_decimal = base::Uint64ToString(tick); |
+ DCHECK(tick_decimal.size() <= kTickStringLength); |
+ std::copy( |
+ tick_decimal.begin(), |
+ tick_decimal.end(), |
+ result.begin() + kPassportSize - tick_decimal.size()); |
+ |
+ out->swap(result); |
+} |
+ |
+} // namespace |
+ |
+namespace browser { |
+ |
+class InternalAuthVerificationService { |
+ public: |
+ InternalAuthVerificationService() |
+ : key_change_tick_(0), |
+ dark_tick_(0) { |
+ } |
+ |
+ bool VerifyPassport( |
+ const std::string& passport, |
+ const std::string& domain, |
+ const VarValueMap& map) { |
+ int64 current_tick = GetCurrentTick(); |
+ int64 tick = PreVerifyPassport(passport, domain, current_tick); |
+ if (tick == 0) |
+ return false; |
+ if (!IsVarValueMapSane(map)) |
+ return false; |
+ std::string reference_passport; |
+ CreatePassport(domain, map, tick, engine_.get(), &reference_passport); |
+ if (passport != reference_passport) { |
+ // Consider old key. |
+ if (key_change_tick_ + get_verification_window_ticks() < tick) { |
+ return false; |
+ } |
+ if (old_key_.empty() || old_engine_ == NULL) |
+ return false; |
+ CreatePassport(domain, map, tick, old_engine_.get(), &reference_passport); |
+ if (passport != reference_passport) |
+ return false; |
+ } |
+ |
+ // Record used tick to prevent reuse. |
+ std::deque<int64>::iterator it = std::lower_bound( |
+ used_ticks_.begin(), used_ticks_.end(), tick); |
+ DCHECK(it == used_ticks_.end() || *it != tick); |
+ used_ticks_.insert(it, tick); |
+ |
+ // Consider pruning |used_ticks_|. |
+ if (used_ticks_.size() > 50) { |
+ dark_tick_ = std::max(dark_tick_, |
+ current_tick - get_verification_window_ticks()); |
+ used_ticks_.erase( |
+ used_ticks_.begin(), |
+ std::lower_bound(used_ticks_.begin(), used_ticks_.end(), |
+ dark_tick_ + 1)); |
+ } |
+ return true; |
+ } |
+ |
+ void ChangeKey(const std::string& key) { |
+ old_key_.swap(key_); |
+ key_.clear(); |
+ old_engine_.swap(engine_); |
+ engine_.reset(NULL); |
+ |
+ if (key.size() != kKeySizeInBytes) |
+ return; |
+ engine_.reset(new crypto::HMAC(crypto::HMAC::SHA256)); |
+ engine_->Init(key); |
+ key_ = key; |
+ key_change_tick_ = GetCurrentTick(); |
+ } |
+ |
+ private: |
+ static int get_verification_window_ticks() { |
+ return InternalAuthVerification::get_verification_window_ticks(); |
+ } |
+ |
+ // Returns tick bound to given passport on success or zero on failure. |
+ int64 PreVerifyPassport( |
+ const std::string& passport, |
+ const std::string& domain, |
+ int64 current_tick) { |
+ if (passport.size() != kPassportSize || |
+ !IsStringASCII(passport) || |
+ !IsDomainSane(domain) || |
+ current_tick <= dark_tick_ || |
+ current_tick > key_change_tick_ + kKeyRegenerationHardTicks || |
+ key_.empty() || |
+ engine_ == NULL) { |
+ return 0; |
+ } |
+ |
+ // Passport consists of 2 parts: first hmac and then tick. |
+ std::string tick_decimal = |
+ passport.substr(BASE64_PER_RAW(kHMACSizeInBytes)); |
+ DCHECK(tick_decimal.size() == kTickStringLength); |
+ int64 tick = 0; |
+ if (!base::StringToInt64(tick_decimal, &tick) || |
+ tick <= dark_tick_ || |
+ tick > key_change_tick_ + kKeyRegenerationHardTicks || |
+ tick < current_tick - get_verification_window_ticks() || |
+ std::binary_search(used_ticks_.begin(), used_ticks_.end(), tick)) { |
+ return 0; |
+ } |
+ return tick; |
+ } |
+ |
+ // Current key. |
+ std::string key_; |
+ |
+ // We keep previous key in order to be able to verify passports during |
+ // regeneration time. Keys are regenerated on a regular basis. |
+ std::string old_key_; |
+ |
+ // Corresponding HMAC engines. |
+ scoped_ptr<crypto::HMAC> engine_; |
+ scoped_ptr<crypto::HMAC> old_engine_; |
+ |
+ // Tick at a time of recent key regeneration. |
+ int64 key_change_tick_; |
+ |
+ // Keeps track of ticks of successfully verified passports to prevent their |
+ // reuse. Size of this container is kept reasonably low by purging outdated |
+ // ticks. |
+ std::deque<int64> used_ticks_; |
+ |
+ // Some ticks before |dark_tick_| were purged from |used_ticks_| container. |
+ // That means that we must not trust any tick less than or equal to dark tick. |
+ int64 dark_tick_; |
+ |
+ DISALLOW_COPY_AND_ASSIGN(InternalAuthVerificationService); |
+}; |
+ |
+} // namespace browser |
+ |
+namespace { |
+ |
+static base::LazyInstance<browser::InternalAuthVerificationService> |
+ g_verification_service(base::LINKER_INITIALIZED); |
+static base::LazyInstance<base::Lock> g_verification_service_lock( |
+ base::LINKER_INITIALIZED); |
+ |
+} // namespace |
+ |
+namespace browser { |
+ |
+class InternalAuthGenerationService : public base::ThreadChecker { |
+ public: |
+ InternalAuthGenerationService() : key_regeneration_tick_(0) { |
+ GenerateNewKey(); |
+ } |
+ |
+ void GenerateNewKey() { |
+ DCHECK(CalledOnValidThread()); |
+ if (!timer_.IsRunning()) { |
+ timer_.Start( |
+ base::TimeDelta::FromMicroseconds( |
+ kKeyRegenerationSoftTicks * kTickUs), |
+ this, |
+ &InternalAuthGenerationService::GenerateNewKey); |
+ } |
+ |
+ engine_.reset(new crypto::HMAC(crypto::HMAC::SHA256)); |
+ std::string key = base::RandBytesAsString(kKeySizeInBytes); |
+ engine_->Init(key); |
+ key_regeneration_tick_ = GetCurrentTick(); |
+ g_verification_service.Get().ChangeKey(key); |
+ std::fill(key.begin(), key.end(), 0); |
+ } |
+ |
+ // Returns zero on failure. |
+ int64 GetUnusedTick(const std::string& domain) { |
+ DCHECK(CalledOnValidThread()); |
+ if (engine_ == NULL) { |
+ NOTREACHED(); |
+ return 0; |
+ } |
+ if (!IsDomainSane(domain)) |
+ return 0; |
+ |
+ int64 current_tick = GetCurrentTick(); |
+ if (!used_ticks_.empty() && used_ticks_.back() > current_tick) |
+ current_tick = used_ticks_.back(); |
+ if (current_tick > key_regeneration_tick_ + kKeyRegenerationHardTicks) |
+ return 0; |
+ |
+ // Forget outdated ticks if any. |
+ used_ticks_.erase( |
+ used_ticks_.begin(), |
+ std::lower_bound(used_ticks_.begin(), used_ticks_.end(), |
+ current_tick - kGenerationWindowTicks + 1)); |
+ DCHECK(used_ticks_.size() <= kGenerationWindowTicks + 0u); |
+ if (used_ticks_.size() >= kGenerationWindowTicks + 0u) { |
+ // Average speed of GeneratePassport calls exceeds limit. |
+ return 0; |
+ } |
+ for (int64 tick = current_tick; |
+ tick > current_tick - kGenerationWindowTicks; |
+ --tick) { |
+ int idx = static_cast<int>(used_ticks_.size()) - |
+ static_cast<int>(current_tick - tick + 1); |
+ if (idx < 0 || used_ticks_[idx] != tick) { |
+ DCHECK(used_ticks_.end() == |
+ std::find(used_ticks_.begin(), used_ticks_.end(), tick)); |
+ return tick; |
+ } |
+ } |
+ NOTREACHED(); |
+ return 0; |
+ } |
+ |
+ std::string GeneratePassport( |
+ const std::string& domain, const VarValueMap& map, int64 tick) { |
+ DCHECK(CalledOnValidThread()); |
+ if (tick == 0) { |
+ tick = GetUnusedTick(domain); |
+ if (tick == 0) |
+ return std::string(); |
+ } |
+ if (!IsVarValueMapSane(map)) |
+ return std::string(); |
+ |
+ std::string result; |
+ CreatePassport(domain, map, tick, engine_.get(), &result); |
+ used_ticks_.insert( |
+ std::lower_bound(used_ticks_.begin(), used_ticks_.end(), tick), tick); |
+ return result; |
+ } |
+ |
+ private: |
+ static int get_verification_window_ticks() { |
+ return InternalAuthVerification::get_verification_window_ticks(); |
+ } |
+ |
+ scoped_ptr<crypto::HMAC> engine_; |
+ base::RepeatingTimer<InternalAuthGenerationService> timer_; |
+ int64 key_regeneration_tick_; |
+ std::deque<int64> used_ticks_; |
+ |
+ DISALLOW_COPY_AND_ASSIGN(InternalAuthGenerationService); |
+}; |
+ |
+} // namespace browser |
+ |
+namespace { |
+ |
+static base::LazyInstance<browser::InternalAuthGenerationService> |
+ g_generation_service(base::LINKER_INITIALIZED); |
+ |
+} // namespace |
+ |
+namespace browser { |
+ |
+// static |
+bool InternalAuthVerification::VerifyPassport( |
+ const std::string& passport, |
+ const std::string& domain, |
+ const VarValueMap& var_value_map) { |
+ base::AutoLock alk(g_verification_service_lock.Get()); |
+ return g_verification_service.Get().VerifyPassport( |
+ passport, domain, var_value_map); |
+} |
+ |
+// static |
+void InternalAuthVerification::ChangeKey(const std::string& key) { |
+ base::AutoLock alk(g_verification_service_lock.Get()); |
+ g_verification_service.Get().ChangeKey(key); |
+}; |
+ |
+// static |
+int InternalAuthVerification::get_verification_window_ticks() { |
+ int candidate = kVerificationWindowTicks; |
+ if (verification_window_seconds_ > 0) |
+ candidate = verification_window_seconds_ * |
+ base::Time::kMicrosecondsPerSecond / kTickUs; |
+ return std::max(1, std::min(candidate, kVerificationWindowTicks)); |
+} |
+ |
+int InternalAuthVerification::verification_window_seconds_ = 0; |
+ |
+// static |
+std::string InternalAuthGeneration::GeneratePassport( |
+ const std::string& domain, const VarValueMap& var_value_map) { |
+ return g_generation_service.Get().GeneratePassport(domain, var_value_map, 0); |
+} |
+ |
+// static |
+void InternalAuthGeneration::GenerateNewKey() { |
+ g_generation_service.Get().GenerateNewKey(); |
+} |
+ |
+} // namespace browser |
+ |