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 match parse_pkcs8_pem(&encoded_key, password.as_deref()) {
42 Err(SshKeyImportError::Parsing) => {
46 let rewrapped = rewrap_pem(&encoded_key).ok_or(SshKeyImportError::Parsing)?;
47 parse_pkcs8_pem(&rewrapped, password.as_deref())
48 }
49 result => result,
50 }
51}
52
53fn parse_pkcs8_pem(
54 encoded_key: &str,
55 password: Option<&str>,
56) -> Result<SshKeyView, SshKeyImportError> {
57 let doc = if let Some(password) = password {
58 SecretDocument::from_pkcs8_encrypted_pem(encoded_key, password.as_bytes()).map_err(
59 |err| match err {
60 pkcs8::Error::EncryptedPrivateKey(pkcs5::Error::DecryptFailed) => {
61 SshKeyImportError::WrongPassword
62 }
63 _ => SshKeyImportError::Parsing,
64 },
65 )?
66 } else {
67 SecretDocument::from_pkcs8_pem(encoded_key).map_err(|_| SshKeyImportError::Parsing)?
68 };
69
70 import_pkcs8_der_key(doc.as_bytes())
71}
72
73fn rewrap_pem(pem: &str) -> Option<String> {
79 let mut lines = pem.lines();
80
81 let header = lines
82 .by_ref()
83 .find(|line| line.starts_with("-----BEGIN "))?;
84
85 let mut body = String::new();
87 let mut footer = None;
88 for line in lines.by_ref() {
89 if line.starts_with("-----END ") {
90 footer = Some(line);
91 break;
92 }
93 body.extend(line.split_whitespace());
94 }
95 let footer = footer?;
96
97 let mut out = String::with_capacity(body.len() + body.len() / 64 + header.len() + 16);
98 out.push_str(header);
99 out.push('\n');
100 let mut chars = body.chars();
102 loop {
103 let chunk: String = chars.by_ref().take(64).collect();
104 if chunk.is_empty() {
105 break;
106 }
107 out.push_str(&chunk);
108 out.push('\n');
109 }
110 out.push_str(footer);
111 out.push('\n');
112
113 Some(out)
114}
115
116pub fn import_pkcs8_der_key(encoded_key: &[u8]) -> Result<SshKeyView, SshKeyImportError> {
119 let private_key_info =
120 PrivateKeyInfo::from_der(encoded_key).map_err(|_| SshKeyImportError::Parsing)?;
121
122 let private_key = match private_key_info.algorithm.oid {
123 ed25519::pkcs8::ALGORITHM_OID => {
124 let private_key: ed25519::KeypairBytes = private_key_info
125 .try_into()
126 .map_err(|_| SshKeyImportError::Parsing)?;
127
128 ssh_key::private::PrivateKey::from(Ed25519Keypair::from(&private_key.secret_key.into()))
129 }
130 rsa::pkcs1::ALGORITHM_OID => {
131 let private_key: rsa::RsaPrivateKey = private_key_info
132 .try_into()
133 .map_err(|_| SshKeyImportError::Parsing)?;
134
135 ssh_key::private::PrivateKey::from(
136 RsaKeypair::try_from(private_key).map_err(|_| SshKeyImportError::Parsing)?,
137 )
138 }
139 #[cfg(feature = "ecdsa-keys")]
140 sec1::ALGORITHM_OID => import_ecdsa_pkcs8_der(encoded_key)?,
141 _ => return Err(SshKeyImportError::UnsupportedKeyType),
142 };
143
144 ssh_private_key_to_view(private_key).map_err(|_| SshKeyImportError::Parsing)
145}
146
147fn import_openssh_key(
148 encoded_key: String,
149 password: Option<String>,
150) -> Result<SshKeyView, SshKeyImportError> {
151 let private_key =
152 ssh_key::private::PrivateKey::from_openssh(&encoded_key).map_err(|err| match err {
153 ssh_key::Error::AlgorithmUnknown | ssh_key::Error::AlgorithmUnsupported { .. } => {
154 SshKeyImportError::UnsupportedKeyType
155 }
156 _ => SshKeyImportError::Parsing,
157 })?;
158
159 let private_key = if private_key.is_encrypted() {
160 let password = password.ok_or(SshKeyImportError::PasswordRequired)?;
161 private_key
162 .decrypt(password.as_bytes())
163 .map_err(|_| SshKeyImportError::WrongPassword)?
164 } else {
165 private_key
166 };
167
168 reject_ecdsa_import(&private_key)?;
169
170 ssh_private_key_to_view(private_key).map_err(|_| SshKeyImportError::Parsing)
171}
172
173fn reject_ecdsa_import(key: &ssh_key::PrivateKey) -> Result<(), SshKeyImportError> {
174 #[cfg(not(feature = "ecdsa-keys"))]
175 if matches!(key.key_data(), ssh_key::private::KeypairData::Ecdsa(_)) {
176 return Err(SshKeyImportError::UnsupportedKeyType);
177 }
178 let _ = key;
179 Ok(())
180}
181
182#[cfg(feature = "ecdsa-keys")]
183fn import_ecdsa_pkcs8_der(encoded_key: &[u8]) -> Result<ssh_key::PrivateKey, SshKeyImportError> {
184 use pkcs8::DecodePrivateKey as _;
185
186 if let Ok(sk) = p256::SecretKey::from_pkcs8_der(encoded_key) {
187 let public_key = sk.public_key();
188 let keypair = ssh_key::private::EcdsaKeypair::NistP256 {
189 public: public_key.into(),
190 private: ssh_key::private::EcdsaPrivateKey::from(sk),
191 };
192 return ssh_key::PrivateKey::new(ssh_key::private::KeypairData::Ecdsa(keypair), "")
193 .map_err(|_| SshKeyImportError::Parsing);
194 }
195 if let Ok(sk) = p384::SecretKey::from_pkcs8_der(encoded_key) {
196 let public_key = sk.public_key();
197 let keypair = ssh_key::private::EcdsaKeypair::NistP384 {
198 public: public_key.into(),
199 private: ssh_key::private::EcdsaPrivateKey::from(sk),
200 };
201 return ssh_key::PrivateKey::new(ssh_key::private::KeypairData::Ecdsa(keypair), "")
202 .map_err(|_| SshKeyImportError::Parsing);
203 }
204 if let Ok(sk) = p521::SecretKey::from_pkcs8_der(encoded_key) {
205 let public_key = sk.public_key();
206 let keypair = ssh_key::private::EcdsaKeypair::NistP521 {
207 public: public_key.into(),
208 private: ssh_key::private::EcdsaPrivateKey::from(sk),
209 };
210 return ssh_key::PrivateKey::new(ssh_key::private::KeypairData::Ecdsa(keypair), "")
211 .map_err(|_| SshKeyImportError::Parsing);
212 }
213 Err(SshKeyImportError::UnsupportedKeyType)
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 #[test]
221 fn import_key_ed25519_openssh_unencrypted() {
222 let private_key = include_str!("../resources/import/ed25519_openssh_unencrypted");
223 let public_key = include_str!("../resources/import/ed25519_openssh_unencrypted.pub").trim();
224 let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
225 assert_eq!(result.public_key, public_key);
226 }
227
228 #[test]
229 fn import_key_ed25519_openssh_encrypted() {
230 let private_key = include_str!("../resources/import/ed25519_openssh_encrypted");
231 let public_key = include_str!("../resources/import/ed25519_openssh_encrypted.pub").trim();
232 let result = import_key(private_key.to_string(), Some("password".to_string())).unwrap();
233 assert_eq!(result.public_key, public_key);
234 }
235
236 #[test]
237 fn import_key_rsa_openssh_unencrypted() {
238 let private_key = include_str!("../resources/import/rsa_openssh_unencrypted");
239 let public_key = include_str!("../resources/import/rsa_openssh_unencrypted.pub").trim();
240 let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
241 assert_eq!(result.public_key, public_key);
242 }
243
244 #[test]
245 fn import_key_rsa_openssh_encrypted() {
246 let private_key = include_str!("../resources/import/rsa_openssh_encrypted");
247 let public_key = include_str!("../resources/import/rsa_openssh_encrypted.pub").trim();
248 let result = import_key(private_key.to_string(), Some("password".to_string())).unwrap();
249 assert_eq!(result.public_key, public_key);
250 }
251
252 #[test]
253 fn import_key_ed25519_pkcs8_unencrypted() {
254 let private_key = include_str!("../resources/import/ed25519_pkcs8_unencrypted");
255 let public_key = include_str!("../resources/import/ed25519_pkcs8_unencrypted.pub")
256 .replace("testkey", "");
257 let public_key = public_key.trim();
258 let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
259 assert_eq!(result.public_key, public_key);
260 }
261
262 #[test]
263 fn import_key_rsa_pkcs8_unencrypted() {
264 let private_key = include_str!("../resources/import/rsa_pkcs8_unencrypted");
265 let public_key =
267 include_str!("../resources/import/rsa_pkcs8_unencrypted.pub").replace("testkey", "");
268 let public_key = public_key.trim();
269 let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
270 assert_eq!(result.public_key, public_key);
271 }
272
273 #[test]
274 fn import_key_rsa_pkcs8_encrypted() {
275 let private_key = include_str!("../resources/import/rsa_pkcs8_encrypted");
276 let public_key =
277 include_str!("../resources/import/rsa_pkcs8_encrypted.pub").replace("testkey", "");
278 let public_key = public_key.trim();
279 let result = import_key(private_key.to_string(), Some("password".to_string())).unwrap();
280 assert_eq!(result.public_key, public_key);
281 }
282
283 #[test]
284 fn import_key_ed25519_openssh_encrypted_wrong_password() {
285 let private_key = include_str!("../resources/import/ed25519_openssh_encrypted");
286 let result = import_key(private_key.to_string(), Some("wrongpassword".to_string()));
287 assert_eq!(result.unwrap_err(), SshKeyImportError::WrongPassword);
288 }
289
290 #[test]
295 fn import_key_ed25519_pkcs8_unencrypted_single_line() {
296 let private_key =
299 include_str!("../resources/import/ed25519_pkcs8_1password_single_line_unencrypted");
300 let public_key = include_str!("../resources/import/ed25519_pkcs8_unencrypted.pub").trim();
301
302 let result = import_key(private_key.to_string(), None).unwrap();
303 assert_eq!(result.public_key, public_key);
304 }
305
306 #[test]
307 fn import_non_key_error() {
308 let result = import_key("not a key".to_string(), Some("".to_string()));
309 assert_eq!(result.unwrap_err(), SshKeyImportError::Parsing);
310 }
311
312 #[test]
313 fn import_wrong_label_error() {
314 let private_key = include_str!("../resources/import/wrong_label");
315 let result = import_key(private_key.to_string(), Some("".to_string()));
316 assert_eq!(result.unwrap_err(), SshKeyImportError::UnsupportedKeyType);
317 }
318
319 #[cfg(not(feature = "ecdsa-keys"))]
320 #[test]
321 fn import_ecdsa_blocked() {
322 let private_key = include_str!("../resources/import/ecdsa_openssh_unencrypted");
323 let result = import_key(private_key.to_string(), Some("".to_string()));
324 assert_eq!(result.unwrap_err(), SshKeyImportError::UnsupportedKeyType);
325 }
326
327 #[cfg(feature = "ecdsa-keys")]
328 #[test]
329 fn import_ecdsa_p256_openssh_unencrypted() {
330 let private_key = include_str!("../resources/import/ecdsa_openssh_unencrypted");
331 let public_key = include_str!("../resources/import/ecdsa_openssh_unencrypted.pub").trim();
332 let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
333 assert_eq!(result.public_key, public_key);
334 }
335
336 #[cfg(feature = "ecdsa-keys")]
337 #[test]
338 fn import_ecdsa_p384_openssh_unencrypted() {
339 let private_key = include_str!("../resources/import/ecdsa_p384_openssh_unencrypted");
340 let public_key =
341 include_str!("../resources/import/ecdsa_p384_openssh_unencrypted.pub").trim();
342 let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
343 assert_eq!(result.public_key, public_key);
344 }
345
346 #[cfg(feature = "ecdsa-keys")]
347 #[test]
348 fn import_ecdsa_p521_openssh_unencrypted() {
349 let private_key = include_str!("../resources/import/ecdsa_p521_openssh_unencrypted");
350 let public_key =
351 include_str!("../resources/import/ecdsa_p521_openssh_unencrypted.pub").trim();
352 let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
353 assert_eq!(result.public_key, public_key);
354 }
355
356 #[test]
357 fn import_key_ed25519_putty() {
358 let private_key = include_str!("../resources/import/ed25519_putty_openssh_unencrypted");
359 let public_key =
360 include_str!("../resources/import/ed25519_putty_openssh_unencrypted.pub").trim();
361 let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
362 assert_eq!(result.public_key, public_key);
363 }
364
365 #[test]
366 fn import_key_rsa_openssh_putty() {
367 let private_key = include_str!("../resources/import/rsa_putty_openssh_unencrypted");
368 let public_key =
369 include_str!("../resources/import/rsa_putty_openssh_unencrypted.pub").trim();
370 let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
371 assert_eq!(result.public_key, public_key);
372 }
373
374 #[test]
375 fn import_key_rsa_pkcs8_putty() {
376 let private_key = include_str!("../resources/import/rsa_putty_pkcs1_unencrypted");
377 let result = import_key(private_key.to_string(), Some("".to_string()));
378 assert_eq!(result.unwrap_err(), SshKeyImportError::UnsupportedKeyType);
379 }
380
381 #[test]
382 fn import_ed25519_key_regression_17028() {
383 let private_key = include_str!("../resources/import/ed25519_regression_17028");
385 let public_key = include_str!("../resources/import/ed25519_regression_17028.pub").trim();
386 let result = import_key(private_key.to_string(), None).unwrap();
387 assert_eq!(result.public_key, public_key);
388 }
389}