use crate::codex32::{Codex32String, Fe};
use crate::consts::{HRP, RESERVED_ID_BLOCKLIST, SHARE_INDEX_V01};
use crate::envelope::{dispatch_payload, extract_wire_fields, payload_wire_bytes, wire_string};
use crate::error::{Error, Result};
use crate::payload::Payload;
use crate::tag::Tag;
use zeroize::Zeroizing;
const CODEX32_ALPHABET: &[u8; 32] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
fn non_s_index_pool() -> Vec<Fe> {
CODEX32_ALPHABET
.iter()
.filter(|&&b| b != b's')
.map(|&b| Fe::from_char(b as char).expect("alphabet char is a valid Fe"))
.collect()
}
fn random_id() -> String {
loop {
let mut raw = [0u8; 4];
getrandom::fill(&mut raw).expect("getrandom::fill must not fail");
let id: [u8; 4] = [
CODEX32_ALPHABET[(raw[0] & 0x1f) as usize],
CODEX32_ALPHABET[(raw[1] & 0x1f) as usize],
CODEX32_ALPHABET[(raw[2] & 0x1f) as usize],
CODEX32_ALPHABET[(raw[3] & 0x1f) as usize],
];
if !RESERVED_ID_BLOCKLIST.contains(&id) {
return String::from_utf8(id.to_vec()).expect("codex32 alphabet is ASCII");
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Threshold(u8);
impl Threshold {
pub const ZERO: Threshold = Threshold(0);
pub fn new(k: u8) -> Result<Threshold> {
if (2..=9).contains(&k) {
Ok(Threshold(k))
} else {
Err(Error::InvalidThreshold(k))
}
}
pub fn get(self) -> u8 {
self.0
}
}
pub fn encode_shares(
tag: Tag,
threshold: Threshold,
n: usize,
secret: &Payload,
) -> Result<Vec<String>> {
secret.validate()?;
let bytes = payload_wire_bytes(secret);
if threshold == Threshold::ZERO {
if n != 1 {
return Err(Error::InvalidShareCount { k: 0, n });
}
let single = Codex32String::from_seed(HRP, 0, tag.as_str(), Fe::S, &bytes[..])?;
return Ok(vec![single.to_string()]);
}
let k = threshold.get();
let k_usize = k as usize;
if !(k_usize <= n && n <= 31) {
return Err(Error::InvalidShareCount { k, n });
}
let id = random_id();
let pool = non_s_index_pool();
let secret_s = Codex32String::from_seed(HRP, k_usize, &id, Fe::S, &bytes[..])?;
let mut defining: Vec<Codex32String> = Vec::with_capacity(k_usize);
defining.push(secret_s);
for pool_idx in pool.iter().take(k_usize - 1) {
let mut filler: Zeroizing<Vec<u8>> = Zeroizing::new(vec![0u8; bytes.len()]);
getrandom::fill(&mut filler[..]).expect("getrandom::fill must not fail");
let share = Codex32String::from_seed(HRP, k_usize, &id, *pool_idx, &filler[..])?;
defining.push(share);
}
let mut distributed: Vec<String> = Vec::with_capacity(n);
for share in defining.iter().skip(1) {
distributed.push(share.to_string());
}
for pool_idx in pool.iter().take(n).skip(k_usize - 1) {
let derived = Codex32String::interpolate_at(&defining, *pool_idx)?;
distributed.push(derived.to_string());
}
debug_assert_eq!(distributed.len(), n);
Ok(distributed)
}
pub fn combine_shares(shares: &[String]) -> Result<(Tag, Payload)> {
let parsed: Vec<Codex32String> = shares
.iter()
.map(|s| Codex32String::from_string(s.clone()).map_err(Error::Codex32))
.collect::<Result<Vec<_>>>()?;
let parsed: Vec<Codex32String> = parsed
.iter()
.map(|c| {
Codex32String::from_string(c.to_string().to_ascii_lowercase()).map_err(Error::Codex32)
})
.collect::<Result<Vec<_>>>()?;
if parsed.is_empty() {
return Err(Error::Codex32(crate::codex32::Error::ThresholdNotPassed {
threshold: 1,
n_shares: 0,
}));
}
let fields: Vec<(u8, u8)> = parsed
.iter()
.map(|c| {
let s = wire_string(c);
extract_wire_fields(&s).map(|f| (f.threshold_byte, f.share_index_byte))
})
.collect::<Result<Vec<_>>>()?;
if fields.iter().any(|&(_, idx)| idx == SHARE_INDEX_V01) {
return Err(Error::SecretShareSuppliedToCombine);
}
let k = (fields[0].0 - b'0') as usize;
if parsed.len() < k {
return Err(Error::Codex32(crate::codex32::Error::ThresholdNotPassed {
threshold: k,
n_shares: parsed.len(),
}));
}
for i in 0..fields.len() {
for j in (i + 1)..fields.len() {
if fields[i].1 == fields[j].1 {
let idx = Fe::from_char(fields[i].1 as char).map_err(Error::Codex32)?;
return Err(Error::Codex32(crate::codex32::Error::RepeatedIndex(idx)));
}
}
}
let k_set = &parsed[..k];
let secret = Codex32String::interpolate_at(k_set, Fe::S).map_err(Error::Codex32)?;
for j in k..parsed.len() {
let idx = Fe::from_char(fields[j].1 as char).map_err(Error::Codex32)?;
let derived = Codex32String::interpolate_at(k_set, idx).map_err(Error::Codex32)?;
if derived != parsed[j] {
return Err(Error::InconsistentShareSet);
}
}
let data: Zeroizing<Vec<u8>> = Zeroizing::new(secret.parts().data());
let payload = dispatch_payload(&data)?;
Ok((Tag::ENTR, payload))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_accepts_2_through_9() {
for k in 2u8..=9 {
let t =
Threshold::new(k).unwrap_or_else(|e| panic!("new({k}) should be Ok, got {e:?}"));
assert_eq!(t.get(), k);
}
}
#[test]
fn new_rejects_zero() {
assert!(matches!(Threshold::new(0), Err(Error::InvalidThreshold(0))));
}
#[test]
fn new_rejects_one() {
assert!(matches!(Threshold::new(1), Err(Error::InvalidThreshold(1))));
}
#[test]
fn new_rejects_ten() {
assert!(matches!(
Threshold::new(10),
Err(Error::InvalidThreshold(10))
));
}
#[test]
fn zero_const_get_is_zero() {
assert_eq!(Threshold::ZERO.get(), 0);
}
#[test]
fn new_five_get_is_five() {
assert_eq!(Threshold::new(5).unwrap().get(), 5);
}
use crate::codex32::{Codex32String, Fe};
use crate::consts::RESERVED_PREFIX;
use crate::encode::encode;
use crate::payload::Payload;
use crate::tag::Tag;
fn entr_p() -> Payload {
Payload::Entr(vec![0xCDu8; 16])
}
fn mnem_p() -> Payload {
Payload::Mnem {
language: 1,
entropy: vec![0xCDu8; 16],
}
}
fn share_header(s: &str) -> (char, char, String) {
let sep = s.rfind('1').unwrap();
let b = s.as_bytes();
let threshold = b[sep + 1] as char;
let id: String = s[sep + 2..sep + 6].to_string();
let index = b[sep + 6] as char;
(threshold, index, id)
}
#[test]
fn zero_share_is_byte_identical_to_encode_entr() {
let p = entr_p();
let shares = encode_shares(Tag::ENTR, Threshold::ZERO, 1, &p).unwrap();
assert_eq!(shares, vec![encode(Tag::ENTR, &p).unwrap()]);
}
#[test]
fn zero_share_is_byte_identical_to_encode_mnem() {
let p = mnem_p();
let shares = encode_shares(Tag::ENTR, Threshold::ZERO, 1, &p).unwrap();
assert_eq!(shares, vec![encode(Tag::ENTR, &p).unwrap()]);
}
#[test]
fn zero_share_requires_n_eq_1() {
let p = entr_p();
assert!(matches!(
encode_shares(Tag::ENTR, Threshold::ZERO, 2, &p),
Err(Error::InvalidShareCount { k: 0, n: 2 })
));
}
#[test]
fn encode_shares_2_of_3_shape() {
let p = entr_p();
let shares = encode_shares(Tag::ENTR, Threshold::new(2).unwrap(), 3, &p).unwrap();
assert_eq!(shares.len(), 3);
let mut indices = Vec::new();
let mut ids = Vec::new();
for s in &shares {
Codex32String::from_string(s.clone()).expect("each share must parse");
let (thr, idx, id) = share_header(s);
assert_eq!(thr, '2', "threshold char");
assert_ne!(idx, 's', "distributed share must not be index s");
indices.push(idx);
ids.push(id);
}
let mut sorted = indices.clone();
sorted.sort_unstable();
sorted.dedup();
assert_eq!(sorted.len(), indices.len(), "indices must be distinct");
assert!(ids.windows(2).all(|w| w[0] == w[1]), "id must be shared");
}
#[test]
fn encode_shares_rejects_n_below_k() {
let p = entr_p();
assert!(matches!(
encode_shares(Tag::ENTR, Threshold::new(2).unwrap(), 1, &p),
Err(Error::InvalidShareCount { k: 2, n: 1 })
));
}
#[test]
fn encode_shares_rejects_n_32() {
let p = entr_p();
assert!(matches!(
encode_shares(Tag::ENTR, Threshold::new(2).unwrap(), 32, &p),
Err(Error::InvalidShareCount { k: 2, n: 32 })
));
}
#[test]
fn encode_shares_id_not_in_blocklist() {
let p = entr_p();
for _ in 0..64 {
let shares = encode_shares(Tag::ENTR, Threshold::new(2).unwrap(), 2, &p).unwrap();
let (_, _, id) = share_header(&shares[0]);
let id_bytes: [u8; 4] = id.as_bytes().try_into().unwrap();
assert!(
!crate::consts::RESERVED_ID_BLOCKLIST.contains(&id_bytes),
"id {id:?} must not be in RESERVED_ID_BLOCKLIST"
);
}
}
#[test]
fn encode_shares_round_trip_via_interpolate_entr_and_mnem() {
for p in [entr_p(), mnem_p()] {
let secret_wire = crate::envelope::payload_wire_bytes(&p);
for k in 2u8..=9 {
let n = (k as usize) + 2; let shares = encode_shares(Tag::ENTR, Threshold::new(k).unwrap(), n, &p).unwrap();
assert_eq!(shares.len(), n);
let parsed: Vec<Codex32String> = shares
.iter()
.map(|s| Codex32String::from_string(s.clone()).unwrap())
.collect();
for subset in [&parsed[..k as usize], &parsed[n - k as usize..]] {
let recovered = Codex32String::interpolate_at(subset, Fe::S).unwrap();
assert_eq!(
recovered.parts().data(),
secret_wire[..],
"k={k} n={n} kind={:?} must recover secret wire bytes",
p.kind()
);
}
}
}
}
#[test]
fn combine_round_trip_entr_and_mnem_all_lengths() {
for ent_len in [16usize, 20, 24, 28, 32] {
let entr = Payload::Entr(vec![0x37u8; ent_len]);
let mnem = Payload::Mnem {
language: 7,
entropy: vec![0x91u8; ent_len],
};
for p in [entr, mnem] {
for k in 2u8..=9 {
let n = (k as usize) + 1;
let shares =
encode_shares(Tag::ENTR, Threshold::new(k).unwrap(), n, &p).unwrap();
for subset in [&shares[..k as usize], &shares[n - k as usize..]] {
let (tag, recovered) = combine_shares(subset).unwrap();
assert_eq!(tag, Tag::ENTR, "combine always returns Tag::ENTR");
assert_eq!(
recovered, p,
"k={k} n={n} ent_len={ent_len} must recover the exact payload"
);
}
}
}
}
}
#[test]
fn combine_rejects_below_threshold() {
let p = entr_p();
let shares = encode_shares(Tag::ENTR, Threshold::new(3).unwrap(), 4, &p).unwrap();
let err = combine_shares(&shares[..2]).unwrap_err();
assert!(
matches!(
err,
Error::Codex32(crate::codex32::Error::ThresholdNotPassed { .. })
),
"expected ThresholdNotPassed, got {err:?}"
);
}
#[test]
fn combine_rejects_duplicate_index() {
let p = entr_p();
let shares = encode_shares(Tag::ENTR, Threshold::new(2).unwrap(), 3, &p).unwrap();
let dup = vec![shares[0].clone(), shares[0].clone()];
assert!(matches!(
combine_shares(&dup),
Err(Error::Codex32(crate::codex32::Error::RepeatedIndex(_)))
));
}
#[test]
fn combine_rejects_secret_share_index_s() {
let bytes = crate::envelope::payload_wire_bytes(&entr_p());
let secret_s = Codex32String::from_seed(HRP, 2, "tst7", Fe::S, &bytes[..])
.unwrap()
.to_string();
let p = entr_p();
let shares = encode_shares(Tag::ENTR, Threshold::new(2).unwrap(), 2, &p).unwrap();
let with_secret = vec![secret_s, shares[0].clone()];
assert!(matches!(
combine_shares(&with_secret),
Err(Error::SecretShareSuppliedToCombine)
));
}
#[test]
fn combine_rejects_mismatched_threshold() {
let p = entr_p();
let set2 = encode_shares(Tag::ENTR, Threshold::new(2).unwrap(), 2, &p).unwrap();
let set3 = encode_shares(Tag::ENTR, Threshold::new(3).unwrap(), 3, &p).unwrap();
let mixed = vec![set2[0].clone(), set3[1].clone()];
let err = combine_shares(&mixed).unwrap_err();
assert!(
matches!(
err,
Error::Codex32(crate::codex32::Error::MismatchedThreshold(..))
),
"expected MismatchedThreshold, got {err:?}"
);
}
#[test]
fn combine_rejects_unparseable() {
let bad = vec!["not-an-ms1-string".to_string(), "also-bad".to_string()];
assert!(matches!(combine_shares(&bad), Err(Error::Codex32(_))));
}
fn nonstandard_entr_distributed(k: usize, n: usize, entropy_len: usize) -> Vec<String> {
let mut bytes = vec![RESERVED_PREFIX];
bytes.extend(std::iter::repeat(0xCDu8).take(entropy_len));
let id = "tst7";
let secret_s = Codex32String::from_seed(HRP, k, id, Fe::S, &bytes[..]).unwrap();
let pool = non_s_index_pool();
let mut defining = vec![secret_s];
for pidx in pool.iter().take(k - 1) {
let filler = vec![0u8; bytes.len()];
defining.push(Codex32String::from_seed(HRP, k, id, *pidx, &filler[..]).unwrap());
}
let mut out = Vec::new();
for s in defining.iter().skip(1) {
out.push(s.to_string());
}
for pidx in pool.iter().take(n).skip(k - 1) {
out.push(
Codex32String::interpolate_at(&defining, *pidx)
.unwrap()
.to_string(),
);
}
out
}
#[test]
fn combine_rejects_nonstandard_entr_length_not_panics() {
let shares = nonstandard_entr_distributed(2, 2, 17);
let res = combine_shares(&shares);
assert!(
matches!(res, Err(Error::PayloadLengthMismatch { got: 17, .. })),
"expected PayloadLengthMismatch{{got:17}}, got {res:?}"
);
}
#[test]
fn dispatch_payload_validates_entr_length() {
let mut bad = vec![RESERVED_PREFIX];
bad.extend(std::iter::repeat(0xCDu8).take(17));
assert!(
matches!(
dispatch_payload(&bad),
Err(Error::PayloadLengthMismatch { got: 17, .. })
),
"non-standard Entr length must Err"
);
let mut good = vec![RESERVED_PREFIX];
good.extend(std::iter::repeat(0xCDu8).take(16));
assert!(
matches!(dispatch_payload(&good), Ok(Payload::Entr(_))),
"standard Entr length must Ok"
);
}
fn same_id_2_of_n(id: &str, secret_byte: u8, filler_byte: u8, n: usize) -> Vec<String> {
let k = 2usize;
let mut bytes = vec![RESERVED_PREFIX];
bytes.extend(std::iter::repeat(secret_byte).take(16));
let secret_s = Codex32String::from_seed(HRP, k, id, Fe::S, &bytes[..]).unwrap();
let pool = non_s_index_pool();
let mut defining = vec![secret_s];
for pidx in pool.iter().take(k - 1) {
let filler = vec![filler_byte; bytes.len()];
defining.push(Codex32String::from_seed(HRP, k, id, *pidx, &filler[..]).unwrap());
}
let mut out = Vec::new();
for s in defining.iter().skip(1) {
out.push(s.to_string());
}
for pidx in pool.iter().take(n).skip(k - 1) {
out.push(
Codex32String::interpolate_at(&defining, *pidx)
.unwrap()
.to_string(),
);
}
out
}
#[test]
fn combine_inconsistent_same_id_set_rejected() {
let set_a = same_id_2_of_n("aaaa", 0x11, 0x22, 3);
let set_b = same_id_2_of_n("aaaa", 0x33, 0x44, 3);
let mixed = vec![set_a[0].clone(), set_a[1].clone(), set_b[2].clone()];
let res = combine_shares(&mixed);
assert!(
matches!(res, Err(Error::InconsistentShareSet)),
"expected InconsistentShareSet for a same-id mixed-polynomial set, got {res:?}"
);
}
#[test]
fn combine_valid_exactly_k_unchanged() {
let p = Payload::Entr(vec![0xCDu8; 16]);
let shares = encode_shares(Tag::ENTR, Threshold::new(2).unwrap(), 3, &p).unwrap();
let (tag, recovered) = combine_shares(&shares[..2]).unwrap();
assert_eq!(tag, Tag::ENTR);
assert_eq!(
recovered, p,
"exactly-k combine must recover the exact payload"
);
}
#[test]
fn combine_valid_n_gt_k_all_consistent() {
let p = Payload::Entr(vec![0xCDu8; 16]);
let shares = encode_shares(Tag::ENTR, Threshold::new(2).unwrap(), 3, &p).unwrap();
let (tag, recovered) = combine_shares(&shares).unwrap();
assert_eq!(tag, Tag::ENTR);
assert_eq!(
recovered, p,
"n>k all-consistent combine must recover the exact payload"
);
}
#[test]
fn combine_inconsistent_extra_share_rejected() {
let set_a = same_id_2_of_n("cqcq", 0x55, 0x66, 4);
let set_b = same_id_2_of_n("cqcq", 0x77, 0x88, 4);
let mixed = vec![
set_a[0].clone(),
set_a[1].clone(),
set_b[2].clone(),
set_a[3].clone(),
];
let res = combine_shares(&mixed);
assert!(
matches!(res, Err(Error::InconsistentShareSet)),
"expected InconsistentShareSet for a consistent-k + inconsistent-extra set, got {res:?}"
);
}
}