0
0
mirror of https://github.com/signalapp/libsignal.git synced 2024-09-20 03:52:17 +02:00

Update message backup proto definition

This commit is contained in:
Alex Konradi 2024-06-13 15:37:26 -04:00 committed by GitHub
parent 4791773954
commit 24c234a5fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 191 additions and 148 deletions

1
Cargo.lock generated
View File

@ -2093,6 +2093,7 @@ dependencies = [
"thiserror",
"usernames",
"uuid",
"zkcredential",
"zkgroup",
]

View File

@ -26,6 +26,7 @@ libsignal-message-backup-macros = { path = "macros" }
libsignal-protocol = { path = "../protocol" }
signal-crypto = { path = "../crypto" }
usernames = { path = "../usernames" }
zkcredential = { path = "../zkcredential", features = ["rayon"] }
zkgroup = { path = "../zkgroup" }
aes = "0.8.3"

View File

@ -393,7 +393,7 @@ impl<M: Method> ChatsData<M> {
fn add_chat_item(
&mut self,
chat_id: ChatId,
mut item: ChatItemData,
mut item: ChatItemData<M>,
) -> Result<(), ChatFrameError> {
let Self {
chat_items_count,

View File

@ -25,6 +25,9 @@ use contact_message::*;
pub(crate) mod chat_style;
mod gift_badge;
use gift_badge::*;
mod group;
use group::*;
@ -103,6 +106,8 @@ pub enum ChatItemError {
StickerMessageMissingSticker,
/// sticker message: {0}
StickerMessage(#[from] MessageStickerError),
/// gift badge: {0}
GiftBadge(#[from] GiftBadgeError),
/// ChatItem.directionalDetails is a oneof but is empty
NoDirection,
/// outgoing message {0}
@ -132,10 +137,10 @@ pub struct InvalidExpiration {
/// Validated version of [`proto::Chat`].
#[derive_where(Debug)]
#[cfg_attr(test, derive_where(PartialEq; M::List<ChatItemData>: PartialEq))]
#[cfg_attr(test, derive_where(PartialEq; M::List<ChatItemData<M>>: PartialEq))]
pub struct ChatData<M: Method> {
pub recipient: RecipientId,
pub(super) items: M::List<ChatItemData>,
pub(super) items: M::List<ChatItemData<M>>,
pub expiration_timer: Option<Duration>,
pub mute_until: Option<Timestamp>,
pub style: Option<ChatStyle>,
@ -149,12 +154,12 @@ pub struct ChatData<M: Method> {
pub struct PinOrder(NonZeroU32);
/// Validated version of [`proto::ChatItem`].
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct ChatItemData {
#[derive_where(Debug)]
#[cfg_attr(test, derive_where(PartialEq; ChatItemMessage<M>: PartialEq))]
pub struct ChatItemData<M: Method> {
pub author: RecipientId,
pub message: ChatItemMessage,
pub revisions: Vec<ChatItemData>,
pub message: ChatItemMessage<M>,
pub revisions: Vec<ChatItemData<M>>,
pub direction: Direction,
pub expire_start: Option<Timestamp>,
pub expires_in: Option<Duration>,
@ -167,9 +172,9 @@ pub struct ChatItemData {
}
/// Validated version of [`proto::chat_item::Item`].
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub enum ChatItemMessage {
#[derive_where(Debug)]
#[cfg_attr(test, derive_where(PartialEq; M::BoxedValue<GiftBadge>: PartialEq))]
pub enum ChatItemMessage<M: Method> {
Standard(StandardMessage),
Contact(ContactMessage),
Voice(VoiceMessage),
@ -177,6 +182,7 @@ pub enum ChatItemMessage {
RemoteDeleted,
Update(UpdateMessage),
PaymentNotification(PaymentNotification),
GiftBadge(M::BoxedValue<GiftBadge>),
}
/// Validated version of [`proto::Reaction`].
@ -317,8 +323,8 @@ impl<M: Method, C: Contains<RecipientId> + Lookup<PinOrder, RecipientId>>
}
}
impl<R: Contains<RecipientId> + AsRef<BackupMeta>> TryFromWith<proto::ChatItem, R>
for ChatItemData
impl<R: Contains<RecipientId> + AsRef<BackupMeta>, M: Method> TryFromWith<proto::ChatItem, R>
for ChatItemData<M>
{
type Error = ChatItemError;
@ -353,7 +359,7 @@ impl<R: Contains<RecipientId> + AsRef<BackupMeta>> TryFromWith<proto::ChatItem,
let revisions: Vec<_> = revisions
.into_iter()
.map(|rev| {
let item: ChatItemData = rev.try_into_with(context)?;
let item: ChatItemData<M> = rev.try_into_with(context)?;
match &item.message {
ChatItemMessage::Update(update) => match update {
UpdateMessage::GroupCall(_) | UpdateMessage::IndividualCall(_) => {
@ -375,6 +381,7 @@ impl<R: Contains<RecipientId> + AsRef<BackupMeta>> TryFromWith<proto::ChatItem,
| ChatItemMessage::Voice(_)
| ChatItemMessage::PaymentNotification(_)
| ChatItemMessage::Sticker(_)
| ChatItemMessage::GiftBadge(_)
| ChatItemMessage::RemoteDeleted => (),
};
Ok(item)
@ -517,8 +524,8 @@ impl<R: Contains<RecipientId>> TryFromWith<proto::SendStatus, R> for OutgoingSen
}
}
impl<R: Contains<RecipientId> + AsRef<BackupMeta>> TryFromWith<proto::chat_item::Item, R>
for ChatItemMessage
impl<R: Contains<RecipientId> + AsRef<BackupMeta>, M: Method> TryFromWith<proto::chat_item::Item, R>
for ChatItemMessage<M>
{
type Error = ChatItemError;
@ -554,6 +561,7 @@ impl<R: Contains<RecipientId> + AsRef<BackupMeta>> TryFromWith<proto::chat_item:
Item::PaymentNotification(message) => {
ChatItemMessage::PaymentNotification(message.try_into()?)
}
Item::GiftBadge(badge) => ChatItemMessage::GiftBadge(M::boxed_value(badge.try_into()?)),
})
}
}
@ -731,7 +739,7 @@ mod test {
fn valid_chat_item() {
assert_eq!(
proto::ChatItem::test_data().try_into_with(&TestContext::default()),
Ok(ChatItemData {
Ok(ChatItemData::<Store> {
author: RecipientId(proto::Recipient::TEST_ID),
message: ChatItemMessage::Standard(StandardMessage::from_proto_test_data()),
revisions: vec![],
@ -806,7 +814,7 @@ mod test {
let result = message
.try_into_with(&TestContext::default())
.map(|_: ChatItemData| ());
.map(|_: ChatItemData<Store>| ());
assert_eq!(result, expected);
}
@ -880,7 +888,7 @@ mod test {
.unwrap();
item.expiresInMs = until_expiration_ms;
let result = ChatItemData::try_from_with(item, &TestContext(meta))
let result = ChatItemData::<Store>::try_from_with(item, &TestContext(meta))
.map(|_| ())
.map_err(|e| assert_matches!(e, ChatItemError::InvalidExpiration(e) => e).to_string());
assert_eq!(result, expected.map_err(ToString::to_string));
@ -893,7 +901,7 @@ mod test {
item.expiresInMs = 0;
assert_matches!(
ChatItemData::try_from_with(item, &TestContext::default()),
ChatItemData::<Store>::try_from_with(item, &TestContext::default()),
Err(ChatItemError::ExpirationMismatch)
);
}

View File

@ -0,0 +1,130 @@
//
// Copyright (C) 2024 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
use zkgroup::receipts::ReceiptCredentialPresentation;
use zkgroup::ZkGroupDeserializationFailure;
use crate::proto::backup as proto;
pub struct GiftBadge {
receipt_credential_presentation: ReceiptCredentialPresentation,
state: proto::gift_badge::State,
}
impl std::fmt::Debug for GiftBadge {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("GiftBadge")
.field(
"receipt_credential_presentation",
&zkcredential::PrintAsHex(
zkgroup::serialize(&self.receipt_credential_presentation).as_slice(),
),
)
.field("state", &self.state)
.finish()
}
}
#[cfg(test)]
impl PartialEq for GiftBadge {
fn eq(&self, other: &Self) -> bool {
zkgroup::serialize(&self.receipt_credential_presentation)
== zkgroup::serialize(&other.receipt_credential_presentation)
&& self.state == other.state
}
}
#[derive(Debug, thiserror::Error, displaydoc::Display)]
#[cfg_attr(test, derive(PartialEq))]
pub enum GiftBadgeError {
/// receipt credential presentation failed to deserialize
InvalidReceiptCredentialPresentation,
}
impl TryFrom<proto::GiftBadge> for GiftBadge {
type Error = GiftBadgeError;
fn try_from(value: proto::GiftBadge) -> Result<Self, Self::Error> {
let proto::GiftBadge {
receiptCredentialPresentation,
state,
special_fields: _,
} = value;
let receipt_credential_presentation = zkgroup::deserialize(&receiptCredentialPresentation)
.map_err(|_: ZkGroupDeserializationFailure| {
GiftBadgeError::InvalidReceiptCredentialPresentation
})?;
use proto::gift_badge::State;
let state = match state.enum_value_or_default() {
s @ (State::UNOPENED | State::OPENED | State::REDEEMED | State::FAILED) => s,
};
Ok(Self {
receipt_credential_presentation,
state,
})
}
}
#[cfg(test)]
mod test {
use zkgroup::RANDOMNESS_LEN;
use super::*;
impl proto::GiftBadge {
fn test_data_presentation() -> ReceiptCredentialPresentation {
const RANDOMNESS: [u8; RANDOMNESS_LEN] = [33; 32];
let server_params = zkgroup::ServerSecretParams::generate(RANDOMNESS);
let server_public_params = server_params.get_public_params();
let request_context = &server_public_params
.create_receipt_credential_request_context(RANDOMNESS, [59; 16]);
let request = request_context.get_request();
let response = server_params.issue_receipt_credential(
RANDOMNESS,
&request,
zkgroup::Timestamp::from_epoch_seconds(123456789),
6,
);
let credential = server_public_params
.receive_receipt_credential(request_context, &response)
.expect("valid request");
server_public_params.create_receipt_credential_presentation(RANDOMNESS, &credential)
}
fn test_data() -> Self {
Self {
receiptCredentialPresentation: zkgroup::serialize(&Self::test_data_presentation()),
state: proto::gift_badge::State::REDEEMED.into(),
special_fields: Default::default(),
}
}
}
#[test]
fn valid_gift_badge() {
assert_eq!(
proto::GiftBadge::test_data().try_into(),
Ok(GiftBadge {
receipt_credential_presentation: proto::GiftBadge::test_data_presentation(),
state: proto::gift_badge::State::REDEEMED,
})
);
}
#[test]
fn invalid_presentation() {
let mut badge = proto::GiftBadge::test_data();
badge.receiptCredentialPresentation = vec![];
assert_eq!(
GiftBadge::try_from(badge),
Err(GiftBadgeError::InvalidReceiptCredentialPresentation)
);
}
}

View File

@ -81,10 +81,12 @@ where
pub trait Method {
type Value<T: Debug>: Debug;
type BoxedValue<T: Debug>: Debug;
type Map<K: Eq + Hash + Debug, V: Debug>: Map<K, V> + Debug;
type List<T: Debug>: Extend<T> + Default + Debug;
fn value<T: Debug>(value: T) -> Self::Value<T>;
fn boxed_value<T: Debug>(value: T) -> Self::BoxedValue<T>;
}
pub enum ValidateOnly {}
@ -100,22 +102,28 @@ impl<T> Extend<T> for ValidateOnlyList {
impl Method for ValidateOnly {
type Value<T: Debug> = ();
type BoxedValue<T: Debug> = ();
type Map<K: Eq + Hash + Debug, V: Debug> = HashSet<K>;
type List<T: Debug> = ValidateOnlyList;
fn value<T: Debug>(_value: T) -> Self::Value<T> {}
fn boxed_value<T: Debug>(_value: T) -> Self::BoxedValue<T> {}
}
pub enum Store {}
impl Method for Store {
type Value<T: Debug> = T;
type BoxedValue<T: Debug> = Box<T>;
type Map<K: Eq + Hash + Debug, V: Debug> = HashMap<K, V>;
type List<T: Debug> = Vec<T>;
fn value<T: Debug>(value: T) -> Self::Value<T> {
value
}
fn boxed_value<T: Debug>(value: T) -> Self::BoxedValue<T> {
Box::new(value)
}
}
#[cfg(test)]

View File

@ -10,30 +10,16 @@
use derive_where::derive_where;
use crate::backup::method::{KeyExists, Map as _, Method};
use crate::backup::WithId;
use crate::backup::method::Method;
use crate::proto::backup as proto;
/// Validated version of [`proto::StickerPack`].
#[derive_where(Debug)]
#[cfg_attr(test, derive_where(PartialEq;
M::Map<StickerId, PackSticker>: PartialEq,
M::Value<Key>: PartialEq,
M::Value<String>: PartialEq
))]
pub struct StickerPack<M: Method> {
pub key: M::Value<Key>,
pub stickers: M::Map<StickerId, PackSticker>,
pub title: M::Value<String>,
pub author: M::Value<String>,
_limit_construction_to_module: (),
}
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct PackSticker {
pub id: StickerId,
pub emoji: String,
_limit_construction_to_module: (),
}
@ -69,22 +55,11 @@ impl TryFrom<Vec<u8>> for Key {
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
pub struct StickerId(u32);
impl WithId for proto::StickerPackSticker {
type Id = StickerId;
fn id(&self) -> Self::Id {
StickerId(self.id)
}
}
#[derive(Debug, thiserror::Error, displaydoc::Display)]
#[cfg_attr(test, derive(PartialEq))]
pub enum StickerPackError {
/// key is invalid
InvalidKey,
/// sticker pack ID is invalid
InvalidPackId,
/// {0:?} contains more than one sticker with {0:?}
DuplicateId(PackId, StickerId),
}
#[derive(Debug, thiserror::Error, displaydoc::Display)]
@ -100,57 +75,17 @@ impl<M: Method> TryFrom<proto::StickerPack> for StickerPack<M> {
type Error = StickerPackError;
fn try_from(value: proto::StickerPack) -> Result<Self, Self::Error> {
let proto::StickerPack {
packId,
packId: _,
packKey,
stickers,
title,
author,
special_fields: _,
} = value;
let pack_id = PackId(
packId
.try_into()
.map_err(|_| StickerPackError::InvalidPackId)?,
);
let key = packKey
.try_into()
.map_err(|_| StickerPackError::InvalidKey)?;
let stickers = {
let mut out = M::Map::default();
for sticker in stickers {
let sticker: PackSticker = sticker.try_into()?;
let id = sticker.id;
out.insert(id, sticker)
.map_err(|KeyExists| StickerPackError::DuplicateId(pack_id, id))?;
}
out
};
Ok(Self {
key: M::value(key),
stickers,
title: M::value(title),
author: M::value(author),
_limit_construction_to_module: (),
})
}
}
impl TryFrom<proto::StickerPackSticker> for PackSticker {
type Error = StickerPackError;
fn try_from(value: proto::StickerPackSticker) -> Result<Self, Self::Error> {
let proto::StickerPackSticker {
id,
emoji,
special_fields: _,
} = value;
Ok(Self {
id: StickerId(id),
emoji,
_limit_construction_to_module: (),
})
}
@ -190,8 +125,6 @@ impl TryFrom<proto::Sticker> for MessageSticker {
#[cfg(test)]
mod test {
use std::collections::HashMap;
use test_case::test_case;
use crate::backup::method::Store;
@ -208,31 +141,19 @@ mod test {
Self {
packId: Self::TEST_ID_BYTES.into(),
packKey: Self::TEST_KEY.into(),
stickers: vec![proto::StickerPackSticker::test_data()],
author: "author".to_owned(),
title: "title".to_owned(),
..Default::default()
}
}
}
impl proto::StickerPackSticker {
impl proto::Sticker {
pub(crate) const TEST_ID: StickerId = StickerId(9988);
fn test_data() -> Self {
Self {
id: Self::TEST_ID.0,
..Self::default()
}
}
}
impl proto::Sticker {
pub(crate) fn test_data() -> Self {
Self {
packId: proto::StickerPack::TEST_ID_BYTES.into(),
packKey: proto::StickerPack::TEST_KEY.into(),
stickerId: proto::StickerPackSticker::TEST_ID.0,
stickerId: Self::TEST_ID.0,
..Default::default()
}
}
@ -244,16 +165,6 @@ mod test {
proto::StickerPack::test_data().try_into(),
Ok(StickerPack::<Store> {
key: Key(proto::StickerPack::TEST_KEY),
stickers: HashMap::from([(
proto::StickerPackSticker::TEST_ID,
PackSticker {
id: proto::StickerPackSticker::TEST_ID,
emoji: "".to_owned(),
_limit_construction_to_module: (),
}
)]),
title: "title".to_owned(),
author: "author".to_owned(),
_limit_construction_to_module: ()
})
)
@ -273,19 +184,8 @@ mod test {
}
}
fn invalid_key(pack: &mut impl StickerPackFields) {
*pack.pack_key_mut() = vec![0xaa; 45];
}
fn no_key(pack: &mut impl StickerPackFields) {
*pack.pack_key_mut() = vec![];
}
fn no_stickers(pack: &mut proto::StickerPack) {
pack.stickers = vec![];
}
#[test_case(invalid_key, Err(StickerPackError::InvalidKey))]
#[test_case(no_key, Err(StickerPackError::InvalidKey))]
#[test_case(no_stickers, Ok(()))]
fn sticker_pack(mutator: fn(&mut proto::StickerPack), expected: Result<(), StickerPackError>) {
let mut sticker_pack = proto::StickerPack::test_data();
mutator(&mut sticker_pack);
@ -316,6 +216,12 @@ mod test {
fn unknown_sticker_id(input: &mut proto::Sticker) {
input.stickerId = 555555;
}
fn invalid_key(pack: &mut impl StickerPackFields) {
*pack.pack_key_mut() = vec![0xaa; 45];
}
fn no_key(pack: &mut impl StickerPackFields) {
*pack.pack_key_mut() = vec![];
}
#[test_case(invalid_key, Err(MessageStickerError::InvalidPackKey))]
#[test_case(no_key, Err(MessageStickerError::InvalidPackKey))]

View File

@ -301,15 +301,6 @@ message DistributionList {
repeated uint64 memberRecipientIds = 4; // generated recipient id
}
message Identity {
bytes serviceId = 1;
bytes identityKey = 2;
uint64 timestamp = 3;
bool firstUse = 4;
bool verified = 5;
bool nonblockingApproval = 6;
}
message ChatItem {
message IncomingMessageDetails {
uint64 dateReceived = 1;
@ -346,6 +337,7 @@ message ChatItem {
RemoteDeletedMessage remoteDeletedMessage = 14;
ChatUpdateMessage updateMessage = 15;
PaymentNotification paymentNotification = 16;
GiftBadge giftBadge = 17;
}
}
@ -436,6 +428,18 @@ message PaymentNotification {
}
message GiftBadge {
enum State {
UNOPENED = 0;
OPENED = 1;
REDEEMED = 2;
FAILED = 3;
}
bytes receiptCredentialPresentation = 1;
State state = 2;
}
message ContactAttachment {
message Name {
optional string givenName = 1;
@ -501,12 +505,6 @@ message ContactAttachment {
optional string organization = 7;
}
message DocumentMessage {
Text text = 1;
FilePointer document = 2;
repeated Reaction reactions = 3;
}
message StickerMessage {
Sticker sticker = 1;
repeated Reaction reactions = 2;
@ -1027,17 +1025,8 @@ message GroupExpirationTimerUpdate {
message StickerPack {
bytes packId = 1;
bytes packKey = 2;
string title = 3;
string author = 4;
repeated StickerPackSticker stickers = 5; // First one should be cover sticker.
}
message StickerPackSticker {
string emoji = 1;
uint32 id = 2;
}
message ChatStyle {
message Gradient {
uint32 angle = 1; // degrees