use crate::env;
use crate::error::{FnoxError, Result};
use async_trait::async_trait;
use std::io::Read;
use std::path::PathBuf;
pub fn env_dependencies() -> &'static [&'static str] {
&[]
}
pub struct AgeEncryptionProvider {
recipients: Vec<String>,
key_file: Option<PathBuf>,
}
impl AgeEncryptionProvider {
pub fn new(recipients: Vec<String>, key_file: Option<String>) -> Result<Self> {
Ok(Self {
recipients,
key_file: key_file.map(|k| PathBuf::from(shellexpand::tilde(&k).to_string())),
})
}
}
#[async_trait]
impl crate::providers::Provider for AgeEncryptionProvider {
fn capabilities(&self) -> Vec<crate::providers::ProviderCapability> {
vec![crate::providers::ProviderCapability::Encryption]
}
async fn encrypt(&self, plaintext: &str) -> Result<String> {
use std::io::Write;
if self.recipients.is_empty() {
return Err(FnoxError::AgeNotConfigured);
}
let mut parsed_recipients: Vec<Box<dyn age::Recipient + Send + Sync>> = Vec::new();
for recipient in &self.recipients {
if let Ok(ssh_recipient) = recipient.parse::<age::ssh::Recipient>() {
parsed_recipients.push(Box::new(ssh_recipient));
continue;
}
match recipient.parse::<age::x25519::Recipient>() {
Ok(age_recipient) => {
parsed_recipients.push(Box::new(age_recipient));
}
Err(e) => {
return Err(FnoxError::AgeEncryptionFailed {
details: format!("Failed to parse recipient '{}': {}", recipient, e),
});
}
}
}
if parsed_recipients.is_empty() {
return Err(FnoxError::AgeNotConfigured);
}
let encryptor = age::Encryptor::with_recipients(
parsed_recipients
.iter()
.map(|r| r.as_ref() as &dyn age::Recipient),
)
.expect("we provided at least one recipient");
let mut encrypted = vec![];
let mut writer =
encryptor
.wrap_output(&mut encrypted)
.map_err(|e| FnoxError::AgeEncryptionFailed {
details: format!("Failed to create encrypted writer: {}", e),
})?;
writer
.write_all(plaintext.as_bytes())
.map_err(|e| FnoxError::AgeEncryptionFailed {
details: format!("Failed to write plaintext: {}", e),
})?;
writer
.finish()
.map_err(|e| FnoxError::AgeEncryptionFailed {
details: format!("Failed to finalize encryption: {}", e),
})?;
use base64::Engine;
let encrypted_base64 = base64::engine::general_purpose::STANDARD.encode(&encrypted);
Ok(encrypted_base64)
}
async fn get_secret(&self, value: &str) -> Result<String> {
let encrypted_bytes =
match base64::Engine::decode(&base64::engine::general_purpose::STANDARD, value) {
Ok(bytes) => bytes,
Err(_) => {
value.as_bytes().to_vec()
}
};
let (identity_content, key_file_path_opt) = if let Some(ref age_key) = *env::FNOX_AGE_KEY {
(age_key.clone(), None)
} else {
let key_file_path = if let Some(ref config_key_file) = self.key_file {
config_key_file.clone()
} else {
let settings = crate::settings::Settings::get();
if let Some(ref age_key_file) = settings.age_key_file {
age_key_file.clone()
} else {
let default_key_path = env::FNOX_CONFIG_DIR.join("age.txt");
if !default_key_path.exists() {
return Err(FnoxError::AgeIdentityNotFound {
path: default_key_path,
});
}
default_key_path
}
};
let content = std::fs::read_to_string(&key_file_path).map_err(|e| {
FnoxError::AgeIdentityReadFailed {
path: key_file_path.clone(),
source: e,
}
})?;
(content, Some(key_file_path))
};
let identities = {
let mut cursor = std::io::Cursor::new(identity_content.as_bytes());
match age::ssh::Identity::from_buffer(
&mut cursor,
key_file_path_opt
.as_ref()
.map(|p| p.to_string_lossy().to_string()),
) {
Ok(ssh_identity) => {
vec![Box::new(ssh_identity) as Box<dyn age::Identity>]
}
Err(_) => {
cursor.set_position(0);
age::IdentityFile::from_buffer(cursor)
.map_err(|e| FnoxError::AgeIdentityParseFailed {
details: e.to_string(),
})?
.into_identities()
.map_err(|e| FnoxError::AgeIdentityParseFailed {
details: e.to_string(),
})?
}
}
};
let decryptor = age::Decryptor::new(encrypted_bytes.as_slice()).map_err(|e| {
FnoxError::AgeDecryptionFailed {
details: format!("Failed to create decryptor: {}", e),
}
})?;
let mut reader = decryptor
.decrypt(identities.iter().map(|i| i.as_ref() as &dyn age::Identity))
.map_err(|e| FnoxError::AgeDecryptionFailed {
details: e.to_string(),
})?;
let mut decrypted = vec![];
reader
.read_to_end(&mut decrypted)
.map_err(|e| FnoxError::AgeDecryptionFailed {
details: format!("Failed to read decrypted data: {}", e),
})?;
String::from_utf8(decrypted).map_err(|e| FnoxError::AgeDecryptionFailed {
details: format!("Failed to decode UTF-8: {}", e),
})
}
}