chromium/chrome/credential_provider/gaiacp/auth_utils.cc

// 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(&copy_password[0], cpus,
                                         &protected_password);

  // Zero out the unencrypted copy of the password.
  SecurelyClearBuffer(&copy_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