// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package entropy implements a CPU jitter-based SP 800-90B entropy source.
package entropy

import (
	"crypto/internal/fips140deps/time"
	"errors"
	"sync/atomic"
	"unsafe"
)

// Version returns the version of the entropy source.
//
// This is independent of the FIPS 140-3 module version, in order to reuse the
// ESV certificate across module versions.
func Version() string {
	return "v1.0.0"
}

// ScratchBuffer is a large buffer that will be written to using atomics, to
// generate noise from memory access timings. Its contents do not matter.
type ScratchBuffer [1 << 25]byte

// Seed returns a 384-bit seed with full entropy.
//
// memory is passed in to allow changing the allocation strategy without
// modifying the frozen and certified entropy source in this package.
//
// Seed returns an error if the entropy source startup health tests fail, which
// has a non-negligible chance of happening.
func Seed(memory *ScratchBuffer) ([48]byte, error) {
	// Collect w = 1024 samples, each certified to provide no less than h = 0.5
	// bits of entropy, for a total of hᵢₙ = w × h = 512 bits of entropy, over
	// nᵢₙ = w × n = 8192 bits of input data.
	var samples [1024]byte
	if err := Samples(samples[:], memory); err != nil {
		return [48]byte{}, err
	}

	// Use a vetted unkeyed conditioning component, SHA-384, with nw = 384 and
	// nₒᵤₜ = 384. Per the formula in SP 800-90B Section 3.1.5.1.2, the output
	// entropy hₒᵤₜ is:
	//
	//     sage: n_in = 8192
	//     sage: n_out = 384
	//     sage: nw = 384
	//     sage: h_in = 512
	//     sage: P_high = 2^(-h_in)
	//     sage: P_low = (1 - P_high) / (2^n_in - 1)
	//     sage: n = min(n_out, nw)
	//     sage: ψ = 2^(n_in - n) * P_low + P_high
	//     sage: U = 2^(n_in - n) + sqrt(2 * n * 2^(n_in - n) * ln(2))
	//     sage: ω = U * P_low
	//     sage: h_out = -log(max(ψ, ω), 2)
	//     sage: h_out.n()
	//     384.000000000000
	//
	// According to Implementation Guidance D.K, Resolution 19, since
	//
	//   - the conditioning component is vetted,
	//   - hᵢₙ = 512 ≥ nₒᵤₜ + 64 = 448, and
	//   - nₒᵤₜ ≤ security strength of SHA-384 = 384 (per SP 800-107 Rev. 1, Table 1),
	//
	// we can claim the output has full entropy.
	return SHA384(&samples), nil
}

// Samples starts a new entropy source, collects the requested number of
// samples, conducts startup health tests, and returns the samples or an error
// if the health tests fail.
//
// The health tests have a non-negligible chance of failing.
func Samples(samples []uint8, memory *ScratchBuffer) error {
	if len(samples) < 1024 {
		return errors.New("entropy: at least 1024 samples are required for startup health tests")
	}
	s := newSource(memory)
	for range 4 {
		// Warm up the source to avoid any initial bias.
		_ = s.Sample()
	}
	for i := range samples {
		samples[i] = s.Sample()
	}
	if err := RepetitionCountTest(samples); err != nil {
		return err
	}
	if err := AdaptiveProportionTest(samples); err != nil {
		return err
	}
	return nil
}

type source struct {
	memory   *ScratchBuffer
	lcgState uint32
	previous int64
}

func newSource(memory *ScratchBuffer) *source {
	return &source{
		memory:   memory,
		lcgState: uint32(time.HighPrecisionNow()),
		previous: time.HighPrecisionNow(),
	}
}

// touchMemory performs a write to memory at the given index.
//
// The memory slice is passed in and may be shared across sources e.g. to avoid
// the significant (~500µs) cost of zeroing a new allocation on every [Seed] call.
func touchMemory(memory *ScratchBuffer, idx uint32) {
	idx = idx / 4 * 4 // align to 32 bits
	u32 := (*uint32)(unsafe.Pointer(&memory[idx]))
	last := atomic.LoadUint32(u32)
	atomic.SwapUint32(u32, last+13)
}

func (s *source) Sample() uint8 {
	// Perform a few memory accesses in an unpredictable pattern to expose the
	// next measurement to as much system noise as possible.
	memory, lcgState := s.memory, s.lcgState
	if memory == nil { // remove the nil check from the inlined touchMemory calls
		panic("entropy: nil memory buffer")
	}
	for range 64 {
		lcgState = 1664525*lcgState + 1013904223
		// Discard the lower bits, which tend to fall into short cycles.
		idx := (lcgState >> 6) & (1<<25 - 1)
		touchMemory(memory, idx)
	}
	s.lcgState = lcgState

	t := time.HighPrecisionNow()
	sample := t - s.previous
	s.previous = t

	// Reduce the symbol space to 256 values, assuming most of the entropy is in
	// the least-significant bits, which represent the highest-resolution timing
	// differences.
	return uint8(sample)
}

// RepetitionCountTest implements the repetition count test from SP 800-90B
// Section 4.4.1. It returns an error if any symbol is repeated C = 41 or more
// times in a row.
//
// This C value is calculated from a target failure probability α = 2⁻²⁰ and a
// claimed min-entropy per symbol h = 0.5 bits, using the formula in SP 800-90B
// Section 4.4.1.
//
//	sage: α = 2^-20
//	sage: H = 0.5
//	sage: 1 + ceil(-log(α, 2) / H)
//	41
func RepetitionCountTest(samples []uint8) error {
	x := samples[0]
	count := 1
	for _, y := range samples[1:] {
		if y == x {
			count++
			if count >= 41 {
				return errors.New("entropy: repetition count health test failed")
			}
		} else {
			x = y
			count = 1
		}
	}
	return nil
}

// AdaptiveProportionTest implements the adaptive proportion test from SP 800-90B
// Section 4.4.2. It returns an error if any symbol appears C = 410 or more
// times in the last W = 512 samples.
//
// This C value is calculated from a target failure probability α = 2⁻²⁰, a
// window size W = 512, and a claimed min-entropy per symbol h = 0.5 bits, using
// the formula in SP 800-90B Section 4.4.2, equivalent to the Microsoft Excel
// formula 1+CRITBINOM(W, power(2,(−H)),1−α).
//
//	sage: from scipy.stats import binom
//	sage: α = 2^-20
//	sage: H = 0.5
//	sage: W = 512
//	sage: C = 1 + binom.ppf(1 - α, W, 2**(-H))
//	sage: ceil(C)
//	410
func AdaptiveProportionTest(samples []uint8) error {
	var counts [256]int
	for i, x := range samples {
		counts[x]++
		if i >= 512 {
			counts[samples[i-512]]--
		}
		if counts[x] >= 410 {
			return errors.New("entropy: adaptive proportion health test failed")
		}
	}
	return nil
}
