# SOPS-style config encryption

Encrypt or decrypt YAML, JSON and .env values with a passphrase. Keys stay readable, the file stays valid, and diffs still show what changed.

## Run it

- **CLI:** `gizza tool sops-encrypt "app: demo
database:
  host_unencrypted: db.internal
  password: s3cr3t
  port: 5432" 'passphrase=A long passphrase you can reproduce later'`
- **Web:** https://gizza.ai/tools/sops-encrypt/
- **Agents:** machine-readable descriptor (parameters JSON Schema) at https://gizza.ai/tools/sops-encrypt/tool.json

## Inputs

- `document` — YAML, JSON or .env document _(field)_
- `passphrase` — Passphrase _(field)_
- `mode` — Mode _(field)_
- `format` — Document format _(field)_
- `encrypted_suffix` — Encrypt only keys ending with _(field)_
- `unencrypted_suffix` — Leave keys ending with in the clear _(field)_
- `encrypted_regex` — Encrypt only keys matching _(field)_
- `unencrypted_regex` — Leave keys matching in the clear _(field)_

## Output

- Rewritten document (text)

## Query parameters

Open the tool pre-filled and auto-run via URL:

- `document` — YAML, JSON or .env document
- `passphrase` — Passphrase
- `mode` — Mode
- `format` — Document format
- `encrypted_suffix` — Encrypt only keys ending with
- `unencrypted_suffix` — Leave keys ending with in the clear
- `encrypted_regex` — Encrypt only keys matching
- `unencrypted_regex` — Leave keys matching in the clear

Example: `https://gizza.ai/tools/sops-encrypt/?document=app%3A%20demo%0Adatabase%3A%0A%20%20host_unencrypted%3A%20db.internal%0A%20%20password%3A%20s3cr3t%0A%20%20port%3A%205432&passphrase=A%20long%20passphrase%20you%20can%20reproduce%20later&mode=encrypt&format=auto&encrypted_suffix=_secret&unencrypted_suffix=_unencrypted&encrypted_regex=%5E%28password%7Ctoken%7C.%2A_key%29%24&unencrypted_regex=%5E%28host%7Cregion%7Cport%29%24`

---

## About this tool

Encrypt the **values** of a YAML, JSON or `.env` file with a passphrase while leaving
every **key** in the clear. Instead of turning the whole file into one opaque blob,
each selected leaf value is replaced with a marker:

```text
password: ENC[GZAE1,data:...,iv:...,tag:...,type:str]
```

The result is still valid YAML, JSON or `.env`, so it can be committed, reviewed, and
diffed — a code review shows *which* secret changed without showing the secret. A small
metadata block (`gizza_sops` in YAML/JSON, `GIZZA_SOPS_*` keys in `.env`) records the KDF
salt so the same passphrase can decrypt it later.

Under the hood: one AES-256 key per document derived with PBKDF2-HMAC-SHA256 (200,000
iterations) over a fresh 16-byte random salt, then AES-256-GCM per value with a fresh
96-bit IV. Each value's dotted key path is used as the authenticated data, so a ciphertext
copied from one key onto another fails to decrypt instead of silently succeeding. The
recorded `type:` restores integers, floats and booleans as their original types on decrypt.

Typical run:

1. Paste your config into the document field.
2. Enter a passphrase you can reproduce later, and keep **Mode** on `Encrypt values`.
3. Leave **Document format** on `Auto-detect`, or pin it if the file is ambiguous.
4. Copy the rewritten document. To reverse it, paste it back and switch Mode to
   `Decrypt values` with the same passphrase.

Choosing what gets encrypted — set **one** rule at a time, or none:

- `Encrypt only keys ending with` — e.g. `_secret`: nothing else is touched.
- `Leave keys ending with in the clear` — defaults to `_unencrypted`; clear it to encrypt
  every value.
- `Encrypt only keys matching` — a regular expression, e.g. `^(password|token|.*_key)$`.
- `Leave keys matching in the clear` — a regular expression, e.g. `^(host|region|port)$`.

