age: Remove PassphraseDecryptor

This commit is contained in:
Jack Grigg 2024-07-29 01:06:59 +00:00
parent f253ff2ff1
commit a1f16094b8
10 changed files with 98 additions and 117 deletions

View file

@ -10,10 +10,18 @@ to 1.0.0 are beta releases.
## [Unreleased]
### Added
- `age::decryptor::RecipientsDecryptor::is_scrypt`
- `age::scrypt`, providing recipient and identity types for passphrase-based
encryption.
- Partial French translation!
### Changed
- `age::Decryptor` no longer has a `Passphrase` variant.
### Removed
- `age::decryptor::PassphraseDecryptor` (use `RecipientsDecryptor` with
`age::scrypt::Identity` instead).
## [0.10.0] - 2024-02-04
### Added
- Russian translation!

View file

@ -3,14 +3,14 @@
use std::{cell::Cell, io};
use crate::{
decryptor::PassphraseDecryptor, fl, Callbacks, DecryptError, Decryptor, EncryptError,
decryptor::RecipientsDecryptor, fl, scrypt, Callbacks, DecryptError, Decryptor, EncryptError,
IdentityFile, IdentityFileEntry,
};
/// The state of the encrypted age identity.
enum IdentityState<R: io::Read> {
Encrypted {
decryptor: PassphraseDecryptor<R>,
decryptor: RecipientsDecryptor<R>,
max_work_factor: Option<u8>,
},
Decrypted(Vec<IdentityFileEntry>),
@ -51,8 +51,13 @@ impl<R: io::Read> IdentityState<R> {
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(&passphrase, max_work_factor)
.decrypt(Some(&identity as _).into_iter())
.map_err(|e| {
if matches!(e, DecryptError::DecryptionFailed) {
DecryptError::KeyDecryptionFailed
@ -93,8 +98,8 @@ impl<R: io::Read, C: Callbacks> Identity<R, C> {
max_work_factor: Option<u8>,
) -> Result<Option<Self>, DecryptError> {
match Decryptor::new(data)? {
Decryptor::Recipients(_) => Ok(None),
Decryptor::Passphrase(decryptor) => Ok(Some(Identity {
Decryptor::Recipients(decryptor) if !decryptor.is_scrypt() => Ok(None),
Decryptor::Recipients(decryptor) => Ok(Some(Identity {
state: Cell::new(IdentityState::Encrypted {
decryptor,
max_work_factor,

View file

@ -84,6 +84,11 @@ impl HeaderV1 {
pub(crate) fn no_scrypt(&self) -> bool {
!self.any_scrypt()
}
/// Enforces structural requirements on the v1 header.
pub(crate) fn is_valid(&self) -> bool {
self.valid_scrypt() || self.no_scrypt()
}
}
impl Header {

View file

@ -110,12 +110,13 @@
//! # fn decrypt(passphrase: &str, encrypted: Vec<u8>) -> Result<Vec<u8>, age::DecryptError> {
//! let decrypted = {
//! let decryptor = match age::Decryptor::new(&encrypted[..])? {
//! age::Decryptor::Passphrase(d) => d,
//! _ => unreachable!(),
//! age::Decryptor::Recipients(d) => d,
//! };
//!
//! let mut decrypted = vec![];
//! let mut reader = decryptor.decrypt(&Secret::new(passphrase.to_owned()), None)?;
//! let mut reader = decryptor.decrypt(
//! Some(&age::scrypt::Identity::new(Secret::new(passphrase.to_owned())) as _).into_iter(),
//! )?;
//! reader.read_to_end(&mut decrypted);
//!
//! decrypted

View file

@ -143,8 +143,6 @@ impl Encryptor {
pub enum Decryptor<R> {
/// Decryption with a list of identities.
Recipients(decryptor::RecipientsDecryptor<R>),
/// Decryption with a passphrase.
Passphrase(decryptor::PassphraseDecryptor<R>),
}
impl<R> From<decryptor::RecipientsDecryptor<R>> for Decryptor<R> {
@ -153,18 +151,10 @@ impl<R> From<decryptor::RecipientsDecryptor<R>> for Decryptor<R> {
}
}
impl<R> From<decryptor::PassphraseDecryptor<R>> for Decryptor<R> {
fn from(decryptor: decryptor::PassphraseDecryptor<R>) -> Self {
Decryptor::Passphrase(decryptor)
}
}
impl<R> Decryptor<R> {
fn from_v1_header(input: R, header: HeaderV1, nonce: Nonce) -> Result<Self, DecryptError> {
// Enforce structural requirements on the v1 header.
if header.valid_scrypt() {
Ok(decryptor::PassphraseDecryptor::new(input, Header::V1(header), nonce).into())
} else if header.no_scrypt() {
if header.is_valid() {
Ok(decryptor::RecipientsDecryptor::new(input, Header::V1(header), nonce).into())
} else {
Err(DecryptError::InvalidHeader)
@ -279,7 +269,7 @@ mod tests {
use super::{Decryptor, Encryptor};
use crate::{
identity::{IdentityFile, IdentityFileEntry},
x25519, Identity, Recipient,
scrypt, x25519, Identity, Recipient,
};
#[cfg(feature = "async")]
@ -373,7 +363,6 @@ mod tests {
}
} {
Decryptor::Recipients(d) => d,
_ => panic!(),
};
let decrypted = {
@ -439,11 +428,14 @@ mod tests {
}
let d = match Decryptor::new(&encrypted[..]) {
Ok(Decryptor::Passphrase(d)) => d,
Ok(Decryptor::Recipients(d)) => d,
_ => panic!(),
};
let mut r = d
.decrypt(&SecretString::new("passphrase".to_string()), None)
.decrypt(
Some(&scrypt::Identity::new(SecretString::new("passphrase".to_string())) as _)
.into_iter(),
)
.unwrap();
let mut decrypted = vec![];
r.read_to_end(&mut decrypted).unwrap();

View file

@ -1,9 +1,6 @@
//! Decryptors for age.
use age_core::{
format::{FileKey, Stanza},
secrecy::SecretString,
};
use age_core::format::{FileKey, Stanza};
use std::io::Read;
use super::Nonce;
@ -12,7 +9,7 @@ use crate::{
format::Header,
keys::v1_payload_key,
primitives::stream::{PayloadKey, Stream, StreamReader},
scrypt, Identity,
Identity,
};
#[cfg(feature = "async")]
@ -53,6 +50,14 @@ impl<R> RecipientsDecryptor<R> {
})
}
/// Returns `true` if the age file is encrypted to a passphrase.
pub fn is_scrypt(&self) -> bool {
match &self.0.header {
Header::V1(header) => header.valid_scrypt(),
Header::Unknown(_) => false,
}
}
fn obtain_payload_key<'a>(
&self,
mut identities: impl Iterator<Item = &'a dyn Identity>,
@ -89,65 +94,3 @@ impl<R: AsyncRead + Unpin> RecipientsDecryptor<R> {
.map(|payload_key| Stream::decrypt_async(payload_key, self.0.input))
}
}
/// Decryptor for an age file encrypted with a passphrase.
pub struct PassphraseDecryptor<R>(BaseDecryptor<R>);
impl<R> PassphraseDecryptor<R> {
pub(super) fn new(input: R, header: Header, nonce: Nonce) -> Self {
PassphraseDecryptor(BaseDecryptor {
input,
header,
nonce,
})
}
fn obtain_payload_key(
&self,
passphrase: &SecretString,
max_work_factor: Option<u8>,
) -> Result<PayloadKey, DecryptError> {
let mut identity = scrypt::Identity::new(passphrase.clone());
if let Some(max_work_factor) = max_work_factor {
identity.set_max_work_factor(max_work_factor);
}
self.0.obtain_payload_key(|r| identity.unwrap_stanzas(r))
}
}
impl<R: Read> PassphraseDecryptor<R> {
/// Attempts to decrypt the age file.
///
/// `max_work_factor` is the maximum accepted work factor. If `None`, the default
/// maximum is adjusted to around 16 seconds of work.
///
/// If successful, returns a reader that will provide the plaintext.
pub fn decrypt(
self,
passphrase: &SecretString,
max_work_factor: Option<u8>,
) -> Result<StreamReader<R>, DecryptError> {
self.obtain_payload_key(passphrase, max_work_factor)
.map(|payload_key| Stream::decrypt(payload_key, self.0.input))
}
}
#[cfg(feature = "async")]
#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
impl<R: AsyncRead + Unpin> PassphraseDecryptor<R> {
/// Attempts to decrypt the age file.
///
/// `max_work_factor` is the maximum accepted work factor. If `None`, the default
/// maximum is adjusted to around 16 seconds of work.
///
/// If successful, returns a reader that will provide the plaintext.
pub fn decrypt_async(
self,
passphrase: &SecretString,
max_work_factor: Option<u8>,
) -> Result<StreamReader<R>, DecryptError> {
self.obtain_payload_key(passphrase, max_work_factor)
.map(|payload_key| Stream::decrypt_async(payload_key, self.0.input))
}
}

View file

@ -1,7 +1,9 @@
use age_core::secrecy::SecretString;
use std::fs;
use std::io::Read;
use age::scrypt;
use age_core::secrecy::SecretString;
#[test]
#[cfg(feature = "cli-common")]
fn age_test_vectors() -> Result<(), Box<dyn std::error::Error>> {
@ -23,7 +25,7 @@ fn age_test_vectors() -> Result<(), Box<dyn std::error::Error>> {
let expect_failure = name.starts_with("fail_");
let res = match age::Decryptor::new(fs::File::open(&path)?)? {
age::Decryptor::Recipients(d) => {
age::Decryptor::Recipients(d) if !d.is_scrypt() => {
let identities = age::cli_common::read_identities(
vec![format!(
"{}/{}_key.txt",
@ -35,7 +37,7 @@ fn age_test_vectors() -> Result<(), Box<dyn std::error::Error>> {
)?;
d.decrypt(identities.iter().map(|i| i.as_ref() as &dyn age::Identity))
}
age::Decryptor::Passphrase(d) => {
age::Decryptor::Recipients(d) => {
let mut passphrase = String::new();
fs::File::open(format!(
"{}/{}_password.txt",
@ -44,7 +46,8 @@ fn age_test_vectors() -> Result<(), Box<dyn std::error::Error>> {
))?
.read_to_string(&mut passphrase)?;
let passphrase = SecretString::new(passphrase);
d.decrypt(&passphrase, None)
let identity = scrypt::Identity::new(passphrase);
d.decrypt(Some(&identity as _).into_iter())
}
};

View file

@ -6,6 +6,7 @@ use std::{
use age::{
armor::{ArmoredReadError, ArmoredReader},
scrypt,
secrecy::SecretString,
x25519, DecryptError, Decryptor, Identity,
};
@ -132,13 +133,15 @@ fn testkit(filename: &str) {
let comment = format_testkit_comment(&testfile);
match Decryptor::new(ArmoredReader::new(&testfile.age_file[..])).and_then(|d| match d {
Decryptor::Recipients(d) => {
Decryptor::Recipients(d) if !d.is_scrypt() => {
let identities = get_testkit_identities(filename, &testfile);
d.decrypt(identities.iter().map(|i| i as &dyn Identity))
}
Decryptor::Passphrase(d) => {
Decryptor::Recipients(d) => {
let passphrase = get_testkit_passphrase(&testfile, &comment);
d.decrypt(&passphrase, Some(16))
let mut identity = scrypt::Identity::new(passphrase);
identity.set_max_work_factor(16);
d.decrypt(Some(&identity as _).into_iter())
}
}) {
Ok(mut r) => {
@ -270,13 +273,15 @@ fn testkit_buffered(filename: &str) {
match Decryptor::new_buffered(ArmoredReader::new(&testfile.age_file[..])).and_then(
|d| match d {
Decryptor::Recipients(d) => {
Decryptor::Recipients(d) if !d.is_scrypt() => {
let identities = get_testkit_identities(filename, &testfile);
d.decrypt(identities.iter().map(|i| i as &dyn Identity))
}
Decryptor::Passphrase(d) => {
Decryptor::Recipients(d) => {
let passphrase = get_testkit_passphrase(&testfile, &comment);
d.decrypt(&passphrase, Some(16))
let mut identity = scrypt::Identity::new(passphrase);
identity.set_max_work_factor(16);
d.decrypt(Some(&identity as _).into_iter())
}
},
) {
@ -411,13 +416,15 @@ async fn testkit_async(filename: &str) {
match Decryptor::new_async(ArmoredReader::from_async_reader(&testfile.age_file[..]))
.await
.and_then(|d| match d {
Decryptor::Recipients(d) => {
Decryptor::Recipients(d) if !d.is_scrypt() => {
let identities = get_testkit_identities(filename, &testfile);
d.decrypt_async(identities.iter().map(|i| i as &dyn Identity))
}
Decryptor::Passphrase(d) => {
Decryptor::Recipients(d) => {
let passphrase = get_testkit_passphrase(&testfile, &comment);
d.decrypt_async(&passphrase, Some(16))
let mut identity = scrypt::Identity::new(passphrase);
identity.set_max_work_factor(16);
d.decrypt_async(Some(&identity as _).into_iter())
}
}) {
Ok(mut r) => {
@ -551,13 +558,15 @@ async fn testkit_async_buffered(filename: &str) {
match Decryptor::new_async_buffered(ArmoredReader::from_async_reader(&testfile.age_file[..]))
.await
.and_then(|d| match d {
Decryptor::Recipients(d) => {
Decryptor::Recipients(d) if !d.is_scrypt() => {
let identities = get_testkit_identities(filename, &testfile);
d.decrypt_async(identities.iter().map(|i| i as &dyn Identity))
}
Decryptor::Passphrase(d) => {
Decryptor::Recipients(d) => {
let passphrase = get_testkit_passphrase(&testfile, &comment);
d.decrypt_async(&passphrase, Some(16))
let mut identity = scrypt::Identity::new(passphrase);
identity.set_max_work_factor(16);
d.decrypt_async(Some(&identity as _).into_iter())
}
}) {
Ok(mut r) => {

View file

@ -3,6 +3,7 @@
use age::{
armor::ArmoredReader,
cli_common::{read_identities, read_secret, StdinGuard},
scrypt,
stream::StreamReader,
};
use clap::{CommandFactory, Parser};
@ -210,12 +211,19 @@ fn main() -> Result<(), Error> {
let mut stdin_guard = StdinGuard::new(false);
match age::Decryptor::new_buffered(ArmoredReader::new(file))? {
age::Decryptor::Passphrase(decryptor) => {
age::Decryptor::Recipients(decryptor) if decryptor.is_scrypt() => {
match read_secret(&fl!("type-passphrase"), &fl!("prompt-passphrase"), None) {
Ok(passphrase) => decryptor
.decrypt(&passphrase, opts.max_work_factor)
.map_err(|e| e.into())
.and_then(|stream| mount_stream(stream, types, mountpoint)),
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(()),
}
}

View file

@ -6,7 +6,7 @@ use age::{
file_io, read_identities, read_or_generate_passphrase, read_recipients, read_secret,
Passphrase, StdinGuard, UiCallbacks,
},
plugin,
plugin, scrypt,
secrecy::ExposeSecret,
Identity,
};
@ -293,7 +293,7 @@ fn decrypt(opts: AgeOptions) -> Result<(), error::DecryptError> {
);
match age::Decryptor::new_buffered(ArmoredReader::new(input))? {
age::Decryptor::Passphrase(decryptor) => {
age::Decryptor::Recipients(decryptor) if decryptor.is_scrypt() => {
if identities_were_provided {
return Err(error::DecryptError::MixedIdentityAndPassphrase);
}
@ -308,10 +308,17 @@ fn decrypt(opts: AgeOptions) -> Result<(), error::DecryptError> {
}
match read_secret(&fl!("type-passphrase"), &fl!("prompt-passphrase"), None) {
Ok(passphrase) => decryptor
.decrypt(&passphrase, opts.max_work_factor)
.map_err(|e| e.into())
.and_then(|input| write_output(input, output)),
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(|input| write_output(input, output))
}
Err(pinentry::Error::Cancelled) => Ok(()),
Err(pinentry::Error::Timeout) => Err(error::DecryptError::PassphraseTimedOut),
Err(pinentry::Error::Encoding(e)) => {