#![deny(missing_docs)]
#![deny(unsafe_code)]
use std::io::Write;
const TAG: u8 = 0x5;
const MAX_SECRET_LEN: usize = 4095;
#[derive(Clone, 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 String::from_utf8(encode_vec(secret, text)?) {
Ok(s) => Ok(s),
Err(err) => Err(Err::Encoding(err)),
}
}
pub fn capacity(text: &str) -> usize {
let len = text.chars().filter(|c| is_encodable(*c)).count();
if 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));
}
dst.clear(); if exp_len == 0 {
return Ok(dst);
}
},
(_, 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 String::from_utf8(decode_vec(text)?) {
Ok(s) => Ok(s),
Err(err) => Err(Err::Encoding(err)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_err_fmt() {
let bad_utf8 = vec![0x80u8];
let from_err = String::from_utf8(bad_utf8).unwrap_err();
let tests = vec![
(Err::SecretLen(4096), "secret length greater than 4095 bytes: 4096"),
(Err::Capacity, "not enough text capacity to encode secret"),
(Err::Write, "buffer write failed"),
(Err::Encoding(from_err), "encode error: invalid utf-8 sequence of 1 bytes from index 0"),
(Err::BadTag(0), "invalid tag: got 0x0, expected 0x5"),
(Err::TruncatedSecret(0, 1), "truncated secret: got 0 bytes, expected 1 bytes"),
];
for (err, exp) in tests {
assert_eq!(err.to_string(), exp, "{err:?}");
}
}
#[test]
fn test_encode_char() {
let tests = vec![
('A', true, 'А'),
('А', true, 'А'),
('A', false, 'A'),
('А', false, 'A'),
('B', true, 'В'),
('В', true, 'В'),
('B', false, 'B'),
('В', false, 'B'),
('C', true, 'С'),
('С', true, 'С'),
('C', false, 'C'),
('С', false, 'C'),
('E', true, 'Е'),
('Е', true, 'Е'),
('E', false, 'E'),
('Е', false, 'E'),
('H', true, 'Н'),
('Н', true, 'Н'),
('H', false, 'H'),
('Н', false, 'H'),
('I', true, 'І'),
('І', true, 'І'),
('I', false, 'I'),
('І', false, 'I'),
('J', true, 'Ј'),
('Ј', true, 'Ј'),
('J', false, 'J'),
('Ј', false, 'J'),
('K', true, 'К'),
('К', true, 'К'),
('K', false, 'K'),
('К', false, 'K'),
('M', true, 'М'),
('М', true, 'М'),
('M', false, 'M'),
('М', false, 'M'),
('O', true, 'О'),
('О', true, 'О'),
('O', false, 'O'),
('О', false, 'O'),
('P', true, 'Р'),
('Р', true, 'Р'),
('P', false, 'P'),
('Р', false, 'P'),
('S', true, 'Ѕ'),
('Ѕ', true, 'Ѕ'),
('S', false, 'S'),
('Ѕ', false, 'S'),
('T', true, 'Т'),
('Т', true, 'Т'),
('T', false, 'T'),
('Т', false, 'T'),
('X', true, 'Х'),
('Х', true, 'Х'),
('X', false, 'X'),
('Х', false, 'X'),
('a', true, 'а'),
('а', true, 'а'),
('a', false, 'a'),
('а', false, 'a'),
('c', true, 'с'),
('с', true, 'с'),
('c', false, 'c'),
('с', false, 'c'),
('e', true, 'е'),
('е', true, 'е'),
('e', false, 'e'),
('е', false, 'e'),
('i', true, 'і'),
('і', true, 'і'),
('i', false, 'i'),
('і', false, 'i'),
('j', true, 'ј'),
('ј', true, 'ј'),
('j', false, 'j'),
('ј', false, 'j'),
('o', true, 'о'),
('о', true, 'о'),
('o', false, 'o'),
('о', false, 'o'),
('p', true, 'р'),
('р', true, 'р'),
('p', false, 'p'),
('р', false, 'p'),
('q', true, 'ԛ'),
('ԛ', true, 'ԛ'),
('q', false, 'q'),
('ԛ', false, 'q'),
('s', true, 'ѕ'),
('ѕ', true, 'ѕ'),
('s', false, 's'),
('ѕ', false, 's'),
('w', true, 'ԝ'),
('ԝ', true, 'ԝ'),
('w', false, 'w'),
('ԝ', false, 'w'),
('x', true, 'х'),
('х', true, 'х'),
('x', false, 'x'),
('х', false, 'x'),
('y', true, 'у'),
('у', true, 'у'),
('y', false, 'y'),
('у', false, 'y'),
('z', false, 'z'),
];
for (c, bit, exp) in tests {
assert_eq!(encode_char(c, bit), exp, "{} (U+{:04x})", c, u32::from(c));
}
}
#[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_encode_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(&msg, text), exp, "{name}");
}
}
#[test]
fn test_encode_pass() {
let tests = vec![(
"aaa", b"A".to_vec(), "aaaaaaaa aaaaaaaa aaaaaaaa", "аaаaаaaa aaaaaaaa аaaaaaаa".to_string(), )];
for (name, secret, text, exp) in tests {
assert_eq!(encode(&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}");
}
}
#[test]
fn test_decode_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(text), exp, "{name}");
}
}
#[test]
fn test_decode_pass() {
let tests = vec![(
"empty", "аaаaaaaa aaaaaaaa".to_string(), "".to_string(), ), (
"A", "аaаaаaaa aaaaaaaa аaaaaaаa".to_string(), "A".to_string(), )];
for (name, text, exp) in tests {
assert_eq!(decode(&text), Ok(exp), "{name}");
}
}
}