1use bitwarden_vault::SshKeyView;
2use ed25519;
3use pem_rfc7468::PemLabel;
4use pkcs8::{DecodePrivateKey, PrivateKeyInfo, SecretDocument, der::Decode, pkcs5};
5use ssh_key::{
6 private::{Ed25519Keypair, RsaKeypair},
7 sec1,
8};
9
10use crate::{error::SshKeyImportError, ssh_private_key_to_view};
11
12pub fn import_key(
21 encoded_key: String,
22 password: Option<String>,
23) -> Result<SshKeyView, SshKeyImportError> {
24 let label = pem_rfc7468::decode_label(encoded_key.as_bytes())
25 .map_err(|_| SshKeyImportError::Parsing)?;
26
27 match label {
28 pkcs8::PrivateKeyInfo::<(), (), ()>::PEM_LABEL => import_pkcs8_key(encoded_key, None),
29 pkcs8::EncryptedPrivateKeyInfo::<()>::PEM_LABEL => import_pkcs8_key(
30 encoded_key,
31 Some(password.ok_or(SshKeyImportError::PasswordRequired)?),
32 ),
33 ssh_key::PrivateKey::PEM_LABEL => import_openssh_key(encoded_key, password),
34 _ => Err(SshKeyImportError::UnsupportedKeyType),
35 }
36}
37
38fn import_pkcs8_key(
39 encoded_key: String,
40 password: Option<String>,
41) -> Result<SshKeyView, SshKeyImportError> {
42 match parse_pkcs8_pem(&encoded_key, password.as_deref()) {
43 Err(SshKeyImportError::Parsing) => {
47 let rewrapped = rewrap_pem(&encoded_key).ok_or(SshKeyImportError::Parsing)?;
48 parse_pkcs8_pem(&rewrapped, password.as_deref())
49 }
50 result => result,
51 }
52}
53
54fn parse_pkcs8_pem(
55 encoded_key: &str,
56 password: Option<&str>,
57) -> Result<SshKeyView, SshKeyImportError> {
58 let doc = if let Some(password) = password {
59 SecretDocument::from_pkcs8_encrypted_pem(encoded_key, password.as_bytes()).map_err(
60 |err| match err {
61 pkcs8::Error::EncryptedPrivateKey(pkcs5::Error::DecryptFailed) => {
62 SshKeyImportError::WrongPassword
63 }
64 _ => SshKeyImportError::Parsing,
65 },
66 )?
67 } else {
68 SecretDocument::from_pkcs8_pem(encoded_key).map_err(|_| SshKeyImportError::Parsing)?
69 };
70
71 import_pkcs8_der_key(doc.as_bytes())
72}
73
74fn rewrap_pem(pem: &str) -> Option<String> {
80 let mut lines = pem.lines();
81
82 let header = lines
83 .by_ref()
84 .find(|line| line.starts_with("-----BEGIN "))?;
85
86 let mut body = String::new();
88 let mut footer = None;
89 for line in lines.by_ref() {
90 if line.starts_with("-----END ") {
91 footer = Some(line);
92 break;
93 }
94 body.extend(line.split_whitespace());
95 }
96 let footer = footer?;
97
98 let mut out = String::with_capacity(body.len() + body.len() / 64 + header.len() + 16);
99 out.push_str(header);
100 out.push('\n');
101 let mut chars = body.chars();
103 loop {
104 let chunk: String = chars.by_ref().take(64).collect();
105 if chunk.is_empty() {
106 break;
107 }
108 out.push_str(&chunk);
109 out.push('\n');
110 }
111 out.push_str(footer);
112 out.push('\n');
113
114 Some(out)
115}
116
117pub fn import_pkcs8_der_key(encoded_key: &[u8]) -> Result<SshKeyView, SshKeyImportError> {
120 let private_key_info =
121 PrivateKeyInfo::from_der(encoded_key).map_err(|_| SshKeyImportError::Parsing)?;
122
123 let private_key = match private_key_info.algorithm.oid {
124 ed25519::pkcs8::ALGORITHM_OID => {
125 let private_key: ed25519::KeypairBytes = private_key_info
126 .try_into()
127 .map_err(|_| SshKeyImportError::Parsing)?;
128
129 ssh_key::private::PrivateKey::from(Ed25519Keypair::from(&private_key.secret_key.into()))
130 }
131 rsa::pkcs1::ALGORITHM_OID => {
132 let private_key: rsa::RsaPrivateKey = private_key_info
133 .try_into()
134 .map_err(|_| SshKeyImportError::Parsing)?;
135
136 ssh_key::private::PrivateKey::from(
137 RsaKeypair::try_from(private_key).map_err(|_| SshKeyImportError::Parsing)?,
138 )
139 }
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 ssh_private_key_to_view(private_key).map_err(|_| SshKeyImportError::Parsing)
169}
170
171fn import_ecdsa_pkcs8_der(encoded_key: &[u8]) -> Result<ssh_key::PrivateKey, SshKeyImportError> {
172 use pkcs8::DecodePrivateKey as _;
173
174 if let Ok(sk) = p256::SecretKey::from_pkcs8_der(encoded_key) {
175 let public_key = sk.public_key();
176 let keypair = ssh_key::private::EcdsaKeypair::NistP256 {
177 public: public_key.into(),
178 private: ssh_key::private::EcdsaPrivateKey::from(sk),
179 };
180 return ssh_key::PrivateKey::new(ssh_key::private::KeypairData::Ecdsa(keypair), "")
181 .map_err(|_| SshKeyImportError::Parsing);
182 }
183 if let Ok(sk) = p384::SecretKey::from_pkcs8_der(encoded_key) {
184 let public_key = sk.public_key();
185 let keypair = ssh_key::private::EcdsaKeypair::NistP384 {
186 public: public_key.into(),
187 private: ssh_key::private::EcdsaPrivateKey::from(sk),
188 };
189 return ssh_key::PrivateKey::new(ssh_key::private::KeypairData::Ecdsa(keypair), "")
190 .map_err(|_| SshKeyImportError::Parsing);
191 }
192 if let Ok(sk) = p521::SecretKey::from_pkcs8_der(encoded_key) {
193 let public_key = sk.public_key();
194 let keypair = ssh_key::private::EcdsaKeypair::NistP521 {
195 public: public_key.into(),
196 private: ssh_key::private::EcdsaPrivateKey::from(sk),
197 };
198 return ssh_key::PrivateKey::new(ssh_key::private::KeypairData::Ecdsa(keypair), "")
199 .map_err(|_| SshKeyImportError::Parsing);
200 }
201 Err(SshKeyImportError::UnsupportedKeyType)
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 #[test]
209 fn import_key_ed25519_openssh_unencrypted() {
210 let private_key = include_str!("../resources/import/ed25519_openssh_unencrypted");
211 let public_key = include_str!("../resources/import/ed25519_openssh_unencrypted.pub").trim();
212 let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
213 assert_eq!(result.public_key, public_key);
214 }
215
216 #[test]
217 fn import_key_ed25519_openssh_encrypted() {
218 let private_key = include_str!("../resources/import/ed25519_openssh_encrypted");
219 let public_key = include_str!("../resources/import/ed25519_openssh_encrypted.pub").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_rsa_openssh_unencrypted() {
226 let private_key = include_str!("../resources/import/rsa_openssh_unencrypted");
227 let public_key = include_str!("../resources/import/rsa_openssh_unencrypted.pub").trim();
228 let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
229 assert_eq!(result.public_key, public_key);
230 }
231
232 #[test]
233 fn import_key_rsa_openssh_encrypted() {
234 let private_key = include_str!("../resources/import/rsa_openssh_encrypted");
235 let public_key = include_str!("../resources/import/rsa_openssh_encrypted.pub").trim();
236 let result = import_key(private_key.to_string(), Some("password".to_string())).unwrap();
237 assert_eq!(result.public_key, public_key);
238 }
239
240 #[test]
241 fn import_key_ed25519_pkcs8_unencrypted() {
242 let private_key = include_str!("../resources/import/ed25519_pkcs8_unencrypted");
243 let public_key = include_str!("../resources/import/ed25519_pkcs8_unencrypted.pub")
244 .replace("testkey", "");
245 let public_key = public_key.trim();
246 let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
247 assert_eq!(result.public_key, public_key);
248 }
249
250 #[test]
251 fn import_key_rsa_pkcs8_unencrypted() {
252 let private_key = include_str!("../resources/import/rsa_pkcs8_unencrypted");
253 let public_key =
255 include_str!("../resources/import/rsa_pkcs8_unencrypted.pub").replace("testkey", "");
256 let public_key = public_key.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 #[test]
262 fn import_key_rsa_pkcs8_encrypted() {
263 let private_key = include_str!("../resources/import/rsa_pkcs8_encrypted");
264 let public_key =
265 include_str!("../resources/import/rsa_pkcs8_encrypted.pub").replace("testkey", "");
266 let public_key = public_key.trim();
267 let result = import_key(private_key.to_string(), Some("password".to_string())).unwrap();
268 assert_eq!(result.public_key, public_key);
269 }
270
271 #[test]
272 fn import_key_ed25519_openssh_encrypted_wrong_password() {
273 let private_key = include_str!("../resources/import/ed25519_openssh_encrypted");
274 let result = import_key(private_key.to_string(), Some("wrongpassword".to_string()));
275 assert_eq!(result.unwrap_err(), SshKeyImportError::WrongPassword);
276 }
277
278 #[test]
283 fn import_key_ed25519_pkcs8_unencrypted_single_line() {
284 let private_key =
287 include_str!("../resources/import/ed25519_pkcs8_1password_single_line_unencrypted");
288 let public_key = include_str!("../resources/import/ed25519_pkcs8_unencrypted.pub").trim();
289
290 let result = import_key(private_key.to_string(), None).unwrap();
291 assert_eq!(result.public_key, public_key);
292 }
293
294 #[test]
295 fn import_non_key_error() {
296 let result = import_key("not a key".to_string(), Some("".to_string()));
297 assert_eq!(result.unwrap_err(), SshKeyImportError::Parsing);
298 }
299
300 #[test]
301 fn import_wrong_label_error() {
302 let private_key = include_str!("../resources/import/wrong_label");
303 let result = import_key(private_key.to_string(), Some("".to_string()));
304 assert_eq!(result.unwrap_err(), SshKeyImportError::UnsupportedKeyType);
305 }
306
307 #[test]
308 fn import_ecdsa_p256_openssh_unencrypted() {
309 let private_key = include_str!("../resources/import/ecdsa_openssh_unencrypted");
310 let public_key = include_str!("../resources/import/ecdsa_openssh_unencrypted.pub").trim();
311 let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
312 assert_eq!(result.public_key, public_key);
313 }
314
315 #[test]
316 fn import_ecdsa_p384_openssh_unencrypted() {
317 let private_key = include_str!("../resources/import/ecdsa_p384_openssh_unencrypted");
318 let public_key =
319 include_str!("../resources/import/ecdsa_p384_openssh_unencrypted.pub").trim();
320 let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
321 assert_eq!(result.public_key, public_key);
322 }
323
324 #[test]
325 fn import_ecdsa_p521_openssh_unencrypted() {
326 let private_key = include_str!("../resources/import/ecdsa_p521_openssh_unencrypted");
327 let public_key =
328 include_str!("../resources/import/ecdsa_p521_openssh_unencrypted.pub").trim();
329 let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
330 assert_eq!(result.public_key, public_key);
331 }
332
333 #[test]
334 fn import_key_ed25519_putty() {
335 let private_key = include_str!("../resources/import/ed25519_putty_openssh_unencrypted");
336 let public_key =
337 include_str!("../resources/import/ed25519_putty_openssh_unencrypted.pub").trim();
338 let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
339 assert_eq!(result.public_key, public_key);
340 }
341
342 #[test]
343 fn import_key_rsa_openssh_putty() {
344 let private_key = include_str!("../resources/import/rsa_putty_openssh_unencrypted");
345 let public_key =
346 include_str!("../resources/import/rsa_putty_openssh_unencrypted.pub").trim();
347 let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
348 assert_eq!(result.public_key, public_key);
349 }
350
351 #[test]
352 fn import_key_rsa_pkcs8_putty() {
353 let private_key = include_str!("../resources/import/rsa_putty_pkcs1_unencrypted");
354 let result = import_key(private_key.to_string(), Some("".to_string()));
355 assert_eq!(result.unwrap_err(), SshKeyImportError::UnsupportedKeyType);
356 }
357
358 #[test]
359 fn import_ed25519_key_regression_17028() {
360 let private_key = include_str!("../resources/import/ed25519_regression_17028");
362 let public_key = include_str!("../resources/import/ed25519_regression_17028.pub").trim();
363 let result = import_key(private_key.to_string(), None).unwrap();
364 assert_eq!(result.public_key, public_key);
365 }
366}