sealed-fields 0.3.0__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sealed_fields-0.3.0 → sealed_fields-0.4.0}/PKG-INFO +1 -1
- {sealed_fields-0.3.0 → sealed_fields-0.4.0}/pyproject.toml +1 -1
- {sealed_fields-0.3.0 → sealed_fields-0.4.0}/src/sealed_fields/__init__.py +7 -3
- {sealed_fields-0.3.0 → sealed_fields-0.4.0}/tests/test_sealed_fields.py +33 -4
- {sealed_fields-0.3.0 → sealed_fields-0.4.0}/.gitignore +0 -0
- {sealed_fields-0.3.0 → sealed_fields-0.4.0}/README.md +0 -0
- {sealed_fields-0.3.0 → sealed_fields-0.4.0}/src/sealed_fields/sqlalchemy.py +0 -0
- {sealed_fields-0.3.0 → sealed_fields-0.4.0}/tests/test_sqlalchemy.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sealed-fields
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Field-level encryption for PII at rest: AES-256-GCM with versioned key rotation, key namespaces, HMAC blind indexes, and row helpers with systemic-failure detection. Wire-compatible with the sealed-fields npm package.
|
|
5
5
|
Project-URL: Homepage, https://github.com/mpointer/sealed-fields
|
|
6
6
|
Project-URL: Issues, https://github.com/mpointer/sealed-fields/issues
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sealed-fields"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "Field-level encryption for PII at rest: AES-256-GCM with versioned key rotation, key namespaces, HMAC blind indexes, and row helpers with systemic-failure detection. Wire-compatible with the sealed-fields npm package."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -313,6 +313,8 @@ class SealedFields:
|
|
|
313
313
|
return value
|
|
314
314
|
raise UnknownKeyVersionError(version, ns)
|
|
315
315
|
raw = _decode_blob(m.group(2))
|
|
316
|
+
if len(raw) < NONCE_BYTES + TAG_BYTES:
|
|
317
|
+
raise SealedFieldsError("unseal: token is too short to be a sealed value.")
|
|
316
318
|
nonce, ct = raw[:NONCE_BYTES], raw[NONCE_BYTES:]
|
|
317
319
|
return AESGCM(key).decrypt(nonce, ct, version.encode("ascii")).decode("utf-8")
|
|
318
320
|
if ring is not None and self._legacy:
|
|
@@ -379,8 +381,8 @@ class SealedFields:
|
|
|
379
381
|
v = out.get(field)
|
|
380
382
|
if not isinstance(v, str) or v == "":
|
|
381
383
|
continue
|
|
382
|
-
|
|
383
|
-
|
|
384
|
+
# Seal unconditionally: a value that merely looks like a token is
|
|
385
|
+
# untrusted input, not ciphertext. Call seal_row once per write.
|
|
384
386
|
out[field] = self.seal(v, namespace)
|
|
385
387
|
return out
|
|
386
388
|
|
|
@@ -393,7 +395,9 @@ class SealedFields:
|
|
|
393
395
|
attempts = 0
|
|
394
396
|
for field in (*fields, *json_fields):
|
|
395
397
|
v = out.get(field)
|
|
396
|
-
if not isinstance(v, str) or v == ""
|
|
398
|
+
if not isinstance(v, str) or v == "":
|
|
399
|
+
continue
|
|
400
|
+
if not (is_sealed(v) or any(_match_legacy(v, f) for f in self._legacy)):
|
|
397
401
|
continue
|
|
398
402
|
attempts += 1
|
|
399
403
|
try:
|
|
@@ -154,11 +154,20 @@ class TestRows:
|
|
|
154
154
|
assert out["extra"] == "untouched"
|
|
155
155
|
assert row["name"] == "Ada"
|
|
156
156
|
|
|
157
|
-
def
|
|
157
|
+
def test_token_shaped_input_is_sealed_not_trusted(self):
|
|
158
|
+
# Regression: seal_row used to skip values that looked sealed, storing
|
|
159
|
+
# attacker-chosen "tokens" unencrypted and tripping SystemicUnsealError.
|
|
158
160
|
sf = make_sf()
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
161
|
+
evil = "v1:" + base64.urlsafe_b64encode(os.urandom(30)).decode().rstrip("=")
|
|
162
|
+
assert is_sealed(evil)
|
|
163
|
+
out = sf.seal_row({"name": evil}, self.FIELDS)
|
|
164
|
+
assert out["name"] != evil
|
|
165
|
+
assert sf.unseal_row(out, self.FIELDS)["name"] == evil
|
|
166
|
+
rows = [
|
|
167
|
+
sf.seal_row({"name": "v1:" + base64.urlsafe_b64encode(os.urandom(30)).decode()}, self.FIELDS)
|
|
168
|
+
for _ in range(5)
|
|
169
|
+
]
|
|
170
|
+
sf.unseal_rows(rows, self.FIELDS) # must not raise
|
|
162
171
|
|
|
163
172
|
def test_mixed_plaintext_rows_mid_migration(self):
|
|
164
173
|
sf = make_sf()
|
|
@@ -245,6 +254,19 @@ class TestLegacy:
|
|
|
245
254
|
token = self._iv_ct_tag("x", self._key_bytes(KEY_A))
|
|
246
255
|
assert sf.unseal(token) == token
|
|
247
256
|
|
|
257
|
+
def test_row_helpers_decode_legacy_tokens(self):
|
|
258
|
+
sf = make_sf(legacy=["iv-ct-tag", "v1-iv-tag-ct"], logger=lambda m: None)
|
|
259
|
+
k = self._key_bytes(KEY_A)
|
|
260
|
+
row = {"a": self._iv_ct_tag("one", k), "b": self._v1_iv_tag_ct("two", k)}
|
|
261
|
+
assert sf.unseal_row(row, ["a", "b"]) == {"a": "one", "b": "two"}
|
|
262
|
+
|
|
263
|
+
def test_row_helper_legacy_failures_count_as_systemic(self):
|
|
264
|
+
sf = make_sf(legacy=["iv-ct-tag"], logger=lambda m: None)
|
|
265
|
+
rows = [{"a": self._iv_ct_tag(f"p{i}", self._key_bytes(KEY_B))} for i in range(5)]
|
|
266
|
+
assert sf.unseal_row(rows[0], ["a"])["a"] is None
|
|
267
|
+
with pytest.raises(SystemicUnsealError):
|
|
268
|
+
sf.unseal_rows(rows, ["a"])
|
|
269
|
+
|
|
248
270
|
def test_ordinary_colon_text_not_mistaken(self):
|
|
249
271
|
sf = make_sf(legacy=["iv-ct-tag", "v1-iv-tag-ct"])
|
|
250
272
|
for s in ["a:b:c", "12:30:45", "v1:looks:like:legacy"]:
|
|
@@ -323,3 +345,10 @@ class TestCrossLanguage:
|
|
|
323
345
|
for name, token in VECTORS["tokens"].items():
|
|
324
346
|
assert sf.unseal(token) == VECTORS["plaintext"], name
|
|
325
347
|
assert sf.blind_index(VECTORS["blindIndex"]["value"]) == VECTORS["blindIndex"]["hex"]
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def test_short_canonical_token_rejected():
|
|
351
|
+
sf = make_sf()
|
|
352
|
+
for t in ["v1:AAAAAA", "v1:AAAAAAAAAAA", "v1:" + "A" * 36]:
|
|
353
|
+
with pytest.raises(SealedFieldsError, match="too short"):
|
|
354
|
+
sf.unseal(t)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|