summaryrefslogtreecommitdiffstats
path: root/internal/entropy/entropy.go
diff options
context:
space:
mode:
authorGab Virebent <gabriel1@virebent.art>2026-07-07 16:24:30 +0200
committerGab Virebent <gabriel1@virebent.art>2026-07-07 16:24:30 +0200
commitcc21fb4978b54d643d8fb3f5a454e62da4b396c2 (patch)
tree3050c8d4b68e28989cbe611001f8c6e961a08e51 /internal/entropy/entropy.go
downloadqpass-cc21fb4978b54d643d8fb3f5a454e62da4b396c2.tar.gz
qpass-cc21fb4978b54d643d8fb3f5a454e62da4b396c2.tar.xz
qpass-cc21fb4978b54d643d8fb3f5a454e62da4b396c2.zip
Initial commitHEADmain
Diffstat (limited to 'internal/entropy/entropy.go')
-rw-r--r--internal/entropy/entropy.go136
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
+ }
+}