Skip to content

Commit f169071

Browse files
authored
feat: added crate kdk-entropy (#1)
This crate aim to mirror dice entropy method from krux and applying same logic to ones used on iancolleman, like coins and cards (expanding the concept of cards to N cards).
1 parent a0d58c2 commit f169071

12 files changed

Lines changed: 934 additions & 1 deletion

File tree

‎Cargo.lock‎

Lines changed: 91 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
resolver = "2"
33
members = [
44
"crates/kdk-zeroize",
5-
# "crates/kdk-entropy",
5+
"crates/kdk-entropy",
66
# "crates/kdk-kef",
77
# "crates/kdk-menu",
88
# "crates/kdk-psbt",

‎crates/kdk-entropy/Cargo.toml‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "kdk-entropy"
3+
version = "0.0.1"
4+
edition.workspace = true
5+
license.workspace = true
6+
repository.workspace = true
7+
description = "Origin-tagged entropy primitives for KDK"
8+
9+
[dependencies]
10+
kdk-zeroize = { path = "../kdk-zeroize" }
11+
sha2 = { version = "0.10", default-features = false }

‎crates/kdk-entropy/src/coin.rs‎

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

‎crates/kdk-entropy/src/deck.rs‎

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

0 commit comments

Comments
 (0)