github.com/onflow/flow-go/crypto@v0.24.8/random/chacha20.go (about)

     1  package random
     2  
     3  import (
     4  	"encoding/binary"
     5  	"fmt"
     6  
     7  	"golang.org/x/crypto/chacha20"
     8  )
     9  
    10  // We use Chacha20, to build a cryptographically secure random number generator
    11  // that uses the ChaCha algorithm.
    12  //
    13  // ChaCha is a stream cipher designed by Daniel J. Bernstein[^1], that we use as a PRG. It is
    14  // an improved variant of the Salsa20 cipher family.
    15  //
    16  // We use Chacha20 with a 256-bit key, a 192-bit stream identifier and a 32-bit counter as
    17  // as specified in RFC 8439 [^2].
    18  // The encryption key is used as the PRG seed while the stream identifier is used as a nonce
    19  // to customize the PRG. The PRG outputs are the successive encryptions of a constant message.
    20  //
    21  // A 32-bit counter over 64-byte blocks allows 256 GiB of output before cycling,
    22  // and the stream identifier allows 2^192 unique streams of output per seed.
    23  // It is the caller's responsibility to avoid the PRG output cycling.
    24  //
    25  // [^1]: D. J. Bernstein, [*ChaCha, a variant of Salsa20*](
    26  //       https://cr.yp.to/chacha.html)
    27  //
    28  // [^2]: [RFC 8439: ChaCha20 and Poly1305 for IETF Protocols](
    29  //       https://datatracker.ietf.org/doc/html/rfc8439)
    30  
    31  // The PRG core, implements the randCore interface
    32  type chachaCore struct {
    33  	cipher chacha20.Cipher
    34  
    35  	// empty message added to minimize allocations and buffer clearing
    36  	emptyMessage [lenEmptyMessage]byte
    37  
    38  	// Only used for State/Restore functionality
    39  
    40  	// Counter of bytes encrypted so far by the sream cipher.
    41  	// Note this is different than the internal 32-bits counter of the chacha state
    42  	// that counts the encrypted blocks of 512 bits.
    43  	bytesCounter uint64
    44  	// initial seed
    45  	seed [keySize]byte
    46  	// initial customizer
    47  	customizer [nonceSize]byte
    48  }
    49  
    50  // The main PRG, implements the Rand interface
    51  type chachaPRG struct {
    52  	genericPRG
    53  	core *chachaCore
    54  }
    55  
    56  const (
    57  	keySize   = chacha20.KeySize
    58  	nonceSize = chacha20.NonceSize
    59  
    60  	// Chacha20SeedLen is the seed length of the Chacha based PRG, it is fixed to 32 bytes.
    61  	Chacha20SeedLen = keySize
    62  	// Chacha20CustomizerMaxLen is the maximum length of the nonce used as a PRG customizer, it is fixed to 24 bytes.
    63  	// Shorter customizers are padded by zeros to 24 bytes.
    64  	Chacha20CustomizerMaxLen = nonceSize
    65  )
    66  
    67  // NewChacha20PRG returns a new Chacha20-based PRG, seeded with
    68  // the input seed (32 bytes) and a customizer (up to 12 bytes).
    69  //
    70  // It is recommended to sample the seed uniformly at random.
    71  // The function errors if the seed is different than 32 bytes,
    72  // or if the customizer is larger than 12 bytes.
    73  // Shorter customizers than 12 bytes are padded by zero bytes.
    74  func NewChacha20PRG(seed []byte, customizer []byte) (*chachaPRG, error) {
    75  
    76  	// check the key size
    77  	if len(seed) != Chacha20SeedLen {
    78  		return nil, fmt.Errorf("chacha20 seed length should be %d, got %d", Chacha20SeedLen, len(seed))
    79  	}
    80  
    81  	// check the nonce size
    82  	if len(customizer) > Chacha20CustomizerMaxLen {
    83  		return nil, fmt.Errorf("chacha20 streamID should be less than %d bytes", Chacha20CustomizerMaxLen)
    84  	}
    85  
    86  	// init the state core
    87  	var core chachaCore
    88  	// core.bytesCounter is set to 0
    89  	copy(core.seed[:], seed)
    90  	copy(core.customizer[:], customizer) // pad the customizer with zero bytes when it's short
    91  
    92  	// create the Chacha20 state, initialized with the seed as a key, and the customizer as a streamID.
    93  	chacha, err := chacha20.NewUnauthenticatedCipher(core.seed[:], core.customizer[:])
    94  	if err != nil {
    95  		return nil, fmt.Errorf("chacha20 instance creation failed: %w", err)
    96  	}
    97  	core.cipher = *chacha
    98  
    99  	prg := &chachaPRG{
   100  		genericPRG: genericPRG{
   101  			randCore: &core,
   102  		},
   103  		core: &core,
   104  	}
   105  	return prg, nil
   106  }
   107  
   108  const lenEmptyMessage = 64
   109  
   110  // Read pulls random bytes from the pseudo-random source.
   111  // The randoms are copied into the input buffer, the number of bytes read
   112  // is equal to the buffer input length.
   113  //
   114  // The stream cipher encrypts a stream of a constant message (empty for simplicity).
   115  func (c *chachaCore) Read(buffer []byte) {
   116  	// message to encrypt
   117  	var message []byte
   118  
   119  	if len(buffer) <= lenEmptyMessage {
   120  		// use a constant message (used for most of the calls)
   121  		message = c.emptyMessage[:len(buffer)]
   122  	} else {
   123  		// when buffer is large, use is as the message to encrypt,
   124  		// but this requires clearing it first.
   125  		for i := 0; i < len(buffer); i++ {
   126  			buffer[i] = 0
   127  		}
   128  		message = buffer
   129  	}
   130  	c.cipher.XORKeyStream(buffer, message)
   131  	// increase the counter
   132  	c.bytesCounter += uint64(len(buffer))
   133  }
   134  
   135  // counter is stored over 8 bytes
   136  const counterBytesLen = 8
   137  
   138  // Store returns the internal state of the concatenated Chacha20s
   139  // This is used for serialization/deserialization purposes.
   140  func (c *chachaPRG) Store() []byte {
   141  	bytes := make([]byte, 0, keySize+nonceSize+counterBytesLen)
   142  	counter := make([]byte, counterBytesLen)
   143  	binary.LittleEndian.PutUint64(counter, c.core.bytesCounter)
   144  	// output is seed || streamID || counter
   145  	bytes = append(bytes, c.core.seed[:]...)
   146  	bytes = append(bytes, c.core.customizer[:]...)
   147  	bytes = append(bytes, counter...)
   148  	return bytes
   149  }
   150  
   151  // RestoreChacha20PRG creates a chacha20 base PRG based on a previously stored state.
   152  // The created PRG is restored at the same state where the previous PRG was stored.
   153  func RestoreChacha20PRG(stateBytes []byte) (*chachaPRG, error) {
   154  	// input should be seed (32 bytes) || streamID (12 bytes) || bytesCounter (8 bytes)
   155  	const expectedLen = keySize + nonceSize + counterBytesLen
   156  
   157  	// check input length
   158  	if len(stateBytes) != expectedLen {
   159  		return nil, fmt.Errorf("Rand state length should be of %d bytes, got %d", expectedLen, len(stateBytes))
   160  	}
   161  
   162  	seed := stateBytes[:keySize]
   163  	streamID := stateBytes[keySize : keySize+nonceSize]
   164  	bytesCounter := binary.LittleEndian.Uint64(stateBytes[keySize+nonceSize:])
   165  
   166  	// create the Chacha20 instance with seed and streamID
   167  	chacha, err := chacha20.NewUnauthenticatedCipher(seed, streamID)
   168  	if err != nil {
   169  		return nil, fmt.Errorf("Chacha20 instance creation failed: %w", err)
   170  	}
   171  	// set the block counter, each chacha internal block is 512 bits
   172  	const bytesPerBlock = 512 >> 3
   173  	blockCount := uint32(bytesCounter / bytesPerBlock)
   174  	remainingBytes := bytesCounter % bytesPerBlock
   175  	chacha.SetCounter(blockCount)
   176  	// query the remaining bytes and to catch the stored chacha state
   177  	remainderStream := make([]byte, remainingBytes)
   178  	chacha.XORKeyStream(remainderStream, remainderStream)
   179  
   180  	core := &chachaCore{
   181  		cipher:       *chacha,
   182  		bytesCounter: bytesCounter,
   183  	}
   184  	copy(core.seed[:], seed)
   185  	copy(core.customizer[:], streamID)
   186  
   187  	prg := &chachaPRG{
   188  		genericPRG: genericPRG{
   189  			randCore: core,
   190  		},
   191  		core: core,
   192  	}
   193  	return prg, nil
   194  }