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.
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) EncryptString ¶
EncryptString encrypts s without additional data.
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. It panics on a nil Encrypter (no APP_KEY).
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>