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