Skip to content

Commit 38ecf13

Browse files
quextenHinton
andauthored
[PM-11924] Add ssh-key item type (#1037)
## 🎟️ Tracking https://bitwarden.atlassian.net/browse/PM-11924 Server PR: https://bitwarden.atlassian.net/browse/PM-10394 ## 📔 Objective Add a new item type for ssh-keys. ## ⏰ Reminders before review - Contributor guidelines followed - All formatters and local linters executed and passed - Written new unit and / or integration tests where applicable - Protected functional changes with optionality (feature flags) - Used internationalization (i18n) for all UI strings - CI builds passed - Communicated to DevOps any deployment requirements - Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team ## 🦮 Reviewer guidelines <!-- Suggested interactions but feel free to use (or not) as you desire! --> - 👍 (`:+1:`) or similar for great changes - 📝 (`:memo:`) or ℹ️ (`:information_source:`) for notes or general info - ❓ (`:question:`) for questions - 🤔 (`:thinking:`) or 💭 (`:thought_balloon:`) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion - 🎨 (`:art:`) for suggestions / improvements - ❌ (`:x:`) or ⚠️ (`:warning:`) for more significant problems or concerns needing attention - 🌱 (`:seedling:`) or ♻️ (`:recycle:`) for future improvements or indications of technical debt - ⛏ (`:pick:`) for minor or nitpick changes --------- Co-authored-by: Oscar Hinton <[email protected]>
1 parent be75311 commit 38ecf13

File tree

9 files changed

+272
-7
lines changed

9 files changed

+272
-7
lines changed

crates/bitwarden-exporters/resources/json_export.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,26 @@
141141
"revisionDate": "2024-01-30T17:54:50.706Z",
142142
"creationDate": "2024-01-30T17:54:50.706Z",
143143
"deletedDate": null
144+
},
145+
{
146+
"id": "646594a9-a9cb-4082-9d57-0024c3fbcaa9",
147+
"folderId": null,
148+
"organizationId": null,
149+
"collectionIds": null,
150+
"name": "My ssh key",
151+
"notes": null,
152+
"type": 5,
153+
"sshKey": {
154+
"privateKey": "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\nQyNTUxOQAAACBinNE5chMtCHh3BV0H1+CpPlEQBwR5cD+Xb9i8MaHGiwAAAKAy48fwMuPH\n8AAAAAtzc2gtZWQyNTUxOQAAACBinNE5chMtCHh3BV0H1+CpPlEQBwR5cD+Xb9i8MaHGiw\nAAAEAYUCIdfLI14K3XIy9V0FDZLQoZ9gcjOnvFjb4uA335HmKc0TlyEy0IeHcFXQfX4Kk+\nURAHBHlwP5dv2LwxocaLAAAAHHF1ZXh0ZW5ATWFjQm9vay1Qcm8tMTYubG9jYWwB\n-----END OPENSSH PRIVATE KEY-----",
155+
"publicKey": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGKc0TlyEy0IeHcFXQfX4Kk+URAHBHlwP5dv2LwxocaL",
156+
"fingerprint": "SHA256:1JjFjvPRkj1Gbf2qRP1dgHiIzEuNAEvp+92x99jw3K0"
157+
},
158+
"favorite": false,
159+
"reprompt": 0,
160+
"passwordHistory": null,
161+
"revisionDate": "2024-01-30T11:25:25.466Z",
162+
"creationDate": "2024-01-30T11:25:25.466Z",
163+
"deletedDate": null
144164
}
145165
]
146166
}

crates/bitwarden-exporters/src/json.rs

Lines changed: 107 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ use chrono::{DateTime, Utc};
22
use thiserror::Error;
33
use uuid::Uuid;
44

5-
use crate::{Card, Cipher, CipherType, Field, Folder, Identity, Login, LoginUri, SecureNote};
5+
use crate::{
6+
Card, Cipher, CipherType, Field, Folder, Identity, Login, LoginUri, SecureNote, SshKey,
7+
};
68

