Documentation
¶
Overview ¶
Package crypt encrypts and authenticates values with AES-256-GCM and supports key rotation: one current key encrypts, previous keys only decrypt. Tokens are unpadded base64url strings, safe for cookies, URLs and database columns.
Token layout (before base64url):
version (1 byte) | key id (4 bytes) | nonce (12) | ciphertext | tag (16)
The key id lets Open pick the right key without trial decryption, and the header is authenticated together with the caller's additional data, so a token sealed for one purpose (a cookie name, a table column) cannot be replayed as another.
Never pass APP_KEY directly: derive a purpose key with app.Key.Derive("crypt", crypt.KeySize). Apps get a ready Encrypter in app.Crypt, built from APP_KEY and APP_PREVIOUS_KEYS.
Boot contract: an Encrypter is built at boot and a nil one means "no key". Open, Decrypt and Rotate return ErrNoKey on it, but Seal and Encrypt panic, since they have no error to return; production refuses to boot without APP_KEY, so only an app run without a key outside production reaches that panic. Code that may run without a key checks for nil first.
Pass a purpose-bound ad (for example "cookie:remember" or "users.ssn:42") at every call site: tokens sealed with a nil ad, which Encrypt and EncryptString use, open under any other nil-ad call, so a value encrypted for one purpose can be replayed for another.
See https://github.com/AlexGalhardo/vukem/blob/main/docs/subsystems.md#encryption.
Example ¶
package main
import (
"bytes"
"fmt"
"github.com/AlexGalhardo/vukem/crypt"
)
func main() {
// In an app, use app.Crypt (keys derived from APP_KEY and
// APP_PREVIOUS_KEYS) instead of literal keys.
oldKey := bytes.Repeat([]byte{1}, crypt.KeySize)
newKey := bytes.Repeat([]byte{2}, crypt.KeySize)
before, _ := crypt.New(oldKey)
token := before.EncryptString("4111 1111 1111 1111")
// After rotating APP_KEY, the old key stays as a previous key.
after, _ := crypt.New(newKey, oldKey)
plain, err := after.DecryptString(token)
fmt.Println(plain, err, after.NeedsRotation(token))
renewed, _ := after.Rotate(token, nil)
fmt.Println(after.NeedsRotation(renewed))
}
Output: 4111 1111 1111 1111 <nil> true false
Index ¶
- Constants
- Variables
- type Encrypter
- func (e *Encrypter) Cookie(r *http.Request, name string) (string, error)
- func (e *Encrypter) Decrypt(token string) ([]byte, error)
- func (e *Encrypter) DecryptString(token string) (string, error)
- func (e *Encrypter) Encrypt(plaintext []byte) string
- func (e *Encrypter) EncryptString(s string) string
- func (e *Encrypter) NeedsRotation(token string) bool
- func (e *Encrypter) Open(token string, ad []byte) ([]byte, error)
- func (e *Encrypter) Rotate(token string, ad []byte) (string, error)
- func (e *Encrypter) Seal(plaintext, ad []byte) string
- func (e *Encrypter) SetCookie(w http.ResponseWriter, r *http.Request, c http.Cookie) error
Examples ¶
Constants ¶
const KeySize = 32
KeySize is the required key length in bytes (AES-256).
Variables ¶
var ErrCookieTooLarge = errors.New("crypt: encrypted cookie value exceeds 4 KB")
ErrCookieTooLarge is returned by SetCookie when the encrypted value exceeds 4 KB; keep large data server-side.
var ErrInvalid = errors.New("crypt: invalid or tampered ciphertext")
ErrInvalid is returned for every token that cannot be opened: malformed, tampered, sealed with an unknown key or with different additional data. The causes are deliberately not distinguished.
var ErrNoKey = errors.New("crypt: no key: APP_KEY is not set (run `vukem key:generate`)")
ErrNoKey is returned when opening with a nil Encrypter: app.Crypt is nil when the app booted without APP_KEY (outside production).
Functions ¶
This section is empty.
Types ¶
type Encrypter ¶
type Encrypter struct {
// contains filtered or unexported fields
}
Encrypter seals and opens tokens. It is safe for concurrent use.
func New ¶
New returns an Encrypter that seals with current and also opens tokens sealed with any of previous (key rotation). Every key must be exactly KeySize bytes.
func (*Encrypter) Cookie ¶
Cookie returns the decrypted value of the request cookie name, set by SetCookie. It returns http.ErrNoCookie when the cookie is absent and ErrInvalid when it was tampered with, renamed or sealed with an unknown key.
func (*Encrypter) DecryptString ¶
DecryptString decrypts a token produced by EncryptString.
func (*Encrypter) Encrypt ¶
Encrypt is Seal without additional data; prefer Seal with a purpose-bound ad. It panics on a nil Encrypter.
func (*Encrypter) EncryptString ¶
EncryptString encrypts s without additional data; prefer Seal with a purpose-bound ad. It panics on a nil Encrypter.
func (*Encrypter) NeedsRotation ¶
NeedsRotation reports whether token is well formed but was sealed with a key other than the current one; re-seal it with Rotate. It does not authenticate the token.
func (*Encrypter) Open ¶
Open decrypts a token produced by Seal with the same ad, using the current or any previous key. Every failure returns ErrInvalid, or ErrNoKey on a nil Encrypter.
func (*Encrypter) Rotate ¶
Rotate opens token with ad and seals the plaintext again with the current key.
func (*Encrypter) Seal ¶
Seal encrypts plaintext with the current key, authenticating ad (additional data, not stored in the token) alongside it. Open must be given the same ad; bind ad to the purpose (see the package doc). It panics on a nil Encrypter (no APP_KEY), unlike Open, which returns ErrNoKey: check for nil where the key may be missing.
Example ¶
package main
import (
"bytes"
"fmt"
"github.com/AlexGalhardo/vukem/crypt"
)
func main() {
enc, _ := crypt.New(bytes.Repeat([]byte{7}, crypt.KeySize))
// The additional data binds the token to its purpose.
token := enc.Seal([]byte("42"), []byte("invoice:total"))
_, err := enc.Open(token, []byte("invoice:discount"))
fmt.Println(err)
plain, _ := enc.Open(token, []byte("invoice:total"))
fmt.Println(string(plain))
}
Output: crypt: invalid or tampered ciphertext 42
func (*Encrypter) SetCookie ¶
SetCookie encrypts c.Value with the cookie name as additional data and adds the Set-Cookie header. Defaults are secure: HttpOnly is always set, SameSite defaults to Lax, and Secure is set when r arrived over TLS (set c.Secure yourself behind a TLS-terminating proxy). Nothing is written on error; a nil Encrypter (no APP_KEY) returns ErrNoKey.
Example ¶
package main
import (
"bytes"
"fmt"
"net/http"
"net/http/httptest"
"github.com/AlexGalhardo/vukem/crypt"
)
func main() {
enc, _ := crypt.New(bytes.Repeat([]byte{9}, crypt.KeySize))
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
_ = enc.SetCookie(w, r, http.Cookie{Name: "cart", Value: "3 items", Path: "/"})
next := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
for _, c := range w.Result().Cookies() {
next.AddCookie(c)
}
value, err := enc.Cookie(next, "cart")
fmt.Println(value, err)
}
Output: 3 items <nil>