OSReauthenticator.cpp (19723B)
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- 2 * 3 * This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "OSReauthenticator.h" 8 9 #include "OSKeyStore.h" 10 #include "nsNetCID.h" 11 #include "mozilla/dom/Promise.h" 12 #include "mozilla/Logging.h" 13 #include "mozilla/Maybe.h" 14 #include "mozilla/Preferences.h" 15 #include "nsComponentManagerUtils.h" 16 #include "nsIBaseWindow.h" 17 #include "nsIDocShell.h" 18 #include "nsISupportsUtils.h" 19 #include "nsIWidget.h" 20 #include "nsPIDOMWindow.h" 21 #include "nsServiceManagerUtils.h" 22 #include "nsThreadUtils.h" 23 #include "mozilla/ipc/IPCTypes.h" 24 25 NS_IMPL_ISUPPORTS(OSReauthenticator, nsIOSReauthenticator) 26 27 extern mozilla::LazyLogModule gCredentialManagerSecretLog; 28 29 using mozilla::LogLevel; 30 using mozilla::Maybe; 31 using mozilla::Preferences; 32 using mozilla::WindowsHandle; 33 using mozilla::dom::Promise; 34 35 #define PREF_BLANK_PASSWORD "security.osreauthenticator.blank_password" 36 #define PREF_PASSWORD_LAST_CHANGED_LO \ 37 "security.osreauthenticator.password_last_changed_lo" 38 #define PREF_PASSWORD_LAST_CHANGED_HI \ 39 "security.osreauthenticator.password_last_changed_hi" 40 41 #if defined(XP_WIN) 42 # include <combaseapi.h> 43 # include <ntsecapi.h> 44 # include <wincred.h> 45 # include <windows.h> 46 # include "nsIWindowsRegKey.h" // Must be included after <windows.h> for HKEY definition 47 # define SECURITY_WIN32 48 # include <security.h> 49 # include <shlwapi.h> 50 # include <lm.h> 51 # undef ACCESS_READ // nsWindowsRegKey defines its own ACCESS_READ 52 struct HandleCloser { 53 typedef HANDLE pointer; 54 void operator()(HANDLE h) { 55 if (h != INVALID_HANDLE_VALUE) { 56 CloseHandle(h); 57 } 58 } 59 }; 60 struct BufferFreer { 61 typedef LPVOID pointer; 62 ULONG mSize; 63 explicit BufferFreer(ULONG size) : mSize(size) {} 64 void operator()(LPVOID b) { 65 SecureZeroMemory(b, mSize); 66 CoTaskMemFree(b); 67 } 68 }; 69 struct LsaDeregistrator { 70 typedef HANDLE pointer; 71 void operator()(HANDLE h) { 72 if (h != INVALID_HANDLE_VALUE) { 73 LsaDeregisterLogonProcess(h); 74 } 75 } 76 }; 77 typedef std::unique_ptr<HANDLE, HandleCloser> ScopedHANDLE; 78 typedef std::unique_ptr<LPVOID, BufferFreer> ScopedBuffer; 79 typedef std::unique_ptr<HANDLE, LsaDeregistrator> ScopedLsaHANDLE; 80 81 constexpr int64_t Int32Modulo = 2147483648; 82 83 // Get the token info holding the sid. 84 std::unique_ptr<char[]> GetTokenInfo(ScopedHANDLE& token) { 85 DWORD length = 0; 86 // https://docs.microsoft.com/en-us/windows/desktop/api/securitybaseapi/nf-securitybaseapi-gettokeninformation 87 (void)GetTokenInformation(token.get(), TokenUser, nullptr, 0, &length); 88 if (!length || GetLastError() != ERROR_INSUFFICIENT_BUFFER) { 89 MOZ_LOG(gCredentialManagerSecretLog, LogLevel::Debug, 90 ("Unable to obtain current token info.")); 91 return nullptr; 92 } 93 std::unique_ptr<char[]> token_info(new char[length]); 94 if (!GetTokenInformation(token.get(), TokenUser, token_info.get(), length, 95 &length)) { 96 MOZ_LOG(gCredentialManagerSecretLog, LogLevel::Debug, 97 ("Unable to obtain current token info (second call, possible " 98 "system error.")); 99 return nullptr; 100 } 101 return token_info; 102 } 103 104 std::unique_ptr<char[]> GetUserTokenInfo() { 105 // Get current user sid to make sure the same user got logged in. 106 HANDLE token; 107 if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) { 108 // Couldn't get a process token. This will fail any unlock attempts later. 109 MOZ_LOG(gCredentialManagerSecretLog, LogLevel::Debug, 110 ("Unable to obtain process token.")); 111 return nullptr; 112 } 113 ScopedHANDLE scopedToken(token); 114 return GetTokenInfo(scopedToken); 115 } 116 117 Maybe<int64_t> GetPasswordLastChanged(const WCHAR* username) { 118 LPUSER_INFO_1 user_info = NULL; 119 DWORD passwordAgeInSeconds = 0; 120 121 NET_API_STATUS ret = 122 NetUserGetInfo(NULL, username, 1, reinterpret_cast<LPBYTE*>(&user_info)); 123 124 if (ret == NERR_Success) { 125 // Returns seconds since last password change. 126 passwordAgeInSeconds = user_info->usri1_password_age; 127 NetApiBufferFree(user_info); 128 } else { 129 return mozilla::Nothing(); 130 } 131 132 // Return the time that the password was changed so we can use this 133 // for future comparisons. 134 return mozilla::Some(PR_Now() - passwordAgeInSeconds * PR_USEC_PER_SEC); 135 } 136 137 bool IsAutoAdminLogonEnabled() { 138 // https://support.microsoft.com/en-us/help/324737/how-to-turn-on-automatic-logon-in-windows 139 nsresult rv; 140 nsCOMPtr<nsIWindowsRegKey> regKey = 141 do_CreateInstance("@mozilla.org/windows-registry-key;1", &rv); 142 if (NS_FAILED(rv)) { 143 return false; 144 } 145 146 rv = regKey->Open( 147 nsIWindowsRegKey::ROOT_KEY_LOCAL_MACHINE, 148 nsLiteralString( 149 u"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon"), 150 nsIWindowsRegKey::ACCESS_READ); 151 if (NS_FAILED(rv)) { 152 return false; 153 } 154 155 nsAutoString value; 156 rv = regKey->ReadStringValue(u"AutoAdminLogon"_ns, value); 157 if (NS_FAILED(rv)) { 158 return false; 159 } 160 regKey->Close(); 161 162 return value.Equals(u"1"_ns); 163 } 164 165 bool IsRequireSignonEnabled() { 166 // https://docs.microsoft.com/en-us/windows-hardware/customize/power-settings/no-subgroup-settings-prompt-for-password-on-resume 167 nsresult rv; 168 nsCOMPtr<nsIWindowsRegKey> regKey = 169 do_CreateInstance("@mozilla.org/windows-registry-key;1", &rv); 170 if (NS_FAILED(rv)) { 171 return true; 172 } 173 174 rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_LOCAL_MACHINE, 175 u"System\\CurrentControlSet\\Control\\Power\\User\\Power" 176 "Schemes"_ns, 177 nsIWindowsRegKey::ACCESS_READ); 178 if (NS_FAILED(rv)) { 179 return true; 180 } 181 182 nsAutoString activePowerScheme; 183 rv = regKey->ReadStringValue(u"ActivePowerScheme"_ns, activePowerScheme); 184 if (NS_FAILED(rv)) { 185 return true; 186 } 187 regKey->Close(); 188 189 rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_LOCAL_MACHINE, 190 u"System\\CurrentControlSet\\Control\\Power\\User\\Power" 191 "Schemes\\"_ns + 192 activePowerScheme + 193 u"\\0e796bdb-100d-47d6-a2d5-f7d2daa51f51"_ns, 194 nsIWindowsRegKey::ACCESS_READ); 195 if (NS_FAILED(rv)) { 196 return true; 197 } 198 199 uint32_t value; 200 rv = regKey->ReadIntValue(u"ACSettingIndex"_ns, &value); 201 if (NS_FAILED(rv)) { 202 return true; 203 } 204 regKey->Close(); 205 206 return !!value; 207 } 208 209 // Use the Windows credential prompt to ask the user to authenticate the 210 // currently used account. 211 static nsresult ReauthenticateUserWindows( 212 const nsAString& aMessageText, const nsAString& aCaptionText, 213 const WindowsHandle& hwndParent, 214 /* out */ bool& reauthenticated, 215 /* inout */ bool& isBlankPassword, 216 /* inout */ int64_t& prefLastChanged, 217 /* out */ bool& isAutoAdminLogonEnabled, 218 /* out */ bool& isRequireSignonEnabled) { 219 reauthenticated = false; 220 isAutoAdminLogonEnabled = false; 221 isRequireSignonEnabled = true; 222 223 // Check if the user has a blank password before proceeding 224 DWORD usernameLength = CREDUI_MAX_USERNAME_LENGTH + 1; 225 WCHAR username[CREDUI_MAX_USERNAME_LENGTH + 1] = {0}; 226 227 if (!GetUserNameEx(NameSamCompatible, username, &usernameLength)) { 228 MOZ_LOG(gCredentialManagerSecretLog, LogLevel::Debug, 229 ("Error getting username")); 230 return NS_ERROR_FAILURE; 231 } 232 233 if (!IsOS(OS_DOMAINMEMBER)) { 234 const WCHAR* usernameNoDomain = username; 235 // Don't include the domain portion of the username when calling LogonUser. 236 LPCWSTR backslash = wcschr(username, L'\\'); 237 if (backslash) { 238 usernameNoDomain = backslash + 1; 239 } 240 241 Maybe<int64_t> lastChanged = GetPasswordLastChanged(usernameNoDomain); 242 if (lastChanged.isSome()) { 243 bool shouldCheckAgain = lastChanged.value() > prefLastChanged; 244 // Update the value stored in preferences 245 prefLastChanged = lastChanged.value(); 246 247 if (shouldCheckAgain) { 248 HANDLE logonUserHandle = INVALID_HANDLE_VALUE; 249 bool result = 250 LogonUser(usernameNoDomain, L".", L"", LOGON32_LOGON_INTERACTIVE, 251 LOGON32_PROVIDER_DEFAULT, &logonUserHandle); 252 if (result) { 253 CloseHandle(logonUserHandle); 254 } 255 // ERROR_ACCOUNT_RESTRICTION: Indicates a referenced user name and 256 // authentication information are valid, but some user account 257 // restriction has prevented successful authentication (such as 258 // time-of-day restrictions). 259 reauthenticated = isBlankPassword = 260 (result || GetLastError() == ERROR_ACCOUNT_RESTRICTION); 261 } else if (isBlankPassword) { 262 reauthenticated = true; 263 } 264 265 if (reauthenticated) { 266 return NS_OK; 267 } 268 } else { 269 isBlankPassword = false; 270 } 271 } else { 272 // Update any preferences, assuming domain members do not have blank 273 // passwords 274 isBlankPassword = false; 275 } 276 277 isAutoAdminLogonEnabled = IsAutoAdminLogonEnabled(); 278 279 isRequireSignonEnabled = IsRequireSignonEnabled(); 280 281 // Is used in next iteration if the previous login failed. 282 DWORD err = 0; 283 std::unique_ptr<char[]> userTokenInfo = GetUserTokenInfo(); 284 285 // CredUI prompt. 286 CREDUI_INFOW credui = {}; 287 credui.cbSize = sizeof(credui); 288 credui.hwndParent = reinterpret_cast<HWND>(hwndParent); 289 const nsString& messageText = PromiseFlatString(aMessageText); 290 credui.pszMessageText = messageText.get(); 291 const nsString& captionText = PromiseFlatString(aCaptionText); 292 credui.pszCaptionText = captionText.get(); 293 credui.hbmBanner = nullptr; // ignored 294 295 while (!reauthenticated) { 296 HANDLE lsa = INVALID_HANDLE_VALUE; 297 // Get authentication handle for future user authentications. 298 // https://docs.microsoft.com/en-us/windows/desktop/api/ntsecapi/nf-ntsecapi-lsaconnectuntrusted 299 if (LsaConnectUntrusted(&lsa) != ERROR_SUCCESS) { 300 MOZ_LOG(gCredentialManagerSecretLog, LogLevel::Debug, 301 ("Error acquiring lsa. Authentication attempts will fail.")); 302 return NS_ERROR_FAILURE; 303 } 304 ScopedLsaHANDLE scopedLsa(lsa); 305 306 if (!userTokenInfo || lsa == INVALID_HANDLE_VALUE) { 307 MOZ_LOG(gCredentialManagerSecretLog, LogLevel::Debug, 308 ("Error setting up login and user token.")); 309 return NS_ERROR_FAILURE; 310 } 311 312 ULONG authPackage = 0; 313 ULONG outCredSize = 0; 314 LPVOID outCredBuffer = nullptr; 315 316 // Get user's Windows credentials. 317 // https://docs.microsoft.com/en-us/windows/desktop/api/wincred/nf-wincred-creduipromptforwindowscredentialsw 318 err = CredUIPromptForWindowsCredentialsW( 319 &credui, err, &authPackage, nullptr, 0, &outCredBuffer, &outCredSize, 320 nullptr, CREDUIWIN_ENUMERATE_CURRENT_USER); 321 ScopedBuffer scopedOutCredBuffer(outCredBuffer, BufferFreer(outCredSize)); 322 if (err == ERROR_CANCELLED) { 323 MOZ_LOG(gCredentialManagerSecretLog, LogLevel::Debug, 324 ("Error getting authPackage for user login, user cancel.")); 325 return NS_OK; 326 } 327 if (err != ERROR_SUCCESS) { 328 MOZ_LOG(gCredentialManagerSecretLog, LogLevel::Debug, 329 ("Error getting authPackage for user login.")); 330 return NS_ERROR_FAILURE; 331 } 332 333 // Verify the credentials. 334 TOKEN_SOURCE source; 335 PCHAR contextName = const_cast<PCHAR>("Mozilla"); 336 size_t nameLength = 337 std::min(TOKEN_SOURCE_LENGTH, static_cast<int>(strlen(contextName))); 338 // Note that the string must not be longer than TOKEN_SOURCE_LENGTH. 339 memcpy(source.SourceName, contextName, nameLength); 340 // https://docs.microsoft.com/en-us/windows/desktop/api/securitybaseapi/nf-securitybaseapi-allocatelocallyuniqueid 341 if (!AllocateLocallyUniqueId(&source.SourceIdentifier)) { 342 MOZ_LOG(gCredentialManagerSecretLog, LogLevel::Debug, 343 ("Error allocating ID for logon process.")); 344 return NS_ERROR_FAILURE; 345 } 346 347 NTSTATUS substs; 348 void* profileBuffer = nullptr; 349 ULONG profileBufferLength = 0; 350 QUOTA_LIMITS limits = {0}; 351 LUID luid; 352 HANDLE token = INVALID_HANDLE_VALUE; 353 LSA_STRING name; 354 name.Buffer = contextName; 355 name.Length = strlen(name.Buffer); 356 name.MaximumLength = name.Length; 357 // https://docs.microsoft.com/en-us/windows/desktop/api/ntsecapi/nf-ntsecapi-lsalogonuser 358 NTSTATUS sts = LsaLogonUser( 359 scopedLsa.get(), &name, (SECURITY_LOGON_TYPE)Interactive, authPackage, 360 scopedOutCredBuffer.get(), outCredSize, nullptr, &source, 361 &profileBuffer, &profileBufferLength, &luid, &token, &limits, &substs); 362 ScopedHANDLE scopedToken(token); 363 LsaFreeReturnBuffer(profileBuffer); 364 if (sts == ERROR_SUCCESS) { 365 MOZ_LOG(gCredentialManagerSecretLog, LogLevel::Debug, 366 ("User logged in successfully.")); 367 } else { 368 err = LsaNtStatusToWinError(sts); 369 MOZ_LOG(gCredentialManagerSecretLog, LogLevel::Debug, 370 ("Login failed with %lx (%lx).", sts, err)); 371 continue; 372 } 373 374 // The user can select any user to log-in on the authentication prompt. 375 // Make sure that the logged in user is the current user. 376 std::unique_ptr<char[]> logonTokenInfo = GetTokenInfo(scopedToken); 377 if (!logonTokenInfo) { 378 MOZ_LOG(gCredentialManagerSecretLog, LogLevel::Debug, 379 ("Error getting logon token info.")); 380 return NS_ERROR_FAILURE; 381 } 382 PSID logonSID = 383 reinterpret_cast<TOKEN_USER*>(logonTokenInfo.get())->User.Sid; 384 PSID userSID = reinterpret_cast<TOKEN_USER*>(userTokenInfo.get())->User.Sid; 385 if (EqualSid(userSID, logonSID)) { 386 MOZ_LOG(gCredentialManagerSecretLog, LogLevel::Debug, 387 ("Login successfully (correct user).")); 388 reauthenticated = true; 389 break; 390 } else { 391 err = ERROR_LOGON_FAILURE; 392 } 393 } 394 return NS_OK; 395 } 396 #endif // XP_WIN 397 398 static nsresult ReauthenticateUser(const nsAString& prompt, 399 const nsAString& caption, 400 const WindowsHandle& hwndParent, 401 /* out */ bool& reauthenticated, 402 /* inout */ bool& isBlankPassword, 403 /* inout */ int64_t& prefLastChanged, 404 /* out */ bool& isAutoAdminLogonEnabled, 405 /* out */ bool& isRequireSignonEnabled) { 406 reauthenticated = false; 407 #if defined(XP_WIN) 408 return ReauthenticateUserWindows( 409 prompt, caption, hwndParent, reauthenticated, isBlankPassword, 410 prefLastChanged, isAutoAdminLogonEnabled, isRequireSignonEnabled); 411 #elif defined(XP_MACOSX) 412 return ReauthenticateUserMacOS(prompt, reauthenticated, isBlankPassword); 413 #else 414 return NS_OK; 415 #endif // Reauthentication is not implemented for this platform. 416 } 417 418 static void BackgroundReauthenticateUser(RefPtr<Promise>& aPromise, 419 const nsAString& aMessageText, 420 const nsAString& aCaptionText, 421 const WindowsHandle& hwndParent, 422 bool isBlankPassword, 423 int64_t prefLastChanged) { 424 nsAutoCString recovery; 425 bool reauthenticated; 426 bool isAutoAdminLogonEnabled; 427 bool isRequireSignonEnabled; 428 nsresult rv = ReauthenticateUser( 429 aMessageText, aCaptionText, hwndParent, reauthenticated, isBlankPassword, 430 prefLastChanged, isAutoAdminLogonEnabled, isRequireSignonEnabled); 431 432 nsTArray<int32_t> prefLastChangedUpdates; 433 #if defined(XP_WIN) 434 // Increase the lastChanged time to account for clock skew. 435 prefLastChanged += PR_USEC_PER_SEC; 436 // Need to split the 64bit integer to its hi and lo bits before sending it 437 // back to JS. 438 int32_t prefLastChangedHi = prefLastChanged / Int32Modulo; 439 int32_t prefLastChangedLo = prefLastChanged % Int32Modulo; 440 prefLastChangedUpdates.AppendElement(prefLastChangedHi); 441 prefLastChangedUpdates.AppendElement(prefLastChangedLo); 442 #endif 443 444 nsTArray<int32_t> results; 445 results.AppendElement(reauthenticated); 446 results.AppendElement(isBlankPassword); 447 #if defined(XP_WIN) 448 results.AppendElement(isAutoAdminLogonEnabled); 449 results.AppendElement(isRequireSignonEnabled); 450 #endif 451 nsCOMPtr<nsIRunnable> runnable(NS_NewRunnableFunction( 452 "BackgroundReauthenticateUserResolve", 453 [rv, results = std::move(results), 454 prefLastChangedUpdates = std::move(prefLastChangedUpdates), 455 aPromise = std::move(aPromise)]() { 456 if (NS_FAILED(rv)) { 457 aPromise->MaybeReject(rv); 458 } else { 459 aPromise->MaybeResolve(results); 460 } 461 462 nsresult rv = Preferences::SetBool(PREF_BLANK_PASSWORD, results[1]); 463 if (NS_FAILED(rv)) { 464 return; 465 } 466 if (prefLastChangedUpdates.Length() > 1) { 467 rv = Preferences::SetInt(PREF_PASSWORD_LAST_CHANGED_HI, 468 prefLastChangedUpdates[0]); 469 if (NS_FAILED(rv)) { 470 return; 471 } 472 Preferences::SetInt(PREF_PASSWORD_LAST_CHANGED_LO, 473 prefLastChangedUpdates[1]); 474 } 475 })); 476 NS_DispatchToMainThread(runnable.forget()); 477 } 478 479 NS_IMETHODIMP 480 OSReauthenticator::AsyncReauthenticateUser(const nsAString& aMessageText, 481 const nsAString& aCaptionText, 482 mozIDOMWindow* aParentWindow, 483 JSContext* aCx, 484 Promise** promiseOut) { 485 NS_ENSURE_ARG_POINTER(aCx); 486 487 RefPtr<Promise> promiseHandle; 488 nsresult rv = GetPromise(aCx, promiseHandle); 489 if (NS_FAILED(rv)) { 490 return rv; 491 } 492 493 WindowsHandle hwndParent = 0; 494 if (aParentWindow) { 495 nsPIDOMWindowInner* win = nsPIDOMWindowInner::From(aParentWindow); 496 nsIDocShell* docShell = win->GetDocShell(); 497 if (docShell) { 498 nsCOMPtr<nsIBaseWindow> baseWindow = do_QueryInterface(docShell); 499 if (baseWindow) { 500 nsCOMPtr<nsIWidget> widget; 501 baseWindow->GetMainWidget(getter_AddRefs(widget)); 502 if (widget) { 503 hwndParent = reinterpret_cast<WindowsHandle>( 504 widget->GetNativeData(NS_NATIVE_WINDOW)); 505 } 506 } 507 } 508 } 509 510 int64_t prefLastChanged = 0; 511 bool isBlankPassword = false; 512 #if defined(XP_WIN) 513 // These preferences are only supported on Windows. 514 // Preferences are read/write main-thread only. 515 int32_t prefLastChangedLo; 516 int32_t prefLastChangedHi; 517 rv = Preferences::GetBool(PREF_BLANK_PASSWORD, &isBlankPassword); 518 if (NS_FAILED(rv)) { 519 return rv; 520 } 521 rv = Preferences::GetInt(PREF_PASSWORD_LAST_CHANGED_LO, &prefLastChangedLo); 522 if (NS_FAILED(rv)) { 523 return rv; 524 } 525 rv = Preferences::GetInt(PREF_PASSWORD_LAST_CHANGED_HI, &prefLastChangedHi); 526 if (NS_FAILED(rv)) { 527 return rv; 528 } 529 prefLastChanged = prefLastChangedHi * Int32Modulo + prefLastChangedLo; 530 #endif 531 532 nsCOMPtr<nsIRunnable> runnable(NS_NewRunnableFunction( 533 "BackgroundReauthenticateUser", 534 [promiseHandle, aMessageText = nsAutoString(aMessageText), 535 aCaptionText = nsAutoString(aCaptionText), hwndParent, isBlankPassword, 536 prefLastChanged]() mutable { 537 BackgroundReauthenticateUser(promiseHandle, aMessageText, aCaptionText, 538 hwndParent, isBlankPassword, 539 prefLastChanged); 540 })); 541 542 nsCOMPtr<nsIEventTarget> target( 543 do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID)); 544 if (!target) { 545 return NS_ERROR_FAILURE; 546 } 547 rv = target->Dispatch(runnable, NS_DISPATCH_NORMAL); 548 if (NS_WARN_IF(NS_FAILED(rv))) { 549 return rv; 550 } 551 552 promiseHandle.forget(promiseOut); 553 return NS_OK; 554 }