use crate::{
message::Message,
server::PORT,
types::{DataCollection, Ed25519Pubkey, IndexMsg},
};
use base32::{encode, Alphabet};
use chacha20poly1305::Key;
use chrono::{DateTime, Local};
use colored::Colorize;
use crossterm::event::{self, Event, KeyCode as KeyCodeT};
use crossterm::{
cursor, execute,
terminal::{self, Clear, ClearType},
ExecutableCommand,
};
use dialoguer::Select;
use ed25519_dalek::{Signer, SigningKey, VerifyingKey};
use indicatif::{HumanDuration, MultiProgress, ProgressBar, ProgressStyle};
use rustyline::{
validate::{
ValidationContext,
ValidationResult::{self, Invalid, Valid},
Validator,
},
{
Cmd, Completer, Editor, EventHandler, Helper, Highlighter, Hinter, KeyCode, KeyEvent,
Modifiers,
},
};
use sha3::{Digest, Sha3_256};
use std::io::stdout;
use std::{
io::Write,
sync::mpsc::{self, Sender},
thread::{self, JoinHandle},
time::{Duration, Instant},
};
use x25519_dalek::{EphemeralSecret, PublicKey};
const VERSION: u8 = 0x03;
const SALT: &[u8] = b".onion checksum";
const VERSION_KKV: &str = env!("CARGO_PKG_VERSION");
pub fn get_onion_domain(public_key: &[u8; 32]) -> String {
let checkdigits = get_checkdigits(public_key);
let mut combined = Vec::new();
combined.extend_from_slice(public_key);
combined.extend_from_slice(&checkdigits);
combined.push(VERSION);
let encoded = encode(Alphabet::Rfc4648 { padding: false }, &combined);
encoded.to_lowercase()
}
fn get_checkdigits(public_key: &[u8; 32]) -> [u8; 2] {
let mut hasher = Sha3_256::new();
hasher.update(SALT);
hasher.update(public_key);
hasher.update([VERSION]);
let checksum = hasher.finalize();
[checksum[0], checksum[1]]
}
pub fn get_publickey_bytes(secret_keypair: [u8; 64]) -> Ed25519Pubkey {
let signing_key: SigningKey = SigningKey::from_keypair_bytes(&secret_keypair).unwrap();
let bob_pubkey = VerifyingKey::from(&signing_key);
let bob_pubkey_bytes = bob_pubkey.as_bytes();
*bob_pubkey_bytes
}
pub fn get_secretkey(secret_keypair: [u8; 64]) -> SigningKey {
SigningKey::from_keypair_bytes(&secret_keypair).unwrap()
}
pub fn new_nonce() -> [u8; 32] {
let nonce_secret = EphemeralSecret::random();
let nonce_public = PublicKey::from(&nonce_secret);
*nonce_public.as_bytes()
}
pub fn waiting_menu(
progress: &'static str,
waiting: String,
success: String,
) -> (Sender<Result<(), String>>, JoinHandle<()>) {
let started = Instant::now();
let spinner_style =
match ProgressStyle::with_template("{prefix:.bold.dim} {spinner} {wide_msg}") {
Ok(spinner_style) => spinner_style.tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ "),
Err(_) => ProgressStyle::default_bar().tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ "),
};
let m = MultiProgress::new();
let pb = m.add(ProgressBar::new(1));
pb.set_style(spinner_style.clone());
pb.set_prefix(progress);
let (tx, rx) = mpsc::channel::<Result<(), String>>();
let handle = thread::spawn(move || {
loop {
thread::sleep(Duration::from_millis(100));
pb.set_message(waiting.clone());
pb.inc(1);
if let Ok(res) = rx.try_recv() {
match res {
Ok(_) => break,
Err(e) => {
pb.finish_with_message(format!("{}", e.red()));
return;
}
}
}
}
pb.finish_with_message(format!(
"{} in {}",
success,
HumanDuration(started.elapsed())
));
});
(tx, handle)
}
pub fn derive_key(shared_secret: &[u8]) -> Key {
let mut key = [0u8; 32];
key.copy_from_slice(&shared_secret[..32]);
Key::from(key)
}
pub fn get_human_time(duration_since_epoch: u64) -> String {
let naive_datetime =
DateTime::from_timestamp(duration_since_epoch as i64, 0).unwrap_or_default();
let naive = naive_datetime.naive_local();
let local_datetime: DateTime<Local> =
DateTime::from_naive_utc_and_offset(naive, *Local::now().offset());
format!("{}", local_datetime.format("%Y-%m-%d %H:%M:%S"))
}
pub fn cleanup_terminal(stdout: &mut std::io::Stdout) {
match stdout.execute(terminal::LeaveAlternateScreen) {
Ok(_) => (),
Err(e) => println!("{}", e.to_string().red()),
}
match stdout.execute(cursor::Show) {
Ok(_) => (),
Err(e) => println!("{}", e.to_string().red()),
}
match terminal::disable_raw_mode() {
Ok(_) => (),
Err(e) => println!("{}", e.to_string().red()),
}
}
#[derive(Completer, Helper, Highlighter, Hinter)]
struct InputMessage {
max_size: usize,
}
#[derive(Completer, Helper, Highlighter, Hinter)]
struct InputDomain {
max_size: usize,
}
impl Validator for InputMessage {
fn validate(
&self,
ctx: &mut ValidationContext,
) -> Result<ValidationResult, rustyline::error::ReadlineError> {
let input = ctx.input();
let size = input.len();
let result = if size > self.max_size {
Invalid(Some(format!(
" The max lenght is {} lenght, you write {}!!",
self.max_size, size
)))
} else {
Valid(None)
};
Ok(result)
}
}
impl Validator for InputDomain {
fn validate(
&self,
ctx: &mut ValidationContext,
) -> Result<ValidationResult, rustyline::error::ReadlineError> {
let input = ctx.input();
let size = input.len();
let result = if size != self.max_size {
Invalid(Some(format!(
" The max lenght is {} lenght, you write {}!!",
self.max_size, size
)))
} else {
Valid(None)
};
Ok(result)
}
}
pub fn get_message<const N: usize>(domain: String) -> Result<String, bool> {
println!("\n Write a message to {domain}");
println!(" We recommend you send a PGP message if you know the recipient's public key");
println!(" <Ctrl + l> for a new line");
println!(" <CTRL + q> or <CTRL + Q> to quit");
println!(" <ENTER> for sending the message\n");
let h = InputMessage { max_size: N };
let mut rl = match Editor::new() {
Ok(rl) => rl,
Err(_) => return Err(false),
};
rl.set_helper(Some(h));
rl.bind_sequence(
KeyEvent(KeyCode::Char('l'), Modifiers::CTRL),
EventHandler::Simple(Cmd::Newline),
);
rl.bind_sequence(
KeyEvent(KeyCode::Char('q'), Modifiers::CTRL),
EventHandler::Simple(Cmd::Interrupt),
);
rl.bind_sequence(
KeyEvent(KeyCode::Char('Q'), Modifiers::CTRL),
EventHandler::Simple(Cmd::Interrupt),
);
match rl.readline("") {
Ok(result) => Ok(result),
Err(_) => Err(false),
}
}
pub fn get_domain<const N: usize>() -> Result<String, bool> {
println!(" Write the .onion domain you want to communicate ");
println!(" <CTRL + q> or <CTRL + Q> to quit");
println!(" <ENTER> for sending the domain \n");
let h = InputDomain { max_size: N };
let mut rl = match Editor::new() {
Ok(rl) => rl,
Err(_) => return Err(false),
};
rl.set_helper(Some(h));
rl.bind_sequence(
KeyEvent(KeyCode::Char('q'), Modifiers::CTRL),
EventHandler::Simple(Cmd::Interrupt),
);
rl.bind_sequence(
KeyEvent(KeyCode::Char('Q'), Modifiers::CTRL),
EventHandler::Simple(Cmd::Interrupt),
);
match rl.readline("") {
Ok(input) => Ok(input),
Err(_) => Err(false),
}
}
pub fn header() {
println!("\n KraKomanoVian Message System - v{VERSION_KKV}");
println!("{}", " ** Your OpSec is our Mission **\n".italic());
}
pub fn get_enter() {
let mut just_get_enter = String::new();
std::io::stdin()
.read_line(&mut just_get_enter)
.unwrap_or_default();
}
pub fn deserialize_pow(pow: [u8; 36]) -> ([u8; 32], u32) {
let (search_bytes, cost_bytes) = pow.split_at(32);
let mut bytes = [0u8; 32];
bytes.copy_from_slice(search_bytes);
let cost_ne_bytes = cost_bytes.try_into().unwrap_or([0; 4]);
let cost = u32::from_le_bytes(cost_ne_bytes);
(bytes, cost)
}
pub async fn handle_client(index: &mut usize, render_new_msg: &mut bool, password: &String) {
let index_msg = IndexMsg::new(*index);
let data = match serde_urlencoded::to_string(&index_msg) {
Ok(data) => data,
Err(e) => {
println!("{}", e.to_string().red());
return;
}
};
let output = match tokio::process::Command::new("curl")
.arg("-X")
.arg("POST")
.arg("-H")
.arg("Content-Type: application/x-www-form-urlencoded")
.arg("-H")
.arg(format!("Authorization: {password}"))
.arg("-d")
.arg(data)
.arg(format!("http://127.0.0.1:{PORT}/get_msg"))
.output()
.await
{
Ok(result) => result,
Err(e) => {
println!("Request failed : {}", e.to_string().red());
get_enter();
return;
}
};
let body = match output.status.success() {
true => String::from_utf8_lossy(&output.stdout),
false => return,
};
let messages: DataCollection = match serde_json::from_str(&body) {
Ok(key) => key,
Err(_) => {
println!("{}", "Deserialization process failed".red());
get_enter();
return;
}
};
let mut preview: Vec<String> = [].to_vec();
for msg in &messages.get_items() {
if msg.get_has_been_read() {
preview.push(format!(
"{} {}",
get_human_time(msg.get_date()),
get_onion_domain(&msg.get_hs_pubkey())
));
} else {
preview.push(format!(
"{} {}",
get_human_time(msg.get_date()).bold(),
get_onion_domain(&msg.get_hs_pubkey()).bold(),
));
}
}
preview.reverse();
preview.push("Next".to_string());
preview.push("Back".to_string());
preview.push("Exit".to_string());
if preview.len() == 3 {
println!("\n There is no messages\n");
get_enter();
return;
}
let selected = match Select::new().items(&preview).interact() {
Ok(selected) => selected,
Err(_) => messages.get_items().len(),
};
let total_msg_size = messages.get_size();
if *render_new_msg {
*render_new_msg = false;
}
let current_msg_size = messages.get_items().len();
if selected < current_msg_size {
read_message_req(total_msg_size - 1 - (selected + *index * 10), password).await;
let message = &messages.get_items()[current_msg_size - selected - 1];
let output = rcv_message(message);
display_message(output);
*render_new_msg = true;
}
if selected == 10 && (*index + 1) * 10 < total_msg_size {
*index += 1;
*render_new_msg = true;
} else if selected == 10 && (*index + 1) * 10 >= total_msg_size {
*render_new_msg = true; }
if selected == 11 && *index != 0 {
*index -= 1;
*render_new_msg = true;
}
}
pub async fn handle_del(index: &mut usize, render_new_msg: &mut bool, password: String) {
let index_msg = IndexMsg::new(*index);
let data = match serde_urlencoded::to_string(&index_msg) {
Ok(data) => data,
Err(e) => {
println!("{}", e.to_string().red());
return;
}
};
let output = match tokio::process::Command::new("curl")
.arg("-X")
.arg("POST")
.arg("-H")
.arg("Content-Type: application/x-www-form-urlencoded")
.arg("-H")
.arg(format!("Authorization: {password}"))
.arg("-d")
.arg(data.clone())
.arg(format!("http://127.0.0.1:{PORT}/get_msg"))
.output()
.await
{
Ok(result) => result,
Err(e) => {
println!("Request failed : {}", e.to_string().red());
get_enter();
return;
}
};
let body = match output.status.success() {
true => String::from_utf8_lossy(&output.stdout),
false => return,
};
let messages: DataCollection = match serde_json::from_str(&body) {
Ok(key) => key,
Err(_) => {
println!("{}", "Deserialization process failed".red());
get_enter();
return;
}
};
let mut preview: Vec<String> = [].to_vec();
for msg in &messages.get_items() {
if msg.get_has_been_read() {
preview.push(format!(
"{} from {}.onion",
get_human_time(msg.get_date()),
get_onion_domain(&msg.get_hs_pubkey())
));
} else {
preview.push(format!(
"{} {} {}{}",
get_human_time(msg.get_date()).bold(),
"from".bold(),
get_onion_domain(&msg.get_hs_pubkey()).bold(),
".onion".bold()
));
}
}
preview.reverse();
preview.push("Next".to_string());
preview.push("Back".to_string());
preview.push("Exit".to_string());
if preview.len() == 3 {
println!("\n There is no messages\n");
get_enter();
return;
}
let selected = match Select::new().items(&preview).interact() {
Ok(selected) => selected,
Err(_) => messages.get_items().len(),
};
let total_msg_size = messages.get_size();
if *render_new_msg {
*render_new_msg = false;
}
let current_msg_size = messages.get_items().len();
if selected >= current_msg_size {
return;
}
let selected_index = current_msg_size - selected - 1;
println!(" Do you want to delete this message?");
let option = Select::new().items(&["Yes", "No"]).interact().unwrap_or(1);
let index_msg = IndexMsg::new(selected_index);
let data = match serde_urlencoded::to_string(&index_msg) {
Ok(data) => data,
Err(e) => {
println!("{}", e.to_string().red());
return;
}
};
if option == 0 {
let delete = match tokio::process::Command::new("curl")
.arg("-X")
.arg("POST")
.arg("-H")
.arg("Content-Type: application/x-www-form-urlencoded")
.arg("-H")
.arg(format!("Authorization: {password}"))
.arg("-d")
.arg(data)
.arg(format!("http://127.0.0.1:{PORT}/del_msg"))
.output()
.await
{
Ok(result) => result,
Err(e) => {
println!("Request failed : {}", e.to_string().red());
get_enter();
return;
}
};
match delete.status.success() {
true => {
*render_new_msg = current_msg_size != 1;
println!(" The message has been successfully deleted");
get_enter();
}
false => {
*render_new_msg = true;
println!(" There was an error while deleting the message");
get_enter();
}
};
}
if selected == 10 && (*index + 1) * 10 < total_msg_size {
*index += 1;
*render_new_msg = true;
} else if selected == 10 && (*index + 1) * 10 >= total_msg_size {
*render_new_msg = true; }
if selected == 11 && *index != 0 {
*index -= 1;
*render_new_msg = true;
}
}
async fn read_message_req(index: usize, password: &String) {
let index_msg = IndexMsg::new(index);
let data = match serde_urlencoded::to_string(&index_msg) {
Ok(data) => data,
Err(e) => {
println!("{}", e.to_string().red());
return;
}
};
match tokio::process::Command::new("curl")
.arg("-X")
.arg("POST")
.arg("-H")
.arg("Content-Type: application/x-www-form-urlencoded")
.arg("-H")
.arg(format!("Authorization: {password}"))
.arg("-d")
.arg(data)
.arg(format!("http://127.0.0.1:{PORT}/read_msg"))
.output()
.await
{
Ok(_) => (),
Err(e) => {
println!("Request failed : {}", e.to_string().red());
get_enter();
}
};
}
pub async fn status_server(password: String) -> bool {
match tokio::process::Command::new("curl")
.arg("-s")
.arg(format!("http://127.0.0.1:{PORT}/status"))
.arg("-H")
.arg(format!("Authorization: {password}"))
.output()
.await
{
Ok(req) => req.status.success(),
Err(_) => false,
}
}
fn display_message(raw_string: String) {
let lines: Vec<String> = raw_string.split('\n').map(String::from).collect();
let mut stdout = stdout();
let mut offset = 0;
match terminal::enable_raw_mode() {
Ok(_) => (),
Err(e) => println!("{}", e),
}
loop {
let (_, terminal_height) = terminal::size().unwrap_or((0, 10));
let max_lines = (terminal_height - 1) as usize;
match execute!(
stdout,
cursor::MoveTo(0, 0),
terminal::Clear(ClearType::All)
) {
Ok(_) => (),
Err(e) => println!("{}", e),
}
match execute!(stdout, cursor::Hide) {
Ok(_) => (),
Err(e) => println!("{}", e),
}
for (i, line) in lines.iter().skip(offset).take(max_lines).enumerate() {
match execute!(stdout, cursor::MoveTo(0, i as u16)) {
Ok(_) => (),
Err(e) => println!("{}", e),
}
match writeln!(stdout, "{}", line) {
Ok(_) => (),
Err(e) => println!("{}", e),
}
}
if let Event::Key(key) = event::read().unwrap_or(Event::FocusGained) {
match key.code {
KeyCodeT::Up => {
offset = offset.saturating_sub(1);
}
KeyCodeT::Down => {
if offset + max_lines < lines.len() {
offset += 1;
}
}
KeyCodeT::Enter => break,
_ => {}
}
}
}
match terminal::disable_raw_mode() {
Ok(_) => (),
Err(e) => println!("{}", e),
}
}
fn rcv_message(msg: &Message) -> String {
clear_screen(&mut stdout());
let message = msg.get_body();
let datetime = get_human_time(msg.get_date());
format!(
"\n KraKomanoVian Message System - v{VERSION_KKV}\n {}\n\nAt: {}\n\n{}",
"** Your OpSec is our Mission **".italic(),
datetime,
message
)
}
pub fn sign_message(message: &[u8], path: [u8; 64]) -> [u8; 64] {
let mut msg = Vec::new();
msg.extend_from_slice(message);
let signing_key: SigningKey = SigningKey::from_keypair_bytes(&path).unwrap();
signing_key.sign(&msg).to_bytes()
}
pub fn enter_alt_screen(stdout: &mut std::io::Stdout) -> Result<(), String> {
match stdout.execute(terminal::EnterAlternateScreen) {
Ok(_) => Ok(()),
Err(e) => Err(e.to_string()),
}
}
pub fn clear_screen(stdout: &mut std::io::Stdout) {
match execute!(stdout, Clear(ClearType::All), cursor::MoveTo(0, 0)) {
Ok(ok) => ok,
Err(e) => println!("{}", e.to_string().red()),
};
}
pub fn get_last_elements(array: Vec<Message>, chunk_index: usize) -> Vec<Message> {
let length = array.len();
let chunk_size = 10;
if length <= chunk_size {
return array;
}
let start = if length > (chunk_size * (chunk_index + 1)) {
length - (chunk_size * (chunk_index + 1))
} else {
0
};
let end = start + chunk_size.min(length - start);
array[start..end].to_vec()
}
pub fn deserialize_keys(data: &[u8; 76]) -> ([u8; 32], u64, [u8; 32], u32) {
let mut byte_array1: [u8; 32] = [0; 32];
byte_array1.copy_from_slice(&data[36..68]);
let u64_value = u64::from_le_bytes(data[68..76].try_into().unwrap_or([0; 8]));
let mut byte_array2: [u8; 32] = [0; 32];
byte_array2.copy_from_slice(&data[0..32]);
let u32_value = u32::from_le_bytes(data[32..36].try_into().unwrap_or([0; 4]));
(byte_array1, u64_value, byte_array2, u32_value)
}