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/DecryptMemBuf.h b/Extensions/FFMpeg/DecryptMemBuf.h new file mode 100644 index 000000000..eba71c103 --- /dev/null +++ b/Extensions/FFMpeg/DecryptMemBuf.h @@ -0,0 +1,31 @@ +#pragma once + +#include "MemBuf.h" +#include "Encryption/Core.h" + +// Plugin-side glue only. DecryptReader remains an independent encryption +// component with no FFmpeg types, error codes or probing responsibilities. +class DecryptMemBuf final : public MemBuf { + encryption::DecryptReader reader; +public: + explicit DecryptMemBuf(encryption::DecryptReader source) : reader(std::move(source)) {} + int read(std::uint8_t* output, int capacity) noexcept override { + if (!output || capacity <= 0) return AVERROR(EINVAL); + try { + const auto count = reader.read(output, static_cast(capacity)); + return count ? static_cast(count) : AVERROR_EOF; + } catch (...) { return AVERROR(EIO); } + } + std::int64_t seek(std::int64_t offset, int whence) noexcept override { + const auto result = seekPosition(offset, whence, + static_cast(reader.tell()), static_cast(reader.size())); + if (result >= 0 && !(whence & AVSEEK_SIZE)) { + try { reader.seek(static_cast(result)); } + catch (...) { return AVERROR(EIO); } + } + return result; + } + std::unique_ptr clone() const override { + return std::make_unique(std::move(*reader.clone())); + } +}; 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..6b0c48609 100644 --- a/Extensions/FFMpeg/FFMpeg.vcxproj +++ b/Extensions/FFMpeg/FFMpeg.vcxproj @@ -306,6 +306,7 @@ copy $(TargetPath) "C:\Steam\steamapps\common\Clickteam Fusion 2.5\Data\Runtime\ + @@ -402,6 +403,7 @@ copy $(TargetPath) "C:\Steam\steamapps\common\Clickteam Fusion 2.5\Data\Runtime\ false + diff --git a/Extensions/FFMpeg/FFMpeg.vcxproj.filters b/Extensions/FFMpeg/FFMpeg.vcxproj.filters index 76c248aad..3b0135b6f 100644 --- a/Extensions/FFMpeg/FFMpeg.vcxproj.filters +++ b/Extensions/FFMpeg/FFMpeg.vcxproj.filters @@ -196,6 +196,9 @@ Header Files + + FFMpeg + FFMpeg diff --git a/Extensions/FFMpeg/Func.h b/Extensions/FFMpeg/Func.h index 7b78a8c45..e0165da5c 100644 --- a/Extensions/FFMpeg/Func.h +++ b/Extensions/FFMpeg/Func.h @@ -6,6 +6,8 @@ #include "FFMpegAdapter.h" #include "CopyAdapter.h" +#include "DecryptMemBuf.h" +#include "Encryption/KeyGen.h" // ----------------------------- // Forward declaration @@ -284,9 +286,21 @@ inline void OpenGeneral(LPRDATA rdPtr, std::wstring& filePath, std::wstring& key break; } - // Encrypted file - rdPtr->pEncrypt = LoadMemVideo(rdPtr, filePath, key); - rdPtr->pFFMpeg = new FFMpeg(rdPtr->pEncrypt->GetOutputData(), rdPtr->pEncrypt->GetOutputDataLength(), opt); + // Encrypted file: build the decryption context in the event bridge. + if (rdPtr->bCache) { + rdPtr->pEncrypt = LoadMemVideo(rdPtr, filePath, key); + rdPtr->pFFMpeg = new FFMpeg(rdPtr->pEncrypt->GetOutputData(), rdPtr->pEncrypt->GetOutputDataLength(), opt); + } else { + auto inputOptions = opt; + try { + auto source = std::make_shared(std::filesystem::path(filePath)); + const auto material = encryption::keygen::from_wide(key, + encryption::keygen::native_seed_width); + inputOptions.input = std::make_shared( + encryption::DecryptReader(std::move(source), material)); + } catch (...) { throw FFMpegException(FFMpegException_InitFailed); } + rdPtr->pFFMpeg = new FFMpeg(filePath, inputOptions); + } } while (false); // update state diff --git a/Extensions/FFMpeg/ToInstall/Files/Help/FFMpeg/FFMpeg.md b/Extensions/FFMpeg/ToInstall/Files/Help/FFMpeg/FFMpeg.md index f379e84cc..f72929aa4 100644 --- a/Extensions/FFMpeg/ToInstall/Files/Help/FFMpeg/FFMpeg.md +++ b/Extensions/FFMpeg/ToInstall/Files/Help/FFMpeg/FFMpeg.md @@ -87,7 +87,7 @@ Here follows all dlls needed by this object: - *If open failed, extension will keep previous frame* - *If filename is an URL, e.g., [Apple iPhone X](https://www.apple.com/105/media/us/iphone-x/2017/01df5b43-28e4-4848-bf20-490c34a926a7/films/feature/iphone-x-feature-tpl-cc-us-20170912_1920x1080h.mp4), the key will be ignored. Note that player may freeze if play over a bad network connection. This lag can be reduced by moving queue to another thread, which is not implemented* - *If filename and key can be convert to number and it's non-zero, then filename will be treat to the data address, key is size of data. Note that extension will not check if the address is valid, and you should not release it before you close the video.* - - *If key is not empty, extension will treat it as an encrypted file. Decode a normal file in encrypted way usually will crash. You can encrypt files by [Encrypter](https://github.com/defisym/OpenFusionExamples/tree/master/Extensions/Encrypter/ToInstall/Files). Encrypt & decrypt are based on AES so without your key no one can get your file, unless they dump it from memory. Play encrypted videos may cost more memory as decrypt stream is not implemented* + - *If key is not empty, extension will treat it as an encrypted file. Decode a normal file in encrypted way usually will crash. You can encrypt files by [Encrypter](https://github.com/defisym/OpenFusionExamples/tree/master/Extensions/Encrypter/ToInstall/Files). Encrypt & decrypt are based on AES so without your key no one can get your file, unless they dump it from memory. Encrypted local files use seekable streaming decryption when memory caching is disabled. Explicit memory caching retains the full decrypted asset in memory* - Open Video To - *Open video to a given timestamp* 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/_3rdLib/FFMpeg.h b/Extensions/_3rdLib/FFMpeg.h index 5185d4fbb..661f2fd86 100644 --- a/Extensions/_3rdLib/FFMpeg.h +++ b/Extensions/_3rdLib/FFMpeg.h @@ -220,6 +220,10 @@ class FFMpegOptions { std::string audioCodecName; int threadCount = 0; + // Optional MemBuf prototype. Copies share the prototype, while each player + // and preview clones an independent cursor and owns its AVIO separately. + std::shared_ptr input; + FFMpegOptions() = default; inline void ResetOverride() { @@ -312,15 +316,14 @@ class FFMpeg { #pragma region FFMpeg FFMpegOptions options; - bool bFromMem = false; bool bNoAudio = false; bool bForceNoAudio = false; AVIOContext* pAvioContext = nullptr; AVIOContext* pSeekAvioContext = nullptr; - MemBuf* pMemBuf = nullptr; - MemBuf* pSeekMemBuf = nullptr; + std::unique_ptr pMemBuf; + std::unique_ptr pSeekMemBuf; AVFormatContext* pFormatContext = nullptr; AVFormatContext* pSeekFormatContext = nullptr; @@ -721,28 +724,19 @@ class FFMpeg { } #endif // AUDIO_TEMPO - // probe video format - static inline const AVInputFormat* init_probe(BYTE* pBuffer, const size_t bfSz, const LPCSTR pFileName = "") { - AVProbeData probeData = { }; - probeData.buf = pBuffer; - probeData.buf_size = static_cast((std::min)(static_cast(MEM_BUFFER_SIZE), bfSz)); - probeData.filename = pFileName; - - // Determine the input-format: - while (true) { - const auto iformat = av_probe_input_format(&probeData, 1); - - if (iformat != nullptr) { - return iformat; - } - - if (static_cast(probeData.buf_size + 1) > bfSz) { - return nullptr; - } + // Probe through AVIO so the same stage works with memory and derived readers. + // FFmpeg retains probe bytes for avformat_open_input, including padding. + static inline const AVInputFormat* init_probe(AVIOContext* context) { + const AVInputFormat* format = nullptr; + return av_probe_input_buffer2(context, &format, "", nullptr, 0, 0) < 0 ? nullptr : format; + } - probeData.buf_size = static_cast((std::min)(static_cast(2 * probeData.buf_size), bfSz)); - } - } + static inline void free_avioContext(AVIOContext** context) { + if (!*context) return; + // Probing may replace the buffer allocated in init_formatContext. + av_freep(&(*context)->buffer); + avio_context_free(context); + } // from file static inline bool init_formatContext(AVFormatContext** ppFormatContext, const std::wstring& filePath, @@ -755,8 +749,6 @@ class FFMpeg { *ppFormatContext = avformat_alloc_context(); if (!*ppFormatContext) { return false; } - //auto iformat = init_probe(nullptr, 0, pFilePath); - // convert to UTF-8 to avoid crash in some versions if (avformat_open_input(ppFormatContext, pFilePath, fmt, nullptr) != 0) { return false; @@ -765,15 +757,14 @@ class FFMpeg { return true; } - // from memory + // from memory or a derived MemBuf; probe/open/AVIO belong to FFMpeg static inline bool init_formatContext(AVFormatContext** ppFormatContext, AVIOContext** ppAvioContext, - MemBuf** ppBuf, unsigned char* pBuffer, const size_t bfSz, - const AVInputFormat* fmt = nullptr) { + MemBuf* pBuffer, const AVInputFormat* fmt = nullptr) { if (pBuffer == nullptr) { return false; } - (*ppBuf) = new MemBuf(pBuffer, static_cast(bfSz)); - - *ppAvioContext = avio_alloc_context((*ppBuf)->get(), (*ppBuf)->getSize(), 0, (*ppBuf), + auto* buffer = static_cast(av_malloc(MEM_BUFFER_SIZE)); + if (!buffer) { return false; } + *ppAvioContext = avio_alloc_context(buffer, MEM_BUFFER_SIZE, 0, pBuffer, [] (void* opaque, uint8_t* buf, const int buf_size) { const auto pMemBuf = static_cast(opaque); return pMemBuf->read(buf, buf_size); @@ -784,21 +775,21 @@ class FFMpeg { return pMemBuf->seek(offset, whence); }); - if (!*ppAvioContext) { return false; } + if (!*ppAvioContext) { av_free(buffer); return false; } + (*ppAvioContext)->seekable = AVIO_SEEKABLE_NORMAL; + if (!fmt) { fmt = init_probe(*ppAvioContext); } + if (!fmt) { free_avioContext(ppAvioContext); return false; } *ppFormatContext = avformat_alloc_context(); - if (!*ppFormatContext) { return false; } - - if (!fmt) { fmt = init_probe(pBuffer, bfSz); } - if (!fmt) { return false; } + if (!*ppFormatContext) { free_avioContext(ppAvioContext); return false; } - // might crash in avformat_open_input due to access violation if not set - // https://www.codeproject.com/Tips/489450/Creating-Custom-FFmpeg-IO-Context + // Set the probed format before opening the custom IO context. (*ppFormatContext)->iformat = fmt; (*ppFormatContext)->pb = *ppAvioContext; (*ppFormatContext)->flags |= AVFMT_FLAG_CUSTOM_IO; - if (avformat_open_input(ppFormatContext, nullptr, nullptr, nullptr) < 0) { + if (avformat_open_input(ppFormatContext, nullptr, fmt, nullptr) < 0) { + free_avioContext(ppAvioContext); return false; } @@ -1779,31 +1770,38 @@ class FFMpeg { return response; } -public: - //Load from file - explicit FFMpeg(const std::wstring& filePath, const FFMpegOptions& opt = FFMpegOptions()) { - bFromMem = false; - this->options = opt; - - if(!init_formatContext(&pFormatContext, filePath)) { - throw FFMpegException(FFMpegException_InitFailed); - } - - if (const auto ret = init_general(); ret != 0) { throw FFMpegException(ret); } - } - - //Load from memory - //https://blog.csdn.net/leixiaohua1020/article/details/12980423 - FFMpeg(unsigned char* pBuffer, const size_t bfSz, const FFMpegOptions& opt = FFMpegOptions()) { - bFromMem = true; - this->options = opt; + // A completed delegate ensures ~FFMpeg also runs if initialization throws. + explicit FFMpeg(const FFMpegOptions& opt) : options(opt) {} + static FFMpegOptions memoryOptions(unsigned char* data, size_t size, const FFMpegOptions& opt) { + auto result = opt; + result.input = std::make_shared(data, size); + return result; + } - if (!init_formatContext(&pFormatContext, &pAvioContext, &pMemBuf, pBuffer, bfSz)) { - throw FFMpegException(FFMpegException_InitFailed); - } +public: + // Options may supply a custom input; otherwise use FFmpeg's file/URL protocols. + explicit FFMpeg(const std::wstring& filePath, const FFMpegOptions& opt = FFMpegOptions()) + : FFMpeg(opt) { + try { + bool opened = false; + if (options.input) { + pMemBuf = options.input->clone(); + opened = init_formatContext(&pFormatContext, &pAvioContext, pMemBuf.get()); + } else { + opened = init_formatContext(&pFormatContext, filePath); + } + if (!opened) throw FFMpegException(FFMpegException_InitFailed); + if (const auto result = init_general(); result != 0) throw FFMpegException(result); + } catch (const FFMpegException&) { + throw; + } catch (...) { + throw FFMpegException(FFMpegException_InitFailed); + } + } - if (const auto ret = init_general(); ret != 0) { throw FFMpegException(ret); } - } + // Retain the original memory entry point; it now sets the input option. + FFMpeg(unsigned char* pBuffer, const size_t bfSz, const FFMpegOptions& opt = FFMpegOptions()) + : FFMpeg(std::wstring{}, memoryOptions(pBuffer, bfSz, opt)) {} ~FFMpeg() { bExit = true; @@ -1850,8 +1848,10 @@ class FFMpeg { sws_freeContext(swsContext); swr_free(&swrContext); - pAdapter->FreeContext(VCodecCtxQpaque.hTextureCtx); - pAdapter->FreeContext(VGetCodecCtxQpaque.hTextureCtx); + if (pAdapter) { + pAdapter->FreeContext(VCodecCtxQpaque.hTextureCtx); + pAdapter->FreeContext(VGetCodecCtxQpaque.hTextureCtx); + } avcodec_free_context(&pVCodecContext); avcodec_free_context(&pVGetCodecContext); @@ -1864,15 +1864,9 @@ class FFMpeg { av_buffer_unref(&hw_device_ctx); #endif // HW_DECODE - if (bFromMem) { - delete pMemBuf; - pMemBuf = nullptr; - delete pSeekMemBuf; - pSeekMemBuf = nullptr; - - avio_context_free(&pAvioContext); - avio_context_free(&pSeekAvioContext); - } + // AVFormatContext must close before its custom AVIO and input. + free_avioContext(&pSeekAvioContext); + free_avioContext(&pAvioContext); delete[] p_global_bgr_buffer; @@ -1931,19 +1925,9 @@ class FFMpeg { // Video info // ------------------------------------ - inline const uint8_t* get_memBufSrc() const { - if (bFromMem == false) { - return nullptr; - } - - if (pMemBuf == nullptr) { - return nullptr; - } - - const auto pSrc = pMemBuf->getSrc(); - - return pSrc; - } + inline const uint8_t* get_memBufSrc() const { + return pMemBuf ? pMemBuf->getSrc() : nullptr; + } inline int64_t get_videoPosition() const { return static_cast(videoPts * 1000); @@ -2152,14 +2136,17 @@ class FFMpeg { // get video frame of given time stamp inline int get_videoFrame(const size_t ms, const bool bAccurateSeek, const FrameDataCallBack& callBack) { // update context - if (!pSeekFormatContext) { - const auto bSuccess = bFromMem - ? init_formatContext(&pSeekFormatContext, &pSeekAvioContext, - &pSeekMemBuf, const_cast(pMemBuf->getSrc()), pMemBuf->getSrcSize(), - pFormatContext->iformat) - : init_formatContext(&pSeekFormatContext, pFormatContext->url, pFormatContext->iformat); - if (!bSuccess) { return FFMpegException_InitFailed; } - } + if (!pSeekFormatContext) { + bool success = false; + if (pMemBuf) { + pSeekMemBuf = pMemBuf->clone(); + success = init_formatContext(&pSeekFormatContext, &pSeekAvioContext, + pSeekMemBuf.get(), pFormatContext->iformat); + } else { + success = init_formatContext(&pSeekFormatContext, pFormatContext->url, pFormatContext->iformat); + } + if (!success) return FFMpegException_InitFailed; + } if (!pVGetCodecContext ) { // force to share context diff --git a/Extensions/_3rdLib/MemBuf.h b/Extensions/_3rdLib/MemBuf.h index 2ca2b6370..50130b705 100644 --- a/Extensions/_3rdLib/MemBuf.h +++ b/Extensions/_3rdLib/MemBuf.h @@ -1,91 +1,65 @@ #pragma once -// #include - +#include +#include +#include +#include +#include +#include +#include extern "C" { -#include +#include +#include } +#include -// 4KB constexpr auto MEM_BUFFER_SIZE = 4 * 1024; +// Simple borrowed-memory reader. Derived readers supply read/seek without +// changing the player's AVIO callbacks. FFmpeg owns its own scratch buffer. class MemBuf { -private: - int64_t bufferIndex = 0; - - uint8_t* pBuf = nullptr; - int bufSize = 0; - - uint8_t* pSrcBuf = nullptr; - int srcBufSize = 0; - + std::int64_t bufferIndex = 0; + const std::uint8_t* pSrcBuf = nullptr; + std::int64_t srcBufSize = 0; +protected: + MemBuf() = default; + static std::int64_t seekPosition(std::int64_t offset, int whence, + std::int64_t position, std::int64_t size) noexcept { + if (whence == AVSEEK_SIZE || whence == (AVSEEK_SIZE | AVSEEK_FORCE)) return size; + std::int64_t base = 0; + switch (whence & ~AVSEEK_FORCE) { + case SEEK_SET: break; + case SEEK_CUR: base = position; break; + case SEEK_END: base = size; break; + default: return AVERROR(EINVAL); + } + if (offset < -base || offset > size - base) return AVERROR(EINVAL); + return base + offset; + } public: - MemBuf(uint8_t* pSrcBuf, int srcBufSize, int bufSize = MEM_BUFFER_SIZE) { - this->pSrcBuf = pSrcBuf; - this->srcBufSize = srcBufSize; - - this->pBuf = new uint8_t[bufSize]; - this->bufSize = bufSize; - } - - ~MemBuf() { - delete[] this->pBuf; - } - - inline const uint8_t* getSrc() const { - return this->pSrcBuf; - } - - inline int getSrcSize() const { - return srcBufSize; - } - - inline uint8_t* get() const { - return pBuf; - } - - inline int getSize() const { - return bufSize; - } - - inline int read(uint8_t* buf, int buf_size) { - auto dataLeft = srcBufSize - bufferIndex; - //memset(buf, 0, buf_size); - - if (dataLeft >= buf_size) { - memcpy(buf, this->pSrcBuf + bufferIndex, buf_size); - bufferIndex += buf_size; - - return buf_size; - } - else if (dataLeft > 0) { - memcpy(buf, this->pSrcBuf + bufferIndex, (size_t)dataLeft); - bufferIndex += dataLeft; - - return (size_t)dataLeft; - } - else { - return AVERROR_EOF; - } - } - - inline int64_t seek(int64_t offset, int whence) { - switch (whence) { - case SEEK_SET: - bufferIndex = 0 + offset; - - return bufferIndex; - case SEEK_CUR: - bufferIndex = bufferIndex + offset; - - return bufferIndex; - case SEEK_END: - bufferIndex = srcBufSize + offset; - - return bufferIndex; - case AVSEEK_SIZE: - default: - return -1; - } - } -}; \ No newline at end of file + MemBuf(const std::uint8_t* source, std::size_t size) : pSrcBuf(source) { + if ((!source && size) || size > static_cast((std::numeric_limits::max)())) + throw std::invalid_argument("Invalid memory input"); + srcBufSize = static_cast(size); + } + virtual ~MemBuf() = default; + const std::uint8_t* getSrc() const { return pSrcBuf; } + std::int64_t getSrcSize() const { return srcBufSize; } + virtual int read(std::uint8_t* output, int capacity) noexcept { + if (!output || capacity <= 0) return AVERROR(EINVAL); + const auto count = static_cast((std::min)(srcBufSize - bufferIndex, static_cast(capacity))); + if (!count) return AVERROR_EOF; + std::memcpy(output, pSrcBuf + bufferIndex, static_cast(count)); + bufferIndex += count; + return count; + } + virtual std::int64_t seek(std::int64_t offset, int whence) noexcept { + const auto result = seekPosition(offset, whence, bufferIndex, srcBufSize); + if (result >= 0 && !(whence & AVSEEK_SIZE)) bufferIndex = result; + return result; + } + // Fresh cursor for preview decoding; source memory must outlive all clones. + virtual std::unique_ptr clone() const { + return std::make_unique(pSrcBuf, static_cast(srcBufSize)); + } +}; 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..a5777ccb4 --- /dev/null +++ b/Extensions/_DeLib/Encryption/CMakeLists.txt @@ -0,0 +1,50 @@ +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") + find_package(PkgConfig QUIET) + if(PkgConfig_FOUND) + pkg_check_modules(AVFORMAT QUIET IMPORTED_TARGET libavformat libavcodec libavutil) + if(AVFORMAT_FOUND) + add_executable(encryption_avio_tests tests/avio.cpp) + target_include_directories(encryption_avio_tests PRIVATE ../../_3rdLib ../../FFMpeg) + target_link_libraries(encryption_avio_tests PRIVATE encryption PkgConfig::AVFORMAT) + add_test(NAME encryption_avio_tests COMMAND encryption_avio_tests) + endif() + endif() +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..2326aabce --- /dev/null +++ b/Extensions/_DeLib/Encryption/README.md @@ -0,0 +1,248 @@ +# 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. +- The FFmpeg integration lives in the FFMpeg plugin. Its `DecryptMemBuf` derives + from the existing `MemBuf` and forwards read/seek to a DecryptReader. Neither + the core nor its installed headers depend on FFmpeg. +- `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. + +The FFMpeg extension selects cached memory or a streaming DecryptReader before +constructing the player. Its thin `DecryptMemBuf : MemBuf` adapter is in +`Extensions/FFMpeg/DecryptMemBuf.h`; it only translates read/seek and errors. +The reusable DecryptReader remains independent of FFmpeg. MemBuf remains the +simple memory implementation and base class, with virtual read/seek/clone. + +FFMpeg retains responsibility for AVIO allocation, its init_probe stage, format +opening and cleanup. Probing uses FFmpeg's av_probe_input_buffer2 on AVIO, so it +can inspect either input without requiring a complete plaintext buffer; FFmpeg +retains the padded probe bytes for subsequent demuxing. The reader never probes +formats or owns FFmpeg contexts. + +The existing file/options and memory constructors remain. The optional +`FFMpegOptions::input` is a `shared_ptr` prototype: copies share only +the prototype, while playback and preview clone independent cursors and own +separate AVIO contexts. No new general input interface or AVIO wrapper is added. +Without a custom MemBuf, file/URL input uses FFmpeg's normal protocols. + +Fusion actions, expressions and file/key parameters retain their interface. +Explicit memory caching retains its full-buffer behavior. MemBuf borrows source +memory, which must outlive playback and preview; get_memBufSrc() continues to +provide its identity for cache-reference tracking. The player closes demuxers +before freeing AVIO buffers and readers, including failed initialization. + +## 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, and the FFMpeg extension's encrypted-input selection. 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. If pkg-config locates FFmpeg development libraries, CMake also tests +real encrypted WAV demuxing, payload equality, AVIO sizes, timestamp seeking and +independent primary/secondary contexts. `.github/workflows/encryption.yml` builds +with native VS MSBuild on Windows and CMake on Linux/macOS. Full Fusion .mfx, +Android APK and Apple device/exporter 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/avio.cpp b/Extensions/_DeLib/Encryption/tests/avio.cpp new file mode 100644 index 000000000..9b30fd09a --- /dev/null +++ b/Extensions/_DeLib/Encryption/tests/avio.cpp @@ -0,0 +1,171 @@ +#include "DecryptMemBuf.h" +#include "Encryption/KeyGen.h" +extern "C" { +#include +#include +#include +} +#include +#define CHECK(x) do { if (!(x)) throw std::runtime_error(#x); } while (false) +using namespace encryption; +class MemorySource final : public RandomAccessSource { + Bytes bytes_; +public: + std::uint64_t transferred = 0; + bool fail = false; + explicit MemorySource(Bytes bytes) : bytes_(std::move(bytes)) {} + 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 { + if (fail) throw std::runtime_error("Injected source failure"); + CHECK(offset <= bytes_.size()); + auto count = (std::min)(capacity, bytes_.size() - static_cast(offset)); + if (count) std::memcpy(output, bytes_.data() + offset, count); + transferred += count; + return count; + } +}; +void word(Bytes& data, std::uint32_t value, int count) { + while (count--) { data.push_back(static_cast(value)); value >>= 8; } +} +// Consumer-side fixture: the player owns AVIO/probing, the reader only read/seek. +struct Demuxer { + std::unique_ptr reader; + AVIOContext* io = nullptr; + AVFormatContext* format = nullptr; + explicit Demuxer(std::unique_ptr source) : reader(std::move(source)) { + auto* buffer = static_cast(av_malloc(MEM_BUFFER_SIZE)); + if (!buffer) throw std::bad_alloc(); + io = avio_alloc_context(buffer, MEM_BUFFER_SIZE, 0, reader.get(), + [](void* opaque, Byte* output, int capacity) { return static_cast(opaque)->read(output, capacity); }, + nullptr, + [](void* opaque, std::int64_t offset, int whence) { return static_cast(opaque)->seek(offset, whence); }); + if (!io) { av_free(buffer); throw std::bad_alloc(); } + } + ~Demuxer() { + avformat_close_input(&format); + av_freep(&io->buffer); + avio_context_free(&io); + } + bool open(const AVInputFormat* type = nullptr) { + if (!type) { + if (av_probe_input_buffer2(io, &type, "", nullptr, 0, 0) < 0) return false; + CHECK(type && std::string(type->name) == "wav"); + CHECK(avio_tell(io) == 0); // Probe bytes remain available to open/read. + } + format = avformat_alloc_context(); + if (!format) return false; + format->iformat = type; + format->pb = io; + format->flags |= AVFMT_FLAG_CUSTOM_IO; + return avformat_open_input(&format, nullptr, type, nullptr) >= 0; + } +}; +void demux(std::unique_ptr source, const Bytes& wav, std::uint32_t samples) { + Demuxer input(std::move(source)); + CHECK(input.open()); + auto* format = input.format; + CHECK(avformat_find_stream_info(format, nullptr) >= 0); + CHECK(format->nb_streams == 1 && format->streams[0]->codecpar->codec_id == AV_CODEC_ID_PCM_S16LE); + CHECK(avio_size(format->pb) == static_cast(wav.size())); + CHECK(avio_seek(format->pb, -1, SEEK_SET) < 0); + CHECK(avio_seek(format->pb, (std::numeric_limits::max)(), SEEK_END) < 0); + CHECK(av_seek_frame(format, 0, 0, AVSEEK_FLAG_BACKWARD) >= 0); + const auto packet = std::unique_ptr(av_packet_alloc(), + [](AVPacket* p) { av_packet_free(&p); }); + CHECK(packet != nullptr); + std::size_t total = 0; + while (av_read_frame(format, packet.get()) >= 0) { + CHECK(total + packet->size <= samples * 2); + CHECK(std::memcmp(packet->data, wav.data() + 44 + total, packet->size) == 0); + total += packet->size; + av_packet_unref(packet.get()); + } + CHECK(total == samples * 2); + CHECK(av_seek_frame(format, 0, 12000, AVSEEK_FLAG_BACKWARD) >= 0); + CHECK(av_read_frame(format, packet.get()) >= 0 && packet->pts == 12000); + av_packet_unref(packet.get()); + Demuxer preview(input.reader->clone()); + CHECK(preview.open(format->iformat)); + CHECK(avformat_find_stream_info(preview.format, nullptr) >= 0); + CHECK(av_read_frame(preview.format, packet.get()) >= 0); + av_packet_unref(packet.get()); + CHECK(av_read_frame(format, packet.get()) >= 0 && packet->pts > 12000); +} + +void seek_contract(MemBuf& input, const Bytes& plain) { + const auto size = static_cast(plain.size()); + CHECK(input.seek(7, SEEK_SET) == 7); + CHECK(input.seek(0, AVSEEK_SIZE | AVSEEK_FORCE) == size); + CHECK(input.seek(0, SEEK_CUR) == 7); // Size queries never move the cursor. + CHECK(input.seek((std::numeric_limits::min)(), SEEK_CUR) < 0); + CHECK(input.seek((std::numeric_limits::max)(), SEEK_END) < 0); + CHECK(input.seek(1, AVSEEK_SIZE | SEEK_CUR) < 0); + CHECK(input.seek(0, SEEK_CUR) == 7); // Rejected seeks also preserve position. + CHECK(input.seek(-3, SEEK_END | AVSEEK_FORCE) == size - 3); + Byte tail[3]; + CHECK(input.read(tail, 3) == 3); + CHECK(std::memcmp(tail, plain.data() + plain.size() - 3, 3) == 0); + CHECK(input.read(tail, 3) == AVERROR_EOF); + CHECK(input.seek(1, SEEK_END) < 0); + auto other = input.clone(); + CHECK(other->seek(0, SEEK_CUR) == 0); + CHECK(input.seek(0, SEEK_CUR) == size); + CHECK(input.seek(-size, SEEK_CUR) == 0); +} + +void failure_contract(const KeyIv& key, const Bytes& wav) { + auto source = std::make_shared(transform_buffer(key, Mode::encrypt, wav.data(), wav.size())); + DecryptMemBuf input(DecryptReader(source, key)); + source->fail = true; + Byte bytes[16]; + CHECK(input.read(bytes, 16) == AVERROR(EIO)); // No exception crosses the AVIO callback. + { + Demuxer failed(std::make_unique(DecryptReader( + std::make_shared(transform_buffer(key, Mode::encrypt, bytes, 16)), key))); + CHECK(!failed.open()); // Unknown format fails during consumer-side probe. + CHECK(failed.format == nullptr); + } + MemBuf empty(nullptr, 0); + Byte byte; + CHECK(empty.seek(0, AVSEEK_SIZE) == 0); + CHECK(empty.read(&byte, 1) == AVERROR_EOF); + CHECK(empty.seek(1, SEEK_SET) < 0); + // Copyable option storage retains the prototype, not an AVIO or live cursor. + std::shared_ptr original = std::make_shared(wav.data(), wav.size()); + auto copy = original; + original.reset(); + auto first = copy->clone(); + auto second = copy->clone(); + copy.reset(); + CHECK(first->seek(9, SEEK_SET) == 9); + CHECK(second->seek(0, SEEK_CUR) == 0); +} + +int main() { + try { + const std::uint32_t samples = 8000 * 30; + Bytes wav{'R','I','F','F'}; + word(wav, samples*2+36, 4); wav.insert(wav.end(), {'W','A','V','E','f','m','t',' '}); + word(wav, 16, 4); word(wav, 1, 2); word(wav, 1, 2); word(wav, 8000, 4); + word(wav, 16000, 4); word(wav, 2, 2); word(wav, 16, 2); wav.insert(wav.end(), {'d','a','t','a'}); + word(wav, samples*2, 4); + for (std::uint32_t i = 0; i < samples; ++i) word(wav, i % 100, 2); + const auto key = keygen::from_utf16(u"abc"); + auto source = std::make_shared(transform_buffer(key, Mode::encrypt, wav.data(), wav.size())); + auto encrypted = std::make_unique(DecryptReader(source, key)); + CHECK(source->transferred == 32); // No full plaintext loading during construction. + CHECK(encrypted->getSrc() == nullptr); + seek_contract(*encrypted, wav); + demux(std::move(encrypted), wav, samples); + auto memory = std::make_unique(wav.data(), wav.size()); + CHECK(memory->getSrc() == wav.data()); // Existing cache-reference identity. + CHECK(memory->clone()->getSrc() == wav.data()); + seek_contract(*memory, wav); + demux(std::move(memory), wav, samples); + failure_contract(key, wav); + std::cout << "PASS: MemBuf-derived decryption, explicit probe, memory/encrypted WAV payload, seek, independent preview and IO failures\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; return 1; + } +} 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; +}