rcypher is a minimal, offline, file-based password storage and encryption tool for technical users. It is designed to protect secrets at rest using modern cryptography, while remaining simple and auditable.
⚠️ Not audited. Use at your own risk.
rcypher operates in two main modes:
- Encrypted key-value storage (default)
- Full file encryption / decryption
When no --encrypt or --decrypt flags are provided, rcypher treats the given file as an encrypted key-value storage.
If the file does not exist, it will be created.
$ rcypher secrets.db
Enter Password for secrets.db:
cypher > help
USER COMMANDS:
put KEY - Store a value (prompted with echo, excluded from history)
get REGEXP - Get values for keys matching regexp
copy KEY - Copy key value into system clipboard
history KEY - Show history of changes for a key
search REGEXP - Search for keys matching regexp
del|rm KEY - Delete a key
help - Show this help
AUTH COMMANDS (multi-factor stores):
auth factor list - List enrolled factors
auth factor add password NAME - Add a password factor (NAME is a label, not the password)
auth factor add fido2 NAME - Add a FIDO2 security-key factor (a hardware security key)
auth factor remove NAME - Remove a factor (not used by the policy)
auth policy show - Show the current unlock policy
auth policy set EXPR - Set the unlock policy, e.g. p1 or (p2 and fido2)-
encrypted at rest
-
never printed to stdout
-
written directly to the terminal (TTY)
put KEY asks for the value on a separate, visibly echoed line-editor prompt.
This keeps long values easy to type, edit, or paste while excluding both the
put command and its value from command history. Because input is intentionally
visible, avoid entering a value where someone can watch the terminal.
New stores support multi-factor unlock (see below); legacy single-password stores still open with their password and are upgraded to the current format automatically (see Upgrading a legacy store).
A store can require more than one secret to unlock. Each secret is a named factor (a password, or a FIDO2 security key — built into the CLI by default), and an access policy — a boolean expression over factor names — decides which combinations open the store.
A new store starts with one password factor named primary and the policy
primary. For example, add a recovery password as a second way in, so a
forgotten primary password doesn't lock you out:
In auth factor add password NAME, NAME is a label (shown by auth factor list once the store is unlocked — it is encrypted at rest) — not the
password; you are prompted for the password separately. rcypher refuses to enrol a
factor whose name equals its password (or whose password duplicates an existing
factor's).
cypher > auth factor add password recovery # 'recovery' is the label; the passphrase is prompted
Enrolling factor 'recovery'. The name is a label (shown by 'auth factor list' once
unlocked; encrypted in the store) — not the password; you'll enter the password next.
New password for factor 'recovery': ********
Confirm password: ********
Factor 'recovery' enrolled. It is not yet used by the policy — run
'auth policy set EXPR' to require or accept it.
cypher > auth policy set primary or recovery # either one unlocks the store
Policy: primary or recovery
cypher > auth factor list
primary (password)
recovery (password)Policies combine factors with and / or and parentheses (and binds tighter
than or):
| Policy | Unlocks when… | Good for |
|---|---|---|
primary |
the primary password is given |
the default single-secret store |
primary or recovery |
either password is given | a backup/recovery password (note: only as strong as the weaker one) |
primary and fido2 |
both the password and the security key | real two-factor |
primary or (recovery and fido2) |
primary alone, or recovery + the key |
day-to-day password, plus a 2-of-2 recovery path |
and of two passwords is allowed (e.g. two custodians who must both be
present), but for one person it just means typing two secrets every time for
little gain — the combination pays off when one factor is a hardware key. The
FIDO2 security-key factor (any hmac-secret-capable authenticator) is built into
the CLI by default; enrol one with auth factor add fido2 NAME. (A
--no-default-features build omits it, for hosts without the USB-HID build deps.)
On open, rcypher asks for a password in a loop — you don't pick a factor, and
because factor names are encrypted until unlock it shows neither the policy nor
which factors exist. Each password is matched against the factors; a match is
reported generically (Factor unlocked., plus a note when more are still needed)
and it keeps asking until the policy is satisfied (an empty entry cancels). So
primary or recovery needs one password; primary and recovery asks again until
both are given.
When the policy includes a security key, the prompt offers it as an option —
Password (empty to use a security key, or to cancel) — and pressing Enter
switches to the key: rcypher asks you to touch it (and for a PIN, if the key has
one) and tries the resulting secret. A CLI built --no-default-features (without
FIDO2) that meets a key-only policy says so up front instead of looping.
A security-key factor is satisfied by a hardware authenticator instead of a typed secret. When you create a store, rcypher offers to add one straight away:
Enrol a FIDO2 security key as a second factor now? [y/N] y
Touch your FIDO2 security key to enrol it…
Require BOTH the password and the key to unlock? [y/N] (No = either one) : n
FIDO2 factor 'key' enrolled; unlock policy is now: primary or keyAnswering yes to "require both" sets primary and key (real two-factor);
no sets primary or key (either one opens it).
From an unlocked store, enrol one with auth factor add fido2 NAME; like any
new factor it stays unused until a policy references it:
cypher > auth factor add fido2 work-key
Enrolling FIDO2 factor 'work-key'. The name is a label (shown by 'auth factor list'
once unlocked; encrypted in the store) — not a secret.
Touch your FIDO2 security key to enrol it…
Factor 'work-key' enrolled. Reference it in 'auth policy set' to require it.
cypher > auth policy set primary and work-key # now both are required
Policy: primary and work-keyPINs are auto-detected. rcypher checks whether the authenticator has a PIN set
and adapts: if it does, you're asked for it (at enrol and at every unlock); if not,
the factor unlocks with a touch alone — you are never prompted for a PIN that
isn't configured. The enrolled mode is bound into the factor and replayed at
unlock, so a key enrolled touch-only stays touch-only even if you later add a PIN
(that direction is safe). The breaking changes are the reverse: removing the PIN
a factor was enrolled with, or turning on the key's "always require user
verification" for a touch-only factor, makes the bound secret unreachable and that
factor can no longer unlock — your data is untouched, but you must open the store
via another policy branch and re-enrol the key. Keep a strong recovery branch (see
Recovery and backup) so a key change can never lock you
out. The full derivation and binding are specified in
docs/auth-protocol.md.
When you create a store or enroll a password factor (interactively), rcypher
checks the password with zxcvbn — the modern
estimator that scores by how many guesses a password resists and flags dictionary
words, sequences, and reuse of context (like the factor name). It follows current
NIST guidance: length and unpredictability matter, not arbitrary "must contain a
symbol" rules. A weak password shows a prominent warning with an estimated crack
time and requires a double confirmation. A trivially guessable one — the factor
name itself, the app name, or something like abc123 — is refused outright, with
no override. A long passphrase of a few random words is the easiest way to score
well.
A password may not be too similar to its factor name (a weak, guessable choice):
it must be at least twice as long as any prefix it shares with the name (so
foobar / foobar1 is rejected). This also blocks accidentally typing
a password into the name slot.
The store payload is encrypted under a random data-encryption key (DEK). The
DEK is split across the policy tree using monotone secret sharing — an or
replicates the secret to each branch, an and XOR-splits it — and each leaf's
share is wrapped under that factor's key. Recovering the DEK therefore requires
satisfying the policy. Enrolling an unused factor only adds its keyslot, but
changing the active policy rotates the DEK and re-encrypts the store. Rotation
means an older snapshot protected by a weaker policy cannot reveal the key used
by the current vault. Saves that do not change policy keep the current DEK and use
fresh random IVs. The full construction is specified in
docs/auth-protocol.md.
- Keep at least one strong recovery branch — e.g. a long, unique recovery password stored in another password manager — so losing a security key doesn't lock you out permanently.
- Enroll a backup factor before tightening the policy, and confirm it unlocks the store on its own branch.
- A lost factor's secret cannot be recovered; only another satisfying branch of the policy can open the store.
An or is only as strong as its weakest satisfying set. A policy like
primary or (recovery and fido2) can be opened by primary alone, so the
extra security key adds nothing against an attacker who has that one password.
That's a fine trade-off when each branch is itself strong (e.g. a recovery
password you keep in another manager) — just choose each branch deliberately.
To encrypt an arbitrary file:
# To encrypt:
$ rcypher --encrypt input.txt --output input.txt.enc
Enter Password for input.txt:
# To decrypt:
$ rcypher --decrypt input.txt.enc --output input.txt
Enter Password for input.txt.enc:
If --output is omitted, resulting file is written to stdout:
$ rcypher --encrypt input.txt > input.txt.enc
$ rcypher --decrypt input.txt.enc > input.txt
A store created before multi-factor support is a legacy single-password store
(version 7). rcypher upgrades it to the current format automatically — there's
no command to run. When you open such a store, it is decrypted with your password
and converted in memory: a fresh random data-encryption key is generated, your
unlock password becomes the primary factor, and your secrets are re-encrypted
under the new key. rcypher tells you the upgrade is pending:
$ rcypher secrets.db
Note: 'secrets.db' is a legacy store; it will be upgraded to the current format on
the next write (the original is backed up to secrets.db.bak first).The on-disk file is rewritten in the new format lazily, on the next write
(put, del, an auth change, …). Before that first write, rcypher copies the
original file to secrets.db.bak so you keep an untouched backup, and prints a
line confirming it. A read-only session leaves the file untouched.
Once upgraded, the auth commands above work immediately, and you can enroll more
factors and set a policy. Your stored secrets are preserved throughout.
If you synchronize your storage file across devices (e.g., via Dropbox, Syncthing), conflicts may occur when changes are made on different devices. The --update-with option helps merge these conflicts:
$ rcypher secrets.db --update-with "secrets (conflicted copy).db"
Enter Password for secrets.db:
Found 3 keys with different values:
[NEW] api_token
New: sk-abc123def456 (2025-12-19 14:30:00)
[CONFLICT] github_pat
Current: ghp_old_token (2025-12-18 10:15:00)
Update: ghp_new_token (2025-12-19 14:25:00)
[CONFLICT] db_password
Current: old_pass (2025-12-17 08:00:00)
Update: new_pass (2025-12-19 14:28:00)
Summary: 1 new key, 2 conflicts
Apply updates? (a)ll at once, (i)nteractive, (c)ancel [a/i/c]:Merge modes:
- (a)ll at once: Apply all updates from the conflicted file automatically
- (i)nteractive: Review each change individually, accepting or rejecting one by one
- (c)ancel: Exit without making any changes
Security note: Values are displayed during comparison to help you make informed decisions. Passwords and sensitive data are written directly to the terminal (TTY), not to stdout, preventing accidental logging. Use --insecure-stdout only in testing environments.
This repository is a Cargo workspace:
rcypher— the reusable encryption library (this is the published library crate).rcypher-cli— the command-line tool described above, built on top of it.
Another application can encrypt and sign its own data format with the exact same envelope rcypher uses (Argon2id → AES-256-CBC → HMAC-SHA256, encrypt-then-MAC), and read those files back. Add the dependency:
[dependencies]
rcypher = "0.3"
# crypto envelope only, without rcypher's bundled key-value storage format:
# rcypher = { version = "0.3", default-features = false }Library feature flags: storage (default) — the bundled key-value store
(SecretStore); fido2 — hardware FIDO2 security-key factors; cli — reusable
interactive terminal plumbing (password/PIN prompts, the zxcvbn strength gate, and
the policy-unlock loop) for building an rcypher-like CLI on top of the library.
Bring your own serialization and hand rcypher the bytes:
use rcypher::{Cypher, CypherVersion, EncryptionKey};
// Encrypt your own bytes (a fresh random salt is embedded in the blob header):
let cypher = Cypher::new(EncryptionKey::from_password(CypherVersion::default(), "pw")?);
let blob = cypher.encrypt(my_serialized_bytes)?;
// Decrypt in memory — the key is re-derived from the salt inside the blob:
let reopened = Cypher::new(EncryptionKey::for_data("pw", &blob)?);
let plaintext = reopened.decrypt(&blob)?;The blob is self-contained ([ header | ciphertext | hmac ]), so it round-trips
from memory (encrypt/decrypt + EncryptionKey::for_data) or from disk
(save_encrypted/load_encrypted + EncryptionKey::for_file).
For a complete password- or multi-factor-protected store — not just the raw
envelope — implement the DataContainer trait for your type and use the
LockedContainer / UnlockedContainer facade: it owns the lock (a password or an
AND/OR factor policy), the AEAD envelope, atomic saves, and transparent legacy-file
upgrades, so a future on-disk format is adopted without any change on your side.
examples/custom_format.rs demonstrates both levels;
see the Cryptography Overview below for the construction details.
Cypher refuses to operate while a debugger is attached by default; for a
legitimately-traced host process, opt out with Cypher::with_trace_detection(key, false).
The quickest way (Linux x86_64) — download the prebuilt binary, verify its
checksum, and put it on your PATH:
curl -fsSL https://raw.githubusercontent.com/justpresident/rcypher/main/scripts/install.sh | bashIt picks the binary from the
latest release,
verifies its checksum, installs to /usr/local/bin (or ~/.local/bin if that
isn't writable), and — if that directory isn't on your PATH — adds it to your
shell's rc file. Override the location with RCYPHER_INSTALL_DIR=..., pin a tag
with RCYPHER_VERSION=v0.2.0, or skip the rc edit with RCYPHER_NO_MODIFY_PATH=1.
Prefer to read before you pipe? It's scripts/install.sh.
Or via cargo (any platform with a Rust toolchain):
cargo install rcypher-cli # installs the `rcypher` binaryPrebuilt binaries cover Linux (x86_64 and ARM64, static musl) and macOS (Intel + Apple Silicon). Windows isn't supported — use WSL or the library.
-
Offline, single-file encrypted storage
-
Encryption key derivation using Argon2id
-
Argon2id is a winner in a "password Hashing Competition 2015", see https://www.password-hashing.net/ for details
-
Random SALT is generated on every encryption operation
-
Strong authenticated encryption with AES-256-CBC-HMAC-SHA256
-
Quantum resistant, widely used in TLS, IPsec, PGP and many other security tools and protocols
-
Random IV is generated on every encrypt operation
-
Secure constant time HMAC check to avoid timing attacks
-
With encrypt-then-hmac approach, see https://www.daemonology.net/blog/2009-06-24-encrypt-then-mac.html for details
-
Secure password input (no terminal echo)
-
Secure terminal output using raw TTY access (bypasses stdout)
-
Automatic zeroing of sensitive memory
-
Optional clipboard copy with warnings
-
Minimal dependencies and attack surface
-
Explicit file format versioning
-
Automatic detection and upgrade of legacy storage formats
-
Conflict resolution for synchronized files (e.g., Dropbox conflicts)
rcypher is designed to protect secrets:
✔ against offline attackers who obtain the encrypted file
✔ against accidental disclosure via plaintext files
✔ against tampering and corruption of encrypted data
It does not protect against:
❌ a compromised operating system
❌ malware, keyloggers, or malicious clipboard managers
❌ privileged (root) attackers
❌ swap attacks or hibernation
❌ shoulder-surfing or screen recording
Partial protection (defense-in-depth, not security guarantees):
This tool focuses on at-rest encryption. Runtime protections are defense-in-depth measures that raise the bar for attackers but do not provide complete runtime secrecy.
-
Argon2id
-
Per-file random salt
-
Tunable memory and time cost
-
Password material is zeroized after use
-
AES-256 in CBC mode
-
Random per-file IV
-
Explicit padding handling
-
HMAC-SHA256
-
Covers file header and ciphertext
-
Verified in constant time
-
Authentication is checked before any decryption
If authentication fails, no data is decrypted or written.
In addition to encrypting data at rest, rcypher applies several defensive measures during runtime to reduce accidental exposure of secrets.
When displaying secrets to the user, rcypher writes directly to the controlling terminal (/dev/tty) instead of standard output.
This helps prevent secrets from being:
- accidentally redirected to files
- captured by shell pipelines
- logged by wrapper scripts
Note that terminal output may still be retained in:
- terminal multiplexers (tmux, screen), which capture the session's screen themselves
- screen recordings
- terminals without alternate-screen support (see below), where the session's output stays on the primary screen and its scrollback
This measure reduces accidental leakage but does not provide complete protection against runtime observation.
The interactive session runs on the terminal's alternate screen buffer — the scratch screen less and vim take over — and the screen is wiped before it switches back, on a normal exit and on the idle timeout or a security trip alike.
The alternate screen has no scrollback of its own, so values printed by get and history cannot be scrolled back to after the session ends; leaving it restores the shell exactly as it was before rcypher started. Terminals that ignore the switch fall back to the wipe alone, which clears the visible screen but not scrollback.
Sensitive values such as:
- encryption keys
- derived key material
- decrypted secret values
are stored in memory using zeroing containers and are explicitly cleared when they go out of scope.
This helps reduce the lifetime of sensitive data in memory and limits exposure in cases such as:
- accidental reuse of memory
- crashes
- partial memory inspection
Memory zeroing is a best-effort mitigation and does not protect against:
- a compromised operating system
- swap or hibernation
- debuggers or core dumps
- privileged attackers
Passwords are read without terminal echo and are not printed, logged, or stored in plaintext on disk.
On Unix-like systems, rcypher disables core dumps using setrlimit(RLIMIT_CORE, 0) at startup.
This prevents unencrypted secrets from being written to disk in the event of a crash, reducing the risk of:
- accidental exposure through crash dumps
- forensic recovery of plaintext secrets from core files
Limitations:
- Does not protect against swap files or hibernation
- Does not prevent privileged (root) attackers from forcing core dumps
- Does not protect against memory inspection by debuggers
rcypher continuously detects debugger/tracer attachment and refuses to decrypt
secrets while one is present. The mechanism is platform-specific:
On Linux — the secure fork model described in ptrace(2):
- At startup, the process forks into parent and child
- Child calls
PTRACE_TRACEMEto be traced by the parent - Child stores the parent PID and continues as the main application
- Parent monitors the child for its entire lifetime
- During runtime, the child continuously verifies that
TracerPid(from/proc/self/status) matches the stored parent PID; if it becomes 0 or changes, the app refuses to decrypt.
On macOS — the same properties without Linux's timer_create/PTRACE_TRACEME:
- continuous detection by checking this process's
P_TRACEDflag viasysctlon every watchdog tick (the analog of the LinuxTracerPidread); PT_DENY_ATTACHat startup to refuse future attaches, followed immediately by aP_TRACEDre-check so an already-attached debugger aborts;- the watchdog is driven by a kernel-signal interval timer (
setitimer(ITIMER_REAL)→SIGALRM), not a sleep loop, so a frozen or single-stepped process is detectable.
What this prevents:
- External debuggers from attaching (e.g.,
gdb,lldb,strace) - Runtime inspection via ptrace-based tools
- Dynamic analysis and memory inspection
Limitations:
- Does not protect against kernel-level debugging (e.g., kprobes, eBPF, DTrace)
- Does not protect against privileged (root) attackers who can modify kernel behavior
- Can be bypassed by tampering with the kernel's view of process state (e.g.
/proc/self/statuson Linux) in a compromised OS - Does not prevent static analysis or reverse engineering
These runtime protections are defense-in-depth measures and do not replace the core threat model. They provide additional barriers against casual memory inspection and debugging but cannot stop a determined attacker with OS-level privileges.
[ header | ciphertext | hmac ]
Header includes:
-
format version
-
padding length
-
encryption parameters
-
IV
-
salt
This allows forward-compatible format upgrades.
The current store format (version 8, the default for new stores) prepends a
keyslot header — the enrolled factors and the access policy, whose leaves carry
the secret-shares of the data-encryption key — to the encrypted payload above. The
leading version tag lets rcypher tell it apart from a plain version-7 password
store and pick the right unlock path. See
docs/auth-protocol.md for the normative spec.
rcypher can copy secrets to the system clipboard for convenience.
-
Desktop clipboard managers (KDE, GNOME, Windows, macOS) may:
-
retain clipboard history
-
synchronize clipboard contents
-
defeat time-based clearing (TTL)
-
Because of this:
-
Clipboard use is explicit
-
Users are warned
-
Clearing is best-effort only
If clipboard retention is unacceptable, use terminal output instead.
-
The interactive session runs on the alternate screen buffer, so what it prints leaves no scrollback behind — but secrets printed to the terminal may still remain in:
-
tmux/screen history
-
screen recordings
-
the scrollback of terminals that don't support the alternate screen
-
-
Clipboard copy trades security for convenience
-
Use strong, unique master passwords
-
No automatic synchronization (manual file sync tools like Dropbox/Syncthing can be used with conflict resolution)
-
No secret sharing or recovery
-
No formal security audit
-
Not intended as a drop-in replacement for audited password managers
- Extend the header with Argon2id parameters, autodetect too fast key derivation and auto-bump complexity
- Warn about weak master password
- Add a command to change master password
- Multi-factor unlock with boolean access policies (password factors) — see "Multi-factor unlock" above
- Add a FIDO2 security-key factor (
hmac-secret) to the multi-factor policy model —fido2feature (on by default in the CLI; opt-in for the library) - Add a user notification at start to perform regular backups in multiple places
- Add memory locking to prevent from swapping
- Enable MIRI in CI
- Add wrapping #[clippy::has_significant_drop] struct DecryptedValue for all decrypted data.
-
Add password generation feature
-
Implement nested hardened encryption for most important secrets. It is a good practice for users to split secrets in two separate places:
- The most important data, which is the most safety critical. This data is usually accessed unfrequently and therefore kept encrypted most of the time.
- Everything else - passwords from least crical services and other non-critical secrets. This would normally be most of the database.
So the idea is to require an extra password for accessing those secrets that are marked as hardened. This would add an extra level of security for this most secure data and prevent from leakage even when an attacker managed to obtain the main encryption key, e.g. on compromised OS or by shoulder-surfing.
-
Add command: rename for keys
-
Add groups/levels/folders
- Support rename for groups/levels/folders
-
Implement or intergrate some existing encrypted fs to store files securely in a storage
- Commands for extracting files outside and adding back in
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (git checkout -b some/amazing-feature)
- Commit your changes (git commit -m 'Add some amazing feature')
- Push to the branch (git push origin some/amazing-feature)
- Open a Pull Request
Apache-2.0
This software is provided as-is, without warranty of any kind. No security claims are made beyond what is explicitly documented.
A note to users
If you need:
-
multi-device sync
-
browser integration
-
audited security guarantees
Consider established, audited tools such as Bitwarden or KeePassXC.
rcypher is for users who prefer a small, transparent, offline tool and understand its limitations.
