Latest commit

History

104 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

cipher

cipher

testlintGo ReferenceGo Report CardLicense

Programmatic SOPS, from Go. One library and one CLI for encrypt, decrypt, rotate, walk, edit, and audit. Drop in next to your existing sops files and keep going.

cipher demo

Every release is exercised end to end against real Vault Transit, AWS KMS through LocalStack, and a fresh PGP keyring. The on disk format is the standard sops format, so the upstream sops binary reads what cipher writes.

What you can do

  • Encrypt and decrypt YAML, JSON, ENV, INI, or binary files with age, AWS KMS, GCP KMS, Vault Transit, Azure Key Vault, or PGP.
  • Edit encrypted files in $EDITOR, re-encrypted on save with the original recipients.
  • Rotate the per-file encryption key on demand or on age (--older-than 90d).
  • Add or drop recipients without re-encrypting the payload.
  • Walk a directory tree in parallel and apply any of the above to every matching file.
  • Route per-path recipient selection from a .sops.yaml policy file.
  • Block plaintext commits with a git pre-commit hook.
  • Stream secrets through Go net/http middleware and emit OpenTelemetry traces.

When to pick cipher

ToolBest forTradeoff
cipherSecrets committed to git plus Go integration, parallel directory walks, audit and drift checks, and a pre-commit hook.Pre-1.0. Go API may break between minor versions.
raw sops CLISecrets in git when one file at a time is enough and no Go consumer needs an encrypt API.No directory walker. The sops Go API only decrypts.
HashiCorp VaultRuntime secrets your app fetches over the network on each request.Server to run and maintain.
AWS Secrets ManagerAWS native runtime secrets resolved by IAM.AWS lock in. Runtime only.
Azure Key VaultAzure native runtime secrets.Azure lock in. Runtime only.

Pick cipher when the secrets live in your repo and you want one tool that behaves the same way from Go code, from CI, and from your editor. Pick Vault or one of the managed runtime stores when the secrets live in a server and the app fetches them at runtime.

Quickstart

CLI

brew install dcadolph/tap/cipher
age-keygen -o key.txt
export SOPS_AGE_KEY_FILE=$PWD/key.txt
PUB=$(age-keygen -y key.txt)echo"db_password: super-secret"> prod.yaml
cipher encrypt --age "$PUB" -i prod.yaml
cipher decrypt prod.yaml

See cmd/README.md for every verb and flag.

Library

import (
"github.com/dcadolph/cipher""github.com/dcadolph/cipher/age"
)
id, _:=age.GenerateIdentity()
kp, _:=age.NewProvider(id.Recipient)
enc:=cipher.NewEncoder(kp)
ciphertext, _:=enc.Encode(ctx, "secrets.yaml", []byte("foo: bar\n"))
dec:=cipher.NewDecoder()
plain, _:=dec.Decode(ctx, "secrets.yaml", ciphertext)

Full API reference at godoc. Runnable per-backend programs live under examples/.

Recipes

Encrypt every plaintext file that matches .sops.yaml

cipher fix ./secrets

Walks the tree, finds plaintext files that match a creation rule, and encrypts them in place with the recipients the rule names.

Edit a secret in your text editor

cipher edit secrets.yaml

Decrypts to a 0600 file in a fresh 0700 temp directory, opens $EDITOR, re-encrypts on save with the original recipients. Plaintext never lands on a shared path.

Add a teammate to an existing file

cipher add-recipient secrets.yaml --age age1bob... -i

The wrapped data key picks up Bob. The encrypted payload itself does not change.

Rotate the data key on every file older than 90 days

cipher walk rotate ./secrets --config .sops.yaml --older-than 90d --parallel 8

Same recipient set, fresh AES key, payload re-encrypted under it. Files newer than the cutoff are skipped.

Audit who can decrypt what

cipher recipients list secrets.yaml --pretty
cipher recipients drift ./secrets --config .sops.yaml

list prints the recipients recorded in a file. drift reports files whose recipient set no longer matches .sops.yaml.

Block plaintext commits

cat > .git/hooks/pre-commit <<'EOF'#!/usr/bin/env bashexec cipher precommitEOF
chmod +x .git/hooks/pre-commit

Refuses any staged file that matches a .sops.yaml rule but is still plaintext.

Architecture

Your Go code or CLI
|
v
cipher.Encoder / cipher.Decoder -----> getsops/sops Go API
|
v
cipher.KeyProvider
|
+--> age
+--> AWS KMS
+--> GCP KMS
+--> Vault Transit
+--> Azure Key Vault
+--> PGP

cipher is a thin Go layer over the SOPS Go API. Each backend implements cipher.KeyProvider and reads credentials the same way the sops binary does. The on disk format is the SOPS format, so anything that decrypts SOPS files (including the upstream sops binary) reads what cipher writes.

Concepts

Each SOPS-encrypted file holds a per-file AES-256 data key that protects the secret payload. That data key is itself wrapped (re-encrypted) once for each recipient you grant access to. A recipient is whatever the backend understands as an identity:

BackendA recipient looks like
ageage1... public key
AWS KMSarn:aws:kms:... key ARN
GCP KMSprojects/.../cryptoKeys/... resource ID
Vault Transithttps://vault/.../keys/<name> URI
Azure Key Vaulthttps://<vault>.vault.azure.net/keys/<key>/<ver> URL
PGPA GPG key fingerprint

Anyone holding the matching private key (or IAM access) can unwrap their copy of the data key and decrypt the file.

Backends

BackendPackage
agecipher/age
AWS KMScipher/kms
GCP KMScipher/gcpkms
Vault Transitcipher/vault
Azure Key Vaultcipher/azkv
PGPcipher/pgp

Each implements cipher.KeyProvider. Mix them with cipher.MergeProviders. Use cipher.NewShamirRule for threshold-of-N across backends.

Install

CLI

# Homebrew (macOS, Linux)
brew install dcadolph/tap/cipher
# Go toolchain
go install github.com/dcadolph/cipher/cmd/cipher@latest
# From source
git clone https://github.com/dcadolph/cipher &&cd cipher && make install

Prebuilt binaries and checksums for Linux, macOS, and Windows ship with every release.

Library

go get github.com/dcadolph/cipher

Requires Go 1.25 or newer.

Decrypt credentials

Each backend reads decryption credentials the same way the SOPS binary does:

BackendDecrypt credential
ageSOPS_AGE_KEY_FILE pointing at the secret key file, or SOPS_AGE_KEY in the environment.
AWS KMSAWS credentials via the default chain.
GCP KMSApplication-default credentials.
Vault TransitVAULT_TOKEN and VAULT_ADDR.
Azure Key VaultAzure default credential chain.
PGPThe gpg binary on PATH with the matching private key in the keyring.

FAQ

Which backend should I pick?

  • age: solo dev or small team, minimum infra, secrets in git. Recommended starting point.
  • AWS KMS / GCP KMS / Azure Key Vault: cloud workloads with IAM-driven access and a managed audit trail.
  • Vault Transit: self-hosted HashiCorp Vault already in your stack.
  • PGP: existing GPG keyring or regulatory requirement.

Why not just use the sops CLI directly?

You can. cipher decrypt is wire-compatible. cipher adds a Go library for encryption (sops ships decrypt only), a parallel directory walker, atomic writes, recipient diff and audit tools, a built-in git pre-commit hook, and an $EDITOR workflow that re-uses the file's existing recipients.

Is my data key safe across rotation?

Yes. Rotation generates a fresh AES-256 key, re-encrypts the payload under it, then wraps the new key for the same recipient set. The old data key is never persisted after the rotation completes.

What about Shamir threshold-of-N?

Supported via cipher.NewShamirRule (library) or --shamir-threshold N (CLI). Split recipients into K key groups. Any N groups can recover the data key.

Can I use cipher with a non-Go service?

Decrypt yes, with the standard sops binary. Encrypt also yes. The on-disk format is identical to what sops produces, so any consumer that decrypts SOPS files reads what cipher writes.

Is the API stable?

Pre-1.0. The on-disk format is the SOPS format and stays compatible. The Go API may break between minor versions until 1.0. Lock to an exact module version if you ship a binary.

Roadmap

cipher is pre-1.0. The on disk format is the sops format and stays compatible across releases. The Go API may break between minor versions until 1.0 lands.

Open work for the road to 1.0:

  • Real round trip integration coverage for GCP KMS and Azure Key Vault. Both backends currently rely on shape and identity format tests because no usable open source emulator exists for either.
  • Stabilize the EncoderOptions surface so a 1.0 tag freezes the public API.
  • Land cipher in homebrew core so brew install cipher works without the tap.

Open an issue or a PR if you want to push any of these forward.

Docs

SurfaceCovers
examples/Runnable Go programs for every backend and most cross-cutting features.
cmd/README.mdEvery CLI verb, every flag, runnable examples.
godocFull library API reference.
SECURITY.mdThreat model, key handling, disclosure path.
CONTRIBUTING.mdDev setup, what CI runs, commit and PR style.
RELEASING.mdRelease workflow, Homebrew tap setup, what every tag publishes.
cipher demoSix in-browser cinematic explainers, about five minutes total.

License

Apache-2.0. See LICENSE.

About

Programmatic SOPS for Go: encrypt, decrypt, rotate, walk, edit, and audit secret files. CLI + library.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Latest commit

