1use bitwarden_core::{Client, OrganizationId};
2use bitwarden_crypto::IdentifyKey;
3#[cfg(feature = "wasm")]
4use wasm_bindgen::prelude::*;
5
6use super::EncryptionContext;
7use crate::{Cipher, CipherError, CipherListView, CipherView, DecryptError, EncryptError};
8
9#[cfg_attr(feature = "wasm", wasm_bindgen)]
10pub struct CiphersClient {
11 pub(crate) client: Client,
12}
13
14#[cfg_attr(feature = "wasm", wasm_bindgen)]
15impl CiphersClient {
16 pub fn encrypt(&self, mut cipher_view: CipherView) -> Result<EncryptionContext, EncryptError> {
17 let user_id = self
18 .client
19 .internal
20 .get_user_id()
21 .ok_or(EncryptError::MissingUserId)?;
22 let key_store = self.client.internal.get_key_store();
23
24 if cipher_view.key.is_none()
27 && self
28 .client
29 .internal
30 .get_flags()
31 .enable_cipher_key_encryption
32 {
33 let key = cipher_view.key_identifier();
34 cipher_view.generate_cipher_key(&mut key_store.context(), key)?;
35 }
36
37 let cipher = key_store.encrypt(cipher_view)?;
38 Ok(EncryptionContext {
39 cipher,
40 encrypted_for: user_id,
41 })
42 }
43
44 pub fn decrypt(&self, cipher: Cipher) -> Result<CipherView, DecryptError> {
45 let key_store = self.client.internal.get_key_store();
46 let cipher_view = key_store.decrypt(&cipher)?;
47 Ok(cipher_view)
48 }
49
50 pub fn decrypt_list(&self, ciphers: Vec<Cipher>) -> Result<Vec<CipherListView>, DecryptError> {
51 let key_store = self.client.internal.get_key_store();
52 let cipher_views = key_store.decrypt_list(&ciphers)?;
53 Ok(cipher_views)
54 }
55
56 pub fn decrypt_fido2_credentials(
57 &self,
58 cipher_view: CipherView,
59 ) -> Result<Vec<crate::Fido2CredentialView>, DecryptError> {
60 let key_store = self.client.internal.get_key_store();
61 let credentials = cipher_view.decrypt_fido2_credentials(&mut key_store.context())?;
62 Ok(credentials)
63 }
64
65 pub fn move_to_organization(
66 &self,
67 mut cipher_view: CipherView,
68 organization_id: OrganizationId,
69 ) -> Result<CipherView, CipherError> {
70 let key_store = self.client.internal.get_key_store();
71 cipher_view.move_to_organization(&mut key_store.context(), organization_id.into())?;
72 Ok(cipher_view)
73 }
74
75 #[cfg(feature = "wasm")]
76 pub fn decrypt_fido2_private_key(
77 &self,
78 cipher_view: CipherView,
79 ) -> Result<String, CipherError> {
80 let key_store = self.client.internal.get_key_store();
81 let decrypted_key = cipher_view.decrypt_fido2_private_key(&mut key_store.context())?;
82 Ok(decrypted_key)
83 }
84}
85
86#[cfg(test)]
87mod tests {
88
89 use bitwarden_core::client::test_accounts::test_bitwarden_com_account;
90
91 use super::*;
92 use crate::{Attachment, CipherRepromptType, CipherType, Login, VaultClientExt};
93
94 #[tokio::test]
95 async fn test_decrypt_list() {
96 let client = Client::init_test_account(test_bitwarden_com_account()).await;
97
98 let dec = client
99 .vault()
100 .ciphers()
101 .decrypt_list(vec![Cipher {
102 id: Some("a1569f46-0797-4d3f-b859-b181009e2e49".parse().unwrap()),
103 organization_id: Some("1bc9ac1e-f5aa-45f2-94bf-b181009709b8".parse().unwrap()),
104 folder_id: None,
105 collection_ids: vec!["66c5ca57-0868-4c7e-902f-b181009709c0".parse().unwrap()],
106 key: None,
107 name: "2.RTdUGVWYl/OZHUMoy68CMg==|sCaT5qHx8i0rIvzVrtJKww==|jB8DsRws6bXBtXNfNXUmFJ0JLDlB6GON6Y87q0jgJ+0=".parse().unwrap(),
108 notes: None,
109 r#type: CipherType::Login,
110 login: Some(Login{
111 username: Some("2.ouEYEk+SViUtqncesfe9Ag==|iXzEJq1zBeNdDbumFO1dUA==|RqMoo9soSwz/yB99g6YPqk8+ASWRcSdXsKjbwWzyy9U=".parse().unwrap()),
112 password: Some("2.6yXnOz31o20Z2kiYDnXueA==|rBxTb6NK9lkbfdhrArmacw==|ogZir8Z8nLgiqlaLjHH+8qweAtItS4P2iPv1TELo5a0=".parse().unwrap()),
113 password_revision_date: None, uris:None, totp: None, autofill_on_page_load: None, fido2_credentials: None }),
114 identity: None,
115 card: None,
116 secure_note: None,
117 ssh_key: None,
118 favorite: false,
119 reprompt: CipherRepromptType::None,
120 organization_use_totp: true,
121 edit: true,
122 permissions: None,
123 view_password: true,
124 local_data: None,
125 attachments: None,
126 fields: None,
127 password_history: None,
128 creation_date: "2024-05-31T09:35:55.12Z".parse().unwrap(),
129 deleted_date: None,
130 revision_date: "2024-05-31T09:35:55.12Z".parse().unwrap(),
131 }])
132
133 .unwrap();
134
135 assert_eq!(dec[0].name, "Test item");
136 }
137
138 fn test_cipher() -> Cipher {
139 Cipher {
140 id: Some("358f2b2b-9326-4e5e-94a8-b18100bb0908".parse().unwrap()),
141 organization_id: None,
142 folder_id: None,
143 collection_ids: vec![],
144 key: None,
145 name: "2.+oPT8B4xJhyhQRe1VkIx0A==|PBtC/bZkggXR+fSnL/pG7g==|UkjRD0VpnUYkjRC/05ZLdEBAmRbr3qWRyJey2bUvR9w=".parse().unwrap(),
146 notes: None,
147 r#type: CipherType::Login,
148 login: Some(Login{
149 username: None,
150 password: None,
151 password_revision_date: None,
152 uris:None,
153 totp: None,
154 autofill_on_page_load: None,
155 fido2_credentials: None,
156 }),
157 identity: None,
158 card: None,
159 secure_note: None,
160 ssh_key: None,
161 favorite: false,
162 reprompt: CipherRepromptType::None,
163 organization_use_totp: true,
164 edit: true,
165 permissions: None,
166 view_password: true,
167 local_data: None,
168 attachments: None,
169 fields: None,
170 password_history: None,
171 creation_date: "2024-05-31T11:20:58.4566667Z".parse().unwrap(),
172 deleted_date: None,
173 revision_date: "2024-05-31T11:20:58.4566667Z".parse().unwrap(),
174 }
175 }
176
177 fn test_attachment_legacy() -> Attachment {
178 Attachment {
179 id: Some("uf7bkexzag04d3cw04jsbqqkbpbwhxs0".to_string()),
180 url: Some("http://localhost:4000/attachments//358f2b2b-9326-4e5e-94a8-b18100bb0908/uf7bkexzag04d3cw04jsbqqkbpbwhxs0".to_string()),
181 file_name: Some("2.mV50WiLq6duhwGbhM1TO0A==|dTufWNH8YTPP0EMlNLIpFA==|QHp+7OM8xHtEmCfc9QPXJ0Ro2BeakzvLgxJZ7NdLuDc=".parse().unwrap()),
182 key: None,
183 size: Some("65".to_string()),
184 size_name: Some("65 Bytes".to_string()),
185 }
186 }
187
188 fn test_attachment_v2() -> Attachment {
189 Attachment {
190 id: Some("a77m56oerrz5b92jm05lq5qoyj1xh2t9".to_string()),
191 url: Some("http://localhost:4000/attachments//358f2b2b-9326-4e5e-94a8-b18100bb0908/uf7bkexzag04d3cw04jsbqqkbpbwhxs0".to_string()),
192 file_name: Some("2.GhazFdCYQcM5v+AtVwceQA==|98bMUToqC61VdVsSuXWRwA==|bsLByMht9Hy5QO9pPMRz0K4d0aqBiYnnROGM5YGbNu4=".parse().unwrap()),
193 key: Some("2.6TPEiYULFg/4+3CpDRwCqw==|6swweBHCJcd5CHdwBBWuRN33XRV22VoroDFDUmiM4OzjPEAhgZK57IZS1KkBlCcFvT+t+YbsmDcdv+Lqr+iJ3MmzfJ40MCB5TfYy+22HVRA=|rkgFDh2IWTfPC1Y66h68Diiab/deyi1p/X0Fwkva0NQ=".parse().unwrap()),
194 size: Some("65".to_string()),
195 size_name: Some("65 Bytes".to_string()),
196 }
197 }
198
199 #[tokio::test]
200 async fn test_move_user_cipher_with_attachment_without_key_to_org_fails() {
201 let client = Client::init_test_account(test_bitwarden_com_account()).await;
202
203 let mut cipher = test_cipher();
204 cipher.attachments = Some(vec![test_attachment_legacy()]);
205
206 let view = client.vault().ciphers().decrypt(cipher.clone()).unwrap();
207
208 let res = client.vault().ciphers().move_to_organization(
210 view,
211 "1bc9ac1e-f5aa-45f2-94bf-b181009709b8".parse().unwrap(),
212 );
213
214 assert!(res.is_err());
215 }
216
217 #[tokio::test]
218 async fn test_encrypt_cipher_with_legacy_attachment_without_key() {
219 let client = Client::init_test_account(test_bitwarden_com_account()).await;
220
221 let mut cipher = test_cipher();
222 let attachment = test_attachment_legacy();
223 cipher.attachments = Some(vec![attachment.clone()]);
224
225 let view = client.vault().ciphers().decrypt(cipher.clone()).unwrap();
226
227 assert!(cipher.key.is_none());
228
229 let EncryptionContext {
231 cipher: new_cipher,
232 encrypted_for: _,
233 } = client.vault().ciphers().encrypt(view).unwrap();
234 assert!(new_cipher.key.is_some());
235
236 let view = client.vault().ciphers().decrypt(new_cipher).unwrap();
237 let attachments = view.clone().attachments.unwrap();
238 let attachment_view = attachments.first().unwrap().clone();
239 assert!(attachment_view.key.is_none());
240
241 assert_eq!(attachment_view.file_name.as_deref(), Some("h.txt"));
242
243 let buf = vec![
244 2, 100, 205, 148, 152, 77, 184, 77, 53, 80, 38, 240, 83, 217, 251, 118, 254, 27, 117,
245 41, 148, 244, 216, 110, 216, 255, 104, 215, 23, 15, 176, 239, 208, 114, 95, 159, 23,
246 211, 98, 24, 145, 166, 60, 197, 42, 204, 131, 144, 253, 204, 195, 154, 27, 201, 215,
247 43, 10, 244, 107, 226, 152, 85, 167, 66, 185,
248 ];
249
250 let content = client
251 .vault()
252 .attachments()
253 .decrypt_buffer(cipher, attachment_view.clone(), buf.as_slice())
254 .unwrap();
255
256 assert_eq!(content, b"Hello");
257 }
258
259 #[tokio::test]
260 async fn test_encrypt_cipher_with_v1_attachment_without_key() {
261 let client = Client::init_test_account(test_bitwarden_com_account()).await;
262
263 let mut cipher = test_cipher();
264 let attachment = test_attachment_v2();
265 cipher.attachments = Some(vec![attachment.clone()]);
266
267 let view = client.vault().ciphers().decrypt(cipher.clone()).unwrap();
268
269 assert!(cipher.key.is_none());
270
271 let EncryptionContext {
273 cipher: new_cipher,
274 encrypted_for: _,
275 } = client.vault().ciphers().encrypt(view).unwrap();
276 assert!(new_cipher.key.is_some());
277
278 let view = client
279 .vault()
280 .ciphers()
281 .decrypt(new_cipher.clone())
282 .unwrap();
283 let attachments = view.clone().attachments.unwrap();
284 let attachment_view = attachments.first().unwrap().clone();
285 assert!(attachment_view.key.is_some());
286
287 assert_ne!(
289 attachment.clone().key.unwrap().to_string(),
290 attachment_view.clone().key.unwrap().to_string()
291 );
292
293 assert_eq!(attachment_view.file_name.as_deref(), Some("h.txt"));
294
295 let buf = vec![
296 2, 114, 53, 72, 20, 82, 18, 46, 48, 137, 97, 1, 100, 142, 120, 187, 28, 36, 180, 46,
297 189, 254, 133, 23, 169, 58, 73, 212, 172, 116, 185, 127, 111, 92, 112, 145, 99, 28,
298 158, 198, 48, 241, 121, 218, 66, 37, 152, 197, 122, 241, 110, 82, 245, 72, 47, 230, 95,
299 188, 196, 170, 127, 67, 44, 129, 90,
300 ];
301
302 let content = client
303 .vault()
304 .attachments()
305 .decrypt_buffer(new_cipher.clone(), attachment_view.clone(), buf.as_slice())
306 .unwrap();
307
308 assert_eq!(content, b"Hello");
309
310 let new_view = client
312 .vault()
313 .ciphers()
314 .move_to_organization(
315 view,
316 "1bc9ac1e-f5aa-45f2-94bf-b181009709b8".parse().unwrap(),
317 )
318 .unwrap();
319 let EncryptionContext {
320 cipher: new_cipher,
321 encrypted_for: _,
322 } = client.vault().ciphers().encrypt(new_view).unwrap();
323
324 let attachment = new_cipher
325 .clone()
326 .attachments
327 .unwrap()
328 .first()
329 .unwrap()
330 .clone();
331
332 assert_eq!(
334 attachment.clone().key.as_ref().unwrap().to_string(),
335 attachment_view.key.as_ref().unwrap().to_string()
336 );
337
338 let content = client
339 .vault()
340 .attachments()
341 .decrypt_buffer(new_cipher, attachment_view, buf.as_slice())
342 .unwrap();
343
344 assert_eq!(content, b"Hello");
345 }
346}