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 allow messages to only be valid while it is 27 // younger than the given time. Set to 0 to disable expiration checking. 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, len(data)+c.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 // Sign takes a data slice and a destination buffer and returns the data with a 93 // signature appended 94 // 95 // If the destination buffer is too small, or nil, it will be allocated accordingly. 96 func (c *Codec) Sign(data, dst []byte) []byte { 97 if cap(dst) < len(data)+c.Overhead() { 98 dst = make([]byte, nonceSize, len(data)+c.Overhead()) 99 } else { 100 dst = dst[:len(data)+c.Overhead()] 101 } 102 103 var nonce [12]byte 104 105 _ = append(dst[:0], data...) 106 107 t := timeNow() 108 109 binary.LittleEndian.PutUint64(nonce[1:], uint64(t.Nanosecond())) // last five bytes are overridden 110 binary.BigEndian.PutUint64(nonce[4:], uint64(t.Unix())) 111 copy(dst[len(data):len(data)+nonceSize], nonce[1:]) 112 113 dst = dst[:len(c.aead.Seal(dst[:len(data)+nonceSize-1], nonce[:], nil, data))+1] 114 115 dst[len(dst)-1] = byte(len(dst) - len(data)) 116 117 return dst 118 } 119 120 // Verify takes data returned from the Sign method and returns the unsigned 121 // data, or and error if the signature is invalid or the optional exiration has 122 // been exceeded. 123 // 124 // If the destination buffer is too small, or nil, it will be allocated accordingly. 125 func (c *Codec) Verify(data []byte) ([]byte, error) { 126 if len(data) < nonceSize { 127 return nil, ErrInvalidData 128 } 129 130 var nonce [12]byte 131 132 sigLen := int(data[len(data)-1]) 133 plain := data[:len(data)-sigLen] 134 copy(nonce[1:], data[len(plain):]) 135 136 sig := data[len(plain)+nonceSize-1 : len(data)-1] 137 138 if c.maxAge > 0 { 139 if t := timeNow().Sub(time.Unix(int64(binary.BigEndian.Uint64(nonce[4:12])), 0)); t > c.maxAge || t < 0 { 140 return nil, ErrExpired 141 } 142 } 143 144 if _, err := c.aead.Open(nil, nonce[:], sig, plain); err != nil { 145 return nil, fmt.Errorf("error verifying signature: %w", err) 146 } 147 148 return plain, nil 149 } 150 151 // Overhead returns the maximum number of bytes that the cipher text will be 152 // longer than the plain text. 153 func (c *Codec) Overhead() int { 154 return c.aead.Overhead() + nonceSize 155 } 156 157 // Errors. 158 var ( 159 ErrInvalidAES = errors.New("invalid AES key, must be 16, 24 or 32 bytes") 160 ErrInvalidData = errors.New("invalid cipher text") 161 ErrExpired = errors.New("data expired") 162 ) 163