use crate::consts::{
CHECKSUM_LEN_SHORT, HRP, RESERVED_PREFIX, SEPARATOR, SHARE_INDEX_V01, THRESHOLD_V01,
};
use crate::error::{Error, Result};
use crate::tag::Tag;
use codex32::{Codex32String, Fe};
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.to_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 discriminate(c: &Codex32String) -> Result<(Tag, Vec<u8>)> {
let s = c.to_string();
let fields = extract_wire_fields(&s)?;
if fields.hrp != HRP {
return Err(Error::WrongHrp {
got: fields.hrp.to_string(),
});
}
if fields.threshold_byte != THRESHOLD_V01 {
return Err(Error::ThresholdNotZero {
got: fields.threshold_byte,
});
}
if fields.share_index_byte != SHARE_INDEX_V01 {
return Err(Error::ShareIndexNotSecret {
got: fields.share_index_byte as char,
});
}
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());
if payload_with_prefix[0] != RESERVED_PREFIX {
return Err(Error::ReservedPrefixViolation {
got: payload_with_prefix[0],
});
}
Ok((tag, payload_with_prefix[1..].to_vec()))
}
pub(crate) fn package(tag: Tag, payload_bytes: &[u8]) -> Result<Codex32String> {
let mut data: Zeroizing<Vec<u8>> =
Zeroizing::new(Vec::with_capacity(1 + payload_bytes.len()));
data.push(RESERVED_PREFIX);
data.extend_from_slice(payload_bytes);
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, 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, 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 { .. })));
}
}
#[cfg(test)]
mod tests_package {
use super::*;
#[test]
fn package_round_trips_through_discriminate() {
for len in [16usize, 20, 24, 28, 32] {
let entropy = vec![0xAAu8; len];
let c = package(Tag::ENTR, &entropy).unwrap();
let (tag, recovered) = discriminate(&c).unwrap();
assert_eq!(tag, Tag::ENTR);
assert_eq!(recovered, 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 c = package(Tag::ENTR, &entropy).unwrap();
let s = c.to_string();
assert_eq!(
s.len(),
expected_lengths[i],
"length mismatch for {}-B entropy: got {}, expected {}",
len,
s.len(),
expected_lengths[i]
);
}
}
}