Skip to content

Commit 1c2c788

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 <luke.champine@gmail.com> from CL 220591. Fixes golang/go#37157 Change-Id: Iba52edb1ba36af391b8fe4ee615c5c41d7e64f48 Reviewed-on: https://go-review.googlesource.com/c/crypto/+/224279 Run-TryBot: Filippo Valsorda <filippo@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Katie Hockman <katie@golang.org>
1 parent 884d27f commit 1c2c788

File tree

2 files changed

+88
-23
lines changed

2 files changed

+88
-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
@@ -139,15 +143,18 @@ func quarterRound(a, b, c, d uint32) (uint32, uint32, uint32, uint32) {
139143
// SetCounter sets the Cipher counter. The next invocation of XORKeyStream will
140144
// behave as if (64 * counter) bytes had been encrypted so far.
141145
//
142-
// To prevent accidental counter reuse, SetCounter panics if counter is
143-
// less than the current value.
146+
// To prevent accidental counter reuse, SetCounter panics if counter is less
147+
// than the current value.
148+
//
149+
// Note that the execution time of XORKeyStream is not independent of the
150+
// counter value.
144151
func (s *Cipher) SetCounter(counter uint32) {
145152
// Internally, s may buffer multiple blocks, which complicates this
146153
// implementation slightly. When checking whether the counter has rolled
147154
// back, we must use both s.counter and s.len to determine how many blocks
148155
// we have already output.
149156
outputCounter := s.counter - uint32(s.len)/blockSize
150-
if counter < outputCounter {
157+
if s.overflow || counter < outputCounter {
151158
panic("chacha20: SetCounter attempted to rollback counter")
152159
}
153160

@@ -196,34 +203,52 @@ func (s *Cipher) XORKeyStream(dst, src []byte) {
196203
dst[i] = src[i] ^ b
197204
}
198205
s.len -= len(keyStream)
199-
src = src[len(keyStream):]
200-
dst = dst[len(keyStream):]
206+
dst, src = dst[len(keyStream):], src[len(keyStream):]
207+
}
208+
if len(src) == 0 {
209+
return
201210
}
202211

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

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

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

220245
// If we have a partial (multi-)block, pad it for xorKeyStreamBlocks, and
221246
// keep the leftover keystream for the next XORKeyStream invocation.
222-
if rem > 0 {
247+
if len(src) > 0 {
223248
s.buf = [bufSize]byte{}
224-
copy(s.buf[:], src[full:])
249+
copy(s.buf[:], src)
225250
s.xorKeyStreamBlocks(s.buf[:], s.buf[:])
226-
s.len = bufSize - copy(dst[full:], s.buf[:])
251+
s.len = bufSize - copy(dst, s.buf[:])
227252
}
228253
}
229254

@@ -304,9 +329,6 @@ func (s *Cipher) xorKeyStreamBlocksGeneric(dst, src []byte) {
304329
x15 += c15
305330

306331
s.counter += 1
307-
if s.counter == 0 {
308-
panic("chacha20: internal error: counter overflow")
309-
}
310332

311333
in, out := src[i:], dst[i:]
312334
in, out = in[:blockSize], out[:blockSize] // bounds check elimination hint

chacha20/chacha_test.go

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -148,11 +148,54 @@ 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+
// setting the counter to 0xffffffff and crypting multiple blocks should
161+
// trigger a panic
162+
s, _ := NewUnauthenticatedCipher(make([]byte, KeySize), make([]byte, NonceSize))
163+
s.SetCounter(0xffffffff)
164+
blocks := make([]byte, blockSize*2)
165+
if !panics(func() { s.XORKeyStream(blocks, blocks) }) {
166+
t.Error("crypting multiple blocks should trigger a panic")
167+
}
168+
169+
// setting the counter to 0xffffffff - 1 and crypting two blocks should not
170+
// trigger a panic
171+
s, _ = NewUnauthenticatedCipher(make([]byte, KeySize), make([]byte, NonceSize))
172+
s.SetCounter(0xffffffff - 1)
173+
if panics(func() { s.XORKeyStream(blocks, blocks) }) {
174+
t.Error("crypting the last blocks should not trigger a panic")
175+
}
176+
// once all the keystream is spent, setting the counter should panic
177+
if !panics(func() { s.SetCounter(0xffffffff) }) {
178+
t.Error("setting the counter after overflow should trigger a panic")
179+
}
180+
// crypting a subsequent block *should* panic
181+
block := make([]byte, blockSize)
182+
if !panics(func() { s.XORKeyStream(block, block) }) {
183+
t.Error("crypting after overflow should trigger a panic")
184+
}
185+
186+
// if we crypt less than a full block, we should be able to crypt the rest
187+
// in a subsequent call without panicking
188+
s, _ = NewUnauthenticatedCipher(make([]byte, KeySize), make([]byte, NonceSize))
189+
s.SetCounter(0xffffffff)
190+
if panics(func() { s.XORKeyStream(block[7:], block[7:]) }) {
191+
t.Error("crypting part of the last block should not trigger a panic")
192+
}
193+
if panics(func() { s.XORKeyStream(block[:7], block[:7]) }) {
194+
t.Error("crypting part of the last block should not trigger a panic")
195+
}
196+
// as before, a third call should trigger a panic because all keystream is spent
197+
if !panics(func() { s.XORKeyStream(block[:1], block[:1]) }) {
198+
t.Error("crypting after overflow should trigger a panic")
156199
}
157200
}
158201

0 commit comments

Comments
 (0)