1use std::collections::HashSet;
2use std::fmt;
3
4use age_core::{
5 format::{FileKey, Stanza},
6 primitives::{aead_encrypt, hkdf},
7 secrecy::ExposeSecret,
8};
9use base64::{
10 prelude::{BASE64_STANDARD, BASE64_STANDARD_NO_PAD},
11 Engine,
12};
13use curve25519_dalek::edwards::EdwardsPoint;
14use nom::{
15 branch::alt,
16 bytes::streaming::{is_not, tag},
17 combinator::map_opt,
18 sequence::{pair, preceded, separated_pair},
19 IResult, Parser,
20};
21use rand::rngs::OsRng;
22use rsa::{traits::PublicKeyParts, Oaep};
23use sha2::Sha256;
24use x25519_dalek::{EphemeralSecret, PublicKey as X25519PublicKey, StaticSecret};
25
26use super::{
27 identity::{Identity, UnencryptedKey},
28 read_ssh, ssh_tag, EncryptedKey, UnsupportedKey, SSH_ED25519_KEY_PREFIX,
29 SSH_ED25519_RECIPIENT_KEY_LABEL, SSH_ED25519_RECIPIENT_TAG, SSH_RSA_KEY_PREFIX,
30 SSH_RSA_OAEP_LABEL, SSH_RSA_RECIPIENT_TAG,
31};
32use crate::{
33 error::EncryptError,
34 util::read::{encoded_str, str_while_encoded},
35};
36
37#[derive(Clone, Debug)]
39pub enum Recipient {
40 SshRsa(Vec<u8>, rsa::RsaPublicKey),
42 SshEd25519(Vec<u8>, EdwardsPoint),
44}
45
46pub(crate) enum ParsedRecipient {
47 Supported(Recipient),
48 RsaModulusTooLarge,
49 RsaModulusTooSmall,
50 Unsupported(String),
51}
52
53#[derive(Debug, PartialEq, Eq)]
55#[non_exhaustive]
56pub enum ParseRecipientKeyError {
57 Ignore,
61 Invalid(&'static str),
63 RsaModulusTooLarge,
65 RsaModulusTooSmall,
67 Unsupported(String),
69}
70
71impl std::str::FromStr for Recipient {
72 type Err = ParseRecipientKeyError;
73
74 fn from_str(s: &str) -> Result<Self, Self::Err> {
76 match ssh_recipient(rsa::RsaPublicKey::MAX_SIZE)(s) {
77 Ok((_, ParsedRecipient::Supported(pk))) => Ok(pk),
78 Ok((_, ParsedRecipient::RsaModulusTooLarge)) => {
79 Err(ParseRecipientKeyError::RsaModulusTooLarge)
80 }
81 Ok((_, ParsedRecipient::RsaModulusTooSmall)) => {
82 Err(ParseRecipientKeyError::RsaModulusTooSmall)
83 }
84 Ok((_, ParsedRecipient::Unsupported(key_type))) => {
85 Err(ParseRecipientKeyError::Unsupported(key_type))
86 }
87 _ => Err(ParseRecipientKeyError::Invalid("invalid SSH recipient")),
88 }
89 }
90}
91
92impl fmt::Display for Recipient {
93 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
94 match self {
95 Recipient::SshRsa(ssh_key, _) => {
96 write!(
97 f,
98 "{} {}",
99 SSH_RSA_KEY_PREFIX,
100 BASE64_STANDARD.encode(ssh_key)
101 )
102 }
103 Recipient::SshEd25519(ssh_key, _) => {
104 write!(
105 f,
106 "{} {}",
107 SSH_ED25519_KEY_PREFIX,
108 BASE64_STANDARD.encode(ssh_key)
109 )
110 }
111 }
112 }
113}
114
115impl TryFrom<Identity> for Recipient {
116 type Error = ParseRecipientKeyError;
117
118 fn try_from(identity: Identity) -> Result<Self, Self::Error> {
119 match identity {
120 Identity::Unencrypted(UnencryptedKey::SshRsa(ssh_key, _))
121 | Identity::Unencrypted(UnencryptedKey::SshEd25519(ssh_key, _))
122 | Identity::Encrypted(EncryptedKey { ssh_key, .. }) => {
123 if let Ok((_, pk)) = read_ssh::rsa_pubkey(rsa::RsaPublicKey::MAX_SIZE)(&ssh_key) {
124 if let Some(pk) = pk {
125 Ok(Recipient::SshRsa(ssh_key, pk))
126 } else {
127 Err(ParseRecipientKeyError::RsaModulusTooLarge)
128 }
129 } else if let Ok((_, pk)) = read_ssh::ed25519_pubkey(&ssh_key) {
130 Ok(Recipient::SshEd25519(ssh_key, pk))
131 } else if let Ok((_, key_type)) = read_ssh::string(&ssh_key) {
132 Err(ParseRecipientKeyError::Unsupported(
133 String::from_utf8_lossy(key_type).to_string(),
134 ))
135 } else {
136 Err(ParseRecipientKeyError::Invalid(
137 "Invalid SSH pubkey in SSH privkey",
138 ))
139 }
140 }
141 Identity::Unsupported(
142 UnsupportedKey::Hardware(key_type) | UnsupportedKey::Type(key_type),
143 ) => Err(ParseRecipientKeyError::Unsupported(key_type)),
144 Identity::Unsupported(_) => Err(ParseRecipientKeyError::Ignore),
145 }
146 }
147}
148
149impl crate::Recipient for Recipient {
150 fn wrap_file_key(
151 &self,
152 file_key: &FileKey,
153 ) -> Result<(Vec<Stanza>, HashSet<String>), EncryptError> {
154 let mut rng = OsRng;
155
156 let stanzas = match self {
157 Recipient::SshRsa(ssh_key, pk) => {
158 let encrypted_file_key = pk
159 .encrypt(
160 &mut rng,
161 Oaep::new_with_label::<Sha256, _>(SSH_RSA_OAEP_LABEL),
162 file_key.expose_secret(),
163 )
164 .expect("pubkey is valid and file key is not too long");
165
166 let encoded_tag = BASE64_STANDARD_NO_PAD.encode(ssh_tag(ssh_key));
167
168 vec![Stanza {
169 tag: SSH_RSA_RECIPIENT_TAG.to_owned(),
170 args: vec![encoded_tag],
171 body: encrypted_file_key,
172 }]
173 }
174 Recipient::SshEd25519(ssh_key, ed25519_pk) => {
175 let pk: X25519PublicKey = ed25519_pk.to_montgomery().to_bytes().into();
176
177 let esk = EphemeralSecret::random_from_rng(rng);
178 let epk: X25519PublicKey = (&esk).into();
179
180 let tweak: StaticSecret =
181 hkdf(ssh_key, SSH_ED25519_RECIPIENT_KEY_LABEL, &[]).into();
182 let shared_secret =
183 tweak.diffie_hellman(&(*esk.diffie_hellman(&pk).as_bytes()).into());
184
185 let mut salt = [0; 64];
186 salt[..32].copy_from_slice(epk.as_bytes());
187 salt[32..].copy_from_slice(pk.as_bytes());
188
189 let enc_key = hkdf(
190 &salt,
191 SSH_ED25519_RECIPIENT_KEY_LABEL,
192 shared_secret.as_bytes(),
193 );
194 let encrypted_file_key = aead_encrypt(&enc_key, file_key.expose_secret());
195
196 let encoded_tag = BASE64_STANDARD_NO_PAD.encode(ssh_tag(ssh_key));
197 let encoded_epk = BASE64_STANDARD_NO_PAD.encode(epk.as_bytes());
198
199 vec![Stanza {
200 tag: SSH_ED25519_RECIPIENT_TAG.to_owned(),
201 args: vec![encoded_tag, encoded_epk],
202 body: encrypted_file_key,
203 }]
204 }
205 };
206
207 Ok((stanzas, HashSet::new()))
208 }
209}
210
211fn ssh_rsa_pubkey(max_size: usize) -> impl Fn(&str) -> IResult<&str, ParsedRecipient> {
212 move |input: &str| {
213 preceded(
214 pair(tag(SSH_RSA_KEY_PREFIX), tag(" ")),
215 map_opt(
216 str_while_encoded(BASE64_STANDARD_NO_PAD),
217 |ssh_key| match read_ssh::rsa_pubkey(max_size)(&ssh_key) {
218 Ok((_, Some(pk))) => Some(if pk.n().bits() < 2048 {
219 ParsedRecipient::RsaModulusTooSmall
220 } else {
221 ParsedRecipient::Supported(Recipient::SshRsa(ssh_key, pk))
222 }),
223 Ok((_, None)) => Some(ParsedRecipient::RsaModulusTooLarge),
224 Err(_) => None,
225 },
226 ),
227 )
228 .parse(input)
229 }
230}
231
232fn ssh_ed25519_pubkey(input: &str) -> IResult<&str, ParsedRecipient> {
233 preceded(
234 pair(tag(SSH_ED25519_KEY_PREFIX), tag(" ")),
235 map_opt(
236 encoded_str(51, BASE64_STANDARD_NO_PAD),
237 |ssh_key| match read_ssh::ed25519_pubkey(&ssh_key) {
238 Ok((_, pk)) => Some(ParsedRecipient::Supported(Recipient::SshEd25519(
239 ssh_key, pk,
240 ))),
241 Err(_) => None,
242 },
243 ),
244 )
245 .parse(input)
246}
247
248fn ssh_ignore_pubkey(input: &str) -> IResult<&str, ParsedRecipient> {
249 map_opt(
252 separated_pair(
253 is_not(" "),
254 tag(" "),
255 str_while_encoded(BASE64_STANDARD_NO_PAD),
256 ),
257 |(key_type, ssh_key)| {
258 read_ssh::string_tag(key_type)(&ssh_key)
259 .map(|_| ParsedRecipient::Unsupported(key_type.to_string()))
260 .ok()
261 },
262 )
263 .parse(input)
264}
265
266pub(crate) fn ssh_recipient(max_size: usize) -> impl Fn(&str) -> IResult<&str, ParsedRecipient> {
267 move |input| {
268 alt((
269 ssh_rsa_pubkey(max_size),
270 ssh_ed25519_pubkey,
271 ssh_ignore_pubkey,
272 ))
273 .parse(input)
274 }
275}
276
277#[cfg(test)]
278pub(crate) mod tests {
279 use super::{ParseRecipientKeyError, Recipient};
280
281 pub(crate) const TEST_SSH_RSA_PK: &str = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDE7nIXTGNuaRBN9toI/wNALuQec8mvlt0iJ7o3OaD2UvoKHJ7S8rmIn4FiQDUed/Vac3OhUibei1k+TBmm16u2Rj3klgWZOIDgi8d4vXKI5N3YBhxr3jsQ+kz1c+iZ4z/tTtz306+4K46XViVMWwyyg9j82Jn41mOAy9vdeDIfQ5fLeaGqn5KwlT61GNkZ+ozWK/ZNlQIlNCcoXxhJULIs9XrtczWyVBAea1nlDo0WHODePxoJjmsNHrpQXn5mf9O83xs10qfTUjnRUt48jRmedFy4tcra3QGmSTQ3KZne+wXXSb0cIpXLGvZjQSPHgG1hc4r3uBpiSzvesGLv79XL alice@rust";
282 pub(crate) const TEST_SSH_ED25519_PK: &str = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHsKLqeplhpW+uObz5dvMgjz1OxfM/XXUB+VHtZ6isGN alice@rust";
283 const TEST_SSH_UNSUPPORTED_PK: &str = "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBHFliOyIZs1gxGF3fmDxFykQhE88wy6AKDGFBfn0R6ZuvRmENABZQa9+pj9hMki+LX0qDJbmHTiWDbYv/cmFt/Q=";
284 const TEST_SSH_INVALID_PK: &str = "ecdsa-sha2-nistp256 AAAAC3NzaC1lZDI1NTE5AAAAIHsKLqeplhpW+uObz5dvMgjz1OxfM/XXUB+VHtZ6isGN alice@rust";
285
286 #[test]
287 fn ssh_rsa_encoding() {
288 let pk: Recipient = TEST_SSH_RSA_PK.parse().unwrap();
289 assert_eq!(pk.to_string() + " alice@rust", TEST_SSH_RSA_PK);
290 }
291
292 #[test]
293 fn ssh_ed25519_encoding() {
294 let pk: Recipient = TEST_SSH_ED25519_PK.parse().unwrap();
295 assert_eq!(pk.to_string() + " alice@rust", TEST_SSH_ED25519_PK);
296 }
297
298 #[test]
299 fn ssh_unsupported_key_type() {
300 let pk: Result<Recipient, ParseRecipientKeyError> = TEST_SSH_UNSUPPORTED_PK.parse();
301 assert_eq!(
302 pk.unwrap_err(),
303 ParseRecipientKeyError::Unsupported("ecdsa-sha2-nistp256".to_string()),
304 );
305 }
306
307 #[test]
308 fn ssh_invalid_encoding() {
309 let pk: Result<Recipient, ParseRecipientKeyError> = TEST_SSH_INVALID_PK.parse();
310 assert_eq!(
311 pk.unwrap_err(),
312 ParseRecipientKeyError::Invalid("invalid SSH recipient")
313 );
314 }
315}