bitwarden_vault/cipher/
ssh_key.rs

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