diff --git a/src/crypt_lib.rs b/src/crypt_lib.rs new file mode 100644 index 0000000..9234ac4 --- /dev/null +++ b/src/crypt_lib.rs @@ -0,0 +1,60 @@ +use crate::utils::{trim_newline}; +/// encrypt & decrypt content with `enc_file` using `XChaCha20Poly1305` and random nonce +///```rust +/// let texto = String::from("Esto es una prueba"); +/// println!("{}",&texto); +/// let ky = String::from("an example very very secret key."); +/// let key= ky.as_str(); //Plaintext to encrypt +/// let res_enc = encrypt(&texto,key); +/// println!("encripty: {}",&res_enc); +/// let res_dec = decrypt(&res_enc,key); +/// println!("decrypt: {}",&res_dec); +///``` +#[must_use] +pub fn encrypt(text: &str, key: &str) -> String { + if key.is_empty() { + return text.to_string(); + } + //let txt = format!(r#"{}"#, text); //Plaintext to encrypt + let text_vec = text.as_bytes().to_vec(); + //let key: &str = "an example very very secret key."; //Key will normally be chosen from keymap and provided to the encrypt_chacha() function + //let text_vec: Vec = txt_char.to_vec(); //Convert text to Vec + //Ciphertext stores the len() of encrypted content, the nonce and the actual ciphertext using bincode + match enc_file::encrypt_chacha(text_vec, key) { + //encrypt vec, returns result(Vec) + Ok(ciphertext) => base64::encode(&ciphertext), + Err(e) => { + println!("Error encrypt: {}", e); + String::from("") + } + } +} +/// decrypt content with `enc_file` using `XChaCha20Poly1305` and random nonce +#[must_use] +pub fn decrypt(text: &str, key: &str) -> String { + if key.is_empty() { + return text.to_string(); + } + let mut input = text.to_owned(); + trim_newline(&mut input); + match base64::decode(&input) { + Ok(inpt_text) => match enc_file::decrypt_chacha(inpt_text, key) { + Ok(res) => match String::from_utf8(res) { + Ok(result) => result, + Err(e) => { + println!("Error utf8 decode: {}", e); + String::from("") + } + }, + Err(e) => { + println!("Error decrypt: {}", e); + String::from("") + } + }, + Err(e) => { + println!("Error base64 decode: {}", e); + String::from("") + } + } + // println!("text: {} plain: {}",format!("{:?}", &text), format!("{:?}", plaintext)); //Check that text == plaintext +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..1277e4f --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,12 @@ +/*! tkdr functions for variety of tasks */ +// +// Copyright 2020, Jesús Pérez Lorenzo + +/// utilities +pub mod utils; +/// Tera utilities +pub mod tera_lib; +/// Crypto & Decrypt +pub mod crypt_lib; +/// Random key +pub mod randomkey; diff --git a/src/randomkey.rs b/src/randomkey.rs new file mode 100644 index 0000000..49c7956 --- /dev/null +++ b/src/randomkey.rs @@ -0,0 +1,127 @@ +/// Random keygen form [rusty-keys](https://github.com/rideron89/rusty-keys) +///```rust +/// let random = RandomKey::default(); +/// let tkn0 = random.from_template("uuid",1)[0]; +/// let tkn1 = random.get_key(); +/// let tkn = format!("{}\n{}\n",tkn0,&tkn1); +///``` +/// `RandomKey` can be configured from Config (config.toml) with values for +/// - length: `config.auth.keys_length`, +/// - `alfanumeric_only`: `config.auth.keys_alfanumeric`, +/// +#[derive(Debug, Clone)] +pub struct RandomKey { + count: u32, + length: u32, + alfanumeric_only: bool, +} +impl Default for RandomKey { + fn default() -> Self { + Self { + count: 1, + length: 16, + alfanumeric_only: false, + } + } +} +impl RandomKey { + #[allow(clippy::missing_docs_in_private_items)] + /// Configure `RandomKey` + #[must_use] + pub fn config(length: u32, alfanumeric_only: bool) -> Self { + Self { + count: 1, + length, + alfanumeric_only, + } + } + #[allow(clippy::missing_docs_in_private_items)] + /// Get a key + #[must_use] + pub fn get_key(&self) -> String { + match self + .get_list(self.length, self.count, self.alfanumeric_only) + .get(0) + { + Some(key) => key.to_owned(), + None => String::from(""), + } + } + /// Get a list of keys + #[must_use] + #[allow( + clippy::missing_docs_in_private_items, + clippy::cast_precision_loss, + clippy::cast_sign_loss, + clippy::cast_possible_truncation + )] + pub fn get_list(&self, length: u32, count: u32, allow_symbols: bool) -> Vec { + let mut alphabet = + String::from("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNPQRSTUVWXYZ0123456789"); + + if allow_symbols { + alphabet.push_str("/.;!@$"); + } + let mut codes: Vec = Vec::new(); + for _ in 0..count { + let mut code = String::new(); + + for _ in 0..length { + let number = rand::random::() * (alphabet.len() as f32); + let number = number.round() as usize; + if let Some(character) = alphabet.chars().nth(number) { + code.push(character) + } + } + codes.push(code); + } + codes + } + /// Print a list of passwords for a given template. + #[must_use] + pub fn from_template(&self, template: &str, count: u32) -> Vec { + let mut codes: Vec = Vec::new(); + for _ in 0..count { + match template { + "uuid" => codes.push(self.generate_uuid()), + &_ => { + // the process should never get to this point, because we are checking + // against valid templates in a previous step + eprintln!("'{}' is an invalid template", template); + } + } + } + codes + } + /// Generate a UUID key. + /// Format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + /// Example: c17247f6-55b6-9226-28fc-952b7becc6e9 + #[must_use] + #[allow( + clippy::unnecessary_cast, + clippy::cast_sign_loss, + clippy::cast_possible_truncation + )] + pub fn generate_uuid(&self) -> String { + let alphabet = String::from("abcdef0123456789"); + + let mut code = String::new(); + + for _ in 0..32 { + // 16 is the length of the above Hex alphabet + let number = rand::random::() * (16 as f32); + let number = number.round() as usize; + + if let Some(character) = alphabet.chars().nth(number) { + code.push(character) + } + } + + code.insert(20, '-'); + code.insert(16, '-'); + code.insert(12, '-'); + code.insert(8, '-'); + + code + } +} \ No newline at end of file diff --git a/src/tera_lib.rs b/src/tera_lib.rs new file mode 100644 index 0000000..734421f --- /dev/null +++ b/src/tera_lib.rs @@ -0,0 +1,231 @@ +/*! Utility functions for variety of tasks */ +// +// Copyright 2020, Jesús Pérez Lorenzo + +use std::collections::HashMap; +use std::fs; +use std::path::Path; +use tera::{Tera}; + +/// make content from template path: from `root_path` + extension search for ".toml" or ".json" +/// template should be `root_path`.html and lang should be included in `root_path` +#[must_use] +#[allow(clippy::implicit_hasher)] +pub fn make_template_content( + root_path: &str, + ctx: &mut tera::Context, + data_hash: &mut HashMap, + load_extend: bool, +) -> String { + let mut data_path = format!("{}.toml", &root_path); + if !Path::new(&data_path).exists() { + data_path = format!("{}.json", &root_path); + } + match hash_from_data(&data_path,ctx,data_hash,load_extend) { + Ok(_) => match data_templated( + "", + format!("{}.html", &root_path).as_str(), + ctx, + data_hash, + ) { + Ok(templated_file) => return templated_file, + Err(e) => println!( + "Error generating template {}.html: {}", + &root_path, e + ), + }, + Err(e) => println!( + "Error with data template {} - {} : {}", + &root_path, &data_path, e + ), + } + "".to_string() +} +/// `get_json_val` is a simple match Option to remove quotes in json values and returned as a String +#[must_use] +pub fn get_json_val(json_value: &serde_json::Value, dflt_val: String) -> String { + match json_value.as_str() { + Some(val) => val.to_string(), + None => dflt_val, + } +} +/// `get_toml_val` is a simple match Option to remove quotes in json values and returned as a String +#[must_use] +pub fn get_toml_val(toml_value: &toml::Value, dflt_val: String) -> String { + match toml_value.as_str() { + Some(val) => val.to_string(), + None => dflt_val, + } +} +/// `get_toml_val` is a simple match Option to remove quotes in json values and returned as a String +#[must_use] +pub fn get_yaml_val(yaml_value: &serde_yaml::Value, dflt_val: String) -> String { + match yaml_value.as_str() { + Some(val) => val.to_string(), + None => dflt_val, + } +} +/// Load a data file, fill a receive HASH, load template file and applied with Tera +/// By file extension is able to load data from JSON or TOML (as a table Value) +/// +/// Unwrap lines in the following example has to be changed to "matched" or ? with -> Result<> +///```rust +/// let mut data_hash: HashMap = HashMap::new(); +/// let path = String::from("resources/mail/test.json".to_string()); +/// let res = hash_from_data(&data_path, &mut data_hash).unwrap(); +/// let root_path = String::from("resources/mail"); +/// let tpl_result = data_templated(&root_path,"test.html",&res).unwrap(); +/// ``` +#[allow(clippy::missing_errors_doc,clippy::implicit_hasher)] + + +// +pub fn hash_from_data(path: &str, ctx: &mut tera::Context, data_hash: &mut HashMap, load_extend: bool) -> anyhow::Result<()> { + // {HashMap> { + let mut check_value = | key: String, v: String | { + if let Some(first_char) = v.chars().next() { + if first_char == '@' { + let field_file = &v[1..]; + let _ = hash_from_data(field_file, ctx, data_hash, load_extend); + // dbg!("{}",&data_hash); + return; + } + } + data_hash.insert(key, v); + }; + let vars_content = fs::read_to_string(&path)?; + //let mut data_hash: HashMap = HashMap::new(); + if let Some(extension) = path.split('.').last() { + //println!("File {} extension {}", &path, &extension); + match extension { + "json" => { + let data: HashMap = serde_json::from_str(&vars_content)?; + if load_extend { + match tera::Context::from_serialize(data) { + Ok(res) => { + ctx.extend(res); + }, + Err(e) => { + println!("Error loading {}: {}",&path, e); + }, + } + } else { + for (key, value) in data { + let val = get_json_val(&value, "".into()); + check_value(key,val); + } + } + } + "toml" => { + // As a table content + let data: toml::Value = toml::from_str(&vars_content)?; + // let data = &vars_content.parse::()?; + if load_extend { + match tera::Context::from_serialize(data) { + Ok(res) => { + ctx.extend(res); + }, + Err(e) => { + println!("Error loading {}: {}",&path, e); + }, + } + } else if let Some(table_data) = data.as_table() { + for (key, value) in table_data { + println!("-{}-{}",&key,&value); + let val = get_toml_val(&value, "".into()); + check_value(key.into(),val); + } + } + } + "yaml" => { + let data: HashMap = serde_yaml::from_str(&vars_content)?; + if load_extend { + match tera::Context::from_serialize(data) { + Ok(res) => { + ctx.extend(res); + }, + Err(e) => { + println!("Error loading {}: {}",&path, e); + }, + } + } else { + for (key, value) in data { + let val = get_yaml_val(&value, "".into()); + check_value(key,val); + } + } + } + _ => { + println!( + "File {} format extension {} not defined ", + &path, &extension + ); + } + } + } else { + println!("File {} unknown extension", &path); + } + // dbg!(&data_hash); + Ok(()) // data_hash) +} +/// Data tempalted create a Tera context form hashmap and reads a file template from path/template_name +/// Finally render to a String +#[allow(clippy::missing_errors_doc, clippy::implicit_hasher)] +pub fn data_templated( + path: &str, + template_name: &str, + tpl_context: &mut tera::Context, + content: &HashMap, +) -> anyhow::Result { + // let mut tpl_context = Context::new(); + for (key, value) in content.iter() { + tpl_context.insert(key, value); + } + let mut tera = Tera::default(); + + let template_head = "header.html"; + let tpl = read_path_file(&path, &template_name, "content")?; + let mut all_tpls = vec![("data-template.html",tpl)]; + match read_path_file(&path, &template_head, "content") { + Ok(tpl_head) => all_tpls.push((&template_head,tpl_head)), + Err(_) => {} // ignore if no header + } + tera.add_raw_templates(all_tpls)?; + let res = tera.render("data-template.html", &tpl_context)?; + Ok(res) +} +/// Read a `file_name` from a path (root / `file_name`) ( "" `file_name_full_path` ) +/// format of reading can be matched with `file_type` +/// TODO implement other types to read +#[allow(clippy::missing_errors_doc)] +pub fn read_path_file(path: &str, file_name: &str, file_type: &str) -> anyhow::Result { + let mut result = String::from(""); + let mut root_path = format!("{}/", path); + if root_path.as_str() == "/" { + root_path = String::from(""); + } + //println!("{}",format!("{}{}", &root_path.as_str(), &file_name)); + match file_type { + "content" => { + result = fs::read_to_string(format!("{}{}", &root_path.as_str(), &file_name).as_str())? + } + _ => println!( + "File {}{} type {} not defined ", + &root_path, &file_name, &file_type + ), + } + Ok(result) +} +/// `hash_content` to hash content with blake3:hash +///```rust +/// let texto = String::from("Esto es una prueba"); +/// println!("{}",&texto); +/// println!("hash: {}",hash_content(&texto)); +///``` +#[must_use] +pub fn hash_content(content: &str) -> String { + let cntnt = content.as_bytes().to_vec(); //std::fmt::Binary(content.as_str()); + // blake3::hash(b"foobarbaz"); + let hash = blake3::hash(&cntnt); + format!("{}", hash.to_hex()) +} \ No newline at end of file diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..b333873 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,123 @@ +/*! Utility functions for variety of tasks */ +// +// Copyright 2020, Jesús Pérez Lorenzo + +use anyhow::anyhow; +use regex::Regex; +use std::fs; +use std::process::Command; + +#[derive(PartialEq, Default, Clone, Debug)] +struct Commit { + hash: String, + message: String, +} +/// A very simple Operating System Command runner capturing output +/// Allows two arguments, they can be empty +///```rust +/// let res = +/// match run_command("pwd", "", "") { +/// Ok(_) => format!("{} {}", "pwd","Done"), +/// Err(e) => format!("{}",e), +/// }; +/// println!("Result: {}",res); +/// ``` +#[allow(dead_code)] +#[allow(clippy::missing_errors_doc, clippy::filter_map)] +pub fn run_command(cmd: &str, arg1: &str, arg2: &str) -> anyhow::Result<()> { + let output = match arg2 { + "" => match arg1 { + "" => Command::new(cmd).output()?, + _ => Command::new(cmd).arg(arg1).output()?, + }, + _ => Command::new(cmd).arg(arg1).arg(arg2).output()?, + }; + + if !output.status.success() { + //dbg!(&output); + let code = match output.status.code() { + Some(val) => val, + None => -1, + }; + let stderr = String::from_utf8(output.stderr)?; + let msg = format!( + "Command {} executed with failing errors({}): {}", + &cmd, &code, &stderr + ); + println!("{}", &msg); + return Err(anyhow!(msg)); + // error_chain::bail!("Command {} executed with failing error code", &cmd); + } + + let pattern = Regex::new( + r"(?x) + ([0-9a-fA-F]+) # commit hash + (.*) # The commit message", + )?; + String::from_utf8(output.stdout)? + .lines() + .filter_map(|line| pattern.captures(line)) + .map(|cap| Commit { + hash: cap[1].to_string(), + message: cap[2].trim().to_string(), + }) + .take(5) + .for_each(|x| println!("{:?}", x)); + + Ok(()) +} +/// Trim newline +/// this function is critical for base64 content as usually adds a newline at the end, causing decode not work properly with code. +/// `trim_newline` should be called to clean content before to be decoded. +pub fn trim_newline(s: &mut String) { + if s.ends_with('\n') { + s.pop(); + if s.ends_with('\r') { + s.pop(); + } + } +} +/// Get last char from String +#[must_use] +pub fn get_last_char_num(text: &str) -> u32 { + let v_last: Vec = text.chars().rev().take(1).collect(); + let s_last: String = v_last.iter().collect(); + let mut num = 0; + match s_last.parse::() { + Ok(n) => num = n, + Err(e) => println!("Error last_char_num: {}", e), + } + num +} +/// Check file permissions +/// Initial idea was capture file mode as 600 but permissions for unix in rust something else has to be done +/// ```rust +/// #[cfg(target_family = "unix")] +/// use std::os::unix::fs::PermissionsExt +/// ``` +#[allow(clippy::missing_errors_doc)] +pub fn check_file_permissions(path: &str) -> anyhow::Result { +// let info = os_info::get(); +// // Print full information: +// println!("OS information: {}", info); + +// // Print information separately: +// println!("Type: {}", info.os_type()); +// println!("Version: {}", info.version()); +// println!("Bitness: {}", info.bitness()); + // let os = os_type::current_platform(); + // let macos = os_type::OSType::OSX; + // match os_type::current_platform().os_type { + // os_type::OSType::OSX => Ok(true), + // _ => { + // let perm = fs::File::open(path)?.metadata()?.permissions(); + // Ok(perm.readonly()) + // } + // } + let perm = fs::File::open(path)?.metadata()?.permissions(); + // if ! perm.readonly() { + // println!("Review permissions: {}", path); + // } + // Ok(true) + Ok(perm.readonly()) +}