History

104 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

cipher

cipher

testlintGo ReferenceGo Report CardLicense

Programmatic SOPS, from Go. One library and one CLI for encrypt, decrypt, rotate, walk, edit, and audit. Drop in next to your existing sops files and keep going.

cipher demo

Every release is exercised end to end against real Vault Transit, AWS KMS through LocalStack, and a fresh PGP keyring. The on disk format is the standard sops format, so the upstream sops binary reads what cipher writes.

What you can do

  • Encrypt and decrypt YAML, JSON, ENV, INI, or binary files with age, AWS KMS, GCP KMS, Vault Transit, Azure Key Vault, or PGP.
  • Edit encrypted files in $EDITOR, re-encrypted on save with the original recipients.
  • Rotate the per-file encryption key on demand or on age (--older-than 90d).
  • Add or drop recipients without re-encrypting the payload.
  • Walk a directory tree in parallel and apply any of the above to every matching file.
  • Route per-path recipient selection from a .sops.yaml policy file.
  • Block plaintext commits with a git pre-commit hook.
  • Stream secrets through Go net/http middleware and emit OpenTelemetry traces.

When to pick cipher

ToolBest forTradeoff
cipherSecrets committed to git plus Go integration, parallel directory walks, audit and drift checks, and a pre-commit hook.Pre-1.0. Go API may break between minor versions.
raw sops CLISecrets in git when one file at a time is enough and no Go consumer needs an encrypt API.No directory walker. The sops Go API only decrypts.
HashiCorp VaultRuntime secrets your app fetches over the network on each request.Server to run and maintain.
AWS Secrets ManagerAWS native runtime secrets resolved by IAM.AWS lock in. Runtime only.
Azure Key VaultAzure native runtime secrets.Azure lock in. Runtime only.

Pick cipher when the secrets live in your repo and you want one tool that behaves the same way from Go code, from CI, and from your editor. Pick Vault or one of the managed runtime stores when the secrets live in a server and the app fetches them at runtime.

Quickstart

CLI

brew install dcadolph/tap/cipher
age-keygen -o key.txt
export SOPS_AGE_KEY_FILE=$PWD/key.txt
PUB=$(age-keygen -y key.txt)echo"db_password: super-secret"> prod.yaml
cipher encrypt --age "$PUB" -i prod.yaml
cipher decrypt prod.yaml

See cmd/README.md for every verb and flag.

Library

import (
"github.com/dcadolph/cipher""github.com/dcadolph/cipher/age"
)
id, _:=age.GenerateIdentity()
kp, _:=age.NewProvider(id.Recipient)
enc:=cipher.NewEncoder(kp)
ciphertext, _:=enc.Encode(ctx, "secrets.yaml", []byte("foo: bar\n"))
dec:=cipher.NewDecoder()
plain, _:=dec.Decode(ctx, "secrets.yaml", ciphertext)

Full API reference at godoc. Runnable per-backend programs live under examples/.

Recipes

Encrypt every plaintext file that matches .sops.yaml

cipher fix ./secrets

Walks the tree, finds plaintext files that match a creation rule, and encrypts them in place with the recipients the rule names.

Edit a secret in your text editor

cipher edit secrets.yaml

Decrypts to a 0600 file in a fresh 0700 temp directory, opens $EDITOR, re-encrypts on save with the original recipients. Plaintext never lands on a shared path.

Add a teammate to an existing file

cipher add-recipient secrets.yaml --age age1bob... -i

The wrapped data key picks up Bob. The encrypted payload itself does not change.

Rotate the data key on every file older than 90 days

cipher walk rotate ./secrets --config .sops.yaml --older-than 90d --parallel 8

Same recipient set, fresh AES key, payload re-encrypted under it. Files newer than the cutoff are skipped.

Audit who can decrypt what

cipher recipients list secrets.yaml --pretty
cipher recipients drift ./secrets --config .sops.yaml

list prints the recipients recorded in a file. drift reports files whose recipient set no longer matches .sops.yaml.

Block plaintext commits

cat > .git/hooks/pre-commit <<'EOF'#!/usr/bin/env bashexec cipher precommitEOF
chmod +x .git/hooks/pre-commit

Refuses any staged file that matches a .sops.yaml rule but is still plaintext.

Architecture

Your Go code or CLI
|
v
cipher.Encoder / cipher.Decoder -----> getsops/sops Go API
|
v
cipher.KeyProvider
|
+--> age
+--> AWS KMS
+--> GCP KMS
+--> Vault Transit
+--> Azure Key Vault
+--> PGP

cipher is a thin Go layer over the SOPS Go API. Each backend implements cipher.KeyProvider and reads credentials the same way the sops binary does. The on disk format is the SOPS format, so anything that decrypts SOPS files (including the upstream sops binary) reads what cipher writes.

Concepts

Each SOPS-encrypted file holds a per-file AES-256 data key that protects the secret payload. That data key is itself wrapped (re-encrypted) once for each recipient you grant access to. A recipient is whatever the backend understands as an identity:

BackendA recipient looks like
ageage1... public key
AWS KMSarn:aws:kms:... key ARN
GCP KMSprojects/.../cryptoKeys/... resource ID
Vault Transithttps://vault/.../keys/<name> URI
Azure Key Vaulthttps://<vault>.vault.azure.net/keys/<key>/<ver> URL
PGPA GPG key fingerprint

Anyone holding the matching private key (or IAM access) can unwrap their copy of the data key and decrypt the file.

Backends

BackendPackage
agecipher/age
AWS KMScipher/kms
GCP KMScipher/gcpkms
Vault Transitcipher/vault
Azure Key Vaultcipher/azkv
PGPcipher/pgp

Each implements cipher.KeyProvider. Mix them with cipher.MergeProviders. Use cipher.NewShamirRule for threshold-of-N across backends.

Install

CLI

# Homebrew (macOS, Linux)
brew install dcadolph/tap/cipher
# Go toolchain
go install github.com/dcadolph/cipher/cmd/cipher@latest
# From source
git clone https://github.com/dcadolph/cipher &&cd cipher && make install

Prebuilt binaries and checksums for Linux, macOS, and Windows ship with every release.

Library

go get github.com/dcadolph/cipher

Requires Go 1.25 or newer.

Decrypt credentials

Each backend reads decryption credentials the same way the SOPS binary does:

BackendDecrypt credential
ageSOPS_AGE_KEY_FILE pointing at the secret key file, or SOPS_AGE_KEY in the environment.
AWS KMSAWS credentials via the default chain.
GCP KMSApplication-default credentials.
Vault TransitVAULT_TOKEN and VAULT_ADDR.
Azure Key VaultAzure default credential chain.
PGPThe gpg binary on PATH with the matching private key in the keyring.

FAQ

Which backend should I pick?

  • age: solo dev or small team, minimum infra, secrets in git. Recommended starting point.
  • AWS KMS / GCP KMS / Azure Key Vault: cloud workloads with IAM-driven access and a managed audit trail.
  • Vault Transit: self-hosted HashiCorp Vault already in your stack.
  • PGP: existing GPG keyring or regulatory requirement.

Why not just use the sops CLI directly?

You can. cipher decrypt is wire-compatible. cipher adds a Go library for encryption (sops ships decrypt only), a parallel directory walker, atomic writes, recipient diff and audit tools, a built-in git pre-commit hook, and an $EDITOR workflow that re-uses the file's existing recipients.

Is my data key safe across rotation?

Yes. Rotation generates a fresh AES-256 key, re-encrypts the payload under it, then wraps the new key for the same recipient set. The old data key is never persisted after the rotation completes.

What about Shamir threshold-of-N?

Supported via cipher.NewShamirRule (library) or --shamir-threshold N (CLI). Split recipients into K key groups. Any N groups can recover the data key.

Can I use cipher with a non-Go service?

Decrypt yes, with the standard sops binary. Encrypt also yes. The on-disk format is identical to what sops produces, so any consumer that decrypts SOPS files reads what cipher writes.

Is the API stable?

Pre-1.0. The on-disk format is the SOPS format and stays compatible. The Go API may break between minor versions until 1.0. Lock to an exact module version if you ship a binary.

Roadmap

cipher is pre-1.0. The on disk format is the sops format and stays compatible across releases. The Go API may break between minor versions until 1.0 lands.

Open work for the road to 1.0:

  • Real round trip integration coverage for GCP KMS and Azure Key Vault. Both backends currently rely on shape and identity format tests because no usable open source emulator exists for either.
  • Stabilize the EncoderOptions surface so a 1.0 tag freezes the public API.
  • Land cipher in homebrew core so brew install cipher works without the tap.

Open an issue or a PR if you want to push any of these forward.

Docs

SurfaceCovers
examples/Runnable Go programs for every backend and most cross-cutting features.
cmd/README.mdEvery CLI verb, every flag, runnable examples.
godocFull library API reference.
SECURITY.mdThreat model, key handling, disclosure path.
CONTRIBUTING.mdDev setup, what CI runs, commit and PR style.
RELEASING.mdRelease workflow, Homebrew tap setup, what every tag publishes.
cipher demoSix in-browser cinematic explainers, about five minutes total.

License

Apache-2.0. See LICENSE.

About

Programmatic SOPS for Go: encrypt, decrypt, rotate, walk, edit, and audit secret files. CLI + library.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

