| // Copyright 2014 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/api/gcd_private/privet_v3_session.h" |
| |
| #include "base/base64.h" |
| #include "base/json/json_reader.h" |
| #include "base/json/json_writer.h" |
| #include "base/location.h" |
| #include "base/logging.h" |
| #include "base/macros.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/single_thread_task_runner.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "chrome/browser/extensions/api/gcd_private/privet_v3_context_getter.h" |
| #include "crypto/hmac.h" |
| #include "crypto/p224_spake.h" |
| #include "net/base/load_flags.h" |
| #include "net/http/http_response_headers.h" |
| #include "net/url_request/url_fetcher_delegate.h" |
| #include "url/gurl.h" |
| |
| namespace extensions { |
| |
| namespace { |
| |
| const char kPrivetV3AuthTokenHeaderPrefix[] = "Authorization: "; |
| |
| const char kPrivetV3AuthAnonymous[] = "Privet anonymous"; |
| const char kPrivetV3CryptoP224Spake2[] = "p224_spake2"; |
| const char kPrivetV3Auto[] = "auto"; |
| |
| const char kPrivetV3InfoKeyAuth[] = "authentication"; |
| const char kPrivetV3InfoKeyVersion[] = "version"; |
| const char kPrivetV3InfoVersion[] = "3.0"; |
| |
| const char kPrivetV3KeyAccessToken[] = "accessToken"; |
| const char kPrivetV3KeyAuthCode[] = "authCode"; |
| const char kPrivetV3KeyCertFingerprint[] = "certFingerprint"; |
| const char kPrivetV3KeyCertSignature[] = "certSignature"; |
| const char kPrivetV3KeyClientCommitment[] = "clientCommitment"; |
| const char kPrivetV3KeyCrypto[] = "crypto"; |
| const char kPrivetV3KeyDeviceCommitment[] = "deviceCommitment"; |
| const char kPrivetV3KeyMode[] = "mode"; |
| const char kPrivetV3KeyPairing[] = "pairing"; |
| const char kPrivetV3KeyRequestedScope[] = "requestedScope"; |
| const char kPrivetV3KeyScope[] = "scope"; |
| const char kPrivetV3KeySessionId[] = "sessionId"; |
| const char kPrivetV3KeyTokenType[] = "tokenType"; |
| const char kPrivetV3KeyError[] = "error"; |
| |
| const char kPrivetV3InfoHttpsPort[] = "endpoints.httpsPort"; |
| |
| const char kPrivetInfoPath[] = "/privet/info"; |
| const char kPrivetV3PairingStartPath[] = "/privet/v3/pairing/start"; |
| const char kPrivetV3PairingConfirmPath[] = "/privet/v3/pairing/confirm"; |
| const char kPrivetV3PairingCancelPath[] = "/privet/v3/pairing/cancel"; |
| const char kPrivetV3AuthPath[] = "/privet/v3/auth"; |
| |
| const char kContentTypeJSON[] = "application/json"; |
| |
| const int kUrlFetcherTimeoutSec = 60; |
| |
| using PairingType = PrivetV3Session::PairingType; |
| |
| bool GetDecodedString(const base::DictionaryValue& response, |
| const std::string& key, |
| std::string* value) { |
| std::string base64; |
| return response.GetString(key, &base64) && base::Base64Decode(base64, value); |
| } |
| |
| bool ContainsString(const base::DictionaryValue& dictionary, |
| const std::string& key, |
| const std::string& expected_value) { |
| const base::ListValue* list_of_string = nullptr; |
| if (!dictionary.GetList(key, &list_of_string)) |
| return false; |
| |
| for (const auto& value : *list_of_string) { |
| std::string string_value; |
| if (value->GetAsString(&string_value) && string_value == expected_value) |
| return true; |
| } |
| return false; |
| } |
| |
| std::unique_ptr<base::DictionaryValue> GetJson(const net::URLFetcher* source) { |
| if (!source->GetStatus().is_success()) |
| return nullptr; |
| |
| const net::HttpResponseHeaders* headers = source->GetResponseHeaders(); |
| if (!headers) |
| return nullptr; |
| |
| std::string content_type; |
| if (!headers->GetMimeType(&content_type)) |
| return nullptr; |
| |
| if (content_type != kContentTypeJSON) |
| return nullptr; |
| |
| std::string json; |
| if (!source->GetResponseAsString(&json)) |
| return nullptr; |
| |
| base::JSONReader json_reader(base::JSON_ALLOW_TRAILING_COMMAS); |
| return base::DictionaryValue::From(json_reader.ReadToValue(json)); |
| } |
| |
| } // namespace |
| |
| class PrivetV3Session::FetcherDelegate : public net::URLFetcherDelegate { |
| public: |
| FetcherDelegate(const base::WeakPtr<PrivetV3Session>& session, |
| const PrivetV3Session::MessageCallback& callback); |
| ~FetcherDelegate() override; |
| |
| // URLFetcherDelegate methods. |
| void OnURLFetchComplete(const net::URLFetcher* source) override; |
| |
| net::URLFetcher* CreateURLFetcher(const GURL& url, |
| net::URLFetcher::RequestType request_type, |
| bool orphaned); |
| |
| private: |
| void ReplyAndDestroyItself(Result result, const base::DictionaryValue& value); |
| void OnTimeout(); |
| |
| std::unique_ptr<net::URLFetcher> url_fetcher_; |
| base::WeakPtr<PrivetV3Session> session_; |
| MessageCallback callback_; |
| |
| base::WeakPtrFactory<FetcherDelegate> weak_ptr_factory_; |
| DISALLOW_COPY_AND_ASSIGN(FetcherDelegate); |
| }; |
| |
| PrivetV3Session::FetcherDelegate::FetcherDelegate( |
| const base::WeakPtr<PrivetV3Session>& session, |
| const PrivetV3Session::MessageCallback& callback) |
| : session_(session), |
| callback_(callback), |
| weak_ptr_factory_(this) {} |
| |
| PrivetV3Session::FetcherDelegate::~FetcherDelegate() {} |
| |
| void PrivetV3Session::FetcherDelegate::OnURLFetchComplete( |
| const net::URLFetcher* source) { |
| VLOG(1) << "PrivetURLFetcher url: " << source->GetURL() |
| << ", status: " << source->GetStatus().status() |
| << ", error: " << source->GetStatus().error() |
| << ", response code: " << source->GetResponseCode(); |
| |
| std::unique_ptr<base::DictionaryValue> value = GetJson(source); |
| if (!value) { |
| return ReplyAndDestroyItself(Result::STATUS_CONNECTIONERROR, |
| base::DictionaryValue()); |
| } |
| |
| bool has_error = value->HasKey(kPrivetV3KeyError); |
| LOG_IF(ERROR, has_error) << "Response: " << value.get(); |
| ReplyAndDestroyItself( |
| has_error ? Result::STATUS_DEVICEERROR : Result::STATUS_SUCCESS, *value); |
| } |
| |
| net::URLFetcher* PrivetV3Session::FetcherDelegate::CreateURLFetcher( |
| const GURL& url, |
| net::URLFetcher::RequestType request_type, |
| bool orphaned) { |
| DCHECK(!url_fetcher_); |
| DCHECK(session_); |
| url_fetcher_ = net::URLFetcher::Create(url, request_type, this); |
| auto timeout_task = |
| orphaned ? base::Bind(&FetcherDelegate::OnTimeout, base::Owned(this)) |
| : base::Bind(&FetcherDelegate::OnTimeout, |
| weak_ptr_factory_.GetWeakPtr()); |
| base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( |
| FROM_HERE, timeout_task, |
| base::TimeDelta::FromSeconds(kUrlFetcherTimeoutSec)); |
| return url_fetcher_.get(); |
| } |
| |
| void PrivetV3Session::FetcherDelegate::ReplyAndDestroyItself( |
| Result result, |
| const base::DictionaryValue& value) { |
| if (session_) { |
| if (!callback_.is_null()) { |
| callback_.Run(result, value); |
| callback_.Reset(); |
| } |
| base::ThreadTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, base::Bind(&PrivetV3Session::DeleteFetcher, session_, |
| base::Unretained(this))); |
| session_.reset(); |
| } |
| url_fetcher_.reset(); |
| } |
| |
| void PrivetV3Session::FetcherDelegate::OnTimeout() { |
| LOG(ERROR) << "PrivetURLFetcher timeout, url: " << url_fetcher_->GetURL(); |
| ReplyAndDestroyItself(Result::STATUS_CONNECTIONERROR, |
| base::DictionaryValue()); |
| } |
| |
| PrivetV3Session::PrivetV3Session( |
| const scoped_refptr<PrivetV3ContextGetter>& context_getter, |
| const net::HostPortPair& host_port) |
| : host_port_(host_port), |
| context_getter_(context_getter), |
| weak_ptr_factory_(this) { |
| CHECK(context_getter_); |
| } |
| |
| PrivetV3Session::~PrivetV3Session() { |
| Cancel(); |
| } |
| |
| void PrivetV3Session::Init(const InitCallback& callback) { |
| DCHECK(fetchers_.empty()); |
| DCHECK(!use_https_); |
| DCHECK(session_id_.empty()); |
| DCHECK(privet_auth_token_.empty()); |
| |
| privet_auth_token_ = kPrivetV3AuthAnonymous; |
| StartGetRequest(kPrivetInfoPath, |
| base::Bind(&PrivetV3Session::OnInfoDone, |
| weak_ptr_factory_.GetWeakPtr(), callback)); |
| } |
| |
| void PrivetV3Session::OnInfoDone(const InitCallback& callback, |
| Result result, |
| const base::DictionaryValue& response) { |
| if (result != Result::STATUS_SUCCESS) |
| return callback.Run(result, response); |
| |
| std::string version; |
| if (!response.GetString(kPrivetV3InfoKeyVersion, &version) || |
| version != kPrivetV3InfoVersion) { |
| LOG(ERROR) << "Response: " << response; |
| return callback.Run(Result::STATUS_SESSIONERROR, response); |
| } |
| |
| const base::DictionaryValue* authentication = nullptr; |
| const base::ListValue* pairing = nullptr; |
| if (!response.GetDictionary(kPrivetV3InfoKeyAuth, &authentication) || |
| !authentication->GetList(kPrivetV3KeyPairing, &pairing)) { |
| LOG(ERROR) << "Response: " << response; |
| return callback.Run(Result::STATUS_SESSIONERROR, response); |
| } |
| |
| // The only supported crypto. |
| if (!ContainsString(*authentication, kPrivetV3KeyCrypto, |
| kPrivetV3CryptoP224Spake2) || |
| !ContainsString(*authentication, kPrivetV3KeyMode, kPrivetV3KeyPairing)) { |
| LOG(ERROR) << "Response: " << response; |
| return callback.Run(Result::STATUS_SESSIONERROR, response); |
| } |
| |
| int port = 0; |
| if (!response.GetInteger(kPrivetV3InfoHttpsPort, &port) || port <= 0 || |
| port > std::numeric_limits<uint16_t>::max()) { |
| LOG(ERROR) << "Response: " << response; |
| return callback.Run(Result::STATUS_SESSIONERROR, response); |
| } |
| https_port_ = port; |
| |
| callback.Run(Result::STATUS_SUCCESS, response); |
| } |
| |
| void PrivetV3Session::StartPairing(PairingType pairing_type, |
| const ResultCallback& callback) { |
| base::DictionaryValue input; |
| input.SetString(kPrivetV3KeyPairing, |
| extensions::api::gcd_private::ToString(pairing_type)); |
| input.SetString(kPrivetV3KeyCrypto, kPrivetV3CryptoP224Spake2); |
| |
| StartPostRequest(kPrivetV3PairingStartPath, input, |
| base::Bind(&PrivetV3Session::OnPairingStartDone, |
| weak_ptr_factory_.GetWeakPtr(), callback)); |
| } |
| |
| void PrivetV3Session::OnPairingStartDone( |
| const ResultCallback& callback, |
| Result result, |
| const base::DictionaryValue& response) { |
| if (result != Result::STATUS_SUCCESS) |
| return callback.Run(result); |
| |
| if (!response.GetString(kPrivetV3KeySessionId, &session_id_) || |
| !GetDecodedString(response, kPrivetV3KeyDeviceCommitment, &commitment_)) { |
| LOG(ERROR) << "Response: " << response; |
| return callback.Run(Result::STATUS_SESSIONERROR); |
| } |
| |
| return callback.Run(Result::STATUS_SUCCESS); |
| } |
| |
| void PrivetV3Session::ConfirmCode(const std::string& code, |
| const ResultCallback& callback) { |
| if (session_id_.empty()) { |
| LOG(ERROR) << "Pairing is not started"; |
| return callback.Run(Result::STATUS_SESSIONERROR); |
| } |
| |
| spake_.reset(new crypto::P224EncryptedKeyExchange( |
| crypto::P224EncryptedKeyExchange::kPeerTypeClient, code)); |
| |
| base::DictionaryValue input; |
| input.SetString(kPrivetV3KeySessionId, session_id_); |
| |
| std::string client_commitment; |
| base::Base64Encode(spake_->GetNextMessage(), &client_commitment); |
| input.SetString(kPrivetV3KeyClientCommitment, client_commitment); |
| |
| // Call ProcessMessage after GetNextMessage(). |
| crypto::P224EncryptedKeyExchange::Result result = |
| spake_->ProcessMessage(commitment_); |
| DCHECK_EQ(result, crypto::P224EncryptedKeyExchange::kResultPending); |
| |
| StartPostRequest(kPrivetV3PairingConfirmPath, input, |
| base::Bind(&PrivetV3Session::OnPairingConfirmDone, |
| weak_ptr_factory_.GetWeakPtr(), callback)); |
| } |
| |
| void PrivetV3Session::OnPairingConfirmDone( |
| const ResultCallback& callback, |
| Result result, |
| const base::DictionaryValue& response) { |
| if (result != Result::STATUS_SUCCESS) |
| return callback.Run(result); |
| |
| std::string fingerprint; |
| std::string signature; |
| if (!GetDecodedString(response, kPrivetV3KeyCertFingerprint, &fingerprint) || |
| !GetDecodedString(response, kPrivetV3KeyCertSignature, &signature)) { |
| LOG(ERROR) << "Invalid response: " << response; |
| return callback.Run(Result::STATUS_SESSIONERROR); |
| } |
| |
| net::SHA256HashValue hash; |
| if (fingerprint.size() != sizeof(hash.data)) { |
| LOG(ERROR) << "Invalid fingerprint size: " << response; |
| return callback.Run(Result::STATUS_SESSIONERROR); |
| } |
| memcpy(hash.data, fingerprint.data(), sizeof(hash.data)); |
| |
| crypto::HMAC hmac(crypto::HMAC::SHA256); |
| // Key will be verified below, using HMAC. |
| const std::string& key = spake_->GetUnverifiedKey(); |
| if (!hmac.Init(reinterpret_cast<const unsigned char*>(key.c_str()), |
| key.size()) || |
| !hmac.Verify(fingerprint, signature)) { |
| LOG(ERROR) << "Verification failed: " << response; |
| return callback.Run(Result::STATUS_BADPAIRINGCODEERROR); |
| } |
| |
| std::string auth_code(hmac.DigestLength(), ' '); |
| if (!hmac.Sign(session_id_, reinterpret_cast<unsigned char*>( |
| base::string_as_array(&auth_code)), |
| auth_code.size())) { |
| LOG(FATAL) << "Signing failed"; |
| return callback.Run(Result::STATUS_SESSIONERROR); |
| } |
| |
| VLOG(1) << "Expected certificate: " << fingerprint; |
| context_getter_->AddPairedHost( |
| host_port_.host(), hash, |
| base::Bind(&PrivetV3Session::OnPairedHostAddedToContext, |
| weak_ptr_factory_.GetWeakPtr(), auth_code, callback)); |
| } |
| |
| void PrivetV3Session::OnPairedHostAddedToContext( |
| const std::string& auth_code, |
| const ResultCallback& callback) { |
| SwitchToHttps(); |
| |
| std::string auth_code_base64; |
| base::Base64Encode(auth_code, &auth_code_base64); |
| |
| base::DictionaryValue input; |
| input.SetString(kPrivetV3KeyAuthCode, auth_code_base64); |
| input.SetString(kPrivetV3KeyMode, kPrivetV3KeyPairing); |
| input.SetString(kPrivetV3KeyRequestedScope, kPrivetV3Auto); |
| |
| // Now we can use SendMessage with certificate validation. |
| SendMessage(kPrivetV3AuthPath, input, |
| base::Bind(&PrivetV3Session::OnAuthenticateDone, |
| weak_ptr_factory_.GetWeakPtr(), callback)); |
| } |
| |
| void PrivetV3Session::OnAuthenticateDone( |
| const ResultCallback& callback, |
| Result result, |
| const base::DictionaryValue& response) { |
| if (result != Result::STATUS_SUCCESS) |
| return callback.Run(result); |
| |
| std::string access_token; |
| std::string token_type; |
| std::string scope; |
| if (!response.GetString(kPrivetV3KeyAccessToken, &access_token) || |
| !response.GetString(kPrivetV3KeyTokenType, &token_type) || |
| !response.GetString(kPrivetV3KeyScope, &scope)) { |
| LOG(ERROR) << "Response: " << response; |
| return callback.Run(Result::STATUS_SESSIONERROR); |
| } |
| |
| privet_auth_token_ = token_type + " " + access_token; |
| |
| return callback.Run(Result::STATUS_SUCCESS); |
| } |
| |
| void PrivetV3Session::SendMessage(const std::string& api, |
| const base::DictionaryValue& input, |
| const MessageCallback& callback) { |
| if (!use_https_) { |
| LOG(ERROR) << "Session is not paired"; |
| return callback.Run(Result::STATUS_SESSIONERROR, base::DictionaryValue()); |
| } |
| |
| StartPostRequest(api, input, callback); |
| } |
| |
| void PrivetV3Session::RunCallback(const base::Closure& callback) { |
| callback.Run(); |
| } |
| |
| void PrivetV3Session::StartGetRequest(const std::string& api, |
| const MessageCallback& callback) { |
| net::URLFetcher* fetcher = |
| CreateFetcher(api, net::URLFetcher::RequestType::GET, callback); |
| if (!fetcher) |
| return; |
| fetcher->Start(); |
| } |
| |
| void PrivetV3Session::StartPostRequest(const std::string& api, |
| const base::DictionaryValue& input, |
| const MessageCallback& callback) { |
| if (!on_post_data_.is_null()) |
| on_post_data_.Run(input); |
| std::string json; |
| base::JSONWriter::WriteWithOptions( |
| input, base::JSONWriter::OPTIONS_PRETTY_PRINT, &json); |
| net::URLFetcher* fetcher = |
| CreateFetcher(api, net::URLFetcher::RequestType::POST, callback); |
| if (!fetcher) |
| return; |
| fetcher->SetUploadData(kContentTypeJSON, json); |
| fetcher->Start(); |
| } |
| |
| void PrivetV3Session::SwitchToHttps() { |
| host_port_.set_port(https_port_); |
| use_https_ = true; |
| } |
| |
| GURL PrivetV3Session::CreatePrivetURL(const std::string& path) const { |
| GURL url("http://host/"); |
| GURL::Replacements replacements; |
| std::string schema = use_https_ ? "https" : "http"; |
| replacements.SetSchemeStr(schema); |
| std::string host = host_port_.HostForURL(); |
| replacements.SetHostStr(host); |
| std::string port = base::UintToString(host_port_.port()); |
| replacements.SetPortStr(port); |
| replacements.SetPathStr(path); |
| url = url.ReplaceComponents(replacements); |
| DCHECK(url.is_valid()) << url; |
| return url; |
| } |
| |
| net::URLFetcher* PrivetV3Session::CreateFetcher( |
| const std::string& api, |
| net::URLFetcher::RequestType request_type, |
| const MessageCallback& callback) { |
| GURL url = CreatePrivetURL(api); |
| if (!url.is_valid()) { |
| callback.Run(Result::STATUS_SESSIONERROR, base::DictionaryValue()); |
| return nullptr; |
| } |
| |
| // Don't abort cancel requests after session object is destroyed. |
| const bool orphaned = (api == kPrivetV3PairingCancelPath); |
| FetcherDelegate* fetcher = |
| new FetcherDelegate(weak_ptr_factory_.GetWeakPtr(), callback); |
| if (!orphaned) |
| fetchers_.push_back(base::WrapUnique(fetcher)); |
| net::URLFetcher* url_fetcher = |
| fetcher->CreateURLFetcher(url, request_type, orphaned); |
| url_fetcher->SetLoadFlags(url_fetcher->GetLoadFlags() | |
| net::LOAD_BYPASS_PROXY | net::LOAD_DISABLE_CACHE | |
| net::LOAD_DO_NOT_SEND_COOKIES); |
| url_fetcher->SetRequestContext(context_getter_.get()); |
| url_fetcher->AddExtraRequestHeader( |
| std::string(kPrivetV3AuthTokenHeaderPrefix) + privet_auth_token_); |
| |
| return url_fetcher; |
| } |
| |
| void PrivetV3Session::DeleteFetcher(const FetcherDelegate* fetcher) { |
| for (std::vector<std::unique_ptr<FetcherDelegate>>::iterator iter = |
| fetchers_.begin(); |
| iter != fetchers_.end(); ++iter) { |
| if (iter->get() == fetcher) { |
| fetchers_.erase(iter); |
| break; |
| } |
| } |
| } |
| |
| void PrivetV3Session::Cancel() { |
| // Only session with pairing in process needs to be canceled. Paired sessions |
| // (in https mode) does not need to be canceled. |
| if (session_id_.empty() || use_https_) |
| return; |
| base::DictionaryValue input; |
| input.SetString(kPrivetV3KeySessionId, session_id_); |
| StartPostRequest(kPrivetV3PairingCancelPath, input, MessageCallback()); |
| } |
| |
| } // namespace extensions |