79
#[derive(Error, Debug)]
810
pub enum JsonError {
@@ -69,6 +71,8 @@ struct JsonCipher {
6971
card: Option<JsonCard>,
7072
#[serde(skip_serializing_if = "Option::is_none")]
7173
secure_note: Option<JsonSecureNote>,
74+
#[serde(skip_serializing_if = "Option::is_none")]
75+
ssh_key: Option<JsonSshKey>,
7276

7377
favorite: bool,
7478
reprompt: u8,
@@ -206,6 +210,24 @@ impl From<Identity> for JsonIdentity {
206210
}
207211
}
208212

213+
#[derive(serde::Serialize)]
214+
#[serde(rename_all = "camelCase")]
215+
struct JsonSshKey {
216+
private_key: Option<String>,
217+
public_key: Option<String>,
218+
fingerprint: Option<String>,
219+
}
220+
221+
impl From<SshKey> for JsonSshKey {
222+
fn from(ssh_key: SshKey) -> Self {
223+
JsonSshKey {
224+
private_key: ssh_key.private_key,
225+
public_key: ssh_key.public_key,
226+
fingerprint: ssh_key.fingerprint,
227+
}
228+
}
229+
}
230+
209231
#[derive(serde::Serialize)]
210232
#[serde(rename_all = "camelCase")]
211233
struct JsonField {
@@ -233,13 +255,15 @@ impl From<Cipher> for JsonCipher {
233255
CipherType::SecureNote(_) => 2,
234256
CipherType::Card(_) => 3,
235257
CipherType::Identity(_) => 4,
258+
CipherType::SshKey(_) => 5,
236259
};
237260

238-
let (login, secure_note, card, identity) = match cipher.r#type {
239-
CipherType::Login(l) => (Some((*l).into()), None, None, None),
240-
CipherType::SecureNote(s) => (None, Some((*s).into()), None, None),
241-
CipherType::Card(c) => (None, None, Some((*c).into()), None),
242-
CipherType::Identity(i) => (None, None, None, Some((*i).into())),
261+
let (login, secure_note, card, identity, ssh_key) = match cipher.r#type {
262+
CipherType::Login(l) => (Some((*l).into()), None, None, None, None),
263+
CipherType::SecureNote(s) => (None, Some((*s).into()), None, None, None),
264+
CipherType::Card(c) => (None, None, Some((*c).into()), None, None),
265+
CipherType::Identity(i) => (None, None, None, Some((*i).into()), None),
266+
CipherType::SshKey(ssh) => (None, None, None, None, Some((*ssh).into())),
243267
};
244268

245269
JsonCipher {
@@ -254,6 +278,7 @@ impl From<Cipher> for JsonCipher {
254278
identity,
255279
card,
256280
secure_note,
281+
ssh_key,
257282
favorite: cipher.favorite,
258283
reprompt: cipher.reprompt,
259284
fields: cipher.fields.into_iter().map(|f| f.into()).collect(),
@@ -594,6 +619,60 @@ mod tests {
594619
)
595620
}
596621

622+
#[test]
623+
fn test_convert_ssh_key() {
624+
let cipher = Cipher {
625+
id: "23f0f877-42b1-4820-a850-b10700bc41eb".parse().unwrap(),
626+
folder_id: None,
627+
628+
name: "My ssh key".to_string(),
629+
notes: None,
630+
631+
r#type: CipherType::SshKey(Box::new(SshKey {
632+
private_key: Some("private".to_string()),
633+
public_key: Some("public".to_string()),
634+
fingerprint: Some("fingerprint".to_string()),
635+
})),
636+
637+
favorite: false,
638+
reprompt: 0,
639+
640+
fields: vec![],
641+
642+
revision_date: "2024-01-30T11:25:25.466Z".parse().unwrap(),
643+
creation_date: "2024-01-30T11:25:25.466Z".parse().unwrap(),
644+
deleted_date: None,
645+
};
646+
647+
let json = serde_json::to_string(&JsonCipher::from(cipher)).unwrap();
648+
649+
let expected = r#"{
650+
"passwordHistory": null,
651+
"revisionDate": "2024-01-30T11:25:25.466Z",
652+
"creationDate": "2024-01-30T11:25:25.466Z",
653+
"deletedDate": null,
654+
"id": "23f0f877-42b1-4820-a850-b10700bc41eb",
655+
"organizationId": null,
656+
"folderId": null,
657+
"type": 5,
658+
"reprompt": 0,
659+
"name": "My ssh key",
660+
"notes": null,
661+
"sshKey": {
662+
"privateKey": "private",
663+
"publicKey": "public",
664+
"fingerprint": "fingerprint"
665+
},
666+
"favorite": false,
667+
"collectionIds": null
668+
}"#;
669+
670+
assert_eq!(
671+
json.parse::<serde_json::Value>().unwrap(),
672+
expected.parse::<serde_json::Value>().unwrap()
673+
)
674+
}
675+
597676
#[test]
598677
pub fn test_export() {
599678
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
@@ -750,6 +829,28 @@ mod tests {
750829
creation_date: "2024-01-30T17:54:50.706Z".parse().unwrap(),
751830
deleted_date: None,
752831
},
832+
Cipher {
833+
id: "646594a9-a9cb-4082-9d57-0024c3fbcaa9".parse().unwrap(),
834+
folder_id: None,
835+
836+
name: "My ssh key".to_string(),
837+
notes: None,
838+
839+
r#type: CipherType::SshKey(Box::new(SshKey {
840+
private_key: Some("-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\nQyNTUxOQAAACBinNE5chMtCHh3BV0H1+CpPlEQBwR5cD+Xb9i8MaHGiwAAAKAy48fwMuPH\n8AAAAAtzc2gtZWQyNTUxOQAAACBinNE5chMtCHh3BV0H1+CpPlEQBwR5cD+Xb9i8MaHGiw\nAAAEAYUCIdfLI14K3XIy9V0FDZLQoZ9gcjOnvFjb4uA335HmKc0TlyEy0IeHcFXQfX4Kk+\nURAHBHlwP5dv2LwxocaLAAAAHHF1ZXh0ZW5ATWFjQm9vay1Qcm8tMTYubG9jYWwB\n-----END OPENSSH PRIVATE KEY-----".to_string()),
841+
public_key: Some("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGKc0TlyEy0IeHcFXQfX4Kk+URAHBHlwP5dv2LwxocaL".to_string()),
842+
fingerprint: Some("SHA256:1JjFjvPRkj1Gbf2qRP1dgHiIzEuNAEvp+92x99jw3K0".to_string()),
843+
})),
844+
845+
favorite: false,
846+
reprompt: 0,
847+
848+
fields: vec![],
849+
850+
revision_date: "2024-01-30T11:25:25.466Z".parse().unwrap(),
851+
creation_date: "2024-01-30T11:25:25.466Z".parse().unwrap(),
852+
deleted_date: None,
853+
}
753854
],
754855
)
755856
.unwrap();

