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::cipher::CopyableCipherFields, Cipher, VaultParseError};
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
98#[cfg(test)]
99mod tests {
100    use bitwarden_core::key_management::create_test_crypto_with_user_key;
101    use bitwarden_crypto::SymmetricCryptoKey;
102
103    use super::*;
104    use crate::cipher::cipher::CopyableCipherFields;
105
106    #[test]
107    fn test_subtitle_ssh_key() {
108        let key = SymmetricCryptoKey::try_from("hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe0+G8EwxvW3v1iywVmSl61iwzd17JW5C/ivzxSP2C9h7Tw==".to_string()).unwrap();
109        let key_store = create_test_crypto_with_user_key(key);
110        let key = SymmetricKeyId::User;
111        let mut ctx = key_store.context();
112
113        let original_subtitle = "SHA256:1JjFjvPRkj1Gbf2qRP1dgHiIzEuNAEvp+92x99jw3K0".to_string();
114        let fingerprint_encrypted = original_subtitle.to_owned().encrypt(&mut ctx, key).unwrap();
115        let private_key_encrypted = "".to_string().encrypt(&mut ctx, key).unwrap();
116        let public_key_encrypted = "".to_string().encrypt(&mut ctx, key).unwrap();
117
118        let ssh_key = SshKey {
119            private_key: private_key_encrypted,
120            public_key: public_key_encrypted,
121            fingerprint: fingerprint_encrypted,
122        };
123
124        assert_eq!(
125            ssh_key.decrypt_subtitle(&mut ctx, key).unwrap(),
126            original_subtitle
127        );
128    }
129
130    #[test]
131    fn test_get_copyable_fields_sshkey() {
132        let ssh_key = SshKey {
133            private_key: "2.tMIugb6zQOL+EuOizna1wQ==|W5dDLoNJtajN68yeOjrr6w==|qS4hwJB0B0gNLI0o+jxn+sKMBmvtVgJCRYNEXBZoGeE=".parse().unwrap(),
134            public_key: "2.tMIugb6zQOL+EuOizna1wQ==|W5dDLoNJtajN68yeOjrr6w==|qS4hwJB0B0gNLI0o+jxn+sKMBmvtVgJCRYNEXBZoGeE=".parse().unwrap(),
135            fingerprint: "2.tMIugb6zQOL+EuOizna1wQ==|W5dDLoNJtajN68yeOjrr6w==|qS4hwJB0B0gNLI0o+jxn+sKMBmvtVgJCRYNEXBZoGeE=".parse().unwrap(),
136        };
137
138        let copyable_fields = ssh_key.get_copyable_fields(None);
139        assert_eq!(copyable_fields, vec![CopyableCipherFields::SshKey]);
140    }
141}