#![forbid(unsafe_code)]
use anubis_age::{
armor::{ArmoredReader, ArmoredWriter, Format},
cli_common::{file_io, read_identities, read_recipients, StdinGuard, UiCallbacks},
pqc::mldsa::SigningKey,
pqc::signed,
secrecy::ExposeSecret,
Identity,
};
use clap::{CommandFactory, Parser};
use i18n_embed::DesktopLanguageRequester;
use std::fs;
use std::io::{self, Read, Write};
use std::path::Path;
use log::warn;
mod cli;
use cli::AgeOptions;
mod error;
mod i18n;
#[macro_export]
macro_rules! fl {
($message_id:literal) => {{
i18n_embed_fl::fl!($crate::i18n::LANGUAGE_LOADER, $message_id)
}};
($message_id:literal, $($args:expr),* $(,)?) => {{
i18n_embed_fl::fl!($crate::i18n::LANGUAGE_LOADER, $message_id, $($args), *)
}};
}
macro_rules! warning {
($warning_id:literal) => {{
eprintln!("{}", fl!("warning-msg", warning = fl!($warning_id)));
}};
}
fn set_up_io(
input: Option<String>,
output: Option<String>,
output_format: file_io::OutputFormat,
) -> io::Result<(file_io::InputReader, file_io::OutputWriter)> {
let input = file_io::InputReader::new(input)?;
let output =
file_io::OutputWriter::new(output, true, output_format, 0o666, input.is_terminal())?;
Ok((input, output))
}
type ReadCheckerMatchCase = (&'static [u8], Box<dyn FnOnce() -> io::Result<()>>);
type ReadCheckerMatcher = Option<(&'static [u8], usize, Box<dyn FnOnce() -> io::Result<()>>)>;
struct ReadChecker<R: io::Read, const N: usize> {
inner: R,
matches: [ReadCheckerMatcher; N],
}
impl<R: io::Read, const N: usize> ReadChecker<R, N> {
fn new(inner: R, matches: [ReadCheckerMatchCase; N]) -> Self {
Self {
inner,
matches: matches.map(|(prefix, on_match)| Some((prefix, 0, on_match))),
}
}
}
impl<R: io::Read, const N: usize> io::Read for ReadChecker<R, N> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let read = self.inner.read(buf)?;
let data = &buf[..read];
for matcher in &mut self.matches {
if let Some((prefix, start, on_match)) = matcher.take() {
let to_check = &prefix[start..];
if to_check.len() > data.len() {
if to_check.starts_with(data) {
*matcher = Some((prefix, start + data.len(), on_match));
}
} else if data.starts_with(to_check) {
on_match()?;
}
}
}
Ok(read)
}
}
fn encrypt(opts: AgeOptions) -> Result<(), error::EncryptError> {
if opts.plugin_name.is_some() {
return Err(error::EncryptError::PluginNameFlag);
}
if opts.passphrase {
return Err(error::EncryptError::PassphraseDisabled);
}
let signing_key_path = opts
.signing_key
.ok_or(error::EncryptError::MissingSigningKey)?;
let key_contents = fs::read_to_string(&signing_key_path)?;
let signing_key: SigningKey = key_contents
.lines()
.find(|line| line.starts_with("ANUBIS-MLDSA-87-SECRET"))
.ok_or_else(|| {
error::EncryptError::SigningError("no signing key found in file".to_string())
})?
.parse()
.map_err(|e| error::EncryptError::SigningError(format!("invalid signing key: {}", e)))?;
let (format, output_format) = if opts.armor {
(Format::AsciiArmor, file_io::OutputFormat::Text)
} else {
(Format::Binary, file_io::OutputFormat::Binary)
};
let (input, output) = set_up_io(opts.input.clone(), opts.output.clone(), output_format)?;
let is_stdin = match input {
file_io::InputReader::File(_) => false,
file_io::InputReader::Stdin(_) => true,
};
let mut stdin_guard = StdinGuard::new(is_stdin);
let is_stdout = match output {
file_io::OutputWriter::File(..) => false,
file_io::OutputWriter::Stdout(..) => true,
};
if opts.recipient.is_empty() && opts.recipients_file.is_empty() && opts.identity.is_empty() {
return Err(error::EncryptError::Age(
anubis_age::EncryptError::MissingRecipients,
));
}
let recipients = read_recipients(
opts.recipient,
opts.recipients_file,
opts.identity,
opts.max_work_factor,
&mut stdin_guard,
)?;
let encryptor =
anubis_age::Encryptor::with_recipients(recipients.iter().map(|r| r.as_ref() as _))?;
let mut encrypted_buffer = Vec::new();
{
let mut enc_output =
encryptor.wrap_output(ArmoredWriter::wrap_output(&mut encrypted_buffer, format)?)?;
const AGE_MAGIC: &[u8] = b"anubis-encryption.org/";
const ARMORED_BEGIN_MARKER: &[u8] = b"-----BEGIN AGE ENCRYPTED FILE-----";
let warn_double_encrypting = Box::new(|| {
warning!("warn-double-encrypting");
Ok(())
});
io::copy(
&mut ReadChecker::new(
input,
[
(AGE_MAGIC, warn_double_encrypting.clone()),
(ARMORED_BEGIN_MARKER, warn_double_encrypting),
],
),
&mut enc_output,
)?;
enc_output.finish().and_then(|armor| armor.finish())?;
}
let mut signed_output = Vec::new();
signed::sign_encrypted_file(&encrypted_buffer, &signing_key, &mut signed_output)
.map_err(|e| error::EncryptError::SigningError(e.to_string()))?;
let map_io_errors = |e: io::Error| match e.kind() {
io::ErrorKind::BrokenPipe => error::EncryptError::BrokenPipe {
is_stdout,
source: e,
},
_ => e.into(),
};
match output {
file_io::OutputWriter::File(mut f) => {
f.write(&signed_output).map_err(map_io_errors)?;
}
file_io::OutputWriter::Stdout(mut s) => {
s.write(&signed_output).map_err(map_io_errors)?;
}
}
eprintln!("File encrypted and signed with ML-DSA-87");
Ok(())
}
fn write_output<R: io::Read, W: io::Write>(
mut input: R,
mut output: W,
) -> Result<(), error::DecryptError> {
io::copy(&mut input, &mut output)?;
Ok(())
}
fn decrypt(opts: AgeOptions) -> Result<(), error::DecryptError> {
if opts.armor {
return Err(error::DecryptError::ArmorFlag);
}
if opts.passphrase {
return Err(error::DecryptError::PassphraseFlag);
}
if !opts.recipient.is_empty() {
return Err(error::DecryptError::RecipientFlag);
}
if !opts.recipients_file.is_empty() {
return Err(error::DecryptError::RecipientsFileFlag);
}
if !(opts.identity.is_empty() || opts.plugin_name.is_none()) {
return Err(error::DecryptError::MixedIdentityAndPluginName);
}
let verify_key_str = opts
.verify_key
.ok_or(error::DecryptError::MissingVerifyKey)?;
let verification_key = if let Ok(vk) = verify_key_str.parse() {
vk
} else {
let key_contents = fs::read_to_string(&verify_key_str)?;
key_contents
.lines()
.find(|line| line.starts_with("anubis1mldsa87"))
.ok_or_else(|| {
error::DecryptError::VerificationError("no verification key found".to_string())
})?
.parse()
.map_err(|e| {
error::DecryptError::VerificationError(format!("invalid verification key: {}", e))
})?
};
let (mut input, output) = set_up_io(opts.input, opts.output, file_io::OutputFormat::Unknown)?;
let is_stdin = match input {
file_io::InputReader::File(_) => false,
file_io::InputReader::Stdin(_) => true,
};
let mut stdin_guard = StdinGuard::new(is_stdin);
let stdin_identity = opts.identity.iter().any(|s| s == "-");
if opts.plugin_name.is_some() {
return Err(error::DecryptError::PluginNameFlag);
}
let identities = read_identities(opts.identity, opts.max_work_factor, &mut stdin_guard)?;
let mut signed_data = Vec::new();
input.read_to_end(&mut signed_data)?;
let encrypted_data = signed::verify_and_extract(&signed_data, &verification_key)
.map_err(|e| error::DecryptError::VerificationError(e.to_string()))?;
eprintln!("Signature verified successfully");
const CRLF_MANGLED_INTRO: &[u8] = b"anubis-encryption.org/v1\r";
const UTF16_MANGLED_INTRO: &[u8] =
b"\xff\xfea\x00g\x00e\x00-\x00e\x00n\x00c\x00r\x00y\x00p\x00";
let err_powershell_corruption = Box::new(|| {
Err(io::Error::new(
io::ErrorKind::InvalidInput,
error::DetectedPowerShellCorruptionError,
))
});
let encrypted_reader = ReadChecker::new(
&encrypted_data[..],
[
(CRLF_MANGLED_INTRO, err_powershell_corruption.clone()),
(UTF16_MANGLED_INTRO, err_powershell_corruption),
],
);
let decryptor = anubis_age::Decryptor::new_buffered(ArmoredReader::new(encrypted_reader))?;
if identities.is_empty() {
return Err(error::DecryptError::MissingIdentities { stdin_identity });
}
decryptor
.decrypt(identities.iter().map(|i| i.as_ref() as &dyn Identity))
.map_err(|e| e.into())
.and_then(|input| write_output(input, output))
}
fn main() -> Result<(), error::Error> {
use std::env::args;
env_logger::builder()
.format_timestamp(None)
.filter_level(log::LevelFilter::Off)
.parse_default_env()
.init();
let requested_languages = DesktopLanguageRequester::requested_languages();
i18n::load_languages(&requested_languages);
anubis_age::localizer()
.select(&requested_languages)
.unwrap();
if console::user_attended() && args().len() == 1 {
AgeOptions::command()
.print_help()
.map_err(error::EncryptError::Io)?;
return Ok(());
}
let opts = AgeOptions::parse();
if opts.encrypt && opts.decrypt {
return Err(error::Error::MixedEncryptAndDecrypt);
}
if !(opts.identity.is_empty() || opts.encrypt || opts.decrypt) {
return Err(error::Error::IdentityFlagAmbiguous);
}
if let (Some(in_file), Some(out_file)) = (&opts.input, &opts.output) {
let in_path = Path::new(&in_file);
let out_path = Path::new(&out_file);
match (in_path.canonicalize(), out_path.canonicalize()) {
(Ok(in_abs), Ok(out_abs)) if in_abs == out_abs => {
return Err(error::Error::SameInputAndOutput(out_file.clone()));
}
_ => (),
}
}
if opts.decrypt {
decrypt(opts).map_err(error::Error::from)
} else {
encrypt(opts).map_err(error::Error::from)
}
}