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    let private_key_info =
53        PrivateKeyInfo::from_der(doc.as_bytes()).map_err(|_| SshKeyImportError::ParsingError)?;
54
55    let private_key = match private_key_info.algorithm.oid {
56        ed25519::pkcs8::ALGORITHM_OID => {
57            let private_key: ed25519::KeypairBytes = private_key_info
58                .try_into()
59                .map_err(|_| SshKeyImportError::ParsingError)?;
60
61            ssh_key::private::PrivateKey::from(Ed25519Keypair::from(&private_key.secret_key.into()))
62        }
63        rsa::pkcs1::ALGORITHM_OID => {
64            let private_key: rsa::RsaPrivateKey = private_key_info
65                .try_into()
66                .map_err(|_| SshKeyImportError::ParsingError)?;
67
68            ssh_key::private::PrivateKey::from(
69                RsaKeypair::try_from(private_key).map_err(|_| SshKeyImportError::ParsingError)?,
70            )
71        }
72        _ => return Err(SshKeyImportError::UnsupportedKeyType),
73    };
74
75    ssh_private_key_to_view(private_key).map_err(|_| SshKeyImportError::ParsingError)
76}
77
78fn import_openssh_key(
79    encoded_key: String,
80    password: Option<String>,
81) -> Result<SshKeyView, SshKeyImportError> {
82    let private_key =
83        ssh_key::private::PrivateKey::from_openssh(&encoded_key).map_err(|err| match err {
84            ssh_key::Error::AlgorithmUnknown | ssh_key::Error::AlgorithmUnsupported { .. } => {
85                SshKeyImportError::UnsupportedKeyType
86            }
87            _ => SshKeyImportError::ParsingError,
88        })?;
89
90    let private_key = if private_key.is_encrypted() {
91        let password = password.ok_or(SshKeyImportError::PasswordRequired)?;
92        private_key
93            .decrypt(password.as_bytes())
94            .map_err(|_| SshKeyImportError::WrongPassword)?
95    } else {
96        private_key
97    };
98
99    ssh_private_key_to_view(private_key).map_err(|_| SshKeyImportError::ParsingError)
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn import_key_ed25519_openssh_unencrypted() {
108        let private_key = include_str!("../resources/import/ed25519_openssh_unencrypted");
109        let public_key = include_str!("../resources/import/ed25519_openssh_unencrypted.pub").trim();
110        let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
111        assert_eq!(result.public_key, public_key);
112    }
113
114    #[test]
115    fn import_key_ed25519_openssh_encrypted() {
116        let private_key = include_str!("../resources/import/ed25519_openssh_encrypted");
117        let public_key = include_str!("../resources/import/ed25519_openssh_encrypted.pub").trim();
118        let result = import_key(private_key.to_string(), Some("password".to_string())).unwrap();
119        assert_eq!(result.public_key, public_key);
120    }
121
122    #[test]
123    fn import_key_rsa_openssh_unencrypted() {
124        let private_key = include_str!("../resources/import/rsa_openssh_unencrypted");
125        let public_key = include_str!("../resources/import/rsa_openssh_unencrypted.pub").trim();
126        let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
127        assert_eq!(result.public_key, public_key);
128    }
129
130    #[test]
131    fn import_key_rsa_openssh_encrypted() {
132        let private_key = include_str!("../resources/import/rsa_openssh_encrypted");
133        let public_key = include_str!("../resources/import/rsa_openssh_encrypted.pub").trim();
134        let result = import_key(private_key.to_string(), Some("password".to_string())).unwrap();
135        assert_eq!(result.public_key, public_key);
136    }
137
138    #[test]
139    fn import_key_ed25519_pkcs8_unencrypted() {
140        let private_key = include_str!("../resources/import/ed25519_pkcs8_unencrypted");
141        let public_key = include_str!("../resources/import/ed25519_pkcs8_unencrypted.pub")
142            .replace("testkey", "");
143        let public_key = public_key.trim();
144        let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
145        assert_eq!(result.public_key, public_key);
146    }
147
148    #[test]
149    fn import_key_rsa_pkcs8_unencrypted() {
150        let private_key = include_str!("../resources/import/rsa_pkcs8_unencrypted");
151        // for whatever reason pkcs8 + rsa does not include the comment in the public key
152        let public_key =
153            include_str!("../resources/import/rsa_pkcs8_unencrypted.pub").replace("testkey", "");
154        let public_key = public_key.trim();
155        let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
156        assert_eq!(result.public_key, public_key);
157    }
158
159    #[test]
160    fn import_key_rsa_pkcs8_encrypted() {
161        let private_key = include_str!("../resources/import/rsa_pkcs8_encrypted");
162        let public_key =
163            include_str!("../resources/import/rsa_pkcs8_encrypted.pub").replace("testkey", "");
164        let public_key = public_key.trim();
165        let result = import_key(private_key.to_string(), Some("password".to_string())).unwrap();
166        assert_eq!(result.public_key, public_key);
167    }
168
169    #[test]
170    fn import_key_ed25519_openssh_encrypted_wrong_password() {
171        let private_key = include_str!("../resources/import/ed25519_openssh_encrypted");
172        let result = import_key(private_key.to_string(), Some("wrongpassword".to_string()));
173        assert_eq!(result.unwrap_err(), SshKeyImportError::WrongPassword);
174    }
175
176    #[test]
177    fn import_non_key_error() {
178        let result = import_key("not a key".to_string(), Some("".to_string()));
179        assert_eq!(result.unwrap_err(), SshKeyImportError::ParsingError);
180    }
181
182    #[test]
183    fn import_wrong_label_error() {
184        let private_key = include_str!("../resources/import/wrong_label");
185        let result = import_key(private_key.to_string(), Some("".to_string()));
186        assert_eq!(result.unwrap_err(), SshKeyImportError::UnsupportedKeyType);
187    }
188
189    #[test]
190    fn import_ecdsa_error() {
191        let private_key = include_str!("../resources/import/ecdsa_openssh_unencrypted");
192        let result = import_key(private_key.to_string(), Some("".to_string()));
193        assert_eq!(result.unwrap_err(), SshKeyImportError::UnsupportedKeyType);
194    }
195
196    // Putty-exported keys should be supported, but are not due to a parser incompatibility.
197    // Should this test start failing, please change it to expect a correct key, and
198    // make sure the documentation support for putty-exported keys this is updated.
199    // https://bitwarden.atlassian.net/browse/PM-14989
200    #[test]
201    fn import_key_ed25519_putty() {
202        let private_key = include_str!("../resources/import/ed25519_putty_openssh_unencrypted");
203        let result = import_key(private_key.to_string(), Some("".to_string()));
204        assert_eq!(result.unwrap_err(), SshKeyImportError::ParsingError);
205    }
206
207    // Putty-exported keys should be supported, but are not due to a parser incompatibility.
208    // Should this test start failing, please change it to expect a correct key, and
209    // make sure the documentation support for putty-exported keys this is updated.
210    // https://bitwarden.atlassian.net/browse/PM-14989
211    #[test]
212    fn import_key_rsa_openssh_putty() {
213        let private_key = include_str!("../resources/import/rsa_putty_openssh_unencrypted");
214        let result = import_key(private_key.to_string(), Some("".to_string()));
215        assert_eq!(result.unwrap_err(), SshKeyImportError::ParsingError);
216    }
217
218    #[test]
219    fn import_key_rsa_pkcs8_putty() {
220        let private_key = include_str!("../resources/import/rsa_putty_pkcs1_unencrypted");
221        let result = import_key(private_key.to_string(), Some("".to_string()));
222        assert_eq!(result.unwrap_err(), SshKeyImportError::UnsupportedKeyType);
223    }
224}