1use std::io::{Read, Write};
33
34pub use age;
36
37use age::armor::{ArmoredReader, ArmoredWriter, Format};
38use zeroize::Zeroizing;
39
40use crate::keyring::{Keyring, KeyringError};
41pub use crate::members::Level;
42use crate::members::{KeyKind, MemberKey, Members};
43use crate::policy::{self, Refusal};
44
45#[derive(Debug)]
47pub enum WrapError {
48 NoMembers,
50 BadKey(String, String),
52 Age(String),
54}
55
56impl std::fmt::Display for WrapError {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 match self {
59 WrapError::NoMembers => write!(f, "the key ring needs at least one member key"),
60 WrapError::BadKey(name, why) => write!(f, "{name}'s key cannot be used: {why}"),
61 WrapError::Age(e) => write!(f, "age: {e}"),
62 }
63 }
64}
65impl std::error::Error for WrapError {}
66
67#[derive(Debug)]
69pub enum UnwrapError {
70 NotARecipient,
73 Malformed(String),
75 NotAKeyring(KeyringError),
78 Mismatch(Refusal),
82}
83
84impl std::fmt::Display for UnwrapError {
85 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86 match self {
87 UnwrapError::NotARecipient => write!(
88 f,
89 "this SSH key is not one the key ring is wrapped to — you are not a member \
90 of this encrypted repository (or were removed before this key ring)"
91 ),
92 UnwrapError::Malformed(e) => write!(f, "the key file is not a readable age file: {e}"),
93 UnwrapError::NotAKeyring(e) => write!(f, "the unwrapped payload is refused: {e}"),
94 UnwrapError::Mismatch(r) => write!(f, "the key ring is refused: {r}"),
95 }
96 }
97}
98impl std::error::Error for UnwrapError {}
99
100pub trait WrapScheme {
103 fn level(&self) -> Level;
105 fn recipient(&self, key: &MemberKey) -> Result<Box<dyn age::Recipient + Send>, String>;
108}
109
110#[derive(Clone, Copy, Debug)]
113pub struct SshClassic;
114
115impl WrapScheme for SshClassic {
116 fn level(&self) -> Level {
117 Level::Classic
118 }
119 fn recipient(&self, key: &MemberKey) -> Result<Box<dyn age::Recipient + Send>, String> {
120 match key.kind {
121 KeyKind::SshEd25519 | KeyKind::SshRsa => key
122 .key
123 .parse::<age::ssh::Recipient>()
124 .map(|r| Box::new(r) as Box<dyn age::Recipient + Send>)
125 .map_err(|e| format!("{e:?}")),
126 KeyKind::Age => key
127 .key
128 .trim()
129 .parse::<age::x25519::Recipient>()
130 .map(|r| Box::new(r) as Box<dyn age::Recipient + Send>)
131 .map_err(|e| e.to_string()),
132 KeyKind::AgePq => Err(
133 "an age1pq1… key is quantum-safe and this repository is classic; run \
134 `gunnar crypt upgrade-pq` to make it quantum-safe"
135 .to_string(),
136 ),
137 }
138 }
139}
140
141#[derive(Clone, Copy, Debug)]
144pub struct StandalonePq;
145
146pub const SSH_IS_NOT_PQ: &str = "SSH keys cannot be quantum-safe recipients: an ed25519 \
148 key's wrap is X25519 and an RSA key's is RSA-OAEP, and a quantum computer breaks both. \
149 Each member makes a separate quantum-safe key with `gunnar crypt keygen` (an age1pq1… \
150 key, ML-KEM-768 + X25519; its private half stays on their machine) and registers the \
151 public half with gunnar";
152
153impl WrapScheme for StandalonePq {
154 fn level(&self) -> Level {
155 Level::QuantumSafe
156 }
157 fn recipient(&self, key: &MemberKey) -> Result<Box<dyn age::Recipient + Send>, String> {
158 match key.kind {
159 KeyKind::AgePq => key
160 .key
161 .parse::<crate::pq::Recipient>()
162 .map(|r| Box::new(r) as Box<dyn age::Recipient + Send>)
163 .map_err(str::to_string),
164 KeyKind::SshEd25519 | KeyKind::SshRsa | KeyKind::Age => Err(SSH_IS_NOT_PQ.to_string()),
165 }
166 }
167}
168
169pub fn scheme(level: Level) -> &'static dyn WrapScheme {
171 match level {
172 Level::Classic => &SshClassic,
173 Level::QuantumSafe => &StandalonePq,
174 }
175}
176
177pub fn wrap(ring: &Keyring, members: &Members) -> Result<String, WrapError> {
180 if members.is_empty() {
181 return Err(WrapError::NoMembers);
182 }
183 let scheme = scheme(members.level());
184 let mut recipients = Vec::new();
185 for m in members.entries() {
186 recipients.push(
187 scheme
188 .recipient(m)
189 .map_err(|e| WrapError::BadKey(m.name.clone(), e))?,
190 );
191 }
192 let encryptor = age::Encryptor::with_recipients(
193 recipients.iter().map(|r| r.as_ref() as &dyn age::Recipient),
194 )
195 .map_err(|e| WrapError::Age(e.to_string()))?;
196 let mut out = Vec::new();
197 let armor = ArmoredWriter::wrap_output(&mut out, Format::AsciiArmor)
198 .map_err(|e| WrapError::Age(e.to_string()))?;
199 let mut w = encryptor
200 .wrap_output(armor)
201 .map_err(|e| WrapError::Age(e.to_string()))?;
202 w.write_all(ring.to_text().as_bytes())
203 .map_err(|e| WrapError::Age(e.to_string()))?;
204 w.finish()
205 .and_then(|a| a.finish())
206 .map_err(|e| WrapError::Age(e.to_string()))?;
207 String::from_utf8(out).map_err(|e| WrapError::Age(e.to_string()))
208}
209
210pub fn unwrap(keys_file: &[u8], identities: &[&dyn age::Identity]) -> Result<Keyring, UnwrapError> {
213 let decryptor = age::Decryptor::new_buffered(ArmoredReader::new(keys_file))
220 .map_err(|e| UnwrapError::Malformed(e.to_string()))?;
221 let mut reader = match decryptor.decrypt(identities.iter().copied()) {
222 Ok(r) => r,
223 Err(age::DecryptError::NoMatchingKeys) => return Err(UnwrapError::NotARecipient),
224 Err(e) => return Err(UnwrapError::Malformed(e.to_string())),
225 };
226 let mut buf = Zeroizing::new(Vec::new());
227 reader
228 .read_to_end(&mut buf)
229 .map_err(|e| UnwrapError::Malformed(e.to_string()))?;
230 let text = std::str::from_utf8(&buf)
231 .map_err(|_| UnwrapError::NotAKeyring(KeyringError::NotAKeyring))?;
232 Keyring::parse(text).map_err(UnwrapError::NotAKeyring)
233}
234
235pub fn unwrap_checked(
241 keys_file: &[u8],
242 members: &str,
243 identities: &[&dyn age::Identity],
244) -> Result<Keyring, UnwrapError> {
245 policy::check_ring(members, keys_file).map_err(UnwrapError::Mismatch)?;
246 unwrap(keys_file, identities)
247}
248
249pub fn pq_identity(text: &str) -> Result<Box<dyn age::Identity + Send + Sync>, IdentityError> {
252 crate::pq::parse_identity_file(text)
253 .map(|id| Box::new(id) as Box<dyn age::Identity + Send + Sync>)
254 .map_err(|e| IdentityError::Read(e.to_string()))
255}
256
257pub trait Passphrase: Clone + Send + Sync + 'static {
259 fn ask(&self, description: &str) -> Option<String>;
261}
262
263#[derive(Clone)]
264struct Callbacks<P: Passphrase>(P);
265
266impl<P: Passphrase> age::Callbacks for Callbacks<P> {
267 fn display_message(&self, message: &str) {
268 eprintln!("{message}");
269 }
270 fn confirm(&self, _: &str, _: &str, _: Option<&str>) -> Option<bool> {
271 None
272 }
273 fn request_public_string(&self, _: &str) -> Option<String> {
274 None
275 }
276 fn request_passphrase(&self, description: &str) -> Option<age::secrecy::SecretString> {
277 self.0
278 .ask(description)
279 .map(age::secrecy::SecretString::from)
280 }
281}
282
283#[derive(Debug)]
285pub enum IdentityError {
286 Read(String),
288 Unsupported(String),
291}
292
293impl std::fmt::Display for IdentityError {
294 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
295 match self {
296 IdentityError::Read(e) => write!(f, "{e}"),
297 IdentityError::Unsupported(e) => write!(
298 f,
299 "{e}. A security-key (sk-) SSH key can only sign and cannot decrypt; \
300 use an ordinary ed25519 key"
301 ),
302 }
303 }
304}
305impl std::error::Error for IdentityError {}
306
307pub fn ssh_identity<P: Passphrase>(
310 pem: &[u8],
311 filename: Option<String>,
312 passphrase: P,
313) -> Result<Box<dyn age::Identity>, IdentityError> {
314 let id = age::ssh::Identity::from_buffer(pem, filename)
315 .map_err(|e| IdentityError::Read(e.to_string()))?;
316 if let age::ssh::Identity::Unsupported(u) = &id {
317 return Err(IdentityError::Unsupported(format!("{u:?}")));
318 }
319 Ok(Box::new(id.with_callbacks(Callbacks(passphrase))))
320}
321
322pub fn ssh_identity_unlocked(
327 pem: &[u8],
328 passphrase: impl Fn(&str) -> Option<String>,
329) -> Result<Box<dyn age::Identity + Send + Sync>, IdentityError> {
330 let id = age::ssh::Identity::from_buffer(pem, None)
331 .map_err(|e| IdentityError::Read(e.to_string()))?;
332 match id {
333 age::ssh::Identity::Unencrypted(_) => Ok(Box::new(id)),
334 age::ssh::Identity::Encrypted(enc) => {
335 let pass = passphrase("Passphrase for the SSH key")
336 .ok_or_else(|| IdentityError::Read("no passphrase given".into()))?;
337 let key = enc
338 .decrypt(age::secrecy::SecretString::from(pass))
339 .map_err(|_| IdentityError::Read("wrong passphrase".into()))?;
340 Ok(Box::new(age::ssh::Identity::from(key)))
341 }
342 age::ssh::Identity::Unsupported(u) => Err(IdentityError::Unsupported(format!("{u:?}"))),
343 }
344}
345
346#[derive(Clone, Copy, Debug)]
348pub struct NoPassphrase;
349
350impl Passphrase for NoPassphrase {
351 fn ask(&self, _: &str) -> Option<String> {
352 None
353 }
354}