Post-quantum secure secrets manager with v7 TC-HKEM vaults (ML-KEM-768 + X25519 with ciphertext binding and passphrase commitment), plus an OS clipboard auto-clear mode and a text-mode interactive shell.
Defense-in-depth cryptography: v7 vaults protect secrets with both classical security (X25519) and post-quantum security (ML-KEM-768), combined via the TC-HKEM (Triple-Committed Hybrid KEM) construction. Security holds if either algorithm is secure. Legacy v1–v6 vaults are migrated in place to v7 on unlock.
# Install
cargo install --path .
# Initialize vault (stored at ~/.dota/vault.json by default)
dota init
# Launch interactive text shell (default command)
dota
# Or use CLI commands
dota set API_KEY # value read from stdin or non-echoing prompt
dota get API_KEY # prints value to stdout
dota get API_KEY --copy # copies to OS clipboard, auto-clears after 30s
dota listflowchart LR
A[Passphrase] -->|Argon2id| B[Master Key mk]
B --> C["ML-KEM-768\nencapsulate -> ss_kem, ct_kem"]
B --> D["X25519 ephemeral DH\n-> ss_dh, eph_pk"]
B -->|"tau = HMAC(mk, ct_kem || eph_pk)"| E
C --> E["TC-HKEM combiner\nHKDF(ss_kem||ss_dh||ct_kem||eph_pk||tau)"]
D --> E
E --> F["AES-256-GCM"]
F --> G[Encrypted Secret]
TC-HKEM: Each secret is encrypted with AES-256-GCM. The per-secret AES key is derived via the Triple-Committed Hybrid KEM combiner:
- ML-KEM-768 encapsulation → 32-byte shared secret (
ss_kem) + ciphertext (ct_kem) - X25519 ephemeral DH → 32-byte shared secret (
ss_dh) + ephemeral public key (eph_pk) - Passphrase commitment: τ =
HMAC-SHA256(mk, ct_kem || eph_pk) - Ciphertext binding:
ct_kemandeph_pkare included directly in the HKDF input HKDF-SHA256(ss_kem || ss_dh || ct_kem || eph_pk || tau, "dota-v7-tchkem-salt", "dota-v7-secret-key")→ 32-byte AES key
The vault stores ML-KEM ciphertexts, X25519 ephemeral public keys, and AES-GCM ciphertexts. A canonical authenticated header is protected by HMAC-SHA256 under the passphrase-derived master key before any private-key decryption occurs. The v7 TC-HKEM combiner achieves best-of-both-worlds IND-CCA security and binds the passphrase into every per-secret key derivation.
- Post-quantum security
- Real ML-KEM-768 (NIST FIPS 203 final standard) -- resists quantum computer attacks.
- Classical security
- X25519 elliptic curve Diffie-Hellman -- protects against classical adversaries.
- Best-of-both-worlds IND-CCA
- TC-HKEM ciphertext binding ensures security if either algorithm holds (GHP18 reduction).
- Passphrase commitment
- Master key
mkis bound into every per-secret key derivation via τ = HMAC(mk,ct_kem || eph_pk). Knowledge of the KEM private keys alone is insufficient. - Memory safety
- Rust with
ZeroizeOnDropon all sensitive types -- passphrases, shared secrets, and AES keys are wiped when their wrappers drop on the normal return path. The release profile usespanic = "abort"for fail-fast behavior, so drop glue does not run on panic; on Linux,harden_processcompensates by disabling core dumps (RLIMIT_CORE = 0), blocking ptrace (PR_SET_DUMPABLE = 0), and pinning all pages withmlockallso freed pages cannot be observed by a same-UID process or written to swap, and the Linux page allocator zeros pages before handing them to the next process. macOS and Windows run with OS defaults only --harden_processis a no-op on those platforms; rely on Secure Enclave / DPAPI and full-disk encryption. - Authenticated metadata
version,min_version, algorithm IDs, public keys, andsuiteare covered by thev7HMAC-SHA256 key commitment before any private-key decryption.- Automatic migration
- Legacy
v1–v6vaults upgrade tov7on unlock; originals are backed up automatically. - Key rotation
- dota rotate-keys generates fresh ML-KEM-768 and X25519 keypairs and re-encrypts all secrets.
- Export to environment
- dota export-env VAR1 VAR2 outputs shell-compatible variable assignments for CI/CD pipelines.
- Interactive shell and CLI
- Text-mode interactive shell (dota / dota unlock) for browsing and editing secrets, plus scriptable command-line operations for CI/CD.
- Clipboard auto-clear
- dota get NAME --copy writes the secret to the OS clipboard and clears it after a timeout (default 30s, override with
DOTA_CLIPBOARD_TIMEOUT_SECS). Keeps secrets out of terminal scrollback and shell history.
- Trust boundary
- The vault file must be protected at rest. Use full-disk encryption (LUKS, FileVault, BitLocker) or store on an encrypted volume.
- Passphrase strength
- Argon2id with 64 MiB memory, 3 iterations, 4 threads (OWASP 2024 recommended parameters). Adjust only if you understand the security tradeoffs.
- No network
- All operations are fully local. There is no cloud sync, remote key escrow, or telemetry.
- Threat model
- Protects against passive adversaries with quantum computers (harvest-now-decrypt-later attacks). Does not protect against active quantum adversaries or compromised endpoints.
- Algorithm choices
v7uses ML-KEM-768 (post-quantum), X25519 (classical), AES-256-GCM (authenticated encryption), HKDF-SHA256 (TC-HKEM combiner), and HMAC-SHA256 (header commitment + passphrase binding).- Side channels
- No explicit protection against timing or cache attacks beyond what the underlying cryptography libraries provide.
- Plaintext metadata
- Secret names,
created/modifiedtimestamps, KDF parameters, and both public keys are stored unencrypted inside the vault JSON. The vault file should be treated as confidential at-rest; full-disk encryption is the recommended container. - Migration backups and tombstones
- When a legacy vault is migrated, the original is preserved as
vault.backup.<timestamp>.json. On dota change-passphrase or dota rotate-keys, those backups are converted to tombstone files (vault.tombstone.<timestamp>.json) that retain version + KDF metadata for forensic correlation but scrub the wrapped private keys, key commitment, and secrets. The original backup is best-effort overwritten with zeros and unlinked; on copy-on-write filesystems (btrfs, ZFS, APFS) the zero-write may land in a fresh block -- recommendshred(1)on a flat-file filesystem if the strict guarantee matters.
DOTA_PASSPHRASE- Passphrase for non-interactive use. Convenient for CI scripts, but visible to same-UID processes via
/proc/<pid>/environ. Unset in the parent shell after use; prefer interactive prompts on shared hosts. DOTA_CLIPBOARD_TIMEOUT_SECS- Auto-clear interval for dota get --copy and the shell
copycommand. Default 30, accepted range 1–600. Out-of-range or unparseable values fall back to the default.
Cryptographic details
- Passphrase → Argon2id (64 MiB, 3 iterations, 4 threads, 32-byte master key
mk) mk→ purpose-labeled wrapping keys for encrypting the ML-KEM-768 and X25519 private keys, HMAC-SHA256 header commitment, and TC-HKEM passphrase commitment τ- ML-KEM-768 and X25519 keypairs are generated from
OsRngand stored encrypted in the vault (no deterministic derivation frommk)
1. ML-KEM-768 encapsulate(pk_kem) -> (ss_kem, ct_kem)
2. X25519 ephemeral DH(pk_x25519) -> (ss_dh, eph_pk)
3. tau = HMAC-SHA256(mk, ct_kem || eph_pk)
4. IKM = ss_kem || ss_dh || ct_kem || eph_pk || tau (~= 1216 bytes for ML-KEM-768)
5. aes_key = HKDF-SHA256(IKM, "dota-v7-tchkem-salt", "dota-v7-secret-key")
6. (ciphertext, tag) = AES-256-GCM(plaintext, aes_key, random_nonce)
Security properties:
- Theorem 1 — Best-of-both-worlds IND-CCA:
Adv <= Adv_ML-KEM^{ind-cca}(B_1) + Adv_X25519^{gap-cdh}(B_2) + q_H/2^256. Ciphertext binding enables the B_1 reduction. - Theorem 2 — Passphrase binding:
Adv^{mk-bind} <= Adv_HMAC^{prf}(B_3) + q_H/2^256. Knowledge of(dk, sk_dh)alone is insufficient withoutmk.
JSON structure with versioning (current: v7, suite: dota-v7-tchkem-mlkem768-x25519-aes256gcm):
version- Protocol version for forward compatibility. Current:
7. min_version- Anti-rollback floor -- vault is rejected by implementations older than this version.
kdf- Argon2id parameters: algorithm, salt, time_cost, memory_cost, parallelism.
key_commitment- HMAC-SHA256 over the canonical header (version, min_version, KDF params, algorithm IDs, public keys, suite). Verified before any private-key decryption.
kem- ML-KEM-768 public key and AES-256-GCM-wrapped private key.
x25519- X25519 public key, algorithm label, and AES-256-GCM-wrapped private key.
suite- Active cipher-suite identifier:
dota-v7-tchkem-mlkem768-x25519-aes256gcm. secrets- Map of name →
{algorithm, kem_ciphertext, x25519_ephemeral_public, ciphertext, nonce, created, modified}. migrated_from- Original version and migration path for vaults upgraded from older formats.
- dota init
- Initialize a new vault at
~/.dota/vault.json(or--vault PATH). - dota / dota unlock
- Launch the interactive text-mode shell (default command when no subcommand is given).
- dota set NAME
- Store or update a secret. The value is read from stdin (when piped) or from an interactive non-echoing prompt; it is never accepted on the command line, because argv is observable to other local processes via
/procand is recorded in shell history. - dota get NAME [--copy]
- Print a secret value to stdout, or with --copy place it on the OS clipboard with auto-clear (default 30s, override via
DOTA_CLIPBOARD_TIMEOUT_SECS). The stdout form is intended for pipelines (dota get TOKEN | ssh-agent); use --copy for interactive retrieval to keep the value out of terminal scrollback. - dota list
- List all secret names (values are never printed).
- dota rm NAME
- Permanently remove a secret.
- dota export-env [NAMES…]
- Print
export KEY=VALUElines for the named secrets (or all secrets if no names given). Safe toevalin shell scripts. - dota change-passphrase
- Re-derive the master key and re-wrap all private key material under a new passphrase.
- dota rotate-keys
- Generate fresh ML-KEM-768 and X25519 keypairs and re-encrypt all secrets.
- dota upgrade
- Explicitly migrate the vault to the current format version (
v7). Migration also happens automatically on any unlock. - dota info
- Show vault metadata: version, suite, KDF parameters, key commitment status, and secret count.
All commands accept --vault PATH to override the default vault location.
Interactive shell commands (dota / dota unlock)
list- List secret names with last-modified timestamps.
get NAME- Print the secret value to stdout.
copy NAME- Copy the secret to the OS clipboard with auto-clear (default 30s).
set NAME- Prompt for a value (not echoed) and store it.
rm NAME- Remove a secret.
info- Show vault metadata.
refresh- Reload the vault from disk (e.g. after an out-of-band dota rotate-keys from another shell).
export- Print all secrets as
export KEY=VALUElines. quit/exit- Exit the shell.
- Failed to decrypt vault: Incorrect passphrase or corrupted vault file. Check
~/.dota/vault.json. - Slow unlock: Argon2id intentionally uses 64 MiB RAM and 3 iterations with 4 threads. This takes roughly 1–3 seconds on modern hardware and is by design.
# Run all tests
cargo test
# Run with debug logging
RUST_LOG=debug cargo run
# Check formatting and lints
cargo fmt --check && cargo clippy
# Build optimized release binary
cargo build --releaseMIT
If you use this in research or security audits:
@software{dota2026,
author = {Fitch, Zack},
title = {Defense of the Artifacts: Post-quantum secure secrets manager},
year = {2026},
url = {https://github.com/johnzfitch/dota}
}