Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ sha2 = "0.10"
sys-locale = "0.3.2"
tabled = {version = "0.19", features = ["ansi"]}
zeroize = "1.8"

age = "0.11"

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You forgot to remove chacha20poly1305 and sha2 as dependencies

# Used by clap, so it's not an additional dependency.
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_json = "1.0"
1 change: 1 addition & 0 deletions locales/en.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions src/dotfiles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ReturnCode> for process::ExitCode {
Expand Down
115 changes: 48 additions & 67 deletions src/secrets.rs
Original file line number Diff line number Diff line change
@@ -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<String>) -> Result<Self, ExitCode> {
let dotfiles_dir = match dotfiles::get_dotfiles_path(profile) {
fn get_dotfiles_dir(ctx: &Context) -> Result<PathBuf, ExitCode> {
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<SecretString, ExitCode>{
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<Vec<u8>, 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<Vec<u8>, 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<Vec<u8>, 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<Vec<u8>, 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
Expand All @@ -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();
}
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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(),
Expand All @@ -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;
Expand All @@ -219,15 +200,15 @@ 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();
}

Ok(())
};

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());
Expand All @@ -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);
Expand Down