Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking โ€œSign up for GitHubโ€, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM-11924] Add ssh-key item type #1037

Merged
merged 20 commits into from
Oct 21, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions crates/bitwarden-exporters/resources/json_export.json
Original file line number Diff line number Diff line change
@@ -141,6 +141,26 @@
"revisionDate": "2024-01-30T17:54:50.706Z",
"creationDate": "2024-01-30T17:54:50.706Z",
"deletedDate": null
},
{
"id": "646594a9-a9cb-4082-9d57-0024c3fbcaa9",
"folderId": null,
"organizationId": null,
"collectionIds": null,
"name": "My ssh key",
"notes": null,
"type": 5,
"sshKey": {
"privateKey": "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\nQyNTUxOQAAACBinNE5chMtCHh3BV0H1+CpPlEQBwR5cD+Xb9i8MaHGiwAAAKAy48fwMuPH\n8AAAAAtzc2gtZWQyNTUxOQAAACBinNE5chMtCHh3BV0H1+CpPlEQBwR5cD+Xb9i8MaHGiw\nAAAEAYUCIdfLI14K3XIy9V0FDZLQoZ9gcjOnvFjb4uA335HmKc0TlyEy0IeHcFXQfX4Kk+\nURAHBHlwP5dv2LwxocaLAAAAHHF1ZXh0ZW5ATWFjQm9vay1Qcm8tMTYubG9jYWwB\n-----END OPENSSH PRIVATE KEY-----",
quexten marked this conversation as resolved.
Show resolved Hide resolved
"publicKey": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGKc0TlyEy0IeHcFXQfX4Kk+URAHBHlwP5dv2LwxocaL",
"fingerprint": "SHA256:1JjFjvPRkj1Gbf2qRP1dgHiIzEuNAEvp+92x99jw3K0"
},
"favorite": false,
"reprompt": 0,
"passwordHistory": null,
"revisionDate": "2024-01-30T11:25:25.466Z",
"creationDate": "2024-01-30T11:25:25.466Z",
"deletedDate": null
}
]
}
113 changes: 107 additions & 6 deletions crates/bitwarden-exporters/src/json.rs
Original file line number Diff line number Diff line change
@@ -2,7 +2,9 @@ use chrono::{DateTime, Utc};
use thiserror::Error;
use uuid::Uuid;

use crate::{Card, Cipher, CipherType, Field, Folder, Identity, Login, LoginUri, SecureNote};
use crate::{
Card, Cipher, CipherType, Field, Folder, Identity, Login, LoginUri, SecureNote, SshKey,
};

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

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

#[derive(serde::Serialize)]
#[serde(rename_all = "camelCase")]
struct JsonSshKey {
private_key: Option<String>,
public_key: Option<String>,
fingerprint: Option<String>,
}

impl From<SshKey> for JsonSshKey {
fn from(ssh_key: SshKey) -> Self {
JsonSshKey {
private_key: ssh_key.private_key,
public_key: ssh_key.public_key,
fingerprint: ssh_key.fingerprint,
}
}
}

#[derive(serde::Serialize)]
#[serde(rename_all = "camelCase")]
struct JsonField {
@@ -233,13 +255,15 @@ impl From<Cipher> for JsonCipher {
CipherType::SecureNote(_) => 2,
CipherType::Card(_) => 3,
CipherType::Identity(_) => 4,
CipherType::SshKey(_) => 5,
};

let (login, secure_note, card, identity) = match cipher.r#type {
CipherType::Login(l) => (Some((*l).into()), None, None, None),
CipherType::SecureNote(s) => (None, Some((*s).into()), None, None),
CipherType::Card(c) => (None, None, Some((*c).into()), None),
CipherType::Identity(i) => (None, None, None, Some((*i).into())),
let (login, secure_note, card, identity, ssh_key) = match cipher.r#type {
CipherType::Login(l) => (Some((*l).into()), None, None, None, None),
CipherType::SecureNote(s) => (None, Some((*s).into()), None, None, None),
CipherType::Card(c) => (None, None, Some((*c).into()), None, None),
CipherType::Identity(i) => (None, None, None, Some((*i).into()), None),
CipherType::SshKey(ssh) => (None, None, None, None, Some((*ssh).into())),
};

