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}