// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif
// Implementation of Windows authentication package building functions need
// to create the authentication packages used to sign the user into a Windows
// system.
#include "chrome/credential_provider/gaiacp/auth_utils.h"
#include <vector>
#include "base/functional/callback.h"
#include "base/strings/string_util.h"
#include "chrome/credential_provider/gaiacp/gcp_utils.h"
#include "chrome/credential_provider/gaiacp/logging.h"
#include "chrome/credential_provider/gaiacp/os_user_manager.h"
#include "third_party/abseil-cpp/absl/cleanup/cleanup.h"
namespace credential_provider {
namespace {
// If |password| should be encrypted, fills |protected_password| with a copy
// encrypted with CredProtect, otherwise just copies |password| to
// |protected_password|.
//
// CredProtectW and CredIsProtectedW expect a non-const password so |password|
// is passed in as a non-const pointer.
HRESULT ProtectIfNecessaryAndCopyPassword(
wchar_t* password,
CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus,
std::vector<wchar_t>* protected_password) {
DCHECK(protected_password);
DCHECK(password);
protected_password->clear();
if (cpus != CPUS_UNLOCK_WORKSTATION && cpus != CPUS_LOGON)
return E_UNEXPECTED;
DWORD password_length = static_cast<DWORD>(wcslen(password));
// ProtectAndCopyString is intended for non-empty strings only. Empty
// passwords do not need to be encrypted.
if (!password_length)
return S_OK;
CRED_PROTECTION_TYPE protection_type;
// Determine if password can be encrypted.
// Encryption should be skipped if the password is already encrypted.
// An encrypted password may be received through SetSerialization in the
// CPUS_LOGON scenario during a Terminal Services connection, for
// instance.
if ((!::CredIsProtectedW(password, &protection_type) ||
CredUnprotected != protection_type)) {
protected_password->resize(password_length + 1);
wcscpy_s(&(*protected_password)[0], password_length + 1, password);
return S_OK;
}
// Determine the size of the buffer needed to generate the protected string.
//
// Note that the third parameter to CredProtect, the number of characters of
// password to encrypt, must include the NULL terminator.
DWORD needed_length = 0;
if (::CredProtectW(FALSE, password, password_length + 1, nullptr,
&needed_length, nullptr)) {
LOGFN(ERROR) << "Failed to get required length of protected string.";
return E_UNEXPECTED;
}
DWORD last_error = GetLastError();
if ((ERROR_INSUFFICIENT_BUFFER != last_error) || (0 == needed_length)) {
LOGFN(ERROR) << "Unexpected error when getting length of proceted string.";
return HRESULT_FROM_WIN32(last_error);
}
protected_password->resize(needed_length);
if (!::CredProtectW(FALSE, password, password_length + 1,
&(*protected_password)[0], &needed_length, nullptr)) {
LOGFN(ERROR) << "Failed to protect string.";
protected_password->clear();
return HRESULT_FROM_WIN32(::GetLastError());
}
return S_OK;
}
// The following function is intended to be used ONLY with the Kerb*Pack
// functions. It does no bounds-checking because its callers have precise
// requirements and are written to respect its limitations. You can read more
// about the UNICODE_STRING type at:
// https://docs.microsoft.com/en-us/windows/desktop/api/subauth/ns-subauth-_unicode_string
void UnicodeStringPackedUnicodeStringCopy(const UNICODE_STRING& rus,
wchar_t* buffer,
UNICODE_STRING* pus) {
pus->Length = rus.Length;
pus->MaximumLength = rus.Length;
pus->Buffer = buffer;
CopyMemory(pus->Buffer, rus.Buffer, pus->Length);
}
// Initialize the members of a KERB_INTERACTIVE_UNLOCK_LOGON with weak
// references to the passed-in strings. Later KerbInteractiveUnlockLogonPack
// will be used to serialize the structure so the strings will not need to
// be copied.
//
// The password is stored in encrypted form for CPUS_LOGON and
// CPUS_UNLOCK_WORKSTATION because the system can accept encrypted credentials.
// For all other scenarios, this function will simply fail.
void KerbInteractiveUnlockLogonInit(wchar_t* domain,
wchar_t* username,
wchar_t* password,
CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus,
KERB_INTERACTIVE_UNLOCK_LOGON* pkiul) {
DCHECK(domain);
DCHECK(username);
DCHECK(password);
DCHECK(pkiul);
ZeroMemory(pkiul, sizeof(KERB_INTERACTIVE_UNLOCK_LOGON));
KERB_INTERACTIVE_LOGON* pkil = &pkiul->Logon;
// Note: this method uses custom logic to pack a KERB_INTERACTIVE_UNLOCK_LOGON
// with a serialized credential. We could replace the calls to
// InitWindowsStringWithString and KerbInteractiveUnlockLogonPack with a
// single call to CredPackAuthenticationBuffer, but that API has a drawback:
// it returns a KERB_INTERACTIVE_UNLOCK_LOGON whose MessageType is always
// KerbInteractiveLogon.
//
// If we only handled CPUS_LOGON, this drawback would not be a problem. For
// CPUS_UNLOCK_WORKSTATION, we could cast the output buffer of
// CredPackAuthenticationBuffer to KERB_INTERACTIVE_UNLOCK_LOGON and modify
// the MessageType to KerbWorkstationUnlockLogon, but such a cast would be
// unsupported -- the output format of CredPackAuthenticationBuffer is not
// officially documented.
// Set a MessageType based on the usage scenario.
switch (cpus) {
case CPUS_UNLOCK_WORKSTATION:
pkil->MessageType = KerbWorkstationUnlockLogon;
break;
case CPUS_LOGON:
pkil->MessageType = KerbInteractiveLogon;
break;
default:
NOTREACHED_IN_MIGRATION();
return;
}
// Initialize the UNICODE_STRINGS to share domain, username and password
// strings.
InitWindowsStringWithString(domain, &pkil->LogonDomainName);
InitWindowsStringWithString(username, &pkil->UserName);
InitWindowsStringWithString(password, &pkil->Password);
}
// WinLogon and LSA consume "packed" KERB_INTERACTIVE_UNLOCK_LOGONs. In these,
// the PWSTR members of each UNICODE_STRING are not actually pointers but byte
// offsets into the overall buffer represented by the packed
// KERB_INTERACTIVE_UNLOCK_LOGON. For example:
//
// Length is in bytes, not characters
// input_logon.Logon.LogonDomainName.Length = 14;
// LogonDomainName begins immediately after the KERB_... struct in the buffer
// input_logon.Logon.LogonDomainName.Buffer =
// sizeof(KERB_INTERACTIVE_UNLOCK_LOGON);
// input_logon.Logon.UserName.Length = 10
// input_logon.Logon.UserName.Buffer = sizeof(KERB_INTERACTIVE_UNLOCK_LOGON) +
// 14 -> UNICODE_STRINGS are NOT null-terminated
//
// input_logon.Logon.Password.Length = 16
// input_logon.Logon.Password.Buffer = sizeof(KERB_INTERACTIVE_UNLOCK_LOGON) +
// 14 + 10
HRESULT KerbInteractiveUnlockLogonPack(
const KERB_INTERACTIVE_UNLOCK_LOGON& input_unlock_logon,
BYTE** prgb,
DWORD* pcb) {
DCHECK(prgb);
DCHECK(pcb);
const KERB_INTERACTIVE_LOGON* input_logon = &input_unlock_logon.Logon;
// Allocate space for struct plus extra for the three strings.
DWORD cb = sizeof(input_unlock_logon) + input_logon->LogonDomainName.Length +
input_logon->UserName.Length + input_logon->Password.Length;
KERB_INTERACTIVE_UNLOCK_LOGON* output_unlock_logon =
reinterpret_cast<KERB_INTERACTIVE_UNLOCK_LOGON*>(::CoTaskMemAlloc(cb));
if (!output_unlock_logon) {
LOGFN(ERROR) << "Failed to allocate kerberos logon package.";
return E_OUTOFMEMORY;
}
::SecureZeroMemory(&output_unlock_logon->LogonId,
sizeof(output_unlock_logon->LogonId));
// Point output_buffer at the beginning of the extra space.
BYTE* output_buffer = reinterpret_cast<BYTE*>(output_unlock_logon) +
sizeof(*output_unlock_logon);
// Set up the Logon structure within the KERB_INTERACTIVE_UNLOCK_LOGON.
KERB_INTERACTIVE_LOGON* output_logon = &output_unlock_logon->Logon;
output_logon->MessageType = input_logon->MessageType;
// For each string: fix up appropriate buffer pointer to be offset, advance
// buffer pointer over copied characters in extra space.
UnicodeStringPackedUnicodeStringCopy(input_logon->LogonDomainName,
(wchar_t*)output_buffer,
&output_logon->LogonDomainName);
output_logon->LogonDomainName.Buffer =
reinterpret_cast<wchar_t*>(output_buffer - (BYTE*)output_unlock_logon);
output_buffer += output_logon->LogonDomainName.Length;
UnicodeStringPackedUnicodeStringCopy(
input_logon->UserName, reinterpret_cast<wchar_t*>(output_buffer),
&output_logon->UserName);
output_logon->UserName.Buffer =
reinterpret_cast<wchar_t*>(output_buffer - (BYTE*)output_unlock_logon);
output_buffer += output_logon->UserName.Length;
UnicodeStringPackedUnicodeStringCopy(
input_logon->Password, reinterpret_cast<wchar_t*>(output_buffer),
&output_logon->Password);
output_logon->Password.Buffer =
reinterpret_cast<wchar_t*>(output_buffer - (BYTE*)output_unlock_logon);
*prgb = (BYTE*)output_unlock_logon;
*pcb = cb;
return S_OK;
}
HRESULT UnpackUserInfoFromAuthenticationBuffer(
const CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION* cpcs,
std::wstring* domain,
std::wstring* username) {
DCHECK(cpcs);
DCHECK(domain);
DCHECK(username);
domain->clear();
username->clear();
KERB_INTERACTIVE_UNLOCK_LOGON* pkiul =
reinterpret_cast<KERB_INTERACTIVE_UNLOCK_LOGON*>(cpcs->rgbSerialization);
ULONG buffer_size = cpcs->cbSerialization;
KERB_INTERACTIVE_LOGON* pkil = &pkiul->Logon;
std::wstring serialization_domain;
std::wstring serialization_username;
// Check to see if the buffer is packed:
// 1. Ensure that the buffer can possibly contain the serialization.
// 2. Also if the range described by each (Buffer + MaximumSize) falls
// within the total bytecount, we can be pretty confident that the Buffers
// are actually offsets and that this is a packed credential.
if (sizeof(*pkiul) <= buffer_size &&
(reinterpret_cast<ptrdiff_t>(pkil->LogonDomainName.Buffer) +
pkil->LogonDomainName.MaximumLength <=
static_cast<ptrdiff_t>(buffer_size)) &&
(reinterpret_cast<ptrdiff_t>(pkil->UserName.Buffer) +
pkil->UserName.MaximumLength <=
static_cast<ptrdiff_t>(buffer_size)) &&
(reinterpret_cast<ptrdiff_t>(pkil->Password.Buffer) +
pkil->Password.MaximumLength <=
static_cast<ptrdiff_t>(buffer_size))) {
// When the authentication package is packed, then each buffer is not
// null terminated and the next buffer starts at the end of the previous
// one. Also the buffers are stored in the order that they are declared
// in the structure.
if (pkil->LogonDomainName.Buffer && pkil->UserName.Buffer) {
const wchar_t* domain_buffer_pos = reinterpret_cast<wchar_t*>(
(reinterpret_cast<ptrdiff_t>(pkiul) +
reinterpret_cast<ptrdiff_t>(pkil->LogonDomainName.Buffer)));
const wchar_t* username_buffer_pos = reinterpret_cast<wchar_t*>(
(reinterpret_cast<ptrdiff_t>(pkiul) +
reinterpret_cast<ptrdiff_t>(pkil->UserName.Buffer)));
serialization_domain =
std::wstring(domain_buffer_pos, pkil->LogonDomainName.MaximumLength /
sizeof(domain_buffer_pos[0]));
serialization_username =
std::wstring(username_buffer_pos, pkil->UserName.MaximumLength /
sizeof(username_buffer_pos[0]));
}
} else {
// If the authentication package is not packed, assume that the buffer
// points to a null terminated string.
serialization_domain = pkiul->Logon.LogonDomainName.Buffer;
serialization_username = pkiul->Logon.UserName.Buffer;
}
if (serialization_domain.length() && serialization_username.length()) {
*domain = serialization_domain;
*username = serialization_username;
return S_OK;
}
return E_FAIL;
}
} // namespace
// Gets the auth package id for NEGOSSP_NAME_A.
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;
InitWindowsStringWithString(NEGOSSP_NAME_A, &name);
status = ::LsaLookupAuthenticationPackage(lsa, &name, id);
::LsaDeregisterLogonProcess(lsa);
hr = HRESULT_FROM_NT(status);
if (FAILED(hr))
LOGFN(ERROR) << "LsaLookupAuthenticationPackage hr=" << putHR(hr);
return hr;
}
HRESULT DetermineUserSidFromAuthenticationBuffer(
const CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION* cpcs,
std::wstring* sid) {
DCHECK(sid);
sid->clear();
std::wstring serialization_domain;
std::wstring serialization_username;
HRESULT hr = UnpackUserInfoFromAuthenticationBuffer(
cpcs, &serialization_domain, &serialization_username);
if (SUCCEEDED(hr)) {
hr = OSUserManager::Get()->GetUserSID(serialization_domain.c_str(),
serialization_username.c_str(), sid);
// The function GetUserSID strictly checks the domain that is passed in and
// determines if a user really exists in that domain. However the behavior
// of RDP connections is more lenient. You can enter any domain you want
// when connecting and if a user in that specific domain is not found, RDP
// will fall back to trying to sign in the local user with the same
// username. To emulate that behavior, we try to also check if there is a
// user on the local domain we could possibly signin and return the SID for
// that user if it exists.
if (FAILED(hr)) {
std::wstring local_domain = OSUserManager::GetLocalDomain();
if (!base::EqualsCaseInsensitiveASCII(local_domain,
serialization_domain)) {
hr = OSUserManager::Get()->GetUserSID(
local_domain.c_str(), serialization_username.c_str(), sid);
if (FAILED(hr)) {
LOGFN(ERROR) << "GetUserSID hr=" << putHR(hr);
return hr;
}
}
}
}
return S_OK;
}
HRESULT BuildCredPackAuthenticationBuffer(
BSTR domain,
BSTR username,
BSTR password,
CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus,
CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION* cpcs) {
DCHECK(username);
DCHECK(password);
DCHECK(cpcs);
HRESULT hr = GetAuthenticationPackageId(&(cpcs->ulAuthenticationPackage));
if (FAILED(hr)) {
LOGFN(ERROR) << "GetAuthenticationPackageId hr=" << putHR(hr);
return hr;
}
// Caller should fill this in.
cpcs->clsidCredentialProvider = GUID_NULL;
std::vector<wchar_t> protected_password;
// Copy the password and pass the copied buffer into
// ProtectIfNecessaryAndCopyPassword since it expects a non-const input
// buffer.
std::vector<wchar_t> copy_password(OLE2W(password),
OLE2W(password) + wcslen(password) + 1);
hr = ProtectIfNecessaryAndCopyPassword(©_password[0], cpus,
&protected_password);
// Zero out the unencrypted copy of the password.
SecurelyClearBuffer(©_password[0], copy_password.size());
if (FAILED(hr)) {
LOGFN(ERROR) << "ProtectIfNecessaryAndCopyPassword hr=" << putHR(hr);
return hr;
}
// Protected password may still be insecure so make sure to zero it out.
absl::Cleanup zero_buffer_on_exit = [&protected_password] {
SecurelyClearBuffer(protected_password.data(), protected_password.size());
};
wchar_t* logon_domain = domain;
// For domain joined users, use a Kerberos authentication package.
KERB_INTERACTIVE_UNLOCK_LOGON kiul;
KerbInteractiveUnlockLogonInit(OLE2W(logon_domain), OLE2W(username),
&protected_password[0], cpus, &kiul);
// Use KERB_INTERACTIVE_UNLOCK_LOGON in both unlock and logon
// scenarios. It contains a KERB_INTERACTIVE_LOGON to hold the creds
// plus a LUID that is filled in for us by Winlogon as necessary.
hr = KerbInteractiveUnlockLogonPack(kiul, &cpcs->rgbSerialization,
&cpcs->cbSerialization);
if (FAILED(hr)) {
hr = HRESULT_FROM_WIN32(::GetLastError());
LOGFN(ERROR) << "KerbInteractiveUnlockLogonPack user=" << username
<< " dn=" << logon_domain << " hr=" << putHR(hr)
<< " length=" << cpcs->cbSerialization;
return hr;
}
return S_OK;
}
} // namespace credential_provider