Skip to content

Conversation

@strawgate
Copy link
Owner

@strawgate strawgate commented Oct 24, 2025

Summary by CodeRabbit

Release Notes

  • New Features

    • Encryption wrapper API refactored to support custom encryption/decryption functions
    • Key derivation from source material and salt using PBKDF2
    • Encryption versioning support for tracking encrypted data compatibility
    • New error types for encryption version mismatches and corrupted data
  • Documentation

    • Updated initialization examples and parameters for encryption wrapper

@strawgate strawgate requested a review from Copilot October 24, 2025 00:03
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 24, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

The encryption wrapper architecture is refactored to separate generic encryption logic from Fernet-specific implementation. EncryptionWrapper is renamed to BaseEncryptionWrapper and redesigned to accept encryption and decryption function callbacks. A new FernetEncryptionWrapper class provides Fernet-based encryption. Changes are mirrored across async and sync codebases with corresponding test updates.

Changes

Cohort / File(s) Summary
Module Exports
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/__init__.py, key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/__init__.py, key-value/key-value-sync/src/key_value/sync/wrappers/encryption/__init__.py
Replaced EncryptionWrapper export with BaseEncryptionWrapper and FernetEncryptionWrapper; updated __all__ and imports accordingly
Base Encryption Wrapper
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/base.py, key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/base.py
Renamed class EncryptionWrapperBaseEncryptionWrapper; replaced encryption-key-based constructor with function-callback pattern accepting encryption_fn, decryption_fn, and encryption_version; updated _encrypt_value and _decrypt_value to use callbacks; added _validate_encrypted_payload helper and new type aliases EncryptionFn and DecryptionFn
Fernet Encryption Wrapper
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/fernet.py, key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/fernet.py
New module implementing FernetEncryptionWrapper with constructor overloads for direct Fernet instance or key derivation from source_material and salt; includes _generate_encryption_key helper using PBKDF2-HMAC-SHA256 and encrypt_with_fernet / decrypt_with_fernet internal methods
Encryption Tests
key-value/key-value-aio/tests/stores/wrappers/test_encryption.py, key-value/key-value-sync/tests/code_gen/stores/wrappers/test_encryption.py
Renamed test class TestEncryptionWrapperTestFernetEncryptionWrapper; added fernet fixture; updated store fixture to accept fernet parameter and return FernetEncryptionWrapper; added test_encryption_with_wrong_encryption_version test; refactored existing tests to use store fixture instead of inline wrapper instantiation
Error Definitions
key-value/key-value-shared/src/key_value/shared/errors/wrappers/encryption.py
Added new exception classes: EncryptionVersionError(EncryptionError) and CorruptedDataError(DecryptionError)
Base Error Class
key-value/key-value-shared/src/key_value/shared/errors/base.py
Added message: str | None attribute to BaseKeyValueError; updated __init__ to store composed message on instance
Documentation
README.md
Updated Wrappers table to replace EncryptionWrapper entry with FernetEncryptionWrapper; changed constructor example parameters from encryption_key to source_material and salt

Sequence Diagram

sequenceDiagram
    participant Client
    participant FernetEncryptionWrapper
    participant BaseEncryptionWrapper
    participant encrypt_with_fernet
    participant decrypt_with_fernet
    participant Fernet

    Note over Client,Fernet: Encryption Flow
    Client->>FernetEncryptionWrapper: put(key, value)
    FernetEncryptionWrapper->>BaseEncryptionWrapper: _encrypt_value(value)
    BaseEncryptionWrapper->>encrypt_with_fernet: call encryption_fn(json_bytes)
    encrypt_with_fernet->>Fernet: encrypt(data)
    Fernet-->>encrypt_with_fernet: encrypted_bytes
    encrypt_with_fernet-->>BaseEncryptionWrapper: base64(encrypted_bytes)
    BaseEncryptionWrapper-->>FernetEncryptionWrapper: {__encrypted_data__, __encryption_version__}
    
    Note over Client,Fernet: Decryption Flow
    Client->>FernetEncryptionWrapper: get(key)
    FernetEncryptionWrapper->>BaseEncryptionWrapper: _decrypt_value(encrypted_payload)
    BaseEncryptionWrapper->>BaseEncryptionWrapper: _validate_encrypted_payload(payload)
    BaseEncryptionWrapper->>decrypt_with_fernet: call decryption_fn(encrypted_bytes, version)
    decrypt_with_fernet->>Fernet: decrypt(data)
    Fernet-->>decrypt_with_fernet: decrypted_bytes
    decrypt_with_fernet-->>BaseEncryptionWrapper: json_bytes
    BaseEncryptionWrapper-->>FernetEncryptionWrapper: value
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

This refactoring spans multiple files and introduces new abstractions (base class, derived class, new exceptions), but follows a consistent and cohesive pattern. The logic changes are concentrated in three areas: the base wrapper redesign (function callbacks), the Fernet wrapper implementation (key derivation and encryption wiring), and test updates (fixture refactoring). The changes are heterogeneous but methodical, with no extreme logic density in individual sections.

Poem

🐰 A wrapper's abstraction, now clean and bright,
From Fernet-bound chains to callbacks of light!
Functions flow through encryption's gentle dance,
As BaseWrapper leads with callbacks' romance,
Keys derive from salt, in PBKDF's prance—
Hops of joy

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 69.57% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "Add FernetEncryptionWrapper and simplify setup" is related to the actual changes in the pull request. The changeset does add a new FernetEncryptionWrapper class across multiple modules (both async and sync versions), making this part of the title accurate. Additionally, the architectural refactoring that decouples the base wrapper from Fernet-specific code to use function-based encryption/decryption does enable a simpler setup, aligning with the "simplify setup" portion of the title. However, the title does not explicitly mention the significant refactoring of the base wrapper (renaming from EncryptionWrapper to BaseEncryptionWrapper and changing its design), which is a substantial part of the changeset alongside the new wrapper addition.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor-encryption-wrapper

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 88130e7 and d96e188.

📒 Files selected for processing (5)
  • key-value/key-value-aio/src/key_value/aio/wrappers/encryption/base.py (1 hunks)
  • key-value/key-value-aio/src/key_value/aio/wrappers/encryption/fernet.py (1 hunks)
  • key-value/key-value-shared/src/key_value/shared/errors/wrappers/encryption.py (1 hunks)
  • key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/base.py (1 hunks)
  • key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/fernet.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • key-value/key-value-aio/src/key_value/aio/wrappers/encryption/fernet.py
🧰 Additional context used
🧬 Code graph analysis (3)
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/base.py (3)
key-value/key-value-shared/src/key_value/shared/errors/key_value.py (1)
  • SerializationError (10-11)
key-value/key-value-shared/src/key_value/shared/errors/wrappers/encryption.py (3)
  • CorruptedDataError (16-17)
  • DecryptionError (8-9)
  • EncryptionError (4-5)
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/base.py (4)
  • BaseEncryptionWrapper (21-175)
  • _encrypt_value (57-87)
  • _validate_encrypted_payload (89-109)
  • _decrypt_value (111-138)
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/fernet.py (4)
key-value/key-value-shared/src/key_value/shared/errors/wrappers/encryption.py (1)
  • EncryptionVersionError (12-13)
key-value/key-value-sync/src/key_value/sync/code_gen/protocols/key_value.py (1)
  • KeyValue (173-178)
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/base.py (1)
  • BaseEncryptionWrapper (23-174)
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/fernet.py (4)
  • FernetEncryptionWrapper (13-89)
  • _generate_encryption_key (92-106)
  • encrypt_with_fernet (74-75)
  • decrypt_with_fernet (77-81)
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/base.py (3)
key-value/key-value-shared/src/key_value/shared/errors/key_value.py (1)
  • SerializationError (10-11)
