bitwarden_vault/cipher/
ssh_key.rs1use bitwarden_api_api::models::CipherSshKeyModel;
2use bitwarden_core::{
3 key_management::{KeyIds, SymmetricKeyId},
4 require,
5};
6use bitwarden_crypto::{
7 CompositeEncryptable, CryptoError, Decryptable, EncString, KeyStoreContext,
8 PrimitiveEncryptable,
9};
10use serde::{Deserialize, Serialize};
11#[cfg(feature = "wasm")]
12use tsify::Tsify;
13
14use super::cipher::CipherKind;
15use crate::{Cipher, VaultParseError, cipher::cipher::CopyableCipherFields};
16
17#[derive(Serialize, Deserialize, Debug, Clone)]
18#[serde(rename_all = "camelCase", deny_unknown_fields)]
19#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
20#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
21pub struct SshKey {
22 pub private_key: EncString,
24 pub public_key: EncString,
26 pub fingerprint: EncString,
28}
29
30#[allow(missing_docs)]
31#[derive(Serialize, Deserialize, Debug, Clone)]
32#[serde(rename_all = "camelCase", deny_unknown_fields)]
33#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
34#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
35pub struct SshKeyView {
36 pub private_key: String,
38 pub public_key: String,
40 pub fingerprint: String,
42}
43
44impl CompositeEncryptable<KeyIds, SymmetricKeyId, SshKey> for SshKeyView {
45 fn encrypt_composite(
46 &self,
47 ctx: &mut KeyStoreContext<KeyIds>,
48 key: SymmetricKeyId,
49 ) -> Result<SshKey, CryptoError> {
50 Ok(SshKey {
51 private_key: self.private_key.encrypt(ctx, key)?,
52 public_key: self.public_key.encrypt(ctx, key)?,
53 fingerprint: self.fingerprint.encrypt(ctx, key)?,
54 })
55 }
56}
57
58impl Decryptable<KeyIds, SymmetricKeyId, SshKeyView> for SshKey {
59 fn decrypt(
60 &self,
61 ctx: &mut KeyStoreContext<KeyIds>,
62 key: SymmetricKeyId,
63 ) -> Result<SshKeyView, CryptoError> {
64 Ok(SshKeyView {
65 private_key: self.private_key.decrypt(ctx, key)?,
66 public_key: self.public_key.decrypt(ctx, key)?,
67 fingerprint: self.fingerprint.decrypt(ctx, key)?,
68 })
69 }
70}
71
72impl CipherKind for SshKey {
73 fn decrypt_subtitle(
74 &self,
75 ctx: &mut KeyStoreContext<KeyIds>,
76 key: SymmetricKeyId,
77 ) -> Result<String, CryptoError> {
78 self.fingerprint.decrypt(ctx, key)
79 }
80
81 fn get_copyable_fields(&self, _: Option<&Cipher>) -> Vec<CopyableCipherFields> {
82 [CopyableCipherFields::SshKey].into_iter().collect()
83 }
84}
85
86impl TryFrom<CipherSshKeyModel> for SshKey {
87 type Error = VaultParseError;
88
89 fn try_from(ssh_key: CipherSshKeyModel) -> Result<Self, Self::Error> {
90 Ok(Self {
91 private_key: require!(EncString::try_from_optional(ssh_key.private_key)?),
92 public_key: require!(EncString::try_from_optional(ssh_key.public_key)?),
93 fingerprint: require!(EncString::try_from_optional(ssh_key.key_fingerprint)?),
94 })
95 }
96}
97
98impl From<SshKey> for CipherSshKeyModel {
99 fn from(ssh_key: SshKey) -> Self {
100 Self {
101 private_key: Some(ssh_key.private_key.to_string()),
102 public_key: Some(ssh_key.public_key.to_string()),
103 key_fingerprint: Some(ssh_key.fingerprint.to_string()),
104 }
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use bitwarden_core::key_management::create_test_crypto_with_user_key;
111 use bitwarden_crypto::SymmetricCryptoKey;
112
113 use super::*;
114 use crate::cipher::cipher::CopyableCipherFields;
115
116 #[test]
117 fn test_subtitle_ssh_key() {
118 let key = SymmetricCryptoKey::try_from("hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe0+G8EwxvW3v1iywVmSl61iwzd17JW5C/ivzxSP2C9h7Tw==".to_string()).unwrap();
119 let key_store = create_test_crypto_with_user_key(key);
120 let key = SymmetricKeyId::User;
121 let mut ctx = key_store.context();
122
123 let original_subtitle = "SHA256:1JjFjvPRkj1Gbf2qRP1dgHiIzEuNAEvp+92x99jw3K0".to_string();
124 let fingerprint_encrypted = original_subtitle.to_owned().encrypt(&mut ctx, key).unwrap();
125 let private_key_encrypted = "".to_string().encrypt(&mut ctx, key).unwrap();
126 let public_key_encrypted = "".to_string().encrypt(&mut ctx, key).unwrap();
127
128 let ssh_key = SshKey {
129 private_key: private_key_encrypted,
130 public_key: public_key_encrypted,
131 fingerprint: fingerprint_encrypted,
132 };
133
134 assert_eq!(
135 ssh_key.decrypt_subtitle(&mut ctx, key).unwrap(),
136 original_subtitle
137 );
138 }
139
140 #[test]
141 fn test_get_copyable_fields_sshkey() {
142 let ssh_key = SshKey {
143 private_key: "2.tMIugb6zQOL+EuOizna1wQ==|W5dDLoNJtajN68yeOjrr6w==|qS4hwJB0B0gNLI0o+jxn+sKMBmvtVgJCRYNEXBZoGeE=".parse().unwrap(),
144 public_key: "2.tMIugb6zQOL+EuOizna1wQ==|W5dDLoNJtajN68yeOjrr6w==|qS4hwJB0B0gNLI0o+jxn+sKMBmvtVgJCRYNEXBZoGeE=".parse().unwrap(),
145 fingerprint: "2.tMIugb6zQOL+EuOizna1wQ==|W5dDLoNJtajN68yeOjrr6w==|qS4hwJB0B0gNLI0o+jxn+sKMBmvtVgJCRYNEXBZoGeE=".parse().unwrap(),
146 };
147
148 let copyable_fields = ssh_key.get_copyable_fields(None);
149 assert_eq!(copyable_fields, vec![CopyableCipherFields::SshKey]);
150 }
151}