Skip to content

Commit b29b289

Browse files
authored
Merge pull request #565 from str4d/identity-file-send-sync
Re-introduce `Send + Sync` support for `IdentityFile` usages
2 parents d7c727a + 84dc1e9 commit b29b289

File tree

5 files changed

+99
-52
lines changed

5 files changed

+99
-52
lines changed

age/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@ and this project adheres to Rust's notion of
99
to 1.0.0 are beta releases.
1010

1111
## [Unreleased]
12+
### Added
13+
- `age::encrypted::EncryptedIdentity`
14+
15+
### Changed
16+
- `age::IdentityFile::into_identities` now returns
17+
`Result<Vec<Box<dyn crate::Identity + Send + Sync>>, DecryptError>` instead of
18+
`Result<Vec<Box<dyn crate::Identity>>, DecryptError>`. This re-enables
19+
cross-thread uses of `IdentityFile`, which were unintentionally disabled in
20+
0.11.0.
1221

1322
## [0.6.1, 0.7.2, 0.8.2, 0.9.3, 0.10.1, 0.11.1] - 2024-11-18
1423
### Security

age/src/cli_common/identities.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ pub fn read_identities(
5252
#[cfg(not(feature = "plugin"))]
5353
let new_identities = new_identities.unwrap();
5454

55-
identities.extend(new_identities);
55+
identities.extend(new_identities.into_iter().map(|i| i as _));
5656

5757
Ok(())
5858
},

age/src/encrypted.rs

Lines changed: 81 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,80 @@ use std::{cell::Cell, io};
44

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

