3
3
// SPDX-License-Identifier: AGPL-3.0-only
4
4
//
5
5
6
- use hex:: ToHex as _;
7
6
use serde_with:: hex:: Hex ;
8
7
use serde_with:: serde_as;
9
8
use uuid:: Uuid ;
@@ -23,20 +22,32 @@ pub enum Locator {
23
22
24
23
#[ serde_as]
25
24
#[ derive( Debug , serde:: Serialize ) ]
26
- #[ cfg_attr( test, derive( Default , PartialEq ) ) ]
25
+ #[ cfg_attr( test, derive( PartialEq ) ) ]
27
26
pub struct LocatorInfo {
28
27
#[ serde( with = "hex" ) ]
29
28
key : Vec < u8 > ,
30
- #[ serde( with = "hex" ) ]
31
- digest : Vec < u8 > ,
32
- size : u32 ,
29
+ integrity_check : IntegrityCheck ,
30
+ plaintext_size : u32 ,
33
31
transit : Option < TransitTierLocator > ,
34
32
media_tier_cdn_number : Option < u32 > ,
35
- media_name : String ,
33
+
36
34
#[ serde_as( as = "Option<Hex>" ) ]
37
35
local_key : Option < Vec < u8 > > ,
38
36
}
39
37
38
+ #[ derive( Debug , serde:: Serialize ) ]
39
+ #[ cfg_attr( test, derive( PartialEq ) ) ]
40
+ pub enum IntegrityCheck {
41
+ EncryptedDigest {
42
+ #[ serde( with = "hex" ) ]
43
+ digest : Vec < u8 > ,
44
+ } ,
45
+ PlaintextHash {
46
+ #[ serde( with = "hex" ) ]
47
+ plaintext_hash : Vec < u8 > ,
48
+ } ,
49
+ }
50
+
40
51
#[ serde_as]
41
52
#[ derive( Debug , serde:: Serialize ) ]
42
53
#[ cfg_attr( test, derive( Default , PartialEq ) ) ]
@@ -51,18 +62,20 @@ pub struct TransitTierLocator {
51
62
pub enum LocatorError {
52
63
/// Missing key
53
64
MissingKey ,
54
- /// Missing digest
55
- MissingDigest ,
65
+ /// Missing integrity check (digest or plaintextHash)
66
+ MissingIntegrityCheck ,
67
+ /// localKey is present but plaintextHash is not set
68
+ UnexpectedLocalKey ,
56
69
/// Locator had exactly one of transitCdnKey and transitCdnNumber
57
70
TransitCdnMismatch ,
58
71
/// Locator had transitCdnUploadTimestamp but not transitCdnKey
59
72
UnexpectedTransitCdnUploadTimestamp ,
60
73
/// transitCdnKey was present but empty
61
74
MissingTransitCdnKey ,
62
- /// mediaName isn't digest encoded as hex (maybe with "_thumbnail" suffix)
63
- InvalidMediaName ,
64
- /// mediaName is empty but mediaTierCdnNumber is present
75
+ /// mediaTierCdnNumber is present but plaintextHash is not set
65
76
UnexpectedMediaTierCdnNumber ,
77
+ /// key is present but neither transitCdnKey nor plaintextHash are set
78
+ UnexpectedKey ,
66
79
/// {0}
67
80
InvalidTimestamp ( #[ from] TimestampError ) ,
68
81
}
@@ -90,48 +103,70 @@ impl<C: ReportUnusualTimestamp + ?Sized> TryIntoWith<LocatorInfo, C>
90
103
fn try_into_with ( self , context : & C ) -> Result < LocatorInfo , Self :: Error > {
91
104
let proto:: file_pointer:: LocatorInfo {
92
105
key,
93
- digest ,
94
- size ,
106
+ size : plaintext_size ,
107
+ integrityCheck ,
95
108
transitCdnKey,
96
109
transitCdnNumber,
97
110
transitTierUploadTimestamp,
98
111
mediaTierCdnNumber,
99
- mediaName,
100
112
localKey,
113
+ legacyDigest : _,
114
+ legacyMediaName : _,
101
115
special_fields : _,
102
116
} = self ;
103
117
104
- if key. is_empty ( ) {
105
- return Err ( LocatorError :: MissingKey ) ;
106
- }
107
- if digest. is_empty ( ) {
108
- return Err ( LocatorError :: MissingDigest ) ;
109
- }
118
+ let integrity_check = match integrityCheck {
119
+ Some ( proto:: file_pointer:: locator_info:: IntegrityCheck :: EncryptedDigest ( digest) ) => {
120
+ if digest. is_empty ( ) {
121
+ return Err ( LocatorError :: MissingIntegrityCheck ) ;
122
+ }
110
123
111
- let media_name = if mediaName. is_empty ( ) {
112
- if mediaTierCdnNumber. is_some ( ) {
113
- return Err ( LocatorError :: UnexpectedMediaTierCdnNumber ) ;
124
+ IntegrityCheck :: EncryptedDigest { digest }
114
125
}
115
- mediaName
116
- } else {
117
- let media_name = mediaName. strip_suffix ( "_thumbnail" ) . unwrap_or ( & mediaName) ;
118
- if !media_name. eq_ignore_ascii_case ( & digest. encode_hex :: < String > ( ) ) {
119
- return Err ( LocatorError :: InvalidMediaName ) ;
126
+ Some ( proto:: file_pointer:: locator_info:: IntegrityCheck :: PlaintextHash ( hash) ) => {
127
+ if hash. is_empty ( ) {
128
+ return Err ( LocatorError :: MissingIntegrityCheck ) ;
129
+ }
130
+
131
+ IntegrityCheck :: PlaintextHash {
132
+ plaintext_hash : hash,
133
+ }
120
134
}
121
- mediaName
135
+ None => return Err ( LocatorError :: MissingIntegrityCheck ) ,
122
136
} ;
123
137
124
138
let transit =
125
139
( transitCdnKey, transitCdnNumber, transitTierUploadTimestamp) . try_into_with ( context) ?;
126
140
141
+ let has_content =
142
+ transit. is_some ( ) || matches ! ( integrity_check, IntegrityCheck :: PlaintextHash { .. } ) ;
143
+ match ( has_content, key. is_empty ( ) ) {
144
+ ( true , true ) => return Err ( LocatorError :: MissingKey ) ,
145
+ ( false , false ) => return Err ( LocatorError :: UnexpectedKey ) ,
146
+ ( true , false ) => { } // Content and key are both present, normal happy case.
147
+ ( false , true ) => { } // Neither content nor key are present, equivalent to old InvalidAttachmentLocator.
148
+ }
149
+
150
+ // If plaintextHash is not set, we have never downloaded the file, so
151
+ // we cannot have a local key. If we have never downloaded it, we also
152
+ // can never have uploaded it to the media tier, so we should not have
153
+ // a media tier CDN number.
154
+ if !matches ! ( integrity_check, IntegrityCheck :: PlaintextHash { .. } ) {
155
+ if localKey. is_some ( ) {
156
+ return Err ( LocatorError :: UnexpectedLocalKey ) ;
157
+ }
158
+ if mediaTierCdnNumber. is_some ( ) {
159
+ return Err ( LocatorError :: UnexpectedMediaTierCdnNumber ) ;
160
+ }
161
+ }
162
+
127
163
Ok ( LocatorInfo {
128
164
key,
129
165
local_key : localKey,
130
- digest,
131
- size,
166
+ plaintext_size,
132
167
transit,
133
168
media_tier_cdn_number : mediaTierCdnNumber,
134
- media_name ,
169
+ integrity_check ,
135
170
} )
136
171
}
137
172
}
@@ -324,48 +359,93 @@ mod test {
324
359
impl proto:: file_pointer:: LocatorInfo {
325
360
fn test_data ( ) -> Self {
326
361
Self {
327
- mediaName : "5678" . into ( ) ,
328
362
key : hex ! ( "1234" ) . into ( ) ,
329
- digest : hex ! ( "5678" ) . into ( ) ,
363
+ legacyDigest : vec ! [ ] ,
364
+ integrityCheck : None , // Will be set by specific test data methods
330
365
size : 123 ,
331
366
transitCdnKey : Some ( "ABCDEFG" . into ( ) ) ,
332
367
transitCdnNumber : Some ( 2 ) ,
333
368
transitTierUploadTimestamp : Some ( MillisecondsSinceEpoch :: TEST_VALUE . 0 ) ,
369
+ localKey : None ,
370
+ mediaTierCdnNumber : None ,
371
+ legacyMediaName : "" . to_string ( ) ,
372
+ special_fields : Default :: default ( ) ,
373
+ }
374
+ }
375
+
376
+ fn test_data_with_plaintext_hash ( ) -> Self {
377
+ Self {
378
+ integrityCheck : Some (
379
+ proto:: file_pointer:: locator_info:: IntegrityCheck :: PlaintextHash (
380
+ b"plaintextHash" . to_vec ( ) ,
381
+ ) ,
382
+ ) ,
334
383
localKey : Some ( b"local key" . to_vec ( ) ) ,
335
384
mediaTierCdnNumber : Some ( 87 ) ,
336
- special_fields : Default :: default ( ) ,
385
+ ..Self :: test_data ( )
386
+ }
387
+ }
388
+
389
+ fn test_data_with_digest ( ) -> Self {
390
+ Self {
391
+ integrityCheck : Some (
392
+ proto:: file_pointer:: locator_info:: IntegrityCheck :: EncryptedDigest (
393
+ hex ! ( "abcd" ) . into ( ) ,
394
+ ) ,
395
+ ) ,
396
+ ..Self :: test_data ( )
337
397
}
338
398
}
339
399
}
340
400
341
401
#[ test]
342
402
fn valid_locator_info ( ) {
343
403
assert_eq ! (
344
- proto:: file_pointer:: LocatorInfo :: test_data( ) . try_into_with( & TestContext :: default ( ) ) ,
404
+ proto:: file_pointer:: LocatorInfo :: test_data_with_plaintext_hash( )
405
+ . try_into_with( & TestContext :: default ( ) ) ,
345
406
Ok ( Locator :: LocatorInfo ( LocatorInfo {
346
407
transit: Some ( TransitTierLocator {
347
408
cdn_key: "ABCDEFG" . into( ) ,
348
409
cdn_number: 2 ,
349
410
upload_timestamp: Some ( Timestamp :: test_value( ) )
350
411
} ) ,
351
412
key: vec![ 0x12 , 0x34 ] ,
352
- digest: vec![ 0x56 , 0x78 ] ,
353
- size: 123 ,
413
+ integrity_check: IntegrityCheck :: PlaintextHash {
414
+ plaintext_hash: b"plaintextHash" . to_vec( )
415
+ } ,
416
+ plaintext_size: 123 ,
354
417
media_tier_cdn_number: Some ( 87 ) ,
355
- media_name: "5678" . into( ) ,
356
418
local_key: Some ( b"local key" . to_vec( ) )
357
419
} ) )
358
420
)
359
421
}
360
422
423
+ #[ test]
424
+ fn valid_locator_info_with_digest ( ) {
425
+ assert_eq ! (
426
+ proto:: file_pointer:: LocatorInfo :: test_data_with_digest( )
427
+ . try_into_with( & TestContext :: default ( ) ) ,
428
+ Ok ( Locator :: LocatorInfo ( LocatorInfo {
429
+ transit: Some ( TransitTierLocator {
430
+ cdn_key: "ABCDEFG" . into( ) ,
431
+ cdn_number: 2 ,
432
+ upload_timestamp: Some ( Timestamp :: test_value( ) )
433
+ } ) ,
434
+ key: vec![ 0x12 , 0x34 ] ,
435
+ integrity_check: IntegrityCheck :: EncryptedDigest {
436
+ digest: vec![ 0xab , 0xcd ]
437
+ } ,
438
+ plaintext_size: 123 ,
439
+ media_tier_cdn_number: None ,
440
+ local_key: None
441
+ } ) )
442
+ )
443
+ }
444
+
361
445
#[ test_case( |_| { } => Ok ( ( ) ) ; "valid" ) ]
362
- #[ test_case( |x| { x. mediaName = "" . into( ) ; x. mediaTierCdnNumber = None } => Ok ( ( ) ) ; "mediaName can be empty" ) ]
363
- #[ test_case( |x| x. mediaName = "1234" . into( ) => Err ( LocatorError :: InvalidMediaName ) ; "invalid mediaName" ) ]
364
- #[ test_case( |x| x. mediaName = "5678_thumbnail" . into( ) => Ok ( ( ) ) ; "thumbnail mediaName" ) ]
365
- #[ test_case( |x| x. mediaName = "" . into( ) => Err ( LocatorError :: UnexpectedMediaTierCdnNumber ) ; "mediaTierCdnNumber without mediaName" ) ]
446
+ #[ test_case( |x| x. integrityCheck = None => Err ( LocatorError :: MissingIntegrityCheck ) ; "no integrityCheck" ) ]
366
447
#[ test_case( |x| x. mediaTierCdnNumber = None => Ok ( ( ) ) ; "no mediaTierCdnNumber" ) ]
367
448
#[ test_case( |x| x. key = vec![ ] => Err ( LocatorError :: MissingKey ) ; "no key" ) ]
368
- #[ test_case( |x| x. digest = vec![ ] => Err ( LocatorError :: MissingDigest ) ; "no digest" ) ]
369
449
#[ test_case( |x| x. size = 0 => Ok ( ( ) ) ; "size zero" ) ]
370
450
#[ test_case( |x| x. transitCdnKey = None => Err ( LocatorError :: TransitCdnMismatch ) ; "no transitCdnKey" ) ]
371
451
#[ test_case( |x| x. transitCdnKey = Some ( "" . into( ) ) => Err ( LocatorError :: MissingTransitCdnKey ) ; "empty transitCdnKey" ) ]
@@ -384,7 +464,32 @@ mod test {
384
464
fn locator_info (
385
465
modifier : impl FnOnce ( & mut proto:: file_pointer:: LocatorInfo ) ,
386
466
) -> Result < ( ) , LocatorError > {
387
- let mut locator = proto:: file_pointer:: LocatorInfo :: test_data ( ) ;
467
+ let mut locator = proto:: file_pointer:: LocatorInfo :: test_data_with_plaintext_hash ( ) ;
468
+ modifier ( & mut locator) ;
469
+ locator
470
+ . try_into_with ( & TestContext :: default ( ) )
471
+ . map ( |_: Locator | ( ) )
472
+ }
473
+
474
+ #[ test_case( |_| { } => Ok ( ( ) ) ; "valid with digest" ) ]
475
+ #[ test_case( |x| x. integrityCheck = Some ( proto:: file_pointer:: locator_info:: IntegrityCheck :: EncryptedDigest ( vec![ ] ) ) => Err ( LocatorError :: MissingIntegrityCheck ) ; "empty digest" ) ]
476
+ #[ test_case( |x| x. localKey = Some ( b"key" . to_vec( ) ) => Err ( LocatorError :: UnexpectedLocalKey ) ; "localKey means we downloaded it, so we should have plaintextHash instead of digest" ) ]
477
+ #[ test_case( |x| x. mediaTierCdnNumber = Some ( 1 ) => Err ( LocatorError :: UnexpectedMediaTierCdnNumber ) ; "mediaTierCdnNumber means we uploaded it to the media tier, so we should have calculated a plaintextHash when we did that" ) ]
478
+ #[ test_case( |x| {
479
+ x. transitCdnKey = None ;
480
+ x. transitCdnNumber = None ;
481
+ x. transitTierUploadTimestamp = None ;
482
+ } => Err ( LocatorError :: UnexpectedKey ) ; "no transit CDN info or plaintextHash, but key is present" ) ]
483
+ #[ test_case( |x| {
484
+ x. key = vec![ ] ;
485
+ x. transitCdnKey = None ;
486
+ x. transitCdnNumber = None ;
487
+ x. transitTierUploadTimestamp = None ;
488
+ } => Ok ( ( ) ) ; "digest-only, no key" ) ]
489
+ fn locator_info_with_digest (
490
+ modifier : impl FnOnce ( & mut proto:: file_pointer:: LocatorInfo ) ,
491
+ ) -> Result < ( ) , LocatorError > {
492
+ let mut locator = proto:: file_pointer:: LocatorInfo :: test_data_with_digest ( ) ;
388
493
modifier ( & mut locator) ;
389
494
locator
390
495
. try_into_with ( & TestContext :: default ( ) )
@@ -420,7 +525,7 @@ mod test {
420
525
421
526
#[ test_case( |_| { } => Ok ( ( ) ) ; "valid" ) ]
422
527
#[ test_case( |x| {
423
- x. locatorInfo = Some ( proto:: file_pointer:: LocatorInfo :: test_data ( ) ) . into( ) ;
528
+ x. locatorInfo = Some ( proto:: file_pointer:: LocatorInfo :: test_data_with_plaintext_hash ( ) ) . into( ) ;
424
529
} => Ok ( ( ) ) ; "with locatorInfo" ) ]
425
530
#[ test_case( |x| x. locator = None => Ok ( ( ) ) ; "no legacy locator" ) ]
426
531
#[ test_case( |x| x. locatorInfo = None . into( ) => Err ( FilePointerError :: NoLocatorInfo ) ; "no locatorInfo" ) ]
0 commit comments