JsonCipher {
@@ -254,6 +278,7 @@ impl From<Cipher> for JsonCipher {
identity,
card,
secure_note,
ssh_key,
favorite: cipher.favorite,
reprompt: cipher.reprompt,
fields: cipher.fields.into_iter().map(|f| f.into()).collect(),
@@ -594,6 +619,60 @@ mod tests {
)
}

#[test]
fn test_convert_ssh_key() {
let cipher = Cipher {
id: "23f0f877-42b1-4820-a850-b10700bc41eb".parse().unwrap(),
folder_id: None,

name: "My ssh key".to_string(),
notes: None,

r#type: CipherType::SshKey(Box::new(SshKey {
private_key: Some("private".to_string()),
public_key: Some("public".to_string()),
fingerprint: Some("fingerprint".to_string()),
})),

favorite: false,
reprompt: 0,

fields: vec![],

revision_date: "2024-01-30T11:25:25.466Z".parse().unwrap(),
creation_date: "2024-01-30T11:25:25.466Z".parse().unwrap(),
deleted_date: None,
};

let json = serde_json::to_string(&JsonCipher::from(cipher)).unwrap();

let expected = r#"{
"passwordHistory": null,
"revisionDate": "2024-01-30T11:25:25.466Z",
"creationDate": "2024-01-30T11:25:25.466Z",
"deletedDate": null,
"id": "23f0f877-42b1-4820-a850-b10700bc41eb",
"organizationId": null,
"folderId": null,
"type": 5,
"reprompt": 0,
"name": "My ssh key",
"notes": null,
"sshKey": {
"privateKey": "private",
"publicKey": "public",
"fingerprint": "fingerprint"
},
"favorite": false,
"collectionIds": null
}"#;

assert_eq!(
json.parse::<serde_json::Value>().unwrap(),
expected.parse::<serde_json::Value>().unwrap()
)
}

#[test]
pub fn test_export() {
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
@@ -750,6 +829,28 @@ mod tests {
creation_date: "2024-01-30T17:54:50.706Z".parse().unwrap(),
deleted_date: None,
},
Cipher {
id: "646594a9-a9cb-4082-9d57-0024c3fbcaa9".parse().unwrap(),
folder_id: None,

name: "My ssh key".to_string(),
notes: None,

r#type: CipherType::SshKey(Box::new(SshKey {
private_key: Some("-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\nQyNTUxOQAAACBinNE5chMtCHh3BV0H1+CpPlEQBwR5cD+Xb9i8MaHGiwAAAKAy48fwMuPH\n8AAAAAtzc2gtZWQyNTUxOQAAACBinNE5chMtCHh3BV0H1+CpPlEQBwR5cD+Xb9i8MaHGiw\nAAAEAYUCIdfLI14K3XIy9V0FDZLQoZ9gcjOnvFjb4uA335HmKc0TlyEy0IeHcFXQfX4Kk+\nURAHBHlwP5dv2LwxocaLAAAAHHF1ZXh0ZW5ATWFjQm9vay1Qcm8tMTYubG9jYWwB\n-----END OPENSSH PRIVATE KEY-----".to_string()),
public_key: Some("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGKc0TlyEy0IeHcFXQfX4Kk+URAHBHlwP5dv2LwxocaL".to_string()),
fingerprint: Some("SHA256:1JjFjvPRkj1Gbf2qRP1dgHiIzEuNAEvp+92x99jw3K0".to_string()),
})),

favorite: false,
reprompt: 0,

fields: vec![],

revision_date: "2024-01-30T11:25:25.466Z".parse().unwrap(),
creation_date: "2024-01-30T11:25:25.466Z".parse().unwrap(),
deleted_date: None,
}
],
)
.unwrap();
11 changes: 11 additions & 0 deletions crates/bitwarden-exporters/src/lib.rs
Original file line number Diff line number Diff line change
@@ -70,6 +70,7 @@
SecureNote(Box<SecureNote>),
Card(Box<Card>),
Identity(Box<Identity>),
SshKey(Box<SshKey>),
}

impl fmt::Display for CipherType {
@@ -79,6 +80,7 @@
CipherType::SecureNote(_) => write!(f, "note"),
CipherType::Card(_) => write!(f, "card"),
CipherType::Identity(_) => write!(f, "identity"),
CipherType::SshKey(_) => write!(f, "ssh_key"),

Check warning on line 83 in crates/bitwarden-exporters/src/lib.rs

Codecov / codecov/patch

crates/bitwarden-exporters/src/lib.rs#L83

Added line #L83 was not covered by tests
}
}
}
@@ -132,3 +134,12 @@
pub passport_number: Option<String>,
pub license_number: Option<String>,
}

