@@ -64,57 +64,29 @@ impl Attachment {
6464 }
6565}
6666
67+ /// The full decrypted view of an attachment, including its cryptographic key.
68+ ///
69+ /// Eventually this will be made SDK-internal and a separate `AttachmentView` (without the key) will
70+ /// be exposed to clients. That requires all attachment operations to already live in the SDK, so
71+ /// clients never need to handle the key themselves.
6772#[ allow( missing_docs) ]
6873#[ derive( Serialize , Deserialize , Debug , Clone ) ]
6974#[ serde( rename_all = "camelCase" , deny_unknown_fields) ]
7075#[ cfg_attr( feature = "uniffi" , derive( uniffi:: Record ) ) ]
7176#[ cfg_attr( feature = "wasm" , derive( Tsify ) , tsify( into_wasm_abi, from_wasm_abi) ) ]
72- pub struct AttachmentView {
77+ pub struct AttachmentFullView {
7378 pub id : Option < String > ,
7479 pub url : Option < String > ,
7580 pub size : Option < String > ,
7681 pub size_name : Option < String > ,
7782 pub file_name : Option < String > ,
78- pub key : Option < EncString > ,
79- /// The decrypted attachmentkey in base64 format.
80- ///
81- /// **TEMPORARY FIELD**: This field is a temporary workaround to provide
82- /// decrypted attachment keys to the TypeScript client during the migration
83- /// process. It will be removed once the encryption/decryption logic is
84- /// fully migrated to the SDK.
85- ///
86- /// **Ticket**: <https://bitwarden.atlassian.net/browse/PM-23005>
83+ /// The decrypted per-attachment key that encrypts the attachment contents.
8784 ///
88- /// Do not rely on this field for long-term use.
89- #[ cfg( feature = "wasm" ) ]
90- pub decrypted_key : Option < String > ,
91- }
92-
93- impl AttachmentView {
94- pub ( crate ) fn reencrypt_key (
95- & mut self ,
96- ctx : & mut KeyStoreContext < KeySlotIds > ,
97- old_key : SymmetricKeySlotId ,
98- new_key : SymmetricKeySlotId ,
99- ) -> Result < ( ) , CryptoError > {
100- if let Some ( attachment_key) = & mut self . key {
101- let tmp_attachment_key_id = ctx. unwrap_symmetric_key ( old_key, attachment_key) ?;
102- * attachment_key = ctx. wrap_symmetric_key ( new_key, tmp_attachment_key_id) ?;
103- }
104- Ok ( ( ) )
105- }
106-
107- pub ( crate ) fn reencrypt_keys (
108- attachment_views : & mut Vec < AttachmentView > ,
109- ctx : & mut KeyStoreContext < KeySlotIds > ,
110- old_key : SymmetricKeySlotId ,
111- new_key : SymmetricKeySlotId ,
112- ) -> Result < ( ) , CryptoError > {
113- for attachment in attachment_views {
114- attachment. reencrypt_key ( ctx, old_key, new_key) ?;
115- }
116- Ok ( ( ) )
117- }
85+ /// This is the decrypted key material, consistent with the [`CompositeEncryptable`] contract
86+ /// that a `*View` holds plaintext. On encryption it is wrapped with the cipher key to produce
87+ /// [`Attachment::key`]; `None` marks a legacy v1 attachment whose contents are encrypted
88+ /// directly with the user/organization key.
89+ pub key : Option < SymmetricCryptoKey > ,
11890}
11991
12092#[ allow( missing_docs) ]
@@ -129,7 +101,7 @@ pub struct AttachmentEncryptResult {
129101#[ allow( missing_docs) ]
130102pub struct AttachmentFile {
131103 pub cipher : Cipher ,
132- pub attachment : AttachmentView ,
104+ pub attachment : AttachmentFullView ,
133105
134106 /// There are three different ways attachments are encrypted.
135107 /// 1. UserKey / OrgKey (Contents) - Legacy
@@ -141,7 +113,7 @@ pub struct AttachmentFile {
141113#[ allow( missing_docs) ]
142114pub struct AttachmentFileView < ' a > {
143115 pub cipher : Cipher ,
144- pub attachment : AttachmentView ,
116+ pub attachment : AttachmentFullView ,
145117 pub contents : & ' a [ u8 ] ,
146118}
147119
@@ -168,12 +140,16 @@ impl CompositeEncryptable<KeySlotIds, SymmetricKeySlotId, AttachmentEncryptResul
168140
169141 let mut attachment = self . attachment . clone ( ) ;
170142
171- // Because this is a new attachment, we have to generate a key for it, encrypt the contents
172- // with it, and then encrypt the key with the cipher key
143+ // Because this is a new attachment, we have to generate a key for it and encrypt the
144+ // contents with it. The decrypted key is stored on the view; wrapping it with the cipher
145+ // key is handled by `AttachmentFullView::encrypt_composite` below.
173146 let attachment_key = ctx. generate_symmetric_key ( ) ;
174147 let encrypted_contents =
175148 OctetStreamBytes :: from ( self . contents ) . encrypt ( ctx, attachment_key) ?;
176- attachment. key = Some ( ctx. wrap_symmetric_key ( ciphers_key, attachment_key) ?) ;
149+ #[ allow( deprecated) ]
150+ {
151+ attachment. key = Some ( ctx. dangerous_get_symmetric_key ( attachment_key) ?. clone ( ) ) ;
152+ }
177153
178154 let contents = encrypted_contents. to_buffer ( ) ?;
179155
@@ -204,30 +180,11 @@ impl Decryptable<KeySlotIds, SymmetricKeySlotId, Vec<u8>> for AttachmentFile {
204180 ctx : & mut KeyStoreContext < KeySlotIds > ,
205181 key : SymmetricKeySlotId ,
206182 ) -> Result < Vec < u8 > , CryptoError > {
207- let ciphers_key = Cipher :: decrypt_cipher_key ( ctx, key, & self . cipher . key ) . map_err ( |e| {
208- tracing:: warn!(
209- attachment_id = ?self . attachment. id,
210- cipher_id = ?self . cipher. id,
211- has_cipher_key = self . cipher. key. is_some( ) ,
212- error = %e,
213- "Failed to decrypt cipher key for attachment"
214- ) ;
215- e
216- } ) ?;
217-
218- // Version 2 or 3, `AttachmentKey` or `CipherKey(AttachmentKey)`
183+ // Version 2 or 3, `AttachmentKey` or `CipherKey(AttachmentKey)`. The view already carries
184+ // the decrypted content key (unwrapped when the attachment was decrypted), so register it
185+ // in the context and decrypt with it directly - the cipher key is not needed here.
219186 if let Some ( attachment_key) = & self . attachment . key {
220- let content_key = ctx
221- . unwrap_symmetric_key ( ciphers_key, attachment_key)
222- . map_err ( |e| {
223- tracing:: warn!(
224- attachment_id = ?self . attachment. id,
225- cipher_id = ?self . cipher. id,
226- error = %e,
227- "Failed to unwrap attachment key (v2/v3)"
228- ) ;
229- e
230- } ) ?;
187+ let content_key = ctx. add_local_symmetric_key ( attachment_key. clone ( ) ) ;
231188 self . contents . decrypt ( ctx, content_key) . map_err ( |e| {
232189 tracing:: warn!(
233190 attachment_id = ?self . attachment. id,
@@ -252,59 +209,62 @@ impl Decryptable<KeySlotIds, SymmetricKeySlotId, Vec<u8>> for AttachmentFile {
252209 }
253210}
254211
255- // ⚠️ CONTRACT VIOLATION of `bitwarden_crypto::CompositeEncryptable`: `AttachmentView` retains
256- // key-bound ciphertext (`key`, the attachment content key wrapped under the cipher key) and copies
257- // it through unchanged (`key: self.key.clone()` below) instead of re-wrapping it under `key`. As a
258- // result decrypt(K) -> encrypt(K1) -> decrypt(K1) does NOT round-trip.
259- impl CompositeEncryptable < KeySlotIds , SymmetricKeySlotId , Attachment > for AttachmentView {
212+ impl CompositeEncryptable < KeySlotIds , SymmetricKeySlotId , Attachment > for AttachmentFullView {
260213 fn encrypt_composite (
261214 & self ,
262215 ctx : & mut KeyStoreContext < KeySlotIds > ,
263216 key : SymmetricKeySlotId ,
264217 ) -> Result < Attachment , CryptoError > {
218+ // Wrap the decrypted attachment key with the cipher key to produce the stored, wrapped
219+ // key. `None` (legacy v1) stays `None`.
220+ let wrapped_key = self
221+ . key
222+ . as_ref ( )
223+ . map ( |attachment_key| {
224+ let key_id = ctx. add_local_symmetric_key ( attachment_key. clone ( ) ) ;
225+ ctx. wrap_symmetric_key ( key, key_id)
226+ } )
227+ . transpose ( ) ?;
228+
265229 Ok ( Attachment {
266230 id : self . id . clone ( ) ,
267231 url : self . url . clone ( ) ,
268232 size : self . size . clone ( ) ,
269233 size_name : self . size_name . clone ( ) ,
270234 file_name : self . file_name . encrypt ( ctx, key) ?,
271- // ⚠️ pass-through of wrapped key-bound ciphertext — see the contract-violation note
272- // above.
273- key : self . key . clone ( ) ,
235+ key : wrapped_key,
274236 } )
275237 }
276238}
277239
278- impl Decryptable < KeySlotIds , SymmetricKeySlotId , AttachmentView > for Attachment {
240+ impl Decryptable < KeySlotIds , SymmetricKeySlotId , AttachmentFullView > for Attachment {
279241 fn decrypt (
280242 & self ,
281243 ctx : & mut KeyStoreContext < KeySlotIds > ,
282244 key : SymmetricKeySlotId ,
283- ) -> Result < AttachmentView , CryptoError > {
245+ ) -> Result < AttachmentFullView , CryptoError > {
284246 // Decrypt the file name or return an error if decryption fails
285247 let file_name = self . file_name . decrypt ( ctx, key) ?;
286248
287- # [ cfg ( feature = "wasm" ) ]
288- let decrypted_key = if let Some ( attachment_key ) = & self . key {
289- let content_key_id = ctx . unwrap_symmetric_key ( key , attachment_key ) ? ;
290-
291- # [ allow ( deprecated ) ]
292- let actual_key = ctx . dangerous_get_symmetric_key ( content_key_id ) ? ;
293-
294- Some ( actual_key . to_base64 ( ) )
295- } else {
296- None
297- } ;
249+ // Unwrap the stored, wrapped attachment key into its decrypted form for the view. `None`
250+ // (legacy v1) stays `None`.
251+ let attachment_key = self
252+ . key
253+ . as_ref ( )
254+ . map ( |wrapped_key| {
255+ let content_key_id = ctx . unwrap_symmetric_key ( key , wrapped_key ) ? ;
256+ # [ allow ( deprecated ) ]
257+ ctx . dangerous_get_symmetric_key ( content_key_id ) . cloned ( )
258+ } )
259+ . transpose ( ) ? ;
298260
299- Ok ( AttachmentView {
261+ Ok ( AttachmentFullView {
300262 id : self . id . clone ( ) ,
301263 url : self . url . clone ( ) ,
302264 size : self . size . clone ( ) ,
303265 size_name : self . size_name . clone ( ) ,
304266 file_name,
305- key : self . key . clone ( ) ,
306- #[ cfg( feature = "wasm" ) ]
307- decrypted_key : decrypted_key. map ( |k| k. to_string ( ) ) ,
267+ key : attachment_key,
308268 } )
309269 }
310270}
@@ -316,7 +276,7 @@ pub(crate) fn decrypt_attachments_with_failures(
316276 attachments : & [ Attachment ] ,
317277 ctx : & mut KeyStoreContext < KeySlotIds > ,
318278 key : SymmetricKeySlotId ,
319- ) -> ( Vec < AttachmentView > , Vec < AttachmentView > ) {
279+ ) -> ( Vec < AttachmentFullView > , Vec < AttachmentFullView > ) {
320280 let mut successes = Vec :: new ( ) ;
321281 let mut failures = Vec :: new ( ) ;
322282
@@ -325,15 +285,14 @@ pub(crate) fn decrypt_attachments_with_failures(
325285 Ok ( decrypted) => successes. push ( decrypted) ,
326286 Err ( e) => {
327287 tracing:: warn!( attachment_id = ?attachment. id, error = %e, "Failed to decrypt attachment" ) ;
328- failures. push ( AttachmentView {
288+ failures. push ( AttachmentFullView {
329289 id : attachment. id . clone ( ) ,
330290 url : attachment. url . clone ( ) ,
331291 size : attachment. size . clone ( ) ,
332292 size_name : attachment. size_name . clone ( ) ,
333293 file_name : None ,
334- key : attachment. key . clone ( ) ,
335- #[ cfg( feature = "wasm" ) ]
336- decrypted_key : None ,
294+ // The attachment failed to decrypt, so its decrypted key is unavailable.
295+ key : None ,
337296 } ) ;
338297 }
339298 }
@@ -361,12 +320,12 @@ impl TryFrom<bitwarden_api_api::models::AttachmentResponseModel> for Attachment
361320
362321#[ cfg( test) ]
363322mod tests {
364- use bitwarden_core:: key_management:: create_test_crypto_with_user_key;
323+ use bitwarden_core:: key_management:: { SymmetricKeySlotId , create_test_crypto_with_user_key} ;
365324 use bitwarden_crypto:: { EncString , SymmetricCryptoKey } ;
366325 use bitwarden_encoding:: B64 ;
367326
368327 use crate :: {
369- AttachmentFile , AttachmentFileView , AttachmentView , Cipher ,
328+ AttachmentFile , AttachmentFileView , AttachmentFullView , Cipher ,
370329 cipher:: cipher:: { CipherRepromptType , CipherType } ,
371330 } ;
372331
@@ -387,15 +346,13 @@ mod tests {
387346 let user_key: SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==" . to_string ( ) . try_into ( ) . unwrap ( ) ;
388347 let key_store = create_test_crypto_with_user_key ( user_key) ;
389348
390- let attachment = AttachmentView {
349+ let attachment = AttachmentFullView {
391350 id : None ,
392351 url : None ,
393352 size : Some ( "100" . into ( ) ) ,
394353 size_name : Some ( "100 Bytes" . into ( ) ) ,
395354 file_name : Some ( "Test.txt" . into ( ) ) ,
396355 key : None ,
397- #[ cfg( feature = "wasm" ) ]
398- decrypted_key : None ,
399356 } ;
400357
401358 let contents = b"This is a test file that we will encrypt. It's 100 bytes long, the encrypted version will be longer!" ;
@@ -450,15 +407,29 @@ mod tests {
450407 let user_key: SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==" . to_string ( ) . try_into ( ) . unwrap ( ) ;
451408 let key_store = create_test_crypto_with_user_key ( user_key) ;
452409
453- let attachment = AttachmentView {
410+ // The view carries the decrypted content key. Unwrap the stored key (wrapped by the cipher
411+ // key, which is itself wrapped by the user key) to obtain it.
412+ let attachment_key = {
413+ let mut ctx = key_store. context ( ) ;
414+ let cipher_key_enc: EncString = "2.Gg8yCM4IIgykCZyq0O4+cA==|GJLBtfvSJTDJh/F7X4cJPkzI6ccnzJm5DYl3yxOW2iUn7DgkkmzoOe61sUhC5dgVdV0kFqsZPcQ0yehlN1DDsFIFtrb4x7LwzJNIkMgxNyg=|1rGkGJ8zcM5o5D0aIIwAyLsjMLrPsP3EWm3CctBO3Fw=" . parse ( ) . unwrap ( ) ;
415+ let cipher_key = ctx
416+ . unwrap_symmetric_key ( SymmetricKeySlotId :: User , & cipher_key_enc)
417+ . unwrap ( ) ;
418+ let wrapped_key: EncString = "2.r288/AOSPiaLFkW07EBGBw==|SAmnnCbOLFjX5lnURvoualOetQwuyPc54PAmHDTRrhT0gwO9ailna9U09q9bmBfI5XrjNNEsuXssgzNygRkezoVQvZQggZddOwHB6KQW5EQ=|erIMUJp8j+aTcmhdE50zEX+ipv/eR1sZ7EwULJm/6DY=" . parse ( ) . unwrap ( ) ;
419+ let content_key_id = ctx. unwrap_symmetric_key ( cipher_key, & wrapped_key) . unwrap ( ) ;
420+ #[ allow( deprecated) ]
421+ ctx. dangerous_get_symmetric_key ( content_key_id)
422+ . unwrap ( )
423+ . clone ( )
424+ } ;
425+
426+ let attachment = AttachmentFullView {
454427 id : None ,
455428 url : None ,
456429 size : Some ( "161" . into ( ) ) ,
457430 size_name : Some ( "161 Bytes" . into ( ) ) ,
458431 file_name : Some ( "Test.txt" . into ( ) ) ,
459- key : Some ( "2.r288/AOSPiaLFkW07EBGBw==|SAmnnCbOLFjX5lnURvoualOetQwuyPc54PAmHDTRrhT0gwO9ailna9U09q9bmBfI5XrjNNEsuXssgzNygRkezoVQvZQggZddOwHB6KQW5EQ=|erIMUJp8j+aTcmhdE50zEX+ipv/eR1sZ7EwULJm/6DY=" . parse ( ) . unwrap ( ) ) ,
460- #[ cfg( feature = "wasm" ) ]
461- decrypted_key : None ,
432+ key : Some ( attachment_key) ,
462433 } ;
463434
464435 let cipher = Cipher {
@@ -514,15 +485,13 @@ mod tests {
514485 let user_key: SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==" . to_string ( ) . try_into ( ) . unwrap ( ) ;
515486 let key_store = create_test_crypto_with_user_key ( user_key) ;
516487
517- let attachment = AttachmentView {
488+ let attachment = AttachmentFullView {
518489 id : None ,
519490 url : None ,
520491 size : Some ( "161" . into ( ) ) ,
521492 size_name : Some ( "161 Bytes" . into ( ) ) ,
522493 file_name : Some ( "Test.txt" . into ( ) ) ,
523494 key : None ,
524- #[ cfg( feature = "wasm" ) ]
525- decrypted_key : None ,
526495 } ;
527496
528497 let cipher = Cipher {
0 commit comments