Documentation
¶
Overview ¶
Package crypter provides AES-GCM authenticated encryption and decryption for string values.
Format ¶
Every encrypted value is stored as a tagged string:
ENCv1[<base64url-raw(nonce || ciphertext || tag)>]
Where:
- "ENCv1[" is the literal start tag (StartTag) identifying the format version.
- The payload is standard base64url without padding (RFC 4648 §5).
- The payload decodes to: 12-byte random nonce, followed by the AES-GCM ciphertext, followed by the 16-byte GCM authentication tag.
- "]" is the literal end tag (EndTag).
Key requirements ¶
The secret passed to New must be exactly 16, 24, or 32 bytes of raw key material (corresponding to AES-128, AES-192, or AES-256). It is used directly as the AES key with no stretching. Use a cryptographically random key (e.g. from pwgen -s or crypto/rand), not a human-memorable passphrase.
Mixed-plaintext configs ¶
Both Encrypt and Decrypt are safe to call on values that may or may not already be encrypted. Encrypt is idempotent (tagged values pass through unchanged); Decrypt returns untagged values as-is. This makes the package suitable as a transparent encrypt/decrypt layer over config maps that mix plaintext and encrypted fields.
Thread safety ¶
A single Crypter may be used concurrently from multiple goroutines without external synchronization. The internal nonce pool is goroutine-safe.
Usage ¶
Initialize once with a 16/24/32-byte key, then reuse the Crypter for all operations:
c, err := crypter.New(os.Getenv("SECRET_KEY")) // e.g. SECRET_KEY=$(pwgen -s 32 1)
if err != nil {
log.Fatal(err)
}
Encrypt a plaintext value:
enc, err := c.Encrypt("my-database-password")
// enc == "ENCv1[<base64url>]"
Decrypt it back:
plain, err := c.Decrypt(enc) // plain == "my-database-password"
Decrypt is safe to call on values that may or may not be encrypted — plaintext passes through unchanged, which makes it suitable as a transparent layer over config maps:
// both calls succeed; second returns "already-plain" as-is
c.Decrypt("ENCv1[...]") // → decrypted plaintext, nil
c.Decrypt("already-plain") // → "already-plain", nil
Check whether a value is encrypted without performing any cryptographic work:
if c.IsEncrypted(value) {
// value came from an encrypted config field
}
Index ¶
Constants ¶
const ( // StartTag is the prefix that identifies a string as encrypted by this package. // It encodes the format version ("v1") so future format changes can use a different prefix. // Any string passed to Encrypt that already begins with StartTag is assumed to be // encrypted and returned unchanged — callers must ensure plaintext values never start // with this prefix. // // Full format: ENCv1[<base64url-raw(nonce||ciphertext||tag)>] StartTag = "ENCv1[" // EndTag is the suffix that closes the encrypted value. // It is a single "]" character, which is unambiguous within the base64url alphabet // (base64url uses A-Za-z0-9_-, none of which are "]"). // Note: when embedding encrypted values in YAML or JSON, quote the value to prevent // parsers from interpreting the surrounding brackets as a flow sequence or array. EndTag = "]" )
Variables ¶
var ( // ErrInvalidCipherText is returned when the ciphertext is structurally malformed: // missing the end tag, the decoded payload is shorter than a GCM nonce, or the // ciphertext was not produced by this package (wrong format version, truncated, etc.). // Authentication failures from a wrong key are reported as ErrOpen, not this error. ErrInvalidCipherText = errors.New("crypter: invalid ciphertext") // ErrInvalidKeyLength is returned by New when the provided secret is not 16, 24, or 32 bytes. // AES requires one of these exact key lengths (AES-128, AES-192, AES-256 respectively). // The length is measured in bytes, not Unicode code points — a 32-rune string with // multi-byte characters may be more than 32 bytes and will be rejected. ErrInvalidKeyLength = errors.New("crypter: invalid key length") // ErrEmptyPayload is returned when the tagged string contains no base64 payload, // i.e. the input is exactly "ENCv1[]". This is distinct from ErrInvalidCipherText // to make the failure mode explicit when debugging configuration values. ErrEmptyPayload = errors.New("crypter: empty payload") // ErrNewCipher is returned (wrapping the underlying error) when aes.NewCipher fails. // In practice this only happens if the key length is invalid, which New already // validates — so this error should never be seen in normal usage. ErrNewCipher = errors.New("crypter: aes.NewCipher failed") // ErrNewGCM is returned (wrapping the underlying error) when cipher.NewGCM fails. // This should never occur in practice; it is included for completeness. ErrNewGCM = errors.New("crypter: cipher.NewGCM failed") // ErrReadNonce is returned when reading random bytes from crypto/rand fails during // encryption. This is an OS-level error (e.g. /dev/urandom exhausted or unavailable) // and is unrecoverable. The encrypted value is not produced. ErrReadNonce = errors.New("crypter: read nonce failed") // ErrBase64Decode is returned when the payload between the tags is not valid base64url. // This means the ciphertext string was corrupted or hand-edited after encryption. ErrBase64Decode = errors.New("crypter: base64 decode failed") // ErrOpen is returned (wrapping the underlying cipher error) when AES-GCM // authentication or decryption fails. This happens when: // - the ciphertext was encrypted with a different key, // - the ciphertext or its authentication tag was tampered with, or // - the nonce was corrupted. // The underlying error is wrapped so errors.Is(err, ErrOpen) works, and the // original cipher message is preserved for debugging via errors.Unwrap. ErrOpen = errors.New("crypter: aead open failed") )
Functions ¶
This section is empty.
Types ¶
type Crypter ¶
type Crypter struct {
// contains filtered or unexported fields
}
Crypter encrypts and decrypts string values using AES-GCM authenticated encryption.
A Crypter is initialized once with a fixed AES key and then reused for any number of Encrypt/Decrypt calls. The internal cipher and nonce pool are immutable after construction, so concurrent use is safe without external locking.
The zero value is not usable; always construct with New.
func New ¶
New initializes a Crypter with the provided AES key.
The secret must be exactly 16, 24, or 32 bytes, selecting AES-128, AES-192, or AES-256 respectively. The bytes are used directly as the AES key — no hashing or stretching is applied. Provide a cryptographically random key (e.g. from pwgen -s 32 or crypto/rand), not a human-memorable passphrase.
Returns ErrInvalidKeyLength if the secret is not one of the three valid lengths. Other errors (ErrNewCipher, ErrNewGCM) indicate internal cipher initialization failure and should not occur with a valid key.
func (*Crypter) Decrypt ¶
Decrypt decrypts a tagged value and returns the original plaintext.
If data is not tagged (IsEncrypted returns false), it is returned unchanged without error — this is intentional for configs that mix plaintext and encrypted values. If you need to enforce that a value is always encrypted, check IsEncrypted before calling.
For tagged values, the base64 payload is decoded, the 12-byte nonce is extracted from the front, and AES-GCM Open is called to authenticate and decrypt the ciphertext.
Error cases for tagged input:
- ErrInvalidCipherText: missing end tag, decoded payload shorter than a GCM nonce, or payload too short to contain the GCM authentication tag.
- ErrEmptyPayload: the tag is present but contains no base64 content ("ENCv1[]").
- ErrBase64Decode: the payload is not valid base64url.
- ErrOpen: AES-GCM authentication failed — wrong key, corrupted ciphertext, or tampered tag.
func (*Crypter) Encrypt ¶
Encrypt encrypts data using AES-GCM and returns the result wrapped in ENCv1[...].
If data is already tagged (IsEncrypted returns true), it is returned unchanged — Encrypt is idempotent. Otherwise a fresh 12-byte random nonce is drawn from crypto/rand, the plaintext is sealed with AES-GCM, and the result is encoded as:
ENCv1[<base64url-raw(nonce || ciphertext || GCM-tag)>]
The nonce is unique per call, so encrypting the same plaintext twice produces different ciphertext each time.
Warning: if data begins with StartTag but is not a valid encrypted value (a violation of the caller contract documented on IsEncrypted), Encrypt returns it unchanged without error. The caller would silently store plaintext while believing it to be encrypted. Always ensure plaintext values cannot begin with StartTag.
Returns ErrReadNonce if crypto/rand is unavailable (OS-level failure).
func (*Crypter) IsEncrypted ¶
IsEncrypted reports whether s looks like a value encrypted by this package.
It is a fast heuristic: it checks only that s begins with StartTag and has at least one byte after it. No base64 decoding or cryptographic verification is performed. A false positive (a plaintext value that happens to start with "ENCv1[") will cause Encrypt to skip encryption and Decrypt to attempt — and fail — decryption.
Contract: plaintext values must never begin with StartTag. This is the caller's responsibility; the package does not enforce it.