Skip to main content

kdk_mnemonic/
game.rs

1use core::fmt;
2use core::marker::PhantomData;
3
4use bip39::Mnemonic;
5use kdk_entropy::EntropyError;
6use kdk_zeroize::{prelude::*, SensitiveBytes};
7
8use crate::error::MnemonicError;
9
10/// Sensitive BIP39 mnemonic wrapper. `O` is a zero-sized origin marker
11/// (e.g. [`kdk_entropy::Coin`], [`kdk_entropy::Dice<F>`],
12/// [`kdk_entropy::Deck<C>`]) and `W` is the word count
13/// (`12 / 15 / 18 / 21 / 24`).
14pub struct GameMnemonic<O, const W: u8> {
15    inner: Mnemonic,
16    _origin: PhantomData<O>,
17}
18
19impl<O, const W: u8> GameMnemonic<O, W> {
20    /// per-game pipelines (`coin_mnemonic`, `dice_mnemonic`, `deck_mnemonic`)
21    /// or the generic [`entropy_to_mnemonic`].
22    pub(crate) fn new(inner: Mnemonic) -> Self {
23        Self {
24            inner,
25            _origin: PhantomData,
26        }
27    }
28
29    /// Word count of this mnemonic — returned from the const generic
30    /// `W` without touching the inner secret. Use this instead of
31    /// `expose_secret().word_count()` whenever you just need the
32    /// metadata.
33    pub const fn word_count(&self) -> usize {
34        W as usize
35    }
36}
37
38impl<O, const W: u8> fmt::Debug for GameMnemonic<O, W> {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        f.write_str("GameMnemonic(REDACTED)")
41    }
42}
43
44impl<O, const W: u8> fmt::Display for GameMnemonic<O, W> {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        f.write_str("GameMnemonic(REDACTED)")
47    }
48}
49
50impl<O, const W: u8> Sensitive for GameMnemonic<O, W> {
51    type Inner = Mnemonic;
52
53    fn expose_secret(&self) -> &Mnemonic {
54        &self.inner
55    }
56}
57
58/// Contract for any entropy-source ("game") that can produce a BIP39
59/// mnemonic. Implementers supply only [`to_entropy`]. Per-`W`.
60pub trait SensitiveGame: Sized {
61    /// Game-specific entropy function: raw input bytes → `N`-byte
62    /// origin-tagged [`SensitiveBytes`].
63    fn to_entropy<const N: usize>(input: &[u8]) -> Result<SensitiveBytes<N, Self>, EntropyError>;
64
65    /// Feature-gated per-`W`, then build the mnemonic.
66    /// Each game shares this code path; only the byte size per `W` differs.
67    fn mnemonic<const W: u8>(input: &[u8]) -> Result<GameMnemonic<Self, W>, MnemonicError> {
68        match W {
69            #[cfg(feature = "words-12")]
70            12 => Self::build::<W, 16>(input),
71            #[cfg(feature = "words-15")]
72            15 => Self::build::<W, 20>(input),
73            #[cfg(feature = "words-18")]
74            18 => Self::build::<W, 24>(input),
75            #[cfg(feature = "words-21")]
76            21 => Self::build::<W, 28>(input),
77            #[cfg(feature = "words-24")]
78            24 => Self::build::<W, 32>(input),
79            _ => Err(MnemonicError::InvalidWordCount(W)),
80        }
81    }
82
83    /// Entropy with explicit `N` to [GameMnemonic<Self, W>].
84    fn build<const W: u8, const N: usize>(
85        input: &[u8],
86    ) -> Result<GameMnemonic<Self, W>, MnemonicError> {
87        let entropy = Self::to_entropy::<N>(input)?;
88        Mnemonic::from_entropy(entropy.expose_secret())
89            .map(GameMnemonic::new)
90            .map_err(MnemonicError::Bip39)
91    }
92}
93
94/// Pre-built entropy bytes sets to a BIP39 [GameMnemonic<O,W>]. Validates that
95/// `W` is enabled and that `N` matches its BIP39 byte count, then wraps the
96/// produced [bip39::Mnemonic].
97pub fn entropy_to_mnemonic<O, const W: u8, const N: usize>(
98    entropy: &SensitiveBytes<N, O>,
99) -> Result<GameMnemonic<O, W>, MnemonicError> {
100    let expected = match W {
101        #[cfg(feature = "words-12")]
102        12 => 16,
103        #[cfg(feature = "words-15")]
104        15 => 20,
105        #[cfg(feature = "words-18")]
106        18 => 24,
107        #[cfg(feature = "words-21")]
108        21 => 28,
109        #[cfg(feature = "words-24")]
110        24 => 32,
111        _ => return Err(MnemonicError::InvalidWordCount(W)),
112    };
113    if N != expected {
114        return Err(MnemonicError::InvalidEntropyLength(N));
115    }
116    Mnemonic::from_entropy(entropy.expose_secret())
117        .map(GameMnemonic::new)
118        .map_err(MnemonicError::Bip39)
119}