Export/import verified state

This commit is contained in:
Fedor Indutny 2024-11-12 12:43:52 -08:00 committed by GitHub
parent b517bb817f
commit 104995e980
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 83 additions and 23 deletions

View file

@ -9,8 +9,7 @@ option java_package = "org.thoughtcrime.securesms.backup.v2.proto";
message BackupInfo {
uint64 version = 1;
uint64 backupTimeMs = 2;
bytes mediaRootBackupKey = 3; // 32-byte random value generated when the
// backup is uploaded for the first time.
bytes mediaRootBackupKey = 3; // 32-byte random value generated when the backup is uploaded for the first time.
}
// Frames must follow in the following ordering rules:
@ -112,6 +111,12 @@ message Recipient {
}
message Contact {
enum IdentityState {
DEFAULT = 0;
VERIFIED = 1;
UNVERIFIED = 2;
}
message Registered { }
message NotRegistered {
uint64 unregisteredTimestamp = 1;
@ -140,6 +145,8 @@ message Contact {
optional string profileGivenName = 11;
optional string profileFamilyName = 12;
bool hideStory = 13;
optional bytes identityKey = 14;
IdentityState identityState = 15;
}
message Group {
@ -240,9 +247,9 @@ message Chat {
uint64 id = 1; // generated id for reference only within this file
uint64 recipientId = 2;
bool archived = 3;
uint32 pinnedOrder = 4; // 0 = unpinned, otherwise chat is considered pinned and will be displayed in ascending order
uint64 expirationTimerMs = 5; // 0 = no expire timer.
uint64 muteUntilMs = 6;
optional uint32 pinnedOrder = 4; // will be displayed in ascending order
optional uint64 expirationTimerMs = 5;
optional uint64 muteUntilMs = 6; // UINT64_MAX (2^63 - 1) = "always muted".
bool markedUnread = 7;
bool dontNotifyForMentionsIfMuted = 8;
ChatStyle style = 9;
@ -268,7 +275,7 @@ message CallLink {
optional bytes adminKey = 2; // Only present if the user is an admin
string name = 3;
Restrictions restrictions = 4;
uint64 expirationMs = 5;
optional uint64 expirationMs = 5;
}
message AdHocCall {
@ -285,6 +292,8 @@ message AdHocCall {
}
message DistributionListItem {
// distribution ids are UUIDv4s. "My Story" is represented
// by an all-0 UUID (00000000-0000-0000-0000-000000000000).
bytes distributionId = 1; // distribution list ids are uuids
oneof item {
@ -310,7 +319,7 @@ message DistributionList {
message ChatItem {
message IncomingMessageDetails {
uint64 dateReceived = 1;
uint64 dateServerSent = 2;
optional uint64 dateServerSent = 2;
bool read = 3;
bool sealedSender = 4;
}
@ -325,8 +334,8 @@ message ChatItem {
uint64 chatId = 1; // conversation id
uint64 authorId = 2; // recipient id
uint64 dateSent = 3;
uint64 expireStartDate = 4; // timestamp of when expiration timer started ticking down
uint64 expiresInMs = 5; // how long timer of message is (ms)
optional uint64 expireStartDate = 4; // timestamp of when expiration timer started ticking down
optional uint64 expiresInMs = 5; // how long timer of message is (ms)
repeated ChatItem revisions = 6; // ordered from oldest to newest
bool sms = 7;
@ -616,7 +625,7 @@ message FilePointer {
message AttachmentLocator {
string cdnKey = 1;
uint32 cdnNumber = 2;
uint64 uploadTimestamp = 3;
optional uint64 uploadTimestamp = 3;
bytes key = 4;
bytes digest = 5;
uint32 size = 6;
@ -650,7 +659,8 @@ message Quote {
enum Type {
UNKNOWN = 0;
NORMAL = 1;
GIFTBADGE = 2;
GIFT_BADGE = 2;
VIEW_ONCE = 3;
}
message QuotedAttachment {
@ -768,8 +778,7 @@ message GroupCall {
optional uint64 ringerRecipientId = 3;
optional uint64 startedCallRecipientId = 4;
uint64 startedCallTimestamp = 5;
// The time the call ended. 0 indicates an unknown time.
uint64 endedCallTimestamp = 6;
optional uint64 endedCallTimestamp = 6; // The time the call ended.
bool read = 7;
}
@ -825,7 +834,6 @@ message SessionSwitchoverChatUpdate {
message GroupChangeChatUpdate {
message Update {
// Note: group expiration timer changes are represented as ExpirationTimerChatUpdate.
oneof update {
GenericGroupUpdate genericGroupUpdate = 1;
GroupCreationUpdate groupCreationUpdate = 2;

View file

@ -17,7 +17,10 @@ import {
pauseWriteAccess,
resumeWriteAccess,
} from '../../sql/Client';
import type { PageMessagesCursorType } from '../../sql/Interface';
import type {
PageMessagesCursorType,
IdentityKeyType,
} from '../../sql/Interface';
import * as log from '../../logging/log';
import { GiftBadgeStates } from '../../components/conversation/Message';
import { type CustomColorType } from '../../types/Colors';
@ -290,6 +293,13 @@ export class BackupExportStream extends Readable {
stickerPacks: 0,
};
const identityKeys = await DataReader.getAllIdentityKeys();
const identityKeysById = new Map(
identityKeys.map(key => {
return [key.id, key];
})
);
for (const { attributes } of window.ConversationController.getAll()) {
const recipientId = this.getRecipientId({
id: attributes.id,
@ -297,7 +307,11 @@ export class BackupExportStream extends Readable {
e164: attributes.e164,
});
const recipient = this.toRecipient(recipientId, attributes);
const recipient = this.toRecipient(
recipientId,
attributes,
identityKeysById
);
if (recipient === undefined) {
// Can't be backed up.
continue;
@ -804,7 +818,8 @@ export class BackupExportStream extends Readable {
convo: Omit<
ConversationAttributesType,
'id' | 'version' | 'expireTimerVersion'
>
>,
identityKeysById?: ReadonlyMap<IdentityKeyType['id'], IdentityKeyType>
): Backups.IRecipient | undefined {
const res: Backups.IRecipient = {
id: recipientId,
@ -824,6 +839,11 @@ export class BackupExportStream extends Readable {
throw missingCaseError(convo.removalStage);
}
let identityKey: IdentityKeyType | undefined;
if (identityKeysById != null && convo.serviceId != null) {
identityKey = identityKeysById.get(convo.serviceId);
}
res.contact = {
aci:
convo.serviceId && convo.serviceId !== convo.pni
@ -856,6 +876,10 @@ export class BackupExportStream extends Readable {
profileGivenName: convo.profileName,
profileFamilyName: convo.profileFamilyName,
hideStory: convo.hideStory === true,
identityKey: identityKey?.publicKey || null,
// Integer values match so we can use it as is
identityState: identityKey?.verified ?? 0,
};
} else if (isGroupV2(convo) && convo.masterKey) {
let storySendMode: Backups.Group.StorySendMode;
@ -2092,6 +2116,15 @@ export class BackupExportStream extends Readable {
return null;
}
let quoteType: Backups.Quote.Type;
if (quote.isGiftBadge) {
quoteType = Backups.Quote.Type.GIFT_BADGE;
} else if (quote.isViewOnce) {
quoteType = Backups.Quote.Type.VIEW_ONCE;
} else {
quoteType = Backups.Quote.Type.NORMAL;
}
return {
targetSentTimestamp: Long.fromNumber(quote.id),
authorId,
@ -2123,9 +2156,7 @@ export class BackupExportStream extends Readable {
}
)
),
type: quote.isGiftBadge
? Backups.Quote.Type.GIFTBADGE
: Backups.Quote.Type.NORMAL,
type: quoteType,
};
}

View file

@ -14,6 +14,7 @@ import { DataReader, DataWriter } from '../../sql/Client';
import {
AttachmentDownloadSource,
type StoryDistributionWithMembersType,
type IdentityKeyType,
} from '../../sql/Interface';
import * as log from '../../logging/log';
import { GiftBadgeStates } from '../../components/conversation/Message';
@ -209,6 +210,7 @@ export class BackupImportStream extends Writable {
string,
ConversationAttributesType
>();
private readonly identityKeys = new Map<ServiceIdString, IdentityKeyType>();
private readonly saveMessageBatch = new Set<MessageAttributesType>();
private readonly stickerPacks = new Array<StickerPackPointerType>();
private ourConversation?: ConversationAttributesType;
@ -489,10 +491,14 @@ export class BackupImportStream extends Writable {
const saves = Array.from(this.conversations.values());
this.conversations.clear();
const identityKeys = Array.from(this.identityKeys.values());
this.identityKeys.clear();
// Queue writes at the same time to prevent races.
await Promise.all([
DataWriter.saveConversations(saves),
DataWriter.updateConversations(updates),
DataWriter.bulkAddIdentityKeys(identityKeys),
]);
}
@ -817,11 +823,13 @@ export class BackupImportStream extends Writable {
break;
}
const serviceId = aci ?? pni;
const attrs: ConversationAttributesType = {
id: generateUuid(),
type: 'private',
version: 2,
serviceId: aci ?? pni,
serviceId,
pni,
e164,
removalStage,
@ -836,6 +844,17 @@ export class BackupImportStream extends Writable {
expireTimerVersion: 1,
};
if (serviceId != null && Bytes.isNotEmpty(contact.identityKey)) {
this.identityKeys.set(serviceId, {
id: serviceId,
publicKey: contact.identityKey,
verified: contact.identityState || 0,
firstUse: true,
timestamp: this.now,
nonblockingApproval: true,
});
}
if (contact.notRegistered) {
const timestamp = contact.notRegistered.unregisteredTimestamp?.toNumber();
attrs.discoveredUnregisteredAt = timestamp || this.now;
@ -848,7 +867,6 @@ export class BackupImportStream extends Writable {
}
if (contact.blocked) {
const serviceId = aci || pni;
if (serviceId) {
await window.storage.blocked.addBlockedServiceId(serviceId);
}
@ -1671,8 +1689,11 @@ export class BackupImportStream extends Writable {
type: Backups.Quote.Type | null | undefined
): SignalService.DataMessage.Quote.Type {
switch (type) {
case Backups.Quote.Type.GIFTBADGE:
case Backups.Quote.Type.GIFT_BADGE:
return SignalService.DataMessage.Quote.Type.GIFT_BADGE;
case Backups.Quote.Type.VIEW_ONCE:
// No special treatment, we'll compute it once we find the message
return SignalService.DataMessage.Quote.Type.NORMAL;
case Backups.Quote.Type.NORMAL:
case Backups.Quote.Type.UNKNOWN:
case null: