1 // Package authenticate provides a simple interface to encrypt and authenticate a message. 2 package authenticate // import "vimagination.zapto.org/authenticate" 3 4 import ( 5 "crypto/aes" 6 "crypto/cipher" 7 "encoding/binary" 8 "errors" 9 "fmt" 10 "time" 11 ) 12 13 var timeNow = time.Now 14 15 const nonceSize = 12 16 17 // Codec represents an initialised encoder/decoder. 18 type Codec struct { 19 aead cipher.AEAD 20 maxAge time.Duration 21 } 22 23 // NewCodec takes the encryption key, which should be 16, 24 or 32 bytes long, 24 // and an optional duration to create a new Codec. 25 // 26 // The optional Duration is used to only allow messages to only be valid while 27 // it is younger than the given time. 28 func NewCodec(key []byte, maxAge time.Duration) (*Codec, error) { 29 if l := len(key); l != 16 && l != 24 && l != 32 { 30 return nil, ErrInvalidAES 31 } 32 33 a := make([]byte, len(key)) 34 35 copy(a, key) 36 37 block, _ := aes.NewCipher(a) 38 aead, _ := cipher.NewGCMWithNonceSize(block, nonceSize) 39 40 return &Codec{ 41 aead: aead, 42 maxAge: maxAge, 43 }, nil 44 } 45 46 // Encode takes a data slice and a destination buffer and returns the encrypted 47 // data. 48 // 49 // If the destination buffer is too small, or nil, it will be allocated accordingly. 50 func (c *Codec) Encode(data, dst []byte) []byte { 51 if cap(dst) < nonceSize { 52 dst = make([]byte, nonceSize, nonceSize+len(data)+c.aead.Overhead()) 53 } else { 54 dst = dst[:nonceSize] 55 } 56 57 t := timeNow() 58 59 binary.LittleEndian.PutUint64(dst, uint64(t.Nanosecond())) // last four bytes are overridden 60 binary.BigEndian.PutUint64(dst[4:], uint64(t.Unix())) 61 62 return c.aead.Seal(dst, dst, data, nil) 63 } 64 65 // Decode takes a cipher text slice and a destination buffer and returns the 66 // decrypted data or an error if the cipher text is invalid or expired. 67 // 68 // If the destination buffer is too small, or nil, it will be allocated accordingly. 69 func (c *Codec) Decode(cipherText, dst []byte) ([]byte, error) { 70 if len(cipherText) < nonceSize { 71 return nil, ErrInvalidData 72 } 73 74 timestamp := time.Unix(int64(binary.BigEndian.Uint64(cipherText[4:12])), 0) 75 76 if c.maxAge > 0 { 77 if t := timeNow().Sub(timestamp); t > c.maxAge || t < 0 { 78 return nil, ErrExpired 79 } 80 } 81 82 var err error 83 84 dst, err = c.aead.Open(dst, cipherText[:nonceSize], cipherText[nonceSize:], nil) 85 if err != nil { 86 return nil, fmt.Errorf("error opening cipher text: %w", err) 87 } 88 89 return dst, nil 90 } 91 92 // Overhead returns the maximum number of bytes that the cipher text will be 93 // longer than the plain text. 94 func (c *Codec) Overhead() int { 95 return c.aead.Overhead() + nonceSize 96 } 97 98 // Errors. 99 var ( 100 ErrInvalidAES = errors.New("invalid AES key, must be 16, 24 or 32 bytes") 101 ErrInvalidData = errors.New("invalid cipher text") 102 ErrExpired = errors.New("data expired") 103 ) 104