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
- Variables
- func AppearsEncrypted(value string) bool
- func Decrypt[T any](e *Encrypter, payload string) (T, error)
- func Encrypt[T any](e *Encrypter, value T) (string, error)
- func GenerateKey() string
- func ParseKey(v string) ([]byte, error)
- func Supported(key []byte, cipher Cipher) bool
- type Cipher
- type Encrypter
- func (e *Encrypter) DecryptString(payload string) (string, error)
- func (e *Encrypter) EncryptString(value string) (string, error)
- func (e *Encrypter) GetAllKeys() [][]byte
- func (e *Encrypter) GetKey() []byte
- func (e *Encrypter) GetPreviousKeys() [][]byte
- func (e *Encrypter) PreviousKeys(keys [][]byte) (*Encrypter, error)
- type Signer
Constants ¶
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 ¶
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.
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.
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.
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.
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.
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 ¶
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 ¶
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 ¶
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 ¶
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.
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 ¶
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 ¶
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 ¶
DecryptString decrypts a string without deserialising it. It is the pair to Encrypter.EncryptString.
func (*Encrypter) EncryptString ¶
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 ¶
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 ¶
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 ¶
GetPreviousKeys returns the retired keys, without the current one.
func (*Encrypter) PreviousKeys ¶
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 ¶
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.