Skip to content

johnzfitch/dota

Repository files navigation

Defense of the Artifacts (dota)

Dota post quantum secure local vault

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 v1v6 vaults are migrated in place to v7 on unlock.

Quickstart

# 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 list

How it works

flowchart 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]
Loading

TC-HKEM: Each secret is encrypted with AES-256-GCM. The per-secret AES key is derived via the Triple-Committed Hybrid KEM combiner:

  1. ML-KEM-768 encapsulation → 32-byte shared secret (ss_kem) + ciphertext (ct_kem)
  2. X25519 ephemeral DH → 32-byte shared secret (ss_dh) + ephemeral public key (eph_pk)
  3. Passphrase commitment: τ = HMAC-SHA256(mk, ct_kem || eph_pk)
  4. Ciphertext binding: ct_kem and eph_pk are included directly in the HKDF input
  5. 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.

Features

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 mk is bound into every per-secret key derivation via τ = HMAC(mkct_kem || eph_pk). Knowledge of the KEM private keys alone is insufficient.
Memory safety
Rust with ZeroizeOnDrop on all sensitive types -- passphrases, shared secrets, and AES keys are wiped when their wrappers drop on the normal return path. The release profile uses panic = "abort" for fail-fast behavior, so drop glue does not run on panic; on Linux, harden_process compensates by disabling core dumps (RLIMIT_CORE = 0), blocking ptrace (PR_SET_DUMPABLE = 0), and pinning all pages with mlockall so 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_process is a no-op on those platforms; rely on Secure Enclave / DPAPI and full-disk encryption.
Authenticated metadata
version, min_version, algorithm IDs, public keys, and suite are covered by the v7 HMAC-SHA256 key commitment before any private-key decryption.
Automatic migration
Legacy v1v6 vaults upgrade to v7 on 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.

Design constraints

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.

Security assumptions

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
v7 uses 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/modified timestamps, 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 -- recommend shred(1) on a flat-file filesystem if the strict guarantee matters.

Environment variables

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 copy command. Default 30, accepted range 1–600. Out-of-range or unparseable values fall back to the default.
Cryptographic details

Key derivation

  1. Passphrase → Argon2id (64 MiB, 3 iterations, 4 threads, 32-byte master key mk)
  2. mk → purpose-labeled wrapping keys for encrypting the ML-KEM-768 and X25519 private keys, HMAC-SHA256 header commitment, and TC-HKEM passphrase commitment τ
  3. ML-KEM-768 and X25519 keypairs are generated from OsRng and stored encrypted in the vault (no deterministic derivation from mk)

Secret encryption (TC-HKEM)

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 without mk.

Vault format

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.

Commands

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 /proc and 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=VALUE lines for the named secrets (or all secrets if no names given). Safe to eval in 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=VALUE lines.
quit / exit
Exit the shell.

Troubleshooting

  • 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.

Development

# 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 --release

License

MIT

Citation

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}
}

About

Defense of the Artifacts - Post-quantum secure secrets manager with TUI

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors