169 lines
4.6 KiB
Go
169 lines
4.6 KiB
Go
// Package fernet takes a user-provided message (an arbitrary
|
|
// sequence of bytes), a key (256 bits), and the current time,
|
|
// and produces a token, which contains the message in a form
|
|
// that can't be read or altered without the key.
|
|
//
|
|
// For more information and background, see the Fernet spec
|
|
// at https://github.com/fernet/spec.
|
|
//
|
|
// Subdirectories in this package provide command-line tools
|
|
// for working with Fernet keys and tokens.
|
|
package fernet
|
|
|
|
import (
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/hmac"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"crypto/subtle"
|
|
"encoding/base64"
|
|
"encoding/binary"
|
|
"io"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
version byte = 0x80
|
|
tsOffset = 1
|
|
ivOffset = tsOffset + 8
|
|
payOffset = ivOffset + aes.BlockSize
|
|
overhead = 1 + 8 + aes.BlockSize + sha256.Size // ver + ts + iv + hmac
|
|
maxClockSkew = 60 * time.Second
|
|
)
|
|
|
|
var encoding = base64.URLEncoding
|
|
|
|
// generates a token from msg, writes it into tok, and returns the
|
|
// number of bytes generated, which is encodedLen(msg).
|
|
// len(tok) must be >= encodedLen(len(msg))
|
|
func gen(tok, msg, iv []byte, ts time.Time, k *Key) int {
|
|
tok[0] = version
|
|
binary.BigEndian.PutUint64(tok[tsOffset:], uint64(ts.Unix()))
|
|
copy(tok[ivOffset:], iv)
|
|
p := tok[payOffset:]
|
|
n := pad(p, msg, aes.BlockSize)
|
|
bc, _ := aes.NewCipher(k.cryptBytes())
|
|
cipher.NewCBCEncrypter(bc, iv).CryptBlocks(p[:n], p[:n])
|
|
genhmac(p[n:n], tok[:payOffset+n], k.signBytes())
|
|
return payOffset + n + sha256.Size
|
|
}
|
|
|
|
// token length for input msg of length n, not including base64
|
|
func encodedLen(n int) int {
|
|
const k = aes.BlockSize
|
|
return n/k*k + k + overhead
|
|
}
|
|
|
|
// max msg length for tok of length n, for binary token (no base64)
|
|
// upper bound; not exact
|
|
func decodedLen(n int) int {
|
|
return n - overhead
|
|
}
|
|
|
|
// if msg is nil, decrypts in place and returns a slice of tok.
|
|
func verify(msg, tok []byte, ttl time.Duration, now time.Time, k *Key) []byte {
|
|
if len(tok) < 1 || tok[0] != version {
|
|
return nil
|
|
}
|
|
ts := time.Unix(int64(binary.BigEndian.Uint64(tok[1:])), 0)
|
|
if ttl >= 0 && (now.After(ts.Add(ttl)) || ts.After(now.Add(maxClockSkew))) {
|
|
return nil
|
|
}
|
|
n := len(tok) - sha256.Size
|
|
var hmac [sha256.Size]byte
|
|
genhmac(hmac[:0], tok[:n], k.signBytes())
|
|
if subtle.ConstantTimeCompare(tok[n:], hmac[:]) != 1 {
|
|
return nil
|
|
}
|
|
pay := tok[payOffset : len(tok)-sha256.Size]
|
|
if len(pay)%aes.BlockSize != 0 {
|
|
return nil
|
|
}
|
|
if msg != nil {
|
|
copy(msg, pay)
|
|
pay = msg
|
|
}
|
|
bc, _ := aes.NewCipher(k.cryptBytes())
|
|
iv := tok[9:][:aes.BlockSize]
|
|
cipher.NewCBCDecrypter(bc, iv).CryptBlocks(pay, pay)
|
|
return unpad(pay)
|
|
}
|
|
|
|
// Pads p to a multiple of k using PKCS #7 standard block padding.
|
|
// See http://tools.ietf.org/html/rfc5652#section-6.3.
|
|
func pad(q, p []byte, k int) int {
|
|
n := len(p)/k*k + k
|
|
copy(q, p)
|
|
c := byte(n - len(p))
|
|
for i := len(p); i < n; i++ {
|
|
q[i] = c
|
|
}
|
|
return n
|
|
}
|
|
|
|
// Removes PKCS #7 standard block padding from p.
|
|
// See http://tools.ietf.org/html/rfc5652#section-6.3.
|
|
// This function is the inverse of pad.
|
|
// If the padding is not well-formed, unpad returns nil.
|
|
func unpad(p []byte) []byte {
|
|
c := p[len(p)-1]
|
|
for i := len(p) - int(c); i < len(p); i++ {
|
|
if i < 0 || p[i] != c {
|
|
return nil
|
|
}
|
|
}
|
|
return p[:len(p)-int(c)]
|
|
}
|
|
|
|
func b64enc(src []byte) []byte {
|
|
dst := make([]byte, encoding.EncodedLen(len(src)))
|
|
encoding.Encode(dst, src)
|
|
return dst
|
|
}
|
|
|
|
func b64dec(src []byte) []byte {
|
|
dst := make([]byte, encoding.DecodedLen(len(src)))
|
|
n, err := encoding.Decode(dst, src)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return dst[:n]
|
|
}
|
|
|
|
func genhmac(q, p, k []byte) {
|
|
h := hmac.New(sha256.New, k)
|
|
h.Write(p)
|
|
h.Sum(q)
|
|
}
|
|
|
|
// EncryptAndSign encrypts and signs msg with key k and returns the resulting
|
|
// fernet token. If msg contains text, the text should be encoded
|
|
// with UTF-8 to follow fernet convention.
|
|
func EncryptAndSign(msg []byte, k *Key) (tok []byte, err error) {
|
|
iv := make([]byte, aes.BlockSize)
|
|
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
|
return nil, err
|
|
}
|
|
b := make([]byte, encodedLen(len(msg)))
|
|
n := gen(b, msg, iv, time.Now(), k)
|
|
tok = make([]byte, encoding.EncodedLen(n))
|
|
encoding.Encode(tok, b[:n])
|
|
return tok, nil
|
|
}
|
|
|
|
// VerifyAndDecrypt verifies that tok is a valid fernet token that was signed
|
|
// with a key in k at most ttl time ago only if ttl is greater than zero.
|
|
// Returns the message contained in tok if tok is valid, otherwise nil.
|
|
func VerifyAndDecrypt(tok []byte, ttl time.Duration, k []*Key) (msg []byte) {
|
|
b := make([]byte, encoding.DecodedLen(len(tok)))
|
|
n, _ := encoding.Decode(b, tok)
|
|
for _, k1 := range k {
|
|
msg = verify(nil, b[:n], ttl, time.Now(), k1)
|
|
if msg != nil {
|
|
return msg
|
|
}
|
|
}
|
|
return nil
|
|
}
|