encryption

package
v0.12.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 21, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package encryption encrypts, decrypts and signs values with the application key.

Encrypter is the type that holds the key. Encrypt and Decrypt carry an arbitrary Go value through JSON; Encrypter.EncryptString and Encrypter.DecryptString carry a string as it is. Supported reports whether a key and cipher pair can be used at all, and AppearsEncrypted recognises a payload written by this package without needing the key.

ParseKey reads the "base64:" form a configuration file holds and returns ErrMissingAppKey for the empty key, so an unkeyed application stops at boot rather than at the first request that needed the key.

Signer signs and verifies URLs and tokens. It lives here because it signs with the same application key, and a second place holding that key is a second place to get it wrong.

Index

Constants

View Source
const KeySize = 32

KeySize is the length of the application key, in bytes.

It is a constant and not a setting: the key feeds HMAC-SHA256, which has one right key length, and a configurable one only lets a project pick a worse value than the default. Everything the application signs -- the session cookie, the CSRF token, the links Signer issues -- is signed with this one key, because an attacker holding it does not need three.

Variables

View Source
var ErrDecrypt = errors.New("encryption: could not decrypt the data")

ErrDecrypt is the answer to anything that arrives and does not decrypt: a payload that was tampered with, one encrypted under a different key, or one that is not a payload at all.

The three are deliberately indistinguishable from outside. Telling a caller WHICH of them happened tells an attacker whether their forgery got closer, which is the oracle that makes padding attacks work. errInvalidPayload wraps this one for the same reason -- it is more specific inside the package and identical to anyone outside it.

Every failure below wraps it, so a caller answers "this value is not ours" once rather than switching on four reasons it is not. The reasons are still in the message, because the one reading it is a developer holding a payload, not a request.

View Source
var ErrEncrypt = errors.New("encryption: could not encrypt the data")

ErrEncrypt wraps every failure on the way out: the value would not serialise to JSON, or the cipher refused the key.

It is not a mistake a caller recovers from by trying something else. Both causes are configuration -- a key of the wrong length, or a value holding something json.Marshal cannot represent -- so a handler that reaches it should report and stop, not retry.

The underlying error is wrapped, so errors.Is finds this one and %v still prints what actually went wrong.

View Source
var ErrExpired = fmt.Errorf("%w: the link has expired", ErrSignature)

ErrExpired is a valid signature that has run out of time. It unwraps to ErrSignature, so a caller that does not care about the difference does not have to check for both -- and one that does can offer a new link.

View Source
var ErrMissingAppKey = errors.New("encryption: no application encryption key has been specified (run `aru key:generate`)")

ErrMissingAppKey is what ParseKey returns when the configured key is empty. Its message names the command that fixes it, because there is no renderer between ParseKey and the process that is refusing to start.

It is a distinct error and not the length error below because the two are different mistakes: a missing key means the application was never keyed, and a short one means it was keyed wrongly. Only the first is what a fresh checkout hits.

View Source
var ErrSignature = errors.New("security: the signature is not valid")

ErrSignature is what every failure below unwraps to, so a caller answers "this link is not valid" once rather than switching on four reasons it is not.

View Source
var ErrUnsupportedCipher = errors.New("encryption: unsupported cipher or incorrect key length; the supported cipher is: aes-256-gcm")

ErrUnsupportedCipher is returned by NewEncrypter and Encrypter.PreviousKeys when the key length does not match the cipher.

Functions

func AppearsEncrypted

func AppearsEncrypted(value string) bool

AppearsEncrypted reports whether a value looks like something this encrypter wrote.

It is a guess and says so by returning a bool rather than an error: it reads the envelope, never a key. It is what a migration re-encrypting a column uses to skip the rows it already did. A true here does not promise [DecryptString] will succeed -- only that it is worth calling.

func Decrypt

func Decrypt[T any](e *Encrypter, payload string) (T, error)

Decrypt decrypts a value written by Encrypt back into T.

It is a function and not a method for the same reason Encrypt is.

func Encrypt

func Encrypt[T any](e *Encrypter, value T) (string, error)

Encrypt encrypts a value of any type, serialising it to JSON first.

It is a function and not a method because it is generic and Go methods cannot be. The type parameter is what the caller states the payload holds, and the compiler holds it to it.

Serialization is encoding/json. A value written by Encrypt is read back by Decrypt and by nothing else; Encrypter.EncryptString is the pair whose payload is portable.

func GenerateKey

func GenerateKey() string

GenerateKey returns a new application key, already written the way a .env file has to hold it: keyPrefix followed by KeySize random bytes in base64.

Cipher.GenerateKey is the one that returns raw bytes. The two are one call apart: GenerateKey is "base64:" plus base64 of AES256GCM.GenerateKey().

It returns the encoded string rather than the raw bytes on purpose. The caller that wants a key wants to print it or write it down, and if it were handed bytes it would have to encode them -- which means the encoding would be known in two places, and the one that drifts is the one ParseKey does not read.

There is no error to return: crypto/rand.Read fills the slice or the process dies trying, and a caller cannot recover from an operating system that has stopped producing randomness.

func ParseKey

func ParseKey(v string) ([]byte, error)

ParseKey reads a configured application key and returns the bytes it names, accepting both the base64 form GenerateKey emits and a raw KeySize-byte string typed by hand.

The empty key is ErrMissingAppKey.

The length is checked here rather than by the caller, so that a key that parses is a key that works. Refusing it at boot is the whole point: a short key is a weaker signature everywhere at once, and it is not visible from any request that would go wrong.

