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  }