Skip to main content

ms_codec/
payload.rs

1//! Payload type — v0.1: Entr (BIP-39 entropy) only.
2
3use crate::consts::VALID_ENTR_LENGTHS;
4use crate::error::{Error, Result};
5use crate::tag::Tag;
6
7/// v0.1 payload kind. Future kinds (Mnem, Seed, Xprv) will arrive in v0.2+
8/// with their own framing per SPEC §1, §3.3, §8.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10#[non_exhaustive]
11pub enum PayloadKind {
12    /// BIP-39 entropy (16/20/24/28/32 B).
13    Entr,
14}
15
16/// v0.1 payload.
17///
18/// **Caller-wrap contract (SPEC v0.9.0 §1 item 2):** the `Vec<u8>` inside
19/// `Payload::Entr` is NOT zeroize-wrapped — widening the public type to
20/// `Zeroizing<Vec<u8>>` is a breaking change deferred indefinitely per
21/// SPEC §3 OOS-2. Callers MUST wrap the byte buffer at the use site
22/// (e.g., `let bytes = Zeroizing::new((*p.as_bytes()).to_vec());`)
23/// so that the secret-material lifetime ends with a scrubbed drop.
24/// ms-codec internally minimizes the un-scrubbed lifetime: encode + decode
25/// path locals are `Zeroizing<Vec<u8>>`; only the public `Payload::Entr`
26/// boundary is unwrapped.
27#[derive(Debug, Clone, PartialEq, Eq)]
28#[non_exhaustive]
29pub enum Payload {
30    /// BIP-39 entropy. Length MUST be in {16, 20, 24, 28, 32} bytes
31    /// (bijective with BIP-39 word counts {12, 15, 18, 21, 24}).
32    ///
33    /// **Caller responsibility:** ms-codec does NOT check the statistical
34    /// quality of these bytes. Callers are responsible for sourcing entropy
35    /// from a vetted CSPRNG, or from a BIP-39 mnemonic the user already trusts.
36    /// FIPS-style entropy-quality checks would slow encoding and provide false
37    /// assurance — they cannot detect attacker-supplied "pseudo-random" seeds
38    /// crafted to pass standard randomness tests. See SPEC §3.6.
39    ///
40    /// **Caller-wrap reminder:** wrap this `Vec<u8>` in `Zeroizing` at the
41    /// use site so it scrubs on drop. ms-codec cannot wrap this for you
42    /// without a breaking public-API change.
43    Entr(Vec<u8>),
44}
45
46impl Payload {
47    /// Validate the payload's intrinsic structure (byte length for Entr).
48    /// Encoder MUST call this before emitting; decoder calls it after extracting
49    /// the payload bytes following the reserved-prefix byte.
50    pub fn validate(&self) -> Result<()> {
51        match self {
52            Payload::Entr(data) => {
53                if !VALID_ENTR_LENGTHS.contains(&data.len()) {
54                    return Err(Error::PayloadLengthMismatch {
55                        tag: *Tag::ENTR.as_bytes(),
56                        expected: VALID_ENTR_LENGTHS,
57                        got: data.len(),
58                    });
59                }
60                Ok(())
61            }
62        }
63    }
64
65    /// The PayloadKind discriminant.
66    pub fn kind(&self) -> PayloadKind {
67        match self {
68            Payload::Entr(_) => PayloadKind::Entr,
69        }
70    }
71
72    /// Borrow the inner byte slice.
73    pub fn as_bytes(&self) -> &[u8] {
74        match self {
75            Payload::Entr(data) => data,
76        }
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn entr_accepts_all_bip39_lengths() {
86        for len in [16usize, 20, 24, 28, 32] {
87            let p = Payload::Entr(vec![0u8; len]);
88            p.validate()
89                .unwrap_or_else(|e| panic!("expected ok for len {}, got {:?}", len, e));
90        }
91    }
92
93    #[test]
94    fn entr_rejects_off_by_one_lengths() {
95        for len in [15usize, 17, 19, 21, 23, 25, 31, 33] {
96            let p = Payload::Entr(vec![0u8; len]);
97            assert!(
98                matches!(p.validate(), Err(Error::PayloadLengthMismatch { .. })),
99                "expected reject for len {}",
100                len
101            );
102        }
103    }
104
105    #[test]
106    fn entr_rejects_zero_length() {
107        let p = Payload::Entr(vec![]);
108        assert!(matches!(
109            p.validate(),
110            Err(Error::PayloadLengthMismatch { .. })
111        ));
112    }
113
114    #[test]
115    fn kind_returns_entr() {
116        assert_eq!(Payload::Entr(vec![0u8; 16]).kind(), PayloadKind::Entr);
117    }
118}