104 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

cipher

cipher

testlintGo ReferenceGo Report CardLicense

Programmatic SOPS, from Go. One library and one CLI for encrypt, decrypt, rotate, walk, edit, and audit. Drop in next to your existing sops files and keep going.

cipher demo

Every release is exercised end to end against real Vault Transit, AWS KMS through LocalStack, and a fresh PGP keyring. The on disk format is the standard sops format, so the upstream sops binary reads what cipher writes.

What you can do

  • Encrypt and decrypt YAML, JSON, ENV, INI, or binary files with age, AWS KMS, GCP KMS, Vault Transit, Azure Key Vault, or PGP.
  • Edit encrypted files in $EDITOR, re-encrypted on save with the original recipients.
  • Rotate the per-file encryption key on demand or on age (--older-than 90d).
  • Add or drop recipients without re-encrypting the payload.
  • Walk a directory tree in parallel and apply any of the above to every matching file.
  • Route per-path recipient selection from a .sops.yaml policy file.
  • Block plaintext commits with a git pre-commit hook.
  • Stream secrets through Go net/http middleware and emit OpenTelemetry traces.

When to pick cipher

ToolBest forTradeoff
cipherSecrets committed to git plus Go integration, parallel directory walks, audit and drift checks, and a pre-commit hook.Pre-1.0. Go API may break between minor versions.
raw sops CLISecrets in git when one file at a time is enough and no Go consumer needs an encrypt API.No directory walker. The sops Go API only decrypts.
HashiCorp VaultRuntime secrets your app fetches over the network on each request.Server to run and maintain.
AWS Secrets ManagerAWS native runtime secrets resolved by IAM.AWS lock in. Runtime only.
Azure Key VaultAzure native runtime secrets.Azure lock in. Runtime only.

Pick cipher when the secrets live in your repo and you want one tool that behaves the same way from Go code, from CI, and from your editor. Pick Vault or one of the managed runtime stores when the secrets live in a server and the app fetches them at runtime.

Quickstart

CLI

brew install dcadolph/tap/cipher
age-keygen -o key.txt
export SOPS_AGE_KEY_FILE=$PWD/key.txt
PUB=$(age-keygen -y key.txt)echo"db_password: super-secret"> prod.yaml
cipher encrypt --age "$PUB" -i prod.yaml
cipher decrypt prod.yaml

See cmd/README.md for every verb and flag.

Library

import (
"github.com/dcadolph/cipher""github.com/dcadolph/cipher/age"
)
id, _:=age.GenerateIdentity()
kp, _:=age.NewProvider(id.Recipient)
enc:=cipher.NewEncoder(kp)
ciphertext, _:=enc.Encode(ctx, "secrets.yaml", []byte("foo: bar\n"))
dec:=cipher.NewDecoder()
plain, _:=dec.Decode(ctx, "secrets.yaml", ciphertext)

Full API reference at godoc. Runnable per-backend programs live under examples/.

Recipes

Encrypt every plaintext file that matches .sops.yaml

cipher fix ./secrets

Walks the tree, finds plaintext files that match a creation rule, and encrypts them in place with the recipients the rule names.

Edit a secret in your text editor

cipher edit secrets.yaml

Decrypts to a 0600 file in a fresh 0700 temp directory, opens $EDITOR, re-encrypts on save with the original recipients. Plaintext never lands on a shared path.

Add a teammate to an existing file

cipher add-recipient secrets.yaml --age age1bob... -i

The wrapped data key picks up Bob. The encrypted payload itself does not change.

Rotate the data key on every file older than 90 days

cipher walk rotate ./secrets --config .sops.yaml --older-than 90d --parallel 8

Same recipient set, fresh AES key, payload re-encrypted under it. Files newer than the cutoff are skipped.

Audit who can decrypt what

cipher recipients list secrets.yaml --pretty
cipher recipients drift ./secrets --config .sops.yaml

list prints the recipients recorded in a file. drift reports files whose recipient set no longer matches .sops.yaml.

Block plaintext commits

cat > .git/hooks/pre-commit <<'EOF'#!/usr/bin/env bashexec cipher precommitEOF
chmod +x .git/hooks/pre-commit

Refuses any staged file that matches a .sops.yaml rule but is still plaintext.

Architecture

Your Go code or CLI
|
v
cipher.Encoder / cipher.Decoder -----> getsops/sops Go API
|
v
cipher.KeyProvider
|
+--> age
+--> AWS KMS
+--> GCP KMS
+--> Vault Transit
+--> Azure Key Vault
+--> PGP

cipher is a thin Go layer over the SOPS Go API. Each backend implements cipher.KeyProvider and reads credentials the same way the sops binary does. The on disk format is the SOPS format, so anything that decrypts SOPS files (including the upstream sops binary) reads what cipher writes.

Concepts

Each SOPS-encrypted file holds a per-file AES-256 data key that protects the secret payload. That data key is itself wrapped (re-encrypted) once for each recipient you grant access to. A recipient is whatever the backend understands as an identity:

BackendA recipient looks like
ageage1... public key
AWS KMSarn:aws:kms:... key ARN
GCP KMSprojects/.../cryptoKeys/... resource ID
Vault Transithttps://vault/.../keys/<name> URI
Azure Key Vaulthttps://<vault>.vault.azure.net/keys/<key>/<ver> URL
PGPA GPG key fingerprint

Anyone holding the matching private key (or IAM access) can unwrap their copy of the data key and decrypt the file.

Backends

BackendPackage
agecipher/age
AWS KMScipher/kms
GCP KMScipher/gcpkms
Vault Transitcipher/vault
Azure Key Vaultcipher/azkv
PGPcipher/pgp

Each implements cipher.KeyProvider. Mix them with cipher.MergeProviders. Use cipher.NewShamirRule for threshold-of-N across backends.

Install

CLI

# Homebrew (macOS, Linux)
brew install dcadolph/tap/cipher
# Go toolchain
go install github.com/dcadolph/cipher/cmd/cipher@latest
# From source
git clone https://github.com/dcadolph/cipher &&cd cipher && make install

Prebuilt binaries and checksums for Linux, macOS, and Windows ship with every release.

Library

go get github.com/dcadolph/cipher

Requires Go 1.25 or newer.

Decrypt credentials

Each backend reads decryption credentials the same way the SOPS binary does:

BackendDecrypt credential
ageSOPS_AGE_KEY_FILE pointing at the secret key file, or SOPS_AGE_KEY in the environment.
AWS KMSAWS credentials via the default chain.
GCP KMSApplication-default credentials.
Vault TransitVAULT_TOKEN and VAULT_ADDR.
Azure Key VaultAzure default credential chain.
PGPThe gpg binary on PATH with the matching private key in the keyring.

FAQ

Which backend should I pick?

  • age: solo dev or small team, minimum infra, secrets in git. Recommended starting point.
  • AWS KMS / GCP KMS / Azure Key Vault: cloud workloads with IAM-driven access and a managed audit trail.
  • Vault Transit: self-hosted HashiCorp Vault already in your stack.
  • PGP: existing GPG keyring or regulatory requirement.

Why not just use the sops CLI directly?

You can. cipher decrypt is wire-compatible. cipher adds a Go library for encryption (sops ships decrypt only), a parallel directory walker, atomic writes, recipient diff and audit tools, a built-in git pre-commit hook, and an $EDITOR workflow that re-uses the file's existing recipients.

Is my data key safe across rotation?

Yes. Rotation generates a fresh AES-256 key, re-encrypts the payload under it, then wraps the new key for the same recipient set. The old data key is never persisted after the rotation completes.

What about Shamir threshold-of-N?

Supported via cipher.NewShamirRule (library) or --shamir-threshold N (CLI). Split recipients into K key groups. Any N groups can recover the data key.

Can I use cipher with a non-Go service?

Decrypt yes, with the standard sops binary. Encrypt also yes. The on-disk format is identical to what sops produces, so any consumer that decrypts SOPS files reads what cipher writes.

Is the API stable?

Pre-1.0. The on-disk format is the SOPS format and stays compatible. The Go API may break between minor versions until 1.0. Lock to an exact module version if you ship a binary.

Roadmap

cipher is pre-1.0. The on disk format is the sops format and stays compatible across releases. The Go API may break between minor versions until 1.0 lands.

Open work for the road to 1.0:

  • Real round trip integration coverage for GCP KMS and Azure Key Vault. Both backends currently rely on shape and identity format tests because no usable open source emulator exists for either.
  • Stabilize the EncoderOptions surface so a 1.0 tag freezes the public API.
  • Land cipher in homebrew core so brew install cipher works without the tap.

Open an issue or a PR if you want to push any of these forward.

Docs

SurfaceCovers
examples/Runnable Go programs for every backend and most cross-cutting features.
cmd/README.mdEvery CLI verb, every flag, runnable examples.
godocFull library API reference.
SECURITY.mdThreat model, key handling, disclosure path.
CONTRIBUTING.mdDev setup, what CI runs, commit and PR style.
RELEASING.mdRelease workflow, Homebrew tap setup, what every tag publishes.
cipher demoSix in-browser cinematic explainers, about five minutes total.

License

Apache-2.0. See LICENSE.

About

Programmatic SOPS for Go: encrypt, decrypt, rotate, walk, edit, and audit secret files. CLI + library.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

