Skip to content

Commit 84dc1e9

Browse files
committed
age: Extract EncryptedIdentity from encrypted::Identity
This type doesn't contain a `Cell` and thus can be `Send + Sync`.
1 parent 7c5099f commit 84dc1e9

File tree

2 files changed

+83
-45
lines changed

2 files changed

+83
-45
lines changed

age/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ 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+
1215
### Changed
1316
- `age::IdentityFile::into_identities` now returns
1417
`Result<Vec<Box<dyn crate::Identity + Send + Sync>>, DecryptError>` instead of

age/src/encrypted.rs

Lines changed: 80 additions & 45 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
}

0 commit comments

Comments
 (0)