██████╗ ██████╗ ████████╗ ██╗ ██████╗ ██████╗ ██╗ ██╗
██╔══██╗ ██╔═══██╗ ╚══██╔══╝ ██║ ██╔═══██╗ ██╔════╝ ██║ ██╔╝
██║ ██║ ██║ ██║ ██║ ██║ ██║ ██║ ██║ █████╔╝
██║ ██║ ██║ ██║ ██║ ██║ ██║ ██║ ██║ ██╔═██╗
██████╔╝ ╚██████╔╝ ██║ ███████╗ ╚██████╔╝ ╚██████╗ ██║ ██╗
╚═════╝ ╚═════╝ ╚═╝ ╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝
Encrypt your .env. Share by key, not by password.
DotLock is a small Rust CLI that encrypts a project's environment variables and lets you run commands with the decrypted values injected into the process environment — without ever writing plaintext secrets to disk.
It is designed to replace ad‑hoc .env files for local development and small teams: secrets stay encrypted at rest, are decrypted only in memory, and can be shared between developers using public‑key cryptography instead of shipping a shared password around.
Features
- Per‑project encrypted vault stored under
.lock/next to your code. - XChaCha20‑Poly1305 authenticated encryption for individual secrets.
- Argon2id key derivation from the master password, with HKDF on top to derive a key‑encryption key.
- DEK / KEK split: a random 32‑byte Data Encryption Key (DEK) encrypts the secrets, and is itself wrapped by a Key Encryption Key (KEK) derived from your password — so rotating the password does not re‑encrypt every secret.
- Short‑lived session cache (
sudo‑style): after the first unlock the DEK is cached for 30 seconds in your user directory, so you don't retype the master password on every command. - Shared access mode (
dotlock share): each recipient gets the DEK wrapped with their own RSA public key, so a project can be unlocked by any authorized identity without a shared password. - Local identity (
dotlock cert): generates a passphrase‑protected RSA key pair under~/.lock/identity/for use as a recipient. - Run with secrets (
dotlock run <cmd>): decrypts in memory and spawns the child process with the variables injected as environment. - Rotation primitives: rotate the master password or generate a brand‑new project key (re‑encrypting every secret).
.envimport (dotlock migrate): bulk‑import an existing.envfile in one step.- Integrity check: the vault metadata stores an authenticated hash of the secrets file so tampering is detected on unlock.
- Defensive memory handling: keys and passwords are wiped with
zeroizeafter use.
Installation
The crate is dotlock-bin (the name dotlock was already taken on crates.io by an unrelated lockfile crate). The installed binary is named dl — short and quick to type.
From crates.io
# binary `dl` will be at ~/.cargo/bin/dl
From source
Rust edition 2024 toolchain required:
# binary will be at ./target/release/dl
Drop the binary somewhere on your PATH, e.g.:
Usage examples
1. Solo developer — replace your .env
# Initialize the vault (prompts for a master password, or generates one for you)
# Pull in everything from your existing .env in one shot
# Verify what's stored — names only, never plaintext
# (optional) once you trust the vault, delete the plaintext .env
Now run your app with the secrets injected only into the child process:
Need to add or update one variable on the fly:
2. Daily workflow with the session cache
The first command of the day prompts for the master password. Subsequent commands within 30 seconds (default TTL) reuse the cached key — sudo‑style:
# Done for the day — drop the cached key:
Want the cache to last longer? Bump the TTL in your shell rc:
# 15 minutes
3. Team workflow — shared access by public key
Every developer keeps a personal RSA identity once, then projects grant access to their public key.
One‑time, on each developer's machine:
On the project (the owner does this once per teammate):
Commit .lock/vault.toml and .lock/secrets.lock to your repo. From now on, anyone listed as a recipient can unlock the project with their identity passphrase — no shared master password ever leaves a developer's laptop.
When someone leaves the team:
# DotLock automatically:
# 1. removes Alice from the recipients list
# 2. generates a new project DEK
# 3. re-encrypts every secret with the new DEK
# 4. re-wraps the new DEK for the remaining recipients
4. Rotating credentials
# Change the master password (keeps existing secrets, only re-wraps the DEK):
# Roll the project key (re-encrypts every secret — do this if you suspect leakage):
5. CI / scripted use
For non‑interactive contexts, the typical pattern is to export DOTLOCK_CACHE_TTL to something workflow‑sized, unlock once, then run several commands:
Command reference
All commands accept short aliases — for example dl s is set, dl g is get, dl l is list, dl rm / dl del is unset.
Project lifecycle
| Command | Purpose |
|---|---|
dl init |
Initialize the current directory as a DotLock project. Prompts for a master password (or generates a strong one), creates .lock/vault.toml and .lock/secrets.lock. |
dl lock (alias logout) |
Invalidate the cached DEK so the next operation re‑prompts for the master password. |
Secrets
| Command | Purpose |
|---|---|
dl set <NAME> <VALUE> |
Store or overwrite a variable. Default algorithm is xchacha20-poly1305. |
dl get <NAME> |
Show the metadata of a variable. The plaintext is not printed by default — use dl run to consume it. |
dl unset <NAME> |
Remove a variable. |
dl list |
List variable names (no plaintext). |
dl migrate [path] |
Import every variable from a .env file (defaults to ./.env) in a single transaction. |
dl run <cmd> [args...] |
Decrypt every secret in memory and spawn <cmd> with them as environment variables. |
Local identity (for shared mode)
| Command | Purpose |
|---|---|
dl cert init [--force] |
Generate a passphrase‑encrypted RSA key pair under ~/.lock/identity/. |
dl cert show |
Print the fingerprint and key paths of the local identity. |
dl cert export-pub [path] |
Print the local public key, or write it to path so it can be shared. |
Shared project access
| Command | Purpose |
|---|---|
dl share enable |
Switch the project from master‑password mode to shared mode. |
dl share grant --pubkey <PATH> --label <NAME> |
Wrap the project DEK for the given public key and add it as a recipient. |
dl share revoke <ID|LABEL|FINGERPRINT> |
Remove a recipient and rotate the project key, re‑encrypting every secret and re‑wrapping for the remaining recipients. |
dl share list |
Show every recipient with label and fingerprint. |
Rotation
| Command | Purpose |
|---|---|
dl rotate master-password |
Change the master password without re‑encrypting secrets — only the wrapping of the DEK changes. |
dl rotate project-key |
Generate a brand‑new project DEK and re‑encrypt every secret. Recipients are re‑wrapped automatically. |
How it works
Files on disk
<project>/
└── .lock/
├── vault.toml # public metadata: KDF params, salt, wrapped DEK, recipients, integrity hash
└── secrets.lock # encrypted secret records (one per variable)
~/.lock/
├── identity/ # local RSA key pair (passphrase-encrypted)
│ ├── identity.pem
│ ├── identity.pub.pem
│ └── identity.toml
└── run/sessions/<project-uuid-prefix>/
└── sessions.toml # short-lived cached DEK (default 30s TTL)
The .lock/ directory is created with 0700 permissions; secret and key files use 0600.
Key hierarchy
master password
└── Argon2id (salt, m=64MiB, t=3, p=1) ──► master_key (32 B)
└── HKDF-SHA256 (project, environment, kek_version) ──► KEK (32 B)
└── XChaCha20-Poly1305 wrap ──► DEK (32 B, random)
└── XChaCha20-Poly1305 ──► each secret value
- The master password never leaves the prompt — only the derived key material is kept (and zeroized as soon as it's consumed).
- The DEK is what actually encrypts secrets. It is generated once at
initand only ever changes duringrotate project-keyorshare revoke. - The KEK is ephemeral: it is re‑derived from the master password on every unlock and discarded.
- Rotating the master password therefore only rewraps the DEK; rotating the project key re‑encrypts every secret in
secrets.lock.
Integrity
Every time the secrets file is written, the vault metadata stores a fresh nonce and authenticated hash of its contents (encrypted under the DEK). On unlock, this is verified — if secrets.lock was edited or replaced out‑of‑band, the operation aborts.
Session cache
After the first successful unlock, the DEK is base64‑encoded and written to ~/.lock/run/sessions/<short-uuid>/sessions.toml with an expiry timestamp. Subsequent commands within the TTL skip the password prompt entirely.
- Default TTL is 30 seconds. Override with the
DOTLOCK_CACHE_TTLenvironment variable (in seconds). - The cache is per project, keyed by the project's UUID stored in
vault.toml. - In shared mode the cache is disabled by default — set
DOTLOCK_SHARED_CACHE=1to opt in. dl lockdeletes the cache file immediately.
Shared mode
When you run dl share enable and grant access to one or more public keys, the vault metadata gains a recipients table. Each recipient stores its label, fingerprint, and the project DEK wrapped with that recipient's RSA public key (RSA‑OAEP). A user that holds the matching private key (managed via dl cert) can unlock the project without ever knowing the master password.
Revocation is destructive on purpose: dl share revoke removes the recipient, generates a new DEK, re‑encrypts every secret with it, and re‑wraps the new DEK for every remaining recipient. The revoked party cannot decrypt anything written after the revocation, even if they kept a copy of the old vault.
Environment variables
| Variable | Default | Purpose |
|---|---|---|
DOTLOCK_CACHE_TTL |
30 |
Lifetime in seconds of the cached DEK. |
DOTLOCK_CACHE_DIR |
$HOME/.lock |
Override the root for the session cache. |
DOTLOCK_SHARED_CACHE |
false |
Set to 1/true to enable session caching while in shared mode. |
DOTLOCK_IDENTITY_DIR |
$HOME/.lock/identity |
Override where the local RSA identity is stored. |
Project layout
src/
├── main.rs # CLI surface (clap)
├── runtime/ # encrypt / decrypt / `run` orchestration
├── crypto/ # KDF, KEK, DEK, integrity hash, RSA share, password generator
├── storage/ # vault file, secrets file, identity, cache, .env parser, atomic FS helpers
├── domain/ # error types and shared model definitions
└── utils.rs # small helpers (variable name normalization, pretty printing)
Security notes
- Master passwords are validated for length and character mix, or you can let DotLock generate a 32‑character random one (printed once, never stored).
- All secret files are written atomically with restricted permissions (
0600). - Sensitive byte buffers are wrapped in
Zeroizing<…>and explicitly zeroed when no longer needed. - The local identity's private key is stored encrypted under a separate passphrase, distinct from any project's master password.
dl getdeliberately does not print the plaintext value — usedl runto consume secrets, so values stay out of shell history and terminal scrollback.
License
Dual‑licensed under either:
- MIT license (LICENSE-MIT or https://opensource.org/licenses/MIT)
- Apache License, Version 2.0 (LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0)
at your option.