diff --git a/.github/workflows/encrypt-sdk-release.yml b/.github/workflows/encrypt-sdk-release.yml new file mode 100644 index 000000000..a7b86c8cb --- /dev/null +++ b/.github/workflows/encrypt-sdk-release.yml @@ -0,0 +1,65 @@ +name: Publish encryption SDK + +on: + push: + tags: ['encrypt-sdk-*'] + workflow_dispatch: + inputs: + run_id: + description: Successful Encryption core run containing both SDK artifacts + required: true + default: '41587062927' + tag: + description: Existing SDK tag to publish + required: true + default: encrypt-sdk-openssl-3.6.5-v143 + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + actions: read + contents: write + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SDK_RUN: ${{ inputs.run_id || '41587062927' }} + SDK_TAG: ${{ inputs.tag || github.ref_name }} + steps: + - uses: actions/checkout@v4 + - name: Verify the source CI run + run: | + gh api "repos/$GITHUB_REPOSITORY/actions/runs/$SDK_RUN" > source-run.json + jq -e --arg repository "$GITHUB_REPOSITORY" \ + '.conclusion == "success" and .head_repository.full_name == $repository and .path == ".github/workflows/encryption.yml"' source-run.json + - uses: actions/download-artifact@v4 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ env.SDK_RUN }} + pattern: Encrypt-sdk-* + merge-multiple: true + path: packages + - name: Verify archives and SDK layouts + run: | + for arch in x86 x64; do + archive="$PWD/packages/openssl-$arch.zip" + hash=$(tr -d '\r\n\357\273\277' < "$archive.sha256") + cmake "-DOFE_ARCH=$arch" "-DOFE_OPENSSL_ROOT=$RUNNER_TEMP/sdk-$arch" \ + "-DOFE_WORK_DIR=$RUNNER_TEMP/stage" "-DOFE_PREBUILT_ARCHIVE=$archive" \ + "-DOFE_PREBUILT_SHA256=$hash" -P Extensions/_3rdLib/Encrypt-build/EnsureSDK.cmake + done + - name: Publish versioned SDK assets + run: | + source_sha=$(jq -r .head_sha source-run.json) + cat > release-notes.md < + +static int checks = 0; + +static void check(bool result) { + ++checks; + if (!result) throw std::runtime_error("Unicode conversion regression"); +} + +template +static void invalid(Function function) { + try { function(); } + catch (const std::invalid_argument&) { ++checks; return; } + throw std::runtime_error("Expected invalid Unicode input"); +} + +int main() { + const std::string utf8 = "A\xe4\xb8\xad\xf0\x9f\x98\x80"; + const std::wstring wide = L"A\u4e2d\U0001f600"; + const std::u16string units = u"A\u4e2d\U0001f600"; + check(to_utf16_string(std::string_view(utf8)) == units); + check(to_utf16_string(std::wstring_view(wide)) == units); + check(to_utf16_bytes(wide) == std::vector({0x41, 0, 0x2d, 0x4e, 0x3d, 0xd8, 0, 0xde})); + check(to_utf16_string(std::string_view()).empty()); + check(to_utf16_string(std::wstring_view()).empty()); + check(to_utf16_bytes(L"").empty()); + check(to_utf16_string(std::string_view("A\0B", 3)) == std::u16string(u"A\0B", 3)); + check(to_utf16_string(std::wstring_view(L"A\0B", 3)) == std::u16string(u"A\0B", 3)); + check(to_utf16_bytes(std::wstring_view(L"A\0B", 3)) == std::vector({0x41, 0, 0, 0, 0x42, 0})); + check(to_utf16_string(std::string_view("\xf4\x8f\xbf\xbf")) == u"\U0010ffff"); + + // Wide input historically preserves raw UTF-16 units, including isolated surrogates. + const std::wstring surrogate(1, static_cast(0xd800)); + check(to_utf16_string(std::wstring_view(surrogate)) == std::u16string(1, 0xd800)); + check(to_utf16_bytes(surrogate) == std::vector({0, 0xd8})); + + for (const auto* text : {"\x80", "\xc0\x80", "\xe4\xb8", "\xe4x\xad", + "\xed\xa0\x80", "\xf0\x9f", "\xf4\x90\x80\x80", "\xf5\x80\x80\x80"}) { + invalid([&] { to_utf16_string(std::string_view(text)); }); + } + if (sizeof(wchar_t) > 2) { + const std::wstring outside(1, static_cast(0x110000)); + invalid([&] { to_utf16_string(std::wstring_view(outside)); }); + invalid([&] { to_utf16_bytes(outside); }); + } + std::cout << "PASS: " << checks << " standalone C++17 Unicode checks\n"; +} diff --git a/Extensions/Android_Gradle/tests/helpers/run_helper_tests.py b/Extensions/Android_Gradle/tests/helpers/run_helper_tests.py index 7aa7e9fe1..1f0b6b374 100644 --- a/Extensions/Android_Gradle/tests/helpers/run_helper_tests.py +++ b/Extensions/Android_Gradle/tests/helpers/run_helper_tests.py @@ -24,6 +24,7 @@ def run(command, env=None): run([args.cmake, '-S', tests, '-B', build / 'cmake', '-DCMAKE_BUILD_TYPE=Release', '-DDELIB_ACP_ENCODING=UTF-8']) run([args.cmake, '--build', build / 'cmake', '--parallel', '2']) run([build / 'cmake/general_definition_test']) +run([build / 'cmake/unicode_conversion_test']) run([build / 'cmake/acp_override_test']) run([build / 'cmake/path_owner_thread_test']) run([build / 'cmake/map_data_test']) diff --git a/Extensions/Deque2D/Deque2D.sln b/Extensions/Deque2D/Deque2D.sln index d35677ba0..764612d04 100644 --- a/Extensions/Deque2D/Deque2D.sln +++ b/Extensions/Deque2D/Deque2D.sln @@ -1,9 +1,14 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.31515.178 MinimumVisualStudioVersion = 10.0.40219.1 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Deque2D", "Deque2D.vcxproj", "{3BCB4C73-71F6-43B6-803D-41411F92652A}" + ProjectSection(ProjectDependencies) = postProject + {F408ED35-2D66-4E36-A659-580895179F86} = {F408ED35-2D66-4E36-A659-580895179F86} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "EncryptDependencies", "..\_3rdLib\Encrypt-build\EncryptDependencies.vcxproj", "{F408ED35-2D66-4E36-A659-580895179F86}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -12,6 +17,12 @@ Global Run_Only Unicode|Win32 = Run_Only Unicode|Win32 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F408ED35-2D66-4E36-A659-580895179F86}.Debug Unicode|Win32.ActiveCfg = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Debug Unicode|Win32.Build.0 = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Release Unicode|Win32.ActiveCfg = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Release Unicode|Win32.Build.0 = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Run_Only Unicode|Win32.ActiveCfg = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Run_Only Unicode|Win32.Build.0 = Release|Win32 {3BCB4C73-71F6-43B6-803D-41411F92652A}.Debug Unicode|Win32.ActiveCfg = Debug Unicode|Win32 {3BCB4C73-71F6-43B6-803D-41411F92652A}.Debug Unicode|Win32.Build.0 = Debug Unicode|Win32 {3BCB4C73-71F6-43B6-803D-41411F92652A}.Release Unicode|Win32.ActiveCfg = Release Unicode|Win32 diff --git a/Extensions/Deque2D/Deque2D.vcxproj b/Extensions/Deque2D/Deque2D.vcxproj index f119c24be..e9cb9be72 100644 --- a/Extensions/Deque2D/Deque2D.vcxproj +++ b/Extensions/Deque2D/Deque2D.vcxproj @@ -304,6 +304,7 @@ copy $(TargetPath) "C:\Steam\steamapps\common\Clickteam Fusion 2.5\Data\Runtime\ + diff --git a/Extensions/Encrypter/Encrypter.sln b/Extensions/Encrypter/Encrypter.sln index d4a441a86..d1f4cf3f1 100644 --- a/Extensions/Encrypter/Encrypter.sln +++ b/Extensions/Encrypter/Encrypter.sln @@ -1,9 +1,14 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.31515.178 MinimumVisualStudioVersion = 10.0.40219.1 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Encrypter", "Encrypter.vcxproj", "{3BCB4C73-71F6-43B6-803D-41411F92652A}" + ProjectSection(ProjectDependencies) = postProject + {F408ED35-2D66-4E36-A659-580895179F86} = {F408ED35-2D66-4E36-A659-580895179F86} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "EncryptDependencies", "..\_3rdLib\Encrypt-build\EncryptDependencies.vcxproj", "{F408ED35-2D66-4E36-A659-580895179F86}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -12,6 +17,12 @@ Global Run_Only Unicode|Win32 = Run_Only Unicode|Win32 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F408ED35-2D66-4E36-A659-580895179F86}.Debug Unicode|Win32.ActiveCfg = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Debug Unicode|Win32.Build.0 = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Release Unicode|Win32.ActiveCfg = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Release Unicode|Win32.Build.0 = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Run_Only Unicode|Win32.ActiveCfg = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Run_Only Unicode|Win32.Build.0 = Release|Win32 {3BCB4C73-71F6-43B6-803D-41411F92652A}.Debug Unicode|Win32.ActiveCfg = Debug Unicode|Win32 {3BCB4C73-71F6-43B6-803D-41411F92652A}.Debug Unicode|Win32.Build.0 = Debug Unicode|Win32 {3BCB4C73-71F6-43B6-803D-41411F92652A}.Release Unicode|Win32.ActiveCfg = Release Unicode|Win32 diff --git a/Extensions/Encrypter/Encrypter.vcxproj b/Extensions/Encrypter/Encrypter.vcxproj index 1c71cb03c..c785c9948 100644 --- a/Extensions/Encrypter/Encrypter.vcxproj +++ b/Extensions/Encrypter/Encrypter.vcxproj @@ -300,6 +300,7 @@ copy $(TargetPath) "C:\Steam\steamapps\common\Clickteam Fusion 2.5\Data\Runtime\ + diff --git a/Extensions/Encrypter/Func.h b/Extensions/Encrypter/Func.h index 5641273f8..5e6348d05 100644 --- a/Extensions/Encrypter/Func.h +++ b/Extensions/Encrypter/Func.h @@ -1,27 +1,19 @@ #pragma once -#define USE_PROCESS_DIRECTLY - -inline bool EncryptCore(LPCTSTR pSrcFilePath, LPCTSTR pDesFilePath, LPCTSTR pKey, bool bEncrypt) { +inline bool EncryptCore(LPCTSTR pSrcFilePath, LPCTSTR pDesFilePath, LPCTSTR pKey, encryption::Mode mode) { Encryption Encrypt; - Encrypt.GenerateKey(pKey); -#ifdef USE_PROCESS_DIRECTLY - auto ret = bEncrypt - ? Encrypt.EncryptFileDirectly(pSrcFilePath) - : Encrypt.DecryptFileDirectly(pSrcFilePath); -#else - auto ret = bEncrypt - ? Encrypt.EncryptFile(pSrcFilePath) - : Encrypt.DecryptFile(pSrcFilePath); -#endif - - if (!ret) { - return false; - } - - Encrypt.SaveFile(pDesFilePath); + if (![&]{ + switch(mode){ + case encryption::Mode::encrypt: + return Encrypt.EncryptFileDirectly(pSrcFilePath); + case encryption::Mode::decrypt: + return Encrypt.DecryptFileDirectly(pSrcFilePath); + default: + return false; + } + }()) { return false; } - return true; -} \ No newline at end of file + return Encrypt.SaveFile(pDesFilePath); +} diff --git a/Extensions/Encrypter/Main.cpp b/Extensions/Encrypter/Main.cpp index 295f795bf..e7cd49ead 100644 --- a/Extensions/Encrypter/Main.cpp +++ b/Extensions/Encrypter/Main.cpp @@ -57,7 +57,7 @@ short WINAPI DLLExport Encrypt(LPRDATA rdPtr, long param1, long param2) { LPCTSTR pDesFilePath = (LPCTSTR)CNC_GetStringParameter(rdPtr); LPCTSTR pKey = (LPCTSTR)CNC_GetStringParameter(rdPtr); - EncryptCore(pSrcFilePath, pDesFilePath, pKey, ENCRY); + EncryptCore(pSrcFilePath, pDesFilePath, pKey, encryption::Mode::encrypt); return 0; } @@ -67,7 +67,7 @@ short WINAPI DLLExport Decrypt(LPRDATA rdPtr, long param1, long param2) { LPCTSTR pDesFilePath = (LPCTSTR)CNC_GetStringParameter(rdPtr); LPCTSTR pKey = (LPCTSTR)CNC_GetStringParameter(rdPtr); - EncryptCore(pSrcFilePath, pDesFilePath, pKey, DECRY); + EncryptCore(pSrcFilePath, pDesFilePath, pKey, encryption::Mode::decrypt); return 0; } diff --git a/Extensions/FFMpeg/FFMpeg.sln b/Extensions/FFMpeg/FFMpeg.sln index 05bdf54c5..ec7152df2 100644 --- a/Extensions/FFMpeg/FFMpeg.sln +++ b/Extensions/FFMpeg/FFMpeg.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.31515.178 @@ -7,12 +7,15 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "FFMpeg", "FFMpeg.vcxproj", ProjectSection(ProjectDependencies) = postProject {E7DA8559-E315-4F4C-AD96-8BE9914F7C75} = {E7DA8559-E315-4F4C-AD96-8BE9914F7C75} {91BF4C20-F127-4BD4-8DF8-EE32126D054A} = {91BF4C20-F127-4BD4-8DF8-EE32126D054A} + {F408ED35-2D66-4E36-A659-580895179F86} = {F408ED35-2D66-4E36-A659-580895179F86} EndProjectSection EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SDLDependencies", "..\_3rdLib\SDL-build\SDLDependencies.vcxproj", "{91BF4C20-F127-4BD4-8DF8-EE32126D054A}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "FFmpegDependencies", "..\_3rdLib\FFmpeg-build\FFmpegDependencies.vcxproj", "{E7DA8559-E315-4F4C-AD96-8BE9914F7C75}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "EncryptDependencies", "..\_3rdLib\Encrypt-build\EncryptDependencies.vcxproj", "{F408ED35-2D66-4E36-A659-580895179F86}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug Unicode|Win32 = Debug Unicode|Win32 @@ -20,6 +23,12 @@ Global Run_Only Unicode|Win32 = Run_Only Unicode|Win32 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F408ED35-2D66-4E36-A659-580895179F86}.Debug Unicode|Win32.ActiveCfg = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Debug Unicode|Win32.Build.0 = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Release Unicode|Win32.ActiveCfg = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Release Unicode|Win32.Build.0 = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Run_Only Unicode|Win32.ActiveCfg = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Run_Only Unicode|Win32.Build.0 = Release|Win32 {E7DA8559-E315-4F4C-AD96-8BE9914F7C75}.Debug Unicode|Win32.ActiveCfg = Release|Win32 {E7DA8559-E315-4F4C-AD96-8BE9914F7C75}.Debug Unicode|Win32.Build.0 = Release|Win32 {E7DA8559-E315-4F4C-AD96-8BE9914F7C75}.Release Unicode|Win32.ActiveCfg = Release|Win32 diff --git a/Extensions/FFMpeg/FFMpeg.vcxproj b/Extensions/FFMpeg/FFMpeg.vcxproj index cb6854d5d..a7c1b75b1 100644 --- a/Extensions/FFMpeg/FFMpeg.vcxproj +++ b/Extensions/FFMpeg/FFMpeg.vcxproj @@ -402,6 +402,7 @@ copy $(TargetPath) "C:\Steam\steamapps\common\Clickteam Fusion 2.5\Data\Runtime\ false + diff --git a/Extensions/Fini/Fini.sln b/Extensions/Fini/Fini.sln index 9acaacf5b..7be7841fb 100644 --- a/Extensions/Fini/Fini.sln +++ b/Extensions/Fini/Fini.sln @@ -1,9 +1,14 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.31515.178 MinimumVisualStudioVersion = 10.0.40219.1 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Template", "Fini.vcxproj", "{3BCB4C73-71F6-43B6-803D-41411F92652A}" + ProjectSection(ProjectDependencies) = postProject + {F408ED35-2D66-4E36-A659-580895179F86} = {F408ED35-2D66-4E36-A659-580895179F86} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "EncryptDependencies", "..\_3rdLib\Encrypt-build\EncryptDependencies.vcxproj", "{F408ED35-2D66-4E36-A659-580895179F86}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -12,6 +17,12 @@ Global Run_Only Unicode|Win32 = Run_Only Unicode|Win32 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F408ED35-2D66-4E36-A659-580895179F86}.Debug Unicode|Win32.ActiveCfg = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Debug Unicode|Win32.Build.0 = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Release Unicode|Win32.ActiveCfg = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Release Unicode|Win32.Build.0 = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Run_Only Unicode|Win32.ActiveCfg = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Run_Only Unicode|Win32.Build.0 = Release|Win32 {3BCB4C73-71F6-43B6-803D-41411F92652A}.Debug Unicode|Win32.ActiveCfg = Debug Unicode|Win32 {3BCB4C73-71F6-43B6-803D-41411F92652A}.Debug Unicode|Win32.Build.0 = Debug Unicode|Win32 {3BCB4C73-71F6-43B6-803D-41411F92652A}.Release Unicode|Win32.ActiveCfg = Release Unicode|Win32 diff --git a/Extensions/Fini/Fini.vcxproj b/Extensions/Fini/Fini.vcxproj index 3f468bd1e..f4c5780db 100644 --- a/Extensions/Fini/Fini.vcxproj +++ b/Extensions/Fini/Fini.vcxproj @@ -304,6 +304,7 @@ copy $(TargetPath) "C:\Steam\steamapps\common\Clickteam Fusion 2.5\Data\Runtime\ + diff --git a/Extensions/NeoPic/NeoPic.sln b/Extensions/NeoPic/NeoPic.sln index 7d0afd4d5..3bc40210b 100644 --- a/Extensions/NeoPic/NeoPic.sln +++ b/Extensions/NeoPic/NeoPic.sln @@ -1,9 +1,14 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.31515.178 MinimumVisualStudioVersion = 10.0.40219.1 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "NeoPic", "NeoPic.vcxproj", "{3BCB4C73-71F6-43B6-803D-41411F92652A}" + ProjectSection(ProjectDependencies) = postProject + {F408ED35-2D66-4E36-A659-580895179F86} = {F408ED35-2D66-4E36-A659-580895179F86} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "EncryptDependencies", "..\_3rdLib\Encrypt-build\EncryptDependencies.vcxproj", "{F408ED35-2D66-4E36-A659-580895179F86}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -12,6 +17,12 @@ Global Run_Only Unicode|Win32 = Run_Only Unicode|Win32 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F408ED35-2D66-4E36-A659-580895179F86}.Debug Unicode|Win32.ActiveCfg = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Debug Unicode|Win32.Build.0 = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Release Unicode|Win32.ActiveCfg = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Release Unicode|Win32.Build.0 = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Run_Only Unicode|Win32.ActiveCfg = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Run_Only Unicode|Win32.Build.0 = Release|Win32 {3BCB4C73-71F6-43B6-803D-41411F92652A}.Debug Unicode|Win32.ActiveCfg = Debug Unicode|Win32 {3BCB4C73-71F6-43B6-803D-41411F92652A}.Debug Unicode|Win32.Build.0 = Debug Unicode|Win32 {3BCB4C73-71F6-43B6-803D-41411F92652A}.Release Unicode|Win32.ActiveCfg = Release Unicode|Win32 diff --git a/Extensions/NeoPic/NeoPic.vcxproj b/Extensions/NeoPic/NeoPic.vcxproj index 897dd15e3..db46ea49e 100644 --- a/Extensions/NeoPic/NeoPic.vcxproj +++ b/Extensions/NeoPic/NeoPic.vcxproj @@ -315,6 +315,7 @@ copy $(TargetPath) "C:\Steam\steamapps\common\Clickteam Fusion 2.5\Data\Runtime\ + diff --git a/Extensions/NeoStr/NeoStr.sln b/Extensions/NeoStr/NeoStr.sln index 57c8831b5..ada9c574d 100644 --- a/Extensions/NeoStr/NeoStr.sln +++ b/Extensions/NeoStr/NeoStr.sln @@ -1,9 +1,14 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.31515.178 MinimumVisualStudioVersion = 10.0.40219.1 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "NeoStr", "NeoStr.vcxproj", "{3BCB4C73-71F6-43B6-803D-41411F92652A}" + ProjectSection(ProjectDependencies) = postProject + {F408ED35-2D66-4E36-A659-580895179F86} = {F408ED35-2D66-4E36-A659-580895179F86} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "EncryptDependencies", "..\_3rdLib\Encrypt-build\EncryptDependencies.vcxproj", "{F408ED35-2D66-4E36-A659-580895179F86}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -12,6 +17,12 @@ Global Run_Only Unicode|Win32 = Run_Only Unicode|Win32 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F408ED35-2D66-4E36-A659-580895179F86}.Debug Unicode|Win32.ActiveCfg = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Debug Unicode|Win32.Build.0 = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Release Unicode|Win32.ActiveCfg = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Release Unicode|Win32.Build.0 = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Run_Only Unicode|Win32.ActiveCfg = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Run_Only Unicode|Win32.Build.0 = Release|Win32 {3BCB4C73-71F6-43B6-803D-41411F92652A}.Debug Unicode|Win32.ActiveCfg = Debug Unicode|Win32 {3BCB4C73-71F6-43B6-803D-41411F92652A}.Debug Unicode|Win32.Build.0 = Debug Unicode|Win32 {3BCB4C73-71F6-43B6-803D-41411F92652A}.Release Unicode|Win32.ActiveCfg = Release Unicode|Win32 diff --git a/Extensions/NeoStr/NeoStr.vcxproj b/Extensions/NeoStr/NeoStr.vcxproj index bb008a23f..6b3a3dda1 100644 --- a/Extensions/NeoStr/NeoStr.vcxproj +++ b/Extensions/NeoStr/NeoStr.vcxproj @@ -328,6 +328,7 @@ copy $(TargetPath) "C:\Steam\steamapps\common\Clickteam Fusion 2.5\Data\Runtime\ + diff --git a/Extensions/SDL_Mixer/SDL_Mixer.sln b/Extensions/SDL_Mixer/SDL_Mixer.sln index 69fec6d4d..d68bf5d8e 100644 --- a/Extensions/SDL_Mixer/SDL_Mixer.sln +++ b/Extensions/SDL_Mixer/SDL_Mixer.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.31515.178 @@ -7,12 +7,15 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SDL_Mixer", "SDL_Mixer.vcxp ProjectSection(ProjectDependencies) = postProject {C90AE0ED-7F66-4E85-A010-EAE8BE60CBD7} = {C90AE0ED-7F66-4E85-A010-EAE8BE60CBD7} {91BF4C20-F127-4BD4-8DF8-EE32126D054A} = {91BF4C20-F127-4BD4-8DF8-EE32126D054A} + {F408ED35-2D66-4E36-A659-580895179F86} = {F408ED35-2D66-4E36-A659-580895179F86} EndProjectSection EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SDLDependencies", "..\_3rdLib\SDL-build\SDLDependencies.vcxproj", "{91BF4C20-F127-4BD4-8DF8-EE32126D054A}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SoundTouchDependencies", "..\_3rdLib\SoundTouch-build\SoundTouchDependencies.vcxproj", "{C90AE0ED-7F66-4E85-A010-EAE8BE60CBD7}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "EncryptDependencies", "..\_3rdLib\Encrypt-build\EncryptDependencies.vcxproj", "{F408ED35-2D66-4E36-A659-580895179F86}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug Unicode|Win32 = Debug Unicode|Win32 @@ -20,6 +23,12 @@ Global Run_Only Unicode|Win32 = Run_Only Unicode|Win32 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F408ED35-2D66-4E36-A659-580895179F86}.Debug Unicode|Win32.ActiveCfg = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Debug Unicode|Win32.Build.0 = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Release Unicode|Win32.ActiveCfg = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Release Unicode|Win32.Build.0 = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Run_Only Unicode|Win32.ActiveCfg = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Run_Only Unicode|Win32.Build.0 = Release|Win32 {C90AE0ED-7F66-4E85-A010-EAE8BE60CBD7}.Debug Unicode|Win32.ActiveCfg = Release|Win32 {C90AE0ED-7F66-4E85-A010-EAE8BE60CBD7}.Debug Unicode|Win32.Build.0 = Release|Win32 {C90AE0ED-7F66-4E85-A010-EAE8BE60CBD7}.Release Unicode|Win32.ActiveCfg = Release|Win32 diff --git a/Extensions/SDL_Mixer/SDL_Mixer.vcxproj b/Extensions/SDL_Mixer/SDL_Mixer.vcxproj index 07e5e03c9..af8498999 100644 --- a/Extensions/SDL_Mixer/SDL_Mixer.vcxproj +++ b/Extensions/SDL_Mixer/SDL_Mixer.vcxproj @@ -318,6 +318,7 @@ copy $(TargetPath) "C:\Steam\steamapps\common\Clickteam Fusion 2.5\Data\Runtime\ false + diff --git a/Extensions/StringSplitter/StringSplitter.sln b/Extensions/StringSplitter/StringSplitter.sln index 866cf2a4c..93bb6e84d 100644 --- a/Extensions/StringSplitter/StringSplitter.sln +++ b/Extensions/StringSplitter/StringSplitter.sln @@ -1,9 +1,14 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.31515.178 MinimumVisualStudioVersion = 10.0.40219.1 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "StringSplitter", "StringSplitter.vcxproj", "{3BCB4C73-71F6-43B6-803D-41411F92652A}" + ProjectSection(ProjectDependencies) = postProject + {F408ED35-2D66-4E36-A659-580895179F86} = {F408ED35-2D66-4E36-A659-580895179F86} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "EncryptDependencies", "..\_3rdLib\Encrypt-build\EncryptDependencies.vcxproj", "{F408ED35-2D66-4E36-A659-580895179F86}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -12,6 +17,12 @@ Global Run_Only Unicode|Win32 = Run_Only Unicode|Win32 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F408ED35-2D66-4E36-A659-580895179F86}.Debug Unicode|Win32.ActiveCfg = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Debug Unicode|Win32.Build.0 = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Release Unicode|Win32.ActiveCfg = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Release Unicode|Win32.Build.0 = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Run_Only Unicode|Win32.ActiveCfg = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Run_Only Unicode|Win32.Build.0 = Release|Win32 {3BCB4C73-71F6-43B6-803D-41411F92652A}.Debug Unicode|Win32.ActiveCfg = Debug Unicode|Win32 {3BCB4C73-71F6-43B6-803D-41411F92652A}.Debug Unicode|Win32.Build.0 = Debug Unicode|Win32 {3BCB4C73-71F6-43B6-803D-41411F92652A}.Release Unicode|Win32.ActiveCfg = Release Unicode|Win32 diff --git a/Extensions/StringSplitter/StringSplitter.vcxproj b/Extensions/StringSplitter/StringSplitter.vcxproj index 097a8c128..a183e792a 100644 --- a/Extensions/StringSplitter/StringSplitter.vcxproj +++ b/Extensions/StringSplitter/StringSplitter.vcxproj @@ -303,6 +303,7 @@ copy $(TargetPath) "C:\Steam\steamapps\common\Clickteam Fusion 2.5\Data\Runtime\ + diff --git a/Extensions/WinAPI/WinAPI.sln b/Extensions/WinAPI/WinAPI.sln index 8c4fa33a5..fd22fb964 100644 --- a/Extensions/WinAPI/WinAPI.sln +++ b/Extensions/WinAPI/WinAPI.sln @@ -1,9 +1,14 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.31515.178 MinimumVisualStudioVersion = 10.0.40219.1 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WinAPI", "WinAPI.vcxproj", "{3BCB4C73-71F6-43B6-803D-41411F92652A}" + ProjectSection(ProjectDependencies) = postProject + {F408ED35-2D66-4E36-A659-580895179F86} = {F408ED35-2D66-4E36-A659-580895179F86} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "EncryptDependencies", "..\_3rdLib\Encrypt-build\EncryptDependencies.vcxproj", "{F408ED35-2D66-4E36-A659-580895179F86}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -12,6 +17,12 @@ Global Run_Only Unicode|x86 = Run_Only Unicode|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F408ED35-2D66-4E36-A659-580895179F86}.Debug Unicode|x86.ActiveCfg = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Debug Unicode|x86.Build.0 = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Release Unicode|x86.ActiveCfg = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Release Unicode|x86.Build.0 = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Run_Only Unicode|x86.ActiveCfg = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Run_Only Unicode|x86.Build.0 = Release|Win32 {3BCB4C73-71F6-43B6-803D-41411F92652A}.Debug Unicode|x86.ActiveCfg = Debug Unicode|Win32 {3BCB4C73-71F6-43B6-803D-41411F92652A}.Debug Unicode|x86.Build.0 = Debug Unicode|Win32 {3BCB4C73-71F6-43B6-803D-41411F92652A}.Release Unicode|x86.ActiveCfg = Release Unicode|Win32 diff --git a/Extensions/WinAPI/WinAPI.vcxproj b/Extensions/WinAPI/WinAPI.vcxproj index d9cb46168..3069cdc10 100644 --- a/Extensions/WinAPI/WinAPI.vcxproj +++ b/Extensions/WinAPI/WinAPI.vcxproj @@ -397,6 +397,7 @@ copy $(TargetPath) "C:\Steam\steamapps\common\Clickteam Fusion 2.5\Data\Runtime\ + diff --git a/Extensions/_3rdLib/Encrypt-build/EncryptDependencies.vcxproj b/Extensions/_3rdLib/Encrypt-build/EncryptDependencies.vcxproj new file mode 100644 index 000000000..fa9eb0861 --- /dev/null +++ b/Extensions/_3rdLib/Encrypt-build/EncryptDependencies.vcxproj @@ -0,0 +1,34 @@ + + + + ReleaseWin32 + Releasex64 + + + {F408ED35-2D66-4E36-A659-580895179F86} + EncryptDependencies + 10.0 + + + Utilityv143 + + + + + + + $(ProjectDir)out\msbuild\$(Platform)\ + $(IntDir) + true + + + + + + + + + + + diff --git a/Extensions/_3rdLib/Encrypt-build/EncryptVersion.cmake b/Extensions/_3rdLib/Encrypt-build/EncryptVersion.cmake new file mode 100644 index 000000000..0d9029284 --- /dev/null +++ b/Extensions/_3rdLib/Encrypt-build/EncryptVersion.cmake @@ -0,0 +1,9 @@ +# Pinned together: the port graph and its OpenSSL version, not a moving vcpkg checkout. +set(OFE_ENCRYPT_VCPKG_REPOSITORY "https://github.com/microsoft/vcpkg.git") +set(OFE_ENCRYPT_VCPKG_COMMIT "9a5dfef5d1a0438a7d24856a85bd60f309d355e9") +set(OFE_ENCRYPT_OPENSSL_VERSION "3.6.5") + +# Versioned assets promoted from this repository's verified Windows CI. +set(OFE_ENCRYPT_SDK_URL "https://github.com/defisym/OpenFusionExamples/releases/download/encrypt-sdk-openssl-3.6.5-v143") +set(OFE_ENCRYPT_SDK_X86_SHA256 "3a2091235ca36fac88953b038c0610635f21b3624cd7d4d085e9e34184159701") +set(OFE_ENCRYPT_SDK_X64_SHA256 "05ec3228a9afe35462cc23be14ca0e4f2c3c6e0705da16385e5bfba53fe3a3bf") diff --git a/Extensions/_3rdLib/Encrypt-build/EnsureSDK.cmake b/Extensions/_3rdLib/Encrypt-build/EnsureSDK.cmake new file mode 100644 index 000000000..54ac297bc --- /dev/null +++ b/Extensions/_3rdLib/Encrypt-build/EnsureSDK.cmake @@ -0,0 +1,163 @@ +cmake_minimum_required(VERSION 3.21) +include("${CMAKE_CURRENT_LIST_DIR}/EncryptVersion.cmake") +include("${CMAKE_CURRENT_LIST_DIR}/ValidateSDK.cmake") +if(NOT OFE_ARCH MATCHES "^(x86|x64)$") + message(FATAL_ERROR "EncryptDependencies supports x86 and x64.") +endif() + +# Ordinary VS builds consume our SDK; source builds are an explicit opt-in. +# User-supplied archives/URLs retain precedence over the repository defaults. +if(NOT OFE_BUILD_FROM_SOURCE AND NOT OFE_PREBUILT_URL AND NOT OFE_PREBUILT_ARCHIVE) + if(OFE_PLATFORM_TOOLSET AND NOT OFE_PLATFORM_TOOLSET STREQUAL "v143") + message(FATAL_ERROR "The prebuilt OpenSSL SDK uses v143. Set OfeEncryptBuildFromSource=true for another toolset.") + endif() + set(OFE_PREBUILT_URL "${OFE_ENCRYPT_SDK_URL}/openssl-${OFE_ARCH}.zip") + if(OFE_ARCH STREQUAL "x86") + set(OFE_PREBUILT_SHA256 "${OFE_ENCRYPT_SDK_X86_SHA256}") + else() + set(OFE_PREBUILT_SHA256 "${OFE_ENCRYPT_SDK_X64_SHA256}") + endif() +endif() + +if(NOT OFE_OPENSSL_ROOT) + set(OFE_OPENSSL_ROOT "${CMAKE_CURRENT_LIST_DIR}/../Encrypt-sdk/${OFE_ARCH}") +endif() +get_filename_component(OFE_OPENSSL_ROOT "${OFE_OPENSSL_ROOT}" ABSOLUTE) +if(NOT OFE_WORK_DIR) + set(OFE_WORK_DIR "${CMAKE_CURRENT_LIST_DIR}/out") +endif() +get_filename_component(OFE_WORK_DIR "${OFE_WORK_DIR}" ABSOLUTE) +file(MAKE_DIRECTORY "${OFE_WORK_DIR}") +file(LOCK "${OFE_WORK_DIR}/.ofe-prepare.lock" GUARD PROCESS TIMEOUT 1800) +set(log "${OFE_WORK_DIR}/build-${OFE_ARCH}.log") +file(WRITE "${log}" "Encrypt SDK preparation: ${OFE_OPENSSL_ROOT}\n") +function(run) + execute_process(COMMAND ${ARGV} RESULT_VARIABLE result + OUTPUT_VARIABLE output ERROR_VARIABLE errors ENCODING UTF-8 + ECHO_OUTPUT_VARIABLE ECHO_ERROR_VARIABLE) + file(APPEND "${log}" "\n${ARGV}\n${output}${errors}\nResult: ${result}\n") + if(NOT result STREQUAL "0") + message(FATAL_ERROR "Encrypt dependency command failed (${result}). See ${log}") + endif() +endfunction() +set(signature "${OFE_ARCH}|${OFE_ENCRYPT_VCPKG_COMMIT}|${OFE_PLATFORM_TOOLSET}|${OFE_VS_INSTANCE}|${OFE_PREBUILT_URL}|${OFE_PREBUILT_SHA256}") +if(OFE_PREBUILT_ARCHIVE) + file(SHA256 "${OFE_PREBUILT_ARCHIVE}" local_hash) + string(APPEND signature "|${local_hash}") +endif() +foreach(script IN ITEMS EncryptVersion.cmake vcpkg.json EnsureSDK.cmake ValidateSDK.cmake) + file(SHA256 "${CMAKE_CURRENT_LIST_DIR}/${script}" hash) + string(APPEND signature "|${hash}") +endforeach() +string(SHA256 signature "${signature}") +set(stamp "${OFE_OPENSSL_ROOT}/.ofe-sdk.stamp") +if(EXISTS "${stamp}") + file(READ "${stamp}" previous) + ofe_validate_encrypt_sdk("${OFE_OPENSSL_ROOT}" "${OFE_ARCH}" valid) + if(valid AND previous STREQUAL signature) + message(STATUS "OpenSSL SDK is up to date (${OFE_ARCH}).") + return() + endif() +endif() +set(triplet "${OFE_ARCH}-windows-static-md") +if(OFE_PREBUILT_URL OR OFE_PREBUILT_ARCHIVE) + string(LENGTH "${OFE_PREBUILT_SHA256}" hash_length) + if(NOT hash_length EQUAL 64 OR NOT OFE_PREBUILT_SHA256 MATCHES "^[0-9a-fA-F]+$") + message(FATAL_ERROR "Prebuilt OpenSSL SDKs require OFE_PREBUILT_SHA256.") + endif() + string(TOLOWER "${OFE_PREBUILT_SHA256}" OFE_PREBUILT_SHA256) + if(NOT OFE_PREBUILT_ARCHIVE) + file(MAKE_DIRECTORY "${OFE_WORK_DIR}/downloads") + set(OFE_PREBUILT_ARCHIVE "${OFE_WORK_DIR}/downloads/${OFE_PREBUILT_SHA256}.zip") + if(NOT EXISTS "${OFE_PREBUILT_ARCHIVE}") + message(STATUS "Downloading ${OFE_PREBUILT_URL}") + file(DOWNLOAD "${OFE_PREBUILT_URL}" "${OFE_PREBUILT_ARCHIVE}.part" + STATUS status TLS_VERIFY ON TIMEOUT 600 INACTIVITY_TIMEOUT 60 SHOW_PROGRESS) + list(GET status 0 result) + if(NOT result EQUAL 0) + file(REMOVE "${OFE_PREBUILT_ARCHIVE}.part") + message(FATAL_ERROR "OpenSSL SDK download failed: ${status}") + endif() + file(RENAME "${OFE_PREBUILT_ARCHIVE}.part" "${OFE_PREBUILT_ARCHIVE}") + endif() + endif() + file(SHA256 "${OFE_PREBUILT_ARCHIVE}" actual) + if(NOT actual STREQUAL OFE_PREBUILT_SHA256) + message(FATAL_ERROR "OpenSSL SDK archive SHA256 mismatch. Expected ${OFE_PREBUILT_SHA256}, got ${actual}") + endif() + set(prepared "${OFE_WORK_DIR}/prebuilt-${OFE_ARCH}") + file(REMOVE_RECURSE "${prepared}") + file(MAKE_DIRECTORY "${prepared}") + file(ARCHIVE_EXTRACT INPUT "${OFE_PREBUILT_ARCHIVE}" DESTINATION "${prepared}") +else() + if(NOT CMAKE_HOST_WIN32) + message(FATAL_ERROR "Source preparation requires Windows + Visual Studio. A prebuilt archive can be staged on any host.") + endif() + find_program(git NAMES git REQUIRED) + set(vcpkg "${OFE_WORK_DIR}/vcpkg") + if(NOT EXISTS "${vcpkg}/.git") + file(MAKE_DIRECTORY "${vcpkg}") + run("${git}" -C "${vcpkg}" init) + run("${git}" -C "${vcpkg}" remote add origin "${OFE_ENCRYPT_VCPKG_REPOSITORY}") + endif() + execute_process(COMMAND "${git}" -C "${vcpkg}" rev-parse HEAD + OUTPUT_VARIABLE current ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) + if(NOT current STREQUAL OFE_ENCRYPT_VCPKG_COMMIT) + execute_process(COMMAND "${git}" -C "${vcpkg}" diff --quiet RESULT_VARIABLE dirty) + if(dirty EQUAL 1) + message(FATAL_ERROR "Managed vcpkg has local tracked changes: ${vcpkg}") + endif() + run("${git}" -C "${vcpkg}" fetch --depth 1 origin "${OFE_ENCRYPT_VCPKG_COMMIT}") + run("${git}" -C "${vcpkg}" checkout --detach "${OFE_ENCRYPT_VCPKG_COMMIT}") + endif() + if(NOT EXISTS "${vcpkg}/vcpkg.exe") + run(cmd /c call "${vcpkg}/bootstrap-vcpkg.bat" -disableMetrics) + endif() + set(overlays "${OFE_WORK_DIR}/triplets") + file(MAKE_DIRECTORY "${overlays}") + set(triplet_body "set(VCPKG_TARGET_ARCHITECTURE ${OFE_ARCH})\nset(VCPKG_CRT_LINKAGE dynamic)\nset(VCPKG_LIBRARY_LINKAGE static)\n") + if(OFE_PLATFORM_TOOLSET) + if(NOT OFE_PLATFORM_TOOLSET MATCHES "^v[0-9]+$") + message(FATAL_ERROR "Unsupported MSVC toolset: ${OFE_PLATFORM_TOOLSET}") + endif() + string(APPEND triplet_body "set(VCPKG_PLATFORM_TOOLSET ${OFE_PLATFORM_TOOLSET})\n") + endif() + file(WRITE "${overlays}/${triplet}.cmake" "${triplet_body}") + set(install "${vcpkg}/vcpkg.exe" install "--vcpkg-root=${vcpkg}" "--triplet=${triplet}" + "--x-manifest-root=${CMAKE_CURRENT_LIST_DIR}" + "--x-install-root=${OFE_WORK_DIR}/installed" + "--overlay-triplets=${overlays}" --disable-metrics) + if(OFE_VS_INSTANCE) + get_filename_component(instance "${OFE_VS_INSTANCE}" REALPATH) + run("${CMAKE_COMMAND}" -E env "VCPKG_VISUAL_STUDIO_PATH=${instance}" ${install}) + else() + run(${install}) + endif() + set(prepared "${OFE_WORK_DIR}/installed/${triplet}") + file(WRITE "${prepared}/ofe-encryption-sdk.json" + "{\"arch\":\"${OFE_ARCH}\",\"version\":\"${OFE_ENCRYPT_OPENSSL_VERSION}\",\"linkage\":\"static\",\"crt\":\"dynamic\",\"vcpkg_commit\":\"${OFE_ENCRYPT_VCPKG_COMMIT}\"}\n") +endif() +ofe_validate_encrypt_sdk("${prepared}" "${OFE_ARCH}" valid) +if(NOT valid) + message(FATAL_ERROR "${OFE_ENCRYPT_VALIDATION_ERROR}") +endif() +# Stage and validate before replacing the previous usable SDK. +set(staging "${OFE_OPENSSL_ROOT}.new") +file(REMOVE_RECURSE "${staging}") +file(MAKE_DIRECTORY "${staging}") +file(COPY "${prepared}/" DESTINATION "${staging}") +file(WRITE "${staging}/.ofe-sdk.stamp" "${signature}") +set(backup "${OFE_OPENSSL_ROOT}.previous") +file(REMOVE_RECURSE "${backup}") +if(EXISTS "${OFE_OPENSSL_ROOT}") + file(RENAME "${OFE_OPENSSL_ROOT}" "${backup}") +endif() +file(RENAME "${staging}" "${OFE_OPENSSL_ROOT}" RESULT error) +if(error) + if(EXISTS "${backup}") + file(RENAME "${backup}" "${OFE_OPENSSL_ROOT}") + endif() + message(FATAL_ERROR "Cannot install OpenSSL SDK: ${error}") +endif() +file(REMOVE_RECURSE "${backup}") +message(STATUS "Prepared OpenSSL ${OFE_ENCRYPT_OPENSSL_VERSION} SDK: ${OFE_OPENSSL_ROOT}") diff --git a/Extensions/_3rdLib/Encrypt-build/README.md b/Extensions/_3rdLib/Encrypt-build/README.md new file mode 100644 index 000000000..0c55a66ff --- /dev/null +++ b/Extensions/_3rdLib/Encrypt-build/README.md @@ -0,0 +1,122 @@ +# EncryptDependencies: Windows OpenSSL SDK preparation + +EncryptDependencies is a Visual Studio utility project, following the repository's +FFmpeg/SDL dependency-project pattern. All nine existing Encryption consumers and +the native Encryption solution reference it. Build the solution normally: +preparation runs before ClCompile and reuses a complete matching SDK afterward. + +Default output: + +```text +Extensions/_3rdLib/Encrypt-sdk/x86/ # Win32 +Extensions/_3rdLib/Encrypt-sdk/x64/ # x64 + include/openssl/ + lib/libcrypto.lib # static library, /MD + debug/lib/libcrypto.lib # static library, /MDd + share/openssl/copyright + ofe-encryption-sdk.json +``` + +The default path downloads OpenSSL **3.6.5** from the repository's fixed +[encrypt-sdk-openssl-3.6.5-v143 release](https://github.com/defisym/OpenFusionExamples/releases/tag/encrypt-sdk-openssl-3.6.5-v143). +EncryptVersion.cmake pins the URL and each architecture's SHA256, like the FFmpeg +SDK preparation. The packages were built and tested with VS 2022 (v143), static +OpenSSL and dynamic CRT; each contains Release and Debug libraries. + +Build the solution normally. No manual download, vcpkg, Perl or NASM is needed. +VS C++ tools and CMake 3.21+ are sufficient; the VS-bundled CMake is discovered +automatically. Preparation does not silently fall back to building from source +when a download fails. + +From a VS Developer Command Prompt, prepare without compiling an extension: + +```bat +msbuild Extensions\_3rdLib\Encrypt-build\EncryptDependencies.vcxproj /m /p:Configuration=Release /p:Platform=Win32 +``` + +Use Platform=x64 for x64. The SDK is validated and installed to the default path +above, and Encryption.props automatically supplies the include/library paths. +Subsequent builds reuse a complete matching SDK, including without network access. +Missing SDK files can be restored from the local archive cache. + +## Custom paths and existing SDKs + +- OfeOpenSSLRoot selects the architecture-specific SDK directory. +- OfeAutoBuildEncrypt defaults to true for the managed default, false for an explicit + external root. Set it to true to prepare a custom destination. +- OfeOpenSSLLibDir overrides the selected lib/debug/lib directory when necessary. +- OfeEncryptCMake selects a CMake executable; the VS-bundled one is discovered first. +- OfeEncryptWorkDir changes the source/tool/download/build cache, defaulting to + Encrypt-build/out. Generated SDKs and caches are ignored by Git. + +For example: + +```bat +msbuild Extensions\Encrypter\Encrypter.sln /p:Configuration="Release Unicode" /p:Platform=Win32 /p:OfeOpenSSLRoot=D:\SDKs\Encrypt\x86 /p:OfeAutoBuildEncrypt=true +``` + +An already prepared external SDK can be selected with OfeOpenSSLRoot alone. Match +architecture, static linkage and /MD vs /MDd. This does not install OpenSSL globally +or change PATH for application runtime loading. + +## Prebuilt/offline SDKs + +Normally the fixed repository release is downloaded automatically. To use a local +archive, download openssl-x86.zip or openssl-x64.zip and its .sha256 file from the +release. A user-supplied archive or URL overrides the repository default: + +```bat +msbuild Extensions\_3rdLib\Encrypt-build\EncryptDependencies.vcxproj /p:Configuration=Release /p:Platform=Win32 /p:OfeEncryptPrebuiltArchive=D:\Downloads\openssl-x86.zip /p:OfeEncryptPrebuiltSha256=THE_64_DIGIT_SHA256 +``` + +OfeEncryptPrebuiltUrl can download the same archive from a chosen HTTPS location. +A SHA256 is required for either input. With a custom destination also pass +OfeOpenSSLRoot and OfeAutoBuildEncrypt=true. Prebuilt staging does not need Git, +vcpkg, Perl or NASM. It can also be invoked with CMake on other hosts: + +```sh +cmake -DOFE_ARCH=x86 -DOFE_OPENSSL_ROOT=/path/to/sdk \ + -DOFE_PREBUILT_ARCHIVE=/path/to/openssl-x86.zip \ + -DOFE_PREBUILT_SHA256=THE_64_DIGIT_SHA256 -P EnsureSDK.cmake +``` + +The ZIP root must contain the SDK files above, including both configurations and +the license. ofe-encryption-sdk.json identifies arch (x86/x64), version (3.6.5), +linkage (static) and crt (dynamic). The downloaded archive's SHA256, required files, +manifest, version header and library archive signatures are checked before replacing +the current SDK. Native CI then compiles/links/runs against the produced libraries. + +Preparation is serialized across projects. A signature covers version/configuration +and preparation scripts. Missing SDK files cause repair; repeated builds reuse the +existing SDK. Failed download, hash/metadata validation or builds preserve the +previous SDK. Logs and downloads remain under Encrypt-build/out. Once prepared, +a complete matching SDK is reusable without a network connection. + +## Explicit source builds + +For another toolset, or to produce an updated SDK, opt in explicitly: + +```bat +msbuild Extensions\_3rdLib\Encrypt-build\EncryptDependencies.vcxproj /m /p:Configuration=Release /p:Platform=Win32 /p:OfeEncryptBuildFromSource=true +``` + +This path pins vcpkg commit `9a5dfef5d1a0438a7d24856a85bd60f309d355e9` and OpenSSL +3.6.5 in EncryptVersion.cmake / vcpkg.json. It needs Windows, VS C++ tools, Git and +CMake; vcpkg acquires its additional build tools. The selected VS instance and +PlatformToolset are passed through. Both configurations use static libraries with +dynamic CRT. An explicit local archive/URL takes precedence over source mode. + +## Publishing SDK releases + +Encryption CI first tests the default downloaded SDK with native VS Release/Debug, +then builds source SDKs and uploads Encrypt-sdk-Win32 / Encrypt-sdk-x64 artifacts. +The Publish encryption SDK workflow promotes these archives, verifies their SHA256 +and SDK layouts, and creates a versioned release without changing the repository's +latest extension release. Its release notes record the source commit and CI run. + +The initial release uses successful CI run 41587062927. For a dependency update, +select a successful Encryption core run containing both SDK artifacts and publish +a new SDK tag through the workflow's manual run inputs. Update the version, URL +and per-architecture hashes in EncryptVersion.cmake together; keep existing tags +and assets unchanged. The publish workflow also runs when an encrypt-sdk-* tag is +pushed, using its configured source run. diff --git a/Extensions/_3rdLib/Encrypt-build/ValidateSDK.cmake b/Extensions/_3rdLib/Encrypt-build/ValidateSDK.cmake new file mode 100644 index 000000000..a31b6bc6a --- /dev/null +++ b/Extensions/_3rdLib/Encrypt-build/ValidateSDK.cmake @@ -0,0 +1,51 @@ +# Managed/prebuilt SDKs share this layout and manifest. Explicit external SDKs +# can still opt out of preparation through OfeAutoBuildEncrypt=false. +function(ofe_validate_encrypt_sdk root arch result) + set(${result} FALSE PARENT_SCOPE) + foreach(path IN ITEMS include/openssl/evp.h include/openssl/opensslv.h + lib/libcrypto.lib debug/lib/libcrypto.lib ofe-encryption-sdk.json) + if(NOT EXISTS "${root}/${path}" OR IS_DIRECTORY "${root}/${path}") + set(OFE_ENCRYPT_VALIDATION_ERROR "Missing OpenSSL SDK file: ${root}/${path}" PARENT_SCOPE) + return() + endif() + endforeach() + file(READ "${root}/ofe-encryption-sdk.json" metadata) + foreach(field IN ITEMS arch version linkage crt) + string(JSON value ERROR_VARIABLE error GET "${metadata}" ${field}) + if(error) + set(OFE_ENCRYPT_VALIDATION_ERROR "Invalid OpenSSL SDK manifest: ${field}" PARENT_SCOPE) + return() + endif() + set(sdk_${field} "${value}") + endforeach() + if(NOT sdk_arch STREQUAL arch OR NOT sdk_version STREQUAL OFE_ENCRYPT_OPENSSL_VERSION + OR NOT sdk_linkage STREQUAL "static" OR NOT sdk_crt STREQUAL "dynamic") + set(OFE_ENCRYPT_VALIDATION_ERROR "OpenSSL SDK must be ${arch}, OpenSSL ${OFE_ENCRYPT_OPENSSL_VERSION}, static libraries with /MD and /MDd." PARENT_SCOPE) + return() + endif() + file(READ "${root}/include/openssl/opensslv.h" version_header) + string(REPLACE "." "\\." version_pattern "${OFE_ENCRYPT_OPENSSL_VERSION}") + if(NOT version_header MATCHES "OPENSSL_VERSION_STR[ \t]+\"${version_pattern}\"") + set(OFE_ENCRYPT_VALIDATION_ERROR "OpenSSL SDK header version does not match its manifest." PARENT_SCOPE) + return() + endif() + set(licensed FALSE) + foreach(path IN ITEMS share/openssl/copyright LICENSE.txt LICENSE) + if(EXISTS "${root}/${path}" AND NOT IS_DIRECTORY "${root}/${path}") + set(licensed TRUE) + endif() + endforeach() + if(NOT licensed) + set(OFE_ENCRYPT_VALIDATION_ERROR "OpenSSL SDK license is missing." PARENT_SCOPE) + return() + endif() + foreach(path IN ITEMS lib/libcrypto.lib debug/lib/libcrypto.lib) + file(READ "${root}/${path}" magic LIMIT 8 HEX) + if(NOT magic STREQUAL "213c617263683e0a") + set(OFE_ENCRYPT_VALIDATION_ERROR "Not a COFF library archive: ${root}/${path}" PARENT_SCOPE) + return() + endif() + endforeach() + set(${result} TRUE PARENT_SCOPE) + set(OFE_ENCRYPT_VALIDATION_ERROR "" PARENT_SCOPE) +endfunction() diff --git a/Extensions/_3rdLib/Encrypt-build/tests/test_sdk.py b/Extensions/_3rdLib/Encrypt-build/tests/test_sdk.py new file mode 100644 index 000000000..94c105057 --- /dev/null +++ b/Extensions/_3rdLib/Encrypt-build/tests/test_sdk.py @@ -0,0 +1,168 @@ +"""Exercise the actual SDK stager with local archives, without Windows/network.""" +import hashlib +import json +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest +import zipfile + +ROOT = Path(__file__).resolve().parents[3] +SCRIPTS = ROOT / '_3rdLib' / 'Encrypt-build' +CMAKE = os.environ.get('CMAKE', shutil.which('cmake') or 'cmake') + +class SDKTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + self.sdk = self.root / 'SDK with spaces' + self.work = self.root / 'work' + def archive(self, arch='x86', version='3.6.5', missing=None): + files = { + 'include/openssl/evp.h': b'/* test header */', + 'include/openssl/opensslv.h': ('#define OPENSSL_VERSION_STR "'+version+'"\n').encode(), + 'lib/libcrypto.lib': b'!\nfixture', + 'debug/lib/libcrypto.lib': b'!\nfixture-debug', + 'share/openssl/copyright': b'Apache-2.0 fixture', + 'ofe-encryption-sdk.json': json.dumps(dict(arch=arch, version=version, linkage='static', crt='dynamic')).encode(), + } + if missing: files.pop(missing) + path = self.root / (arch+'-'+version+'-'+str(len(list(self.root.glob('*.zip'))))+'.zip') + with zipfile.ZipFile(path, 'w') as archive: + for name, content in files.items(): archive.writestr(name, content) + return path + def prepare(self, archive, sha=None): + digest = sha or hashlib.sha256(archive.read_bytes()).hexdigest() + return subprocess.run([CMAKE, '-DOFE_ARCH=x86', '-DOFE_OPENSSL_ROOT='+str(self.sdk), + '-DOFE_WORK_DIR='+str(self.work), '-DOFE_PREBUILT_ARCHIVE='+str(archive), + '-DOFE_PREBUILT_SHA256='+digest, '-P', str(SCRIPTS/'EnsureSDK.cmake')], + text=True, capture_output=True) + def test_install_reuse_and_repair(self): + archive = self.archive() + result = self.prepare(archive) + self.assertEqual(result.returncode, 0, result.stdout+result.stderr) + stamp = self.sdk/'.ofe-sdk.stamp' + before = stamp.stat().st_mtime_ns + result = self.prepare(archive) + self.assertEqual(result.returncode, 0, result.stdout+result.stderr) + self.assertIn('up to date', result.stdout) + self.assertEqual(stamp.stat().st_mtime_ns, before) + (self.sdk/'debug/lib/libcrypto.lib').unlink() + self.assertEqual(self.prepare(archive).returncode, 0) + self.assertTrue((self.sdk/'debug/lib/libcrypto.lib').exists()) + def test_url_download_and_cache(self): + archive = self.archive() + sha = hashlib.sha256(archive.read_bytes()).hexdigest() + result = subprocess.run([CMAKE, '-DOFE_ARCH=x86', '-DOFE_OPENSSL_ROOT='+str(self.sdk), + '-DOFE_WORK_DIR='+str(self.work), '-DOFE_PREBUILT_URL='+archive.as_uri(), + '-DOFE_PREBUILT_SHA256='+sha, '-P', str(SCRIPTS/'EnsureSDK.cmake')], + text=True, capture_output=True) + self.assertEqual(result.returncode, 0, result.stdout+result.stderr) + self.assertEqual((self.work/'downloads'/(sha+'.zip')).read_bytes(), archive.read_bytes()) + + def test_default_release_by_architecture_and_offline_repair(self): + import re + scripts = self.root / 'scripts' + scripts.mkdir() + for name in ['EnsureSDK.cmake', 'ValidateSDK.cmake', 'EncryptVersion.cmake', 'vcpkg.json']: + shutil.copyfile(SCRIPTS/name, scripts/name) + version = (scripts/'EncryptVersion.cmake').read_text() + mirror = self.root/'release' + mirror.mkdir() + version = re.sub(r'set\(OFE_ENCRYPT_SDK_URL "[^"]+"\)', + 'set(OFE_ENCRYPT_SDK_URL "'+mirror.as_uri()+'")', version) + digests = {} + for arch in ['x86', 'x64']: + archive = self.archive(arch) + shutil.copyfile(archive, mirror/('openssl-'+arch+'.zip')) + digests[arch] = hashlib.sha256(archive.read_bytes()).hexdigest() + version = re.sub(r'set\(OFE_ENCRYPT_SDK_'+arch.upper()+r'_SHA256 "[^"]+"\)', + 'set(OFE_ENCRYPT_SDK_'+arch.upper()+'_SHA256 "'+digests[arch]+'")', version) + (scripts/'EncryptVersion.cmake').write_text(version) + + for arch in ['x86', 'x64']: + sdk = self.sdk/arch + command = [CMAKE, '-DOFE_ARCH='+arch, '-DOFE_PLATFORM_TOOLSET=v143', + '-DOFE_OPENSSL_ROOT='+str(sdk), '-DOFE_WORK_DIR='+str(self.work), + '-P', str(scripts/'EnsureSDK.cmake')] + result = subprocess.run(command, text=True, capture_output=True) + self.assertEqual(result.returncode, 0, result.stdout+result.stderr) + metadata = json.loads((sdk/'ofe-encryption-sdk.json').read_text()) + self.assertEqual(metadata['arch'], arch) + self.assertTrue((sdk/'lib/libcrypto.lib').exists()) + self.assertTrue((sdk/'debug/lib/libcrypto.lib').exists()) + self.assertFalse((self.work/'vcpkg').exists()) + + # After the first install, the prepared SDK and cached archive work offline. + (mirror/('openssl-'+arch+'.zip')).unlink() + result = subprocess.run(command, text=True, capture_output=True) + self.assertEqual(result.returncode, 0, result.stdout+result.stderr) + self.assertIn('up to date', result.stdout) + (sdk/'debug/lib/libcrypto.lib').unlink() + result = subprocess.run(command, text=True, capture_output=True) + self.assertEqual(result.returncode, 0, result.stdout+result.stderr) + self.assertTrue((sdk/'debug/lib/libcrypto.lib').exists()) + + def test_source_build_is_explicit_and_toolset_is_checked(self): + command = [CMAKE, '-DOFE_ARCH=x86', '-DOFE_OPENSSL_ROOT='+str(self.sdk), + '-DOFE_WORK_DIR='+str(self.work), '-DOFE_PLATFORM_TOOLSET=v142'] + result = subprocess.run(command+['-P', str(SCRIPTS/'EnsureSDK.cmake')], text=True, capture_output=True) + self.assertNotEqual(result.returncode, 0) + self.assertIn('OfeEncryptBuildFromSource=true', result.stderr) + if os.name != 'nt': + result = subprocess.run(command+['-DOFE_BUILD_FROM_SOURCE=true', '-P', + str(SCRIPTS/'EnsureSDK.cmake')], text=True, capture_output=True) + self.assertNotEqual(result.returncode, 0) + self.assertIn('Source preparation requires Windows', result.stderr) + + def test_rejections_preserve_previous_sdk(self): + good = self.archive() + self.assertEqual(self.prepare(good).returncode, 0) + marker = self.sdk/'keep.txt' + marker.write_text('previous SDK') + original = (self.sdk/'.ofe-sdk.stamp').read_bytes() + for archive, sha in [(good, '0'*64), (self.archive('x64'), None), + (self.archive(version='3.0.0'), None), + (self.archive(missing='debug/lib/libcrypto.lib'), None), + (self.archive(missing='share/openssl/copyright'), None)]: + result = self.prepare(archive, sha) + self.assertNotEqual(result.returncode, 0, result.stdout) + self.assertEqual(marker.read_text(), 'previous SDK') + self.assertEqual((self.sdk/'.ofe-sdk.stamp').read_bytes(), original) + def test_version_pin_and_projects(self): + import re + import xml.etree.ElementTree as ET + cmake = (SCRIPTS/'EncryptVersion.cmake').read_text() + baseline = json.loads((SCRIPTS/'vcpkg.json').read_text())['builtin-baseline'] + self.assertIn(baseline, cmake) + self.assertRegex(baseline, r'^[0-9a-f]{40}$') + self.assertIn('defisym/OpenFusionExamples/releases/download/encrypt-sdk-', cmake) + for arch in ['X86', 'X64']: + self.assertRegex(cmake, 'OFE_ENCRYPT_SDK_'+arch+r'_SHA256 "[0-9a-f]{64}"') + ns = {'m': 'http://schemas.microsoft.com/developer/msbuild/2003'} + props = ET.parse(ROOT/'_DeLib/Encryption.props') + source_mode = props.find('.//m:OfeEncryptBuildFromSource', ns) + self.assertEqual(source_mode.text, 'false') + reference = props.find('.//m:ProjectReference', ns) + self.assertIsNotNone(reference) + self.assertIn('EncryptDependencies.vcxproj', reference.attrib['Include']) + count = 0 + for project in ROOT.glob('*/*.vcxproj'): + text = project.read_text(encoding='utf-8-sig') + if 'Include="..\\_DeLib\\Encryption.cpp"' not in text: continue + count += 1 + self.assertIn('Encryption.props', text) + solution = project.with_suffix('.sln').read_text(encoding='utf-8-sig') + self.assertEqual(solution.count(' = "EncryptDependencies"'), 1) + self.assertRegex(solution, r'\.Debug Unicode\|(?:Win32|x86)\.ActiveCfg = Release\|Win32') + self.assertNotIn('ActiveCfg = Release|x86', solution) + self.assertEqual(count, 9) + ET.parse(SCRIPTS/'EncryptDependencies.vcxproj') + header = (ROOT/'_DeLib/Encryption.h').read_text() + self.assertNotRegex(header, r'#include\s*[<"](?:windows|bcrypt)\.h') + self.assertNotRegex(header, r'\b(?:BYTE|DWORD|PBYTE|LPCWSTR|BCRYPT_[A-Z_]+)\b') + +if __name__ == '__main__': unittest.main() diff --git a/Extensions/_3rdLib/Encrypt-build/vcpkg.json b/Extensions/_3rdLib/Encrypt-build/vcpkg.json new file mode 100644 index 000000000..3c47e0c55 --- /dev/null +++ b/Extensions/_3rdLib/Encrypt-build/vcpkg.json @@ -0,0 +1,6 @@ +{ + "name": "ofe-encryption-dependencies", + "version-string": "1", + "builtin-baseline": "9a5dfef5d1a0438a7d24856a85bd60f309d355e9", + "dependencies": [{ "name": "openssl", "default-features": false }] +} diff --git a/Extensions/_DeLib/Encryption.cpp b/Extensions/_DeLib/Encryption.cpp index c295058da..64509a10b 100644 --- a/Extensions/_DeLib/Encryption.cpp +++ b/Extensions/_DeLib/Encryption.cpp @@ -1,174 +1,211 @@ #include "Encryption.h" -Encryption::~Encryption() { - Release(this->InputData); - Release(this->OutputData); - Release(this->Key); - Release(this->IV); - Release(this->InputStr); - Release(this->OutputStr); +#include - Release(this->HashStr); +namespace { + +std::optional digest(const wchar_t* name) { + if (!name) return std::nullopt; + + const std::wstring_view value(name); + if (value == L"MD5") return encryption::Digest::md5; + if (value == L"SHA1") return encryption::Digest::sha1; + if (value == L"SHA256") return encryption::Digest::sha256; + if (value == L"SHA384") return encryption::Digest::sha384; + if (value == L"SHA512") return encryption::Digest::sha512; + return std::nullopt; } -bool Encryption::OpenFile(const wchar_t* FileName) { - return OpenFileGeneral(FileName, [this] (FILE* fp, long sz) { - this->InputData = new BYTE[this->InputLength]; - fread(this->InputData, this->InputLength, 1, fp); - }); } -bool Encryption::SaveFile(const wchar_t* pFileName, PBYTE pData, DWORD sz) { - if (pData == nullptr) { return false; } +bool Encryption::store(encryption::Bytes& destination, const std::uint8_t* buffer, std::size_t size) { + // The compatibility API exposes 32-bit lengths, including a string terminator. + if ((buffer == nullptr && size != 0) || size >= (std::numeric_limits::max)()) { + destination.clear(); + return false; + } - FILE* fp = nullptr; + // Zero-length data is valid for empty-string/file hashes. Keep its trailing + // zero to distinguish a valid empty buffer from cleared/invalid input. + encryption::Bytes next(size + 1, 0); + if (size) std::memcpy(next.data(), buffer, size); + destination = std::move(next); + return true; +} - const auto err = _wfopen_s(&fp, pFileName, L"wb"); - if (err != 0|| fp == nullptr) { return false; } +std::uint8_t* Encryption::data(const encryption::Bytes& buffer) noexcept { + return buffer.empty() ? nullptr : const_cast(buffer.data()); +} - const auto elementCount = fwrite(pData, sz, 1, fp); - const auto ret = fclose(fp); +std::uint32_t Encryption::length(const encryption::Bytes& buffer) noexcept { + return buffer.empty() ? 0 : static_cast(buffer.size() - 1); +} - return ret == 0; +void Encryption::ClearEncryptData() noexcept { + input_.clear(); + output_.clear(); + input_string_.clear(); + output_string_.clear(); + hash_string_.clear(); } -bool Encryption::SaveFile(const wchar_t* FileName, bool SaveSrc) const { - PBYTE Output = SaveSrc ? this->InputData : this->OutputData; - DWORD Length = SaveSrc ? this->InputLength : this->OutputLength; +void Encryption::SetEncryptStr(const std::string& text) { + if (!store(input_, reinterpret_cast(text.data()), text.size())) { + ClearEncryptData(); + return; + } - return SaveFile(FileName, Output, Length); + output_.clear(); hash_string_.clear(); } -template -void Encryption::SetEncryptData(const T* pBuf, DWORD sz) { - Release(this->InputData); +void Encryption::SetEncryptStr(const char* text, std::uint32_t size) { SetEncryptData(text, size); } - this->InputLength = sz * sizeof(T); - this->InputData = new BYTE[this->InputLength]; - memcpy(this->InputData, pBuf, this->InputLength); +void Encryption::SetEncryptStr(const std::wstring& text) { + encryption::Bytes bytes; + try { bytes = to_utf16_bytes(text); } + catch (const std::invalid_argument& error) { throw encryption::Error(encryption::ErrorCode::argument, error.what()); } - return; -} - -void Encryption::SetEncryptStr(const std::string& Str) { - SetEncryptData(Str.c_str(), (DWORD)Str.length()); -} + if (!store(input_, bytes.data(), bytes.size())) { + ClearEncryptData(); + return; + } -void Encryption::SetEncryptStr(const char* Str, DWORD StrLength) { - SetEncryptData(Str, StrLength); + output_.clear(); hash_string_.clear(); } -void Encryption::SetEncryptStr(const std::wstring& Str) { - SetEncryptData(Str.c_str(), (DWORD)Str.length()); -} +void Encryption::SetEncryptStr(const wchar_t* text, std::uint32_t size) { + if (text == nullptr && size != 0) { + ClearEncryptData(); + return; + } -void Encryption::SetEncryptStr(const wchar_t* Str, DWORD StrLength) { - SetEncryptData(Str, StrLength); + SetEncryptStr(text ? std::wstring(text, size) : std::wstring()); } char* Encryption::GetInputStr() { - return GetStr(this->InputStr, this->InputData, this->InputLength); -} - -DWORD Encryption::GetInputStrLength() const { - return this->InputLength + 1; + if (input_.empty()) return nullptr; + input_string_.assign(reinterpret_cast(input_.data()), length(input_)); + return input_string_.data(); } -void Encryption::ReleaseInputStr() { - Release(this->InputStr); - this->InputLength = 0; +char* Encryption::GetOutputStr() { + if (output_.empty()) return nullptr; + output_string_.assign(reinterpret_cast(output_.data()), length(output_)); + return output_string_.data(); } -void Encryption::ReleaseInputData() { - Release(this->InputData); - this->InputLength = 0; +void Encryption::GenerateKey(const wchar_t* text) { + material_ = encryption::keygen::from_wide(text ? text : L"", + encryption::keygen::native_seed_width); } -char* Encryption::GetOutputStr() { - return GetStr(this->OutputStr, this->OutputData, this->OutputLength); -} +bool Encryption::OpenFile(const wchar_t* file) { + input_.clear(); output_.clear(); hash_string_.clear(); + if (!file || !*file) return false; -DWORD Encryption::GetOutputStrLength() const { - return this->OutputLength + 1; -} + try { + encryption::FileSource source{std::filesystem::path(file)}; + if (source.size() >= (std::numeric_limits::max)()) return false; -void Encryption::ReleaseOutputStr() { - Release(this->OutputStr); - this->OutputLength = 0; + encryption::Bytes next(static_cast(source.size()) + 1, 0); + source.read_exact(0, next.data(), next.size() - 1); + input_ = std::move(next); + return true; + } catch (const encryption::Error&) { return false; } + catch (const std::filesystem::filesystem_error&) { return false; } } -void Encryption::GenerateKey(const wchar_t* KeyStr) { - //release old - Release(this->Key); - Release(this->IV); +bool Encryption::SaveFile(const wchar_t* file, std::uint8_t* buffer, std::uint32_t size) { + if (!file || !*file || !buffer) return false; - this->Key = new BYTE[KEY_LENGTH]; - this->IV = new BYTE[KEY_LENGTH]; + try { + const auto path = std::filesystem::path(file); +#ifdef _WIN32 + FILE* output = _wfopen(path.c_str(), L"wb"); +#else + FILE* output = std::fopen(path.c_str(), "wb"); +#endif + if (!output) return false; + const bool written = std::fwrite(buffer, 1, size, output) == size; + const bool closed = std::fclose(output) == 0; + return written && closed; + } catch (const encryption::Error&) { return false; } + catch (const std::filesystem::filesystem_error&) { return false; } +} - // empty, use default string - if (KeyStr == nullptr || wcslen(KeyStr) == 0) { - memcpy(this->Key, DefaultKey, KEY_LENGTH); - memcpy(this->IV, DefaultIV, KEY_LENGTH); +bool Encryption::SaveFile(const wchar_t* file, bool source) const { + return source ? SaveFile(file, GetInputData(), GetInputDataLength()) + : SaveFile(file, GetOutputData(), GetOutputDataLength()); +} - return; - } +bool Encryption::process(encryption::Mode mode, const wchar_t* algorithm) { + output_.clear(); hash_string_.clear(); + if (input_.empty() || !algorithm || std::wcscmp(algorithm, L"AES")) return false; - const auto keyStrLength = wcslen(KeyStr); + try { + const auto bytes = encryption::transform_buffer(material_, mode, input_.data(), length(input_)); - const DWORD strLength = sizeof(wchar_t) * static_cast(keyStrLength); - const DWORD wantedLength = this->KeyLength + this->IVLength; + return store(output_, bytes.data(), bytes.size()); + } catch (const encryption::Error&) { return false; } +} - // length enough - if (strLength == wantedLength) { - memcpy(this->Key, KeyStr, KEY_LENGTH); - memcpy(this->IV, KeyStr + STR_LENGTH, KEY_LENGTH); +bool Encryption::EncryptFile(const wchar_t* file, const wchar_t* algorithm) { + const bool result = OpenFile(file) && Encrypt(algorithm); + ReleaseInputData(); + return result; +} - return; - } +bool Encryption::DecryptFile(const wchar_t* file, const wchar_t* algorithm) { + const bool result = OpenFile(file) && Decrypt(algorithm); + ReleaseInputData(); + return result; +} - // too long, do hash move - if (strLength > wantedLength) { - size_t loop = keyStrLength - KEY_LENGTH; - auto pChar = KeyStr + KEY_LENGTH; +bool Encryption::process_file(const wchar_t* file, encryption::Mode mode, const wchar_t* algorithm) { + input_.clear(); output_.clear(); hash_string_.clear(); + if (!file || !*file || !algorithm || std::wcscmp(algorithm, L"AES")) return false; - memcpy(this->Key, KeyStr, KEY_LENGTH); - memcpy(this->IV, KeyStr + STR_LENGTH, KEY_LENGTH); + try { + encryption::FileSource source{std::filesystem::path(file)}; - for (; loop != 0; loop--) { - const auto curChar = pChar[0]; - pChar++; + encryption::Bytes result; + std::uint64_t position = 0; - for (size_t pos = 0; pos < STR_LENGTH; pos++) { - (reinterpret_cast(this->Key) + pos)[0] ^= curChar; - (reinterpret_cast(this->IV) + pos)[0] ^= curChar; - } - } + const auto read = [&](std::uint8_t* buffer, std::size_t capacity) { + const auto requested = static_cast((std::min)(std::uint64_t(capacity), std::uint64_t(buffer_size_) * 16)); + const auto count = source.read_at(position, buffer, requested); + position += count; + return count; + }; - return; - } + encryption::transform(material_, mode, read, [&](const std::uint8_t* bytes, std::size_t size) { + result.insert(result.end(), bytes, bytes + size); + }); - // too short, loop to fill - size_t seed = keyStrLength; + return store(output_, result.data(), result.size()); + } catch (const encryption::Error&) { return false; } + catch (const std::filesystem::filesystem_error&) { return false; } +} - for (size_t loop = keyStrLength; loop != 0; loop--) { - seed ^= KeyStr[loop - 1] + HASHER_MOVE(seed); - } +const wchar_t* Encryption::GetHash(const wchar_t* algorithm) { + output_.clear(); hash_string_.clear(); + const auto selected = digest(algorithm); + if (input_.empty() || !selected) return nullptr; - const auto generatedKeyStr = new wchar_t[KEY_LENGTH]; - memset(generatedKeyStr, 0, KEY_LENGTH * sizeof(wchar_t)); - memcpy(generatedKeyStr, KeyStr, keyStrLength * sizeof(wchar_t)); + try { + encryption::Hash hash(*selected); + hash.update(input_.data(), length(input_)); - for (size_t loop = keyStrLength; loop < KEY_LENGTH; loop++) { - const auto pChar = generatedKeyStr + loop; - const wchar_t high = HIBYTE(seed); - const wchar_t low = LOBYTE(seed); - pChar[0] = high ^ low; + const auto bytes = hash.finish(); - seed ^= pChar[0] + HASHER_MOVE(seed); - } + store(output_, bytes.data(), bytes.size()); - memcpy(this->Key, generatedKeyStr, KEY_LENGTH); - memcpy(this->IV, generatedKeyStr + STR_LENGTH, KEY_LENGTH); + constexpr wchar_t hex[] = L"0123456789abcdef"; + for (const auto byte : bytes) { + hash_string_.push_back(hex[byte >> 4]); + hash_string_.push_back(hex[byte & 15]); + } - delete[] generatedKeyStr; -} \ No newline at end of file + return hash_string_.c_str(); + } catch (const encryption::Error&) { return nullptr; } +} diff --git a/Extensions/_DeLib/Encryption.h b/Extensions/_DeLib/Encryption.h index 43e6b475d..c3783b27c 100644 --- a/Extensions/_DeLib/Encryption.h +++ b/Extensions/_DeLib/Encryption.h @@ -1,825 +1,131 @@ #pragma once -#define NOMINMAX -#define WIN32_LEAN_AND_MEAN -#include +// Source-compatible Fusion facade; every operation delegates to the new core. -#include -#include -#include +#include "Encryption/KeyGen.h" -#include +#include #include #include +#include -//#include - -#include "GeneralDefinition.h" - -#define USER_MODE - -#ifdef USER_MODE -#pragma comment(lib, "bcrypt.lib") -#else -// requires cng.sys -#pragma comment(lib, "Cng.lib") -#endif - -// for hash -#pragma comment(lib, "Crypt32.lib") - -//// compatible with WinBase -//#pragma push_macro("EncryptFile") -//#pragma push_macro("DecryptFile") - +// Windows SDK EFS aliases must not rename the compatibility methods. #undef EncryptFile #undef DecryptFile -constexpr auto ENCRY = true; -constexpr auto DECRY = false; - -constexpr auto KEY_LENGTH = 16; -constexpr auto STR_LENGTH = KEY_LENGTH / sizeof(wchar_t); - -constexpr auto BUFFER_SIZE = 1024 * 256; - -constexpr static BYTE DefaultKey[KEY_LENGTH] = { - 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, - 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F -}; - -constexpr static BYTE DefaultIV[KEY_LENGTH] = { - 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, - 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F -}; - -#define STATUS_SUCCESS ((NTSTATUS)0x00000000L) - -#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0) -#define STATUS_UNSUCCESSFUL ((NTSTATUS)0xC0000001L) +inline constexpr std::uint32_t BUFFER_SIZE = 1024 * 256; class Encryption { -private: - PBYTE InputData = nullptr; - PBYTE OutputData = nullptr; - - DWORD InputLength = 0; - DWORD OutputLength = 0; - - PBYTE Key = nullptr; - PBYTE IV = nullptr; - - DWORD KeyLength = KEY_LENGTH; - DWORD IVLength = KEY_LENGTH; - - char* InputStr = nullptr; - char* OutputStr = nullptr; - - DWORD HashLength = 0; - LPWSTR HashStr = nullptr; - - DWORD BufferSize = BUFFER_SIZE; - - template - static inline T* GetStr(T*& pOutput, const PBYTE pSrc, size_t sz) { - if (pSrc == nullptr) { - return nullptr; - } - - //allocate and ensure NULL terminated - Alloc(pOutput, sz + 1); - memcpy(pOutput, pSrc, sz); - pOutput[sz] = 0; - - return pOutput; - } - - template - static inline void Release(T*& Pointer) { - delete[] Pointer; - Pointer = nullptr; - - return; - } - - template - static inline bool Alloc(T*& Pointer, const size_t sz) { - Release(Pointer); - Pointer = new T[sz]; - memset(Pointer, 0, sizeof(T) * sz); - - return nullptr != Pointer; - } - - inline void GetReadableHash() { - //Release - Release(this->HashStr); - - //Update output length - CryptBinaryToString(this->OutputData, this->OutputLength - , CRYPT_STRING_HEXRAW | CRYPT_STRING_NOCRLF - , nullptr, &this->HashLength); - - //Output - this->HashStr = new WCHAR[this->HashLength]; - CryptBinaryToString(this->OutputData, this->OutputLength - , CRYPT_STRING_HEXRAW | CRYPT_STRING_NOCRLF - , this->HashStr, &this->HashLength); - } - - using HandlerType = decltype(BCryptEncrypt); - - inline bool Encrypt_Core(HandlerType handler - , const LPCWSTR Algorithm = BCRYPT_AES_ALGORITHM - , const std::function& externalEncrypter = nullptr) { - //do nothing if input is invalid - if (this->InputData == nullptr) { - return false; - } - - //If no key generated, generate default key - if ((this->IV == nullptr) || (this->Key == nullptr)) { - this->GenerateKey(L""); - } - - //Define Vars - BCRYPT_ALG_HANDLE hAesAlg = nullptr; - BCRYPT_KEY_HANDLE hKey = nullptr; - NTSTATUS status = STATUS_UNSUCCESSFUL; - - DWORD - cbData = 0, - cbKeyObject = 0, - cbBlockLen = 0; - PBYTE - pbKeyObject = nullptr, - pbIV = nullptr; - - bool result = true; - - auto AllocOutputBuffer = [&] (HandlerType pHandler) { - if (!NT_SUCCESS(status = pHandler( - hKey, - this->InputData, this->InputLength, - NULL, - pbIV, cbBlockLen, - NULL, 0, - &(this->OutputLength), - BCRYPT_BLOCK_PADDING))) { - //wprintf(L"**** Error 0x%x returned by BCryptEncrypt\n", status); - return false; - } + encryption::Bytes input_, output_; + std::string input_string_, output_string_; + std::wstring hash_string_; + encryption::KeyIv material_ = encryption::keygen::from_utf16(u""); + std::uint32_t buffer_size_ = BUFFER_SIZE; - return Alloc(this->OutputData, this->OutputLength); - }; + static bool store(encryption::Bytes& destination, const std::uint8_t* data, std::size_t size); + static std::uint8_t* data(const encryption::Bytes& buffer) noexcept; + static std::uint32_t length(const encryption::Bytes& buffer) noexcept; - // General routine of En/Decrypt - // Open an algorithm handle. - if (!NT_SUCCESS(status = BCryptOpenAlgorithmProvider( - &hAesAlg, - Algorithm, - NULL, - 0))) { - //wprintf(L"**** Error 0x%x returned by BCryptOpenAlgorithmProvider\n", status); - result = false; - goto Cleanup; - } - - // Calculate the size of the buffer to hold the KeyObject. - if (!NT_SUCCESS(status = BCryptGetProperty( - hAesAlg, - BCRYPT_OBJECT_LENGTH, - (PBYTE)&cbKeyObject, sizeof(DWORD), - &cbData, 0))) { - //wprintf(L"**** Error 0x%x returned by BCryptGetProperty\n", status); - result = false; - goto Cleanup; - } - - // Allocate the key object on the heap. - pbKeyObject = (PBYTE)HeapAlloc(GetProcessHeap(), 0, cbKeyObject); - if (nullptr == pbKeyObject) { - //wprintf(L"**** memory allocation failed\n"); - result = false; - goto Cleanup; - } - - // Calculate the block length for the IV. - if (!NT_SUCCESS(status = BCryptGetProperty( - hAesAlg, - BCRYPT_BLOCK_LENGTH, - (PBYTE)&cbBlockLen, sizeof(DWORD), - &cbData, 0))) { - //wprintf(L"**** Error 0x%x returned by BCryptGetProperty\n", status); - result = false; - goto Cleanup; - } - - // Determine whether the cbBlockLen is not longer than the IV length. - if (cbBlockLen > this->IVLength) { - //wprintf(L"**** block length is longer than the provided IV length\n"); - result = false; - goto Cleanup; - } - - // Allocate a buffer for the IV. The buffer is consumed during the - // encrypt/decrypt process. - pbIV = (PBYTE)HeapAlloc(GetProcessHeap(), 0, cbBlockLen); - if (nullptr == pbIV) { - //wprintf(L"**** memory allocation failed\n"); - result = false; - goto Cleanup; - } - - memcpy(pbIV, this->IV, cbBlockLen); - - if (!NT_SUCCESS(status = BCryptSetProperty( - hAesAlg, - BCRYPT_CHAINING_MODE, - (PBYTE)BCRYPT_CHAIN_MODE_CBC, sizeof(BCRYPT_CHAIN_MODE_CBC), - 0))) { - //wprintf(L"**** Error 0x%x returned by BCryptSetProperty\n", status); - result = false; - goto Cleanup; - } - - // Generate the key from supplied input key bytes. - if (!NT_SUCCESS(status = BCryptGenerateSymmetricKey( - hAesAlg, - &hKey, - pbKeyObject, cbKeyObject, - this->Key, this->KeyLength, - 0))) { - //wprintf(L"**** Error 0x%x returned by BCryptGenerateSymmetricKey\n", status); - result = false; - goto Cleanup; - } - - // alloc output buffer. - if (!AllocOutputBuffer(handler)) { - // wprintf(L"**** memory allocation failed\n"); - result = false; - goto Cleanup; - } - - // Use the key to encrypt the plaintext buffer. - // For block sized messages, block padding will add an extra block. - status = externalEncrypter != nullptr - ? externalEncrypter(handler - , hKey - , pbIV, cbBlockLen) - : handler( - hKey, - this->InputData, this->InputLength, - nullptr, - pbIV, cbBlockLen, - this->OutputData, this->OutputLength, - &(this->OutputLength), - BCRYPT_BLOCK_PADDING); - - if (!NT_SUCCESS(status)) { - //wprintf(L"**** Error 0x%x returned by BCryptEncrypt\n", status); - result = false; - goto Cleanup; - } - - Cleanup: - - if (hAesAlg) { - BCryptCloseAlgorithmProvider(hAesAlg, 0); - } - - if (hKey) { - BCryptDestroyKey(hKey); - } - - if (pbKeyObject) { - HeapFree(GetProcessHeap(), 0, pbKeyObject); - } - - if (pbIV) { - HeapFree(GetProcessHeap(), 0, pbIV); - } - - return result; - } - - inline bool OpenFileGeneral(const wchar_t* FileName, const std::function& callBack) { - Release(this->InputData); - - FILE* fp = nullptr; - - _wfopen_s(&fp, FileName, L"rb"); - if (fp == nullptr) { -#ifdef _DEBUG - OutputDebugStringW(L"Load File Error"); -#endif - - return false; - } - - fseek(fp, 0, SEEK_END); - this->InputLength = ftell(fp); - rewind(fp); - - callBack(fp, this->InputLength); - - fclose(fp); - - return true; - } - -protected: + bool process(encryption::Mode mode, const wchar_t* algorithm); + bool process_file(const wchar_t* path, encryption::Mode mode, const wchar_t* algorithm); public: - Encryption() = default; - ~Encryption(); - auto GetKeyInfo() { - return std::make_tuple(this->Key, this->KeyLength, - this->IV,this->IVLength); - } + Encryption() = default; + ~Encryption() = default; + Encryption(const Encryption&) = default; + Encryption& operator=(const Encryption&) = default; + Encryption(Encryption&&) noexcept = default; + Encryption& operator=(Encryption&&) noexcept = default; - bool OpenFile(const wchar_t* FileName); - bool SaveFile(const wchar_t* FileName, bool SaveSrc = false) const; - bool static SaveFile(const wchar_t* pFileName, PBYTE pData, DWORD sz); + auto GetKeyInfo() { + return std::make_tuple(material_.key.data(), std::uint32_t{16}, + material_.iv.data(), std::uint32_t{16}); + } - inline PBYTE GetData() const { - return this->OutputData == nullptr - ? this->InputData - : this->OutputData; - } - inline DWORD GetDataLength() const { - return this->OutputData == nullptr - ? this->InputLength - : this->OutputLength; - } + bool OpenFile(const wchar_t* file); + bool SaveFile(const wchar_t* file, bool SaveSrc = false) const; + static bool SaveFile(const wchar_t* file, std::uint8_t* buffer, std::uint32_t size); - inline PBYTE GetInputData() const { - return this->InputData; - } - inline DWORD GetInputDataLength() const { - return this->InputLength; - } + std::uint8_t* GetData() const { return output_.empty() ? GetInputData() : GetOutputData(); } + std::uint32_t GetDataLength() const { return output_.empty() ? GetInputDataLength() : GetOutputDataLength(); } - inline PBYTE GetOutputData() const { - return this->OutputData; - } - inline DWORD GetOutputDataLength() const { - return this->OutputLength; - } + std::uint8_t* GetInputData() const { return data(input_); } + std::uint32_t GetInputDataLength() const { return length(input_); } - template - void SetEncryptData(const T* pBuf, DWORD sz); + std::uint8_t* GetOutputData() const { return data(output_); } + std::uint32_t GetOutputDataLength() const { return length(output_); } - void SetEncryptStr(const std::string& Str); - void SetEncryptStr(const char* Str, DWORD StrLength); + // Reset data and cached results, retaining Key/IV and buffer settings. + void ClearEncryptData() noexcept; - void SetEncryptStr(const std::wstring& Str); - void SetEncryptStr(const wchar_t* Str, DWORD StrLength); + template void SetEncryptData(const T* buffer, std::uint32_t count) { + if (count > (std::numeric_limits::max)() / sizeof(T)) { + ClearEncryptData(); + return; + } + if (!store(input_, reinterpret_cast(buffer), std::size_t(count) * sizeof(T))) { + ClearEncryptData(); + return; + } - char* GetInputStr(); - void ReleaseInputStr(); + output_.clear(); hash_string_.clear(); + } - DWORD GetInputStrLength() const; + void SetEncryptStr(const std::string& text); + void SetEncryptStr(const char* text, std::uint32_t size); + void SetEncryptStr(const std::wstring& text); + void SetEncryptStr(const wchar_t* text, std::uint32_t size); - void ReleaseInputData(); + char* GetInputStr(); + std::uint32_t GetInputStrLength() const { return GetInputDataLength() + 1; } + void ReleaseInputStr() { input_string_.clear(); } + void ReleaseInputData() { input_.clear(); input_string_.clear(); } - char* GetOutputStr(); - void ReleaseOutputStr(); + char* GetOutputStr(); + std::uint32_t GetOutputStrLength() const { return GetOutputDataLength() + 1; } + void ReleaseOutputStr() { output_string_.clear(); } - DWORD GetOutputStrLength() const; + void GenerateKey(const wchar_t* text); + bool EncryptFile(const wchar_t* file, const wchar_t* algorithm = L"AES"); + bool DecryptFile(const wchar_t* file, const wchar_t* algorithm = L"AES"); + bool Encrypt(const wchar_t* algorithm = L"AES") { return process(encryption::Mode::encrypt, algorithm); } + bool Decrypt(const wchar_t* algorithm = L"AES") { return process(encryption::Mode::decrypt, algorithm); } - void GenerateKey(const wchar_t* KeyStr); - -#ifdef _DEBUG - inline bool CompareGeneratedKey(const wchar_t* pL, const wchar_t* pR) { - auto CopyResult = [&] (PBYTE pCache) { - memcpy(pCache, this->Key, KEY_LENGTH); - memcpy(pCache + KEY_LENGTH, this->IV, KEY_LENGTH); - }; - - // save old - const auto pOld = new BYTE[32]; - CopyResult(pOld); - - // compare result - const auto pLResult = new BYTE[32]; - GenerateKey(pL); - CopyResult(pLResult); - - const auto pRResult = new BYTE[32]; - GenerateKey(pR); - CopyResult(pRResult); - - bool bEqual = true; + void SetBufferSize(std::uint32_t size = BUFFER_SIZE) { buffer_size_ = (std::max)(std::uint32_t{1}, size); } - for (size_t idx = 0; idx < 32; idx++) { - bEqual = bEqual && (pLResult[idx] == pRResult[idx]); + bool EncryptFileDirectly(const wchar_t* file, const wchar_t* algorithm = L"AES") { + return process_file(file, encryption::Mode::encrypt, algorithm); + } - if (!bEqual) { break; } - } + bool DecryptFileDirectly(const wchar_t* file, const wchar_t* algorithm = L"AES") { + return process_file(file, encryption::Mode::decrypt, algorithm); + } - // restore old - memcpy(this->Key, pOld, KEY_LENGTH); - memcpy(this->IV, pOld + KEY_LENGTH, KEY_LENGTH); + const wchar_t* GetHash(const wchar_t* algorithm = L"MD5"); - // clean up - delete[] pOld; - delete[] pLResult; - delete[] pRResult; - - return bEqual; - } +#ifdef _DEBUG + bool CompareGeneratedKey(const wchar_t* left, const wchar_t* right) { + const auto a = encryption::keygen::from_wide(left ? left : L"", encryption::keygen::native_seed_width); + const auto b = encryption::keygen::from_wide(right ? right : L"", encryption::keygen::native_seed_width); + return a.key == b.key && a.iv == b.iv; + } #endif - - // load to memory - inline bool EncryptFile(const wchar_t* pFileName, LPCWSTR Algorithm = BCRYPT_AES_ALGORITHM) { - this->OpenFile(pFileName); - const auto ret = this->Encrypt(); - this->ReleaseInputData(); - - return ret; - } - inline bool DecryptFile(const wchar_t* pFileName, LPCWSTR Algorithm = BCRYPT_AES_ALGORITHM) { - this->OpenFile(pFileName); - const auto ret = this->Decrypt(); - this->ReleaseInputData(); - - return ret; - } - - inline bool Encrypt(const LPCWSTR Algorithm = BCRYPT_AES_ALGORITHM) { - return Encrypt_Core(BCryptEncrypt, Algorithm); - } - inline bool Decrypt(const LPCWSTR Algorithm = BCRYPT_AES_ALGORITHM) { - return Encrypt_Core(BCryptDecrypt, Algorithm); - } - -#define PROCESS_DIRECTLY - -#ifdef PROCESS_DIRECTLY -private: - inline bool ProcessFileDirectly(const wchar_t* FileName, HandlerType handler, const LPCWSTR Algorithm = BCRYPT_AES_ALGORITHM) { - bool bRet = false; - - OpenFileGeneral(FileName, [&] (FILE* fp, long fileSz) { - this->InputData = (PBYTE)fp; - - bRet = Encrypt_Core(handler, Algorithm, [&] (HandlerType handler - , const BCRYPT_KEY_HANDLE hKey - , const PBYTE pbIV, const DWORD cbBlockLen) { - const auto bufSz = cbBlockLen * this->BufferSize; - const auto pBuf = new BYTE[bufSz]; - - bool bFinish = false; - - DWORD byteWrite = 0; - DWORD totalByteWrite = 0; - - auto GetInputLeft = [&] () { - auto fPos = ftell(fp); - - return this->InputLength - ftell(fp); - }; - - do { - auto sz = (std::min)(bufSz, GetInputLeft()); - // Encrypt with padding, so the last block must be padded - auto bPadding = (GetInputLeft() <= bufSz) /*|| (sz < bufSz)*/; - - memset(pBuf, 0, bufSz); - fread(pBuf, sz, 1, fp); - - auto ret = handler( - hKey, - pBuf, sz, - nullptr, - pbIV, cbBlockLen, - this->OutputData + totalByteWrite, this->OutputLength - totalByteWrite, - &byteWrite, - bPadding ? BCRYPT_BLOCK_PADDING : 0); - - if (!NT_SUCCESS(ret)) { - break; - } - - totalByteWrite += byteWrite; - bFinish = (GetInputLeft() == 0); - - if (bFinish) { - this->OutputLength = totalByteWrite; - - break; - } - } while (true); - - delete[] pBuf; - - return bFinish ? STATUS_SUCCESS : STATUS_UNSUCCESSFUL; - }); - - this->InputData = nullptr; - this->InputLength = 0; - }); - - return bRet; - } - -public: - // actual buf size = cbBlockLen * sz - inline void SetBufferSize(const DWORD sz = BUFFER_SIZE) { - this->BufferSize = (std::max)((DWORD)1, sz); - } - - // read from disk - inline bool EncryptFileDirectly(const wchar_t* FileName, const LPCWSTR Algorithm = BCRYPT_AES_ALGORITHM) { - return ProcessFileDirectly(FileName, BCryptEncrypt, Algorithm); - } - inline bool DecryptFileDirectly(const wchar_t* FileName, const LPCWSTR Algorithm = BCRYPT_AES_ALGORITHM) { - return ProcessFileDirectly(FileName, BCryptDecrypt, Algorithm); - } -#endif - -// incompatible with windows 7 -//#define ENCRYPTER_USE_CONVENIENCE_HASH - -#ifndef ENCRYPTER_USE_CONVENIENCE_HASH - // BCRYPT_SHA256_ALGORITHM, - inline LPCWSTR GetHash(const LPCWSTR Algorithm = BCRYPT_MD5_ALGORITHM) { - //do nothing if input is invalid - if (this->InputData == nullptr) { - return nullptr; - } - - //Define Vars - BCRYPT_ALG_HANDLE hAlg = nullptr; - BCRYPT_HASH_HANDLE hHash = nullptr; - NTSTATUS status = STATUS_UNSUCCESSFUL; - - DWORD - cbData = 0, - cbHashObject = 0; - PBYTE - pbHashObject = nullptr; - - bool result = true; - - //open an algorithm handle - if (!NT_SUCCESS(status = BCryptOpenAlgorithmProvider( - &hAlg, - Algorithm, - NULL, - 0))) { - // wprintf(L"**** Error 0x%x returned by BCryptOpenAlgorithmProvider\n", status); - result = false; - goto Cleanup; - } - - //calculate the size of the buffer to hold the hash object - if (!NT_SUCCESS(status = BCryptGetProperty( - hAlg, - BCRYPT_OBJECT_LENGTH, - (PBYTE)&cbHashObject, sizeof(DWORD), - &cbData, 0))) { - // wprintf(L"**** Error 0x%x returned by BCryptGetProperty\n", status); - result = false; - goto Cleanup; - } - - //allocate the hash object on the heap - pbHashObject = (PBYTE)HeapAlloc(GetProcessHeap(), 0, cbHashObject); - if (nullptr == pbHashObject) { - // wprintf(L"**** memory allocation failed\n"); - result = false; - goto Cleanup; - } - - //calculate the length of the hash - if (!NT_SUCCESS(status = BCryptGetProperty( - hAlg, - BCRYPT_HASH_LENGTH, - (PBYTE) & (this->OutputLength), sizeof(DWORD), - &cbData, 0))) { - // wprintf(L"**** Error 0x%x returned by BCryptGetProperty\n", status); - result = false; - goto Cleanup; - } - - if (!Alloc(this->OutputData, this->OutputLength)) { - // wprintf(L"**** memory allocation failed\n"); - result = false; - goto Cleanup; - } - - //create a hash - if (!NT_SUCCESS(status = BCryptCreateHash( - hAlg, - &hHash, - pbHashObject, cbHashObject, - NULL, 0, - 0))) { - // wprintf(L"**** Error 0x%x returned by BCryptCreateHash\n", status); - result = false; - goto Cleanup; - } - - //hash some data - if (!NT_SUCCESS(status = BCryptHashData( - hHash, - this->InputData, this->InputLength, - 0))) { - // wprintf(L"**** Error 0x%x returned by BCryptHashData\n", status); - result = false; - goto Cleanup; - } - - //close the hash - if (!NT_SUCCESS(status = BCryptFinishHash( - hHash, - this->OutputData, this->OutputLength, - 0))) { - // wprintf(L"**** Error 0x%x returned by BCryptFinishHash\n", status); - result = false; - goto Cleanup; - } - - GetReadableHash(); - - Cleanup: - - if (hAlg) { - BCryptCloseAlgorithmProvider(hAlg, 0); - } - - if (hHash) { - BCryptDestroyHash(hHash); - } - - if (pbHashObject) { - HeapFree(GetProcessHeap(), 0, pbHashObject); - } - - return result ? this->HashStr : nullptr; - } -#else - inline LPCWSTR GetHash(LPCWSTR Algorithm = BCRYPT_MD5_ALGORITHM) { - //do nothing if input is invalid - if (this->InputData == nullptr) { - return nullptr; - } - - //Define Vars - BCRYPT_ALG_HANDLE hAlg = NULL; - BCRYPT_HASH_HANDLE hHash = NULL; - NTSTATUS status = STATUS_UNSUCCESSFUL; - - DWORD - cbData = 0; - - bool result = true; - - //open an algorithm handle - if (!NT_SUCCESS(status = BCryptOpenAlgorithmProvider( - &hAlg, - Algorithm, - NULL, - 0))) { - // wprintf(L"**** Error 0x%x returned by BCryptOpenAlgorithmProvider\n", status); - result = false; - goto Cleanup; - } - - if (!NT_SUCCESS(status = BCryptGetProperty( - hAlg, - BCRYPT_HASH_LENGTH, - (PBYTE) & (this->OutputLength), sizeof(DWORD), - &cbData, 0))) { - // wprintf(L"**** Error 0x%x returned by BCryptGetProperty\n", status); - result = false; - goto Cleanup; - } - - if (!Alloc(this->OutputData, this->OutputLength)) { - // wprintf(L"**** memory allocation failed\n"); - result = false; - goto Cleanup; - } - - //Output - if (!NT_SUCCESS(status = BCryptHash( - hAlg, - NULL, 0, - this->InputData, this->InputLength, - this->OutputData, this->OutputLength - ))) { - // wprintf(L"**** Error 0x%x returned by BCryptFinishHash\n", status); - result = false; - goto Cleanup; - }; - - GetReadableHash(); - - Cleanup: - - if (hAlg) { - BCryptCloseAlgorithmProvider(hAlg, 0); - } - - if (hHash) { - BCryptDestroyHash(hHash); - } - - return result ? this->HashStr : nullptr; - } -#endif }; -// hash -inline std::wstring GetFileHash(LPCWSTR filePath) { - // protection for null file - if (wcscmp(filePath, L"") == 0) { - return L""; - } - - Encryption hash; - hash.OpenFile(filePath); - - const auto ret = hash.GetHash(); - - return ret == nullptr ? L"" : ret; -} -inline std::wstring GetFileHash(const std::wstring& filePath) { - return GetFileHash(filePath.c_str()); -} -inline std::wstring GetFileHash(LPBYTE pData, DWORD StrLength) { - Encryption hash; - hash.SetEncryptStr(reinterpret_cast(pData), StrLength); - - const auto ret = hash.GetHash(); - - return ret == nullptr ? L"" : ret; +inline std::wstring GetFileHash(const wchar_t* file) { + if (!file || !*file) return L""; + Encryption hash; + if (!hash.OpenFile(file)) return L""; + const auto result = hash.GetHash(); + return result ? result : L""; } -#ifdef PROCESS_DIRECTLY -//#define BUFFER_BENCHMARK - -#ifdef BUFFER_BENCHMARK - -#include -#include - -/* -Result: - -BufferSize Input : 1 KB, Time : 694 MS -BufferSize Input : 2 KB, Time : 659 MS -BufferSize Input : 4 KB, Time : 583 MS -BufferSize Input : 8 KB, Time : 582 MS -BufferSize Input : 16 KB, Time : 533 MS -BufferSize Input : 32 KB, Time : 530 MS -BufferSize Input : 64 KB, Time : 527 MS -BufferSize Input : 128 KB, Time : 517 MS -BufferSize Input : 256 KB, Time : 510 MS -BufferSize Input : 512 KB, Time : 558 MS -BufferSize Input : 1024 KB, Time : 589 MS -BufferSize Input : 2048 KB, Time : 572 MS -BufferSize Input : 4096 KB, Time : 609 MS +inline std::wstring GetFileHash(const std::wstring& file) { return GetFileHash(file.c_str()); } -BufferSize Input : 1024 * 256 -*/ - -inline void BufferBenchMark(Encryption& Encrypt, std::wstring wFilePath, bool encrypt) { - constexpr auto KB = 1024; - using namespace std::chrono_literals; - - for (int i = 1; i <= 4096; i*=2) { - auto before = std::chrono::steady_clock::now(); - - Encrypt.SetBufferSize(KB * i); - encrypt - ? Encrypt.EncryptFileDirectly(wFilePath.c_str()) - : Encrypt.DecryptFileDirectly(wFilePath.c_str()); - - auto duration = (std::chrono::steady_clock::now() - before) / 1ms; - auto info = (std::wstring)L"BufferSize: "; - info += (std::wstring)_itos(i).c_str(); - info += (std::wstring)L" KB"; - info += (std::wstring)L", "; - info += (std::wstring)L"Time: "; - info += (std::wstring)_itos(duration).c_str(); - info += (std::wstring)L" MS"; - - OutputDebugString(info.c_str()); - OutputDebugString(L"\n"); - } +inline std::wstring GetFileHash(std::uint8_t* bytes, std::uint32_t size) { + Encryption hash; + hash.SetEncryptStr(reinterpret_cast(bytes), size); + const auto result = hash.GetHash(); + return result ? result : L""; } -#endif - -#endif - -//// compatible with WinBase -//#pragma pop_macro("EncryptFile") -//#pragma pop_macro("DecryptFile") \ No newline at end of file diff --git a/Extensions/_DeLib/Encryption.props b/Extensions/_DeLib/Encryption.props new file mode 100644 index 000000000..3c8d5e80e --- /dev/null +++ b/Extensions/_DeLib/Encryption.props @@ -0,0 +1,42 @@ + + + + <_OfeEncryptDependencies>$(MSBuildThisFileDirectory)..\_3rdLib\ + x86 + x64 + <_OfeDefaultOpenSSLRoot>$(_OfeEncryptDependencies)Encrypt-sdk\$(OfeEncryptArch) + $(_OfeDefaultOpenSSLRoot) + true + false + false + $(OfeOpenSSLRoot)\debug\lib + $(OfeOpenSSLRoot)\lib + $(VSInstallDir)Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe + cmake + $(_OfeEncryptDependencies)Encrypt-build\out + <_OfeEncryptVSInstance Condition="'$(VSInstallDir)'!=''">$(VSInstallDir). + + + + $(OfeOpenSSLRoot)\include;%(AdditionalIncludeDirectories) + + + $(OfeOpenSSLLibDir);%(AdditionalLibraryDirectories) + + libcrypto.lib;crypt32.lib;ws2_32.lib;advapi32.lib;user32.lib;bcrypt.lib;%(AdditionalDependencies) + + + + + {F408ED35-2D66-4E36-A659-580895179F86} + Configuration=Release + false + false + + + + + + + diff --git a/Extensions/_DeLib/Encryption/.gitignore b/Extensions/_DeLib/Encryption/.gitignore new file mode 100644 index 000000000..f2583ffb9 --- /dev/null +++ b/Extensions/_DeLib/Encryption/.gitignore @@ -0,0 +1,3 @@ +/build/ +/.vs/ +*.vcxproj.user diff --git a/Extensions/_DeLib/Encryption/CAPI.cpp b/Extensions/_DeLib/Encryption/CAPI.cpp new file mode 100644 index 000000000..e115d9198 --- /dev/null +++ b/Extensions/_DeLib/Encryption/CAPI.cpp @@ -0,0 +1,487 @@ +#include "CAPI.h" +#include "Core.h" +#include "KeyGen.h" + +#include +#include +#include +#include +#include + +// Shared exception/result translation is private to this implementation. + +namespace encryption::c_api { + +struct CallbackError { EncryptionResult result; }; + +inline void callback_result(EncryptionResult result) { + if (result != ENCRYPTION_OK) throw CallbackError{result}; +} + +template EncryptionResult checked(F&& call) noexcept { + try { call(); return ENCRYPTION_OK; } + catch (const CallbackError& error) { return error.result; } + catch (const std::bad_alloc&) { return ENCRYPTION_OUT_OF_MEMORY; } + catch (const std::length_error&) { return ENCRYPTION_OUT_OF_MEMORY; } + catch (const Error& error) { + switch (error.code) { + case ErrorCode::argument: return ENCRYPTION_INVALID_ARGUMENT; + case ErrorCode::state: return ENCRYPTION_INVALID_STATE; + case ErrorCode::padding: return ENCRYPTION_INVALID_PADDING; + case ErrorCode::io: return ENCRYPTION_IO_ERROR; + default: return ENCRYPTION_CRYPTO_ERROR; + } + } catch (...) { return ENCRYPTION_CRYPTO_ERROR; } +} + +inline KeyIv material(const uint8_t* key, const uint8_t* iv) { + KeyIv result{}; + std::copy_n(key, 16, result.key.begin()); + std::copy_n(iv, 16, result.iv.begin()); + return result; +} + +} // namespace encryption::c_api + +using encryption::c_api::checked; +using encryption::c_api::material; + +struct EncryptionCipherContext { + encryption::Cipher cipher; + explicit EncryptionCipherContext(const encryption::KeyIv& material, encryption::Mode mode) + : cipher(material, mode) {} +}; + +struct EncryptionHashContext { + encryption::Hash hash; + explicit EncryptionHashContext(encryption::Digest algorithm) : hash(algorithm) {} +}; + +struct EncryptionReaderContext { + encryption::DecryptReader reader; + explicit EncryptionReaderContext(encryption::DecryptReader value) : reader(std::move(value)) {} +}; + +class CallbackSource final : public encryption::RandomAccessSource { + uint64_t size_; + EncryptionReadAtCallback callback_; + void* user_; + +public: + + CallbackSource(uint64_t size, EncryptionReadAtCallback callback, void* user) + : size_(size), callback_(callback), user_(user) {} + uint64_t size() const override { return size_; } + + size_t read_at(uint64_t offset, uint8_t* output, size_t capacity) override { + size_t count = 0; + const auto result = callback_(user_, offset, output, capacity, &count); + encryption::require(result == ENCRYPTION_OK && count <= capacity, + encryption::ErrorCode::io, "Read ciphertext callback"); + return count; + } +}; + +// Callback streams +namespace { + +EncryptionResult cipher_reader(encryption::Mode mode, + const uint8_t* key, const uint8_t* iv, + EncryptionReadCallback read, void* read_user, + EncryptionWriteCallback write, void* write_user) { + if (!key || !iv || !read || !write) return ENCRYPTION_INVALID_ARGUMENT; + + return encryption::c_api::checked([&] { + encryption::transform(encryption::c_api::material(key, iv), mode, + [&](uint8_t* data, size_t capacity) { + size_t count = 0; + encryption::c_api::callback_result(read(read_user, data, capacity, &count)); + return count; + }, + [&](const uint8_t* data, size_t size) { + encryption::c_api::callback_result(write(write_user, data, size)); + }); + }); +} +} + +// Memory buffers +namespace { + +struct BufferSink { + uint8_t* output; + size_t capacity; + size_t size; +}; + +EncryptionResult append_to_buffer(void* user_data, const uint8_t* data, + size_t data_length) { + auto* sink = static_cast(user_data); + if (data_length > sink->capacity - sink->size) { + return ENCRYPTION_BUFFER_TOO_SMALL; + } + if (data_length != 0) { + std::memcpy(sink->output + sink->size, data, data_length); + sink->size += data_length; + } + return ENCRYPTION_OK; +} + +EncryptionResult transform_buffer(encryption::Mode mode, const uint8_t* key, const uint8_t* iv, + const uint8_t* input, size_t size, uint8_t* output, size_t capacity, size_t* written) { + if (written) *written = 0; + if (!key || !iv || !written || (!input && size)) return ENCRYPTION_INVALID_ARGUMENT; + + return encryption::c_api::checked([&] { + const auto result = encryption::transform_buffer(encryption::c_api::material(key, iv), mode, input, size); + if (!output || capacity < result.size()) + throw encryption::c_api::CallbackError{ENCRYPTION_BUFFER_TOO_SMALL}; + if (!result.empty()) std::memcpy(output, result.data(), result.size()); + *written = result.size(); + }); +} + +} // namespace + +// FILE handles +namespace { + +struct FileReader { FILE* file; }; + +struct FileWriter { FILE* file; }; + +EncryptionResult read_file(void* user_data, uint8_t* buffer, + size_t capacity, size_t* bytes_read) { + if (user_data == nullptr || buffer == nullptr || bytes_read == nullptr) { + return ENCRYPTION_INVALID_ARGUMENT; + } + auto* reader = static_cast(user_data); + *bytes_read = std::fread(buffer, 1, capacity, reader->file); + return std::ferror(reader->file) ? ENCRYPTION_IO_ERROR : ENCRYPTION_OK; +} + +EncryptionResult write_file(void* user_data, const uint8_t* data, + size_t data_length) { + if (user_data == nullptr || (data == nullptr && data_length != 0)) { + return ENCRYPTION_INVALID_ARGUMENT; + } + auto* writer = static_cast(user_data); + return std::fwrite(data, 1, data_length, writer->file) == data_length + ? ENCRYPTION_OK : ENCRYPTION_IO_ERROR; +} + +} // namespace + +extern "C" { + +EncryptionResult Encryption_DeriveKeyAndIv(const void* input, size_t size, + uint8_t key[16], uint8_t iv[16]) { + return Encryption_DeriveKeyAndIvWithSeedWidth(input, size, 32, key, iv); +} + +EncryptionResult Encryption_DeriveKeyAndIvWithSeedWidth(const void* input, size_t size, + unsigned word_bits, uint8_t key[16], uint8_t iv[16]) { + if ((!input && size) || !key || !iv || (word_bits != 32 && word_bits != 64)) + return ENCRYPTION_INVALID_ARGUMENT; + + return checked([&] { + const auto* bytes = static_cast(input); + std::u16string text; + text.reserve(size / 2 + size % 2); + for (size_t i = 0; i < size;) { + uint16_t unit = bytes[i++]; + if (i < size) unit |= uint16_t(bytes[i++]) << 8; + text.push_back(static_cast(unit)); + } + const auto result = encryption::keygen::from_utf16(text, word_bits == 32 + ? encryption::keygen::SeedWidth::bits32 : encryption::keygen::SeedWidth::bits64); + std::copy(result.key.begin(), result.key.end(), key); + std::copy(result.iv.begin(), result.iv.end(), iv); + }); +} + +EncryptionResult Encryption_CipherCreate(EncryptionCipherMode mode, const uint8_t key[16], + const uint8_t iv[16], EncryptionCipherContext** context) { + if (context) *context = nullptr; + if (!key || !iv || !context || (mode != ENCRYPTION_ENCRYPT && mode != ENCRYPTION_DECRYPT)) + return ENCRYPTION_INVALID_ARGUMENT; + + return checked([&] { *context = new EncryptionCipherContext(material(key, iv), + mode == ENCRYPTION_ENCRYPT ? encryption::Mode::encrypt : encryption::Mode::decrypt); }); +} + +EncryptionResult Encryption_CipherUpdate(EncryptionCipherContext* context, + const uint8_t* input, size_t length, uint8_t* output, size_t capacity, size_t* written) { + if (written) *written = 0; + if (!context || !written || (!input && length) || (!output && capacity)) + return ENCRYPTION_INVALID_ARGUMENT; + if (length > (std::numeric_limits::max)() - 16) return ENCRYPTION_INVALID_ARGUMENT; + if (length && (!output || capacity < length + 16)) return ENCRYPTION_BUFFER_TOO_SMALL; + + return checked([&] { + const auto result = context->cipher.update(input, length); + if (!result.empty()) std::memcpy(output, result.data(), result.size()); + *written = result.size(); + }); +} + +EncryptionResult Encryption_CipherFinal(EncryptionCipherContext* context, + uint8_t* output, size_t capacity, size_t* written) { + if (written) *written = 0; + if (!context || !output || !written) return ENCRYPTION_INVALID_ARGUMENT; + if (capacity < 16) return ENCRYPTION_BUFFER_TOO_SMALL; + + return checked([&] { + const auto result = context->cipher.finish(); + if (!result.empty()) std::memcpy(output, result.data(), result.size()); + *written = result.size(); + }); +} + +void Encryption_CipherDestroy(EncryptionCipherContext* context) { delete context; } + +EncryptionResult Encryption_HashCreate(EncryptionHashAlgorithm algorithm, EncryptionHashContext** context) { + if (context) *context = nullptr; + if (!context || (algorithm != ENCRYPTION_HASH_MD5 && algorithm != ENCRYPTION_HASH_SHA256)) + return ENCRYPTION_INVALID_ARGUMENT; + + return checked([&] { *context = new EncryptionHashContext(algorithm == ENCRYPTION_HASH_MD5 + ? encryption::Digest::md5 : encryption::Digest::sha256); }); +} + +EncryptionResult Encryption_HashUpdate(EncryptionHashContext* context, const uint8_t* input, size_t length) { + if (!context || (!input && length)) return ENCRYPTION_INVALID_ARGUMENT; + + return checked([&] { context->hash.update(input, length); }); +} + +EncryptionResult Encryption_HashFinal(EncryptionHashContext* context, + uint8_t* output, size_t capacity, size_t* written) { + if (written) *written = 0; + if (!context || !output || !written) return ENCRYPTION_INVALID_ARGUMENT; + if (capacity < context->hash.size()) return ENCRYPTION_BUFFER_TOO_SMALL; + + return checked([&] { + const auto result = context->hash.finish(); + std::memcpy(output, result.data(), result.size()); + *written = result.size(); + }); +} + +void Encryption_HashDestroy(EncryptionHashContext* context) { delete context; } + +EncryptionResult Encryption_ReaderCreate(uint64_t size, EncryptionReadAtCallback read_at, + void* user, const uint8_t key[16], const uint8_t iv[16], EncryptionReaderContext** context) { + if (context) *context = nullptr; + if (!read_at || !key || !iv || !context) return ENCRYPTION_INVALID_ARGUMENT; + + return checked([&] { + *context = new EncryptionReaderContext(encryption::DecryptReader( + std::make_shared(size, read_at, user), material(key, iv))); + }); +} + +EncryptionResult Encryption_ReaderRead(EncryptionReaderContext* context, + uint8_t* output, size_t capacity, size_t* count) { + if (count) *count = 0; + if (!context || !count || (!output && capacity)) return ENCRYPTION_INVALID_ARGUMENT; + + return checked([&] { *count = context->reader.read(output, capacity); }); +} + +EncryptionResult Encryption_ReaderSeek(EncryptionReaderContext* context, uint64_t offset) { + if (!context) return ENCRYPTION_INVALID_ARGUMENT; + + return checked([&] { context->reader.seek(offset); }); +} + +EncryptionResult Encryption_ReaderSize(const EncryptionReaderContext* context, uint64_t* size) { + if (!context || !size) return ENCRYPTION_INVALID_ARGUMENT; + *size = context->reader.size(); + return ENCRYPTION_OK; +} + +void Encryption_ReaderDestroy(EncryptionReaderContext* context) { delete context; } + +// ------------------------------------------ +// Callback streams +// ------------------------------------------ + +EncryptionResult Encryption_EncryptReader(const uint8_t key[16], const uint8_t iv[16], + EncryptionReadCallback read, void* read_user, EncryptionWriteCallback write, void* write_user) { + return cipher_reader(encryption::Mode::encrypt, key, iv, read, read_user, write, write_user); +} + +EncryptionResult Encryption_DecryptReader(const uint8_t key[16], const uint8_t iv[16], + EncryptionReadCallback read, void* read_user, EncryptionWriteCallback write, void* write_user) { + return cipher_reader(encryption::Mode::decrypt, key, iv, read, read_user, write, write_user); +} + +// ------------------------------------------ +// Memory buffers +// ------------------------------------------ + +EncryptionResult Encryption_EncryptReaderToBuffer( + const uint8_t key[ENCRYPTION_AES_KEY_SIZE], + const uint8_t iv[ENCRYPTION_AES_IV_SIZE], + EncryptionReadCallback read_callback, void* read_user_data, + uint8_t* output, size_t output_capacity, size_t* output_length) { + if (output_length != nullptr) *output_length = 0; + if (output == nullptr || output_length == nullptr) return ENCRYPTION_INVALID_ARGUMENT; + BufferSink sink{output, output_capacity, 0}; + const auto result = Encryption_EncryptReader( + key, iv, read_callback, read_user_data, append_to_buffer, &sink); + if (result == ENCRYPTION_OK) *output_length = sink.size; + return result; +} + +EncryptionResult Encryption_DecryptReaderToBuffer( + const uint8_t key[ENCRYPTION_AES_KEY_SIZE], + const uint8_t iv[ENCRYPTION_AES_IV_SIZE], + EncryptionReadCallback read_callback, void* read_user_data, + uint8_t* output, size_t output_capacity, size_t* output_length) { + if (output_length != nullptr) *output_length = 0; + if (output == nullptr || output_length == nullptr) return ENCRYPTION_INVALID_ARGUMENT; + BufferSink sink{output, output_capacity, 0}; + const auto result = Encryption_DecryptReader( + key, iv, read_callback, read_user_data, append_to_buffer, &sink); + if (result == ENCRYPTION_OK) *output_length = sink.size; + return result; +} + +EncryptionResult Encryption_EncryptBuffer( + const uint8_t key[16], const uint8_t iv[16], const uint8_t* input, size_t size, + uint8_t* output, size_t capacity, size_t* written) { + return transform_buffer(encryption::Mode::encrypt, key, iv, input, size, output, capacity, written); +} + +EncryptionResult Encryption_DecryptBuffer( + const uint8_t key[16], const uint8_t iv[16], const uint8_t* input, size_t size, + uint8_t* output, size_t capacity, size_t* written) { + return transform_buffer(encryption::Mode::decrypt, key, iv, input, size, output, capacity, written); +} + +EncryptionResult Encryption_HashBuffer( + EncryptionHashAlgorithm algorithm, + const uint8_t* input, size_t input_length, + uint8_t* output, size_t output_capacity, size_t* output_length) { + if (output_length != nullptr) *output_length = 0; + if (output_length == nullptr || (input == nullptr && input_length != 0) || + output == nullptr) return ENCRYPTION_INVALID_ARGUMENT; + + EncryptionHashContext* context = nullptr; + auto result = Encryption_HashCreate(algorithm, &context); + if (result == ENCRYPTION_OK) result = Encryption_HashUpdate(context, input, input_length); + if (result == ENCRYPTION_OK) result = Encryption_HashFinal(context, output, output_capacity, output_length); + Encryption_HashDestroy(context); + return result; +} + +// ------------------------------------------ +// FILE handles +// ------------------------------------------ + +EncryptionResult Encryption_EncryptFileStream( + FILE* input_file, const uint8_t key[ENCRYPTION_AES_KEY_SIZE], + const uint8_t iv[ENCRYPTION_AES_IV_SIZE], + EncryptionWriteCallback write_callback, void* write_user_data) { + if (input_file == nullptr) return ENCRYPTION_INVALID_ARGUMENT; + FileReader reader{input_file}; + return Encryption_EncryptReader(key, iv, read_file, &reader, + write_callback, write_user_data); +} + +EncryptionResult Encryption_DecryptFileStream( + FILE* input_file, const uint8_t key[ENCRYPTION_AES_KEY_SIZE], + const uint8_t iv[ENCRYPTION_AES_IV_SIZE], + EncryptionWriteCallback write_callback, void* write_user_data) { + if (input_file == nullptr) return ENCRYPTION_INVALID_ARGUMENT; + FileReader reader{input_file}; + return Encryption_DecryptReader(key, iv, read_file, &reader, + write_callback, write_user_data); +} + +EncryptionResult Encryption_EncryptFile( + FILE* input_file, FILE* output_file, + const uint8_t key[ENCRYPTION_AES_KEY_SIZE], + const uint8_t iv[ENCRYPTION_AES_IV_SIZE]) { + if (input_file == nullptr || output_file == nullptr || input_file == output_file) { + return ENCRYPTION_INVALID_ARGUMENT; + } + FileReader reader{input_file}; + FileWriter writer{output_file}; + return Encryption_EncryptReader(key, iv, read_file, &reader, + write_file, &writer); +} + +EncryptionResult Encryption_DecryptFile( + FILE* input_file, FILE* output_file, + const uint8_t key[ENCRYPTION_AES_KEY_SIZE], + const uint8_t iv[ENCRYPTION_AES_IV_SIZE]) { + if (input_file == nullptr || output_file == nullptr || input_file == output_file) { + return ENCRYPTION_INVALID_ARGUMENT; + } + FileReader reader{input_file}; + FileWriter writer{output_file}; + return Encryption_DecryptReader(key, iv, read_file, &reader, + write_file, &writer); +} + +EncryptionResult Encryption_EncryptFileToBuffer( + FILE* input_file, const uint8_t key[ENCRYPTION_AES_KEY_SIZE], + const uint8_t iv[ENCRYPTION_AES_IV_SIZE], uint8_t* output, + size_t output_capacity, size_t* output_length) { + if (input_file == nullptr) { + if (output_length != nullptr) *output_length = 0; + return ENCRYPTION_INVALID_ARGUMENT; + } + FileReader reader{input_file}; + return Encryption_EncryptReaderToBuffer(key, iv, read_file, &reader, + output, output_capacity, output_length); +} + +EncryptionResult Encryption_DecryptFileToBuffer( + FILE* input_file, const uint8_t key[ENCRYPTION_AES_KEY_SIZE], + const uint8_t iv[ENCRYPTION_AES_IV_SIZE], uint8_t* output, + size_t output_capacity, size_t* output_length) { + if (input_file == nullptr) { + if (output_length != nullptr) *output_length = 0; + return ENCRYPTION_INVALID_ARGUMENT; + } + FileReader reader{input_file}; + return Encryption_DecryptReaderToBuffer(key, iv, read_file, &reader, + output, output_capacity, output_length); +} + +EncryptionResult Encryption_HashFile( + FILE* input_file, EncryptionHashAlgorithm algorithm, + uint8_t* output, size_t output_capacity, size_t* output_length) { + if (output_length != nullptr) *output_length = 0; + if (input_file == nullptr || output == nullptr || output_length == nullptr) { + return ENCRYPTION_INVALID_ARGUMENT; + } + constexpr size_t capacity = 64 * 1024; + std::unique_ptr buffer(new (std::nothrow) uint8_t[capacity]); + if (!buffer) return ENCRYPTION_OUT_OF_MEMORY; + + EncryptionHashContext* context = nullptr; + auto result = Encryption_HashCreate(algorithm, &context); + if (result != ENCRYPTION_OK) return result; + + FileReader reader{input_file}; + for (;;) { + size_t bytes_read = 0; + result = read_file(&reader, buffer.get(), capacity, &bytes_read); + if (result != ENCRYPTION_OK || bytes_read == 0) break; + result = Encryption_HashUpdate(context, buffer.get(), bytes_read); + if (result != ENCRYPTION_OK) break; + } + if (result == ENCRYPTION_OK) { + result = Encryption_HashFinal(context, output, output_capacity, output_length); + } + + Encryption_HashDestroy(context); + return result; +} + +} // extern "C" diff --git a/Extensions/_DeLib/Encryption/CAPI.h b/Extensions/_DeLib/Encryption/CAPI.h new file mode 100644 index 000000000..7e5a364b3 --- /dev/null +++ b/Extensions/_DeLib/Encryption/CAPI.h @@ -0,0 +1,224 @@ +#ifndef ENCRYPTION_CAPI_H +#define ENCRYPTION_CAPI_H + +#include +#include +#include + +#if defined(_WIN32) && defined(ENCRYPTION_SHARED) +# ifdef ENCRYPTION_BUILD +# define ENCRYPTION_API __declspec(dllexport) +# else +# define ENCRYPTION_API __declspec(dllimport) +# endif +#else +# define ENCRYPTION_API +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +#define ENCRYPTION_AES_KEY_SIZE 16 +#define ENCRYPTION_AES_IV_SIZE 16 +#define ENCRYPTION_BLOCK_SIZE 16 +#define ENCRYPTION_MD5_SIZE 16 +#define ENCRYPTION_SHA256_SIZE 32 +#define ENCRYPTION_KEY_IV_WORD_COUNT 16 + +typedef enum EncryptionResult { + ENCRYPTION_OK = 0, + ENCRYPTION_INVALID_ARGUMENT, + ENCRYPTION_OUT_OF_MEMORY, + ENCRYPTION_BUFFER_TOO_SMALL, + ENCRYPTION_CRYPTO_ERROR, + ENCRYPTION_INVALID_PADDING, + ENCRYPTION_INVALID_STATE, + ENCRYPTION_IO_ERROR +} EncryptionResult; + +typedef enum EncryptionCipherMode { + ENCRYPTION_DECRYPT = 0, + ENCRYPTION_ENCRYPT = 1 +} EncryptionCipherMode; + +typedef enum EncryptionHashAlgorithm { + ENCRYPTION_HASH_MD5 = 0, + ENCRYPTION_HASH_SHA256 = 1 +} EncryptionHashAlgorithm; + +typedef struct EncryptionCipherContext EncryptionCipherContext; +typedef struct EncryptionHashContext EncryptionHashContext; +typedef struct EncryptionReaderContext EncryptionReaderContext; + +typedef EncryptionResult (*EncryptionReadCallback)( + void* user_data, uint8_t* buffer, size_t capacity, size_t* bytes_read); +typedef EncryptionResult (*EncryptionWriteCallback)( + void* user_data, const uint8_t* data, size_t data_length); + +// Random access to caller-owned ciphertext. Callback and user_data must outlive +// the reader. A successful short read is allowed; zero before EOF is an I/O error. +typedef EncryptionResult (*EncryptionReadAtCallback)(void* user_data, + uint64_t offset, uint8_t* output, size_t capacity, size_t* bytes_read); +ENCRYPTION_API EncryptionResult Encryption_ReaderCreate(uint64_t encrypted_size, + EncryptionReadAtCallback read_at, void* user_data, + const uint8_t key[16], const uint8_t iv[16], EncryptionReaderContext** context); +ENCRYPTION_API EncryptionResult Encryption_ReaderRead(EncryptionReaderContext* context, + uint8_t* output, size_t capacity, size_t* bytes_read); +ENCRYPTION_API EncryptionResult Encryption_ReaderSeek(EncryptionReaderContext* context, uint64_t offset); +ENCRYPTION_API EncryptionResult Encryption_ReaderSize(const EncryptionReaderContext* context, uint64_t* size); +ENCRYPTION_API void Encryption_ReaderDestroy(EncryptionReaderContext* context); + +// Arbitrary-length password-to-Key/IV mapping from UTF-16LE bytes (32-bit seed). +// Odd trailing bytes have a zero high byte. Empty input selects legacy defaults. +// Feed the result to CipherCreate; equivalent text must use the same UTF-16 units. +ENCRYPTION_API EncryptionResult Encryption_DeriveKeyAndIv( + const void* input, + size_t input_size, + uint8_t key[ENCRYPTION_AES_KEY_SIZE], + uint8_t iv[ENCRYPTION_AES_IV_SIZE]); + +ENCRYPTION_API EncryptionResult Encryption_DeriveKeyAndIvWithSeedWidth( + const void* input, size_t input_size, unsigned word_bits, + uint8_t key[ENCRYPTION_AES_KEY_SIZE], uint8_t iv[ENCRYPTION_AES_IV_SIZE]); + +// Incremental AES-128-CBC with PKCS#7 padding. Create one context per +// operation. Each Update output buffer must have input_length + 16 bytes +// capacity. Call Final exactly once at end-of-input; it may emit up to 16 +// bytes. Update/Final report actual output length through output_length. +ENCRYPTION_API EncryptionResult Encryption_CipherCreate( + EncryptionCipherMode mode, + const uint8_t key[ENCRYPTION_AES_KEY_SIZE], + const uint8_t iv[ENCRYPTION_AES_IV_SIZE], + EncryptionCipherContext** context); +ENCRYPTION_API EncryptionResult Encryption_CipherUpdate( + EncryptionCipherContext* context, + const uint8_t* input, + size_t input_length, + uint8_t* output, + size_t output_capacity, + size_t* output_length); +ENCRYPTION_API EncryptionResult Encryption_CipherFinal( + EncryptionCipherContext* context, + uint8_t* output, + size_t output_capacity, + size_t* output_length); +ENCRYPTION_API void Encryption_CipherDestroy(EncryptionCipherContext* context); + +// Incremental hashes. MD5 is available to match the legacy GetHash default; +// SHA-256 is available for new callers. +ENCRYPTION_API EncryptionResult Encryption_HashCreate( + EncryptionHashAlgorithm algorithm, + EncryptionHashContext** context); +ENCRYPTION_API EncryptionResult Encryption_HashUpdate( + EncryptionHashContext* context, + const uint8_t* input, + size_t input_length); +ENCRYPTION_API EncryptionResult Encryption_HashFinal( + EncryptionHashContext* context, + uint8_t* output, + size_t output_capacity, + size_t* output_length); +ENCRYPTION_API void Encryption_HashDestroy(EncryptionHashContext* context); + +// ------------------------------------------ +// Callback streams +// ------------------------------------------ + +// Push-style streaming: reads until EOF and sends each result chunk to the +// writer callback. bytes_read == 0 with ENCRYPTION_OK signals EOF. +ENCRYPTION_API EncryptionResult Encryption_EncryptReader( + const uint8_t key[ENCRYPTION_AES_KEY_SIZE], + const uint8_t iv[ENCRYPTION_AES_IV_SIZE], + EncryptionReadCallback read_callback, + void* read_user_data, + EncryptionWriteCallback write_callback, + void* write_user_data); +ENCRYPTION_API EncryptionResult Encryption_DecryptReader( + const uint8_t key[ENCRYPTION_AES_KEY_SIZE], + const uint8_t iv[ENCRYPTION_AES_IV_SIZE], + EncryptionReadCallback read_callback, + void* read_user_data, + EncryptionWriteCallback write_callback, + void* write_user_data); + +// ------------------------------------------ +// Memory buffers +// ------------------------------------------ + +// One-shot helpers for data already in memory. Encryption needs input_length +// + 16 output bytes; decryption needs at most ciphertext length bytes. +ENCRYPTION_API EncryptionResult Encryption_EncryptBuffer( + const uint8_t key[ENCRYPTION_AES_KEY_SIZE], + const uint8_t iv[ENCRYPTION_AES_IV_SIZE], + const uint8_t* input, size_t input_length, + uint8_t* output, size_t output_capacity, size_t* output_length); +ENCRYPTION_API EncryptionResult Encryption_DecryptBuffer( + const uint8_t key[ENCRYPTION_AES_KEY_SIZE], + const uint8_t iv[ENCRYPTION_AES_IV_SIZE], + const uint8_t* input, size_t input_length, + uint8_t* output, size_t output_capacity, size_t* output_length); +ENCRYPTION_API EncryptionResult Encryption_HashBuffer( + EncryptionHashAlgorithm algorithm, + const uint8_t* input, size_t input_length, + uint8_t* output, size_t output_capacity, size_t* output_length); + +// Reads through a callback and collects the result in caller-owned memory. +// Discard output if an error is returned. +ENCRYPTION_API EncryptionResult Encryption_EncryptReaderToBuffer( + const uint8_t key[ENCRYPTION_AES_KEY_SIZE], + const uint8_t iv[ENCRYPTION_AES_IV_SIZE], + EncryptionReadCallback read_callback, void* read_user_data, + uint8_t* output, size_t output_capacity, size_t* output_length); +ENCRYPTION_API EncryptionResult Encryption_DecryptReaderToBuffer( + const uint8_t key[ENCRYPTION_AES_KEY_SIZE], + const uint8_t iv[ENCRYPTION_AES_IV_SIZE], + EncryptionReadCallback read_callback, void* read_user_data, + uint8_t* output, size_t output_capacity, size_t* output_length); + +// ------------------------------------------ +// FILE handles +// ------------------------------------------ + +// FILE helpers use caller-owned handles from their current positions. +// They neither rewind nor close handles. Source and destination must be +// distinct files; discard partial output if an operation fails. +ENCRYPTION_API EncryptionResult Encryption_EncryptFileStream( + FILE* input_file, + const uint8_t key[ENCRYPTION_AES_KEY_SIZE], + const uint8_t iv[ENCRYPTION_AES_IV_SIZE], + EncryptionWriteCallback write_callback, + void* write_user_data); +ENCRYPTION_API EncryptionResult Encryption_DecryptFileStream( + FILE* input_file, + const uint8_t key[ENCRYPTION_AES_KEY_SIZE], + const uint8_t iv[ENCRYPTION_AES_IV_SIZE], + EncryptionWriteCallback write_callback, + void* write_user_data); +ENCRYPTION_API EncryptionResult Encryption_EncryptFile( + FILE* input_file, FILE* output_file, + const uint8_t key[ENCRYPTION_AES_KEY_SIZE], + const uint8_t iv[ENCRYPTION_AES_IV_SIZE]); +ENCRYPTION_API EncryptionResult Encryption_DecryptFile( + FILE* input_file, FILE* output_file, + const uint8_t key[ENCRYPTION_AES_KEY_SIZE], + const uint8_t iv[ENCRYPTION_AES_IV_SIZE]); +ENCRYPTION_API EncryptionResult Encryption_EncryptFileToBuffer( + FILE* input_file, + const uint8_t key[ENCRYPTION_AES_KEY_SIZE], + const uint8_t iv[ENCRYPTION_AES_IV_SIZE], + uint8_t* output, size_t output_capacity, size_t* output_length); +ENCRYPTION_API EncryptionResult Encryption_DecryptFileToBuffer( + FILE* input_file, + const uint8_t key[ENCRYPTION_AES_KEY_SIZE], + const uint8_t iv[ENCRYPTION_AES_IV_SIZE], + uint8_t* output, size_t output_capacity, size_t* output_length); +ENCRYPTION_API EncryptionResult Encryption_HashFile( + FILE* input_file, EncryptionHashAlgorithm algorithm, + uint8_t* output, size_t output_capacity, size_t* output_length); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // ENCRYPTION_CAPI_H diff --git a/Extensions/_DeLib/Encryption/CMakeLists.txt b/Extensions/_DeLib/Encryption/CMakeLists.txt new file mode 100644 index 000000000..bbab66dfe --- /dev/null +++ b/Extensions/_DeLib/Encryption/CMakeLists.txt @@ -0,0 +1,41 @@ +cmake_minimum_required(VERSION 3.16) +project(Encryption LANGUAGES C CXX) +find_package(OpenSSL 1.1.1 REQUIRED COMPONENTS Crypto) +add_library(encryption CAPI.cpp ../Encryption.cpp) +target_include_directories(encryption PUBLIC + $ + $) +target_compile_features(encryption PUBLIC cxx_std_17) +target_link_libraries(encryption PUBLIC OpenSSL::Crypto) +target_compile_definitions(encryption PRIVATE ENCRYPTION_BUILD) +if(BUILD_SHARED_LIBS) + target_compile_definitions(encryption PUBLIC ENCRYPTION_SHARED) +endif() +if(NOT WIN32) + target_compile_definitions(encryption PUBLIC _FILE_OFFSET_BITS=64) +endif() +if(MSVC) + target_compile_options(encryption PRIVATE /W4) +else() + target_compile_options(encryption PRIVATE -Wall -Wextra -Wpedantic) +endif() +option(ENCRYPTION_BUILD_TESTS "Build native regression tests (disable when cross-compiling)" OFF) +if(ENCRYPTION_BUILD_TESTS) + enable_testing() + add_executable(encryption_tests tests/core.cpp tests/c_api.c tests/windows_headers.cpp tests/asset.cpp) + target_link_libraries(encryption_tests PRIVATE encryption) + add_test(NAME encryption_tests COMMAND encryption_tests + "${CMAKE_CURRENT_SOURCE_DIR}/../../NeoPic/ToInstall/Files/Examples/NeoPic/Test_Encrypt.dat" + "${CMAKE_CURRENT_SOURCE_DIR}/../../NeoPic/ToInstall/Files/Examples/NeoPic/Test_Origin.jpg") + +endif() + +include(GNUInstallDirs) +install(TARGETS encryption + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) +install(FILES ../Encryption.h ../EncryptionStream.h ../UnicodeConversion.h + DESTINATION include/encryption) +install(FILES Core.h KeyGen.h CAPI.h + DESTINATION include/encryption/Encryption) diff --git a/Extensions/_DeLib/Encryption/Core.h b/Extensions/_DeLib/Encryption/Core.h new file mode 100644 index 000000000..55ab5e9e3 --- /dev/null +++ b/Extensions/_DeLib/Encryption/Core.h @@ -0,0 +1,404 @@ +#pragma once + +#ifdef _MSC_VER +#include +#pragma warning(disable : 4996) +#endif + +// Shared C++17 implementation. Every platform, including Windows, uses OpenSSL. +// Legacy Fusion projects import Encryption.props and keep compiling Encryption.cpp. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace encryption { + +using Byte = std::uint8_t; +using Bytes = std::vector; +using Block = std::array; + +struct KeyIv { Block key; Block iv; }; + +enum class Mode { decrypt, encrypt }; +enum class Digest { md5, sha1, sha256, sha384, sha512 }; +enum class ErrorCode { argument, state, crypto, padding, io }; + +class Error : public std::runtime_error { +public: + + ErrorCode code; + + Error(ErrorCode c, const char* message) : std::runtime_error(message), code(c) {} +}; + +#ifdef _MSC_VER +// A normal return establishes the checked condition for callers of this guard. +_Post_satisfies_(ok) +#endif +inline void require(bool ok, ErrorCode code, const char* message) { + if (!ok) throw Error(code, message); +} + +namespace detail { + +// Padding, buffering, file I/O and seeking are implemented once above EVP. +class CbcBlocks { + struct Delete { void operator()(EVP_CIPHER_CTX* p) const { EVP_CIPHER_CTX_free(p); } }; + std::unique_ptr context_{EVP_CIPHER_CTX_new()}; + +public: + + CbcBlocks(const KeyIv& material, Mode mode) { + require(static_cast(context_), ErrorCode::crypto, "Allocate AES context"); + require(EVP_CipherInit_ex(context_.get(), EVP_aes_128_cbc(), nullptr, + material.key.data(), material.iv.data(), mode == Mode::encrypt) == 1 && + EVP_CIPHER_CTX_set_padding(context_.get(), 0) == 1, + ErrorCode::crypto, "Initialize AES"); + } + + CbcBlocks(const CbcBlocks&) = delete; + CbcBlocks& operator=(const CbcBlocks&) = delete; + + void process(const Byte* input, std::size_t length, Byte* output) { + require(length != 0 && length % 16 == 0 && length <= 65536, + ErrorCode::argument, "Invalid AES block batch"); + int written = 0; + require(EVP_CipherUpdate(context_.get(), output, &written, input, + static_cast(length)) == 1 && static_cast(written) == length, + ErrorCode::crypto, "AES block operation"); + } +}; + +} // namespace detail + +// Move-only owning cipher. Update accepts arbitrary chunk boundaries; finish +// adds/validates PKCS#7 exactly once. A failed operation poisons the context. +class Cipher { + std::unique_ptr backend_; + Mode mode_; + Bytes pending_; + bool done_ = false; + +public: + + Cipher(const KeyIv& material, Mode mode) + : backend_(std::make_unique(material, mode)), mode_(mode) {} + + Cipher(Cipher&&) noexcept = default; + Cipher& operator=(Cipher&&) noexcept = default; + Cipher(const Cipher&) = delete; + Cipher& operator=(const Cipher&) = delete; + + Bytes update(const Byte* data, std::size_t size) { + require(!done_ && backend_, ErrorCode::state, "Cipher is finished or moved"); + require(data != nullptr || size == 0, ErrorCode::argument, "Null cipher input"); + require(size <= (std::numeric_limits::max)() - 16, + ErrorCode::argument, "Cipher input too large"); + + Bytes output; + try { + output.reserve(size + 16); + while (size != 0) { + const auto count = (std::min)(size, std::size_t{65536} - pending_.size()); + pending_.insert(pending_.end(), data, data + count); + data += count; size -= count; + + const auto blocks = mode_ == Mode::decrypt + ? (pending_.size() - 1) / 16 : pending_.size() / 16; + const auto length = blocks * 16; + if (length) { + const auto offset = output.size(); + output.resize(offset + length); + backend_->process(pending_.data(), length, output.data() + offset); + pending_.erase(pending_.begin(), pending_.begin() + length); + } + } + } catch (...) { done_ = true; throw; } + + return output; + } + + Bytes finish() { + require(!done_ && backend_, ErrorCode::state, "Cipher is finished or moved"); + done_ = true; + if (mode_ == Mode::encrypt) { + const auto padding = static_cast(16 - pending_.size()); + pending_.resize(16, padding); + } else { + require(pending_.size() == 16, ErrorCode::padding, "Truncated ciphertext"); + } + + Bytes output(16); + backend_->process(pending_.data(), 16, output.data()); + if (mode_ == Mode::decrypt) { + const auto padding = output.back(); + require(padding >= 1 && padding <= 16, ErrorCode::padding, "Invalid PKCS#7 padding"); + unsigned mismatch = 0; + for (std::size_t i = 16 - padding; i < 16; ++i) mismatch |= output[i] ^ padding; + require(mismatch == 0, ErrorCode::padding, "Invalid PKCS#7 padding"); + output.resize(16 - padding); + } + + pending_.clear(); + + return output; + } +}; + +class Hash { + struct Delete { void operator()(EVP_MD_CTX* p) const { EVP_MD_CTX_free(p); } }; + std::unique_ptr context_{EVP_MD_CTX_new()}; + std::size_t size_ = 0; + bool done_ = false; + +public: + + explicit Hash(Digest digest) { + const EVP_MD* md = nullptr; + switch (digest) { + case Digest::md5: md = EVP_md5(); break; + case Digest::sha1: md = EVP_sha1(); break; + case Digest::sha256: md = EVP_sha256(); break; + case Digest::sha384: md = EVP_sha384(); break; + case Digest::sha512: md = EVP_sha512(); break; + } + + require(context_ && md && EVP_DigestInit_ex(context_.get(), md, nullptr) == 1, + ErrorCode::crypto, "Initialize hash"); + size_ = static_cast(EVP_MD_size(md)); + } + + Hash(Hash&&) noexcept = default; + Hash& operator=(Hash&&) noexcept = default; + Hash(const Hash&) = delete; + Hash& operator=(const Hash&) = delete; + + std::size_t size() const noexcept { return size_; } + + void update(const Byte* input, std::size_t length) { + require(!done_ && context_, ErrorCode::state, "Hash is finished or moved"); + require(input || length == 0, ErrorCode::argument, "Null hash input"); + if (!length) return; + + if (EVP_DigestUpdate(context_.get(), input, length) != 1) { + done_ = true; throw Error(ErrorCode::crypto, "Hash update"); + } + } + + Bytes finish() { + require(!done_ && context_, ErrorCode::state, "Hash is finished or moved"); + done_ = true; + + Bytes output(size_); + unsigned written = 0; + require(EVP_DigestFinal_ex(context_.get(), output.data(), &written) == 1 && written == size_, + ErrorCode::crypto, "Hash finish"); + + return output; + } +}; + +// Reader returns 0 at EOF; writer consumes a complete chunk or throws. +template +void transform(const KeyIv& material, Mode mode, Reader&& read, Writer&& write) { + Cipher cipher(material, mode); + Bytes input(65536); + + for (;;) { + const auto count = read(input.data(), input.size()); + require(count <= input.size(), ErrorCode::argument, "Reader exceeded capacity"); + if (!count) break; + const auto output = cipher.update(input.data(), count); + if (!output.empty()) write(output.data(), output.size()); + } + + const auto output = cipher.finish(); + if (!output.empty()) write(output.data(), output.size()); +} + +inline Bytes transform_buffer(const KeyIv& material, Mode mode, const Byte* input, std::size_t size) { + Cipher cipher(material, mode); + auto output = cipher.update(input, size); + const auto tail = cipher.finish(); + output.insert(output.end(), tail.begin(), tail.end()); + return output; +} + +// Block-aligned CBC without padding, for the historical stream-helper facade. +inline Bytes transform_blocks(const KeyIv& material, Mode mode, const Byte* input, std::size_t size) { + require((input || size == 0) && size % 16 == 0, ErrorCode::argument, "Unaligned CBC input"); + + detail::CbcBlocks cipher(material, mode); + + Bytes output(size); + for (std::size_t offset = 0; offset < size;) { + const auto count = (std::min)(std::size_t{65536}, size - offset); + cipher.process(input + offset, count, output.data() + offset); + offset += count; + } + return output; +} + +// Sources must support concurrent read_at calls if shared between decoders. +class RandomAccessSource { +public: + + virtual ~RandomAccessSource() = default; + virtual std::uint64_t size() const = 0; + virtual std::size_t read_at(std::uint64_t offset, Byte* output, std::size_t capacity) = 0; + + void read_exact(std::uint64_t offset, Byte* output, std::size_t size) { + while (size) { + const auto count = read_at(offset, output, size); + require(count != 0 && count <= size, ErrorCode::io, "Truncated source"); + offset += count; output += count; size -= count; + } + } +}; + +class FileSource final : public RandomAccessSource { + struct Close { void operator()(FILE* p) const { std::fclose(p); } }; + std::unique_ptr file_; + std::uint64_t size_ = 0; + std::mutex mutex_; + + static int seek(FILE* file, std::int64_t offset, int whence) { +#ifdef _WIN32 + return _fseeki64(file, offset, whence); +#else + static_assert(sizeof(off_t) >= 8, "Build with _FILE_OFFSET_BITS=64"); + return fseeko(file, static_cast(offset), whence); +#endif + } + +public: + + explicit FileSource(const std::filesystem::path& path) { +#ifdef _WIN32 + file_.reset(_wfopen(path.c_str(), L"rb")); +#else + file_.reset(std::fopen(path.c_str(), "rb")); +#endif + require(file_ != nullptr, ErrorCode::io, "Open asset"); + require(seek(file_.get(), 0, SEEK_END) == 0, ErrorCode::io, "Seek asset"); +#ifdef _WIN32 + const auto size = _ftelli64(file_.get()); +#else + const auto size = ftello(file_.get()); +#endif + require(size >= 0, ErrorCode::io, "Get asset size"); + size_ = static_cast(size); + } + + std::uint64_t size() const override { return size_; } + + std::size_t read_at(std::uint64_t offset, Byte* output, std::size_t capacity) override { + require(offset <= size_, ErrorCode::argument, "Source offset beyond EOF"); + if (capacity == 0) return 0; + require(output != nullptr, ErrorCode::argument, "Null source output"); + const auto count = static_cast((std::min)(size_ - offset, + static_cast(capacity))); + if (!count) return 0; + + std::lock_guard lock(mutex_); + require(seek(file_.get(), static_cast(offset), SEEK_SET) == 0, + ErrorCode::io, "Seek asset"); + const auto read = std::fread(output, 1, count, file_.get()); + require(!std::ferror(file_.get()), ErrorCode::io, "Read asset"); + return read; + } +}; + +// Pull-style, independently seekable CBC view for FFmpeg/other decoders. +// Only the last block is read on construction (padding/size), then a bounded +// 64 KiB window as requested. CBC has no authenticity; padding is not a MAC. +class DecryptReader { + std::shared_ptr source_; + KeyIv material_; + std::uint64_t size_ = 0, position_ = 0, window_start_ = 0; + Bytes window_; + + Bytes decrypt_range(std::uint64_t start, std::size_t length, bool final) { + auto material = material_; + if (start) source_->read_exact(start - 16, material.iv.data(), 16); + + Bytes encrypted(length), plain(length); + source_->read_exact(start, encrypted.data(), length); + detail::CbcBlocks cipher(material, Mode::decrypt); + cipher.process(encrypted.data(), length, plain.data()); + + if (final) { + const auto padding = plain.back(); + require(padding >= 1 && padding <= 16, ErrorCode::padding, "Invalid asset padding"); + for (std::size_t i = plain.size() - padding; i < plain.size(); ++i) + require(plain[i] == padding, ErrorCode::padding, "Invalid asset padding"); + plain.resize(plain.size() - padding); + } + return plain; + } + +public: + + DecryptReader(std::shared_ptr source, const KeyIv& material) + : source_(std::move(source)), material_(material) { + require(source_ != nullptr, ErrorCode::argument, "Null asset source"); + + const auto length = source_->size(); + require(length >= 16 && length % 16 == 0 && + length <= static_cast((std::numeric_limits::max)()), + ErrorCode::padding, "Invalid encrypted asset size"); + + const auto tail = decrypt_range(length - 16, 16, true); + size_ = length - 16 + tail.size(); + } + + std::uint64_t size() const noexcept { return size_; } + std::uint64_t tell() const noexcept { return position_; } + + void seek(std::uint64_t position) { + require(position <= size_, ErrorCode::argument, "Seek beyond plaintext EOF"); + position_ = position; + } + + std::unique_ptr clone() const { + return std::make_unique(source_, material_); + } + + std::size_t read(Byte* output, std::size_t capacity) { + require(output || capacity == 0, ErrorCode::argument, "Null asset output"); + + std::size_t total = 0; + while (capacity && position_ < size_) { + if (window_.empty() || position_ < window_start_ || position_ - window_start_ >= window_.size()) { + const auto start = position_ / 16 * 16; + const auto length = static_cast((std::min)( + std::uint64_t{65536}, source_->size() - start)); + auto window = decrypt_range(start, length, start + length == source_->size()); + window_start_ = start; + window_ = std::move(window); + } + + const auto offset = static_cast(position_ - window_start_); + const auto count = (std::min)(capacity, window_.size() - offset); + std::memcpy(output, window_.data() + offset, count); + output += count; capacity -= count; total += count; position_ += count; + } + + return total; + } +}; + +} // namespace encryption diff --git a/Extensions/_DeLib/Encryption/Encryption.sln b/Extensions/_DeLib/Encryption/Encryption.sln new file mode 100644 index 000000000..a4a9195c3 --- /dev/null +++ b/Extensions/_DeLib/Encryption/Encryption.sln @@ -0,0 +1,47 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{BC8A1FFA-BEE3-4634-8014-F334798102B3}") = "Encryption", "Encryption.vcxproj", "{A6CE0358-BCA4-4803-BF84-21C812C0B399}" + ProjectSection(ProjectDependencies) = postProject + {F408ED35-2D66-4E36-A659-580895179F86} = {F408ED35-2D66-4E36-A659-580895179F86} + EndProjectSection +EndProject +Project("{BC8A1FFA-BEE3-4634-8014-F334798102B3}") = "EncryptionTests", "EncryptionTests.vcxproj", "{EF39FC7A-6792-419A-A4A1-BD73F81F22B3}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "EncryptDependencies", "..\..\_3rdLib\Encrypt-build\EncryptDependencies.vcxproj", "{F408ED35-2D66-4E36-A659-580895179F86}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Win32 = Debug|Win32 + Debug|x64 = Debug|x64 + Release|Win32 = Release|Win32 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F408ED35-2D66-4E36-A659-580895179F86}.Debug|Win32.ActiveCfg = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Debug|Win32.Build.0 = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Debug|x64.ActiveCfg = Release|x64 + {F408ED35-2D66-4E36-A659-580895179F86}.Debug|x64.Build.0 = Release|x64 + {F408ED35-2D66-4E36-A659-580895179F86}.Release|Win32.ActiveCfg = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Release|Win32.Build.0 = Release|Win32 + {F408ED35-2D66-4E36-A659-580895179F86}.Release|x64.ActiveCfg = Release|x64 + {F408ED35-2D66-4E36-A659-580895179F86}.Release|x64.Build.0 = Release|x64 + {A6CE0358-BCA4-4803-BF84-21C812C0B399}.Debug|Win32.ActiveCfg = Debug|Win32 + {A6CE0358-BCA4-4803-BF84-21C812C0B399}.Debug|Win32.Build.0 = Debug|Win32 + {A6CE0358-BCA4-4803-BF84-21C812C0B399}.Debug|x64.ActiveCfg = Debug|x64 + {A6CE0358-BCA4-4803-BF84-21C812C0B399}.Debug|x64.Build.0 = Debug|x64 + {A6CE0358-BCA4-4803-BF84-21C812C0B399}.Release|Win32.ActiveCfg = Release|Win32 + {A6CE0358-BCA4-4803-BF84-21C812C0B399}.Release|Win32.Build.0 = Release|Win32 + {A6CE0358-BCA4-4803-BF84-21C812C0B399}.Release|x64.ActiveCfg = Release|x64 + {A6CE0358-BCA4-4803-BF84-21C812C0B399}.Release|x64.Build.0 = Release|x64 + {EF39FC7A-6792-419A-A4A1-BD73F81F22B3}.Debug|Win32.ActiveCfg = Debug|Win32 + {EF39FC7A-6792-419A-A4A1-BD73F81F22B3}.Debug|Win32.Build.0 = Debug|Win32 + {EF39FC7A-6792-419A-A4A1-BD73F81F22B3}.Debug|x64.ActiveCfg = Debug|x64 + {EF39FC7A-6792-419A-A4A1-BD73F81F22B3}.Debug|x64.Build.0 = Debug|x64 + {EF39FC7A-6792-419A-A4A1-BD73F81F22B3}.Release|Win32.ActiveCfg = Release|Win32 + {EF39FC7A-6792-419A-A4A1-BD73F81F22B3}.Release|Win32.Build.0 = Release|Win32 + {EF39FC7A-6792-419A-A4A1-BD73F81F22B3}.Release|x64.ActiveCfg = Release|x64 + {EF39FC7A-6792-419A-A4A1-BD73F81F22B3}.Release|x64.Build.0 = Release|x64 + EndGlobalSection +EndGlobal diff --git a/Extensions/_DeLib/Encryption/Encryption.vcxproj b/Extensions/_DeLib/Encryption/Encryption.vcxproj new file mode 100644 index 000000000..69e024385 --- /dev/null +++ b/Extensions/_DeLib/Encryption/Encryption.vcxproj @@ -0,0 +1,45 @@ + + + + DebugWin32 + Debugx64 + ReleaseWin32 + Releasex64 + + {A6CE0358-BCA4-4803-BF84-21C812C0B399}10.0 + + + StaticLibraryv143 + true + false + + + + $(MSBuildThisFileDirectory)build\$(Platform)\$(Configuration)\ + $(OutDir)obj\$(ProjectName)\ + + + + stdcpp17Level4 + Syncfalse + MultiThreadedDebugDLL + MultiThreadedDLL + $(MSBuildThisFileDirectory);$(MSBuildThisFileDirectory)..;%(AdditionalIncludeDirectories) + _CRT_SECURE_NO_WARNINGS;NOMINMAX;%(PreprocessorDefinitions) + + + + + + + + + + + + + + + + + diff --git a/Extensions/_DeLib/Encryption/EncryptionTests.vcxproj b/Extensions/_DeLib/Encryption/EncryptionTests.vcxproj new file mode 100644 index 000000000..76740bf21 --- /dev/null +++ b/Extensions/_DeLib/Encryption/EncryptionTests.vcxproj @@ -0,0 +1,40 @@ + + + + DebugWin32 + Debugx64 + ReleaseWin32 + Releasex64 + + {EF39FC7A-6792-419A-A4A1-BD73F81F22B3}10.0 + + + Applicationv143 + true + false + + + + $(MSBuildThisFileDirectory)build\$(Platform)\$(Configuration)\ + $(OutDir)obj\$(ProjectName)\ + + + + stdcpp17Level4 + Syncfalse + MultiThreadedDebugDLL + MultiThreadedDLL + $(MSBuildThisFileDirectory);$(MSBuildThisFileDirectory)..;%(AdditionalIncludeDirectories) + _CRT_SECURE_NO_WARNINGS;NOMINMAX;%(PreprocessorDefinitions) + + + + + + + + + {A6CE0358-BCA4-4803-BF84-21C812C0B399} + + + diff --git a/Extensions/_DeLib/Encryption/KeyGen.h b/Extensions/_DeLib/Encryption/KeyGen.h new file mode 100644 index 000000000..43602641a --- /dev/null +++ b/Extensions/_DeLib/Encryption/KeyGen.h @@ -0,0 +1,76 @@ +#pragma once + +#include "Core.h" +#include "../UnicodeConversion.h" + +#include + +namespace encryption::keygen { + +// Deterministic password-to-KeyIv mapping. All UTF-16 units participate; input +// length is unrestricted. This preserves existing assets, not password hardening. +enum class SeedWidth { bits32, bits64 }; + +inline constexpr SeedWidth native_seed_width = +#ifdef _WIN64 + SeedWidth::bits64; +#else + SeedWidth::bits32; +#endif + +template +inline KeyIv generate_key_iv(std::u16string_view text) { + std::array words{}; + if (text.empty()) { + KeyIv result{}; + for (std::size_t i = 0; i < 16; ++i) result.key[i] = result.iv[i] = static_cast(i); + return result; + } + + const auto count = (std::min)(text.size(), words.size()); + std::copy_n(text.begin(), count, words.begin()); + + if (text.size() < 16) { + Word seed = static_cast(text.size()); + const auto move = [](Word value) -> Word { + return Word(Word{0x9e3779b9u} + (value << 6) + (value >> 2)); + }; + + for (std::size_t i = text.size(); i != 0; --i) + seed ^= Word(Word(text[i - 1]) + move(seed)); + + for (std::size_t i = text.size(); i < 16; ++i) { + words[i] = static_cast(((seed >> 8) & 255) ^ (seed & 255)); + seed ^= Word(Word(words[i]) + move(seed)); + } + } else { + for (std::size_t i = 16; i < text.size(); ++i) + for (auto& word : words) word ^= static_cast(text[i]); + } + + KeyIv result{}; + for (std::size_t i = 0; i < 8; ++i) { + result.key[i * 2] = static_cast(words[i]); + result.key[i * 2 + 1] = static_cast(words[i] >> 8); + result.iv[i * 2] = static_cast(words[i + 8]); + result.iv[i * 2 + 1] = static_cast(words[i + 8] >> 8); + } + + return result; +} + +inline KeyIv from_utf16(std::u16string_view text, SeedWidth width = SeedWidth::bits32) { + return width == SeedWidth::bits32 ? generate_key_iv(text) : generate_key_iv(text); +} + +inline KeyIv from_wide(std::wstring_view input, SeedWidth width = SeedWidth::bits32) { + try { return from_utf16(to_utf16_string(input), width); } + catch (const std::invalid_argument& error) { throw Error(ErrorCode::argument, error.what()); } +} + +inline KeyIv from_utf8(std::string_view input, SeedWidth width = SeedWidth::bits32) { + try { return from_utf16(to_utf16_string(input), width); } + catch (const std::invalid_argument& error) { throw Error(ErrorCode::argument, error.what()); } +} + +} // namespace encryption::keygen diff --git a/Extensions/_DeLib/Encryption/README.md b/Extensions/_DeLib/Encryption/README.md new file mode 100644 index 000000000..a5f55184f --- /dev/null +++ b/Extensions/_DeLib/Encryption/README.md @@ -0,0 +1,218 @@ +# Shared encryption core and Fusion compatibility + +All platforms, **including Windows**, use the same OpenSSL EVP implementation. +`Encryption/Core.h` is the C++17 API. `Encryption.h` / `Encryption.cpp` only retain +the historical Fusion interface and delegate to that core. There is no fallback +to the former BCrypt encryption/hash implementation. + +## File layout + +The existing `../Encryption.h` and `../Encryption.cpp` remain the Fusion compatibility +entry point. `../EncryptionStream.h` retains the historical push-stream interface. +New implementations, C adapters, build projects and tests live in this directory: + +- `Core.h`: the C++ cipher, hash, file source and seekable decryption reader. +- `KeyGen.h`: password-to-Key/IV mapping. +- `CAPI.h` / `CAPI.cpp`: the C ABI and its stream, buffer and FILE adapters. +- `Encryption.sln` / `Encryption.vcxproj`: VS library and tests; CMake target `encryption`. + +Public C++ and C headers use the same paths in the repository and after installation, +for example `Encryption/Core.h`, `Encryption/KeyGen.h` and `Encryption/CAPI.h`. +The exported `Encryption_*` C function names are unchanged. + +## Interfaces and ownership + +- `encryption::Cipher` owns an incremental AES-128-CBC operation. Supply an explicit + `KeyIv`, call `update` with arbitrary chunks, then call `finish` once for PKCS#7. + Cipher and Hash contexts are movable; an update/final failure invalidates them. +- `encryption::transform` connects a reader and writer with bounded 64 KiB input + chunks. The reader returns zero at EOF; the writer consumes the whole chunk or + throws. `transform_buffer` is the explicitly full-memory convenience operation. +- `encryption::DecryptReader` provides pull reads, a 64-bit plaintext size/cursor, + absolute seeking and independent clones over a shared `RandomAccessSource`. + It reads only the last ciphertext block and its predecessor to validate padding + and determine size, then caches at most 64 KiB of plaintext on demand. FileSource + serializes positioned reads so clones can serve separate decoders safely. +- `CAPI.h` exposes C functions with opaque, explicitly destroyed + contexts, caller-owned buffers and exception-to-result translation. The C ABI + supports incremental ciphers/hashes, callback streams, FILE adapters and pull + readers (`Encryption_ReaderCreate/Read/Seek/Size/Destroy`). Callback storage must + outlive its reader and C callbacks must not throw. Buffer outputs must be + discarded on failure; stream writers may already have received partial output. + +```cpp +#include "Encryption/Core.h" + +auto source = std::make_shared(asset_path); +encryption::DecryptReader reader(source, material); // explicit key + IV +reader.seek(12345); +std::array bytes; +auto count = reader.read(bytes.data(), bytes.size()); +``` + +The unused prototype `DecryptReaderContext` C++ buffer wrapper is removed; C++ callers +use the core directly. C stream adapters delegate to the core streaming loop. + +The historical Encryption API keeps its class/method names, algorithm-name +parameters, 32-bit lengths, string accessors and output ownership. FILE/direct +operations still collect output in the object for SaveFile; they do not overwrite +the source. Rebuild existing extensions: C++ class layout/binary ABI is not retained. +C++ callers should use the core for large/streamed assets instead of the legacy +32-bit/full-buffer accessors. Copies of the facade now own independent buffers. +Instances already contain valid default Key/IV on construction, so GetKeyInfo +returns them immediately; GenerateKey replaces the current values. Routine facade +argument/algorithm failures use the existing false/null/empty-input results. +The 32-bit length limit is checked at the compatibility buffer boundary, without +adding that restriction to the core. Directly methods still read incrementally +and keep the complete result in memory for GetOutputData/SaveFile. +ClearEncryptData explicitly clears input, output and cached string/hash results, +while retaining Key/IV and buffer settings. A cleared input is invalid; a supplied +zero-length input is valid and has the standard empty-string/file hash. + +## Existing asset compatibility + +`Encryption/KeyGen.h` is the string-to-Key/IV entry point. It accepts arbitrary-length +UTF-8, UTF-16 or wide strings, so callers can use meaningful asset passwords. It converts +wide strings to Windows UTF-16 units and reproduces the original seed expansion +for inputs shorter than 16 units, and XOR folding for longer inputs. Empty input +retains bytes `00..0f`. The modern core does not interpret passwords or string encodings. + +```cpp +#include "Encryption/KeyGen.h" +auto material = encryption::keygen::from_utf8("project/chapter-2/video-key"); +encryption::Cipher encryptor(material, encryption::Mode::encrypt); +``` + +`from_utf8`, `from_utf16` and `from_wide` map equivalent Unicode input to the +same key/IV. There is no 16-character input restriction: short input expands and +all excess UTF-16 units are folded into the result. UTF-8 is validated, no Unicode +normalization is applied, and string_view lengths preserve embedded NUL units. +This retains the existing asset mapping; it is not a new salted/password KDF. + +KeyGen is used by Encryption::GenerateKey, the C ABI string derivation +functions. The shared global helpers in `UnicodeConversion.h` +also preserve the byte representation of old wide-string inputs. This header is a C++17 +utility with no OpenSSL, Windows API or iconv dependency and is included by the +`GeneralDefinition.h` umbrella. File I/O uses `std::filesystem::path` directly; the C ABI +accepts caller-opened `FILE*` handles or callbacks. New +Cipher/DecryptReader callers supply KeyIv directly and do not need password expansion. + +For an existing file, matching password-to-key/IV bytes is sufficient because the +remaining format is kept fixed: AES-128-CBC, PKCS#7, raw ciphertext with no new +salt/header, and the same plaintext bytes. Existing assets need no re-encryption. + +The old algorithm used `size_t`; short passwords can yield different IVs in +32-bit and 64-bit Windows tools. The facade preserves its Windows architecture's +behavior; non-Windows defaults to the Fusion Win32 format. C++ callers can choose +`keygen::SeedWidth::bits32` or `bits64`. C callers pass UTF-16LE bytes to +`Encryption_DeriveKeyAndIv` (Win32), or use `Encryption_DeriveKeyAndIvWithSeedWidth` with +32 or 64 explicitly. Android Java adapters should pass UTF-16 units explicitly, +not reinterpret a native `wchar_t` or use modified UTF-8. No encoding detection or +silent fallback between asset formats is performed. + +AES-CBC and deterministic legacy key/IV derivation are retained for compatibility; +there is no new file header. CBC does not authenticate ciphertext: valid padding +is not proof of the right password or unmodified content. This is an asset format, +not a password-hardening API. + +Intentional bug fixes in the facade: file/read/write failures propagate; failed +operations clear stale output; releasing a temporary string does not erase the +corresponding binary buffer's length; empty files work; wide input is UTF-16LE on +all platforms. Caller-owned data is never freed. + +## Windows: Visual Studio builds + +Open `Encryption.sln` in VS 2022 or build with MSBuild from a VS Developer +Command Prompt. Select Win32 for Fusion or x64 for native tools. Release and Debug +use `/MD` and `/MDd`, matching the existing extension projects, and preserve the +repository's `/Zc:wchar_t-` convention. Override PlatformToolset for another VS. + +Every consuming solution now contains **EncryptDependencies**. Its project reference +runs before compilation and downloads the repository's pinned OpenSSL SDK release +for the selected architecture. SHA256 and SDK contents are checked before use. +Release and Debug static libraries with dynamic CRT are included together. +Ordinary builds do not need vcpkg, Perl or NASM. +The default SDK destination is `Extensions/_3rdLib/Encrypt-sdk/x86` or `x64`. + +```bat +msbuild Encryption.sln /m /p:Configuration=Release /p:Platform=Win32 +build\Win32\Release\EncryptionTests.exe +``` + +VS needs the C++ tools and CMake 3.21+. The VS-bundled CMake is discovered +automatically. Set OfeEncryptBuildFromSource=true to opt into the pinned vcpkg +source build, which additionally needs Git and acquires its OpenSSL build tools. +The default prebuilt packages use VS 2022 (v143); another toolset requires source +mode or a matching explicitly supplied SDK. +`../Encryption.props` supplies the generated headers/library paths and references +the utility project for all nine existing consumers and the native library/tests. + +For an existing SDK, set OfeOpenSSLRoot to a matching static OpenSSL SDK; +this disables automatic preparation by default. For a custom generated destination, +set OfeOpenSSLRoot and OfeAutoBuildEncrypt=true. A prebuilt SDK can instead be staged +using OfeEncryptPrebuiltArchive (or OfeEncryptPrebuiltUrl) plus OfeEncryptPrebuiltSha256. +The repository publishes fixed per-architecture SDK release assets and SHA256 +files, promoted from tested CI artifacts. The SDK contains include/lib/debug/share +plus an architecture/version/CRT manifest. See [dependency preparation](../../_3rdLib/Encrypt-build/README.md) for +commands, version pins, prebuilt layout, offline reuse and generated output paths. + +Static OpenSSL avoids introducing a shared runtime/version/install dependency +across independently distributed .mfx files. The small core contains no global +cache/service. It can also be built as one shared C ABI library where a consumer +controls deployment; static linkage does not deduplicate libcrypto across plugins. +A common dynamic deployment can be evaluated using actual linked sizes and the +exporter's loader contract. The compatibility headers use standard byte/integer/ +pointer types. They do not +include Windows.h or bcrypt.h. The stream helper takes encryption::Mode rather +than a BCrypt function pointer. + +## Other platforms: library builds + +Use target OpenSSL (prefer a supported OpenSSL 3 release) and CMake. Tests are opt-in +so a mobile cross-build does not try to execute a target binary on the host. + +```sh +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DOPENSSL_ROOT_DIR=/path/to/target/openssl +cmake --build build --parallel +cmake --install build --prefix /path/to/staging +``` + +This stages the library and public headers under `include/encryption`. Set +BUILD_SHARED_LIBS=ON for a C ABI shared library; Windows C declarations have explicit +DLL exports/imports. Existing Windows plugins compile the C++ facade themselves. + +For Android, configure once per ABI with the NDK toolchain, ANDROID_ABI and the +consumer's native API level. Supply ABI-specific OpenSSL headers/libraries. The +plugin's publisher must prebuild/package its owned native libraries and required +STL/dependency binaries under jniLibs; do not replace Fusion's Gradle templates. +The core needs no Java Runtime or JNI changes by itself. + +For iOS, compile separate device/simulator libraries with Apple's SDK and matching +OpenSSL slices, then include the required libraries/headers in the consuming +plugin's static-library/XCFramework build. Verify the consumer's deployment target +supports its libc++ filesystem use. This core does not publish an .ext, replace +Runtime sources, or establish that a Fusion mobile exporter packages dependencies. +Use the platform's existing publisher and verify its final link/install/export. + +## Verification + +```sh +cmake -S . -B build -DENCRYPTION_BUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Debug +cmake --build build --parallel +ctest --test-dir build --output-on-failure +``` + +The suite covers NIST CBC answers, frozen old Win32/Win64 derivation vectors, +Unicode, arbitrary chunks, finalization/errors, short reads, random/EOF seeking, +>4 GiB offsets, bounded first-read I/O, facade operations and compilation/calls +from real C. `.github/workflows/encryption.yml` builds with native VS MSBuild on +Windows and CMake on Linux/macOS. Full Fusion .mfx, Android APK and Apple +exporter/device validation are separate from these core tests. + +The existing NeoPic `Test_Encrypt.dat` / `Test_Origin.jpg` sample pair is also an +independent asset-format regression, using password `12345678ABCDEFGH`. Tests +verify the fixed input hashes, exact decrypted JPEG bytes, exact ciphertext +reproduction, C ABI and method-based facade results, unaligned streaming chunks +and 100 deterministic random seeks. No sample copies or image re-encoding are +introduced. EncryptionTests accepts explicit ciphertext/original paths as its +first two arguments, or finds the existing repository fixture pair. diff --git a/Extensions/_DeLib/Encryption/tests/asset.cpp b/Extensions/_DeLib/Encryption/tests/asset.cpp new file mode 100644 index 000000000..9f227d66f --- /dev/null +++ b/Extensions/_DeLib/Encryption/tests/asset.cpp @@ -0,0 +1,89 @@ +#include "Encryption.h" +#include "Encryption/CAPI.h" +#include +#include +#include +#include +using namespace encryption; +namespace { +void check(bool ok, const char* message) { if (!ok) throw std::runtime_error(message); } +Bytes read(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + check(file.is_open(), "Open asset compatibility fixture"); + return Bytes(std::istreambuf_iterator(file), std::istreambuf_iterator()); +} +std::string sha256(const Bytes& bytes) { + Hash hash(Digest::sha256); hash.update(bytes.data(), bytes.size()); + std::string result; + constexpr char digits[] = "0123456789abcdef"; + for (const auto byte : hash.finish()) { result += digits[byte >> 4]; result += digits[byte & 15]; } + return result; +} +std::filesystem::path find_fixtures() { + auto parent = std::filesystem::current_path(); + for (;;) { + auto path = parent / "Extensions/NeoPic/ToInstall/Files/Examples/NeoPic"; + if (std::filesystem::exists(path / "Test_Encrypt.dat")) return path; + if (parent.parent_path() == parent) break; + parent = parent.parent_path(); + } + throw std::runtime_error("Supply Test_Encrypt.dat and Test_Origin.jpg paths to EncryptionTests"); +} +} +void test_existing_asset(int argc, char** argv) { + std::filesystem::path encrypted_path, original_path; + if (argc >= 3) { encrypted_path = argv[1]; original_path = argv[2]; } + else { + const auto fixtures = find_fixtures(); + encrypted_path = fixtures / "Test_Encrypt.dat"; original_path = fixtures / "Test_Origin.jpg"; + } + const auto encrypted = read(encrypted_path), original = read(original_path); + // These are the supplied old ciphertext and its existing repository original. + check(sha256(encrypted) == "fe86c6641b02d5ad6f2629e441c872c796453aeb98938cc17abdbf76d777d199", "Old ciphertext fixture changed"); + check(sha256(original) == "da90458ef9ad3c0db78b208c29b397aece46f352f7065b2c96dd36140423d437", "Original JPEG fixture changed"); + const auto key = keygen::from_utf8("12345678ABCDEFGH"); + check(transform_buffer(key, Mode::decrypt, encrypted.data(), encrypted.size()) == original, "Core must restore the exact old JPEG bytes"); + check(transform_buffer(key, Mode::encrypt, original.data(), original.size()) == encrypted, "Core must reproduce the exact old ciphertext"); + for (const auto mode : {Mode::encrypt, Mode::decrypt}) { + const auto& source = mode == Mode::encrypt ? original : encrypted; + const auto& expected = mode == Mode::encrypt ? encrypted : original; + for (const std::size_t chunk : {17u, 4093u, 65536u}) { + std::size_t offset = 0; Bytes output; + transform(key, mode, [&](Byte* buffer, std::size_t capacity) { + const auto count = (std::min)({capacity, chunk, source.size() - offset}); + if (count) std::memcpy(buffer, source.data() + offset, count); + offset += count; return count; + }, [&](const Byte* data, std::size_t count) { output.insert(output.end(), data, data + count); }); + check(output == expected, "Streaming must match the old file for unaligned chunks"); + } + } + DecryptReader reader(std::make_shared(encrypted_path), key); + check(reader.size() == original.size(), "Seekable plaintext size"); + std::mt19937 random(2026); + std::array output{}; + for (int i = 0; i < 100; ++i) { + const auto position = i == 0 ? original.size() : random() % (original.size() + 1); + reader.seek(position); + const auto count = reader.read(output.data(), output.size()); + check(count == (std::min)(output.size(), original.size() - position), "Seekable read length"); + check(std::equal(output.begin(), output.begin() + count, original.begin() + position), "Random seek must match original bytes"); + } + Bytes c_output(encrypted.size() + 16); std::size_t count = 0; + check(Encryption_DecryptBuffer(key.key.data(), key.iv.data(), encrypted.data(), encrypted.size(), + c_output.data(), c_output.size(), &count) == ENCRYPTION_OK, "C ABI old-file decryption"); + c_output.resize(count); check(c_output == original, "C ABI original JPEG equality"); + c_output.resize(original.size() + 16); + check(Encryption_EncryptBuffer(key.key.data(), key.iv.data(), original.data(), original.size(), + c_output.data(), c_output.size(), &count) == ENCRYPTION_OK, "C ABI old-file encryption"); + c_output.resize(count); check(c_output == encrypted, "C ABI original ciphertext equality"); + Encryption facade; facade.GenerateKey(L"12345678ABCDEFGH"); + check(facade.DecryptFileDirectly(encrypted_path.wstring().c_str()), "Existing DecryptFileDirectly API"); + check(facade.GetOutputDataLength() == original.size() && + std::equal(original.begin(), original.end(), facade.GetOutputData()), "Facade original JPEG equality"); + if (argc >= 4) check(facade.SaveFile(std::filesystem::path(argv[3]).wstring().c_str()), "Save decrypted compatibility preview"); + check(facade.EncryptFileDirectly(original_path.wstring().c_str()), "Existing EncryptFileDirectly API"); + check(facade.GetOutputDataLength() == encrypted.size() && + std::equal(encrypted.begin(), encrypted.end(), facade.GetOutputData()), "Facade original ciphertext equality"); + std::cout << "PASS: supplied old asset; " << original.size() << " plaintext bytes, " << encrypted.size() + << " ciphertext bytes; exact decrypt/re-encrypt, C ABI/facade, streaming and 100 seeks\n"; +} diff --git a/Extensions/_DeLib/Encryption/tests/c_api.c b/Extensions/_DeLib/Encryption/tests/c_api.c new file mode 100644 index 000000000..397b3e48a --- /dev/null +++ b/Extensions/_DeLib/Encryption/tests/c_api.c @@ -0,0 +1,74 @@ +#include "Encryption/CAPI.h" +#include +#define CHECK(x) do { if (!(x)) return __LINE__; } while (0) +struct Source { const unsigned char* bytes; size_t length; }; +static EncryptionResult fail_read(void* user, uint8_t* output, size_t capacity, size_t* count) { + (void)user; (void)output; (void)capacity; *count = 0; + return ENCRYPTION_IO_ERROR; +} +static EncryptionResult discard(void* user, const uint8_t* bytes, size_t size) { + (void)user; (void)bytes; (void)size; + return ENCRYPTION_OK; +} +static EncryptionResult read_once(void* user, uint8_t* output, size_t capacity, size_t* count) { + struct Source* source = (struct Source*)user; + *count = source->length < capacity ? source->length : capacity; + memcpy(output, source->bytes, *count); + source->bytes += *count; source->length -= *count; + return ENCRYPTION_OK; +} +static EncryptionResult fail_write(void* user, const uint8_t* bytes, size_t size) { + (void)user; (void)bytes; (void)size; + return ENCRYPTION_IO_ERROR; +} +static EncryptionResult read_at(void* user, uint64_t offset, uint8_t* output, size_t capacity, size_t* count) { + struct Source* source = (struct Source*)user; + if (offset > source->length) return ENCRYPTION_IO_ERROR; + *count = source->length - (size_t)offset; + if (*count > capacity) *count = capacity; + memcpy(output, source->bytes + offset, *count); + return ENCRYPTION_OK; +} +int test_c_api(void) { + uint8_t key[16], iv[16], cipher[64], output[64]; + size_t n = 0, count = 0; + uint64_t length = 0; + EncryptionCipherContext* context = NULL; + EncryptionReaderContext* reader = NULL; + const uint8_t plain[] = "sample asset"; + struct Source source; + FILE* file; + const uint8_t abc_md5[16] = {0x90,0x01,0x50,0x98,0x3c,0xd2,0x4f,0xb0,0xd6,0x96,0x3f,0x7d,0x28,0xe1,0x7f,0x72}; + const uint8_t empty_md5[16] = {0xd4,0x1d,0x8c,0xd9,0x8f,0x00,0xb2,0x04,0xe9,0x80,0x09,0x98,0xec,0xf8,0x42,0x7e}; + CHECK(Encryption_DeriveKeyAndIv(NULL, 0, key, iv) == ENCRYPTION_OK); + CHECK(Encryption_EncryptReader(key, iv, fail_read, NULL, discard, NULL) == ENCRYPTION_IO_ERROR); + source.bytes = plain; source.length = sizeof plain; + CHECK(Encryption_EncryptReader(key, iv, read_once, &source, fail_write, NULL) == ENCRYPTION_IO_ERROR); + CHECK(Encryption_EncryptBuffer(key, iv, plain, sizeof plain, cipher, sizeof cipher, &n) == ENCRYPTION_OK); + source.bytes = cipher; source.length = n; + CHECK(Encryption_ReaderCreate(n, read_at, &source, key, iv, &reader) == ENCRYPTION_OK); + CHECK(Encryption_ReaderSize(reader, &length) == ENCRYPTION_OK && length == sizeof plain); + CHECK(Encryption_ReaderSeek(reader, 3) == ENCRYPTION_OK); + CHECK(Encryption_ReaderRead(reader, output, 64, &count) == ENCRYPTION_OK && count == sizeof plain - 3); + CHECK(memcmp(output, plain + 3, count) == 0); + CHECK(Encryption_ReaderRead(reader, output, 64, &count) == ENCRYPTION_OK && count == 0); + CHECK(Encryption_ReaderSeek(reader, sizeof plain + 1) == ENCRYPTION_INVALID_ARGUMENT); + Encryption_ReaderDestroy(reader); + CHECK(Encryption_CipherCreate(ENCRYPTION_ENCRYPT, key, iv, &context) == ENCRYPTION_OK); + CHECK(Encryption_CipherUpdate(context, plain, sizeof plain, output, 1, &count) == ENCRYPTION_BUFFER_TOO_SMALL); + CHECK(Encryption_CipherUpdate(context, plain, sizeof plain, output, 64, &count) == ENCRYPTION_OK); + CHECK(Encryption_CipherFinal(context, output, 1, &count) == ENCRYPTION_BUFFER_TOO_SMALL); + CHECK(Encryption_CipherFinal(context, output, 64, &count) == ENCRYPTION_OK); + CHECK(Encryption_CipherFinal(context, output, 64, &count) == ENCRYPTION_INVALID_STATE); + CHECK(Encryption_CipherUpdate(context, NULL, 0, NULL, 0, &count) == ENCRYPTION_INVALID_STATE); + Encryption_CipherDestroy(context); + file = tmpfile(); + CHECK(file != NULL && fwrite("abc", 1, 3, file) == 3); + rewind(file); + CHECK(Encryption_HashFile(file, ENCRYPTION_HASH_MD5, output, sizeof output, &count) == ENCRYPTION_OK); + CHECK(count == 16 && memcmp(output, abc_md5, 16) == 0); + CHECK(Encryption_HashFile(file, ENCRYPTION_HASH_MD5, output, sizeof output, &count) == ENCRYPTION_OK); + CHECK(count == 16 && memcmp(output, empty_md5, 16) == 0); /* caller's current position is EOF */ + CHECK(fclose(file) == 0); + return 0; +} diff --git a/Extensions/_DeLib/Encryption/tests/core.cpp b/Extensions/_DeLib/Encryption/tests/core.cpp new file mode 100644 index 000000000..af200dfad --- /dev/null +++ b/Extensions/_DeLib/Encryption/tests/core.cpp @@ -0,0 +1,328 @@ +#include "Encryption.h" +#include "EncryptionStream.h" +#include "Encryption/CAPI.h" +#include +#include +#include + +#define CHECK(condition) do { if (!(condition)) throw std::runtime_error(#condition); } while (false) +extern "C" int test_c_api(void); +void test_windows_headers(); +void test_existing_asset(int argc, char** argv); +using namespace encryption; +Bytes hex(const std::string& text) { + Bytes result; + for (std::size_t i = 0; i < text.size(); i += 2) + result.push_back(static_cast(std::stoul(text.substr(i, 2), nullptr, 16))); + return result; +} +Block block(const std::string& text) { + Block result{}; + const auto bytes = hex(text); + CHECK(bytes.size() == 16); + std::copy(bytes.begin(), bytes.end(), result.begin()); + return result; +} +template void fails(Call&& call, ErrorCode code) { + try { call(); } catch (const Error& e) { CHECK(e.code == code); return; } + throw std::runtime_error("Expected encryption::Error"); +} +void known_answers() { + // NIST SP 800-38A F.2.1, independent of round-trip tests. + const KeyIv material{block("2b7e151628aed2a6abf7158809cf4f3c"), block("000102030405060708090a0b0c0d0e0f")}; + const auto plain = hex("6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e51" + "30c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710"); + const auto encrypted = hex("7649abac8119b246cee98e9b12e9197d5086cb9b507219ee95db113a917678b2" + "73bed6b8e3c1743b7116e69e222295163ff1caa1681fac09120eca307586e1a7"); + CHECK(transform_blocks(material, Mode::encrypt, plain.data(), plain.size()) == encrypted); + CHECK(transform_blocks(material, Mode::decrypt, encrypted.data(), encrypted.size()) == plain); + Hash hash(Digest::sha256); + hash.update(reinterpret_cast("abc"), 3); + CHECK(hash.finish() == hex("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad")); + fails([&] { hash.finish(); }, ErrorCode::state); + + // Frozen outputs of the original Windows GenerateKey algorithm. + struct Vector { const char16_t* text; const char* key; const char* iv32; const char* iv64; }; + const Vector vectors[] = { + {u"abc", "6100620063002d0026009b00f900c100", "e7005c0096001d003900e0005000ad00", "e7005c0096001d00b9000000ab006c00"}, + {u"12345678", "31003200330034003500360037003800", "e1004b006b009e0058009100ef00d400", "e1004b006b009e00d800700051006800"}, + {u"123456789012345", "31003200330034003500360037003800", "39003000310032003300340035001400", "3900300031003200330034003500be00"}, + {u"1234567890123456", "31003200330034003500360037003800", "39003000310032003300340035003600", "39003000310032003300340035003600"}, + {u"12345678901234567", "06000500040003000200010000000f00", "0e000700060005000400030002000100", "0e000700060005000400030002000100"}, + {u"\u5bc6\u94a5\U0001f600", "c65ba5943dd800de4100a1000200e400", "af00850042002f00d50011008a001900", "af00850042002f0055003100b2001a00"} + }; + for (const auto& v : vectors) { + const auto a = keygen::from_utf16(v.text, keygen::SeedWidth::bits32); + const auto b = keygen::from_utf16(v.text, keygen::SeedWidth::bits64); + CHECK(a.key == block(v.key) && a.iv == block(v.iv32)); + CHECK(b.key == block(v.key) && b.iv == block(v.iv64)); + } +} +void keygen_strings() { + // Public input encodings have the same mapping; 16 is not an input limit. + const std::string phrase = "asset/\xe5\xaf\x86\xe9\x92\xa5/\xf0\x9f\x98\x80"; + const std::u16string units = u"asset/\u5bc6\u94a5/\U0001f600"; + const std::wstring wide = L"asset/\u5bc6\u94a5/\U0001f600"; + const auto same = [](const KeyIv& a, const KeyIv& b) { return a.key == b.key && a.iv == b.iv; }; + for (const auto width : {keygen::SeedWidth::bits32, keygen::SeedWidth::bits64}) { + const auto expected = keygen::from_utf16(units, width); + CHECK(same(keygen::from_utf8(phrase, width), expected)); + CHECK(same(keygen::from_wide(wide, width), expected)); + for (const std::size_t length : {0u,1u,7u,8u,15u,16u,17u,31u,32u,33u,127u,1024u,65537u}) { + const std::string input(length, 'a'); + const std::u16string utf16(length, u'a'); + const auto material = keygen::from_utf8(input, width); + CHECK(same(material, keygen::from_utf16(utf16, width))); + Bytes encoded(length * 2); + for (std::size_t i = 0; i < length; ++i) encoded[i * 2] = 'a'; + KeyIv c_material{}; + CHECK(Encryption_DeriveKeyAndIvWithSeedWidth(encoded.data(), encoded.size(), + width == keygen::SeedWidth::bits32 ? 32 : 64, + c_material.key.data(), c_material.iv.data()) == ENCRYPTION_OK); + CHECK(same(material, c_material)); + if (length > 16) { + auto changed = input; changed.back() = 'b'; + CHECK(!same(material, keygen::from_utf8(changed, width))); + } + } + } + const std::string embedded("a\0b", 3); + CHECK(same(keygen::from_utf8(embedded), keygen::from_utf16(std::u16string_view(u"a\0b", 3)))); + fails([&] { keygen::from_utf8(std::string("\xc0\x80", 2)); }, ErrorCode::argument); + fails([&] { keygen::from_utf8(std::string("\xf0\x9f", 2)); }, ErrorCode::argument); + fails([&] { keygen::from_utf8(std::string("\xed\xa0\x80", 3)); }, ErrorCode::argument); + if constexpr (sizeof(wchar_t) > 2) { + const std::wstring invalid(1, static_cast(0x110000)); + fails([&] { keygen::from_wide(invalid); }, ErrorCode::argument); + Encryption facade; + fails([&] { facade.SetEncryptStr(invalid); }, ErrorCode::argument); + } +} +class MemorySource final : public RandomAccessSource { +public: + Bytes bytes; + std::uint64_t transferred = 0; + std::size_t max_read = 65536; + explicit MemorySource(Bytes value) : bytes(std::move(value)) {} + std::uint64_t size() const override { return bytes.size(); } + std::size_t read_at(std::uint64_t offset, Byte* output, std::size_t capacity) override { + CHECK(offset <= bytes.size()); + const auto count = (std::min)({capacity, max_read, bytes.size() - static_cast(offset)}); + if (count) std::memcpy(output, bytes.data() + offset, count); + transferred += count; + return count; + } +}; +void streaming_and_seeking() { + const auto key = keygen::from_utf16(u"abc"); + std::mt19937 random(42); + for (const std::size_t size : {0u,1u,15u,16u,17u,65535u,65536u,65537u,200003u}) { + Bytes input(size); + for (auto& byte : input) byte = static_cast(random()); + const auto expected = transform_buffer(key, Mode::encrypt, input.data(), input.size()); + for (const std::size_t chunk : {1u,15u,16u,17u,65536u}) { + Cipher cipher(key, Mode::encrypt); + Bytes output; + for (std::size_t i = 0; i < size;) { + const auto n = (std::min)(chunk, size - i); + const auto bytes = cipher.update(input.data() + i, n); + output.insert(output.end(), bytes.begin(), bytes.end()); i += n; + } + const auto tail = cipher.finish(); output.insert(output.end(), tail.begin(), tail.end()); + CHECK(output == expected); + fails([&] { cipher.update(nullptr, 0); }, ErrorCode::state); + fails([&] { cipher.finish(); }, ErrorCode::state); + Cipher decrypt(key, Mode::decrypt); + Bytes plain; + for (std::size_t i = 0; i < output.size();) { + const auto n = (std::min)(chunk, output.size() - i); + const auto bytes = decrypt.update(output.data() + i, n); + plain.insert(plain.end(), bytes.begin(), bytes.end()); i += n; + } + const auto last = decrypt.finish(); plain.insert(plain.end(), last.begin(), last.end()); + CHECK(plain == input); + } + auto source = std::make_shared(expected); + source->max_read = 7; // Exercise read_exact with short source reads. + DecryptReader reader(source, key); + CHECK(reader.size() == size && source->transferred <= 32); + auto second = reader.clone(); + for (int i = 0; i < 25; ++i) { + const auto position = size ? random() % (size + 1) : 0; + reader.seek(position); + std::array buffer{}; + const auto n = reader.read(buffer.data(), buffer.size()); + CHECK(n == (std::min)(buffer.size(), size - position)); + CHECK(std::equal(buffer.begin(), buffer.begin() + n, input.begin() + position)); + CHECK(second->tell() == 0); + } + reader.seek(size); + Byte byte = 0; + CHECK(reader.read(&byte, 1) == 0); + fails([&] { reader.seek(size + 1); }, ErrorCode::argument); + auto truncated = expected; truncated.pop_back(); + fails([&] { transform_buffer(key, Mode::decrypt, truncated.data(), truncated.size()); }, ErrorCode::padding); + auto bad_padding = expected; + // Alter the byte in the preceding CBC block/IV controlling the final pad byte. + auto bad_key = key; + if (bad_padding.size() == 16) bad_key.iv.back() ^= 16; + else bad_padding[bad_padding.size() - 17] ^= 16; + fails([&] { DecryptReader bad(std::make_shared(bad_padding), bad_key); }, ErrorCode::padding); + } + auto source = std::make_shared(transform_buffer(key, Mode::encrypt, Bytes(8*1024*1024).data(), 8*1024*1024)); + DecryptReader reader(source, key); + Byte first{}; reader.read(&first, 1); + CHECK(source->transferred <= 65568); // First read must not decrypt the whole asset. + auto failing_source = std::make_shared(source->bytes); + DecryptReader failing_reader(failing_source, key); + failing_source->max_read = 0; + fails([&] { failing_reader.read(&first, 1); }, ErrorCode::io); + failing_source->max_read = 100; + CHECK(failing_reader.read(&first, 1) == 1); // No stale window after an I/O failure. +} +void large_offsets() { + class SparseSource final : public RandomAccessSource { + Block tail_; + public: + explicit SparseSource(Block tail) : tail_(tail) {} + std::uint64_t size() const override { return (std::uint64_t{1} << 32) + 32; } + std::size_t read_at(std::uint64_t offset, Byte* output, std::size_t capacity) override { + const auto count = static_cast((std::min)(std::uint64_t(capacity), size() - offset)); + std::fill_n(output, count, Byte{0}); + for (std::size_t i = 0; i < count; ++i) + if (offset + i >= size() - 16) output[i] = tail_[static_cast(offset + i - (size() - 16))]; + return count; + } + }; + const KeyIv key{}; + const auto encrypted = transform_buffer(key, Mode::encrypt, nullptr, 0); + Block tail{}; std::copy(encrypted.begin(), encrypted.end(), tail.begin()); + DecryptReader reader(std::make_shared(tail), key); + CHECK(reader.size() == (std::uint64_t{1} << 32) + 16); + reader.seek((std::uint64_t{1} << 32) + 3); + std::array output{}; + CHECK(reader.read(output.data(), output.size()) == output.size()); + const Block zero{}; + const auto expected = transform_blocks(key, Mode::decrypt, zero.data(), zero.size()); + CHECK(std::equal(output.begin(), output.end(), expected.begin() + 3)); +} +void compatibility() { + Encryption e; + e.GenerateKey(L"abc"); + const std::string text("hello\0asset", 11); + e.SetEncryptStr(text); + CHECK(e.Encrypt()); + const Bytes cipher(e.GetOutputData(), e.GetOutputData() + e.GetOutputDataLength()); + CHECK(e.GetOutputStr() != nullptr); + e.ReleaseOutputStr(); + CHECK(e.GetOutputDataLength() == cipher.size()); + e.SetEncryptData(cipher.data(), static_cast(cipher.size())); + CHECK(e.Decrypt()); + CHECK(std::string(e.GetOutputStr(), e.GetOutputDataLength()) == text); + e.SetEncryptStr(std::wstring(L"\u5bc6\U0001f600")); + CHECK(e.GetInputDataLength() == 6); // Windows UTF-16 storage on every host. + e.SetEncryptStr(std::string("abc")); + CHECK(std::wstring(e.GetHash()) == L"900150983cd24fb0d6963f7d28e17f72"); + CHECK(e.GetOutputDataLength() == 16); + e.SetEncryptStr(std::string()); + CHECK(e.Encrypt() && e.GetOutputDataLength() == 16); + e.SetEncryptData(e.GetOutputData(), e.GetOutputDataLength()); + CHECK(e.Decrypt() && e.GetOutputDataLength() == 0 && e.GetOutputData()); + const auto stamp = std::chrono::steady_clock::now().time_since_epoch().count(); + const auto file = std::filesystem::temp_directory_path().wstring() + L"/ofe-\u5bc6\u6587-" + std::to_wstring(stamp); + CHECK(e.SaveFile(file.c_str())); + CHECK(e.OpenFile(file.c_str()) && e.GetInputDataLength() == 0); + CHECK(GetFileHash(file) == L"d41d8cd98f00b204e9800998ecf8427e"); + e.SetEncryptStr(text); + CHECK(e.SaveFile(file.c_str(), true)); + CHECK(e.EncryptFileDirectly(file.c_str())); + const Bytes output(e.GetOutputData(), e.GetOutputData() + e.GetOutputDataLength()); + CHECK(e.SaveFile(file.c_str())); + CHECK(e.DecryptFileDirectly(file.c_str())); + CHECK(std::string(e.GetOutputStr(), e.GetOutputDataLength()) == text); + CHECK(e.EncryptFile(L"/missing/ofe-asset") == false && !e.GetOutputData()); + std::filesystem::remove(std::filesystem::path(file)); + EncryptionStreamHelper stream(L"abc"); + Bytes src(32, 42), result(48); + auto n = stream.Stream(src.data(), 16, result.data(), result.size(), Mode::encrypt, false); + n += stream.Stream(src.data() + 16, 16, result.data() + n, result.size() - n, Mode::encrypt, true); + const auto material = keygen::from_utf16(u"abc", keygen::native_seed_width); + CHECK(result == transform_buffer(material, Mode::encrypt, src.data(), src.size()) && n == 48); + EncryptionStreamHelper decrypt_stream(L"abc"); + Bytes restored(48); + auto count = decrypt_stream.Stream(result.data(), 16, restored.data(), restored.size(), Mode::decrypt, false); + count += decrypt_stream.Stream(result.data() + 16, 32, restored.data() + count, + restored.size() - count, Mode::decrypt, true); + restored.resize(count); + CHECK(restored == src); + +} + +void facade_defaults_and_failures() { + Encryption encryption; + const auto check_default = [&] { + const auto [key, key_size, iv, iv_size] = encryption.GetKeyInfo(); + CHECK(key && iv && key_size == 16 && iv_size == 16); + for (std::size_t i = 0; i < 16; ++i) CHECK(key[i] == i && iv[i] == i); + }; + check_default(); + + const std::string input = "default key without GenerateKey"; + encryption.SetEncryptStr(input); + CHECK(encryption.Encrypt()); + const auto expected = transform_buffer(keygen::from_utf16(u""), Mode::encrypt, + reinterpret_cast(input.data()), input.size()); + CHECK(encryption.GetOutputDataLength() == expected.size()); + CHECK(std::equal(expected.begin(), expected.end(), encryption.GetOutputData())); + + encryption.GenerateKey(L"abc"); + CHECK(encryption.GetInputStr() && encryption.GetOutputStr() && encryption.GetHash()); + encryption.ClearEncryptData(); + CHECK(!encryption.GetInputData() && !encryption.GetOutputData()); + CHECK(encryption.GetInputDataLength() == 0 && encryption.GetOutputDataLength() == 0); + CHECK(!encryption.GetInputStr() && !encryption.GetOutputStr() && !encryption.GetHash()); + const auto [key, key_size, iv, iv_size] = encryption.GetKeyInfo(); + const auto custom = keygen::from_utf16(u"abc", keygen::native_seed_width); + CHECK(key_size == 16 && iv_size == 16); + CHECK(std::equal(custom.key.begin(), custom.key.end(), key)); + CHECK(std::equal(custom.iv.begin(), custom.iv.end(), iv)); + + encryption.GenerateKey(nullptr); + check_default(); + + encryption.SetEncryptData(static_cast(nullptr), 1); + CHECK(!encryption.GetInputData() && !encryption.GetOutputData() && !encryption.Encrypt()); + encryption.SetEncryptStr(input); + CHECK(encryption.GetInputStr() && encryption.Encrypt() && encryption.GetOutputStr()); + encryption.SetEncryptStr(static_cast(nullptr), 1); + CHECK(!encryption.GetInputData() && !encryption.GetOutputData()); + CHECK(!encryption.GetInputStr() && !encryption.GetOutputStr()); + + const std::uint64_t value = 0; + encryption.SetEncryptData(&value, (std::numeric_limits::max)() / sizeof(value) + 1); + CHECK(!encryption.GetInputData()); // Reject an unrepresentable legacy length before copying. + + encryption.SetEncryptData(static_cast(nullptr), 0); + CHECK(encryption.GetInputData() && encryption.GetInputDataLength() == 0); + CHECK(std::wstring(encryption.GetHash()) == L"d41d8cd98f00b204e9800998ecf8427e"); + encryption.SetEncryptStr(static_cast(nullptr), 0); + CHECK(encryption.GetInputData() && encryption.GetInputDataLength() == 0); + CHECK(!encryption.GetHash(nullptr) && !encryption.GetOutputData()); + CHECK(!encryption.GetHash(L"unsupported")); + CHECK(encryption.GetHash(L"SHA256") && encryption.GetOutputDataLength() == 32); + CHECK(!encryption.Encrypt(nullptr) && !encryption.GetOutputData()); + CHECK(!encryption.OpenFile(nullptr) && !encryption.OpenFile(L"")); + CHECK(!encryption.SaveFile(nullptr) && !encryption.SaveFile(L"")); + CHECK(!encryption.EncryptFileDirectly(nullptr) && !encryption.DecryptFileDirectly(L"")); +} + +int main(int argc, char** argv) { + try { + test_windows_headers(); known_answers(); keygen_strings(); streaming_and_seeking(); large_offsets(); compatibility(); + facade_defaults_and_failures(); + CHECK(test_c_api() == 0); + test_existing_asset(argc, argv); + std::cout << "PASS: NIST AES, legacy Win32/Win64 vectors, short reads, random seeks, >4GiB, facade, C ABI\n"; + return 0; + } catch (const std::exception& e) { std::cerr << e.what() << '\n'; return 1; } +} diff --git a/Extensions/_DeLib/Encryption/tests/windows_headers.cpp b/Extensions/_DeLib/Encryption/tests/windows_headers.cpp new file mode 100644 index 000000000..37a65a688 --- /dev/null +++ b/Extensions/_DeLib/Encryption/tests/windows_headers.cpp @@ -0,0 +1,19 @@ +// Simulate existing Windows plugin TUs that include the SDK before Encryption.h. +#ifdef _WIN32 +#include +#include "Encryption.h" +#include +#endif +void test_windows_headers() { +#ifdef _WIN32 + Encryption encryption; + BYTE bytes[] = {1, 2, 3}; + DWORD size = 3; + encryption.SetEncryptData(bytes, size); + PBYTE input = encryption.GetInputData(); + DWORD actual = encryption.GetInputDataLength(); + LPCWSTR hash = encryption.GetHash(); + if (!input || actual != size || !hash || !encryption.Encrypt()) + throw std::runtime_error("Existing Windows SDK callers must keep working"); +#endif +} diff --git a/Extensions/_DeLib/EncryptionStream.h b/Extensions/_DeLib/EncryptionStream.h index 887fb25ba..94f920a5b 100644 --- a/Extensions/_DeLib/EncryptionStream.h +++ b/Extensions/_DeLib/EncryptionStream.h @@ -2,132 +2,45 @@ #include "Encryption.h" +// Historical push API. New code should use encryption::Cipher/DecryptReader. class EncryptionStreamHelper { -private: - //Define Vars - BCRYPT_ALG_HANDLE hAesAlg = NULL; - BCRYPT_KEY_HANDLE hKey = NULL; - NTSTATUS status = STATUS_UNSUCCESSFUL; - - DWORD - cbData = 0, - cbKeyObject = 0, - cbBlockLen = 0; - PBYTE - pbKeyObject = NULL, - pbIV = NULL; + encryption::KeyIv material_; + bool valid_ = true; public: - EncryptionStreamHelper(const wchar_t* pKeyStr, LPCWSTR Algorithm = BCRYPT_AES_ALGORITHM) { - // make key - Encryption e; - e.GenerateKey(pKeyStr); - - auto& [pKey, keyLength, - pIV, ivLength] = e.GetKeyInfo(); - - // General routine of En/Decrypt - // Open an algorithm handle. - if (!NT_SUCCESS(status = BCryptOpenAlgorithmProvider( - &hAesAlg, - Algorithm, - NULL, - 0))) { - return; - } - - // Calculate the size of the buffer to hold the KeyObject. - if (!NT_SUCCESS(status = BCryptGetProperty( - hAesAlg, - BCRYPT_OBJECT_LENGTH, - (PBYTE)&cbKeyObject, sizeof(DWORD), - &cbData, 0))) { - return; - } - - // Allocate the key object on the heap. - pbKeyObject = (PBYTE)HeapAlloc(GetProcessHeap(), 0, cbKeyObject); - if (NULL == pbKeyObject) { - return; - } - - // Calculate the block length for the IV. - if (!NT_SUCCESS(status = BCryptGetProperty( - hAesAlg, - BCRYPT_BLOCK_LENGTH, - (PBYTE)&cbBlockLen, sizeof(DWORD), - &cbData, 0))) { - return; - } - - // Determine whether the cbBlockLen is not longer than the IV length. - if (cbBlockLen > ivLength) { - return; - } - - // Allocate a buffer for the IV. The buffer is consumed during the - // encrypt/decrypt process. - pbIV = (PBYTE)HeapAlloc(GetProcessHeap(), 0, cbBlockLen); - if (NULL == pbIV) { - return; - } - - memcpy(pbIV, pIV, cbBlockLen); - - if (!NT_SUCCESS(status = BCryptSetProperty( - hAesAlg, - BCRYPT_CHAINING_MODE, - (PBYTE)BCRYPT_CHAIN_MODE_CBC, sizeof(BCRYPT_CHAIN_MODE_CBC), - 0))) { - return; - } - // Generate the key from supplied input key bytes. - if (!NT_SUCCESS(status = BCryptGenerateSymmetricKey( - hAesAlg, - &hKey, - pbKeyObject, cbKeyObject, - pKey, keyLength, - 0))) { - return; - } - - } - ~EncryptionStreamHelper() { - if (hAesAlg) { - BCryptCloseAlgorithmProvider(hAesAlg, 0); - } - - if (hKey) { - BCryptDestroyKey(hKey); - } - - if (pbKeyObject) { - HeapFree(GetProcessHeap(), 0, pbKeyObject); - } - - if (pbIV) { - HeapFree(GetProcessHeap(), 0, pbIV); - } + explicit EncryptionStreamHelper(const wchar_t* text, + const wchar_t* algorithm = L"AES") + : material_(encryption::keygen::from_wide(text ? text : L"", + encryption::keygen::native_seed_width)), + valid_(algorithm && std::wcscmp(algorithm, L"AES") == 0) {} + EncryptionStreamHelper(const EncryptionStreamHelper&) = default; + EncryptionStreamHelper& operator=(const EncryptionStreamHelper&) = default; + + size_t Stream(std::uint8_t* input, size_t size, std::uint8_t* output, size_t capacity, + encryption::Mode mode, bool padding) { + if (!valid_ || !output || (!input && size)) return 0; + if (size > (std::numeric_limits::max)() - 16 || + capacity < (mode == encryption::Mode::encrypt && padding ? size / 16 * 16 + 16 : size)) return 0; + + try { + // Capture chaining input before writing (in-place decryption is allowed). + auto iv = material_.iv; + if (mode == encryption::Mode::decrypt && size >= 16) + std::copy_n(input + size - 16, 16, iv.begin()); + + const auto result = padding + ? encryption::transform_buffer(material_, mode, input, size) + : encryption::transform_blocks(material_, mode, input, size); + + if (!result.empty()) std::memcpy(output, result.data(), result.size()); + if (mode == encryption::Mode::encrypt && result.size() >= 16) + std::copy_n(result.end() - 16, 16, iv.begin()); + + material_.iv = iv; + if (padding) valid_ = false; + return result.size(); + } catch (const encryption::Error&) { valid_ = false; return 0; } } - using HandlerType = decltype(BCryptEncrypt); - - inline size_t Stream(PBYTE pInBuf, size_t inBufSz, - PBYTE pOutBuf, size_t outBufSz, - HandlerType handler, - bool bPadding) { - DWORD byteWrite = 0; - - handler( - hKey, - pInBuf, inBufSz, - NULL, - pbIV, cbBlockLen, - pOutBuf, outBufSz, - &(byteWrite), - bPadding ? BCRYPT_BLOCK_PADDING : 0); - - return byteWrite; - } -}; \ No newline at end of file +}; diff --git a/Extensions/_DeLib/GeneralDefinition-Portability.md b/Extensions/_DeLib/GeneralDefinition-Portability.md index cb9ff046a..1c8e0b507 100644 --- a/Extensions/_DeLib/GeneralDefinition-Portability.md +++ b/Extensions/_DeLib/GeneralDefinition-Portability.md @@ -14,6 +14,7 @@ platform behavior are unchanged by the split. | HashUtilities.h | Hash-combination constant and `HASHER_MOVE` | | StringTraits.h | Existing string/character concepts and traits | | StringConversion.h | Code-page conversion and BOM-aware `LoadData`/`SaveData` | +| UnicodeConversion.h | Locale-independent UTF-8/wide to UTF-16 and UTF-16LE bytes | | StringUtilities.h | String copying, trimming, case conversion, and comparison | | StringSplit.h | Delimiter splitting and callback variants | | ArithmeticTraits.h | Existing arithmetic concept | @@ -23,6 +24,12 @@ platform behavior are unchanged by the split. Each header declares its own include dependencies. `StringConversion.h` retains the Windows API / iconv backend selection; other functional headers do not pull in either backend. The umbrella continues to expose the previous standard-library and traits includes. +`UnicodeConversion.h` also works independently in C++17 and uses standard types, without +OpenSSL, Windows API or iconv. Its global `to_utf16_string` overloads preserve UTF-16 code +units for KeyGen compatibility; `to_utf16_bytes` writes little-endian bytes independently +of the host byte order. Invalid input throws `std::invalid_argument`; Encryption maps +that exception to its existing argument error at its API boundary. File paths use `std::filesystem::path` +at file I/O call sites rather than a general string-conversion helper. Platform differences are implemented in the existing shared helpers, which remain header-only. `FindTheWay.h` and `AOE.h` use `HASHER_MOVE`, `Range`, `InsertSortedUpperBound`, and `_itos` again. diff --git a/Extensions/_DeLib/GeneralDefinition.h b/Extensions/_DeLib/GeneralDefinition.h index 57755382d..56df3ad03 100644 --- a/Extensions/_DeLib/GeneralDefinition.h +++ b/Extensions/_DeLib/GeneralDefinition.h @@ -10,6 +10,7 @@ #include "StringTraits.h" #include "StringConversion.h" +#include "UnicodeConversion.h" #include "StringUtilities.h" #include "StringSplit.h" diff --git a/Extensions/_DeLib/UnicodeConversion.h b/Extensions/_DeLib/UnicodeConversion.h new file mode 100644 index 000000000..edea7121d --- /dev/null +++ b/Extensions/_DeLib/UnicodeConversion.h @@ -0,0 +1,73 @@ +#pragma once + +#include +#include +#include +#include +#include + +// Locale-independent UTF-8 / native wide strings to UTF-16 code units. +// Invalid UTF-8 or out-of-range wide input throws std::invalid_argument. +// Wide input retains existing UTF-16 units, including unpaired surrogates. + +inline std::u16string to_utf16_string(std::string_view input) { + std::u16string result; + for (std::size_t i = 0; i < input.size();) { + const auto first = static_cast(input[i++]); + std::uint32_t code = 0, minimum = 0; + unsigned trailing = 0; + if (first < 0x80) code = first; + else if (first >= 0xc2 && first <= 0xdf) { code = first & 31; trailing = 1; minimum = 0x80; } + else if (first >= 0xe0 && first <= 0xef) { code = first & 15; trailing = 2; minimum = 0x800; } + else if (first >= 0xf0 && first <= 0xf4) { code = first & 7; trailing = 3; minimum = 0x10000; } + else throw std::invalid_argument("Invalid UTF-8 input"); + + if (trailing > input.size() - i) throw std::invalid_argument("Truncated UTF-8 input"); + + while (trailing--) { + const auto byte = static_cast(input[i++]); + if ((byte & 0xc0) != 0x80) throw std::invalid_argument("Invalid UTF-8 continuation"); + code = (code << 6) | (byte & 63); + } + + if (code < minimum || code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) + throw std::invalid_argument("Invalid UTF-8 code point"); + + if (code <= 0xffff) result.push_back(static_cast(code)); + else { + result.push_back(static_cast(0xd800 + ((code - 0x10000) >> 10))); + result.push_back(static_cast(0xdc00 + ((code - 0x10000) & 0x3ff))); + } + } + + return result; +} + +inline std::u16string to_utf16_string(std::wstring_view text) { + std::u16string result; + for (auto value : text) { + const auto code = static_cast(value); + if constexpr (sizeof(wchar_t) == 2) result.push_back(static_cast(code)); + else if (code <= 0xffff) result.push_back(static_cast(code)); + else { + if (code > 0x10ffff) throw std::invalid_argument("Invalid wide character"); + result.push_back(static_cast(0xd800 + ((code - 0x10000) >> 10))); + result.push_back(static_cast(0xdc00 + ((code - 0x10000) & 0x3ff))); + } + } + + return result; +} + +// UTF-16LE without a BOM, independent of the host byte order. +inline std::vector to_utf16_bytes(std::wstring_view text) { + const auto units = to_utf16_string(text); + std::vector bytes; + bytes.reserve(units.size() * 2); + for (const auto unit : units) { + bytes.push_back(static_cast(unit)); + bytes.push_back(static_cast(unit >> 8)); + } + + return bytes; +}