1use std::io::{Read, Write};
21
22pub use age;
24
25use age::armor::{ArmoredReader, ArmoredWriter, Format};
26use zeroize::Zeroizing;
27
28use crate::keyring::{Keyring, KeyringError};
29pub use crate::members::Level;
30use crate::members::{KeyKind, MemberKey, Members};
31use crate::policy::{self, Refusal};
32
33#[derive(Debug)]
35pub enum WrapError {
36 NoMembers,
38 BadKey(String, String),
40 Age(String),
42}
43
44impl std::fmt::Display for WrapError {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 match self {
47 WrapError::NoMembers => write!(f, "the key ring needs at least one member key"),
48 WrapError::BadKey(name, why) => write!(f, "{name}'s key cannot be used: {why}"),
49 WrapError::Age(e) => write!(f, "age: {e}"),
50 }
51 }
52}
53impl std::error::Error for WrapError {}
54
55#[derive(Debug)]
57pub enum UnwrapError {
58 NotARecipient,
61 Malformed(String),
63 NotAKeyring(KeyringError),
65 Mismatch(Refusal),
69}
70
71impl std::fmt::Display for UnwrapError {
72 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73 match self {
74 UnwrapError::NotARecipient => write!(
75 f,
76 "your encryption key is not one this key ring is wrapped to: you are not a member \
77 of this encrypted repository (or were removed before this key ring)"
78 ),
79 UnwrapError::Malformed(e) => write!(f, "the key file is not a readable age file: {e}"),
80 UnwrapError::NotAKeyring(e) => write!(f, "the unwrapped payload is refused: {e}"),
81 UnwrapError::Mismatch(r) => write!(f, "the key ring is refused: {r}"),
82 }
83 }
84}
85impl std::error::Error for UnwrapError {}
86
87pub const SSH_IS_NOT_PQ: &str = "SSH keys cannot be quantum-safe recipients: an ed25519 \
89 key's wrap is X25519 and an RSA key's is RSA-OAEP, and a quantum computer breaks both. \
90 A quantum-safe repository is wrapped to its members' quantum-safe encryption keys \
91 (age1pq1…, ML-KEM-768 + X25519), which their SSH keys unlock";
92
93pub fn recipient(key: &MemberKey) -> Result<Box<dyn age::Recipient + Send>, String> {
95 match key.kind {
96 KeyKind::Age => key
97 .key
98 .trim()
99 .parse::<age::x25519::Recipient>()
100 .map(|r| Box::new(r) as Box<dyn age::Recipient + Send>)
101 .map_err(|e| e.to_string()),
102 #[cfg(feature = "pq")]
103 KeyKind::AgePq => key
104 .key
105 .parse::<crate::pq::Recipient>()
106 .map(|r| Box::new(r) as Box<dyn age::Recipient + Send>)
107 .map_err(str::to_string),
108 #[cfg(not(feature = "pq"))]
109 KeyKind::AgePq => {
110 Err("a quantum-safe key ring needs gunnar-crypt's `pq` feature".to_string())
111 }
112 KeyKind::SshEd25519 | KeyKind::SshRsa => Err(
113 "an SSH key is an unlock method of its owner's encryption key, never a member key"
114 .to_string(),
115 ),
116 }
117}
118
119pub fn wrap(ring: &Keyring, members: &Members) -> Result<String, WrapError> {
122 if members.is_empty() {
123 return Err(WrapError::NoMembers);
124 }
125 let mut recipients = Vec::new();
126 for m in members.entries() {
127 recipients.push(recipient(m).map_err(|e| WrapError::BadKey(m.name.clone(), e))?);
128 }
129 let encryptor = age::Encryptor::with_recipients(
130 recipients.iter().map(|r| r.as_ref() as &dyn age::Recipient),
131 )
132 .map_err(|e| WrapError::Age(e.to_string()))?;
133 let mut out = Vec::new();
134 let armor = ArmoredWriter::wrap_output(&mut out, Format::AsciiArmor)
135 .map_err(|e| WrapError::Age(e.to_string()))?;
136 let mut w = encryptor
137 .wrap_output(armor)
138 .map_err(|e| WrapError::Age(e.to_string()))?;
139 w.write_all(ring.to_text().as_bytes())
140 .map_err(|e| WrapError::Age(e.to_string()))?;
141 w.finish()
142 .and_then(|a| a.finish())
143 .map_err(|e| WrapError::Age(e.to_string()))?;
144 String::from_utf8(out).map_err(|e| WrapError::Age(e.to_string()))
145}
146
147pub fn unwrap(keys_file: &[u8], identities: &[&dyn age::Identity]) -> Result<Keyring, UnwrapError> {
150 let decryptor = age::Decryptor::new_buffered(ArmoredReader::new(keys_file))
157 .map_err(|e| UnwrapError::Malformed(e.to_string()))?;
158 let mut reader = match decryptor.decrypt(identities.iter().copied()) {
159 Ok(r) => r,
160 Err(age::DecryptError::NoMatchingKeys) => return Err(UnwrapError::NotARecipient),
161 Err(e) => return Err(UnwrapError::Malformed(e.to_string())),
162 };
163 let mut buf = Zeroizing::new(Vec::new());
164 reader
165 .read_to_end(&mut buf)
166 .map_err(|e| UnwrapError::Malformed(e.to_string()))?;
167 let text = std::str::from_utf8(&buf)
168 .map_err(|_| UnwrapError::NotAKeyring(KeyringError::NotAKeyring))?;
169 Keyring::parse(text).map_err(UnwrapError::NotAKeyring)
170}
171
172pub fn unwrap_checked(
178 keys_file: &[u8],
179 members: &str,
180 identities: &[&dyn age::Identity],
181) -> Result<Keyring, UnwrapError> {
182 policy::check_ring(members, keys_file).map_err(UnwrapError::Mismatch)?;
183 unwrap(keys_file, identities)
184}
185
186#[cfg(feature = "pq")]
189pub fn pq_identity(text: &str) -> Result<Box<dyn age::Identity + Send + Sync>, IdentityError> {
190 crate::pq::parse_identity_file(text)
191 .map(|id| Box::new(id) as Box<dyn age::Identity + Send + Sync>)
192 .map_err(|e| IdentityError::Read(e.to_string()))
193}
194
195#[derive(Debug)]
197pub enum IdentityError {
198 Read(String),
200 Unsupported(String),
203}
204
205impl std::fmt::Display for IdentityError {
206 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
207 match self {
208 IdentityError::Read(e) => write!(f, "{e}"),
209 IdentityError::Unsupported(e) => write!(
210 f,
211 "{e}. A security-key (sk-) SSH key can only sign and cannot decrypt; \
212 use an ordinary ed25519 key"
213 ),
214 }
215 }
216}
217impl std::error::Error for IdentityError {}
218
219#[cfg(feature = "wrap")]
220pub use ssh::*;
221
222#[cfg(feature = "wrap")]
223mod ssh {
224 use super::*;
225
226 pub trait Passphrase: Clone + Send + Sync + 'static {
228 fn ask(&self, description: &str) -> Option<String>;
230 }
231
232 #[derive(Clone)]
233 struct Callbacks<P: Passphrase>(P);
234
235 impl<P: Passphrase> age::Callbacks for Callbacks<P> {
236 fn display_message(&self, message: &str) {
237 eprintln!("{message}");
238 }
239 fn confirm(&self, _: &str, _: &str, _: Option<&str>) -> Option<bool> {
240 None
241 }
242 fn request_public_string(&self, _: &str) -> Option<String> {
243 None
244 }
245 fn request_passphrase(&self, description: &str) -> Option<age::secrecy::SecretString> {
246 self.0
247 .ask(description)
248 .map(age::secrecy::SecretString::from)
249 }
250 }
251
252 pub fn ssh_identity<P: Passphrase>(
255 pem: &[u8],
256 filename: Option<String>,
257 passphrase: P,
258 ) -> Result<Box<dyn age::Identity>, IdentityError> {
259 let id = age::ssh::Identity::from_buffer(pem, filename)
260 .map_err(|e| IdentityError::Read(e.to_string()))?;
261 if let age::ssh::Identity::Unsupported(u) = &id {
262 return Err(IdentityError::Unsupported(format!("{u:?}")));
263 }
264 Ok(Box::new(id.with_callbacks(Callbacks(passphrase))))
265 }
266
267 pub fn ssh_identity_unlocked(
271 pem: &[u8],
272 passphrase: impl Fn(&str) -> Option<String>,
273 ) -> Result<Box<dyn age::Identity + Send + Sync>, IdentityError> {
274 let id = age::ssh::Identity::from_buffer(pem, None)
275 .map_err(|e| IdentityError::Read(e.to_string()))?;
276 match id {
277 age::ssh::Identity::Unencrypted(_) => Ok(Box::new(id)),
278 age::ssh::Identity::Encrypted(enc) => {
279 let pass = passphrase("Passphrase for the SSH key")
280 .ok_or_else(|| IdentityError::Read("no passphrase given".into()))?;
281 let key = enc
282 .decrypt(age::secrecy::SecretString::from(pass))
283 .map_err(|_| IdentityError::Read("wrong passphrase".into()))?;
284 Ok(Box::new(age::ssh::Identity::from(key)))
285 }
286 age::ssh::Identity::Unsupported(u) => Err(IdentityError::Unsupported(format!("{u:?}"))),
287 }
288 }
289
290 #[derive(Clone, Copy, Debug)]
292 pub struct NoPassphrase;
293
294 impl Passphrase for NoPassphrase {
295 fn ask(&self, _: &str) -> Option<String> {
296 None
297 }
298 }
299}