use std::{env, fs, process};
use aes_gcm::{
aead::{Aead, AeadCore, KeyInit},
Aes256Gcm, Key, Nonce,
};
use argon2::Argon2;
use rand::{rngs::OsRng, RngCore};
use rpassword::prompt_password;
const MAGIC: &[u8; 4] = b"AE56";
const SALT_LEN: usize = 16;
const NONCE_LEN: usize = 12;
const HEADER_LEN: usize = 4 + SALT_LEN + NONCE_LEN;
fn die(msg: &str) -> ! {
eprintln!("error: {msg}");
process::exit(1);
}
fn derive_key(password: &[u8], salt: &[u8]) -> [u8; 32] {
let mut key = [0u8; 32];
Argon2::default()
.hash_password_into(password, salt, &mut key)
.unwrap_or_else(|_| die("key derivation failed"));
key
}
fn encrypt(path: &str) {
let pass = prompt_password("Password: ").unwrap_or_else(|_| die("failed to read password"));
let confirm = prompt_password("Confirm: ").unwrap_or_else(|_| die("failed to read password"));
if pass != confirm {
die("passwords do not match");
}
let plaintext = fs::read(path)
.unwrap_or_else(|e| die(&format!("cannot read '{path}': {e}")));
let mut salt = [0u8; SALT_LEN];
OsRng.fill_bytes(&mut salt);
let nonce_arr = Aes256Gcm::generate_nonce(&mut OsRng);
let key_bytes = derive_key(pass.as_bytes(), &salt);
let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(&key_bytes));
let ciphertext = cipher
.encrypt(&nonce_arr, plaintext.as_ref())
.unwrap_or_else(|_| die("encryption failed"));
let mut out = Vec::with_capacity(HEADER_LEN + ciphertext.len());
out.extend_from_slice(MAGIC);
out.extend_from_slice(&salt);
out.extend_from_slice(nonce_arr.as_slice());
out.extend_from_slice(&ciphertext);
let out_path = format!("{path}.enc");
fs::write(&out_path, &out)
.unwrap_or_else(|e| die(&format!("cannot write '{out_path}': {e}")));
let meta = fs::metadata(path).unwrap_or_else(|e| die(&format!("cannot stat '{path}': {e}")));
let mut perms = meta.permissions();
#[allow(clippy::permissions_set_readonly_false)]
perms.set_readonly(false);
let _ = fs::set_permissions(path, perms);
fs::remove_file(path).unwrap_or_else(|e| die(&format!("cannot delete '{path}': {e}")));
println!("Encrypted as {out_path}");
}
fn decrypt(path: &str) {
let pass = prompt_password("Password: ").unwrap_or_else(|_| die("failed to read password"));
let data = fs::read(path)
.unwrap_or_else(|e| die(&format!("cannot read '{path}': {e}")));
if data.len() < HEADER_LEN + 16 || &data[..4] != MAGIC {
die("not a valid .enc file");
}
let salt = &data[4..4 + SALT_LEN];
let nonce = Nonce::from_slice(&data[4 + SALT_LEN..HEADER_LEN]);
let cipher_bytes = &data[HEADER_LEN..];
let key_bytes = derive_key(pass.as_bytes(), salt);
let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(&key_bytes));
let plaintext = cipher
.decrypt(nonce, cipher_bytes)
.unwrap_or_else(|_| die("decryption failed — wrong password or corrupted file"));
let out_path = match path.strip_suffix(".enc") {
Some(p) => p.to_string(),
None => format!("{path}.dec"),
};
fs::write(&out_path, &plaintext)
.unwrap_or_else(|e| die(&format!("cannot write '{out_path}': {e}")));
let meta = fs::metadata(path).unwrap_or_else(|e| die(&format!("cannot stat '{path}': {e}")));
let mut perms = meta.permissions();
#[allow(clippy::permissions_set_readonly_false)]
perms.set_readonly(false);
let _ = fs::set_permissions(path, perms);
fs::remove_file(path).unwrap_or_else(|e| die(&format!("cannot delete '{path}': {e}")));
println!("Decrypted as {out_path}");
}
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() != 3 {
eprintln!("Usage:");
eprintln!(" aes256 -e <file> encrypt");
eprintln!(" aes256 -d <file> decrypt");
process::exit(1);
}
match args[1].as_str() {
"-e" | "--encrypt" => encrypt(&args[2]),
"-d" | "--decrypt" => decrypt(&args[2]),
flag => {
eprintln!("unknown flag '{flag}' — use -e or -d");
process::exit(1);
}
}
}