Skip to content

Comments

Modernize packaging, add fast path options#21

Open
mahmoud wants to merge 5 commits intoSimpleLegal:masterfrom
mahmoud:master
Open

Modernize packaging, add fast path options#21
mahmoud wants to merge 5 commits intoSimpleLegal:masterfrom
mahmoud:master

Conversation

@mahmoud
Copy link
Contributor

@mahmoud mahmoud commented Feb 14, 2026

Still using pocket protector in production. Ran into an issue where I wanted to speed up application startup and pprotect's KDF functionality was slowing me down. Added two options for fast paths: "raw" where there's no KDF (just a huge passphrase) and "fast" where the KDF isn't quite as hard. "hard" is still the default and I tested that all formats still work. Also test coverage is up to 98% (from 95%).

- Remove all Python 2 compat (unicode shims, cStringIO, imp, __future__)
- Add per-custodian KDF parameter storage (v1 pwdkm format)
 - v0 custodians (existing) continue using module-level OPSLIMIT/MEMLIMIT
 - v1 custodians store opslimit/memlimit in their pwdkm field
 - Backward compatible: v0 files load and work unchanged
- Add KDF_SENSITIVE and KDF_INTERACTIVE named parameter tuples
 - SENSITIVE: ~0.8s per KDF call (production default)
 - INTERACTIVE: ~0.1s per KDF call (dev/testing, 8x faster)
- Add --fast-crypto CLI flag for init/add-key-custodian
- Convert setup.py to pyproject.toml
- Modernize tox.ini (py39-py313)
- Expand test suite: 11 tests (was 4), covering v0/v1 round-trip,
 coexistence, KDF params, check_creds, audit log, error cases
- Bump version to 24.0.0dev
- KeyFile.migrate_owner: batch add owner across all domains
- KeyFile.get_custodian_domains: list domains a custodian owns
- set_key_custodian_passphrase: accept opslimit/memlimit params
- CLI: migrate-owner subcommand with confirmation prompt
- CLI: list-user-secrets subcommand (read-only)
- CLI: set-key-custodian-passphrase now supports --fast-crypto flag
- Tests for all new functionality
Raw-key custodians bypass argon2 entirely. The passphrase IS the
entropy (256 bits), so password stretching adds only latency.

Format: P<64 lowercase hex chars>P (e.g. P0a1b2c...P)

- v2 pwdkm format: version(1=0x02) + salt(8) + pubkey(32)
- _kdf_raw: sha512(passphrase + salt + name)[:32], no argon2
- generate_raw_passphrase(): creates P<64hex>P from os.urandom(32)
- is_raw_passphrase(): validates format
- KeyFile.add_raw_key_custodian(): creates v2 custodian
- CLI: add-raw-key-custodian subcommand
 - Generates key, displays once with stern warning
 - Requires typing YES to confirm key was saved
 - Aborts cleanly on decline
- v0/v1/v2 custodians coexist in same protected.yaml
- 22 tests (6 new)
Breaking change: --fast-crypto flag removed in favor of --key-type.

 pprotect add-key-custodian --key-type fast # was --fast-crypto
 pprotect add-key-custodian --key-type raw # was add-raw-key-custodian
 pprotect add-key-custodian # default: hard (unchanged)

New: rekey-custodian command
 Changes an existing custodian's key type and passphrase in one step.
 Re-encrypts all domain ownership keys automatically.

 pprotect rekey-custodian --key-type fast # move existing custodian to fast KDF
 pprotect rekey-custodian --key-type raw # move to raw key (generates P<hex>P)

Internal: KeyFile._replace_key_custodian extracted from
set_key_custodian_passphrase for reuse by rekey_custodian.

23 tests pass.
@mahmoud
Copy link
Contributor Author

mahmoud commented Feb 14, 2026

cc @kurtbrose

@kurtbrose
Copy link
Contributor

oh nice, you added raw key as well, so there are three modes :-)

yea, in practice the way I've used it locally and in prod is that there's a high entropy machine generated key anyway, so we could just skip the KDF entirely actually with no loss in functionality

I'll give it a deeper look soon, but at first glance looks pretty good

Comment on lines +104 to +109
def _kdf_raw(creds, salt):
'''Derive key directly from high-entropy passphrase (P<64hex>P format).
No argon2 — the passphrase IS the entropy.'''
name = creds.name.encode('utf8')
passphrase = creds.passphrase.encode('utf8')
return hashlib.sha512(passphrase + salt + name).digest()[:nacl.public.PrivateKey.SIZE]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need for hashlib, it should already be 32 bytes, just check that len(passphrase) == PrivateKey.SIZE (which I believe is 32 bytes) and send it on through


def generate_raw_passphrase():
'''Generate a P<64hex>P raw-key passphrase with 256 bits of entropy.'''
return 'P' + os.urandom(32).hex() + 'P'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you don't care about pre-3.6 compatibility, secrets.key_hex() is basically a "stlib approved" way to call os.urandom().hex() -- it's literally the same API, just exposed through a different interface

(I learned this the other week)

Copy link
Contributor

@kurtbrose kurtbrose left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice -- a lot of modernization; also I like the idea of baking in the "high entropy passphrase" to the data, that makes sense

small suggestion is just, skip the hmac; high entropy with size = size, you are done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants