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
11pub 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
57pub 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 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 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}