Skip to content

Reference

Supported files

Keyloom determines the serialization format from the filename extension.

Format Extensions
TOML .toml
YAML .yaml, .yml
JSON .json

YAML input uses the KORML v0 reader: unversioned, single-document, JSON-shaped YAML with block and flow collections. YAML anchors, aliases, custom tags and multi-document streams are not supported. Duplicate mapping keys and malformed collections are rejected. Every YAML scalar is a string, quoted or not: port: 5432 reads as the string "5432", and Keyloom gives no meaning to the values it does not encrypt. New YAML files can therefore hold strings, lists and mappings only. When changing a loaded file, Keyloom replaces only the values it changes, written double-quoted, and keeps every comment and the formatting of everything else.

The __keyloom__ representation has the same semantics in every format.

Secret marker

A mapping is a Keyloom secret when it contains:

__keyloom__ = "secret"
value = <scalar>

__keyloom__ is reserved. If it is present, its value must currently be secret, the mapping must contain value, and it may contain no other field.

A value of exactly __keyloom__secret__ is a placeholder. encrypt asks for its secret; every other operation rejects it:

__keyloom__ = "secret"
value = "__keyloom__secret__"

TOML

token = { __keyloom__ = "secret", value = "plaintext" }

YAML

token:
  __keyloom__: secret
  value: plaintext

JSON

{
  "token": {
    "__keyloom__": "secret",
    "value": "plaintext"
  }
}

Keyloom encrypts strings only, in every format; a secret that is a number or a boolean is written as a string, such as "5432". Any other value in a secret wrapper is rejected with InvalidSecret, and an envelope that decrypts to anything but a string with MalformedEnvelope. A placeholder always receives a string.

Kinds of values

The form of a value alone decides its kind. The placeholder __keyloom__secret__ is a placeholder. A string that starts with $KEYLOOM; is an envelope: encrypted if it is well-formed, as described below, and malformed otherwise. Any other value is plaintext.

encrypt turns plaintext and placeholders into envelopes and stops at a malformed envelope. A secret entered for a placeholder that starts with $KEYLOOM; is rejected, because it would not be plaintext. Every other operation accepts only encrypted values.

Encrypted value format

An encrypted scalar is one string, in this exact form:

$KEYLOOM;1;argon2-v2;s=<salt>;n=<nonce>;c=<data>
Field Meaning
1 Version of the envelope grammar
argon2-v2 The algorithm that encrypted the value
s URL-safe Base64 salt, 16 random bytes per scalar
n URL-safe Base64 AES-GCM nonce, 12 random bytes per scalar
c URL-safe Base64 ciphertext followed by the 16-byte GCM tag

Each of s, n and c is URL-safe Base64 without = padding, in the canonical form of RFC 4648, section 3.5: the unused bits of the last character are zero. A field in any other spelling is malformed, even when it decodes to the expected bytes, so every envelope has exactly one spelling.

The envelope version only changes if the layout of the string changes. The algorithm is a separate axis.

Algorithms

An algorithm is a frozen suite: its name fixes every cryptographic parameter, and neither the command line nor the API can choose them. Changing a parameter means adding an algorithm under a new name, so a name never changes meaning and every envelope stays readable by later versions. Decryption takes the algorithm from the envelope; only encryption chooses one. The command line takes the name with --algorithm; the API takes the member of keyloom.Algorithm, ARGON2_V2 or ARGON2.

Algorithm Key derivation Password Padding Encryption
argon2-v2 (default) Argon2id, 64 MiB, 3 passes, 4 lanes NFC, UTF-8 32-byte blocks AES-256-GCM
argon2 Argon2id, 64 MiB, 3 passes, 4 lanes UTF-8 as entered none AES-256-GCM

The password is normalized to Unicode NFC before it is encoded as UTF-8, so a password typed with a precomposed é and one typed as e plus a combining accent derive the same key. argon2 encodes the password exactly as entered.

AES-GCM encrypts the JSON serialization of the scalar and authenticates it together with the first three fields, $KEYLOOM;1;<algorithm>, as associated data. The file and the location of the value are not part of it, so an envelope can be copied to another file or field; How it works explains the tag. An envelope with another layout, field order or field length is malformed, and an algorithm this version does not know raises UnsupportedAlgorithm.

With padding, the serialization is followed by one 0x80 byte and as many zero bytes as reach the next multiple of the block size, as in ISO/IEC 7816-4. An envelope therefore reveals the length of its secret only to the nearest block: every string whose serialization takes at most 31 bytes, such as a string of up to 29 ASCII characters, gives an envelope of the same length. Without padding, as in argon2, the envelope reveals the exact length.

Command line

keyloom encrypt [PASSWORD] [--algorithm NAME] [-o OUTPUT] FILE
keyloom decrypt [PASSWORD] [--unwrap] [-o OUTPUT] FILE
keyloom reencrypt [PASSWORD] [NEW] [--algorithm NAME] [-o OUTPUT] FILE
keyloom check [--verify [PASSWORD]] FILE...

PASSWORD is one of the password sources below: --password-stdin or --password-file FILE. Without one, the command prompts. NEW is --change-password or --new-password-file FILE.

keyloom encrypt

Encrypt all marked plaintext values in a file, and ask for the value of every placeholder.

keyloom encrypt credentials.toml

Without --output, the source file is replaced atomically. Its existing permission bits are retained when possible.

With an explicit output:

keyloom encrypt \
  --output credentials.encrypted.toml \
  credentials.toml

