#![allow(clippy::suspicious_arithmetic_impl)]
#![allow(missing_docs)]
#![allow(clippy::precedence)]
#![allow(clippy::needless_lifetimes)]
mod checksum;
mod field;
pub use checksum::Engine as ChecksumEngine;
pub use field::Fe;
use std::{cmp, fmt};
#[derive(Debug)]
pub enum Error {
Field(field::Error),
IdNotLength4(usize),
IncompleteGroup(usize),
InvalidLength(usize),
InvalidChar(char),
InvalidCase(Case, char),
InvalidChecksum {
checksum: &'static str,
string: String,
},
InvalidThreshold(char),
InvalidThresholdN(usize),
InvalidShareIndex(Fe),
MismatchedLength(usize, usize),
MismatchedHrp(String, String),
MismatchedThreshold(usize, usize),
MismatchedId(String, String),
RepeatedIndex(Fe),
ThresholdNotPassed { threshold: usize, n_shares: usize },
}
impl From<field::Error> for Error {
fn from(e: field::Error) -> Error {
Error::Field(e)
}
}
#[derive(Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Hash, Debug)]
pub enum Case {
Lower,
Upper,
}
#[derive(Clone, PartialEq, Eq, Hash, zeroize::ZeroizeOnDrop)]
pub struct Codex32String(String);
impl fmt::Display for Codex32String {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
impl fmt::Debug for Codex32String {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Codex32String([REDACTED; {} chars])",
self.0.chars().count()
)
}
}
impl Codex32String {
fn sanity_check(&self) -> Result<(), Error> {
let parts = self.parts_inner()?;
let incomplete_group = (parts.payload.len() * 5) % 8;
if incomplete_group > 4 {
return Err(Error::IncompleteGroup(incomplete_group));
}
Ok(())
}
pub fn from_unchecksummed_string(mut s: String) -> Result<Self, Error> {
let (len, mut checksum) = if s.len() < 81 {
(13, checksum::Engine::new_codex32_short())
} else {
(15, checksum::Engine::new_codex32_long())
};
s.reserve_exact(len);
let (hrp, real_string) = match s.rsplit_once('1') {
Some((s1, s2)) => (s1, s2),
None => ("", &s[..]),
};
checksum.input_hrp(hrp)?;
checksum.input_data_str(real_string)?;
for ch in checksum.into_residue() {
s.push(ch.to_char());
}
let ret = Codex32String(s);
ret.sanity_check()?;
Ok(ret)
}
pub fn from_string(s: String) -> Result<Self, Error> {
let (name, mut checksum) = if s.len() >= 48 && s.len() < 94 {
("short", checksum::Engine::new_codex32_short())
} else if s.len() >= 125 && s.len() < 128 {
("long", checksum::Engine::new_codex32_long())
} else {
return Err(Error::InvalidLength(s.len()));
};
let (hrp, real_string) = match s.rsplit_once('1') {
Some((s1, s2)) => (s1, s2),
None => ("", &s[..]),
};
checksum.input_hrp(hrp)?;
checksum.input_data_str(real_string)?;
if !checksum.is_valid() {
return Err(Error::InvalidChecksum {
checksum: name,
string: s,
});
}
let ret = Codex32String(s);
ret.sanity_check()?;
Ok(ret)
}
fn parts_inner(&self) -> Result<Parts, Error> {
let (hrp, s) = match self.0.rsplit_once('1') {
Some((s1, s2)) => (s1, s2),
None => ("", &self.0[..]),
};
let checksum_len = if self.0.len() > 93 { 15 } else { 13 };
let ret = Parts {
hrp,
threshold: match s.as_bytes()[0] {
b'0' => 0,
b'2' => 2,
b'3' => 3,
b'4' => 4,
b'5' => 5,
b'6' => 6,
b'7' => 7,
b'8' => 8,
b'9' => 9,
_ => return Err(Error::InvalidThreshold(s.as_bytes()[0].into())),
},
id: &s[1..5],
share_index: Fe::from_char(s.as_bytes()[5].into()).unwrap(),
payload: &s[6..s.len() - checksum_len],
checksum: &s[s.len() - checksum_len..],
};
if ret.threshold == 0 && ret.share_index != Fe::S {
return Err(Error::InvalidShareIndex(ret.share_index));
}
Ok(ret)
}
pub fn parts(&self) -> Parts {
self.parts_inner().unwrap()
}
pub fn interpolate_at(shares: &[Codex32String], target: Fe) -> Result<Codex32String, Error> {
if shares.is_empty() {
return Err(Error::ThresholdNotPassed {
threshold: 1,
n_shares: 0,
});
}
let mut indices = Vec::with_capacity(shares.len());
let s0_parts = shares[0].parts();
if s0_parts.threshold > shares.len() {
return Err(Error::ThresholdNotPassed {
threshold: s0_parts.threshold,
n_shares: shares.len(),
});
}
for share in shares {
let parts = share.parts();
if shares[0].0.len() != share.0.len() {
return Err(Error::MismatchedLength(shares[0].0.len(), share.0.len()));
}
if s0_parts.hrp != parts.hrp {
return Err(Error::MismatchedHrp(s0_parts.hrp.into(), parts.hrp.into()));
}
if s0_parts.threshold != parts.threshold {
return Err(Error::MismatchedThreshold(
s0_parts.threshold,
parts.threshold,
));
}
if s0_parts.id != parts.id {
return Err(Error::MismatchedId(s0_parts.id.into(), parts.id.into()));
}
indices.push(parts.share_index);
}
let mut mult = Fe::P;
for i in 0..shares.len() {
if indices[i] == target {
return Ok(shares[i].clone());
}
mult *= indices[i] + target;
}
let payload_len = 6 + s0_parts.payload.len() + s0_parts.checksum.len();
let hrp_len = shares[0].0.len() - payload_len;
let mut result = vec![Fe::Q; payload_len];
for i in 0..shares.len() {
let mut inv = Fe::P;
for j in 0..shares.len() {
inv *= indices[j]
+ if i == j {
target
} else {
if indices[i] == indices[j] {
return Err(Error::RepeatedIndex(indices[i]));
}
indices[i]
}
}
for (j, res_j) in result.iter_mut().enumerate() {
let ch_at_i = char::from(shares[i].0.as_bytes()[hrp_len + j]);
*res_j += mult / inv * Fe::from_char(ch_at_i).unwrap();
}
}
let mut s = s0_parts.hrp.to_owned();
s.push('1');
if s0_parts.hrp.chars().all(char::is_uppercase) {
s.extend(
result
.into_iter()
.map(Fe::to_char)
.map(|c| c.to_ascii_uppercase()),
);
} else {
s.extend(result.into_iter().map(Fe::to_char));
}
Ok(Codex32String(s))
}
pub fn from_seed(
hrp: &str,
threshold: usize,
id: &str,
share_idx: Fe,
data: &[u8],
) -> Result<Codex32String, Error> {
if id.len() != 4 {
return Err(Error::IdNotLength4(id.len()));
}
let mut ret = String::with_capacity(hrp.len() + 6 + (data.len() * 8 + 4) / 5);
ret.push_str(hrp);
ret.push('1');
let k = match threshold {
0 => Fe::_0,
2 => Fe::_2,
3 => Fe::_3,
4 => Fe::_4,
5 => Fe::_5,
6 => Fe::_6,
7 => Fe::_7,
8 => Fe::_8,
9 => Fe::_9,
x => return Err(Error::InvalidThresholdN(x)),
};
ret.push(k.to_char());
ret.push_str(id);
ret.push(share_idx.to_char());
let mut next_u5 = 0;
let mut rem = 0;
for byte in data {
let u5 = (next_u5 << (5 - rem)) | byte >> (3 + rem);
ret.push(Fe::from_u8(u5).unwrap().to_char());
next_u5 = byte & ((1 << (3 + rem)) - 1);
if rem >= 2 {
ret.push(Fe::from_u8(next_u5 >> (rem - 2)).unwrap().to_char());
next_u5 &= (1 << (rem - 2)) - 1;
}
rem = (rem + 8) % 5;
}
if rem > 0 {
ret.push(Fe::from_u8(next_u5 << (5 - rem)).unwrap().to_char());
}
let mut checksum = if data.len() < 51 {
checksum::Engine::new_codex32_short()
} else {
checksum::Engine::new_codex32_long()
};
checksum.input_hrp(hrp)?;
checksum.input_data_str(&ret[hrp.len() + 1..])?;
checksum.input_own_target();
ret.extend(checksum.into_residue().into_iter().map(Fe::to_char));
let mut checksum = checksum::Engine::new_codex32_short();
checksum.input_hrp(hrp)?;
checksum.input_data_str(&ret[hrp.len() + 1..])?;
Ok(Codex32String(ret))
}
}
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
pub struct Parts<'s> {
hrp: &'s str,
threshold: usize,
id: &'s str,
share_index: Fe,
payload: &'s str,
checksum: &'s str,
}
impl<'s> Parts<'s> {
pub fn data(&self) -> Vec<u8> {
let mut ret = Vec::with_capacity((self.payload.len() * 5 + 7) / 8);
let mut next_byte = 0;
let mut rem = 0;
for ch in self.payload.chars() {
let fe = Fe::from_char(ch).unwrap(); match rem.cmp(&3) {
cmp::Ordering::Less => {
next_byte |= fe.to_u8() << (3 - rem);
}
cmp::Ordering::Equal => {
ret.push(next_byte | fe.to_u8());
next_byte = 0;
}
cmp::Ordering::Greater => {
let overshoot = rem - 3;
assert!(overshoot > 0);
ret.push(next_byte | (fe.to_u8() >> overshoot));
next_byte = fe.to_u8() << (8 - overshoot);
}
}
rem = (rem + 5) % 8;
}
debug_assert!(rem <= 4); ret
}
}