104 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

cipher

cipher

testlintGo ReferenceGo Report CardLicense

Programmatic SOPS, from Go. One library and one CLI for encrypt, decrypt, rotate, walk, edit, and audit. Drop in next to your existing sops files and keep going.

cipher demo

Every release is exercised end to end against real Vault Transit, AWS KMS through LocalStack, and a fresh PGP keyring. The on disk format is the standard sops format, so the upstream sops binary reads what cipher writes.

What you can do

  • Encrypt and decrypt YAML, JSON, ENV, INI, or binary files with age, AWS KMS, GCP KMS, Vault Transit, Azure Key Vault, or PGP.
  • Edit encrypted files in $EDITOR, re-encrypted on save with the original recipients.
  • Rotate the per-file encryption key on demand or on age (--older-than 90d).
  • Add or drop recipients without re-encrypting the payload.
  • Walk a directory tree in parallel and apply any of the above to every matching file.
  • Route per-path recipient selection from a .sops.yaml policy file.
  • Block plaintext commits with a git pre-commit hook.
  • Stream secrets through Go net/http middleware and emit OpenTelemetry traces.

When to pick cipher

ToolBest forTradeoff
cipherSecrets committed to git plus Go integration, parallel directory walks, audit and drift checks, and a pre-commit hook.Pre-1.0. Go API may break between minor versions.
raw sops CLISecrets in git when one file at a time is enough and no Go consumer needs an encrypt API.No directory walker. The sops Go API only decrypts.
HashiCorp VaultRuntime secrets your app fetches over the network on each request.Server to run and maintain.
AWS Secrets ManagerAWS native runtime secrets resolved by IAM.AWS lock in. Runtime only.
Azure Key VaultAzure native runtime secrets.Azure lock in. Runtime only.

Pick cipher when the secrets live in your repo and you want one tool that behaves the same way from Go code, from CI, and from your editor. Pick Vault or one of the managed runtime stores when the secrets live in a server and the app fetches them at runtime.

Quickstart

CLI

brew install dcadolph/tap/cipher
age-keygen -o key.txt
export SOPS_AGE_KEY_FILE=$PWD/key.txt
PUB=$(age-keygen -y key.txt)echo"db_password: super-secret"> prod.yaml
cipher encrypt --age "$PUB" -i prod.yaml
cipher decrypt prod.yaml

See cmd/README.md for every verb and flag.

Library

import (
"github.com/dcadolph/cipher""github.com/dcadolph/cipher/age"
)
id, _:=age.GenerateIdentity()
kp, _:=age.NewProvider(id.Recipient)
enc:=cipher.NewEncoder(kp)
ciphertext, _:=enc.Encode(ctx, "secrets.yaml", []byte("foo: bar\n"))
dec:=cipher.NewDecoder()
plain, _:=dec.Decode(ctx, "secrets.yaml", ciphertext)

Full API reference at godoc. Runnable per-backend programs live under examples/.

Recipes

Encrypt every plaintext file that matches .sops.yaml

cipher fix ./secrets

Walks the tree, finds plaintext files that match a creation rule, and encrypts them in place with the recipients the rule names.

Edit a secret in your text editor

cipher edit secrets.yaml

Decrypts to a 0600 file in a fresh 0700 temp directory, opens $EDITOR, re-encrypts on save with the original recipients. Plaintext never lands on a shared path.

Add a teammate to an existing file

cipher add-recipient secrets.yaml --age age1bob... -i

The wrapped data key picks up Bob. The encrypted payload itself does not change.

Rotate the data key on every file older than 90 days

cipher walk rotate ./secrets --config .sops.yaml --older-than 90d --parallel 8

Same recipient set, fresh AES key, payload re-encrypted under it. Files newer than the cutoff are skipped.

Audit who can decrypt what

cipher recipients list secrets.yaml --pretty
cipher recipients drift ./secrets --config .sops.yaml

list prints the recipients recorded in a file. drift reports files whose recipient set no longer matches .sops.yaml.

Block plaintext commits

cat > .git/hooks/pre-commit <<'EOF'#!/usr/bin/env bashexec cipher precommitEOF
chmod +x .git/hooks/pre-commit

Refuses any staged file that matches a .sops.yaml rule but is still plaintext.

Architecture

Your Go code or CLI
|
v
cipher.Encoder / cipher.Decoder -----> getsops/sops Go API
|
v
cipher.KeyProvider
|
+--> age
+--> AWS KMS
+--> GCP KMS
+--> Vault Transit
+--> Azure Key Vault
+--> PGP

cipher is a thin Go layer over the SOPS Go API. Each backend implements cipher.KeyProvider and reads credentials the same way the sops binary does. The on disk format is the SOPS format, so anything that decrypts SOPS files (including the upstream sops binary) reads what cipher writes.

Concepts

Each SOPS-encrypted file holds a per-file AES-256 data key that protects the secret payload. That data key is itself wrapped (re-encrypted) once for each recipient you grant access to. A recipient is whatever the backend understands as an identity:

BackendA recipient looks like
ageage1... public key
AWS KMSarn:aws:kms:... key ARN
GCP KMSprojects/.../cryptoKeys/... resource ID
Vault Transithttps://vault/.../keys/<name> URI
Azure Key Vaulthttps://<vault>.vault.azure.net/keys/<key>/<ver> URL
PGPA GPG key fingerprint

Anyone holding the matching private key (or IAM access) can unwrap their copy of the data key and decrypt the file.

Backends

BackendPackage
agecipher/age
AWS KMScipher/kms
GCP KMScipher/gcpkms
Vault Transitcipher/vault
Azure Key Vaultcipher/azkv
PGPcipher/pgp

Each implements cipher.KeyProvider. Mix them with cipher.MergeProviders. Use cipher.NewShamirRule for threshold-of-N across backends.

Install

CLI

# Homebrew (macOS, Linux)
brew install dcadolph/tap/cipher
# Go toolchain
go install github.com/dcadolph/cipher/cmd/cipher@latest
# From source
git clone https://github.com/dcadolph/cipher &&cd cipher && make install

Prebuilt binaries and checksums for Linux, macOS, and Windows ship with every release.

Library

go get github.com/dcadolph/cipher

Requires Go 1.25 or newer.

Decrypt credentials

Each backend reads decryption credentials the same way the SOPS binary does:

BackendDecrypt credential
ageSOPS_AGE_KEY_FILE pointing at the secret key file, or SOPS_AGE_KEY in the environment.
AWS KMSAWS credentials via the default chain.
GCP KMSApplication-default credentials.
Vault TransitVAULT_TOKEN and VAULT_ADDR.
Azure Key VaultAzure default credential chain.
PGPThe gpg binary on PATH with the matching private key in the keyring.

FAQ

Which backend should I pick?

  • age: solo dev or small team, minimum infra, secrets in git. Recommended starting point.
  • AWS KMS / GCP KMS / Azure Key Vault: cloud workloads with IAM-driven access and a managed audit trail.
  • Vault Transit: self-hosted HashiCorp Vault already in your stack.
  • PGP: existing GPG keyring or regulatory requirement.

Why not just use the sops CLI directly?

You can. cipher decrypt is wire-compatible. cipher adds a Go library for encryption (sops ships decrypt only), a parallel directory walker, atomic writes, recipient diff and audit tools, a built-in git pre-commit hook, and an $EDITOR workflow that re-uses the file's existing recipients.

Is my data key safe across rotation?

Yes. Rotation generates a fresh AES-256 key, re-encrypts the payload under it, then wraps the new key for the same recipient set. The old data key is never persisted after the rotation completes.

What about Shamir threshold-of-N?

Supported via cipher.NewShamirRule (library) or --shamir-threshold N (CLI). Split recipients into K key groups. Any N groups can recover the data key.

Can I use cipher with a non-Go service?

Decrypt yes, with the standard sops binary. Encrypt also yes. The on-disk format is identical to what sops produces, so any consumer that decrypts SOPS files reads what cipher writes.

Is the API stable?

Pre-1.0. The on-disk format is the SOPS format and stays compatible. The Go API may break between minor versions until 1.0. Lock to an exact module version if you ship a binary.

Roadmap

cipher is pre-1.0. The on disk format is the sops format and stays compatible across releases. The Go API may break between minor versions until 1.0 lands.

Open work for the road to 1.0:

  • Real round trip integration coverage for GCP KMS and Azure Key Vault. Both backends currently rely on shape and identity format tests because no usable open source emulator exists for either.
  • Stabilize the EncoderOptions surface so a 1.0 tag freezes the public API.
  • Land cipher in homebrew core so brew install cipher works without the tap.

Open an issue or a PR if you want to push any of these forward.

Docs

SurfaceCovers
examples/Runnable Go programs for every backend and most cross-cutting features.
cmd/README.mdEvery CLI verb, every flag, runnable examples.
godocFull library API reference.
SECURITY.mdThreat model, key handling, disclosure path.
CONTRIBUTING.mdDev setup, what CI runs, commit and PR style.
RELEASING.mdRelease workflow, Homebrew tap setup, what every tag publishes.
cipher demoSix in-browser cinematic explainers, about five minutes total.

License

Apache-2.0. See LICENSE.

About

Programmatic SOPS for Go: encrypt, decrypt, rotate, walk, edit, and audit secret files. CLI + library.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Latest commit

