use crate::codex32::{Codex32String, Fe};
use crate::consts::{
CHECKSUM_LEN_SHORT, HRP, MNEM_PREFIX, RESERVED_PREFIX, SEPARATOR, SHARE_INDEX_V01,
THRESHOLD_V01,
};
use crate::error::{Error, Result};
use crate::payload::Payload;
use crate::tag::Tag;
use zeroize::Zeroizing;
const THRESHOLD_OFFSET: usize = 1;
const ID_START_OFFSET: usize = 2;
const ID_END_OFFSET: usize = 6;
const SHARE_INDEX_OFFSET: usize = 6;
const PAYLOAD_START_OFFSET: usize = 7;
#[derive(Debug, Clone, Copy)]
pub(crate) struct WireFields<'s> {
pub hrp: &'s str,
pub threshold_byte: u8,
pub id_bytes: [u8; 4],
pub share_index_byte: u8,
}
pub(crate) fn extract_wire_fields(s: &str) -> Result<WireFields<'_>> {
let sep = s.rfind(SEPARATOR).ok_or_else(|| Error::WrongHrp {
got: s.chars().take(4).collect::<String>(),
})?;
if s.len() < sep + PAYLOAD_START_OFFSET + CHECKSUM_LEN_SHORT {
return Err(Error::UnexpectedStringLength {
got: s.len(),
allowed: crate::consts::VALID_STR_LENGTHS,
});
}
let bytes = s.as_bytes();
let id_slice = &bytes[sep + ID_START_OFFSET..sep + ID_END_OFFSET];
Ok(WireFields {
hrp: &s[..sep],
threshold_byte: bytes[sep + THRESHOLD_OFFSET],
id_bytes: [id_slice[0], id_slice[1], id_slice[2], id_slice[3]],
share_index_byte: bytes[sep + SHARE_INDEX_OFFSET],
})
}
pub(crate) fn wire_string(c: &Codex32String) -> String {
c.to_string().to_ascii_lowercase()
}
pub(crate) fn discriminate(c: &Codex32String) -> Result<(Tag, Payload)> {
let s = wire_string(c);
let fields = extract_wire_fields(&s)?;
if fields.hrp != HRP {
return Err(Error::WrongHrp {
got: fields.hrp.chars().take(4).collect::<String>(),
});
}
match fields.threshold_byte {
THRESHOLD_V01 => {
if fields.share_index_byte != SHARE_INDEX_V01 {
return Err(Error::ShareIndexNotSecret {
got: fields.share_index_byte as char,
});
}
}
b'2'..=b'9' => {
return Err(Error::IsShareNotSingleString {
threshold: fields.threshold_byte as char,
index: fields.share_index_byte as char,
});
}
other => {
return Err(Error::ThresholdNotZero { got: other });
}
}
let tag_bytes = fields.id_bytes;
let tag_str = std::str::from_utf8(&tag_bytes)
.map_err(|_| Error::TagInvalidAlphabet { got: tag_bytes })?;
let tag = Tag::try_new(tag_str)?;
let payload_with_prefix: Zeroizing<Vec<u8>> = Zeroizing::new(c.parts().data());
let payload = dispatch_payload(&payload_with_prefix)?;
Ok((tag, payload))
}
pub(crate) fn dispatch_payload(data: &[u8]) -> Result<Payload> {
let payload = match data[0] {
RESERVED_PREFIX => {
let p = Payload::Entr(data[1..].to_vec());
p.validate()?;
p
}
MNEM_PREFIX => {
let language = data[1];
let entropy = data[2..].to_vec();
let p = Payload::Mnem { language, entropy };
p.validate()?;
p
}
other => {
return Err(Error::ReservedPrefixViolation { got: other });
}
};
Ok(payload)
}
pub(crate) fn payload_wire_bytes(p: &Payload) -> Zeroizing<Vec<u8>> {
match p {
Payload::Entr(e) => {
let mut v = Zeroizing::new(Vec::with_capacity(1 + e.len()));
v.push(RESERVED_PREFIX);
v.extend_from_slice(e);
v
}
Payload::Mnem { language, entropy } => {
let mut v = Zeroizing::new(Vec::with_capacity(2 + entropy.len()));
v.push(MNEM_PREFIX);
v.push(*language);
v.extend_from_slice(entropy);
v
}
}
}
pub(crate) fn package(tag: Tag, payload: &Payload) -> Result<Codex32String> {
let data: Zeroizing<Vec<u8>> = payload_wire_bytes(payload);
Ok(Codex32String::from_seed(
HRP,
0,
tag.as_str(),
Fe::S,
&data[..],
)?)
}
#[cfg(test)]
mod tests_extract {
use super::*;
#[test]
fn bip93_test_vector_1_extracts_correctly() {
let s = "ms10testsxxxxxxxxxxxxxxxxxxxxxxxxxx4nzvca9cmczlw";
let fields = extract_wire_fields(s).unwrap();
assert_eq!(fields.hrp, "ms");
assert_eq!(fields.threshold_byte, b'0');
assert_eq!(&fields.id_bytes, b"test");
assert_eq!(fields.share_index_byte, b's');
}
#[test]
fn rejects_too_short_string() {
assert!(matches!(
extract_wire_fields("ms1"),
Err(Error::UnexpectedStringLength { .. })
));
}
}
#[cfg(test)]
mod tests_discriminate {
use super::*;
fn build_v01_entr(entropy: &[u8]) -> Codex32String {
let mut data = vec![RESERVED_PREFIX];
data.extend_from_slice(entropy);
Codex32String::from_seed(HRP, 0, "entr", Fe::S, &data).unwrap()
}
#[test]
fn v01_entr_16_round_trips_through_discriminate() {
let entropy = vec![0xAAu8; 16];
let c = build_v01_entr(&entropy);
let (tag, recovered) = discriminate(&c).unwrap();
assert_eq!(tag, Tag::ENTR);
assert_eq!(recovered, Payload::Entr(entropy));
}
#[test]
fn v01_entr_32_round_trips_through_discriminate() {
let entropy = vec![0x55u8; 32];
let c = build_v01_entr(&entropy);
let (tag, recovered) = discriminate(&c).unwrap();
assert_eq!(tag, Tag::ENTR);
assert_eq!(recovered, Payload::Entr(entropy));
}
#[test]
fn discriminate_rejects_non_zero_prefix() {
let mut data = vec![0x01u8];
data.extend_from_slice(&[0xAAu8; 16]);
let c = Codex32String::from_seed(HRP, 0, "entr", Fe::S, &data).unwrap();
assert!(matches!(
discriminate(&c),
Err(Error::ReservedPrefixViolation { got: 0x01 })
));
}
#[test]
fn discriminate_rejects_wrong_hrp() {
let mut data = vec![RESERVED_PREFIX];
data.extend_from_slice(&[0xAAu8; 16]);
let c = Codex32String::from_seed("mq", 0, "entr", Fe::S, &data).unwrap();
assert!(matches!(discriminate(&c), Err(Error::WrongHrp { .. })));
}
#[test]
fn discriminate_mnem_prefix_returns_mnem_payload() {
let entropy = vec![0xBBu8; 16];
let mut data = vec![MNEM_PREFIX, 0x02u8]; data.extend_from_slice(&entropy);
let c = Codex32String::from_seed(HRP, 0, "entr", Fe::S, &data).unwrap();
let (tag, recovered) = discriminate(&c).unwrap();
assert_eq!(tag, Tag::ENTR);
assert_eq!(
recovered,
Payload::Mnem {
language: 2,
entropy
}
);
}
#[test]
fn discriminate_routes_threshold_2_to_is_share() {
let data = vec![0xAAu8; 16];
let c = Codex32String::from_seed(HRP, 2, "tst7", Fe::P, &data).unwrap();
match discriminate(&c) {
Err(Error::IsShareNotSingleString { threshold, index }) => {
assert_eq!(threshold, '2');
assert_eq!(index, 'p');
}
other => panic!("expected IsShareNotSingleString, got {other:?}"),
}
}
#[test]
fn threshold_1_is_unconstructible_so_never_reaches_discriminate() {
let mut data = vec![0x00u8];
data.extend_from_slice(&[0xAAu8; 16]);
match Codex32String::from_seed(HRP, 1, "tst7", Fe::A, &data) {
Err(crate::codex32::Error::InvalidThresholdN(1)) => {}
other => panic!("expected InvalidThresholdN(1) from from_seed, got {other:?}"),
}
let s2 = Codex32String::from_seed(HRP, 2, "tst7", Fe::A, &data)
.unwrap()
.to_string();
let sep = s2.rfind('1').expect("codex32 separator '1' present");
let mut bytes = s2.into_bytes();
bytes[sep + 1] = b'1'; let forged = String::from_utf8(bytes).unwrap();
assert!(
matches!(
Codex32String::from_string(forged),
Err(crate::codex32::Error::InvalidChecksum { .. })
),
"a forged threshold-'1' char must fail the BCH checksum at from_string",
);
}
}
#[cfg(test)]
mod tests_wire_bytes {
use super::*;
#[test]
fn entr_wire_bytes_are_prefix_plus_entropy() {
let p = Payload::Entr(vec![0xABu8; 16]);
let mut expected = vec![0x00u8];
expected.extend(std::iter::repeat_n(0xABu8, 16));
assert_eq!(&payload_wire_bytes(&p)[..], &expected[..]);
}
#[test]
fn mnem_wire_bytes_are_prefix_lang_entropy() {
let p = Payload::Mnem {
language: 1,
entropy: vec![0xABu8; 16],
};
let mut expected = vec![0x02u8, 0x01u8];
expected.extend(std::iter::repeat_n(0xABu8, 16));
assert_eq!(&payload_wire_bytes(&p)[..], &expected[..]);
}
}
#[cfg(test)]
mod tests_package {
use super::*;
#[test]
fn package_entr_round_trips_through_discriminate() {
for len in [16usize, 20, 24, 28, 32] {
let entropy = vec![0xAAu8; len];
let p = Payload::Entr(entropy.clone());
let c = package(Tag::ENTR, &p).unwrap();
let (tag, recovered) = discriminate(&c).unwrap();
assert_eq!(tag, Tag::ENTR);
assert_eq!(recovered, Payload::Entr(entropy));
}
}
#[test]
fn package_mnem_round_trips_through_discriminate() {
for len in [16usize, 20, 24, 28, 32] {
let entropy = vec![0xCCu8; len];
let p = Payload::Mnem {
language: 3,
entropy: entropy.clone(),
};
let c = package(Tag::ENTR, &p).unwrap();
let (tag, recovered) = discriminate(&c).unwrap();
assert_eq!(tag, Tag::ENTR);
assert_eq!(
recovered,
Payload::Mnem {
language: 3,
entropy
}
);
}
}
#[test]
fn package_produces_str_lengths_in_v01_set() {
let expected_lengths = crate::consts::VALID_STR_LENGTHS;
for (i, len) in [16usize, 20, 24, 28, 32].iter().enumerate() {
let entropy = vec![0xAAu8; *len];
let p = Payload::Entr(entropy);
let c = package(Tag::ENTR, &p).unwrap();
let s = c.to_string();
assert_eq!(
s.len(),
expected_lengths[i],
"length mismatch for {}-B entr entropy: got {}, expected {}",
len,
s.len(),
expected_lengths[i]
);
}
}
#[test]
fn package_mnem_produces_str_lengths_in_mnem_set() {
let expected_lengths = crate::consts::VALID_MNEM_STR_LENGTHS;
for (i, len) in [16usize, 20, 24, 28, 32].iter().enumerate() {
let entropy = vec![0xAAu8; *len];
let p = Payload::Mnem {
language: 0,
entropy,
};
let c = package(Tag::ENTR, &p).unwrap();
let s = c.to_string();
assert_eq!(
s.len(),
expected_lengths[i],
"length mismatch for {}-B mnem entropy: got {}, expected {}",
len,
s.len(),
expected_lengths[i]
);
}
}
}