key-value/key-value-shared/src/key_value/shared/errors/wrappers/encryption.py (3)
  • CorruptedDataError (16-17)
  • DecryptionError (8-9)
  • EncryptionError (4-5)
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/base.py (4)
  • BaseEncryptionWrapper (23-174)
  • _encrypt_value (59-86)
  • _validate_encrypted_payload (88-108)
  • _decrypt_value (110-137)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: test_quick (3.10, ubuntu-latest, key-value/key-value-sync)
  • GitHub Check: test_quick (3.10, windows-latest, key-value/key-value-aio)
  • GitHub Check: test_quick (3.10, windows-latest, key-value/key-value-sync)
  • GitHub Check: test_quick (3.10, ubuntu-latest, key-value/key-value-aio)
🔇 Additional comments (18)
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/base.py (7)

3-3: LGTM! Import changes support the refactoring well.

The addition of Callable and the import of EncryptionError from the shared module are appropriate for the new function-based encryption design. The previous review comment about importing EncryptionError has been successfully addressed.

Also applies to: 7-7


17-18: LGTM! Type aliases clearly define the encryption interface.

The type aliases provide clear, self-documenting signatures for the pluggable encryption and decryption functions. The inclusion of encryption_version in DecryptionFn enables version-aware decryption strategies.


21-55: LGTM! Constructor design enables flexible encryption strategies.

The refactored constructor successfully decouples the encryption logic from specific implementations by accepting function callbacks. The class name BaseEncryptionWrapper appropriately signals its role as a base class for specialized implementations like FernetEncryptionWrapper.


57-87: LGTM! Encryption logic is well-structured.

The method correctly serializes to JSON, encrypts using the provided function, and encodes the result. The error handling appropriately distinguishes between serialization errors and encryption errors.


89-109: LGTM! Robust payload validation enhances data integrity.

The extracted validation method provides comprehensive checks for the encrypted payload structure and types. The clear error messages and use of CorruptedDataError will aid in debugging corrupted data scenarios.


111-138: LGTM! Decryption logic handles edge cases gracefully.

The method correctly handles unencrypted values for backward compatibility, validates the payload structure, and provides appropriate error handling. The granular exception handling (separating CorruptedDataError from general decryption failures) enables precise error recovery based on the raise_on_decryption_error flag.


140-175: LGTM! Public methods correctly integrate encryption/decryption.

All public methods properly delegate to the wrapped key_value instance while transparently applying encryption on writes and decryption on reads. The implementation maintains the AsyncKeyValue protocol contract.

key-value/key-value-shared/src/key_value/shared/errors/wrappers/encryption.py (1)

12-17: LGTM! Well-defined exception hierarchy.

The new exception classes provide clear error taxonomy for encryption version mismatches and data corruption scenarios, enabling more precise error handling in the wrapper implementations.

key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/fernet.py (4)

11-13: LGTM! Strong KDF configuration.

The 1.2 million PBKDF2 iterations provide robust protection against brute-force attacks, exceeding current OWASP recommendations.


50-62: LGTM! Thorough parameter validation.

The mutual exclusivity enforcement and empty/whitespace checks for source_material and salt properly guard against misconfiguration.


64-79: LGTM! Clean wrapper integration.

The encryption/decryption closures properly integrate with the base wrapper, and the version check correctly rejects only newer encryption versions while maintaining backward compatibility.


82-93: LGTM! Correct key derivation.

The function properly derives a Fernet-compatible key using PBKDF2-HMAC-SHA256 and correctly applies base64 URL-safe encoding. The past review concern about raw bytes has been resolved.

key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/base.py (6)

19-20: LGTM! Clear type aliases for pluggable encryption.

The type aliases properly define the contract for encryption and decryption functions, enabling the separation of generic wrapper logic from specific encryption implementations.


31-57: LGTM! Well-designed pluggable architecture.

The refactored constructor cleanly separates generic encryption wrapper logic from specific encryption implementations, enabling flexible encryption strategies via function callbacks.


59-86: LGTM! Robust encryption pipeline.

The method properly handles JSON serialization, encryption, and base64 encoding with appropriate error handling for each stage.


88-108: LGTM! Thorough payload validation.

The method properly validates the encrypted payload structure with membership checks before accessing keys and comprehensive type validation. The past review concern about KeyError has been resolved by checking key existence before access.


110-137: LGTM! Robust error handling in decryption.

The method implements layered error handling that distinguishes between structural corruption (CorruptedDataError) and cryptographic failures (general exceptions → DecryptionError), properly respecting the raise_on_decryption_error configuration for both cases.


139-174: LGTM! Clean wrapper delegation.

The wrapper methods correctly delegate to the underlying store while transparently applying encryption on writes and decryption on reads, maintaining the dict[str, Any] interface.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This pull request refactors the encryption wrapper implementation by introducing a new FernetEncryptionWrapper class that extends a new BaseEncryptionWrapper class, simplifying initialization and improving the API design for encryption functionality.

Key changes:

  • Introduced BaseEncryptionWrapper and FernetEncryptionWrapper classes, replacing the previous EncryptionWrapper
  • Added support for deriving encryption keys from source material and salt, rather than requiring pre-generated Fernet keys
  • Removed redundant test setup by utilizing the shared fernet fixture

Reviewed Changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
key-value/key-value-sync/tests/code_gen/stores/wrappers/test_encryption.py Updated tests to use FernetEncryptionWrapper and shared fernet fixture, removing redundant setup code
key-value/key-value-sync/src/key_value/sync/wrappers/encryption/__init__.py Updated exports to include BaseEncryptionWrapper and FernetEncryptionWrapper
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/fernet.py New file implementing FernetEncryptionWrapper with support for both Fernet instances and source material
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/base.py Refactored base encryption wrapper to accept encryption/decryption functions instead of Fernet key
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/__init__.py Updated exports to include BaseEncryptionWrapper and FernetEncryptionWrapper
key-value/key-value-aio/tests/stores/wrappers/test_encryption.py Updated async tests to use FernetEncryptionWrapper and shared fernet fixture
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/fernet.py New file implementing async version of FernetEncryptionWrapper
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/base.py Refactored async base encryption wrapper with encryption/decryption functions
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/__init__.py Updated exports for async encryption wrappers
key-value/key-value-aio/pyproject.toml Modified pytest configuration to enable inline snapshot fixes and creation
README.md Updated documentation example to show new initialization pattern

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/base.py (1)

101-107: Honor raise_on_decryption_error for malformed types

If "encrypted_data" exists but isn’t a str, this raises TypeError even when raise_on_decryption_error is False. That breaks the contract used elsewhere for corrupted data.

-        if not isinstance(base64_str, str):
-            # Corrupted data, return as-is
-            msg = f"Corrupted data: expected str, got {type(base64_str)}"
-            raise TypeError(msg)
+        if not isinstance(base64_str, str):
+            msg = f"Corrupted data: expected str, got {type(base64_str)}"
+            if self.raise_on_decryption_error:
+                raise TypeError(msg)
+            return None
🧹 Nitpick comments (12)
key-value/key-value-aio/pyproject.toml (1)

52-52: Avoid auto-creating inline snapshots by default

Using --inline-snapshot=fix,create in addopts can silently rewrite snapshots (especially on CI). Recommend dropping create and running it only locally when intentionally updating.