History

104 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

cipher

cipher

testlintGo ReferenceGo Report CardLicense

Programmatic SOPS, from Go. One library and one CLI for encrypt, decrypt, rotate, walk, edit, and audit. Drop in next to your existing sops files and keep going.

cipher demo

Every release is exercised end to end against real Vault Transit, AWS KMS through LocalStack, and a fresh PGP keyring. The on disk format is the standard sops format, so the upstream sops binary reads what cipher writes.

What you can do

  • Encrypt and decrypt YAML, JSON, ENV, INI, or binary files with age, AWS KMS, GCP KMS, Vault Transit, Azure Key Vault, or PGP.
  • Edit encrypted files in $EDITOR, re-encrypted on save with the original recipients.
  • Rotate the per-file encryption key on demand or on age (--older-than 90d).
  • Add or drop recipients without re-encrypting the payload.
  • Walk a directory tree in parallel and apply any of the above to every matching file.
  • Route per-path recipient selection from a .sops.yaml policy file.
  • Block plaintext commits with a git pre-commit hook.
  • Stream secrets through Go net/http middleware and emit OpenTelemetry traces.

When to pick cipher

ToolBest forTradeoff
cipherSecrets committed to git plus Go integration, parallel directory walks, audit and drift checks, and a pre-commit hook.Pre-1.0. Go API may break between minor versions.
raw sops CLISecrets in git when one file at a time is enough and no Go consumer needs an encrypt API.No directory walker. The sops Go API only decrypts.
HashiCorp VaultRuntime secrets your app fetches over the network on each request.Server to run and maintain.
AWS Secrets ManagerAWS native runtime secrets resolved by IAM.AWS lock in. Runtime only.
Azure Key VaultAzure native runtime secrets.Azure lock in. Runtime only.

Pick cipher when the secrets live in your repo and you want one tool that behaves the same way from Go code, from CI, and from your editor. Pick Vault or one of the managed runtime stores when the secrets live in a server and the app fetches them at runtime.

Quickstart

CLI

brew install dcadolph/tap/cipher
age-keygen -o key.txt
export SOPS_AGE_KEY_FILE=$PWD/key.txt
PUB=$(age-keygen -y key.txt)echo"db_password: super-secret"> prod.yaml
cipher encrypt --age "$PUB" -i prod.yaml
cipher decrypt prod.yaml

See cmd/README.md for every verb and flag.

Library

import (
"github.com/dcadolph/cipher""github.com/dcadolph/cipher/age"
)
id, _:=age.GenerateIdentity()
kp, _:=age.NewProvider(id.Recipient)
enc:=cipher.NewEncoder(kp)
ciphertext, _:=enc.Encode(ctx, "secrets.yaml", []byte("foo: bar\n"))
dec:=cipher.NewDecoder()
plain, _:=dec.Decode(ctx, "secrets.yaml", ciphertext)

Full API reference at godoc. Runnable per-backend programs live under examples/.

Recipes

Encrypt every plaintext file that matches .sops.yaml

cipher fix ./secrets

Walks the tree, finds plaintext files that match a creation rule, and encrypts them in place with the recipients the rule names.

Edit a secret in your text editor

cipher edit secrets.yaml

Decrypts to a 0600 file in a fresh 0700 temp directory, opens $EDITOR, re-encrypts on save with the original recipients. Plaintext never lands on a shared path.

Add a teammate to an existing file

cipher add-recipient secrets.yaml --age age1bob... -i

The wrapped data key picks up Bob. The encrypted payload itself does not change.

Rotate the data key on every file older than 90 days

cipher walk rotate ./secrets --config .sops.yaml --older-than 90d --parallel 8

Same recipient set, fresh AES key, payload re-encrypted under it. Files newer than the cutoff are skipped.

Audit who can decrypt what

cipher recipients list secrets.yaml --pretty
cipher recipients drift ./secrets --config .sops.yaml

list prints the recipients recorded in a file. drift reports files whose recipient set no longer matches .sops.yaml.

Block plaintext commits

cat > .git/hooks/pre-commit <<'EOF'#!/usr/bin/env bashexec cipher precommitEOF
chmod +x .git/hooks/pre-commit

Refuses any staged file that matches a .sops.yaml rule but is still plaintext.

Architecture

Your Go code or CLI
|
v
cipher.Encoder / cipher.Decoder -----> getsops/sops Go API
|
v
cipher.KeyProvider
|
+--> age
+--> AWS KMS
+--> GCP KMS
+--> Vault Transit
+--> Azure Key Vault
+--> PGP

cipher is a thin Go layer over the SOPS Go API. Each backend implements cipher.KeyProvider and reads credentials the same way the sops binary does. The on disk format is the SOPS format, so anything that decrypts SOPS files (including the upstream sops binary) reads what cipher writes.

Concepts

Each SOPS-encrypted file holds a per-file AES-256 data key that protects the secret payload. That data key is itself wrapped (re-encrypted) once for each recipient you grant access to. A recipient is whatever the backend understands as an identity:

BackendA recipient looks like
ageage1... public key
AWS KMSarn:aws:kms:... key ARN
GCP KMSprojects/.../cryptoKeys/... resource ID
Vault Transithttps://vault/.../keys/<name> URI
Azure Key Vaulthttps://<vault>.vault.azure.net/keys/<key>/<ver> URL
PGPA GPG key fingerprint

Anyone holding the matching private key (or IAM access) can unwrap their copy of the data key and decrypt the file.

Backends

BackendPackage
agecipher/age
AWS KMScipher/kms
GCP KMScipher/gcpkms
Vault Transitcipher/vault
Azure Key Vaultcipher/azkv
PGPcipher/pgp

Each implements cipher.KeyProvider. Mix them with cipher.MergeProviders. Use cipher.NewShamirRule for threshold-of-N across backends.

Install

CLI

# Homebrew (macOS, Linux)
brew install dcadolph/tap/cipher
# Go toolchain
go install github.com/dcadolph/cipher/cmd/cipher@latest
# From source
git clone https://github.com/dcadolph/cipher &&cd cipher && make install

Prebuilt binaries and checksums for Linux, macOS, and Windows ship with every release.

Library

go get github.com/dcadolph/cipher

Requires Go 1.25 or newer.

Decrypt credentials

Each backend reads decryption credentials the same way the SOPS binary does:

BackendDecrypt credential
ageSOPS_AGE_KEY_FILE pointing at the secret key file, or SOPS_AGE_KEY in the environment.
AWS KMSAWS credentials via the default chain.
GCP KMSApplication-default credentials.
Vault TransitVAULT_TOKEN and VAULT_ADDR.
Azure Key VaultAzure default credential chain.
PGPThe gpg binary on PATH with the matching private key in the keyring.

FAQ

Which backend should I pick?

  • age: solo dev or small team, minimum infra, secrets in git. Recommended starting point.
  • AWS KMS / GCP KMS / Azure Key Vault: cloud workloads with IAM-driven access and a managed audit trail.
  • Vault Transit: self-hosted HashiCorp Vault already in your stack.
  • PGP: existing GPG keyring or regulatory requirement.

Why not just use the sops CLI directly?

You can. cipher decrypt is wire-compatible. cipher adds a Go library for encryption (sops ships decrypt only), a parallel directory walker, atomic writes, recipient diff and audit tools, a built-in git pre-commit hook, and an $EDITOR workflow that re-uses the file's existing recipients.

Is my data key safe across rotation?

Yes. Rotation generates a fresh AES-256 key, re-encrypts the payload under it, then wraps the new key for the same recipient set. The old data key is never persisted after the rotation completes.

What about Shamir threshold-of-N?

Supported via cipher.NewShamirRule (library) or --shamir-threshold N (CLI). Split recipients into K key groups. Any N groups can recover the data key.

Can I use cipher with a non-Go service?

Decrypt yes, with the standard sops binary. Encrypt also yes. The on-disk format is identical to what sops produces, so any consumer that decrypts SOPS files reads what cipher writes.

Is the API stable?

Pre-1.0. The on-disk format is the SOPS format and stays compatible. The Go API may break between minor versions until 1.0. Lock to an exact module version if you ship a binary.

Roadmap

cipher is pre-1.0. The on disk format is the sops format and stays compatible across releases. The Go API may break between minor versions until 1.0 lands.

Open work for the road to 1.0:

  • Real round trip integration coverage for GCP KMS and Azure Key Vault. Both backends currently rely on shape and identity format tests because no usable open source emulator exists for either.
  • Stabilize the EncoderOptions surface so a 1.0 tag freezes the public API.
  • Land cipher in homebrew core so brew install cipher works without the tap.

Open an issue or a PR if you want to push any of these forward.

Docs

SurfaceCovers
examples/Runnable Go programs for every backend and most cross-cutting features.
cmd/README.mdEvery CLI verb, every flag, runnable examples.
godocFull library API reference.
SECURITY.mdThreat model, key handling, disclosure path.
CONTRIBUTING.mdDev setup, what CI runs, commit and PR style.
RELEASING.mdRelease workflow, Homebrew tap setup, what every tag publishes.
cipher demoSix in-browser cinematic explainers, about five minutes total.

License

Apache-2.0. See LICENSE.

About

Programmatic SOPS for Go: encrypt, decrypt, rotate, walk, edit, and audit secret files. CLI + library.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

