Skip to main content

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
python3 -m johnslock setup-path   # once: puts `johnslock` on your PATH

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.

"command not found: johnslock"

pip installs console scripts into a directory your shell often doesn't search — ~/Library/Python/3.9/bin on macOS, ~/.local/bin on Linux — and says so in a warning most people scroll past. One command fixes it permanently:

python3 -m johnslock setup-path
source ~/.zshrc          # or just open a new terminal

It finds the script directory, picks the right rc file for your shell (zsh, bash, fish, sh), backs that file up, and appends a single marked line. Running it twice does nothing the second time. --dry-run shows the change without making it; --rc, --shell, and --directory override the guesses.

python3 -m johnslock works for every command, so nothing is ever locked behind the PATH problem.

Commands

johnslock setup-path                 # put this command on your PATH (once, after install)
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 .johnslock file
  • 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 --passphrase mode 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 unlock into 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

johnslock-0.1.1.tar.gz (28.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

johnslock-0.1.1-py3-none-any.whl (25.3 kB view details)

Uploaded Python 3

File details

Details for the file johnslock-0.1.1.tar.gz.

File metadata

  • Download URL: johnslock-0.1.1.tar.gz
  • Upload date:
  • Size: 28.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.9.6

File hashes

Hashes for johnslock-0.1.1.tar.gz
Algorithm Hash digest
SHA256 1281bce1db7499635f6d5827add45d60de980ec4c56ad5e171e2fc4ea1d96ff7
MD5 231e795d515b8490f1d4df0d6bbc3612
BLAKE2b-256 4328f3772d35efc32e0398da69262edc06cbc69c1135d12f7d70f6ad3f7ae37b

See more details on using hashes here.

File details

Details for the file johnslock-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: johnslock-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 25.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.9.6

File hashes

Hashes for johnslock-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 85910796f328e24f8790e33bd1093bf1a6af3f1b8d4dee460d9b34b9203a17a8
MD5 238fa5e3a5fcc8e69a4411e8a86f5108
BLAKE2b-256 82a97575f925773aab9dfff1bd4725fe188caa9778cd86d6d0f30cd73aef2ea0

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page