2 Commits
Author SHA1 Message Date
dsql 16205e810a fix: deepcopy in reencrypt/decrypt_record so input is not mutated
both used record.copy() (shallow), leaving unencrypted mutable fields shared between the input and the returned dict, violating the documented 'input is not mutated' contract. switched to copy.deepcopy.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-28 17:18:28 -04:00
dsql 313b0c7d56 fix: forward password to private-key fingerprinting (v0.1.1)
get_rsa_key_fingerprint(is_private=True) called load_pem_private_key(password=None),
so an encrypted private key raised a raw TypeError. add an optional password param
forwarded to the load; unencrypted keys ignore it.

verified: encrypted private key fingerprints with its password and matches the
public key's fingerprint; missing password still raises.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-28 15:53:04 -04:00
3 changed files with 17 additions and 8 deletions
+2 -2
View File
@@ -11,13 +11,13 @@ and storage-agnostic.
`requirements.txt`: `requirements.txt`:
``` ```
envelope_crypto @ git+ssh://git@git.rethinkstudios.io/rethink-public/envelope_crypto.git@v0.1.0 envelope_crypto @ git+ssh://git@git.rethinkstudios.io/rethink-public/envelope_crypto.git@v0.1.1
``` ```
Direct: Direct:
```bash ```bash
pip install "envelope_crypto @ git+ssh://git@git.rethinkstudios.io/rethink-public/envelope_crypto.git@v0.1.0" pip install "envelope_crypto @ git+ssh://git@git.rethinkstudios.io/rethink-public/envelope_crypto.git@v0.1.1"
``` ```
Requires `cryptography` (pulled transitively). Requires `cryptography` (pulled transitively).
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "envelope_crypto" name = "envelope_crypto"
version = "0.1.0" version = "0.1.1"
description = "Envelope encryption (RSA-OAEP wrapped AES-256-GCM) for dict records — config-free, storage-agnostic, installable." description = "Envelope encryption (RSA-OAEP wrapped AES-256-GCM) for dict records — config-free, storage-agnostic, installable."
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = [ dependencies = [
+14 -5
View File
@@ -54,6 +54,7 @@ function variants are the same functions — use whichever fits your storage.
""" """
import os import os
import copy
import json import json
import base64 import base64
import hashlib import hashlib
@@ -147,9 +148,15 @@ class EnvelopeCrypto:
return key return key
def get_rsa_key_fingerprint( def get_rsa_key_fingerprint(
self, key_path_or_data: str, is_private: bool = False, is_file: bool = True self, key_path_or_data: str, is_private: bool = False, is_file: bool = True,
password: Optional[str] = None,
) -> str: ) -> str:
"""return a base64 SHA-256 fingerprint of an RSA key for identification""" """return a base64 SHA-256 fingerprint of an RSA key for identification
for an encrypted private key (is_private=True), pass its `password`; an
unencrypted key ignores it. fingerprinting always uses the public half, so a
private and its public key produce the same fingerprint.
"""
if is_file: if is_file:
with open(key_path_or_data, "rb") as key_file: with open(key_path_or_data, "rb") as key_file:
key_data = key_file.read() key_data = key_file.read()
@@ -161,7 +168,9 @@ class EnvelopeCrypto:
) )
if is_private: if is_private:
private_key = serialization.load_pem_private_key(key_data, password=None) private_key = serialization.load_pem_private_key(
key_data, password=password.encode() if password else None
)
public_key = private_key.public_key() public_key = private_key.public_key()
else: else:
try: try:
@@ -305,7 +314,7 @@ class EnvelopeCrypto:
if not self.master_key: if not self.master_key:
raise ValueError("destination not initialized with data key") raise ValueError("destination not initialized with data key")
result = record.copy() result = copy.deepcopy(record)
for key, value in record.items(): for key, value in record.items():
if isinstance(value, dict) and value.get("secure") is True and "iv" in value and "data" in value: if isinstance(value, dict) and value.get("secure") is True and "iv" in value and "data" in value:
result[key] = self.encrypt_data(source_crypto.decrypt_data(value)) result[key] = self.encrypt_data(source_crypto.decrypt_data(value))
@@ -353,7 +362,7 @@ def decrypt_record(crypto: EnvelopeCrypto, record, traversal_level: int = 2) ->
if not isinstance(record, dict): if not isinstance(record, dict):
return record return record
result = record.copy() result = copy.deepcopy(record)
for key, value in record.items(): for key, value in record.items():
if isinstance(value, dict) and value.get("secure") is True and "iv" in value and "data" in value: if isinstance(value, dict) and value.get("secure") is True and "iv" in value and "data" in value:
try: try: