SIMD implementations of ChaCha20 (such as CL 35842) interleave block computations in order to achieve high performance. This means that they produce more than 64 bytes of output at a time. Unfortunately when encrypting small amounts of data (such as Poly1305 keys) the current interface to ChaCha20 forces the additional encrypted blocks of output to be discarded and recomputed later since it does not maintain any state. This additional overhead slows down the encryption of small amounts of data when using such optimized code. This CL makes the generic ChaCha20 implementation stateful, caching key, nonce and counter values and buffering any unused key stream bytes. ChaCha20 now also implements the high level cipher.Stream interface which makes the API more consistent with other stream ciphers in the standard library's crypto package. This will make it easier to add high performance SIMD implementations in the future. In addition to modifying the API I have also added some optimizations to improve the performance of the generic implementation. Note that the performance will improve further on amd64 with Go 1.11 due to CL 95475 (binary.LittleEndian.PutUint32 optimization). These benchmarks are based on Go 1.10.1. name old speed new speed delta ChaCha20/32 174MB/s ± 2% 174MB/s ± 1% ~ (p=0.796 n=10+10) ChaCha20/63 309MB/s ± 1% 337MB/s ± 2% +9.32% (p=0.000 n=10+9) ChaCha20/64 299MB/s ± 2% 350MB/s ± 1% +17.12% (p=0.000 n=9+8) ChaCha20/256 297MB/s ± 2% 390MB/s ± 1% +31.40% (p=0.000 n=10+10) ChaCha20/1024 300MB/s ± 0% 400MB/s ± 3% +33.38% (p=0.000 n=7+10) ChaCha20/1350 290MB/s ± 1% 386MB/s ± 2% +33.10% (p=0.000 n=9+10) ChaCha20/65536 301MB/s ± 1% 416MB/s ± 2% +38.25% (p=0.000 n=9+10) ChaCha20-Poly1305 (AEAD optimizations manually disabled): name old speed new speed delta Chacha20Poly1305Open_64 122MB/s ± 7% 131MB/s ± 2% +7.23% (p=0.000 n=18+18) Chacha20Poly1305Seal_64 125MB/s ± 4% 137MB/s ± 2% +9.88% (p=0.000 n=20+19) Chacha20Poly1305Open_1350 244MB/s ± 4% 305MB/s ± 3% +25.04% (p=0.000 n=20+19) Chacha20Poly1305Seal_1350 242MB/s ± 3% 309MB/s ± 2% +27.56% (p=0.000 n=20+19) Chacha20Poly1305Open_8K 260MB/s ± 7% 338MB/s ± 3% +29.96% (p=0.000 n=20+19) Chacha20Poly1305Seal_8K 262MB/s ± 5% 335MB/s ± 4% +27.80% (p=0.000 n=20+19) No change in allocations for either set of benchmarks. Change-Id: I28ca7947904e9d79debe2d5aac6623526fe5e595 Reviewed-on: https://go-review.googlesource.com/104856 Run-TryBot: Michael Munday <mike.munday@ibm.com> Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
189 lines
5.0 KiB
Go
189 lines
5.0 KiB
Go
// Copyright 2016 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package chacha20
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"fmt"
|
|
"math/rand"
|
|
"testing"
|
|
)
|
|
|
|
func TestCore(t *testing.T) {
|
|
// This is just a smoke test that checks the example from
|
|
// https://tools.ietf.org/html/rfc7539#section-2.3.2. The
|
|
// chacha20poly1305 package contains much more extensive tests of this
|
|
// code.
|
|
var key [32]byte
|
|
for i := range key {
|
|
key[i] = byte(i)
|
|
}
|
|
|
|
var input [16]byte
|
|
input[0] = 1
|
|
input[7] = 9
|
|
input[11] = 0x4a
|
|
|
|
var out [64]byte
|
|
XORKeyStream(out[:], out[:], &input, &key)
|
|
const expected = "10f1e7e4d13b5915500fdd1fa32071c4c7d1f4c733c068030422aa9ac3d46c4ed2826446079faa0914c2d705d98b02a2b5129cd1de164eb9cbd083e8a2503c4e"
|
|
if result := hex.EncodeToString(out[:]); result != expected {
|
|
t.Errorf("wanted %x but got %x", expected, result)
|
|
}
|
|
}
|
|
|
|
// Run the test cases with the input and output in different buffers.
|
|
func TestNoOverlap(t *testing.T) {
|
|
for _, c := range testVectors {
|
|
s := New(c.key, c.nonce)
|
|
input, err := hex.DecodeString(c.input)
|
|
if err != nil {
|
|
t.Fatalf("cannot decode input %#v: %v", c.input, err)
|
|
}
|
|
output := make([]byte, c.length)
|
|
s.XORKeyStream(output, input)
|
|
got := hex.EncodeToString(output)
|
|
if got != c.output {
|
|
t.Errorf("length=%v: got %#v, want %#v", c.length, got, c.output)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Run the test cases with the input and output overlapping entirely.
|
|
func TestOverlap(t *testing.T) {
|
|
for _, c := range testVectors {
|
|
s := New(c.key, c.nonce)
|
|
data, err := hex.DecodeString(c.input)
|
|
if err != nil {
|
|
t.Fatalf("cannot decode input %#v: %v", c.input, err)
|
|
}
|
|
s.XORKeyStream(data, data)
|
|
got := hex.EncodeToString(data)
|
|
if got != c.output {
|
|
t.Errorf("length=%v: got %#v, want %#v", c.length, got, c.output)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Run the test cases with various source and destination offsets.
|
|
func TestUnaligned(t *testing.T) {
|
|
const max = 8 // max offset (+1) to test
|
|
for _, c := range testVectors {
|
|
input := make([]byte, c.length+max)
|
|
output := make([]byte, c.length+max)
|
|
for i := 0; i < max; i++ { // input offsets
|
|
for j := 0; j < max; j++ { // output offsets
|
|
s := New(c.key, c.nonce)
|
|
|
|
input := input[i : i+c.length]
|
|
output := output[j : j+c.length]
|
|
|
|
data, err := hex.DecodeString(c.input)
|
|
if err != nil {
|
|
t.Fatalf("cannot decode input %#v: %v", c.input, err)
|
|
}
|
|
copy(input, data)
|
|
s.XORKeyStream(output, input)
|
|
got := hex.EncodeToString(output)
|
|
if got != c.output {
|
|
t.Errorf("length=%v: got %#v, want %#v", c.length, got, c.output)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Run the test cases by calling XORKeyStream multiple times.
|
|
func TestStep(t *testing.T) {
|
|
// wide range of step sizes to try and hit edge cases
|
|
steps := [...]int{1, 3, 4, 7, 8, 17, 24, 30, 64, 256}
|
|
rnd := rand.New(rand.NewSource(123))
|
|
for _, c := range testVectors {
|
|
s := New(c.key, c.nonce)
|
|
input, err := hex.DecodeString(c.input)
|
|
if err != nil {
|
|
t.Fatalf("cannot decode input %#v: %v", c.input, err)
|
|
}
|
|
output := make([]byte, c.length)
|
|
|
|
// step through the buffers
|
|
i, step := 0, steps[rnd.Intn(len(steps))]
|
|
for i+step < c.length {
|
|
s.XORKeyStream(output[i:i+step], input[i:i+step])
|
|
if i+step < c.length && output[i+step] != 0 {
|
|
t.Errorf("length=%v, i=%v, step=%v: output overwritten", c.length, i, step)
|
|
}
|
|
i += step
|
|
step = steps[rnd.Intn(len(steps))]
|
|
}
|
|
// finish the encryption
|
|
s.XORKeyStream(output[i:], input[i:])
|
|
|
|
got := hex.EncodeToString(output)
|
|
if got != c.output {
|
|
t.Errorf("length=%v: got %#v, want %#v", c.length, got, c.output)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Test that Advance() discards bytes until a block boundary is hit.
|
|
func TestAdvance(t *testing.T) {
|
|
for _, c := range testVectors {
|
|
for i := 0; i < 63; i++ {
|
|
s := New(c.key, c.nonce)
|
|
z := New(c.key, c.nonce)
|
|
input, err := hex.DecodeString(c.input)
|
|
if err != nil {
|
|
t.Fatalf("cannot decode input %#v: %v", c.input, err)
|
|
}
|
|
zeros, discard := make([]byte, 64), make([]byte, 64)
|
|
so, zo := make([]byte, c.length), make([]byte, c.length)
|
|
for j := 0; j < c.length; j += 64 {
|
|
lim := j + i
|
|
if lim > c.length {
|
|
lim = c.length
|
|
}
|
|
s.XORKeyStream(so[j:lim], input[j:lim])
|
|
// calling s.Advance() multiple times should have no effect
|
|
for k := 0; k < i%3+1; k++ {
|
|
s.Advance()
|
|
}
|
|
z.XORKeyStream(zo[j:lim], input[j:lim])
|
|
if lim < c.length {
|
|
end := 64 - i
|
|
if c.length-lim < end {
|
|
end = c.length - lim
|
|
}
|
|
z.XORKeyStream(discard[:], zeros[:end])
|
|
}
|
|
}
|
|
|
|
got := hex.EncodeToString(so)
|
|
want := hex.EncodeToString(zo)
|
|
if got != want {
|
|
t.Errorf("length=%v: got %#v, want %#v", c.length, got, want)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkChaCha20(b *testing.B) {
|
|
sizes := []int{32, 63, 64, 256, 1024, 1350, 65536}
|
|
for _, size := range sizes {
|
|
s := size
|
|
b.Run(fmt.Sprint(s), func(b *testing.B) {
|
|
k := [32]byte{}
|
|
c := [16]byte{}
|
|
src := make([]byte, s)
|
|
dst := make([]byte, s)
|
|
b.SetBytes(int64(s))
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
XORKeyStream(dst, src, &c, &k)
|
|
}
|
|
})
|
|
}
|
|
}
|