/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "OSReauthenticator.h" #include "OSKeyStore.h" #include "nsNetCID.h" #include "mozilla/dom/Promise.h" #include "mozilla/Logging.h" #include "mozilla/Maybe.h" #include "mozilla/Preferences.h" #include "nsComponentManagerUtils.h" #include "nsIBaseWindow.h" #include "nsIDocShell.h" #include "nsISupportsUtils.h" #include "nsIWidget.h" #include "nsPIDOMWindow.h" #include "nsServiceManagerUtils.h" #include "nsThreadUtils.h" #include "mozilla/ipc/IPCTypes.h" NS_IMPL_ISUPPORTS(OSReauthenticator, nsIOSReauthenticator) extern mozilla::LazyLogModule gCredentialManagerSecretLog; using mozilla::LogLevel; using mozilla::Maybe; using mozilla::Preferences; using mozilla::WindowsHandle; using mozilla::dom::Promise; #define PREF_BLANK_PASSWORD "security.osreauthenticator.blank_password" #define PREF_PASSWORD_LAST_CHANGED_LO \ "security.osreauthenticator.password_last_changed_lo" #define PREF_PASSWORD_LAST_CHANGED_HI \ "security.osreauthenticator.password_last_changed_hi" #if defined(XP_WIN) # include # include # include # include # include "nsIWindowsRegKey.h" // Must be included after for HKEY definition # define SECURITY_WIN32 # include # include # if !defined(__MINGW32__) # include # undef ACCESS_READ // nsWindowsRegKey defines its own ACCESS_READ # endif // !defined(__MINGW32__) struct HandleCloser { typedef HANDLE pointer; void operator()(HANDLE h) { if (h != INVALID_HANDLE_VALUE) { CloseHandle(h); } } }; struct BufferFreer { typedef LPVOID pointer; ULONG mSize; explicit BufferFreer(ULONG size) : mSize(size) {} void operator()(LPVOID b) { SecureZeroMemory(b, mSize); CoTaskMemFree(b); } }; struct LsaDeregistrator { typedef HANDLE pointer; void operator()(HANDLE h) { if (h != INVALID_HANDLE_VALUE) { LsaDeregisterLogonProcess(h); } } }; typedef std::unique_ptr ScopedHANDLE; typedef std::unique_ptr ScopedBuffer; typedef std::unique_ptr ScopedLsaHANDLE; constexpr int64_t Int32Modulo = 2147483648; // Get the token info holding the sid. std::unique_ptr GetTokenInfo(ScopedHANDLE& token) { DWORD length = 0; // https://docs.microsoft.com/en-us/windows/desktop/api/securitybaseapi/nf-securitybaseapi-gettokeninformation mozilla::Unused << GetTokenInformation(token.get(), TokenUser, nullptr, 0, &length); if (!length || GetLastError() != ERROR_INSUFFICIENT_BUFFER) { MOZ_LOG(gCredentialManagerSecretLog, LogLevel::Debug, ("Unable to obtain current token info.")); return nullptr; } std::unique_ptr token_info(new char[length]); if (!GetTokenInformation(token.get(), TokenUser, token_info.get(), length, &length)) { MOZ_LOG(gCredentialManagerSecretLog, LogLevel::Debug, ("Unable to obtain current token info (second call, possible " "system error.")); return nullptr; } return token_info; } std::unique_ptr GetUserTokenInfo() { // Get current user sid to make sure the same user got logged in. HANDLE token; if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) { // Couldn't get a process token. This will fail any unlock attempts later. MOZ_LOG(gCredentialManagerSecretLog, LogLevel::Debug, ("Unable to obtain process token.")); return nullptr; } ScopedHANDLE scopedToken(token); return GetTokenInfo(scopedToken); } Maybe GetPasswordLastChanged(const WCHAR* username) { # if defined(__MINGW32__) // NetUserGetInfo requires Lm.h which is not provided in MinGW builds return mozilla::Nothing(); # else LPUSER_INFO_1 user_info = NULL; DWORD passwordAgeInSeconds = 0; NET_API_STATUS ret = NetUserGetInfo(NULL, username, 1, reinterpret_cast(&user_info)); if (ret == NERR_Success) { // Returns seconds since last password change. passwordAgeInSeconds = user_info->usri1_password_age; NetApiBufferFree(user_info); } else { return mozilla::Nothing(); } // Return the time that the password was changed so we can use this // for future comparisons. return mozilla::Some(PR_Now() - passwordAgeInSeconds * PR_USEC_PER_SEC); # endif } bool IsAutoAdminLogonEnabled() { // https://support.microsoft.com/en-us/help/324737/how-to-turn-on-automatic-logon-in-windows nsresult rv; nsCOMPtr regKey = do_CreateInstance("@mozilla.org/windows-registry-key;1", &rv); if (NS_FAILED(rv)) { return false; } rv = regKey->Open( nsIWindowsRegKey::ROOT_KEY_LOCAL_MACHINE, nsLiteralString( u"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon"), nsIWindowsRegKey::ACCESS_READ); if (NS_FAILED(rv)) { return false; } nsAutoString value; rv = regKey->ReadStringValue(u"AutoAdminLogon"_ns, value); if (NS_FAILED(rv)) { return false; } regKey->Close(); return value.Equals(u"1"_ns); } bool IsRequireSignonEnabled() { // https://docs.microsoft.com/en-us/windows-hardware/customize/power-settings/no-subgroup-settings-prompt-for-password-on-resume nsresult rv; nsCOMPtr regKey = do_CreateInstance("@mozilla.org/windows-registry-key;1", &rv); if (NS_FAILED(rv)) { return true; } rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_LOCAL_MACHINE, u"System\\CurrentControlSet\\Control\\Power\\User\\Power" "Schemes"_ns, nsIWindowsRegKey::ACCESS_READ); if (NS_FAILED(rv)) { return true; } nsAutoString activePowerScheme; rv = regKey->ReadStringValue(u"ActivePowerScheme"_ns, activePowerScheme); if (NS_FAILED(rv)) { return true; } regKey->Close(); rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_LOCAL_MACHINE, u"System\\CurrentControlSet\\Control\\Power\\User\\Power" "Schemes\\"_ns + activePowerScheme + u"\\0e796bdb-100d-47d6-a2d5-f7d2daa51f51"_ns, nsIWindowsRegKey::ACCESS_READ); if (NS_FAILED(rv)) { return true; } uint32_t value; rv = regKey->ReadIntValue(u"ACSettingIndex"_ns, &value); if (NS_FAILED(rv)) { return true; } regKey->Close(); return !!value; } // Use the Windows credential prompt to ask the user to authenticate the // currently used account. static nsresult ReauthenticateUserWindows( const nsAString& aMessageText, const nsAString& aCaptionText, const WindowsHandle& hwndParent, /* out */ bool& reauthenticated, /* inout */ bool& isBlankPassword, /* inout */ int64_t& prefLastChanged, /* out */ bool& isAutoAdminLogonEnabled, /* out */ bool& isRequireSignonEnabled) { reauthenticated = false; isAutoAdminLogonEnabled = false; isRequireSignonEnabled = true; // Check if the user has a blank password before proceeding DWORD usernameLength = CREDUI_MAX_USERNAME_LENGTH + 1; WCHAR username[CREDUI_MAX_USERNAME_LENGTH + 1] = {0}; if (!GetUserNameEx(NameSamCompatible, username, &usernameLength)) { MOZ_LOG(gCredentialManagerSecretLog, LogLevel::Debug, ("Error getting username")); return NS_ERROR_FAILURE; } if (!IsOS(OS_DOMAINMEMBER)) { const WCHAR* usernameNoDomain = username; // Don't include the domain portion of the username when calling LogonUser. LPCWSTR backslash = wcschr(username, L'\\'); if (backslash) { usernameNoDomain = backslash + 1; } Maybe lastChanged = GetPasswordLastChanged(usernameNoDomain); if (lastChanged.isSome()) { bool shouldCheckAgain = lastChanged.value() > prefLastChanged; // Update the value stored in preferences prefLastChanged = lastChanged.value(); if (shouldCheckAgain) { HANDLE logonUserHandle = INVALID_HANDLE_VALUE; bool result = LogonUser(usernameNoDomain, L".", L"", LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, &logonUserHandle); if (result) { CloseHandle(logonUserHandle); } // ERROR_ACCOUNT_RESTRICTION: Indicates a referenced user name and // authentication information are valid, but some user account // restriction has prevented successful authentication (such as // time-of-day restrictions). reauthenticated = isBlankPassword = (result || GetLastError() == ERROR_ACCOUNT_RESTRICTION); } else if (isBlankPassword) { reauthenticated = true; } if (reauthenticated) { return NS_OK; } } else { isBlankPassword = false; } } else { // Update any preferences, assuming domain members do not have blank // passwords isBlankPassword = false; } isAutoAdminLogonEnabled = IsAutoAdminLogonEnabled(); isRequireSignonEnabled = IsRequireSignonEnabled(); // Is used in next iteration if the previous login failed. DWORD err = 0; std::unique_ptr userTokenInfo = GetUserTokenInfo(); // CredUI prompt. CREDUI_INFOW credui = {}; credui.cbSize = sizeof(credui); credui.hwndParent = reinterpret_cast(hwndParent); const nsString& messageText = PromiseFlatString(aMessageText); credui.pszMessageText = messageText.get(); const nsString& captionText = PromiseFlatString(aCaptionText); credui.pszCaptionText = captionText.get(); credui.hbmBanner = nullptr; // ignored while (!reauthenticated) { HANDLE lsa = INVALID_HANDLE_VALUE; // Get authentication handle for future user authentications. // https://docs.microsoft.com/en-us/windows/desktop/api/ntsecapi/nf-ntsecapi-lsaconnectuntrusted if (LsaConnectUntrusted(&lsa) != ERROR_SUCCESS) { MOZ_LOG(gCredentialManagerSecretLog, LogLevel::Debug, ("Error acquiring lsa. Authentication attempts will fail.")); return NS_ERROR_FAILURE; } ScopedLsaHANDLE scopedLsa(lsa); if (!userTokenInfo || lsa == INVALID_HANDLE_VALUE) { MOZ_LOG(gCredentialManagerSecretLog, LogLevel::Debug, ("Error setting up login and user token.")); return NS_ERROR_FAILURE; } ULONG authPackage = 0; ULONG outCredSize = 0; LPVOID outCredBuffer = nullptr; // Get user's Windows credentials. // https://docs.microsoft.com/en-us/windows/desktop/api/wincred/nf-wincred-creduipromptforwindowscredentialsw err = CredUIPromptForWindowsCredentialsW( &credui, err, &authPackage, nullptr, 0, &outCredBuffer, &outCredSize, nullptr, CREDUIWIN_ENUMERATE_CURRENT_USER); ScopedBuffer scopedOutCredBuffer(outCredBuffer, BufferFreer(outCredSize)); if (err == ERROR_CANCELLED) { MOZ_LOG(gCredentialManagerSecretLog, LogLevel::Debug, ("Error getting authPackage for user login, user cancel.")); return NS_OK; } if (err != ERROR_SUCCESS) { MOZ_LOG(gCredentialManagerSecretLog, LogLevel::Debug, ("Error getting authPackage for user login.")); return NS_ERROR_FAILURE; } // Verify the credentials. TOKEN_SOURCE source; PCHAR contextName = const_cast("Mozilla"); size_t nameLength = std::min(TOKEN_SOURCE_LENGTH, static_cast(strlen(contextName))); // Note that the string must not be longer than TOKEN_SOURCE_LENGTH. memcpy(source.SourceName, contextName, nameLength); // https://docs.microsoft.com/en-us/windows/desktop/api/securitybaseapi/nf-securitybaseapi-allocatelocallyuniqueid if (!AllocateLocallyUniqueId(&source.SourceIdentifier)) { MOZ_LOG(gCredentialManagerSecretLog, LogLevel::Debug, ("Error allocating ID for logon process.")); return NS_ERROR_FAILURE; } NTSTATUS substs; void* profileBuffer = nullptr; ULONG profileBufferLength = 0; QUOTA_LIMITS limits = {0}; LUID luid; HANDLE token = INVALID_HANDLE_VALUE; LSA_STRING name; name.Buffer = contextName; name.Length = strlen(name.Buffer); name.MaximumLength = name.Length; // https://docs.microsoft.com/en-us/windows/desktop/api/ntsecapi/nf-ntsecapi-lsalogonuser NTSTATUS sts = LsaLogonUser( scopedLsa.get(), &name, (SECURITY_LOGON_TYPE)Interactive, authPackage, scopedOutCredBuffer.get(), outCredSize, nullptr, &source, &profileBuffer, &profileBufferLength, &luid, &token, &limits, &substs); ScopedHANDLE scopedToken(token); LsaFreeReturnBuffer(profileBuffer); if (sts == ERROR_SUCCESS) { MOZ_LOG(gCredentialManagerSecretLog, LogLevel::Debug, ("User logged in successfully.")); } else { err = LsaNtStatusToWinError(sts); MOZ_LOG(gCredentialManagerSecretLog, LogLevel::Debug, ("Login failed with %lx (%lx).", sts, err)); continue; } // The user can select any user to log-in on the authentication prompt. // Make sure that the logged in user is the current user. std::unique_ptr logonTokenInfo = GetTokenInfo(scopedToken); if (!logonTokenInfo) { MOZ_LOG(gCredentialManagerSecretLog, LogLevel::Debug, ("Error getting logon token info.")); return NS_ERROR_FAILURE; } PSID logonSID = reinterpret_cast(logonTokenInfo.get())->User.Sid; PSID userSID = reinterpret_cast(userTokenInfo.get())->User.Sid; if (EqualSid(userSID, logonSID)) { MOZ_LOG(gCredentialManagerSecretLog, LogLevel::Debug, ("Login successfully (correct user).")); reauthenticated = true; break; } else { err = ERROR_LOGON_FAILURE; } } return NS_OK; } #endif // XP_WIN static nsresult ReauthenticateUser(const nsAString& prompt, const nsAString& caption, const WindowsHandle& hwndParent, /* out */ bool& reauthenticated, /* inout */ bool& isBlankPassword, /* inout */ int64_t& prefLastChanged, /* out */ bool& isAutoAdminLogonEnabled, /* out */ bool& isRequireSignonEnabled) { reauthenticated = false; #if defined(XP_WIN) return ReauthenticateUserWindows( prompt, caption, hwndParent, reauthenticated, isBlankPassword, prefLastChanged, isAutoAdminLogonEnabled, isRequireSignonEnabled); #elif defined(XP_MACOSX) return ReauthenticateUserMacOS(prompt, reauthenticated, isBlankPassword); #else return NS_OK; #endif // Reauthentication is not implemented for this platform. } static void BackgroundReauthenticateUser(RefPtr& aPromise, const nsAString& aMessageText, const nsAString& aCaptionText, const WindowsHandle& hwndParent, bool isBlankPassword, int64_t prefLastChanged) { nsAutoCString recovery; bool reauthenticated; bool isAutoAdminLogonEnabled; bool isRequireSignonEnabled; nsresult rv = ReauthenticateUser( aMessageText, aCaptionText, hwndParent, reauthenticated, isBlankPassword, prefLastChanged, isAutoAdminLogonEnabled, isRequireSignonEnabled); nsTArray prefLastChangedUpdates; #if defined(XP_WIN) // Increase the lastChanged time to account for clock skew. prefLastChanged += PR_USEC_PER_SEC; // Need to split the 64bit integer to its hi and lo bits before sending it // back to JS. int32_t prefLastChangedHi = prefLastChanged / Int32Modulo; int32_t prefLastChangedLo = prefLastChanged % Int32Modulo; prefLastChangedUpdates.AppendElement(prefLastChangedHi); prefLastChangedUpdates.AppendElement(prefLastChangedLo); #endif nsTArray results; results.AppendElement(reauthenticated); results.AppendElement(isBlankPassword); #if defined(XP_WIN) results.AppendElement(isAutoAdminLogonEnabled); results.AppendElement(isRequireSignonEnabled); #endif nsCOMPtr runnable(NS_NewRunnableFunction( "BackgroundReauthenticateUserResolve", [rv, results = std::move(results), prefLastChangedUpdates = std::move(prefLastChangedUpdates), aPromise = std::move(aPromise)]() { if (NS_FAILED(rv)) { aPromise->MaybeReject(rv); } else { aPromise->MaybeResolve(results); } nsresult rv = Preferences::SetBool(PREF_BLANK_PASSWORD, results[1]); if (NS_FAILED(rv)) { return; } if (prefLastChangedUpdates.Length() > 1) { rv = Preferences::SetInt(PREF_PASSWORD_LAST_CHANGED_HI, prefLastChangedUpdates[0]); if (NS_FAILED(rv)) { return; } Preferences::SetInt(PREF_PASSWORD_LAST_CHANGED_LO, prefLastChangedUpdates[1]); } })); NS_DispatchToMainThread(runnable.forget()); } NS_IMETHODIMP OSReauthenticator::AsyncReauthenticateUser(const nsAString& aMessageText, const nsAString& aCaptionText, mozIDOMWindow* aParentWindow, JSContext* aCx, Promise** promiseOut) { NS_ENSURE_ARG_POINTER(aCx); RefPtr promiseHandle; nsresult rv = GetPromise(aCx, promiseHandle); if (NS_FAILED(rv)) { return rv; } WindowsHandle hwndParent = 0; if (aParentWindow) { nsPIDOMWindowInner* win = nsPIDOMWindowInner::From(aParentWindow); nsIDocShell* docShell = win->GetDocShell(); if (docShell) { nsCOMPtr baseWindow = do_QueryInterface(docShell); if (baseWindow) { nsCOMPtr widget; baseWindow->GetMainWidget(getter_AddRefs(widget)); if (widget) { hwndParent = reinterpret_cast( widget->GetNativeData(NS_NATIVE_WINDOW)); } } } } int64_t prefLastChanged = 0; bool isBlankPassword = false; #if defined(XP_WIN) // These preferences are only supported on Windows. // Preferences are read/write main-thread only. int32_t prefLastChangedLo; int32_t prefLastChangedHi; rv = Preferences::GetBool(PREF_BLANK_PASSWORD, &isBlankPassword); if (NS_FAILED(rv)) { return rv; } rv = Preferences::GetInt(PREF_PASSWORD_LAST_CHANGED_LO, &prefLastChangedLo); if (NS_FAILED(rv)) { return rv; } rv = Preferences::GetInt(PREF_PASSWORD_LAST_CHANGED_HI, &prefLastChangedHi); if (NS_FAILED(rv)) { return rv; } prefLastChanged = prefLastChangedHi * Int32Modulo + prefLastChangedLo; #endif nsCOMPtr runnable(NS_NewRunnableFunction( "BackgroundReauthenticateUser", [promiseHandle, aMessageText = nsAutoString(aMessageText), aCaptionText = nsAutoString(aCaptionText), hwndParent, isBlankPassword, prefLastChanged]() mutable { BackgroundReauthenticateUser(promiseHandle, aMessageText, aCaptionText, hwndParent, isBlankPassword, prefLastChanged); })); nsCOMPtr target( do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID)); if (!target) { return NS_ERROR_FAILURE; } rv = target->Dispatch(runnable, NS_DISPATCH_NORMAL); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } promiseHandle.forget(promiseOut); return NS_OK; }