pub struct SshKey {
/// [OpenSSH private key](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key), in PEM encoding.
pub private_key: Option<String>,
quexten marked this conversation as resolved.
Show resolved Hide resolved
/// Ssh public key (ed25519/rsa) according to [RFC4253](https://datatracker.ietf.org/doc/html/rfc4253#section-6.6)
pub public_key: Option<String>,
/// SSH fingerprint using SHA256 in the format: `SHA256:BASE64_ENCODED_FINGERPRINT`
pub fingerprint: Option<String>,
}
9 changes: 9 additions & 0 deletions crates/bitwarden-exporters/src/models.rs
Original file line number Diff line number Diff line change
@@ -74,6 +74,14 @@
license_number: i.license_number,
}))
}
CipherType::SshKey => {
let s = require!(value.ssh_key);
crate::CipherType::SshKey(Box::new(crate::SshKey {
private_key: s.private_key,
public_key: s.public_key,
fingerprint: s.fingerprint,
}))

Check warning on line 83 in crates/bitwarden-exporters/src/models.rs

Codecov / codecov/patch

crates/bitwarden-exporters/src/models.rs#L78-L83

Added lines #L78 - L83 were not covered by tests
}
};

Ok(Self {
@@ -172,6 +180,7 @@
identity: None,
card: None,
secure_note: None,
ssh_key: None,
favorite: false,
reprompt: CipherRepromptType::None,
organization_use_totp: true,
3 changes: 3 additions & 0 deletions crates/bitwarden-vault/src/cipher/attachment.rs
Original file line number Diff line number Diff line change
@@ -208,6 +208,7 @@ mod tests {
identity: None,
card: None,
secure_note: None,
ssh_key: None,
favorite: false,
reprompt: CipherRepromptType::None,
organization_use_totp: false,
@@ -258,6 +259,7 @@ mod tests {
identity: None,
card: None,
secure_note: None,
ssh_key: None,
favorite: false,
reprompt: CipherRepromptType::None,
organization_use_totp: false,
@@ -312,6 +314,7 @@ mod tests {
identity: None,
card: None,
secure_note: None,
ssh_key: None,
favorite: false,
reprompt: CipherRepromptType::None,
organization_use_totp: false,
71 changes: 70 additions & 1 deletion crates/bitwarden-vault/src/cipher/cipher.rs
quexten marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@
use super::{
attachment, card, field, identity,
local_data::{LocalData, LocalDataView},
secure_note,
secure_note, ssh_key,
};
use crate::{
password_history, Fido2CredentialFullView, Fido2CredentialView, Login, LoginView,
@@ -41,6 +41,7 @@
SecureNote = 2,
Card = 3,
Identity = 4,
SshKey = 5,
}

#[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, JsonSchema, PartialEq)]
@@ -72,6 +73,7 @@
pub identity: Option<identity::Identity>,
pub card: Option<card::Card>,
pub secure_note: Option<secure_note::SecureNote>,
pub ssh_key: Option<ssh_key::SshKey>,

pub favorite: bool,
pub reprompt: CipherRepromptType,
@@ -109,6 +111,7 @@
pub identity: Option<identity::IdentityView>,
pub card: Option<card::CardView>,
pub secure_note: Option<secure_note::SecureNoteView>,
pub ssh_key: Option<ssh_key::SshKeyView>,

pub favorite: bool,
pub reprompt: CipherRepromptType,
@@ -137,6 +140,7 @@
SecureNote,
Card,
Identity,
SshKey,
}

#[derive(Serialize, Deserialize, Debug, JsonSchema, PartialEq)]
@@ -211,6 +215,7 @@
identity: self.identity.encrypt_with_key(key)?,
card: self.card.encrypt_with_key(key)?,
secure_note: self.secure_note.encrypt_with_key(key)?,
ssh_key: self.ssh_key.encrypt_with_key(key)?,
favorite: self.favorite,
reprompt: self.reprompt,
organization_use_totp: self.organization_use_totp,
@@ -245,6 +250,7 @@
identity: self.identity.decrypt_with_key(key).ok().flatten(),
card: self.card.decrypt_with_key(key).ok().flatten(),
secure_note: self.secure_note.decrypt_with_key(key).ok().flatten(),
ssh_key: self.ssh_key.decrypt_with_key(key).ok().flatten(),
favorite: self.favorite,
reprompt: self.reprompt,
organization_use_totp: self.organization_use_totp,
@@ -329,6 +335,18 @@
.transpose()?,
)
}
CipherType::SshKey => {
let Some(ssh_key) = &self.ssh_key else {
return Ok(String::new());

Check warning on line 340 in crates/bitwarden-vault/src/cipher/cipher.rs

Codecov / codecov/patch

crates/bitwarden-vault/src/cipher/cipher.rs#L340

Added line #L340 was not covered by tests
};

ssh_key
.fingerprint
.as_ref()
.map(|c| c.decrypt_with_key(key))
.transpose()?
.unwrap_or_default()
}
})
}
}
@@ -553,6 +571,7 @@
CipherType::SecureNote => CipherListViewType::SecureNote,
CipherType::Card => CipherListViewType::Card,
CipherType::Identity => CipherListViewType::Identity,
CipherType::SshKey => CipherListViewType::SshKey,

