bitwarden_vault/cipher/
ssh_key.rs

1use bitwarden_core::key_management::{KeyIds, SymmetricKeyId};
2use bitwarden_crypto::{CryptoError, Decryptable, EncString, Encryptable, KeyStoreContext};
3use serde::{Deserialize, Serialize};
4#[cfg(feature = "wasm")]
5use tsify_next::Tsify;
6
7use super::cipher::CipherKind;
8use crate::{cipher::cipher::CopyableCipherFields, Cipher};
9
10#[derive(Serialize, Deserialize, Debug, Clone)]
11#[serde(rename_all = "camelCase", deny_unknown_fields)]
12#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
13#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
14pub struct SshKey {
15    /// SSH private key (ed25519/rsa) in unencrypted openssh private key format [OpenSSH private key](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key)
16    pub private_key: EncString,
17    /// SSH public key (ed25519/rsa) according to [RFC4253](https://datatracker.ietf.org/doc/html/rfc4253#section-6.6)
18    pub public_key: EncString,
19    /// SSH fingerprint using SHA256 in the format: `SHA256:BASE64_ENCODED_FINGERPRINT`
20    pub fingerprint: EncString,
21}
22
23#[allow(missing_docs)]
24#[derive(Serialize, Deserialize, Debug, Clone)]
25#[serde(rename_all = "camelCase", deny_unknown_fields)]
26#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
27#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
28pub struct SshKeyView {
29    /// SSH private key (ed25519/rsa) in unencrypted openssh private key format [OpenSSH private key](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key)
30    pub private_key: String,
31    /// SSH public key (ed25519/rsa) according to [RFC4253](https://datatracker.ietf.org/doc/html/rfc4253#section-6.6)
32    pub public_key: String,
33    /// SSH fingerprint using SHA256 in the format: `SHA256:BASE64_ENCODED_FINGERPRINT`
34    pub fingerprint: String,
35}
36
37impl Encryptable<KeyIds, SymmetricKeyId, SshKey> for SshKeyView {
38    fn encrypt(
39        &self,
40        ctx: &mut KeyStoreContext<KeyIds>,
41        key: SymmetricKeyId,
42    ) -> Result<SshKey, CryptoError> {
43        Ok(SshKey {
44            private_key: self.private_key.encrypt(ctx, key)?,
45            public_key: self.public_key.encrypt(ctx, key)?,
46            fingerprint: self.fingerprint.encrypt(ctx, key)?,
47        })
48    }
49}
50
51impl Decryptable<KeyIds, SymmetricKeyId, SshKeyView> for SshKey {
52    fn decrypt(
53        &self,
54        ctx: &mut KeyStoreContext<KeyIds>,
55        key: SymmetricKeyId,
56    ) -> Result<SshKeyView, CryptoError> {
57        Ok(SshKeyView {
58            private_key: self.private_key.decrypt(ctx, key)?,
59            public_key: self.public_key.decrypt(ctx, key)?,
60            fingerprint: self.fingerprint.decrypt(ctx, key)?,
61        })
62    }
63}
64
65impl CipherKind for SshKey {
66    fn decrypt_subtitle(
67        &self,
68        ctx: &mut KeyStoreContext<KeyIds>,
69        key: SymmetricKeyId,
70    ) -> Result<String, CryptoError> {
71        self.fingerprint.decrypt(ctx, key)
72    }
73
74    fn get_copyable_fields(&self, _: Option<&Cipher>) -> Vec<CopyableCipherFields> {
75        [CopyableCipherFields::SshKey].into_iter().collect()
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use bitwarden_core::key_management::create_test_crypto_with_user_key;
82    use bitwarden_crypto::SymmetricCryptoKey;
83
84    use super::*;
85    use crate::cipher::cipher::CopyableCipherFields;
86
87    #[test]
88    fn test_subtitle_ssh_key() {
89        let key = SymmetricCryptoKey::try_from("hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe0+G8EwxvW3v1iywVmSl61iwzd17JW5C/ivzxSP2C9h7Tw==".to_string()).unwrap();
90        let key_store = create_test_crypto_with_user_key(key);
91        let key = SymmetricKeyId::User;
92        let mut ctx = key_store.context();
93
94        let original_subtitle = "SHA256:1JjFjvPRkj1Gbf2qRP1dgHiIzEuNAEvp+92x99jw3K0".to_string();
95        let fingerprint_encrypted = original_subtitle.to_owned().encrypt(&mut ctx, key).unwrap();
96        let private_key_encrypted = "".to_string().encrypt(&mut ctx, key).unwrap();
97        let public_key_encrypted = "".to_string().encrypt(&mut ctx, key).unwrap();
98
99        let ssh_key = SshKey {
100            private_key: private_key_encrypted,
101            public_key: public_key_encrypted,
102            fingerprint: fingerprint_encrypted,
103        };
104
105        assert_eq!(
106            ssh_key.decrypt_subtitle(&mut ctx, key).unwrap(),
107            original_subtitle
108        );
109    }
110
111    #[test]
112    fn test_get_copyable_fields_sshkey() {
113        let ssh_key = SshKey {
114            private_key: "2.tMIugb6zQOL+EuOizna1wQ==|W5dDLoNJtajN68yeOjrr6w==|qS4hwJB0B0gNLI0o+jxn+sKMBmvtVgJCRYNEXBZoGeE=".parse().unwrap(),
115            public_key: "2.tMIugb6zQOL+EuOizna1wQ==|W5dDLoNJtajN68yeOjrr6w==|qS4hwJB0B0gNLI0o+jxn+sKMBmvtVgJCRYNEXBZoGeE=".parse().unwrap(),
116            fingerprint: "2.tMIugb6zQOL+EuOizna1wQ==|W5dDLoNJtajN68yeOjrr6w==|qS4hwJB0B0gNLI0o+jxn+sKMBmvtVgJCRYNEXBZoGeE=".parse().unwrap(),
117        };
118
119        let copyable_fields = ssh_key.get_copyable_fields(None);
120        assert_eq!(copyable_fields, vec![CopyableCipherFields::SshKey]);
121    }
122}