package entropy import ( crand "crypto/rand" "crypto/sha512" "encoding/binary" "errors" "fmt" "io" "sync" ) const blockSize = sha512.Size var ErrExternalEntropyRequired = errors.New("external entropy source required") type HybridOptions struct { RequireExternal bool } type HybridReader struct { base io.Reader external io.Reader requireExternal bool mu sync.Mutex counter uint64 degraded bool degradeErr error buffer []byte } func NewHybridReader(base io.Reader, external io.Reader, opts HybridOptions) (*HybridReader, error) { if base == nil { base = crand.Reader } if external == nil { return nil, ErrExternalEntropyRequired } return &HybridReader{ base: base, external: external, requireExternal: opts.RequireExternal, }, nil } func (r *HybridReader) Read(p []byte) (int, error) { if len(p) == 0 { return 0, nil } r.mu.Lock() defer r.mu.Unlock() written := 0 for written < len(p) { if len(r.buffer) > 0 { n := copy(p[written:], r.buffer) written += n r.buffer = r.buffer[n:] continue } block, err := r.nextBlockLocked() if err != nil { return written, err } n := copy(p[written:], block[:]) written += n if n < len(block) { r.buffer = append(r.buffer[:0], block[n:]...) } } return written, nil } func (r *HybridReader) Degraded() bool { r.mu.Lock() defer r.mu.Unlock() return r.degraded } func (r *HybridReader) DegradeError() error { r.mu.Lock() defer r.mu.Unlock() return r.degradeErr } func (r *HybridReader) nextBlockLocked() ([blockSize]byte, error) { var base [blockSize]byte if _, err := io.ReadFull(r.base, base[:]); err != nil { return [blockSize]byte{}, fmt.Errorf("read OS CSPRNG: %w", err) } if r.degraded { return base, nil } var external [blockSize]byte if _, err := io.ReadFull(r.external, external[:]); err != nil { if r.requireExternal { return [blockSize]byte{}, fmt.Errorf("read external entropy: %w", err) } r.degraded = true r.degradeErr = err return base, nil } var counter [8]byte binary.BigEndian.PutUint64(counter[:], r.counter) r.counter++ h := sha512.New() h.Write([]byte("qpass hybrid entropy v1\x00")) h.Write(counter[:]) h.Write(base[:]) h.Write(external[:]) var out [blockSize]byte copy(out[:], h.Sum(nil)) return out, nil } func InstallGlobal(reader io.Reader) func() { if reader == nil { reader = crand.Reader } previous := crand.Reader crand.Reader = reader return func() { crand.Reader = previous } }