func Supported

func Supported(key []byte, cipher Cipher) bool

Supported reports whether the key and cipher combination is usable.

Both halves matter: an unknown cipher fails, and so does a key of the wrong length for a known one. The key is measured in bytes.

Types

type Cipher

type Cipher string

Cipher names an encryption algorithm.

This package supports one, AES256GCM. A second cipher would be a second way to encrypt, and the alternatives are not equally safe: an unauthenticated mode has to carry an HMAC alongside the ciphertext and check it by hand, getting that wrong is silent, and the mistake is only visible to whoever is forging payloads. AES-256-GCM authenticates as part of decrypting, so there is no second thing to check and no way to forget to check it.

The value is compared case-insensitively.

const AES256GCM Cipher = "aes-256-gcm"

AES256GCM is the cipher this package encrypts with.

func (Cipher) GenerateKey

func (c Cipher) GenerateKey() []byte

GenerateKey returns a new random key of the length the cipher requires.

The cipher is the receiver rather than an argument because Go has no overloading and the package already exports a GenerateKey that returns the printable "base64:" form a configuration file holds.

An unrecognised cipher yields 32 bytes rather than an error. There is no error to return: crypto/rand fills the slice or the process dies trying.

type Encrypter

type Encrypter struct {
	// contains filtered or unexported fields
}

Encrypter encrypts and decrypts values with the application key.

The payload it writes is base64 of a JSON object with iv, value, mac and tag, in that order, each of them base64 itself. The mac is empty, because with an AEAD cipher the tag is the MAC; the field stays in place because a reader holding a column of mixed formats needs it to tell them apart.

It is safe for concurrent use. Nothing mutates after construction except PreviousKeys, which is a boot-time call.

func NewEncrypter

func NewEncrypter(key []byte, cipher Cipher) (*Encrypter, error)

NewEncrypter returns an Encrypter over the given key.

The cipher has no default. Passing AES256GCM is the only call that works, and requiring it keeps the payload format a thing the caller chose rather than a thing it inherited.

The key is copied, so a caller that zeroes or reuses its buffer does not silently change what the application encrypts with.

func (*Encrypter) DecryptString

func (e *Encrypter) DecryptString(payload string) (string, error)

DecryptString decrypts a string without deserialising it. It is the pair to Encrypter.EncryptString.

func (*Encrypter) EncryptString

func (e *Encrypter) EncryptString(value string) (string, error)

EncryptString encrypts a string without serializing it.

The payload is the portable one: anything else holding the same key and reading this format can decrypt it, and this package can decrypt what such a reader wrote. An empty string is a legal input and produces a real payload: GCM over no plaintext is a nonce and a tag, and Encrypter.DecryptString returns "" from it.

func (*Encrypter) GetAllKeys

func (e *Encrypter) GetAllKeys() [][]byte

GetAllKeys returns the current key followed by every previous key.

The order is the order decryption tries them, and the current key is first because it is the one that will work.

func (*Encrypter) GetKey

func (e *Encrypter) GetKey() []byte

GetKey returns the key the encrypter encrypts with.

The copy is not politeness. A []byte is a window onto the encrypter's own memory, and a caller that appended to it would be rewriting the application key from outside.

func (*Encrypter) GetPreviousKeys

func (e *Encrypter) GetPreviousKeys() [][]byte

GetPreviousKeys returns the retired keys, without the current one.

func (*Encrypter) PreviousKeys

func (e *Encrypter) PreviousKeys(keys [][]byte) (*Encrypter, error)

PreviousKeys sets the retired keys that decryption falls back to, and returns the encrypter so the call chains.

Every key is checked against the cipher first, and none is stored if any fails, so a bad list leaves the encrypter untouched. A key that is the wrong length would not be used at decryption time anyway; the point of refusing it here is that a typo in APP_PREVIOUS_KEYS should stop a deploy, not quietly stop old payloads from opening.

type Signer

type Signer struct {
	// contains filtered or unexported fields
}

Signer issues links that prove something without storing anything.

It is what an e-mail verification link is made of, and what an unsubscribe link should be made of. The alternative -- a table of tokens -- costs a write, a read, a cleanup job and a decision about what happens when the row is gone; this costs a signature, and a link that has expired says so rather than saying "unknown token" three months after the row was deleted.

Two properties are what make it safe to put in a URL:

  • The purpose is signed. A token issued to verify an address does not work on a password reset, even though both are signed with the same key. Reusing one for the other is the mistake this prevents, and it is not an unlikely one: both are "a link in an e-mail with an id in it".
  • The expiry is signed. Moving it is changing the payload, so a link that has run out cannot be extended by editing the URL.

What it deliberately does NOT do is make the link single-use. That needs state, and where single use matters -- a password reset -- the state is the password itself: the token carries the current hash, so using the link changes what it was signed against.

func NewSigner

func NewSigner(appKey []byte) *Signer

NewSigner returns a Signer over the application key.

The same key as the session and the CSRF token, because they are the same secret: an attacker who has it does not need three.

func (*Signer) Sign

func (s *Signer) Sign(purpose, payload string, ttl time.Duration) string

Sign returns a token carrying payload, valid for ttl, usable only for purpose.

The payload is not secret -- it is base64 in a URL, and anyone can read it. What the signature buys is that nobody can change it.

func (*Signer) Verify

func (s *Signer) Verify(purpose, token string) (string, error)

Verify checks a token and returns what was signed into it.

The order matters: the signature is checked before the expiry is read, because an unsigned expiry is a number the client chose.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL