Skip to main content

kdk_entropy/
deck.rs

1use kdk_zeroize::prelude::*;
2use sha2::{Digest, Sha256};
3
4use crate::error::EntropyError;
5use crate::utils::check_length;
6use crate::DeckEntropy;
7
8/// Minimum card draws required to fill an `N`-byte entropy buffer
9/// from a `CARDS`-deck. Each entry is the smallest `k` such that
10/// `log₂(CARDS! / (CARDS-k)!) ≥ 8·N`.
11pub const fn min_draws<const CARDS: u8, const N: usize>() -> Result<usize, EntropyError> {
12    match (CARDS, N) {
13        (40, 16) => Ok(28),
14        (48, 16) => Ok(26),
15        (52, 16) => Ok(25),
16        (58, 16) => Ok(24),
17        (58, 32) => Ok(55),
18        (78, 16) => Ok(22),
19        (78, 32) => Ok(45),
20        (108, 16) => Ok(22),
21        (108, 32) => Ok(41),
22        (112, 16) => Ok(22),
23        (112, 32) => Ok(42),
24        _ => Err(EntropyError::UnsupportedConfig(CARDS, N)),
25    }
26}
27
28/// Practical upper bound on draw count — `2 * min_draws`, capped at `CARDS`.
29pub const fn max_draws<const CARDS: u8, const N: usize>() -> Result<usize, EntropyError> {
30    match min_draws::<CARDS, N>() {
31        Ok(m) => {
32            let doubled = m * 2;
33            let cap = CARDS as usize;
34            Ok(if doubled > cap { cap } else { doubled })
35        }
36        Err(e) => Err(e),
37    }
38}
39
40/// Fold a sequence of drawn cards into an `N`-byte entropy buffer.
41///
42/// Cards are formatted as decimal indices, hyphen-separated, SHA-256
43/// hashed, then truncated to `N` bytes. Mirrors Krux's d20 string
44/// shape (`"-".join`) for multi-digit values.
45///
46/// # Example
47///
48/// ```
49/// use kdk_entropy::{deck_to_entropy, DeckEntropy};
50/// use kdk_zeroize::prelude::*;
51///
52/// // SHA-256("0-1-2-3-...-24")[:16].
53/// let cards: [u8; 25] = [
54///     0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
55///     10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
56///     20, 21, 22, 23, 24,
57/// ];
58/// let e: DeckEntropy<52, 16> = deck_to_entropy(&cards).unwrap();
59/// assert_eq!(
60///     e.expose_secret(),
61///     &[
62///         0x92, 0xe2, 0x92, 0xe3, 0x4c, 0x44, 0x48, 0xc1,
63///         0x57, 0x73, 0x1a, 0xff, 0x3d, 0x40, 0x10, 0x38,
64///     ]
65/// );
66/// ```
67pub fn deck_to_entropy<const CARDS: u8, const N: usize>(
68    cards: &[u8],
69) -> Result<DeckEntropy<CARDS, N>, EntropyError> {
70    let required = min_draws::<CARDS, N>()?;
71    let allowed = max_draws::<CARDS, N>()?;
72    check_length(cards.len(), required, allowed)?;
73
74    // Validation pass: range + uniqueness. `present[i]` is non-secret
75    // bookkeeping for which indices remain undrawn.
76    let mut present = [true; 256];
77    for (i, &card) in cards.iter().enumerate() {
78        if card >= CARDS {
79            return Err(EntropyError::RollOutOfRange(card, i));
80        }
81        if !present[card as usize] {
82            return Err(EntropyError::DuplicateCard(card, i));
83        }
84        present[card as usize] = false;
85    }
86
87    let mut hasher = Sha256::new();
88    for (i, &card) in cards.iter().enumerate() {
89        if i > 0 {
90            hasher.update(b"-");
91        }
92        if card >= 100 {
93            hasher.update([b'0' + card / 100]);
94        }
95        if card >= 10 {
96            hasher.update([b'0' + (card / 10) % 10]);
97        }
98        hasher.update([b'0' + card % 10]);
99    }
100    let digest = hasher.finalize();
101
102    let mut acc: DeckEntropy<CARDS, N> = DeckEntropy::new([0u8; N]);
103    acc.expose_secret_mut().copy_from_slice(&digest[..N]);
104    Ok(acc)
105}