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 initilised 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 a := make([]byte, len(key)) 33 copy(a, key) 34 block, _ := aes.NewCipher(a) 35 aead, _ := cipher.NewGCMWithNonceSize(block, nonceSize) 36 return &Codec{ 37 aead: aead, 38 maxAge: maxAge, 39 }, nil 40 } 41 42 // Encode takes a data slice and a destination buffer and returns the encrypted 43 // data. 44 // 45 // If the destination buffer is too small, or nil, it will be allocated accordingly. 46 func (c *Codec) Encode(data, dst []byte) []byte { 47 if cap(dst) < nonceSize { 48 dst = make([]byte, nonceSize, nonceSize+len(data)+c.aead.Overhead()) 49 } else { 50 dst = dst[:nonceSize] 51 } 52 t := timeNow() 53 binary.LittleEndian.PutUint64(dst, uint64(t.Nanosecond())) // last four bytes are overriden 54 binary.BigEndian.PutUint64(dst[4:], uint64(t.Unix())) 55 56 return c.aead.Seal(dst, dst, data, nil) 57 } 58 59 // Decode takes a ciphertext slice and a destination buffer and returns the 60 // decrypted data or an error if the ciphertext is invalid or expired. 61 // 62 // If the destination buffer is too small, or nil, it will be allocated accordingly. 63 func (c *Codec) Decode(cipherText, dst []byte) ([]byte, error) { 64 if len(cipherText) < nonceSize { 65 return nil, ErrInvalidData 66 } 67 68 timestamp := time.Unix(int64(binary.BigEndian.Uint64(cipherText[4:12])), 0) 69 70 if c.maxAge > 0 { 71 if t := timeNow().Sub(timestamp); t > c.maxAge || t < 0 { 72 return nil, ErrExpired 73 } 74 } 75 76 var err error 77 dst, err = c.aead.Open(dst, cipherText[:nonceSize], cipherText[nonceSize:], nil) 78 79 if err != nil { 80 return nil, fmt.Errorf("error opening ciphertext: %w", err) 81 } 82 83 return dst, nil 84 } 85 86 // Overhead returns the maximum number of bytes that the ciphertext will be 87 // longer than the plain text 88 func (c *Codec) Overhead() int { 89 return c.aead.Overhead() + nonceSize 90 } 91 92 // Errors 93 var ( 94 ErrInvalidAES = errors.New("invalid AES key, must be 16, 24 or 32 bytes") 95 ErrInvalidData = errors.New("invalid cipher text") 96 ErrExpired = errors.New("data expired") 97 )