diff --git a/HashCodec.cpp b/HashCodec.cpp index ac24682..a12ab1c 100644 --- a/HashCodec.cpp +++ b/HashCodec.cpp @@ -698,6 +698,88 @@ void ApplyLicenseNameShaMix(const U32 base_a[4], out->b[3] ^= w3; } +bool ComputeHashUserChoice(const std::wstring &canonical_input, + bool lowercase_output, + std::wstring *out_hash, + DebugData *debug_data) +{ + if (out_hash == NULL) + { + return false; + } + + WorkingSeeds seeds; + LoadProvidedSeeds(&seeds); + + const std::wstring lowered = ToLowerWide(canonical_input); + const size_t char_count = lowered.size(); + + // UTF-16LE encode + null terminator (matching SFTA.ps1: $bytesBaseInfo += 0x00, 0x00) + std::vector bytes((char_count + 1U) * 2U, 0U); + memcpy(&bytes[0], lowered.c_str(), char_count * 2U); + + U32 md5_words[4] = { 0U, 0U, 0U, 0U }; + if (!Md5Raw(&bytes[0], static_cast(bytes.size()), md5_words)) + { + return false; + } + if (debug_data != NULL) + { + debug_data->packed_words.clear(); + memcpy(debug_data->md5_words, md5_words, sizeof(md5_words)); + } + + const int byte_len = static_cast(bytes.size()); + const int dword_len = byte_len >> 2; + const int mix_count = ((byte_len & 4) == 0) ? dword_len : (dword_len - 1); + + std::vector dwords(dword_len, 0U); + for (int i = 0; i < dword_len; ++i) + { + dwords[i] = ReadLe32(&bytes[i * 4]); + } + + U32 pair_a[2] = { 0U, 0U }; + U32 pair_b[2] = { 0U, 0U }; + if (mix_count >= 2) + { + MixA(&dwords[0], mix_count, md5_words, seeds.a, pair_a); + MixB(&dwords[0], mix_count, md5_words, seeds.b, pair_b); + } + if (debug_data != NULL) + { + debug_data->pair_a[0] = pair_a[0]; + debug_data->pair_a[1] = pair_a[1]; + debug_data->pair_b[0] = pair_b[0]; + debug_data->pair_b[1] = pair_b[1]; + } + + BYTE final8[8]; + const U32 low = pair_a[0] ^ pair_b[0]; + const U32 high = pair_a[1] ^ pair_b[1]; + final8[0] = static_cast(low & 0xFFU); + final8[1] = static_cast((low >> 8) & 0xFFU); + final8[2] = static_cast((low >> 16) & 0xFFU); + final8[3] = static_cast((low >> 24) & 0xFFU); + final8[4] = static_cast(high & 0xFFU); + final8[5] = static_cast((high >> 8) & 0xFFU); + final8[6] = static_cast((high >> 16) & 0xFFU); + final8[7] = static_cast((high >> 24) & 0xFFU); + + std::wstring hash; + if (!Base64NoCrLf(final8, sizeof(final8), &hash)) + { + return false; + } + if (lowercase_output) + { + hash = ToLowerWide(hash); + } + + *out_hash = hash; + return true; +} + bool ComputeHash(const std::wstring &canonical_input, const WorkingSeeds &seeds, bool lowercase_output, @@ -803,4 +885,4 @@ bool ComputeHash(const std::wstring &canonical_input, return true; } } // namespace UserChoiceLatestHash - + diff --git a/HashCommon.h b/HashCommon.h index 1be97fd..727d2ca 100644 --- a/HashCommon.h +++ b/HashCommon.h @@ -92,6 +92,7 @@ struct AssocContext std::wstring canonical_alternate; std::wstring computed_primary; std::wstring computed_alternate; + FILETIME last_write_raw; int mod_class; bool alternate_used; }; @@ -109,6 +110,11 @@ bool ComputeHash(const std::wstring &canonical_input, std::wstring *out_hash, DebugData *debug_data); +bool ComputeHashUserChoice(const std::wstring &canonical_input, + bool lowercase_output, + std::wstring *out_hash, + DebugData *debug_data); + void ApplyLicenseNameShaMix(const U32 base_a[4], const U32 base_b[4], const std::wstring &license_name, @@ -125,4 +131,4 @@ int RunStandaloneCli(int argc, wchar_t **argv); } // namespace UserChoiceLatestHash #endif - + diff --git a/README.md b/README.md index c414dc8..ebecca4 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,11 @@ From this directory: ```bat cmd /c "call \"C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars64.bat\" >nul && cl /nologo /EHsc /W4 /TP /c main.cpp HashTables.cpp HashCodec.cpp RegistryContext.cpp Cli.cpp && link /NOLOGO /OUT:UserChoiceLatestHash.exe main.obj HashTables.obj HashCodec.obj RegistryContext.obj Cli.obj advapi32.lib crypt32.lib" ``` +Or use the provided one-click packaging script: +```bat +build_msvc.bat +``` ## Usage ### 1. Debug one canonical input @@ -109,4 +113,4 @@ Typical fields in the output: The hash calculated by this implementation. - `match` `true` when both values are identical. - + diff --git a/RegistryContext.cpp b/RegistryContext.cpp index 6bf0cc0..7710994 100644 --- a/RegistryContext.cpp +++ b/RegistryContext.cpp @@ -9,6 +9,9 @@ const wchar_t *kSalt97B6 = const wchar_t *kSaltD185 = L"Copyright (C) Microsoft. All rights reserved {D185E0A1-E265-4724-AA21-3A17B038D72E}"; +const wchar_t *kUserExperience = + L"User Choice set via Windows User Experience {D18B6DD5-6124-4341-9318-804003BAFA0B}"; + bool QueryRegString(HKEY root, const std::wstring &subkey, const wchar_t *value_name, std::wstring *out) { HKEY key = NULL; @@ -116,6 +119,26 @@ bool FormatTimestampHexFromFileTime(const FILETIME &last_write, std::wstring *ou return true; } +bool FormatTimestampHexMinuteRounded(const FILETIME &last_write, std::wstring *out) +{ + SYSTEMTIME st; + FILETIME normalized; + if (!FileTimeToSystemTime(&last_write, &st)) + { + return false; + } + st.wSecond = 0; + st.wMilliseconds = 0; + if (!SystemTimeToFileTime(&st, &normalized)) + { + return false; + } + wchar_t buffer[17]; + swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"%08x%08x", normalized.dwHighDateTime, normalized.dwLowDateTime); + *out = buffer; + return true; +} + const wchar_t *PrimarySaltForClass(int mod_class) { if (mod_class == 0) @@ -162,6 +185,19 @@ std::wstring BuildCanonicalInput(const UserChoiceLatestHash::AssocContext &ctx, return result; } +std::wstring BuildCanonicalInputUserChoice(const UserChoiceLatestHash::AssocContext &ctx) +{ + // SFTA.ps1 format: assoc + sid + progid + timestamp + experience + // Fixed order, no salt, no mod_class + std::wstring result; + result += ctx.assoc; + result += ctx.sid; + result += ctx.progid; + result += ctx.timestamp_hex; + result += kUserExperience; + return result; +} + bool LoadAssociationContext(const std::wstring &assoc, UserChoiceLatestHash::AssocContext *ctx) { ctx->assoc = assoc; @@ -203,23 +239,38 @@ bool LoadAssociationContext(const std::wstring &assoc, UserChoiceLatestHash::Ass return false; } - const std::wstring progid_key = ctx->base_key + L"\\" + ctx->choice_name + L"\\ProgId"; - if (!QueryRegString(HKEY_CURRENT_USER, progid_key, L"ProgId", &ctx->progid) - && !QueryRegString(HKEY_CURRENT_USER, ctx->base_key + L"\\" + ctx->choice_name, L"ProgId", &ctx->progid)) + const std::wstring user_choice_key = ctx->base_key + L"\\" + ctx->choice_name; + const std::wstring progid_key = user_choice_key + L"\\ProgId"; + if (!QueryRegString(HKEY_CURRENT_USER, user_choice_key, L"ProgId", &ctx->progid) + && !QueryRegString(HKEY_CURRENT_USER, progid_key, L"ProgId", &ctx->progid)) { return false; } FILETIME last_write; - if (!QueryRegLastWriteTime(HKEY_CURRENT_USER, progid_key, &last_write) - && !QueryRegLastWriteTime(HKEY_CURRENT_USER, ctx->base_key + L"\\" + ctx->choice_name, &last_write)) + if (!QueryRegLastWriteTime(HKEY_CURRENT_USER, user_choice_key, &last_write) + && !QueryRegLastWriteTime(HKEY_CURRENT_USER, progid_key, &last_write)) { return false; } - if (!FormatTimestampHexFromFileTime(last_write, &ctx->timestamp_hex)) + ctx->last_write_raw = last_write; + + // UserChoice: minute-rounded timestamp (matching SFTA.ps1 Get-HexDateTime) + // UserChoiceLatest: SYSTEMTIME-normalized timestamp (existing behavior) + if (ctx->choice_name == L"UserChoice") { - return false; + if (!FormatTimestampHexMinuteRounded(last_write, &ctx->timestamp_hex)) + { + return false; + } + } + else + { + if (!FormatTimestampHexFromFileTime(last_write, &ctx->timestamp_hex)) + { + return false; + } } ctx->mod_class = static_cast(ctx->machine_id_trimmed[ctx->machine_id_trimmed.size() - 1U] % 3); @@ -263,10 +314,21 @@ bool VerifyCurrentAssociation(const std::wstring &assoc, return false; } - ctx->canonical_primary = BuildCanonicalInput(*ctx, PrimarySaltForClass(ctx->mod_class)); - if (!UserChoiceLatestHash::ComputeHash(ctx->canonical_primary, seeds, false, &ctx->computed_primary, NULL)) + if (ctx->choice_name == L"UserChoiceLatest") { - return false; + ctx->canonical_primary = BuildCanonicalInput(*ctx, PrimarySaltForClass(ctx->mod_class)); + if (!UserChoiceLatestHash::ComputeHash(ctx->canonical_primary, seeds, false, &ctx->computed_primary, NULL)) + { + return false; + } + } + else + { + ctx->canonical_primary = BuildCanonicalInputUserChoice(*ctx); + if (!UserChoiceLatestHash::ComputeHashUserChoice(ctx->canonical_primary, false, &ctx->computed_primary, NULL)) + { + return false; + } } return true; @@ -292,4 +354,4 @@ int PrintVerificationResult(const AssocContext &ctx) return match ? 0 : 2; } } // namespace UserChoiceLatestHash - + diff --git a/UserChoiceLatestHash.exe b/UserChoiceLatestHash.exe index df2a426..0ae30a3 100644 Binary files a/UserChoiceLatestHash.exe and b/UserChoiceLatestHash.exe differ diff --git a/build_msvc.bat b/build_msvc.bat new file mode 100644 index 0000000..318d798 --- /dev/null +++ b/build_msvc.bat @@ -0,0 +1,54 @@ +@echo off +setlocal +pushd "%~dp0" + +echo [1/3] Locating Visual Studio build environment... +if defined VSINSTALLDIR ( + set "VS_VCVARS=%VSINSTALLDIR%VC\Auxiliary\Build\vcvars64.bat" +) else if exist "%ProgramFiles(x86)%\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars64.bat" ( + set "VS_VCVARS=%ProgramFiles(x86)%\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars64.bat" +) else if exist "%ProgramFiles%\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat" ( + set "VS_VCVARS=%ProgramFiles%\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat" +) + +if not defined VS_VCVARS ( + echo ERROR: Could not find vcvars64.bat. Please run from a Visual Studio developer command prompt or set VSINSTALLDIR. + popd + exit /b 1 +) + +echo [2/3] Initializing build tools... +call "%VS_VCVARS%" >nul +if errorlevel 1 ( + echo ERROR: Failed to initialize Visual Studio build tools. + popd + exit /b 1 +) + +echo [3/3] Building UserChoiceLatestHash.exe... +cl /nologo /EHsc /W4 /TP /c main.cpp HashTables.cpp HashCodec.cpp RegistryContext.cpp Cli.cpp +if errorlevel 1 ( + echo ERROR: Compilation failed. + popd + exit /b 1 +) +link /NOLOGO /OUT:UserChoiceLatestHash.exe main.obj HashTables.obj HashCodec.obj RegistryContext.obj Cli.obj advapi32.lib crypt32.lib +if errorlevel 1 ( + echo ERROR: Link failed. + popd + exit /b 1 +) + +echo [4/4] Packaging output to UserChoiceLatestHash.zip... +powershell -NoProfile -Command "Compress-Archive -Force -Path 'UserChoiceLatestHash.exe','README.md','LICENSE','*.h','*.cpp','*.inc' -DestinationPath 'UserChoiceLatestHash.zip'" +if errorlevel 1 ( + echo WARNING: Packaging failed, but build succeeded. + popd + exit /b 1 +) + +echo Build and packaging complete. +echo Output: %~dp0UserChoiceLatestHash.exe +echo Package: %~dp0UserChoiceLatestHash.zip +popd +exit /b 0