docker builds can't use git+ssh (no ssh key / agent in the build), so the inter-lib
dependency references move to git+https (repos are public, anonymous clone). pins are
unchanged in target; bump to 1.0.2 so the https dependency spec ships under a new tag.
README install lines intentionally keep the ssh form for local/dev use.
Signed-off-by: disqualifier <dev@disqualifier.me>
the v1.0.0 release still pinned pre-1.0.0 sibling tags, so a fresh install dragged in
stale transitive deps. update the pin(s) to the current v1.0.x release and bump this lib
to 1.0.1 so the corrected dependency chain ships under a new tag (v1.0.0 left intact).
Signed-off-by: disqualifier <dev@disqualifier.me>
first stable release. pre-1.0.0 verification complete: all surviving MED regressions and
gaps resolved and independently re-fired, tree audited clean across the suite.
Signed-off-by: disqualifier <dev@disqualifier.me>
A stored doc with a present-but-null _id crashed the whole list table: doc.get('_id',
'')[:16] slices None (the '' default only applies when _id is absent), so one bad doc
aborted the render - now uses (doc.get('_id') or '')[:16], mirroring revoke's finder, so
it renders a blank placeholder row. cli.py imports cryptography.exceptions.InvalidTag but
pyproject only pulled cryptography transitively via envelope_crypto - declare it directly.
Signed-off-by: disqualifier <dev@disqualifier.me>
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 <dev@disqualifier.me>
a key doc with an explicit `_id: null` made doc.get("_id", "") return None
instead of the default, so .startswith(prefix) raised an uncaught
AttributeError instead of the contracted clean [✘] + exit 1.
Signed-off-by: disqualifier <dev@disqualifier.me>
envauth-2: catch PyMongoError in cli.py (guarded import) so an unreachable
mongo backend fails fast with a clean [x] line instead of an uncaught
traceback after the driver's server-selection timeout.
envauth-3: coerce a non-dict meta block to {} before rendering `list`, so a
"meta": null doc no longer crashes the table mid-render.
envauth-4: revoke checks args.friendly is not None instead of truthiness, so
--friendly "" correctly routes to the friendly-name lookup instead of
misrouting to the fingerprint branch.
envauth-5: add an optional [keys].password config field and thread it through
boot_local's decrypt_aes_key_with_rsa and verify's self_test, so an encrypted
local private key can be unwrapped.
envauth-6: add Config.storage_path (routed through the existing _expand
helper) so the JSON storage path expands $ENV_VARS the same way the key
paths already do, not just ~.
envauth-7: verified CLAUDE.md and README already correctly describe
authorize's re-run-is-refused behavior (fixed under envauth-1); no doc
change needed.
Also compresses several essay-length docstrings/comments (init's TOCTOU
note, authorize's replace-guard note, mongo_store's module docstring,
json_store's _write docstring, list_keys' _can_authorize docstring) with no
behavior change; re-verified the full CLI flow (JSON + live mongod) after.
Signed-off-by: disqualifier <dev@disqualifier.me>
envauth used envelope_crypto pinned at v0.1.0, two data-loss fixes behind (the lib
is now v0.1.4). envauth uses only stable crypto primitives (initialize, encrypt_data/
decrypt_data, create_aes_key, *_aes_key_with_rsa, get_rsa_key_fingerprint, self_test)
— never the record-rotation functions whose contract changed — so tracking latest is
safe and keeps the crypto fixes flowing without a manual bump each release.
Verified: full CLI round-trip (init -> authorize server -> privilege gate -> envauth-1
self-authorize refusal -> list) against envelope_crypto v0.1.4 source; all 8 used
primitives present. v0.1.4.
Signed-off-by: disqualifier <dev@disqualifier.me>
authorize never checked the target fingerprint against existing docs before
save()'s upsert-by-_id, so authorizing the local machine's own public key
under a new friendly name silently replaced the local authorizer record
(can_authorize demoted to False) while printing a success banner. With a
sole authorizer this bricks the CLI: authorize refuses (not permitted),
init refuses (already initialized), and revoke of the local key refuses
(refusing to revoke the local key) -- no in-CLI recovery. Mirror revoke's
local-key guard and extend it to any existing _id, so a duplicate target
is refused with a clear message instead of silently replacing the record.
Signed-off-by: disqualifier <dev@disqualifier.me>
EA-1: main dispatch catches KeyError/TypeError so a structurally-malformed flag/doc prints
a clean [x] line instead of a traceback. EA-2: fingerprint revoke rejects an empty prefix
and an ambiguous prefix (was: silently revoked the first match). json_store closes the raw
fd if os.fdopen raises before taking ownership (was: leaked). init TOCTOU documented as
by-design (trusted-DEK model, save upserts by _id). list '?' wording clarified.
Signed-off-by: disqualifier <dev@disqualifier.me>
- config init catches OSError (read-only dir, ENOSPC, gone cwd) alongside CommandError
and prints a clean [x] line; the main dispatch catches the full OSError family instead
of only FileNotFoundError (L13)
- document read_flag's fail-closed (non-dict -> not allowed) as a deliberate privilege-
gate default (nit).
Signed-off-by: disqualifier <dev@disqualifier.me>
a tampered or foreign GCM capability flag raises cryptography's InvalidTag (subclasses Exception, not ValueError/RuntimeError), which escaped the CLI's catch tuple as a raw traceback on the authorize/verify paths. main() now catches InvalidTag and surfaces '[\xe2\x9c\x98] capability flag failed authentication — tampered or wrong DEK'. also corrected the stale CLAUDE.md storage note that still described the swallow-wrapped mongo methods instead of the fail-loud raw-collection path.
Signed-off-by: disqualifier <dev@disqualifier.me>
two regressions in the [mongo] storage backend: (1) the four finally blocks did 'await db.close()' but the mongo lib's close() became synchronous this session, so await None raised TypeError on every op — dropped the await. (2) the backend consumed mongo's swallow-and-return-default wrapped methods raw, conflating a driver error with 'no document / not initialized' in the lib that gates authority; it now goes through the raw db.collection(name) escape hatch (the motor collection, which raises) and raises on a no-op upsert, matching the CLI's fail-loud stance.
Signed-off-by: disqualifier <dev@disqualifier.me>
the docs claimed 'you cannot grant yourself authority without already having it', which is false in the shared-DEK model: a DEK-holder with write access can copy a sealed True flag onto its own doc. replaced with the honest boundary (the flag is unforgeable WITHOUT the DEK, but is not a defense against a malicious DEK-holder, which is out of scope by design) and added operational guidance to detect a self-grant by auditing authorization state. no code or storage-format change.
Signed-off-by: disqualifier <dev@disqualifier.me>
- JsonStore._write used a fixed '<path>.tmp' name with no lock, so two concurrent
authorizer invocations could clobber each other's temp and corrupt/lose the key
store. use tempfile.mkstemp in the same dir (unique per write) then os.replace
(atomic), cleaning up the temp on failure.
- list 'created_at' formatting did int(raw) unguarded; one hand-edited/legacy doc
with a bad timestamp aborted the whole table. guard per-row, fall back to '-'.
verified by execution: 20 concurrent writers -> 0 errors, file stays valid JSON,
no leftover .tmp; upsert still dedupes/updates; bad/absent created_at -> '-'.
Signed-off-by: disqualifier <dev@disqualifier.me>