@bryan237l/qev-cli
v0.30.0
Published
Command-line vault for offline encrypted message envelopes. Same XChaCha20-Poly1305 + Argon2id crypto as the QEV desktop and web apps. Cross-platform, zero network access, lock a file with a phrase and share it through any channel.
Maintainers
Readme
@bryan237l/qev-cli
Command-line vault for offline encrypted message envelopes.
Same crypto and vault format as QEV (Qira Encryption Vault) on Mac,
Windows, and secure.imagineqira.com/vault.
A vault locked here decrypts there, and vice versa. Zero network access. One
runtime dependency.
npm install -g @bryan237l/qev-cli
qev self-test
echo "the secret message" | qev lock --out secret.vault
# Phrase: ************
# Phrase (confirm): ************
# locking with strong preset (~4s) ...
# wrote secret.vault
qev unlock secret.vault
# Phrase: ************
# the secret messageWhat it is
A Node CLI that encrypts UTF-8 plaintext into a
BRY-NFET-SX-VAULT-V2 JSON file and decrypts it back.
The vault format is the same one the desktop app and web app use, so anywhere
you can run QEV, you can read the file.
┌───────────┐ qev lock ┌───────────────┐ qev unlock ┌───────────┐
│ plaintext ├──────────────▶│ secret.vault ├───────────────▶│ plaintext │
│ (stdin) │ +phrase │ (JSON) │ +phrase │ (stdout) │
└───────────┘ └───────────────┘ └───────────┘Why it exists
- Private one-shot sharing. Lock a note, secret, log snippet, or proof blob into a vault file and send the vault through any channel.
- Sysadmins and scripts.
qev lockinto a file you check into git.qev unlockon deploy. Similar tosops/git-crypt, friendlier. - Cross-platform. The desktop QEV is Mac + Windows only. The CLI adds Linux coverage — any machine with Node 18.17+.
- Pipe-friendly. Input is stdin, output is stdout. Compose with every Unix tool you already use.
- Same vault format. A vault produced here decrypts in the desktop app or the web app, byte-for-byte. One format, multiple implementations.
Install
npm install -g @bryan237l/qev-cliOr run without installing:
npx @bryan237l/qev-cli self-testCommands
qev lock [--out FILE] [--mode self|share] [--strength quick|strong|vault]
Encrypts plaintext from stdin. Prompts for the phrase twice with confirmation.
Writes the vault JSON to --out FILE or stdout.
# Interactive: type the message, Ctrl-D when done
qev lock --out note.vault
# Pipe-in: plaintext from any source
echo "wi-fi password: hunter2" | qev lock --out wifi.vault
# Larger files too (up to 256 KiB of plaintext):
cat secrets.txt | qev lock --out secrets.vault --strength vaultStrength presets (Argon2id parameters):
| preset | opslimit | memlimit | roughly |
|---|---:|---:|---:|
| quick | 1 | 32 MiB | ~1 s |
| strong (default) | 4 | 96 MiB | ~4 s |
| vault | 6 | 128 MiB | ~7 s |
qev unlock VAULT_FILE
Decrypts a vault file. Prompts for the phrase. Writes plaintext to stdout.
qev unlock note.vault
# Pipe into anything
qev unlock note.vault | less
qev unlock creds.vault | base64 -d > secrets.binqev gen-phrase
Prints a freshly generated 4-word passphrase. Roughly ~37 bits of entropy — use it as a starting point, or prefer a longer phrase you chose yourself.
$ qev gen-phrase
river-purple-dragon-cloud-47qev self-test
Runs a round-trip + tamper + wrong-phrase self-test with the quick preset. Exits 0 on success, 1 on any failure.
$ qev self-test
qev self-test: encrypt → decrypt → tamper → wrong-phrase ... okqev version
$ qev version
qev 0.28.1Crypto
- AEAD: XChaCha20-Poly1305 (24-byte nonce, 16-byte Poly1305 MAC)
- KDF: Argon2id, default 4 opslimit / 96 MiB memlimit
- Wrap pattern: the phrase stretches into a wrap-key which encrypts a per-vault random 32-byte content key. The content key is what encrypts your plaintext. This means the phrase is never the content-encryption key itself — it is one unlock path for the data key. The format can grow additional unlock paths later without re-encrypting the data.
- AAD binding: the vault metadata (schema, version, created_at, mode, kdf block, algorithms, nonces) is fed into both AEAD operations as Additional Authenticated Data. Tampering with any bound field breaks at least one AEAD tag cleanly.
- Library:
libsodium-wrappers-sumo— the same libsodium WASM binary the browser implementation ships. Not a pure-JS reimplementation.
Primitives are not invented. No custom cryptography was written. The value-add is the vault format, the cross-platform packaging, and the honest local-first workflow.
Vault format
{
"schema": "BRY-NFET-SX-VAULT-V2",
"version": "0.28.1",
"created_at": "2026-04-15T23:59:59.000Z",
"mode": "self",
"kdf": {
"algorithm": "argon2id",
"opslimit": 4,
"memlimit": 100663296,
"salt": "<b64url, 16 bytes>"
},
"wrap": {
"algorithm": "XChaCha20-Poly1305",
"nonce": "<b64url, 24 bytes>",
"wrapped_key": "<b64url, 48 bytes (32-byte key + 16-byte MAC)>"
},
"content": {
"algorithm": "XChaCha20-Poly1305",
"nonce": "<b64url, 24 bytes>",
"ciphertext": "<b64url>"
}
}All binary fields are base64url without padding. The AAD is not stored. It is derived deterministically on both encrypt and decrypt by canonical-JSON serializing a fixed subset of vault metadata. The canonical JSON serializer sorts object keys recursively with no whitespace, so the AAD bytes are identical regardless of which implementation produced the vault.
Safety rules enforced by the CLI
- The phrase is never a command-line argument.
qev lock --phrase "..."is rejected explicitly. Shell history,ps, and/procwould leak it. The phrase is always typed at a raw-mode TTY prompt with no echo. - Stdin phrase input is refused unless stdin is a TTY. A scripted wrapper that pipes a phrase in would be a foot-gun pattern; the CLI refuses to read phrases from non-TTY stdin.
- No logging of user data. The library modules have a top-of-file rule and the CLI front-end never logs the phrase, plaintext, or derived key.
- Errors are concise, not stack-trace dumps. User-visible errors are
single-line
qev: error: ...messages with a clean exit code 1.
Threat model — honest caveats
What this protects:
- Confidentiality against an attacker who does not have the phrase, at the cost of Argon2id's hardness parameter.
- Integrity of the ciphertext, nonces, salt, KDF parameters, schema, version,
mode, and
created_atvia AEAD AAD binding. - Cross-platform portability — a vault made on one OS opens on another.
What it does not protect against:
- A weak phrase. Argon2id raises the cost; it does not eliminate it.
- A compromised endpoint. If your machine is compromised while creating or opening the vault, the phrase and plaintext can be stolen.
- A forgotten phrase. There is no reset, no backdoor, no recovery email.
- Transmission metadata. The channel still sees who sent what to whom and when.
- A motivated adversary with the phrase. Once the phrase is known, the vault is open.
- Supply-chain compromise. Pin versions, audit dependencies, and prefer
npm ciin CI.
What it is not:
- It is not a messenger.
- It is not a password manager.
- It is not cloud storage encryption.
- It is not a new encryption algorithm.
QEV fills a narrow gap: encrypt a single thing, share it once, through any channel, without an account.
Programmatic use
import { encryptVaultV2, decryptVaultV2, runSelfTest } from "@bryan237l/qev-cli";
await runSelfTest(); // throws on any failure
const vault = await encryptVaultV2({
plaintext: "hello",
password: "a-reasonably-long-phrase",
mode: "self",
opslimit: 4,
memlimit: 96 * 1024 * 1024,
});
const pt = await decryptVaultV2({ vault, password: "a-reasonably-long-phrase" });
console.log(pt); // "hello"Development
git clone https://github.com/TheArtOfSound/qev-desktop.git
cd qev-desktop/qev-cli
npm install
npm test
./bin/qev.js self-testLicense
MIT © Bryan Leonard / Qira LLC. libsodium is ISC licensed. See LICENSE and
vendor/libsodium-license.txt for the full texts.
Support
- Desktop app:
secure.imagineqira.com/downloads - Web app:
secure.imagineqira.com/vault - npm:
@bryan237l/qev-cli - Questions, bugs, refunds:
[email protected]
