use super::diagram::*;
use bitcoin::hashes::{sha256, Hash};
pub const CELL_CHARS_LIMIT: usize = 50;
#[derive(Debug)]
pub struct ComplexDiagram {
data: [[String; 7]; 7],
}
impl Diagram for ComplexDiagram {
type Item = String;
fn new() -> Self {
let data = core::array::from_fn(|_| core::array::from_fn(|_| String::new()));
ComplexDiagram { data }
}
fn is_empty(&self) -> bool {
self.data.iter().all(|row| row.iter().all(|v| v.is_empty()))
}
fn from_items(mut items: Vec<String>, indices: &[(usize, usize)]) -> DiagramResult<Self> {
if items.is_empty() || items.iter().any(|s| s.is_empty()) {
return Err(DiagramError::InvalidParameter(
"items cannot contains empty value.",
));
}
if indices.is_empty() || indices.len() != items.len() {
return Err(DiagramError::InvalidParameter(
"indices len should equal to items len.",
));
}
let mut diagram = Self::new();
items.reverse();
indices.iter().for_each(|&(row, col)| {
diagram.data[row][col] = items.pop().unwrap_or_default();
});
Ok(diagram)
}
fn set(&mut self, str: String, row: usize, col: usize) -> DiagramResult<()> {
if str.chars().count() > CELL_CHARS_LIMIT {
return Err(DiagramError::Overflows("chars count <= 50"));
}
debug_assert!(str.len() < u8::MAX as usize);
self.data[row][col] = str;
Ok(())
}
fn get(&self, row: usize, col: usize) -> Option<&Self::Item> {
match self.data[row][col].is_empty() {
false => Some(&self.data[row][col]),
true => None,
}
}
fn from_secret(mut secret: Vec<u8>) -> DiagramResult<Self> {
if secret.len() < 10 {
return Err(DiagramError::InvalidParameter("secret too short.")); }
if let Some(check) = secret.pop() {
if check != sha256::Hash::hash(&secret).as_byte_array()[0] {
return Err(DiagramError::InvalidParameter("checksum fail.")); }
}
let indices: Vec<u8> = secret.split_off(secret.len() - 7);
let (version, item_count) = indices
.iter()
.enumerate()
.map(|(i, &v)| {
(
(v & VERSION_MASK) >> (7 - i),
(v & INDICES_ALL).count_ones() as usize,
)
})
.reduce(|(ver, count), (v, n)| (ver + v, count + n))
.unwrap_or_default();
if version != 1 {
return Err(DiagramError::InvalidVersion); }
if item_count == 0 {
return Err(DiagramError::InvalidParameter("indices is empty.")); }
let mut str_lens: Vec<u8> = secret.split_off(secret.len() - item_count);
let (amount, has_zero) = str_lens
.iter()
.fold((0, false), |(a, z), &v| (a + v as usize, z || v == 0));
if str_lens.is_empty() || has_zero || amount != secret.len() {
return Err(DiagramError::InvalidParameter("str lens invalid.")); }
let mut diagram = ComplexDiagram::new();
for row in (0..7).rev() {
for col in (0..7).rev() {
if indices[row] & INDICES_MASK[col] > 0 {
let n = str_lens.pop().unwrap_or_default();
let bs = secret.split_off(secret.len() - n as usize);
match String::from_utf8(bs) {
Ok(s) => diagram.data[row][col] = s,
Err(_) => {
return Err(DiagramError::InvalidParameter("invalid utf8 string."))
} }
}
}
}
Ok(diagram)
}
fn to_secret(&self) -> DiagramResult<Vec<u8>> {
if self.is_empty() {
return Err(DiagramError::EmptyDiagram);
}
let mut str_list: Vec<&str> = vec![];
let mut str_lens: Vec<u8> = vec![];
let mut indices: [u8; 7] = [0; 7];
(0..7).for_each(|row| {
(0..7).for_each(|col| {
let s = &self.data[row][col];
if s.is_empty() {
return;
}
debug_assert!(s.len() < u8::MAX as usize);
str_list.push(s);
str_lens.push(s.len() as u8);
indices[row] |= INDICES_MASK[col];
});
});
indices[0] |= VERSION_MASK; let mut secret = [str_list.join("").as_bytes(), &str_lens[..], &indices[..]].concat();
let check = sha256::Hash::hash(&secret).as_byte_array()[0];
secret.push(check);
Ok(secret)
}
}
#[cfg(test)]
mod complex_diagram_test {
use super::*;
use bitcoin::hex::{DisplayHex, FromHex};
#[test]
fn test_complex_empty() {
let art = ComplexDiagram::new();
assert!(art.to_secret().is_err());
let empty = vec![0; 8];
assert!(ComplexDiagram::from_secret(empty).is_err());
}
#[test]
fn test_complex_invalid() {
const INVALID_SECRET_LEN: [u8; 9] = [0; 9];
assert!(ComplexDiagram::from_secret(INVALID_SECRET_LEN.to_vec()).is_err());
const INVALID_CHECKSUM: [u8; 10] = [b'A', 1, 0, 0, 0, 0, 0, 0, 0, 0xc5];
assert!(ComplexDiagram::from_secret(INVALID_CHECKSUM.to_vec()).is_err());
const INVALID_VERSION: [u8; 10] = [b'A', 1, 0, 0, 0, 0, 0, 0, 0, 0xc5];
assert!(ComplexDiagram::from_secret(INVALID_VERSION.to_vec())
.is_err_and(|e| e == DiagramError::InvalidVersion));
const EMPTY_INDICES: [u8; 10] = [b'A', 1, 0b1000_0000, 0, 0, 0, 0, 0, 0, 0xce];
assert!(ComplexDiagram::from_secret(EMPTY_INDICES.to_vec()).is_err());
const INVALID_STR_LENS: [u8; 12] = [b'A', b'Z', 1, 2, 0b1000_0001, 0, 0, 0, 1, 0, 0, 0x82];
assert!(ComplexDiagram::from_secret(INVALID_STR_LENS.to_vec()).is_err());
const ZERO_STR_LENS: [u8; 12] = [b'A', b'Z', 0, 2, 0b1000_0001, 0, 0, 0, 1, 0, 0, 0x7e];
assert!(ComplexDiagram::from_secret(ZERO_STR_LENS.to_vec()).is_err());
const INVALID_UTF8: [u8; 12] = [0xff, b'Z', 1, 1, 0b1000_0001, 0, 0, 0, 1, 0, 0, 0x20];
assert!(ComplexDiagram::from_secret(INVALID_UTF8.to_vec()).is_err());
let mut strs = vec!["ABC".to_string(), "123".to_string()];
assert!(ComplexDiagram::from_items(vec![], &[(1, 1)]).is_err());
assert!(ComplexDiagram::from_items(strs.clone(), &[]).is_err());
assert!(ComplexDiagram::from_items(strs.clone(), &[(1, 5)]).is_err());
assert!(ComplexDiagram::from_items(strs.clone(), &[(0, 0), (0, 1), (1, 0)]).is_err());
strs.push("".to_string());
assert!(ComplexDiagram::from_items(strs, &[(0, 0), (0, 1), (1, 1)]).is_err());
}
#[test]
fn test_complex_secret() {
const STR_LIST: &[&str] = &["ABC", "123", "测试", "混A1", "A&*王😊"];
const INDICES: &[(usize, usize)] = &[(0, 6), (1, 1), (1, 3), (4, 2), (6, 6)];
const SECRET_HEX: &str =
"414243313233e6b58be8af95e6b7b7413141262ae78e8bf09f988a030306050a8128000010000132";
let mut diagram = ComplexDiagram::new();
let success = INDICES
.iter()
.zip(STR_LIST)
.all(|(&(row, col), &s)| diagram.set(s.to_owned(), row, col).is_ok());
assert_eq!(success, true);
let indices: Vec<(usize, usize)> = diagram.indices().collect();
assert_eq!(&indices, INDICES);
let secret = diagram.to_secret().unwrap_or_default();
assert_eq!(secret.to_lower_hex_string(), SECRET_HEX);
if let Ok(diagram) = ComplexDiagram::from_secret(secret) {
assert_eq!(diagram.get(6, 6).unwrap_or(&String::new()), STR_LIST[4]);
let indices: Vec<(usize, usize)> = diagram.indices().collect();
assert_eq!(&indices, INDICES);
let secret = diagram.to_secret().unwrap_or_default();
assert_eq!(secret.to_lower_hex_string(), SECRET_HEX);
} else {
assert!(false, "from_secret() fail");
}
}
#[test]
fn test_complex_entropy() -> DiagramResult<()> {
const SECRET_HEX: &str =
"414243313233e6b58be8af95e6b7b7413141262ae78e8bf09f988a030306050a8128000010004052";
const RAW_ENTROPY: &str =
"f273657eb2394dbe4874571abf8d6f78b149bd86d1eec6c666509371e93004d3";
const SALT_STR: &str = "123abc";
const SALT_ENTROPY: &str =
"3ff854b9f188d428068e3a9b7655d37795f1aaf1e6461b757f12935dee796bbf";
let secret = Vec::from_hex(SECRET_HEX).expect("SECRET_HEX");
let diag = ComplexDiagram::from_secret(secret)?;
let entropy = diag.to_entropy(Default::default())?;
assert_eq!(entropy.to_lower_hex_string(), RAW_ENTROPY);
let salt_entropy = diag.to_entropy(SALT_STR.as_bytes())?;
assert_eq!(salt_entropy.to_lower_hex_string(), SALT_ENTROPY);
Ok(())
}
}