oracle/go is the Go code at 4c1507c, moved unchanged, plus test-only
generators that record what it computes. tests/vectors holds the
deterministic crypto/store vectors and synthetic fixtures they produce.
Co-Authored-By: Claude <noreply@anthropic.com>
193 lines
4.7 KiB
Go
193 lines
4.7 KiB
Go
// Copyright 2026 The fury Authors.
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package stream
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/cipher"
|
|
"errors"
|
|
"io"
|
|
"sync"
|
|
|
|
"golang.org/x/crypto/chacha20poly1305"
|
|
)
|
|
|
|
// pipelineDepthPerWorker bounds how many chunks may be queued for, or
|
|
// awaiting collection from, a single worker at once. It caps the pipeline's
|
|
// memory use to a small multiple of ChunkSize regardless of input size.
|
|
const pipelineDepthPerWorker = 2
|
|
|
|
// NewParallelEncryptWriter is equivalent to [NewEncryptWriter], except that
|
|
// chunks are sealed by a pool of worker goroutines instead of inline on the
|
|
// calling goroutine. Its output is byte-for-byte identical to
|
|
// NewEncryptWriter's: STREAM chunk nonces are a pure counter, so sealing
|
|
// chunk i never depends on chunk i-1. Only the write order to dst must stay
|
|
// sequential, which a dedicated collector goroutine enforces.
|
|
//
|
|
// workers <= 1 returns a plain [NewEncryptWriter] with no pipeline overhead.
|
|
func NewParallelEncryptWriter(key []byte, dst io.Writer, workers int) (io.WriteCloser, error) {
|
|
if workers <= 1 {
|
|
return NewEncryptWriter(key, dst)
|
|
}
|
|
aead, err := chacha20poly1305.New(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
depth := workers * pipelineDepthPerWorker
|
|
w := ¶llelEncryptWriter{
|
|
a: aead,
|
|
dst: dst,
|
|
jobs: make(chan encJob, depth),
|
|
order: make(chan chan []byte, depth),
|
|
done: make(chan struct{}),
|
|
}
|
|
w.wg.Add(workers)
|
|
for i := 0; i < workers; i++ {
|
|
go w.work()
|
|
}
|
|
go w.collect()
|
|
return w, nil
|
|
}
|
|
|
|
type encJob struct {
|
|
nonce [chacha20poly1305.NonceSize]byte
|
|
plaintext []byte
|
|
result chan []byte
|
|
}
|
|
|
|
type parallelEncryptWriter struct {
|
|
a cipher.AEAD
|
|
dst io.Writer
|
|
|
|
buf bytes.Buffer
|
|
index int64
|
|
|
|
jobs chan encJob
|
|
order chan chan []byte
|
|
wg sync.WaitGroup
|
|
done chan struct{} // closed once the collector goroutine has returned
|
|
|
|
mu sync.Mutex
|
|
err error
|
|
}
|
|
|
|
func (w *parallelEncryptWriter) getErr() error {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
return w.err
|
|
}
|
|
|
|
func (w *parallelEncryptWriter) setErr(err error) {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
if w.err == nil {
|
|
w.err = err
|
|
}
|
|
}
|
|
|
|
func (w *parallelEncryptWriter) Write(p []byte) (n int, err error) {
|
|
if err := w.getErr(); err != nil {
|
|
return 0, err
|
|
}
|
|
if len(p) == 0 {
|
|
return 0, nil
|
|
}
|
|
|
|
total := len(p)
|
|
for len(p) > 0 {
|
|
room := ChunkSize - w.buf.Len()
|
|
nn := min(len(p), room)
|
|
w.buf.Write(p[:nn])
|
|
p = p[nn:]
|
|
|
|
// Only submit a chunk once we know more data is coming, so the very
|
|
// last chunk (submitted from Close) is always the one that gets the
|
|
// last-chunk-flag nonce, exactly like the sequential EncryptWriter.
|
|
if w.buf.Len() == ChunkSize && len(p) > 0 {
|
|
if err := w.submitChunk(false); err != nil {
|
|
w.setErr(err)
|
|
return 0, err
|
|
}
|
|
}
|
|
}
|
|
return total, nil
|
|
}
|
|
|
|
// Close flushes the last chunk and waits for every queued chunk to be
|
|
// sealed and written to dst, in order. It does not close the underlying
|
|
// Writer.
|
|
func (w *parallelEncryptWriter) Close() error {
|
|
if err := w.getErr(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// submitChunk cannot fail here: we just confirmed there is no prior
|
|
// error, and channel sends to w.jobs/w.order only block, they never
|
|
// return an error.
|
|
_ = w.submitChunk(true)
|
|
|
|
close(w.jobs)
|
|
close(w.order)
|
|
w.wg.Wait()
|
|
<-w.done
|
|
|
|
if err := w.getErr(); err != nil {
|
|
return err
|
|
}
|
|
w.setErr(errors.New("stream.Writer is already closed"))
|
|
return nil
|
|
}
|
|
|
|
// submitChunk copies out the current buffer as a chunk of plaintext, assigns
|
|
// it the next sequential nonce, and hands it to the worker pool. last must be
|
|
// true only for the final call, made from Close.
|
|
func (w *parallelEncryptWriter) submitChunk(last bool) error {
|
|
if err := w.getErr(); err != nil {
|
|
return err
|
|
}
|
|
|
|
plaintext := append([]byte(nil), w.buf.Bytes()...)
|
|
w.buf.Reset()
|
|
|
|
nonce := nonceForChunk(w.index)
|
|
if last {
|
|
setLastChunkFlag(nonce)
|
|
}
|
|
w.index++
|
|
|
|
result := make(chan []byte, 1)
|
|
w.order <- result
|
|
w.jobs <- encJob{nonce: *nonce, plaintext: plaintext, result: result}
|
|
return nil
|
|
}
|
|
|
|
// work seals queued chunks. Seal cannot fail: the nonce is always the
|
|
// correct size, so the only possible failure in this pipeline is dst.Write,
|
|
// handled in collect.
|
|
func (w *parallelEncryptWriter) work() {
|
|
defer w.wg.Done()
|
|
for j := range w.jobs {
|
|
ciphertext := w.a.Seal(make([]byte, 0, len(j.plaintext)+w.a.Overhead()), j.nonce[:], j.plaintext, nil)
|
|
j.result <- ciphertext
|
|
}
|
|
}
|
|
|
|
// collect writes sealed chunks to dst strictly in submission order, even
|
|
// though workers may finish sealing them in a different order.
|
|
func (w *parallelEncryptWriter) collect() {
|
|
defer close(w.done)
|
|
var failed bool
|
|
for result := range w.order {
|
|
ciphertext := <-result
|
|
if failed {
|
|
continue
|
|
}
|
|
if _, err := w.dst.Write(ciphertext); err != nil {
|
|
w.setErr(err)
|
|
failed = true
|
|
}
|
|
}
|
|
}
|