github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/pkg/crypto/mac.go (about) 1 package crypto 2 3 import ( 4 "crypto/hmac" 5 "crypto/sha256" 6 "encoding/base64" 7 "encoding/binary" 8 "errors" 9 "time" 10 ) 11 12 var ( 13 errMACExpired = errors.New("mac: expired") 14 errMACInvalid = errors.New("mac: the value is not valid") 15 ) 16 17 const ( 18 macLen = 32 // sha256 hash size 19 timeLen = 8 // int64 for unix timestamp in seconds 20 ) 21 22 // MACConfig contains all the options to encode or decode a message along with 23 // a proof of integrity and authenticity. 24 // 25 // Key is the secret used for the HMAC key. It should contain at least 16 bytes 26 // and should be generated by a PRNG. 27 // 28 // Name is an optional message name that won't be contained in the MACed 29 // messaged itself but will be MACed against. 30 type MACConfig struct { 31 Name string 32 MaxAge time.Duration 33 MaxLen int 34 } 35 36 // EncodeAuthMessage associates the given value with a message authentication 37 // code for integrity and authenticity. 38 // 39 // If the value, when base64 encoded with a fixed size header is longer than 40 // the configured maximum length, it will panic. 41 // 42 // Message format (name prefix is in MAC but removed from message): 43 // 44 // <---------------- MAC input ----------------> 45 // <---------- Message ----------> 46 // | name | additional data | time | value | hmac | 47 // | ---- | --- | 8 bytes | --- | 32 bytes | 48 func EncodeAuthMessage(c MACConfig, key, value, additionalData []byte) ([]byte, error) { 49 // Create message with MAC 50 preludeLen := len(c.Name) + len(additionalData) 51 messageAndMACLen := timeLen + len(value) + macLen 52 totalCap := preludeLen + messageAndMACLen 53 54 buf := make([]byte, 0, totalCap) 55 56 // Append name and additional data if any 57 if len(c.Name) > 0 { 58 buf = append(buf, c.Name...) 59 } 60 if len(additionalData) > 0 { 61 buf = append(buf, additionalData...) 62 } 63 64 // Append timestamp. 65 // Increase the len of the buffer of 8 bytes; its capacity allows it. 66 buf = buf[:preludeLen+timeLen] 67 binary.BigEndian.PutUint64(buf[preludeLen:], uint64(Timestamp())) 68 69 // Append value if any 70 if len(value) > 0 { 71 buf = append(buf, value...) 72 } 73 74 // Append MAC signature 75 buf = append(buf, createMAC(key, buf)...) 76 77 // Skip name and additional data prelude 78 buf = buf[preludeLen:] 79 80 // Check length 81 if c.MaxLen > 0 { 82 if base64.RawURLEncoding.EncodedLen(len(buf)) > c.MaxLen { 83 panic("the value is too long") 84 } 85 } 86 87 // Encode to base64 88 return Base64Encode(buf), nil 89 } 90 91 // DecodeAuthMessage verifies a message authentified with message 92 // authentication code and returns the message value algon with the issued time 93 // of the message. 94 func DecodeAuthMessage(c MACConfig, key, enc, additionalData []byte) ([]byte, error) { 95 // Check length 96 if c.MaxLen > 0 { 97 if len(enc) > c.MaxLen { 98 return nil, errMACInvalid 99 } 100 } 101 102 preludeLen := len(c.Name) + len(additionalData) 103 decCap := base64.RawURLEncoding.DecodedLen(len(enc)) 104 totalCap := decCap + preludeLen 105 106 // decCap is the maximum size of the decoded value. The real decoded size may 107 // be inferior. This is an early check only. 108 if decCap < macLen+timeLen { 109 return nil, errMACInvalid 110 } 111 112 buf := make([]byte, 0, totalCap) 113 114 // Prepend name and additional data if any 115 if len(c.Name) > 0 { 116 buf = append(buf, c.Name...) 117 } 118 if len(additionalData) > 0 { 119 buf = append(buf, additionalData...) 120 } 121 122 // Decode the base64-encoded MAC 123 { 124 // Increase len of buffer to write the decoded value, its capacity allows 125 // it, using the maximum buffer size calculated. 126 buf = buf[:preludeLen+decCap] 127 decLen, err := base64.RawURLEncoding.Decode(buf[preludeLen:], enc) 128 if err != nil { 129 return nil, errMACInvalid 130 } 131 // We re-check the len of the buffer, that is already checked against the 132 // maximum size of the decoded value `decCap`. 133 if decLen < macLen+timeLen { 134 return nil, errMACInvalid 135 } 136 // Shrink the buffer back to the exact size of the base64 decoded value. 137 buf = buf[:preludeLen+decLen] 138 } 139 140 // Verify message with MAC 141 { 142 mac := buf[len(buf)-macLen:] 143 buf = buf[:len(buf)-macLen] 144 if !verifyMAC(key, buf, mac) { 145 return nil, errMACInvalid 146 } 147 } 148 149 // Skip hidden prefix 150 buf = buf[preludeLen:] 151 152 // Read time and verify time ranges 153 var timeBuf []byte 154 timeBuf, buf = buf[:timeLen], buf[timeLen:] 155 if c.MaxAge != 0 { 156 t := time.Unix(int64(binary.BigEndian.Uint64(timeBuf)), 0) 157 if t.Add(c.MaxAge).Before(time.Now()) { 158 return nil, errMACExpired 159 } 160 } 161 162 // Returns the value 163 return buf, nil 164 } 165 166 // createMAC creates a MAC with HMAC-SHA256 167 func createMAC(key, value []byte) []byte { 168 mac := hmac.New(sha256.New, key) 169 _, _ = mac.Write(value) 170 return mac.Sum(nil) 171 } 172 173 // verifyMAC returns true is the MAC is valid 174 func verifyMAC(key, value []byte, mac []byte) bool { 175 expectedMAC := createMAC(key, value) 176 return hmac.Equal(mac, expectedMAC) 177 }