Batteries-included encryption for Go: AEAD ciphers, RSA, and a ready-made envelope-encryption scheme, all behind a small, hard-to-misuse API.
crypt wraps Go's standard crypto packages and golang.org/x/crypto so you
can encrypt and decrypt data with well-established primitives without writing
the fiddly plumbing yourself. Every function authenticates its output, generates
nonces for you, and returns plain []byte / string values.
- AES-GCM: AES-128/192/256 authenticated encryption.
- ChaCha20-Poly1305: fast AEAD with a 96-bit nonce.
- XChaCha20-Poly1305: AEAD with a 192-bit nonce, safe for huge numbers of messages under one key.
- RSA-OAEP: public-key encryption with SHA-256 (default) or SHA-512.
- Base64 helpers: Std, RawStd, URL and RawURL encoders/decoders.
envelopesubpackage: a complete KEK/DEK envelope-encryption scheme for protecting many records under a single rotatable secret.
go get github.com/pilinux/cryptRequires Go 1.25+. The only external dependency is golang.org/x/crypto.
package main
import (
"crypto/rand"
"fmt"
"golang.org/x/crypto/argon2"
"github.com/pilinux/crypt"
)
func main() {
// 1. Derive a 32-byte key. crypt never derives keys for you;
// bring your own KDF (here: Argon2id over a passphrase).
salt := make([]byte, 16)
if _, err := rand.Read(salt); err != nil {
panic(err)
}
key := argon2.IDKey([]byte("s3cr3t-passphrase"), salt, 2, 64*1024, 2, 32)
// 2. Encrypt. A random nonce is generated and prepended to the
// ciphertext, so you only ever store a single blob.
ciphertext, err := crypt.EncryptAesGcmWithNonceAppended(key, "attack at dawn")
if err != nil {
panic(err)
}
// 3. Decrypt. This also verifies authenticity: any tampering
// (or a wrong key) returns an error instead of garbage.
plaintext, err := crypt.DecryptAesGcmWithNonceAppended(key, ciphertext)
if err != nil {
panic(err)
}
fmt.Println(plaintext) // attack at dawn
}Every symmetric cipher follows the same four-way naming pattern, so once you know one you know them all:
| Variant | Input / output | Nonce |
|---|---|---|
Encrypt<Cipher> |
string |
returned separately |
EncryptByte<Cipher> |
[]byte |
returned separately |
Encrypt<Cipher>WithNonceAppended |
string |
prepended to ciphertext |
EncryptByte<Cipher>WithNonceAppended |
[]byte |
prepended to ciphertext |
Swap EncryptAesGcm for EncryptXChacha20poly1305 (or the ChaCha20 variant) to
change algorithms; the shape is identical.
// publicKeyPEM / privateKeyPEM are strings loaded from .pem files
// (PKIX "PUBLIC KEY" and PKCS#8 "PRIVATE KEY" blocks; see below).
enc := crypt.NewEncoder(publicKeyPEM)
if enc.Err != nil {
panic(enc.Err) // the constructor reports PEM problems via .Err
}
ciphertext, err := enc.EncryptRSA("attack at dawn")
if err != nil {
panic(err)
}
dec := crypt.NewDecoder(privateKeyPEM)
if dec.Err != nil {
panic(dec.Err)
}
plaintext, err := dec.DecryptRSA(ciphertext)
if err != nil {
panic(err)
}
// Want SHA-512 instead of the SHA-256 default? Set it on both sides:
// enc.HashAlg = crypt.SHA512
// dec.HashAlg = crypt.SHA512Use the envelope
subpackage when you need to protect lots of items (rows, files, fields) and be
able to rotate the top-level secret without re-encrypting everything.
package main
import (
"fmt"
"os"
"github.com/pilinux/crypt/envelope"
)
func main() {
// Configure once with your app's domain-separation labels.
scheme := envelope.New(envelope.Config{
KEKLabel: "myapp:kek:v1",
SubKeyLabel: "myapp:data-subkey:v1",
})
// Bootstrap: derive a key-encryption key (KEK) from a rotatable secret,
// then generate a master key and store it *wrapped*. (Errors omitted
// for brevity; handle them in real code.)
// The secret must be machine-generated randomness, >= 32 chars
// (e.g. `openssl rand -hex 32`), never a human-chosen passphrase.
kek, _ := scheme.DeriveKEK(os.Getenv("ENCRYPTION_SECRET"))
masterKey, _ := envelope.GenerateMasterKey()
wrapped, _ := envelope.WrapKey(kek, masterKey) // persist `wrapped`, not masterKey
_ = wrapped
// Per item: seal to a base64 token, then open it back.
token, _ := scheme.SealString(masterKey, "top secret")
plain, _ := scheme.OpenString(masterKey, token)
fmt.Println(plain) // top secret
// Optional context binding: authenticate the record/field the token
// belongs to, so valid tokens cannot be swapped between rows.
bound, _ := scheme.SealStringAAD(masterKey, "top secret", []byte("user:42:note"))
_, err := scheme.OpenStringAAD(masterKey, bound, []byte("user:7:note"))
fmt.Println(err != nil) // wrong context fails to decrypt
}Under the hood every item gets a fresh per-item sub-key (HKDF) and its own
random nonce, so a nonce can never repeat under the same key.
The envelope header is authenticated, and every Seal*/Open* function
has an AAD variant that additionally authenticates caller-supplied context.
| If you want to… | Reach for | Key |
|---|---|---|
| Encrypt data with a key you already hold or derive | AES-256-GCM or XChaCha20-Poly1305 | 32 bytes |
| Encrypt many messages under one key without nonce worries | XChaCha20-Poly1305 | 32 bytes |
| Let someone encrypt to you using your public key | RSA-OAEP | PEM key pair |
| Protect many records under one rotatable secret | envelope subpackage |
derived |
| Area | Key functions |
|---|---|
AES-GCM (aes.go) |
EncryptAesGcm / DecryptAesGcm (+ Byte and WithNonceAppended variants) |
ChaCha20-Poly1305 (chaCha20.go) |
EncryptChacha20poly1305 / DecryptChacha20poly1305 (96-bit nonce) |
XChaCha20-Poly1305 (chaCha20.go) |
EncryptXChacha20poly1305 / DecryptXChacha20poly1305 (192-bit nonce) |
RSA-OAEP (rsa.go) |
Encoder.EncryptRSA / Decoder.DecryptRSA (+ Byte variants) |
Base64 (base64.go) |
Encoder.ToBase64* / Decoder.FromBase64* (Std, RawStd, URL, RawURL) |
Envelope (envelope/) |
Scheme.Seal*/Open* (+ AAD variants), DeriveKEK, WrapKey/UnwrapKey, Zero, Sha256Hex, RandomHex |
The ChaCha20/XChaCha20 Byte...WithNonceAppended functions also come in
...AAD forms that bind caller-supplied associated data (authenticated, not
encrypted) into the ciphertext.
Full, always-current reference lives on pkg.go.dev.
Each folder under _example is a standalone program you can run
with go run ./_example/<name>:
RSA works with a PKIX public key (PUBLIC KEY) and a PKCS#8 private key
(PRIVATE KEY): exactly what these OpenSSL commands produce.
openssl genpkey -algorithm RSA -out private-key.pem -pkeyopt rsa_keygen_bits:2048
openssl rsa -in private-key.pem -pubout -out public-key.pemopenssl genpkey -algorithm RSA -out private-key.pem -pkeyopt rsa_keygen_bits:3072
openssl rsa -in private-key.pem -pubout -out public-key.pemopenssl genpkey -algorithm RSA -out private-key.pem -pkeyopt rsa_keygen_bits:4096
openssl rsa -in private-key.pem -pubout -out public-key.pem- Bring your own key derivation.
cryptencrypts with the key you give it; it never derives one. Use Argon2id for passwords and HKDF for high-entropy secrets (theenvelopesubpackage does the latter for you). - The envelope secret must be machine-generated.
DeriveKEKuses HKDF, which does no password stretching: generateENCRYPTION_SECRETwithopenssl rand -hex 32(or similar) and never use a human-chosen passphrase. A guessable secret can be brute-forced offline from the wrapped master key. - Key sizes. AES accepts 16/24/32-byte keys; ChaCha20 and XChaCha20 require exactly 32 bytes.
- Never reuse a (key, nonce) pair. Nonces come from
crypto/rand. When encrypting many items under one key, prefer XChaCha20-Poly1305 or theenvelopescheme, which give each item its own key or a large random nonce. - Everything is authenticated. All AEAD modes and RSA-OAEP fail closed: tampered ciphertext or a wrong key returns an error, never partial plaintext.
- Fail closed on bad input, never panic. The
Decrypt…functions that take a nonce directly validate its length (12 bytes for AES-GCM and ChaCha20-Poly1305, 24 for XChaCha20-Poly1305) and return an error on a mismatch instead of letting the underlying cipher panic. - Per-message size limit. A single message is capped by the underlying AEAD: roughly 256 GiB for ChaCha20/XChaCha20-Poly1305 and 64 GiB for AES-GCM. Anything larger returns an error rather than panicking. These bounds sit far above any realistic payload; stream or chunk data that big.
- RSA key formats. The public key must be a PKIX
PUBLIC KEYblock and the private key a PKCS#8PRIVATE KEYblock. Always check.Errright afterNewEncoder/NewDecoder.
go test -race -cover ./... # unit tests, race detector, coverage
go vet ./... # static analysis
golangci-lint run ./... # aggregate lintersMIT. See LICENSE.