diff --git a/.github/workflows/nuget_publish.yml b/.github/workflows/nuget_publish.yml new file mode 100644 index 0000000..c1ab9bc --- /dev/null +++ b/.github/workflows/nuget_publish.yml @@ -0,0 +1,61 @@ +name: Publish to nuget + +on: + push: + branches: + - master + paths: + - "**/*.cs" + - "**/*.csproj" + +jobs: + + nuget_publish: + name: Build, pack and nuget publish + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-dotnet@v5 + with: + dotnet-version: | + 6.0.x + 8.0.x + + - name: Launch unit tests + run: dotnet test --nologo + + - name: NuGet publish + id: nuget_worker + uses: alirezanet/publish-nuget@v3.1.0 + with: + # Filepath of the project to be packaged, relative to root of repository + PROJECT_FILE_PATH: SQLCipherED/SQLCipherED.csproj + + # NuGet package id, used for version detection & defaults to project name + # PACKAGE_NAME: AesIge + + # Filepath with version info, relative to root of repository & defaults to PROJECT_FILE_PATH + # VERSION_FILE_PATH: Directory.Build.props + + # Regex pattern to extract version info in a capturing group + # Version>([\d]+(.[\d]+)*)<\/\w*Version>$ - NET Core csproj regex + VERSION_REGEX: Version>([\d]+(.[\d]+)*)<\/\w*Version>$ + + # Useful with external providers like Nerdbank.GitVersioning, ignores VERSION_FILE_PATH & VERSION_REGEX + # VERSION_STATIC: 1.0.0 + + # Flag to toggle git tagging, enabled by default + TAG_COMMIT: false + + # Format of the git tag, [*] gets replaced with actual version + # TAG_FORMAT: v* + + # API key to authenticate with NuGet server + NUGET_KEY: ${{secrets.NUGET_API_KEY}} + + # NuGet server uri hosting the packages, defaults to https://api.nuget.org + # NUGET_SOURCE: https://api.nuget.org + + # Flag to toggle pushing symbols along with nuget package to the server, disabled by default + INCLUDE_SYMBOLS: false \ No newline at end of file diff --git a/.github/workflows/pr_unit_test.yml b/.github/workflows/pr_unit_test.yml index 8477935..3b86045 100644 --- a/.github/workflows/pr_unit_test.yml +++ b/.github/workflows/pr_unit_test.yml @@ -17,8 +17,8 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-dotnet@v3 + - uses: actions/checkout@v6 + - uses: actions/setup-dotnet@v5 with: dotnet-version: '6.x' - name: Launch tests diff --git a/.github/workflows/release_parallel.yml b/.github/workflows/release_parallel.yml index 893f95b..cc7557a 100644 --- a/.github/workflows/release_parallel.yml +++ b/.github/workflows/release_parallel.yml @@ -18,12 +18,21 @@ jobs: unit_tests: name: Unit tests runs-on: ubuntu-latest + outputs: + release_ver: ${{ steps.lib_proj_version.outputs.assembly-version }} steps: - - uses: actions/checkout@v3 - - uses: actions/setup-dotnet@v3 + - uses: actions/checkout@v6 + - uses: actions/setup-dotnet@v5 with: dotnet-version: '6.x' + + - name: Parse lib csproj version + id: lib_proj_version + uses: kzrnm/get-net-sdk-project-versions-action@v2 + with: + proj-path: SQLCipherED/SQLCipherED.csproj + - name: Launch tests run: dotnet test --nologo @@ -34,31 +43,22 @@ jobs: env: BUILD_ARCH: win-x64 BUILD_DIR: ./build/SQLCipherED.UI/Release/net6.0/win-x64 - BUILD_ARTIFACTS: SQLCipherED.dll runner-sqlcipher-ed.dll runner-sqlcipher-ed.exe runner-sqlcipher-ed.runtimeconfig.json ZIP_FILE: sqlcipher-ed_win-x64.zip - outputs: - release_ver: ${{ steps.app_proj_version.outputs.assembly-version }} steps: - - uses: actions/checkout@v3 - - uses: actions/setup-dotnet@v3 + - uses: actions/checkout@v6 + - uses: actions/setup-dotnet@v5 with: dotnet-version: '6.x' - - - name: Parse app csproj version - id: app_proj_version - uses: kzrnm/get-net-sdk-project-versions-action@v1 - with: - proj-path: SQLCipherED.UI/SQLCipherED.UI.csproj - name: Build CLI app - run: dotnet build SQLCipherED.UI/SQLCipherED.UI.csproj -r ${{env.BUILD_ARCH}} -c Release + run: dotnet build SQLCipherED.UI/SQLCipherED.UI.csproj -r ${{env.BUILD_ARCH}} -c Release --self-contained - name: Zip build - run: cd ${{env.BUILD_DIR}} && zip ${{env.ZIP_FILE}} ${{env.BUILD_ARTIFACTS}} + run: cd ${{env.BUILD_DIR}} && zip ${{env.ZIP_FILE}} ./ - name: Upload artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v6 with: name: app_${{env.BUILD_ARCH}} path: ${{env.BUILD_DIR}}/${{env.ZIP_FILE}} @@ -70,23 +70,22 @@ jobs: env: BUILD_ARCH: win-x86 BUILD_DIR: ./build/SQLCipherED.UI/Release/net6.0/win-x86 - BUILD_ARTIFACTS: SQLCipherED.dll runner-sqlcipher-ed.dll runner-sqlcipher-ed.exe runner-sqlcipher-ed.runtimeconfig.json ZIP_FILE: sqlcipher-ed_win-x86.zip steps: - - uses: actions/checkout@v3 - - uses: actions/setup-dotnet@v3 + - uses: actions/checkout@v6 + - uses: actions/setup-dotnet@v5 with: dotnet-version: '6.x' - name: Build CLI app - run: dotnet build SQLCipherED.UI/SQLCipherED.UI.csproj -r ${{env.BUILD_ARCH}} -c Release + run: dotnet build SQLCipherED.UI/SQLCipherED.UI.csproj -r ${{env.BUILD_ARCH}} -c Release --self-contained - name: Zip build - run: cd ${{env.BUILD_DIR}} && zip ${{env.ZIP_FILE}} ${{env.BUILD_ARTIFACTS}} + run: cd ${{env.BUILD_DIR}} && zip ${{env.ZIP_FILE}} ./ - name: Upload artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v6 with: name: app_${{env.BUILD_ARCH}} path: ${{env.BUILD_DIR}}/${{env.ZIP_FILE}} @@ -98,37 +97,62 @@ jobs: env: BUILD_ARCH: linux-x64 BUILD_DIR: ./build/SQLCipherED.UI/Release/net6.0/linux-x64 - #BUILD_ARTIFACTS: SQLCipherED.so runner-sqlcipher-ed.so runner-sqlcipher-ed runner-sqlcipher-ed.runtimeconfig.json - BUILD_ARTIFACTS: SQLCipherED.dll runner-sqlcipher-ed.dll runner-sqlcipher-ed runner-sqlcipher-ed.runtimeconfig.json ZIP_FILE: sqlcipher-ed_linux-x64.zip steps: - - uses: actions/checkout@v3 - - uses: actions/setup-dotnet@v3 + - uses: actions/checkout@v6 + - uses: actions/setup-dotnet@v5 with: dotnet-version: '6.x' - name: Build CLI app - run: dotnet build SQLCipherED.UI/SQLCipherED.UI.csproj -r ${{env.BUILD_ARCH}} -c Release - + run: dotnet build SQLCipherED.UI/SQLCipherED.UI.csproj -r ${{env.BUILD_ARCH}} -c Release --self-contained + - name: Zip build - run: cd ${{env.BUILD_DIR}} && zip ${{env.ZIP_FILE}} ${{env.BUILD_ARTIFACTS}} + run: cd ${{env.BUILD_DIR}} && zip ${{env.ZIP_FILE}} ./ - name: Upload artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v6 with: name: app_${{env.BUILD_ARCH}} path: ${{env.BUILD_DIR}}/${{env.ZIP_FILE}} + + build_nuget: + name: Build NuGet package + needs: unit_tests + runs-on: ubuntu-latest + env: + RELEASE_VERSION: ${{ needs.unit_tests.outputs.release_ver }} + BUILD_DIR: ./build/SQLCipherED/Release + + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-dotnet@v5 + with: + dotnet-version: | + 6.0.x + 8.0.x + + - name: Release build + run: dotnet build SQLCipherED/SQLCipherED.csproj -c Release + + - name: Upload artifact + uses: actions/upload-artifact@v6 + with: + name: nuget_artifact + path: ${{ env.BUILD_DIR }}/SQLCipherED.${{ env.RELEASE_VERSION }}.nupkg github_release_draft: name: Make Github release draft needs: + - unit_tests - build_win_x64 - build_win_x86 - build_linux_x64 + - build_nuget env: GITHUB_TOKEN: ${{ secrets.RELEASE_BUILD_TOKEN }} - RELEASE_VERSION: ${{ needs.build_win_x64.outputs.release_ver }} + RELEASE_VERSION: ${{ needs.unit_tests.outputs.release_ver }} outputs: upload_url: ${{ steps.create_release.outputs.upload_url }} runs-on: ubuntu-latest @@ -136,28 +160,25 @@ jobs: steps: - name: Create Release id: create_release - uses: actions/create-release@v1 + uses: softprops/action-gh-release@v2 with: tag_name: ${{ env.RELEASE_VERSION }} - release_name: ${{ github.event.repository.name }} ${{ env.RELEASE_VERSION }} + name: ${{ github.event.repository.name }} ${{ env.RELEASE_VERSION }} draft: true - github_upload_release_artifacts: - name: Upload build artifacts to GIthub release + github_upload_win_x64: + name: Upload Windows x64 build artifact to Github release needs: + - unit_tests - github_release_draft - - build_win_x64 - - build_win_x86 - - build_linux_x64 env: GITHUB_TOKEN: ${{ secrets.RELEASE_BUILD_TOKEN }} - RELEASE_VERSION: ${{ needs.build_win_x64.outputs.release_ver }} UPLOAD_URL: ${{ needs.github_release_draft.outputs.upload_url }} runs-on: ubuntu-latest steps: - name: Download win-x64 artifact - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v4 with: name: app_win-x64 @@ -166,11 +187,22 @@ jobs: with: upload_url: ${{ env.UPLOAD_URL }} asset_path: sqlcipher-ed_win-x64.zip - asset_name: ${{ github.event.repository.name }} ${{ env.RELEASE_VERSION }}_win-x64.zip + asset_name: ${{ github.event.repository.name }}_win-x64.zip asset_content_type: application/zip - + + github_upload_win_x86: + name: Upload Windows x86 build artifact to Github release + needs: + - unit_tests + - github_release_draft + env: + GITHUB_TOKEN: ${{ secrets.RELEASE_BUILD_TOKEN }} + UPLOAD_URL: ${{ needs.github_release_draft.outputs.upload_url }} + runs-on: ubuntu-latest + + steps: - name: Download win-x86 artifact - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v4 with: name: app_win-x86 @@ -179,11 +211,23 @@ jobs: with: upload_url: ${{ env.UPLOAD_URL }} asset_path: sqlcipher-ed_win-x86.zip - asset_name: ${{ github.event.repository.name }} ${{ env.RELEASE_VERSION }}_win-x86.zip + asset_name: ${{ github.event.repository.name }}_win-x86.zip asset_content_type: application/zip - + + github_upload_linux_x64: + name: Upload Linux x64 build artifacts (self-contained, AppImage) to Github release + needs: + - unit_tests + - github_release_draft + env: + GITHUB_TOKEN: ${{ secrets.RELEASE_BUILD_TOKEN }} + RELEASE_VERSION: ${{ needs.unit_tests.outputs.release_ver }} + UPLOAD_URL: ${{ needs.github_release_draft.outputs.upload_url }} + runs-on: ubuntu-latest + + steps: - name: Download linux-x64 artifact - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v4 with: name: app_linux-x64 @@ -193,4 +237,29 @@ jobs: upload_url: ${{ env.UPLOAD_URL }} asset_path: sqlcipher-ed_linux-x64.zip asset_name: ${{ github.event.repository.name }} ${{ env.RELEASE_VERSION }}_linux-x64.zip + asset_content_type: application/zip + + github_upload_nuget: + name: Upload NuGet build artifact to Github release + needs: + - unit_tests + - github_release_draft + env: + GITHUB_TOKEN: ${{ secrets.RELEASE_BUILD_TOKEN }} + RELEASE_VERSION: ${{ needs.unit_tests.outputs.release_ver }} + UPLOAD_URL: ${{ needs.github_release_draft.outputs.upload_url }} + runs-on: ubuntu-latest + + steps: + - name: Download NuGet artifact + uses: actions/download-artifact@v7 + with: + name: nuget_artifact + + - name: Upload NuGet artifact to release assets + uses: actions/upload-release-asset@v1.0.1 + with: + upload_url: ${{ env.UPLOAD_URL }} + asset_path: SQLCipherED.${{ env.RELEASE_VERSION }}.nupkg + asset_name: SQLCipherED.${{ env.RELEASE_VERSION }}.nupkg asset_content_type: application/zip \ No newline at end of file diff --git a/LICENSE b/LICENSE index c6aa90d..d184441 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 SQLCipherED +Copyright (c) 2023-2026 mIwr Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/SQLCipherED.Tests/Directory.Build.props b/SQLCipherED.Tests/Directory.Build.props new file mode 100644 index 0000000..475b477 --- /dev/null +++ b/SQLCipherED.Tests/Directory.Build.props @@ -0,0 +1,5 @@ + + + ../build/$(MSBuildProjectName)/obj/ + + \ No newline at end of file diff --git a/SQLCipherED.Tests/SQLCipherED.Tests.csproj b/SQLCipherED.Tests/SQLCipherED.Tests.csproj index 4c65ab8..f82124a 100644 --- a/SQLCipherED.Tests/SQLCipherED.Tests.csproj +++ b/SQLCipherED.Tests/SQLCipherED.Tests.csproj @@ -11,23 +11,7 @@ ../build/SQLCipherED.Tests - x64;x86 - - - - 7 - - - - 7 - - - - 7 - - - - 7 + AnyCPU diff --git a/SQLCipherED.UI/Directory.Build.props b/SQLCipherED.UI/Directory.Build.props new file mode 100644 index 0000000..475b477 --- /dev/null +++ b/SQLCipherED.UI/Directory.Build.props @@ -0,0 +1,5 @@ + + + ../build/$(MSBuildProjectName)/obj/ + + \ No newline at end of file diff --git a/SQLCipherED.UI/Program.cs b/SQLCipherED.UI/Program.cs index 6984cfc..04d86cf 100644 --- a/SQLCipherED.UI/Program.cs +++ b/SQLCipherED.UI/Program.cs @@ -31,34 +31,37 @@ class Program static void Main(string[] args) { - var parseStatus = ProcessArgs(args); - if (parseStatus) - { - var sqlcipherCryptEng = new SQLCipherCryptEng(_kdfIter, _kdfAlgo, _pageSize, _kdfAlgo); - var reader = new BinaryReader(File.OpenRead(_sourcePath)); - var outData = Array.Empty(); - Console.WriteLine("Decrypting '" + _sourcePath + '\''); - try + if (!ProcessArgs(args)) + { +#if DEBUG + Console.ReadKey(); +#endif + return; + } + var sqlcipherCryptEng = new SQLCipherCryptEng(_kdfIter, _kdfAlgo, _pageSize, _kdfAlgo); + var reader = new BinaryReader(File.OpenRead(_sourcePath)); + var outData = Array.Empty(); + Console.WriteLine("Decrypting '" + _sourcePath + '\''); + try + { + if (_hexKey.Length != 0) { - if (_hexKey.Length != 0) - { - outData = sqlcipherCryptEng.Decode(reader, _hexKey); - } - else - { - outData = sqlcipherCryptEng.Decode(reader, _passphrase); - } - Console.WriteLine("Done"); + outData = sqlcipherCryptEng.Decode(reader, _hexKey); } - catch (Exception ex) - { - Console.WriteLine("Error: " + ex.Message); - } - reader.Close(); - if (outData.Length != 0) + else { - File.WriteAllBytes(_outPath, outData); - } + outData = sqlcipherCryptEng.Decode(reader, _passphrase); + } + Console.WriteLine("Done"); + } + catch (Exception ex) + { + Console.WriteLine("Error: " + ex.Message); + } + reader.Close(); + if (outData.Length != 0) + { + File.WriteAllBytes(_outPath, outData); } #if DEBUG Console.ReadKey(); diff --git a/SQLCipherED.UI/SQLCipherED.UI.csproj b/SQLCipherED.UI/SQLCipherED.UI.csproj index 102ce1a..e994288 100644 --- a/SQLCipherED.UI/SQLCipherED.UI.csproj +++ b/SQLCipherED.UI/SQLCipherED.UI.csproj @@ -5,31 +5,15 @@ net6.0 disable enable - x64;x86 + AnyCPU SQLCipherED.UI.Program runner-sqlcipher-ed ../build/SQLCipherED.UI - 0.3.1.3 - 0.3.1.3 + 1.0.0 + 1.0.0 False - - 7 - - - - 7 - - - - 7 - - - - 7 - - diff --git a/SQLCipherED.sln b/SQLCipherED.sln index 2d9188e..aa332da 100644 --- a/SQLCipherED.sln +++ b/SQLCipherED.sln @@ -11,36 +11,22 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SQLCipherED.Tests", "SQLCip EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 - Release|x64 = Release|x64 - Release|x86 = Release|x86 + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {F3E5C644-DD0C-4002-87AE-631273CE9DFD}.Debug|x64.ActiveCfg = Debug|x64 - {F3E5C644-DD0C-4002-87AE-631273CE9DFD}.Debug|x64.Build.0 = Debug|x64 - {F3E5C644-DD0C-4002-87AE-631273CE9DFD}.Debug|x86.ActiveCfg = Debug|x86 - {F3E5C644-DD0C-4002-87AE-631273CE9DFD}.Debug|x86.Build.0 = Debug|x86 - {F3E5C644-DD0C-4002-87AE-631273CE9DFD}.Release|x64.ActiveCfg = Release|x64 - {F3E5C644-DD0C-4002-87AE-631273CE9DFD}.Release|x64.Build.0 = Release|x64 - {F3E5C644-DD0C-4002-87AE-631273CE9DFD}.Release|x86.ActiveCfg = Release|x86 - {F3E5C644-DD0C-4002-87AE-631273CE9DFD}.Release|x86.Build.0 = Release|x86 - {EF1B7284-3B04-48F4-865D-71E2112F5DEB}.Debug|x64.ActiveCfg = Debug|x64 - {EF1B7284-3B04-48F4-865D-71E2112F5DEB}.Debug|x64.Build.0 = Debug|x64 - {EF1B7284-3B04-48F4-865D-71E2112F5DEB}.Debug|x86.ActiveCfg = Debug|x86 - {EF1B7284-3B04-48F4-865D-71E2112F5DEB}.Debug|x86.Build.0 = Debug|x86 - {EF1B7284-3B04-48F4-865D-71E2112F5DEB}.Release|x64.ActiveCfg = Release|x64 - {EF1B7284-3B04-48F4-865D-71E2112F5DEB}.Release|x64.Build.0 = Release|x64 - {EF1B7284-3B04-48F4-865D-71E2112F5DEB}.Release|x86.ActiveCfg = Release|x86 - {EF1B7284-3B04-48F4-865D-71E2112F5DEB}.Release|x86.Build.0 = Release|x86 - {8633AC27-412E-4DF9-B482-DF6891BC09AA}.Debug|x64.ActiveCfg = Debug|x64 - {8633AC27-412E-4DF9-B482-DF6891BC09AA}.Debug|x64.Build.0 = Debug|x64 - {8633AC27-412E-4DF9-B482-DF6891BC09AA}.Debug|x86.ActiveCfg = Debug|x86 - {8633AC27-412E-4DF9-B482-DF6891BC09AA}.Debug|x86.Build.0 = Debug|x86 - {8633AC27-412E-4DF9-B482-DF6891BC09AA}.Release|x64.ActiveCfg = Release|x64 - {8633AC27-412E-4DF9-B482-DF6891BC09AA}.Release|x64.Build.0 = Release|x64 - {8633AC27-412E-4DF9-B482-DF6891BC09AA}.Release|x86.ActiveCfg = Release|x86 - {8633AC27-412E-4DF9-B482-DF6891BC09AA}.Release|x86.Build.0 = Release|x86 + {F3E5C644-DD0C-4002-87AE-631273CE9DFD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F3E5C644-DD0C-4002-87AE-631273CE9DFD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F3E5C644-DD0C-4002-87AE-631273CE9DFD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F3E5C644-DD0C-4002-87AE-631273CE9DFD}.Release|Any CPU.Build.0 = Release|Any CPU + {EF1B7284-3B04-48F4-865D-71E2112F5DEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EF1B7284-3B04-48F4-865D-71E2112F5DEB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EF1B7284-3B04-48F4-865D-71E2112F5DEB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EF1B7284-3B04-48F4-865D-71E2112F5DEB}.Release|Any CPU.Build.0 = Release|Any CPU + {8633AC27-412E-4DF9-B482-DF6891BC09AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8633AC27-412E-4DF9-B482-DF6891BC09AA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8633AC27-412E-4DF9-B482-DF6891BC09AA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8633AC27-412E-4DF9-B482-DF6891BC09AA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/SQLCipherED/Constants.cs b/SQLCipherED/Constants.cs index 0b6a002..1c5fcbb 100644 --- a/SQLCipherED/Constants.cs +++ b/SQLCipherED/Constants.cs @@ -1,10 +1,25 @@ namespace SQLCipherED { + /// + /// SQLCipher constants + /// internal static class Constants - { + { + /// + /// Plain-text SQLite DB magic header + /// internal const string SQLiteHeader = "SQLite format 3\0"; + /// + /// SQLCipher salt size in bytes + /// internal const byte SaltSize = 16; + /// + /// SQLCipher AES IV size in bytes + /// internal const byte IVSize = 16; + /// + /// SQLCipher decrptyion key size in bytes + /// internal const byte KeySize = 32; } } diff --git a/SQLCipherED/Directory.Build.props b/SQLCipherED/Directory.Build.props new file mode 100644 index 0000000..475b477 --- /dev/null +++ b/SQLCipherED/Directory.Build.props @@ -0,0 +1,5 @@ + + + ../build/$(MSBuildProjectName)/obj/ + + \ No newline at end of file diff --git a/SQLCipherED/SQLCipherCryptEng.cs b/SQLCipherED/SQLCipherCryptEng.cs index 07cde8f..1568f1b 100644 --- a/SQLCipherED/SQLCipherCryptEng.cs +++ b/SQLCipherED/SQLCipherCryptEng.cs @@ -84,7 +84,14 @@ public SQLCipherCryptEng(SQLCipherStandard standard) KdfIter = standard.KdfIter(); PbkdfAlgo = standard.PbkdfAlgo(); PageSize = standard.PageSize(); - HmacPbkdfAlgo = standard == SQLCipherStandard.V1 ? null : standard.PbkdfAlgo(); + if (standard == SQLCipherStandard.V1) + { + HmacPbkdfAlgo = null; + } + else + { + HmacPbkdfAlgo = standard.PbkdfAlgo(); + } HmacKdfIter = standard.FastKdfIter(); HmacSaltMask = standard.HmacSaltMask(); } @@ -95,6 +102,7 @@ public SQLCipherCryptEng(SQLCipherStandard standard) /// Key derivation iterations count /// Key derivation algo /// SQLCipher db page size + /// .NET hash algo name /// /// /// @@ -108,7 +116,7 @@ public SQLCipherCryptEng(int kdfIter, HashAlgorithmName kdfAlgo, ushort pageSize { throw new ArgumentException(message: "Incorrect HMAC KDF iterations value, must be at least 1"); } - var logVal = Math.Log2(pageSize); + var logVal = Math.Log(pageSize, newBase: 2); var delta = logVal - Math.Truncate(logVal); if (pageSize < 512 || delta != 0.0) { @@ -329,7 +337,8 @@ public byte[] Decode(BinaryReader sqlcipher, byte[] keyBytes) } var decodedWriter = new MemoryStream(); - decodedWriter.Write(Encoding.UTF8.GetBytes(Constants.SQLiteHeader)); + var bytes = Encoding.UTF8.GetBytes(Constants.SQLiteHeader); + decodedWriter.Write(bytes, offset: 0, count: bytes.Length); var buffer = new byte[PageSize]; int readCount; //Decrypt the custom 1st page @@ -341,8 +350,8 @@ public byte[] Decode(BinaryReader sqlcipher, byte[] keyBytes) if (finalBlock.Length != 0) { decodedWriter.Write(finalBlock, offset: 0, finalBlock.Length); - } - decodedWriter.Write(new byte[PageSize - readCount - Constants.SaltSize - finalBlock.Length]); + } + decodedWriter.Write(new byte[PageSize - readCount - Constants.SaltSize - finalBlock.Length], offset: 0, count: PageSize - readCount - Constants.SaltSize - finalBlock.Length); //Decrypt the next pages while (sqlcipher.BaseStream.Position < sqlcipher.BaseStream.Length) @@ -373,7 +382,7 @@ public byte[] Decode(BinaryReader sqlcipher, byte[] keyBytes) { decodedWriter.Write(finalBlock, offset: 0, finalBlock.Length); } - decodedWriter.Write(new byte[PageSize - readCount - finalBlock.Length]); + decodedWriter.Write(new byte[PageSize - readCount - finalBlock.Length], offset: 0, count: PageSize - readCount - finalBlock.Length); } var decrypted = decodedWriter.ToArray(); decodedWriter.Close(); diff --git a/SQLCipherED/SQLCipherED.csproj b/SQLCipherED/SQLCipherED.csproj index d85d9fc..9eb6f6d 100644 --- a/SQLCipherED/SQLCipherED.csproj +++ b/SQLCipherED/SQLCipherED.csproj @@ -1,15 +1,14 @@  - net6.0 + netstandard2.0;netstandard2.1;net6.0;net8.0 disable - enable - x64;x86 + AnyCPU ../build/SQLCipherED - False + True SQLCipher DB Decrypt - 0.3.1.3 - 0.3.1.3 + 0.3.3 + 0.3.3 LICENSE True $(AssemblyVersion) @@ -17,25 +16,12 @@ https://github.com/mIwr/SQLCipherED README.md https://github.com/mIwr/SQLCipherED - Github + 6.0 False - - - - 7 - - - - 7 - - - - 7 - - - - 7 + True + mIwr + AnyCPU diff --git a/SQLCipherED/SQLCipherHashAlgo.cs b/SQLCipherED/SQLCipherHashAlgo.cs index 3fcdb97..bf95c8c 100644 --- a/SQLCipherED/SQLCipherHashAlgo.cs +++ b/SQLCipherED/SQLCipherHashAlgo.cs @@ -2,13 +2,35 @@ namespace SQLCipherED { + /// + /// SQLCipher hash algo + /// public enum SQLCipherHashAlgo: byte { - SHA1, SHA256, SHA512 + /// + /// SHA1 + /// + SHA1, + /// + /// SHA256 + /// + SHA256, + /// + /// SHA512 + /// + SHA512 } + /// + /// SQLCipher hash algo extension + /// public static class SQLCipherHashAlgoExt { + /// + /// Tries to parse SQLCipher hash algo from .NET hash algo + /// + /// .NET hash algo name + /// public static SQLCipherHashAlgo? From(HashAlgorithmName algName) { var name = algName.Name ?? string.Empty; @@ -28,6 +50,11 @@ public static class SQLCipherHashAlgoExt return null; } + /// + /// .NET hash algo + /// + /// SQLCipher hash algo + /// public static HashAlgorithmName HashAlg(this SQLCipherHashAlgo algo) { switch (algo) @@ -40,6 +67,11 @@ public static HashAlgorithmName HashAlg(this SQLCipherHashAlgo algo) return HashAlgorithmName.SHA1; } + /// + /// Hash size + /// + /// SQLCipher hash algo + /// public static byte Size(this SQLCipherHashAlgo algo) { switch (algo) diff --git a/SQLCipherED/SQLCipherStandard.cs b/SQLCipherED/SQLCipherStandard.cs index a311cff..a4aedd6 100644 --- a/SQLCipherED/SQLCipherStandard.cs +++ b/SQLCipherED/SQLCipherStandard.cs @@ -9,9 +9,27 @@ namespace SQLCipherED /// public enum SQLCipherStandard : byte { - V1 = 1, V2 = 2, V3 = 3, V4 = 4 + /// + /// Revision 1 - no tag (0 bytes) + /// + V1 = 1, + /// + /// Revision 2 - SHA1 tag (20 bytes) + /// + V2 = 2, + /// + /// Revision 3 - SHA1 tag (20 bytes) + /// + V3 = 3, + /// + /// Revision 4 - SHA512 (64 bytes) or optionally SHA256 (32 bytes) tag + /// + V4 = 4 } + /// + /// Default SQLCipher settings extension + /// public static class SQLCipherStandardExt { internal const byte SHA1Size = 20; @@ -28,22 +46,30 @@ public static class SQLCipherStandardExt internal const byte FastPbkdf2Iter = 2; internal const byte DefaultHmacSaltMask = 0x3a; + /// + /// Tries to parse SQLCipher revision standard from number + /// + /// Version number + /// SQLCipher revision standard. Optional public static SQLCipherStandard? From(byte versionNumber) - { - var values = Enum.GetValues(); - foreach (var enumItem in values) + { + switch(versionNumber) { - if ((byte)enumItem != versionNumber) - { - continue; - } - return enumItem; + case 1: return SQLCipherStandard.V1; + case 2: return SQLCipherStandard.V2; + case 3: return SQLCipherStandard.V3; + case 4: return SQLCipherStandard.V4; } return null; } - public static int KdfIter (this SQLCipherStandard standard) + /// + /// Default key derivation iterations count + /// + /// SQLCipher revision standard + /// + public static int KdfIter(this SQLCipherStandard standard) { switch(standard) { @@ -53,11 +79,21 @@ public static int KdfIter (this SQLCipherStandard standard) } } + /// + /// Default fast key derivation iterations count + /// + /// SQLCipher revision standard + /// public static int FastKdfIter(this SQLCipherStandard standard) { return FastPbkdf2Iter; } + /// + /// Default key derivation .NET hash algo + /// + /// SQLCipher revision standard + /// public static HashAlgorithmName PbkdfAlgoName(this SQLCipherStandard standard) { switch (standard) @@ -67,6 +103,11 @@ public static HashAlgorithmName PbkdfAlgoName(this SQLCipherStandard standard) } } + /// + /// Default key derivation SQLCipher hash algo + /// + /// SQLCipher revision standard + /// public static SQLCipherHashAlgo PbkdfAlgo(this SQLCipherStandard standard) { switch(standard) @@ -76,6 +117,11 @@ public static SQLCipherHashAlgo PbkdfAlgo(this SQLCipherStandard standard) } } + /// + /// Default SQLCipher data page size in bytes + /// + /// SQLCipher revision standard + /// public static ushort PageSize(this SQLCipherStandard standard) { switch(standard) @@ -85,6 +131,11 @@ public static ushort PageSize(this SQLCipherStandard standard) } } + /// + /// SQLCipher HMAC salt mask magic byte + /// + /// SQLCipher revision standard + /// public static byte HmacSaltMask(this SQLCipherStandard standard) { return DefaultHmacSaltMask; diff --git a/SQLCipherED/SQLCipherUtil.cs b/SQLCipherED/SQLCipherUtil.cs index 846c843..38cc12a 100644 --- a/SQLCipherED/SQLCipherUtil.cs +++ b/SQLCipherED/SQLCipherUtil.cs @@ -4,8 +4,16 @@ namespace SQLCipherED { + /// + /// SQLCipher utils + /// public static class SQLCipherUtil { + /// + /// Checks the database bytes (encrypted/decrypted) + /// + /// Raw DB bytes + /// Returns False, if the database in hex starts from magic header, otherwise - True public static bool IsEncrypted(byte[] sqlcipherBytes) { if (sqlcipherBytes.Length < Constants.SQLiteHeader.Length) @@ -26,31 +34,77 @@ public static bool IsEncrypted(byte[] sqlcipherBytes) return false; } + /// + /// Checks the data base bytes (encrypted/decrypted) + /// + /// Raw DB bytes + /// >Returns True, if the database in hex starts from magic header, otherwise - False public static bool IsDecrypted(byte[] sqlcipherBytes) { return !IsEncrypted(sqlcipherBytes); } + /// + /// Generates randomized salt for SQLCipher encryption/decryption ops + /// + /// Salt bytes public static byte[] GenerateSalt() { - var salt = RandomNumberGenerator.GetBytes(Constants.SaltSize); + var rnd = RandomNumberGenerator.Create(); + var salt = new byte[Constants.SaltSize]; + rnd.GetBytes(salt); return salt; } + /// + /// Generates SQLCipher encryption/decryption key + /// + /// Salt + /// Passphrase bytes + /// Result key derrivation iterations count + /// Result key derrivation hash algo + /// Result key size + /// public static byte[] GenerateKey(byte[] salt, byte[] passBytes, int kdfIter, HashAlgorithmName hashAlgo, byte keySize) { +#if NET var key = Rfc2898DeriveBytes.Pbkdf2(passBytes, salt, kdfIter, hashAlgo, keySize); - +#else + var key = PBKDF2.DeriveBytes(hashAlgo.Name, salt, passBytes, kdfIter, keySize); +#endif return key; } + /// + /// Generates SQLCipher encryption/decryption key + /// + /// Salt + /// Passhrase string + /// Result key derrivation iterations count + /// Result key derrivation hash algo + /// Result key size + /// public static byte[] GenerateKey(byte[] salt, string passphrase, int kdfIter, HashAlgorithmName hashAlgo, byte keySize) { +#if NET var key = Rfc2898DeriveBytes.Pbkdf2(passphrase, salt, kdfIter, hashAlgo, keySize); - +#else + var passBytes = Encoding.UTF8.GetBytes(passphrase); + var key = PBKDF2.DeriveBytes(hashAlgo.Name, salt, passBytes, kdfIter, keySize); +#endif return key; } + /// + /// Generates SQLCipher data page HMAC + /// + /// HMAC key + /// Page data raw bytes + /// Page index + /// IV + HMAC (if exists) bytes count at the end of page + /// SQLCipher HMAC algo + /// Returns generated HMAC of the page data + /// Throws, when unable to convert SQLCipher hash algo to .NET hash algo public static byte[] GeneratePageHMAC(byte[] hmacKey, byte[] pageBytes, int sqlCipherPageIndex, int reserveSize, SQLCipherHashAlgo hmacAlgo) { HMAC hmac; @@ -79,6 +133,15 @@ public static byte[] GeneratePageHMAC(byte[] hmacKey, byte[] pageBytes, int sqlC return calculatedHmac; } + /// + /// Compares generated and stock checksums of the page + /// + /// HMAC key + /// Raw page bytes + /// Page index + /// IV + HMAC (if exists) bytes count at the end of page + /// SQLCipher HMAC algo + /// Returns True, if generated and stock checksums are equal, which means correct decryption. Otherwise returns False public static bool CheckPageHMAC(byte[] hmacKey, byte[] pageBytes, int sqlCipherPageIndex, int reserveSize, SQLCipherHashAlgo hmacAlgo) { var pageHmac = new byte[hmacAlgo.Size()]; @@ -102,4 +165,171 @@ public static bool CheckPageHMAC(byte[] hmacKey, byte[] pageBytes, int sqlCipher return true; } } + +#if !NET + #region .NET Standard PBKDF2 + /// + /// Provides an easy to understand PBKDF2 implementation. + /// Note: This will create correct values, but it's very slow. + /// In security critical applications you want to use instead. + /// + internal static class PBKDF2 + { + /// + /// Derives bytes using PBKDF2 + /// + /// + /// This is the name of the hash function. Usually SHA1 + /// but can be any other such as (but not exhaustively): + /// SHA256, SHA512, RIPEMD160. + /// Any algorithm that has a matching HMAC function will work. + /// + /// + /// Salt for the function. + /// This should be randomly generated and be between 16 and 32 bytes. + /// You need to store this somewhere if you want to create the same hash later. + /// + /// + /// Key for the function. + /// If your key is a string, use Encoding.UTF8.GetBytes(string) to convert it into a byte array. + /// + /// + /// The number of iterations. + /// Normally you want this to be 100'000 or more, + /// but this implementation is not made to be fast, but easy to understand. + /// Keep it around 1'000 - 10'000 for this demo. + /// + /// + /// The number of bytes you want to get out of this function + /// + /// + /// Randomly looking but deterministic bytes + /// + internal static byte[] DeriveBytes(string rngFunction, byte[] salt, byte[] password, int iterations, int byteCount) + { + #region Validation + //Make sure something was specified + if (string.IsNullOrEmpty(rngFunction)) + { + throw new ArgumentException($"'{nameof(rngFunction)}' cannot be null or empty.", nameof(rngFunction)); + } + //Cannot use null as salt + if (salt is null) + { + throw new ArgumentNullException(nameof(salt)); + } + //Cannot use null as password either + if (password is null) + { + throw new ArgumentNullException(nameof(password)); + } + //Require at least one iteration + if (iterations < 1) + { + throw new ArgumentOutOfRangeException(nameof(iterations)); + } + //Require at least one output byte + if (byteCount < 1) + { + throw new ArgumentOutOfRangeException(nameof(byteCount)); + } + #endregion + + //Create the HMAC of the supplied algorithm + var keyHash = HMAC.Create("HMAC" + rngFunction); + if (keyHash == null) + { + return Array.Empty(); + } + + //Size of the algorithm in bytes. + //Note that sizes in cryptographic algorithms are often reported in bits, not bytes, + //hence why we divide the value by 8 + var size = keyHash.HashSize / 8; + + using (keyHash) + { + //Number of hash blocks we need to fit the requested byte count + var BlockCount = byteCount / size; + //If there is a remainder, we need to add one extra block + //For example with SHA1 (block size 20 bytes) and 30 bytes of data: + //30/20=1 (integer division always rounds down) + //30%20=10 (remainder is not zero, so increase block size by one) + //Final size: 2 blocks + //If you prefer this in one line: + //BlockCount = ByteCount / Size + (ByteCount % Size == 0 ? 0 : 1); + if (byteCount % size != 0) + { + ++BlockCount; + } + //This holds the final output data + byte[] Output = new byte[byteCount]; + //Set key + keyHash.Key = password; + //Hash as many blocks as needed + for (var BlockIndex = 1; BlockIndex <= BlockCount; BlockIndex++) + { + //Hash a block of data + var Round = HashBlock(keyHash, salt, BlockIndex, iterations); + //The location of where the data goes in the output + //This basically starts each output at the end of the previous one + var BlockOffset = (BlockIndex - 1) * size; + //Normally we copy all bytes but the last chunk may be smaller + var BytesToCopy = Math.Min(Round.Length, byteCount - BlockOffset); + Array.Copy(Round, 0, Output, BlockOffset, BytesToCopy); + } + return Output; + } + } + + /// + /// Hashes a block of output data + /// + /// Hash algorithm + /// Salt value + /// Block index (starts at 1, not 0) + /// Number of iterations + /// Hashed block data + private static byte[] HashBlock(KeyedHashAlgorithm Hasher, byte[] Salt, int BlockIndex, int IterationCount) + { + //First round is special by additionally using the block index in the input + byte[] Data = Hasher.ComputeHash(GetFirstBlockData(Salt, BlockIndex)); + //Holds the final result + byte[] Result = (byte[])Data.Clone(); + //rounds 2 to IterationCount use the result "Data" of the previous run + for (var i = 2; i <= IterationCount; i++) + { + byte[] Temp = Hasher.ComputeHash(Data); + //The result is XOR combined with the input data + for (var j = 0; j < Temp.Length; j++) + { + Result[j] ^= Temp[j]; + } + //Use the last result as the data for the next iteration + Data = Temp; + } + return Result; + } + + /// + /// Gets the value used for the first block hash + /// + /// Salt + /// Block index (starts at 1, not 0) + /// value for first iteration of the hasher + private static byte[] GetFirstBlockData(byte[] Salt, int BlockIndex) + { + var Data = new byte[Salt.Length + 4]; + //Copy key into the buffer + Array.Copy(Salt, Data, Salt.Length); + //Append the BlockIndex as 32 bit big endian integer + Data[Salt.Length] = (byte)(BlockIndex >> 24); + Data[Salt.Length + 1] = (byte)(BlockIndex >> 16 & 0xFF); + Data[Salt.Length + 2] = (byte)(BlockIndex >> 8 & 0xFF); + Data[Salt.Length + 3] = (byte)(BlockIndex & 0xFF); + return Data; + } + } + #endregion +#endif } \ No newline at end of file