summaryrefslogtreecommitdiffstats
path: root/browser/components/shell/WindowsUserChoice.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/shell/WindowsUserChoice.cpp')
-rw-r--r--browser/components/shell/WindowsUserChoice.cpp422
1 files changed, 422 insertions, 0 deletions
diff --git a/browser/components/shell/WindowsUserChoice.cpp b/browser/components/shell/WindowsUserChoice.cpp
new file mode 100644
index 0000000000..4d6f24704a
--- /dev/null
+++ b/browser/components/shell/WindowsUserChoice.cpp
@@ -0,0 +1,422 @@
+/* -*- 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/. */
+
+/*
+ * Generate and check the UserChoice Hash, which protects file and protocol
+ * associations on Windows 10.
+ *
+ * NOTE: This is also used in the WDBA, so it avoids XUL and XPCOM.
+ *
+ * References:
+ * - PS-SFTA by Danysys <https://github.com/DanysysTeam/PS-SFTA>
+ * - based on a PureBasic version by LMongrain
+ * <https://github.com/DanysysTeam/SFTA>
+ * - AssocHashGen by "halfmeasuresdisabled", see bug 1225660 and
+ * <https://www.reddit.com/r/ReverseEngineering/comments/3t7q9m/assochashgen_a_reverse_engineered_version_of/>
+ * - SetUserFTA changelog
+ * <https://kolbi.cz/blog/2017/10/25/setuserfta-userchoice-hash-defeated-set-file-type-associations-per-user/>
+ */
+
+#include <windows.h>
+#include <sddl.h> // for ConvertSidToStringSidW
+#include <wincrypt.h> // for CryptoAPI base64
+#include <bcrypt.h> // for CNG MD5
+#include <winternl.h> // for NT_SUCCESS()
+
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/UniquePtr.h"
+#include "nsWindowsHelpers.h"
+
+#include "WindowsUserChoice.h"
+
+using namespace mozilla;
+
+UniquePtr<wchar_t[]> GetCurrentUserStringSid() {
+ HANDLE rawProcessToken;
+ if (!::OpenProcessToken(::GetCurrentProcess(), TOKEN_QUERY,
+ &rawProcessToken)) {
+ return nullptr;
+ }
+ nsAutoHandle processToken(rawProcessToken);
+
+ DWORD userSize = 0;
+ if (!(!::GetTokenInformation(processToken.get(), TokenUser, nullptr, 0,
+ &userSize) &&
+ GetLastError() == ERROR_INSUFFICIENT_BUFFER)) {
+ return nullptr;
+ }
+
+ auto userBytes = MakeUnique<unsigned char[]>(userSize);
+ if (!::GetTokenInformation(processToken.get(), TokenUser, userBytes.get(),
+ userSize, &userSize)) {
+ return nullptr;
+ }
+
+ wchar_t* rawSid = nullptr;
+ if (!::ConvertSidToStringSidW(
+ reinterpret_cast<PTOKEN_USER>(userBytes.get())->User.Sid, &rawSid)) {
+ return nullptr;
+ }
+ UniquePtr<wchar_t, LocalFreeDeleter> sid(rawSid);
+
+ // Copy instead of passing UniquePtr<wchar_t, LocalFreeDeleter> back to
+ // the caller.
+ int sidLen = ::lstrlenW(sid.get()) + 1;
+ auto outSid = MakeUnique<wchar_t[]>(sidLen);
+ memcpy(outSid.get(), sid.get(), sidLen * sizeof(wchar_t));
+
+ return outSid;
+}
+
+/*
+ * Create the string which becomes the input to the UserChoice hash.
+ *
+ * @see GenerateUserChoiceHash() for parameters.
+ *
+ * @return The formatted string, nullptr on failure.
+ *
+ * NOTE: This uses the format as of Windows 10 20H2 (latest as of this writing),
+ * used at least since 1803.
+ * There was at least one older version, not currently supported: On Win10 RTM
+ * (build 10240, aka 1507) the hash function is the same, but the timestamp and
+ * User Experience string aren't included; instead (for protocols) the string
+ * ends with the exe path. The changelog of SetUserFTA suggests the algorithm
+ * changed in 1703, so there may be two versions: before 1703, and 1703 to now.
+ */
+static UniquePtr<wchar_t[]> FormatUserChoiceString(const wchar_t* aExt,
+ const wchar_t* aUserSid,
+ const wchar_t* aProgId,
+ SYSTEMTIME aTimestamp) {
+ aTimestamp.wSecond = 0;
+ aTimestamp.wMilliseconds = 0;
+
+ FILETIME fileTime = {0};
+ if (!::SystemTimeToFileTime(&aTimestamp, &fileTime)) {
+ return nullptr;
+ }
+
+ // This string is built into Windows as part of the UserChoice hash algorithm.
+ // It might vary across Windows SKUs (e.g. Windows 10 vs. Windows Server), or
+ // across builds of the same SKU, but this is the only currently known
+ // version. There isn't any known way of deriving it, so we assume this
+ // constant value. If we are wrong, we will not be able to generate correct
+ // UserChoice hashes.
+ const wchar_t* userExperience =
+ L"User Choice set via Windows User Experience "
+ L"{D18B6DD5-6124-4341-9318-804003BAFA0B}";
+
+ const wchar_t* userChoiceFmt =
+ L"%s%s%s"
+ L"%08lx"
+ L"%08lx"
+ L"%s";
+ int userChoiceLen = _scwprintf(userChoiceFmt, aExt, aUserSid, aProgId,
+ fileTime.dwHighDateTime,
+ fileTime.dwLowDateTime, userExperience);
+ userChoiceLen += 1; // _scwprintf does not include the terminator
+
+ auto userChoice = MakeUnique<wchar_t[]>(userChoiceLen);
+ _snwprintf_s(userChoice.get(), userChoiceLen, _TRUNCATE, userChoiceFmt, aExt,
+ aUserSid, aProgId, fileTime.dwHighDateTime,
+ fileTime.dwLowDateTime, userExperience);
+
+ ::CharLowerW(userChoice.get());
+
+ return userChoice;
+}
+
+// @return The MD5 hash of the input, nullptr on failure.
+static UniquePtr<DWORD[]> CNG_MD5(const unsigned char* bytes, ULONG bytesLen) {
+ constexpr ULONG MD5_BYTES = 16;
+ constexpr ULONG MD5_DWORDS = MD5_BYTES / sizeof(DWORD);
+ UniquePtr<DWORD[]> hash;
+
+ BCRYPT_ALG_HANDLE hAlg = nullptr;
+ if (NT_SUCCESS(::BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_MD5_ALGORITHM,
+ nullptr, 0))) {
+ BCRYPT_HASH_HANDLE hHash = nullptr;
+ // As of Windows 7 the hash handle will manage its own object buffer when
+ // pbHashObject is nullptr and cbHashObject is 0.
+ if (NT_SUCCESS(
+ ::BCryptCreateHash(hAlg, &hHash, nullptr, 0, nullptr, 0, 0))) {
+ // BCryptHashData promises not to modify pbInput.
+ if (NT_SUCCESS(::BCryptHashData(hHash, const_cast<unsigned char*>(bytes),
+ bytesLen, 0))) {
+ hash = MakeUnique<DWORD[]>(MD5_DWORDS);
+ if (!NT_SUCCESS(::BCryptFinishHash(
+ hHash, reinterpret_cast<unsigned char*>(hash.get()),
+ MD5_DWORDS * sizeof(DWORD), 0))) {
+ hash.reset();
+ }
+ }
+ ::BCryptDestroyHash(hHash);
+ }
+ ::BCryptCloseAlgorithmProvider(hAlg, 0);
+ }
+
+ return hash;
+}
+
+// @return The input bytes encoded as base64, nullptr on failure.
+static UniquePtr<wchar_t[]> CryptoAPI_Base64Encode(const unsigned char* bytes,
+ DWORD bytesLen) {
+ DWORD base64Len = 0;
+ if (!::CryptBinaryToStringW(bytes, bytesLen,
+ CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF,
+ nullptr, &base64Len)) {
+ return nullptr;
+ }
+ auto base64 = MakeUnique<wchar_t[]>(base64Len);
+ if (!::CryptBinaryToStringW(bytes, bytesLen,
+ CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF,
+ base64.get(), &base64Len)) {
+ return nullptr;
+ }
+
+ return base64;
+}
+
+static inline DWORD WordSwap(DWORD v) { return (v >> 16) | (v << 16); }
+
+/*
+ * Generate the UserChoice Hash.
+ *
+ * This implementation is based on the references listed above.
+ * It is organized to show the logic as clearly as possible, but at some
+ * point the reasoning is just "this is how it works".
+ *
+ * @param inputString A null-terminated string to hash.
+ *
+ * @return The base64-encoded hash, or nullptr on failure.
+ */
+static UniquePtr<wchar_t[]> HashString(const wchar_t* inputString) {
+ auto inputBytes = reinterpret_cast<const unsigned char*>(inputString);
+ int inputByteCount = (::lstrlenW(inputString) + 1) * sizeof(wchar_t);
+
+ constexpr size_t DWORDS_PER_BLOCK = 2;
+ constexpr size_t BLOCK_SIZE = sizeof(DWORD) * DWORDS_PER_BLOCK;
+ // Incomplete blocks are ignored.
+ int blockCount = inputByteCount / BLOCK_SIZE;
+
+ if (blockCount == 0) {
+ return nullptr;
+ }
+
+ // Compute an MD5 hash. md5[0] and md5[1] will be used as constant multipliers
+ // in the scramble below.
+ auto md5 = CNG_MD5(inputBytes, inputByteCount);
+ if (!md5) {
+ return nullptr;
+ }
+
+ // The following loop effectively computes two checksums, scrambled like a
+ // hash after every DWORD is added.
+
+ // Constant multipliers for the scramble, one set for each DWORD in a block.
+ const DWORD C0s[DWORDS_PER_BLOCK][5] = {
+ {md5[0] | 1, 0xCF98B111uL, 0x87085B9FuL, 0x12CEB96DuL, 0x257E1D83uL},
+ {md5[1] | 1, 0xA27416F5uL, 0xD38396FFuL, 0x7C932B89uL, 0xBFA49F69uL}};
+ const DWORD C1s[DWORDS_PER_BLOCK][5] = {
+ {md5[0] | 1, 0xEF0569FBuL, 0x689B6B9FuL, 0x79F8A395uL, 0xC3EFEA97uL},
+ {md5[1] | 1, 0xC31713DBuL, 0xDDCD1F0FuL, 0x59C3AF2DuL, 0x35BD1EC9uL}};
+
+ // The checksums.
+ DWORD h0 = 0;
+ DWORD h1 = 0;
+ // Accumulated total of the checksum after each DWORD.
+ DWORD h0Acc = 0;
+ DWORD h1Acc = 0;
+
+ for (int i = 0; i < blockCount; ++i) {
+ for (size_t j = 0; j < DWORDS_PER_BLOCK; ++j) {
+ const DWORD* C0 = C0s[j];
+ const DWORD* C1 = C1s[j];
+
+ DWORD input;
+ memcpy(&input, &inputBytes[(i * DWORDS_PER_BLOCK + j) * sizeof(DWORD)],
+ sizeof(DWORD));
+
+ h0 += input;
+ // Scramble 0
+ h0 *= C0[0];
+ h0 = WordSwap(h0) * C0[1];
+ h0 = WordSwap(h0) * C0[2];
+ h0 = WordSwap(h0) * C0[3];
+ h0 = WordSwap(h0) * C0[4];
+ h0Acc += h0;
+
+ h1 += input;
+ // Scramble 1
+ h1 = WordSwap(h1) * C1[1] + h1 * C1[0];
+ h1 = (h1 >> 16) * C1[2] + h1 * C1[3];
+ h1 = WordSwap(h1) * C1[4] + h1;
+ h1Acc += h1;
+ }
+ }
+
+ DWORD hash[2] = {h0 ^ h1, h0Acc ^ h1Acc};
+
+ return CryptoAPI_Base64Encode(reinterpret_cast<const unsigned char*>(hash),
+ sizeof(hash));
+}
+
+UniquePtr<wchar_t[]> GetAssociationKeyPath(const wchar_t* aExt) {
+ const wchar_t* keyPathFmt;
+ if (aExt[0] == L'.') {
+ keyPathFmt =
+ L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\%s";
+ } else {
+ keyPathFmt =
+ L"SOFTWARE\\Microsoft\\Windows\\Shell\\Associations\\"
+ L"UrlAssociations\\%s";
+ }
+
+ int keyPathLen = _scwprintf(keyPathFmt, aExt);
+ keyPathLen += 1; // _scwprintf does not include the terminator
+
+ auto keyPath = MakeUnique<wchar_t[]>(keyPathLen);
+ _snwprintf_s(keyPath.get(), keyPathLen, _TRUNCATE, keyPathFmt, aExt);
+
+ return keyPath;
+}
+
+UniquePtr<wchar_t[]> GenerateUserChoiceHash(const wchar_t* aExt,
+ const wchar_t* aUserSid,
+ const wchar_t* aProgId,
+ SYSTEMTIME aTimestamp) {
+ auto userChoice = FormatUserChoiceString(aExt, aUserSid, aProgId, aTimestamp);
+ if (!userChoice) {
+ return nullptr;
+ }
+ return HashString(userChoice.get());
+}
+
+/*
+ * NOTE: The passed-in current user SID is used here, instead of getting the SID
+ * for the owner of the key. We are assuming that this key in HKCU is owned by
+ * the current user, since we want to replace that key ourselves. If the key is
+ * owned by someone else, then this check will fail; this is ok because we would
+ * likely not want to replace that other user's key anyway.
+ */
+CheckUserChoiceHashResult CheckUserChoiceHash(const wchar_t* aExt,
+ const wchar_t* aUserSid) {
+ auto keyPath = GetAssociationKeyPath(aExt);
+ if (!keyPath) {
+ return CheckUserChoiceHashResult::ERR_OTHER;
+ }
+
+ HKEY rawAssocKey;
+ if (::RegOpenKeyExW(HKEY_CURRENT_USER, keyPath.get(), 0, KEY_READ,
+ &rawAssocKey) != ERROR_SUCCESS) {
+ return CheckUserChoiceHashResult::ERR_OTHER;
+ }
+ nsAutoRegKey assocKey(rawAssocKey);
+
+ FILETIME lastWriteFileTime;
+ {
+ HKEY rawUserChoiceKey;
+ if (::RegOpenKeyExW(assocKey.get(), L"UserChoice", 0, KEY_READ,
+ &rawUserChoiceKey) != ERROR_SUCCESS) {
+ return CheckUserChoiceHashResult::ERR_OTHER;
+ }
+ nsAutoRegKey userChoiceKey(rawUserChoiceKey);
+
+ if (::RegQueryInfoKeyW(userChoiceKey.get(), nullptr, nullptr, nullptr,
+ nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
+ nullptr, &lastWriteFileTime) != ERROR_SUCCESS) {
+ return CheckUserChoiceHashResult::ERR_OTHER;
+ }
+ }
+
+ SYSTEMTIME lastWriteSystemTime;
+ if (!::FileTimeToSystemTime(&lastWriteFileTime, &lastWriteSystemTime)) {
+ return CheckUserChoiceHashResult::ERR_OTHER;
+ }
+
+ // Read ProgId
+ DWORD dataSizeBytes = 0;
+ if (::RegGetValueW(assocKey.get(), L"UserChoice", L"ProgId", RRF_RT_REG_SZ,
+ nullptr, nullptr, &dataSizeBytes) != ERROR_SUCCESS) {
+ return CheckUserChoiceHashResult::ERR_OTHER;
+ }
+ // +1 in case dataSizeBytes was odd, +1 to ensure termination
+ DWORD dataSizeChars = (dataSizeBytes / sizeof(wchar_t)) + 2;
+ UniquePtr<wchar_t[]> progId(new wchar_t[dataSizeChars]());
+ if (::RegGetValueW(assocKey.get(), L"UserChoice", L"ProgId", RRF_RT_REG_SZ,
+ nullptr, progId.get(), &dataSizeBytes) != ERROR_SUCCESS) {
+ return CheckUserChoiceHashResult::ERR_OTHER;
+ }
+
+ // Read Hash
+ dataSizeBytes = 0;
+ if (::RegGetValueW(assocKey.get(), L"UserChoice", L"Hash", RRF_RT_REG_SZ,
+ nullptr, nullptr, &dataSizeBytes) != ERROR_SUCCESS) {
+ return CheckUserChoiceHashResult::ERR_OTHER;
+ }
+ dataSizeChars = (dataSizeBytes / sizeof(wchar_t)) + 2;
+ UniquePtr<wchar_t[]> storedHash(new wchar_t[dataSizeChars]());
+ if (::RegGetValueW(assocKey.get(), L"UserChoice", L"Hash", RRF_RT_REG_SZ,
+ nullptr, storedHash.get(),
+ &dataSizeBytes) != ERROR_SUCCESS) {
+ return CheckUserChoiceHashResult::ERR_OTHER;
+ }
+
+ auto computedHash =
+ GenerateUserChoiceHash(aExt, aUserSid, progId.get(), lastWriteSystemTime);
+ if (!computedHash) {
+ return CheckUserChoiceHashResult::ERR_OTHER;
+ }
+
+ if (::CompareStringOrdinal(computedHash.get(), -1, storedHash.get(), -1,
+ FALSE) != CSTR_EQUAL) {
+ return CheckUserChoiceHashResult::ERR_MISMATCH;
+ }
+
+ return CheckUserChoiceHashResult::OK_V1;
+}
+
+bool CheckBrowserUserChoiceHashes() {
+ auto userSid = GetCurrentUserStringSid();
+ if (!userSid) {
+ return false;
+ }
+
+ const wchar_t* exts[] = {L"https", L"http", L".html", L".htm"};
+
+ for (size_t i = 0; i < ArrayLength(exts); ++i) {
+ switch (CheckUserChoiceHash(exts[i], userSid.get())) {
+ case CheckUserChoiceHashResult::OK_V1:
+ break;
+ case CheckUserChoiceHashResult::ERR_MISMATCH:
+ case CheckUserChoiceHashResult::ERR_OTHER:
+ return false;
+ }
+ }
+
+ return true;
+}
+
+UniquePtr<wchar_t[]> FormatProgID(const wchar_t* aProgIDBase,
+ const wchar_t* aAumi) {
+ const wchar_t* progIDFmt = L"%s-%s";
+ int progIDLen = _scwprintf(progIDFmt, aProgIDBase, aAumi);
+ progIDLen += 1; // _scwprintf does not include the terminator
+
+ auto progID = MakeUnique<wchar_t[]>(progIDLen);
+ _snwprintf_s(progID.get(), progIDLen, _TRUNCATE, progIDFmt, aProgIDBase,
+ aAumi);
+
+ return progID;
+}
+
+bool CheckProgIDExists(const wchar_t* aProgID) {
+ HKEY key;
+ if (::RegOpenKeyExW(HKEY_CLASSES_ROOT, aProgID, 0, KEY_READ, &key) !=
+ ERROR_SUCCESS) {
+ return false;
+ }
+ ::RegCloseKey(key);
+ return true;
+}