johnslock
Encrypted secrets that replace your .env file.
.env files are plaintext. You can't commit them, you end up mailing them
around, and one stray cat in a screen share leaks everything. johnslock keeps
the same one-file-per-project workflow, but the file is encrypted and the key
that opens it never lives in the project directory.
pip install johnslock
johnslock init
johnslock lock API_KEY=verysecretkey
import johnslock
api_key = johnslock.require("API_KEY")
That's it. Commit .johnslock — it is ciphertext.
How it works
| File | Where | Contains |
|---|---|---|
.johnslock |
your project, committed | AES-256-GCM ciphertext of every secret |
master.key |
~/.johnslock/, mode 0600, never committed |
the key that opens your vaults |
The vault stores one ciphertext covering all secrets at once, so the file leaks neither values nor names. Each vault has a random id and salt, both authenticated, so ciphertext cannot be moved between vaults or edited without detection. The data key is derived per vault with HKDF-SHA256 from the master key.
Steal the .johnslock file on its own and you get nothing usable — no names, no
values, and no way to test a guess offline, because there is no password to
guess.
Commands
johnslock init # create the vault + this machine's master key
johnslock init --passphrase # ...and protect the master key with a passphrase
johnslock lock API_KEY=verysecretkey # encrypt a secret in
johnslock lock DB_PASSWORD # type the value at a hidden prompt instead
johnslock list # names only, never values
johnslock unlock API_KEY # print one value (for shells and scripts)
johnslock rm API_KEY # remove a secret
johnslock run -- python app.py # run something with the secrets in its env
johnslock path # where is my vault, where is my key
johnslock export # make a shareable bundle + a one-time key
johnslock import bundle --key KEY # merge someone's bundle into your vault
johnslock unlock NAME prints the bare value, so shell use is easy:
export API_KEY="$(johnslock unlock API_KEY)"
Sending secrets to someone else
Your master key never leaves your machine, so you can't just send the vault.
export re-encrypts the secrets under a fresh one-time key instead:
$ johnslock export
bundle /work/myapp/myapp.johnslock-export (3 secrets)
transfer key K7QMT-3XBVR-9HJZD-2PWLS-6NCFA
Send the bundle and the key over two different channels.
They unlock it with:
johnslock import myapp.johnslock-export --key K7QMT-3XBVR-9HJZD-2PWLS-6NCFA
The key is stored nowhere — copy it now.
The transfer key carries ~116 bits of entropy, is stretched with scrypt, and is stored nowhere — not in the bundle, not on disk. Send the bundle over one channel (email, Slack, a file drop) and the key over another (a phone call, a Signal message). On the other side:
johnslock import myapp.johnslock-export --key K7QMT-3XBVR-9HJZD-2PWLS-6NCFA
The secrets land in their vault, re-encrypted under their master key. The transfer key is dead weight afterwards.
Python API
import johnslock
johnslock.require("API_KEY") # raises SecretNotFoundError if missing
johnslock.get("DEBUG", "0") # returns a default instead
johnslock.names() # ['API_KEY', 'DEBUG'] — no values
johnslock.as_dict() # every secret, as a plain dict
johnslock.load() # copy everything into os.environ
johnslock.load(override=True) # ...even over existing env vars
load() leaves existing environment variables alone by default, so a real
deployment's configuration is never silently replaced by a checked-in vault.
The vault is found by walking up from the current directory, exactly like
.env. Secrets are cached in memory and re-read automatically when the file
changes.
Drop-in replacement for python-dotenv:
# before
from dotenv import load_dotenv; load_dotenv()
# after
import johnslock; johnslock.load()
The strongest setting
johnslock init --passphrase encrypts the master key itself with a passphrase
you type (scrypt + AES-GCM). Then nothing on disk opens your vault — an
attacker with a full copy of your laptop still needs something that exists only
in your head.
For CI, set the passphrase in the environment:
export JOHNSLOCK_PASSPHRASE="..."
johnslock run -- pytest
Threat model — read this
johnslock is honest about what encryption on your own machine can and cannot do.
It protects you against:
- committing secrets in plaintext, and leaking them through git history
- a stolen, copied, or accidentally published
.johnslockfile - someone reading your screen, your repo, or a backup of it
- tampering — any edit to the vault is detected, not silently decrypted
- sending secrets to a colleague without a plaintext file existing anywhere
It does not protect you against:
- someone who already runs code as you on your machine in default mode. They
can read
~/.johnslock/master.key, exactly as they could read.env. Use--passphrasemode to close this gap. - malware that reads your process memory after you have decrypted secrets
- a secret you print to a log, paste into a chat, or
johnslock unlockinto a shell history
No local tool can make a secret readable by your program but unreadable by you. What johnslock does is stop the secret ever sitting on disk in the clear, and keep the key out of the directory the file lives in.
Environment variables
| Variable | Effect |
|---|---|
JOHNSLOCK_HOME |
keystore directory (default ~/.johnslock) |
JOHNSLOCK_VAULT |
use this vault file instead of searching upward |
JOHNSLOCK_PASSPHRASE |
passphrase for passphrase-mode keys, for CI |
Crypto
- AES-256-GCM for every ciphertext (authenticated, tamper-evident)
- HKDF-SHA256 for per-vault key derivation from the master key
- scrypt (N=2^15, r=8, p=1) for passphrase-derived keys
- all randomness from
os.urandom - one dependency:
cryptography
Development
pip install -e ".[dev]"
pytest --cov=johnslock
./publish.sh --build-only
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file johnslock-0.1.0.tar.gz.
File metadata
- Download URL: johnslock-0.1.0.tar.gz
- Upload date:
- Size: 24.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0b6a11e6c53206bcbe7fae82c19ef0b8ef5e8c297ee7bc8bce1608beaec28186
|
|
| MD5 |
88a699d482fac8d643e1d9e4a2daf7ec
|
|
| BLAKE2b-256 |
eec3fb8d25503da97c0d3354f3b72df85a2c81866aecc805381437a68b099ef3
|
File details
Details for the file johnslock-0.1.0-py3-none-any.whl.
File metadata
- Download URL: johnslock-0.1.0-py3-none-any.whl
- Upload date:
- Size: 21.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6de16903d357d53791d4b5bc31e154ac45c2411259d254e1ffa22f1a5d8daa13
|
|
| MD5 |
0b692499fa70f9fa43b8c1c98f793883
|
|
| BLAKE2b-256 |
90491dfc30c6f3e39394df1498a71194c440bb6f0e6174882fa509df888773b4
|