bitwarden_vault/cipher/
ssh_key.rs

1use 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    /// SSH private key (ed25519/rsa) in unencrypted openssh private key format [OpenSSH private key](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key)
23    pub private_key: EncString,
24    /// SSH public key (ed25519/rsa) according to [RFC4253](https://datatracker.ietf.org/doc/html/rfc4253#section-6.6)
25    pub public_key: EncString,
26    /// SSH fingerprint using SHA256 in the format: `SHA256:BASE64_ENCODED_FINGERPRINT`
27    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    /// SSH private key (ed25519/rsa) in unencrypted openssh private key format [OpenSSH private key](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key)
37    pub private_key: String,
38    /// SSH public key (ed25519/rsa) according to [RFC4253](https://datatracker.ietf.org/doc/html/rfc4253#section-6.6)
39    pub public_key: String,
40    /// SSH fingerprint using SHA256 in the format: `SHA256:BASE64_ENCODED_FINGERPRINT`
41    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}