Skip to main content

bitwarden_vault/cipher/
secure_note.rs

1use bitwarden_api_api::models::CipherSecureNoteModel;
2use bitwarden_core::{
3    key_management::{KeySlotIds, SymmetricKeySlotId},
4    require,
5};
6use bitwarden_crypto::{CompositeEncryptable, CryptoError, Decryptable, KeyStoreContext};
7use serde::{Deserialize, Deserializer, Serialize};
8use serde_repr::Serialize_repr;
9#[cfg(feature = "wasm")]
10use tsify::Tsify;
11#[cfg(feature = "wasm")]
12use wasm_bindgen::prelude::wasm_bindgen;
13
14use crate::{
15    Cipher, VaultParseError,
16    cipher::cipher::{CipherKind, CopyableCipherFields},
17};
18
19#[allow(missing_docs)]
20#[derive(Clone, Copy, Serialize_repr, Debug, PartialEq)]
21#[repr(u8)]
22#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
23#[cfg_attr(feature = "wasm", wasm_bindgen)]
24pub enum SecureNoteType {
25    Generic = 0,
26}
27
28#[derive(Clone, Serialize, Deserialize, Debug)]
29#[serde(rename_all = "camelCase")]
30#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
31#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
32pub struct SecureNote {
33    r#type: SecureNoteType,
34}
35
36/// Use the custom deserializer for SecureNoteType
37/// Older notes may have a Type greater than 0. If so set them to Generic
38impl<'de> Deserialize<'de> for SecureNoteType {
39    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
40    where
41        D: Deserializer<'de>,
42    {
43        match u8::deserialize(deserializer) {
44            Ok(0) => Ok(SecureNoteType::Generic),
45            Ok(_) => {
46                // Any unknown type (like type 1) defaults to Generic
47                Ok(SecureNoteType::Generic)
48            }
49            Err(_) => Ok(SecureNoteType::Generic),
50        }
51    }
52}
53
54#[allow(missing_docs)]
55#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
56#[serde(rename_all = "camelCase", deny_unknown_fields)]
57#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
58#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
59pub struct SecureNoteView {
60    pub r#type: SecureNoteType,
61}
62
63impl CompositeEncryptable<KeySlotIds, SymmetricKeySlotId, SecureNote> for SecureNoteView {
64    fn encrypt_composite(
65        &self,
66        _ctx: &mut KeyStoreContext<KeySlotIds>,
67        _key: SymmetricKeySlotId,
68    ) -> Result<SecureNote, CryptoError> {
69        Ok(SecureNote {
70            r#type: self.r#type,
71        })
72    }
73}
74
75impl Decryptable<KeySlotIds, SymmetricKeySlotId, SecureNoteView> for SecureNote {
76    fn decrypt(
77        &self,
78        _ctx: &mut KeyStoreContext<KeySlotIds>,
79        _key: SymmetricKeySlotId,
80    ) -> Result<SecureNoteView, CryptoError> {
81        Ok(SecureNoteView {
82            r#type: self.r#type,
83        })
84    }
85}
86
87impl TryFrom<CipherSecureNoteModel> for SecureNote {
88    type Error = VaultParseError;
89
90    fn try_from(model: CipherSecureNoteModel) -> Result<Self, Self::Error> {
91        Ok(Self {
92            r#type: require!(model.r#type).try_into()?,
93        })
94    }
95}
96
97impl TryFrom<bitwarden_api_api::models::SecureNoteType> for SecureNoteType {
98    type Error = bitwarden_core::MissingFieldError;
99
100    fn try_from(model: bitwarden_api_api::models::SecureNoteType) -> Result<Self, Self::Error> {
101        Ok(match model {
102            bitwarden_api_api::models::SecureNoteType::Generic => SecureNoteType::Generic,
103            bitwarden_api_api::models::SecureNoteType::__Unknown(_) => {
104                return Err(bitwarden_core::MissingFieldError("type"));
105            }
106        })
107    }
108}
109
110impl From<SecureNoteType> for bitwarden_api_api::models::SecureNoteType {
111    fn from(model: SecureNoteType) -> Self {
112        match model {
113            SecureNoteType::Generic => bitwarden_api_api::models::SecureNoteType::Generic,
114        }
115    }
116}
117
118impl From<SecureNote> for CipherSecureNoteModel {
119    fn from(model: SecureNote) -> Self {
120        Self {
121            r#type: Some(model.r#type.into()),
122        }
123    }
124}
125
126impl CipherKind for SecureNote {
127    fn get_copyable_fields(&self, cipher: Option<&Cipher>) -> Vec<CopyableCipherFields> {
128        [cipher
129            .and_then(|c| c.notes.as_ref())
130            .map(|_| CopyableCipherFields::SecureNotes)]
131        .into_iter()
132        .flatten()
133        .collect()
134    }
135
136    fn decrypt_subtitle(
137        &self,
138        _ctx: &mut KeyStoreContext<KeySlotIds>,
139        _key: SymmetricKeySlotId,
140    ) -> Result<String, CryptoError> {
141        Ok(String::new())
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use crate::{
148        CipherRepromptType, CipherType, SecureNoteType,
149        cipher::cipher::{Cipher, CipherKind, CopyableCipherFields},
150        secure_note::SecureNote,
151    };
152
153    fn create_cipher_for_note(note: SecureNote) -> Cipher {
154        Cipher {
155            partial_data: None,
156            id: Some("090c19ea-a61a-4df6-8963-262b97bc6266".parse().unwrap()),
157            organization_id: None,
158            folder_id: None,
159            collection_ids: vec![],
160            r#type: CipherType::Login,
161            key: None,
162            name: Some("2.iovOJUb186UXu+0AlQggjw==|LeWZhrT0B7rqFtDufOJMlJsftwmMGuaoBxf/Cig4D4A9XHhUqacd8uOYP7M5bd/k|++gmrHIyt8hvvPP9dwFS/CGd+POfzmeXzKOsuyJpDDc=".parse().unwrap()),
163            notes: None,
164            login: None,
165            identity: None,
166            card: None,
167            secure_note: Some(note),
168            ssh_key: None,
169            bank_account: None,
170            drivers_license: None,
171            passport: None,
172            favorite: false,
173            reprompt: CipherRepromptType::None,
174            organization_use_totp: false,
175            edit: true,
176            permissions: None,
177            view_password: true,
178            local_data: None,
179            attachments: None,
180            fields: None,
181            password_history: None,
182            creation_date: "2024-01-01T00:00:00.000Z".parse().unwrap(),
183            deleted_date: None,
184            revision_date: "2024-01-01T00:00:00.000Z".parse().unwrap(),
185            archived_date: None,
186            data: None,
187        }
188    }
189
190    #[test]
191    fn test_get_copyable_fields_secure_note_empty() {
192        let secure_note = SecureNote {
193            r#type: SecureNoteType::Generic,
194        };
195
196        let cipher = create_cipher_for_note(secure_note.clone());
197
198        let copyable_fields = secure_note.get_copyable_fields(Some(&cipher));
199        assert_eq!(copyable_fields, vec![]);
200    }
201
202    #[test]
203    fn test_get_copyable_fields_secure_note_has_notes() {
204        let secure_note = SecureNote {
205            r#type: SecureNoteType::Generic,
206        };
207
208        let mut cipher = create_cipher_for_note(secure_note.clone());
209        cipher.notes = Some("2.iovOJUb186UXu+0AlQggjw==|LeWZhrT0B7rqFtDufOJMlJsftwmMGuaoBxf/Cig4D4A9XHhUqacd8uOYP7M5bd/k|++gmrHIyt8hvvPP9dwFS/CGd+POfzmeXzKOsuyJpDDc=".parse().unwrap());
210
211        let copyable_fields = secure_note.get_copyable_fields(Some(&cipher));
212        assert_eq!(copyable_fields, vec![CopyableCipherFields::SecureNotes]);
213    }
214
215    #[test]
216    fn test_get_copyable_fields_secure_no_cipher() {
217        let secure_note = SecureNote {
218            r#type: SecureNoteType::Generic,
219        };
220
221        let copyable_fields = secure_note.get_copyable_fields(None);
222        assert_eq!(copyable_fields, vec![]);
223    }
224}