Symmetric encryption for Go - AES-128/256 CBC with HMAC, key rotation, and portable payloads.
go get github.com/goforj/cryptcrypt provides symmetric encryption for Go services with authenticated payloads (AES-CBC + HMAC) and key rotation via APP_PREVIOUS_KEYS. It also supports Laravel/PHP-compatible payloads for interoperability.
- AES-128 / AES-256 encryption (Laravel/PHP-compatible payload format)
- Authenticated encryption (AES-CBC + HMAC)
- Transparent key rotation via
APP_PREVIOUS_KEYS - Zero dependencies (stdlib only)
- Deterministic, testable API
- Instanced and global usage styles
- Safe defaults with explicit failure modes
crypt exists to solve one problem well: encrypting small application payloads with safe defaults and painless key rotation.
It is not a general-purpose cryptography library.
It is a focused, application-layer utility designed to be boring, predictable, and interoperable.
package main
import (
"fmt"
"github.com/goforj/crypt"
)
func main() {
appKey := "base64:..." // 16-byte (AES-128) or 32-byte (AES-256) key after decoding.
key, err := crypt.ReadAppKey(appKey)
if err != nil {
panic(err)
}
c, err := crypt.New(key)
if err != nil {
panic(err)
}
ciphertext, err := c.Encrypt("secret")
if err != nil {
panic(err)
}
plaintext, err := c.Decrypt(ciphertext)
if err != nil {
panic(err)
}
fmt.Println(plaintext) // "secret"
}package main
import (
"fmt"
"os"
"github.com/goforj/crypt"
)
func main() {
_ = os.Setenv("APP_KEY", "base64:...")
ciphertext, _ := crypt.Encrypt("secret")
plaintext, _ := crypt.Decrypt(ciphertext)
fmt.Println(plaintext) // "secret"
}crypt uses a base64-prefixed key format and supports key rotation. This matches Laravel/PHP conventions when interoperability is needed.
APP_KEYmust be prefixed withbase64:and decode to either 16 bytes (AES-128) or 32 bytes (AES-256).APP_PREVIOUS_KEYSis optional and may contain a comma-separated list of older keys in the same format.- During decryption, the current key is tried first, followed by any previous keys.
- Encryption always uses the current
APP_KEY; previous keys are never used for encryption.
export APP_KEY="base64:J63qRTDLub5NuZvP+kb8YIorGS6qFYHKVo6u7179stY="
export APP_PREVIOUS_KEYS="base64:2nLsGFGzyoae2ax3EF2Lyq/hH6QghBGLIq5uL+Gp8/w="Every function has a corresponding runnable example under ./examples.
Examples are generated directly from function doc comments, and the same snippets power the README and GoDoc examples.
An automated test builds every example so the docs stay valid as the API evolves.
Global = package-level functions (env-based convenience).
Instanced = methods on *crypt.Cipher with injected keys.
| Group | Namespace | Functions |
|---|---|---|
| Encryption | Global | Decrypt Encrypt |
| Encryption | Instanced | Cipher.Decrypt Cipher.Encrypt |
| Key management | Global | GenerateAppKey GenerateKeyToEnv GetAppKey GetPreviousAppKeys New NewFromEnv ReadAppKey RotateKeyInEnv |
Decrypt decrypts an encrypted payload using the APP_KEY from environment. Falls back to APP_PREVIOUS_KEYS when the current key cannot decrypt.
Example: decrypt using current key
appKey, _ := crypt.GenerateAppKey()
_ = os.Setenv("APP_KEY", appKey)
ciphertext, _ := crypt.Encrypt("secret")
plaintext, _ := crypt.Decrypt(ciphertext)
godump.Dump(plaintext)
// #string "secret"Example: decrypt ciphertext encrypted with a previous key
oldAppKey, _ := crypt.GenerateAppKey()
newAppKey, _ := crypt.GenerateAppKey()
// Encrypt with the old key first.
_ = os.Setenv("APP_KEY", oldAppKey)
rotatedCiphertext, _ := crypt.Encrypt("rotated")
// Rotate to a new current key, but keep the old key in APP_PREVIOUS_KEYS.
_ = os.Setenv("APP_KEY", newAppKey)
_ = os.Setenv("APP_PREVIOUS_KEYS", oldAppKey)
plaintext, err := crypt.Decrypt(rotatedCiphertext)
godump.Dump(plaintext, err)
// #string "rotated"
// #error <nil>Encrypt encrypts a plaintext using the APP_KEY from environment.
appKey, _ := crypt.GenerateAppKey()
_ = os.Setenv("APP_KEY", appKey)
ciphertext, err := crypt.Encrypt("secret")
godump.Dump(err == nil, ciphertext != "")
// #bool true
// #bool trueDecrypt decrypts ciphertext with the current key, then any configured previous keys.
Encrypt encrypts plaintext with the Cipher's injected current key.
GenerateAppKey generates a random base64 app key prefixed with "base64:".
key, _ := crypt.GenerateAppKey()
godump.Dump(key)
// #string "base64:..."GenerateKeyToEnv mimics Laravel's key:generate. It generates a new APP_KEY and writes it to the provided .env path. Other keys are preserved; APP_KEY is replaced/added.
envPath := filepath.Join(os.TempDir(), ".env")
key, err := crypt.GenerateKeyToEnv(envPath)
godump.Dump(err, key)
// #error <nil>
// #string "base64:..."GetAppKey retrieves the APP_KEY from the environment and parses it.
appKey, _ := crypt.GenerateAppKey()
_ = os.Setenv("APP_KEY", appKey)
key, err := crypt.GetAppKey()
godump.Dump(len(key), err)
// #int 32
// #error <nil>GetPreviousAppKeys retrieves and parses APP_PREVIOUS_KEYS from the environment. Keys are expected to be comma-delimited and prefixed with "base64:".
oldKeyA, _ := crypt.GenerateAppKey()
oldKeyB, _ := crypt.GenerateAppKey()
// APP_PREVIOUS_KEYS is a comma-separated list.
_ = os.Setenv("APP_PREVIOUS_KEYS", oldKeyA+", "+oldKeyB)
keys, err := crypt.GetPreviousAppKeys()
godump.Dump(len(keys), err)
// #int 2
// #error <nil>New constructs a Cipher with an injected current key and optional previous keys. Keys must be 16 bytes (AES-128) or 32 bytes (AES-256). Inputs are copied.
NewFromEnv constructs a Cipher from APP_KEY and APP_PREVIOUS_KEYS.
ReadAppKey parses a base64 encoded app key with "base64:" prefix. Accepts 16-byte keys (AES-128) or 32-byte keys (AES-256) after decoding.
// Build a 16-byte (AES-128) key string manually.
raw16 := make([]byte, 16)
_, _ = rand.Read(raw16)
key16 := "base64:" + base64.StdEncoding.EncodeToString(raw16)
// Generate a 32-byte (AES-256) key string with the helper.
key32, _ := crypt.GenerateAppKey()
parsed16, _ := crypt.ReadAppKey(key16)
parsed32, _ := crypt.ReadAppKey(key32)
godump.Dump(len(parsed16), len(parsed32))
// #int 16
// #int 32RotateKeyInEnv mimics Laravel's key:rotate. It moves the current APP_KEY into APP_PREVIOUS_KEYS (prepended) and writes a new APP_KEY.
envPath := filepath.Join(os.TempDir(), ".env")
currentKey, _ := crypt.GenerateAppKey()
// Seed a minimal .env with an existing APP_KEY.
_ = os.WriteFile(envPath, []byte("APP_KEY="+currentKey+"\n"), 0o644)
newKey, err := crypt.RotateKeyInEnv(envPath)
godump.Dump(err == nil, newKey != "")
// #bool true
// #bool true