Skip to content
Merged
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
9 changes: 9 additions & 0 deletions age/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ and this project adheres to Rust's notion of
to 1.0.0 are beta releases.

## [Unreleased]
### Added
- `age::encrypted::EncryptedIdentity`

### Changed
- `age::IdentityFile::into_identities` now returns
`Result<Vec<Box<dyn crate::Identity + Send + Sync>>, DecryptError>` instead of
`Result<Vec<Box<dyn crate::Identity>>, DecryptError>`. This re-enables
cross-thread uses of `IdentityFile`, which were unintentionally disabled in
0.11.0.

## [0.6.1, 0.7.2, 0.8.2, 0.9.3, 0.10.1, 0.11.1] - 2024-11-18
### Security
Expand Down
2 changes: 1 addition & 1 deletion age/src/cli_common/identities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ pub fn read_identities(
#[cfg(not(feature = "plugin"))]
let new_identities = new_identities.unwrap();

identities.extend(new_identities);
identities.extend(new_identities.into_iter().map(|i| i as _));

Ok(())
},
Expand Down
127 changes: 81 additions & 46 deletions age/src/encrypted.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,80 @@ use std::{cell::Cell, io};

use crate::{fl, scrypt, Callbacks, DecryptError, Decryptor, EncryptError, IdentityFile};

/// An encrypted age identity file.
///
/// This type can be explicitly decrypted to obtain an [`IdentityFile`]. If you want a
/// type that can be used directly as an identity and caches the decryption result
/// internally, use [`Identity`].
pub struct EncryptedIdentity<R: io::Read, C: Callbacks> {
decryptor: Decryptor<R>,
max_work_factor: Option<u8>,
callbacks: C,
}

impl<R: io::Read, C: Callbacks> EncryptedIdentity<R, C> {
/// Parses an encrypted identity from an input containing valid UTF-8.
///
/// Returns `Ok(None)` if the input contains an age ciphertext that is not encrypted
/// to a passphrase.
pub(crate) fn from_buffer(
data: R,
callbacks: C,
max_work_factor: Option<u8>,
) -> Result<Option<Self>, DecryptError> {
let decryptor = Decryptor::new(data)?;
Ok(Self::new(decryptor, callbacks, max_work_factor))
}

/// Constructs a new encrypted identity from a [`Decryptor`].
///
/// Returns `Ok(None)` if the input contains an age ciphertext that is not encrypted
/// to a passphrase.
pub fn new(decryptor: Decryptor<R>, callbacks: C, max_work_factor: Option<u8>) -> Option<Self> {
decryptor.is_scrypt().then_some(EncryptedIdentity {
decryptor,
max_work_factor,
callbacks,
})
}

/// Decrypts this encrypted identity.
///
/// The provided filename (if any) will be used in the passphrase request message.
pub fn decrypt(self, filename: Option<&str>) -> Result<IdentityFile<C>, DecryptError> {
let passphrase = match self.callbacks.request_passphrase(&fl!(
"encrypted-passphrase-prompt",
filename = filename.unwrap_or_default()
)) {
Some(passphrase) => passphrase,
None => todo!(),
};

let mut identity = scrypt::Identity::new(passphrase);
if let Some(max_work_factor) = self.max_work_factor {
identity.set_max_work_factor(max_work_factor);
}

self.decryptor
.decrypt(Some(&identity as _).into_iter())
.map_err(|e| {
if matches!(e, DecryptError::DecryptionFailed) {
DecryptError::KeyDecryptionFailed
} else {
e
}
})
.and_then(|stream| {
let file = IdentityFile::from_buffer(io::BufReader::new(stream))?
.with_callbacks(self.callbacks);
Ok(file)
})
}
}