104 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

cipher

cipher

testlintGo ReferenceGo Report CardLicense

Programmatic SOPS, from Go. One library and one CLI for encrypt, decrypt, rotate, walk, edit, and audit. Drop in next to your existing sops files and keep going.

cipher demo

Every release is exercised end to end against real Vault Transit, AWS KMS through LocalStack, and a fresh PGP keyring. The on disk format is the standard sops format, so the upstream sops binary reads what cipher writes.

What you can do

  • Encrypt and decrypt YAML, JSON, ENV, INI, or binary files with age, AWS KMS, GCP KMS, Vault Transit, Azure Key Vault, or PGP.
  • Edit encrypted files in $EDITOR, re-encrypted on save with the original recipients.
  • Rotate the per-file encryption key on demand or on age (--older-than 90d).
  • Add or drop recipients without re-encrypting the payload.
  • Walk a directory tree in parallel and apply any of the above to every matching file.
  • Route per-path recipient selection from a .sops.yaml policy file.
  • Block plaintext commits with a git pre-commit hook.
  • Stream secrets through Go net/http middleware and emit OpenTelemetry traces.

When to pick cipher

ToolBest forTradeoff
cipherSecrets committed to git plus Go integration, parallel directory walks, audit and drift checks, and a pre-commit hook.Pre-1.0. Go API may break between minor versions.
raw sops CLISecrets in git when one file at a time is enough and no Go consumer needs an encrypt API.No directory walker. The sops Go API only decrypts.
HashiCorp VaultRuntime secrets your app fetches over the network on each request.Server to run and maintain.
AWS Secrets ManagerAWS native runtime secrets resolved by IAM.AWS lock in. Runtime only.
Azure Key VaultAzure native runtime secrets.Azure lock in. Runtime only.

Pick cipher when the secrets live in your repo and you want one tool that behaves the same way from Go code, from CI, and from your editor. Pick Vault or one of the managed runtime stores when the secrets live in a server and the app fetches them at runtime.

Quickstart

CLI

brew install dcadolph/tap/cipher
age-keygen -o key.txt
export SOPS_AGE_KEY_FILE=$PWD/key.txt
PUB=$(age-keygen -y key.txt)echo"db_password: super-secret"> prod.yaml
cipher encrypt --age "$PUB" -i prod.yaml
cipher decrypt prod.yaml

See cmd/README.md for every verb and flag.

Library

import (
"github.com/dcadolph/cipher""github.com/dcadolph/cipher/age"
)
id, _:=age.GenerateIdentity()
kp, _:=age.NewProvider(id.Recipient)
enc:=cipher.NewEncoder(kp)
ciphertext, _:=enc.Encode(ctx, "secrets.yaml", []byte("foo: bar\n"))
dec:=cipher.NewDecoder()
plain, _:=dec.Decode(ctx, "secrets.yaml", ciphertext)

Full API reference at godoc. Runnable per-backend programs live under examples/.

Recipes

Encrypt every plaintext file that matches .sops.yaml

cipher fix ./secrets

Walks the tree, finds plaintext files that match a creation rule, and encrypts them in place with the recipients the rule names.

Edit a secret in your text editor

cipher edit secrets.yaml

Decrypts to a 0600 file in a fresh 0700 temp directory, opens $EDITOR, re-encrypts on save with the original recipients. Plaintext never lands on a shared path.

Add a teammate to an existing file

cipher add-recipient secrets.yaml --age age1bob... -i

The wrapped data key picks up Bob. The encrypted payload itself does not change.

Rotate the data key on every file older than 90 days

cipher walk rotate ./secrets --config .sops.yaml --older-than 90d --parallel 8

Same recipient set, fresh AES key, payload re-encrypted under it. Files newer than the cutoff are skipped.

Audit who can decrypt what

cipher recipients list secrets.yaml --pretty
cipher recipients drift ./secrets --config .sops.yaml

list prints the recipients recorded in a file. drift reports files whose recipient set no longer matches .sops.yaml.

Block plaintext commits

cat > .git/hooks/pre-commit <<'EOF'#!/usr/bin/env bashexec cipher precommitEOF
chmod +x .git/hooks/pre-commit

Refuses any staged file that matches a .sops.yaml rule but is still plaintext.

Architecture

Your Go code or CLI
|
v
cipher.Encoder / cipher.Decoder -----> getsops/sops Go API
|
v
cipher.KeyProvider
|
+--> age
+--> AWS KMS
+--> GCP KMS
+--> Vault Transit
+--> Azure Key Vault
+--> PGP

cipher is a thin Go layer over the SOPS Go API. Each backend implements cipher.KeyProvider and reads credentials the same way the sops binary does. The on disk format is the SOPS format, so anything that decrypts SOPS files (including the upstream sops binary) reads what cipher writes.

Concepts

Each SOPS-encrypted file holds a per-file AES-256 data key that protects the secret payload. That data key is itself wrapped (re-encrypted) once for each recipient you grant access to. A recipient is whatever the backend understands as an identity:

BackendA recipient looks like
ageage1... public key
AWS KMSarn:aws:kms:... key ARN
GCP KMSprojects/.../cryptoKeys/... resource ID
Vault Transithttps://vault/.../keys/<name> URI
Azure Key Vaulthttps://<vault>.vault.azure.net/keys/<key>/<ver> URL
PGPA GPG key fingerprint

Anyone holding the matching private key (or IAM access) can unwrap their copy of the data key and decrypt the file.

Backends

BackendPackage
agecipher/age
AWS KMScipher/kms
GCP KMScipher/gcpkms
Vault Transitcipher/vault
Azure Key Vaultcipher/azkv
PGPcipher/pgp

Each implements cipher.KeyProvider. Mix them with cipher.MergeProviders. Use cipher.NewShamirRule for threshold-of-N across backends.

Install

CLI

# Homebrew (macOS, Linux)
brew install dcadolph/tap/cipher
# Go toolchain
go install github.com/dcadolph/cipher/cmd/cipher@latest
# From source
git clone https://github.com/dcadolph/cipher &&cd cipher && make install

Prebuilt binaries and checksums for Linux, macOS, and Windows ship with every release.

Library

go get github.com/dcadolph/cipher

Requires Go 1.25 or newer.

Decrypt credentials

Each backend reads decryption credentials the same way the SOPS binary does:

BackendDecrypt credential
ageSOPS_AGE_KEY_FILE pointing at the secret key file, or SOPS_AGE_KEY in the environment.
AWS KMSAWS credentials via the default chain.
GCP KMSApplication-default credentials.
Vault TransitVAULT_TOKEN and VAULT_ADDR.
Azure Key VaultAzure default credential chain.
PGPThe gpg binary on PATH with the matching private key in the keyring.

FAQ

Which backend should I pick?

  • age: solo dev or small team, minimum infra, secrets in git. Recommended starting point.
  • AWS KMS / GCP KMS / Azure Key Vault: cloud workloads with IAM-driven access and a managed audit trail.
  • Vault Transit: self-hosted HashiCorp Vault already in your stack.
  • PGP: existing GPG keyring or regulatory requirement.

Why not just use the sops CLI directly?

You can. cipher decrypt is wire-compatible. cipher adds a Go library for encryption (sops ships decrypt only), a parallel directory walker, atomic writes, recipient diff and audit tools, a built-in git pre-commit hook, and an $EDITOR workflow that re-uses the file's existing recipients.

Is my data key safe across rotation?

Yes. Rotation generates a fresh AES-256 key, re-encrypts the payload under it, then wraps the new key for the same recipient set. The old data key is never persisted after the rotation completes.

What about Shamir threshold-of-N?

Supported via cipher.NewShamirRule (library) or --shamir-threshold N (CLI). Split recipients into K key groups. Any N groups can recover the data key.

Can I use cipher with a non-Go service?

Decrypt yes, with the standard sops binary. Encrypt also yes. The on-disk format is identical to what sops produces, so any consumer that decrypts SOPS files reads what cipher writes.

Is the API stable?

Pre-1.0. The on-disk format is the SOPS format and stays compatible. The Go API may break between minor versions until 1.0. Lock to an exact module version if you ship a binary.

Roadmap

cipher is pre-1.0. The on disk format is the sops format and stays compatible across releases. The Go API may break between minor versions until 1.0 lands.

Open work for the road to 1.0:

  • Real round trip integration coverage for GCP KMS and Azure Key Vault. Both backends currently rely on shape and identity format tests because no usable open source emulator exists for either.
  • Stabilize the EncoderOptions surface so a 1.0 tag freezes the public API.
  • Land cipher in homebrew core so brew install cipher works without the tap.

Open an issue or a PR if you want to push any of these forward.

Docs

SurfaceCovers
examples/Runnable Go programs for every backend and most cross-cutting features.
cmd/README.mdEvery CLI verb, every flag, runnable examples.
godocFull library API reference.
SECURITY.mdThreat model, key handling, disclosure path.
CONTRIBUTING.mdDev setup, what CI runs, commit and PR style.
RELEASING.mdRelease workflow, Homebrew tap setup, what every tag publishes.
cipher demoSix in-browser cinematic explainers, about five minutes total.

License

Apache-2.0. See LICENSE.

About

Programmatic SOPS for Go: encrypt, decrypt, rotate, walk, edit, and audit secret files. CLI + library.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