-[tool.pytest.ini_options]
-asyncio_mode = "auto"
-addopts = ["--inline-snapshot=fix,create"]
+[tool.pytest.ini_options]
+asyncio_mode = "auto"
+addopts = ["--inline-snapshot=fix"]

Optionally, document running pytest --inline-snapshot=create locally when needed.

key-value/key-value-aio/src/key_value/aio/wrappers/encryption/base.py (4)

29-31: Docstring should be algorithm-agnostic

BaseEncryptionWrapper now accepts arbitrary encryption/decryption functions; “using Fernet” is misleading. Reword to “using the provided encryption function.”

-    This wrapper encrypts the JSON-serialized value using Fernet (symmetric encryption)
+    This wrapper encrypts the JSON‑serialized value using the provided symmetric encryption function

66-71: Prevent accidental double-encryption

The guard is commented out. If callers pass an already-encrypted dict (or stack the wrapper twice), get() will only decrypt once and return an encrypted payload to users.

-        # # Don't encrypt if it's already encrypted
-        # if _ENCRYPTED_DATA_KEY in value:
-        #     return value
+        # Don't encrypt if it's already encrypted
+        if _ENCRYPTED_DATA_KEY in value:
+            return value

Add a unit test for idempotence on already-encrypted inputs.


81-83: Clarify comment: not necessarily Fernet

This encrypts via the injected function, not specifically Fernet.

-        # Encrypt with Fernet
+        # Encrypt via provided encryption function

112-114: Clarify comment: not necessarily Fernet

This decrypts via the injected function, not specifically Fernet.

-            # Decrypt with Fernet
+            # Decrypt via provided decryption function
key-value/key-value-aio/tests/stores/wrappers/test_encryption.py (1)

52-57: Avoid snapshotting exact encrypted token; assert structure instead

Fernet tokens are non-deterministic (timestamp/IV). Exact snapshot will change across runs and, with --inline-snapshot=create, can mask issues. Assert shape/content invariants instead.

-        raw_result = await memory_store.get(collection="test", key="test")
-        assert raw_result == snapshot(
-            {
-                "__encrypted_data__": "Z0FBQUFBQm8tc0ZsYWhZUmJqUnN0VGlyeGVoUWZuczlPUllyWWxyVEotTVNMVFMtd1hoalNTQk56eFdzNGVocEg0T0xDeEVkTHpJckc2Z0lGZGpCTWZpS3o3cmVWRmRUTl91RENvSW8zNnI3QTlJVmtrQ1FtNnc9",
-                "__encryption_version__": 1,
-            }
-        )
+        raw_result = await memory_store.get(collection="test", key="test")
+        assert raw_result is not None
+        assert set(raw_result.keys()) == {"__encrypted_data__", "__encryption_version__"}
+        assert isinstance(raw_result["__encrypted_data__"], str)
+        assert raw_result["__encryption_version__"] == 1
+        # sanity: ciphertext doesn't leak plaintext
+        assert "test" not in raw_result["__encrypted_data__"]
+        assert "value" not in raw_result["__encrypted_data__"]
key-value/key-value-sync/src/key_value/sync/wrappers/encryption/__init__.py (1)

4-7: Potential breaking change: restore a deprecated alias for EncryptionWrapper

Downstream code importing EncryptionWrapper from this module will break. Consider adding a temporary alias to ease migration.

Apply in the source (not this generated file):

 from key_value.sync.code_gen.wrappers.encryption.base import BaseEncryptionWrapper
 from key_value.sync.code_gen.wrappers.encryption.fernet import FernetEncryptionWrapper

-__all__ = ["BaseEncryptionWrapper", "FernetEncryptionWrapper"]
+# Deprecated alias for backward compatibility. Remove in a future major.
+EncryptionWrapper = FernetEncryptionWrapper
+__all__ = ["BaseEncryptionWrapper", "FernetEncryptionWrapper", "EncryptionWrapper"]

Please confirm if a hard break is intended; if so, update release notes and migration docs.

key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/fernet.py (1)

35-53: KDF choice and defaults may encourage weak keys if source_material is password-like

HKDF is fine for key expansion from high-entropy input, but not for password hardening. If source_material may be a human password, prefer PBKDF2HMAC/Scrypt (with parameters per cryptography guidance) or explicitly document that source_material must be high-entropy.

Option A (doc/param rename): Rename to key_material and document “must be high-entropy; not a password.”

Option B (stronger derivation): Use Scrypt for password-like input:

-from cryptography.hazmat.primitives.kdf.hkdf import HKDF
+from cryptography.hazmat.primitives.kdf.hkdf import HKDF
+from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
@@
-def _generate_encryption_key(source_material: str, salt: str | None = None) -> bytes:
+def _generate_encryption_key(source_material: str, salt: str | None = None, *, password_like: bool = False) -> bytes:
@@
-    raw_key = HKDF(
-        algorithm=hashes.SHA256(),
-        length=32,
-        salt=salt.encode(),
-        info=b"Fernet",
-    ).derive(key_material=source_material.encode())
+    if password_like:
+        kdf = Scrypt(salt=salt.encode(), length=32, n=2**14, r=8, p=1)
+        raw_key = kdf.derive(source_material.encode())
+    else:
+        raw_key = HKDF(algorithm=hashes.SHA256(), length=32, salt=salt.encode(), info=b"Fernet").derive(
+            key_material=source_material.encode()
+        )

Confirm intended usage and we can finalize docs/tests accordingly.

Also applies to: 68-74

key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/base.py (2)

65-70: Tidy up comments and remove dead code

  • Remove the commented “already encrypted” block to avoid drift.
  • Update comments to reflect pluggable crypto, not Fernet.

Apply:

-        # # Don't encrypt if it's already encrypted
-        # if _ENCRYPTED_DATA_KEY in value:
-        #     return value
@@
-        # Encrypt with Fernet
+        # Encrypt with the provided encryption function
@@
-            # Decrypt with Fernet
+            # Decrypt with the provided decryption function

Also applies to: 80-82, 108-110


25-27: Remove unused EncryptionError (avoid confusion with shared errors)

This local exception isn’t used; shared DecryptionError is used instead. Drop it or start using it consistently.

Apply:

-class EncryptionError(Exception):
-    """Exception raised when encryption or decryption fails."""
-
key-value/key-value-sync/tests/code_gen/stores/wrappers/test_encryption.py (2)

109-116: Rename test to reflect behavior (raises error with wrong key)

Name and docstring don’t match behavior. Rename for clarity.

Apply:

-    def test_decryption_with_wrong_key_returns_original(self, memory_store: MemoryStore):
-        """Test that decryption with the wrong key raises an error."""
+    def test_decryption_with_wrong_key_raises_error(self, memory_store: MemoryStore):
+        """Decryption with the wrong key raises an error."""

15-18: Optional: add a test for source_material path

Current tests exercise only the fernet instance path. Add a simple round‑trip using source_material to guard the KDF branch.

I can draft the test if you want it included in this PR.

Also applies to: 23-25

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6c46b5b and f1d9df5.

