fix: is_encrypted_record never false-negatives at odd traversal_level; encrypt_data rejects nested non-str keys

Signed-off-by: disqualifier <dev@disqualifier.me>
This commit is contained in:
2026-07-03 19:03:53 -04:00
parent dfd794159e
commit dc8f80c690
3 changed files with 54 additions and 29 deletions
+13 -11
View File
@@ -11,18 +11,18 @@ and storage-agnostic.
`requirements.txt`:
```
envelope_crypto @ git+ssh://git@git.rethinkstudios.io/rethink-public/envelope_crypto.git@v0.1.8
envelope_crypto @ git+ssh://git@git.rethinkstudios.io/rethink-public/envelope_crypto.git@v0.1.10
```
Direct:
```bash
pip install "envelope_crypto @ git+ssh://git@git.rethinkstudios.io/rethink-public/envelope_crypto.git@v0.1.8"
pip install "envelope_crypto @ git+ssh://git@git.rethinkstudios.io/rethink-public/envelope_crypto.git@v0.1.10"
```
Requires `cryptography` (pulled transitively).
Drop the `@v0.1.8` suffix from the line above to install the latest unpinned.
Drop the `@v0.1.10` suffix from the line above to install the latest unpinned.
## First-time setup
@@ -80,10 +80,12 @@ enc = crypto.encrypt_data({"ssn": "..."}) # -> {"secure": True, "iv": ...,
plain = crypto.decrypt_data(enc) # -> {"ssn": "..."}
```
Dict keys must be `str`. `encrypt_data` raises `TypeError` on a non-str key (e.g. an
int-keyed dict of Discord snowflakes) instead of silently stringifying it — the
underlying JSON encoding has no other key type, so a coerced key would come back out
of `decrypt_data` as a `str` and no longer match the original lookup key.
Dict keys must be `str`, at any nesting depth (including a dict nested inside a list
or tuple). `encrypt_data` raises `TypeError` on a non-str key anywhere in the payload
(e.g. an int-keyed dict of Discord snowflakes, even nested a few levels down) instead
of silently stringifying it — the underlying JSON encoding has no other key type, so a
coerced key would come back out of `decrypt_data` as a `str` and no longer match the
original lookup key.
For whole records: `decrypt_record(crypto, doc)` decrypts every `{secure, iv, data}`
field (nested up to `traversal_level`, default 2); `is_encrypted_record(doc)` reports
@@ -98,11 +100,11 @@ if is_encrypted_record(doc):
doc = decrypt_record(crypto, doc)
```
`is_encrypted_record` falls back to an unbounded-depth scan once `traversal_level` is
exhausted, so it reliably reports `True` for a blob left behind by a shallower
`is_encrypted_record` always falls back to an unbounded-depth scan whenever its bounded
pass finds nothing, so it reliably reports `True` for a blob left behind by a shallower
`decrypt_record`/`reencrypt` call — safe to use as a leftover-detecting audit after
rotation, regardless of how deep the blob is nested, including inside a list or
tuple at any depth.
rotation, regardless of how deep the blob is nested (including inside a list or tuple
at any depth) and regardless of which `traversal_level` you pass; it never false-negatives.
Naming aliases (same objects): `EnvelopeCrypto` = `DocumentCrypto` = `RecordCrypto`
= `PCICrypto` (deprecated legacy alias). `decrypt_record` = `decrypt_document` =