104 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

cipher

cipher

testlintGo ReferenceGo Report CardLicense

Programmatic SOPS, from Go. One library and one CLI for encrypt, decrypt, rotate, walk, edit, and audit. Drop in next to your existing sops files and keep going.

cipher demo

Every release is exercised end to end against real Vault Transit, AWS KMS through LocalStack, and a fresh PGP keyring. The on disk format is the standard sops format, so the upstream sops binary reads what cipher writes.

What you can do

  • Encrypt and decrypt YAML, JSON, ENV, INI, or binary files with age, AWS KMS, GCP KMS, Vault Transit, Azure Key Vault, or PGP.
  • Edit encrypted files in $EDITOR, re-encrypted on save with the original recipients.
  • Rotate the per-file encryption key on demand or on age (--older-than 90d).
  • Add or drop recipients without re-encrypting the payload.
  • Walk a directory tree in parallel and apply any of the above to every matching file.
  • Route per-path recipient selection from a .sops.yaml policy file.
  • Block plaintext commits with a git pre-commit hook.
  • Stream secrets through Go net/http middleware and emit OpenTelemetry traces.

When to pick cipher

ToolBest forTradeoff
cipherSecrets committed to git plus Go integration, parallel directory walks, audit and drift checks, and a pre-commit hook.Pre-1.0. Go API may break between minor versions.
raw sops CLISecrets in git when one file at a time is enough and no Go consumer needs an encrypt API.No directory walker. The sops Go API only decrypts.
HashiCorp VaultRuntime secrets your app fetches over the network on each request.Server to run and maintain.
AWS Secrets ManagerAWS native runtime secrets resolved by IAM.AWS lock in. Runtime only.
Azure Key VaultAzure native runtime secrets.Azure lock in. Runtime only.

Pick cipher when the secrets live in your repo and you want one tool that behaves the same way from Go code, from CI, and from your editor. Pick Vault or one of the managed runtime stores when the secrets live in a server and the app fetches them at runtime.

Quickstart

CLI

brew install dcadolph/tap/cipher
age-keygen -o key.txt
export SOPS_AGE_KEY_FILE=$PWD/key.txt
PUB=$(age-keygen -y key.txt)echo"db_password: super-secret"> prod.yaml
cipher encrypt --age "$PUB" -i prod.yaml
cipher decrypt prod.yaml

See cmd/README.md for every verb and flag.

Library

import (
"github.com/dcadolph/cipher""github.com/dcadolph/cipher/age"
)
id, _:=age.GenerateIdentity()
kp, _:=age.NewProvider(id.Recipient)
enc:=cipher.NewEncoder(kp)
ciphertext, _:=enc.Encode(ctx, "secrets.yaml", []byte("foo: bar\n"))
dec:=cipher.NewDecoder()
plain, _:=dec.Decode(ctx, "secrets.yaml", ciphertext)

Full API reference at godoc. Runnable per-backend programs live under examples/.

Recipes

Encrypt every plaintext file that matches .sops.yaml

cipher fix ./secrets

Walks the tree, finds plaintext files that match a creation rule, and encrypts them in place with the recipients the rule names.

Edit a secret in your text editor

cipher edit secrets.yaml

Decrypts to a 0600 file in a fresh 0700 temp directory, opens $EDITOR, re-encrypts on save with the original recipients. Plaintext never lands on a shared path.

Add a teammate to an existing file

cipher add-recipient secrets.yaml --age age1bob... -i

The wrapped data key picks up Bob. The encrypted payload itself does not change.

Rotate the data key on every file older than 90 days

cipher walk rotate ./secrets --config .sops.yaml --older-than 90d --parallel 8

Same recipient set, fresh AES key, payload re-encrypted under it. Files newer than the cutoff are skipped.

Audit who can decrypt what

cipher recipients list secrets.yaml --pretty
cipher recipients drift ./secrets --config .sops.yaml

list prints the recipients recorded in a file. drift reports files whose recipient set no longer matches .sops.yaml.

Block plaintext commits

cat > .git/hooks/pre-commit <<'EOF'#!/usr/bin/env bashexec cipher precommitEOF
chmod +x .git/hooks/pre-commit

Refuses any staged file that matches a .sops.yaml rule but is still plaintext.

Architecture

Your Go code or CLI
|
v
cipher.Encoder / cipher.Decoder -----> getsops/sops Go API
|
v
cipher.KeyProvider
|
+--> age
+--> AWS KMS
+--> GCP KMS
+--> Vault Transit
+--> Azure Key Vault
+--> PGP

cipher is a thin Go layer over the SOPS Go API. Each backend implements cipher.KeyProvider and reads credentials the same way the sops binary does. The on disk format is the SOPS format, so anything that decrypts SOPS files (including the upstream sops binary) reads what cipher writes.

Concepts

Each SOPS-encrypted file holds a per-file AES-256 data key that protects the secret payload. That data key is itself wrapped (re-encrypted) once for each recipient you grant access to. A recipient is whatever the backend understands as an identity:

BackendA recipient looks like
ageage1... public key
AWS KMSarn:aws:kms:... key ARN
GCP KMSprojects/.../cryptoKeys/... resource ID
Vault Transithttps://vault/.../keys/<name> URI
Azure Key Vaulthttps://<vault>.vault.azure.net/keys/<key>/<ver> URL
PGPA GPG key fingerprint

Anyone holding the matching private key (or IAM access) can unwrap their copy of the data key and decrypt the file.

Backends

BackendPackage
agecipher/age
AWS KMScipher/kms
GCP KMScipher/gcpkms
Vault Transitcipher/vault
Azure Key Vaultcipher/azkv
PGPcipher/pgp

Each implements cipher.KeyProvider. Mix them with cipher.MergeProviders. Use cipher.NewShamirRule for threshold-of-N across backends.

Install

CLI

# Homebrew (macOS, Linux)
brew install dcadolph/tap/cipher
# Go toolchain
go install github.com/dcadolph/cipher/cmd/cipher@latest
# From source
git clone https://github.com/dcadolph/cipher &&cd cipher && make install

Prebuilt binaries and checksums for Linux, macOS, and Windows ship with every release.

Library

go get github.com/dcadolph/cipher

Requires Go 1.25 or newer.

Decrypt credentials

Each backend reads decryption credentials the same way the SOPS binary does:

BackendDecrypt credential
ageSOPS_AGE_KEY_FILE pointing at the secret key file, or SOPS_AGE_KEY in the environment.
AWS KMSAWS credentials via the default chain.
GCP KMSApplication-default credentials.
Vault TransitVAULT_TOKEN and VAULT_ADDR.
Azure Key VaultAzure default credential chain.
PGPThe gpg binary on PATH with the matching private key in the keyring.

FAQ

Which backend should I pick?

  • age: solo dev or small team, minimum infra, secrets in git. Recommended starting point.
  • AWS KMS / GCP KMS / Azure Key Vault: cloud workloads with IAM-driven access and a managed audit trail.
  • Vault Transit: self-hosted HashiCorp Vault already in your stack.
  • PGP: existing GPG keyring or regulatory requirement.

Why not just use the sops CLI directly?

You can. cipher decrypt is wire-compatible. cipher adds a Go library for encryption (sops ships decrypt only), a parallel directory walker, atomic writes, recipient diff and audit tools, a built-in git pre-commit hook, and an $EDITOR workflow that re-uses the file's existing recipients.

Is my data key safe across rotation?

Yes. Rotation generates a fresh AES-256 key, re-encrypts the payload under it, then wraps the new key for the same recipient set. The old data key is never persisted after the rotation completes.

What about Shamir threshold-of-N?

Supported via cipher.NewShamirRule (library) or --shamir-threshold N (CLI). Split recipients into K key groups. Any N groups can recover the data key.

Can I use cipher with a non-Go service?

Decrypt yes, with the standard sops binary. Encrypt also yes. The on-disk format is identical to what sops produces, so any consumer that decrypts SOPS files reads what cipher writes.

Is the API stable?

Pre-1.0. The on-disk format is the SOPS format and stays compatible. The Go API may break between minor versions until 1.0. Lock to an exact module version if you ship a binary.

Roadmap

cipher is pre-1.0. The on disk format is the sops format and stays compatible across releases. The Go API may break between minor versions until 1.0 lands.

Open work for the road to 1.0:

  • Real round trip integration coverage for GCP KMS and Azure Key Vault. Both backends currently rely on shape and identity format tests because no usable open source emulator exists for either.
  • Stabilize the EncoderOptions surface so a 1.0 tag freezes the public API.
  • Land cipher in homebrew core so brew install cipher works without the tap.

Open an issue or a PR if you want to push any of these forward.

Docs

SurfaceCovers
examples/Runnable Go programs for every backend and most cross-cutting features.
cmd/README.mdEvery CLI verb, every flag, runnable examples.
godocFull library API reference.
SECURITY.mdThreat model, key handling, disclosure path.
CONTRIBUTING.mdDev setup, what CI runs, commit and PR style.
RELEASING.mdRelease workflow, Homebrew tap setup, what every tag publishes.
cipher demoSix in-browser cinematic explainers, about five minutes total.

License

Apache-2.0. See LICENSE.

About

Programmatic SOPS for Go: encrypt, decrypt, rotate, walk, edit, and audit secret files. CLI + library.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Latest commit

