chacha20: add SetCounter method Fixes golang/go#35506 Change-Id: I5cfc6b4dc07ab368e370edaee11841c2c1377f82 GitHub-Last-Rev: 16147a1668a903532f2d3777b873ddad8f0f26f5 GitHub-Pull-Request: golang/crypto#108 Reviewed-on: https://go-review.googlesource.com/c/crypto/+/206638 Run-TryBot: Filippo Valsorda <filippo@golang.org> Reviewed-by: Filippo Valsorda <filippo@golang.org>
diff --git a/chacha20/chacha_generic.go b/chacha20/chacha_generic.go index 098ec9f..7c498e9 100644 --- a/chacha20/chacha_generic.go +++ b/chacha20/chacha_generic.go
@@ -136,6 +136,33 @@ return a, b, c, d } +// SetCounter sets the Cipher counter. The next invocation of XORKeyStream will +// behave as if (64 * counter) bytes had been encrypted so far. +// +// To prevent accidental counter reuse, SetCounter panics if counter is +// less than the current value. +func (s *Cipher) SetCounter(counter uint32) { + // Internally, s may buffer multiple blocks, which complicates this + // implementation slightly. When checking whether the counter has rolled + // back, we must use both s.counter and s.len to determine how many blocks + // we have already output. + outputCounter := s.counter - uint32(s.len)/blockSize + if counter < outputCounter { + panic("chacha20: SetCounter attempted to rollback counter") + } + + // In the general case, we set the new counter value and reset s.len to 0, + // causing the next call to XORKeyStream to refill the buffer. However, if + // we're advancing within the existing buffer, we can save work by simply + // setting s.len. + if counter < s.counter { + s.len = int(s.counter-counter) * blockSize + } else { + s.counter = counter + s.len = 0 + } +} + // XORKeyStream XORs each byte in the given slice with a byte from the // cipher's key stream. Dst and src must overlap entirely or not at all. //
diff --git a/chacha20/chacha_test.go b/chacha20/chacha_test.go index 033867b..554afbf 100644 --- a/chacha20/chacha_test.go +++ b/chacha20/chacha_test.go
@@ -110,6 +110,52 @@ } } +func TestSetCounter(t *testing.T) { + newCipher := func() *Cipher { + s, _ := NewUnauthenticatedCipher(make([]byte, KeySize), make([]byte, NonceSize)) + return s + } + s := newCipher() + src := bytes.Repeat([]byte("test"), 32) // two 64-byte blocks + dst1 := make([]byte, len(src)) + s.XORKeyStream(dst1, src) + // advance counter to 1 and xor second block + s = newCipher() + s.SetCounter(1) + dst2 := make([]byte, len(src)) + s.XORKeyStream(dst2[64:], src[64:]) + if !bytes.Equal(dst1[64:], dst2[64:]) { + t.Error("failed to produce identical output using SetCounter") + } + + // test again with unaligned blocks; SetCounter should reset the buffer + s = newCipher() + s.XORKeyStream(dst1[:70], src[:70]) + s = newCipher() + s.XORKeyStream([]byte{0}, []byte{0}) + s.SetCounter(1) + s.XORKeyStream(dst2[64:70], src[64:70]) + if !bytes.Equal(dst1[64:70], dst2[64:70]) { + t.Error("SetCounter did not reset buffer") + } + + // advancing to a lower counter value should cause a panic + panics := func(fn func()) (p bool) { + defer func() { p = recover() != nil }() + fn() + return + } + if !panics(func() { s.SetCounter(0) }) { + t.Error("counter decreasing should trigger a panic") + } + // advancing to ^uint32(0) and then calling XORKeyStream should cause a panic + s = newCipher() + s.SetCounter(^uint32(0)) + if !panics(func() { s.XORKeyStream([]byte{0}, []byte{0}) }) { + t.Error("counter overflowing should trigger a panic") + } +} + func benchmarkChaCha20(b *testing.B, step, count int) { tot := step * count src := make([]byte, tot)