📒 Files selected for processing (11)
  • README.md (1 hunks)
  • key-value/key-value-aio/pyproject.toml (1 hunks)
  • key-value/key-value-aio/src/key_value/aio/wrappers/encryption/__init__.py (1 hunks)
  • key-value/key-value-aio/src/key_value/aio/wrappers/encryption/base.py (5 hunks)
  • key-value/key-value-aio/src/key_value/aio/wrappers/encryption/fernet.py (1 hunks)
  • key-value/key-value-aio/tests/stores/wrappers/test_encryption.py (5 hunks)
  • key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/__init__.py (1 hunks)
  • key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/base.py (5 hunks)
  • key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/fernet.py (1 hunks)
  • key-value/key-value-sync/src/key_value/sync/wrappers/encryption/__init__.py (1 hunks)
  • key-value/key-value-sync/tests/code_gen/stores/wrappers/test_encryption.py (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (9)
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/fernet.py (3)
key-value/key-value-sync/src/key_value/sync/code_gen/protocols/key_value.py (1)
  • KeyValue (173-178)
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/base.py (1)
  • BaseEncryptionWrapper (29-155)
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/fernet.py (4)
  • FernetEncryptionWrapper (8-73)
  • _generate_encryption_key (76-87)
  • encrypt_with_fernet (62-63)
  • decrypt_with_fernet (65-66)
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/fernet.py (3)
key-value/key-value-aio/tests/stores/wrappers/test_encryption.py (1)
  • fernet (13-14)
key-value/key-value-aio/src/key_value/aio/protocols/key_value.py (1)
  • AsyncKeyValue (175-180)
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/base.py (1)
  • BaseEncryptionWrapper (26-159)
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/__init__.py (2)
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/base.py (1)
  • BaseEncryptionWrapper (29-155)
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/fernet.py (1)
  • FernetEncryptionWrapper (11-65)
key-value/key-value-aio/tests/stores/wrappers/test_encryption.py (3)
key-value/key-value-shared/src/key_value/shared/errors/wrappers/encryption.py (1)
  • DecryptionError (8-9)
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/fernet.py (1)
  • FernetEncryptionWrapper (8-73)
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/base.py (4)
  • put (145-147)
  • get (125-127)
  • put_many (150-159)
  • get_many (130-132)
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/base.py (2)
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/base.py (2)
  • BaseEncryptionWrapper (26-159)
  • _encrypt_value (66-90)
key-value/key-value-sync/src/key_value/sync/code_gen/protocols/key_value.py (1)
  • KeyValue (173-178)
key-value/key-value-sync/src/key_value/sync/wrappers/encryption/__init__.py (2)
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/base.py (1)
  • BaseEncryptionWrapper (29-155)
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/fernet.py (1)
  • FernetEncryptionWrapper (11-65)
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/__init__.py (2)
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/base.py (1)
  • BaseEncryptionWrapper (26-159)
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/fernet.py (1)
  • FernetEncryptionWrapper (8-73)
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/base.py (2)
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/base.py (2)
  • BaseEncryptionWrapper (29-155)
  • _encrypt_value (65-86)
key-value/key-value-aio/src/key_value/aio/protocols/key_value.py (1)
  • AsyncKeyValue (175-180)
key-value/key-value-sync/tests/code_gen/stores/wrappers/test_encryption.py (4)
key-value/key-value-shared/src/key_value/shared/errors/wrappers/encryption.py (1)
  • DecryptionError (8-9)
key-value/key-value-aio/tests/stores/wrappers/test_encryption.py (2)
  • store (20-21)
  • fernet (13-14)
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/fernet.py (1)
  • FernetEncryptionWrapper (11-65)
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/base.py (4)
  • put (141-143)
  • get (121-123)
  • put_many (146-155)
  • get_many (126-128)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: test_quick (3.10, windows-latest, key-value/key-value-aio)
  • GitHub Check: test_quick (3.10, macos-latest, key-value/key-value-aio)
  • GitHub Check: test_quick (3.10, windows-latest, key-value/key-value-sync)
  • GitHub Check: test_quick (3.10, ubuntu-latest, key-value/key-value-sync)
  • GitHub Check: test_quick (3.10, ubuntu-latest, key-value/key-value-aio)
🔇 Additional comments (3)
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/__init__.py (1)

4-7: LGTM: public exports match refactor

BaseEncryptionWrapper and FernetEncryptionWrapper are correctly exported. No issues.

key-value/key-value-aio/src/key_value/aio/wrappers/encryption/__init__.py (1)

1-4: LGTM: updated exports

Exports align with the new base/fernet split.

key-value/key-value-aio/src/key_value/aio/wrappers/encryption/fernet.py (1)

52-60: Constructor validation is good

Mutual exclusivity and presence checks are clear and helpful. Nice.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (5)
key-value/key-value-shared/src/key_value/shared/errors/base.py (1)

8-8: Refine the type annotation for accuracy.

The message attribute is typed as str | None, but after __init__ completes, self.message will always be a str (line 23 sets it to the result of ": ".join(message_parts), which returns an empty string when the list is empty, never None). Consider updating the type annotation to just str for accuracy.

-    message: str | None = None
+    message: str = ""
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/base.py (4)

29-44: Docstring still references Fernet; generalize to function-based crypto.

After the refactor, this wrapper is algorithm-agnostic. Update wording. Apply in the original source.

-    This wrapper encrypts the JSON-serialized value using Fernet (symmetric encryption)
+    This wrapper encrypts the JSON-serialized value using the provided encryption_fn (symmetric encryption)
@@
-    Note: The encryption key must be kept secret and secure. If the key is lost,
-    encrypted data cannot be recovered.
+    Note: Keep encryption material (keys/secrets) secure. If lost, encrypted data cannot be recovered.

46-54: Constructor changes look good; add a quick guard for the version.

Validate encryption_version is a positive int at runtime. Apply in the original source.

-        self.encryption_version: int = encryption_version
+        if not isinstance(encryption_version, int) or encryption_version < 1:
+            raise ValueError("encryption_version must be a positive int")
+        self.encryption_version: int = encryption_version

Also applies to: 67-71


86-92: Comments mention Fernet; update to reflect generic hooks.

Purely cosmetic, keeps code and docs aligned. Apply in the original source.

-        # Encrypt with Fernet
+        # Encrypt using the provided encryption_fn
@@
-        # Encode to base64 for storage in dict (though Fernet output is already base64)
+        # Encode to base64 for storage in dict

118-118: Stricter base64 decoding.

Use validate=True to fail fast on non-base64 data. Apply in the original source.

-            encrypted_bytes: bytes = base64.b64decode(base64_str)
+            encrypted_bytes: bytes = base64.b64decode(base64_str, validate=True)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f1d9df5 and f60a8d9.

📒 Files selected for processing (8)
  • key-value/key-value-aio/src/key_value/aio/wrappers/encryption/base.py (5 hunks)
  • key-value/key-value-aio/src/key_value/aio/wrappers/encryption/fernet.py (1 hunks)
  • key-value/key-value-aio/tests/stores/wrappers/test_encryption.py (5 hunks)
  • key-value/key-value-shared/src/key_value/shared/errors/base.py (2 hunks)
  • key-value/key-value-shared/src/key_value/shared/errors/wrappers/encryption.py (1 hunks)
  • key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/base.py (5 hunks)
  • key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/fernet.py (1 hunks)
  • key-value/key-value-sync/tests/code_gen/stores/wrappers/test_encryption.py (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/fernet.py
🧰 Additional context used
🧬 Code graph analysis (6)
key-value/key-value-shared/src/key_value/shared/errors/wrappers/encryption.py (5)
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/base.py (1)
  • EncryptionError (22-23)
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/base.py (1)
  • EncryptionError (25-26)
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/wrapper.py (1)
  • EncryptionError (23-24)
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/wrapper.py (1)
  • EncryptionError (20-21)
key-value/key-value-shared/src/key_value/shared/errors/key_value.py (3)
  • SerializationError (10-11)
  • KeyValueOperationError (6-7)
  • DeserializationError (14-15)
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/base.py (3)
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/base.py (2)
  • EncryptionError (22-23)
  • BaseEncryptionWrapper (26-167)
key-value/key-value-shared/src/key_value/shared/errors/wrappers/encryption.py (1)
  • EncryptionError (4-5)
key-value/key-value-sync/src/key_value/sync/code_gen/protocols/key_value.py (1)
  • KeyValue (173-178)
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/fernet.py (4)
key-value/key-value-aio/tests/stores/wrappers/test_encryption.py (1)
  • fernet (14-15)
key-value/key-value-shared/src/key_value/shared/errors/wrappers/encryption.py (1)
  • EncryptionVersionError (12-13)
key-value/key-value-aio/src/key_value/aio/protocols/key_value.py (1)
  • AsyncKeyValue (175-180)
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/base.py (1)
  • BaseEncryptionWrapper (26-167)
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/base.py (2)
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/base.py (1)
  • BaseEncryptionWrapper (29-167)
key-value/key-value-aio/src/key_value/aio/protocols/key_value.py (1)
  • AsyncKeyValue (175-180)
key-value/key-value-sync/tests/code_gen/stores/wrappers/test_encryption.py (3)
key-value/key-value-shared/src/key_value/shared/errors/wrappers/encryption.py (1)
  • DecryptionError (8-9)
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/fernet.py (1)
  • FernetEncryptionWrapper (14-76)
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/base.py (4)
  • put (153-155)
  • get (133-135)
  • put_many (158-167)
  • get_many (138-140)
key-value/key-value-aio/tests/stores/wrappers/test_encryption.py (3)
key-value/key-value-shared/src/key_value/shared/errors/wrappers/encryption.py (1)
  • DecryptionError (8-9)
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/fernet.py (1)
  • FernetEncryptionWrapper (11-86)
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/base.py (4)
  • put (153-155)
  • get (133-135)
  • put_many (158-167)
  • get_many (138-140)
🔇 Additional comments (17)
key-value/key-value-shared/src/key_value/shared/errors/base.py (1)

23-25: LGTM!

Storing the composed message in self.message before passing it to the parent exception is a good enhancement. This allows callers to access the final formatted message via the message attribute while maintaining backward compatibility with standard exception behavior.

key-value/key-value-shared/src/key_value/shared/errors/wrappers/encryption.py (1)

10-13: LGTM!

The new EncryptionVersionError exception follows the established pattern and provides clear semantics for version mismatch errors.

key-value/key-value-aio/src/key_value/aio/wrappers/encryption/fernet.py (3)

55-66: LGTM!

The validation logic correctly enforces mutual exclusivity and required parameters with clear error messages.


68-78: LGTM!

The encryption and decryption functions correctly implement version checking. The encryption check at lines 69-71 is defensive (since _encrypt_value always passes self.encryption_version), while the decryption check at lines 75-77 is essential for preventing attempts to decrypt data encrypted with newer, potentially incompatible versions.


89-102: LGTM!

The HKDF key derivation has been correctly fixed to base64-encode the derived key (line 102), addressing the previous review feedback. Fernet now receives a properly formatted key.

key-value/key-value-aio/src/key_value/aio/wrappers/encryption/base.py (3)

18-19: LGTM!

The new type aliases improve code readability and make the function signature requirements explicit.


43-69: LGTM!

The refactored constructor successfully decouples encryption strategy from the base wrapper, enabling flexible encryption implementations through the function-based interface.


110-121: LGTM!

The decryption logic correctly extracts and validates the stored encryption version before passing it to the decryption function, enabling version-aware decryption.

key-value/key-value-sync/tests/code_gen/stores/wrappers/test_encryption.py (3)

16-25: LGTM!

The fixtures are well-structured, providing a clean test setup with a Fernet instance for the encryption wrapper.


67-68: LGTM!

The test now correctly uses IsStr(min_length=32) instead of asserting exact Fernet ciphertext, addressing the non-deterministic nature of Fernet tokens and preventing flaky tests.


47-57: LGTM!

The test correctly validates that the wrapper cannot decrypt data encrypted with a newer version, ensuring proper version checking behavior.

key-value/key-value-aio/tests/stores/wrappers/test_encryption.py (3)

13-22: LGTM!

The fixtures are properly structured for async testing, providing a Fernet instance and correctly constructing the async encryption wrapper.


44-70: LGTM!

The async tests correctly implement the same validation logic as the sync tests, with proper async/await usage and non-deterministic-safe assertions.


125-137: LGTM!

The test and its docstring are now consistent, correctly indicating that decryption with the wrong key raises a DecryptionError.

key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/base.py (3)

21-22: Type aliases are clear and helpful.

Good API surface for injecting crypto strategies.


132-167: Sync wrapper methods look correct.

Decrypt-after-read and encrypt-before-write flows are consistent; signatures align with KeyValue protocol.


25-27: Let me verify the generation relationship between aio and sync files:

Let me verify the generation relationship:

The review comment is incorrect about which file to edit; redirect to the original source.

The sync file (key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/base.py) is auto-generated from the aio module and carries a "DO NOT CHANGE" warning. Unasync transforms async code into synchronous counterparts by tokenizing code and re-rendering it into a new file. Editing the generated sync file directly won't persist—changes must be made in the original aio file (key-value/key-value-aio/src/key_value/aio/wrappers/encryption/base.py).

Both modules define an unused EncryptionError(Exception) class (lines 22–24 in aio, lines 25–27 in sync). The actual error raised is DecryptionError, imported from key_value.shared.errors.wrappers.encryption. Delete the local EncryptionError class from the aio file, then regenerate the sync variant via build_sync_library.py.

-class EncryptionError(Exception):
-    """Exception raised when encryption or decryption fails."""
-

Apply this change in key-value/key-value-aio/src/key_value/aio/wrappers/encryption/base.py instead.

Likely an incorrect or invalid review comment.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/fernet.py (1)

68-72: Redundant encryption version check.

The version check at line 69 compares encryption_version (passed from BaseEncryptionWrapper._encrypt_value) against self.encryption_version. Since the base class always passes self.encryption_version, this check evaluates to self.encryption_version > self.encryption_version, which is always False.

The decryption version check (line 75) is useful because the base class passes the version from stored data, which may differ. Consider removing the encryption check or adding a comment explaining it's defensive programming for potential future base class changes.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f60a8d9 and 8fefb88.

📒 Files selected for processing (3)
  • README.md (1 hunks)
  • key-value/key-value-aio/src/key_value/aio/wrappers/encryption/fernet.py (1 hunks)
  • key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/fernet.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • README.md
🧰 Additional context used
🧬 Code graph analysis (2)
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/fernet.py (3)
key-value/key-value-shared/src/key_value/shared/errors/wrappers/encryption.py (1)
  • EncryptionVersionError (12-13)
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/base.py (1)
  • BaseEncryptionWrapper (29-167)
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/fernet.py (1)
  • _generate_encryption_key (89-103)
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/fernet.py (3)
key-value/key-value-shared/src/key_value/shared/errors/wrappers/encryption.py (1)
  • EncryptionVersionError (12-13)
key-value/key-value-aio/src/key_value/aio/protocols/key_value.py (1)
  • AsyncKeyValue (175-180)
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/base.py (1)
  • BaseEncryptionWrapper (26-167)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: test_quick (3.10, windows-latest, key-value/key-value-sync)
  • GitHub Check: test_quick (3.10, windows-latest, key-value/key-value-aio)
  • GitHub Check: test_quick (3.10, ubuntu-latest, key-value/key-value-sync)
  • GitHub Check: test_quick (3.10, ubuntu-latest, key-value/key-value-aio)
🔇 Additional comments (2)
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/fernet.py (1)

89-103: LGTM! Key derivation implementation is secure.

The PBKDF2 implementation correctly uses 1.2 million iterations (exceeding OWASP's 600k recommendation), SHA-256, and properly base64-encodes the derived key for Fernet compatibility.

key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/fernet.py (1)

1-90: LGTM! Implementation is correct and secure.

The FernetEncryptionWrapper implementation is sound:

  • Input validation properly enforces mutual exclusivity and required parameters
  • PBKDF2 configuration (1.2M iterations, SHA256) exceeds current security recommendations
  • The critical bug from the previous review (Fernet key encoding) has been correctly addressed—base64.urlsafe_b64encode() is now used at line 90
  • Version checks prevent decryption of data encrypted with unsupported future versions while maintaining backward compatibility
  • Closure over fernet instance in the inner functions is appropriate

Note: Since this is auto-generated code, any future refinements should be made to the source file referenced in the header.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/base.py (1)

108-116: TypeError bypasses raise_on_decryption_error flag.

Lines 108-116 raise TypeError directly when the encryption version is missing or has the wrong type, but these exceptions are not caught by the try-except block at lines 118-132. This means the raise_on_decryption_error flag is bypassed for corrupted metadata, inconsistent with the behavior for decryption failures.

Based on past review comments, these validation errors should respect the configured error-handling policy.

Apply this diff to route validation errors through the error handler:

-        if _ENCRYPTION_VERSION_KEY not in value:
-            msg = "Corrupted data: missing encryption version"
-            raise TypeError(msg)
-
-        encryption_version = value[_ENCRYPTION_VERSION_KEY]
-        if not isinstance(encryption_version, int):
-            # Corrupted data, return as-is
-            msg = f"Corrupted data: expected int, got {type(encryption_version)}"
-            raise TypeError(msg)
+        if _ENCRYPTION_VERSION_KEY not in value:
+            msg = "Corrupted data: missing encryption version"
+            if self.raise_on_decryption_error:
+                raise DecryptionError(msg)
+            return None
+
+        encryption_version = value[_ENCRYPTION_VERSION_KEY]
+        if not isinstance(encryption_version, int):
+            msg = f"Corrupted data: expected int, got {type(encryption_version)}"
+            if self.raise_on_decryption_error:
+                raise DecryptionError(msg)
+            return None
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/fernet.py (1)

60-67: Reject whitespace-only source_material/salt (strengthen validation)

Whitespace-only inputs currently pass truthiness checks, weakening KDF inputs. Treat them as invalid.

-        if fernet is None:
-            if not source_material:
-                msg = "Must provide either fernet or source_material"
-                raise ValueError(msg)
-            if not salt:
-                msg = "Must provide a salt"
-                raise ValueError(msg)
-            fernet = Fernet(key=_generate_encryption_key(source_material=source_material, salt=salt))
+        if fernet is None:
+            if source_material is None or salt is None:
+                raise ValueError("Must provide source_material and salt when fernet is not provided")
+            if not source_material.strip():
+                raise ValueError("source_material cannot be empty or whitespace")
+            if not salt.strip():
+                raise ValueError("salt cannot be empty or whitespace")
+            fernet = Fernet(key=_generate_encryption_key(source_material=source_material, salt=salt))
🧹 Nitpick comments (11)
key-value/key-value-sync/tests/code_gen/stores/wrappers/test_encryption.py (1)

48-58: Consider testing version mismatch without mutating wrapper state.

The test directly mutates store.encryption_version to simulate version incompatibility. While this works, it couples the test to the internal implementation (mutable attribute).

A more robust approach would instantiate two wrappers or test with pre-encrypted payloads from different versions.

Example alternative:

def test_encryption_with_wrong_encryption_version(self, memory_store: MemoryStore, fernet: Fernet):
    """Test that decryption fails when encryption version is not supported."""
    store_v2 = FernetEncryptionWrapper(key_value=memory_store, fernet=fernet)
    store_v2.encryption_version = 2  # Simulate future version
    original_value = {"test": "value"}
    store_v2.put(collection="test", key="test", value=original_value)
    
    store_v1 = FernetEncryptionWrapper(key_value=memory_store, fernet=fernet)
    # store_v1 has version 1 by default
    
    with pytest.raises(DecryptionError):
        store_v1.get(collection="test", key="test")
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/fernet.py (2)

55-58: Clarify mixed-argument error message

Wording is awkward. Prefer explicitly disallowing fernet alongside either source_material or salt.

-            if source_material or salt:
-                msg = "Cannot provide both fernet and source_material and salt"
+            if source_material or salt:
+                msg = "Cannot provide fernet together with source_material or salt"
                 raise ValueError(msg)

8-9: Extract PBKDF2 iteration count into a named constant

Avoid magic numbers and make iteration tuning explicit.

 ENCRYPTION_VERSION = 1
+
+# PBKDF2-HMAC-SHA256 iteration count used for key derivation.
+# Keep high enough for security while maintaining acceptable latency.
+KDF_ITERATIONS = 1_200_000
@@
-    pbkdf2 = PBKDF2HMAC(
+    pbkdf2 = PBKDF2HMAC(
         algorithm=hashes.SHA256(),
         length=32,
         salt=salt.encode(),
-        iterations=1_200_000,
+        iterations=KDF_ITERATIONS,
     ).derive(key_material=source_material.encode())

Also applies to: 87-101

key-value/key-value-aio/src/key_value/aio/wrappers/encryption/base.py (6)

29-41: Make docstring algorithm-agnostic

Base wrapper is pluggable now; avoid hard-coding “Fernet” in docs.

-    This wrapper encrypts the JSON-serialized value using Fernet (symmetric encryption)
+    This wrapper encrypts the JSON-serialized value using an injected symmetric cipher

83-85: Update inline comment to reflect pluggable encryption

References “Fernet” but uses injected callable.

-        # Encrypt with Fernet
+        # Encrypt with provided encryption_fn

122-124: Update inline comment to reflect pluggable decryption

References “Fernet” but uses injected callable.

-            # Decrypt with Fernet
+            # Decrypt with provided decryption_fn

118-124: Use strict base64 decoding to catch corruption earlier

Enable validation to reject non-base64 characters instead of silently ignoring them.

-            encrypted_bytes: bytes = base64.b64decode(base64_str)
+            encrypted_bytes: bytes = base64.b64decode(base64_str, validate=True)

16-16: Remove unused _ENCRYPTION_VERSION constant

Dead code after moving to per-instance encryption_version.

-_ENCRYPTION_VERSION = 1

22-24: Remove unused local EncryptionError

Shadowing shared error types; not referenced.

-class EncryptionError(Exception):
-    """Exception raised when encryption or decryption fails."""
key-value/key-value-aio/tests/stores/wrappers/test_encryption.py (2)

45-47: Fix test docstring wording

It’s testing decryption with mismatched version.

-        """Test that encryption fails with the wrong encryption version."""
+        """Test that decryption fails with the wrong encryption version."""

141-164: Strengthen key-derivation assertions

Also verify the key decodes via urlsafe base64 to exactly 32 bytes.

 def test_key_generation():
@@
-    key = _generate_encryption_key(source_material=source_material, salt=salt)
+    key = _generate_encryption_key(source_material=source_material, salt=salt)
+    import base64
+    assert len(base64.urlsafe_b64decode(key)) == 32
@@
-    key = _generate_encryption_key(source_material=source_material, salt=salt)
+    key = _generate_encryption_key(source_material=source_material, salt=salt)
+    assert len(base64.urlsafe_b64decode(key)) == 32
@@
-    key = _generate_encryption_key(source_material=source_material, salt=salt)
+    key = _generate_encryption_key(source_material=source_material, salt=salt)
+    assert len(base64.urlsafe_b64decode(key)) == 32
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8fefb88 and 2280fac.

📒 Files selected for processing (6)
  • key-value/key-value-aio/src/key_value/aio/wrappers/encryption/base.py (4 hunks)
  • key-value/key-value-aio/src/key_value/aio/wrappers/encryption/fernet.py (1 hunks)
  • key-value/key-value-aio/tests/stores/wrappers/test_encryption.py (5 hunks)
  • key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/base.py (4 hunks)
  • key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/fernet.py (1 hunks)
  • key-value/key-value-sync/tests/code_gen/stores/wrappers/test_encryption.py (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
key-value/key-value-sync/tests/code_gen/stores/wrappers/test_encryption.py (3)
key-value/key-value-shared/src/key_value/shared/errors/wrappers/encryption.py (1)
  • DecryptionError (8-9)
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/fernet.py (2)
  • FernetEncryptionWrapper (14-74)
  • _generate_encryption_key (77-88)
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/base.py (4)
  • put (155-157)
  • get (135-137)
  • put_many (160-169)
  • get_many (140-142)
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/fernet.py (3)
key-value/key-value-shared/src/key_value/shared/errors/wrappers/encryption.py (1)
  • EncryptionVersionError (12-13)
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/base.py (1)
  • BaseEncryptionWrapper (29-169)
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/fernet.py (4)
  • FernetEncryptionWrapper (11-84)
  • _generate_encryption_key (87-101)
  • encrypt_with_fernet (69-70)
  • decrypt_with_fernet (72-76)
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/base.py (3)
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/base.py (3)
  • EncryptionError (25-26)
  • BaseEncryptionWrapper (29-169)
  • _decrypt_value (94-132)
key-value/key-value-shared/src/key_value/shared/errors/wrappers/encryption.py (1)
  • EncryptionError (4-5)
key-value/key-value-aio/src/key_value/aio/protocols/key_value.py (1)
  • AsyncKeyValue (175-180)
key-value/key-value-aio/tests/stores/wrappers/test_encryption.py (3)
key-value/key-value-shared/src/key_value/shared/errors/wrappers/encryption.py (1)
  • DecryptionError (8-9)
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/fernet.py (2)
  • FernetEncryptionWrapper (11-84)
  • _generate_encryption_key (87-101)
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/base.py (4)
  • put (155-157)
  • get (135-137)
  • put_many (160-169)
  • get_many (140-142)
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/base.py (2)
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/base.py (2)
  • BaseEncryptionWrapper (26-169)
  • _decrypt_value (94-132)
key-value/key-value-sync/src/key_value/sync/code_gen/protocols/key_value.py (1)
  • KeyValue (173-178)
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/fernet.py (4)
key-value/key-value-aio/tests/stores/wrappers/test_encryption.py (1)
  • fernet (15-16)
key-value/key-value-shared/src/key_value/shared/errors/wrappers/encryption.py (1)
  • EncryptionVersionError (12-13)
key-value/key-value-aio/src/key_value/aio/protocols/key_value.py (1)
  • AsyncKeyValue (175-180)
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/base.py (1)
  • BaseEncryptionWrapper (26-169)
🔇 Additional comments (5)
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/fernet.py (2)

14-74: LGTM! Clean implementation of two-mode initialization.

The wrapper properly validates mutual exclusivity between fernet and source_material/salt parameters, and the closure-based encryption/decryption functions correctly defer access to self.encryption_version until runtime (after initialization completes).


77-88: LGTM! Secure key derivation with appropriate iteration count.

The function correctly uses PBKDF2-HMAC-SHA256 with 1.2 million iterations (exceeds OWASP 2023 recommendation of 600K+) and properly encodes the output as urlsafe base64 for Fernet compatibility.

key-value/key-value-sync/tests/code_gen/stores/wrappers/test_encryption.py (1)

133-156: LGTM! Comprehensive test of key derivation determinism.

The test properly verifies that _generate_encryption_key produces deterministic outputs and that different source materials or salts yield distinct keys.

key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/base.py (2)

21-22: LGTM! Clear type aliases for pluggable encryption.

The function type definitions properly capture the encryption/decryption signatures, with DecryptionFn correctly including the encryption_version parameter.


46-72: LGTM! Clean refactoring to function-based encryption.

The initialization properly accepts pluggable encryption/decryption functions and stores them for later use, enabling support for different encryption backends beyond Fernet.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
key-value/key-value-shared/src/key_value/shared/errors/wrappers/encryption.py (1)

4-5: Import EncryptionError from shared module instead of defining locally in aio and sync wrappers.

The key-value-aio and key-value-sync modules define their own EncryptionError extending Exception (aio/base.py:21, sync/base.py:23), while the shared module defines EncryptionError extending KeyValueOperationError (shared/encryption.py:4). This inconsistency creates conflicting exception hierarchies.

Both aio and sync modules already import other error classes from shared (e.g., DecryptionError, EncryptionVersionError), so they should import EncryptionError from key_value.shared.errors.wrappers.encryption to maintain a consistent exception hierarchy across the codebase.

♻️ Duplicate comments (3)
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/base.py (2)

105-112: Consider error handling consistency.

Same issue as the aio version: structural validation errors (lines 105-112) bypass the raise_on_decryption_error flag. Since this is auto-generated, fixing the original will resolve this.


23-24: Import EncryptionError from shared instead of defining locally.

Same issue as the aio version: a local EncryptionError is defined here (lines 23-24) instead of importing from key_value.shared.errors.wrappers.encryption where it's already defined. Since this is auto-generated from the aio version, fixing the original key-value-aio/src/key_value/aio/wrappers/encryption/base.py will resolve this.

key-value/key-value-aio/src/key_value/aio/wrappers/encryption/fernet.py (1)

77-81: Verify version check logic is intentional.

The version check uses > (line 78), meaning it only rejects encryption versions newer than the wrapper's version, but accepts older versions. This allows a version 2 wrapper to decrypt version 1 data (backwards compatibility), but prevents a version 1 wrapper from attempting version 2 data.

Is this asymmetric behavior intentional? If so, consider clarifying the error message from "is not supported" to "is newer than supported version".

🧹 Nitpick comments (2)
key-value/key-value-shared/src/key_value/shared/errors/wrappers/encryption.py (1)

16-17: Consider extending DecryptionError for better semantic hierarchy.

Since CorruptedEncryptionDataError is raised when corrupted data is detected during decryption, it would be more semantically appropriate to extend DecryptionError instead of EncryptionError directly.

Apply this diff to improve the exception hierarchy:

-class CorruptedEncryptionDataError(EncryptionError):
+class CorruptedEncryptionDataError(DecryptionError):
     """Exception raised when the encrypted data is corrupted."""
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/base.py (1)

105-113: Consider error handling consistency.

Lines 106-113 raise CorruptedEncryptionDataError for missing or invalid encryption_version outside the try-except block (lines 115-127), which means these errors bypass the raise_on_decryption_error flag. This is inconsistent with other decryption failures (like corrupted base64 at line 116) that respect the flag.

If this is intentional—treating structural corruption differently from decryption failures—consider adding a comment explaining the distinction. Otherwise, these checks could be moved inside the try-except to unify error handling.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2280fac and 88130e7.

📒 Files selected for processing (8)
  • key-value/key-value-aio/src/key_value/aio/wrappers/encryption/base.py (1 hunks)
  • key-value/key-value-aio/src/key_value/aio/wrappers/encryption/fernet.py (1 hunks)
  • key-value/key-value-aio/tests/stores/wrappers/test_encryption.py (5 hunks)
  • key-value/key-value-shared/src/key_value/shared/errors/wrappers/encryption.py (1 hunks)
  • key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (1 hunks)
  • key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/base.py (1 hunks)
  • key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/fernet.py (1 hunks)
  • key-value/key-value-sync/tests/code_gen/stores/wrappers/test_encryption.py (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (7)
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/fernet.py (3)
key-value/key-value-shared/src/key_value/shared/errors/wrappers/encryption.py (1)
  • EncryptionVersionError (12-13)
key-value/key-value-aio/src/key_value/aio/protocols/key_value.py (1)
  • AsyncKeyValue (175-180)
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/base.py (1)
  • BaseEncryptionWrapper (25-164)
key-value/key-value-sync/tests/code_gen/stores/wrappers/test_encryption.py (3)
key-value/key-value-aio/tests/stores/wrappers/test_encryption.py (11)
  • fernet (15-16)
  • store (22-23)
  • test_encryption_encrypts_value (25-43)
  • test_encryption_with_wrong_encryption_version (45-55)
  • test_encryption_with_string_key (57-71)
  • test_encryption_many_operations (73-88)
  • test_decryption_handles_unencrypted_data (90-98)
  • test_decryption_handles_corrupted_data (100-111)
  • test_decryption_ignores_corrupted_data (113-124)
  • test_decryption_with_multi_fernet (126-137)
  • test_decryption_with_wrong_key_raises_error (139-151)
key-value/key-value-shared/src/key_value/shared/errors/wrappers/encryption.py (1)
  • DecryptionError (8-9)
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/fernet.py (2)
  • FernetEncryptionWrapper (16-79)
  • _generate_encryption_key (82-93)
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/fernet.py (2)
key-value/key-value-shared/src/key_value/shared/errors/wrappers/encryption.py (1)
  • EncryptionVersionError (12-13)
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/base.py (1)
  • BaseEncryptionWrapper (27-163)
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/base.py (3)
key-value/key-value-shared/src/key_value/shared/errors/key_value.py (1)
  • SerializationError (10-11)
key-value/key-value-shared/src/key_value/shared/errors/wrappers/encryption.py (3)
  • CorruptedEncryptionDataError (16-17)
  • DecryptionError (8-9)
  • EncryptionError (4-5)
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/base.py (4)
  • EncryptionError (21-22)
  • BaseEncryptionWrapper (25-164)
  • _encrypt_value (61-91)
  • _decrypt_value (93-127)
key-value/key-value-shared/src/key_value/shared/errors/wrappers/encryption.py (2)
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/base.py (1)
  • EncryptionError (21-22)
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/base.py (1)
  • EncryptionError (23-24)
key-value/key-value-aio/tests/stores/wrappers/test_encryption.py (3)
key-value/key-value-shared/src/key_value/shared/errors/wrappers/encryption.py (1)
  • DecryptionError (8-9)
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/fernet.py (2)
  • FernetEncryptionWrapper (13-89)
  • _generate_encryption_key (92-106)
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/base.py (4)
  • put (150-152)
  • get (130-132)
  • put_many (155-164)
  • get_many (135-137)
key-value/key-value-aio/src/key_value/aio/wrappers/encryption/base.py (4)
key-value/key-value-shared/src/key_value/shared/errors/key_value.py (1)
  • SerializationError (10-11)
key-value/key-value-shared/src/key_value/shared/errors/wrappers/encryption.py (3)
  • CorruptedEncryptionDataError (16-17)
  • DecryptionError (8-9)
  • EncryptionError (4-5)
key-value/key-value-aio/src/key_value/aio/protocols/key_value.py (1)
  • AsyncKeyValue (175-180)
key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/base.py (4)
  • EncryptionError (23-24)
  • BaseEncryptionWrapper (27-163)
  • _encrypt_value (63-90)
  • _decrypt_value (92-126)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: test_quick (3.10, windows-latest, key-value/key-value-sync)
  • GitHub Check: test_quick (3.10, windows-latest, key-value/key-value-aio)
  • GitHub Check: test_quick (3.10, ubuntu-latest, key-value/key-value-aio)
  • GitHub Check: test_quick (3.10, ubuntu-latest, key-value/key-value-sync)
🔇 Additional comments (10)
key-value/key-value-shared/src/key_value/shared/utils/managed_entry.py (1)

86-91: LGTM! Good optimization for storage efficiency.

Adding compact separators reduces the serialized JSON size by removing unnecessary whitespace. This is particularly beneficial given the PR's encryption focus, as it minimizes the size of data being encrypted and stored.

key-value/key-value-shared/src/key_value/shared/errors/wrappers/encryption.py (1)

12-14: LGTM!

The new exception class is well-defined with a clear docstring. The hierarchy (extending EncryptionError) is appropriate for version-related encryption errors.

key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/fernet.py (3)

50-62: LGTM! Validation logic is solid.

The mutual exclusivity checks and empty-string validation (using .strip()) properly prevent misconfiguration.


67-70: Verify version check logic is intentional.

The version check uses > (line 68), meaning it only rejects encryption versions newer than the wrapper's version, but accepts older versions. This allows a version 2 wrapper to decrypt version 1 data (backwards compatibility), but prevents a version 1 wrapper from attempting version 2 data.

Is this asymmetric behavior intentional? If so, consider clarifying the error message from "is not supported" to "is newer than supported version".


82-93: LGTM! Key derivation is secure.

PBKDF2 with 1.2M iterations and proper base64 encoding for Fernet compatibility.

key-value/key-value-aio/src/key_value/aio/wrappers/encryption/base.py (1)

129-164: LGTM! Delegation pattern is correct.

The wrapper methods properly delegate to the underlying store while applying encryption/decryption.

key-value/key-value-sync/tests/code_gen/stores/wrappers/test_encryption.py (1)

17-169: LGTM! Comprehensive test coverage.

The tests cover encryption, decryption, version mismatches, MultiFernet support, corrupted data handling, and key generation. Using IsStr(min_length=32) in snapshots (line 69) correctly avoids hardcoding non-deterministic encrypted values.

key-value/key-value-aio/src/key_value/aio/wrappers/encryption/fernet.py (2)

60-72: LGTM! Validation logic is solid.

The mutual exclusivity checks and empty-string validation (using .strip()) properly prevent misconfiguration.


92-106: LGTM! Key derivation is secure.

PBKDF2 with 1.2M iterations and proper base64 encoding for Fernet compatibility.

key-value/key-value-aio/tests/stores/wrappers/test_encryption.py (1)

14-177: LGTM! Comprehensive test coverage.

The tests cover encryption, decryption, version mismatches, MultiFernet support, corrupted data handling, and key generation. Using IsStr(min_length=32) in snapshots (line 69) correctly avoids hardcoding non-deterministic encrypted values.

@strawgate strawgate merged commit 6a409c6 into main Oct 24, 2025
73 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants