From dfae955d044dac35b846050cbb9f5e9a1c55c44f Mon Sep 17 00:00:00 2001 From: Georgios Smyrlis Date: Wed, 17 Jun 2026 23:01:39 +0200 Subject: [PATCH 1/2] Add CI workflow and v1 roadmap --- .github/dependabot.yml | 27 +++++++++++++++++++ .github/workflows/ci.yml | 57 ++++++++++++++++++++++++++++++++++++++++ README.md | 2 ++ 3 files changed, 86 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..50c035e --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,27 @@ +version: 2 + +updates: + # .NET / NuGet package references across the solution. + - package-ecosystem: nuget + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 5 + # Bundle minor and patch bumps into a single weekly PR to cut noise; major bumps stay + # individual so a potentially breaking update gets its own review. + groups: + nuget-minor-and-patch: + update-types: + - minor + - patch + + # GitHub Actions used by the workflows. Few and low-risk, so group them into one PR. + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 5 + groups: + github-actions: + patterns: + - "*" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5192170 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,57 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +# Least privilege: the build only needs to read the repository. No publishing, no secrets. +permissions: + contents: read + +# Cancel superseded runs on the same ref to save CI minutes. +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-test: + name: Build & Test + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET 8 + uses: actions/setup-dotnet@v4 + with: + # Floats to the latest 8.0 patch (picks up SDK security fixes) while staying on the + # net8.0 target line the projects build against. No global.json to maintain. + dotnet-version: 8.0.x + + - name: Restore + run: dotnet restore + + - name: Build + run: dotnet build -c Release --no-restore + + - name: Test + run: dotnet test -c Release --no-build + + # Pack runs only on push to main: it validates packaging (the bundled netstandard2.0 analyzer + # and package validation) without publishing anything. Release is required because the EF Core + # package picks up the analyzer DLL from its bin/Release output. + - name: Pack (validation only, no publish) + if: github.event_name == 'push' + run: dotnet pack -c Release --no-build --output artifacts + + - name: Upload packages + if: github.event_name == 'push' + uses: actions/upload-artifact@v4 + with: + name: nuget-packages + path: | + artifacts/*.nupkg + artifacts/*.snupkg + if-no-files-found: error diff --git a/README.md b/README.md index db438f7..243469a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Proteos — Application Layer Encryption for .NET +[![CI](https://github.com/ProteosEncryption/Proteos.Encryption/actions/workflows/ci.yml/badge.svg)](https://github.com/ProteosEncryption/Proteos.Encryption/actions/workflows/ci.yml) + Encrypt sensitive entity fields **in the application**, before they reach the database, blob storage or any cloud system. A database, backup or storage leak then exposes only ciphertext — not your customers' data. From fd503818911a7d8cb09debb29b60c3c8b961ab99 Mon Sep 17 00:00:00 2001 From: Georgios Smyrlis Date: Fri, 19 Jun 2026 10:53:52 +0200 Subject: [PATCH 2/2] Narrow EF public surface before v1 --- .../DecryptingMaterializationInterceptor.cs | 2 +- .../EncryptingSaveChangesInterceptor.cs | 2 +- .../Metadata/BlindIndexNormalizerResolver.cs | 6 +++--- .../Migration/EncryptionMigrationPlanner.cs | 2 +- .../Migration/EncryptionMigrationService.cs | 2 +- ...roteos.Encryption.EntityFrameworkCore.csproj | 4 ++++ .../PublicAPI.Shipped.txt | 17 ----------------- .../AwsKmsKeyProviderTests.cs | 2 +- .../AzureKeyVaultKeyProviderTests.cs | 2 +- 9 files changed, 13 insertions(+), 26 deletions(-) diff --git a/src/Proteos.Encryption.EntityFrameworkCore/Interceptors/DecryptingMaterializationInterceptor.cs b/src/Proteos.Encryption.EntityFrameworkCore/Interceptors/DecryptingMaterializationInterceptor.cs index 0702fd4..c96c3d7 100644 --- a/src/Proteos.Encryption.EntityFrameworkCore/Interceptors/DecryptingMaterializationInterceptor.cs +++ b/src/Proteos.Encryption.EntityFrameworkCore/Interceptors/DecryptingMaterializationInterceptor.cs @@ -14,7 +14,7 @@ namespace Proteos.Encryption.EntityFrameworkCore; /// Decryption happens before the change-tracking snapshot is taken, so the decrypted values become /// the entity's original values and a freshly loaded entity is not seen as modified. /// -public sealed class DecryptingMaterializationInterceptor : IMaterializationInterceptor +internal sealed class DecryptingMaterializationInterceptor : IMaterializationInterceptor { private readonly ITenantResolver _tenantResolver; private readonly AesGcmValueEncryptionService _encryptionService; diff --git a/src/Proteos.Encryption.EntityFrameworkCore/Interceptors/EncryptingSaveChangesInterceptor.cs b/src/Proteos.Encryption.EntityFrameworkCore/Interceptors/EncryptingSaveChangesInterceptor.cs index c75590b..7fe760e 100644 --- a/src/Proteos.Encryption.EntityFrameworkCore/Interceptors/EncryptingSaveChangesInterceptor.cs +++ b/src/Proteos.Encryption.EntityFrameworkCore/Interceptors/EncryptingSaveChangesInterceptor.cs @@ -15,7 +15,7 @@ namespace Proteos.Encryption.EntityFrameworkCore; /// later. Encryption happens only for added entities and for the actually-modified properties of /// modified entities, so calling SaveChanges twice never produces ciphertext of ciphertext. /// -public sealed class EncryptingSaveChangesInterceptor : SaveChangesInterceptor +internal sealed class EncryptingSaveChangesInterceptor : SaveChangesInterceptor { private readonly ITenantResolver _tenantResolver; private readonly AesGcmValueEncryptionService _encryptionService; diff --git a/src/Proteos.Encryption.EntityFrameworkCore/Metadata/BlindIndexNormalizerResolver.cs b/src/Proteos.Encryption.EntityFrameworkCore/Metadata/BlindIndexNormalizerResolver.cs index f15a5da..c78f1be 100644 --- a/src/Proteos.Encryption.EntityFrameworkCore/Metadata/BlindIndexNormalizerResolver.cs +++ b/src/Proteos.Encryption.EntityFrameworkCore/Metadata/BlindIndexNormalizerResolver.cs @@ -3,10 +3,10 @@ namespace Proteos.Encryption.EntityFrameworkCore; /// -/// Maps a to the concrete crypto-core normalizer. This is -/// the stable bridge the later EF Core integration will use; it performs no EF runtime work. +/// Maps a to the concrete crypto-core normalizer. Internal +/// helper used by the EF integration; not part of the public API. /// -public static class BlindIndexNormalizerResolver +internal static class BlindIndexNormalizerResolver { public static IBlindIndexNormalizer Resolve(BlindIndexNormalizerKind kind) => kind switch { diff --git a/src/Proteos.Encryption.EntityFrameworkCore/Migration/EncryptionMigrationPlanner.cs b/src/Proteos.Encryption.EntityFrameworkCore/Migration/EncryptionMigrationPlanner.cs index 00b7c9d..8c68e80 100644 --- a/src/Proteos.Encryption.EntityFrameworkCore/Migration/EncryptionMigrationPlanner.cs +++ b/src/Proteos.Encryption.EntityFrameworkCore/Migration/EncryptionMigrationPlanner.cs @@ -8,7 +8,7 @@ namespace Proteos.Encryption.EntityFrameworkCore; /// stored envelope (Base64 for a string property, raw bytes for a byte[] property), reads /// Header.KeyId and compares it with the tenant's current key id — no decryption, no plaintext. /// -public sealed class EncryptionMigrationPlanner : IEncryptionMigrationPlanner +internal sealed class EncryptionMigrationPlanner : IEncryptionMigrationPlanner { private readonly ICiphertextEnvelopeCodec _codec; private readonly IKeyMaterialProvider _keyProvider; diff --git a/src/Proteos.Encryption.EntityFrameworkCore/Migration/EncryptionMigrationService.cs b/src/Proteos.Encryption.EntityFrameworkCore/Migration/EncryptionMigrationService.cs index 10a07cb..644f2cc 100644 --- a/src/Proteos.Encryption.EntityFrameworkCore/Migration/EncryptionMigrationService.cs +++ b/src/Proteos.Encryption.EntityFrameworkCore/Migration/EncryptionMigrationService.cs @@ -12,7 +12,7 @@ namespace Proteos.Encryption.EntityFrameworkCore; /// both steps are done. It reuses the same encryption and blind index services the interceptors use, /// so a migrated value is indistinguishable from a freshly written one. /// -public sealed class EncryptionMigrationService : IEncryptionMigrationService +internal sealed class EncryptionMigrationService : IEncryptionMigrationService { private readonly AesGcmValueEncryptionService _encryptionService; private readonly IBlindIndexProvider _blindIndexProvider; diff --git a/src/Proteos.Encryption.EntityFrameworkCore/Proteos.Encryption.EntityFrameworkCore.csproj b/src/Proteos.Encryption.EntityFrameworkCore/Proteos.Encryption.EntityFrameworkCore.csproj index feaeb29..98c78de 100644 --- a/src/Proteos.Encryption.EntityFrameworkCore/Proteos.Encryption.EntityFrameworkCore.csproj +++ b/src/Proteos.Encryption.EntityFrameworkCore/Proteos.Encryption.EntityFrameworkCore.csproj @@ -39,4 +39,8 @@ + + + + diff --git a/src/Proteos.Encryption.EntityFrameworkCore/PublicAPI.Shipped.txt b/src/Proteos.Encryption.EntityFrameworkCore/PublicAPI.Shipped.txt index fec7da5..351fabb 100644 --- a/src/Proteos.Encryption.EntityFrameworkCore/PublicAPI.Shipped.txt +++ b/src/Proteos.Encryption.EntityFrameworkCore/PublicAPI.Shipped.txt @@ -7,8 +7,6 @@ Microsoft.EntityFrameworkCore.ProteosEncryptionModelBuilderExtensions Microsoft.EntityFrameworkCore.ProteosEncryptionPropertyBuilderExtensions Microsoft.EntityFrameworkCore.ProteosEncryptionQueryableExtensions Microsoft.Extensions.DependencyInjection.ProteosEncryptionServiceCollectionExtensions -override Proteos.Encryption.EntityFrameworkCore.EncryptingSaveChangesInterceptor.SavingChanges(Microsoft.EntityFrameworkCore.Diagnostics.DbContextEventData! eventData, Microsoft.EntityFrameworkCore.Diagnostics.InterceptionResult result) -> Microsoft.EntityFrameworkCore.Diagnostics.InterceptionResult -override Proteos.Encryption.EntityFrameworkCore.EncryptingSaveChangesInterceptor.SavingChangesAsync(Microsoft.EntityFrameworkCore.Diagnostics.DbContextEventData! eventData, Microsoft.EntityFrameworkCore.Diagnostics.InterceptionResult result, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask> override Proteos.Encryption.EntityFrameworkCore.EncryptionAuditReport.ToString() -> string! override Proteos.Encryption.EntityFrameworkCore.ReEncryptResumeToken.Equals(object? obj) -> bool override Proteos.Encryption.EntityFrameworkCore.ReEncryptResumeToken.GetHashCode() -> int @@ -20,10 +18,6 @@ Proteos.Encryption.EntityFrameworkCore.AlreadyEncryptedValueException.PropertyNa Proteos.Encryption.EntityFrameworkCore.BlindIndexNormalizerKind Proteos.Encryption.EntityFrameworkCore.BlindIndexNormalizerKind.Default = 0 -> Proteos.Encryption.EntityFrameworkCore.BlindIndexNormalizerKind Proteos.Encryption.EntityFrameworkCore.BlindIndexNormalizerKind.Email = 1 -> Proteos.Encryption.EntityFrameworkCore.BlindIndexNormalizerKind -Proteos.Encryption.EntityFrameworkCore.BlindIndexNormalizerResolver -Proteos.Encryption.EntityFrameworkCore.DecryptingMaterializationInterceptor -Proteos.Encryption.EntityFrameworkCore.DecryptingMaterializationInterceptor.DecryptingMaterializationInterceptor(Proteos.Encryption.EntityFrameworkCore.ITenantResolver! tenantResolver, Proteos.Encryption.Core.AesGcmValueEncryptionService! encryptionService, System.IServiceProvider! serviceProvider) -> void -Proteos.Encryption.EntityFrameworkCore.DecryptingMaterializationInterceptor.InitializedInstance(Microsoft.EntityFrameworkCore.Diagnostics.MaterializationInterceptionData materializationData, object! instance) -> object! Proteos.Encryption.EntityFrameworkCore.EncryptedAttribute Proteos.Encryption.EntityFrameworkCore.EncryptedAttribute.EncryptedAttribute() -> void Proteos.Encryption.EntityFrameworkCore.EncryptedAttribute.EncryptedAttribute(string! name) -> void @@ -85,8 +79,6 @@ Proteos.Encryption.EntityFrameworkCore.EncryptedSearchableAttribute.IndexPropert Proteos.Encryption.EntityFrameworkCore.EncryptedSearchableAttribute.IndexProperty.set -> void Proteos.Encryption.EntityFrameworkCore.EncryptedSearchableAttribute.Normalizer.get -> Proteos.Encryption.EntityFrameworkCore.BlindIndexNormalizerKind Proteos.Encryption.EntityFrameworkCore.EncryptedSearchableAttribute.Normalizer.set -> void -Proteos.Encryption.EntityFrameworkCore.EncryptingSaveChangesInterceptor -Proteos.Encryption.EntityFrameworkCore.EncryptingSaveChangesInterceptor.EncryptingSaveChangesInterceptor(Proteos.Encryption.EntityFrameworkCore.ITenantResolver! tenantResolver, Proteos.Encryption.Core.AesGcmValueEncryptionService! encryptionService, Proteos.Encryption.Abstractions.IBlindIndexProvider! blindIndexProvider, Proteos.Encryption.Core.ICiphertextEnvelopeCodec! codec, System.IServiceProvider! serviceProvider) -> void Proteos.Encryption.EntityFrameworkCore.EncryptionAuditEntry Proteos.Encryption.EntityFrameworkCore.EncryptionAuditEntry.Classification.get -> Proteos.Encryption.EntityFrameworkCore.EncryptionClassification Proteos.Encryption.EntityFrameworkCore.EncryptionAuditEntry.Classification.init -> void @@ -109,14 +101,6 @@ Proteos.Encryption.EntityFrameworkCore.EncryptionClassification.Encrypted = 0 -> Proteos.Encryption.EntityFrameworkCore.EncryptionClassification.EncryptedSearchable = 1 -> Proteos.Encryption.EntityFrameworkCore.EncryptionClassification Proteos.Encryption.EntityFrameworkCore.EncryptionClassification.Plaintext = 2 -> Proteos.Encryption.EntityFrameworkCore.EncryptionClassification Proteos.Encryption.EntityFrameworkCore.EncryptionClassification.Unclassified = 3 -> Proteos.Encryption.EntityFrameworkCore.EncryptionClassification -Proteos.Encryption.EntityFrameworkCore.EncryptionMigrationPlanner -Proteos.Encryption.EntityFrameworkCore.EncryptionMigrationPlanner.CreatePlan(Proteos.Encryption.EntityFrameworkCore.EncryptedEntityMetadata! metadata, System.Collections.Generic.IReadOnlyDictionary! storedValues, Proteos.Encryption.Abstractions.TenantId! tenant) -> Proteos.Encryption.EntityFrameworkCore.EncryptedEntityMigrationPlan! -Proteos.Encryption.EntityFrameworkCore.EncryptionMigrationPlanner.EncryptionMigrationPlanner(Proteos.Encryption.Core.ICiphertextEnvelopeCodec! codec, Proteos.Encryption.Core.IKeyMaterialProvider! keyProvider) -> void -Proteos.Encryption.EntityFrameworkCore.EncryptionMigrationPlanner.NeedsReEncryption(System.Type! propertyType, object? storedValue, Proteos.Encryption.Abstractions.KeyId! currentKeyId) -> bool -Proteos.Encryption.EntityFrameworkCore.EncryptionMigrationPlanner.ReadStoredKeyId(System.Type! propertyType, object? storedValue) -> Proteos.Encryption.Abstractions.KeyId? -Proteos.Encryption.EntityFrameworkCore.EncryptionMigrationService -Proteos.Encryption.EntityFrameworkCore.EncryptionMigrationService.EncryptionMigrationService(Proteos.Encryption.Core.AesGcmValueEncryptionService! encryptionService, Proteos.Encryption.Abstractions.IBlindIndexProvider! blindIndexProvider) -> void -Proteos.Encryption.EntityFrameworkCore.EncryptionMigrationService.ReEncrypt(Proteos.Encryption.EntityFrameworkCore.EncryptedPropertyDescriptor! descriptor, object! storedValue, Proteos.Encryption.Abstractions.TenantId! tenant) -> Proteos.Encryption.EntityFrameworkCore.MigratedEncryptedProperty! Proteos.Encryption.EntityFrameworkCore.IEncryptionMigrationPlanner Proteos.Encryption.EntityFrameworkCore.IEncryptionMigrationPlanner.CreatePlan(Proteos.Encryption.EntityFrameworkCore.EncryptedEntityMetadata! metadata, System.Collections.Generic.IReadOnlyDictionary! storedValues, Proteos.Encryption.Abstractions.TenantId! tenant) -> Proteos.Encryption.EntityFrameworkCore.EncryptedEntityMigrationPlan! Proteos.Encryption.EntityFrameworkCore.IEncryptionMigrationPlanner.NeedsReEncryption(System.Type! propertyType, object? storedValue, Proteos.Encryption.Abstractions.KeyId! currentKeyId) -> bool @@ -186,7 +170,6 @@ static Microsoft.EntityFrameworkCore.ProteosEncryptionQueryableExtensions.WhereE static Microsoft.EntityFrameworkCore.ProteosEncryptionQueryableExtensions.WhereEncryptedEquals(this System.Linq.IQueryable! source, Microsoft.EntityFrameworkCore.DbContext! dbContext, System.Linq.Expressions.Expression!>! propertySelector, string? value) -> System.Linq.IQueryable! static Microsoft.EntityFrameworkCore.ProteosEncryptionQueryableExtensions.WhereEncryptedIn(this System.Linq.IQueryable! source, Microsoft.EntityFrameworkCore.DbContext! dbContext, System.Linq.Expressions.Expression!>! propertySelector, System.Collections.Generic.IEnumerable! values) -> System.Linq.IQueryable! static Microsoft.Extensions.DependencyInjection.ProteosEncryptionServiceCollectionExtensions.AddProteosEncryption(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Proteos.Encryption.EntityFrameworkCore.BlindIndexNormalizerResolver.Resolve(Proteos.Encryption.EntityFrameworkCore.BlindIndexNormalizerKind kind) -> Proteos.Encryption.Core.IBlindIndexNormalizer! static Proteos.Encryption.EntityFrameworkCore.EncryptedEntityMetadataScanner.Scan(System.Type! entityType) -> Proteos.Encryption.EntityFrameworkCore.EncryptedEntityMetadata! static Proteos.Encryption.EntityFrameworkCore.EncryptionAuditReport.Create(Microsoft.EntityFrameworkCore.Metadata.IReadOnlyModel! model) -> Proteos.Encryption.EntityFrameworkCore.EncryptionAuditReport! static Proteos.Encryption.EntityFrameworkCore.ReEncryptBatchOptions.Default.get -> Proteos.Encryption.EntityFrameworkCore.ReEncryptBatchOptions! diff --git a/tests/Proteos.Encryption.AwsKms.Tests/AwsKmsKeyProviderTests.cs b/tests/Proteos.Encryption.AwsKms.Tests/AwsKmsKeyProviderTests.cs index bc53e57..2de086a 100644 --- a/tests/Proteos.Encryption.AwsKms.Tests/AwsKmsKeyProviderTests.cs +++ b/tests/Proteos.Encryption.AwsKms.Tests/AwsKmsKeyProviderTests.cs @@ -144,7 +144,7 @@ public void AddProteosAwsKms_InvalidKeyId_Throws() [Theory] [InlineData(typeof(AesGcmValueEncryptionService))] // Proteos.Encryption.Core - [InlineData(typeof(EncryptingSaveChangesInterceptor))] // Proteos.Encryption.EntityFrameworkCore + [InlineData(typeof(EncryptedAttribute))] // Proteos.Encryption.EntityFrameworkCore [InlineData(typeof(AzureKeyVaultKeyProvider))] // Proteos.Encryption.AzureKeyVault public void CoreEfAndAzure_DoNotReferenceAws(Type typeFromAssembly) { diff --git a/tests/Proteos.Encryption.AzureKeyVault.Tests/AzureKeyVaultKeyProviderTests.cs b/tests/Proteos.Encryption.AzureKeyVault.Tests/AzureKeyVaultKeyProviderTests.cs index d0f84a6..063a761 100644 --- a/tests/Proteos.Encryption.AzureKeyVault.Tests/AzureKeyVaultKeyProviderTests.cs +++ b/tests/Proteos.Encryption.AzureKeyVault.Tests/AzureKeyVaultKeyProviderTests.cs @@ -153,7 +153,7 @@ public void AddProteosAzureKeyVault_InvalidKeyIdentifier_Throws() [Theory] [InlineData(typeof(AesGcmValueEncryptionService))] // Proteos.Encryption.Core - [InlineData(typeof(EncryptingSaveChangesInterceptor))] // Proteos.Encryption.EntityFrameworkCore + [InlineData(typeof(EncryptedAttribute))] // Proteos.Encryption.EntityFrameworkCore public void CoreAndEf_DoNotReferenceAzure(Type typeFromAssembly) { var referenced = typeFromAssembly.Assembly.GetReferencedAssemblies();