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 3ccb3eb7e9816219c4d43af4816002bbd13f2b48 Mon Sep 17 00:00:00 2001 From: Georgios Smyrlis Date: Fri, 19 Jun 2026 13:47:44 +0200 Subject: [PATCH 2/2] Move IKeyMaterialProvider and KeyResolutionException to Abstractions --- docs/compatibility-promise.md | 4 ++-- .../Infrastructure/ShowcaseHost.cs | 1 + .../Keys/IKeyMaterialProvider.cs | 4 +--- .../Keys/KeyResolutionException.cs | 4 +--- .../PublicAPI.Shipped.txt | 7 +++++++ src/Proteos.Encryption.Core/PublicAPI.Shipped.txt | 11 ++--------- .../PublicAPI.Shipped.txt | 2 +- 7 files changed, 15 insertions(+), 18 deletions(-) rename src/{Proteos.Encryption.Core => Proteos.Encryption.Abstractions}/Keys/IKeyMaterialProvider.cs (96%) rename src/{Proteos.Encryption.Core => Proteos.Encryption.Abstractions}/Keys/KeyResolutionException.cs (85%) diff --git a/docs/compatibility-promise.md b/docs/compatibility-promise.md index 9b4a391..3d59d7b 100644 --- a/docs/compatibility-promise.md +++ b/docs/compatibility-promise.md @@ -38,6 +38,8 @@ way that breaks stored data or removes/alters public surface. - Core value types: `KeyId`, `TenantId`, `LogicalName`, `EncryptedDataScope`, `EncryptionContext`, `KeyDescriptor`, `CiphertextEnvelope`, `CiphertextEnvelopeHeader`, and the envelope id types (`EnvelopeVersion`, `CryptoSuiteId`, `AadSchemeId`). +- `IKeyMaterialProvider` and `KeyResolutionException` live in the dependency-free + `Proteos.Encryption.Abstractions` package (the provider seam is public via `UseKeyProvider`). - Entity Framework Core integration: the attributes (`[Encrypted]`, `[EncryptedSearchable]`, `[EncryptedEmail]`, `[Plaintext]`, `[EncryptedEntity]`), the fluent configuration API, the query helpers (`WhereEncryptedEquals`, `WhereEncryptedIn`, `WhereEncryptedEmail`), the registration entry @@ -93,8 +95,6 @@ Areas that may evolve after `1.0` without violating the promise: While still on `0.x`, the following changes are considered acceptable and may land before `1.0`. They are listed so the intent is on record; none is guaranteed to happen. -- Moving `IKeyMaterialProvider` from `Proteos.Encryption.Core` to `Proteos.Encryption.Abstractions` for - cleaner layering. (Currently expected to be left as-is, but allowed.) - Adjusting the re-encryption (preview) API surface. - Refining registration ergonomics and adding startup-time configuration validation. - Analyzer message wording and rule scope. diff --git a/samples/Proteos.FeatureShowcase/Infrastructure/ShowcaseHost.cs b/samples/Proteos.FeatureShowcase/Infrastructure/ShowcaseHost.cs index c512241..665e70c 100644 --- a/samples/Proteos.FeatureShowcase/Infrastructure/ShowcaseHost.cs +++ b/samples/Proteos.FeatureShowcase/Infrastructure/ShowcaseHost.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Proteos.Encryption.Abstractions; using Proteos.Encryption.Core; namespace Proteos.FeatureShowcase.Infrastructure; diff --git a/src/Proteos.Encryption.Core/Keys/IKeyMaterialProvider.cs b/src/Proteos.Encryption.Abstractions/Keys/IKeyMaterialProvider.cs similarity index 96% rename from src/Proteos.Encryption.Core/Keys/IKeyMaterialProvider.cs rename to src/Proteos.Encryption.Abstractions/Keys/IKeyMaterialProvider.cs index 8114e14..50665b2 100644 --- a/src/Proteos.Encryption.Core/Keys/IKeyMaterialProvider.cs +++ b/src/Proteos.Encryption.Abstractions/Keys/IKeyMaterialProvider.cs @@ -1,6 +1,4 @@ -using Proteos.Encryption.Abstractions; - -namespace Proteos.Encryption.Core; +namespace Proteos.Encryption.Abstractions; /// /// Resolves the symmetric key material the encryption layer uses for a tenant and a specific diff --git a/src/Proteos.Encryption.Core/Keys/KeyResolutionException.cs b/src/Proteos.Encryption.Abstractions/Keys/KeyResolutionException.cs similarity index 85% rename from src/Proteos.Encryption.Core/Keys/KeyResolutionException.cs rename to src/Proteos.Encryption.Abstractions/Keys/KeyResolutionException.cs index 9b8c351..dd72f21 100644 --- a/src/Proteos.Encryption.Core/Keys/KeyResolutionException.cs +++ b/src/Proteos.Encryption.Abstractions/Keys/KeyResolutionException.cs @@ -1,6 +1,4 @@ -using Proteos.Encryption.Abstractions; - -namespace Proteos.Encryption.Core; +namespace Proteos.Encryption.Abstractions; /// /// Thrown when a key cannot be resolved for the requested tenant and descriptor — for example diff --git a/src/Proteos.Encryption.Abstractions/PublicAPI.Shipped.txt b/src/Proteos.Encryption.Abstractions/PublicAPI.Shipped.txt index b301648..9e99702 100644 --- a/src/Proteos.Encryption.Abstractions/PublicAPI.Shipped.txt +++ b/src/Proteos.Encryption.Abstractions/PublicAPI.Shipped.txt @@ -216,3 +216,10 @@ static readonly Proteos.Encryption.Abstractions.CryptoSuiteId.Aes256GcmSiv -> Pr static readonly Proteos.Encryption.Abstractions.CryptoSuiteId.Aes256SivDeterministic -> Proteos.Encryption.Abstractions.CryptoSuiteId static readonly Proteos.Encryption.Abstractions.CryptoSuiteId.XChaCha20Poly1305 -> Proteos.Encryption.Abstractions.CryptoSuiteId static readonly Proteos.Encryption.Abstractions.EnvelopeVersion.V1 -> Proteos.Encryption.Abstractions.EnvelopeVersion +Proteos.Encryption.Abstractions.IKeyMaterialProvider +Proteos.Encryption.Abstractions.IKeyMaterialProvider.DeriveKey(Proteos.Encryption.Abstractions.TenantId! tenant, Proteos.Encryption.Abstractions.KeyDescriptor! descriptor) -> byte[]! +Proteos.Encryption.Abstractions.IKeyMaterialProvider.GetCurrentKeyId(Proteos.Encryption.Abstractions.TenantId! tenant) -> Proteos.Encryption.Abstractions.KeyId! +Proteos.Encryption.Abstractions.IKeyMaterialProvider.GetKnownKeyIds(Proteos.Encryption.Abstractions.TenantId! tenant) -> System.Collections.Generic.IReadOnlyCollection! +Proteos.Encryption.Abstractions.IKeyMaterialProvider.ProviderId.get -> string! +Proteos.Encryption.Abstractions.KeyResolutionException +Proteos.Encryption.Abstractions.KeyResolutionException.KeyResolutionException(string! message) -> void diff --git a/src/Proteos.Encryption.Core/PublicAPI.Shipped.txt b/src/Proteos.Encryption.Core/PublicAPI.Shipped.txt index bacef94..14e93ed 100644 --- a/src/Proteos.Encryption.Core/PublicAPI.Shipped.txt +++ b/src/Proteos.Encryption.Core/PublicAPI.Shipped.txt @@ -2,7 +2,7 @@ const Proteos.Encryption.Core.LocalDevelopmentKeyProvider.DerivedKeyLength = 32 -> int const Proteos.Encryption.Core.LocalDevelopmentKeyProvider.RootKeyMinLength = 32 -> int Proteos.Encryption.Core.AesGcmValueEncryptionService -Proteos.Encryption.Core.AesGcmValueEncryptionService.AesGcmValueEncryptionService(Proteos.Encryption.Core.IKeyMaterialProvider! keyProvider, Proteos.Encryption.Core.ICiphertextEnvelopeCodec! codec) -> void +Proteos.Encryption.Core.AesGcmValueEncryptionService.AesGcmValueEncryptionService(Proteos.Encryption.Abstractions.IKeyMaterialProvider! keyProvider, Proteos.Encryption.Core.ICiphertextEnvelopeCodec! codec) -> void Proteos.Encryption.Core.AesGcmValueEncryptionService.Decrypt(Proteos.Encryption.Abstractions.CiphertextEnvelope! envelope, Proteos.Encryption.Abstractions.EncryptionContext! context) -> byte[]! Proteos.Encryption.Core.AesGcmValueEncryptionService.DecryptFromBytes(System.ReadOnlySpan envelopeBytes, Proteos.Encryption.Abstractions.EncryptionContext! context) -> byte[]! Proteos.Encryption.Core.AesGcmValueEncryptionService.Encrypt(System.ReadOnlySpan plaintext, Proteos.Encryption.Abstractions.EncryptionContext! context) -> Proteos.Encryption.Abstractions.CiphertextEnvelope! @@ -50,7 +50,7 @@ Proteos.Encryption.Core.HmacBlindIndexProvider.Compute(System.ReadOnlySpan Proteos.Encryption.Core.HmacBlindIndexProvider.ComputeForAllKnownKeys(System.ReadOnlySpan value, Proteos.Encryption.Abstractions.BlindIndexDescriptor! descriptor, Proteos.Encryption.Abstractions.EncryptionContext! context) -> System.Collections.Generic.IReadOnlyCollection! Proteos.Encryption.Core.HmacBlindIndexProvider.CreateIndex(string! value, Proteos.Encryption.Abstractions.EncryptionContext! context, Proteos.Encryption.Abstractions.BlindIndexPurpose purpose) -> Proteos.Encryption.Abstractions.BlindIndexValue! Proteos.Encryption.Core.HmacBlindIndexProvider.CreateIndex(string! value, Proteos.Encryption.Abstractions.EncryptionContext! context, Proteos.Encryption.Abstractions.BlindIndexPurpose purpose, Proteos.Encryption.Core.IBlindIndexNormalizer! normalizer) -> Proteos.Encryption.Abstractions.BlindIndexValue! -Proteos.Encryption.Core.HmacBlindIndexProvider.HmacBlindIndexProvider(Proteos.Encryption.Core.IKeyMaterialProvider! keyProvider) -> void +Proteos.Encryption.Core.HmacBlindIndexProvider.HmacBlindIndexProvider(Proteos.Encryption.Abstractions.IKeyMaterialProvider! keyProvider) -> void Proteos.Encryption.Core.IBlindIndexNormalizer Proteos.Encryption.Core.IBlindIndexNormalizer.Normalize(string! value) -> string! Proteos.Encryption.Core.ICiphertextEnvelopeCodec @@ -58,16 +58,9 @@ Proteos.Encryption.Core.ICiphertextEnvelopeCodec.CreateAad(Proteos.Encryption.Ab Proteos.Encryption.Core.ICiphertextEnvelopeCodec.Parse(System.ReadOnlySpan data) -> Proteos.Encryption.Abstractions.CiphertextEnvelope! Proteos.Encryption.Core.ICiphertextEnvelopeCodec.Serialize(Proteos.Encryption.Abstractions.CiphertextEnvelope! envelope) -> byte[]! Proteos.Encryption.Core.ICiphertextEnvelopeCodec.TryParse(System.ReadOnlySpan data, out Proteos.Encryption.Abstractions.CiphertextEnvelope? envelope, out Proteos.Encryption.Core.EnvelopeParseError? error) -> bool -Proteos.Encryption.Core.IKeyMaterialProvider -Proteos.Encryption.Core.IKeyMaterialProvider.DeriveKey(Proteos.Encryption.Abstractions.TenantId! tenant, Proteos.Encryption.Abstractions.KeyDescriptor! descriptor) -> byte[]! -Proteos.Encryption.Core.IKeyMaterialProvider.GetCurrentKeyId(Proteos.Encryption.Abstractions.TenantId! tenant) -> Proteos.Encryption.Abstractions.KeyId! -Proteos.Encryption.Core.IKeyMaterialProvider.GetKnownKeyIds(Proteos.Encryption.Abstractions.TenantId! tenant) -> System.Collections.Generic.IReadOnlyCollection! -Proteos.Encryption.Core.IKeyMaterialProvider.ProviderId.get -> string! Proteos.Encryption.Core.InMemoryTenantKeyRegistry Proteos.Encryption.Core.InMemoryTenantKeyRegistry.GetRecord(Proteos.Encryption.Abstractions.TenantId! tenant) -> Proteos.Encryption.Abstractions.TenantKeyRecord! Proteos.Encryption.Core.InMemoryTenantKeyRegistry.InMemoryTenantKeyRegistry(System.Collections.Generic.IEnumerable! records) -> void -Proteos.Encryption.Core.KeyResolutionException -Proteos.Encryption.Core.KeyResolutionException.KeyResolutionException(string! message) -> void Proteos.Encryption.Core.LocalDevelopmentKeyProvider Proteos.Encryption.Core.LocalDevelopmentKeyProvider.CurrentVersion.get -> ushort Proteos.Encryption.Core.LocalDevelopmentKeyProvider.DeriveKey(Proteos.Encryption.Abstractions.TenantId! tenant, Proteos.Encryption.Abstractions.KeyDescriptor! descriptor) -> byte[]! diff --git a/src/Proteos.Encryption.EntityFrameworkCore/PublicAPI.Shipped.txt b/src/Proteos.Encryption.EntityFrameworkCore/PublicAPI.Shipped.txt index 351fabb..0f3f9f7 100644 --- a/src/Proteos.Encryption.EntityFrameworkCore/PublicAPI.Shipped.txt +++ b/src/Proteos.Encryption.EntityFrameworkCore/PublicAPI.Shipped.txt @@ -125,7 +125,7 @@ Proteos.Encryption.EntityFrameworkCore.PlaintextAttribute.PlaintextAttribute() - Proteos.Encryption.EntityFrameworkCore.ProteosEncryptionOptions Proteos.Encryption.EntityFrameworkCore.ProteosEncryptionOptions.EnableStrictMode() -> Proteos.Encryption.EntityFrameworkCore.ProteosEncryptionOptions! Proteos.Encryption.EntityFrameworkCore.ProteosEncryptionOptions.ProteosEncryptionOptions() -> void -Proteos.Encryption.EntityFrameworkCore.ProteosEncryptionOptions.UseKeyProvider(System.Func! factory) -> Proteos.Encryption.EntityFrameworkCore.ProteosEncryptionOptions! +Proteos.Encryption.EntityFrameworkCore.ProteosEncryptionOptions.UseKeyProvider(System.Func! factory) -> Proteos.Encryption.EntityFrameworkCore.ProteosEncryptionOptions! Proteos.Encryption.EntityFrameworkCore.ProteosEncryptionOptions.UseLocalDevelopmentKeyProvider(byte[]? rootKey = null) -> Proteos.Encryption.EntityFrameworkCore.ProteosEncryptionOptions! Proteos.Encryption.EntityFrameworkCore.ProteosEncryptionOptions.UseSingleTenant(string! tenantId) -> Proteos.Encryption.EntityFrameworkCore.ProteosEncryptionOptions! Proteos.Encryption.EntityFrameworkCore.ProteosEncryptionOptions.UseTenant(System.Func! tenantResolver) -> Proteos.Encryption.EntityFrameworkCore.ProteosEncryptionOptions!