A rule matches against the key **and its ancestors**, so exempting `public_unencrypted`
exempts the whole subtree beneath it. Setting two rules at once is rejected rather than
silently resolved.

Limits and edge cases: documents are capped at 2 MiB. The YAML and JSON roots must be a
mapping/object so the metadata block has somewhere to live. `null` values are skipped, and
list items are walked by index. Comments and key order survive in `.env`; YAML and JSON
are re-serialized, so comments and exotic formatting in those two are not preserved.
Encrypting an already-encrypted document is refused, and so is decrypting a document with
no metadata block. Every run produces different ciphertext for the same input — that is the
fresh salt and IV doing their job, not a bug.

This is a passphrase-based container format, deliberately **not** the on-disk format of the
`sops` command-line tool, whose metadata describes KMS, `age` or PGP keys. Output from this
page is not interchangeable with that CLI.

## FAQ

<details>
<summary>Can the sops CLI read a file encrypted here?</summary>

No. This page uses its own `ENC[GZAE1,...]` marker and a passphrase-derived key, because a
passphrase has no place in the KMS/`age`/PGP metadata that the `sops` binary expects.
Reusing that marker would claim compatibility that does not exist. Decrypt here, with the
same passphrase.

</details>

<details>
<summary>Why are the keys still readable?</summary>

That is the point of value-level encryption. Readable keys mean the file still parses,
still validates against a schema, and still produces a meaningful `git diff` — you can see
that `database.password` was rotated without anyone seeing the password. If you need the
key names hidden too, encrypt the whole file with a file-level tool instead.

</details>

<details>
<summary>What happens if I lose the passphrase?</summary>

The values are unrecoverable. The key is derived from the passphrase alone; only the salt
is stored in the document, never the key or a verifier. Store the passphrase in a password
manager or a secrets service before you encrypt anything you care about.

</details>

<details>
<summary>Why does encrypting the same file twice give different output?</summary>

Each run draws a new random salt and a new random IV for every value. Identical output
would leak that two values are the same, so the difference is intentional. Both outputs
decrypt to the same document with the same passphrase.

</details>

<details>
<summary>Can I move an encrypted value to a different key?</summary>

No, and that is enforced. Each value is bound to its dotted key path through AES-GCM's
additional authenticated data, so a marker pasted under a different key fails to decrypt
with a clear error. Rename a key by decrypting, renaming, and encrypting again.

</details>

<details>
<summary>Is my passphrase safe to put in a shared link?</summary>

No. The page can pre-fill fields from the URL, which is handy for a local test but means
the passphrase would sit in browser history, bookmarks, and anything the link is pasted
into. Type it into the form instead, and never share a link that carries both the passphrase
and the encrypted document.

</details>

## Related tools

- [AES cipher](https://gizza.ai/tools/aes-cipher/): Encrypt or decrypt text with AES in CBC, CTR, GCM or ECB mode and 128/192/256-bit keys, with hex/base64 I/O — in your browser. Nothing is uploaded.
- [AES Key Wrap](https://gizza.ai/tools/aes-key-wrap/): Wrap and unwrap cryptographic keys with AES Key Wrap (KW / RFC 3394, KWP / RFC 5649) using a 128/192/256-bit KEK — hex or base64, free and in-browser.
- [Age encrypt](https://gizza.ai/tools/age-encrypt/): Encrypt small text into an ASCII-armored age ciphertext with a passphrase or native X25519 recipients.
- [Age Key Generator](https://gizza.ai/tools/age-keygen/): Generate an age X25519 identity and its shareable age1 public recipient in your browser, or derive the recipient from an identity you already have.
- [Amcache Parser](https://gizza.ai/tools/amcache-parser/): Parse Windows Amcache.hve bytes from hex or Base64 and report application, executable, driver, shortcut, SHA-1 hash and timestamp evidence locally.
