#![warn(clippy::pedantic)]
mod atomic;
mod header;
mod kek;
mod passwd;
use clap::Parser;
use std::{
fs::File,
io::Read,
path::{Path, PathBuf},
process::ExitCode,
};
const VERSION: u64 = 1;
#[derive(Parser)]
#[command(name = "oxismet", about = "A Headless Encryption Tool for Servers.")]
#[command(author, version)]
struct OxiSmet {
#[arg(short, long, help = "Password to be used for encryption/decryption.")]
password: Option<String>,
#[arg(
long,
help = "Password to be used for encryption/decryption, sourced from a file."
)]
password_file: Option<PathBuf>,
#[arg(
short,
long,
help = "Path to Key Encryption Key to be used for encryption/decryption."
)]
kek: Option<PathBuf>,
#[arg(
short,
long,
help = "Special mode: rotates the KEK protecting an encrypted DEK in place. \nTreats --file as the path to the encrypted DEK, this path as the old KEK, and --kek as the new KEK. \nIgnored if --kek is not supplied."
)]
migrate_dek: Option<PathBuf>,
#[arg(short, long, help = "Path to the file to be encrypted/decrypted.")]
file: PathBuf,
#[arg(short, long, help = "Path to the output file.")]
output: Option<PathBuf>,
#[clap(subcommand)]
command: EncOrDec,
}
#[derive(Parser)]
enum EncOrDec {
Encrypt,
Decrypt {
dek_encrypted: Option<PathBuf>,
},
}
fn main() -> ExitCode {
match run(OxiSmet::parse()) {
Ok(()) => ExitCode::SUCCESS,
Err(code) => code,
}
}
fn output_file_generator(command: &EncOrDec, input_path: &Path) -> Result<PathBuf, ExitCode> {
match command {
EncOrDec::Encrypt => Ok(input_path.with_added_extension("smet")),
EncOrDec::Decrypt { dek_encrypted: _ } => {
if let Some(ext) = input_path.extension()
&& ext.to_str() == Some("smet")
{
Ok(input_path.with_extension(""))
} else {
eprintln!(
"Error: No output file provided for Decrypt mode, and input is not a .smet file!"
);
Err(ExitCode::FAILURE)
}
}
}
}
fn run(args: OxiSmet) -> Result<(), ExitCode> {
if let Some(new_kek_path) = &args.kek
&& let Some(old_kek_path) = &args.migrate_dek
{
if args.password.is_some() || args.password_file.is_some() {
eprintln!("Fatal: Password and KEK provided");
return Err(ExitCode::FAILURE);
}
let old_kek_bytes = kek::read_kek_bytes(old_kek_path)?;
let new_kek_bytes = kek::read_kek_bytes(new_kek_path)?;
return kek::migrate_dek(&args.file, &old_kek_bytes, &new_kek_bytes);
}
if let EncOrDec::Decrypt { dek_encrypted } = &args.command
&& args.kek.is_some()
&& dek_encrypted.is_none()
{
eprintln!("Error: dek_encrypted must be provided when using a KEK for decryption.");
return Err(ExitCode::FAILURE);
}
let mut infile = open_file_helper(&args.file, "Error opening input file.")?;
let outfile_path = match args.output {
Some(p) => p,
None => output_file_generator(&args.command, &args.file)?,
};
let mut outfile =
atomic::AtomicFile::create(&outfile_path, "Error creating output file for ciphertext.")?;
let result = if let Some(kek) = args.kek {
if args.password.is_some() || args.password_file.is_some() {
eprintln!("Fatal: Password and KEK provided");
return Err(ExitCode::FAILURE);
}
let kek_bytes = kek::read_kek_bytes(&kek)?;
match args.command {
EncOrDec::Encrypt => {
kek::encrypt_with_kek(&mut infile, &kek_bytes, &outfile_path, &mut outfile)
}
EncOrDec::Decrypt { dek_encrypted } => {
kek::decrypt_with_kek(&mut infile, &kek_bytes, dek_encrypted, &mut outfile)
}
}
} else if let Some(password) = args.password {
if args.password_file.is_some() {
eprintln!("Fatal: Password and Password File provided");
return Err(ExitCode::FAILURE);
}
match args.command {
EncOrDec::Encrypt => {
passwd::encrypt_with_password(&password, &mut infile, &mut outfile)
}
EncOrDec::Decrypt { dek_encrypted: _ } => {
passwd::decrypt_with_password(&password, &mut infile, &mut outfile)
}
}
} else if let Some(passpath) = args.password_file {
let mut passfile = open_file_helper(&passpath, "Failed to open path to password file")?;
let mut password = String::new();
passfile.read_to_string(&mut password).map_err(|e| {
eprintln!("Error reading password file: {e}");
ExitCode::FAILURE
})?;
match args.command {
EncOrDec::Encrypt => {
passwd::encrypt_with_password(&password, &mut infile, &mut outfile)
}
EncOrDec::Decrypt { dek_encrypted: _ } => {
passwd::decrypt_with_password(&password, &mut infile, &mut outfile)
}
}
} else {
eprintln!("Must provide Password or KEK to proceed.");
Err(ExitCode::FAILURE)
};
result?;
outfile.commit()
}
fn open_file_helper(path: &Path, msg: &str) -> Result<File, ExitCode> {
File::open(path).map_err(|e| {
eprintln!("{msg}");
eprintln!("Root Error: {e}");
ExitCode::FAILURE
})
}