Skip to main content

kdk_entropy/
coin.rs

1use kdk_zeroize::prelude::*;
2use sha2::{Digest, Sha256};
3
4use crate::error::EntropyError;
5use crate::utils::check_length;
6use crate::CoinEntropy;
7
8/// Minimum flips required to fill an `N`-byte entropy buffer with a
9/// fair coin.
10pub const fn min_flips<const N: usize>() -> Result<usize, EntropyError> {
11    match N {
12        16 => Ok(128),
13        20 => Ok(160),
14        24 => Ok(192),
15        28 => Ok(224),
16        32 => Ok(256),
17        _ => Err(EntropyError::UnsupportedConfig(2, N)),
18    }
19}
20
21/// Practical upper bound on flip count — 2× [`min_flips`].
22pub const fn max_flips<const N: usize>() -> Result<usize, EntropyError> {
23    match min_flips::<N>() {
24        Ok(m) => Ok(m * 2),
25        Err(e) => Err(e),
26    }
27}
28
29/// Fold a sequence of coin flips into an `N`-byte entropy buffer.
30///
31/// Flips are formatted as a concatenated string of `"0"` / `"1"` ASCII
32/// digits, SHA-256 hashed, then the first `N` bytes of the digest are
33/// taken. Mirrors Krux's dice-string approach for `FACES < 10`.
34///
35/// # Example
36///
37/// ```
38/// use kdk_entropy::{coin_to_entropy, CoinEntropy};
39/// use kdk_zeroize::prelude::*;
40///
41/// // SHA-256("0" * 128)[:16].
42/// let flips = [0u8; 128];
43/// let e: CoinEntropy<16> = coin_to_entropy(&flips).unwrap();
44/// assert_eq!(
45///     e.expose_secret(),
46///     &[
47///         0x45, 0x72, 0x57, 0x91, 0xc4, 0x7b, 0x32, 0x61,
48///         0x8c, 0xc5, 0x7b, 0x88, 0x34, 0x3e, 0x2b, 0xce,
49///     ]
50/// );
51/// ```
52pub fn coin_to_entropy<const N: usize>(flips: &[u8]) -> Result<CoinEntropy<N>, EntropyError> {
53    let required = min_flips::<N>()?;
54    let allowed = max_flips::<N>()?;
55    check_length(flips.len(), required, allowed)?;
56
57    for (i, &flip) in flips.iter().enumerate() {
58        if flip > 1 {
59            return Err(EntropyError::RollOutOfRange(flip, i));
60        }
61    }
62
63    let mut hasher = Sha256::new();
64    for &flip in flips {
65        hasher.update([b'0' + flip]);
66    }
67    let digest = hasher.finalize();
68
69    let mut acc: CoinEntropy<N> = CoinEntropy::new([0u8; N]);
70    acc.expose_secret_mut().copy_from_slice(&digest[..N]);
71    Ok(acc)
72}