Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/) and this
* `PerformanceBuilder.EnableQueryCaching()` — compose the existing `WithCaching(c => c.UseMemoryCache())` configuration with your query path; QuerySpec does not own a query-result cache.
* `PerformanceBuilder.OptimizeExpressions()` — no replacement; expression compilation is already handled by EF Core / `IQueryable` providers.
* `SecurityBuilder.RotateKeysEvery(int)` — implement key rotation at the storage layer following the same pattern as the `RotateKeyAsync` removal in [#138](https://github.com/AbongileBoja/QuerySpec/issues/138). Closes [#139](https://github.com/AbongileBoja/QuerySpec/issues/139).
* **security:** `IEncryptionProvider.RotateKeyAsync()` and `IEncryptionProvider.RotateKeyAsync(CancellationToken)` removed. Both overloads were `[Obsolete(error: true)]` in 3.x and every shipping implementation (`AesEncryptionProvider`, `AesGcmEncryptionProvider`, `MigratingEncryptionProvider`) threw `NotSupportedException`. ApiCompat reports `CP0001` (member removed) on both. Implement key rotation at the storage layer instead: construct a new provider with the new key, decrypt under the old provider, re-encrypt under the new provider, persist, then cut over reads. For `MigratingEncryptionProvider` specifically, the existing prefix-tag dispatch already supports this pattern — register the previous writer as a legacy reader and promote the new writer. Closes [#138](https://github.com/AbongileBoja/QuerySpec/issues/138).

### Added

Expand Down
14 changes: 0 additions & 14 deletions src/QuerySpec.Core/Security/AesEncryptionProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,20 +110,6 @@ public string Decrypt(string ciphertext)
return Encoding.UTF8.GetString(plainBytes);
}

/// <summary>
/// Key rotation requires re-encrypting every ciphertext under the new key. This provider
/// does not own the persisted ciphertexts. The method previously returned silently which
/// gave callers a false belief that rotation had happened. Deprecated and will be removed
/// in 3.0; implement rotation at the storage layer.
/// </summary>
/// <exception cref="NotSupportedException">Always thrown.</exception>
[Obsolete("Key rotation is a storage-layer concern; this method will be removed in 3.0. Implement rotation in the storage layer (Azure Key Vault, AWS KMS, etc.) rather than on IEncryptionProvider.", error: true)]
public Task RotateKeyAsync() =>
throw new NotSupportedException(
"AesEncryptionProvider does not own the persisted ciphertexts and cannot rotate. " +
"Construct a new provider with the new key, decrypt with the old, re-encrypt with the new at the storage layer. " +
"Prefer AesGcmEncryptionProvider for new ciphertexts.");

/// <summary>Generates a new 256-bit encryption key.</summary>
/// <returns>A base64-encoded 32-byte AES key suitable for the <see cref="AesEncryptionProvider(string)"/> constructor.</returns>
public static string GenerateKey()
Expand Down
14 changes: 0 additions & 14 deletions src/QuerySpec.Core/Security/AesGcmEncryptionProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,20 +102,6 @@ public string Decrypt(string ciphertext, ReadOnlySpan<byte> associatedData)
return Encoding.UTF8.GetString(plain);
}

/// <summary>
/// Key rotation requires re-encrypting every ciphertext under the new key. This provider
/// holds a single immutable key and does not own the persisted ciphertexts; callers must
/// implement rotation at their storage layer (decrypt under old key, re-encrypt under new
/// key). The method is implemented as a throwing stub so callers cannot mistake a no-op
/// for a real rotation. Deprecated and will be removed in 3.0.
/// </summary>
/// <exception cref="NotSupportedException">Always thrown.</exception>
[Obsolete("Key rotation is a storage-layer concern; this method will be removed in 3.0. Implement rotation in the storage layer (Azure Key Vault, AWS KMS, etc.) rather than on IEncryptionProvider.", error: true)]
public Task RotateKeyAsync() =>
throw new NotSupportedException(
"AesGcmEncryptionProvider does not own the persisted ciphertexts and therefore cannot rotate. " +
"Construct a new instance with the new key, decrypt with the old, re-encrypt with the new at the storage layer.");

