mirror of
https://github.com/str4d/rage.git
synced 2025-04-04 11:27:43 +03:00
Merge pull request #518 from str4d/380-expose-ife-methods
Refactor `IdentityFile` APIs
This commit is contained in:
commit
d76c85d585
19 changed files with 423 additions and 270 deletions
|
@ -11,6 +11,11 @@ to 1.0.0 are beta releases.
|
|||
## [Unreleased]
|
||||
### Added
|
||||
- `age::Decryptor::{decrypt, decrypt_async, is_scrypt}`
|
||||
- `age::IdentityFile::to_recipients`
|
||||
- `age::IdentityFile::with_callbacks`
|
||||
- `age::IdentityFile::write_recipients_file`
|
||||
- `age::IdentityFileConvertError`
|
||||
- `age::NoCallbacks`
|
||||
- `age::scrypt`, providing recipient and identity types for passphrase-based
|
||||
encryption.
|
||||
- Partial French translation!
|
||||
|
@ -19,6 +24,11 @@ to 1.0.0 are beta releases.
|
|||
- Migrated to `i18n-embed 0.15`.
|
||||
- `age::Decryptor` is now an opaque struct instead of an enum with `Recipients`
|
||||
and `Passphrase` variants.
|
||||
- `age::IdentityFile` now has a `C: Callbacks` generic parameter, which defaults
|
||||
to `NoCallbacks`.
|
||||
- `age::IdentityFile::into_identities` now returns
|
||||
`Result<Vec<Box<dyn crate::Identity>>, DecryptError>` instead of
|
||||
`Vec<IdentityFileEntry>`.
|
||||
- `age::Recipient::wrap_file_key` now returns `(Vec<Stanza>, HashSet<String>)`:
|
||||
a tuple of the stanzas to be placed in an age file header, and labels that
|
||||
constrain how the stanzas may be combined with those from other recipients.
|
||||
|
@ -28,6 +38,7 @@ to 1.0.0 are beta releases.
|
|||
- `age::decryptor::PassphraseDecryptor` (use `age::Decryptor` with
|
||||
`age::scrypt::Identity` instead).
|
||||
- `age::decryptor::RecipientsDecryptor` (use `age::Decryptor` instead).
|
||||
- `age::IdentityFileEntry`
|
||||
|
||||
## [0.10.0] - 2024-02-04
|
||||
### Added
|
||||
|
|
|
@ -46,6 +46,16 @@ rec-deny-binary-output = Did you mean to use {-flag-armor}? {rec-detected-binary
|
|||
|
||||
err-deny-overwrite-file = refusing to overwrite existing file '{$filename}'.
|
||||
|
||||
## Identity file errors
|
||||
|
||||
err-failed-to-write-output = Failed to write to output: {$err}
|
||||
|
||||
err-identity-file-contains-plugin = Identity file '{$filename}' contains identities for '{-age-plugin-}{$plugin_name}'.
|
||||
rec-identity-file-contains-plugin = Try using '{-age-plugin-}{$plugin_name}' to convert this identity to a recipient.
|
||||
|
||||
err-no-identities-in-file = No identities found in file '{$filename}'.
|
||||
err-no-identities-in-stdin = No identities found in standard input.
|
||||
|
||||
## Errors
|
||||
|
||||
err-decryption-failed = Decryption failed
|
||||
|
|
|
@ -46,6 +46,16 @@ rec-deny-binary-output = Est-ce que vous vouliez utiliser {-flag-armor}? {rec-de
|
|||
|
||||
err-deny-overwrite-file = refus d'écraser le fichier existant '{$filename}'.
|
||||
|
||||
## Identity file errors
|
||||
|
||||
err-failed-to-write-output = Echec d'écriture vers la sortie: {$err}
|
||||
|
||||
err-identity-file-contains-plugin = Le ficher d'identité '{$filename}' contient des identités pour '{-age-plugin-}{$plugin_name}'.
|
||||
rec-identity-file-contains-plugin = Essayez d'utiliser {-age-plugin-}{$plugin_name}' pour convertir cette identité en un destinataire.
|
||||
|
||||
err-no-identities-in-file = Aucune identité trouvée dans le fichier '{$filename}'.
|
||||
err-no-identities-in-stdin = Aucune identité trouvée dans l'entrée standard (stdin).
|
||||
|
||||
## Errors
|
||||
|
||||
err-decryption-failed = Echec du déchiffrement
|
||||
|
|
|
@ -46,6 +46,16 @@ rec-deny-binary-output = Intendevi usare {-flag-armor}? {rec-detected-binary}
|
|||
|
||||
err-deny-overwrite-file = rifiuto di sovrascrivere il file esistente '{$filename}'.
|
||||
|
||||
## Identity file errors
|
||||
|
||||
err-failed-to-write-output = Impossibile scrivere sull'output: {$err}
|
||||
|
||||
err-identity-file-contains-plugin = Il file '{$filename}' contiene identità per '{-age-plugin-}{$plugin_name}'.
|
||||
rec-identity-file-contains-plugin = Prova a usare '{-age-plugin-}{$plugin_name}' per convertire questa identità in destinatario.
|
||||
|
||||
err-no-identities-in-file = Nessuna identità trovata nel file '{$filename}'.
|
||||
err-no-identities-in-stdin = Nessuna identità trovata tramite standard input.
|
||||
|
||||
## Errors
|
||||
|
||||
err-decryption-failed = Decifrazione fallita
|
||||
|
|
|
@ -46,6 +46,16 @@ rec-deny-binary-output = Возможно, вы хотели использов
|
|||
|
||||
err-deny-overwrite-file = отказ от перезаписи существующего файла '{$filename}'.
|
||||
|
||||
## Identity file errors
|
||||
|
||||
err-failed-to-write-output = Не удалось записать в выходной файл: {$err}
|
||||
|
||||
err-identity-file-contains-plugin = Файл идентификации '{$filename}' содержит идентификаторы для '{-age-plugin-}{$plugin_name}'.
|
||||
rec-identity-file-contains-plugin = Попробуйте использовать '{-age-plugin-}{$plugin_name}' для преобразования этого идентификатора в получателя.
|
||||
|
||||
err-no-identities-in-file = Идентификаторы в файле '{$filename}' не найдены.
|
||||
err-no-identities-in-stdin = Идентификаторы в стандартном вводе не найдены.
|
||||
|
||||
## Errors
|
||||
|
||||
err-decryption-failed = Ошибка дешифрования
|
||||
|
|
|
@ -33,11 +33,11 @@ pub fn read_identities(
|
|||
identities.push(Box::new(identity.with_callbacks(UiCallbacks)));
|
||||
Ok(())
|
||||
},
|
||||
|identities, entry| {
|
||||
let entry = entry.into_identity(UiCallbacks);
|
||||
|identities, identity_file| {
|
||||
let new_identities = identity_file.into_identities();
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
let entry = entry.map_err(|e| match e {
|
||||
let new_identities = new_identities.map_err(|e| match e {
|
||||
#[cfg(feature = "plugin")]
|
||||
crate::DecryptError::MissingPlugin { binary_name } => {
|
||||
ReadError::MissingPlugin { binary_name }
|
||||
|
@ -50,9 +50,9 @@ pub fn read_identities(
|
|||
// IdentityFileEntry::into_identity will never return a MissingPlugin error
|
||||
// when plugin feature is not enabled.
|
||||
#[cfg(not(feature = "plugin"))]
|
||||
let entry = entry.unwrap();
|
||||
let new_identities = new_identities.unwrap();
|
||||
|
||||
identities.push(entry);
|
||||
identities.extend(new_identities);
|
||||
|
||||
Ok(())
|
||||
},
|
||||
|
@ -72,7 +72,7 @@ pub(super) fn parse_identity_files<Ctx, E: From<ReadError> + From<io::Error>>(
|
|||
crate::encrypted::Identity<ArmoredReader<BufReader<InputReader>>, UiCallbacks>,
|
||||
) -> Result<(), E>,
|
||||
#[cfg(feature = "ssh")] ssh_identity: impl Fn(&mut Ctx, &str, crate::ssh::Identity) -> Result<(), E>,
|
||||
identity_file_entry: impl Fn(&mut Ctx, crate::IdentityFileEntry) -> Result<(), E>,
|
||||
identity_file: impl Fn(&mut Ctx, crate::IdentityFile<UiCallbacks>) -> Result<(), E>,
|
||||
) -> Result<(), E> {
|
||||
for filename in filenames {
|
||||
#[cfg_attr(not(any(feature = "armor", feature = "ssh")), allow(unused_mut))]
|
||||
|
@ -135,11 +135,10 @@ pub(super) fn parse_identity_files<Ctx, E: From<ReadError> + From<io::Error>>(
|
|||
reader.reset()?;
|
||||
|
||||
// Try parsing as multiple single-line age identities.
|
||||
let identity_file = IdentityFile::from_buffer(reader)?;
|
||||
|
||||
for entry in identity_file.into_identities() {
|
||||
identity_file_entry(ctx, entry)?;
|
||||
}
|
||||
identity_file(
|
||||
ctx,
|
||||
IdentityFile::from_buffer(reader)?.with_callbacks(UiCallbacks),
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -2,7 +2,8 @@ use std::io::{self, BufReader};
|
|||
|
||||
use super::StdinGuard;
|
||||
use super::{identities::parse_identity_files, ReadError};
|
||||
use crate::{x25519, IdentityFileEntry, Recipient};
|
||||
use crate::identity::RecipientsAccumulator;
|
||||
use crate::{x25519, Recipient};
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
use crate::{cli_common::UiCallbacks, plugin};
|
||||
|
@ -52,8 +53,7 @@ where
|
|||
fn parse_recipient(
|
||||
_filename: &str,
|
||||
s: String,
|
||||
recipients: &mut Vec<Box<dyn Recipient + Send>>,
|
||||
#[cfg(feature = "plugin")] plugin_recipients: &mut Vec<plugin::Recipient>,
|
||||
recipients: &mut RecipientsAccumulator,
|
||||
) -> Result<(), ReadError> {
|
||||
if let Ok(pk) = s.parse::<x25519::Recipient>() {
|
||||
recipients.push(Box::new(pk));
|
||||
|
@ -78,7 +78,7 @@ fn parse_recipient(
|
|||
None::<Infallible>
|
||||
} {
|
||||
#[cfg(feature = "plugin")]
|
||||
plugin_recipients.push(_recipient);
|
||||
recipients.push_plugin(_recipient);
|
||||
} else {
|
||||
return Err(ReadError::InvalidRecipient(s));
|
||||
}
|
||||
|
@ -90,8 +90,7 @@ fn parse_recipient(
|
|||
fn read_recipients_list<R: io::BufRead>(
|
||||
filename: &str,
|
||||
buf: R,
|
||||
recipients: &mut Vec<Box<dyn Recipient + Send>>,
|
||||
#[cfg(feature = "plugin")] plugin_recipients: &mut Vec<plugin::Recipient>,
|
||||
recipients: &mut RecipientsAccumulator,
|
||||
) -> Result<(), ReadError> {
|
||||
for (line_number, line) in buf.lines().enumerate() {
|
||||
let line = line?;
|
||||
|
@ -99,13 +98,7 @@ fn read_recipients_list<R: io::BufRead>(
|
|||
// Skip empty lines and comments
|
||||
if line.is_empty() || line.find('#') == Some(0) {
|
||||
continue;
|
||||
} else if let Err(_e) = parse_recipient(
|
||||
filename,
|
||||
line,
|
||||
recipients,
|
||||
#[cfg(feature = "plugin")]
|
||||
plugin_recipients,
|
||||
) {
|
||||
} else if let Err(_e) = parse_recipient(filename, line, recipients) {
|
||||
#[cfg(feature = "ssh")]
|
||||
match _e {
|
||||
ReadError::RsaModulusTooLarge
|
||||
|
@ -140,20 +133,10 @@ pub fn read_recipients(
|
|||
max_work_factor: Option<u8>,
|
||||
stdin_guard: &mut StdinGuard,
|
||||
) -> Result<Vec<Box<dyn Recipient + Send>>, ReadError> {
|
||||
let mut recipients: Vec<Box<dyn Recipient + Send>> = vec![];
|
||||
#[cfg(feature = "plugin")]
|
||||
let mut plugin_recipients: Vec<plugin::Recipient> = vec![];
|
||||
#[cfg(feature = "plugin")]
|
||||
let mut plugin_identities: Vec<plugin::Identity> = vec![];
|
||||
let mut recipients = RecipientsAccumulator::new();
|
||||
|
||||
for arg in recipient_strings {
|
||||
parse_recipient(
|
||||
"",
|
||||
arg,
|
||||
&mut recipients,
|
||||
#[cfg(feature = "plugin")]
|
||||
&mut plugin_recipients,
|
||||
)?;
|
||||
parse_recipient("", arg, &mut recipients)?;
|
||||
}
|
||||
|
||||
for arg in recipients_file_strings {
|
||||
|
@ -164,29 +147,16 @@ pub fn read_recipients(
|
|||
_ => e,
|
||||
})?;
|
||||
let buf = BufReader::new(f);
|
||||
read_recipients_list(
|
||||
&arg,
|
||||
buf,
|
||||
&mut recipients,
|
||||
#[cfg(feature = "plugin")]
|
||||
&mut plugin_recipients,
|
||||
)?;
|
||||
read_recipients_list(&arg, buf, &mut recipients)?;
|
||||
}
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
let ctx = &mut (&mut recipients, &mut plugin_identities);
|
||||
#[cfg(not(feature = "plugin"))]
|
||||
let ctx = &mut recipients;
|
||||
|
||||
parse_identity_files::<_, ReadError>(
|
||||
identity_strings,
|
||||
max_work_factor,
|
||||
stdin_guard,
|
||||
ctx,
|
||||
&mut recipients,
|
||||
#[cfg(feature = "armor")]
|
||||
|recipients, identity| {
|
||||
#[cfg(feature = "plugin")]
|
||||
let (recipients, _) = recipients;
|
||||
recipients.extend(identity.recipients().map_err(|e| {
|
||||
// Only one error can occur here.
|
||||
if let EncryptError::EncryptedIdentities(e) = e {
|
||||
|
@ -199,8 +169,6 @@ pub fn read_recipients(
|
|||
},
|
||||
#[cfg(feature = "ssh")]
|
||||
|recipients, filename, identity| {
|
||||
#[cfg(feature = "plugin")]
|
||||
let (recipients, _) = recipients;
|
||||
let recipient = parse_ssh_recipient(
|
||||
|| ssh::Recipient::try_from(identity),
|
||||
|| Err(ReadError::InvalidRecipient(filename.to_owned())),
|
||||
|
@ -210,49 +178,29 @@ pub fn read_recipients(
|
|||
recipients.push(recipient);
|
||||
Ok(())
|
||||
},
|
||||
|recipients, entry| {
|
||||
#[cfg(feature = "plugin")]
|
||||
let (recipients, plugin_identities) = recipients;
|
||||
match entry {
|
||||
IdentityFileEntry::Native(i) => recipients.push(Box::new(i.to_public())),
|
||||
#[cfg(feature = "plugin")]
|
||||
IdentityFileEntry::Plugin(i) => plugin_identities.push(i),
|
||||
}
|
||||
|recipients, identity_file| {
|
||||
recipients.with_identities(identity_file);
|
||||
Ok(())
|
||||
},
|
||||
)?;
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
{
|
||||
// Collect the names of the required plugins.
|
||||
let mut plugin_names = plugin_recipients
|
||||
.iter()
|
||||
.map(|r| r.plugin())
|
||||
.chain(plugin_identities.iter().map(|i| i.plugin()))
|
||||
.collect::<Vec<_>>();
|
||||
plugin_names.sort_unstable();
|
||||
plugin_names.dedup();
|
||||
recipients
|
||||
.build(
|
||||
#[cfg(feature = "plugin")]
|
||||
UiCallbacks,
|
||||
)
|
||||
.map_err(|_e| {
|
||||
// Only one error can occur here.
|
||||
#[cfg(feature = "plugin")]
|
||||
{
|
||||
if let EncryptError::MissingPlugin { binary_name } = _e {
|
||||
ReadError::MissingPlugin { binary_name }
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
// Find the required plugins.
|
||||
for plugin_name in plugin_names {
|
||||
recipients.push(Box::new(
|
||||
plugin::RecipientPluginV1::new(
|
||||
plugin_name,
|
||||
&plugin_recipients,
|
||||
&plugin_identities,
|
||||
UiCallbacks,
|
||||
)
|
||||
.map_err(|e| {
|
||||
// Only one error can occur here.
|
||||
if let EncryptError::MissingPlugin { binary_name } = e {
|
||||
ReadError::MissingPlugin { binary_name }
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
})?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
Ok(recipients)
|
||||
#[cfg(not(feature = "plugin"))]
|
||||
unreachable!()
|
||||
})
|
||||
}
|
||||
|
|
|
@ -2,17 +2,16 @@
|
|||
|
||||
use std::{cell::Cell, io};
|
||||
|
||||
use crate::{
|
||||
fl, scrypt, Callbacks, DecryptError, Decryptor, EncryptError, IdentityFile, IdentityFileEntry,
|
||||
};
|
||||
use crate::{fl, scrypt, Callbacks, DecryptError, Decryptor, EncryptError, IdentityFile};
|
||||
|
||||
/// The state of the encrypted age identity.
|
||||
enum IdentityState<R: io::Read> {
|
||||
enum IdentityState<R: io::Read, C: Callbacks> {
|
||||
Encrypted {
|
||||
decryptor: Decryptor<R>,
|
||||
max_work_factor: Option<u8>,
|
||||
callbacks: C,
|
||||
},
|
||||
Decrypted(Vec<IdentityFileEntry>),
|
||||
Decrypted(IdentityFile<C>),
|
||||
|
||||
/// The file was not correctly encrypted, or did not contain age identities. We cache
|
||||
/// this error in case the caller tries to use this identity again. The `Option` is to
|
||||
|
@ -21,26 +20,23 @@ enum IdentityState<R: io::Read> {
|
|||
Poisoned(Option<DecryptError>),
|
||||
}
|
||||
|
||||
impl<R: io::Read> Default for IdentityState<R> {
|
||||
impl<R: io::Read, C: Callbacks> Default for IdentityState<R, C> {
|
||||
fn default() -> Self {
|
||||
Self::Poisoned(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: io::Read> IdentityState<R> {
|
||||
impl<R: io::Read, C: Callbacks> IdentityState<R, C> {
|
||||
/// Decrypts this encrypted identity if necessary.
|
||||
///
|
||||
/// Returns the (possibly cached) identities, and a boolean marking if the identities
|
||||
/// were not cached (and we just asked the user for a passphrase).
|
||||
fn decrypt<C: Callbacks>(
|
||||
self,
|
||||
filename: Option<&str>,
|
||||
callbacks: C,
|
||||
) -> Result<(Vec<IdentityFileEntry>, bool), DecryptError> {
|
||||
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",
|
||||
|
@ -65,11 +61,12 @@ impl<R: io::Read> IdentityState<R> {
|
|||
}
|
||||
})
|
||||
.and_then(|stream| {
|
||||
let file = IdentityFile::from_buffer(io::BufReader::new(stream))?;
|
||||
Ok((file.into_identities(), true))
|
||||
let file = IdentityFile::from_buffer(io::BufReader::new(stream))?
|
||||
.with_callbacks(callbacks);
|
||||
Ok((file, true))
|
||||
})
|
||||
}
|
||||
Self::Decrypted(identities) => Ok((identities, false)),
|
||||
Self::Decrypted(identity_file) => Ok((identity_file, false)),
|
||||
// `IdentityState::decrypt` is only ever called with `Some`.
|
||||
Self::Poisoned(e) => Err(e.unwrap()),
|
||||
}
|
||||
|
@ -78,9 +75,8 @@ impl<R: io::Read> IdentityState<R> {
|
|||
|
||||
/// An encrypted age identity file.
|
||||
pub struct Identity<R: io::Read, C: Callbacks> {
|
||||
state: Cell<IdentityState<R>>,
|
||||
state: Cell<IdentityState<R, C>>,
|
||||
filename: Option<String>,
|
||||
callbacks: C,
|
||||
}
|
||||
|
||||
impl<R: io::Read, C: Callbacks> Identity<R, C> {
|
||||
|
@ -101,9 +97,9 @@ impl<R: io::Read, C: Callbacks> Identity<R, C> {
|
|||
state: Cell::new(IdentityState::Encrypted {
|
||||
decryptor,
|
||||
max_work_factor,
|
||||
callbacks,
|
||||
}),
|
||||
filename,
|
||||
callbacks,
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -112,18 +108,10 @@ impl<R: io::Read, C: Callbacks> 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 + Send>>, EncryptError> {
|
||||
match self
|
||||
.state
|
||||
.take()
|
||||
.decrypt(self.filename.as_deref(), self.callbacks.clone())
|
||||
{
|
||||
Ok((identities, _)) => {
|
||||
let recipients = identities
|
||||
.iter()
|
||||
.map(|entry| entry.to_recipient(self.callbacks.clone()))
|
||||
.collect::<Result<Vec<_>, _>>();
|
||||
|
||||
self.state.set(IdentityState::Decrypted(identities));
|
||||
match self.state.take().decrypt(self.filename.as_deref()) {
|
||||
Ok((identity_file, _)) => {
|
||||
let recipients = identity_file.to_recipients();
|
||||
self.state.set(IdentityState::Decrypted(identity_file));
|
||||
recipients
|
||||
}
|
||||
Err(e) => {
|
||||
|
@ -153,27 +141,20 @@ impl<R: io::Read, C: Callbacks> Identity<R, C> {
|
|||
Result<Box<dyn crate::Identity>, DecryptError>,
|
||||
) -> Option<Result<age_core::format::FileKey, DecryptError>>,
|
||||
{
|
||||
match self
|
||||
.state
|
||||
.take()
|
||||
.decrypt(self.filename.as_deref(), self.callbacks.clone())
|
||||
{
|
||||
Ok((identities, requested_passphrase)) => {
|
||||
let result = identities
|
||||
.iter()
|
||||
.map(|entry| entry.clone().into_identity(self.callbacks.clone()))
|
||||
.find_map(filter);
|
||||
match self.state.take().decrypt(self.filename.as_deref()) {
|
||||
Ok((identity_file, requested_passphrase)) => {
|
||||
let result = identity_file.to_identities().find_map(filter);
|
||||
|
||||
// If we requested a passphrase to decrypt, and none of the identities
|
||||
// matched, warn the user.
|
||||
if requested_passphrase && result.is_none() {
|
||||
self.callbacks.display_message(&fl!(
|
||||
identity_file.callbacks.display_message(&fl!(
|
||||
"encrypted-warn-no-match",
|
||||
filename = self.filename.as_deref().unwrap_or_default()
|
||||
));
|
||||
}
|
||||
|
||||
self.state.set(IdentityState::Decrypted(identities));
|
||||
self.state.set(IdentityState::Decrypted(identity_file));
|
||||
result
|
||||
}
|
||||
Err(e) => {
|
||||
|
|
|
@ -9,6 +9,72 @@ use crate::{wfl, wlnfl};
|
|||
#[cfg(feature = "plugin")]
|
||||
use age_core::format::Stanza;
|
||||
|
||||
/// Errors returned when converting an identity file to a recipients file.
|
||||
#[derive(Debug)]
|
||||
pub enum IdentityFileConvertError {
|
||||
/// An I/O error occurred while writing out a recipient corresponding to an identity
|
||||
/// in this file.
|
||||
FailedToWriteOutput(io::Error),
|
||||
/// The identity file contains a plugin identity, which can be converted to a
|
||||
/// recipient for encryption purposes, but not for writing a recipients file.
|
||||
#[cfg(feature = "plugin")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "plugin")))]
|
||||
IdentityFileContainsPlugin {
|
||||
/// The given identity file.
|
||||
filename: Option<String>,
|
||||
/// The name of the plugin.
|
||||
plugin_name: String,
|
||||
},
|
||||
/// The identity file contains no identities, and thus cannot be used to produce a
|
||||
/// recipients file.
|
||||
NoIdentities {
|
||||
/// The given identity file.
|
||||
filename: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
impl fmt::Display for IdentityFileConvertError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
IdentityFileConvertError::FailedToWriteOutput(e) => {
|
||||
wfl!(f, "err-failed-to-write-output", err = e.to_string())
|
||||
}
|
||||
#[cfg(feature = "plugin")]
|
||||
IdentityFileConvertError::IdentityFileContainsPlugin {
|
||||
filename,
|
||||
plugin_name,
|
||||
} => {
|
||||
wlnfl!(
|
||||
f,
|
||||
"err-identity-file-contains-plugin",
|
||||
filename = filename.as_deref().unwrap_or_default(),
|
||||
plugin_name = plugin_name.as_str(),
|
||||
)?;
|
||||
wfl!(
|
||||
f,
|
||||
"rec-identity-file-contains-plugin",
|
||||
plugin_name = plugin_name.as_str(),
|
||||
)
|
||||
}
|
||||
IdentityFileConvertError::NoIdentities { filename } => match filename {
|
||||
Some(filename) => {
|
||||
wfl!(f, "err-no-identities-in-file", filename = filename.as_str())
|
||||
}
|
||||
None => wfl!(f, "err-no-identities-in-stdin"),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for IdentityFileConvertError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
IdentityFileConvertError::FailedToWriteOutput(e) => Some(e),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors returned by a plugin.
|
||||
#[cfg(feature = "plugin")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "plugin")))]
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use std::fs::File;
|
||||
use std::io;
|
||||
|
||||
use crate::{x25519, Callbacks, DecryptError, EncryptError};
|
||||
use crate::{x25519, Callbacks, DecryptError, EncryptError, IdentityFileConvertError, NoCallbacks};
|
||||
|
||||
#[cfg(feature = "cli-common")]
|
||||
use crate::cli_common::file_io::InputReader;
|
||||
|
@ -11,7 +11,7 @@ use crate::plugin;
|
|||
|
||||
/// The supported kinds of identities within an [`IdentityFile`].
|
||||
#[derive(Clone)]
|
||||
pub enum IdentityFileEntry {
|
||||
enum IdentityFileEntry {
|
||||
/// The standard age identity type.
|
||||
Native(x25519::Identity),
|
||||
/// A plugin-compatible identity.
|
||||
|
@ -29,38 +29,25 @@ impl IdentityFileEntry {
|
|||
match self {
|
||||
IdentityFileEntry::Native(i) => Ok(Box::new(i)),
|
||||
#[cfg(feature = "plugin")]
|
||||
IdentityFileEntry::Plugin(i) => Ok(Box::new(crate::plugin::IdentityPluginV1::new(
|
||||
i.plugin(),
|
||||
&[i.clone()],
|
||||
callbacks,
|
||||
)?)),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
pub(crate) fn to_recipient(
|
||||
&self,
|
||||
callbacks: impl Callbacks,
|
||||
) -> Result<Box<dyn crate::Recipient + Send>, EncryptError> {
|
||||
match self {
|
||||
IdentityFileEntry::Native(i) => Ok(Box::new(i.to_public())),
|
||||
#[cfg(feature = "plugin")]
|
||||
IdentityFileEntry::Plugin(i) => Ok(Box::new(crate::plugin::RecipientPluginV1::new(
|
||||
i.plugin(),
|
||||
&[],
|
||||
&[i.clone()],
|
||||
callbacks,
|
||||
)?)),
|
||||
IdentityFileEntry::Plugin(i) => Ok(Box::new(
|
||||
crate::plugin::Plugin::new(i.plugin())
|
||||
.map_err(|binary_name| DecryptError::MissingPlugin { binary_name })
|
||||
.map(|plugin| {
|
||||
crate::plugin::IdentityPluginV1::from_parts(plugin, vec![i], callbacks)
|
||||
})?,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A list of identities that has been parsed from some input file.
|
||||
pub struct IdentityFile {
|
||||
pub struct IdentityFile<C: Callbacks> {
|
||||
filename: Option<String>,
|
||||
identities: Vec<IdentityFileEntry>,
|
||||
pub(crate) callbacks: C,
|
||||
}
|
||||
|
||||
impl IdentityFile {
|
||||
impl IdentityFile<NoCallbacks> {
|
||||
/// Parses one or more identities from a file containing valid UTF-8.
|
||||
pub fn from_file(filename: String) -> io::Result<Self> {
|
||||
File::open(&filename)
|
||||
|
@ -129,12 +116,177 @@ impl IdentityFile {
|
|||
}
|
||||
}
|
||||
|
||||
Ok(IdentityFile { identities })
|
||||
Ok(IdentityFile {
|
||||
filename,
|
||||
identities,
|
||||
callbacks: NoCallbacks,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Callbacks> IdentityFile<C> {
|
||||
/// Sets the provided callbacks on this identity file, so that if this is an encrypted
|
||||
/// identity, it can potentially be decrypted.
|
||||
pub fn with_callbacks<D: Callbacks>(self, callbacks: D) -> IdentityFile<D> {
|
||||
IdentityFile {
|
||||
filename: self.filename,
|
||||
identities: self.identities,
|
||||
callbacks,
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes a recipients file containing the recipients corresponding to the identities
|
||||
/// in this file.
|
||||
///
|
||||
/// Returns an error if this file is empty, or if it contains plugin identities (which
|
||||
/// can only be converted by the plugin binary itself).
|
||||
pub fn write_recipients_file<W: io::Write>(
|
||||
&self,
|
||||
mut output: W,
|
||||
) -> Result<(), IdentityFileConvertError> {
|
||||
if self.identities.is_empty() {
|
||||
return Err(IdentityFileConvertError::NoIdentities {
|
||||
filename: self.filename.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
for identity in &self.identities {
|
||||
match identity {
|
||||
IdentityFileEntry::Native(sk) => writeln!(output, "{}", sk.to_public())
|
||||
.map_err(IdentityFileConvertError::FailedToWriteOutput)?,
|
||||
#[cfg(feature = "plugin")]
|
||||
IdentityFileEntry::Plugin(id) => {
|
||||
return Err(IdentityFileConvertError::IdentityFileContainsPlugin {
|
||||
filename: self.filename.clone(),
|
||||
plugin_name: id.plugin().to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns recipients for the identities in this file.
|
||||
///
|
||||
/// Plugin identities will be merged into one [`Recipient`] per unique plugin.
|
||||
///
|
||||
/// [`Recipient`]: crate::Recipient
|
||||
pub fn to_recipients(&self) -> Result<Vec<Box<dyn crate::Recipient + Send>>, EncryptError> {
|
||||
let mut recipients = RecipientsAccumulator::new();
|
||||
recipients.with_identities_ref(self);
|
||||
recipients.build(
|
||||
#[cfg(feature = "plugin")]
|
||||
self.callbacks.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the identities in this file.
|
||||
pub fn into_identities(self) -> Vec<IdentityFileEntry> {
|
||||
pub(crate) fn to_identities(
|
||||
&self,
|
||||
) -> impl Iterator<Item = Result<Box<dyn crate::Identity>, 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> {
|
||||
self.identities
|
||||
.into_iter()
|
||||
.map(|entry| entry.into_identity(self.callbacks.clone()))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct RecipientsAccumulator {
|
||||
recipients: Vec<Box<dyn crate::Recipient + Send>>,
|
||||
#[cfg(feature = "plugin")]
|
||||
plugin_recipients: Vec<plugin::Recipient>,
|
||||
#[cfg(feature = "plugin")]
|
||||
plugin_identities: Vec<plugin::Identity>,
|
||||
}
|
||||
|
||||
impl RecipientsAccumulator {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
recipients: vec![],
|
||||
#[cfg(feature = "plugin")]
|
||||
plugin_recipients: vec![],
|
||||
#[cfg(feature = "plugin")]
|
||||
plugin_identities: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "cli-common")]
|
||||
pub(crate) fn push(&mut self, recipient: Box<dyn crate::Recipient + Send>) {
|
||||
self.recipients.push(recipient);
|
||||
}
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
pub(crate) fn push_plugin(&mut self, recipient: plugin::Recipient) {
|
||||
self.plugin_recipients.push(recipient);
|
||||
}
|
||||
|
||||
#[cfg(feature = "armor")]
|
||||
pub(crate) fn extend(
|
||||
&mut self,
|
||||
iter: impl IntoIterator<Item = Box<dyn crate::Recipient + Send>>,
|
||||
) {
|
||||
self.recipients.extend(iter);
|
||||
}
|
||||
|
||||
#[cfg(feature = "cli-common")]
|
||||
pub(crate) fn with_identities<C: Callbacks>(&mut self, identity_file: IdentityFile<C>) {
|
||||
for entry in identity_file.identities {
|
||||
match entry {
|
||||
IdentityFileEntry::Native(i) => self.recipients.push(Box::new(i.to_public())),
|
||||
#[cfg(feature = "plugin")]
|
||||
IdentityFileEntry::Plugin(i) => self.plugin_identities.push(i),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn with_identities_ref<C: Callbacks>(&mut self, identity_file: &IdentityFile<C>) {
|
||||
for entry in &identity_file.identities {
|
||||
match entry {
|
||||
IdentityFileEntry::Native(i) => self.recipients.push(Box::new(i.to_public())),
|
||||
#[cfg(feature = "plugin")]
|
||||
IdentityFileEntry::Plugin(i) => self.plugin_identities.push(i.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(not(feature = "plugin"), allow(unused_mut))]
|
||||
pub(crate) fn build(
|
||||
mut self,
|
||||
#[cfg(feature = "plugin")] callbacks: impl Callbacks,
|
||||
) -> Result<Vec<Box<dyn crate::Recipient + Send>>, EncryptError> {
|
||||
#[cfg(feature = "plugin")]
|
||||
{
|
||||
// Collect the names of the required plugins.
|
||||
let mut plugin_names = self
|
||||
.plugin_recipients
|
||||
.iter()
|
||||
.map(|r| r.plugin())
|
||||
.chain(self.plugin_identities.iter().map(|i| i.plugin()))
|
||||
.collect::<Vec<_>>();
|
||||
plugin_names.sort_unstable();
|
||||
plugin_names.dedup();
|
||||
|
||||
// Find the required plugins.
|
||||
for plugin_name in plugin_names {
|
||||
self.recipients
|
||||
.push(Box::new(plugin::RecipientPluginV1::new(
|
||||
plugin_name,
|
||||
&self.plugin_recipients,
|
||||
&self.plugin_identities,
|
||||
callbacks.clone(),
|
||||
)?))
|
||||
}
|
||||
}
|
||||
|
||||
Ok(self.recipients)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -149,8 +149,8 @@ mod primitives;
|
|||
mod protocol;
|
||||
mod util;
|
||||
|
||||
pub use error::{DecryptError, EncryptError};
|
||||
pub use identity::{IdentityFile, IdentityFileEntry};
|
||||
pub use error::{DecryptError, EncryptError, IdentityFileConvertError};
|
||||
pub use identity::IdentityFile;
|
||||
pub use primitives::stream;
|
||||
pub use protocol::{Decryptor, Encryptor};
|
||||
|
||||
|
@ -278,6 +278,9 @@ pub trait Callbacks: Clone + Send + Sync + 'static {
|
|||
///
|
||||
/// This can be used to prompt the user to take some physical action, such as
|
||||
/// inserting a hardware key.
|
||||
///
|
||||
/// No guarantee is provided that the user sees this message (for example, if there is
|
||||
/// no UI for displaying messages).
|
||||
fn display_message(&self, message: &str);
|
||||
|
||||
/// Requests that the user provides confirmation for some action.
|
||||
|
@ -300,12 +303,45 @@ pub trait Callbacks: Clone + Send + Sync + 'static {
|
|||
/// Requests non-private input from the user.
|
||||
///
|
||||
/// To request private inputs, use [`Callbacks::request_passphrase`].
|
||||
///
|
||||
/// Returns:
|
||||
/// - `Some(input)` with the user-provided input.
|
||||
/// - `None` if no input could be requested from the user (for example, if there is no
|
||||
/// UI for displaying messages or typing inputs).
|
||||
fn request_public_string(&self, description: &str) -> Option<String>;
|
||||
|
||||
/// Requests a passphrase to decrypt a key.
|
||||
///
|
||||
/// Returns:
|
||||
/// - `Some(passphrase)` with the user-provided passphrase.
|
||||
/// - `None` if no passphrase could be requested from the user (for example, if there
|
||||
/// is no UI for displaying messages or typing inputs).
|
||||
fn request_passphrase(&self, description: &str) -> Option<SecretString>;
|
||||
}
|
||||
|
||||
/// An implementation of [`Callbacks`] that does not allow callbacks.
|
||||
///
|
||||
/// No user interaction will occur; [`Recipient`] or [`Identity`] implementations will
|
||||
/// receive `None` from the callbacks that return responses, and will act accordingly.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct NoCallbacks;
|
||||
|
||||
impl Callbacks for NoCallbacks {
|
||||
fn display_message(&self, _: &str) {}
|
||||
|
||||
fn confirm(&self, _: &str, _: &str, _: Option<&str>) -> Option<bool> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_public_string(&self, _: &str) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_passphrase(&self, _: &str) -> Option<SecretString> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper for fuzzing the Header parser and serializer.
|
||||
#[cfg(fuzzing)]
|
||||
pub fn fuzz_header(data: &[u8]) {
|
||||
|
|
|
@ -190,7 +190,7 @@ impl Identity {
|
|||
}
|
||||
|
||||
/// An age plugin.
|
||||
struct Plugin {
|
||||
pub(crate) struct Plugin {
|
||||
binary_name: String,
|
||||
path: PathBuf,
|
||||
}
|
||||
|
@ -199,7 +199,7 @@ impl Plugin {
|
|||
/// Finds the age plugin with the given name in `$PATH`.
|
||||
///
|
||||
/// On error, returns the binary name that could not be located.
|
||||
fn new(name: &str) -> Result<Self, String> {
|
||||
pub(crate) fn new(name: &str) -> Result<Self, String> {
|
||||
let binary_name = binary_name(name);
|
||||
match which::which(&binary_name).or_else(|e| {
|
||||
// If we are running in WSL, try appending `.exe`; plugins installed in
|
||||
|
@ -565,17 +565,24 @@ impl<C: Callbacks> IdentityPluginV1<C> {
|
|||
) -> Result<Self, DecryptError> {
|
||||
Plugin::new(plugin_name)
|
||||
.map_err(|binary_name| DecryptError::MissingPlugin { binary_name })
|
||||
.map(|plugin| IdentityPluginV1 {
|
||||
plugin,
|
||||
identities: identities
|
||||
.map(|plugin| {
|
||||
let identities = identities
|
||||
.iter()
|
||||
.filter(|r| r.name == plugin_name)
|
||||
.cloned()
|
||||
.collect(),
|
||||
callbacks,
|
||||
.collect();
|
||||
Self::from_parts(plugin, identities, callbacks)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn from_parts(plugin: Plugin, identities: Vec<Identity>, callbacks: C) -> Self {
|
||||
IdentityPluginV1 {
|
||||
plugin,
|
||||
identities,
|
||||
callbacks,
|
||||
}
|
||||
}
|
||||
|
||||
fn unwrap_stanzas<'a>(
|
||||
&self,
|
||||
stanzas: impl Iterator<Item = &'a Stanza>,
|
||||
|
|
|
@ -326,10 +326,7 @@ mod tests {
|
|||
use std::iter;
|
||||
|
||||
use super::{Decryptor, Encryptor};
|
||||
use crate::{
|
||||
identity::{IdentityFile, IdentityFileEntry},
|
||||
scrypt, x25519, EncryptError, Identity, Recipient,
|
||||
};
|
||||
use crate::{identity::IdentityFile, scrypt, x25519, EncryptError, Identity, Recipient};
|
||||
|
||||
#[cfg(feature = "async")]
|
||||
use futures::{
|
||||
|
@ -445,11 +442,7 @@ mod tests {
|
|||
let pk: x25519::Recipient = crate::x25519::tests::TEST_PK.parse().unwrap();
|
||||
recipient_round_trip(
|
||||
vec![Box::new(pk)],
|
||||
f.into_identities().iter().map(|sk| match sk {
|
||||
IdentityFileEntry::Native(sk) => sk as &dyn Identity,
|
||||
#[cfg(feature = "plugin")]
|
||||
IdentityFileEntry::Plugin(_) => unreachable!(),
|
||||
}),
|
||||
f.into_identities().unwrap().iter().map(|i| i.as_ref()),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -461,11 +454,7 @@ mod tests {
|
|||
let pk: x25519::Recipient = crate::x25519::tests::TEST_PK.parse().unwrap();
|
||||
recipient_async_round_trip(
|
||||
vec![Box::new(pk)],
|
||||
f.into_identities().iter().map(|sk| match sk {
|
||||
IdentityFileEntry::Native(sk) => sk as &dyn Identity,
|
||||
#[cfg(feature = "plugin")]
|
||||
IdentityFileEntry::Plugin(_) => unreachable!(),
|
||||
}),
|
||||
f.into_identities().unwrap().iter().map(|i| i.as_ref()),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -146,14 +146,6 @@ err-ux-B = Tell us
|
|||
# Put (len(A) - len(B) - 32) spaces here.
|
||||
err-ux-C = {" "}
|
||||
|
||||
## Keygen errors
|
||||
|
||||
err-identity-file-contains-plugin = Identity file '{$filename}' contains identities for '{-age-plugin-}{$plugin_name}'.
|
||||
rec-identity-file-contains-plugin = Try using '{-age-plugin-}{$plugin_name}' to convert this identity to a recipient.
|
||||
|
||||
err-no-identities-in-file = No identities found in file '{$filename}'.
|
||||
err-no-identities-in-stdin = No identities found in standard input.
|
||||
|
||||
## Encryption errors
|
||||
|
||||
err-enc-broken-stdout = Could not write to stdout: {$err}
|
||||
|
|
|
@ -151,14 +151,6 @@ err-ux-B = Dites-le nous
|
|||
# Put (len(A) - len(B) - 32) spaces here.
|
||||
err-ux-C = {" "}
|
||||
|
||||
## Keygen errors
|
||||
|
||||
err-identity-file-contains-plugin = Le ficher d'identité '{$filename}' contient des identités pour '{-age-plugin-}{$plugin_name}'.
|
||||
rec-identity-file-contains-plugin = Essayez d'utiliser {-age-plugin-}{$plugin_name}' pour convertir cette identité en un destinataire.
|
||||
|
||||
err-no-identities-in-file = Aucune identité trouvée dans le fichier '{$filename}'.
|
||||
err-no-identities-in-stdin = Aucune identité trouvée dans l'entrée standard (stdin).
|
||||
|
||||
## Encryption errors
|
||||
|
||||
err-enc-broken-stdout = N'a pas pu écrire sur stdout: {$err}
|
||||
|
|
|
@ -145,14 +145,6 @@ err-ux-B = Faccelo sapere
|
|||
# Put (len(A) - len(B) - 32) spaces here.
|
||||
err-ux-C = {" "}
|
||||
|
||||
## Keygen errors
|
||||
|
||||
err-identity-file-contains-plugin = Il file '{$filename}' contiene identità per '{-age-plugin-}{$plugin_name}'.
|
||||
rec-identity-file-contains-plugin = Prova a usare '{-age-plugin-}{$plugin_name}' per convertire questa identità in destinatario.
|
||||
|
||||
err-no-identities-in-file = Nessuna identità trovata nel file '{$filename}'.
|
||||
err-no-identities-in-stdin = Nessuna identità trovata tramite standard input.
|
||||
|
||||
## Encryption errors
|
||||
|
||||
err-enc-broken-stdout = Impossibile scrivere sullo standard output: {$err}
|
||||
|
|
|
@ -147,14 +147,6 @@ err-ux-B = Сообщите нам
|
|||
# Поставьте здесь пробелы (len(A) - len(B) - 32).
|
||||
err-ux-C = {" "}
|
||||
|
||||
## Keygen errors
|
||||
|
||||
err-identity-file-contains-plugin = Файл идентификации '{$filename}' содержит идентификаторы для '{-age-plugin-}{$plugin_name}'.
|
||||
rec-identity-file-contains-plugin = Попробуйте использовать '{-age-plugin-}{$plugin_name}' для преобразования этого идентификатора в получателя.
|
||||
|
||||
err-no-identities-in-file = Идентификаторы в файле '{$filename}' не найдены.
|
||||
err-no-identities-in-stdin = Идентификаторы в стандартном вводе не найдены.
|
||||
|
||||
## Encryption errors
|
||||
|
||||
err-enc-broken-stdout = Не удалось записать в stdout: {$err}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
use std::fmt;
|
||||
use std::io;
|
||||
|
||||
use age::IdentityFileConvertError;
|
||||
|
||||
macro_rules! wlnfl {
|
||||
($f:ident, $message_id:literal) => {
|
||||
writeln!($f, "{}", $crate::fl!($message_id))
|
||||
|
@ -16,13 +18,7 @@ pub(crate) enum Error {
|
|||
FailedToOpenOutput(io::Error),
|
||||
FailedToReadInput(io::Error),
|
||||
FailedToWriteOutput(io::Error),
|
||||
IdentityFileContainsPlugin {
|
||||
filename: Option<String>,
|
||||
plugin_name: String,
|
||||
},
|
||||
NoIdentities {
|
||||
filename: Option<String>,
|
||||
},
|
||||
IdentityFileConvert(IdentityFileConvertError),
|
||||
}
|
||||
|
||||
// Rust only supports `fn main() -> Result<(), E: Debug>`, so we implement `Debug`
|
||||
|
@ -42,28 +38,7 @@ impl fmt::Debug for Error {
|
|||
Error::FailedToWriteOutput(e) => {
|
||||
wlnfl!(f, "err-failed-to-write-output", err = e.to_string())?
|
||||
}
|
||||
Error::IdentityFileContainsPlugin {
|
||||
filename,
|
||||
plugin_name,
|
||||
} => {
|
||||
wlnfl!(
|
||||
f,
|
||||
"err-identity-file-contains-plugin",
|
||||
filename = filename.as_deref().unwrap_or_default(),
|
||||
plugin_name = plugin_name.as_str(),
|
||||
)?;
|
||||
wlnfl!(
|
||||
f,
|
||||
"rec-identity-file-contains-plugin",
|
||||
plugin_name = plugin_name.as_str(),
|
||||
)?
|
||||
}
|
||||
Error::NoIdentities { filename } => match filename {
|
||||
Some(filename) => {
|
||||
wlnfl!(f, "err-no-identities-in-file", filename = filename.as_str())?
|
||||
}
|
||||
None => wlnfl!(f, "err-no-identities-in-stdin")?,
|
||||
},
|
||||
Error::IdentityFileConvert(e) => writeln!(f, "{e}")?,
|
||||
}
|
||||
writeln!(f)?;
|
||||
writeln!(f, "[ {} ]", crate::fl!("err-ux-A"))?;
|
||||
|
|
|
@ -73,33 +73,14 @@ fn generate(mut output: file_io::OutputWriter) -> io::Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn convert(
|
||||
filename: Option<String>,
|
||||
mut output: file_io::OutputWriter,
|
||||
) -> Result<(), error::Error> {
|
||||
fn convert(filename: Option<String>, output: file_io::OutputWriter) -> Result<(), error::Error> {
|
||||
let file = age::IdentityFile::from_input_reader(
|
||||
file_io::InputReader::new(filename.clone()).map_err(error::Error::FailedToOpenInput)?,
|
||||
file_io::InputReader::new(filename).map_err(error::Error::FailedToOpenInput)?,
|
||||
)
|
||||
.map_err(error::Error::FailedToReadInput)?;
|
||||
|
||||
let identities = file.into_identities();
|
||||
if identities.is_empty() {
|
||||
return Err(error::Error::NoIdentities { filename });
|
||||
}
|
||||
|
||||
for identity in identities {
|
||||
match identity {
|
||||
age::IdentityFileEntry::Native(sk) => {
|
||||
writeln!(output, "{}", sk.to_public()).map_err(error::Error::FailedToWriteOutput)?
|
||||
}
|
||||
age::IdentityFileEntry::Plugin(id) => {
|
||||
return Err(error::Error::IdentityFileContainsPlugin {
|
||||
filename,
|
||||
plugin_name: id.plugin().to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
file.write_recipients_file(output)
|
||||
.map_err(error::Error::IdentityFileConvert)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue