Metadata-Version: 2.4
Name: johnslock
Version: 0.1.1
Summary: Encrypted secrets that replace your .env file — one command to lock, one import to read.
Author: johnslock contributors
License-Expression: MIT
Project-URL: Homepage, https://github.com/johnslock/johnslock
Project-URL: Issues, https://github.com/johnslock/johnslock/issues
Keywords: secrets,dotenv,env,encryption,aes-gcm,configuration,security,vault
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security :: Cryptography
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Systems Administration
Classifier: Topic :: Utilities
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: cryptography>=41.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0; extra == "dev"
Requires-Dist: build>=1.0; extra == "dev"
Requires-Dist: twine>=5.0; extra == "dev"
Dynamic: license-file

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

```bash
pip install johnslock
python3 -m johnslock setup-path   # once: puts `johnslock` on your PATH

johnslock init
johnslock lock API_KEY=verysecretkey
```

```python
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:

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

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

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

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

```python
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:

```python
# 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:

```bash
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`](https://pypi.org/project/cryptography/)

## Development

```bash
pip install -e ".[dev]"
pytest --cov=johnslock
./publish.sh --build-only
```

## License

MIT
