новый файл: .gitignore
новый файл: Cargo.lock новый файл: Cargo.toml новый файл: english.chrs новый файл: russian.chrs новый файл: src/alphabet.rs новый файл: src/app.rs новый файл: src/ciphertext.rs новый файл: src/cli.rs новый файл: src/decoder.rs новый файл: src/main.rs новый файл: src/storage.rs новый файл: src/tui.rs
This commit is contained in:
commit
ff049620d5
13 changed files with 2273 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/target
|
||||
1119
Cargo.lock
generated
Normal file
1119
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
13
Cargo.toml
Normal file
13
Cargo.toml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
[package]
|
||||
name = "evolution_crypting"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
bit-set = "0.9.1"
|
||||
bitvec = "1.0.1"
|
||||
clap = { version = "4.6.0", features = ["derive"] }
|
||||
rand = "0.10.0"
|
||||
rand_chacha = "0.10.0"
|
||||
ratatui = "0.29"
|
||||
crossterm = "0.28"
|
||||
2
english.chrs
Normal file
2
english.chrs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzcvbnm
|
||||
,. ?!"'/
|
||||
2
russian.chrs
Normal file
2
russian.chrs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
ЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЭЯЧСМИТЬБЮ
|
||||
йцукенгшщзхъфывапролджэячсмитьбю .,!?"/
|
||||
433
src/alphabet.rs
Normal file
433
src/alphabet.rs
Normal file
|
|
@ -0,0 +1,433 @@
|
|||
use bitvec::prelude::{BitSlice, BitVec};
|
||||
use rand::{CryptoRng, Rng, RngExt};
|
||||
use std::io::{Error, ErrorKind, Result};
|
||||
|
||||
const MAGIC: [u8; 4] = [b'E', b'C', b'A', b'1'];
|
||||
|
||||
pub struct BitFlip {
|
||||
pub index: usize,
|
||||
pub old: bool,
|
||||
pub new: bool,
|
||||
}
|
||||
|
||||
pub struct DecipherPatch {
|
||||
pub symbol: char,
|
||||
pub distance: usize,
|
||||
pub flips: Vec<BitFlip>,
|
||||
}
|
||||
|
||||
pub struct Alphabet<R: CryptoRng> {
|
||||
r: R,
|
||||
chars: String,
|
||||
dict: BitVec<u8>,
|
||||
len: usize,
|
||||
mut_rate: usize,
|
||||
}
|
||||
|
||||
impl<R: CryptoRng + Rng> Alphabet<R> {
|
||||
|
||||
fn symbol_index(&self, c: char) -> Result<usize> {
|
||||
self.chars
|
||||
.chars()
|
||||
.position(|x| x == c)
|
||||
.ok_or_else(|| Error::new(ErrorKind::InvalidInput, format!("symbol {:?} not found", c)))
|
||||
}
|
||||
|
||||
pub fn symbol_bits(&self, c: char) -> Result<BitVec<u8>> {
|
||||
let index = self.symbol_index(c)?;
|
||||
let start = index * self.len;
|
||||
let end = (index + 1) * self.len;
|
||||
Ok(self.dict[start..end].to_bitvec())
|
||||
}
|
||||
|
||||
pub fn replace_symbol_bits(
|
||||
&mut self,
|
||||
c: char,
|
||||
bits: &BitSlice<u8>,
|
||||
) -> Result<BitVec<u8>> {
|
||||
if bits.len() < self.len {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"not enough bits for symbol replacement",
|
||||
));
|
||||
}
|
||||
|
||||
let index = self.symbol_index(c)?;
|
||||
let start = index * self.len;
|
||||
let end = (index + 1) * self.len;
|
||||
|
||||
let old = self.dict[start..end].to_bitvec();
|
||||
|
||||
for i in 0..self.len {
|
||||
self.dict.set(start + i, bits[i]);
|
||||
}
|
||||
|
||||
Ok(old)
|
||||
}
|
||||
|
||||
pub fn replace_symbol_bits_by_index(
|
||||
&mut self,
|
||||
index: usize,
|
||||
bits: &BitSlice<u8>,
|
||||
) -> Result<BitVec<u8>> {
|
||||
if bits.len() < self.len {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"not enough bits for symbol replacement",
|
||||
));
|
||||
}
|
||||
|
||||
let start = index * self.len;
|
||||
let end = (index + 1) * self.len;
|
||||
|
||||
if end > self.dict.len() {
|
||||
return Err(Error::new(ErrorKind::InvalidInput, "symbol index out of range"));
|
||||
}
|
||||
|
||||
let old = self.dict[start..end].to_bitvec();
|
||||
|
||||
for i in 0..self.len {
|
||||
self.dict.set(start + i, bits[i]);
|
||||
}
|
||||
|
||||
Ok(old)
|
||||
}
|
||||
|
||||
pub fn symbol_distance(&self, c: char, bits: &BitSlice<u8>) -> Result<usize> {
|
||||
if bits.len() < self.len {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"not enough bits for one symbol",
|
||||
));
|
||||
}
|
||||
|
||||
let index = self.symbol_index(c)?;
|
||||
let start = index * self.len;
|
||||
let end = (index + 1) * self.len;
|
||||
let letter = &self.dict[start..end];
|
||||
|
||||
Ok(bits[..self.len]
|
||||
.iter()
|
||||
.zip(letter.iter())
|
||||
.filter(|(a, b)| *a != *b)
|
||||
.count())
|
||||
}
|
||||
|
||||
pub fn apply_symbol_choice(
|
||||
&self,
|
||||
bits: &mut BitSlice<u8>,
|
||||
symbol: char,
|
||||
) -> Result<DecipherPatch> {
|
||||
if bits.len() < self.len {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"not enough bits for one symbol",
|
||||
));
|
||||
}
|
||||
|
||||
let index = self
|
||||
.chars
|
||||
.chars()
|
||||
.position(|x| x == symbol)
|
||||
.ok_or_else(|| Error::new(ErrorKind::InvalidInput, "symbol not found in alphabet"))?;
|
||||
|
||||
let start = index * self.len;
|
||||
let end = (index + 1) * self.len;
|
||||
let letter = &self.dict[start..end];
|
||||
|
||||
let mut flips = Vec::new();
|
||||
let mut distance = 0;
|
||||
|
||||
for i in 0..self.len {
|
||||
let old = bits[i];
|
||||
let new = letter[i];
|
||||
|
||||
if old != new {
|
||||
distance += 1;
|
||||
bits.set(i, new);
|
||||
flips.push(BitFlip { index: i, old, new });
|
||||
}
|
||||
}
|
||||
|
||||
Ok(DecipherPatch {
|
||||
symbol,
|
||||
distance,
|
||||
flips,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn best_symbol_distance_no_effect(&self, bits: &BitSlice<u8>) -> Result<(char, usize)> {
|
||||
self.decipher_candidates_no_effect(bits, 1)?
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| Error::new(ErrorKind::InvalidData, "alphabet is empty"))
|
||||
}
|
||||
|
||||
pub fn generate(mut rng: R, chars: String, len: usize, mut_rate: usize) -> Self {
|
||||
let str_len = chars.chars().count();
|
||||
let dict_bit_len = len * str_len + rng.random_range(0..len);
|
||||
let dict_byte_len = dict_bit_len.div_ceil(8);
|
||||
let mut random_bytes = vec![0u8; dict_byte_len];
|
||||
rng.fill(&mut random_bytes[..]);
|
||||
let mut dict = BitVec::<u8>::from_vec(random_bytes);
|
||||
dict.truncate(dict_bit_len);
|
||||
Alphabet {
|
||||
r: rng,
|
||||
chars,
|
||||
dict,
|
||||
len,
|
||||
mut_rate,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_bytes(self) -> Vec<u8> {
|
||||
let dict_bit_len = (self.dict.len() as u64).to_le_bytes();
|
||||
let d = self.dict.into_vec();
|
||||
let s = self.chars.into_bytes();
|
||||
let bl = (self.len as u64).to_le_bytes();
|
||||
let sl_bytes = (s.len() as u64).to_le_bytes();
|
||||
let mut_rate = (self.mut_rate as u64).to_le_bytes();
|
||||
|
||||
[
|
||||
&MAGIC[..],
|
||||
&bl[..],
|
||||
&sl_bytes[..],
|
||||
&dict_bit_len[..],
|
||||
&mut_rate[..],
|
||||
&s[..],
|
||||
&d[..],
|
||||
]
|
||||
.concat()
|
||||
}
|
||||
|
||||
pub fn symbol_len(&self) -> usize {
|
||||
self.len
|
||||
}
|
||||
|
||||
pub fn from_bytes(rng: R, bytes: &[u8]) -> Result<Self> {
|
||||
const HEADER_LEN: usize = 4 + 8 + 8 + 8 + 8;
|
||||
|
||||
if bytes.len() < HEADER_LEN {
|
||||
return Err(Error::new(ErrorKind::InvalidData, "file too short"));
|
||||
}
|
||||
|
||||
if bytes[0..4] != MAGIC {
|
||||
return Err(Error::new(ErrorKind::InvalidData, "bad magic"));
|
||||
}
|
||||
|
||||
let bl = u64::from_le_bytes(
|
||||
bytes[4..12]
|
||||
.try_into()
|
||||
.map_err(|_| Error::new(ErrorKind::InvalidData, "bad len field"))?,
|
||||
) as usize;
|
||||
|
||||
let sl_bytes = u64::from_le_bytes(
|
||||
bytes[12..20]
|
||||
.try_into()
|
||||
.map_err(|_| Error::new(ErrorKind::InvalidData, "bad string length field"))?,
|
||||
) as usize;
|
||||
|
||||
let dict_bit_len = u64::from_le_bytes(
|
||||
bytes[20..28]
|
||||
.try_into()
|
||||
.map_err(|_| Error::new(ErrorKind::InvalidData, "bad dict bit length field"))?,
|
||||
) as usize;
|
||||
|
||||
let mut_rate = u64::from_le_bytes(
|
||||
bytes[28..36]
|
||||
.try_into()
|
||||
.map_err(|_| Error::new(ErrorKind::InvalidData, "bad mut_rate field"))?,
|
||||
) as usize;
|
||||
|
||||
let dict_byte_len = dict_bit_len.div_ceil(8);
|
||||
let expected_len = HEADER_LEN + sl_bytes + dict_byte_len;
|
||||
|
||||
if bytes.len() < expected_len {
|
||||
return Err(Error::new(ErrorKind::InvalidData, "wrong file size"));
|
||||
}
|
||||
|
||||
let s_start = HEADER_LEN;
|
||||
let s_end = s_start + sl_bytes;
|
||||
let d_start = s_end;
|
||||
let d_end = d_start + dict_byte_len;
|
||||
|
||||
let chars = String::from_utf8(bytes[s_start..s_end].to_vec())
|
||||
.map_err(|_| Error::new(ErrorKind::InvalidData, "chars is not valid UTF-8"))?;
|
||||
|
||||
let mut dict = BitVec::<u8>::from_vec(bytes[d_start..d_end].to_vec());
|
||||
dict.truncate(dict_bit_len);
|
||||
|
||||
Ok(Alphabet {
|
||||
r: rng,
|
||||
chars,
|
||||
dict,
|
||||
len: bl,
|
||||
mut_rate,
|
||||
})
|
||||
}
|
||||
|
||||
fn cipher_no_effect(&self, c: char) -> Result<BitVec<u8>> {
|
||||
let index = self.chars.chars().position(|x| x == c).ok_or_else(|| {
|
||||
Error::new(ErrorKind::InvalidInput, format!("symbol {:?} not found", c))
|
||||
})?;
|
||||
let start = index * self.len;
|
||||
let end = (index + 1) * self.len;
|
||||
let letter = self.dict[start..end].to_bitvec();
|
||||
|
||||
Ok(letter)
|
||||
}
|
||||
|
||||
pub fn decipher_candidates_no_effect(
|
||||
&self,
|
||||
bits: &bitvec::slice::BitSlice<u8>,
|
||||
top: usize,
|
||||
) -> Result<Vec<(char, usize)>> {
|
||||
if bits.len() < self.len {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"not enough bits for one symbol",
|
||||
));
|
||||
}
|
||||
|
||||
let needle = &bits[..self.len];
|
||||
let mut candidates = Vec::new();
|
||||
|
||||
for (index, ch) in self.chars.chars().enumerate() {
|
||||
let start = index * self.len;
|
||||
let end = (index + 1) * self.len;
|
||||
let letter = &self.dict[start..end];
|
||||
|
||||
let distance = needle
|
||||
.iter()
|
||||
.zip(letter.iter())
|
||||
.filter(|(a, b)| *a != *b)
|
||||
.count();
|
||||
|
||||
candidates.push((ch, distance));
|
||||
}
|
||||
|
||||
candidates.sort_by_key(|(_, distance)| *distance);
|
||||
|
||||
if candidates.len() > top {
|
||||
candidates.truncate(top);
|
||||
}
|
||||
|
||||
Ok(candidates)
|
||||
}
|
||||
|
||||
pub fn apply_symbol_repair(
|
||||
&self,
|
||||
bits: &mut bitvec::slice::BitSlice<u8>,
|
||||
) -> Result<DecipherPatch> {
|
||||
if bits.len() < self.len {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"not enough bits for one symbol",
|
||||
));
|
||||
}
|
||||
|
||||
let best = self
|
||||
.decipher_candidates_no_effect(bits, 1)?
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| Error::new(ErrorKind::InvalidData, "alphabet is empty"))?;
|
||||
|
||||
let (symbol, distance) = best;
|
||||
|
||||
let index = self
|
||||
.chars
|
||||
.chars()
|
||||
.position(|x| x == symbol)
|
||||
.ok_or_else(|| Error::new(ErrorKind::InvalidData, "best symbol not found"))?;
|
||||
|
||||
let start = index * self.len;
|
||||
let end = (index + 1) * self.len;
|
||||
let letter = &self.dict[start..end];
|
||||
|
||||
let mut flips = Vec::new();
|
||||
|
||||
for i in 0..self.len {
|
||||
let old = bits[i];
|
||||
let new = letter[i];
|
||||
|
||||
if old != new {
|
||||
bits.set(i, new);
|
||||
flips.push(BitFlip { index: i, old, new });
|
||||
}
|
||||
}
|
||||
|
||||
Ok(DecipherPatch {
|
||||
symbol,
|
||||
distance,
|
||||
flips,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn undo_patch(
|
||||
&self,
|
||||
bits: &mut bitvec::slice::BitSlice<u8>,
|
||||
patch: &DecipherPatch,
|
||||
) -> Result<()> {
|
||||
if bits.len() < self.len {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"not enough bits for one symbol",
|
||||
));
|
||||
}
|
||||
|
||||
for flip in &patch.flips {
|
||||
bits.set(flip.index, flip.old);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mutate(&mut self) {
|
||||
let end = self.dict.len();
|
||||
|
||||
if end == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let v: Vec<usize> = (0..self.mut_rate)
|
||||
.map(|_| self.r.random_range(0..end))
|
||||
.collect();
|
||||
|
||||
for i in v {
|
||||
let val = self.dict[i];
|
||||
self.dict.set(i, !val);
|
||||
}
|
||||
}
|
||||
}
|
||||
pub trait MutationallyCipherable<R: CryptoRng + Rng> {
|
||||
fn cipher(self, a: &mut Alphabet<R>) -> Result<BitVec<u8>>;
|
||||
}
|
||||
impl<R: CryptoRng + Rng> MutationallyCipherable<R> for char {
|
||||
fn cipher(self, a: &mut Alphabet<R>) -> Result<BitVec<u8>> {
|
||||
let res = a.cipher_no_effect(self)?;
|
||||
a.mutate();
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
impl<R: CryptoRng + Rng> MutationallyCipherable<R> for String {
|
||||
fn cipher(self, a: &mut Alphabet<R>) -> Result<BitVec<u8>> {
|
||||
let mut res: BitVec<u8> = BitVec::new();
|
||||
for c in self.chars() {
|
||||
res.extend(c.cipher(a)?)
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
impl<R: CryptoRng + Rng> MutationallyCipherable<R> for &str {
|
||||
fn cipher(self, a: &mut Alphabet<R>) -> Result<BitVec<u8>> {
|
||||
let mut res: BitVec<u8> = BitVec::new();
|
||||
for c in self.chars() {
|
||||
res.extend(c.cipher(a)?)
|
||||
}
|
||||
let tail_len = a.r.random_range(..a.dict.len());
|
||||
for _ in 1..tail_len {
|
||||
res.push(a.r.random_bool(0.5));
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
156
src/app.rs
Normal file
156
src/app.rs
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
use crate::alphabet::{Alphabet, MutationallyCipherable};
|
||||
use crate::ciphertext::Ciphertext;
|
||||
use crate::storage::AlphabetFile;
|
||||
use bitvec::prelude::BitVec;
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
use std::fs;
|
||||
use std::fs::File;
|
||||
use std::io::{self, Error, ErrorKind, Read, Write};
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn parse_bitvec_input(s: &str) -> io::Result<BitVec<u8>> {
|
||||
let mut bits = BitVec::<u8>::new();
|
||||
|
||||
for ch in s.chars() {
|
||||
match ch {
|
||||
'0' => bits.push(false),
|
||||
'1' => bits.push(true),
|
||||
c if c.is_whitespace() => {}
|
||||
other => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("unexpected character in bitstream: {:?}", other),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(bits)
|
||||
}
|
||||
|
||||
fn read_input(file: Option<PathBuf>) -> io::Result<String> {
|
||||
let mut s = String::new();
|
||||
|
||||
match file {
|
||||
Some(path) => File::open(path)?.read_to_string(&mut s)?,
|
||||
None => io::stdin().read_to_string(&mut s)?,
|
||||
};
|
||||
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
pub fn generate_cmd(
|
||||
path: PathBuf,
|
||||
chars: Option<String>,
|
||||
chars_file: Option<PathBuf>,
|
||||
len: usize,
|
||||
mut_rate: usize,
|
||||
) -> io::Result<()> {
|
||||
if len == 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"len must be > 0",
|
||||
));
|
||||
}
|
||||
|
||||
let rng: ChaCha20Rng = rand::make_rng();
|
||||
let chars = read_chars(chars, chars_file)?;
|
||||
let alphabet = Alphabet::generate(rng, chars, len, mut_rate);
|
||||
let af = AlphabetFile::create(path, alphabet)?;
|
||||
af.save()
|
||||
}
|
||||
|
||||
pub fn cipher_cmd(
|
||||
alphabet_path: PathBuf,
|
||||
file: Option<PathBuf>,
|
||||
output: Option<PathBuf>,
|
||||
) -> io::Result<()> {
|
||||
let rng: ChaCha20Rng = rand::make_rng();
|
||||
let mut af = AlphabetFile::open(alphabet_path, rng)?;
|
||||
let input = read_input(file)?;
|
||||
let bits = input
|
||||
.as_str()
|
||||
.cipher(&mut af.dict)
|
||||
.map_err(io::Error::other)?;
|
||||
|
||||
let ciphertext = Ciphertext::from_bits(bits);
|
||||
let bytes = ciphertext.into_bytes();
|
||||
write_output_bytes(output, &bytes)
|
||||
}
|
||||
|
||||
fn read_chars(chars: Option<String>, chars_file: Option<PathBuf>) -> io::Result<String> {
|
||||
if let Some(path) = chars_file {
|
||||
let s = fs::read_to_string(path)?;
|
||||
if s.is_empty() {
|
||||
return Err(Error::new(ErrorKind::InvalidInput, "chars file is empty"));
|
||||
}
|
||||
return Ok(s);
|
||||
}
|
||||
|
||||
if let Some(s) = chars {
|
||||
if s.is_empty() {
|
||||
return Err(Error::new(ErrorKind::InvalidInput, "chars is empty"));
|
||||
}
|
||||
return Ok(s);
|
||||
}
|
||||
|
||||
Ok("abcdefghijklmoprqstuvwxyz ".to_string())
|
||||
}
|
||||
|
||||
fn read_input_bytes(file: Option<PathBuf>) -> io::Result<Vec<u8>> {
|
||||
let mut buf = Vec::new();
|
||||
|
||||
match file {
|
||||
Some(path) => File::open(path)?.read_to_end(&mut buf)?,
|
||||
None => io::stdin().read_to_end(&mut buf)?,
|
||||
};
|
||||
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
pub fn decipher_cmd(
|
||||
alphabet_path: PathBuf,
|
||||
file: Option<PathBuf>,
|
||||
output: Option<PathBuf>,
|
||||
) -> io::Result<()> {
|
||||
let rng: ChaCha20Rng = rand::make_rng();
|
||||
let af = AlphabetFile::open(alphabet_path, rng)?;
|
||||
let bytes = read_input_bytes(file)?;
|
||||
let mut bits = Ciphertext::from_bytes(&bytes)
|
||||
.map_err(io::Error::other)?
|
||||
.bits;
|
||||
|
||||
let mut out = String::new();
|
||||
let mut pos = 0usize;
|
||||
let sym_len = af.dict.symbol_len();
|
||||
|
||||
while pos + sym_len <= bits.len() {
|
||||
let end = pos + sym_len;
|
||||
let patch = af
|
||||
.dict
|
||||
.apply_symbol_repair(&mut bits[pos..end])
|
||||
.map_err(io::Error::other)?;
|
||||
|
||||
out.push(patch.symbol);
|
||||
pos = end;
|
||||
}
|
||||
|
||||
write_output_text(output, &out)
|
||||
}
|
||||
|
||||
fn write_output_bytes(output: Option<PathBuf>, bytes: &[u8]) -> io::Result<()> {
|
||||
match output {
|
||||
Some(path) => fs::write(path, bytes),
|
||||
None => io::stdout().write_all(bytes),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_output_text(output: Option<PathBuf>, text: &str) -> io::Result<()> {
|
||||
match output {
|
||||
Some(path) => fs::write(path, text),
|
||||
None => {
|
||||
println!("{text}");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
54
src/ciphertext.rs
Normal file
54
src/ciphertext.rs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
use bitvec::prelude::BitVec;
|
||||
use std::io::{Error, ErrorKind, Result};
|
||||
|
||||
const MAGIC: [u8; 4] = [b'E', b'C', b'T', b'1'];
|
||||
|
||||
pub struct Ciphertext {
|
||||
pub bits: BitVec<u8>,
|
||||
}
|
||||
|
||||
impl Ciphertext {
|
||||
pub fn from_bits(bits: BitVec<u8>) -> Self {
|
||||
Self { bits }
|
||||
}
|
||||
|
||||
pub fn into_bytes(self) -> Vec<u8> {
|
||||
let bit_len = (self.bits.len() as u64).to_le_bytes();
|
||||
let payload = self.bits.into_vec();
|
||||
|
||||
[&MAGIC[..], &bit_len[..], &payload[..]].concat()
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
|
||||
const HEADER_LEN: usize = 4 + 8;
|
||||
|
||||
if bytes.len() < HEADER_LEN {
|
||||
return Err(Error::new(ErrorKind::InvalidData, "ciphertext too short"));
|
||||
}
|
||||
|
||||
if bytes[0..4] != MAGIC {
|
||||
return Err(Error::new(ErrorKind::InvalidData, "bad ciphertext magic"));
|
||||
}
|
||||
|
||||
let bit_len = u64::from_le_bytes(
|
||||
bytes[4..12]
|
||||
.try_into()
|
||||
.map_err(|_| Error::new(ErrorKind::InvalidData, "bad ciphertext bit length"))?,
|
||||
) as usize;
|
||||
|
||||
let payload = bytes[12..].to_vec();
|
||||
let expected_payload_len = bit_len.div_ceil(8);
|
||||
|
||||
if payload.len() < expected_payload_len {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
"ciphertext payload too short",
|
||||
));
|
||||
}
|
||||
|
||||
let mut bits = BitVec::<u8>::from_vec(payload);
|
||||
bits.truncate(bit_len);
|
||||
|
||||
Ok(Self { bits })
|
||||
}
|
||||
}
|
||||
56
src/cli.rs
Normal file
56
src/cli.rs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
use clap::{Parser, Subcommand};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "cli")]
|
||||
#[command(version)]
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
pub command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Commands {
|
||||
Generate {
|
||||
path: PathBuf,
|
||||
|
||||
#[arg(long = "chars")]
|
||||
chars: Option<String>,
|
||||
|
||||
#[arg(long = "chars-file")]
|
||||
chars_file: Option<PathBuf>,
|
||||
|
||||
#[arg(long = "len", default_value_t = 8)]
|
||||
len: usize,
|
||||
|
||||
#[arg(long = "mut-rate", default_value_t = 8)]
|
||||
mut_rate: usize,
|
||||
},
|
||||
Cipher {
|
||||
#[arg(short = 'a', long = "alphabet")]
|
||||
alphabet: PathBuf,
|
||||
|
||||
#[arg(short = 'f', long = "file")]
|
||||
file: Option<PathBuf>,
|
||||
|
||||
#[arg(short = 'o', long = "output")]
|
||||
output: Option<PathBuf>,
|
||||
},
|
||||
Decipher {
|
||||
#[arg(short = 'a', long = "alphabet")]
|
||||
alphabet: PathBuf,
|
||||
|
||||
#[arg(short = 'f', long = "file")]
|
||||
file: Option<PathBuf>,
|
||||
|
||||
#[arg(short = 'o', long = "output")]
|
||||
output: Option<PathBuf>,
|
||||
},
|
||||
Tui {
|
||||
#[arg(short = 'a', long = "alphabet")]
|
||||
alphabet: PathBuf,
|
||||
|
||||
#[arg(short = 'f', long = "file")]
|
||||
file: PathBuf,
|
||||
},
|
||||
}
|
||||
202
src/decoder.rs
Normal file
202
src/decoder.rs
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
use crate::alphabet::Alphabet;
|
||||
use bitvec::prelude::BitVec;
|
||||
use rand::SeedableRng;
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
use std::io::{Error, ErrorKind, Result};
|
||||
|
||||
pub struct CandidateScore {
|
||||
pub symbol: char,
|
||||
pub distance: usize,
|
||||
pub future_distance: usize,
|
||||
}
|
||||
|
||||
pub enum RankingMode {
|
||||
LocalFirst,
|
||||
FutureFirst,
|
||||
}
|
||||
|
||||
pub struct DecoderState {
|
||||
alphabet_bytes: Vec<u8>,
|
||||
rng_seed: <ChaCha20Rng as SeedableRng>::Seed,
|
||||
base_ciphertext: BitVec<u8>,
|
||||
|
||||
pub cursor: usize,
|
||||
pub text_scroll: usize,
|
||||
pub choices: Vec<Option<char>>,
|
||||
pub rendered_text: String,
|
||||
pub working_bits: BitVec<u8>,
|
||||
pub ranking_mode: RankingMode,
|
||||
}
|
||||
|
||||
impl DecoderState {
|
||||
pub fn new(
|
||||
alphabet_bytes: Vec<u8>,
|
||||
rng_seed: <ChaCha20Rng as SeedableRng>::Seed,
|
||||
ciphertext: BitVec<u8>,
|
||||
) -> Result<Self> {
|
||||
let rng = ChaCha20Rng::from_seed(rng_seed);
|
||||
let alphabet = Alphabet::from_bytes(rng, &alphabet_bytes)?;
|
||||
let symbol_count = ciphertext.len() / alphabet.symbol_len();
|
||||
|
||||
let mut this = Self {
|
||||
alphabet_bytes,
|
||||
rng_seed,
|
||||
base_ciphertext: ciphertext.clone(),
|
||||
cursor: 0,
|
||||
text_scroll: 0,
|
||||
choices: vec![None; symbol_count],
|
||||
rendered_text: String::new(),
|
||||
working_bits: ciphertext,
|
||||
ranking_mode: RankingMode::LocalFirst,
|
||||
};
|
||||
|
||||
|
||||
this.rebuild_all()?;
|
||||
Ok(this)
|
||||
}
|
||||
|
||||
fn make_alphabet(&self) -> Result<Alphabet<ChaCha20Rng>> {
|
||||
let rng = ChaCha20Rng::from_seed(self.rng_seed);
|
||||
Alphabet::from_bytes(rng, &self.alphabet_bytes)
|
||||
}
|
||||
|
||||
pub fn ensure_cursor_visible(&mut self, visible_symbols: usize) {
|
||||
if self.cursor < self.text_scroll {
|
||||
self.text_scroll = self.cursor;
|
||||
} else if self.cursor >= self.text_scroll + visible_symbols {
|
||||
self.text_scroll = self.cursor + 1 - visible_symbols;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn symbol_count(&self) -> usize {
|
||||
self.choices.len()
|
||||
}
|
||||
|
||||
pub fn move_left(&mut self, visible: usize) {
|
||||
if self.cursor > 0 {
|
||||
self.cursor -= 1;
|
||||
}
|
||||
self.ensure_cursor_visible(visible);
|
||||
}
|
||||
|
||||
pub fn move_right(&mut self, visible: usize) {
|
||||
if self.cursor + 1 < self.symbol_count() {
|
||||
self.cursor += 1;
|
||||
}
|
||||
self.ensure_cursor_visible(visible);
|
||||
}
|
||||
|
||||
pub fn set_ranking_mode(&mut self, mode: RankingMode) {
|
||||
self.ranking_mode = mode;
|
||||
}
|
||||
|
||||
fn current_range(&self, symbol_len: usize) -> std::ops::Range<usize> {
|
||||
let start = self.cursor * symbol_len;
|
||||
let end = start + symbol_len;
|
||||
start..end
|
||||
}
|
||||
|
||||
pub fn current_candidates(&self) -> Result<Vec<CandidateScore>> {
|
||||
let mut alphabet = self.make_alphabet()?;
|
||||
let len = alphabet.symbol_len();
|
||||
|
||||
for i in 0..self.cursor {
|
||||
let start = i * len;
|
||||
let end = start + len;
|
||||
let chunk = &self.base_ciphertext[start..end];
|
||||
|
||||
let chosen = if let Some(ch) = self.choices[i] {
|
||||
ch
|
||||
} else {
|
||||
alphabet.best_symbol_distance_no_effect(chunk)?.0
|
||||
};
|
||||
|
||||
alphabet.replace_symbol_bits(chosen, chunk)?;
|
||||
}
|
||||
|
||||
let cur = self.current_range(len);
|
||||
let top = alphabet.decipher_candidates_no_effect(&self.base_ciphertext[cur], 5)?;
|
||||
|
||||
let mut out = Vec::new();
|
||||
for (symbol, distance) in top {
|
||||
let future_distance = self.simulate_future_distance(symbol)?;
|
||||
out.push(CandidateScore {
|
||||
symbol,
|
||||
distance,
|
||||
future_distance,
|
||||
});
|
||||
}
|
||||
|
||||
match self.ranking_mode {
|
||||
RankingMode::LocalFirst => out.sort_by_key(|c| (c.distance, c.future_distance)),
|
||||
RankingMode::FutureFirst => out.sort_by_key(|c| (c.future_distance, c.distance)),
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn simulate_future_distance(&self, chosen_symbol: char) -> Result<usize> {
|
||||
let mut alphabet = self.make_alphabet()?;
|
||||
let len = alphabet.symbol_len();
|
||||
let mut total = 0usize;
|
||||
|
||||
for i in 0..self.symbol_count() {
|
||||
let start = i * len;
|
||||
let end = start + len;
|
||||
let chunk = &self.base_ciphertext[start..end];
|
||||
|
||||
let symbol = if i == self.cursor {
|
||||
chosen_symbol
|
||||
} else if let Some(ch) = self.choices[i] {
|
||||
ch
|
||||
} else {
|
||||
alphabet.best_symbol_distance_no_effect(chunk)?.0
|
||||
};
|
||||
|
||||
total += alphabet.symbol_distance(symbol, chunk)?;
|
||||
alphabet.replace_symbol_bits(symbol, chunk)?;
|
||||
}
|
||||
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
pub fn apply_candidate(&mut self, rank: usize) -> Result<()> {
|
||||
let candidates = self.current_candidates()?;
|
||||
let cand = candidates
|
||||
.get(rank)
|
||||
.ok_or_else(|| Error::new(ErrorKind::InvalidInput, "candidate rank out of range"))?;
|
||||
|
||||
self.choices[self.cursor] = Some(cand.symbol);
|
||||
self.rebuild_all()
|
||||
}
|
||||
|
||||
pub fn undo_current(&mut self) -> Result<()> {
|
||||
self.choices[self.cursor] = None;
|
||||
self.rebuild_all()
|
||||
}
|
||||
|
||||
pub fn rebuild_all(&mut self) -> Result<()> {
|
||||
let mut alphabet = self.make_alphabet()?;
|
||||
let len = alphabet.symbol_len();
|
||||
let mut out = String::new();
|
||||
|
||||
for i in 0..self.symbol_count() {
|
||||
let start = i * len;
|
||||
let end = start + len;
|
||||
let chunk = &self.base_ciphertext[start..end];
|
||||
|
||||
let symbol = if let Some(ch) = self.choices[i] {
|
||||
ch
|
||||
} else {
|
||||
alphabet.best_symbol_distance_no_effect(chunk)?.0
|
||||
};
|
||||
|
||||
out.push(symbol);
|
||||
alphabet.replace_symbol_bits(symbol, chunk)?;
|
||||
}
|
||||
|
||||
self.rendered_text = out;
|
||||
self.working_bits = self.base_ciphertext.clone();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
35
src/main.rs
Normal file
35
src/main.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
mod alphabet;
|
||||
mod app;
|
||||
mod ciphertext;
|
||||
mod cli;
|
||||
mod decoder;
|
||||
mod storage;
|
||||
mod tui;
|
||||
|
||||
use clap::Parser;
|
||||
use cli::{Cli, Commands};
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
match cli.command {
|
||||
Commands::Generate {
|
||||
path,
|
||||
chars,
|
||||
chars_file,
|
||||
len,
|
||||
mut_rate,
|
||||
} => app::generate_cmd(path, chars, chars_file, len, mut_rate),
|
||||
Commands::Cipher {
|
||||
alphabet,
|
||||
file,
|
||||
output,
|
||||
} => app::cipher_cmd(alphabet, file, output),
|
||||
Commands::Decipher {
|
||||
alphabet,
|
||||
file,
|
||||
output,
|
||||
} => app::decipher_cmd(alphabet, file, output),
|
||||
Commands::Tui { alphabet, file } => tui::run_decoder_tui(alphabet, file),
|
||||
}
|
||||
}
|
||||
32
src/storage.rs
Normal file
32
src/storage.rs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
use crate::alphabet::Alphabet;
|
||||
use rand::{CryptoRng, Rng};
|
||||
use std::fs::File;
|
||||
use std::io::{Error, ErrorKind, Read, Result, Write};
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub struct AlphabetFile<R: CryptoRng> {
|
||||
file: File,
|
||||
pub dict: Alphabet<R>,
|
||||
}
|
||||
|
||||
impl<R: CryptoRng + Rng> AlphabetFile<R> {
|
||||
pub fn create(path: PathBuf, dict: Alphabet<R>) -> Result<Self> {
|
||||
let file = File::create(path)?;
|
||||
Ok(Self { file, dict })
|
||||
}
|
||||
|
||||
pub fn save(mut self) -> Result<()> {
|
||||
self.file.write_all(self.dict.into_bytes().as_slice())
|
||||
}
|
||||
|
||||
pub fn open(name: PathBuf, rng: R) -> Result<Self> {
|
||||
let mut file = File::open(&name)?;
|
||||
let mut bytes = Vec::new();
|
||||
file.read_to_end(&mut bytes)?;
|
||||
|
||||
let dict =
|
||||
Alphabet::from_bytes(rng, &bytes).map_err(|e| Error::new(ErrorKind::InvalidData, e))?;
|
||||
|
||||
Ok(Self { file, dict })
|
||||
}
|
||||
}
|
||||
168
src/tui.rs
Normal file
168
src/tui.rs
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
use crate::ciphertext::Ciphertext;
|
||||
use crate::decoder::DecoderState;
|
||||
use crate::storage::AlphabetFile;
|
||||
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
|
||||
};
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{Block, Borders, List, ListItem, Paragraph},
|
||||
};
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
struct TuiGuard;
|
||||
|
||||
impl Drop for TuiGuard {
|
||||
fn drop(&mut self) {
|
||||
let _ = disable_raw_mode();
|
||||
let _ = execute!(io::stdout(), LeaveAlternateScreen);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_visible_symbols(
|
||||
terminal: &Terminal<CrosstermBackend<std::io::Stdout>>,
|
||||
) -> io::Result<usize> {
|
||||
let size = terminal.size()?;
|
||||
let inner_width = size.width.saturating_sub(2) as usize;
|
||||
Ok(inner_width.max(1))
|
||||
}
|
||||
|
||||
pub fn run() -> io::Result<()> {
|
||||
todo!("call run_decoder_tui(alphabet_path, ciphertext_path) from CLI later")
|
||||
}
|
||||
|
||||
pub fn run_decoder_tui(alphabet_path: PathBuf, ciphertext_path: PathBuf) -> io::Result<()> {
|
||||
let alphabet_bytes = std::fs::read(&alphabet_path)?;
|
||||
let bytes = std::fs::read(ciphertext_path)?;
|
||||
let ciphertext = Ciphertext::from_bytes(&bytes).map_err(io::Error::other)?;
|
||||
|
||||
let rng_seed = [7u8; 32];
|
||||
let mut state = DecoderState::new(alphabet_bytes, rng_seed, ciphertext.bits)?;
|
||||
|
||||
enable_raw_mode()?;
|
||||
execute!(io::stdout(), EnterAlternateScreen)?;
|
||||
let _guard = TuiGuard;
|
||||
|
||||
let mut terminal = Terminal::new(CrosstermBackend::new(io::stdout()))?;
|
||||
|
||||
loop {
|
||||
terminal.draw(|f| draw_ui(f, &state))?;
|
||||
|
||||
if event::poll(Duration::from_millis(100))? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind != KeyEventKind::Press {
|
||||
continue;
|
||||
}
|
||||
|
||||
let visible = get_visible_symbols(&terminal)?;
|
||||
|
||||
match key.code {
|
||||
KeyCode::Char('q') => break,
|
||||
KeyCode::Left | KeyCode::Char('h') => state.move_left(visible),
|
||||
KeyCode::Right | KeyCode::Char('l') => state.move_right(visible),
|
||||
KeyCode::Char('u') => {
|
||||
let _ = state.undo_current();
|
||||
}
|
||||
KeyCode::Char('1') => {
|
||||
let _ = state.apply_candidate(0);
|
||||
}
|
||||
KeyCode::Char('2') => {
|
||||
let _ = state.apply_candidate(1);
|
||||
}
|
||||
KeyCode::Char('3') => {
|
||||
let _ = state.apply_candidate(2);
|
||||
}
|
||||
KeyCode::Char('4') => {
|
||||
let _ = state.apply_candidate(3);
|
||||
}
|
||||
KeyCode::Char('5') => {
|
||||
let _ = state.apply_candidate(4);
|
||||
}
|
||||
KeyCode::Char('m') => {
|
||||
state.set_ranking_mode(crate::decoder::RankingMode::LocalFirst);
|
||||
}
|
||||
KeyCode::Char('M') => {
|
||||
state.set_ranking_mode(crate::decoder::RankingMode::FutureFirst);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw_ui(f: &mut Frame, state: &DecoderState) {
|
||||
let layout = Layout::vertical([
|
||||
Constraint::Length(5),
|
||||
Constraint::Min(10),
|
||||
Constraint::Length(3),
|
||||
])
|
||||
.split(f.area());
|
||||
|
||||
draw_text_panel(f, layout[0], state);
|
||||
draw_candidates_panel(f, layout[1], state);
|
||||
draw_help_panel(f, layout[2]);
|
||||
}
|
||||
|
||||
fn draw_text_panel(f: &mut Frame, area: Rect, state: &DecoderState) {
|
||||
let cursor = state.cursor;
|
||||
let rendered: String = state
|
||||
.rendered_text
|
||||
.chars()
|
||||
.enumerate()
|
||||
.flat_map(|(i, ch)| {
|
||||
if i == cursor {
|
||||
vec!['[', ch, ']']
|
||||
} else {
|
||||
vec![ch]
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let p = Paragraph::new(rendered)
|
||||
.block(Block::default().title("Decoded text").borders(Borders::ALL));
|
||||
|
||||
f.render_widget(p, area);
|
||||
}
|
||||
|
||||
fn draw_candidates_panel(f: &mut Frame, area: Rect, state: &DecoderState) {
|
||||
let items = match state.current_candidates() {
|
||||
Ok(cands) => cands
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, c)| {
|
||||
ListItem::new(format!(
|
||||
"{}. {:?} local={} future={}",
|
||||
i + 1,
|
||||
c.symbol,
|
||||
c.distance,
|
||||
c.future_distance
|
||||
))
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
Err(err) => vec![ListItem::new(format!("error: {}", err))],
|
||||
};
|
||||
|
||||
let list = List::new(items).block(
|
||||
Block::default()
|
||||
.title("Top-5 candidates")
|
||||
.borders(Borders::ALL),
|
||||
);
|
||||
|
||||
f.render_widget(list, area);
|
||||
}
|
||||
|
||||
fn draw_help_panel(f: &mut Frame, area: Rect) {
|
||||
let p = Paragraph::new("h/← left l/→ right 1..5 choose u undo q quit")
|
||||
.block(Block::default().title("Keys").borders(Borders::ALL));
|
||||
|
||||
f.render_widget(p, area);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue