fnox_core/providers/
age.rs1use crate::env;
2use crate::error::{FnoxError, Result};
3use async_trait::async_trait;
4use std::io::Read;
5use std::path::PathBuf;
6
7pub fn env_dependencies() -> &'static [&'static str] {
8 &[]
9}
10
11pub struct AgeEncryptionProvider {
12 recipients: Vec<String>,
13 key_file: Option<PathBuf>,
14}
15
16impl AgeEncryptionProvider {
17 pub fn new(recipients: Vec<String>, key_file: Option<String>) -> Result<Self> {
18 Ok(Self {
19 recipients,
20 key_file: key_file.map(|k| PathBuf::from(shellexpand::tilde(&k).to_string())),
21 })
22 }
23}
24
25#[async_trait]
26impl crate::providers::Provider for AgeEncryptionProvider {
27 fn capabilities(&self) -> Vec<crate::providers::ProviderCapability> {
28 vec![crate::providers::ProviderCapability::Encryption]
29 }
30
31 async fn encrypt(&self, plaintext: &str) -> Result<String> {
32 use std::io::Write;
33
34 if self.recipients.is_empty() {
35 return Err(FnoxError::AgeNotConfigured);
36 }
37
38 let mut parsed_recipients: Vec<Box<dyn age::Recipient + Send + Sync>> = Vec::new();
40
41 for recipient in &self.recipients {
42 if let Ok(ssh_recipient) = recipient.parse::<age::ssh::Recipient>() {
44 parsed_recipients.push(Box::new(ssh_recipient));
45 continue;
46 }
47
48 match recipient.parse::<age::x25519::Recipient>() {
50 Ok(age_recipient) => {
51 parsed_recipients.push(Box::new(age_recipient));
52 }
53 Err(e) => {
54 return Err(FnoxError::AgeEncryptionFailed {
55 details: format!("Failed to parse recipient '{}': {}", recipient, e),
56 });
57 }
58 }
59 }
60
61 if parsed_recipients.is_empty() {
62 return Err(FnoxError::AgeNotConfigured);
63 }
64
65 let encryptor = age::Encryptor::with_recipients(
67 parsed_recipients
68 .iter()
69 .map(|r| r.as_ref() as &dyn age::Recipient),
70 )
71 .expect("we provided at least one recipient");
72
73 let mut encrypted = vec![];
75 let mut writer =
76 encryptor
77 .wrap_output(&mut encrypted)
78 .map_err(|e| FnoxError::AgeEncryptionFailed {
79 details: format!("Failed to create encrypted writer: {}", e),
80 })?;
81
82 writer
83 .write_all(plaintext.as_bytes())
84 .map_err(|e| FnoxError::AgeEncryptionFailed {
85 details: format!("Failed to write plaintext: {}", e),
86 })?;
87
88 writer
89 .finish()
90 .map_err(|e| FnoxError::AgeEncryptionFailed {
91 details: format!("Failed to finalize encryption: {}", e),
92 })?;
93
94 use base64::Engine;
96 let encrypted_base64 = base64::engine::general_purpose::STANDARD.encode(&encrypted);
97
98 Ok(encrypted_base64)
99 }
100
101 async fn get_secret(&self, value: &str) -> Result<String> {
102 let encrypted_bytes =
106 match base64::Engine::decode(&base64::engine::general_purpose::STANDARD, value) {
107 Ok(bytes) => bytes,
108 Err(_) => {
109 value.as_bytes().to_vec()
111 }
112 };
113
114 let (identity_content, key_file_path_opt) = if let Some(ref age_key) = *env::FNOX_AGE_KEY {
120 (age_key.clone(), None)
122 } else {
123 let key_file_path = if let Some(ref config_key_file) = self.key_file {
125 config_key_file.clone()
127 } else {
128 let settings = crate::settings::Settings::get();
130
131 if let Some(ref age_key_file) = settings.age_key_file {
132 age_key_file.clone()
134 } else {
135 let default_key_path = env::FNOX_CONFIG_DIR.join("age.txt");
137 if !default_key_path.exists() {
138 return Err(FnoxError::AgeIdentityNotFound {
139 path: default_key_path,
140 });
141 }
142 default_key_path
143 }
144 };
145
146 let content = std::fs::read_to_string(&key_file_path).map_err(|e| {
148 FnoxError::AgeIdentityReadFailed {
149 path: key_file_path.clone(),
150 source: e,
151 }
152 })?;
153
154 (content, Some(key_file_path))
155 };
156
157 let identities = {
159 let mut cursor = std::io::Cursor::new(identity_content.as_bytes());
160
161 match age::ssh::Identity::from_buffer(
163 &mut cursor,
164 key_file_path_opt
165 .as_ref()
166 .map(|p| p.to_string_lossy().to_string()),
167 ) {
168 Ok(ssh_identity) => {
169 vec![Box::new(ssh_identity) as Box<dyn age::Identity>]
171 }
172 Err(_) => {
173 cursor.set_position(0);
175 age::IdentityFile::from_buffer(cursor)
176 .map_err(|e| FnoxError::AgeIdentityParseFailed {
177 details: e.to_string(),
178 })?
179 .into_identities()
180 .map_err(|e| FnoxError::AgeIdentityParseFailed {
181 details: e.to_string(),
182 })?
183 }
184 }
185 };
186
187 let decryptor = age::Decryptor::new(encrypted_bytes.as_slice()).map_err(|e| {
188 FnoxError::AgeDecryptionFailed {
189 details: format!("Failed to create decryptor: {}", e),
190 }
191 })?;
192
193 let mut reader = decryptor
194 .decrypt(identities.iter().map(|i| i.as_ref() as &dyn age::Identity))
195 .map_err(|e| FnoxError::AgeDecryptionFailed {
196 details: e.to_string(),
197 })?;
198
199 let mut decrypted = vec![];
200 reader
201 .read_to_end(&mut decrypted)
202 .map_err(|e| FnoxError::AgeDecryptionFailed {
203 details: format!("Failed to read decrypted data: {}", e),
204 })?;
205
206 String::from_utf8(decrypted).map_err(|e| FnoxError::AgeDecryptionFailed {
207 details: format!("Failed to decode UTF-8: {}", e),
208 })
209 }
210}