use aes_gcm::aead::{Aead, KeyInit};
use aes_gcm::{Aes256Gcm, Nonce};
use colored::Colorize;
use core::str;
use serde::{Deserialize, Serialize};
use sha2::Digest;
use std::fs::{self, File};
use std::io::{self, Write};
use std::ops::Not;
use std::path::{Path, PathBuf};
use std::sync::{Arc, OnceLock};
use tracing::instrument;
use uuid::Uuid;
use error::Error;
use rand::Rng;
pub(crate) mod r#box;
pub(crate) mod bundle;
pub mod container;
pub mod error;
pub mod pgp;
pub mod storage;
const BOX_DELIMITER: &str = "::";
const BUNDLE_DELIMITER: &str = ",";
pub const BAZA_DIR: &str = ".baza";
pub const DEFAULT_EMAIL: &str = "root@baza";
pub const DEFAULT_AUTHOR: &str = "Root Baza";
pub const TTL_SECONDS: u64 = 45;
static CTX: OnceLock<Arc<Config>> = OnceLock::new();
pub enum MessageType {
Clean,
Data,
Info,
Warning,
Error,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Config {
pub main: MainConfig,
pub gitfs: GitConfig,
pub gix: GixConfig,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct MainConfig {
pub datadir: String,
pub box_delimiter: String,
pub bundle_delimiter: String,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct GitConfig {
pub enable: Option<bool>,
pub url: Option<String>,
pub privatekey: Option<String>,
pub passphrase: Option<String>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct GixConfig {
pub enable: Option<bool>,
}
impl Config {
fn new() -> Config {
let home = std::env::var("HOME").unwrap();
Config {
main: MainConfig {
box_delimiter: String::from(BOX_DELIMITER),
bundle_delimiter: String::from(BUNDLE_DELIMITER),
datadir: format!("{}/{}", home, String::from(BAZA_DIR)),
},
gitfs: GitConfig {
enable: Some(true),
url: None,
privatekey: None,
passphrase: None,
},
gix: GixConfig {
enable: Some(false),
},
}
}
fn init() -> Self {
let config_str: String = if Path::new("Baza.toml").exists() {
tracing::debug!("Use config in current folder Baza.toml");
fs::read_to_string("Baza.toml").expect("Failed to read config file")
} else {
let home = std::env::var("HOME").unwrap();
let config_path = format!("{}/.Baza.toml", home);
if !Path::new(&config_path).exists() {
tracing::info!("A new configuration file has been created");
let config = Config::new();
let toml = toml::to_string(&config).expect("Failed to serialize struct");
fs::write(&config_path, toml).expect("Failed to write config file");
};
fs::read_to_string(config_path).expect("Failed to read config file")
};
toml::from_str(&config_str).expect("Failed to parse TOML")
}
pub fn get() -> Arc<Self> {
CTX.get_or_init(|| Arc::new(Config::init())).clone()
}
}
pub type BazaR<T> = Result<T, Error>;
pub fn generate(length: u8, no_latters: bool, no_symbols: bool, no_numbers: bool) -> BazaR<String> {
let latters = "abcdefghijklmnopqrstuvwxyz\
ABCDEFGHIJKLMNOPQRSTUVWXYZ";
let numbers = "0123456789";
let symbols = "!@#$%^&*()_-+=<>?";
let mut chars: String = Default::default();
no_latters.not().then(|| chars.push_str(latters));
no_numbers.not().then(|| chars.push_str(numbers));
no_symbols.not().then(|| chars.push_str(symbols));
let chars = chars.as_bytes();
Ok((0..length)
.map(|_| {
let idx = rand::thread_rng().gen_range(0..chars.len());
chars[idx] as char
})
.collect())
}
fn as_hash(str: &str) -> [u8; 32] {
let mut hasher = sha2::Sha256::new();
hasher.update(str.as_bytes());
let result = hasher.finalize();
result.into()
}
pub(crate) fn key_file() -> String {
let config = Config::get();
let datadir = &config.main.datadir;
format!("{datadir}/key.bin")
}
pub fn lock() -> BazaR<()> {
fs::remove_file(key_file())?;
Ok(())
}
pub fn sync() -> BazaR<()> {
if let Some(_url) = &Config::get().gitfs.url {
storage::sync()?;
} else {
tracing::info!("Please set url for git remote");
}
Ok(())
}
pub fn unlock(passphrase: Option<String>) -> BazaR<()> {
let passphrase = if let Some(passphrase) = passphrase {
passphrase
} else {
let mut passphrase = String::new();
m("Enter your password: ", MessageType::Warning);
io::stdout().flush()?;
io::stdin().read_line(&mut passphrase)?;
passphrase
};
let datadir = &Config::get().main.datadir;
let key = as_hash(passphrase.trim());
fs::create_dir_all(datadir)?;
let mut file = File::create(key_file())?;
file.write_all(&key)?;
Ok(())
}
#[instrument(skip_all)]
pub(crate) fn key() -> BazaR<Vec<u8>> {
let data = match fs::read(key_file()) {
Ok(data) => data,
Err(_) => {
return Err(Error::KeyNotFound);
}
};
Ok(data)
}
pub fn m(msg: &str, r#type: MessageType) {
let msg = match r#type {
MessageType::Clean => msg,
MessageType::Data => &format!("{}", msg.bright_blue()),
MessageType::Info => &format!("{}", msg.bright_green()),
MessageType::Warning => &format!("{}", msg.bright_yellow()),
MessageType::Error => &format!("{}", msg.bright_red()),
};
print!("{}", msg);
}
pub fn cleanup_tmp_folder() -> BazaR<()> {
let datadir = &Config::get().main.datadir;
let tmpdir = format!("{}/tmp", datadir);
if std::fs::remove_dir_all(&tmpdir)
.map_err(Error::CleanupTmpFolder)
.is_err()
{
tracing::debug!("Tmp folder already cleaned");
};
std::fs::create_dir_all(format!("{}/tmp", datadir)).map_err(Error::CleanupTmpFolder)?;
Ok(())
}
pub fn init(passphrase: Option<String>) -> BazaR<()> {
let datadir = &Config::get().main.datadir;
fs::create_dir_all(format!("{}/data", datadir))?;
storage::initialize()?;
let passphrase = passphrase.unwrap_or(Uuid::new_v4().hyphenated().to_string());
tracing::info!("Initializing baza in data directory");
tracing::warn!(passphrase, "!!! Save this password phrase for future use");
unlock(Some(passphrase))?;
pgp::generate()?;
Ok(())
}
pub(crate) fn encrypt_file(path: &PathBuf) -> BazaR<()> {
let data = fs::read(path)?;
let encrypted = encrypt_data(&data, &key()?)?;
let mut file = File::create(path)?;
file.write_all(&encrypted)?;
Ok(())
}
pub(crate) fn encrypt_data(plaintext: &[u8], key: &[u8]) -> BazaR<Vec<u8>> {
let cipher = Aes256Gcm::new(key.into());
let mut nonce = [0u8; 12];
rand::thread_rng().fill(&mut nonce);
let nonce = Nonce::from_slice(&nonce);
let ciphertext = cipher
.encrypt(nonce, plaintext)
.map_err(Error::Encription)?;
Ok([nonce.as_slice(), &ciphertext].concat())
}
pub(crate) fn decrypt_file(path: &PathBuf) -> BazaR<()> {
let ciphertext = fs::read(path)?;
let encrypted = decrypt_data(&ciphertext, &key()?)?;
let mut file = File::create(path)?;
file.write_all(&encrypted)?;
Ok(())
}
#[instrument(skip_all)]
pub(crate) fn decrypt_data(ciphertext: &[u8], key: &[u8]) -> BazaR<Vec<u8>> {
let cipher = Aes256Gcm::new(key.into());
let nonce = Nonce::from_slice(&ciphertext[..12]);
let ciphertext = &ciphertext[12..];
cipher.decrypt(nonce, ciphertext).map_err(Error::Decription)
}