Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 83 additions & 1 deletion HashCodec.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<U8> 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<DWORD>(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<int>(bytes.size());
const int dword_len = byte_len >> 2;
const int mix_count = ((byte_len & 4) == 0) ? dword_len : (dword_len - 1);

std::vector<U32> 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<BYTE>(low & 0xFFU);
final8[1] = static_cast<BYTE>((low >> 8) & 0xFFU);
final8[2] = static_cast<BYTE>((low >> 16) & 0xFFU);
final8[3] = static_cast<BYTE>((low >> 24) & 0xFFU);
final8[4] = static_cast<BYTE>(high & 0xFFU);
final8[5] = static_cast<BYTE>((high >> 8) & 0xFFU);
final8[6] = static_cast<BYTE>((high >> 16) & 0xFFU);
final8[7] = static_cast<BYTE>((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,
Expand Down Expand Up @@ -803,4 +885,4 @@ bool ComputeHash(const std::wstring &canonical_input,
return true;
}
} // namespace UserChoiceLatestHash

8 changes: 7 additions & 1 deletion HashCommon.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand All @@ -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,
Expand All @@ -125,4 +131,4 @@ int RunStandaloneCli(int argc, wchar_t **argv);
} // namespace UserChoiceLatestHash

#endif

6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -109,4 +113,4 @@ Typical fields in the output:
The hash calculated by this implementation.
- `match`
`true` when both values are identical.

84 changes: 73 additions & 11 deletions RegistryContext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<int>(ctx->machine_id_trimmed[ctx->machine_id_trimmed.size() - 1U] % 3);
Expand Down Expand Up @@ -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;
Expand All @@ -292,4 +354,4 @@ int PrintVerificationResult(const AssocContext &ctx)
return match ? 0 : 2;
}
} // namespace UserChoiceLatestHash

Binary file modified UserChoiceLatestHash.exe
Binary file not shown.
54 changes: 54 additions & 0 deletions build_msvc.bat
Original file line number Diff line number Diff line change
@@ -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