git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/crypto/chacha20blake3/chacha20_blake3.go (about)

     1  package chacha20blake3
     2  
     3  import (
     4  	"crypto/cipher"
     5  	"crypto/subtle"
     6  	"encoding/binary"
     7  	"errors"
     8  
     9  	"github.com/zeebo/blake3"
    10  
    11  	// "golang.org/x/crypto/chacha20"
    12  
    13  	"git.sr.ht/~pingoo/stdx/crypto/chacha20"
    14  )
    15  
    16  const (
    17  	KeySize   = 32
    18  	NonceSize = 8
    19  	TagSize   = 32
    20  )
    21  
    22  var (
    23  	ErrOpen           = errors.New("chacha20blake3: error decrypting ciphertext")
    24  	ErrBadKeyLength   = errors.New("chacha20blake3: bad key length for ChaCha20Blake3. 32 bytes required")
    25  	ErrBadNonceLength = errors.New("chacha20blake3: bad nonce length for ChaCha20Blake3. 8 bytes required")
    26  )
    27  
    28  type ChaCha20Blake3 struct {
    29  	key [KeySize]byte
    30  }
    31  
    32  // ensure that ChaCha20Blake3 implements `cipher.AEAD` interface at build time
    33  var _ cipher.AEAD = (*ChaCha20Blake3)(nil)
    34  
    35  func New(key []byte) (*ChaCha20Blake3, error) {
    36  	if len(key) != KeySize {
    37  		return nil, ErrBadKeyLength
    38  	}
    39  
    40  	var ret ChaCha20Blake3
    41  	copy(ret.key[:], key)
    42  
    43  	return &ret, nil
    44  }
    45  
    46  func (*ChaCha20Blake3) NonceSize() int {
    47  	return NonceSize
    48  }
    49  
    50  func (*ChaCha20Blake3) Overhead() int {
    51  	return TagSize
    52  }
    53  
    54  func (cipher *ChaCha20Blake3) Seal(dst, nonce, plaintext, additionalData []byte) []byte {
    55  	var authenticationKey [32]byte
    56  
    57  	if len(nonce) != NonceSize {
    58  		panic(ErrBadNonceLength)
    59  	}
    60  
    61  	ret, out := sliceForAppend(dst, len(plaintext)+TagSize)
    62  	ciphertext, tag := out[:len(plaintext)], out[len(plaintext):]
    63  
    64  	// first get a new ChaCha20 cipher instance
    65  	chacha20Cipher, _ := chacha20.New(cipher.key[:], nonce)
    66  
    67  	// then perform the KDF step to get the authentication key and increase the ChaCha20 counter
    68  	chacha20Cipher.XORKeyStream(authenticationKey[:], authenticationKey[:])
    69  	chacha20Cipher.SetCounter(1)
    70  
    71  	// then encrypt the plaintext
    72  	chacha20Cipher.XORKeyStream(ciphertext, plaintext)
    73  
    74  	// and finally MAC the AAD + ciphertext with the authentication key
    75  	macHasher, _ := blake3.NewKeyed(authenticationKey[:])
    76  	macHasher.Write(additionalData)
    77  	macHasher.Write(ciphertext)
    78  	writeUint64(macHasher, uint64(len(additionalData)))
    79  	writeUint64(macHasher, uint64(len(ciphertext)))
    80  	macHasher.Sum(tag[:0])
    81  
    82  	return ret
    83  }
    84  
    85  func (cipher *ChaCha20Blake3) Open(dst, nonce, ciphertext, additionalData []byte) ([]byte, error) {
    86  	var authenticationKey [32]byte
    87  
    88  	if len(nonce) != NonceSize {
    89  		panic(ErrBadNonceLength)
    90  	}
    91  
    92  	tag := ciphertext[len(ciphertext)-TagSize:]
    93  	ciphertext = ciphertext[:len(ciphertext)-TagSize]
    94  
    95  	chacha20Cipher, _ := chacha20.New(cipher.key[:], nonce)
    96  
    97  	chacha20Cipher.XORKeyStream(authenticationKey[:], authenticationKey[:])
    98  	chacha20Cipher.SetCounter(1)
    99  
   100  	var computedTag [TagSize]byte
   101  	macHasher, _ := blake3.NewKeyed(authenticationKey[:])
   102  	macHasher.Write(additionalData)
   103  	macHasher.Write(ciphertext)
   104  	writeUint64(macHasher, uint64(len(additionalData)))
   105  	writeUint64(macHasher, uint64(len(ciphertext)))
   106  	macHasher.Sum(computedTag[:0])
   107  
   108  	ret, plaintext := sliceForAppend(dst, len(ciphertext))
   109  
   110  	if subtle.ConstantTimeCompare(computedTag[:], tag) != 1 {
   111  		// for i := range plaintext {
   112  		// 	plaintext[i] = 0
   113  		// }
   114  		return nil, ErrOpen
   115  	}
   116  
   117  	chacha20Cipher.XORKeyStream(plaintext, ciphertext)
   118  
   119  	return ret, nil
   120  }
   121  
   122  // sliceForAppend takes a slice and a requested number of bytes. It returns a
   123  // slice with the contents of the given slice followed by that many bytes and a
   124  // second slice that aliases into it and contains only the extra bytes. If the
   125  // original slice has sufficient capacity then no allocation is performed.
   126  func sliceForAppend(in []byte, n int) (head, tail []byte) {
   127  	if total := len(in) + n; cap(in) >= total {
   128  		head = in[:total]
   129  	} else {
   130  		head = make([]byte, total)
   131  		copy(head, in)
   132  	}
   133  	tail = head[len(in):]
   134  	return
   135  }
   136  
   137  func writeUint64(p *blake3.Hasher, n uint64) {
   138  	var buf [8]byte
   139  	binary.LittleEndian.PutUint64(buf[:], n)
   140  	p.Write(buf[:])
   141  }