Skip to content

Repository files navigation

CI Coverage

rcypher

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.

Usage

rcypher operates in two main modes:

  1. Encrypted key-value storage (default)
  2. Full file encryption / decryption

Encrypted storage (default)

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)

demo Secrets are:

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

Multi-factor unlock

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.

Security keys (FIDO2)

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 key

Answering 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-key

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

Password strength

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.

How it works

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.

Recovery and backup

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

A note on or branches

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.

Encrypting/Decrypting a file

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

Upgrading a legacy store to multi-factor

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.

Merging conflicting storage files

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.


Use as a library

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

Installing the CLI

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

It 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` binary

Prebuilt binaries cover Linux (x86_64 and ARM64, static musl) and macOS (Intel + Apple Silicon). Windows isn't supported — use WSL or the library.


Features

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

Threat Model

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):

⚠️ Debugger attachment: ptrace-based protection prevents casual debugging but can be bypassed by privileged attackers or kernel-level tools

⚠️ Core dumps: disabled at startup to prevent crash dumps, but doesn't protect against forced dumps or swap

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.

Cryptography Overview

Key Derivation

  • Argon2id

  • Per-file random salt

  • Tunable memory and time cost

  • Password material is zeroized after use

Encryption

  • AES-256 in CBC mode

  • Random per-file IV

  • Explicit padding handling

Authentication

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

Runtime Safety Measures

In addition to encrypting data at rest, rcypher applies several defensive measures during runtime to reduce accidental exposure of secrets.

Direct TTY Output

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.

Alternate Screen

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.

Memory Zeroing

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

Secure Password Input

Passwords are read without terminal echo and are not printed, logged, or stored in plaintext on disk.

Core Dump Protection (Linux/Unix)

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

Anti-Debugging Protection

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):

  1. At startup, the process forks into parent and child
  2. Child calls PTRACE_TRACEME to be traced by the parent
  3. Child stores the parent PID and continues as the main application
  4. Parent monitors the child for its entire lifetime
  5. 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_TRACED flag via sysctl on every watchdog tick (the analog of the Linux TracerPid read);
  • PT_DENY_ATTACH at startup to refuse future attaches, followed immediately by a P_TRACED re-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/status on 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.

File Format (High-Level)

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

Clipboard Behavior (Important)

rcypher can copy secrets to the system clipboard for convenience.

⚠️ Clipboard security is inherently limited:

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

Usage Notes

  • 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

Limitations

  • 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

Ideas for the next steps

Security

  • 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 — fido2 feature (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.

Features

  • 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

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b some/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin some/amazing-feature)
  5. Open a Pull Request

License

Apache-2.0

Disclaimer

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.

About

minimal, offline, file-based password storage and encryption tool for technical users

Resources

Security policy

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages