Skip to main content

bitwarden_ssh/
import.rs

1use bitwarden_vault::SshKeyView;
2use ed25519;
3use pem_rfc7468::PemLabel;
4use pkcs8::{DecodePrivateKey, PrivateKeyInfo, SecretDocument, der::Decode, pkcs5};
5use ssh_key::private::{Ed25519Keypair, RsaKeypair};
6#[cfg(feature = "ecdsa-keys")]
7use ssh_key::sec1;
8
9use crate::{error::SshKeyImportError, ssh_private_key_to_view};
10
11/// Import a PKCS8 or OpenSSH encoded private key, and returns a decoded [SshKeyView],
12/// with the public key and fingerprint, and the private key in OpenSSH format.
13/// A password can be provided for encrypted keys.
14/// # Returns
15/// - [SshKeyImportError::PasswordRequired] if the key is encrypted and no password is provided
16/// - [SshKeyImportError::WrongPassword] if the password provided is incorrect
17/// - [SshKeyImportError::UnsupportedKeyType] if the key type is not supported
18/// - [SshKeyImportError::Parsing] if the key is otherwise malformed and cannot be parsed
19pub fn import_key(
20    encoded_key: String,
21    password: Option<String>,
22) -> Result<SshKeyView, SshKeyImportError> {
23    let label = pem_rfc7468::decode_label(encoded_key.as_bytes())
24        .map_err(|_| SshKeyImportError::Parsing)?;
25
26    match label {
27        pkcs8::PrivateKeyInfo::<(), (), ()>::PEM_LABEL => import_pkcs8_key(encoded_key, None),
28        pkcs8::EncryptedPrivateKeyInfo::<()>::PEM_LABEL => import_pkcs8_key(
29            encoded_key,
30            Some(password.ok_or(SshKeyImportError::PasswordRequired)?),
31        ),
32        ssh_key::PrivateKey::PEM_LABEL => import_openssh_key(encoded_key, password),
33        _ => Err(SshKeyImportError::UnsupportedKeyType),
34    }
35}
36
37fn import_pkcs8_key(
38    encoded_key: String,
39    password: Option<String>,
40) -> Result<SshKeyView, SshKeyImportError> {
41    let doc = if let Some(password) = password {
42        SecretDocument::from_pkcs8_encrypted_pem(&encoded_key, password.as_bytes()).map_err(
43            |err| match err {
44                pkcs8::Error::EncryptedPrivateKey(pkcs5::Error::DecryptFailed) => {
45                    SshKeyImportError::WrongPassword
46                }
47                _ => SshKeyImportError::Parsing,
48            },
49        )?
50    } else {
51        SecretDocument::from_pkcs8_pem(&encoded_key).map_err(|_| SshKeyImportError::Parsing)?
52    };
53
54    import_pkcs8_der_key(doc.as_bytes())
55}
56
57/// Import a DER encoded private key, and returns a decoded [SshKeyView]. This is primarily used for
58/// importing SSH keys from other Credential Managers through Credential Exchange.
59pub fn import_pkcs8_der_key(encoded_key: &[u8]) -> Result<SshKeyView, SshKeyImportError> {
60    let private_key_info =
61        PrivateKeyInfo::from_der(encoded_key).map_err(|_| SshKeyImportError::Parsing)?;
62
63    let private_key = match private_key_info.algorithm.oid {
64        ed25519::pkcs8::ALGORITHM_OID => {
65            let private_key: ed25519::KeypairBytes = private_key_info
66                .try_into()
67                .map_err(|_| SshKeyImportError::Parsing)?;
68
69            ssh_key::private::PrivateKey::from(Ed25519Keypair::from(&private_key.secret_key.into()))
70        }
71        rsa::pkcs1::ALGORITHM_OID => {
72            let private_key: rsa::RsaPrivateKey = private_key_info
73                .try_into()
74                .map_err(|_| SshKeyImportError::Parsing)?;
75
76            ssh_key::private::PrivateKey::from(
77                RsaKeypair::try_from(private_key).map_err(|_| SshKeyImportError::Parsing)?,
78            )
79        }
80        #[cfg(feature = "ecdsa-keys")]
81        sec1::ALGORITHM_OID => import_ecdsa_pkcs8_der(encoded_key)?,
82        _ => return Err(SshKeyImportError::UnsupportedKeyType),
83    };
84
85    ssh_private_key_to_view(private_key).map_err(|_| SshKeyImportError::Parsing)
86}
87
88fn import_openssh_key(
89    encoded_key: String,
90    password: Option<String>,
91) -> Result<SshKeyView, SshKeyImportError> {
92    let private_key =
93        ssh_key::private::PrivateKey::from_openssh(&encoded_key).map_err(|err| match err {
94            ssh_key::Error::AlgorithmUnknown | ssh_key::Error::AlgorithmUnsupported { .. } => {
95                SshKeyImportError::UnsupportedKeyType
96            }
97            _ => SshKeyImportError::Parsing,
98        })?;
99
100    let private_key = if private_key.is_encrypted() {
101        let password = password.ok_or(SshKeyImportError::PasswordRequired)?;
102        private_key
103            .decrypt(password.as_bytes())
104            .map_err(|_| SshKeyImportError::WrongPassword)?
105    } else {
106        private_key
107    };
108
109    reject_ecdsa_import(&private_key)?;
110
111    ssh_private_key_to_view(private_key).map_err(|_| SshKeyImportError::Parsing)
112}
113
114fn reject_ecdsa_import(key: &ssh_key::PrivateKey) -> Result<(), SshKeyImportError> {
115    #[cfg(not(feature = "ecdsa-keys"))]
116    if matches!(key.key_data(), ssh_key::private::KeypairData::Ecdsa(_)) {
117        return Err(SshKeyImportError::UnsupportedKeyType);
118    }
119    let _ = key;
120    Ok(())
121}
122
123#[cfg(feature = "ecdsa-keys")]
124fn import_ecdsa_pkcs8_der(encoded_key: &[u8]) -> Result<ssh_key::PrivateKey, SshKeyImportError> {
125    use pkcs8::DecodePrivateKey as _;
126
127    if let Ok(sk) = p256::SecretKey::from_pkcs8_der(encoded_key) {
128        let public_key = sk.public_key();
129        let keypair = ssh_key::private::EcdsaKeypair::NistP256 {
130            public: public_key.into(),
131            private: ssh_key::private::EcdsaPrivateKey::from(sk),
132        };
133        return ssh_key::PrivateKey::new(ssh_key::private::KeypairData::Ecdsa(keypair), "")
134            .map_err(|_| SshKeyImportError::Parsing);
135    }
136    if let Ok(sk) = p384::SecretKey::from_pkcs8_der(encoded_key) {
137        let public_key = sk.public_key();
138        let keypair = ssh_key::private::EcdsaKeypair::NistP384 {
139            public: public_key.into(),
140            private: ssh_key::private::EcdsaPrivateKey::from(sk),
141        };
142        return ssh_key::PrivateKey::new(ssh_key::private::KeypairData::Ecdsa(keypair), "")
143            .map_err(|_| SshKeyImportError::Parsing);
144    }
145    if let Ok(sk) = p521::SecretKey::from_pkcs8_der(encoded_key) {
146        let public_key = sk.public_key();
147        let keypair = ssh_key::private::EcdsaKeypair::NistP521 {
148            public: public_key.into(),
149            private: ssh_key::private::EcdsaPrivateKey::from(sk),
150        };
151        return ssh_key::PrivateKey::new(ssh_key::private::KeypairData::Ecdsa(keypair), "")
152            .map_err(|_| SshKeyImportError::Parsing);
153    }
154    Err(SshKeyImportError::UnsupportedKeyType)
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn import_key_ed25519_openssh_unencrypted() {
163        let private_key = include_str!("../resources/import/ed25519_openssh_unencrypted");
164        let public_key = include_str!("../resources/import/ed25519_openssh_unencrypted.pub").trim();
165        let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
166        assert_eq!(result.public_key, public_key);
167    }
168
169    #[test]
170    fn import_key_ed25519_openssh_encrypted() {
171        let private_key = include_str!("../resources/import/ed25519_openssh_encrypted");
172        let public_key = include_str!("../resources/import/ed25519_openssh_encrypted.pub").trim();
173        let result = import_key(private_key.to_string(), Some("password".to_string())).unwrap();
174        assert_eq!(result.public_key, public_key);
175    }
176
177    #[test]
178    fn import_key_rsa_openssh_unencrypted() {
179        let private_key = include_str!("../resources/import/rsa_openssh_unencrypted");
180        let public_key = include_str!("../resources/import/rsa_openssh_unencrypted.pub").trim();
181        let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
182        assert_eq!(result.public_key, public_key);
183    }
184
185    #[test]
186    fn import_key_rsa_openssh_encrypted() {
187        let private_key = include_str!("../resources/import/rsa_openssh_encrypted");
188        let public_key = include_str!("../resources/import/rsa_openssh_encrypted.pub").trim();
189        let result = import_key(private_key.to_string(), Some("password".to_string())).unwrap();
190        assert_eq!(result.public_key, public_key);
191    }
192
193    #[test]
194    fn import_key_ed25519_pkcs8_unencrypted() {
195        let private_key = include_str!("../resources/import/ed25519_pkcs8_unencrypted");
196        let public_key = include_str!("../resources/import/ed25519_pkcs8_unencrypted.pub")
197            .replace("testkey", "");
198        let public_key = public_key.trim();
199        let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
200        assert_eq!(result.public_key, public_key);
201    }
202
203    #[test]
204    fn import_key_rsa_pkcs8_unencrypted() {
205        let private_key = include_str!("../resources/import/rsa_pkcs8_unencrypted");
206        // for whatever reason pkcs8 + rsa does not include the comment in the public key
207        let public_key =
208            include_str!("../resources/import/rsa_pkcs8_unencrypted.pub").replace("testkey", "");
209        let public_key = public_key.trim();
210        let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
211        assert_eq!(result.public_key, public_key);
212    }
213
214    #[test]
215    fn import_key_rsa_pkcs8_encrypted() {
216        let private_key = include_str!("../resources/import/rsa_pkcs8_encrypted");
217        let public_key =
218            include_str!("../resources/import/rsa_pkcs8_encrypted.pub").replace("testkey", "");
219        let public_key = public_key.trim();
220        let result = import_key(private_key.to_string(), Some("password".to_string())).unwrap();
221        assert_eq!(result.public_key, public_key);
222    }
223
224    #[test]
225    fn import_key_ed25519_openssh_encrypted_wrong_password() {
226        let private_key = include_str!("../resources/import/ed25519_openssh_encrypted");
227        let result = import_key(private_key.to_string(), Some("wrongpassword".to_string()));
228        assert_eq!(result.unwrap_err(), SshKeyImportError::WrongPassword);
229    }
230
231    #[test]
232    fn import_non_key_error() {
233        let result = import_key("not a key".to_string(), Some("".to_string()));
234        assert_eq!(result.unwrap_err(), SshKeyImportError::Parsing);
235    }
236
237    #[test]
238    fn import_wrong_label_error() {
239        let private_key = include_str!("../resources/import/wrong_label");
240        let result = import_key(private_key.to_string(), Some("".to_string()));
241        assert_eq!(result.unwrap_err(), SshKeyImportError::UnsupportedKeyType);
242    }
243
244    #[cfg(not(feature = "ecdsa-keys"))]
245    #[test]
246    fn import_ecdsa_blocked() {
247        let private_key = include_str!("../resources/import/ecdsa_openssh_unencrypted");
248        let result = import_key(private_key.to_string(), Some("".to_string()));
249        assert_eq!(result.unwrap_err(), SshKeyImportError::UnsupportedKeyType);
250    }
251
252    #[cfg(feature = "ecdsa-keys")]
253    #[test]
254    fn import_ecdsa_p256_openssh_unencrypted() {
255        let private_key = include_str!("../resources/import/ecdsa_openssh_unencrypted");
256        let public_key = include_str!("../resources/import/ecdsa_openssh_unencrypted.pub").trim();
257        let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
258        assert_eq!(result.public_key, public_key);
259    }
260
261    #[cfg(feature = "ecdsa-keys")]
262    #[test]
263    fn import_ecdsa_p384_openssh_unencrypted() {
264        let private_key = include_str!("../resources/import/ecdsa_p384_openssh_unencrypted");
265        let public_key =
266            include_str!("../resources/import/ecdsa_p384_openssh_unencrypted.pub").trim();
267        let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
268        assert_eq!(result.public_key, public_key);
269    }
270
271    #[cfg(feature = "ecdsa-keys")]
272    #[test]
273    fn import_ecdsa_p521_openssh_unencrypted() {
274        let private_key = include_str!("../resources/import/ecdsa_p521_openssh_unencrypted");
275        let public_key =
276            include_str!("../resources/import/ecdsa_p521_openssh_unencrypted.pub").trim();
277        let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
278        assert_eq!(result.public_key, public_key);
279    }
280
281    #[test]
282    fn import_key_ed25519_putty() {
283        let private_key = include_str!("../resources/import/ed25519_putty_openssh_unencrypted");
284        let public_key =
285            include_str!("../resources/import/ed25519_putty_openssh_unencrypted.pub").trim();
286        let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
287        assert_eq!(result.public_key, public_key);
288    }
289
290    #[test]
291    fn import_key_rsa_openssh_putty() {
292        let private_key = include_str!("../resources/import/rsa_putty_openssh_unencrypted");
293        let public_key =
294            include_str!("../resources/import/rsa_putty_openssh_unencrypted.pub").trim();
295        let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
296        assert_eq!(result.public_key, public_key);
297    }
298
299    #[test]
300    fn import_key_rsa_pkcs8_putty() {
301        let private_key = include_str!("../resources/import/rsa_putty_pkcs1_unencrypted");
302        let result = import_key(private_key.to_string(), Some("".to_string()));
303        assert_eq!(result.unwrap_err(), SshKeyImportError::UnsupportedKeyType);
304    }
305
306    #[test]
307    fn import_ed25519_key_regression_17028() {
308        // https://github.com/bitwarden/clients/issues/17028#issuecomment-3455975763
309        let private_key = include_str!("../resources/import/ed25519_regression_17028");
310        let public_key = include_str!("../resources/import/ed25519_regression_17028.pub").trim();
311        let result = import_key(private_key.to_string(), None).unwrap();
312        assert_eq!(result.public_key, public_key);
313    }
314}