Skip to content

Commit fd34ab3

Browse files
Backups: remove mediaName from LocatorInfo, add in integrityCheck
Co-authored-by: Andrew <[email protected]>
1 parent 14082f1 commit fd34ab3

File tree

4 files changed

+186
-59
lines changed

4 files changed

+186
-59
lines changed

RELEASE_NOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
v0.74.1
22

3+
- backups: Add integrityCheck to LocatorInfo

rust/message-backup/src/backup/file.rs

Lines changed: 151 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
// SPDX-License-Identifier: AGPL-3.0-only
44
//
55

6-
use hex::ToHex as _;
76
use serde_with::hex::Hex;
87
use serde_with::serde_as;
98
use uuid::Uuid;
@@ -23,20 +22,32 @@ pub enum Locator {
2322

2423
#[serde_as]
2524
#[derive(Debug, serde::Serialize)]
26-
#[cfg_attr(test, derive(Default, PartialEq))]
25+
#[cfg_attr(test, derive(PartialEq))]
2726
pub struct LocatorInfo {
2827
#[serde(with = "hex")]
2928
key: Vec<u8>,
30-
#[serde(with = "hex")]
31-
digest: Vec<u8>,
32-
size: u32,
29+
integrity_check: IntegrityCheck,
30+
plaintext_size: u32,
3331
transit: Option<TransitTierLocator>,
3432
media_tier_cdn_number: Option<u32>,
35-
media_name: String,
33+
3634
#[serde_as(as = "Option<Hex>")]
3735
local_key: Option<Vec<u8>>,
3836
}
3937

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+
4051
#[serde_as]
4152
#[derive(Debug, serde::Serialize)]
4253
#[cfg_attr(test, derive(Default, PartialEq))]
@@ -51,18 +62,20 @@ pub struct TransitTierLocator {
5162
pub enum LocatorError {
5263
/// Missing key
5364
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,
5669
/// Locator had exactly one of transitCdnKey and transitCdnNumber
5770
TransitCdnMismatch,
5871
/// Locator had transitCdnUploadTimestamp but not transitCdnKey
5972
UnexpectedTransitCdnUploadTimestamp,
6073
/// transitCdnKey was present but empty
6174
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
6576
UnexpectedMediaTierCdnNumber,
77+
/// key is present but neither transitCdnKey nor plaintextHash are set
78+
UnexpectedKey,
6679
/// {0}
6780
InvalidTimestamp(#[from] TimestampError),
6881
}
@@ -90,48 +103,70 @@ impl<C: ReportUnusualTimestamp + ?Sized> TryIntoWith<LocatorInfo, C>
90103
fn try_into_with(self, context: &C) -> Result<LocatorInfo, Self::Error> {
91104
let proto::file_pointer::LocatorInfo {
92105
key,
93-
digest,
94-
size,
106+
size: plaintext_size,
107+
integrityCheck,
95108
transitCdnKey,
96109
transitCdnNumber,
97110
transitTierUploadTimestamp,
98111
mediaTierCdnNumber,
99-
mediaName,
100112
localKey,
113+
legacyDigest: _,
114+
legacyMediaName: _,
101115
special_fields: _,
102116
} = self;
103117

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+
}
110123

111-
let media_name = if mediaName.is_empty() {
112-
if mediaTierCdnNumber.is_some() {
113-
return Err(LocatorError::UnexpectedMediaTierCdnNumber);
124+
IntegrityCheck::EncryptedDigest { digest }
114125
}
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+
}
120134
}
121-
mediaName
135+
None => return Err(LocatorError::MissingIntegrityCheck),
122136
};
123137

124138
let transit =
125139
(transitCdnKey, transitCdnNumber, transitTierUploadTimestamp).try_into_with(context)?;
126140

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+
127163
Ok(LocatorInfo {
128164
key,
129165
local_key: localKey,
130-
digest,
131-
size,
166+
plaintext_size,
132167
transit,
133168
media_tier_cdn_number: mediaTierCdnNumber,
134-
media_name,
169+
integrity_check,
135170
})
136171
}
137172
}
@@ -324,48 +359,93 @@ mod test {
324359
impl proto::file_pointer::LocatorInfo {
325360
fn test_data() -> Self {
326361
Self {
327-
mediaName: "5678".into(),
328362
key: hex!("1234").into(),
329-
digest: hex!("5678").into(),
363+
legacyDigest: vec![],
364+
integrityCheck: None, // Will be set by specific test data methods
330365
size: 123,
331366
transitCdnKey: Some("ABCDEFG".into()),
332367
transitCdnNumber: Some(2),
333368
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+
),
334383
localKey: Some(b"local key".to_vec()),
335384
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()
337397
}
338398
}
339399
}
340400

