diff --git a/Cargo.toml b/Cargo.toml index fda5886..5b6019a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ sha2 = "0.10" sys-locale = "0.3.2" tabled = {version = "0.19", features = ["ansi"]} zeroize = "1.8" - +age = "0.11" # Used by clap, so it's not an additional dependency. serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" +serde_json = "1.0" \ No newline at end of file diff --git a/locales/en.toml b/locales/en.toml index bcd24a1..b4583f4 100644 --- a/locales/en.toml +++ b/locales/en.toml @@ -48,6 +48,7 @@ no_x_setup_yet = "No %{x} have been setup yet" not_a_tuckr_dotfile = "`%{file}` is not a tuckr dotfile." wrong_password = "Wrong password." no_configs_dir_in_dotfiles = "There is no Configs directory in dotfiles (%{dotfiles})" +could_not_read_enc = "Could not read the encrypted dotfile at %{path}" # group name validation path_is_not_dotfiles = "`%{path}` does not belong to dotfiles" diff --git a/src/dotfiles.rs b/src/dotfiles.rs index 873b054..0673775 100644 --- a/src/dotfiles.rs +++ b/src/dotfiles.rs @@ -81,6 +81,8 @@ pub enum ReturnCode { EncryptionFailed = 5, /// Failed to decrypt referenced file DecryptionFailed = 6, + /// Failed to read encrypted referenced file + EncryptedReadFailed = 7, } impl From for process::ExitCode { diff --git a/src/secrets.rs b/src/secrets.rs index 7b282a5..c75f9a7 100644 --- a/src/secrets.rs +++ b/src/secrets.rs @@ -1,82 +1,58 @@ //! Manages encrypted files //! -//! Encrypts files into dotfiles/Secrets using the chacha20poly1305 algorithm +//! Encrypts files into dotfiles/Secrets using the age library use crate::Context; use crate::dotfiles::{self, Dotfile, ReturnCode}; use crate::fileops::DirWalk; -use chacha20poly1305::{AeadCore, KeyInit, XChaCha20Poly1305, aead, aead::Aead}; use owo_colors::OwoColorize; use rust_i18n::t; -use sha2::{Digest, Sha256}; use std::fs; use std::path::{Path, PathBuf}; use std::process::ExitCode; -struct SecretsHandler { - dotfiles_dir: PathBuf, - key: chacha20poly1305::Key, - nonce: chacha20poly1305::XNonce, -} +use age::secrecy::SecretString; -impl SecretsHandler { - fn try_new(profile: Option) -> Result { - let dotfiles_dir = match dotfiles::get_dotfiles_path(profile) { +fn get_dotfiles_dir(ctx: &Context) -> Result { + let dotfiles_dir = match dotfiles::get_dotfiles_path(ctx.profile.clone()) { Ok(path) => path, Err(e) => { eprintln!("{e}"); return Err(ReturnCode::CouldntFindDotfiles.into()); } }; + Ok(dotfiles_dir) +} - // makes a hash of the password so that it can fit on the 256 bit buffer used by the - // algorithm +fn read_passphrase()-> Result{ let input_key = rpassword::prompt_password(format!("{}: ", t!("info.password"))).unwrap(); - let input_hash = Sha256::digest(input_key); - - Ok(SecretsHandler { - dotfiles_dir, - key: input_hash, - nonce: XChaCha20Poly1305::generate_nonce(&mut aead::OsRng), - }) - } - - /// takes a path to a file and returns its encrypted content - fn encrypt(&self, dotfile: &Path) -> Result, ExitCode> { - let cipher = XChaCha20Poly1305::new(&self.key); - let Ok(dotfile) = fs::read(dotfile) else { - eprintln!( - "{}", - t!("errors.x_doesnt_exist", x = dotfile.display()).red() - ); - return Err(ReturnCode::NoSuchFileOrDir.into()); - }; - - match cipher.encrypt(&self.nonce, dotfile.as_slice()) { - Ok(f) => Ok(f), - Err(e) => { - eprintln!("{}", e.red()); - Err(ReturnCode::EncryptionFailed.into()) - } - } - } + Ok(SecretString::from(input_key)) +} +fn encrypt(recipient: &age::scrypt::Recipient, dotfile: &Path) -> Result, ExitCode> { + let Ok(dotfile) = fs::read(dotfile) else { + eprintln!( + "{}", + t!("errors.x_doesnt_exist", x = dotfile.display()).red() + ); + return Err(ReturnCode::NoSuchFileOrDir.into()); + }; - /// takes a path to a file and returns its decrypted content - fn decrypt(&self, dotfile: &str) -> Result, ExitCode> { - let cipher = XChaCha20Poly1305::new(&self.key); - let dotfile = fs::read(dotfile).expect("Couldn't read dotfile"); + age::encrypt(recipient, &dotfile).map_err(|e|{ + eprintln!("{}", e.red()); + ReturnCode::EncryptionFailed.into() + }) +} - // extracts the nonce from the first 24 bytes in the file - let (nonce, contents) = dotfile.split_at(24); +fn decrypt(identity: &age::scrypt::Identity, dotfile: &Path) -> Result, ExitCode> { + let Ok(dotfile) = fs::read(dotfile) else { + eprintln!("{}", t!("errors.could_not_read_enc", path = dotfile.display()).red()); + return Err(ReturnCode::EncryptedReadFailed.into()) + }; - match cipher.decrypt(nonce.into(), contents) { - Ok(f) => Ok(f), - Err(_) => { - eprintln!("{}", t!("errors.wrong_password").red()); - Err(ReturnCode::DecryptionFailed.into()) - } - } - } + age::decrypt(identity, &dotfile).map_err(|_|{ + eprintln!("{}", t!("errors.wrong_password").red()); + ReturnCode::DecryptionFailed.into() + }) } /// Encrypts secrets @@ -95,9 +71,11 @@ pub fn encrypt_cmd(ctx: &Context, group: &str, dotfiles: &[String]) -> Result<() } } - let handler = SecretsHandler::try_new(ctx.profile.clone())?; + //let handler = SecretsHandler::try_new(profile)?; + let passphrase = read_passphrase()?; + let recipient = age::scrypt::Recipient::new(passphrase); - let dest_dir = handler.dotfiles_dir.join("Secrets").join(group); + let dest_dir = get_dotfiles_dir(ctx)?.join("Secrets").join(group); if !dest_dir.exists() { fs::create_dir_all(&dest_dir).unwrap(); } @@ -133,10 +111,7 @@ pub fn encrypt_cmd(ctx: &Context, group: &str, dotfiles: &[String]) -> Result<() tf }; - let mut encrypted = handler.encrypt(dotfile)?; - let mut encrypted_file = handler.nonce.to_vec(); - // appends a 24 byte nonce to the beginning of the file - encrypted_file.append(&mut encrypted); + let encrypted_file = encrypt(&recipient, dotfile)?; // makes sure all parent directories of the dotfile are created fs::create_dir_all(dest_dir.join(dir_path)).unwrap(); @@ -167,8 +142,14 @@ pub fn encrypt_cmd(ctx: &Context, group: &str, dotfiles: &[String]) -> Result<() } /// Decrypts secrets -pub fn decrypt_cmd(ctx: &Context, groups: &[String], exclude: &[String]) -> Result<(), ExitCode> { - let handler = SecretsHandler::try_new(ctx.profile.clone())?; +pub fn decrypt_cmd( + ctx: &Context, + groups: &[String], + exclude: &[String], +) -> Result<(), ExitCode> { + let passphrase = read_passphrase()?; + let identity = age::scrypt::Identity::new(passphrase); + let dotfiles_dir = get_dotfiles_dir(ctx)?; if let Some(invalid_groups) = dotfiles::get_nonexistent_groups( ctx.profile.clone(), @@ -194,7 +175,7 @@ pub fn decrypt_cmd(ctx: &Context, groups: &[String], exclude: &[String]) -> Resu return Ok(()); } - let group_dir = handler.dotfiles_dir.join("Secrets").join(&group.group_path); + let group_dir = dotfiles_dir.join("Secrets").join(&group.group_path); for secret in DirWalk::new(&group_dir) { if secret.is_dir() { continue; @@ -219,7 +200,7 @@ pub fn decrypt_cmd(ctx: &Context, groups: &[String], exclude: &[String]) -> Resu let decrypted_parent_dir = decrypted_dest.parent().unwrap(); fs::create_dir_all(decrypted_parent_dir).unwrap(); - let decrypted = handler.decrypt(secret.to_str().unwrap())?; + let decrypted = decrypt(&identity, &secret)?; fs::write(decrypted_dest, decrypted).unwrap(); } @@ -227,7 +208,7 @@ pub fn decrypt_cmd(ctx: &Context, groups: &[String], exclude: &[String]) -> Resu }; if groups.contains(&"*".to_string()) { - let groups_dir = handler.dotfiles_dir.join("Secrets"); + let groups_dir = dotfiles_dir.join("Secrets"); for group in fs::read_dir(groups_dir).unwrap() { let Ok(group) = Dotfile::try_from(group.unwrap().path()) else { eprintln!("{}", t!("errors.got_invalid_group").red()); @@ -240,7 +221,7 @@ pub fn decrypt_cmd(ctx: &Context, groups: &[String], exclude: &[String]) -> Resu } for group in groups { - let group = handler.dotfiles_dir.join("Secrets").join(group); + let group = dotfiles_dir.join("Secrets").join(group); let Ok(group) = Dotfile::try_from(group) else { eprintln!("{}", t!("errors.got_invalid_group").red()); return Err(ExitCode::FAILURE);