#![deny(missing_docs)]
#![deny(unsafe_code)]
use std::io::Write;
const TAG: u8 = 0x5;
const MAX_SECRET_LEN: usize = 4095;
#[derive(Debug, PartialEq)]
pub enum Err {
SecretLen(usize),
Capacity,
Write,
Encoding(std::string::FromUtf8Error),
BadTag(u8),
TruncatedSecret(usize, usize),
}
impl std::error::Error for Err {}
impl std::fmt::Display for Err {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Err::SecretLen(len) => write!(f, "secret length greater than {MAX_SECRET_LEN} bytes: {len}"),
Err::Capacity => write!(f, "not enough text capacity to encode secret"),
Err::Write => write!(f, "buffer write failed"),
Err::Encoding(err) => write!(f, "encode error: {err}"),
Err::BadTag(val) => write!(f, "invalid tag: got 0x{val:x}, expected 0x{TAG:x}"),
Err::TruncatedSecret(got, exp) => write!(f, "truncated secret: got {got} bytes, expected {exp} bytes"),
}
}
}
fn is_encodable(c: char) -> bool {
matches!(c, 'A' | 'B' | 'C' | 'E' | 'H' | 'I' | 'J' | 'K' | 'M' | 'O' | 'P' | 'S' | 'T' | 'X' | 'a' | 'c' | 'e' | 'i' | 'j' | 'o' | 'p' | 'q' | 's' | 'w' | 'x' | 'y' | 'А' | 'В' | 'С' | 'Е' | 'Н' | 'І' | 'Ј' | 'К' | 'М' | 'О' | 'Р' | 'Ѕ' | 'Т' | 'Х' | 'а' | 'с' | 'е' | 'і' | 'ј' | 'о' | 'р' | 'ԛ' | 'ѕ' | 'ԝ' | 'х' | 'у')
}
fn encode_char(c: char, bit: bool) -> char {
match (c, bit) {
('A' | 'А', true) => 'А',
('A' | 'А', false) => 'A',
('B' | 'В', true) => 'В',
('B' | 'В', false) => 'B',
('C' | 'С', true) => 'С',
('C' | 'С', false) => 'C',
('E' | 'Е', true) => 'Е',
('E' | 'Е', false) => 'E',
('H' | 'Н', true) => 'Н',
('H' | 'Н', false) => 'H',
('I' | 'І', true) => 'І',
('I' | 'І', false) => 'I',
('J' | 'Ј', true) => 'Ј',
('J' | 'Ј', false) => 'J',
('K' | 'К', true) => 'К',
('K' | 'К', false) => 'K',
('M' | 'М', true) => 'М',
('M' | 'М', false) => 'M',
('O' | 'О', true) => 'О',
('O' | 'О', false) => 'O',
('P' | 'Р', true) => 'Р',
('P' | 'Р', false) => 'P',
('S' | 'Ѕ', true) => 'Ѕ',
('S' | 'Ѕ', false) => 'S',
('T' | 'Т', true) => 'Т',
('T' | 'Т', false) => 'T',
('X' | 'Х', true) => 'Х',
('X' | 'Х', false) => 'X',
('a' | 'а', true) => 'а',
('a' | 'а', false) => 'a',
('c' | 'с', true) => 'с',
('c' | 'с', false) => 'c',
('e' | 'е', true) => 'е',
('e' | 'е', false) => 'e',
('i' | 'і', true) => 'і',
('i' | 'і', false) => 'i',
('j' | 'ј', true) => 'ј',
('j' | 'ј', false) => 'j',
('o' | 'о', true) => 'о',
('o' | 'о', false) => 'o',
('p' | 'р', true) => 'р',
('p' | 'р', false) => 'p',
('q' | 'ԛ', true) => 'ԛ',
('q' | 'ԛ', false) => 'q',
('s' | 'ѕ', true) => 'ѕ',
('s' | 'ѕ', false) => 's',
('w' | 'ԝ', true) => 'ԝ',
('w' | 'ԝ', false) => 'w',
('x' | 'х', true) => 'х',
('x' | 'х', false) => 'x',
('y' | 'у', true) => 'у',
('y' | 'у', false) => 'y',
_ => c,
}
}
pub fn encode_vec(secret: &[u8], text: &str) -> Result<Vec<u8>, Err> {
if secret.len() > MAX_SECRET_LEN {
return Err(Err::SecretLen(secret.len()));
}
let tag_byte = (((secret.len() & 0xf) << 4) as u8) | TAG;
let mut src = vec![tag_byte, (secret.len() >> 4) as u8];
src.extend(secret);
let mut src_pos: usize = 0; let src_bit_len = src.len() << 3;
let mut dst = Vec::new();
for c in text.chars() {
let c = if src_pos < src_bit_len && is_encodable(c) {
let bit = ((src[src_pos >> 3] >> (src_pos & 7)) & 1) == 1;
src_pos += 1;
encode_char(c, bit)
} else {
c };
match write!(dst, "{c}") {
Ok(_) => {},
Err(_) => return Err(Err::Write),
}
}
if src_pos != src_bit_len {
return Err(Err::Capacity)
}
Ok(dst)
}
pub fn encode(secret: &[u8], text: &str) -> Result<String, Err> {
match encode_vec(secret, text) {
Ok(vec) => match String::from_utf8(vec) {
Ok(s) => Ok(s),
Err(err) => Err(Err::Encoding(err)),
}
Err(err) => Err(err),
}
}
pub fn capacity(text: &str) -> usize {
if let len = text.chars().filter(|c| is_encodable(*c)).count() && len > 16 {
(len / 8) - 2
} else {
0
}
}
fn decode_char(c: char) -> Option<bool> {
match c {
'A' | 'B' | 'C' | 'E' | 'H' | 'I' | 'J' | 'K' | 'M' | 'O' | 'P' | 'S' | 'T' | 'X' | 'a' | 'c' | 'e' | 'i' | 'j' | 'o' | 'p' | 'q' | 's' | 'w' | 'x' | 'y' => Some(false), 'А' | 'В' | 'С' | 'Е' | 'Н' | 'І' | 'Ј' | 'К' | 'М' | 'О' | 'Р' | 'Ѕ' | 'Т' | 'Х' | 'а' | 'с' | 'е' | 'і' | 'ј' | 'о' | 'р' | 'ԛ' | 'ѕ' | 'ԝ' | 'х' | 'у' => Some(true), _ => None,
}
}
pub fn decode_vec(text: &str) -> Result<Vec<u8>, Err> {
let mut pos: usize = 0; let mut byte: u8 = 0; let mut exp_len: usize = 0;
let mut dst = Vec::new(); for c in text.chars() {
if let Some(bit) = decode_char(c) {
byte |= u8::from(bit) << pos;
pos += 1;
if pos == 8 {
dst.push(byte); byte = 0; pos = 0;
match (exp_len, dst.len()) {
(0, 2) => {
let tag = dst[0] & 0xf;
exp_len = ((dst[0] >> 4) as usize) | ((dst[1] as usize) << 8);
if tag != TAG {
return Err(Err::BadTag(tag));
} else if exp_len == 0 {
return Ok(dst);
}
dst.clear(); },
(_, dst_len) if dst_len == exp_len => break,
(_, _) => {},
};
}
}
}
if dst.len() == exp_len {
Ok(dst)
} else {
Err(Err::TruncatedSecret(dst.len(), exp_len))
}
}
pub fn decode(text: &str) -> Result<String, Err> {
match decode_vec(text) {
Ok(vec) => match String::from_utf8(vec) {
Ok(s) => Ok(s),
Err(err) => Err(Err::Encoding(err)),
},
Err(err) => Err(err),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encode_vec_fail() {
let tests = vec![(
"long secret", [0u8; MAX_SECRET_LEN+1].to_vec(), "aaaaaaaa aaaaaaaa aaaaaaaa", Err(Err::SecretLen(MAX_SECRET_LEN+1)),
), (
"short text", vec![0u8], "aaaaaaaa aaaaaaaa", Err(Err::Capacity),
)];
for (name, msg, text, exp) in tests {
assert_eq!(encode_vec(&msg, text), exp, "{name}");
}
}
#[test]
fn test_encode_vec_pass() {
let tests = vec![(
"aaa", vec![1], "aaaaaaaa aaaaaaaa aaaaaaaa", vec![ 0xd0, 0xb0, 0x61, 0xd0, 0xb0, 0x61, 0xd0, 0xb0, 0x61, 0x61, 0x61,
0x20,
0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61,
0x20,
0xd0, 0xb0, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61,
],
), (
"acc", vec![1], "aaaaaaaa cccccccc cccccccc", vec![ 0xd0, 0xb0, 0x61, 0xd0, 0xb0, 0x61, 0xd0, 0xb0, 0x61, 0x61, 0x61,
0x20,
0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63,
0x20,
0xd1, 0x81, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63,
],
), (
"ace", vec![1], "aaaaaaaa cccccccc eeeeeeee", vec![ 0xd0, 0xb0, 0x61, 0xd0, 0xb0, 0x61, 0xd0, 0xb0, 0x61, 0x61, 0x61,
0x20,
0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63,
0x20,
0xd0, 0xb5, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65,
],
), (
"aop", vec![1], "aaaaaaaa oooooooo pppppppp", vec![ 0xd0, 0xb0, 0x61, 0xd0, 0xb0, 0x61, 0xd0, 0xb0, 0x61, 0x61, 0x61,
0x20,
0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f,
0x20,
0xd1, 0x80, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70,
],
), (
"pxy", vec![1], "pppppppp xxxxxxxx yyyyyyyy", vec![ 0xd1, 0x80, 0x70, 0xd1, 0x80, 0x70, 0xd1, 0x80, 0x70, 0x70, 0x70,
0x20,
0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78,
0x20,
0xd1, 0x83, 0x79, 0x79, 0x79, 0x79, 0x79, 0x79, 0x79,
],
), (
"ijqs", vec![1, 1], "iiiiiiii jjjjjjjj qqqqqqqq ssssssss", vec![ 0xd1, 0x96, 0x69, 0xd1, 0x96, 0x69, 0x69, 0xd1, 0x96, 0x69, 0x69,
0x20,
0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a,
0x20,
0xd4, 0x9b, 0x71, 0x71, 0x71, 0x71, 0x71, 0x71, 0x71,
0x20,
0xd1, 0x95, 0x73, 0x73, 0x73, 0x73, 0x73, 0x73, 0x73,
],
), (
"wwww", vec![1, 1], "wwwwwwww wwwwwwww wwwwwwww wwwwwwww", vec![ 0xd4, 0x9d, 0x77, 0xd4, 0x9d, 0x77, 0x77, 0xd4, 0x9d, 0x77, 0x77,
0x20,
0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77,
0x20,
0xd4, 0x9d, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77,
0x20,
0xd4, 0x9d, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77,
],
), (
"ABCE", vec![1, 1], "AAAAAAAA BBBBBBBB CCCCCCCC EEEEEEEE", vec![ 0xd0, 0x90, 0x41, 0xd0, 0x90, 0x41, 0x41, 0xd0, 0x90, 0x41, 0x41,
0x20,
0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42,
0x20,
0xd0, 0xa1, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43,
0x20,
0xd0, 0x95, 0x45, 0x45, 0x45, 0x45, 0x45, 0x45, 0x45,
],
), (
"HIJKM", vec![1, 1, 1], "HHHHHHHH IIIIIIII JJJJJJJJ KKKKKKKK MMMMMMMM", vec![ 0xd0, 0x9d, 0x48, 0xd0, 0x9d, 0x48, 0xd0, 0x9d, 0xd0, 0x9d, 0x48, 0x48,
0x20,
0x49, 0x49, 0x49, 0x49, 0x49, 0x49, 0x49, 0x49,
0x20,
0xd0, 0x88, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a,
0x20,
0xd0, 0x9a, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b,
0x20,
0xd0, 0x9c, 0x4d, 0x4d, 0x4d, 0x4d, 0x4d, 0x4d, 0x4d,
],
), (
"OPSTX", vec![1, 1, 1], "OOOOOOOO PPPPPPPP SSSSSSSS TTTTTTTT XXXXXXXX", vec![ 0xd0, 0x9e, 0x4f, 0xd0, 0x9e, 0x4f, 0xd0, 0x9e, 0xd0, 0x9e, 0x4f, 0x4f,
0x20,
0x50, 0x50, 0x50, 0x50, 0x50, 0x50, 0x50, 0x50,
0x20,
0xd0, 0x85, 0x53, 0x53, 0x53, 0x53, 0x53, 0x53, 0x53,
0x20,
0xd0, 0xa2, 0x54, 0x54, 0x54, 0x54, 0x54, 0x54, 0x54,
0x20,
0xd0, 0xa5, 0x58, 0x58, 0x58, 0x58, 0x58, 0x58, 0x58,
],
)];
for (name, secret, text, exp) in tests {
assert_eq!(encode_vec(&secret, text), Ok(exp), "{name}");
}
}
#[test]
fn test_capacity() {
let tests = vec![
("", 0),
("aaaa", 0),
("aaaaaaaa aaaaaaaa", 0),
("aaaaaaaa aaaaaaaa aaaaaaa", 0),
("aaaaaaaa aaaaaaaa aaaaaaaa", 1),
("aaaaaaaa aaaaaaaa aaaaaaaa aa", 1),
("aaaaaaaa aaaaaaaa aaaaaaaa a", 1),
("aaaaaaaa aaaaaaaa aaaaaaaa aaaaaaaa", 2),
("aaaaaaaa aaaaaaaa aaaaaaaa aaaaaaaa ", 2),
];
for (s, exp) in tests {
assert_eq!(capacity(s), exp, "s = {s}");
}
}
#[test]
fn test_decode_vec_fail() {
let invalid_len_str = String::from_utf8(vec![ 0xd0, 0x9e, 0x4f, 0xd0, 0x9e, 0x4f, 0xd0, 0x9e, 0xd0, 0x9e, 0x4f, 0x4f,
0x20,
0x50, 0x50, 0x50, 0x50, 0x50, 0x50, 0x50, 0x50,
0x20,
0xd0, 0x85, 0x53, 0x53, 0x53, 0x53, 0x53, 0x53, 0x53,
0x20,
0xd0, 0xa2, 0x54, 0x54, 0x54, 0x54, 0x54, 0x54, 0x54,
0x20,
]).unwrap();
let tests = vec![(
"invalid tag", "aaaaaaaa aaaaaaaa aaaaaaaa", Err(Err::BadTag(0)),
), (
"invalid length", invalid_len_str.as_str(),
Err(Err::TruncatedSecret(2, 3)),
)];
for (name, text, exp) in tests {
assert_eq!(decode_vec(text), exp, "{name}");
}
}
#[test]
fn test_decode_vec_pass() {
let trips_str = String::from_utf8(vec![
0xd0, 0x9e, 0x4f, 0xd0, 0x9e, 0x4f, 0xd0, 0x9e, 0xd0, 0x9e, 0x4f, 0x4f,
0x20,
0x50, 0x50, 0x50, 0x50, 0x50, 0x50, 0x50, 0x50,
0x20,
0xd0, 0x85, 0x53, 0x53, 0x53, 0x53, 0x53, 0x53, 0x53,
0x20,
0xd0, 0xa2, 0x54, 0x54, 0x54, 0x54, 0x54, 0x54, 0x54,
0x20,
0xd0, 0xa5, 0x58, 0x58, 0x58, 0x58, 0x58, 0x58, 0x58,
]).unwrap();
let tests = vec![(
"none", "", Ok(Vec::<u8>::new()), ), (
"trips", trips_str.as_str(), Ok(vec![1, 1, 1]),
)];
for (name, text, exp) in tests {
assert_eq!(decode_vec(text), exp, "{name}");
}
}
}