bitwarden_ssh/
export.rs

1use pkcs8::EncodePrivateKey;
2use rsa::RsaPrivateKey;
3use ssh_key::PrivateKey;
4
5use crate::error::SshKeyExportError;
6
7/// Convert an OpenSSH private key to PKCS#8 DER format
8///
9/// This is primarily used for exporting SSH keys to other credential managers using Credential
10/// Exchange.
11pub fn export_pkcs8_der_key(private_key: &str) -> Result<Vec<u8>, SshKeyExportError> {
12    // Parse the OpenSSH private key
13    let private_key =
14        PrivateKey::from_openssh(private_key).map_err(|_| SshKeyExportError::KeyConversion)?;
15
16    match private_key.key_data() {
17        ssh_key::private::KeypairData::Ed25519(keypair) => {
18            let sk: ed25519_dalek::SigningKey = keypair
19                .try_into()
20                .map_err(|_| SshKeyExportError::KeyConversion)?;
21
22            Ok(sk
23                .to_pkcs8_der()
24                .map_err(|_| SshKeyExportError::KeyConversion)?
25                .as_bytes()
26                .to_vec())
27        }
28        ssh_key::private::KeypairData::Rsa(keypair) => {
29            let rk: RsaPrivateKey = keypair
30                .try_into()
31                .map_err(|_| SshKeyExportError::KeyConversion)?;
32
33            Ok(rk
34                .to_pkcs8_der()
35                .map_err(|_| SshKeyExportError::KeyConversion)?
36                .as_bytes()
37                .to_vec())
38        }
39        _ => Err(SshKeyExportError::KeyConversion),
40    }
41}
42
43#[cfg(test)]
44mod tests {
45    use super::*;
46    use crate::import::{import_key, import_pkcs8_der_key};
47
48    #[test]
49    fn export_ed25519_openssh_unencrypted() {
50        let private_key = include_str!("../resources/import/ed25519_openssh_unencrypted");
51        let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
52
53        let exported_key = export_pkcs8_der_key(&result.private_key).unwrap();
54        let expected_pkcs8_der: Vec<u8> = vec![
55            48, 81, 2, 1, 1, 48, 5, 6, 3, 43, 101, 112, 4, 34, 4, 32, 139, 118, 81, 75, 32, 150,
56            196, 136, 90, 63, 127, 68, 78, 117, 115, 13, 100, 3, 199, 24, 243, 97, 189, 182, 223,
57            181, 163, 236, 81, 145, 35, 104, 129, 33, 0, 50, 66, 141, 182, 77, 117, 205, 170, 241,
58            126, 47, 200, 212, 73, 35, 94, 187, 197, 42, 174, 192, 227, 189, 255, 105, 192, 140, 3,
59            11, 211, 11, 234,
60        ];
61        assert_eq!(exported_key, expected_pkcs8_der);
62
63        // Confirm the public key of the re-imported key is the same ignoring the key comment
64        let reimported_key = import_pkcs8_der_key(&exported_key).unwrap();
65        assert_eq!(
66            reimported_key.public_key,
67            result.public_key.strip_suffix(" testkey").unwrap()
68        );
69    }
70
71    #[test]
72    fn export_rsa_openssh_unencrypted() {
73        let private_key = include_str!("../resources/import/rsa_openssh_unencrypted");
74        let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
75
76        let exported_key = export_pkcs8_der_key(&result.private_key).unwrap();
77
78        // Confirm the public key of the re-imported key is the same ignoring the key comment
79        let reimported_key = import_pkcs8_der_key(&exported_key).unwrap();
80        assert_eq!(
81            reimported_key.public_key,
82            result.public_key.strip_suffix(" testkey").unwrap()
83        );
84    }
85}