diff --git a/keccak_asm.go b/keccak_asm.go index 37ce284..0687d4d 100644 --- a/keccak_asm.go +++ b/keccak_asm.go @@ -95,13 +95,13 @@ func (s *sponge) Read(out []byte) (int, error) { n := len(out) for len(out) > 0 { - x := copy(out, s.state[s.readIdx:rate]) - s.readIdx += x - out = out[x:] if s.readIdx == rate { keccakF1600(&s.state) s.readIdx = 0 } + x := copy(out, s.state[s.readIdx:rate]) + s.readIdx += x + out = out[x:] } return n, nil } diff --git a/keccak_asm_test.go b/keccak_asm_test.go index 44a8f64..46b890a 100644 --- a/keccak_asm_test.go +++ b/keccak_asm_test.go @@ -3,8 +3,12 @@ package keccak import ( + "bytes" "math/rand" + "strconv" "testing" + + "golang.org/x/crypto/sha3" ) // TestXorAndPermute verifies the fused XOR+permute assembly entry point @@ -34,3 +38,54 @@ func TestXorAndPermute(t *testing.T) { } } } + +// TestSpongeReadDefersBoundaryPermutation covers Read(rate) followed by +// Read(k), the call boundary neither TestReadMatchesXCrypto (one read per +// hasher) nor TestReadMultipleCalls (chunks of 37) lands on. Deferring the +// permutation is output-neutral, so readIdx is what pins the deferral; the +// byte comparison catches a reorder that permutes the wrong number of times. +func TestSpongeReadDefersBoundaryPermutation(t *testing.T) { + if !useASM { + t.Skip("hardware acceleration unavailable on this CPU") + } + const input = "input" + for _, k := range []int{1, 7, rate - 1, rate, rate + 1} { + t.Run(strconv.Itoa(k), func(t *testing.T) { + ref := sha3.NewLegacyKeccak256().(KeccakState) + ref.Write([]byte(input)) + want := make([]byte, rate+k) + ref.Read(want) + + var s sponge + s.Write([]byte(input)) + got := make([]byte, rate+k) + s.Read(got[:rate]) + if s.readIdx != rate { + t.Fatalf("read index after one full block = %d, want %d", s.readIdx, rate) + } + s.Read(got[rate:]) + + if !bytes.Equal(got, want) { + t.Fatalf("Read(%d)+Read(%d) = %x, want %x", rate, k, got, want) + } + }) + } +} + +func BenchmarkSpongeRead(b *testing.B) { + if !useASM { + b.Skip("hardware acceleration unavailable on this CPU") + } + for _, size := range []int{rate - 1, rate, rate + 1} { + b.Run(strconv.Itoa(size), func(b *testing.B) { + out := make([]byte, size) + b.SetBytes(int64(size)) + b.ReportAllocs() + for b.Loop() { + var h sponge + h.Write([]byte("input")) + h.Read(out) + } + }) + } +}