1use age_core::{
2 format::{FileKey, Stanza, FILE_KEY_BYTES},
3 primitives::{aead_decrypt, hkdf},
4 secrecy::{ExposeSecret, SecretBox},
5};
6use base64::prelude::BASE64_STANDARD;
7use nom::{
8 branch::alt,
9 bytes::streaming::{is_not, tag},
10 character::streaming::{line_ending, newline},
11 combinator::{map_opt, opt},
12 sequence::{pair, preceded, terminated, tuple},
13 IResult,
14};
15use rand::rngs::OsRng;
16use rsa::{pkcs1::DecodeRsaPrivateKey, Oaep};
17use sha2::{Digest, Sha256, Sha512};
18use std::fmt;
19use std::io;
20use x25519_dalek::{PublicKey as X25519PublicKey, StaticSecret};
21use zeroize::Zeroize;
22
23use super::{
24 read_ssh, ssh_tag, write_ssh, EncryptedKey, SSH_ED25519_RECIPIENT_KEY_LABEL,
25 SSH_ED25519_RECIPIENT_TAG, SSH_RSA_OAEP_LABEL, SSH_RSA_RECIPIENT_TAG, TAG_LEN_BYTES,
26};
27use crate::{
28 error::DecryptError,
29 fl,
30 util::read::{base64_arg, wrapped_str_while_encoded},
31 wfl, wlnfl, Callbacks,
32};
33
34pub enum UnencryptedKey {
36 SshRsa(Vec<u8>, Box<rsa::RsaPrivateKey>),
38 SshEd25519(Vec<u8>, SecretBox<[u8; 64]>),
40}
41
42impl Clone for UnencryptedKey {
43 fn clone(&self) -> Self {
44 match self {
45 Self::SshRsa(ssh_key, sk) => Self::SshRsa(ssh_key.clone(), sk.clone()),
46 Self::SshEd25519(ssh_key, privkey) => Self::SshEd25519(
47 ssh_key.clone(),
48 SecretBox::new({
49 let mut cloned_privkey = Box::new([0; 64]);
50 cloned_privkey.copy_from_slice(privkey.expose_secret());
51 cloned_privkey
52 }),
53 ),
54 }
55 }
56}
57
58impl UnencryptedKey {
59 pub(crate) fn unwrap_stanza(&self, stanza: &Stanza) -> Option<Result<FileKey, DecryptError>> {
64 match (self, stanza.tag.as_str()) {
65 (UnencryptedKey::SshRsa(ssh_key, sk), SSH_RSA_RECIPIENT_TAG) => {
66 let tag = base64_arg::<_, TAG_LEN_BYTES, 6>(stanza.args.get(0)?)?;
67 if ssh_tag(ssh_key) != tag {
68 return None;
69 }
70
71 let mut rng = OsRng;
72
73 Some(
76 sk.decrypt_blinded(
77 &mut rng,
78 Oaep::new_with_label::<Sha256, _>(SSH_RSA_OAEP_LABEL),
79 &stanza.body,
80 )
81 .map_err(DecryptError::from)
82 .and_then(|mut pt| {
83 FileKey::try_init_with_mut(|file_key| {
85 let ret = if pt.len() == file_key.len() {
86 file_key.copy_from_slice(&pt);
87 Ok(())
88 } else {
89 Err(DecryptError::DecryptionFailed)
90 };
91 pt.zeroize();
92 ret
93 })
94 }),
95 )
96 }
97 (UnencryptedKey::SshEd25519(ssh_key, privkey), SSH_ED25519_RECIPIENT_TAG) => {
98 let tag = base64_arg::<_, TAG_LEN_BYTES, 6>(stanza.args.get(0)?)?;
99 if ssh_tag(ssh_key) != tag {
100 return None;
101 }
102 if stanza.body.len() != crate::x25519::ENCRYPTED_FILE_KEY_BYTES {
103 return Some(Err(DecryptError::InvalidHeader));
104 }
105
106 let epk =
107 base64_arg::<_, { crate::x25519::EPK_LEN_BYTES }, 33>(stanza.args.get(1)?)?
108 .into();
109
110 let sk: StaticSecret = {
111 let mut sk = [0; 32];
112 sk.copy_from_slice(&Sha512::digest(&privkey.expose_secret()[0..32])[0..32]);
114 sk.into()
115 };
116 let pk = X25519PublicKey::from(&sk);
117
118 let tweak: StaticSecret =
119 hkdf(ssh_key, SSH_ED25519_RECIPIENT_KEY_LABEL, &[]).into();
120 let shared_secret = tweak
121 .diffie_hellman(&X25519PublicKey::from(*sk.diffie_hellman(&epk).as_bytes()));
122
123 let mut salt = [0; 64];
124 salt[..32].copy_from_slice(epk.as_bytes());
125 salt[32..].copy_from_slice(pk.as_bytes());
126
127 let enc_key = hkdf(
128 &salt,
129 SSH_ED25519_RECIPIENT_KEY_LABEL,
130 shared_secret.as_bytes(),
131 );
132
133 Some(
136 aead_decrypt(&enc_key, FILE_KEY_BYTES, &stanza.body)
137 .map_err(DecryptError::from)
138 .map(|mut pt| {
139 FileKey::init_with_mut(|file_key| {
141 file_key.copy_from_slice(&pt);
142 pt.zeroize();
143 })
144 }),
145 )
146 }
147 _ => None,
148 }
149 }
150}
151
152#[derive(Clone, Debug, PartialEq, Eq)]
157pub enum UnsupportedKey {
158 EncryptedPem,
160 EncryptedSsh(String),
162 Hardware(String),
164 Type(String),
166}
167
168impl UnsupportedKey {
169 pub(crate) fn from_key_type(key_type: String) -> Self {
170 if key_type.starts_with("sk-ssh-") {
171 Self::Hardware(key_type)
172 } else {
173 Self::Type(key_type)
174 }
175 }
176
177 pub fn display(&self, f: &mut fmt::Formatter, filename: Option<&str>) -> fmt::Result {
179 if let Some(name) = filename {
180 wlnfl!(f, "ssh-unsupported-key", name = name)?;
181 writeln!(f)?;
182 }
183 match self {
184 UnsupportedKey::EncryptedPem => wfl!(
185 f,
186 "ssh-insecure-key-format",
187 change_passphrase = "ssh-keygen -o -p",
188 gen_new = "ssh-keygen -o",
189 )?,
190 UnsupportedKey::EncryptedSsh(cipher) => {
191 let new_issue = format!(
192 "https://github.com/str4d/rage/issues/new?title=Support%20OpenSSH%20key%20encryption%20cipher%20{}",
193 cipher,
194 );
195 wfl!(
196 f,
197 "ssh-unsupported-cipher",
198 cipher = cipher.as_str(),
199 new_issue = new_issue.as_str(),
200 )?;
201 }
202 UnsupportedKey::Hardware(key_type) => wfl!(
203 f,
204 "ssh-unsupported-security-key",
205 key_type = key_type.as_str(),
206 age_plugin_yubikey_url = "https://str4d.xyz/age-plugin-yubikey",
207 )?,
208 UnsupportedKey::Type(key_type) => {
209 wfl!(f, "ssh-unsupported-key-type", key_type = key_type.as_str())?
210 }
211 }
212 Ok(())
213 }
214}
215
216#[derive(Clone)]
218pub enum Identity {
219 Unencrypted(UnencryptedKey),
221 Encrypted(EncryptedKey),
223 Unsupported(UnsupportedKey),
225}
226
227impl From<UnencryptedKey> for Identity {
228 fn from(key: UnencryptedKey) -> Self {
229 Identity::Unencrypted(key)
230 }
231}
232
233impl From<EncryptedKey> for Identity {
234 fn from(key: EncryptedKey) -> Self {
235 Identity::Encrypted(key)
236 }
237}
238
239impl From<UnsupportedKey> for Identity {
240 fn from(key: UnsupportedKey) -> Self {
241 Identity::Unsupported(key)
242 }
243}
244
245impl Identity {
246 pub fn from_buffer<R: io::BufRead>(mut data: R, filename: Option<String>) -> io::Result<Self> {
250 let mut buf = String::new();
251 loop {
252 match ssh_identity(&buf) {
253 Ok((_, mut identity)) => {
254 if let Identity::Encrypted(key) = &mut identity {
256 key.filename = filename;
257 }
258
259 break Ok(identity);
260 }
261 Err(nom::Err::Incomplete(nom::Needed::Size(_))) => {
262 if data.read_line(&mut buf)? == 0 {
263 break Err(io::Error::new(
264 io::ErrorKind::Interrupted,
265 "incomplete SSH identity in file",
266 ));
267 };
268 }
269 Err(_) => {
270 break Err(io::Error::new(
271 io::ErrorKind::InvalidData,
272 "invalid SSH identity",
273 ));
274 }
275 }
276 }
277 }
278
279 pub fn with_callbacks<C: Callbacks>(self, callbacks: C) -> impl crate::Identity {
282 DecryptableIdentity {
283 identity: self,
284 callbacks,
285 }
286 }
287}
288
289impl crate::Identity for Identity {
290 fn unwrap_stanza(&self, stanza: &Stanza) -> Option<Result<FileKey, DecryptError>> {
291 match self {
292 Identity::Unencrypted(key) => key.unwrap_stanza(stanza),
293 Identity::Encrypted(_) | Identity::Unsupported(_) => None,
294 }
295 }
296}
297
298struct DecryptableIdentity<C: Callbacks> {
299 identity: Identity,
300 callbacks: C,
301}
302
303impl<C: Callbacks> crate::Identity for DecryptableIdentity<C> {
304 fn unwrap_stanza(&self, stanza: &Stanza) -> Option<Result<FileKey, DecryptError>> {
305 match &self.identity {
306 Identity::Unencrypted(key) => key.unwrap_stanza(stanza),
307 Identity::Encrypted(enc) => {
308 let passphrase = self.callbacks.request_passphrase(&fl!(
309 "ssh-passphrase-prompt",
310 filename = enc.filename.as_deref().unwrap_or_default()
311 ))?;
312 let decrypted = match enc.decrypt(passphrase) {
313 Ok(d) => d,
314 Err(e) => return Some(Err(e)),
315 };
316 decrypted.unwrap_stanza(stanza)
317 }
318 Identity::Unsupported(_) => None,
319 }
320 }
321}
322
323fn rsa_pem_encryption_header(input: &str) -> IResult<&str, &str> {
324 preceded(
325 tuple((tag("Proc-Type: 4,ENCRYPTED"), newline, tag("DEK-Info: "))),
326 terminated(is_not("\n"), newline),
327 )(input)
328}
329
330fn rsa_privkey(input: &str) -> IResult<&str, Identity> {
331 preceded(
332 pair(tag("-----BEGIN RSA PRIVATE KEY-----"), line_ending),
333 terminated(
334 map_opt(
335 pair(
336 opt(terminated(rsa_pem_encryption_header, line_ending)),
337 wrapped_str_while_encoded(BASE64_STANDARD),
338 ),
339 |(enc_header, privkey)| {
340 if enc_header.is_some() {
341 Some(UnsupportedKey::EncryptedPem.into())
342 } else {
343 rsa::RsaPrivateKey::from_pkcs1_der(&privkey)
344 .ok()
345 .map(|privkey| {
346 let mut ssh_key = vec![];
347 cookie_factory::gen(
348 write_ssh::rsa_pubkey(&privkey.to_public_key()),
349 &mut ssh_key,
350 )
351 .expect("can write into a Vec");
352 UnencryptedKey::SshRsa(ssh_key, Box::new(privkey)).into()
353 })
354 }
355 },
356 ),
357 pair(line_ending, tag("-----END RSA PRIVATE KEY-----")),
358 ),
359 )(input)
360}
361
362fn openssh_privkey(input: &str) -> IResult<&str, Identity> {
363 preceded(
364 pair(tag("-----BEGIN OPENSSH PRIVATE KEY-----"), line_ending),
365 terminated(
366 map_opt(wrapped_str_while_encoded(BASE64_STANDARD), |privkey| {
367 read_ssh::openssh_privkey(&privkey).ok().map(|(_, key)| key)
368 }),
369 pair(line_ending, tag("-----END OPENSSH PRIVATE KEY-----")),
370 ),
371 )(input)
372}
373
374pub(crate) fn ssh_identity(input: &str) -> IResult<&str, Identity> {
375 alt((rsa_privkey, openssh_privkey))(input)
376}
377
378#[cfg(test)]
379pub(crate) mod tests {
380 use age_core::{
381 format::FileKey,
382 secrecy::{ExposeSecret, SecretString},
383 };
384 use std::io::BufReader;
385
386 use super::{Identity, UnsupportedKey};
387 use crate::{
388 ssh::recipient::{
389 tests::{TEST_SSH_ED25519_PK, TEST_SSH_RSA_PK},
390 Recipient,
391 },
392 Callbacks, Identity as _, Recipient as _,
393 };
394
395 pub(crate) const TEST_SSH_RSA_SK: &str = "-----BEGIN RSA PRIVATE KEY-----
396MIIEogIBAAKCAQEAxO5yF0xjbmkQTfbaCP8DQC7kHnPJr5bdIie6Nzmg9lL6Chye
3970vK5iJ+BYkA1Hnf1WnNzoVIm3otZPkwZptertkY95JYFmTiA4IvHeL1yiOTd2AYc
398a947EPpM9XPomeM/7U7c99OvuCuOl1YlTFsMsoPY/NiZ+NZjgMvb3XgyH0OXy3mh
399qp+SsJU+tRjZGfqM1iv2TZUCJTQnKF8YSVCyLPV67XM1slQQHmtZ5Q6NFhzg3j8a
400CY5rDR66UF5+Zn/TvN8bNdKn01I50VLePI0ZnnRcuLXK2t0Bpkk0NymZ3vsF10m9
401HCKVyxr2Y0Ejx4BtYXOK97gaYks73rBi7+/VywIDAQABAoIBADGsf8TWtOH9yGoS
402ES9hu90ttsbjqAUNhdv+r18Mv0hC5+UzEPDe3uPScB1rWrrDwXS+WHVhtoI+HhWz
403tmi6UArbLvOA0Aq1EPUS7Q7Mop5bNIYwDG09EiMXL+BeC1b91nsygFRW5iULf502
4040pOvB8XjshEdRcFZuqGbSmtTzTjLLxYS/aboBtZLHrH4cRlFMpHWCSuJng8Psahp
405SnJbkjL7fHG81dlH+M3qm5EwdDJ1UmNkBfoSfGRs2pupk2cSJaL+SPkvNX+6Xyoy
406yvfnbJzKUTcV6rf+0S0P0yrWK3zRK9maPJ1N60lFui9LvFsunCLkSAluGKiMwEjb
407fm40F4kCgYEA+QzIeIGMwnaOQdAW4oc7hX5MgRPXJ836iALy56BCkZpZMjZ+VKpk
4088P4E1HrEywpgqHMox08hfCTGX3Ph6fFIlS1/mkLojcgkrqmg1IrRvh8vvaZqzaAf
409GKEhxxRta9Pvm44E2nUY97iCKzE3Vfh+FIyQLRuc+0COu49Me4HPtBUCgYEAym1T
410vNZKPfC/eTMh+MbWMsQArOePdoHQyRC38zeWrLaDFOUVzwzEvCQ0IzSs0PnLWkZ4
411xx60wBg5ZdU4iH4cnOYgjavQrbRFrCmZ1KDUm2+NAMw3avcLQqu41jqzyAlkktUL
412fZzyqHIBmKYLqut5GslkGnQVg6hB4psutHhiel8CgYA3yy9WH9/C6QBxqgaWdSlW
413fLby69j1p+WKdu6oCXUgXW3CHActPIckniPC3kYcHpUM58+o5wdfYnW2iKWB3XYf
414RXQiwP6MVNwy7PmE5Byc9Sui1xdyPX75648/pEnnMDGrraNUtYsEZCd1Oa9l6SeF
415vv/Fuzvt5caUKkQ+HxTDCQKBgFhqUiXr7zeIvQkiFVeE+a/ovmbHKXlYkCoSPFZm
416VFCR00VAHjt2V0PaCE/MRSNtx61hlIVcWxSAQCnDbNLpSnQZa+SVRCtqzve4n/Eo
417YlSV75+GkzoMN4XiXXRs5XOc7qnXlhJCiBac3Segdv4rpZTWm/uV8oOz7TseDtNS
418tai/AoGAC0CiIJAzmmXscXNS/stLrL9bb3Yb+VZi9zN7Cb/w7B0IJ35N5UOFmKWA
419QIGpMU4gh6p52S1eLttpIf2+39rEDzo8pY6BVmEp3fKN3jWmGS4mJQ31tWefupC+
420fGNu+wyKxPnSU3svsuvrOdwwDKvfqCNyYK878qKAAaBqbGT1NJ8=
421-----END RSA PRIVATE KEY-----";
422
423 const TEST_SSH_ED25519_SK_LIST: &[(&str, &str)] = &[
425 (
426 "none",
427 "-----BEGIN OPENSSH PRIVATE KEY-----
428b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
429QyNTUxOQAAACB7Ci6nqZYaVvrjm8+XbzII89TsXzP111AflR7WeorBjQAAAJCfEwtqnxML
430agAAAAtzc2gtZWQyNTUxOQAAACB7Ci6nqZYaVvrjm8+XbzII89TsXzP111AflR7WeorBjQ
431AAAEADBJvjZT8X6JRJI8xVq/1aU8nMVgOtVnmdwqWwrSlXG3sKLqeplhpW+uObz5dvMgjz
4321OxfM/XXUB+VHtZ6isGNAAAADHN0cjRkQGNhcmJvbgE=
433-----END OPENSSH PRIVATE KEY-----",
434 ),
435 (
436 "aes256-cbc",
437 "-----BEGIN OPENSSH PRIVATE KEY-----
438b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jYmMAAAAGYmNyeXB0AAAAGAAAABC0OgNmiw
439QW/kJ8kCmmTA2TAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIHsKLqeplhpW+uOb
440z5dvMgjz1OxfM/XXUB+VHtZ6isGNAAAAkPhBKsZoNmaeuWYJQxOl+ofEmue/sFJnW+4IOt
441oTrS/orMBJ4b/phQcv/ejWYJ4RYYVhSLiI6hf0KwNGefxI90E8iG/yDOKcrxb34tqDEYrY
442FARDaJVRd9QtWLEqoP7pgdBR2BTP7aK1y6Mx3eFDgiQI9f/0Sjxd8V0apOPXv4i4kuQ1Nt
443LF7kNlDznn/nyZlg==
444-----END OPENSSH PRIVATE KEY-----",
445 ),
446 (
447 "aes128-ctr",
448 "-----BEGIN OPENSSH PRIVATE KEY-----
449b3BlbnNzaC1rZXktdjEAAAAACmFlczEyOC1jdHIAAAAGYmNyeXB0AAAAGAAAABBub+J2jZ
450gyLfNBpxN08TqrAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIHsKLqeplhpW+uOb
451z5dvMgjz1OxfM/XXUB+VHtZ6isGNAAAAkLXOo/xKLiv8ToPkQ9l838+Lps5NAkJ/dnJLt9
452134yXn7q/7DLtsbc6KesgELApQ3Niwirqom+GwDiuNra8/JspF6iz9HZHPjFvdCLQkpQnZ
453eB6tzoh6FNmfP2HlQjmJ2w0dNMov4/0PKSAYOnW7kXq0Li/E/Gxju/raMa+pU5guk2B93v
454D/wSEe2BjjIuXZ8g==
455-----END OPENSSH PRIVATE KEY-----",
456 ),
457 (
458 "aes192-ctr",
459 "-----BEGIN OPENSSH PRIVATE KEY-----
460b3BlbnNzaC1rZXktdjEAAAAACmFlczE5Mi1jdHIAAAAGYmNyeXB0AAAAGAAAABCQRxCxO3
461qnd3DPzT+ICJvfAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIHsKLqeplhpW+uOb
462z5dvMgjz1OxfM/XXUB+VHtZ6isGNAAAAkIZMU3zFGbvSR/gvmNd9qiKr+/XCxgE3NOCrWe
463dIAveOwKzR4eXNO94TN4FF6iZv5USO1m4Mjbn3jiW4pSB6lnfctOCBWR6QPtssH0ZrmXMW
464OeOG1Nmlj2FG8LmfVNNrZ9JnXVrQYNqbvkxShb90DEFJwHWRCpzXIJEUepFJPyUPB+xLAm
465QMSqncd3IdGNmcQQ==
466-----END OPENSSH PRIVATE KEY-----",
467 ),
468 (
469 "aes256-ctr",
470 "-----BEGIN OPENSSH PRIVATE KEY-----
471b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABBSs0SUhQ
472958xWERf6ibyf2AAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIHsKLqeplhpW+uOb
473z5dvMgjz1OxfM/XXUB+VHtZ6isGNAAAAkLvH9UsJa+ulewsZT2YtEkme1y9UZKI/vUbTms
474LVqWdLprBQIm3IClfGso6IPW7+imkwYRHPKYfBYGYuexzO8b+LRiZU5/lDQmsvZA3asNxp
475KjW7kUOJnI8dAeaqJa18P7XkAuzcuZmVoCTurqEOSeb5Ww9Nq0csB0zkF22/PeWy3+BZW5
476hDsL1OfQl4WbakZQ==
477-----END OPENSSH PRIVATE KEY-----",
478 ),
479 (
480 "aes256-gcm@openssh.com",
481 "-----BEGIN OPENSSH PRIVATE KEY-----
482b3BlbnNzaC1rZXktdjEAAAAAFmFlczI1Ni1nY21Ab3BlbnNzaC5jb20AAAAGYmNyeXB0AA
483AAGAAAABCPl8ey+kOWEfNDWjsOW+yeAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAA
484IHsKLqeplhpW+uObz5dvMgjz1OxfM/XXUB+VHtZ6isGNAAAAkHK4lAYpbPto7eVDnl7RM5
485smu3f1Gi/Ov305gASYkCWxL3cvzxTgP2prG7ky4FS5EnFeCoZU4GR49nMjTtJwVJz9vUmQ
486csGgRF9XqsdNcNwroWoIeejitFjrQ/n+zVreeMtCWU3gvVSHV97ZhcBVCxCQyPdeaQoUr9
487k38nvmwdar9EY4Mb7LrSqR6oybE/g9Hjg6cxzVcvDQKga6tJVM5oY=
488-----END OPENSSH PRIVATE KEY-----",
489 ),
490 ];
491 pub(crate) const TEST_SSH_ED25519_SK: &str = TEST_SSH_ED25519_SK_LIST[0].1;
492
493 pub(crate) const TEST_SSH_ECDSA_SK: &str = "-----BEGIN OPENSSH PRIVATE KEY-----
494b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS
4951zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQQQ0odKVFtwOmuCl6RXfwzExGs9dP9a
496V9H5xAfETILMd7sLFgqyOxz1FA84EZV0vKdW5c0HPB7/JxQw0vFmNSWeAAAAqGOGFFJjhh
497RSAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBBDSh0pUW3A6a4KX
498pFd/DMTEaz10/1pX0fnEB8RMgsx3uwsWCrI7HPUUDzgRlXS8p1blzQc8Hv8nFDDS8WY1JZ
4994AAAAgBQ5LA+stpdk3TYwB/4xhiOaDHzxaacv+u47ciigD8bQAAAAKc3RyNGRAY3ViZQEC
500AwQFBg==
501-----END OPENSSH PRIVATE KEY-----";
502
503 #[derive(Clone)]
504 struct TestPassphrase(&'static str);
505
506 impl Callbacks for TestPassphrase {
507 fn display_message(&self, _: &str) {
508 unimplemented!()
509 }
510
511 fn confirm(&self, _: &str, _: &str, _: Option<&str>) -> Option<bool> {
512 unimplemented!()
513 }
514
515 fn request_public_string(&self, _: &str) -> Option<String> {
516 unimplemented!()
517 }
518
519 fn request_passphrase(&self, _: &str) -> Option<SecretString> {
520 Some(SecretString::from(self.0.to_owned()))
521 }
522 }
523
524 #[test]
525 fn ssh_rsa_round_trip() {
526 let buf = BufReader::new(TEST_SSH_RSA_SK.as_bytes());
527 let identity = Identity::from_buffer(buf, None).unwrap();
528 match &identity {
529 Identity::Unencrypted(_) => (),
530 _ => panic!("key should be unencrypted"),
531 };
532 let pk: Recipient = TEST_SSH_RSA_PK.parse().unwrap();
533
534 let file_key = FileKey::new(Box::new([12; 16]));
535
536 let (wrapped, labels) = pk.wrap_file_key(&file_key).unwrap();
537 assert!(labels.is_empty());
538 let unwrapped = identity.unwrap_stanzas(&wrapped);
539 assert_eq!(
540 unwrapped.unwrap().unwrap().expose_secret(),
541 file_key.expose_secret()
542 );
543 }
544
545 #[test]
546 fn ssh_ed25519_round_trip() {
547 for (kind, sk) in TEST_SSH_ED25519_SK_LIST {
548 eprintln!("Testing cipher '{}'", kind);
549 let buf = BufReader::new(sk.as_bytes());
550 let identity = Identity::from_buffer(buf, None).unwrap();
551 match (*kind, &identity) {
552 ("none", Identity::Unencrypted(_)) => (),
553 ("none", _) => panic!("key should be unencrypted"),
554 (_, Identity::Encrypted(_)) => (),
555 (_, Identity::Unsupported(_)) => panic!("{} cipher is unsupported", kind),
556 (_, _) => panic!("key should be encrypted"),
557 };
558 let identity = identity.with_callbacks(TestPassphrase("passphrase"));
559 let pk: Recipient = TEST_SSH_ED25519_PK.parse().unwrap();
560
561 let file_key = FileKey::new(Box::new([12; 16]));
562
563 let (wrapped, labels) = pk.wrap_file_key(&file_key).unwrap();
564 assert!(labels.is_empty());
565 let unwrapped = identity.unwrap_stanzas(&wrapped);
566 assert_eq!(
567 unwrapped.unwrap().unwrap().expose_secret(),
568 file_key.expose_secret()
569 );
570 }
571 }
572
573 #[test]
574 fn ssh_unsupported_key_type() {
575 let buf = BufReader::new(TEST_SSH_ECDSA_SK.as_bytes());
576 let identity = Identity::from_buffer(buf, None).unwrap();
577 let unsupported = match &identity {
578 Identity::Unsupported(res) => res,
579 _ => panic!("key should be unencrypted"),
580 };
581 assert_eq!(
582 unsupported,
583 &UnsupportedKey::Type("ecdsa-sha2-nistp256".to_string()),
584 );
585 }
586}