blob: 05606eca68997b963c4cee2f5698afa81f7198b6 [file] [log] [blame]
// Copyright 2018 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.
#include "chrome/credential_provider/gaiacp/os_user_manager.h"
#include <windows.h>
#include <lm.h>
#include <Shellapi.h> // For <Shlobj.h>
#include <Shlobj.h> // For SHFileOperation()
#include <sddl.h> // For ConvertSidToStringSid()
#include <userenv.h> // For GetUserProfileDirectory()
#include <wincrypt.h> // For CryptXXX()
#include <atlconv.h>
#include <malloc.h>
#include <memory.h>
#include <stdlib.h>
#include <iomanip>
#include <memory>
#include "base/files/file_path.h"
#include "base/macros.h"
#include "base/scoped_native_library.h"
#include "base/stl_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/win/registry.h"
#include "base/win/win_util.h"
#include "chrome/credential_provider/gaiacp/gcp_utils.h"
#include "chrome/credential_provider/gaiacp/logging.h"
namespace credential_provider {
// static
OSUserManager** OSUserManager::GetInstanceStorage() {
static OSUserManager* instance = new OSUserManager();
return &instance;
}
// static
OSUserManager* OSUserManager::Get() {
return *GetInstanceStorage();
}
// static
void OSUserManager::SetInstanceForTesting(OSUserManager* instance) {
*GetInstanceStorage() = instance;
}
OSUserManager::~OSUserManager() {}
#define IS_PASSWORD_STRONG_ENOUGH() \
(cur_length > kMinPasswordLength) && \
(has_upper + has_lower + has_digit + has_punct > 3)
HRESULT OSUserManager::GenerateRandomPassword(wchar_t* password, int length) {
HRESULT hr;
HCRYPTPROV prov;
// TODO(rogerta): read password policy GPOs to see what the password policy
// is for this machine in order to create one that adheres correctly. For
// now will generate a random password that fits typical strong password
// policies on windows.
const wchar_t kValidPasswordChars[] =
L"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
L"abcdefghijklmnopqrstuvwxyz"
L"`1234567890-="
L"~!@#$%^&*()_+"
L"[]\\;',./"
L"{}|:\"<>?";
if (length < kMinPasswordLength)
return E_INVALIDARG;
if (!::CryptAcquireContext(&prov, nullptr, nullptr, PROV_RSA_FULL,
CRYPT_VERIFYCONTEXT | CRYPT_SILENT)) {
hr = HRESULT_FROM_WIN32(::GetLastError());
LOGFN(ERROR) << "CryptAcquireContext hr=" << putHR(hr);
return hr;
}
int cur_length;
int has_upper;
int has_lower;
int has_digit;
int has_punct;
do {
cur_length = 0;
has_upper = 0;
has_lower = 0;
has_digit = 0;
has_punct = 0;
wchar_t* p = password;
int remaining_length = length;
while (remaining_length > 1) {
BYTE r;
if (!::CryptGenRandom(prov, sizeof(r), &r)) {
hr = HRESULT_FROM_WIN32(::GetLastError());
LOGFN(ERROR) << "CryptGenRandom hr=" << putHR(hr);
::CryptReleaseContext(prov, 0);
return hr;
}
wchar_t c =
kValidPasswordChars[r % (base::size(kValidPasswordChars) - 1)];
*p++ = c;
++cur_length;
--remaining_length;
// Check if we have all the requirements for a strong password.
if (isupper(c))
has_upper = 1;
if (islower(c))
has_lower = 1;
if (isdigit(c))
has_digit = 1;
if (ispunct(c))
has_punct = 1;
if (IS_PASSWORD_STRONG_ENOUGH())
break;
}
// Make sure password is terminated.
*p = 0;
// Because this is a random function, there is a chance that the password
// might not be strong enough by the time the code reaches this point. This
// could happen if two categories of characters were never included.
//
// This highest probability of this happening would be if the caller
// specified the shortest password allowed and the missing characters came
// from the smallest two sets (say digits and lower case letters).
//
// This probability is equal to 1 minus the probability that all characters
// are upper case or punctuation:
//
// P(!strong) = 1 - P(all chars either upper or punctuation)
//
// There are valid 96 characters in all, of which 56 are not digits or
// lower case. The probability that any single character is upper case or
// punctuation is 56/96. If the minimum length is used, then:
//
// P(all chars either upper or punctuation) = (56/96)^23 = 4.1e-6
//
// P(!strong) = 1 - 1.8e-4 = 0.999996
//
// or about 4 in every million. The exponent is 23 and not 24 because the
// minimum password length includes the null terminiator.
//
// This means:
// - ~4 in every million will run this loop at least twice
// - ~4 in every 250 billion will run this loop at least thrice
// - ~4 in every 6.25e13 will run this loop at least four times
} while (!IS_PASSWORD_STRONG_ENOUGH());
::CryptReleaseContext(prov, 0);
return S_OK;
}
HRESULT OSUserManager::AddUser(const wchar_t* username,
const wchar_t* password,
const wchar_t* fullname,
const wchar_t* comment,
bool add_to_users_group,
BSTR* sid,
DWORD* error) {
DCHECK(sid);
bool user_found = false;
USER_INFO_1 info;
memset(&info, 0, sizeof(info));
info.usri1_comment = _wcsdup(comment);
info.usri1_flags =
UF_PASSWD_CANT_CHANGE | UF_DONT_EXPIRE_PASSWD | UF_NORMAL_ACCOUNT;
info.usri1_name = const_cast<wchar_t*>(username);
info.usri1_password = const_cast<wchar_t*>(password);
info.usri1_priv = USER_PRIV_USER;
NET_API_STATUS nsts =
::NetUserAdd(nullptr, 1, reinterpret_cast<LPBYTE>(&info), error);
free(info.usri1_comment);
// Set the user's full name.
if (nsts == NERR_Success) {
USER_INFO_1011 info1011;
memset(&info1011, 0, sizeof(info1011));
info1011.usri1011_full_name = const_cast<wchar_t*>(fullname);
nsts = ::NetUserSetInfo(nullptr, info.usri1_name, 1011,
reinterpret_cast<LPBYTE>(&info1011), error);
if (nsts != NERR_Success) {
LOGFN(ERROR) << "NetUserSetInfo(set full name) nsts=" << nsts;
}
} else if (nsts == NERR_UserExists) {
// TODO: If adding the special "gaia" account might want to check that
// account permissions are not too permissive.
LOGFN(INFO) << "Using existing user '" << username << "'";
user_found = true;
} else {
LOGFN(ERROR) << "NetUserAdd nsts=" << nsts;
return HRESULT_FROM_WIN32(nsts);
}
// Get the new user's SID and return it to caller.
LPBYTE buffer = nullptr;
nsts = ::NetUserGetInfo(nullptr, info.usri1_name, 4, &buffer);
if (nsts == NERR_Success) {
const USER_INFO_4* user_info = reinterpret_cast<const USER_INFO_4*>(buffer);
wchar_t* sidstr = nullptr;
if (::ConvertSidToStringSid(user_info->usri4_user_sid, &sidstr)) {
*sid = SysAllocString(T2COLE(sidstr));
LOGFN(INFO) << "sid=" << sidstr;
::LocalFree(sidstr);
} else {
LOGFN(ERROR) << "Could not convert SID to string";
*sid = nullptr;
nsts = NERR_ProgNeedsExtraMem;
}
if (nsts == NERR_Success && add_to_users_group) {
// Add to the "Users" group so that it appears on login screen.
LOCALGROUP_MEMBERS_INFO_0 member_info;
memset(&member_info, 0, sizeof(member_info));
member_info.lgrmi0_sid = user_info->usri4_user_sid;
nsts = ::NetLocalGroupAddMembers(
nullptr, L"Users", 0, reinterpret_cast<LPBYTE>(&member_info), 1);
if (nsts != NERR_Success && nsts != ERROR_MEMBER_IN_ALIAS) {
LOGFN(ERROR) << "NetLocalGroupAddMembers nsts=" << nsts;
} else {
nsts = NERR_Success;
}
}
::NetApiBufferFree(buffer);
}
return (nsts == NERR_Success && user_found)
? HRESULT_FROM_WIN32(NERR_UserExists)
: (nsts == NERR_Success ? S_OK : HRESULT_FROM_WIN32(nsts));
}
HRESULT OSUserManager::ChangeUserPassword(const wchar_t* username,
const wchar_t* old_password,
const wchar_t* new_password) {
// Remove the UF_PASSWD_CANT_CHANGE flag temporarily so that the password can
// be changed.
LPBYTE buffer = nullptr;
NET_API_STATUS nsts = ::NetUserGetInfo(nullptr, username, 4, &buffer);
if (nsts != NERR_Success) {
LOGFN(ERROR) << "NetUserGetInfo(get password change flag) nsts=" << nsts;
return HRESULT_FROM_WIN32(nsts);
}
USER_INFO_4* user_info = reinterpret_cast<USER_INFO_4*>(buffer);
DWORD original_user_flags = user_info->usri4_flags;
user_info->usri4_flags &= ~UF_PASSWD_CANT_CHANGE;
nsts = ::NetUserSetInfo(nullptr, username, 4, buffer, nullptr);
if (nsts != NERR_Success) {
LOGFN(ERROR) << "NetUserSetInfo(allow password change) nsts=" << nsts;
::NetApiBufferFree(buffer);
return HRESULT_FROM_WIN32(nsts);
}
NET_API_STATUS changepassword_nsts =
::NetUserChangePassword(L"\\\\.", username, old_password, new_password);
if (changepassword_nsts != NERR_Success) {
LOGFN(ERROR) << "Unable to change password for '" << username
<< "' nsts=" << changepassword_nsts;
}
user_info->usri4_flags = original_user_flags;
nsts = ::NetUserSetInfo(nullptr, username, 4, buffer, nullptr);
if (nsts != NERR_Success) {
LOGFN(ERROR) << "NetUserSetInfo(reset password change flag) nsts=" << nsts;
}
::NetApiBufferFree(buffer);
return HRESULT_FROM_WIN32(changepassword_nsts);
}
HRESULT OSUserManager::IsWindowsPasswordValid(const wchar_t* username,
const wchar_t* password) {
// Check if the user exists before trying to log them on, because an error
// of ERROR_LOGON_FAILURE will be returned if the user does not exist
// or if the password is invalid. This function only wants to return
// S_FALSE on an ERROR_LOGON_FAILURE if the user exists.
PSID sid;
HRESULT hr = GetUserSID(username, &sid);
if (SUCCEEDED(hr)) {
::LocalFree(sid);
base::win::ScopedHandle handle;
hr = CreateLogonToken(username, password, /*interactive=*/true, &handle);
return hr == HRESULT_FROM_WIN32(ERROR_LOGON_FAILURE) ? S_FALSE : hr;
}
return hr;
}
HRESULT OSUserManager::CreateLogonToken(const wchar_t* username,
const wchar_t* password,
bool interactive,
base::win::ScopedHandle* token) {
return ::credential_provider::CreateLogonToken(username, password,
interactive, token);
}
HRESULT OSUserManager::GetUserSID(const wchar_t* username, PSID* sid) {
DCHECK(username);
DCHECK(sid);
LPBYTE buffer = nullptr;
NET_API_STATUS nsts = ::NetUserGetInfo(nullptr, username, 4, &buffer);
if (nsts == NERR_Success) {
const USER_INFO_4* user_info = reinterpret_cast<const USER_INFO_4*>(buffer);
if (::IsValidSid(user_info->usri4_user_sid)) {
DWORD sid_length = GetLengthSid(user_info->usri4_user_sid);
*sid = ::LocalAlloc(LMEM_FIXED, sid_length);
::CopySid(sid_length, *sid, user_info->usri4_user_sid);
} else {
LOGFN(ERROR) << "Invalid SID for username=" << username;
}
::NetApiBufferFree(buffer);
} else {
LOGFN(ERROR) << "NetUserGetInfo nsts=" << nsts;
}
return HRESULT_FROM_WIN32(nsts);
}
HRESULT OSUserManager::FindUserBySID(const wchar_t* sid,
wchar_t* username,
DWORD length) {
PSID psid;
if (!::ConvertStringSidToSidW(sid, &psid)) {
HRESULT hr = HRESULT_FROM_WIN32(::GetLastError());
LOGFN(ERROR) << "ConvertStringSidToSidW sid=" << sid << " hr=" << putHR(hr);
return hr;
}
// Maximum domain length is 256 characters including null.
// https://support.microsoft.com/en-ca/help/909264/naming-conventions-in-active-directory-for-computers-domains-sites-and
HRESULT hr = S_OK;
DWORD name_length = username ? length : 0;
wchar_t nt_domain[256];
DWORD domain_length = base::size(nt_domain);
SID_NAME_USE use;
if (!::LookupAccountSid(nullptr, psid, username, &name_length, nt_domain,
&domain_length, &use)) {
hr = HRESULT_FROM_WIN32(::GetLastError());
if (hr != HRESULT_FROM_WIN32(ERROR_NONE_MAPPED)) {
if (length == 0 && hr == HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER)) {
hr = S_OK;
} else {
LOGFN(ERROR) << "LookupAccountSid hr=" << putHR(hr);
}
}
}
LOGFN(INFO) << "username=" << username << " ntdomain=" << nt_domain;
::LocalFree(psid);
return hr;
}
HRESULT OSUserManager::RemoveUser(const wchar_t* username,
const wchar_t* password) {
DCHECK(username);
DCHECK(password);
// Get the user's profile directory.
base::win::ScopedHandle token;
wchar_t profiledir[MAX_PATH + 1];
// Get the user's profile directory. Try a batch logon first, and if that
// fails then try an interactive logon.
HRESULT hr =
CreateLogonToken(username, password, /*interactive=*/false, &token);
if (FAILED(hr))
hr = CreateLogonToken(username, password, /*interactive=*/true, &token);
if (SUCCEEDED(hr)) {
// Get the gaia user's profile directory so that it can be deleted.
DWORD length = base::size(profiledir) - 1;
if (!::GetUserProfileDirectory(token.Get(), profiledir, &length)) {
hr = HRESULT_FROM_WIN32(::GetLastError());
if (hr != HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND))
LOGFN(ERROR) << "GetUserProfileDirectory hr=" << putHR(hr);
profiledir[0] = 0;
} else {
// Double null terminate the profile directory for SHFileOperation().
profiledir[length] = 0;
}
} else {
LOGFN(ERROR) << "CreateLogonToken hr=" << putHR(hr);
}
// Remove the OS user.
NET_API_STATUS nsts = ::NetUserDel(nullptr, username);
if (nsts != NERR_Success)
LOGFN(ERROR) << "NetUserDel nsts=" << nsts;
// Force delete the user's profile directory.
if (profiledir[0] != 0) {
SHFILEOPSTRUCT op;
memset(&op, 0, sizeof(op));
op.wFunc = FO_DELETE;
op.pFrom = profiledir; // Double null terminated above.
op.fFlags = FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_NO_UI | FOF_SILENT;
int ret = ::SHFileOperation(&op);
if (ret != 0) {
LOGFN(ERROR) << "SHFileOperation ret=" << ret;
}
}
return S_OK;
}
} // namespace credential_provider