blob: 9fbb9764c344282744b18656406e826a7d882a27 [file] [log] [blame]
// Copyright 2019 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/credential_provider/gaiacp/password_recovery_manager.h"
#include <windows.h>
#include <winternl.h>
#include <lm.h> // Needed for LSA_UNICODE_STRING
#include <process.h>
#define _NTDEF_ // Prevent redefition errors, must come after <winternl.h>
#include <ntsecapi.h> // For POLICY_ALL_ACCESS types
#include "base/base64.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/stl_util.h"
#include "base/strings/strcat.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/credential_provider/common/gcp_strings.h"
#include "chrome/credential_provider/gaiacp/gcp_utils.h"
#include "chrome/credential_provider/gaiacp/logging.h"
#include "chrome/credential_provider/gaiacp/mdm_utils.h"
#include "chrome/credential_provider/gaiacp/reg_utils.h"
#include "chrome/credential_provider/gaiacp/scoped_lsa_policy.h"
#include "chrome/credential_provider/gaiacp/win_http_url_fetcher.h"
namespace credential_provider {
const base::TimeDelta
PasswordRecoveryManager::kDefaultEscrowServiceRequestTimeout =
base::TimeDelta::FromMilliseconds(3000);
namespace {
typedef std::vector<std::pair<std::string, std::string*>>
UrlFetchResultNeedOutputs;
// Constants for storing password recovery information in the LSA.
constexpr char kUserPasswordLsaStoreIdKey[] = "resource_id";
constexpr char kUserPasswordLsaStoreEncryptedPasswordKey[] =
"encrypted_password";
// Constants used for contacting the password escrow service.
const char kEscrowServiceGenerateKeyPairPath[] = "/v1/generatekeypair";
const char kGenerateKeyPairRequestDeviceIdParameterName[] = "deviceId";
const char kGenerateKeyPairResponsePublicKeyParameterName[] = "base64PublicKey";
const char kGenerateKeyPairResponseResourceIdParameterName[] = "resourceId";
const char kEscrowServiceGetPrivateKeyPath[] = "/v1/getprivatekey";
const char kGetPrivateKeyResponsePrivateKeyParameterName[] = "base64PrivateKey";
constexpr wchar_t kUserPasswordLsaStoreKeyPrefix[] =
#if defined(GOOGLE_CHROME_BUILD)
L"Chrome-GCPW-";
#else
L"Chromium-GCPW-";
#endif
// Self deleting escrow service requester. This class will try to make a query
// using the given url fetcher. It will delete itself when the request is
// completed, either because the request completed successfully within the
// timeout or the request has timed out and is allowed to complete in the
// background without having the result read by anyone.
// There are two situations where the request will be deleted:
// 1. If the background thread making the request returns within the given
// timeout, the function is guaranteed to return the result that was fetched.
// 2. If however the background thread times out there are two potential
// race conditions that can occur:
// 1. The main thread making the request can mark that the background thread
// is orphaned before it can complete. In this case when the background
// thread completes it will check whether the request is orphaned and self
// delete.
// 2. The background thread completes before the main thread can mark the
// request as orphaned. In this case the background thread will have
// marked that the request is no longer processing and thus the main
// thread can self delete.
class EscrowServiceRequest {
public:
explicit EscrowServiceRequest(std::unique_ptr<WinHttpUrlFetcher> fetcher)
: fetcher_(std::move(fetcher)) {
DCHECK(fetcher_);
}
// Tries to fetch the request stored in |fetcher_| in a background thread
// within the given |request_timeout|. If the background thread returns before
// the timeout expires, it is guaranteed that a result can be returned and the
// requester will delete itself.
base::Optional<base::Value> WaitForResponseFromEscrowService(
const base::TimeDelta& request_timeout) {
base::Optional<base::Value> result;
// Start the thread and wait on its handle until |request_timeout| expires
// or the thread finishes.
unsigned wait_thread_id;
uintptr_t wait_thread = ::_beginthreadex(
nullptr, 0, &EscrowServiceRequest::FetchResultFromEscrowService,
reinterpret_cast<void*>(this), 0, &wait_thread_id);
HRESULT hr = S_OK;
if (wait_thread == 0) {
return result;
} else {
// Hold the handle in the scoped handle so that it can be immediately
// closed when the wait is complete allowing the thread to finish
// completely if needed.
base::win::ScopedHandle thread_handle(
reinterpret_cast<HANDLE>(wait_thread));
hr = ::WaitForSingleObject(thread_handle.Get(),
request_timeout.InMilliseconds());
}
// The race condition starts here. It is possible that between the expiry of
// the timeout in the call for WaitForSingleObject and the call to
// OrphanRequest, the fetching thread could have finished. So there is a two
// part handshake. Either the background thread has called ProcessingDone
// in which case it has already passed its own check for |is_orphaned_| and
// the call to OrphanRequest should delete this object right now. Otherwise
// the background thread is still running and will be able to query the
// |is_orphaned_| state and delete the object after thread completion.
if (hr != WAIT_OBJECT_0) {
LOGFN(ERROR) << "Wait for response timed out or failed hr=" << putHR(hr);
OrphanRequest();
return result;
}
result = base::JSONReader::Read(
base::StringPiece(response_.data(), response_.size()),
base::JSON_ALLOW_TRAILING_COMMAS);
if (!result || !result->is_dict()) {
LOGFN(ERROR) << "Failed to read json result from server response";
result.reset();
}
delete this;
return result;
}
private:
void OrphanRequest() {
bool delete_self = false;
{
base::AutoLock locker(orphan_lock_);
CHECK(!is_orphaned_);
if (!is_processing_) {
delete_self = true;
} else {
is_orphaned_ = true;
}
}
if (delete_self)
delete this;
}
void ProcessingDone() {
bool delete_self = false;
{
base::AutoLock locker(orphan_lock_);
CHECK(is_processing_);
if (is_orphaned_) {
delete_self = true;
} else {
is_processing_ = false;
}
}
if (delete_self)
delete this;
}
// Background thread function that is used to query the request to the
// escrow service. This thread never times out and simply marks the fetcher
// as finished processing when it is done.
static unsigned __stdcall FetchResultFromEscrowService(void* param) {
DCHECK(param);
EscrowServiceRequest* requester =
reinterpret_cast<EscrowServiceRequest*>(param);
HRESULT hr = requester->fetcher_->Fetch(&requester->response_);
if (FAILED(hr))
LOGFN(INFO) << "fetcher.Fetch hr=" << putHR(hr);
requester->ProcessingDone();
return 0;
}
base::Lock orphan_lock_;
std::unique_ptr<WinHttpUrlFetcher> fetcher_;
std::vector<char> response_;
bool is_orphaned_ = false;
bool is_processing_ = true;
};
// Builds the required json request to be sent to the escrow service and fetches
// the json response from the escrow service (if any). Returns S_OK if
// |needed_outputs| can be filled correctly with the requested data, otherwise
// returns an error code.
// |request_url| is the full query url from which to fetch a response.
// |headers| are all the header key value pairs to be sent with the request.
// |parameters| are all the json parameters to be sent with the request. This
// argument will be converted to a json string and sent as part of the body of
// the request.
// |request_timeout| is the maximum time to wait for a response.
// |needed_outputs| is the mapping of the desired result key to an address where
// the result can be stored.
// If any |needed_outputs| is missing, all of the outputs are cleared.
HRESULT BuildRequestAndFetchResultFromEscrowService(
const GURL& request_url,
const std::vector<std::pair<std::string, std::string>>& headers,
const std::vector<std::pair<std::string, std::string>>& parameters,
const UrlFetchResultNeedOutputs& needed_outputs,
const base::TimeDelta& request_timeout) {
DCHECK(needed_outputs.size());
if (request_url.is_empty()) {
LOGFN(ERROR) << "No escrow service url specified";
return E_FAIL;
}
auto url_fetcher = WinHttpUrlFetcher::Create(request_url);
if (!url_fetcher) {
LOGFN(ERROR) << "Could not create valid fetcher for url="
<< request_url.spec();
return E_FAIL;
}
url_fetcher->SetRequestHeader("Content-Type", "application/json");
for (auto& header : headers)
url_fetcher->SetRequestHeader(header.first.c_str(), header.second.c_str());
HRESULT hr = S_OK;
if (!parameters.empty()) {
base::Value request_dict(base::Value::Type::DICTIONARY);
for (auto& parameter : parameters)
request_dict.SetStringKey(parameter.first, parameter.second);
std::string json;
if (!base::JSONWriter::Write(request_dict, &json)) {
LOGFN(ERROR) << "base::JSONWriter::Write failed";
return E_FAIL;
}
hr = url_fetcher->SetRequestBody(json.c_str());
if (FAILED(hr)) {
LOGFN(ERROR) << "fetcher.SetRequestBody hr=" << putHR(hr);
return E_FAIL;
}
}
base::Optional<base::Value> request_result =
(new EscrowServiceRequest(std::move(url_fetcher)))
->WaitForResponseFromEscrowService(request_timeout);
if (!request_result)
return E_FAIL;
for (const std::pair<std::string, std::string*>& output : needed_outputs) {
const std::string* output_value =
request_result->FindStringKey(output.first);
if (!output_value) {
LOGFN(ERROR) << "Could not extract value '" << output.first
<< "' from server response";
hr = E_FAIL;
break;
}
DCHECK(output.second);
*output.second = *output_value;
}
if (FAILED(hr)) {
for (const std::pair<std::string, std::string*>& output : needed_outputs)
output.second->clear();
}
return hr;
}
// Makes a standard: "Authorization: Bearer $TOKEN" header for passing
// authorization information to a server.
std::pair<std::string, std::string> MakeAuthorizationHeader(
const std::string& access_token) {
return {"Authorization", "Bearer " + access_token};
}
// Request a new public key and corresponding resource id from the escrow
// service in order to encrypt |password|. |access_token| is used to authorize
// the request on the escrow service. |device_id| is used to identify the device
// making the request. Fills in |encrypted_data| the resource id for the
// encryption key and also with the encryped password.
HRESULT EncryptUserPasswordUsingEscrowService(
const std::string& access_token,
const std::string& device_id,
const base::string16& password,
const base::TimeDelta& request_timeout,
base::Optional<base::Value>* encrypted_data) {
DCHECK(encrypted_data);
DCHECK(!(*encrypted_data));
std::string resource_id;
std::string public_key;
// Fetch the results and extract the |resource_id| for the key and the
// |public_key| to be used for encryption.
HRESULT hr = BuildRequestAndFetchResultFromEscrowService(
PasswordRecoveryManager::Get()->GetEscrowServiceGenerateKeyPairUrl(),
{MakeAuthorizationHeader(access_token)},
{{kGenerateKeyPairRequestDeviceIdParameterName, device_id}},
{
{kGenerateKeyPairResponseResourceIdParameterName, &resource_id},
{kGenerateKeyPairResponsePublicKeyParameterName, &public_key},
},
request_timeout);
if (FAILED(hr)) {
LOGFN(ERROR) << "BuildRequestAndFetchResultFromEscrowService hr="
<< putHR(hr);
return E_FAIL;
}
encrypted_data->emplace(base::Value(base::Value::Type::DICTIONARY));
(*encrypted_data)->SetStringKey(kUserPasswordLsaStoreIdKey, resource_id);
(*encrypted_data)
->SetStringKey(kUserPasswordLsaStoreEncryptedPasswordKey,
base::UTF16ToUTF8(password));
return hr;
}
// Given the |encrypted_data| which would contain the resource id of the
// encryption key and the encrypted password, recovers the |decrypted_password|
// by getting the private key from the escrow service and decrypting the
// password. |access_token| is used to authorize the request on the escrow
// service.
HRESULT DecryptUserPasswordUsingEscrowService(
const std::string& access_token,
const base::Optional<base::Value>& encrypted_data,
const base::TimeDelta& request_timeout,
base::string16* decrypted_password) {
if (!encrypted_data)
return E_FAIL;
DCHECK(decrypted_password);
DCHECK(encrypted_data && encrypted_data->is_dict());
const std::string* resource_id =
encrypted_data->FindStringKey(kUserPasswordLsaStoreIdKey);
const std::string* encrypted_password =
encrypted_data->FindStringKey(kUserPasswordLsaStoreEncryptedPasswordKey);
if (!resource_id) {
LOGFN(ERROR) << "No password resource id found to restore";
return E_FAIL;
}
if (!encrypted_password) {
LOGFN(ERROR) << "No encrypted password found to restore";
return E_FAIL;
}
std::string private_key;
// Fetch the results and extract the |private_key| to be used for decryption.
HRESULT hr = BuildRequestAndFetchResultFromEscrowService(
PasswordRecoveryManager::Get()->GetEscrowServiceGetPrivateKeyUrl(
*resource_id),
{MakeAuthorizationHeader(access_token)}, {},
{
{kGetPrivateKeyResponsePrivateKeyParameterName, &private_key},
},
request_timeout);
if (FAILED(hr)) {
LOGFN(ERROR) << "BuildRequestAndFetchResultFromEscrowService hr="
<< putHR(hr);
return E_FAIL;
}
// Move semantics should ensure the temporary password is directly moved
// into |decrypted_password| and thus does not need to be securely zeroed.
*decrypted_password = base::UTF8ToUTF16(*encrypted_password);
return S_OK;
}
} // namespace
// static
PasswordRecoveryManager* PasswordRecoveryManager::Get() {
return *GetInstanceStorage();
}
// static
PasswordRecoveryManager** PasswordRecoveryManager::GetInstanceStorage() {
static PasswordRecoveryManager instance(kDefaultEscrowServiceRequestTimeout);
static PasswordRecoveryManager* instance_storage = &instance;
return &instance_storage;
}
PasswordRecoveryManager::PasswordRecoveryManager(
base::TimeDelta request_timeout)
: request_timeout_(request_timeout) {}
PasswordRecoveryManager::~PasswordRecoveryManager() = default;
HRESULT PasswordRecoveryManager::GetUserPasswordLsaStoreKey(
const base::string16& sid,
base::string16* store_key) {
DCHECK(store_key);
DCHECK(sid.size());
*store_key = kUserPasswordLsaStoreKeyPrefix + sid;
return S_OK;
}
HRESULT PasswordRecoveryManager::ClearUserRecoveryPassword(
const base::string16& sid) {
auto policy = ScopedLsaPolicy::Create(POLICY_ALL_ACCESS);
if (!policy) {
HRESULT hr = HRESULT_FROM_WIN32(::GetLastError());
LOGFN(ERROR) << "ScopedLsaPolicy::Create hr=" << putHR(hr);
return hr;
}
base::string16 store_key;
HRESULT hr = GetUserPasswordLsaStoreKey(sid, &store_key);
if (FAILED(hr)) {
LOGFN(ERROR) << "GetUserPasswordLsaStoreKey hr=" << putHR(hr);
return hr;
}
return policy->RemovePrivateData(store_key.c_str());
}
HRESULT PasswordRecoveryManager::StoreWindowsPasswordIfNeeded(
const base::string16& sid,
const std::string& access_token,
const base::string16& password) {
if (!MdmPasswordRecoveryEnabled())
return E_NOTIMPL;
base::string16 machine_guid;
HRESULT hr = GetMachineGuid(&machine_guid);
if (FAILED(hr)) {
LOGFN(ERROR) << "Failed to get machine GUID hr=" << putHR(hr);
return hr;
}
std::string device_id = base::UTF16ToUTF8(machine_guid);
auto policy = ScopedLsaPolicy::Create(POLICY_ALL_ACCESS);
if (!policy) {
HRESULT hr = HRESULT_FROM_WIN32(::GetLastError());
LOGFN(ERROR) << "ScopedLsaPolicy::Create hr=" << putHR(hr);
return hr;
}
// See if a password key is already stored in the LSA for this user.
base::string16 store_key;
hr = GetUserPasswordLsaStoreKey(sid, &store_key);
if (FAILED(hr)) {
LOGFN(ERROR) << "GetUserPasswordLsaStoreKey hr=" << putHR(hr);
return hr;
}
// Only check if a value already exists for the user's password. The call to
// RetrievePrivateData always succeeds if the value exists, regardless of
// the size of the buffer passed in. It will merely copy whatever it can
// into the buffer. In this case we don't care about the contents and
// just want to check the existence of a value.
wchar_t password_lsa_data[32];
hr = policy->RetrievePrivateData(store_key.c_str(), password_lsa_data,
base::size(password_lsa_data));
if (SUCCEEDED(hr)) {
SecurelyClearBuffer(password_lsa_data, sizeof(password_lsa_data));
return S_OK;
}
base::Optional<base::Value> encrypted_dict;
hr = EncryptUserPasswordUsingEscrowService(access_token, device_id, password,
request_timeout_, &encrypted_dict);
if (SUCCEEDED(hr)) {
std::string lsa_value;
if (base::JSONWriter::Write(encrypted_dict.value(), &lsa_value)) {
base::string16 lsa_value16 = base::UTF8ToUTF16(lsa_value);
hr = policy->StorePrivateData(store_key.c_str(), lsa_value16.c_str());
SecurelyClearString(lsa_value16);
SecurelyClearString(lsa_value);
if (FAILED(hr)) {
LOGFN(ERROR) << "StorePrivateData hr=" << putHR(hr);
return hr;
}
} else {
LOGFN(ERROR) << "base::JSONWriter::Write failed";
return E_FAIL;
}
SecurelyClearDictionaryValueWithKey(
&encrypted_dict, kUserPasswordLsaStoreEncryptedPasswordKey);
} else {
LOGFN(ERROR) << "EncryptUserPasswordUsingEscrowService hr=" << putHR(hr);
return E_FAIL;
}
return S_OK;
}
HRESULT PasswordRecoveryManager::RecoverWindowsPasswordIfPossible(
const base::string16& sid,
const std::string& access_token,
base::string16* recovered_password) {
if (!MdmPasswordRecoveryEnabled())
return E_NOTIMPL;
DCHECK(recovered_password);
auto policy = ScopedLsaPolicy::Create(POLICY_ALL_ACCESS);
if (!policy) {
HRESULT hr = HRESULT_FROM_WIN32(::GetLastError());
LOGFN(ERROR) << "ScopedLsaPolicy::Create hr=" << putHR(hr);
return hr;
}
// See if a password key is already stored in the LSA for this user.
base::string16 store_key;
HRESULT hr = GetUserPasswordLsaStoreKey(sid, &store_key);
if (FAILED(hr)) {
LOGFN(ERROR) << "GetUserPasswordLsaStoreKey hr=" << putHR(hr);
return hr;
}
wchar_t password_lsa_data[1024];
hr = policy->RetrievePrivateData(store_key.c_str(), password_lsa_data,
base::size(password_lsa_data));
if (FAILED(hr))
LOGFN(ERROR) << "RetrievePrivateData hr=" << putHR(hr);
std::string json_string = base::UTF16ToUTF8(password_lsa_data);
base::Optional<base::Value> encrypted_dict =
base::JSONReader::Read(json_string, base::JSON_ALLOW_TRAILING_COMMAS);
SecurelyClearString(json_string);
SecurelyClearBuffer(password_lsa_data, sizeof(password_lsa_data));
base::string16 decrypted_password;
hr = DecryptUserPasswordUsingEscrowService(
access_token, encrypted_dict, request_timeout_, &decrypted_password);
if (encrypted_dict) {
SecurelyClearDictionaryValueWithKey(
&encrypted_dict, kUserPasswordLsaStoreEncryptedPasswordKey);
}
if (SUCCEEDED(hr))
*recovered_password = decrypted_password;
SecurelyClearString(decrypted_password);
return hr;
}
GURL PasswordRecoveryManager::GetEscrowServiceGenerateKeyPairUrl() {
if (!MdmPasswordRecoveryEnabled())
return GURL();
GURL escrow_service_server = MdmEscrowServiceUrl();
if (escrow_service_server.is_empty()) {
LOGFN(ERROR) << "No escrow service server specified";
return GURL();
}
return escrow_service_server.Resolve(kEscrowServiceGenerateKeyPairPath);
}
GURL PasswordRecoveryManager::GetEscrowServiceGetPrivateKeyUrl(
const std::string& resource_id) {
if (!MdmPasswordRecoveryEnabled())
return GURL();
GURL escrow_service_server = MdmEscrowServiceUrl();
if (escrow_service_server.is_empty()) {
LOGFN(ERROR) << "No escrow service server specified";
return GURL();
}
return escrow_service_server.Resolve(
base::StrCat({kEscrowServiceGetPrivateKeyPath, "/", resource_id}));
}
std::string PasswordRecoveryManager::MakeGenerateKeyPairResponseForTesting(
const std::string& public_key,
const std::string& resource_id) {
return base::StringPrintf(
R"({"%s": "%s", "%s": "%s"})",
kGenerateKeyPairResponsePublicKeyParameterName, public_key.c_str(),
kGenerateKeyPairResponseResourceIdParameterName, resource_id.c_str());
}
std::string PasswordRecoveryManager::MakeGetPrivateKeyResponseForTesting(
const std::string& private_key) {
return base::StringPrintf(R"({"%s": "%s"})",
kGetPrivateKeyResponsePrivateKeyParameterName,
private_key.c_str());
}
} // namespace credential_provider