History

104 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

cipher

cipher

testlintGo ReferenceGo Report CardLicense

Programmatic SOPS, from Go. One library and one CLI for encrypt, decrypt, rotate, walk, edit, and audit. Drop in next to your existing sops files and keep going.

cipher demo

Every release is exercised end to end against real Vault Transit, AWS KMS through LocalStack, and a fresh PGP keyring. The on disk format is the standard sops format, so the upstream sops binary reads what cipher writes.

What you can do

  • Encrypt and decrypt YAML, JSON, ENV, INI, or binary files with age, AWS KMS, GCP KMS, Vault Transit, Azure Key Vault, or PGP.
  • Edit encrypted files in $EDITOR, re-encrypted on save with the original recipients.
  • Rotate the per-file encryption key on demand or on age (--older-than 90d).
  • Add or drop recipients without re-encrypting the payload.
  • Walk a directory tree in parallel and apply any of the above to every matching file.
  • Route per-path recipient selection from a .sops.yaml policy file.
  • Block plaintext commits with a git pre-commit hook.
  • Stream secrets through Go net/http middleware and emit OpenTelemetry traces.

When to pick cipher

ToolBest forTradeoff
cipherSecrets committed to git plus Go integration, parallel directory walks, audit and drift checks, and a pre-commit hook.Pre-1.0. Go API may break between minor versions.
raw sops CLISecrets in git when one file at a time is enough and no Go consumer needs an encrypt API.No directory walker. The sops Go API only decrypts.
HashiCorp VaultRuntime secrets your app fetches over the network on each request.Server to run and maintain.
AWS Secrets ManagerAWS native runtime secrets resolved by IAM.AWS lock in. Runtime only.
Azure Key VaultAzure native runtime secrets.Azure lock in. Runtime only.

Pick cipher when the secrets live in your repo and you want one tool that behaves the same way from Go code, from CI, and from your editor. Pick Vault or one of the managed runtime stores when the secrets live in a server and the app fetches them at runtime.

Quickstart

CLI

brew install dcadolph/tap/cipher
age-keygen -o key.txt
export SOPS_AGE_KEY_FILE=$PWD/key.txt
PUB=$(age-keygen -y key.txt)echo"db_password: super-secret"> prod.yaml
cipher encrypt --age "$PUB" -i prod.yaml
cipher decrypt prod.yaml

See cmd/README.md for every verb and flag.

Library

import (
"github.com/dcadolph/cipher""github.com/dcadolph/cipher/age"
)
id, _:=age.GenerateIdentity()
kp, _:=age.NewProvider(id.Recipient)
enc:=cipher.NewEncoder(kp)
ciphertext, _:=enc.Encode(ctx, "secrets.yaml", []byte("foo: bar\n"))
dec:=cipher.NewDecoder()
plain, _:=dec.Decode(ctx, "secrets.yaml", ciphertext)

Full API reference at godoc. Runnable per-backend programs live under examples/.

Recipes

Encrypt every plaintext file that matches .sops.yaml

cipher fix ./secrets

Walks the tree, finds plaintext files that match a creation rule, and encrypts them in place with the recipients the rule names.

Edit a secret in your text editor

cipher edit secrets.yaml

Decrypts to a 0600 file in a fresh 0700 temp directory, opens $EDITOR, re-encrypts on save with the original recipients. Plaintext never lands on a shared path.

Add a teammate to an existing file

cipher add-recipient secrets.yaml --age age1bob... -i

The wrapped data key picks up Bob. The encrypted payload itself does not change.

Rotate the data key on every file older than 90 days

cipher walk rotate ./secrets --config .sops.yaml --older-than 90d --parallel 8

Same recipient set, fresh AES key, payload re-encrypted under it. Files newer than the cutoff are skipped.

Audit who can decrypt what

cipher recipients list secrets.yaml --pretty
cipher recipients drift ./secrets --config .sops.yaml

list prints the recipients recorded in a file. drift reports files whose recipient set no longer matches .sops.yaml.

Block plaintext commits

cat > .git/hooks/pre-commit <<'EOF'#!/usr/bin/env bashexec cipher precommitEOF
chmod +x .git/hooks/pre-commit

Refuses any staged file that matches a .sops.yaml rule but is still plaintext.

Architecture

Your Go code or CLI
|
v
cipher.Encoder / cipher.Decoder -----> getsops/sops Go API
|
v
cipher.KeyProvider
|
+--> age
+--> AWS KMS
+--> GCP KMS
+--> Vault Transit
+--> Azure Key Vault
+--> PGP

cipher is a thin Go layer over the SOPS Go API. Each backend implements cipher.KeyProvider and reads credentials the same way the sops binary does. The on disk format is the SOPS format, so anything that decrypts SOPS files (including the upstream sops binary) reads what cipher writes.

Concepts

Each SOPS-encrypted file holds a per-file AES-256 data key that protects the secret payload. That data key is itself wrapped (re-encrypted) once for each recipient you grant access to. A recipient is whatever the backend understands as an identity:

BackendA recipient looks like
ageage1... public key
AWS KMSarn:aws:kms:... key ARN
GCP KMSprojects/.../cryptoKeys/... resource ID
Vault Transithttps://vault/.../keys/<name> URI
Azure Key Vaulthttps://<vault>.vault.azure.net/keys/<key>/<ver> URL
PGPA GPG key fingerprint

Anyone holding the matching private key (or IAM access) can unwrap their copy of the data key and decrypt the file.

Backends

BackendPackage
agecipher/age
AWS KMScipher/kms
GCP KMScipher/gcpkms
Vault Transitcipher/vault
Azure Key Vaultcipher/azkv
PGPcipher/pgp

Each implements cipher.KeyProvider. Mix them with cipher.MergeProviders. Use cipher.NewShamirRule for threshold-of-N across backends.

Install

CLI

# Homebrew (macOS, Linux)
brew install dcadolph/tap/cipher
# Go toolchain
go install github.com/dcadolph/cipher/cmd/cipher@latest
# From source
git clone https://github.com/dcadolph/cipher &&cd cipher && make install

Prebuilt binaries and checksums for Linux, macOS, and Windows ship with every release.

Library

go get github.com/dcadolph/cipher

Requires Go 1.25 or newer.

Decrypt credentials

Each backend reads decryption credentials the same way the SOPS binary does:

BackendDecrypt credential
ageSOPS_AGE_KEY_FILE pointing at the secret key file, or SOPS_AGE_KEY in the environment.
AWS KMSAWS credentials via the default chain.
GCP KMSApplication-default credentials.
Vault TransitVAULT_TOKEN and VAULT_ADDR.
Azure Key VaultAzure default credential chain.
PGPThe gpg binary on PATH with the matching private key in the keyring.

FAQ

Which backend should I pick?

  • age: solo dev or small team, minimum infra, secrets in git. Recommended starting point.
  • AWS KMS / GCP KMS / Azure Key Vault: cloud workloads with IAM-driven access and a managed audit trail.
  • Vault Transit: self-hosted HashiCorp Vault already in your stack.
  • PGP: existing GPG keyring or regulatory requirement.

Why not just use the sops CLI directly?

You can. cipher decrypt is wire-compatible. cipher adds a Go library for encryption (sops ships decrypt only), a parallel directory walker, atomic writes, recipient diff and audit tools, a built-in git pre-commit hook, and an $EDITOR workflow that re-uses the file's existing recipients.

Is my data key safe across rotation?

Yes. Rotation generates a fresh AES-256 key, re-encrypts the payload under it, then wraps the new key for the same recipient set. The old data key is never persisted after the rotation completes.

What about Shamir threshold-of-N?

Supported via cipher.NewShamirRule (library) or --shamir-threshold N (CLI). Split recipients into K key groups. Any N groups can recover the data key.

Can I use cipher with a non-Go service?

Decrypt yes, with the standard sops binary. Encrypt also yes. The on-disk format is identical to what sops produces, so any consumer that decrypts SOPS files reads what cipher writes.

Is the API stable?

Pre-1.0. The on-disk format is the SOPS format and stays compatible. The Go API may break between minor versions until 1.0. Lock to an exact module version if you ship a binary.

Roadmap

cipher is pre-1.0. The on disk format is the sops format and stays compatible across releases. The Go API may break between minor versions until 1.0 lands.

Open work for the road to 1.0:

  • Real round trip integration coverage for GCP KMS and Azure Key Vault. Both backends currently rely on shape and identity format tests because no usable open source emulator exists for either.
  • Stabilize the EncoderOptions surface so a 1.0 tag freezes the public API.
  • Land cipher in homebrew core so brew install cipher works without the tap.

Open an issue or a PR if you want to push any of these forward.

Docs

SurfaceCovers
examples/Runnable Go programs for every backend and most cross-cutting features.
cmd/README.mdEvery CLI verb, every flag, runnable examples.
godocFull library API reference.
SECURITY.mdThreat model, key handling, disclosure path.
CONTRIBUTING.mdDev setup, what CI runs, commit and PR style.
RELEASING.mdRelease workflow, Homebrew tap setup, what every tag publishes.
cipher demoSix in-browser cinematic explainers, about five minutes total.

License

Apache-2.0. See LICENSE.

About

Programmatic SOPS for Go: encrypt, decrypt, rotate, walk, edit, and audit secret files. CLI + library.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages