diff options
| author | Gab Virebent <gabriel1@virebent.art> | 2026-07-07 16:24:30 +0200 |
|---|---|---|
| committer | Gab Virebent <gabriel1@virebent.art> | 2026-07-07 16:24:30 +0200 |
| commit | cc21fb4978b54d643d8fb3f5a454e62da4b396c2 (patch) | |
| tree | 3050c8d4b68e28989cbe611001f8c6e961a08e51 /internal/entropy/entropy.go | |
| download | qpass-cc21fb4978b54d643d8fb3f5a454e62da4b396c2.tar.gz qpass-cc21fb4978b54d643d8fb3f5a454e62da4b396c2.tar.xz qpass-cc21fb4978b54d643d8fb3f5a454e62da4b396c2.zip | |
Diffstat (limited to 'internal/entropy/entropy.go')
| -rw-r--r-- | internal/entropy/entropy.go | 136 |
1 files changed, 136 insertions, 0 deletions
diff --git a/internal/entropy/entropy.go b/internal/entropy/entropy.go new file mode 100644 index 0000000..886ecba --- /dev/null +++ b/internal/entropy/entropy.go @@ -0,0 +1,136 @@ +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 + } +} |
