Skip to content

Commit 4bdfaf4

Browse files
committed
chacha20: don't panic encrypting the final blocks
Certain operations with counter values close to overflowing were causing an unnecessary panic, which was reachable due to the SetCounter API and actually observed in QUIC. Tests by lukechampine <[email protected]> from CL 220591. Fixes golang/go#37157 Relanding of CL 224279, which was broken on multi-block buffers. Reviewed-on: https://go-review.googlesource.com/c/crypto/+/224279 Run-TryBot: Filippo Valsorda <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Katie Hockman <[email protected]> Change-Id: Ia382c6f62ae49ffe257b67f7b794e8d7124d981e (cherry picked from commit 1c2c788) Reviewed-on: https://go-review.googlesource.com/c/crypto/+/229119 Run-TryBot: Filippo Valsorda <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Katie Hockman <[email protected]>
1 parent 18b771b commit 4bdfaf4

File tree

2 files changed

+100
-23
lines changed

2 files changed

+100
-23
lines changed

chacha20/chacha_generic.go

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,14 @@ type Cipher struct {
4242

4343
// The last len bytes of buf are leftover key stream bytes from the previous
4444
// XORKeyStream invocation. The size of buf depends on how many blocks are
45-
// computed at a time.
45+
// computed at a time by xorKeyStreamBlocks.
4646
buf [bufSize]byte
4747
len int
4848

49+
// overflow is set when the counter overflowed, no more blocks can be
50+
// generated, and the next XORKeyStream call should panic.
51+
overflow bool
52+
4953
// The counter-independent results of the first round are cached after they
5054
// are computed the first time.
5155
precompDone bool
@@ -140,15 +144,18 @@ func quarterRound(a, b, c, d uint32) (uint32, uint32, uint32, uint32) {
140144
// SetCounter sets the Cipher counter. The next invocation of XORKeyStream will
141145
// behave as if (64 * counter) bytes had been encrypted so far.
142146
//
143-
// To prevent accidental counter reuse, SetCounter panics if counter is
144-
// less than the current value.
147+
// To prevent accidental counter reuse, SetCounter panics if counter is less
148+
// than the current value.
149+
//
150+
// Note that the execution time of XORKeyStream is not independent of the
151+
// counter value.
145152
func (s *Cipher) SetCounter(counter uint32) {
146153
// Internally, s may buffer multiple blocks, which complicates this
147154
// implementation slightly. When checking whether the counter has rolled
148155
// back, we must use both s.counter and s.len to determine how many blocks
149156
// we have already output.
150157
outputCounter := s.counter - uint32(s.len)/blockSize
151-
if counter < outputCounter {
158+
if s.overflow || counter < outputCounter {
152159
panic("chacha20: SetCounter attempted to rollback counter")
153160
}
154161

@@ -197,34 +204,52 @@ func (s *Cipher) XORKeyStream(dst, src []byte) {
197204
dst[i] = src[i] ^ b
198205
}
199206
s.len -= len(keyStream)
200-
src = src[len(keyStream):]
201-
dst = dst[len(keyStream):]
207+
dst, src = dst[len(keyStream):], src[len(keyStream):]
208+
}
209+
if len(src) == 0 {
210+
return
202211
}
203212

204-
const blocksPerBuf = bufSize / blockSize
205-
numBufs := (uint64(len(src)) + bufSize - 1) / bufSize
206-
if uint64(s.counter)+numBufs*blocksPerBuf >= 1<<32 {
213+
// If we'd need to let the counter overflow and keep generating output,
214+
// panic immediately. If instead we'd only reach the last block, remember
215+
// not to generate any more output after the buffer is drained.
216+
numBlocks := (uint64(len(src)) + blockSize - 1) / blockSize
217+
if s.overflow || uint64(s.counter)+numBlocks > 1<<32 {
207218
panic("chacha20: counter overflow")
219+
} else if uint64(s.counter)+numBlocks == 1<<32 {
220+
s.overflow = true
208221
}
209222

210223
// xorKeyStreamBlocks implementations expect input lengths that are a
211224
// multiple of bufSize. Platform-specific ones process multiple blocks at a
212225
// time, so have bufSizes that are a multiple of blockSize.
213226

214-
rem := len(src) % bufSize
215-
full := len(src) - rem
216-
227+
full := len(src) - len(src)%bufSize
217228
if full > 0 {
218229
s.xorKeyStreamBlocks(dst[:full], src[:full])
219230
}
231+
dst, src = dst[full:], src[full:]
232+
233+
// If using a multi-block xorKeyStreamBlocks would overflow, use the generic
234+
// one that does one block at a time.
235+
const blocksPerBuf = bufSize / blockSize
236+
if uint64(s.counter)+blocksPerBuf > 1<<32 {
237+
s.buf = [bufSize]byte{}
238+
numBlocks := (len(src) + blockSize - 1) / blockSize
239+
buf := s.buf[bufSize-numBlocks*blockSize:]
240+
copy(buf, src)
241+
s.xorKeyStreamBlocksGeneric(buf, buf)
242+
s.len = len(buf) - copy(dst, buf)
243+
return
244+
}
220245

221246
// If we have a partial (multi-)block, pad it for xorKeyStreamBlocks, and
222247
// keep the leftover keystream for the next XORKeyStream invocation.
223-
if rem > 0 {
248+
if len(src) > 0 {
224249
s.buf = [bufSize]byte{}
225-
copy(s.buf[:], src[full:])
250+
copy(s.buf[:], src)
226251
s.xorKeyStreamBlocks(s.buf[:], s.buf[:])
227-
s.len = bufSize - copy(dst[full:], s.buf[:])
252+
s.len = bufSize - copy(dst, s.buf[:])
228253
}
229254
}
230255

@@ -308,9 +333,6 @@ func (s *Cipher) xorKeyStreamBlocksGeneric(dst, src []byte) {
308333
addXor(dst[60:64], src[60:64], x15, c15)
309334

310335
s.counter += 1
311-
if s.counter == 0 {
312-
panic("chacha20: internal error: counter overflow")
313-
}
314336

315337
src, dst = src[blockSize:], dst[blockSize:]
316338
}

chacha20/chacha_test.go

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -148,11 +148,66 @@ func TestSetCounter(t *testing.T) {
148148
if !panics(func() { s.SetCounter(0) }) {
149149
t.Error("counter decreasing should trigger a panic")
150150
}
151-
// advancing to ^uint32(0) and then calling XORKeyStream should cause a panic
152-
s = newCipher()
153-
s.SetCounter(^uint32(0))
154-
if !panics(func() { s.XORKeyStream([]byte{0}, []byte{0}) }) {
155-
t.Error("counter overflowing should trigger a panic")
151+
}
152+
153+
func TestLastBlock(t *testing.T) {
154+
panics := func(fn func()) (p bool) {
155+
defer func() { p = recover() != nil }()
156+
fn()
157+
return
158+
}
159+
160+
checkLastBlock := func(b []byte) {
161+
t.Helper()
162+
// Hardcoded result to check all implementations generate the same output.
163+
lastBlock := "ace4cd09e294d1912d4ad205d06f95d9c2f2bfcf453e8753f128765b62215f4d" +
164+
"92c74f2f626c6a640c0b1284d839ec81f1696281dafc3e684593937023b58b1d"
165+
if got := hex.EncodeToString(b); got != lastBlock {
166+
t.Errorf("wrong output for the last block, got %q, want %q", got, lastBlock)
167+
}
168+
}
169+
170+
// setting the counter to 0xffffffff and crypting multiple blocks should
171+
// trigger a panic
172+
s, _ := NewUnauthenticatedCipher(make([]byte, KeySize), make([]byte, NonceSize))
173+
s.SetCounter(0xffffffff)
174+
blocks := make([]byte, blockSize*2)
175+
if !panics(func() { s.XORKeyStream(blocks, blocks) }) {
176+
t.Error("crypting multiple blocks should trigger a panic")
177+
}
178+
179+
// setting the counter to 0xffffffff - 1 and crypting two blocks should not
180+
// trigger a panic
181+
s, _ = NewUnauthenticatedCipher(make([]byte, KeySize), make([]byte, NonceSize))
182+
s.SetCounter(0xffffffff - 1)
183+
if panics(func() { s.XORKeyStream(blocks, blocks) }) {
184+
t.Error("crypting the last blocks should not trigger a panic")
185+
}
186+
checkLastBlock(blocks[blockSize:])
187+
// once all the keystream is spent, setting the counter should panic
188+
if !panics(func() { s.SetCounter(0xffffffff) }) {
189+
t.Error("setting the counter after overflow should trigger a panic")
190+
}
191+
// crypting a subsequent block *should* panic
192+
block := make([]byte, blockSize)
193+
if !panics(func() { s.XORKeyStream(block, block) }) {
194+
t.Error("crypting after overflow should trigger a panic")
195+
}
196+
197+
// if we crypt less than a full block, we should be able to crypt the rest
198+
// in a subsequent call without panicking
199+
s, _ = NewUnauthenticatedCipher(make([]byte, KeySize), make([]byte, NonceSize))
200+
s.SetCounter(0xffffffff)
201+
if panics(func() { s.XORKeyStream(block[:7], block[:7]) }) {
202+
t.Error("crypting part of the last block should not trigger a panic")
203+
}
204+
if panics(func() { s.XORKeyStream(block[7:], block[7:]) }) {
205+
t.Error("crypting part of the last block should not trigger a panic")
206+
}
207+
checkLastBlock(block)
208+
// as before, a third call should trigger a panic because all keystream is spent
209+
if !panics(func() { s.XORKeyStream(block[:1], block[:1]) }) {
210+
t.Error("crypting after overflow should trigger a panic")
156211
}
157212
}
158213

0 commit comments

Comments
 (0)