1 // Copyright 2018 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 // Implementation of Windows authentication package building functions need
6 // to create the authentication packages used to sign the user into a Windows
7 // system.
8 
9 #include <vector>
10 
11 #include "chrome/credential_provider/gaiacp/auth_utils.h"
12 
13 #include "base/callback.h"
14 #include "base/callback_helpers.h"
15 #include "base/stl_util.h"
16 #include "base/strings/string_util.h"
17 #include "chrome/credential_provider/gaiacp/gcp_utils.h"
18 #include "chrome/credential_provider/gaiacp/logging.h"
19 #include "chrome/credential_provider/gaiacp/os_user_manager.h"
20 
21 namespace credential_provider {
22 
23 namespace {
24 
25 // If |password| should be encrypted, fills |protected_password| with a copy
26 // encrypted with CredProtect, otherwise just copies |password| to
27 // |protected_password|.
28 //
29 // CredProtectW and CredIsProtectedW expect a non-const password so |password|
30 // is passed in as a non-const pointer.
ProtectIfNecessaryAndCopyPassword(wchar_t * password,CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus,std::vector<wchar_t> * protected_password)31 HRESULT ProtectIfNecessaryAndCopyPassword(
32     wchar_t* password,
33     CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus,
34     std::vector<wchar_t>* protected_password) {
35   DCHECK(protected_password);
36   DCHECK(password);
37 
38   protected_password->clear();
39 
40   if (cpus != CPUS_UNLOCK_WORKSTATION && cpus != CPUS_LOGON)
41     return E_UNEXPECTED;
42 
43   DWORD password_length = static_cast<DWORD>(wcslen(password));
44 
45   // ProtectAndCopyString is intended for non-empty strings only.  Empty
46   // passwords do not need to be encrypted.
47   if (!password_length)
48     return S_OK;
49 
50   CRED_PROTECTION_TYPE protection_type;
51   // Determine if password can be encrypted.
52   // Encryption should be skipped if the password is already encrypted.
53   // An encrypted password may be received through SetSerialization in the
54   // CPUS_LOGON scenario during a Terminal Services connection, for
55   // instance.
56   if ((!::CredIsProtectedW(password, &protection_type) ||
57        CredUnprotected != protection_type)) {
58     protected_password->resize(password_length + 1);
59     wcscpy_s(&(*protected_password)[0], password_length + 1, password);
60     return S_OK;
61   }
62 
63   // Determine the size of the buffer needed to generate the protected string.
64   //
65   // Note that the third parameter to CredProtect, the number of characters of
66   // password to encrypt, must include the NULL terminator.
67   DWORD needed_length = 0;
68   if (::CredProtectW(FALSE, password, password_length + 1, nullptr,
69                      &needed_length, nullptr)) {
70     LOGFN(ERROR) << "Failed to get required length of protected string.";
71     return E_UNEXPECTED;
72   }
73 
74   DWORD last_error = GetLastError();
75   if ((ERROR_INSUFFICIENT_BUFFER != last_error) || (0 == needed_length)) {
76     LOGFN(ERROR) << "Unexpected error when getting length of proceted string.";
77     return HRESULT_FROM_WIN32(last_error);
78   }
79 
80   protected_password->resize(needed_length);
81   if (!::CredProtectW(FALSE, password, password_length + 1,
82                       &(*protected_password)[0], &needed_length, nullptr)) {
83     LOGFN(ERROR) << "Failed to protect string.";
84     protected_password->clear();
85     return HRESULT_FROM_WIN32(::GetLastError());
86   }
87 
88   return S_OK;
89 }
90 
91 // The following function is intended to be used ONLY with the Kerb*Pack
92 // functions.  It does no bounds-checking because its callers have precise
93 // requirements and are written to respect its limitations. You can read more
94 // about the UNICODE_STRING type at:
95 // https://docs.microsoft.com/en-us/windows/desktop/api/subauth/ns-subauth-_unicode_string
UnicodeStringPackedUnicodeStringCopy(const UNICODE_STRING & rus,wchar_t * buffer,UNICODE_STRING * pus)96 void UnicodeStringPackedUnicodeStringCopy(const UNICODE_STRING& rus,
97                                           wchar_t* buffer,
98                                           UNICODE_STRING* pus) {
99   pus->Length = rus.Length;
100   pus->MaximumLength = rus.Length;
101   pus->Buffer = buffer;
102 
103   CopyMemory(pus->Buffer, rus.Buffer, pus->Length);
104 }
105 
106 // Initialize the members of a KERB_INTERACTIVE_UNLOCK_LOGON with weak
107 // references to the passed-in strings.  Later KerbInteractiveUnlockLogonPack
108 // will be used to serialize the structure so the strings will not need to
109 // be copied.
110 //
111 // The password is stored in encrypted form for CPUS_LOGON and
112 // CPUS_UNLOCK_WORKSTATION because the system can accept encrypted credentials.
113 // For all other scenarios, this function will simply fail.
KerbInteractiveUnlockLogonInit(wchar_t * domain,wchar_t * username,wchar_t * password,CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus,KERB_INTERACTIVE_UNLOCK_LOGON * pkiul)114 void KerbInteractiveUnlockLogonInit(wchar_t* domain,
115                                     wchar_t* username,
116                                     wchar_t* password,
117                                     CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus,
118                                     KERB_INTERACTIVE_UNLOCK_LOGON* pkiul) {
119   DCHECK(domain);
120   DCHECK(username);
121   DCHECK(password);
122   DCHECK(pkiul);
123 
124   ZeroMemory(pkiul, sizeof(KERB_INTERACTIVE_UNLOCK_LOGON));
125 
126   KERB_INTERACTIVE_LOGON* pkil = &pkiul->Logon;
127 
128   // Note: this method uses custom logic to pack a KERB_INTERACTIVE_UNLOCK_LOGON
129   // with a serialized credential.  We could replace the calls to
130   // InitWindowsStringWithString and KerbInteractiveUnlockLogonPack with a
131   // single call to CredPackAuthenticationBuffer, but that API has a drawback:
132   // it returns a KERB_INTERACTIVE_UNLOCK_LOGON whose MessageType is always
133   // KerbInteractiveLogon.
134   //
135   // If we only handled CPUS_LOGON, this drawback would not be a problem.  For
136   // CPUS_UNLOCK_WORKSTATION, we could cast the output buffer of
137   // CredPackAuthenticationBuffer to KERB_INTERACTIVE_UNLOCK_LOGON and modify
138   // the MessageType to KerbWorkstationUnlockLogon, but such a cast would be
139   // unsupported -- the output format of CredPackAuthenticationBuffer is not
140   // officially documented.
141 
142   // Set a MessageType based on the usage scenario.
143   switch (cpus) {
144     case CPUS_UNLOCK_WORKSTATION:
145       pkil->MessageType = KerbWorkstationUnlockLogon;
146       break;
147     case CPUS_LOGON:
148       pkil->MessageType = KerbInteractiveLogon;
149       break;
150     default:
151       NOTREACHED();
152       return;
153   }
154 
155   // Initialize the UNICODE_STRINGS to share domain, username and password
156   // strings.
157   InitWindowsStringWithString(domain, &pkil->LogonDomainName);
158   InitWindowsStringWithString(username, &pkil->UserName);
159   InitWindowsStringWithString(password, &pkil->Password);
160 }
161 
162 // WinLogon and LSA consume "packed" KERB_INTERACTIVE_UNLOCK_LOGONs.  In these,
163 // the PWSTR members of each UNICODE_STRING are not actually pointers but byte
164 // offsets into the overall buffer represented by the packed
165 // KERB_INTERACTIVE_UNLOCK_LOGON.  For example:
166 //
167 // Length is in bytes, not characters
168 // input_logon.Logon.LogonDomainName.Length = 14;
169 // LogonDomainName begins immediately after the KERB_... struct in the buffer
170 // input_logon.Logon.LogonDomainName.Buffer =
171 //      sizeof(KERB_INTERACTIVE_UNLOCK_LOGON);
172 // input_logon.Logon.UserName.Length = 10
173 // input_logon.Logon.UserName.Buffer = sizeof(KERB_INTERACTIVE_UNLOCK_LOGON) +
174 // 14 -> UNICODE_STRINGS are NOT null-terminated
175 //
176 // input_logon.Logon.Password.Length = 16
177 // input_logon.Logon.Password.Buffer = sizeof(KERB_INTERACTIVE_UNLOCK_LOGON) +
178 // 14 + 10
KerbInteractiveUnlockLogonPack(const KERB_INTERACTIVE_UNLOCK_LOGON & input_unlock_logon,BYTE ** prgb,DWORD * pcb)179 HRESULT KerbInteractiveUnlockLogonPack(
180     const KERB_INTERACTIVE_UNLOCK_LOGON& input_unlock_logon,
181     BYTE** prgb,
182     DWORD* pcb) {
183   DCHECK(prgb);
184   DCHECK(pcb);
185   const KERB_INTERACTIVE_LOGON* input_logon = &input_unlock_logon.Logon;
186 
187   // Allocate space for struct plus extra for the three strings.
188   DWORD cb = sizeof(input_unlock_logon) + input_logon->LogonDomainName.Length +
189              input_logon->UserName.Length + input_logon->Password.Length;
190 
191   KERB_INTERACTIVE_UNLOCK_LOGON* output_unlock_logon =
192       reinterpret_cast<KERB_INTERACTIVE_UNLOCK_LOGON*>(::CoTaskMemAlloc(cb));
193   if (!output_unlock_logon) {
194     LOGFN(ERROR) << "Failed to allocate kerberos logon package.";
195     return E_OUTOFMEMORY;
196   }
197 
198   ::SecureZeroMemory(&output_unlock_logon->LogonId,
199                      sizeof(output_unlock_logon->LogonId));
200 
201   // Point output_buffer at the beginning of the extra space.
202   BYTE* output_buffer = reinterpret_cast<BYTE*>(output_unlock_logon) +
203                         sizeof(*output_unlock_logon);
204 
205   // Set up the Logon structure within the KERB_INTERACTIVE_UNLOCK_LOGON.
206   KERB_INTERACTIVE_LOGON* output_logon = &output_unlock_logon->Logon;
207 
208   output_logon->MessageType = input_logon->MessageType;
209 
210   // For each string: fix up appropriate buffer pointer to be offset, advance
211   // buffer pointer over copied characters in extra space.
212   UnicodeStringPackedUnicodeStringCopy(input_logon->LogonDomainName,
213                                        (wchar_t*)output_buffer,
214                                        &output_logon->LogonDomainName);
215   output_logon->LogonDomainName.Buffer =
216       reinterpret_cast<wchar_t*>(output_buffer - (BYTE*)output_unlock_logon);
217   output_buffer += output_logon->LogonDomainName.Length;
218 
219   UnicodeStringPackedUnicodeStringCopy(
220       input_logon->UserName, reinterpret_cast<wchar_t*>(output_buffer),
221       &output_logon->UserName);
222   output_logon->UserName.Buffer =
223       reinterpret_cast<wchar_t*>(output_buffer - (BYTE*)output_unlock_logon);
224   output_buffer += output_logon->UserName.Length;
225 
226   UnicodeStringPackedUnicodeStringCopy(
227       input_logon->Password, reinterpret_cast<wchar_t*>(output_buffer),
228       &output_logon->Password);
229   output_logon->Password.Buffer =
230       reinterpret_cast<wchar_t*>(output_buffer - (BYTE*)output_unlock_logon);
231 
232   *prgb = (BYTE*)output_unlock_logon;
233   *pcb = cb;
234 
235   return S_OK;
236 }
237 
UnpackUserInfoFromAuthenticationBuffer(const CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION * cpcs,base::string16 * domain,base::string16 * username)238 HRESULT UnpackUserInfoFromAuthenticationBuffer(
239     const CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION* cpcs,
240     base::string16* domain,
241     base::string16* username) {
242   DCHECK(cpcs);
243   DCHECK(domain);
244   DCHECK(username);
245   domain->clear();
246   username->clear();
247   KERB_INTERACTIVE_UNLOCK_LOGON* pkiul =
248       reinterpret_cast<KERB_INTERACTIVE_UNLOCK_LOGON*>(cpcs->rgbSerialization);
249   ULONG buffer_size = cpcs->cbSerialization;
250   KERB_INTERACTIVE_LOGON* pkil = &pkiul->Logon;
251 
252   base::string16 serialization_domain;
253   base::string16 serialization_username;
254   // Check to see if the buffer is packed:
255   // 1. Ensure that the buffer can possibly contain the serialization.
256   // 2. Also if the range described by each (Buffer + MaximumSize) falls
257   // within the total bytecount, we can be pretty confident that the Buffers
258   // are actually offsets and that this is a packed credential.
259   if (sizeof(*pkiul) <= buffer_size &&
260       (reinterpret_cast<ptrdiff_t>(pkil->LogonDomainName.Buffer) +
261            pkil->LogonDomainName.MaximumLength <=
262        static_cast<ptrdiff_t>(buffer_size)) &&
263       (reinterpret_cast<ptrdiff_t>(pkil->UserName.Buffer) +
264            pkil->UserName.MaximumLength <=
265        static_cast<ptrdiff_t>(buffer_size)) &&
266       (reinterpret_cast<ptrdiff_t>(pkil->Password.Buffer) +
267            pkil->Password.MaximumLength <=
268        static_cast<ptrdiff_t>(buffer_size))) {
269     // When the authentication package is packed, then each buffer is not
270     // null terminated and the next buffer starts at the end of the previous
271     // one. Also the buffers are stored in the order that they are declared
272     // in the structure.
273 
274     if (pkil->LogonDomainName.Buffer && pkil->UserName.Buffer) {
275       const wchar_t* domain_buffer_pos = reinterpret_cast<wchar_t*>(
276           (reinterpret_cast<ptrdiff_t>(pkiul) +
277            reinterpret_cast<ptrdiff_t>(pkil->LogonDomainName.Buffer)));
278 
279       const wchar_t* username_buffer_pos = reinterpret_cast<wchar_t*>(
280           (reinterpret_cast<ptrdiff_t>(pkiul) +
281            reinterpret_cast<ptrdiff_t>(pkil->UserName.Buffer)));
282       serialization_domain = base::string16(
283           domain_buffer_pos,
284           pkil->LogonDomainName.MaximumLength / sizeof(domain_buffer_pos[0]));
285       serialization_username = base::string16(
286           username_buffer_pos,
287           pkil->UserName.MaximumLength / sizeof(username_buffer_pos[0]));
288     }
289   } else {
290     // If the authentication package is not packed, assume that the buffer
291     // points to a null terminated string.
292     serialization_domain = pkiul->Logon.LogonDomainName.Buffer;
293     serialization_username = pkiul->Logon.UserName.Buffer;
294   }
295 
296   if (serialization_domain.length() && serialization_username.length()) {
297     *domain = serialization_domain;
298     *username = serialization_username;
299     return S_OK;
300   }
301   return E_FAIL;
302 }
303 
304 }  // namespace
305 
306 // Gets the auth package id for NEGOSSP_NAME_A.
GetAuthenticationPackageId(ULONG * id)307 HRESULT GetAuthenticationPackageId(ULONG* id) {
308   DCHECK(id);
309 
310   HANDLE lsa;
311   NTSTATUS status = ::LsaConnectUntrusted(&lsa);
312   HRESULT hr = HRESULT_FROM_NT(status);
313   if (FAILED(hr)) {
314     LOGFN(ERROR) << "LsaConnectUntrusted hr=" << putHR(hr);
315     return hr;
316   }
317 
318   LSA_STRING name;
319   InitWindowsStringWithString(NEGOSSP_NAME_A, &name);
320 
321   status = ::LsaLookupAuthenticationPackage(lsa, &name, id);
322   ::LsaDeregisterLogonProcess(lsa);
323   hr = HRESULT_FROM_NT(status);
324   if (FAILED(hr))
325     LOGFN(ERROR) << "LsaLookupAuthenticationPackage hr=" << putHR(hr);
326 
327   return hr;
328 }
329 
DetermineUserSidFromAuthenticationBuffer(const CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION * cpcs,base::string16 * sid)330 HRESULT DetermineUserSidFromAuthenticationBuffer(
331     const CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION* cpcs,
332     base::string16* sid) {
333   DCHECK(sid);
334 
335   sid->clear();
336 
337   base::string16 serialization_domain;
338   base::string16 serialization_username;
339   HRESULT hr = UnpackUserInfoFromAuthenticationBuffer(
340       cpcs, &serialization_domain, &serialization_username);
341 
342   if (SUCCEEDED(hr)) {
343     hr = OSUserManager::Get()->GetUserSID(serialization_domain.c_str(),
344                                           serialization_username.c_str(), sid);
345     // The function GetUserSID strictly checks the domain that is passed in and
346     // determines if a user really exists in that domain. However the behavior
347     // of RDP connections is more lenient. You can enter any domain you want
348     // when connecting and if a user in that specific domain is not found, RDP
349     // will fall back to trying to sign in the local user with the same
350     // username. To emulate that behavior, we try to also check if there is a
351     // user on the local domain we could possibly signin and return the SID for
352     // that user if it exists.
353     if (FAILED(hr)) {
354       base::string16 local_domain = OSUserManager::GetLocalDomain();
355       if (!base::EqualsCaseInsensitiveASCII(local_domain,
356                                             serialization_domain)) {
357         hr = OSUserManager::Get()->GetUserSID(
358             local_domain.c_str(), serialization_username.c_str(), sid);
359 
360         if (FAILED(hr)) {
361           LOGFN(ERROR) << "GetUserSID hr=" << putHR(hr);
362           return hr;
363         }
364       }
365     }
366   }
367   return S_OK;
368 }
369 
BuildCredPackAuthenticationBuffer(BSTR domain,BSTR username,BSTR password,CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus,CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION * cpcs)370 HRESULT BuildCredPackAuthenticationBuffer(
371     BSTR domain,
372     BSTR username,
373     BSTR password,
374     CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus,
375     CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION* cpcs) {
376   DCHECK(username);
377   DCHECK(password);
378   DCHECK(cpcs);
379 
380   HRESULT hr = GetAuthenticationPackageId(&(cpcs->ulAuthenticationPackage));
381   if (FAILED(hr)) {
382     LOGFN(ERROR) << "GetAuthenticationPackageId hr=" << putHR(hr);
383     return hr;
384   }
385 
386   // Caller should fill this in.
387   cpcs->clsidCredentialProvider = GUID_NULL;
388 
389   std::vector<wchar_t> protected_password;
390   // Copy the password and pass the copied buffer into
391   // ProtectIfNecessaryAndCopyPassword since it expects a non-const input
392   // buffer.
393   std::vector<wchar_t> copy_password(OLE2W(password),
394                                      OLE2W(password) + wcslen(password) + 1);
395   hr = ProtectIfNecessaryAndCopyPassword(&copy_password[0], cpus,
396                                          &protected_password);
397 
398   // Zero out the unencrypted copy of the password.
399   SecurelyClearBuffer(&copy_password[0], copy_password.size());
400   if (FAILED(hr)) {
401     LOGFN(ERROR) << "ProtectIfNecessaryAndCopyPassword hr=" << putHR(hr);
402     return hr;
403   }
404 
405   // Protected password may still be insecure so make sure to zero it out.
406   base::ScopedClosureRunner zero_buffer_on_exit(
407       base::BindOnce(base::IgnoreResult(&SecurelyClearBuffer),
408                      &protected_password[0], protected_password.size()));
409 
410   wchar_t* logon_domain = domain;
411 
412   // For domain joined users, use a Kerberos authentication package.
413   KERB_INTERACTIVE_UNLOCK_LOGON kiul;
414   KerbInteractiveUnlockLogonInit(OLE2W(logon_domain), OLE2W(username),
415                                  &protected_password[0], cpus, &kiul);
416 
417   // Use KERB_INTERACTIVE_UNLOCK_LOGON in both unlock and logon
418   // scenarios. It contains a KERB_INTERACTIVE_LOGON to hold the creds
419   // plus a LUID that is filled in for us by Winlogon as necessary.
420   hr = KerbInteractiveUnlockLogonPack(kiul, &cpcs->rgbSerialization,
421                                       &cpcs->cbSerialization);
422   if (FAILED(hr)) {
423     hr = HRESULT_FROM_WIN32(::GetLastError());
424     LOGFN(ERROR) << "KerbInteractiveUnlockLogonPack user=" << username
425                  << " dn=" << logon_domain << " hr=" << putHR(hr)
426                  << " length=" << cpcs->cbSerialization;
427     return hr;
428   }
429 
430   return S_OK;
431 }
432 
433 }  // namespace credential_provider
434