bitwarden_ssh/
import.rs

1use bitwarden_vault::SshKeyView;
2use ed25519;
3use pem_rfc7468::PemLabel;
4use pkcs8::{der::Decode, pkcs5, DecodePrivateKey, PrivateKeyInfo, SecretDocument};
5use ssh_key::private::{Ed25519Keypair, RsaKeypair};
6
7use crate::{error::SshKeyImportError, ssh_private_key_to_view};
8
9/// Import a PKCS8 or OpenSSH encoded private key, and returns a decoded [SshKeyView],
10/// with the public key and fingerprint, and the private key in OpenSSH format.
11/// A password can be provided for encrypted keys.
12/// # Returns
13/// - [SshKeyImportError::PasswordRequired] if the key is encrypted and no password is provided
14/// - [SshKeyImportError::WrongPassword] if the password provided is incorrect
15/// - [SshKeyImportError::UnsupportedKeyType] if the key type is not supported
16/// - [SshKeyImportError::ParsingError] if the key is otherwise malformed and cannot be parsed
17pub fn import_key(
18    encoded_key: String,
19    password: Option<String>,
20) -> Result<SshKeyView, SshKeyImportError> {
21    let label = pem_rfc7468::decode_label(encoded_key.as_bytes())
22        .map_err(|_| SshKeyImportError::ParsingError)?;
23
24    match label {
25        pkcs8::PrivateKeyInfo::PEM_LABEL => import_pkcs8_key(encoded_key, None),
26        pkcs8::EncryptedPrivateKeyInfo::PEM_LABEL => import_pkcs8_key(
27            encoded_key,
28            Some(password.ok_or(SshKeyImportError::PasswordRequired)?),
29        ),
30        ssh_key::PrivateKey::PEM_LABEL => import_openssh_key(encoded_key, password),
31        _ => Err(SshKeyImportError::UnsupportedKeyType),
32    }
33}
34
35fn import_pkcs8_key(
36    encoded_key: String,
37    password: Option<String>,
38) -> Result<SshKeyView, SshKeyImportError> {
39    let doc = if let Some(password) = password {
40        SecretDocument::from_pkcs8_encrypted_pem(&encoded_key, password.as_bytes()).map_err(
41            |err| match err {
42                pkcs8::Error::EncryptedPrivateKey(pkcs5::Error::DecryptFailed) => {
43                    SshKeyImportError::WrongPassword
44                }
45                _ => SshKeyImportError::ParsingError,
46            },
47        )?
48    } else {
49        SecretDocument::from_pkcs8_pem(&encoded_key).map_err(|_| SshKeyImportError::ParsingError)?
50    };
51
52    import_pkcs8_der_key(doc.as_bytes())
53}
54
55/// Import a DER encoded private key, and returns a decoded [SshKeyView]. This is primarily used for
56/// importing SSH keys from other Credential Managers through Credential Exchange.
57pub fn import_pkcs8_der_key(encoded_key: &[u8]) -> Result<SshKeyView, SshKeyImportError> {
58    let private_key_info =
59        PrivateKeyInfo::from_der(encoded_key).map_err(|_| SshKeyImportError::ParsingError)?;
60
61    let private_key = match private_key_info.algorithm.oid {
62        ed25519::pkcs8::ALGORITHM_OID => {
63            let private_key: ed25519::KeypairBytes = private_key_info
64                .try_into()
65                .map_err(|_| SshKeyImportError::ParsingError)?;
66
67            ssh_key::private::PrivateKey::from(Ed25519Keypair::from(&private_key.secret_key.into()))
68        }
69        rsa::pkcs1::ALGORITHM_OID => {
70            let private_key: rsa::RsaPrivateKey = private_key_info
71                .try_into()
72                .map_err(|_| SshKeyImportError::ParsingError)?;
73
74            ssh_key::private::PrivateKey::from(
75                RsaKeypair::try_from(private_key).map_err(|_| SshKeyImportError::ParsingError)?,
76            )
77        }
78        _ => return Err(SshKeyImportError::UnsupportedKeyType),
79    };
80
81    ssh_private_key_to_view(private_key).map_err(|_| SshKeyImportError::ParsingError)
82}
83
84fn import_openssh_key(
85    encoded_key: String,
86    password: Option<String>,
87) -> Result<SshKeyView, SshKeyImportError> {
88    let private_key =
89        ssh_key::private::PrivateKey::from_openssh(&encoded_key).map_err(|err| match err {
90            ssh_key::Error::AlgorithmUnknown | ssh_key::Error::AlgorithmUnsupported { .. } => {
91                SshKeyImportError::UnsupportedKeyType
92            }
93            _ => SshKeyImportError::ParsingError,
94        })?;
95
96    let private_key = if private_key.is_encrypted() {
97        let password = password.ok_or(SshKeyImportError::PasswordRequired)?;
98        private_key
99            .decrypt(password.as_bytes())
100            .map_err(|_| SshKeyImportError::WrongPassword)?
101    } else {
102        private_key
103    };
104
105    ssh_private_key_to_view(private_key).map_err(|_| SshKeyImportError::ParsingError)
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn import_key_ed25519_openssh_unencrypted() {
114        let private_key = include_str!("../resources/import/ed25519_openssh_unencrypted");
115        let public_key = include_str!("../resources/import/ed25519_openssh_unencrypted.pub").trim();
116        let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
117        assert_eq!(result.public_key, public_key);
118    }
119
120    #[test]
121    fn import_key_ed25519_openssh_encrypted() {
122        let private_key = include_str!("../resources/import/ed25519_openssh_encrypted");
123        let public_key = include_str!("../resources/import/ed25519_openssh_encrypted.pub").trim();
124        let result = import_key(private_key.to_string(), Some("password".to_string())).unwrap();
125        assert_eq!(result.public_key, public_key);
126    }
127
128    #[test]
129    fn import_key_rsa_openssh_unencrypted() {
130        let private_key = include_str!("../resources/import/rsa_openssh_unencrypted");
131        let public_key = include_str!("../resources/import/rsa_openssh_unencrypted.pub").trim();
132        let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
133        assert_eq!(result.public_key, public_key);
134    }
135
136    #[test]
137    fn import_key_rsa_openssh_encrypted() {
138        let private_key = include_str!("../resources/import/rsa_openssh_encrypted");
139        let public_key = include_str!("../resources/import/rsa_openssh_encrypted.pub").trim();
140        let result = import_key(private_key.to_string(), Some("password".to_string())).unwrap();
141        assert_eq!(result.public_key, public_key);
142    }
143
144    #[test]
145    fn import_key_ed25519_pkcs8_unencrypted() {
146        let private_key = include_str!("../resources/import/ed25519_pkcs8_unencrypted");
147        let public_key = include_str!("../resources/import/ed25519_pkcs8_unencrypted.pub")
148            .replace("testkey", "");
149        let public_key = public_key.trim();
150        let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
151        assert_eq!(result.public_key, public_key);
152    }
153
154    #[test]
155    fn import_key_rsa_pkcs8_unencrypted() {
156        let private_key = include_str!("../resources/import/rsa_pkcs8_unencrypted");
157        // for whatever reason pkcs8 + rsa does not include the comment in the public key
158        let public_key =
159            include_str!("../resources/import/rsa_pkcs8_unencrypted.pub").replace("testkey", "");
160        let public_key = public_key.trim();
161        let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
162        assert_eq!(result.public_key, public_key);
163    }
164
165    #[test]
166    fn import_key_rsa_pkcs8_encrypted() {
167        let private_key = include_str!("../resources/import/rsa_pkcs8_encrypted");
168        let public_key =
169            include_str!("../resources/import/rsa_pkcs8_encrypted.pub").replace("testkey", "");
170        let public_key = public_key.trim();
171        let result = import_key(private_key.to_string(), Some("password".to_string())).unwrap();
172        assert_eq!(result.public_key, public_key);
173    }
174
175    #[test]
176    fn import_key_ed25519_openssh_encrypted_wrong_password() {
177        let private_key = include_str!("../resources/import/ed25519_openssh_encrypted");
178        let result = import_key(private_key.to_string(), Some("wrongpassword".to_string()));
179        assert_eq!(result.unwrap_err(), SshKeyImportError::WrongPassword);
180    }
181
182    #[test]
183    fn import_non_key_error() {
184        let result = import_key("not a key".to_string(), Some("".to_string()));
185        assert_eq!(result.unwrap_err(), SshKeyImportError::ParsingError);
186    }
187
188    #[test]
189    fn import_wrong_label_error() {
190        let private_key = include_str!("../resources/import/wrong_label");
191        let result = import_key(private_key.to_string(), Some("".to_string()));
192        assert_eq!(result.unwrap_err(), SshKeyImportError::UnsupportedKeyType);
193    }
194
195    #[test]
196    fn import_ecdsa_error() {
197        let private_key = include_str!("../resources/import/ecdsa_openssh_unencrypted");
198        let result = import_key(private_key.to_string(), Some("".to_string()));
199        assert_eq!(result.unwrap_err(), SshKeyImportError::UnsupportedKeyType);
200    }
201
202    // Putty-exported keys should be supported, but are not due to a parser incompatibility.
203    // Should this test start failing, please change it to expect a correct key, and
204    // make sure the documentation support for putty-exported keys this is updated.
205    // https://bitwarden.atlassian.net/browse/PM-14989
206    #[test]
207    fn import_key_ed25519_putty() {
208        let private_key = include_str!("../resources/import/ed25519_putty_openssh_unencrypted");
209        let result = import_key(private_key.to_string(), Some("".to_string()));
210        assert_eq!(result.unwrap_err(), SshKeyImportError::ParsingError);
211    }
212
213    // Putty-exported keys should be supported, but are not due to a parser incompatibility.
214    // Should this test start failing, please change it to expect a correct key, and
215    // make sure the documentation support for putty-exported keys this is updated.
216    // https://bitwarden.atlassian.net/browse/PM-14989
217    #[test]
218    fn import_key_rsa_openssh_putty() {
219        let private_key = include_str!("../resources/import/rsa_putty_openssh_unencrypted");
220        let result = import_key(private_key.to_string(), Some("".to_string()));
221        assert_eq!(result.unwrap_err(), SshKeyImportError::ParsingError);
222    }
223
224    #[test]
225    fn import_key_rsa_pkcs8_putty() {
226        let private_key = include_str!("../resources/import/rsa_putty_pkcs1_unencrypted");
227        let result = import_key(private_key.to_string(), Some("".to_string()));
228        assert_eq!(result.unwrap_err(), SshKeyImportError::UnsupportedKeyType);
229    }
230}