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 }