From e5c3547c51f1e264aa6ce05d5fbaca61dd3ed127 Mon Sep 17 00:00:00 2001 From: disqualifier Date: Mon, 6 Jul 2026 00:16:52 -0400 Subject: [PATCH] fix: a stored key doc with meta:null no longer crashes authorize/revoke find_by_friendly, authorize.py's existing-target check, and revoke.py's success-print all did doc.get("meta", {}).get(...) directly - the {} default only applies when the key is ABSENT, not when its value is null (plausible in shared mongo or a hand-edited JSON store), so a null meta raised AttributeError. revoke's crash landed AFTER storage.delete() already succeeded, so a completed revoke was reported as a raw traceback instead of a clean exit. list_keys._meta already coerced correctly; that fix is now hoisted into commands/__init__.py as doc_meta, the single source every meta-reading call site goes through. cli.py's catch tuple also gains AttributeError as defense in depth for any future unguarded meta access. Signed-off-by: disqualifier --- src/envelope_authorizer/cli.py | 7 +++++-- src/envelope_authorizer/commands/__init__.py | 14 +++++++++++++- src/envelope_authorizer/commands/authorize.py | 3 ++- src/envelope_authorizer/commands/list_keys.py | 10 ++-------- src/envelope_authorizer/commands/revoke.py | 4 ++-- 5 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/envelope_authorizer/cli.py b/src/envelope_authorizer/cli.py index be4846c..54a363c 100644 --- a/src/envelope_authorizer/cli.py +++ b/src/envelope_authorizer/cli.py @@ -99,9 +99,12 @@ def main() -> int: return _fail("capability flag failed authentication - tampered or wrong DEK") except PyMongoError as error: return _fail(f"storage backend error: {error}") - except (ConfigError, CommandError, RuntimeError, ValueError, OSError, KeyError, TypeError) as error: + except ( + ConfigError, CommandError, RuntimeError, ValueError, OSError, KeyError, TypeError, AttributeError, + ) as error: # covers a malformed config/doc/flag (incl. KeyError/TypeError from unguarded - # indexing) - all print a clean [✘] line, not a raw traceback + # indexing, AttributeError from a null field a caller assumed was a dict) - + # all print a clean [✘] line, not a raw traceback return _fail(str(error)) diff --git a/src/envelope_authorizer/commands/__init__.py b/src/envelope_authorizer/commands/__init__.py index 9e4c90e..faeb076 100644 --- a/src/envelope_authorizer/commands/__init__.py +++ b/src/envelope_authorizer/commands/__init__.py @@ -68,9 +68,21 @@ def boot_local(config, storage) -> Tuple[EnvelopeCrypto, dict]: return crypto, doc +def doc_meta(doc: dict) -> dict: + """the doc's meta block as a dict, coercing a missing/null/malformed one to {} + + a stored doc's `meta` can be null (shared mongo, a hand-edited JSON file) - the + dict.get(key, {}) default only applies when the key is ABSENT, not when its + value is null, so every meta-reading call site must go through this rather than + `doc.get("meta", {})` directly or it raises AttributeError on a null meta. + """ + meta = doc.get("meta") + return meta if isinstance(meta, dict) else {} + + def find_by_friendly(storage, friendly: str) -> Optional[dict]: """return the doc whose meta.friendly matches, or None""" for doc in storage.get_all(): - if doc.get("meta", {}).get("friendly") == friendly: + if doc_meta(doc).get("friendly") == friendly: return doc return None diff --git a/src/envelope_authorizer/commands/authorize.py b/src/envelope_authorizer/commands/authorize.py index 574f062..56de4ab 100644 --- a/src/envelope_authorizer/commands/authorize.py +++ b/src/envelope_authorizer/commands/authorize.py @@ -9,6 +9,7 @@ from . import ( CommandError, boot_local, build_doc, + doc_meta, find_by_friendly, local_fingerprint, make_flag, @@ -35,7 +36,7 @@ def run(config, storage, args) -> None: ) existing = storage.get(new_fp) if existing: - existing_friendly = existing.get("meta", {}).get("friendly", "?") + existing_friendly = doc_meta(existing).get("friendly", "?") raise CommandError( f"target key is already authorized as '{existing_friendly}'; " f"authorize would silently replace that record - revoke it first " diff --git a/src/envelope_authorizer/commands/list_keys.py b/src/envelope_authorizer/commands/list_keys.py index e8682f2..e8a0e08 100644 --- a/src/envelope_authorizer/commands/list_keys.py +++ b/src/envelope_authorizer/commands/list_keys.py @@ -7,7 +7,7 @@ the wrapped key or DEK. from datetime import datetime, timezone -from . import boot_local, read_flag +from . import boot_local, doc_meta, read_flag def _can_authorize(crypto, doc) -> str: @@ -18,12 +18,6 @@ def _can_authorize(crypto, doc) -> str: return "?" -def _meta(doc) -> dict: - """the doc's meta block as a dict, coercing a missing/null/malformed one to {}""" - meta = doc.get("meta") - return meta if isinstance(meta, dict) else {} - - def _created(meta: dict) -> str: """format created_at as a UTC timestamp string, or '-' if absent/unparseable""" raw = meta.get("created_at") @@ -44,7 +38,7 @@ def run(config, storage, args) -> None: print(header) print("-" * len(header)) for doc in docs: - meta = _meta(doc) + meta = doc_meta(doc) print( f"{doc.get('_id', '')[:16]:<18} " f"{str(meta.get('friendly', '-')):<16} " diff --git a/src/envelope_authorizer/commands/revoke.py b/src/envelope_authorizer/commands/revoke.py index a7b28b0..1894aad 100644 --- a/src/envelope_authorizer/commands/revoke.py +++ b/src/envelope_authorizer/commands/revoke.py @@ -7,7 +7,7 @@ rotate the DEK or scrub it from a machine that already unwrapped it. from envelope_crypto import EnvelopeCrypto -from . import CommandError, find_by_friendly, local_fingerprint +from . import CommandError, doc_meta, find_by_friendly, local_fingerprint _WARNING = ( "[!] Revoke removes the record only. It does NOT rotate the DEK or scrub it\n" @@ -47,6 +47,6 @@ def run(config, storage, args) -> None: raise CommandError("refusing to revoke the local key") storage.delete(doc["_id"]) - friendly = doc.get("meta", {}).get("friendly", "?") + friendly = doc_meta(doc).get("friendly", "?") print(f"[✔] Revoked: {friendly} ({doc['_id'][:16]})") print(_WARNING)