bitwarden_ssh/
export.rs

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