/// <summary>Generates a new 256-bit AES key, base64-encoded.</summary>
public static string GenerateKey()
{
Expand Down
25 changes: 0 additions & 25 deletions src/QuerySpec.Core/Security/IEncryptionProvider.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
using System;
using System.Threading;
using System.Threading.Tasks;

namespace QuerySpec.Core.Security;

/// <summary>
Expand All @@ -17,25 +13,4 @@ public interface IEncryptionProvider
/// <param name="ciphertext">Provider-defined ciphertext envelope as produced by <see cref="Encrypt"/>.</param>
/// <returns>The recovered UTF-8 plaintext.</returns>
string Decrypt(string ciphertext);

/// <summary>
/// Rotates the encryption key. Deprecated: every shipping implementation throws
/// <see cref="NotSupportedException"/> because the provider does not own the persisted
/// ciphertexts. Implement rotation at the storage layer (Azure Key Vault, AWS KMS, etc.):
/// decrypt under the old provider, re-encrypt under the new provider.
/// </summary>
/// <returns>A task that completes once the rotation has been performed by the implementation.</returns>
[Obsolete("Key rotation is a storage-layer concern; this method will be removed in 3.0. Implement rotation in the storage layer (Azure Key Vault, AWS KMS, etc.) rather than on IEncryptionProvider.", error: true)]
Task RotateKeyAsync();
/// <summary>
/// Cancellation-aware overload of <see cref="RotateKeyAsync()"/>. Deprecated alongside the
/// no-token overload; will be removed in 3.0. Implement key rotation at the storage layer.
/// </summary>
/// <param name="cancellationToken">Token observed by implementations that perform I/O during rotation.</param>
/// <returns>A task that completes once the rotation has been performed by the implementation.</returns>
[Obsolete("Key rotation is a storage-layer concern; this method will be removed in 3.0. Implement rotation in the storage layer (Azure Key Vault, AWS KMS, etc.) rather than on IEncryptionProvider.", error: true)]
Task RotateKeyAsync(CancellationToken cancellationToken)
#pragma warning disable CS0619 // Obsolete-error self-reference: the DIM delegates to the CT-less overload that is itself obsolete; both ship as a coupled deprecation pair removed together in 3.0.
=> RotateKeyAsync();
#pragma warning restore CS0619
}
14 changes: 0 additions & 14 deletions src/QuerySpec.Core/Security/MigratingEncryptionProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,20 +128,6 @@ public string Decrypt(string ciphertext)
/// <summary>The tag stamped on every ciphertext written by this instance.</summary>
public string WriteTag { get; }

/// <summary>
/// Migration is meaningful only at the storage layer (decrypt under reader, re-encrypt
/// under writer). Calling this on the wrapper is almost always a mistake — it would
/// rotate the inner writer's key without re-encrypting any persisted ciphertexts and
/// would invalidate the readers for tags pointing at the same physical provider.
/// Deprecated and will be removed in 3.0.
/// </summary>
/// <exception cref="NotSupportedException">Always thrown.</exception>
[Obsolete("Key rotation is a storage-layer concern; this method will be removed in 3.0. Implement rotation in the storage layer (Azure Key Vault, AWS KMS, etc.) rather than on IEncryptionProvider.", error: true)]
public Task RotateKeyAsync() =>
throw new NotSupportedException(
"MigratingEncryptionProvider does not own the inner providers' keys and cannot rotate. " +
"Construct a new instance with the new writer (and the previous writer demoted to a reader) and re-encrypt at the storage layer.");

internal static void ValidateTag(string tag, string paramName)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tag, paramName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,16 +155,6 @@ public void EnvelopeLength_Equals_NoncePlusTagPlusUtf8PlaintextLength()
Assert.Equal(12 + 16 + utf8Len, bytes.Length);
}

[Fact]
public void RotateKeyAsync_Throws_NotSupported()
{
var p = new AesGcmEncryptionProvider(NewKey());

var method = typeof(AesGcmEncryptionProvider).GetMethod(nameof(IEncryptionProvider.RotateKeyAsync), Type.EmptyTypes)!;
var ex = Assert.Throws<System.Reflection.TargetInvocationException>(() => method.Invoke(p, null));
Assert.IsType<NotSupportedException>(ex.InnerException);
}

[Fact]
public void GenerateKey_Produces_Base64_32Bytes()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,16 +142,6 @@ public void Aead_Constructor_NullWriter_Throws()
new MigratingAuthenticatedEncryptionProvider("v2", writer: null!));
}

[Fact]
public void RotateKeyAsync_AlwaysThrows()
{
var migrating = new MigratingEncryptionProvider("v2", new AesGcmEncryptionProvider(NewKeyB64()));

var method = typeof(MigratingEncryptionProvider).GetMethod(nameof(IEncryptionProvider.RotateKeyAsync), Type.EmptyTypes)!;
var ex = Assert.Throws<System.Reflection.TargetInvocationException>(() => method.Invoke(migrating, null));
Assert.IsType<NotSupportedException>(ex.InnerException);
}

[Fact]
public void RegisteredTags_IncludesWriterAndReaders()
{
Expand Down
Loading