--algorithm chooses the algorithm for newly encrypted values; the default is argon2-v2. Already-encrypted marked values are not encrypted again. They are authenticated with the supplied password before the operation proceeds.

The command reports how many secrets it encrypted. A file without plaintext secrets is not rewritten, so its formatting stays exactly as it was:

credentials.toml: encrypted 2 secrets
credentials.toml: nothing to encrypt

Interactive password input prompts twice:

Vault password:
Confirm vault password:

After the password, whatever its source, and after every existing envelope has been checked against it, the command asks for each placeholder in the order of the file. Each secret is entered twice, without echo, and the prompt names its location:

Secret for git."gitlab.example.org".token:
Confirm secret for git."gitlab.example.org".token:

An empty secret or two different entries end the command, and so does a placeholder when there is no terminal to prompt on. A prompt reads one line. The file is written only after every secret has been entered.

keyloom decrypt

Decrypt marked values and write the resulting document to standard output. Every secret must be encrypted; a plaintext secret is an error.

keyloom decrypt credentials.toml

The secret wrappers are retained:

token = { __keyloom__ = "secret", value = "plaintext" }

Use --unwrap to replace every wrapper by its value in the emitted document. Comments and formatting of the rest of the document are kept:

keyloom decrypt --unwrap credentials.toml

which emits:

token = "plaintext"

Writing plaintext to disk is explicit:

keyloom decrypt \
  --output credentials.plain.toml \
  credentials.toml

The plaintext output file is written atomically with mode 0600; the permissions of a file it replaces are not inherited. Keyloom refuses to write the plaintext onto the input file itself, including through a symlink or a hard link.

Every command that writes a file refuses two kinds of output by default. A symbolic link at the output path would redirect the output, plaintext included, to any file you can write; --dangerous-allow-writing-to-symlink writes to the file the link points to instead and keeps the link. An output that other hard links point to is replaced by a new file, so the other links keep its old content, which for an in-place encrypt is the plaintext; --dangerous-allow-hard-links writes anyway. Symlinked parent directories are followed. The library functions take the same options as dangerous_allow_writing_to_symlink and dangerous_allow_hard_links.

keyloom reencrypt

Encrypt every secret of a file again, in memory, without writing plaintext anywhere. This moves a file to another algorithm, or to a new password:

keyloom reencrypt --algorithm argon2-v2 credentials.toml
keyloom reencrypt --change-password credentials.toml
keyloom reencrypt --new-password-file new-password credentials.toml

Without --change-password or --new-password-file, the password stays the same. --change-password prompts for the new password twice. The file is replaced atomically unless --output names another file. Every secret must already be encrypted.

keyloom check

Fail if any secret in the given files is not encrypted:

keyloom check credentials.toml deploy/*.yaml

Without a password, check verifies the structure: every wrapper must be a valid secret whose value is a well-formed envelope. It needs no password, so it suits a CI job or a pre-commit hook that guards against committing plaintext. With --verify, it also decrypts every secret with the vault password, which shows that the file is complete and uses one password:

keyloom check --verify --password-file /run/secrets/vault credentials.toml

Every problem in every file is printed on its own line, and the exit status is 1 if there was any:

credentials.toml: token: secret is not encrypted; run keyloom encrypt
credentials.toml: key: secret is a placeholder; run keyloom encrypt to enter it

A password source without --verify is a usage error.

Password sources

Password-source options are mutually exclusive.

Interactive prompt

If no password option is supplied, Keyloom reads the password with a terminal prompt.

keyloom decrypt credentials.toml

The password is never echoed. Without a terminal, as in a CI job, the command fails instead of reading it from standard input with echo; use --password-stdin or --password-file there.

--password-stdin

Read one password line from standard input:

printf '%s\n' "$KEYLOOM_PASSWORD" |
  keyloom decrypt --password-stdin credentials.toml

One trailing \n or \r\n line ending is removed.

--password-file FILE

Read the password from a file:

keyloom decrypt \
  --password-file /run/secrets/keyloom-password \
  credentials.toml

One trailing \n or \r\n line ending is removed. On POSIX systems, a file that other users can access, such as one with mode 0644, gives a warning on standard error; the command still runs. chmod 600 silences it. The same holds for --new-password-file.

An empty password is rejected.

Errors and exit status

Status Meaning
0 Success
1 A failure the user can fix, reported in one line
2 Invalid command-line usage
70 An internal error: a bug in keyloom
130 Interrupted
141 The reader of standard output went away, as when piped into head

No command ends with a Python traceback. A failure the user can fix, such as a wrong password, a malformed file, an invalid secret wrapper or a missing file, names the file and the location of the value inside it:

keyloom: app.toml: token: wrong password or modified keyloom value

Error messages name keys and positions but never quote a line of the file or a secret value, so a secret near a syntax error does not end up in a terminal or a CI log.

Any other exception is a bug. The command then prints a bug report to standard error, ready to paste into an issue at the tracker named in the package metadata:

keyloom: internal error: RuntimeError: ...

This is a bug in keyloom 0.1.0. Please open an issue at ...

### Bug report

Command: `keyloom decrypt --unwrap --password-file`

```text
keyloom 0.1.0
python 3.12.3 on Linux-...
cryptography ...
```

```text
Traceback (most recent call last):
  ...
```

The report names the command and its options but none of their values, such as file names. A traceback shows code, not variables, but an exception message can quote data, so check the report for secrets before posting it. Nothing is saved to disk.

In Python, every failure the user can fix raises a subclass of keyloom.KeyloomError; the Python API describes each one.