github.com/pion/dtls/v2@v2.2.12/pkg/crypto/ccm/ccm.go (about)

     1  // SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
     2  // SPDX-License-Identifier: MIT
     3  
     4  // Package ccm implements a CCM, Counter with CBC-MAC
     5  // as per RFC 3610.
     6  //
     7  // See https://tools.ietf.org/html/rfc3610
     8  //
     9  // This code was lifted from https://github.com/bocajim/dtls/blob/a3300364a283fcb490d28a93d7fcfa7ba437fbbe/ccm/ccm.go
    10  // and as such was not written by the Pions authors. Like Pions this
    11  // code is licensed under MIT.
    12  //
    13  // A request for including CCM into the Go standard library
    14  // can be found as issue #27484 on the https://github.com/golang/go/
    15  // repository.
    16  package ccm
    17  
    18  import (
    19  	"crypto/cipher"
    20  	"crypto/subtle"
    21  	"encoding/binary"
    22  	"errors"
    23  	"math"
    24  )
    25  
    26  // ccm represents a Counter with CBC-MAC with a specific key.
    27  type ccm struct {
    28  	b cipher.Block
    29  	M uint8
    30  	L uint8
    31  }
    32  
    33  const ccmBlockSize = 16
    34  
    35  // CCM is a block cipher in Counter with CBC-MAC mode.
    36  // Providing authenticated encryption with associated data via the cipher.AEAD interface.
    37  type CCM interface {
    38  	cipher.AEAD
    39  	// MaxLength returns the maxium length of plaintext in calls to Seal.
    40  	// The maximum length of ciphertext in calls to Open is MaxLength()+Overhead().
    41  	// The maximum length is related to CCM's `L` parameter (15-noncesize) and
    42  	// is 1<<(8*L) - 1 (but also limited by the maxium size of an int).
    43  	MaxLength() int
    44  }
    45  
    46  var (
    47  	errInvalidBlockSize = errors.New("ccm: NewCCM requires 128-bit block cipher")
    48  	errInvalidTagSize   = errors.New("ccm: tagsize must be 4, 6, 8, 10, 12, 14, or 16")
    49  	errInvalidNonceSize = errors.New("ccm: invalid nonce size")
    50  )
    51  
    52  // NewCCM returns the given 128-bit block cipher wrapped in CCM.
    53  // The tagsize must be an even integer between 4 and 16 inclusive
    54  // and is used as CCM's `M` parameter.
    55  // The noncesize must be an integer between 7 and 13 inclusive,
    56  // 15-noncesize is used as CCM's `L` parameter.
    57  func NewCCM(b cipher.Block, tagsize, noncesize int) (CCM, error) {
    58  	if b.BlockSize() != ccmBlockSize {
    59  		return nil, errInvalidBlockSize
    60  	}
    61  	if tagsize < 4 || tagsize > 16 || tagsize&1 != 0 {
    62  		return nil, errInvalidTagSize
    63  	}
    64  	lensize := 15 - noncesize
    65  	if lensize < 2 || lensize > 8 {
    66  		return nil, errInvalidNonceSize
    67  	}
    68  	c := &ccm{b: b, M: uint8(tagsize), L: uint8(lensize)}
    69  	return c, nil
    70  }
    71  
    72  func (c *ccm) NonceSize() int { return 15 - int(c.L) }
    73  func (c *ccm) Overhead() int  { return int(c.M) }
    74  func (c *ccm) MaxLength() int { return maxlen(c.L, c.Overhead()) }
    75  
    76  func maxlen(l uint8, tagsize int) int {
    77  	max := (uint64(1) << (8 * l)) - 1
    78  	if m64 := uint64(math.MaxInt64) - uint64(tagsize); l > 8 || max > m64 {
    79  		max = m64 // The maximum lentgh on a 64bit arch
    80  	}
    81  	if max != uint64(int(max)) {
    82  		return math.MaxInt32 - tagsize // We have only 32bit int's
    83  	}
    84  	return int(max)
    85  }
    86  
    87  // MaxNonceLength returns the maximum nonce length for a given plaintext length.
    88  // A return value <= 0 indicates that plaintext length is too large for
    89  // any nonce length.
    90  func MaxNonceLength(pdatalen int) int {
    91  	const tagsize = 16
    92  	for L := 2; L <= 8; L++ {
    93  		if maxlen(uint8(L), tagsize) >= pdatalen {
    94  			return 15 - L
    95  		}
    96  	}
    97  	return 0
    98  }
    99  
   100  func (c *ccm) cbcRound(mac, data []byte) {
   101  	for i := 0; i < ccmBlockSize; i++ {
   102  		mac[i] ^= data[i]
   103  	}
   104  	c.b.Encrypt(mac, mac)
   105  }
   106  
   107  func (c *ccm) cbcData(mac, data []byte) {
   108  	for len(data) >= ccmBlockSize {
   109  		c.cbcRound(mac, data[:ccmBlockSize])
   110  		data = data[ccmBlockSize:]
   111  	}
   112  	if len(data) > 0 {
   113  		var block [ccmBlockSize]byte
   114  		copy(block[:], data)
   115  		c.cbcRound(mac, block[:])
   116  	}
   117  }
   118  
   119  var errPlaintextTooLong = errors.New("ccm: plaintext too large")
   120  
   121  func (c *ccm) tag(nonce, plaintext, adata []byte) ([]byte, error) {
   122  	var mac [ccmBlockSize]byte
   123  
   124  	if len(adata) > 0 {
   125  		mac[0] |= 1 << 6
   126  	}
   127  	mac[0] |= (c.M - 2) << 2
   128  	mac[0] |= c.L - 1
   129  	if len(nonce) != c.NonceSize() {
   130  		return nil, errInvalidNonceSize
   131  	}
   132  	if len(plaintext) > c.MaxLength() {
   133  		return nil, errPlaintextTooLong
   134  	}
   135  	binary.BigEndian.PutUint64(mac[ccmBlockSize-8:], uint64(len(plaintext)))
   136  	copy(mac[1:ccmBlockSize-c.L], nonce)
   137  	c.b.Encrypt(mac[:], mac[:])
   138  
   139  	var block [ccmBlockSize]byte
   140  	if n := uint64(len(adata)); n > 0 {
   141  		// First adata block includes adata length
   142  		i := 2
   143  		if n <= 0xfeff {
   144  			binary.BigEndian.PutUint16(block[:i], uint16(n))
   145  		} else {
   146  			block[0] = 0xfe
   147  			block[1] = 0xff
   148  			if n < uint64(1<<32) {
   149  				i = 2 + 4
   150  				binary.BigEndian.PutUint32(block[2:i], uint32(n))
   151  			} else {
   152  				i = 2 + 8
   153  				binary.BigEndian.PutUint64(block[2:i], n)
   154  			}
   155  		}
   156  		i = copy(block[i:], adata)
   157  		c.cbcRound(mac[:], block[:])
   158  		c.cbcData(mac[:], adata[i:])
   159  	}
   160  
   161  	if len(plaintext) > 0 {
   162  		c.cbcData(mac[:], plaintext)
   163  	}
   164  
   165  	return mac[:c.M], nil
   166  }
   167  
   168  // sliceForAppend takes a slice and a requested number of bytes. It returns a
   169  // slice with the contents of the given slice followed by that many bytes and a
   170  // second slice that aliases into it and contains only the extra bytes. If the
   171  // original slice has sufficient capacity then no allocation is performed.
   172  // From crypto/cipher/gcm.go
   173  func sliceForAppend(in []byte, n int) (head, tail []byte) {
   174  	if total := len(in) + n; cap(in) >= total {
   175  		head = in[:total]
   176  	} else {
   177  		head = make([]byte, total)
   178  		copy(head, in)
   179  	}
   180  	tail = head[len(in):]
   181  	return
   182  }
   183  
   184  // Seal encrypts and authenticates plaintext, authenticates the
   185  // additional data and appends the result to dst, returning the updated
   186  // slice. The nonce must be NonceSize() bytes long and unique for all
   187  // time, for a given key.
   188  // The plaintext must be no longer than MaxLength() bytes long.
   189  //
   190  // The plaintext and dst may alias exactly or not at all.
   191  func (c *ccm) Seal(dst, nonce, plaintext, adata []byte) []byte {
   192  	tag, err := c.tag(nonce, plaintext, adata)
   193  	if err != nil {
   194  		// The cipher.AEAD interface doesn't allow for an error return.
   195  		panic(err) // nolint
   196  	}
   197  
   198  	var iv, s0 [ccmBlockSize]byte
   199  	iv[0] = c.L - 1
   200  	copy(iv[1:ccmBlockSize-c.L], nonce)
   201  	c.b.Encrypt(s0[:], iv[:])
   202  	for i := 0; i < int(c.M); i++ {
   203  		tag[i] ^= s0[i]
   204  	}
   205  	iv[len(iv)-1] |= 1
   206  	stream := cipher.NewCTR(c.b, iv[:])
   207  	ret, out := sliceForAppend(dst, len(plaintext)+int(c.M))
   208  	stream.XORKeyStream(out, plaintext)
   209  	copy(out[len(plaintext):], tag)
   210  	return ret
   211  }
   212  
   213  var (
   214  	errOpen               = errors.New("ccm: message authentication failed")
   215  	errCiphertextTooShort = errors.New("ccm: ciphertext too short")
   216  	errCiphertextTooLong  = errors.New("ccm: ciphertext too long")
   217  )
   218  
   219  func (c *ccm) Open(dst, nonce, ciphertext, adata []byte) ([]byte, error) {
   220  	if len(ciphertext) < int(c.M) {
   221  		return nil, errCiphertextTooShort
   222  	}
   223  	if len(ciphertext) > c.MaxLength()+c.Overhead() {
   224  		return nil, errCiphertextTooLong
   225  	}
   226  
   227  	tag := make([]byte, int(c.M))
   228  	copy(tag, ciphertext[len(ciphertext)-int(c.M):])
   229  	ciphertextWithoutTag := ciphertext[:len(ciphertext)-int(c.M)]
   230  
   231  	var iv, s0 [ccmBlockSize]byte
   232  	iv[0] = c.L - 1
   233  	copy(iv[1:ccmBlockSize-c.L], nonce)
   234  	c.b.Encrypt(s0[:], iv[:])
   235  	for i := 0; i < int(c.M); i++ {
   236  		tag[i] ^= s0[i]
   237  	}
   238  	iv[len(iv)-1] |= 1
   239  	stream := cipher.NewCTR(c.b, iv[:])
   240  
   241  	// Cannot decrypt directly to dst since we're not supposed to
   242  	// reveal the plaintext to the caller if authentication fails.
   243  	plaintext := make([]byte, len(ciphertextWithoutTag))
   244  	stream.XORKeyStream(plaintext, ciphertextWithoutTag)
   245  	expectedTag, err := c.tag(nonce, plaintext, adata)
   246  	if err != nil {
   247  		return nil, err
   248  	}
   249  
   250  	if subtle.ConstantTimeCompare(tag, expectedTag) != 1 {
   251  		return nil, errOpen
   252  	}
   253  	return append(dst, plaintext...), nil
   254  }