diff options
author | dilmah@chromium.org <dilmah@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-05-18 11:58:44 +0000 |
---|---|---|
committer | dilmah@chromium.org <dilmah@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-05-18 11:58:44 +0000 |
commit | c6e584c20129f8745e6fc9170a220eb58e13e172 (patch) | |
tree | 6491e890f845af7443f6be07d15d9e60c89ec998 | |
parent | 37e7790801761dc99be00d69f102b7319f2d6a8e (diff) | |
download | chromium_src-c6e584c20129f8745e6fc9170a220eb58e13e172.zip chromium_src-c6e584c20129f8745e6fc9170a220eb58e13e172.tar.gz chromium_src-c6e584c20129f8745e6fc9170a220eb58e13e172.tar.bz2 |
Private API for extensions like ssh-client that need access to websocket-to-tcp proxy.
Access to TCP is obtained in following way:
(1) extension requests authentication token via call to private API like:
chrome.webSocketProxyPrivate.getPassportForTCP('netbsd.org', 25, callback);
if API validates this request
then extension obtains some string token (in callback).
(2) open websocket connection to local websocket-to-tcp proxy ws://127.0.0.1:10101/tcpproxy
(3) pass header containing hostname, port and token obtained at step (1)
(4) communicate (in base64 encoding at this moment).
Proxy (running in chrome process) verifies those tokens by calls to InternalAuthVerification::VerifyPassport
Passports are one-time; no passport can be reused.
Passports expire in short period of time (20 seconds).
BUG=chromium-os:9667
TEST=unit_test,apitest
Review URL: http://codereview.chromium.org/6683060
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@85757 0039d316-1c4b-4281-b951-d872f2087c98
29 files changed, 1174 insertions, 82 deletions
diff --git a/chrome/browser/chromeos/web_socket_proxy.cc b/chrome/browser/chromeos/web_socket_proxy.cc index 8a52746..d84ee8d 100644 --- a/chrome/browser/chromeos/web_socket_proxy.cc +++ b/chrome/browser/chromeos/web_socket_proxy.cc @@ -30,13 +30,12 @@ #include "base/memory/scoped_ptr.h" #include "base/string_number_conversions.h" #include "base/string_util.h" +#include "chrome/browser/internal_auth.h" #include "content/browser/browser_thread.h" #include "content/common/notification_service.h" #include "content/common/notification_type.h" -// TODO(dilmah): enable this once webSocketProxyPrivate.getToken is wired. -#if 0 -#include "chrome/browser/internal_auth.h" -#endif +#include "content/common/url_constants.h" +#include "googleurl/src/gurl.h" #include "third_party/libevent/evdns.h" #include "third_party/libevent/event.h" @@ -105,10 +104,10 @@ bool FetchDecimalDigits(const std::string& s, uint32* result) { return got_something; } -// Parses "token:hostname:port:" string. Returns true on success. -bool FetchTokenNamePort( +// Parses "passport:hostname:port:" string. Returns true on success. +bool FetchPassportNamePort( uint8* begin, uint8* end, - std::string* token, std::string* name, uint32* port) { + std::string* passport, std::string* name, uint32* port) { std::string input(begin, end); if (input[input.size() - 1] != ':') return false; @@ -134,14 +133,17 @@ bool FetchTokenNamePort( pos = input.find_first_of(':'); if (pos == std::string::npos) return false; - token->assign(input, 0, pos); + passport->assign(input, 0, pos); name->assign(input, pos + 1, std::string::npos); return !name->empty(); } -std::string FetchExtensionIdFromOrigin(const std::string origin) { - // Origin of extension looks like "chrome-extension://EXTENSION_ID". - return origin.substr(origin.find_last_of('/')); +std::string FetchExtensionIdFromOrigin(const std::string &origin) { + GURL url(origin); + if (url.SchemeIs(chrome::kExtensionScheme)) + return url.host(); + else + return std::string(); } inline size_t strlen(const uint8* s) { @@ -733,16 +735,22 @@ Conn::Status Conn::ConsumeHeader(struct evbuffer* evb) { return STATUS_ABORT; } - if (!master_->IsOriginAllowed(header_fields_["origin"])) + // Normalize origin (e.g. leading slash). + GURL origin = GURL(header_fields_["origin"]).GetOrigin(); + if (!origin.is_valid()) + return STATUS_ABORT; + // Here we check origin. This check may seem redundant because we verify + // passport token later. However the earlier we can reject connection the + // better. We receive origin field in websocket header way before receiving + // passport string. + if (!master_->IsOriginAllowed(origin.spec())) return STATUS_ABORT; static const std::string kSecKey1 = "sec-websocket-key1"; static const std::string kSecKey2 = "sec-websocket-key2"; uint32 key_number1, key_number2; - if (!FetchDecimalDigits(header_fields_[kSecKey1], - &key_number1) || - !FetchDecimalDigits(header_fields_[kSecKey2], - &key_number2)) { + if (!FetchDecimalDigits(header_fields_[kSecKey1], &key_number1) || + !FetchDecimalDigits(header_fields_[kSecKey2], &key_number2)) { return STATUS_ABORT; } @@ -828,20 +836,19 @@ Conn::Status Conn::ConsumeDestframe(struct evbuffer* evb) { return STATUS_INCOMPLETE; } - std::string token; - if (!FetchTokenNamePort(buf + 1, term_pos, &token, &destname_, &destport_)) + std::string passport; + if (!FetchPassportNamePort( + buf + 1, term_pos, &passport, &destname_, &destport_)) { return STATUS_ABORT; - // TODO(dilmah): enable this once webSocketProxyPrivate.getToken is wired. -#if 0 + } std::map<std::string, std::string> map; map["hostname"] = destname_; map["port"] = base::IntToString(destport_); map["extension_id"] = FetchExtensionIdFromOrigin(header_fields_["origin"]); - if (!browser::InternalAuthVerification::VerifyToken( - "web_socket_proxy", token, map)) { + if (!browser::InternalAuthVerification::VerifyPassport( + passport, "web_socket_proxy", map)) { return STATUS_ABORT; } -#endif evbuffer_drain(evb, term_pos - buf + 1); return STATUS_OK; diff --git a/chrome/browser/chromeos/web_socket_proxy.h b/chrome/browser/chromeos/web_socket_proxy.h index 9f15cbc..09e783a 100644 --- a/chrome/browser/chromeos/web_socket_proxy.h +++ b/chrome/browser/chromeos/web_socket_proxy.h @@ -16,14 +16,15 @@ namespace chromeos { class WebSocketProxy { public: - static const size_t kReadBufferLimit = 6 * 1024 * 1024; + static const size_t kReadBufferLimit = 12 * 1024 * 1024; // Limits incoming websocket headers in initial stage of connection. static const size_t kHeaderLimit = 32 * 1024; // Limits number of simultaneously open connections. - static const size_t kConnPoolLimit = 1000; + static const size_t kConnPoolLimit = 40; + // Empty |allowed_origins| vector disables check for origin. WebSocketProxy( const std::vector<std::string>& allowed_origins, struct sockaddr* addr, int addr_len); diff --git a/chrome/browser/chromeos/web_socket_proxy_controller.cc b/chrome/browser/chromeos/web_socket_proxy_controller.cc index 3387071..b6cb61f 100644 --- a/chrome/browser/chromeos/web_socket_proxy_controller.cc +++ b/chrome/browser/chromeos/web_socket_proxy_controller.cc @@ -4,19 +4,95 @@ #include "web_socket_proxy_controller.h" +#include <algorithm> + #include <netinet/in.h> #include <sys/wait.h> #include <unistd.h> +#include "base/command_line.h" #include "base/lazy_instance.h" #include "base/message_loop.h" +#include "base/string_tokenizer.h" #include "base/threading/thread.h" #include "chrome/browser/browser_process.h" #include "chrome/browser/chromeos/web_socket_proxy.h" +#include "chrome/common/chrome_switches.h" +#include "chrome/common/extensions/extension.h" #include "content/browser/browser_thread.h" +#include "content/common/url_constants.h" +#include "googleurl/src/gurl.h" namespace { +const char* kWebAppId = "haiffjcadagjlijoggckpgfnoeiflnem"; + +const char* kAllowedIds[] = { + kWebAppId +}; + +class OriginValidator { + public: + OriginValidator() + : allowed_ids_(kAllowedIds, kAllowedIds + arraysize(kAllowedIds)) { + CommandLine* command_line = CommandLine::ForCurrentProcess(); + DCHECK(command_line); + std::string allowed_list = + command_line->GetSwitchValueASCII(switches::kAllowWebSocketProxy); + if (!allowed_list.empty()) { + StringTokenizer t(allowed_list, ","); + while (t.GetNext()) { + // It must be either extension id or origin. + if (Extension::IdIsValid(t.token())) { + allowed_ids_.push_back(t.token()); + } else { + // It is not extension id, check if it is an origin. + GURL origin = GURL(t.token()).GetOrigin(); + if (!origin.is_valid()) { + LOG(ERROR) << "Invalid extension id or origin specified via " + << switches::kAllowWebSocketProxy << " switch"; + break; + } + allowed_origins_.push_back(origin.spec()); + if (origin.SchemeIs(chrome::kExtensionScheme)) + allowed_ids_.push_back(origin.host()); + } + } + } + for (size_t i = 0; i < allowed_ids_.size(); ++i) { + allowed_origins_.push_back(Extension::GetBaseURLFromExtensionId( + allowed_ids_[i]).GetOrigin().spec()); + } + std::sort(allowed_ids_.begin(), allowed_ids_.end()); + allowed_ids_.resize(std::unique( + allowed_ids_.begin(), allowed_ids_.end()) - allowed_ids_.begin()); + std::sort(allowed_origins_.begin(), allowed_origins_.end()); + allowed_origins_.resize(std::unique(allowed_origins_.begin(), + allowed_origins_.end()) - allowed_origins_.begin()); + } + + bool CheckCredentials( + const std::string& extension_id, + const std::string& hostname, + unsigned short port, + chromeos::WebSocketProxyController::ConnectionFlags flags) { + if (flags & chromeos::WebSocketProxyController::TLS_OVER_TCP) { + NOTIMPLEMENTED(); + return false; + } + return std::binary_search( + allowed_ids_.begin(), allowed_ids_.end(), extension_id); + } + + const std::vector<std::string>& allowed_origins() { return allowed_origins_; } + + private: + std::vector<std::string> allowed_ids_; + std::vector<std::string> allowed_origins_; +}; + +base::LazyInstance<OriginValidator> g_validator(base::LINKER_INITIALIZED); + class ProxyTask : public Task { virtual void Run() OVERRIDE; }; @@ -34,7 +110,7 @@ struct ProxyLifetime { volatile bool shutdown_requested; - base::Lock shutdown_lock; + base::Lock lock; }; base::LazyInstance<ProxyLifetime> g_proxy_lifetime(base::LINKER_INITIALIZED); @@ -43,11 +119,6 @@ void ProxyTask::Run() { LOG(INFO) << "Attempt to run web socket proxy task"; const int kPort = 10101; - // Configure allowed origins. Empty vector allows any origin. - std::vector<std::string> allowed_origins; - allowed_origins.push_back( - "chrome-extension://haiffjcadagjlijoggckpgfnoeiflnem"); - struct sockaddr_in sa; memset(&sa, 0, sizeof(sa)); sa.sin_family = AF_INET; @@ -55,9 +126,10 @@ void ProxyTask::Run() { sa.sin_addr.s_addr = htonl(INADDR_LOOPBACK); chromeos::WebSocketProxy* server = new chromeos::WebSocketProxy( - allowed_origins, reinterpret_cast<sockaddr*>(&sa), sizeof(sa)); + g_validator.Get().allowed_origins(), + reinterpret_cast<sockaddr*>(&sa), sizeof(sa)); { - base::AutoLock alk(g_proxy_lifetime.Get().shutdown_lock); + base::AutoLock alk(g_proxy_lifetime.Get().lock); if (g_proxy_lifetime.Get().shutdown_requested) return; delete g_proxy_lifetime.Get().server; @@ -65,7 +137,7 @@ void ProxyTask::Run() { } server->Run(); { - base::AutoLock alk(g_proxy_lifetime.Get().shutdown_lock); + base::AutoLock alk(g_proxy_lifetime.Get().lock); delete server; g_proxy_lifetime.Get().server = NULL; if (!g_proxy_lifetime.Get().shutdown_requested) { @@ -99,12 +171,22 @@ bool WebSocketProxyController::IsInitiated() { void WebSocketProxyController::Shutdown() { if (IsInitiated()) { LOG(INFO) << "WebSocketProxyController shutdown"; - base::AutoLock alk(g_proxy_lifetime.Get().shutdown_lock); + base::AutoLock alk(g_proxy_lifetime.Get().lock); g_proxy_lifetime.Get().shutdown_requested = true; if (g_proxy_lifetime.Get().server) g_proxy_lifetime.Get().server->Shutdown(); } } +// static +bool WebSocketProxyController::CheckCredentials( + const std::string& extension_id, + const std::string& hostname, + unsigned short port, + ConnectionFlags flags) { + return g_validator.Get().CheckCredentials( + extension_id, hostname, port, flags); +} + } // namespace chromeos diff --git a/chrome/browser/chromeos/web_socket_proxy_controller.h b/chrome/browser/chromeos/web_socket_proxy_controller.h index e5749e7..f96fedc 100644 --- a/chrome/browser/chromeos/web_socket_proxy_controller.h +++ b/chrome/browser/chromeos/web_socket_proxy_controller.h @@ -6,17 +6,30 @@ #define CHROME_BROWSER_CHROMEOS_WEB_SOCKET_PROXY_CONTROLLER_H_ #pragma once +#include <string> + namespace chromeos { // Controls webproxy to TCP service. class WebSocketProxyController { public: + enum ConnectionFlags { + PLAIN_TCP = 0, + TLS_OVER_TCP = 1 << 0 + }; + // Can be called on any thread. Subsequent calls are cheap and do nothing. static void Initiate(); // All methods can be called on any thread. static void Shutdown(); static bool IsInitiated(); + + static bool CheckCredentials( + const std::string& extension_id, + const std::string& hostname, + unsigned short port, + ConnectionFlags); }; } // namespace chromeos diff --git a/chrome/browser/extensions/extension_apitest.h b/chrome/browser/extensions/extension_apitest.h index 52b492e..1bab742 100644 --- a/chrome/browser/extensions/extension_apitest.h +++ b/chrome/browser/extensions/extension_apitest.h @@ -118,7 +118,7 @@ class ExtensionApiTest : public ExtensionBrowserTest { private: bool RunExtensionTestImpl(const char* extension_name, const std::string& test_page, - bool enable_incogntio, + bool enable_incognito, bool enable_fileaccess, bool load_as_component); diff --git a/chrome/browser/extensions/extension_function_dispatcher.cc b/chrome/browser/extensions/extension_function_dispatcher.cc index 9a6b372..a64d6a1 100644 --- a/chrome/browser/extensions/extension_function_dispatcher.cc +++ b/chrome/browser/extensions/extension_function_dispatcher.cc @@ -38,6 +38,7 @@ #include "chrome/browser/extensions/extension_tabs_module.h" #include "chrome/browser/extensions/extension_test_api.h" #include "chrome/browser/extensions/extension_tts_api.h" +#include "chrome/browser/extensions/extension_web_socket_proxy_private_api.h" #include "chrome/browser/extensions/extension_web_ui.h" #include "chrome/browser/extensions/extension_webrequest_api.h" #include "chrome/browser/extensions/extension_webstore_private_api.h" @@ -337,6 +338,9 @@ void FactoryRegistry::ResetFunctions() { #endif #endif + // Websocket to TCP proxy. Currently noop on anything other than ChromeOS. + RegisterFunction<WebSocketProxyPrivateGetPassportForTCPFunction>(); + // Debugger RegisterFunction<AttachDebuggerFunction>(); RegisterFunction<DetachDebuggerFunction>(); diff --git a/chrome/browser/extensions/extension_web_socket_proxy_private_api.cc b/chrome/browser/extensions/extension_web_socket_proxy_private_api.cc new file mode 100644 index 0000000..5acbfbe --- /dev/null +++ b/chrome/browser/extensions/extension_web_socket_proxy_private_api.cc @@ -0,0 +1,78 @@ +// 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/extensions/extension_web_socket_proxy_private_api.h" + +#include "base/logging.h" +#include "base/values.h" +#include "base/string_number_conversions.h" +#include "chrome/browser/internal_auth.h" +#include "chrome/common/extensions/extension.h" +#include "content/common/notification_service.h" + +#if defined(OS_CHROMEOS) +#include "chrome/browser/chromeos/web_socket_proxy_controller.h" +#endif + +bool WebSocketProxyPrivateGetPassportForTCPFunction::RunImpl() { + bool delay_response = false; + result_.reset(Value::CreateStringValue("")); + +#if defined(OS_CHROMEOS) + std::string hostname; + EXTENSION_FUNCTION_VALIDATE(args_->GetString(0, &hostname)); + int port = -1; + EXTENSION_FUNCTION_VALIDATE(args_->GetInteger(1, &port)); + + if (chromeos::WebSocketProxyController::CheckCredentials( + extension_id(), hostname, port, + chromeos::WebSocketProxyController::PLAIN_TCP)) { + if (!chromeos::WebSocketProxyController::IsInitiated()) { + delay_response = true; + registrar_.Add( + this, NotificationType::WEB_SOCKET_PROXY_STARTED, + NotificationService::AllSources()); + chromeos::WebSocketProxyController::Initiate(); + } + + std::map<std::string, std::string> map; + map["hostname"] = hostname; + map["port"] = base::IntToString(port); + map["extension_id"] = extension_id(); + StringValue* passport = Value::CreateStringValue( + browser::InternalAuthGeneration::GeneratePassport( + "web_socket_proxy", map)); + result_.reset(passport); + } +#endif // defined(OS_CHROMEOS) + + // TODO(dilmah): get idea why notification is not observed and timer callback + // is not called and remove this line. + delay_response = false; + + if (delay_response) { + timer_.Start(base::TimeDelta::FromSeconds(3), + this, &WebSocketProxyPrivateGetPassportForTCPFunction::Finalize); + } else { + Finalize(); + } + return true; +} + +void WebSocketProxyPrivateGetPassportForTCPFunction::Observe( + NotificationType type, const NotificationSource& source, + const NotificationDetails& details) { +#if defined(OS_CHROMEOS) + DCHECK(type.value == NotificationType::WEB_SOCKET_PROXY_STARTED); +#else + NOTREACHED(); +#endif + timer_.Stop(); // Cancel timeout timer. + Finalize(); +} + +void WebSocketProxyPrivateGetPassportForTCPFunction::Finalize() { + SendResponse(true); +} + diff --git a/chrome/browser/extensions/extension_web_socket_proxy_private_api.h b/chrome/browser/extensions/extension_web_socket_proxy_private_api.h new file mode 100644 index 0000000..bb32262 --- /dev/null +++ b/chrome/browser/extensions/extension_web_socket_proxy_private_api.h @@ -0,0 +1,36 @@ +// 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. + +#ifndef CHROME_BROWSER_EXTENSIONS_EXTENSION_WEB_SOCKET_PROXY_PRIVATE_API_H_ +#define CHROME_BROWSER_EXTENSIONS_EXTENSION_WEB_SOCKET_PROXY_PRIVATE_API_H_ +#pragma once + +#include "base/timer.h" +#include "content/common/notification_observer.h" +#include "content/common/notification_registrar.h" +#include "chrome/browser/extensions/extension_function.h" + +class WebSocketProxyPrivateGetPassportForTCPFunction + : public AsyncExtensionFunction, public NotificationObserver { + private: + // ExtensionFunction implementation. + virtual bool RunImpl() OVERRIDE; + + // NotificationObserver implementation. + virtual void Observe( + NotificationType type, const NotificationSource& source, + const NotificationDetails& details) OVERRIDE; + + // Finalizes async operation. + void Finalize(); + + // Used to signal timeout (when waiting for proxy initial launch). + base::OneShotTimer<WebSocketProxyPrivateGetPassportForTCPFunction> timer_; + + NotificationRegistrar registrar_; + + DECLARE_EXTENSION_FUNCTION_NAME("webSocketProxyPrivate.getPassportForTCP") +}; + +#endif // CHROME_BROWSER_EXTENSIONS_EXTENSION_WEB_SOCKET_PROXY_PRIVATE_API_H_ diff --git a/chrome/browser/extensions/extension_web_socket_proxy_private_apitest.cc b/chrome/browser/extensions/extension_web_socket_proxy_private_apitest.cc new file mode 100644 index 0000000..ef2afd6 --- /dev/null +++ b/chrome/browser/extensions/extension_web_socket_proxy_private_apitest.cc @@ -0,0 +1,19 @@ +// 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/extensions/extension_apitest.h" +#include "chrome/common/chrome_switches.h" + +class ExtensionWebSocketProxyPrivateApiTest : public ExtensionApiTest { + void SetUpCommandLine(CommandLine* command_line) { + ExtensionApiTest::SetUpCommandLine(command_line); + command_line->AppendSwitchASCII( + switches::kAllowWebSocketProxy, "mknjjldhaihcdajjbihghhiehamnpcak"); + } +}; + +IN_PROC_BROWSER_TEST_F(ExtensionWebSocketProxyPrivateApiTest, Pass) { + ASSERT_TRUE(RunExtensionTest("web_socket_proxy_private")) << message_; +} + diff --git a/chrome/browser/internal_auth.cc b/chrome/browser/internal_auth.cc new file mode 100644 index 0000000..268ee1b --- /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 + diff --git a/chrome/browser/internal_auth.h b/chrome/browser/internal_auth.h new file mode 100644 index 0000000..008559b --- /dev/null +++ b/chrome/browser/internal_auth.h @@ -0,0 +1,74 @@ +// 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. + +#ifndef CHROME_BROWSER_INTERNAL_AUTH_H_ +#define CHROME_BROWSER_INTERNAL_AUTH_H_ +#pragma once + +#include <map> +#include <string> + +#include "base/gtest_prod_util.h" + +class WebSocketProxyPrivateGetPassportForTCPFunction; + +namespace browser { + +// Call InternalAuthVerification methods on any thread. +class InternalAuthVerification { + public: + // Used by consumer of passport in order to verify credentials. + static bool VerifyPassport( + const std::string& passport, + const std::string& domain, + const std::map<std::string, std::string>& var_value_map); + + private: + // We allow for easy separation of InternalAuthVerification and + // InternalAuthGeneration so the only thing they share (besides time) is + // a key (regenerated infrequently). + static void ChangeKey(const std::string& key); + +#ifdef UNIT_TEST + static void set_verification_window_seconds(int seconds) { + verification_window_seconds_ = seconds; + } +#endif + + static int get_verification_window_ticks(); + + static int verification_window_seconds_; + + friend class InternalAuthGeneration; + friend class InternalAuthVerificationService; + friend class InternalAuthGenerationService; + + FRIEND_TEST_ALL_PREFIXES(InternalAuthTest, ExpirationAndBruteForce); +}; + +// Not thread-safe. Make all calls on the same thread (UI thread). +class InternalAuthGeneration { + private: + // Generates passport; do this only after successful check of credentials. + static std::string GeneratePassport( + const std::string& domain, + const std::map<std::string, std::string>& var_value_map); + + // Used only by tests. + static void GenerateNewKey(); + + friend class ::WebSocketProxyPrivateGetPassportForTCPFunction; + + FRIEND_TEST_ALL_PREFIXES(InternalAuthTest, BasicGeneration); + FRIEND_TEST_ALL_PREFIXES(InternalAuthTest, DoubleGeneration); + FRIEND_TEST_ALL_PREFIXES(InternalAuthTest, BadGeneration); + FRIEND_TEST_ALL_PREFIXES(InternalAuthTest, BasicVerification); + FRIEND_TEST_ALL_PREFIXES(InternalAuthTest, BruteForce); + FRIEND_TEST_ALL_PREFIXES(InternalAuthTest, ExpirationAndBruteForce); + FRIEND_TEST_ALL_PREFIXES(InternalAuthTest, ChangeKey); +}; + +} // namespace browser + +#endif // CHROME_BROWSER_INTERNAL_AUTH_H_ diff --git a/chrome/browser/internal_auth_unittest.cc b/chrome/browser/internal_auth_unittest.cc new file mode 100644 index 0000000..1e81692 --- /dev/null +++ b/chrome/browser/internal_auth_unittest.cc @@ -0,0 +1,199 @@ +// 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 "base/lazy_instance.h" +#include "base/time.h" +#include "content/browser/browser_thread.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace browser { + +class InternalAuthTest : public ::testing::Test { + public: + InternalAuthTest() { + long_string_ = "seed"; + for (int i = 20; i--;) + long_string_ += long_string_; + } + virtual ~InternalAuthTest() {} + + virtual void SetUp() { + } + + virtual void TearDown() { + } + + MessageLoop message_loop_; + std::string long_string_; +}; + +TEST_F(InternalAuthTest, BasicGeneration) { + std::map<std::string, std::string> map; + map["key"] = "value"; + std::string token = browser::InternalAuthGeneration::GeneratePassport( + "zapata", map); + ASSERT_GT(token.size(), 10u); // short token is insecure. + + map["key2"] = "value2"; + token = browser::InternalAuthGeneration::GeneratePassport("zapata", map); + ASSERT_GT(token.size(), 10u); +} + +TEST_F(InternalAuthTest, DoubleGeneration) { + std::map<std::string, std::string> map; + map["key"] = "value"; + std::string token1 = browser::InternalAuthGeneration::GeneratePassport( + "zapata", map); + ASSERT_GT(token1.size(), 10u); + + std::string token2 = browser::InternalAuthGeneration::GeneratePassport( + "zapata", map); + ASSERT_GT(token2.size(), 10u); + // tokens are different even if credentials coincide. + ASSERT_NE(token1, token2); +} + +TEST_F(InternalAuthTest, BadGeneration) { + std::map<std::string, std::string> map; + map["key"] = "value"; + // Trying huge domain. + std::string token = browser::InternalAuthGeneration::GeneratePassport( + long_string_, map); + ASSERT_TRUE(token.empty()); + ASSERT_FALSE(browser::InternalAuthVerification::VerifyPassport( + token, long_string_, map)); + + // Trying empty domain. + token = browser::InternalAuthGeneration::GeneratePassport("", map); + ASSERT_TRUE(token.empty()); + ASSERT_FALSE(browser::InternalAuthVerification::VerifyPassport( + token, "", map)); + + std::string dummy("abcdefghij"); + for (size_t i = 1000; i--;) { + std::string key = dummy; + std::next_permutation(dummy.begin(), dummy.end()); + std::string value = dummy; + std::next_permutation(dummy.begin(), dummy.end()); + map[key] = value; + } + // Trying huge var=value map. + token = browser::InternalAuthGeneration::GeneratePassport("zapata", map); + ASSERT_TRUE(token.empty()); + ASSERT_FALSE(browser::InternalAuthVerification::VerifyPassport( + token, "zapata", map)); + + map.clear(); + map[""] = "value"; + // Trying empty key. + token = browser::InternalAuthGeneration::GeneratePassport("zapata", map); + ASSERT_TRUE(token.empty()); + ASSERT_FALSE(browser::InternalAuthVerification::VerifyPassport( + token, "zapata", map)); +} + +TEST_F(InternalAuthTest, BasicVerification) { + std::map<std::string, std::string> map; + map["key"] = "value"; + std::string token = browser::InternalAuthGeneration::GeneratePassport( + "zapata", map); + ASSERT_GT(token.size(), 10u); + ASSERT_TRUE(browser::InternalAuthVerification::VerifyPassport( + token, "zapata", map)); + // Passport can not be reused. + for (int i = 10000; i--;) { + ASSERT_FALSE(browser::InternalAuthVerification::VerifyPassport( + token, "zapata", map)); + } +} + +TEST_F(InternalAuthTest, BruteForce) { + std::map<std::string, std::string> map; + map["key"] = "value"; + std::string token = browser::InternalAuthGeneration::GeneratePassport( + "zapata", map); + ASSERT_GT(token.size(), 10u); + + // Trying bruteforce. + std::string dummy = token; + for (size_t i = 10000; i--;) { + std::next_permutation(dummy.begin(), dummy.end()); + ASSERT_FALSE(browser::InternalAuthVerification::VerifyPassport( + dummy, "zapata", map)); + } + dummy = token; + for (size_t i = 10000; i--;) { + std::next_permutation(dummy.begin(), dummy.begin() + dummy.size() / 2); + ASSERT_FALSE(browser::InternalAuthVerification::VerifyPassport( + dummy, "zapata", map)); + } + // We brute forced just too little, so original token must not expire yet. + ASSERT_TRUE(browser::InternalAuthVerification::VerifyPassport( + token, "zapata", map)); +} + +TEST_F(InternalAuthTest, ExpirationAndBruteForce) { + int kCustomVerificationWindow = 2; + browser::InternalAuthVerification::set_verification_window_seconds( + kCustomVerificationWindow); + + std::map<std::string, std::string> map; + map["key"] = "value"; + std::string token = browser::InternalAuthGeneration::GeneratePassport( + "zapata", map); + ASSERT_GT(token.size(), 10u); + + // We want to test token expiration, so we need to wait some amount of time, + // so we are brute-forcing during this time. + base::Time timestamp = base::Time::Now(); + std::string dummy1 = token; + std::string dummy2 = token; + for (;;) { + for (size_t i = 10000; i--;) { + std::next_permutation(dummy1.begin(), dummy1.end()); + ASSERT_FALSE(browser::InternalAuthVerification::VerifyPassport( + dummy1, "zapata", map)); + } + for (size_t i = 10000; i--;) { + std::next_permutation(dummy2.begin(), dummy2.begin() + dummy2.size() / 2); + ASSERT_FALSE(browser::InternalAuthVerification::VerifyPassport( + dummy2, "zapata", map)); + } + if (base::Time::Now() - timestamp > base::TimeDelta::FromSeconds( + kCustomVerificationWindow + 1)) { + break; + } + } + ASSERT_FALSE(browser::InternalAuthVerification::VerifyPassport( + token, "zapata", map)); + // Reset verification window to default. + browser::InternalAuthVerification::set_verification_window_seconds(0); +} + +TEST_F(InternalAuthTest, ChangeKey) { + std::map<std::string, std::string> map; + map["key"] = "value"; + std::string token = browser::InternalAuthGeneration::GeneratePassport( + "zapata", map); + ASSERT_GT(token.size(), 10u); + + browser::InternalAuthGeneration::GenerateNewKey(); + // Passport should survive key change. + ASSERT_TRUE(browser::InternalAuthVerification::VerifyPassport( + token, "zapata", map)); + + token = browser::InternalAuthGeneration::GeneratePassport("zapata", map); + ASSERT_GT(token.size(), 10u); + for (int i = 20; i--;) + browser::InternalAuthGeneration::GenerateNewKey(); + // Passport should not survive series of key changes. + ASSERT_FALSE(browser::InternalAuthVerification::VerifyPassport( + token, "zapata", map)); +} + +} // namespace browser diff --git a/chrome/chrome_browser.gypi b/chrome/chrome_browser.gypi index fcac008..3879ffe 100644 --- a/chrome/chrome_browser.gypi +++ b/chrome/chrome_browser.gypi @@ -1001,6 +1001,8 @@ 'browser/extensions/extension_uninstall_dialog.h', 'browser/extensions/extension_updater.cc', 'browser/extensions/extension_updater.h', + 'browser/extensions/extension_web_socket_proxy_private_api.cc', + 'browser/extensions/extension_web_socket_proxy_private_api.h', 'browser/extensions/extension_web_ui.cc', 'browser/extensions/extension_web_ui.h', 'browser/extensions/extension_webrequest_api.cc', @@ -1265,6 +1267,8 @@ 'browser/instant/instant_unload_handler.h', 'browser/instant/promo_counter.cc', 'browser/instant/promo_counter.h', + 'browser/internal_auth.cc', + 'browser/internal_auth.h', 'browser/intranet_redirect_detector.cc', 'browser/intranet_redirect_detector.h', 'browser/io_thread.cc', diff --git a/chrome/chrome_tests.gypi b/chrome/chrome_tests.gypi index 6ebfcc5..d3b1927 100644 --- a/chrome/chrome_tests.gypi +++ b/chrome/chrome_tests.gypi @@ -1176,12 +1176,12 @@ '../jingle/jingle.gyp:jingle_glue_test_util', '../media/media.gyp:media_test_support', '../net/net.gyp:net_test_support', + '../testing/gmock.gyp:gmock', + '../testing/gtest.gyp:gtest', 'test_support_common', 'test_support_sync', 'test_support_syncapi', 'test_support_unit', - '../testing/gmock.gyp:gmock', - '../testing/gtest.gyp:gtest', # 3) anything tests directly depend on '../app/app.gyp:app_resources', '../skia/skia.gyp:skia', @@ -1415,6 +1415,8 @@ 'browser/importer/toolbar_importer_unittest.cc', 'browser/instant/instant_loader_manager_unittest.cc', 'browser/instant/promo_counter_unittest.cc', + 'browser/internal_auth_unittest.cc', + 'browser/internal_auth_unittest.h', 'browser/mach_broker_mac_unittest.cc', 'browser/metrics/metrics_log_unittest.cc', 'browser/metrics/metrics_response_unittest.cc', @@ -2365,6 +2367,7 @@ 'browser/extensions/extension_toolbar_model_browsertest.cc', 'browser/extensions/extension_tts_apitest.cc', 'browser/extensions/extension_url_rewrite_browsertest.cc', + 'browser/extensions/extension_web_socket_proxy_private_apitest.cc', 'browser/extensions/extension_webglbackground_apitest.cc', 'browser/extensions/extension_webnavigation_apitest.cc', 'browser/extensions/extension_webrequest_apitest.cc', diff --git a/chrome/common/chrome_switches.cc b/chrome/common/chrome_switches.cc index 4afe3a8..169c2a1 100644 --- a/chrome/common/chrome_switches.cc +++ b/chrome/common/chrome_switches.cc @@ -37,6 +37,10 @@ const char kAllowOutdatedPlugins[] = "allow-outdated-plugins"; // useful for automation testing of the gallery. const char kAllowScriptingGallery[] = "allow-scripting-gallery"; +// Specifies comma separated list of extension ids to grant access to local +// websocket proxy. +const char kAllowWebSocketProxy[] = "allow-websocket-proxy"; + // This prevents Chrome from requiring authorization to run certain widely // installed but less commonly used plug-ins. const char kAlwaysAuthorizePlugins[] = "always-authorize-plugins"; diff --git a/chrome/common/chrome_switches.h b/chrome/common/chrome_switches.h index aa4cb99..a9c6d8e 100644 --- a/chrome/common/chrome_switches.h +++ b/chrome/common/chrome_switches.h @@ -29,6 +29,7 @@ extern const char kAllowFileAccess[]; extern const char kAllowOutdatedPlugins[]; extern const char kAllowHTTPBackgroundPage[]; extern const char kAllowScriptingGallery[]; +extern const char kAllowWebSocketProxy[]; extern const char kAlwaysAuthorizePlugins[]; extern const char kAlwaysEnableDevTools[]; extern const char kApp[]; diff --git a/chrome/common/extensions/api/extension_api.json b/chrome/common/extensions/api/extension_api.json index 8ae8075..00febf8 100644 --- a/chrome/common/extensions/api/extension_api.json +++ b/chrome/common/extensions/api/extension_api.json @@ -5240,6 +5240,44 @@ ] }, { + "namespace": "webSocketProxyPrivate", + "nodoc": true, + "types": [], + "functions": [ + { + "name": "getPassportForTCP", + "description": "requests authorization token for websocket to TCP proxy.", + "parameters": [ + { + "type": "string", + "name": "hostname", + "minLength": 1, + "description": "hostname to which TCP connection is requested." + }, + { + "type": "integer", + "name": "port", + "minimum": 1, + "maximum": 65535, + "description": "TCP port number." + }, + { + "type": "function", + "name": "callback", + "parameters": [ + { + "type": "string", + "name": "passport", + "description": "Passport for passing to proxy." + } + ] + } + ] + } + ], + "events": [] + }, + { "namespace": "experimental.extension", "types": [ { diff --git a/chrome/common/extensions/extension.cc b/chrome/common/extensions/extension.cc index 7f472c1..5914bec 100644 --- a/chrome/common/extensions/extension.cc +++ b/chrome/common/extensions/extension.cc @@ -268,7 +268,7 @@ const char Extension::kClipboardWritePermission[] = "clipboardWrite"; const char Extension::kContextMenusPermission[] = "contextMenus"; const char Extension::kContentSettingsPermission[] = "contentSettings"; const char Extension::kCookiePermission[] = "cookies"; -const char Extension::kChromeosInfoPrivatePermissions[] = "chromeosInfoPrivate"; +const char Extension::kChromeosInfoPrivatePermission[] = "chromeosInfoPrivate"; const char Extension::kDebuggerPermission[] = "debugger"; const char Extension::kExperimentalPermission[] = "experimental"; const char Extension::kFileBrowserHandlerPermission[] = "fileBrowserHandler"; @@ -282,35 +282,37 @@ const char Extension::kProxyPermission[] = "proxy"; const char Extension::kTabPermission[] = "tabs"; const char Extension::kUnlimitedStoragePermission[] = "unlimitedStorage"; const char Extension::kWebstorePrivatePermission[] = "webstorePrivate"; +const char Extension::kWebSocketProxyPrivatePermission[] = + "webSocketProxyPrivate"; // In general, all permissions should have an install message. // See ExtensionsTest.PermissionMessages for an explanation of each // exception. const Extension::Permission Extension::kPermissions[] = { - { kBackgroundPermission, PermissionMessage::ID_NONE }, - { kBookmarkPermission, PermissionMessage::ID_BOOKMARKS }, - { kChromeosInfoPrivatePermissions, PermissionMessage::ID_NONE }, - { kClipboardReadPermission, PermissionMessage::ID_CLIPBOARD }, - { kClipboardWritePermission, PermissionMessage::ID_NONE }, - { kContentSettingsPermission, PermissionMessage::ID_NONE }, - { kContextMenusPermission, PermissionMessage::ID_NONE }, - { kCookiePermission, PermissionMessage::ID_NONE }, - { kDebuggerPermission, PermissionMessage::ID_DEBUGGER }, - { kExperimentalPermission, PermissionMessage::ID_NONE }, - { kFileBrowserHandlerPermission, PermissionMessage::ID_NONE }, - { kFileBrowserPrivatePermission, PermissionMessage::ID_NONE }, - { kGeolocationPermission, PermissionMessage::ID_GEOLOCATION }, - { kIdlePermission, PermissionMessage::ID_NONE }, - { kHistoryPermission, PermissionMessage::ID_BROWSING_HISTORY }, - { kManagementPermission, PermissionMessage::ID_MANAGEMENT }, - { kNotificationPermission, PermissionMessage::ID_NONE }, - { kProxyPermission, PermissionMessage::ID_NONE }, - { kTabPermission, PermissionMessage::ID_TABS }, - { kUnlimitedStoragePermission, PermissionMessage::ID_NONE }, - { kWebstorePrivatePermission, PermissionMessage::ID_NONE } + { kBackgroundPermission, PermissionMessage::ID_NONE }, + { kBookmarkPermission, PermissionMessage::ID_BOOKMARKS }, + { kChromeosInfoPrivatePermission, PermissionMessage::ID_NONE }, + { kClipboardReadPermission, PermissionMessage::ID_CLIPBOARD }, + { kClipboardWritePermission, PermissionMessage::ID_NONE }, + { kContentSettingsPermission, PermissionMessage::ID_NONE }, + { kContextMenusPermission, PermissionMessage::ID_NONE }, + { kCookiePermission, PermissionMessage::ID_NONE }, + { kDebuggerPermission, PermissionMessage::ID_DEBUGGER }, + { kExperimentalPermission, PermissionMessage::ID_NONE }, + { kFileBrowserHandlerPermission, PermissionMessage::ID_NONE }, + { kFileBrowserPrivatePermission, PermissionMessage::ID_NONE }, + { kGeolocationPermission, PermissionMessage::ID_GEOLOCATION }, + { kIdlePermission, PermissionMessage::ID_NONE }, + { kHistoryPermission, PermissionMessage::ID_BROWSING_HISTORY }, + { kManagementPermission, PermissionMessage::ID_MANAGEMENT }, + { kNotificationPermission, PermissionMessage::ID_NONE }, + { kProxyPermission, PermissionMessage::ID_NONE }, + { kTabPermission, PermissionMessage::ID_TABS }, + { kUnlimitedStoragePermission, PermissionMessage::ID_NONE }, + { kWebSocketProxyPrivatePermission, PermissionMessage::ID_NONE }, + { kWebstorePrivatePermission, PermissionMessage::ID_NONE }, }; -const size_t Extension::kNumPermissions = - arraysize(Extension::kPermissions); +const size_t Extension::kNumPermissions = arraysize(Extension::kPermissions); const char* const Extension::kHostedAppPermissionNames[] = { Extension::kBackgroundPermission, @@ -325,7 +327,7 @@ const size_t Extension::kNumHostedAppPermissions = const char* const Extension::kComponentPrivatePermissionNames[] = { Extension::kFileBrowserPrivatePermission, Extension::kWebstorePrivatePermission, - Extension::kChromeosInfoPrivatePermissions, + Extension::kChromeosInfoPrivatePermission, }; const size_t Extension::kNumComponentPrivatePermissions = arraysize(Extension::kComponentPrivatePermissionNames); @@ -2765,8 +2767,7 @@ void Extension::InitEffectiveHostPermissions() { } } -bool Extension::IsComponentOnlyPermission - (const std::string& permission) const { +bool Extension::IsComponentOnlyPermission(const std::string& permission) const { if (location() == Extension::COMPONENT) return true; diff --git a/chrome/common/extensions/extension.h b/chrome/common/extensions/extension.h index bb60afc..8fc1641 100644 --- a/chrome/common/extensions/extension.h +++ b/chrome/common/extensions/extension.h @@ -283,7 +283,7 @@ class Extension : public base::RefCountedThreadSafe<Extension> { static const char kContentSettingsPermission[]; static const char kContextMenusPermission[]; static const char kCookiePermission[]; - static const char kChromeosInfoPrivatePermissions[]; + static const char kChromeosInfoPrivatePermission[]; static const char kDebuggerPermission[]; static const char kExperimentalPermission[]; static const char kFileBrowserHandlerPermission[]; @@ -297,6 +297,7 @@ class Extension : public base::RefCountedThreadSafe<Extension> { static const char kTabPermission[]; static const char kUnlimitedStoragePermission[]; static const char kWebstorePrivatePermission[]; + static const char kWebSocketProxyPrivatePermission[]; static const Permission kPermissions[]; static const size_t kNumPermissions; diff --git a/chrome/common/extensions/extension_unittest.cc b/chrome/common/extensions/extension_unittest.cc index 8d432c9..8488984 100644 --- a/chrome/common/extensions/extension_unittest.cc +++ b/chrome/common/extensions/extension_unittest.cc @@ -1031,10 +1031,11 @@ TEST(ExtensionTest, PermissionMessages) { // to warn you further. skip.insert(Extension::kExperimentalPermission); - // These are only usable by component extensions. + // These are private. skip.insert(Extension::kWebstorePrivatePermission); skip.insert(Extension::kFileBrowserPrivatePermission); - skip.insert(Extension::kChromeosInfoPrivatePermissions); + skip.insert(Extension::kChromeosInfoPrivatePermission); + skip.insert(Extension::kWebSocketProxyPrivatePermission); const Extension::PermissionMessage::MessageId ID_NONE = Extension::PermissionMessage::ID_NONE; diff --git a/chrome/renderer/resources/renderer_extension_bindings.js b/chrome/renderer/resources/renderer_extension_bindings.js index fb98466..4bf6e4b 100644 --- a/chrome/renderer/resources/renderer_extension_bindings.js +++ b/chrome/renderer/resources/renderer_extension_bindings.js @@ -6,8 +6,7 @@ // extensions. It is loaded by any extension-related context, such as content // scripts or toolstrips. // See user_script_slave.cc for script that is loaded by content scripts only. -// TODO(mpcomplete): we also load this in regular web pages, but don't need -// to. +// TODO(mpcomplete): we also load this in regular web pages, but don't need to. var chrome = chrome || {}; (function () { @@ -328,6 +327,7 @@ var chrome = chrome || {}; "tabs", "test", "toolstrip", + "webSocketProxyPrivate", "webstorePrivate", "windows", diff --git a/chrome/test/data/extensions/api_test/web_socket_proxy_private/background.html b/chrome/test/data/extensions/api_test/web_socket_proxy_private/background.html new file mode 100644 index 0000000..9b2034e --- /dev/null +++ b/chrome/test/data/extensions/api_test/web_socket_proxy_private/background.html @@ -0,0 +1,32 @@ +<script> + var hostname = '127.0.0.1'; + var port = 20202; + var proxy = "ws://127.0.0.1:10101/tcpproxy"; + + function gotMessage(msg) { + chrome.test.assertEq(msg.data, window.btoa("aloha\n")); + } + + function gotPassport(passport) { + ws = new WebSocket(proxy); + + /* TODO(dilmah): envelope gotMessage into chrome.test.callbackPass after + setting up testserver */ + ws.onmessage = gotMessage; + + ws.onopen = function() { + var request = passport + ':' + hostname + ':' + port + ':'; + ws.send(request); + + /* Further on we can send base64-encoded data */ + ws.send(window.btoa("HELO localhost\n")); + }; + } + + function test_connect() { + chrome.webSocketProxyPrivate.getPassportForTCP( + hostname, port, chrome.test.callbackPass(gotPassport)); + } + + chrome.test.runTests([test_connect]); +</script> diff --git a/chrome/test/data/extensions/api_test/web_socket_proxy_private/manifest.json b/chrome/test/data/extensions/api_test/web_socket_proxy_private/manifest.json new file mode 100644 index 0000000..adac703 --- /dev/null +++ b/chrome/test/data/extensions/api_test/web_socket_proxy_private/manifest.json @@ -0,0 +1,8 @@ +{ + "key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCzxp8WFAeJGgwQFdaktyst1HW/8yqbgFeD6z5yKK6MOhGn0HSsiDSU0cjEkBbqbyr4WJFDpAB3uLhUhUOszLtjKJt0c8XdqITKlSXKfa16e4P6z4lA+cVmcCuTvQtop2iXaJrTon7dL6BFPq8xKuaCX8yb+uZo91KmkNpd3OEF6QIDAQAB", + "name": "chrome.webSocketProxyPrivate", + "version": "0.1", + "description": "Test for webSocketProxyPrivate.getPassportForTCP", + "background_page": "background.html", + "permissions": ["webSocketProxyPrivate", "chromeosInfoPrivate"] +} diff --git a/crypto/hmac.h b/crypto/hmac.h index 7852797..c0706d8 100644 --- a/crypto/hmac.h +++ b/crypto/hmac.h @@ -51,7 +51,9 @@ class HMAC { // to the constructor and the key supplied to the Init method. The HMAC is // returned in |digest|, which has |digest_length| bytes of storage available. // TODO(abarth): digest_length should be a size_t. - bool Sign(const std::string& data, unsigned char* digest, int digest_length); + bool Sign(const std::string& data, + unsigned char* digest, + int digest_length) const; // TODO(albertb): Add a Verify method. diff --git a/crypto/hmac_mac.cc b/crypto/hmac_mac.cc index d7cec61..fefd6e7 100644 --- a/crypto/hmac_mac.cc +++ b/crypto/hmac_mac.cc @@ -41,7 +41,7 @@ HMAC::~HMAC() { bool HMAC::Sign(const std::string& data, unsigned char* digest, - int digest_length) { + int digest_length) const { CCHmacAlgorithm algorithm; int algorithm_digest_length; switch (hash_alg_) { diff --git a/crypto/hmac_nss.cc b/crypto/hmac_nss.cc index 957f9db..722fcf1 100644 --- a/crypto/hmac_nss.cc +++ b/crypto/hmac_nss.cc @@ -75,7 +75,7 @@ bool HMAC::Init(const unsigned char *key, int key_length) { bool HMAC::Sign(const std::string& data, unsigned char* digest, - int digest_length) { + int digest_length) const { if (!plat_->sym_key_.get()) { // Init has not been called before Sign. NOTREACHED(); diff --git a/crypto/hmac_openssl.cc b/crypto/hmac_openssl.cc index 6fbc437..8b7b96d 100644 --- a/crypto/hmac_openssl.cc +++ b/crypto/hmac_openssl.cc @@ -42,7 +42,7 @@ HMAC::~HMAC() { bool HMAC::Sign(const std::string& data, unsigned char* digest, - int digest_length) { + int digest_length) const { DCHECK_GE(digest_length, 0); DCHECK(!plat_->key.empty()); // Init must be called before Sign. diff --git a/crypto/hmac_win.cc b/crypto/hmac_win.cc index e5511e0..1e6954a 100644 --- a/crypto/hmac_win.cc +++ b/crypto/hmac_win.cc @@ -156,7 +156,7 @@ HMAC::~HMAC() { bool HMAC::Sign(const std::string& data, unsigned char* digest, - int digest_length) { + int digest_length) const { if (hash_alg_ == SHA256) { if (plat_->raw_key_.empty()) return false; diff --git a/crypto/symmetric_key_mac.cc b/crypto/symmetric_key_mac.cc index 47193a08..a92c43a 100644 --- a/crypto/symmetric_key_mac.cc +++ b/crypto/symmetric_key_mac.cc @@ -32,7 +32,7 @@ CSSM_KEY_TYPE CheckKeyParams(crypto::SymmetricKey::Algorithm algorithm, } } -void* CreateRandomBytes(size_t size) { +uint8_t* CreateRandomBytes(size_t size) { CSSM_RETURN err; CSSM_CC_HANDLE ctx; err = CSSM_CSP_CreateRandomGenContext(crypto::GetSharedCSPHandle(), @@ -50,7 +50,7 @@ void* CreateRandomBytes(size_t size) { random_data.Data = NULL; } CSSM_DeleteContext(ctx); - return random_data.Data; // Caller responsible for freeing this + return random_data.Data; // Caller responsible for freeing this. } inline CSSM_DATA StringToData(const std::string& str) { @@ -65,16 +65,20 @@ inline CSSM_DATA StringToData(const std::string& str) { namespace crypto { -SymmetricKey::~SymmetricKey() {} +SymmetricKey::~SymmetricKey() { + std::fill(key_.begin(), key_.end(), 0); +} // static SymmetricKey* SymmetricKey::GenerateRandomKey(Algorithm algorithm, size_t key_size_in_bits) { CheckKeyParams(algorithm, key_size_in_bits); - void* random_bytes = CreateRandomBytes((key_size_in_bits + 7) / 8); + size_t key_size_in_bytes = (key_size_in_bits + 7) / 8; + uint8_t* random_bytes = CreateRandomBytes(key_size_in_bytes); if (!random_bytes) return NULL; SymmetricKey *key = new SymmetricKey(random_bytes, key_size_in_bits); + std::fill(random_bytes, random_bytes + key_size_in_bytes, 0); free(random_bytes); return key; } @@ -139,9 +143,9 @@ SymmetricKey* SymmetricKey::Import(Algorithm algorithm, return new SymmetricKey(raw_key.data(), raw_key.size() * 8); } -SymmetricKey::SymmetricKey(const void *key_data, size_t key_size_in_bits) - : key_(reinterpret_cast<const char*>(key_data), - key_size_in_bits / 8) {} +SymmetricKey::SymmetricKey(const void* key_data, size_t key_size_in_bits) + : key_(reinterpret_cast<const char*>(key_data), key_size_in_bits / 8) { +} bool SymmetricKey::GetRawKey(std::string* raw_key) { *raw_key = key_; |