Check warning on line 574 in crates/bitwarden-vault/src/cipher/cipher.rs

Codecov / codecov/patch

crates/bitwarden-vault/src/cipher/cipher.rs#L574

Added line #L574 was not covered by tests
},
favorite: self.favorite,
reprompt: self.reprompt,
@@ -614,6 +633,8 @@
identity: cipher.identity.map(|i| (*i).try_into()).transpose()?,
card: cipher.card.map(|c| (*c).try_into()).transpose()?,
secure_note: cipher.secure_note.map(|s| (*s).try_into()).transpose()?,
// TODO: add ssh_key when api bindings have been updated
ssh_key: None,

Check warning on line 637 in crates/bitwarden-vault/src/cipher/cipher.rs

Codecov / codecov/patch

crates/bitwarden-vault/src/cipher/cipher.rs#L637

Added line #L637 was not covered by tests
favorite: cipher.favorite.unwrap_or(false),
reprompt: cipher
.reprompt
@@ -650,6 +671,7 @@
bitwarden_api_api::models::CipherType::SecureNote => CipherType::SecureNote,
bitwarden_api_api::models::CipherType::Card => CipherType::Card,
bitwarden_api_api::models::CipherType::Identity => CipherType::Identity,
// TODO: add ssh_key when api bindings have been updated
}
}
}
@@ -669,6 +691,7 @@
use std::collections::HashMap;

use attachment::AttachmentView;
use ssh_key::SshKey;

use super::*;
use crate::Fido2Credential;
@@ -695,6 +718,7 @@
identity: None,
card: None,
secure_note: None,
ssh_key: None,
favorite: false,
reprompt: CipherRepromptType::None,
organization_use_totp: true,
@@ -753,6 +777,7 @@
identity: None,
card: None,
secure_note: None,
ssh_key: None,
favorite: false,
reprompt: CipherRepromptType::None,
organization_use_totp: false,
@@ -1159,4 +1184,48 @@
let subtitle = build_subtitle_identity(first_name, last_name);
assert_eq!(subtitle, "");
}

#[test]
fn test_subtitle_ssh_key() {
let key = "hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe0+G8EwxvW3v1iywVmSl61iwzd17JW5C/ivzxSP2C9h7Tw==".to_string();
let key = SymmetricCryptoKey::try_from(key).unwrap();
let original_subtitle = "SHA256:1JjFjvPRkj1Gbf2qRP1dgHiIzEuNAEvp+92x99jw3K0".to_string();
let fingerprint_encrypted = original_subtitle.to_owned().encrypt_with_key(&key).unwrap();
let ssh_key_cipher = Cipher {
id: Some("090c19ea-a61a-4df6-8963-262b97bc6266".parse().unwrap()),
organization_id: None,
folder_id: None,
collection_ids: vec![],
r#type: CipherType::SshKey,
key: None,
name: "My test ssh key"
.to_string()
.encrypt_with_key(&key)
.unwrap(),
notes: None,
login: None,
identity: None,
card: None,
secure_note: None,
ssh_key: Some(SshKey {
private_key: None,
public_key: None,
fingerprint: Some(fingerprint_encrypted),
}),
favorite: false,
reprompt: CipherRepromptType::None,
organization_use_totp: false,
edit: true,
view_password: true,
local_data: None,
attachments: None,
fields: None,
password_history: None,
creation_date: "2024-01-01T00:00:00.000Z".parse().unwrap(),
deleted_date: None,
revision_date: "2024-01-01T00:00:00.000Z".parse().unwrap(),
};
let subtitle = ssh_key_cipher.get_decrypted_subtitle(&key).unwrap();
assert_eq!(subtitle, original_subtitle);
}
}
1 change: 1 addition & 0 deletions crates/bitwarden-vault/src/cipher/mod.rs
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@ pub(crate) mod linked_id;
pub(crate) mod local_data;
pub(crate) mod login;
pub(crate) mod secure_note;
pub(crate) mod ssh_key;

