Skip to main content

ms_codec/
payload.rs

1//! Payload type — v0.2: Entr (BIP-39 entropy) and Mnem (BIP-39 mnemonic with language).
2
3use crate::consts::VALID_ENTR_LENGTHS;
4use crate::error::{Error, Result};
5use crate::tag::Tag;
6
7/// v0.2 payload kind.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9#[non_exhaustive]
10pub enum PayloadKind {
11    /// BIP-39 entropy (16/20/24/28/32 B).
12    Entr,
13    /// BIP-39 mnemonic entropy with wordlist language tag (16/20/24/28/32 B entropy).
14    Mnem,
15}
16
17/// v0.1 payload.
18///
19/// **Caller-wrap contract (SPEC v0.9.0 §1 item 2):** the `Vec<u8>` inside
20/// `Payload::Entr` is NOT zeroize-wrapped — widening the public type to
21/// `Zeroizing<Vec<u8>>` is a breaking change deferred indefinitely per
22/// SPEC §3 OOS-2. Callers MUST wrap the byte buffer at the use site
23/// (e.g., `let bytes = Zeroizing::new((*p.as_bytes()).to_vec());`)
24/// so that the secret-material lifetime ends with a scrubbed drop.
25/// ms-codec internally minimizes the un-scrubbed lifetime: encode + decode
26/// path locals are `Zeroizing<Vec<u8>>`; only the public `Payload::Entr`
27/// boundary is unwrapped.
28#[derive(Debug, Clone, PartialEq, Eq)]
29#[non_exhaustive]
30pub enum Payload {
31    /// BIP-39 entropy. Length MUST be in {16, 20, 24, 28, 32} bytes
32    /// (bijective with BIP-39 word counts {12, 15, 18, 21, 24}).
33    ///
34    /// **Caller responsibility:** ms-codec does NOT check the statistical
35    /// quality of these bytes. Callers are responsible for sourcing entropy
36    /// from a vetted CSPRNG, or from a BIP-39 mnemonic the user already trusts.
37    /// FIPS-style entropy-quality checks would slow encoding and provide false
38    /// assurance — they cannot detect attacker-supplied "pseudo-random" seeds
39    /// crafted to pass standard randomness tests. See SPEC §3.6.
40    ///
41    /// **Caller-wrap reminder:** wrap this `Vec<u8>` in `Zeroizing` at the
42    /// use site so it scrubs on drop. ms-codec cannot wrap this for you
43    /// without a breaking public-API change.
44    Entr(Vec<u8>),
45    /// BIP-39 mnemonic entropy with wordlist language tag. On-wire payload:
46    /// `[0x02][language_byte][entropy:N]` where `language_byte` indexes into
47    /// `consts::MNEM_LANGUAGE_NAMES` (0 = English, 1 = Japanese, …, 9 = Portuguese).
48    /// Entropy length MUST be in {16, 20, 24, 28, 32} bytes.
49    ///
50    /// **Caller-wrap reminder:** wrap `entropy` in `Zeroizing` at the use site.
51    Mnem {
52        /// BIP-39 wordlist language index (0..=9).
53        language: u8,
54        /// BIP-39 entropy bytes (16/20/24/28/32 B).
55        entropy: Vec<u8>,
56    },
57}
58
59impl Payload {
60    /// Validate the payload's intrinsic structure (byte length for Entr/Mnem;
61    /// language code range for Mnem).
62    /// Encoder MUST call this before emitting; decoder calls it after extracting
63    /// the payload bytes following the prefix byte.
64    pub fn validate(&self) -> Result<()> {
65        match self {
66            Payload::Entr(data) => {
67                if !VALID_ENTR_LENGTHS.contains(&data.len()) {
68                    return Err(Error::PayloadLengthMismatch {
69                        tag: *Tag::ENTR.as_bytes(),
70                        expected: VALID_ENTR_LENGTHS,
71                        got: data.len(),
72                    });
73                }
74                Ok(())
75            }
76            Payload::Mnem { language, entropy } => {
77                if *language >= 10 {
78                    return Err(Error::MnemUnknownLanguage(*language));
79                }
80                if !VALID_ENTR_LENGTHS.contains(&entropy.len()) {
81                    return Err(Error::PayloadLengthMismatch {
82                        tag: *Tag::ENTR.as_bytes(),
83                        expected: VALID_ENTR_LENGTHS,
84                        got: entropy.len(),
85                    });
86                }
87                Ok(())
88            }
89        }
90    }
91
92    /// The PayloadKind discriminant.
93    pub fn kind(&self) -> PayloadKind {
94        match self {
95            Payload::Entr(_) => PayloadKind::Entr,
96            Payload::Mnem { .. } => PayloadKind::Mnem,
97        }
98    }
99
100    /// Borrow the inner entropy byte slice.
101    /// For `Payload::Mnem`, returns the entropy bytes only (without prefix or language byte).
102    pub fn as_bytes(&self) -> &[u8] {
103        match self {
104            Payload::Entr(data) => data,
105            Payload::Mnem { entropy, .. } => entropy,
106        }
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    // --- Mnem failing tests (written before impl per TDD) ---
115
116    #[test]
117    fn mnem_valid_language_and_entropy_accepts() {
118        let p = Payload::Mnem { language: 1, entropy: vec![0u8; 16] };
119        assert!(matches!(p.validate(), Ok(())));
120    }
121
122    #[test]
123    fn mnem_language_10_rejects() {
124        let p = Payload::Mnem { language: 10, entropy: vec![0u8; 16] };
125        assert!(matches!(p.validate(), Err(Error::MnemUnknownLanguage(10))));
126    }
127
128    #[test]
129    fn mnem_language_0x10_rejects() {
130        let p = Payload::Mnem { language: 0x10, entropy: vec![0u8; 16] };
131        assert!(matches!(p.validate(), Err(Error::MnemUnknownLanguage(0x10))));
132    }
133
134    #[test]
135    fn mnem_bad_entropy_length_rejects() {
136        let p = Payload::Mnem { language: 0, entropy: vec![0u8; 17] };
137        assert!(matches!(p.validate(), Err(Error::PayloadLengthMismatch { .. })));
138    }
139
140    #[test]
141    fn mnem_kind_returns_mnem() {
142        let p = Payload::Mnem { language: 0, entropy: vec![0u8; 16] };
143        assert_eq!(p.kind(), PayloadKind::Mnem);
144    }
145
146    // --- Entr tests (pre-existing) ---
147
148    #[test]
149    fn entr_accepts_all_bip39_lengths() {
150        for len in [16usize, 20, 24, 28, 32] {
151            let p = Payload::Entr(vec![0u8; len]);
152            p.validate()
153                .unwrap_or_else(|e| panic!("expected ok for len {}, got {:?}", len, e));
154        }
155    }
156
157    #[test]
158    fn entr_rejects_off_by_one_lengths() {
159        for len in [15usize, 17, 19, 21, 23, 25, 31, 33] {
160            let p = Payload::Entr(vec![0u8; len]);
161            assert!(
162                matches!(p.validate(), Err(Error::PayloadLengthMismatch { .. })),
163                "expected reject for len {}",
164                len
165            );
166        }
167    }
168
169    #[test]
170    fn entr_rejects_zero_length() {
171        let p = Payload::Entr(vec![]);
172        assert!(matches!(
173            p.validate(),
174            Err(Error::PayloadLengthMismatch { .. })
175        ));
176    }
177
178    #[test]
179    fn kind_returns_entr() {
180        assert_eq!(Payload::Entr(vec![0u8; 16]).kind(), PayloadKind::Entr);
181    }
182}