7+
/// An encrypted age identity file.
8+
///
9+
/// This type can be explicitly decrypted to obtain an [`IdentityFile`]. If you want a
10+
/// type that can be used directly as an identity and caches the decryption result
11+
/// internally, use [`Identity`].
12+
pub struct EncryptedIdentity<R: io::Read, C: Callbacks> {
13+
decryptor: Decryptor<R>,
14+
max_work_factor: Option<u8>,
15+
callbacks: C,
16+
}
17+
18+
impl<R: io::Read, C: Callbacks> EncryptedIdentity<R, C> {
19+
/// Parses an encrypted identity from an input containing valid UTF-8.
20+
///
21+
/// Returns `Ok(None)` if the input contains an age ciphertext that is not encrypted
22+
/// to a passphrase.
23+
pub(crate) fn from_buffer(
24+
data: R,
25+
callbacks: C,
26+
max_work_factor: Option<u8>,
27+
) -> Result<Option<Self>, DecryptError> {
28+
let decryptor = Decryptor::new(data)?;
29+
Ok(Self::new(decryptor, callbacks, max_work_factor))
30+
}
31+
32+
/// Constructs a new encrypted identity from a [`Decryptor`].
33+
///
34+
/// Returns `Ok(None)` if the input contains an age ciphertext that is not encrypted
35+
/// to a passphrase.
36+
pub fn new(decryptor: Decryptor<R>, callbacks: C, max_work_factor: Option<u8>) -> Option<Self> {
37+
decryptor.is_scrypt().then_some(EncryptedIdentity {
38+
decryptor,
39+
max_work_factor,
40+
callbacks,
41+
})
42+
}
43+
44+
/// Decrypts this encrypted identity.
45+
///
46+
/// The provided filename (if any) will be used in the passphrase request message.
47+
pub fn decrypt(self, filename: Option<&str>) -> Result<IdentityFile<C>, DecryptError> {
48+
let passphrase = match self.callbacks.request_passphrase(&fl!(
49+
"encrypted-passphrase-prompt",
50+
filename = filename.unwrap_or_default()
51+
)) {
52+
Some(passphrase) => passphrase,
53+
None => todo!(),
54+
};
55+
56+
let mut identity = scrypt::Identity::new(passphrase);
57+
if let Some(max_work_factor) = self.max_work_factor {
58+
identity.set_max_work_factor(max_work_factor);
59+
}
60+
61+
self.decryptor
62+
.decrypt(Some(&identity as _).into_iter())
63+
.map_err(|e| {
64+
if matches!(e, DecryptError::DecryptionFailed) {
65+
DecryptError::KeyDecryptionFailed
66+
} else {
67+
e
68+
}
69+
})
70+
.and_then(|stream| {
71+
let file = IdentityFile::from_buffer(io::BufReader::new(stream))?
72+
.with_callbacks(self.callbacks);
73+
Ok(file)
74+
})
75+
}
76+
}
77+
778
/// The state of the encrypted age identity.
879
enum IdentityState<R: io::Read, C: Callbacks> {
9-
Encrypted {
10-
decryptor: Decryptor<R>,
11-
max_work_factor: Option<u8>,
12-
callbacks: C,
13-
},
80+
Encrypted(EncryptedIdentity<R, C>),
1481
Decrypted(IdentityFile<C>),
1582

1683
/// The file was not correctly encrypted, or did not contain age identities. We cache
@@ -33,39 +100,7 @@ impl<R: io::Read, C: Callbacks> IdentityState<R, C> {
33100
/// were not cached (and we just asked the user for a passphrase).
34101
fn decrypt(self, filename: Option<&str>) -> Result<(IdentityFile<C>, bool), DecryptError> {
35102
match self {
36-
Self::Encrypted {
37-
decryptor,
38-
max_work_factor,
39-
callbacks,
40-
} => {
41-
let passphrase = match callbacks.request_passphrase(&fl!(
42-
"encrypted-passphrase-prompt",
43-
filename = filename.unwrap_or_default()
44-
)) {
45-
Some(passphrase) => passphrase,
46-
None => todo!(),
47-
};
48-
49-
let mut identity = scrypt::Identity::new(passphrase);
50-
if let Some(max_work_factor) = max_work_factor {
51-
identity.set_max_work_factor(max_work_factor);
52-
}
53-
54-
decryptor
55-
.decrypt(Some(&identity as _).into_iter())
56-
.map_err(|e| {
57-
if matches!(e, DecryptError::DecryptionFailed) {
58-
DecryptError::KeyDecryptionFailed
59-
} else {
60-
e
61-
}
62-
})
63-
.and_then(|stream| {
64-
let file = IdentityFile::from_buffer(io::BufReader::new(stream))?
65-
.with_callbacks(callbacks);
66-
Ok((file, true))
67-
})
68-
}
103+
Self::Encrypted(encrypted) => encrypted.decrypt(filename).map(|file| (file, true)),
69104
Self::Decrypted(identity_file) => Ok((identity_file, false)),
70105
// `IdentityState::decrypt` is only ever called with `Some`.
71106
Self::Poisoned(e) => Err(e.unwrap()),
@@ -74,6 +109,10 @@ impl<R: io::Read, C: Callbacks> IdentityState<R, C> {
74109
}
75110

76111
/// An encrypted age identity file.
112+
///
113+
/// This type can be used directly as an identity and caches the decryption result
114+
/// internally. If you want a type that can be explicitly decrypted to obtain an
115+
/// [`IdentityFile`], use [`EncryptedIdentity`].
77116
pub struct Identity<R: io::Read, C: Callbacks> {
78117
state: Cell<IdentityState<R, C>>,
79118
filename: Option<String>,
@@ -92,13 +131,9 @@ impl<R: io::Read, C: Callbacks> Identity<R, C> {
92131
callbacks: C,
93132
max_work_factor: Option<u8>,
94133
) -> Result<Option<Self>, DecryptError> {
95-
let decryptor = Decryptor::new(data)?;
96-
Ok(decryptor.is_scrypt().then_some(Identity {
97-
state: Cell::new(IdentityState::Encrypted {
98-
decryptor,
99-
max_work_factor,
100-
callbacks,
101-
}),
134+
let encrypted = EncryptedIdentity::from_buffer(data, callbacks, max_work_factor)?;
135+
Ok(encrypted.map(|encrypted| Identity {
136+
state: Cell::new(IdentityState::Encrypted(encrypted)),
102137
filename,
103138
}))
104139
}
@@ -138,7 +173,7 @@ impl<R: io::Read, C: Callbacks> Identity<R, C> {
138173
) -> Option<Result<age_core::format::FileKey, DecryptError>>
139174
where
140175
F: Fn(
141-
Result<Box<dyn crate::Identity>, DecryptError>,
176+
Result<Box<dyn crate::Identity + Send + Sync>, DecryptError>,
142177
) -> Option<Result<age_core::format::FileKey, DecryptError>>,
143178
{
144179
match self.state.take().decrypt(self.filename.as_deref()) {

age/src/identity.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ impl IdentityFileEntry {
2525
pub(crate) fn into_identity(
2626
self,
2727
callbacks: impl Callbacks,
28-
) -> Result<Box<dyn crate::Identity>, DecryptError> {
28+
) -> Result<Box<dyn crate::Identity + Send + Sync>, DecryptError> {
2929
match self {
3030
IdentityFileEntry::Native(i) => Ok(Box::new(i)),
3131
#[cfg(feature = "plugin")]
@@ -184,14 +184,17 @@ impl<C: Callbacks> IdentityFile<C> {
184184
/// Returns the identities in this file.
185185
pub(crate) fn to_identities(
186186
&self,
187-
) -> impl Iterator<Item = Result<Box<dyn crate::Identity>, DecryptError>> + '_ {
187+
) -> impl Iterator<Item = Result<Box<dyn crate::Identity + Send + Sync>, DecryptError>> + '_
188+
{
188189
self.identities
189190
.iter()
190191
.map(|entry| entry.clone().into_identity(self.callbacks.clone()))
191192
}
192193

193194
/// Returns the identities in this file.
194-
pub fn into_identities(self) -> Result<Vec<Box<dyn crate::Identity>>, DecryptError> {
195+
pub fn into_identities(
196+
self,
197+
) -> Result<Vec<Box<dyn crate::Identity + Send + Sync>>, DecryptError> {
195198
self.identities
196199
.into_iter()
197200
.map(|entry| entry.into_identity(self.callbacks.clone()))

age/src/protocol.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -457,7 +457,7 @@ mod tests {
457457
let pk: x25519::Recipient = crate::x25519::tests::TEST_PK.parse().unwrap();
458458
recipient_round_trip(
459459
iter::once(&pk as _),
460-
f.into_identities().unwrap().iter().map(|i| i.as_ref()),
460+
f.into_identities().unwrap().iter().map(|i| i.as_ref() as _),
461461
);
462462
}
463463

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

0 commit comments

Comments
 (0)