2026-04-01 21:06:55 -04:00
|
|
|
pub mod decrypt;
|
2026-04-01 21:02:57 -04:00
|
|
|
pub mod encrypt;
|
2026-04-01 20:44:05 -04:00
|
|
|
pub mod error;
|
2026-04-01 20:47:00 -04:00
|
|
|
pub mod format;
|
2026-04-01 20:44:05 -04:00
|
|
|
pub mod identities;
|
|
|
|
|
pub mod recipients;
|
2026-03-31 17:26:10 -04:00
|
|
|
pub mod sources;
|
|
|
|
|
|
2026-04-01 21:06:55 -04:00
|
|
|
pub use decrypt::Decryptor;
|
2026-04-01 21:02:57 -04:00
|
|
|
pub use encrypt::Encryptor;
|
2026-04-01 21:16:39 -04:00
|
|
|
pub use error::AgeError;
|
|
|
|
|
pub use identities::{
|
|
|
|
|
EncryptedIdentity, FileKey, Header, Identity, ScryptIdentity, SshEd25519Identity,
|
|
|
|
|
SshRsaIdentity, Stanza, X25519Identity,
|
|
|
|
|
};
|
|
|
|
|
pub use recipients::{
|
|
|
|
|
Recipient, ScryptRecipient, SshEd25519Recipient, SshRsaRecipient, X25519Recipient,
|
2026-04-01 20:44:05 -04:00
|
|
|
};
|
2026-04-01 21:16:39 -04:00
|
|
|
pub use sources::{BitwardenSource, FileSource, IdentitySource, PromptSource};
|
|
|
|
|
|
|
|
|
|
/// Try each `IdentitySource` in order; collect all identities that load successfully.
|
|
|
|
|
///
|
|
|
|
|
/// Returns an error only if all sources fail. Partial success is accepted because
|
|
|
|
|
/// not every source needs to hold the key for the target file.
|
|
|
|
|
pub fn resolve_identities(
|
|
|
|
|
sources: Vec<Box<dyn sources::IdentitySource>>,
|
|
|
|
|
) -> Result<Vec<Box<dyn Identity>>, AgeError> {
|
|
|
|
|
let mut identities: Vec<Box<dyn Identity>> = Vec::new();
|
|
|
|
|
let mut last_err: Option<AgeError> = None;
|
|
|
|
|
|
|
|
|
|
for source in sources {
|
|
|
|
|
match source.load() {
|
|
|
|
|
Ok(identity) => identities.push(identity),
|
|
|
|
|
Err(e) => {
|
|
|
|
|
eprintln!(" [{}] {e}", source.name());
|
|
|
|
|
last_err = Some(e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if identities.is_empty() {
|
|
|
|
|
Err(last_err.unwrap_or(AgeError::SourceError {
|
|
|
|
|
name: "resolve_identities".to_string(),
|
|
|
|
|
source: anyhow::anyhow!("no sources provided"),
|
|
|
|
|
}))
|
|
|
|
|
} else {
|
|
|
|
|
Ok(identities)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
use crate::sources::IdentitySource;
|
|
|
|
|
|
|
|
|
|
struct AlwaysFailSource;
|
|
|
|
|
impl IdentitySource for AlwaysFailSource {
|
|
|
|
|
fn name(&self) -> &str {
|
|
|
|
|
"always-fail"
|
|
|
|
|
}
|
|
|
|
|
fn load(&self) -> Result<Box<dyn Identity>, AgeError> {
|
|
|
|
|
Err(AgeError::SourceError {
|
|
|
|
|
name: "always-fail".to_string(),
|
|
|
|
|
source: anyhow::anyhow!("intentional failure"),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct StaticX25519Source;
|
|
|
|
|
impl IdentitySource for StaticX25519Source {
|
|
|
|
|
fn name(&self) -> &str {
|
|
|
|
|
"static-x25519"
|
|
|
|
|
}
|
|
|
|
|
fn load(&self) -> Result<Box<dyn Identity>, AgeError> {
|
|
|
|
|
use rand::rngs::OsRng;
|
|
|
|
|
use x25519_dalek::StaticSecret;
|
|
|
|
|
Ok(Box::new(
|
|
|
|
|
crate::identities::x25519::X25519Identity::from_static_secret(
|
|
|
|
|
StaticSecret::random_from_rng(OsRng),
|
|
|
|
|
),
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn resolve_identities_all_fail_returns_error() {
|
|
|
|
|
let sources: Vec<Box<dyn IdentitySource>> = vec![Box::new(AlwaysFailSource)];
|
|
|
|
|
assert!(resolve_identities(sources).is_err());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn resolve_identities_partial_success_returns_loaded() {
|
|
|
|
|
let sources: Vec<Box<dyn IdentitySource>> = vec![
|
|
|
|
|
Box::new(AlwaysFailSource),
|
|
|
|
|
Box::new(StaticX25519Source),
|
|
|
|
|
];
|
|
|
|
|
let ids = resolve_identities(sources).expect("at least one source succeeded");
|
|
|
|
|
assert_eq!(ids.len(), 1);
|
|
|
|
|
}
|
|
|
|
|
}
|