/// The state of the encrypted age identity.
enum IdentityState<R: io::Read, C: Callbacks> {
Encrypted {
decryptor: Decryptor<R>,
max_work_factor: Option<u8>,
callbacks: C,
},
Encrypted(EncryptedIdentity<R, C>),
Decrypted(IdentityFile<C>),

/// The file was not correctly encrypted, or did not contain age identities. We cache
Expand All @@ -33,39 +100,7 @@ impl<R: io::Read, C: Callbacks> IdentityState<R, C> {
/// were not cached (and we just asked the user for a passphrase).
fn decrypt(self, filename: Option<&str>) -> Result<(IdentityFile<C>, bool), DecryptError> {
match self {
Self::Encrypted {
decryptor,
max_work_factor,
callbacks,
} => {
let passphrase = match callbacks.request_passphrase(&fl!(
"encrypted-passphrase-prompt",
filename = filename.unwrap_or_default()
)) {
Some(passphrase) => passphrase,
None => todo!(),
};

let mut identity = scrypt::Identity::new(passphrase);
if let Some(max_work_factor) = max_work_factor {
identity.set_max_work_factor(max_work_factor);
}

decryptor
.decrypt(Some(&identity as _).into_iter())
.map_err(|e| {
if matches!(e, DecryptError::DecryptionFailed) {
DecryptError::KeyDecryptionFailed
} else {
e
}
})
.and_then(|stream| {
let file = IdentityFile::from_buffer(io::BufReader::new(stream))?
.with_callbacks(callbacks);
Ok((file, true))
})
}
Self::Encrypted(encrypted) => encrypted.decrypt(filename).map(|file| (file, true)),
Self::Decrypted(identity_file) => Ok((identity_file, false)),
// `IdentityState::decrypt` is only ever called with `Some`.
Self::Poisoned(e) => Err(e.unwrap()),
Expand All @@ -74,6 +109,10 @@ impl<R: io::Read, C: Callbacks> IdentityState<R, C> {
}

/// An encrypted age identity file.
///
/// This type can be used directly as an identity and caches the decryption result
/// internally. If you want a type that can be explicitly decrypted to obtain an
/// [`IdentityFile`], use [`EncryptedIdentity`].
pub struct Identity<R: io::Read, C: Callbacks> {
state: Cell<IdentityState<R, C>>,
filename: Option<String>,
Expand All @@ -92,13 +131,9 @@ impl<R: io::Read, C: Callbacks> Identity<R, C> {
callbacks: C,
max_work_factor: Option<u8>,
) -> Result<Option<Self>, DecryptError> {
let decryptor = Decryptor::new(data)?;
Ok(decryptor.is_scrypt().then_some(Identity {
state: Cell::new(IdentityState::Encrypted {
decryptor,
max_work_factor,
callbacks,
}),
let encrypted = EncryptedIdentity::from_buffer(data, callbacks, max_work_factor)?;
Ok(encrypted.map(|encrypted| Identity {
state: Cell::new(IdentityState::Encrypted(encrypted)),
filename,
}))
}
Expand Down Expand Up @@ -138,7 +173,7 @@ impl<R: io::Read, C: Callbacks> 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.take().decrypt(self.filename.as_deref()) {
Expand Down
9 changes: 6 additions & 3 deletions age/src/identity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ impl IdentityFileEntry {
pub(crate) fn into_identity(
self,
callbacks: impl Callbacks,
) -> Result<Box<dyn crate::Identity>, DecryptError> {
) -> Result<Box<dyn crate::Identity + Send + Sync>, DecryptError> {
match self {
IdentityFileEntry::Native(i) => Ok(Box::new(i)),
#[cfg(feature = "plugin")]
Expand Down Expand Up @@ -184,14 +184,17 @@ impl<C: Callbacks> IdentityFile<C> {
/// Returns the identities in this file.
pub(crate) fn to_identities(
&self,
) -> impl Iterator<Item = Result<Box<dyn crate::Identity>, DecryptError>> + '_ {
) -> impl Iterator<Item = Result<Box<dyn crate::Identity + Send + Sync>, DecryptError>> + '_
{
self.identities
.iter()
.map(|entry| entry.clone().into_identity(self.callbacks.clone()))
}

/// Returns the identities in this file.
pub fn into_identities(self) -> Result<Vec<Box<dyn crate::Identity>>, DecryptError> {
pub fn into_identities(
self,
) -> Result<Vec<Box<dyn crate::Identity + Send + Sync>>, DecryptError> {
self.identities
.into_iter()
.map(|entry| entry.into_identity(self.callbacks.clone()))
Expand Down
4 changes: 2 additions & 2 deletions age/src/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,7 @@ mod tests {
let pk: x25519::Recipient = crate::x25519::tests::TEST_PK.parse().unwrap();
recipient_round_trip(
iter::once(&pk as _),
f.into_identities().unwrap().iter().map(|i| i.as_ref()),
f.into_identities().unwrap().iter().map(|i| i.as_ref() as _),
);
}

Expand All @@ -469,7 +469,7 @@ mod tests {
let pk: x25519::Recipient = crate::x25519::tests::TEST_PK.parse().unwrap();
recipient_async_round_trip(
iter::once(&pk as _),
f.into_identities().unwrap().iter().map(|i| i.as_ref()),
f.into_identities().unwrap().iter().map(|i| i.as_ref() as _),
);
}

Expand Down
Loading