monkeys

pack

Seal a profile's secrets into one encrypted file.

monkeys pack [name] [--path DIR] [--ask] [--open] [--only [KEY[,KEY...]] [@profile[,profile...] [KEY[,KEY...]]]...]

Writes a project's secrets as one encrypted file, the only way they leave the vault. With no arguments it takes every profile the .monkeys file declares:

monkeys pack
wrote /tmp/a.monsecrets: +foo @test,production @production, 5 secrets
copied its passphrase to your clipboard; send that the other way

Send the file however you like, and paste the passphrase into something else: a different messenger, a call, a password manager's share. Both down one channel is one leak away from being no encryption at all.

The file goes to /tmp, which is never inside a repository, and the message shows the path. --path names a directory to write into, and the name before --only names the file inside it. A name carries no directory of its own:

monkeys pack --path ~/Desktop
monkeys pack for-sam --path ~/Desktop

Inside a git checkout it asks first, since a bundle a commit can reach is a bundle that can be published:

a bundle into ~/code/shop/a.monsecrets, where a commit can take it. go ahead? [y/N] y
wrote ~/Desktop/a.monsecrets: +foo @test,production @production, 5 secrets
wrote ~/Desktop/for-sam.monsecrets: +foo @test,production @production, 5 secrets

It carries the project's namespace, each profile's name, the keys the project lists for it, and their secrets, sealed with ChaCha20-Poly1305 under a key scrypt derives from the passphrase. The values of the packed profiles, the KEY=value lines, ride along whole, since they are public anyway, and the message counts them:

wrote /tmp/a.monsecrets: +foo @test, 2 secrets, 2 values

One bundle carries one project. The file is safe to send over whatever you already use; the passphrase goes another way.

--open then reveals the file, in Finder with the file selected, or its folder through xdg-open on Linux, since the next thing to do with a bundle is to drag it somewhere.

A pack with a secret still missing refuses, since a bundle that fills half a profile is a bug for whoever receives it.

A bundle that would carry @personal asks first, whichever way that profile got in. The name is the convention for a key each person holds their own copy of, so handing one over is a decision rather than a step, and the question takes the word instead of a letter:

@personal is yours, not the team's, and a bundle hands it to whoever opens it. [UNDERSTOOD/N] UNDERSTOOD

Anything else answers no, and so does a pack with nobody to answer: the question needs a terminal, which is what keeps a script from bundling one person's keys by itself. The refusal names the profiles the file has besides it:

monkeys: nothing written. to pack the rest: monkeys pack --only @test,production

The passphrase

Everything else about the file is fixed, so the passphrase is the only part anyone decides, and the only part an attacker can work on. A stolen bundle is guessed at offline for as long as someone cares to, and scrypt's 128 MB a guess buys time only in proportion to how hard the passphrase is to guess.

So pack does not ask for one. It draws 24 random characters and puts them on your clipboard, and they are never printed, never stored and never in your shell history. Paste that into whatever carries it to the other person, which is not the thing carrying the file.

A clipboard manager keeps what it is given, sometimes for good. If you run one, take the passphrase out of its history once the bundle has arrived.

--ask prompts for one you choose instead, and leaves the clipboard alone:

monkeys pack --ask
passphrase:
again:
wrote /tmp/a.monsecrets: +foo @test,production @production, 5 secrets

Choose that way and the file is only as good as the passphrase, so take one from a password manager rather than thinking one up. On a machine with no clipboard tool pack says so and asks, since a file whose passphrase nobody has is a file nobody can open. The passphrase is still read from standard input when it is not a terminal, so a script that pipes one keeps working.

--only

--only says which profiles and keys, and reads the way the file is written. A @profile opens a block, @a,b opens one for several profiles at once, and the keys after it belong to every profile in that block. A block with no keys after it goes whole; keys before any @ come from the default profile, the first the file mentions. That is how a teammate gets test and never production, or one key on its own:

monkeys pack --only @test
monkeys pack shared --only @test @production
monkeys pack --only DATABASE_URL
monkeys pack --only @test,production DATABASE_URL
monkeys pack --only @test DATABASE_URL @production SENTRY_DSN
wrote /tmp/a.monsecrets: +foo @test, 2 secrets
wrote shared.monsecrets: +foo @test @production, 5 secrets
wrote /tmp/a.monsecrets: +foo @test, 1 secret
wrote /tmp/a.monsecrets: +foo @test,production, 2 secrets
wrote /tmp/a.monsecrets: +foo @test @production, 2 secrets

The bundle keeps that shape, block for block, and unpack writes it back as the project file. The path goes before --only, which takes the rest of the line. A key a profile does not list is refused rather than left out.

Outside a project, --only @foo.test names a profile in full, and a bundle made there carries its profiles under their full names, with no + line.

The passphrase is read from standard input when it is not a terminal, for the rare script that needs to.

On this page