blob: 7dd324735c6b23dc1698433f4fbaafe09de7e61f [file] [log] [blame]
// Copyright 2018 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/gcp_utils.h"
#include <wincred.h> // For <ntsecapi.h>
#include <windows.h>
#include <winternl.h>
#define _NTDEF_ // Prevent redefition errors, must come after <winternl.h>
#include <MDMRegistration.h> // For RegisterDeviceWithManagement()
#include <ntsecapi.h> // For LsaLookupAuthenticationPackage()
#include <sddl.h> // For ConvertSidToStringSid()
#include <security.h> // For NEGOSSP_NAME_A
#include <wbemidl.h>
#include <atlbase.h>
#include <atlcom.h>
#include <atlcomcli.h>
#include <atlconv.h>
#include <malloc.h>
#include <memory.h>
#include <stdlib.h>
#include <iomanip>
#include <memory>
#include "base/base64.h"
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/json/json_writer.h"
#include "base/macros.h"
#include "base/path_service.h"
#include "base/scoped_native_library.h"
#include "base/stl_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/win/current_module.h"
#include "base/win/registry.h"
#include "base/win/win_util.h"
#include "chrome/credential_provider/common/gcp_strings.h"
#include "chrome/credential_provider/gaiacp/logging.h"
namespace credential_provider {
namespace {
HRESULT RegisterWithGoogleDeviceManagement(const base::string16& mdm_url,
const base::string16& email,
const std::string& data) {
base::ScopedNativeLibrary library(
base::FilePath(FILE_PATH_LITERAL("MDMRegistration.dll")));
if (!library.is_valid()) {
LOGFN(ERROR) << "base::ScopedNativeLibrary hr=" << putHR(E_NOTIMPL);
return E_NOTIMPL;
}
using RegisterDeviceWithManagementFunction =
decltype(&::RegisterDeviceWithManagement);
RegisterDeviceWithManagementFunction
register_device_with_management_function =
reinterpret_cast<RegisterDeviceWithManagementFunction>(
library.GetFunctionPointer("RegisterDeviceWithManagement"));
if (!register_device_with_management_function) {
LOGFN(ERROR) << "library.GetFunctionPointer hr=" << putHR(E_NOTIMPL);
return E_NOTIMPL;
}
std::string data_encoded;
base::Base64Encode(data, &data_encoded);
return register_device_with_management_function(
email.c_str(), mdm_url.c_str(), base::UTF8ToWide(data_encoded).c_str());
}
} // namespace
// StdParentHandles ///////////////////////////////////////////////////////////
StdParentHandles::StdParentHandles() {}
StdParentHandles::~StdParentHandles() {}
// ScopedStartupInfo //////////////////////////////////////////////////////////
ScopedStartupInfo::ScopedStartupInfo() {
memset(&info_, 0, sizeof(info_));
info_.hStdInput = INVALID_HANDLE_VALUE;
info_.hStdOutput = INVALID_HANDLE_VALUE;
info_.hStdError = INVALID_HANDLE_VALUE;
info_.cb = sizeof(info_);
}
ScopedStartupInfo::ScopedStartupInfo(const wchar_t* desktop)
: ScopedStartupInfo() {
DCHECK(desktop);
desktop_.assign(desktop);
info_.lpDesktop = const_cast<wchar_t*>(desktop_.c_str());
}
ScopedStartupInfo::~ScopedStartupInfo() {
Shutdown();
}
HRESULT ScopedStartupInfo::SetStdHandles(base::win::ScopedHandle* hstdin,
base::win::ScopedHandle* hstdout,
base::win::ScopedHandle* hstderr) {
if ((info_.dwFlags & STARTF_USESTDHANDLES) == STARTF_USESTDHANDLES) {
LOGFN(ERROR) << "Already set";
return E_UNEXPECTED;
}
// CreateProcessWithTokenW will fail if any of the std handles provided are
// invalid and the STARTF_USESTDHANDLES flag is set. So supply the default
// standard handle if no handle is given for some of the handles. This tells
// the process it can create its own local handles for these pipes as needed.
info_.dwFlags |= STARTF_USESTDHANDLES;
if (hstdin && hstdin->IsValid()) {
info_.hStdInput = hstdin->Take();
} else {
info_.hStdInput = ::GetStdHandle(STD_INPUT_HANDLE);
}
if (hstdout && hstdout->IsValid()) {
info_.hStdOutput = hstdout->Take();
} else {
info_.hStdOutput = ::GetStdHandle(STD_OUTPUT_HANDLE);
}
if (hstderr && hstderr->IsValid()) {
info_.hStdError = hstderr->Take();
} else {
info_.hStdError = ::GetStdHandle(STD_ERROR_HANDLE);
}
return S_OK;
}
void ScopedStartupInfo::Shutdown() {
if ((info_.dwFlags & STARTF_USESTDHANDLES) == STARTF_USESTDHANDLES) {
info_.dwFlags &= ~STARTF_USESTDHANDLES;
if (info_.hStdInput != ::GetStdHandle(STD_INPUT_HANDLE))
::CloseHandle(info_.hStdInput);
if (info_.hStdOutput != ::GetStdHandle(STD_OUTPUT_HANDLE))
::CloseHandle(info_.hStdOutput);
if (info_.hStdError != ::GetStdHandle(STD_ERROR_HANDLE))
::CloseHandle(info_.hStdError);
info_.hStdInput = INVALID_HANDLE_VALUE;
info_.hStdOutput = INVALID_HANDLE_VALUE;
info_.hStdError = INVALID_HANDLE_VALUE;
}
}
// Waits for a process to terminate while capturing output from |output_handle|
// to the buffer |output_buffer| of size |buffer_size|. The buffer is expected
// to be relatively small. The exit code of the process is written to
// |exit_code|.
HRESULT WaitForProcess(base::win::ScopedHandle::Handle process_handle,
const StdParentHandles& parent_handles,
DWORD* exit_code,
char* output_buffer,
int buffer_size) {
LOGFN(INFO);
DCHECK(exit_code);
DCHECK_GT(buffer_size, 0);
output_buffer[0] = 0;
HANDLE output_handle = parent_handles.hstdout_read.Get();
for (bool is_done = false; !is_done;) {
char buffer[80];
DWORD length = base::size(buffer) - 1;
HRESULT hr = S_OK;
const DWORD kThreeMinutesInMs = 3 * 60 * 1000;
DWORD ret = ::WaitForSingleObject(output_handle,
kThreeMinutesInMs); // timeout ms
switch (ret) {
case WAIT_OBJECT_0: {
int index = ret - WAIT_OBJECT_0;
LOGFN(INFO) << "WAIT_OBJECT_" << index;
if (!::ReadFile(output_handle, buffer, length, &length, nullptr)) {
hr = HRESULT_FROM_WIN32(::GetLastError());
if (hr != HRESULT_FROM_WIN32(ERROR_BROKEN_PIPE))
LOGFN(ERROR) << "ReadFile(" << index << ") hr=" << putHR(hr);
} else {
LOGFN(INFO) << "ReadFile(" << index << ") length=" << length;
buffer[length] = 0;
}
break;
}
case WAIT_IO_COMPLETION:
// This is normal. Just ignore.
LOGFN(INFO) << "WaitForMultipleObjectsEx WAIT_IO_COMPLETION";
break;
case WAIT_TIMEOUT: {
// User took too long to log in, so kill UI process.
LOGFN(INFO) << "WaitForMultipleObjectsEx WAIT_TIMEOUT, killing UI";
::TerminateProcess(process_handle, kUiecTimeout);
is_done = true;
break;
}
case WAIT_FAILED:
default: {
HRESULT hr = HRESULT_FROM_WIN32(::GetLastError());
LOGFN(ERROR) << "WaitForMultipleObjectsEx hr=" << putHR(hr);
is_done = true;
break;
}
}
// Copy the read buffer to the output buffer. If the pipe was broken,
// we can break our loop and wait for the process to die.
if (hr == HRESULT_FROM_WIN32(ERROR_BROKEN_PIPE)) {
LOGFN(INFO) << "Stop waiting for output buffer";
break;
} else {
strcat_s(output_buffer, buffer_size, buffer);
}
}
// At this point both stdout and stderr have been closed. Wait on the process
// handle for the process to terminate, getting the exit code. If the
// process does not terminate gracefully, kill it before returning.
DWORD ret = ::WaitForSingleObject(process_handle, 10000);
if (ret == 0) {
if (::GetExitCodeProcess(process_handle, exit_code)) {
LOGFN(INFO) << "Process terminated with exit code " << *exit_code;
} else {
LOGFN(INFO) << "Process terminated without exit code";
*exit_code = kUiecAbort;
}
} else {
LOGFN(INFO) << "UI did not terminiate within 10 seconds, killing now";
::TerminateProcess(process_handle, kUiecKilled);
*exit_code = kUiecKilled;
}
return S_OK;
}
HRESULT CreateLogonToken(const wchar_t* username,
const wchar_t* password,
bool interactive,
base::win::ScopedHandle* token) {
DCHECK(username);
DCHECK(password);
DCHECK(token);
DWORD logon_type =
interactive ? LOGON32_LOGON_INTERACTIVE : LOGON32_LOGON_BATCH;
base::win::ScopedHandle::Handle handle;
if (!::LogonUserW(username, L".", password, logon_type,
LOGON32_PROVIDER_DEFAULT, &handle)) {
HRESULT hr = HRESULT_FROM_WIN32(::GetLastError());
LOGFN(ERROR) << "LogonUserW hr=" << putHR(hr);
return hr;
}
base::win::ScopedHandle primary_token(handle);
if (!::CreateRestrictedToken(primary_token.Get(), DISABLE_MAX_PRIVILEGE, 0,
nullptr, 0, nullptr, 0, nullptr, &handle)) {
HRESULT hr = HRESULT_FROM_WIN32(::GetLastError());
LOGFN(ERROR) << "CreateRestrictedToken hr=" << putHR(hr);
return hr;
}
token->Set(handle);
return S_OK;
}
HRESULT CreateJobForSignin(base::win::ScopedHandle* job) {
LOGFN(INFO);
DCHECK(job);
job->Set(::CreateJobObject(nullptr, nullptr));
if (!job->IsValid()) {
HRESULT hr = HRESULT_FROM_WIN32(::GetLastError());
LOGFN(ERROR) << "CreateJobObject hr=" << putHR(hr);
return hr;
}
JOBOBJECT_BASIC_UI_RESTRICTIONS ui;
ui.UIRestrictionsClass =
JOB_OBJECT_UILIMIT_DESKTOP | // Create/switch desktops.
JOB_OBJECT_UILIMIT_HANDLES | // Only access own handles.
JOB_OBJECT_UILIMIT_SYSTEMPARAMETERS | // Cannot set sys params.
JOB_OBJECT_UILIMIT_WRITECLIPBOARD; // Cannot write to clipboard.
if (!::SetInformationJobObject(job->Get(), JobObjectBasicUIRestrictions, &ui,
sizeof(ui))) {
HRESULT hr = HRESULT_FROM_WIN32(::GetLastError());
LOGFN(ERROR) << "SetInformationJobObject hr=" << putHR(hr);
return hr;
}
return S_OK;
}
HRESULT CreatePipeForChildProcess(bool child_reads,
bool use_nul,
base::win::ScopedHandle* reading,
base::win::ScopedHandle* writing) {
// Make sure that all handles created here are inheritable. It is important
// that the child side handle is inherited.
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.bInheritHandle = TRUE;
sa.lpSecurityDescriptor = nullptr;
if (use_nul) {
base::win::ScopedHandle h(
::CreateFileW(L"nul:", FILE_GENERIC_READ | FILE_GENERIC_WRITE,
FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
&sa, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr));
if (!h.IsValid()) {
HRESULT hr = HRESULT_FROM_WIN32(::GetLastError());
LOGFN(ERROR) << "CreateFile(nul) hr=" << putHR(hr);
return hr;
}
if (child_reads) {
reading->Set(h.Take());
} else {
writing->Set(h.Take());
}
} else {
base::win::ScopedHandle::Handle temp_handle1;
base::win::ScopedHandle::Handle temp_handle2;
if (!::CreatePipe(&temp_handle1, &temp_handle2, &sa, 0)) {
HRESULT hr = HRESULT_FROM_WIN32(::GetLastError());
LOGFN(ERROR) << "CreatePipe(reading) hr=" << putHR(hr);
return hr;
}
reading->Set(temp_handle1);
writing->Set(temp_handle2);
// Make sure parent side is not inherited.
if (!::SetHandleInformation(child_reads ? writing->Get() : reading->Get(),
HANDLE_FLAG_INHERIT, 0)) {
HRESULT hr = HRESULT_FROM_WIN32(::GetLastError());
LOGFN(ERROR) << "SetHandleInformation(parent) hr=" << putHR(hr);
return hr;
}
}
return S_OK;
}
HRESULT InitializeStdHandles(CommDirection direction,
StdHandlesToCreate to_create,
ScopedStartupInfo* startupinfo,
StdParentHandles* parent_handles) {
LOGFN(INFO);
DCHECK(startupinfo);
DCHECK(parent_handles);
base::win::ScopedHandle hstdin_read;
base::win::ScopedHandle hstdin_write;
if ((to_create & kStdInput) != 0) {
HRESULT hr = CreatePipeForChildProcess(
true, // child reads
direction == CommDirection::kChildToParentOnly, // use nul
&hstdin_read, &hstdin_write);
if (FAILED(hr)) {
LOGFN(ERROR) << "CreatePipeForChildProcess(stdin) hr=" << putHR(hr);
return hr;
}
}
base::win::ScopedHandle hstdout_read;
base::win::ScopedHandle hstdout_write;
if ((to_create & kStdOutput) != 0) {
HRESULT hr = CreatePipeForChildProcess(
false, // child reads
direction == CommDirection::kParentToChildOnly, // use nul
&hstdout_read, &hstdout_write);
if (FAILED(hr)) {
LOGFN(ERROR) << "CreatePipeForChildProcess(stdout) hr=" << putHR(hr);
return hr;
}
}
base::win::ScopedHandle hstderr_read;
base::win::ScopedHandle hstderr_write;
if ((to_create & kStdError) != 0) {
HRESULT hr = CreatePipeForChildProcess(
false, // child reads
direction == CommDirection::kParentToChildOnly, // use nul
&hstderr_read, &hstderr_write);
if (FAILED(hr)) {
LOGFN(ERROR) << "CreatePipeForChildProcess(stderr) hr=" << putHR(hr);
return hr;
}
}
HRESULT hr =
startupinfo->SetStdHandles(&hstdin_read, &hstdout_write, &hstderr_write);
if (FAILED(hr)) {
LOGFN(ERROR) << "startupinfo->SetStdHandles hr=" << putHR(hr);
return hr;
}
parent_handles->hstdin_write.Set(hstdin_write.Take());
parent_handles->hstdout_read.Set(hstdout_read.Take());
parent_handles->hstderr_read.Set(hstderr_read.Take());
return S_OK;
}
HRESULT GetPathToDllFromHandle(HINSTANCE dll_handle,
base::FilePath* path_to_dll) {
wchar_t path[MAX_PATH];
DWORD length = base::size(path);
length = ::GetModuleFileName(dll_handle, path, length);
if (length == 0 || length >= base::size(path)) {
HRESULT hr = HRESULT_FROM_WIN32(::GetLastError());
LOGFN(ERROR) << "GetModuleFileNameW hr=" << putHR(hr);
return hr;
}
*path_to_dll = base::FilePath(base::StringPiece16(path, length));
return S_OK;
}
HRESULT GetEntryPointArgumentForRunDll(HINSTANCE dll_handle,
const wchar_t* entrypoint,
base::string16* entrypoint_arg) {
DCHECK(entrypoint);
DCHECK(entrypoint_arg);
entrypoint_arg->clear();
// rundll32 expects the first command line argument to be the path to the
// DLL, followed by a comma and the name of the function to call. There can
// be no spaces around the comma. There can be no spaces in the path. It
// is recommended to use the short path name of the DLL.
base::FilePath path_to_dll;
HRESULT hr = GetPathToDllFromHandle(dll_handle, &path_to_dll);
if (FAILED(hr))
return hr;
wchar_t short_path[MAX_PATH];
DWORD short_length = base::size(short_path);
short_length =
::GetShortPathName(path_to_dll.value().c_str(), short_path, short_length);
if (short_length >= base::size(short_path)) {
HRESULT hr = HRESULT_FROM_WIN32(::GetLastError());
LOGFN(ERROR) << "GetShortPathNameW hr=" << putHR(hr);
return hr;
}
*entrypoint_arg =
base::string16(base::StringPrintf(L"%ls,%ls", short_path, entrypoint));
// In tests, the current module is the unittest exe, not the real dll.
// The unittest exe does not expose entrypoints, so return S_FALSE as a hint
// that this will not work. The command line is built anyway though so
// tests of the command line construction can be written.
return wcsicmp(wcsrchr(path_to_dll.value().c_str(), L'.'), L".dll") == 0
? S_OK
: S_FALSE;
}
HRESULT GetCommandLineForEntrypoint(HINSTANCE dll_handle,
const wchar_t* entrypoint,
base::CommandLine* command_line) {
DCHECK(entrypoint);
DCHECK(command_line);
// Build the full path to rundll32.
base::FilePath system_dir;
if (!base::PathService::Get(base::DIR_SYSTEM, &system_dir))
return HRESULT_FROM_WIN32(::GetLastError());
command_line->SetProgram(
system_dir.Append(FILE_PATH_LITERAL("rundll32.exe")));
base::string16 entrypoint_arg;
HRESULT hr =
GetEntryPointArgumentForRunDll(dll_handle, entrypoint, &entrypoint_arg);
if (SUCCEEDED(hr))
command_line->AppendArgNative(entrypoint_arg);
return hr;
}
// Gets the serial number of the machine based on the recipe found at
// https://docs.microsoft.com/en-us/windows/desktop/WmiSdk/example-creating-a-wmi-application
HRESULT GetMachineSerialNumber(base::string16* serial_number) {
USES_CONVERSION;
DCHECK(serial_number);
serial_number->clear();
// Make sure COM is initialized.
HRESULT hr = ::CoInitializeEx(nullptr, COINIT_MULTITHREADED);
if (FAILED(hr)) {
LOGFN(ERROR) << "CoInitializeEx hr=" << putHR(hr);
return hr;
}
hr = ::CoInitializeSecurity(
nullptr, -1, nullptr, nullptr, RPC_C_AUTHN_LEVEL_DEFAULT,
RPC_C_IMP_LEVEL_IMPERSONATE, nullptr, EOAC_NONE, nullptr);
if (FAILED(hr)) {
LOGFN(ERROR) << "CoInitializeSecurity hr=" << putHR(hr);
return hr;
}
CComPtr<IWbemLocator> locator;
hr = locator.CoCreateInstance(CLSID_WbemLocator, 0, CLSCTX_INPROC_SERVER);
if (FAILED(hr)) {
LOGFN(ERROR) << "CoCreateInstance(CLSID_WbemLocator) hr=" << putHR(hr);
return hr;
}
CComPtr<IWbemServices> services;
hr = locator->ConnectServer(CComBSTR(W2COLE(L"ROOT\\CIMV2")), nullptr,
nullptr, nullptr, 0, nullptr, nullptr, &services);
if (FAILED(hr)) {
LOGFN(ERROR) << "locator->ConnectServer hr=" << putHR(hr);
return hr;
}
CComPtr<IEnumWbemClassObject> enum_wbem;
hr = services->ExecQuery(CComBSTR(W2COLE(L"WQL")),
CComBSTR(W2COLE(L"select * from Win32_Bios")),
WBEM_FLAG_FORWARD_ONLY, nullptr, &enum_wbem);
if (FAILED(hr)) {
LOGFN(ERROR) << "services->ExecQuery hr=" << putHR(hr);
return hr;
}
while (SUCCEEDED(hr) && serial_number->empty()) {
CComPtr<IWbemClassObject> class_obj;
ULONG count = 1;
hr = enum_wbem->Next(WBEM_INFINITE, 1, &class_obj, &count);
if (count == 0)
break;
VARIANT var;
hr = class_obj->Get(L"SerialNumber", 0, &var, 0, 0);
if (SUCCEEDED(hr) && var.vt == VT_BSTR)
serial_number->assign(OLE2CW(var.bstrVal));
VariantClear(&var);
}
LOGFN(INFO) << "GetMachineSerialNumber sn=" << *serial_number
<< " hr=" << putHR(hr);
return hr;
}
HRESULT EnrollToGoogleMdmIfNeeded(const base::DictionaryValue& properties) {
USES_CONVERSION;
LOGFN(INFO);
// Enroll to Google MDM if not already enrolled.
HRESULT hr = E_FAIL;
BOOL is_registered = base::win::IsDeviceRegisteredWithManagement();
LOGFN(INFO) << "MDM is_registered=" << is_registered;
if (!is_registered) {
base::string16 email = GetDictString(&properties, kKeyEmail);
base::string16 token = GetDictString(&properties, kKeyMdmIdToken);
base::string16 mdm_url = GetDictString(&properties, kKeyMdmUrl);
if (email.empty()) {
LOGFN(ERROR) << "Email is empty";
return E_INVALIDARG;
}
if (token.empty()) {
LOGFN(ERROR) << "MDM id token is empty";
return E_INVALIDARG;
}
if (mdm_url.empty()) {
LOGFN(ERROR) << "No MDM URL specified";
return E_INVALIDARG;
}
LOGFN(INFO) << "MDM_URL=" << mdm_url
<< " token=" << base::string16(token.c_str(), 10);
// Build the json data needed by the server.
base::DictionaryValue registration_data;
base::string16 serial_number;
hr = GetMachineSerialNumber(&serial_number);
if (FAILED(hr)) {
LOGFN(ERROR) << "GetMachineSerialNumber hr=" << putHR(hr);
return hr;
}
registration_data.SetString("serial_number", serial_number);
registration_data.SetString("id_token", token);
std::string registration_data_str;
if (!base::JSONWriter::Write(registration_data, &registration_data_str)) {
LOGFN(ERROR) << "JSONWriter::Write(registration_data)";
return E_FAIL;
}
hr = RegisterWithGoogleDeviceManagement(mdm_url, email,
registration_data_str);
LOGFN(INFO) << "RegisterWithGoogleDeviceManagement hr=" << putHR(hr);
}
return hr;
}
HRESULT GetAuthenticationPackageId(ULONG* id) {
DCHECK(id);
HANDLE lsa;
NTSTATUS status = ::LsaConnectUntrusted(&lsa);
HRESULT hr = HRESULT_FROM_NT(status);
if (FAILED(hr)) {
LOGFN(ERROR) << "LsaConnectUntrusted hr=" << putHR(hr);
return hr;
}
LSA_STRING name;
name.Buffer = const_cast<PCHAR>(NEGOSSP_NAME_A);
name.Length = static_cast<USHORT>(strlen(name.Buffer));
name.MaximumLength = name.Length + 1;
status = ::LsaLookupAuthenticationPackage(lsa, &name, id);
::LsaDeregisterLogonProcess(lsa);
hr = HRESULT_FROM_NT(status);
if (FAILED(hr))
LOGFN(ERROR) << "LsaLookupAuthenticationPackage hr=" << putHR(hr);
return hr;
}
base::string16 GetStringResource(UINT id) {
// When LoadStringW receives 0 as the fourth argument (buffer length), it
// assumes the third argument (buffer) is a pointer. The returned pointer
// is still owned by the system and must not be freed. Furthermore the string
// pointed at is not null terminated, so the returned length must be used to
// construct the final base::string16.
wchar_t* str;
int length =
::LoadStringW(CURRENT_MODULE(), id, reinterpret_cast<wchar_t*>(&str), 0);
return base::string16(str, length);
}
base::string16 GetDictString(const base::DictionaryValue* dict,
const char* name) {
DCHECK(name);
auto* value = dict->FindKey(name);
return value && value->is_string() ? base::UTF8ToUTF16(value->GetString())
: base::string16();
}
base::string16 GetDictString(const std::unique_ptr<base::DictionaryValue>& dict,
const char* name) {
return GetDictString(dict.get(), name);
}
std::string GetDictStringUTF8(const base::DictionaryValue* dict,
const char* name) {
DCHECK(name);
auto* value = dict->FindKey(name);
return value && value->is_string() ? value->GetString() : std::string();
}
std::string GetDictStringUTF8(
const std::unique_ptr<base::DictionaryValue>& dict,
const char* name) {
return GetDictStringUTF8(dict.get(), name);
}
FakesForTesting::FakesForTesting() {}
FakesForTesting::~FakesForTesting() {}
} // namespace credential_provider