Skip to content

rage-mount-dir: OverlayFS that transparently decrypts age-encrypted files #233

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
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
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion age-core/src/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ impl<'a> AgeStanza<'a> {
/// recipient.
///
/// This is the owned type; see [`AgeStanza`] for the reference type.
#[derive(Debug, PartialEq)]
#[derive(Debug, Clone, PartialEq)]
pub struct Stanza {
/// A tag identifying this stanza type.
pub tag: String,
Expand Down
4 changes: 2 additions & 2 deletions age/src/cli_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ pub fn read_identities<E, G, H>(
file_not_found: G,
identity_encrypted_without_passphrase: H,
#[cfg(feature = "ssh")] unsupported_ssh: impl Fn(String, crate::ssh::UnsupportedKey) -> E,
) -> Result<Vec<Box<dyn Identity>>, E>
) -> Result<Vec<Box<dyn Identity + Send + Sync>>, E>
where
E: From<crate::DecryptError>,
E: From<io::Error>,
G: Fn(String) -> E,
H: Fn(String) -> E,
{
let mut identities: Vec<Box<dyn Identity>> = vec![];
let mut identities: Vec<Box<dyn Identity + Send + Sync>> = vec![];

for filename in filenames {
// Try parsing as an encrypted age identity.
Expand Down
51 changes: 31 additions & 20 deletions age/src/encrypted.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! The "encrypted age identity file" identity type.

use std::{cell::Cell, io};
use std::{io, ops::DerefMut, sync::Mutex};

use i18n_embed_fl::fl;

Expand Down Expand Up @@ -77,12 +77,12 @@ impl<R: io::Read> IdentityState<R> {

/// An encrypted age identity file.
pub struct Identity<R: io::Read, C: Callbacks> {
state: Cell<IdentityState<R>>,
state: Mutex<Option<IdentityState<R>>>,
filename: Option<String>,
callbacks: C,
}

impl<R: io::Read, C: Callbacks + Clone + 'static> Identity<R, C> {
impl<R: io::Read, C: Callbacks + Clone + Send + Sync + 'static> Identity<R, C> {
/// Parses an encrypted identity from an input containing valid UTF-8.
///
/// `filename` is the path to the file that the input is reading from, if any.
Expand All @@ -98,10 +98,10 @@ impl<R: io::Read, C: Callbacks + Clone + 'static> Identity<R, C> {
match Decryptor::new(data)? {
Decryptor::Recipients(_) => Ok(None),
Decryptor::Passphrase(decryptor) => Ok(Some(Identity {
state: Cell::new(IdentityState::Encrypted {
state: Mutex::new(Some(IdentityState::Encrypted {
decryptor,
max_work_factor,
}),
})),
filename,
callbacks,
})),
Expand All @@ -113,9 +113,11 @@ impl<R: io::Read, C: Callbacks + Clone + 'static> Identity<R, C> {
/// If this encrypted identity has not been decrypted yet, calling this method will
/// trigger a passphrase request.
pub fn recipients(&self) -> Result<Vec<Box<dyn crate::Recipient>>, EncryptError> {
match self
.state
let mut state = self.state.lock().unwrap();
match state
.deref_mut()
.take()
.expect("We never leave this set to None")
.decrypt(self.filename.as_deref(), self.callbacks.clone())
{
Ok((identities, _)) => {
Expand All @@ -124,11 +126,11 @@ impl<R: io::Read, C: Callbacks + Clone + 'static> Identity<R, C> {
.map(|entry| entry.to_recipient(self.callbacks.clone()))
.collect::<Result<Vec<_>, _>>();

self.state.set(IdentityState::Decrypted(identities));
*state = Some(IdentityState::Decrypted(identities));
recipients
}
Err(e) => {
self.state.set(IdentityState::Poisoned(Some(e.clone())));
*state = Some(IdentityState::Poisoned(Some(e.clone())));
Err(EncryptError::EncryptedIdentities(e))
}
}
Expand All @@ -151,12 +153,14 @@ impl<R: io::Read, C: Callbacks + Clone + 'static> Identity<R, C> {
) -> Option<Result<age_core::format::FileKey, DecryptError>>
where
F: Fn(
Result<Box<dyn crate::Identity>, DecryptError>,
Result<Box<dyn crate::Identity + Send + Sync>, DecryptError>,
) -> Option<Result<age_core::format::FileKey, DecryptError>>,
{
match self
.state
let mut state = self.state.lock().unwrap();
match state
.deref_mut()
.take()
.expect("We never leave this set to None")
.decrypt(self.filename.as_deref(), self.callbacks.clone())
{
Ok((identities, requested_passphrase)) => {
Expand All @@ -175,18 +179,18 @@ impl<R: io::Read, C: Callbacks + Clone + 'static> Identity<R, C> {
));
}

self.state.set(IdentityState::Decrypted(identities));
*state = Some(IdentityState::Decrypted(identities));
result
}
Err(e) => {
self.state.set(IdentityState::Poisoned(Some(e.clone())));
*state = Some(IdentityState::Poisoned(Some(e.clone())));
Some(Err(e))
}
}
}
}

impl<R: io::Read, C: Callbacks + Clone + 'static> crate::Identity for Identity<R, C> {
impl<R: io::Read, C: Callbacks + Clone + Send + Sync + 'static> crate::Identity for Identity<R, C> {
fn unwrap_stanza(
&self,
stanza: &age_core::format::Stanza,
Expand All @@ -210,7 +214,7 @@ impl<R: io::Read, C: Callbacks + Clone + 'static> crate::Identity for Identity<R

#[cfg(test)]
mod tests {
use std::{cell::Cell, io::BufReader};
use std::{io::BufReader, sync::Mutex};

use secrecy::{ExposeSecret, SecretString};

Expand All @@ -235,12 +239,17 @@ fOrxrKTj7xCdNS3+OrCdnBC8Z9cKDxjCGWW3fkjLsYha0Jo=

const TEST_RECIPIENT: &str = "age1ysxuaeqlk7xd8uqsh8lsnfwt9jzzjlqf49ruhpjrrj5yatlcuf7qke4pqe";

#[derive(Clone)]
struct MockCallbacks(Cell<Option<&'static str>>);
struct MockCallbacks(Mutex<Option<&'static str>>);

impl Clone for MockCallbacks {
fn clone(&self) -> Self {
Self(Mutex::new(self.0.lock().unwrap().clone()))
}
}

impl MockCallbacks {
fn new(passphrase: &'static str) -> Self {
MockCallbacks(Cell::new(Some(passphrase)))
MockCallbacks(Mutex::new(Some(passphrase)))
}
}

Expand All @@ -255,7 +264,9 @@ fOrxrKTj7xCdNS3+OrCdnBC8Z9cKDxjCGWW3fkjLsYha0Jo=

/// This intentionally panics if called twice.
fn request_passphrase(&self, _: &str) -> Option<secrecy::SecretString> {
Some(SecretString::new(self.0.take().unwrap().to_owned()))
Some(SecretString::new(
self.0.lock().unwrap().take().unwrap().to_owned(),
))
}
}

Expand Down
4 changes: 2 additions & 2 deletions age/src/identity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ pub enum IdentityFileEntry {
impl IdentityFileEntry {
pub(crate) fn into_identity(
self,
callbacks: impl Callbacks + 'static,
) -> Result<Box<dyn crate::Identity>, DecryptError> {
callbacks: impl Callbacks + Send + Sync + 'static,
) -> Result<Box<dyn crate::Identity + Send + Sync>, DecryptError> {
match self {
IdentityFileEntry::Native(i) => Ok(Box::new(i)),
#[cfg(feature = "plugin")]
Expand Down
9 changes: 8 additions & 1 deletion rage/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,10 @@ rust-embed = "5"
secrecy = "0.8"

# rage-mount dependencies
age-core = { version = "0.6.0", path = "../age-core", optional = true }
fuse_mt = { version = "0.5.1", optional = true }
libc = { version = "0.2", optional = true }
nix = { version = "0.20", optional = true }
tar = { version = "0.4", optional = true }
time = { version = "0.1", optional = true }
zip = { version = "0.5.9", optional = true }
Expand All @@ -71,7 +73,7 @@ man = "0.3"

[features]
default = ["ssh"]
mount = ["fuse_mt", "libc", "tar", "time", "zip"]
mount = ["age-core", "fuse_mt", "libc", "nix", "tar", "time", "zip"]
ssh = ["age/ssh"]
unstable = ["age/unstable"]

Expand All @@ -87,3 +89,8 @@ bench = false
name = "rage-mount"
required-features = ["mount"]
bench = false

[[bin]]
name = "rage-mount-dir"
required-features = ["mount"]
bench = false
3 changes: 3 additions & 0 deletions rage/i18n/en-US/rage.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,12 @@ rec-dec-recipient-flag = Did you mean to use {-flag-identity} to specify a priva
info-decrypting = Decrypting {$filename}
info-mounting-as-fuse = Mounting as FUSE filesystem

err-mnt-missing-source = Missing source.
err-mnt-missing-filename = Missing filename.
err-mnt-missing-mountpoint = Missing mountpoint.
err-mnt-missing-types = Missing {-flag-mnt-types}.
err-mnt-must-be-dir = Mountpoint must be a directory.
err-mnt-source-must-be-dir = Source must be a directory.
err-mnt-unknown-type = Unknown filesystem type "{$fs_type}"

## Unstable features
Expand Down
Loading