pub use attachment::{
Attachment, AttachmentEncryptResult, AttachmentFile, AttachmentFileView, AttachmentView,
49 changes: 49 additions & 0 deletions crates/bitwarden-vault/src/cipher/ssh_key.rs
quexten marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
use bitwarden_crypto::{
CryptoError, EncString, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)]

Check warning on line 7 in crates/bitwarden-vault/src/cipher/ssh_key.rs

Codecov / codecov/patch

crates/bitwarden-vault/src/cipher/ssh_key.rs#L7

Added line #L7 was not covered by tests
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
pub struct SshKey {
/// SSH private key (ed25519/rsa) in unencrypted openssh private key format [OpenSSH private key](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key)
pub private_key: Option<EncString>,
/// SSH public key (ed25519/rsa) according to [RFC4253](https://datatracker.ietf.org/doc/html/rfc4253#section-6.6)
pub public_key: Option<EncString>,
/// SSH fingerprint using SHA256 in the format: `SHA256:BASE64_ENCODED_FINGERPRINT`
pub fingerprint: Option<EncString>,
}

#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)]

Check warning on line 19 in crates/bitwarden-vault/src/cipher/ssh_key.rs

Codecov / codecov/patch

crates/bitwarden-vault/src/cipher/ssh_key.rs#L19

Added line #L19 was not covered by tests
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
pub struct SshKeyView {
/// SSH private key (ed25519/rsa) in unencrypted openssh private key format [OpenSSH private key](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key)
pub private_key: Option<String>,
quexten marked this conversation as resolved.
Show resolved Hide resolved
/// SSH public key (ed25519/rsa) according to [RFC4253](https://datatracker.ietf.org/doc/html/rfc4253#section-6.6)
pub public_key: Option<String>,
/// SSH fingerprint using SHA256 in the format: `SHA256:BASE64_ENCODED_FINGERPRINT`
pub fingerprint: Option<String>,
}

impl KeyEncryptable<SymmetricCryptoKey, SshKey> for SshKeyView {
fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result<SshKey, CryptoError> {
Ok(SshKey {
private_key: self.private_key.encrypt_with_key(key)?,
public_key: self.public_key.encrypt_with_key(key)?,
fingerprint: self.fingerprint.encrypt_with_key(key)?,

Check warning on line 36 in crates/bitwarden-vault/src/cipher/ssh_key.rs

Codecov / codecov/patch

crates/bitwarden-vault/src/cipher/ssh_key.rs#L32-L36

Added lines #L32 - L36 were not covered by tests
})
}

Check warning on line 38 in crates/bitwarden-vault/src/cipher/ssh_key.rs

Codecov / codecov/patch

crates/bitwarden-vault/src/cipher/ssh_key.rs#L38

Added line #L38 was not covered by tests
}

impl KeyDecryptable<SymmetricCryptoKey, SshKeyView> for SshKey {
fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result<SshKeyView, CryptoError> {
Ok(SshKeyView {
private_key: self.private_key.decrypt_with_key(key).ok().flatten(),
public_key: self.public_key.decrypt_with_key(key).ok().flatten(),
fingerprint: self.fingerprint.decrypt_with_key(key).ok().flatten(),
})
}

Check warning on line 48 in crates/bitwarden-vault/src/cipher/ssh_key.rs

Codecov / codecov/patch

crates/bitwarden-vault/src/cipher/ssh_key.rs#L42-L48

Added lines #L42 - L48 were not covered by tests
}
2 changes: 2 additions & 0 deletions crates/bitwarden-vault/src/mobile/client_ciphers.rs
Original file line number Diff line number Diff line change
@@ -118,6 +118,7 @@ mod tests {
identity: None,
card: None,
secure_note: None,
ssh_key: None,
favorite: false,
reprompt: CipherRepromptType::None,
organization_use_totp: true,
@@ -159,6 +160,7 @@ mod tests {
identity: None,
card: None,
secure_note: None,
ssh_key: None,
favorite: false,
reprompt: CipherRepromptType::None,
organization_use_totp: true,