| // Copyright 2013 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // windows.h must be first otherwise Win8 SDK breaks. |
| #include <windows.h> |
| #include <LM.h> |
| #include <ntsecapi.h> |
| #include <objbase.h> // For CoTaskMemFree() |
| #include <stddef.h> |
| #include <stdint.h> |
| #include <wincred.h> |
| |
| // SECURITY_WIN32 must be defined in order to get |
| // EXTENDED_NAME_FORMAT enumeration. |
| #define SECURITY_WIN32 1 |
| #include <security.h> |
| #undef SECURITY_WIN32 |
| |
| #include <memory> |
| |
| #include "chrome/browser/password_manager/password_manager_util_win.h" |
| |
| #include "base/bind.h" |
| #include "base/bind_helpers.h" |
| #include "base/feature_list.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/stl_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/task/post_task.h" |
| #include "base/time/time.h" |
| #include "base/win/win_util.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/grit/chromium_strings.h" |
| #include "components/password_manager/core/browser/password_manager.h" |
| #include "components/password_manager/core/common/password_manager_pref_names.h" |
| #include "components/prefs/pref_service.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/render_view_host.h" |
| #include "content/public/browser/render_widget_host_view.h" |
| #include "ui/aura/window.h" |
| #include "ui/aura/window_tree_host.h" |
| #include "ui/base/l10n/l10n_util.h" |
| |
| namespace password_manager_util_win { |
| namespace { |
| |
| enum OsPasswordStatus { |
| PASSWORD_STATUS_UNKNOWN = 0, |
| PASSWORD_STATUS_UNSUPPORTED, |
| PASSWORD_STATUS_BLANK, |
| PASSWORD_STATUS_NONBLANK, |
| PASSWORD_STATUS_WIN_DOMAIN, |
| // NOTE: Add new status types only immediately above this line. Also, |
| // make sure the enum list in tools/histogram/histograms.xml is |
| // updated with any change in here. |
| MAX_PASSWORD_STATUS |
| }; |
| |
| const unsigned kMaxPasswordRetries = 3; |
| |
| const unsigned kCredUiDefaultFlags = |
| CREDUI_FLAGS_GENERIC_CREDENTIALS | |
| CREDUI_FLAGS_EXCLUDE_CERTIFICATES | |
| CREDUI_FLAGS_KEEP_USERNAME | |
| CREDUI_FLAGS_ALWAYS_SHOW_UI | |
| CREDUI_FLAGS_DO_NOT_PERSIST; |
| |
| struct PasswordCheckPrefs { |
| PasswordCheckPrefs() : pref_last_changed_(0), blank_password_(false) {} |
| |
| void Read(PrefService* local_state); |
| void Write(PrefService* local_state); |
| |
| int64_t pref_last_changed_; |
| bool blank_password_; |
| }; |
| |
| // Validates whether a credential buffer contains the credentials for the |
| // currently signed in user. |
| class CredentialBufferValidator { |
| public: |
| CredentialBufferValidator(); |
| ~CredentialBufferValidator(); |
| |
| // Returns ERROR_SUCCESS if the credential buffer given matches the |
| // credentials of the user running Chrome. Otherwise an error describing |
| // the issue. |
| DWORD IsValid(ULONG auth_package, void* cred_buffer, ULONG cred_length); |
| |
| private: |
| std::unique_ptr<char[]> GetTokenInformation(HANDLE token); |
| |
| // Name of app calling LsaLogonUser(). In this case, "chrome". |
| LSA_STRING name_; |
| |
| // Handle to LSA server. |
| HANDLE lsa_ = INVALID_HANDLE_VALUE; |
| |
| // Buffer holding information about the current process token. |
| std::unique_ptr<char[]> cur_token_info_; |
| |
| DISALLOW_COPY_AND_ASSIGN(CredentialBufferValidator); |
| }; |
| |
| CredentialBufferValidator::CredentialBufferValidator() { |
| // Windows 7 does not support pseudo tokens with GetTokenInformation(), so |
| // make sure to open a real token. |
| HANDLE token; |
| if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) { |
| DLOG(ERROR) << "Unable to obtain process token " << GetLastError(); |
| return; |
| } |
| |
| cur_token_info_ = GetTokenInformation(token); |
| CloseHandle(token); |
| if (!cur_token_info_) { |
| DLOG(ERROR) << "Unable to obtain current token info " << GetLastError(); |
| return; |
| } |
| |
| NTSTATUS sts = LsaConnectUntrusted(&lsa_); |
| if (sts != ERROR_SUCCESS) { |
| lsa_ = INVALID_HANDLE_VALUE; |
| return; |
| } |
| |
| name_.Buffer = const_cast<PCHAR>("Chrome"); |
| name_.Length = strlen(name_.Buffer); |
| name_.MaximumLength = name_.Length + 1; |
| } |
| |
| CredentialBufferValidator::~CredentialBufferValidator() { |
| if (lsa_ != INVALID_HANDLE_VALUE) |
| LsaDeregisterLogonProcess(lsa_); |
| } |
| |
| DWORD CredentialBufferValidator::IsValid(ULONG auth_package, |
| void* auth_buffer, |
| ULONG auth_length) { |
| if (lsa_ == INVALID_HANDLE_VALUE) |
| return ERROR_LOGON_FAILURE; |
| |
| NTSTATUS sts; |
| NTSTATUS substs; |
| TOKEN_SOURCE source; |
| void* profile_buffer = nullptr; |
| ULONG profile_buffer_length = 0; |
| QUOTA_LIMITS limits; |
| LUID luid; |
| HANDLE token; |
| |
| strcpy_s(source.SourceName, base::size(source.SourceName), "Chrome"); |
| if (!AllocateLocallyUniqueId(&source.SourceIdentifier)) |
| return GetLastError(); |
| |
| sts = LsaLogonUser(lsa_, &name_, Interactive, auth_package, auth_buffer, |
| auth_length, nullptr, &source, &profile_buffer, |
| &profile_buffer_length, &luid, &token, &limits, &substs); |
| LsaFreeReturnBuffer(profile_buffer); |
| std::unique_ptr<char[]> logon_token_info = GetTokenInformation(token); |
| CloseHandle(token); |
| if (sts != S_OK) |
| return LsaNtStatusToWinError(sts); |
| if (!logon_token_info) |
| return ERROR_NOT_ENOUGH_MEMORY; |
| |
| PSID cur_sid = reinterpret_cast<TOKEN_USER*>(cur_token_info_.get())->User.Sid; |
| PSID logon_sid = |
| reinterpret_cast<TOKEN_USER*>(logon_token_info.get())->User.Sid; |
| return EqualSid(cur_sid, logon_sid) ? ERROR_SUCCESS : ERROR_LOGON_FAILURE; |
| } |
| |
| std::unique_ptr<char[]> CredentialBufferValidator::GetTokenInformation( |
| HANDLE token) { |
| DWORD token_info_length = 0; |
| ::GetTokenInformation(token, TokenUser, nullptr, 0, &token_info_length); |
| if (ERROR_INSUFFICIENT_BUFFER != GetLastError()) |
| return nullptr; |
| |
| std::unique_ptr<char[]> token_info_buffer(new char[token_info_length]); |
| if (!::GetTokenInformation(token, TokenUser, token_info_buffer.get(), |
| token_info_length, &token_info_length)) { |
| return nullptr; |
| } |
| |
| return token_info_buffer; |
| } |
| |
| // TODO(crbug.com/574581) Remove this feature once this is confirmed to work |
| // as expected. |
| const base::Feature kCredUIPromptForWindowsCredentialsFeature{ |
| "CredUIPromptForWindowsCredentials", base::FEATURE_ENABLED_BY_DEFAULT}; |
| |
| void PasswordCheckPrefs::Read(PrefService* local_state) { |
| blank_password_ = |
| local_state->GetBoolean(password_manager::prefs::kOsPasswordBlank); |
| pref_last_changed_ = |
| local_state->GetInt64(password_manager::prefs::kOsPasswordLastChanged); |
| } |
| |
| void PasswordCheckPrefs::Write(PrefService* local_state) { |
| local_state->SetBoolean(password_manager::prefs::kOsPasswordBlank, |
| blank_password_); |
| local_state->SetInt64(password_manager::prefs::kOsPasswordLastChanged, |
| pref_last_changed_); |
| } |
| |
| int64_t GetPasswordLastChanged(const WCHAR* username) { |
| LPUSER_INFO_1 user_info = NULL; |
| DWORD age = 0; |
| |
| NET_API_STATUS ret = NetUserGetInfo(NULL, username, 1, |
| reinterpret_cast<LPBYTE*>(&user_info)); |
| |
| if (ret == NERR_Success) { |
| // Returns seconds since last password change. |
| age = user_info->usri1_password_age; |
| NetApiBufferFree(user_info); |
| } else { |
| return -1; |
| } |
| |
| base::Time changed = base::Time::Now() - base::TimeDelta::FromSeconds(age); |
| |
| return changed.ToInternalValue(); |
| } |
| |
| bool CheckBlankPasswordWithPrefs(const WCHAR* username, |
| PasswordCheckPrefs* prefs) { |
| // If the user name has a backslash, then it is of the form DOMAIN\username. |
| // NetUserGetInfo() (called from GetPasswordLastChanged()) as well as |
| // LogonUser() below only wants the username portion. |
| LPCWSTR backslash = wcschr(username, L'\\'); |
| if (backslash) |
| username = backslash + 1; |
| |
| int64_t last_changed = GetPasswordLastChanged(username); |
| |
| // If we cannot determine when the password was last changed |
| // then assume the password is not blank |
| if (last_changed == -1) |
| return false; |
| |
| bool blank_password = prefs->blank_password_; |
| bool need_recheck = true; |
| if (prefs->pref_last_changed_ > 0 && |
| last_changed <= prefs->pref_last_changed_) { |
| need_recheck = false; |
| } |
| |
| if (need_recheck) { |
| HANDLE handle = INVALID_HANDLE_VALUE; |
| |
| // Attempt to login using blank password. |
| DWORD logon_result = LogonUser(username, |
| L".", |
| L"", |
| LOGON32_LOGON_INTERACTIVE, |
| LOGON32_PROVIDER_DEFAULT, |
| &handle); |
| |
| auto last_error = GetLastError(); |
| // Win XP and later return ERROR_ACCOUNT_RESTRICTION for blank password. |
| if (logon_result) |
| CloseHandle(handle); |
| |
| // In the case the password is blank, then LogonUser returns a failure, |
| // handle is INVALID_HANDLE_VALUE, and GetLastError() is |
| // ERROR_ACCOUNT_RESTRICTION. |
| // http://msdn.microsoft.com/en-us/library/windows/desktop/ms681385 |
| blank_password = (logon_result || |
| last_error == ERROR_ACCOUNT_RESTRICTION); |
| } |
| |
| // Account for clock skew between pulling the password age and |
| // writing to the preferences by adding a small skew factor here. |
| last_changed += base::Time::kMicrosecondsPerSecond; |
| |
| // Update the preferences with new values. |
| prefs->pref_last_changed_ = last_changed; |
| prefs->blank_password_ = blank_password; |
| return blank_password; |
| } |
| |
| // Wrapper around CheckBlankPasswordWithPrefs to be called on UI thread. |
| bool CheckBlankPassword(const WCHAR* username) { |
| PrefService* local_state = g_browser_process->local_state(); |
| PasswordCheckPrefs prefs; |
| prefs.Read(local_state); |
| bool result = CheckBlankPasswordWithPrefs(username, &prefs); |
| prefs.Write(local_state); |
| return result; |
| } |
| |
| void GetOsPasswordStatusInternal(PasswordCheckPrefs* prefs, |
| OsPasswordStatus* status) { |
| DWORD username_length = CREDUI_MAX_USERNAME_LENGTH; |
| WCHAR username[CREDUI_MAX_USERNAME_LENGTH+1] = {}; |
| *status = PASSWORD_STATUS_UNKNOWN; |
| |
| if (GetUserNameEx(NameUserPrincipal, username, &username_length)) { |
| // If we are on a domain, it is almost certain that the password is not |
| // blank, but we do not actively check any further than this to avoid any |
| // failed login attempts hitting the domain controller. |
| *status = PASSWORD_STATUS_WIN_DOMAIN; |
| } else { |
| username_length = CREDUI_MAX_USERNAME_LENGTH; |
| if (GetUserName(username, &username_length)) { |
| *status = CheckBlankPasswordWithPrefs(username, prefs) ? |
| PASSWORD_STATUS_BLANK : |
| PASSWORD_STATUS_NONBLANK; |
| } |
| } |
| } |
| |
| void ReplyOsPasswordStatus(std::unique_ptr<PasswordCheckPrefs> prefs, |
| std::unique_ptr<OsPasswordStatus> status) { |
| PrefService* local_state = g_browser_process->local_state(); |
| prefs->Write(local_state); |
| UMA_HISTOGRAM_ENUMERATION("PasswordManager.OsPasswordStatus", *status, |
| MAX_PASSWORD_STATUS); |
| } |
| |
| void GetOsPasswordStatus() { |
| // Preferences can be accessed on the UI thread only. |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| PrefService* local_state = g_browser_process->local_state(); |
| std::unique_ptr<PasswordCheckPrefs> prefs(new PasswordCheckPrefs); |
| prefs->Read(local_state); |
| std::unique_ptr<OsPasswordStatus> status( |
| new OsPasswordStatus(PASSWORD_STATUS_UNKNOWN)); |
| PasswordCheckPrefs* prefs_weak = prefs.get(); |
| OsPasswordStatus* status_weak = status.get(); |
| // This task calls ::LogonUser(), hence MayBlock(). |
| base::PostTaskWithTraitsAndReply( |
| FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT}, |
| base::Bind(&GetOsPasswordStatusInternal, prefs_weak, status_weak), |
| base::Bind(&ReplyOsPasswordStatus, base::Passed(&prefs), |
| base::Passed(&status))); |
| } |
| |
| // Authenticate the user using the old Windows credential prompt. |
| // TODO(crbug.com/574581) Remove this feature once this is confirmed to work |
| // as expected. |
| bool AuthenticateUserOld(gfx::NativeWindow window, |
| password_manager::ReauthPurpose purpose) { |
| bool retval = false; |
| CREDUI_INFO cui = {}; |
| WCHAR username[CREDUI_MAX_USERNAME_LENGTH+1] = {}; |
| WCHAR displayname[CREDUI_MAX_USERNAME_LENGTH+1] = {}; |
| WCHAR password[CREDUI_MAX_PASSWORD_LENGTH+1] = {}; |
| DWORD username_length = CREDUI_MAX_USERNAME_LENGTH; |
| base::string16 product_name = l10n_util::GetStringUTF16(IDS_PRODUCT_NAME); |
| base::string16 password_prompt; |
| switch (purpose) { |
| case password_manager::ReauthPurpose::VIEW_PASSWORD: |
| password_prompt = |
| l10n_util::GetStringUTF16(IDS_PASSWORDS_PAGE_AUTHENTICATION_PROMPT); |
| break; |
| case password_manager::ReauthPurpose::EXPORT: |
| password_prompt = l10n_util::GetStringUTF16( |
| IDS_PASSWORDS_PAGE_EXPORT_AUTHENTICATION_PROMPT); |
| break; |
| } |
| HANDLE handle = INVALID_HANDLE_VALUE; |
| size_t tries = 0; |
| bool use_displayname = false; |
| bool use_principalname = false; |
| DWORD logon_result = 0; |
| |
| // On a domain, we obtain the User Principal Name |
| // for domain authentication. |
| if (GetUserNameEx(NameUserPrincipal, username, &username_length)) { |
| use_principalname = true; |
| } else { |
| username_length = CREDUI_MAX_USERNAME_LENGTH; |
| // Otherwise, we're a workstation, use the plain local username. |
| if (!GetUserName(username, &username_length)) { |
| DLOG(ERROR) << "Unable to obtain username " << GetLastError(); |
| return false; |
| } else { |
| // As we are on a workstation, it's possible the user |
| // has no password, so check here. |
| if (CheckBlankPassword(username)) |
| return true; |
| } |
| } |
| |
| // Try and obtain a friendly display name. |
| username_length = CREDUI_MAX_USERNAME_LENGTH; |
| if (GetUserNameEx(NameDisplay, displayname, &username_length)) |
| use_displayname = true; |
| |
| cui.cbSize = sizeof(CREDUI_INFO); |
| cui.hwndParent = NULL; |
| cui.hwndParent = window->GetHost()->GetAcceleratedWidget(); |
| |
| cui.pszMessageText = password_prompt.c_str(); |
| cui.pszCaptionText = product_name.c_str(); |
| |
| cui.hbmBanner = NULL; |
| BOOL save_password = FALSE; |
| DWORD credErr = NO_ERROR; |
| |
| do { |
| tries++; |
| |
| // TODO(wfh) Make sure we support smart cards here. |
| credErr = CredUIPromptForCredentials( |
| &cui, |
| product_name.c_str(), |
| NULL, |
| 0, |
| use_displayname ? displayname : username, |
| CREDUI_MAX_USERNAME_LENGTH+1, |
| password, |
| CREDUI_MAX_PASSWORD_LENGTH+1, |
| &save_password, |
| kCredUiDefaultFlags | |
| (tries > 1 ? CREDUI_FLAGS_INCORRECT_PASSWORD : 0)); |
| |
| if (credErr == NO_ERROR) { |
| logon_result = LogonUser(username, |
| use_principalname ? NULL : L".", |
| password, |
| LOGON32_LOGON_INTERACTIVE, |
| LOGON32_PROVIDER_DEFAULT, |
| &handle); |
| if (logon_result) { |
| retval = true; |
| CloseHandle(handle); |
| } else { |
| if (GetLastError() == ERROR_ACCOUNT_RESTRICTION && |
| wcslen(password) == 0) { |
| // Password is blank, so permit. |
| retval = true; |
| } else { |
| DLOG(WARNING) << "Unable to authenticate " << GetLastError(); |
| } |
| } |
| SecureZeroMemory(password, sizeof(password)); |
| } |
| } while (credErr == NO_ERROR && |
| (retval == false && tries < kMaxPasswordRetries)); |
| return retval; |
| } |
| |
| // Authenticate the user using the new Windows credential prompt. The new |
| // prompt allows the user to authenticate using additional credential providers, |
| // such as PINs, smartcards, fingerprint scanners, and so on. It also still |
| // allows the user to authenticate with their password. This old prompt only |
| // supported password authentication which is not enough for enterprise |
| // environments. |
| bool AuthenticateUserNew(gfx::NativeWindow window, |
| password_manager::ReauthPurpose purpose) { |
| bool retval = false; |
| WCHAR cur_username[CREDUI_MAX_USERNAME_LENGTH + 1] = {}; |
| DWORD cur_username_length = base::size(cur_username); |
| |
| // If this is a standlone workstation, it's possible the current user has no |
| // password, so check here and allow it. |
| if (!GetUserNameEx(NameSamCompatible, cur_username, &cur_username_length)) { |
| DLOG(ERROR) << "Unable to obtain username " << GetLastError(); |
| return false; |
| } |
| |
| if (!base::win::IsEnrolledToDomain() && CheckBlankPassword(cur_username)) |
| return true; |
| |
| // Build the strings to display in the credential UI. If these strings are |
| // left empty on domain joined machines, CredUIPromptForWindowsCredentials() |
| // fails to run. |
| base::string16 product_name = l10n_util::GetStringUTF16(IDS_PRODUCT_NAME); |
| base::string16 password_prompt; |
| switch (purpose) { |
| case password_manager::ReauthPurpose::VIEW_PASSWORD: |
| password_prompt = |
| l10n_util::GetStringUTF16(IDS_PASSWORDS_PAGE_AUTHENTICATION_PROMPT); |
| break; |
| case password_manager::ReauthPurpose::EXPORT: |
| password_prompt = l10n_util::GetStringUTF16( |
| IDS_PASSWORDS_PAGE_EXPORT_AUTHENTICATION_PROMPT); |
| break; |
| } |
| CREDUI_INFO cui; |
| cui.cbSize = sizeof(cui); |
| cui.hwndParent = window->GetHost()->GetAcceleratedWidget(); |
| cui.pszMessageText = password_prompt.c_str(); |
| cui.pszCaptionText = product_name.c_str(); |
| cui.hbmBanner = nullptr; |
| |
| CredentialBufferValidator validator; |
| |
| DWORD err = 0; |
| size_t tries = 0; |
| do { |
| tries++; |
| |
| // Show credential prompt, displaying error from previous try if needed. |
| // TODO(wfh): Make sure we support smart cards here. |
| ULONG auth_package = 0; |
| LPVOID cred_buffer = nullptr; |
| ULONG cred_buffer_size = 0; |
| err = CredUIPromptForWindowsCredentials( |
| &cui, err, &auth_package, nullptr, 0, &cred_buffer, &cred_buffer_size, |
| nullptr, CREDUIWIN_ENUMERATE_CURRENT_USER); |
| if (err != ERROR_SUCCESS) |
| break; |
| |
| // While CredUIPromptForWindowsCredentials() shows the currently logged |
| // on user by default, it can be changed at runtime. This is important, |
| // as it allows users to change to a different type of authentication |
| // mechanism, such as PIN or smartcard. However, this also allows the |
| // user to change to a completely different account on the machine. Make |
| // sure the user authenticated with the credentials of the currently |
| // logged on user. |
| err = validator.IsValid(auth_package, cred_buffer, cred_buffer_size); |
| retval = err == ERROR_SUCCESS; |
| } while (!retval && tries < kMaxPasswordRetries); |
| |
| return retval; |
| } |
| |
| } // namespace |
| |
| void DelayReportOsPassword() { |
| base::PostDelayedTaskWithTraits(FROM_HERE, {content::BrowserThread::UI}, |
| base::BindOnce(&GetOsPasswordStatus), |
| base::TimeDelta::FromSeconds(40)); |
| } |
| |
| bool AuthenticateUser(gfx::NativeWindow window, |
| password_manager::ReauthPurpose purpose) { |
| return base::FeatureList::IsEnabled(kCredUIPromptForWindowsCredentialsFeature) |
| ? AuthenticateUserNew(window, purpose) |
| : AuthenticateUserOld(window, purpose); |
| } |
| |
| } // namespace password_manager_util_win |