341401
#[test]
342402
fn valid_locator_info() {
343403
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()),
345406
Ok(Locator::LocatorInfo(LocatorInfo {
346407
transit: Some(TransitTierLocator {
347408
cdn_key: "ABCDEFG".into(),
348409
cdn_number: 2,
349410
upload_timestamp: Some(Timestamp::test_value())
350411
}),
351412
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,
354417
media_tier_cdn_number: Some(87),
355-
media_name: "5678".into(),
356418
local_key: Some(b"local key".to_vec())
357419
}))
358420
)
359421
}
360422

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+
361445
#[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")]
366447
#[test_case(|x| x.mediaTierCdnNumber = None => Ok(()); "no mediaTierCdnNumber")]
367448
#[test_case(|x| x.key = vec![] => Err(LocatorError::MissingKey); "no key")]
368-
#[test_case(|x| x.digest = vec![] => Err(LocatorError::MissingDigest); "no digest")]
369449
#[test_case(|x| x.size = 0 => Ok(()); "size zero")]
370450
#[test_case(|x| x.transitCdnKey = None => Err(LocatorError::TransitCdnMismatch); "no transitCdnKey")]
371451
#[test_case(|x| x.transitCdnKey = Some("".into()) => Err(LocatorError::MissingTransitCdnKey); "empty transitCdnKey")]
@@ -384,7 +464,32 @@ mod test {
384464
fn locator_info(
385465
modifier: impl FnOnce(&mut proto::file_pointer::LocatorInfo),
386466
) -> 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();
388493
modifier(&mut locator);
389494
locator
390495
.try_into_with(&TestContext::default())
@@ -420,7 +525,7 @@ mod test {
420525

421526
#[test_case(|_| {} => Ok(()); "valid")]
422527
#[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();
424529
} => Ok(()); "with locatorInfo")]
425530
#[test_case(|x| x.locator = None => Ok(()); "no legacy locator")]
426531
#[test_case(|x| x.locatorInfo = None.into() => Err(FilePointerError::NoLocatorInfo); "no locatorInfo")]

rust/message-backup/src/proto/backup.proto

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -749,14 +749,24 @@ message FilePointer {
749749
}
750750

751751
message LocatorInfo {
752-
// Must be non-empty if transitCdnKey or mediaName are set/nonempty.
752+
// Must be non-empty if transitCdnKey or plaintextHash are set/nonempty.
753753
// Otherwise must be empty.
754754
bytes key = 1;
755+
755756
// From the sender of the attachment (incl. ourselves)
756-
// Must be non-empty if transitCdnKey or mediaName are set/nonempty.
757-
// Otherwise must be empty.
758-
bytes digest = 2;
759-
// Must be non-zero if transitCdnKey or mediaName are set/nonempty.
757+
// Will be reserved once all clients start reading integrityCheck
758+
bytes legacyDigest = 2;
759+
760+
oneof integrityCheck {
761+
// Set if file was at one point downloaded and its plaintextHash was calculated
762+
bytes plaintextHash = 10;
763+
764+
// Set if file has not been downloaded so its integrity has not been verified
765+
// From the sender of the attachment
766+
bytes encryptedDigest = 11;
767+
}
768+
769+
// Must be non-zero if transitCdnKey or plaintextHash are set/nonempty.
760770
// Otherwise must be zero.
761771
uint32 size = 3;
762772

@@ -775,7 +785,9 @@ message FilePointer {
775785

776786
// Nonempty any time the attachment was downloaded and its
777787
// digest validated, whether free tier or paid subscription.
778-
string mediaName = 8;
788+
// Will be reserved once all clients start reading integrityCheck,
789+
// when mediaName will be derived from the plaintextHash and encryption key
790+
string legacyMediaName = 8;
779791

780792
// Separate key used to encrypt this file for the local backup.
781793
// Generally required for local backups.

0 commit comments

Comments
 (0)