#![forbid(unsafe_code)]
use age::{
armor::ArmoredReader,
cli_common::{read_identities, read_secret, StdinGuard},
scrypt,
stream::StreamReader,
};
use clap::{CommandFactory, Parser};
use fuse_mt::FilesystemMT;
use fuser::MountOption;
use i18n_embed::DesktopLanguageRequester;
use log::info;
use std::fmt;
use std::fs::File;
use std::io;
use std::sync::mpsc;
mod cli;
mod tar;
mod zip;
mod i18n {
include!("../rage/i18n.rs");
}
#[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! wfl {
($f:ident, $message_id:literal) => {
write!($f, "{}", fl!($message_id))
};
($f:ident, $message_id:literal, $($args:expr),* $(,)?) => {
write!($f, "{}", fl!($message_id, $($args), *))
};
}
macro_rules! wlnfl {
($f:ident, $message_id:literal) => {
writeln!($f, "{}", fl!($message_id))
};
($f:ident, $message_id:literal, $($args:expr),* $(,)?) => {
writeln!($f, "{}", fl!($message_id, $($args), *))
};
}
enum Error {
Age(age::DecryptError),
IdentityRead(age::cli_common::ReadError),
Io(io::Error),
MissingFilename,
MissingIdentities,
MissingMountpoint,
MissingType,
UnknownType(String),
}
impl From<age::DecryptError> for Error {
fn from(e: age::DecryptError) -> Self {
Error::Age(e)
}
}
impl From<age::cli_common::ReadError> for Error {
fn from(e: age::cli_common::ReadError) -> Self {
Error::IdentityRead(e)
}
}
impl From<io::Error> for Error {
fn from(e: io::Error) -> Self {
Error::Io(e)
}
}
impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Age(e) => match e {
age::DecryptError::ExcessiveWork { required, .. } => {
writeln!(f, "{}", e)?;
wfl!(f, "rec-dec-excessive-work", wf = required)
}
_ => write!(f, "{}", e),
},
Error::IdentityRead(e) => write!(f, "{}", e),
Error::Io(e) => write!(f, "{}", e),
Error::MissingFilename => wfl!(f, "err-mnt-missing-filename"),
Error::MissingIdentities => {
wlnfl!(f, "err-dec-missing-identities")?;
wlnfl!(f, "rec-dec-missing-identities")
}
Error::MissingMountpoint => wfl!(f, "err-mnt-missing-mountpoint"),
Error::MissingType => wfl!(f, "err-mnt-missing-types"),
Error::UnknownType(t) => wfl!(f, "err-mnt-unknown-type", fs_type = t.as_str()),
}?;
writeln!(f)?;
writeln!(f, "[ {} ]", fl!("err-ux-A"))?;
write!(
f,
"[ {}: https://str4d.xyz/rage/report {} ]",
fl!("err-ux-B"),
fl!("err-ux-C")
)
}
}
fn mount_fs<T: FilesystemMT + Send + Sync + 'static, F>(
open: F,
mountpoint: String,
finished: mpsc::Receiver<()>,
) -> Result<(), Error>
where
F: FnOnce() -> io::Result<T>,
{
let fs = open().map(|fs| fuse_mt::FuseMT::new(fs, 1))?;
info!("{}", fl!("info-mounting-as-fuse"));
let handle = fuser::spawn_mount2(fs, mountpoint, &[MountOption::RO])?;
finished.recv().expect("Could not receive from channel.");
handle.join();
Ok(())
}
fn mount_stream(
stream: StreamReader<ArmoredReader<io::BufReader<File>>>,
types: String,
mountpoint: String,
) -> Result<(), Error> {
let (tx, finished) = mpsc::sync_channel(2);
let destroy_tx = tx.clone();
ctrlc::set_handler(move || tx.send(()).expect("Could not send signal on channel."))
.expect("Error setting Ctrl-C handler");
match types.as_str() {
"tar" => mount_fs(
|| crate::tar::AgeTarFs::open(stream, destroy_tx),
mountpoint,
finished,
),
"zip" => mount_fs(
|| crate::zip::AgeZipFs::open(stream, destroy_tx),
mountpoint,
finished,
),
_ => Err(Error::UnknownType(types)),
}
}
fn main() -> Result<(), 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);
age::localizer().select(&requested_languages).unwrap();
if console::user_attended() && args().len() == 1 {
cli::AgeMountOptions::command().print_help()?;
return Ok(());
}
let opts = cli::AgeMountOptions::parse();
if opts.filename.is_empty() {
return Err(Error::MissingFilename);
}
if opts.mountpoint.is_empty() {
return Err(Error::MissingMountpoint);
}
if opts.types.is_empty() {
return Err(Error::MissingType);
}
info!(
"{}",
fl!("info-decrypting", filename = opts.filename.as_str()),
);
let file = File::open(opts.filename)?;
let types = opts.types;
let mountpoint = opts.mountpoint;
let mut stdin_guard = StdinGuard::new(false);
let decryptor = age::Decryptor::new_buffered(ArmoredReader::new(file))?;
if decryptor.is_scrypt() {
match read_secret(&fl!("type-passphrase"), &fl!("prompt-passphrase"), None) {
Ok(passphrase) => {
let mut identity = scrypt::Identity::new(passphrase);
if let Some(max_work_factor) = opts.max_work_factor {
identity.set_max_work_factor(max_work_factor);
}
decryptor
.decrypt(Some(&identity as _).into_iter())
.map_err(|e| e.into())
.and_then(|stream| mount_stream(stream, types, mountpoint))
}
Err(_) => Ok(()),
}
} else {
let identities = read_identities(opts.identity, opts.max_work_factor, &mut stdin_guard)?;
if identities.is_empty() {
return Err(Error::MissingIdentities);
}
decryptor
.decrypt(identities.iter().map(|i| &**i))
.map_err(|e| e.into())
.and_then(|stream| mount_stream(stream, types, mountpoint))
}
}