crates/bitwarden-exporters/src/lib.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ pub enum CipherType {
7070
SecureNote(Box<SecureNote>),
7171
Card(Box<Card>),
7272
Identity(Box<Identity>),
73+
SshKey(Box<SshKey>),
7374
}
7475

7576
impl fmt::Display for CipherType {
@@ -79,6 +80,7 @@ impl fmt::Display for CipherType {
7980
CipherType::SecureNote(_) => write!(f, "note"),
8081
CipherType::Card(_) => write!(f, "card"),
8182
CipherType::Identity(_) => write!(f, "identity"),
83+
CipherType::SshKey(_) => write!(f, "ssh_key"),
8284
}
8385
}
8486
}
@@ -132,3 +134,12 @@ pub struct Identity {
132134
pub passport_number: Option<String>,
133135
pub license_number: Option<String>,
134136
}
137+
138+
pub struct SshKey {
139+
/// [OpenSSH private key](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key), in PEM encoding.
140+
pub private_key: Option<String>,
141+
/// Ssh public key (ed25519/rsa) according to [RFC4253](https://datatracker.ietf.org/doc/html/rfc4253#section-6.6)
142+
pub public_key: Option<String>,
143+
/// SSH fingerprint using SHA256 in the format: `SHA256:BASE64_ENCODED_FINGERPRINT`
144+
pub fingerprint: Option<String>,
145+
}

crates/bitwarden-exporters/src/models.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,14 @@ impl TryFrom<CipherView> for crate::Cipher {
7474
license_number: i.license_number,
7575
}))
7676
}
77+
CipherType::SshKey => {
78+
let s = require!(value.ssh_key);
79+
crate::CipherType::SshKey(Box::new(crate::SshKey {
80+
private_key: s.private_key,
81+
public_key: s.public_key,
82+
fingerprint: s.fingerprint,
83+
}))
84+
}
7785
};
7886

7987
Ok(Self {
@@ -172,6 +180,7 @@ mod tests {
172180
identity: None,
173181
card: None,
174182
secure_note: None,
183+
ssh_key: None,
175184
favorite: false,
176185
reprompt: CipherRepromptType::None,
177186
organization_use_totp: true,

crates/bitwarden-vault/src/cipher/attachment.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ mod tests {
208208
identity: None,
209209
card: None,
210210
secure_note: None,
211+
ssh_key: None,
211212
favorite: false,
212213
reprompt: CipherRepromptType::None,
213214
organization_use_totp: false,
@@ -258,6 +259,7 @@ mod tests {
258259
identity: None,
259260
card: None,
260261
secure_note: None,
262+
ssh_key: None,
261263
favorite: false,
262264
reprompt: CipherRepromptType::None,
263265
organization_use_totp: false,
@@ -312,6 +314,7 @@ mod tests {
312314
identity: None,
313315
card: None,
314316
secure_note: None,
317+
ssh_key: None,
315318
favorite: false,
316319
reprompt: CipherRepromptType::None,
317320
organization_use_totp: false,

0 commit comments

Comments
 (0)