Use new compact representations in protobufs

Co-authored-by: trevor-signal <131492920+trevor-signal@users.noreply.github.com>
This commit is contained in:
Fedor Indutny 2025-06-25 10:30:40 -07:00 committed by GitHub
parent 157496f822
commit 8251720444
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
64 changed files with 1000 additions and 459 deletions

View file

@ -222,7 +222,7 @@
"@indutny/parallel-prettier": "3.0.0",
"@indutny/rezip-electron": "2.0.1",
"@napi-rs/canvas": "0.1.61",
"@signalapp/mock-server": "12.0.0",
"@signalapp/mock-server": "13.0.0",
"@storybook/addon-a11y": "8.4.4",
"@storybook/addon-actions": "8.4.4",
"@storybook/addon-controls": "8.4.4",

10
pnpm-lock.yaml generated
View file

@ -430,8 +430,8 @@ importers:
specifier: 0.1.61
version: 0.1.61
'@signalapp/mock-server':
specifier: 12.0.0
version: 12.0.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)
specifier: 13.0.0
version: 13.0.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)
'@storybook/addon-a11y':
specifier: 8.4.4
version: 8.4.4(storybook@8.4.4(bufferutil@4.0.9)(prettier@3.3.3)(utf-8-validate@5.0.10))
@ -2770,8 +2770,8 @@ packages:
'@signalapp/libsignal-client@0.74.1':
resolution: {integrity: sha512-PEJou0yrBvxaAGg7JjONlRNM/t3PCBuY96wu7W6+57e38/7Mibo9kAMfE5B8DgVv+DUNMW9AgJhx5McCoIXYew==}
'@signalapp/mock-server@12.0.0':
resolution: {integrity: sha512-5Ebu2c3/BViNsZ4yId8zfHyazMGUmsSfjMXXXFwNn7IYw0M0l/u+FFiR8SJdFnLoBbcxHG+KC3P+QqPdn91FIQ==}
'@signalapp/mock-server@13.0.0':
resolution: {integrity: sha512-2HsUu8CDtf6g4yt34hDO7tfWVqZ4lPdXLAPM8NElKr/OjO1KiUGXljZFfxNIC5UDySVHaC2//ROcRE+qrhqCyw==}
'@signalapp/parchment-cjs@3.0.1':
resolution: {integrity: sha512-hSBMQ1M7wE4GcC8ZeNtvpJF+DAJg3eIRRf1SiHS3I3Algav/sgJJNm6HIYm6muHuK7IJmuEjkL3ILSXgmu0RfQ==}
@ -12466,7 +12466,7 @@ snapshots:
type-fest: 4.26.1
uuid: 11.0.2
'@signalapp/mock-server@12.0.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)':
'@signalapp/mock-server@13.0.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)':
dependencies:
'@indutny/parallel-prettier': 3.0.0(prettier@3.3.3)
'@signalapp/libsignal-client': 0.60.2

View file

@ -1,41 +1,56 @@
// Copyright 2014 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/*
* Copyright 2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
syntax = "proto2";
package signalservice;
message ProvisioningUuid {
optional string uuid = 1;
}
option java_package = "org.whispersystems.signalservice.internal.push";
option java_outer_classname = "ProvisioningProtos";
// An opaque address sent by the server when clients first open a provisioning
// WebSocket
message ProvisioningAddress {
// The opaque provisioning address for the active provisioning WebSocket
// session; clients should not attempt to interpret or modify the contents
// of the address string
optional string address = 1;
}
message ProvisionEnvelope {
optional bytes publicKey = 1;
optional bytes body = 2; // Encrypted ProvisionMessage
optional bytes body = 2; // Encrypted ProvisionMessage
}
message ProvisionMessage {
optional bytes aciIdentityKeyPublic = 1;
optional bytes aciIdentityKeyPrivate = 2;
optional bytes pniIdentityKeyPublic = 11;
optional bytes pniIdentityKeyPrivate = 12;
optional string aci = 8;
optional string pni = 10;
optional string number = 3;
optional string provisioningCode = 4;
optional string userAgent = 5;
optional bytes profileKey = 6;
optional bool readReceipts = 7;
optional uint32 ProvisioningVersion = 9;
optional bytes masterKey = 13;
optional bytes ephemeralBackupKey = 14; // 32 bytes
optional string accountEntropyPool = 15;
optional bytes mediaRootBackupKey = 16; // 32-bytes
optional bytes aciIdentityKeyPublic = 1;
optional bytes aciIdentityKeyPrivate = 2;
optional bytes pniIdentityKeyPublic = 11;
optional bytes pniIdentityKeyPrivate = 12;
optional string aci = 8;
optional string pni = 10;
optional string number = 3;
optional string provisioningCode = 4;
optional string userAgent = 5;
optional bytes profileKey = 6;
optional bool readReceipts = 7;
optional uint32 provisioningVersion = 9;
optional bytes masterKey = 13; // Deprecated, but required by linked devices
optional bytes ephemeralBackupKey = 14; // 32 bytes
optional string accountEntropyPool = 15;
optional bytes mediaRootBackupKey = 16; // 32-bytes
optional bytes aciBinary = 17; // 16-byte UUID
optional bytes pniBinary = 18; // 16-byte UUID
// NEXT ID: 19
}
enum ProvisioningVersion {
option allow_alias = true;
INITIAL = 0;
INITIAL = 0;
TABLET_SUPPORT = 1;
CURRENT = 1;
CURRENT = 1;
}

41
protos/Migrations.proto Normal file
View file

@ -0,0 +1,41 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
syntax = "proto3";
package migrations;
// Snapshot made at 9f22445e9
message Envelope {
// Our parser does not handle reserved in enums: DESKTOP-1569
enum Type {
UNKNOWN = 0;
CIPHERTEXT = 1; // content => (version byte | SignalMessage{Content})
// reserved 2;
// reserved "KEY_EXCHANGE";
PREKEY_BUNDLE = 3; // content => (version byte | PreKeySignalMessage{Content})
SERVER_DELIVERY_RECEIPT = 5; // legacyMessage => [] AND content => []
UNIDENTIFIED_SENDER = 6; // legacyMessage => [] AND content => ((version byte | UnidentifiedSenderMessage) OR (version byte | Multi-Recipient Sealed Sender Format))
SENDERKEY_MESSAGE = 7; // legacyMessage => [] AND content => (version byte | SenderKeyMessage)
PLAINTEXT_CONTENT = 8; // legacyMessage => [] AND content => (marker byte | Content)
}
optional Type type = 1;
reserved 2; // formerly optional string sourceE164 = 2;
optional string sourceServiceId = 11;
optional uint32 sourceDevice = 7;
optional string destinationServiceId = 13;
reserved 3; // formerly optional string relay = 3;
optional uint64 timestamp = 5;
reserved 6; // formerly optional bytes legacyMessage = 6; // Contains an encrypted DataMessage; this field could have been set historically for type 1 or 3 messages; no longer in use
optional bytes content = 8; // Contains an encrypted Content
optional string serverGuid = 9;
optional uint64 serverTimestamp = 10;
optional bool ephemeral = 12; // indicates that the message should not be persisted if the recipient is offline
optional bool urgent = 14 [default = true]; // indicates that the content is considered timely by the sender; defaults to true so senders have to opt-out to say something isn't time critical
optional string updatedPni = 15; // for number-change synchronization messages, provides the new server-assigned phone number identifier associated with the changed number
optional bool story = 16; // indicates that the content is a story.
optional bytes report_spam_token = 17; // token sent when reporting spam
reserved 18; // internal server use
// next: 19
}

View file

@ -3,32 +3,89 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
syntax = "proto2";
package signalservice;
option java_package = "org.whispersystems.signalservice.internal.push";
option java_outer_classname = "SignalServiceProtos";
message Envelope {
// Our parser does not handle reserved in enums: DESKTOP-1569
enum Type {
UNKNOWN = 0;
CIPHERTEXT = 1; // content => (version byte | SignalMessage{Content})
// reserved 2;
// reserved "KEY_EXCHANGE";
PREKEY_BUNDLE = 3; // content => (version byte | PreKeySignalMessage{Content})
SERVER_DELIVERY_RECEIPT = 5; // legacyMessage => [] AND content => []
UNIDENTIFIED_SENDER = 6; // legacyMessage => [] AND content => ((version byte | UnidentifiedSenderMessage) OR (version byte | Multi-Recipient Sealed Sender Format))
SENDERKEY_MESSAGE = 7; // legacyMessage => [] AND content => (version byte | SenderKeyMessage)
PLAINTEXT_CONTENT = 8; // legacyMessage => [] AND content => (marker byte | Content)
/**
* A double-ratchet message represents a "normal," "unsealed-sender" message
* encrypted using the Double Ratchet within an established Signal session.
* Double-ratchet messages include sender information in the plaintext
* portion of the `Envelope`.
*/
DOUBLE_RATCHET = 1; // content => (version byte | SignalMessage{Content})
reserved 2;
reserved "KEY_EXCHANGE";
/**
* A prekey message begins a new Signal session. The `content` of a prekey
* message is a superset of a double-ratchet message's `content` and
* contains the sender's identity public key and information identifying the
* pre-keys used in the message's ciphertext. Like double-ratchet messages,
* prekey messages contain sender information in the plaintext portion of
* the `Envelope`.
*/
PREKEY_MESSAGE = 3; // content => (version byte | PreKeySignalMessage{Content})
/**
* Server delivery receipts are generated by the server when
* "unsealed-sender" messages are delivered to and acknowledged by the
* destination device. Server delivery receipts identify the sender in the
* plaintext portion of the `Envelope` and have no `content`. Note that
* receipts for sealed-sender messages are generated by clients as
* `UNIDENTIFIED_SENDER` messages.
*
* Note that, with server delivery receipts, the "client timestamp" on
* the envelope refers to the timestamp of the original message (i.e. the
* message the server just delivered) and not to the time of delivery. The
* "server timestamp" refers to the time of delivery.
*/
SERVER_DELIVERY_RECEIPT = 5; // content => []
/**
* An unidentified sender message represents a message with no sender
* information in the plaintext portion of the `Envelope`. Unidentified
* sender messages always contain an additional `subtype` in their
* `content`. They may or may not be part of an existing Signal session
* (i.e. an unidentified sender message may have a "prekey message"
* subtype or may indicate an encryption error).
*/
UNIDENTIFIED_SENDER = 6; // content => ((version byte | UnidentifiedSenderMessage) OR (version byte | Multi-Recipient Sealed Sender Format))
reserved 7;
reserved "SENDERKEY_MESSAGE";
/**
* A plaintext message is used solely to convey encryption error receipts
* and never contains encrypted message content. Encryption error receipts
* must be delivered in plaintext because, encryption/decryption of a prior
* message failed and there is no reason to believe that
* encryption/decryption of subsequent messages with the same key material
* would succeed.
*
* Critically, plaintext messages never have "real" message content
* generated by users. Plaintext messages include sender information.
*/
PLAINTEXT_CONTENT = 8; // content => (marker byte | Content)
// next: 9
}
optional Type type = 1;
reserved 2; // formerly optional string sourceE164 = 2;
optional string sourceServiceId = 11;
optional uint32 sourceDevice = 7;
optional uint32 sourceDeviceId = 7;
optional string destinationServiceId = 13;
reserved 3; // formerly optional string relay = 3;
optional uint64 timestamp = 5;
optional uint64 clientTimestamp = 5;
reserved 6; // formerly optional bytes legacyMessage = 6; // Contains an encrypted DataMessage; this field could have been set historically for type 1 or 3 messages; no longer in use
optional bytes content = 8; // Contains an encrypted Content
optional string serverGuid = 9;
@ -39,7 +96,11 @@ message Envelope {
optional bool story = 16; // indicates that the content is a story.
optional bytes report_spam_token = 17; // token sent when reporting spam
reserved 18; // internal server use
// next: 19
optional bytes sourceServiceIdBinary = 19; // service ID binary (i.e. 16 byte UUID for ACI, 1 byte prefix + 16 byte UUID for PNI)
optional bytes destinationServiceIdBinary = 20; // service ID binary (i.e. 16 byte UUID for ACI, 1 byte prefix + 16 byte UUID for PNI)
optional bytes serverGuidBinary = 21; // 16-byte UUID
optional bytes updatedPniBinary = 22; // 16-byte UUID
// next: 22
}
message Content {
@ -191,6 +252,7 @@ message DataMessage {
repeated QuotedAttachment attachments = 4;
repeated BodyRange bodyRanges = 6;
optional Type type = 7;
optional bytes authorAciBinary = 8; // 16-byte UUID
}
message Contact {
@ -275,6 +337,7 @@ message DataMessage {
reserved /* targetAuthorE164 */ 3;
optional string targetAuthorAci = 4;
optional uint64 targetSentTimestamp = 5;
optional bytes targetAuthorAciBinary = 6; // 16-byte UUID
}
message Delete {
@ -288,6 +351,7 @@ message DataMessage {
message StoryContext {
optional string authorAci = 1;
optional uint64 sentTimestamp = 2;
optional bytes authorAciBinary = 3; // 16-byte UUID
}
enum ProtocolVersion {
@ -419,6 +483,7 @@ message Verified {
optional bytes identityKey = 2;
optional State state = 3;
optional bytes nullMessage = 4;
optional bytes destinationAciBinary = 6; // 16-byte UUID
}
message SyncMessage {
@ -429,6 +494,7 @@ message SyncMessage {
optional bool unidentified = 2;
reserved /*destinationPni */ 4;
optional bytes destinationPniIdentityKey = 5; // Only set for PNI destinations
optional bytes destinationServiceIdBinary = 6; // service ID binary (i.e. 16 byte UUID for ACI, 1 byte prefix + 16 byte UUID for PNI)
}
message StoryMessageRecipient {
@ -436,6 +502,7 @@ message SyncMessage {
repeated string distributionListIds = 2;
optional bool isAllowedToReply = 3;
reserved /*destinationPni */ 4;
optional bytes destinationServiceIdBinary = 5; // service ID binary (i.e. 16 byte UUID for ACI, 1 byte prefix + 16 byte UUID for PNI)
}
optional string destinationE164 = 1;
@ -449,7 +516,8 @@ message SyncMessage {
repeated StoryMessageRecipient storyMessageRecipients = 9;
optional EditMessage editMessage = 10;
reserved /*destinationPni */ 11;
// Next ID: 12
optional bytes destinationServiceIdBinary = 12; // service ID binary (i.e. 16 byte UUID for ACI, 1 byte prefix + 16 byte UUID for PNI)
// Next ID: 13
}
message Contacts {
@ -461,6 +529,7 @@ message SyncMessage {
repeated string numbers = 1;
repeated string acis = 3;
repeated bytes groupIds = 2;
repeated bytes acisBinary = 4; // 16-byte UUID
}
message Request {
@ -481,12 +550,14 @@ message SyncMessage {
reserved /*senderE164*/ 1;
optional string senderAci = 3;
optional uint64 timestamp = 2;
optional bytes senderAciBinary = 4; // 16-byte UUID
}
message Viewed {
reserved /*senderE164*/ 1;
optional string senderAci = 3;
optional uint64 timestamp = 2;
optional bytes senderAciBinary = 4; // 16-byte UUID
}
message Configuration {
@ -513,6 +584,7 @@ message SyncMessage {
reserved /*senderE164*/ 1;
optional string senderAci = 3;
optional uint64 timestamp = 2;
optional bytes senderAciBinary = 4; // 16-byte UUID
}
message FetchLatest {
@ -553,6 +625,25 @@ message SyncMessage {
optional string threadAci = 2;
optional bytes groupId = 3;
optional Type type = 4;
optional bytes threadAciBinary = 5; // 16-byte UUID
}
message OutgoingPayment {
message MobileCoin {
optional bytes recipientAddress = 1;
optional uint64 amountPicoMob = 2;
optional uint64 feePicoMob = 3;
optional bytes receipt = 4;
optional uint64 ledgerBlockTimestamp = 5;
optional uint64 ledgerBlockIndex = 6;
repeated bytes spentKeyImages = 7;
repeated bytes outputPublicKeys = 8;
}
optional string recipientServiceId = 1;
optional string note = 2;
oneof attachment_identifier {
MobileCoin mobileCoin = 3;
}
}
message PniChangeNumber {
@ -719,6 +810,7 @@ message SyncMessage {
optional FetchLatest fetchLatest = 12;
optional Keys keys = 13;
optional MessageRequestResponse messageRequestResponse = 14;
optional OutgoingPayment outgoingPayment = 15;
repeated Viewed viewed = 16;
reserved /*pniIdentity*/ 17;
optional PniChangeNumber pniChangeNumber = 18;
@ -732,11 +824,10 @@ message SyncMessage {
}
message AttachmentPointer {
// Our parser does not handle reserved in enums: DESKTOP-1569
enum Flags {
VOICE_MESSAGE = 1;
BORDERLESS = 2;
// reserved 4;
reserved 4;
GIF = 8;
}
@ -781,6 +872,7 @@ message ContactDetails {
optional string number = 1;
optional string aci = 9;
optional bytes aciBinary = 13; // 16-byte UUID
optional string name = 2;
optional Avatar avatar = 3;
reserved /* color */ 4;
@ -791,7 +883,18 @@ message ContactDetails {
optional uint32 expireTimerVersion = 12;
optional uint32 inboxPosition = 10;
reserved /* archived */ 11;
// NEXT ID: 13
// NEXT ID: 14
}
message PaymentAddress {
message MobileCoin {
optional bytes publicAddress = 1;
optional bytes signature = 2;
}
oneof Address {
MobileCoin mobileCoin = 1;
}
}
message DecryptionErrorMessage {
@ -827,6 +930,7 @@ message BodyRange {
oneof associatedValue {
string mentionAci = 3;
Style style = 4;
bytes mentionAciBinary = 5; // 16-byte UUID
}
}
@ -834,6 +938,7 @@ message AddressableMessage {
oneof author {
string authorServiceId = 1;
string authorE164 = 2;
bytes authorServiceIdBinary = 4; // service ID binary (i.e. 16 byte UUID for ACI, 1 byte prefix + 16 byte UUID for PNI)
}
optional uint64 sentTimestamp = 3;
}
@ -843,5 +948,6 @@ message ConversationIdentifier {
string threadServiceId = 1;
bytes threadGroupId = 2;
string threadE164 = 3;
bytes threadServiceIdBinary = 4; // service ID binary (i.e. 16 byte UUID for ACI, 1 byte prefix + 16 byte UUID for PNI)
}
}

View file

@ -142,7 +142,9 @@ message ContactRecord {
Name nickname = 22;
string note = 23;
optional AvatarColor avatarColor = 24;
// Next ID: 25
bytes aciBinary = 25; // 16-byte UUID
bytes pniBinary = 26; // 16-byte UUID
// Next ID: 27
}
message GroupV1Record {
@ -174,6 +176,11 @@ message GroupV2Record {
optional AvatarColor avatarColor = 11;
}
message Payments {
bool enabled = 1;
bytes entropy = 2;
}
message AccountRecord {
enum PhoneNumberSharingMode {
@ -186,6 +193,7 @@ message AccountRecord {
message Contact {
string serviceId = 1;
string e164 = 2;
bytes serviceIdBinary = 3; // service ID binary (i.e. 16 byte UUID for ACI, 1 byte prefix + 16 byte UUID for PNI)
}
oneof identifier {
@ -301,6 +309,7 @@ message StoryDistributionListRecord {
uint64 deletedAtTimestamp = 4;
bool allowsReplies = 5;
bool isBlockList = 6;
repeated bytes recipientServiceIdsBinary = 7; // service ID binary (i.e. 16 byte UUID for ACI, 1 byte prefix + 16 byte UUID for PNI)
}
message StickerPackRecord {
@ -335,6 +344,7 @@ message Recipient {
message Contact {
string serviceId = 1;
string e164 = 2;
bytes serviceIdBinary = 3; // service ID binary (i.e. 16 byte UUID for ACI, 1 byte prefix + 16 byte UUID for PNI)
}
oneof identifier {

View file

@ -7,6 +7,7 @@ import {
signal as Signal,
signalbackups as Backups,
signalservice as SignalService,
migrations as Migrations,
} from './compiled';
export { Backups, SignalService, Signal };
export { Backups, SignalService, Signal, Migrations };

View file

@ -10,7 +10,6 @@ import {
parseContactsV2,
type ContactDetailsWithAvatar,
} from '../textsecure/ContactsParser';
import { normalizeAci } from '../util/normalizeAci';
import * as Conversation from '../types/Conversation';
import * as Errors from '../types/errors';
import type { ValidateConversationType } from '../model-types.d';
@ -152,7 +151,7 @@ async function doContactSync({
for (const details of contacts) {
const partialConversation: ValidateConversationType = {
e164: details.number,
serviceId: normalizeAci(details.aci, 'doContactSync'),
serviceId: details.aci,
type: 'private',
};
@ -167,7 +166,7 @@ async function doContactSync({
const { conversation } = window.ConversationController.maybeMergeContacts({
e164: details.number,
aci: normalizeAci(details.aci, 'contactSync.aci'),
aci: details.aci,
reason: logId,
});

View file

@ -81,6 +81,7 @@ import {
callLinkFromRecord,
getRoomIdFromRootKeyString,
} from '../util/callLinksRingrtc';
import { fromPniUuidBytesOrUntaggedString } from '../util/ServiceId';
import { callLinkRefreshJobQueue } from '../jobs/callLinkRefreshJobQueue';
const log = createLogger('storage');
@ -1800,11 +1801,16 @@ async function processRemoteRecords(
return true;
}
if (!contact.e164 || !contact.pni) {
const pni = fromPniUuidBytesOrUntaggedString(
contact.pniBinary,
contact.pni,
'splitPNIContacts'
);
if (!contact.e164 || !pni) {
return true;
}
const localAci = window.ConversationController.get(contact.pni)?.getAci();
const localAci = window.ConversationController.get(pni)?.getAci();
if (!localAci) {
return true;
}

View file

@ -45,14 +45,10 @@ import { normalizeStoryDistributionId } from '../types/StoryDistributionId';
import type { StoryDistributionIdString } from '../types/StoryDistributionId';
import type { ServiceIdString } from '../types/ServiceId';
import {
normalizeServiceId,
normalizePni,
ServiceIdKind,
isUntaggedPniString,
normalizeServiceId,
toUntaggedPni,
toTaggedPni,
} from '../types/ServiceId';
import { normalizeAci } from '../util/normalizeAci';
import { isAciString } from '../util/isAciString';
import * as Stickers from '../types/Stickers';
import type {
@ -84,6 +80,15 @@ import {
generateBackupsSubscriberData,
saveBackupsSubscriberData,
} from '../util/backupSubscriptionData';
import {
toAciObject,
toPniObject,
toServiceIdObject,
fromServiceIdBinaryOrString,
fromAciUuidBytesOrString,
fromPniUuidBytesOrUntaggedString,
} from '../util/ServiceId';
import { isProtoBinaryEncodingEnabled } from '../util/isProtoBinaryEncodingEnabled';
import { getLinkPreviewSetting } from '../types/LinkPreview';
import {
getReadReceiptSetting,
@ -228,7 +233,11 @@ export async function toContactRecord(
const contactRecord = new Proto.ContactRecord();
const aci = conversation.getAci();
if (aci) {
contactRecord.aci = aci;
if (isProtoBinaryEncodingEnabled()) {
contactRecord.aciBinary = toAciObject(aci).getRawUuidBytes();
} else {
contactRecord.aci = aci;
}
}
const e164 = conversation.get('e164');
if (e164) {
@ -241,7 +250,11 @@ export async function toContactRecord(
}
const pni = conversation.getPni();
if (pni) {
contactRecord.pni = toUntaggedPni(pni);
if (isProtoBinaryEncodingEnabled()) {
contactRecord.pniBinary = toPniObject(pni).getRawUuidBytes();
} else {
contactRecord.pni = toUntaggedPni(pni);
}
}
contactRecord.pniSignatureVerified =
conversation.get('pniSignatureVerified') ?? false;
@ -411,9 +424,19 @@ export function toAccountRecord(
new Proto.AccountRecord.PinnedConversation();
if (pinnedConversation.get('type') === 'private') {
const serviceId = pinnedConversation.getServiceId();
pinnedConversationRecord.identifier = 'contact';
pinnedConversationRecord.contact = {
serviceId: pinnedConversation.getServiceId(),
...(isProtoBinaryEncodingEnabled()
? {
serviceIdBinary:
serviceId == null
? null
: toServiceIdObject(serviceId).getServiceIdBinary(),
}
: {
serviceId,
}),
e164: pinnedConversation.get('e164'),
};
} else if (isGroupV1(pinnedConversation.attributes)) {
@ -621,8 +644,16 @@ export function toStoryDistributionListRecord(
storyDistributionListRecord.isBlockList = Boolean(
storyDistributionList.isBlockList
);
storyDistributionListRecord.recipientServiceIds =
storyDistributionList.members;
if (isProtoBinaryEncodingEnabled()) {
storyDistributionListRecord.recipientServiceIdsBinary =
storyDistributionList.members.map(serviceId => {
return toServiceIdObject(serviceId).getServiceIdBinary();
});
} else {
storyDistributionListRecord.recipientServiceIds =
storyDistributionList.members;
}
if (storyDistributionList.storageUnknownFields) {
storyDistributionListRecord.$unknownFields = [
@ -1108,17 +1139,16 @@ export async function mergeContactRecord(
const contactRecord = {
...originalContactRecord,
aci: originalContactRecord.aci
? normalizeAci(originalContactRecord.aci, 'ContactRecord.aci')
: undefined,
pni:
originalContactRecord.pni &&
isUntaggedPniString(originalContactRecord.pni)
? normalizePni(
toTaggedPni(originalContactRecord.pni),
'ContactRecord.pni'
)
: undefined,
aci: fromAciUuidBytesOrString(
originalContactRecord.aciBinary,
originalContactRecord.aci,
'ContactRecord.aci'
),
pni: fromPniUuidBytesOrUntaggedString(
originalContactRecord.pniBinary,
originalContactRecord.pni,
'ContactRecord.pni'
),
};
const e164 = dropNull(contactRecord.e164);
@ -1481,19 +1511,22 @@ export async function mergeAccountRecord(
let convo: ConversationModel | undefined;
if (contact) {
if (!contact.serviceId && !contact.e164) {
if (
!contact.serviceId &&
!Bytes.isNotEmpty(contact.serviceIdBinary) &&
!contact.e164
) {
log.error(
'storageService.mergeAccountRecord: No serviceId or e164 on contact'
);
return undefined;
}
convo = window.ConversationController.lookupOrCreate({
serviceId: contact.serviceId
? normalizeServiceId(
contact.serviceId,
'AccountRecord.pin.serviceId'
)
: undefined,
serviceId: fromServiceIdBinaryOrString(
contact.serviceIdBinary,
contact.serviceId,
'AccountRecord.pin.serviceId'
),
e164: contact.e164,
reason: 'storageService.mergeAccountRecord',
});
@ -1751,9 +1784,20 @@ export async function mergeStoryDistributionListRecord(
storyDistributionListRecord
);
const remoteListMembers: Array<ServiceIdString> = (
storyDistributionListRecord.recipientServiceIds || []
).map(id => normalizeServiceId(id, 'mergeStoryDistributionListRecord'));
let remoteListMembers: Array<ServiceIdString>;
if (storyDistributionListRecord.recipientServiceIdsBinary?.length) {
remoteListMembers =
storyDistributionListRecord.recipientServiceIdsBinary.map(id =>
fromServiceIdBinaryOrString(id, undefined, 'unused')
);
} else if (storyDistributionListRecord.recipientServiceIds?.length) {
remoteListMembers = storyDistributionListRecord.recipientServiceIds.map(
id => normalizeServiceId(id, 'mergeStoryDistributionListRecord')
);
} else {
remoteListMembers = [];
}
if (storyDistributionListRecord.$unknownFields) {
details.push('adding unknown fields');

View file

@ -9,7 +9,7 @@ import {
toTaggedPni,
isUntaggedPniString,
} from '../../types/ServiceId';
import { SignalService as Proto } from '../../protobuf';
import { Migrations as Proto } from '../../protobuf';
import { sql } from '../util';
import type { WritableDB } from '../Interface';
import { getOurUuid } from './41-uuid-keys';

View file

@ -13,9 +13,11 @@ import type { ProcessedAttachment } from '../textsecure/Types.d';
import { SignalService as Proto } from '../protobuf';
import { IMAGE_GIF, IMAGE_JPEG, LONG_MESSAGE } from '../types/MIME';
import { generateAci } from '../types/ServiceId';
import { toAciObject } from '../util/ServiceId';
import { uuidToBytes } from '../util/uuidToBytes';
const ACI_1 = generateAci();
const ACI_BINARY_1 = toAciObject(ACI_1).getRawUuidBytes();
const FLAGS = Proto.DataMessage.Flags;
const TIMESTAMP = Date.now();
@ -205,7 +207,7 @@ describe('processDataMessage', () => {
const out = check({
quote: {
id: Long.fromNumber(1),
authorAci: ACI_1,
authorAciBinary: ACI_BINARY_1,
text: 'text',
attachments: [
{
@ -298,7 +300,7 @@ describe('processDataMessage', () => {
check({
reaction: {
emoji: '😎',
targetAuthorAci: ACI_1,
targetAuthorAciBinary: ACI_BINARY_1,
targetSentTimestamp: Long.fromNumber(TIMESTAMP),
},
}).reaction,
@ -315,7 +317,7 @@ describe('processDataMessage', () => {
reaction: {
emoji: '😎',
remove: true,
targetAuthorAci: ACI_1,
targetAuthorAciBinary: ACI_BINARY_1,
targetSentTimestamp: Long.fromNumber(TIMESTAMP),
},
}).reaction,

View file

@ -13,6 +13,7 @@ import { createLogger } from '../logging/log';
import * as Bytes from '../Bytes';
import * as Errors from '../types/errors';
import { APPLICATION_OCTET_STREAM } from '../types/MIME';
import { type AciString, generateAci } from '../types/ServiceId';
import { SignalService as Proto } from '../protobuf';
import {
ParseContactsTransform,
@ -21,12 +22,15 @@ import {
import type { ContactDetailsWithAvatar } from '../textsecure/ContactsParser';
import { createTempDir, deleteTempDir } from '../updater/common';
import { strictAssert } from '../util/assert';
import { toAciObject } from '../util/ServiceId';
import { generateKeys, encryptAttachmentV2ToDisk } from '../AttachmentCrypto';
const log = createLogger('ContactsParser_test');
const { Writer } = protobuf;
const DEFAULT_ACI = generateAci();
describe('ContactsParser', () => {
let tempDir: string;
@ -130,9 +134,9 @@ describe('ContactsParser', () => {
try {
const avatarBuffer = generateAvatar();
const bytes = Bytes.concatenate([
generatePrefixedContact(avatarBuffer, 'invalid'),
generatePrefixedContact(avatarBuffer, null),
avatarBuffer,
generatePrefixedContact(undefined, 'invalid'),
generatePrefixedContact(undefined, null),
getTestBuffer(),
]);
@ -216,12 +220,12 @@ function getTestBuffer(): Uint8Array {
function generatePrefixedContact(
avatarBuffer: Uint8Array | undefined,
aci = '7198E1BD-1293-452A-A098-F982FF201902'
aci: AciString | null = DEFAULT_ACI
) {
const contactInfoBuffer = Proto.ContactDetails.encode({
name: 'Zero Cool',
number: '+10000000000',
aci,
aciBinary: aci == null ? null : toAciObject(aci).getRawUuidBytes(),
avatar: avatarBuffer
? { contentType: 'image/jpeg', length: avatarBuffer.length }
: undefined,
@ -239,7 +243,7 @@ async function verifyContact(
): Promise<void> {
assert.strictEqual(contact.name, 'Zero Cool');
assert.strictEqual(contact.number, '+10000000000');
assert.strictEqual(contact.aci, '7198e1bd-1293-452a-a098-f982ff201902');
assert.strictEqual(contact.aci, DEFAULT_ACI);
if (avatarIsMissing) {
return;

View file

@ -11,6 +11,7 @@ import { IncomingWebSocketRequestLegacy } from '../textsecure/WebsocketResources
import type { DecryptionErrorEvent } from '../textsecure/messageReceiverEvents';
import { generateAci } from '../types/ServiceId';
import type { AciString } from '../types/ServiceId';
import { toAciObject } from '../util/ServiceId';
import { SignalService as Proto } from '../protobuf';
import * as Crypto from '../Crypto';
import { toBase64 } from '../Bytes';
@ -47,10 +48,10 @@ describe('MessageReceiver', () => {
});
const body = Proto.Envelope.encode({
type: Proto.Envelope.Type.CIPHERTEXT,
sourceServiceId: someAci,
sourceDevice: deviceId,
timestamp: Long.fromNumber(Date.now()),
type: Proto.Envelope.Type.DOUBLE_RATCHET,
sourceServiceIdBinary: toAciObject(someAci).getRawUuidBytes(),
sourceDeviceId: deviceId,
clientTimestamp: Long.fromNumber(Date.now()),
content: Crypto.getRandomBytes(200),
}).finish();

View file

@ -106,7 +106,7 @@ describe('backups', function (this: Mocha.Suite) {
identifier: uuidToBytes(MY_STORY_ID),
isBlockList: true,
name: MY_STORY_ID,
recipientServiceIds: [pinned.device.aci],
recipientServiceIdsBinary: [pinned.device.aciBinary],
},
},
});
@ -119,7 +119,7 @@ describe('backups', function (this: Mocha.Suite) {
identifier: uuidToBytes(DISTRIBUTION1),
isBlockList: false,
name: 'friend',
recipientServiceIds: [friend.device.aci],
recipientServiceIdsBinary: [friend.device.aciBinary],
},
},
});
@ -262,14 +262,14 @@ describe('backups', function (this: Mocha.Suite) {
async (window, snapshot) => {
const leftPane = window.locator('#LeftPane');
const pinnedElem = leftPane.locator(
`[data-testid="${pinned.toContact().aci}"] >> "cat photo"`
`[data-testid="${pinned.device.aci}"] >> "cat photo"`
);
debug('Waiting for messages to pinned contact to come through');
await pinnedElem.click();
const contactElem = leftPane.locator(
`[data-testid="${friend.toContact().aci}"] >> "respond 4"`
`[data-testid="${friend.device.aci}"] >> "respond 4"`
);
debug('Waiting for messages to regular contact to come through');

View file

@ -62,9 +62,7 @@ Bootstrap.benchmark(async (bootstrap: Bootstrap): Promise<void> => {
const openConvo = async (contact: PrimaryDevice): Promise<void> => {
debug('opening conversation', contact.profileName);
const item = leftPane.locator(
`[data-testid="${contact.toContact().aci}"]`
);
const item = leftPane.locator(`[data-testid="${contact.device.aci}"]`);
await item.click();
};

View file

@ -126,39 +126,49 @@ Bootstrap.benchmark(async (bootstrap: Bootstrap): Promise<void> => {
}
debug('encrypted');
await Promise.all(messages.map(message => server.send(desktop, message)));
debug('sending first message');
{
const firstMessage = messages.shift();
if (firstMessage != null) {
await server.send(desktop, firstMessage);
}
}
const window = await app.getWindow();
debug('waiting for conversation');
{
const leftPane = window.locator('#LeftPane');
// Left pane should show either the message preview or
// "You were added to the group".
await leftPane
.locator(
`.module-conversation-list__item--contact-or-conversation[data-testid="${group.id}"]`
)
.waitFor();
}
debug('sending the rest of messages');
await Promise.all(messages.map(message => server.send(desktop, message)));
debug('opening conversation');
{
const leftPane = window.locator('#LeftPane');
const item = leftPane
await leftPane
.locator(
`.module-conversation-list__item--contact-or-conversation[data-testid="${group.id}"]`
`.module-conversation-list__item--contact-or-conversation[data-testid="${group.id}"]` +
` >> text=${LAST_MESSAGE}`
)
.first();
// Wait for unread indicator to give desktop time to process messages without
// the timeline open
await item
.locator(
'.module-conversation-list__item--contact-or-conversation__content'
)
.locator(
'.module-conversation-list__item--contact-or-conversation__unread-indicator'
)
.first()
.waitFor();
await item.click();
.click();
}
debug('scrolling to bottom of timeline');
await window
.locator('.module-timeline__messages__at-bottom-detector')
.scrollIntoViewIfNeeded();
.locator('.ScrollDownButton')
.or(window.locator(`.module-message >> text="${LAST_MESSAGE}"`))
.click({ timeout: MINUTE });
debug('finding message in timeline');
{

View file

@ -68,7 +68,7 @@ Bootstrap.benchmark(async (bootstrap: Bootstrap): Promise<void> => {
{
const leftPane = window.locator('#LeftPane');
const item = leftPane.locator(
`[data-testid="${first.toContact().aci}"] >> text=${LAST_MESSAGE}`
`[data-testid="${first.device.aci}"] >> text=${LAST_MESSAGE}`
);
await item.click();
}

View file

@ -49,9 +49,7 @@ Bootstrap.benchmark(async (bootstrap: Bootstrap): Promise<void> => {
const leftPane = window.locator('#LeftPane');
const item = leftPane.locator(
`[data-testid="${lastContact?.toContact().aci}"]`
);
const item = leftPane.locator(`[data-testid="${lastContact?.device.aci}"]`);
await item.waitFor();
const duration = Date.now() - start;

View file

@ -129,11 +129,11 @@ function maybeWrapInSyncMessage({
? {
syncMessage: {
sent: {
destinationServiceId: getDevice(to).aci,
destinationServiceIdBinary: getDevice(to).aciBinary,
message: dataMessage,
timestamp: dataMessage.timestamp,
unidentifiedStatus: (sentTo ?? [to]).map(contact => ({
destinationServiceId: getDevice(contact).aci,
destinationServiceIdBinary: getDevice(contact).aciBinary,
destination: getDevice(contact).number,
})),
},
@ -222,7 +222,7 @@ export function sendReaction({
timestamp: Long.fromNumber(reactionTimestamp),
reaction: {
emoji,
targetAuthorAci: getDevice(targetAuthor).aci,
targetAuthorAciBinary: getDevice(targetAuthor).aciRawUuid,
targetSentTimestamp: Long.fromNumber(targetMessageTimestamp),
},
},

View file

@ -119,13 +119,13 @@ describe('attachment backfill', function (this: Mocha.Suite) {
return entry.syncMessage.attachmentBackfillRequest != null;
});
assert.strictEqual(
request?.targetConversation?.threadServiceId,
unknownContact.device.aci
assert.deepEqual(
request?.targetConversation?.threadServiceIdBinary,
unknownContact.device.aciBinary
);
assert.strictEqual(
request?.targetMessage?.authorServiceId,
unknownContact.device.aci
assert.deepEqual(
request?.targetMessage?.authorServiceIdBinary,
unknownContact.device.aciBinary
);
assert.strictEqual(
request?.targetMessage?.sentTimestamp?.toNumber(),
@ -302,13 +302,13 @@ describe('attachment backfill', function (this: Mocha.Suite) {
return entry.syncMessage.attachmentBackfillRequest != null;
});
assert.strictEqual(
request?.targetConversation?.threadServiceId,
unknownContact.device.aci
assert.deepEqual(
request?.targetConversation?.threadServiceIdBinary,
unknownContact.device.aciBinary
);
assert.strictEqual(
request?.targetMessage?.authorServiceId,
unknownContact.device.aci
assert.deepEqual(
request?.targetMessage?.authorServiceIdBinary,
unknownContact.device.aciBinary
);
assert.strictEqual(
request?.targetMessage?.sentTimestamp?.toNumber(),
@ -380,13 +380,13 @@ describe('attachment backfill', function (this: Mocha.Suite) {
return entry.syncMessage.attachmentBackfillRequest != null;
});
assert.strictEqual(
request?.targetConversation?.threadServiceId,
unknownContact.device.aci
assert.deepEqual(
request?.targetConversation?.threadServiceIdBinary,
unknownContact.device.aciBinary
);
assert.strictEqual(
request?.targetMessage?.authorServiceId,
unknownContact.device.aci
assert.deepEqual(
request?.targetMessage?.authorServiceIdBinary,
unknownContact.device.aciBinary
);
assert.strictEqual(
request?.targetMessage?.sentTimestamp?.toNumber(),
@ -439,7 +439,7 @@ describe('attachment backfill', function (this: Mocha.Suite) {
desktop,
quote: {
id: Long.fromNumber(bootstrap.getTimestamp()),
authorAci: unknownContact.device.aci,
authorAciBinary: unknownContact.device.aciRawUuid,
text: 'quote text',
attachments: [
{

View file

@ -2,6 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
import { Proto } from '@signalapp/mock-server';
import { Aci } from '@signalapp/libsignal-client';
import { assert } from 'chai';
import createDebug from 'debug';
import Long from 'long';
@ -23,6 +24,7 @@ import { sleep } from '../../util/sleep';
export const debug = createDebug('mock:test:edit');
const ACI_1 = generateAci();
const ACI_1_BINARY = Aci.parseFromServiceIdString(ACI_1).getRawUuidBytes();
const UNPROCESSED_ATTACHMENT: Proto.IAttachmentPointer = {
cdnId: Long.fromNumber(123),
key: new Uint8Array([1, 2, 3]),
@ -57,7 +59,7 @@ function createMessageWithQuote(body: string): Proto.IDataMessage {
body,
quote: {
id: Long.fromNumber(1),
authorAci: ACI_1,
authorAciBinary: ACI_1_BINARY,
text: 'text',
attachments: [
{
@ -516,7 +518,6 @@ describe('editing', function (this: Mocha.Suite) {
const { contacts, desktop } = bootstrap;
const [friend] = contacts;
const contact = friend.toContact();
const page = await app.getWindow();
@ -567,7 +568,7 @@ describe('editing', function (this: Mocha.Suite) {
debug("getting friend's conversationId");
const conversationId = await page.evaluate(
serviceId => window.SignalCI?.getConversationId(serviceId),
contact.aci
friend.device.aci
);
debug(`got friend's conversationId: ${conversationId}`);
strictAssert(conversationId, 'conversationId exists');

View file

@ -70,7 +70,6 @@ describe('messaging/expireTimerVersion', function (this: Mocha.Suite) {
identifier: uuidToBytes(MY_STORY_ID),
isBlockList: true,
name: MY_STORY_ID,
recipientServiceIds: [],
},
},
});
@ -180,11 +179,11 @@ describe('messaging/expireTimerVersion', function (this: Mocha.Suite) {
const sendSync = async () => {
debug('Send a sync message');
const timestamp = bootstrap.getTimestamp();
const destinationServiceId = stranger.device.aci;
const destinationServiceIdBinary = stranger.device.aciBinary;
const content = {
syncMessage: {
sent: {
destinationServiceId,
destinationServiceIdBinary,
timestamp: Long.fromNumber(timestamp),
message: {
body: 'request',
@ -194,7 +193,7 @@ describe('messaging/expireTimerVersion', function (this: Mocha.Suite) {
},
unidentifiedStatus: [
{
destinationServiceId,
destinationServiceIdBinary,
},
],
},

View file

@ -102,14 +102,14 @@ describe('readSync', function (this: Mocha.Suite) {
Long.fromNumber(timestamp)
);
const senderAci = friend.device.aci;
const senderAciBinary = friend.device.aciRawUuid;
await phone.sendRaw(
desktop,
{
syncMessage: {
read: longTimestamps.map(timestamp => ({
senderAci,
senderAciBinary,
timestamp,
})),
},

View file

@ -55,7 +55,7 @@ describe('safety number', function (this: Mocha.Suite) {
identifier: uuidToBytes(MY_STORY_ID),
isBlockList: false,
name: MY_STORY_ID,
recipientServiceIds: [alice.device.aci],
recipientServiceIdsBinary: [alice.device.aciBinary],
},
},
});

View file

@ -68,7 +68,7 @@ describe('sendSync', function (this: Mocha.Suite) {
timestamp: Long.fromNumber(timestamp),
message: originalDataMessage,
unidentifiedStatus: members.map(member => ({
destinationServiceId: member.device.aci,
destinationServiceIdBinary: member.device.aciBinary,
destination: member.device.number,
})),
},

View file

@ -51,7 +51,6 @@ describe('story/messaging', function (this: Mocha.Suite) {
identifier: uuidToBytes(MY_STORY_ID),
isBlockList: false,
name: MY_STORY_ID,
recipientServiceIds: [],
},
},
});
@ -65,7 +64,7 @@ describe('story/messaging', function (this: Mocha.Suite) {
identifier: uuidToBytes(DISTRIBUTION1),
isBlockList: false,
name: 'first',
recipientServiceIds: [first.device.aci],
recipientServiceIdsBinary: [first.device.aciBinary],
},
},
});
@ -77,7 +76,7 @@ describe('story/messaging', function (this: Mocha.Suite) {
identifier: uuidToBytes(DISTRIBUTION2),
isBlockList: false,
name: 'second',
recipientServiceIds: [second.device.aci],
recipientServiceIdsBinary: [second.device.aciBinary],
},
},
});
@ -148,12 +147,12 @@ describe('story/messaging', function (this: Mocha.Suite) {
},
storyMessageRecipients: [
{
destinationServiceId: first.device.aci,
destinationServiceIdBinary: first.device.aciBinary,
distributionListIds: [DISTRIBUTION1],
isAllowedToReply: true,
},
{
destinationServiceId: second.device.aci,
destinationServiceIdBinary: second.device.aciBinary,
distributionListIds: [DISTRIBUTION2],
isAllowedToReply: true,
},
@ -171,7 +170,7 @@ describe('story/messaging', function (this: Mocha.Suite) {
dataMessage: {
body: 'first reply',
storyContext: {
authorAci: phone.device.aci,
authorAciBinary: phone.device.aciRawUuid,
sentTimestamp: Long.fromNumber(sentAt),
},
timestamp: Long.fromNumber(sentAt + 1),
@ -185,7 +184,7 @@ describe('story/messaging', function (this: Mocha.Suite) {
dataMessage: {
body: 'second reply',
storyContext: {
authorAci: phone.device.aci,
authorAciBinary: phone.device.aciRawUuid,
sentTimestamp: Long.fromNumber(sentAt),
},
timestamp: Long.fromNumber(sentAt + 2),
@ -245,7 +244,7 @@ describe('story/messaging', function (this: Mocha.Suite) {
dataMessage: {
body: 'first reply',
storyContext: {
authorAci: desktop.aci,
authorAciBinary: desktop.aciRawUuid,
sentTimestamp: Long.fromNumber(sentAt),
},
groupV2: {

View file

@ -98,7 +98,7 @@ describe('unknown contacts', function (this: Mocha.Suite) {
syncMessage: {
messageRequestResponse: {
type: Proto.SyncMessage.MessageRequestResponse.Type.ACCEPT,
threadAci: unknownContact.device.aci,
threadAciBinary: unknownContact.device.aciRawUuid,
},
},
});

View file

@ -64,7 +64,7 @@ describe('Libsignal-net', function (this: Mocha.Suite) {
{
const leftPane = window.locator('#LeftPane');
const item = leftPane
.getByTestId(contact.toContact().aci)
.getByTestId(contact.device.aci)
.getByText('incoming message');
await item.click();
}

View file

@ -81,7 +81,7 @@ describe('pnp/calling', function (this: Mocha.Suite) {
});
debug('Open conversation with a known contact');
await leftPane.locator(`[data-testid="${alice.toContact().aci}"]`).click();
await leftPane.locator(`[data-testid="${alice.device.aci}"]`).click();
debug('Accept conversation from a known contact');
await acceptConversation(window);

View file

@ -61,7 +61,7 @@ describe('pnp/change number', function (this: Mocha.Suite) {
]);
debug('opening conversation with the first contact');
await leftPane.locator(`[data-testid="${first.toContact().aci}"]`).click();
await leftPane.locator(`[data-testid="${first.device.aci}"]`).click();
debug('done');
});

View file

@ -1,6 +1,7 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { timingSafeEqual } from 'node:crypto';
import { assert } from 'chai';
import { ServiceIdKind, Proto, StorageState } from '@signalapp/mock-server';
import type { PrimaryDevice } from '@signalapp/mock-server';
@ -10,7 +11,6 @@ import Long from 'long';
import * as durations from '../../util/durations';
import { uuidToBytes } from '../../util/uuidToBytes';
import { generateConfigMatrix } from '../../util/generateConfigMatrix';
import { toUntaggedPni } from '../../types/ServiceId';
import { MY_STORY_ID } from '../../types/Stories';
import { Bootstrap } from '../bootstrap';
import type { App } from '../bootstrap';
@ -87,7 +87,6 @@ describe('pnp/merge', function (this: Mocha.Suite) {
identifier: uuidToBytes(MY_STORY_ID),
isBlockList: true,
name: MY_STORY_ID,
recipientServiceIds: [],
},
},
});
@ -282,7 +281,7 @@ describe('pnp/merge', function (this: Mocha.Suite) {
let state = await phone.expectStorageState('consistency check');
state = state.updateContact(pniContact, {
pni: undefined,
pniBinary: undefined,
e164: undefined,
unregisteredAtTimestamp: Long.fromNumber(bootstrap.getTimestamp()),
});
@ -403,25 +402,34 @@ describe('pnp/merge', function (this: Mocha.Suite) {
throw new Error('Invalid record');
}
const { aci, e164, pni } = contact;
if (aci === pniContact.device.aci) {
const { aciBinary, e164, pniBinary } = contact;
if (
aciBinary?.length &&
timingSafeEqual(aciBinary, pniContact.device.aciRawUuid)
) {
aciContacts += 1;
assert.strictEqual(pni, '');
assert.strictEqual(pniBinary?.length, 0);
assert.strictEqual(e164, '');
} else if (pni === toUntaggedPni(pniContact.device.pni)) {
} else if (
pniBinary?.length &&
timingSafeEqual(pniBinary, pniContact.device.pniRawUuid)
) {
pniContacts += 1;
assert.strictEqual(aci, '');
assert.strictEqual(aciBinary?.length, 0);
assert.strictEqual(e164, pniContact.device.number);
}
}
assert.strictEqual(aciContacts, 1);
assert.strictEqual(pniContacts, 1);
assert.strictEqual(
removed[0].contact?.pni,
toUntaggedPni(pniContact.device.pni)
assert.deepEqual(
removed[0].contact?.pniBinary,
pniContact.device.pniRawUuid
);
assert.deepEqual(
removed[0].contact?.aciBinary,
pniContact.device.aciRawUuid
);
assert.strictEqual(removed[0].contact?.aci, pniContact.device.aci);
// Pin PNI so that it appears in the left pane
const updated = newState.pin(pniContact, ServiceIdKind.PNI);
@ -556,12 +564,12 @@ describe('pnp/merge', function (this: Mocha.Suite) {
for (const key of ['aci' as const, 'pni' as const]) {
debug(`Send a ${key} sync message`);
const timestamp = bootstrap.getTimestamp();
const destinationServiceId = pniContact.device[key];
const destinationServiceIdBinary = pniContact.device[`${key}Binary`];
const destination = key === 'pni' ? pniContact.device.number : undefined;
const content = {
syncMessage: {
sent: {
destinationServiceId,
destinationServiceIdBinary,
destination,
timestamp: Long.fromNumber(timestamp),
message: {
@ -572,7 +580,7 @@ describe('pnp/merge', function (this: Mocha.Suite) {
},
unidentifiedStatus: [
{
destinationServiceId,
destinationServiceIdBinary,
destination,
},
],

View file

@ -9,7 +9,6 @@ import createDebug from 'debug';
import * as durations from '../../util/durations';
import { uuidToBytes } from '../../util/uuidToBytes';
import { MY_STORY_ID } from '../../types/Stories';
import { toUntaggedPni } from '../../types/ServiceId';
import { Bootstrap } from '../bootstrap';
import type { App } from '../bootstrap';
import {
@ -73,7 +72,6 @@ describe('pnp/phone discovery', function (this: Mocha.Suite) {
identifier: uuidToBytes(MY_STORY_ID),
isBlockList: true,
name: MY_STORY_ID,
recipientServiceIds: [],
},
},
});
@ -119,7 +117,7 @@ describe('pnp/phone discovery', function (this: Mocha.Suite) {
whitelisted: true,
identityKey: pniContact.publicKey.serialize(),
profileKey: pniContact.profileKey.serialize(),
pni: toUntaggedPni(pniContact.device.pni),
pniBinary: pniContact.device.pniRawUuid,
})
);
await phone.sendFetchStorage({

View file

@ -1,13 +1,15 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { timingSafeEqual } from 'node:crypto';
import { assert } from 'chai';
import { ServiceIdKind, StorageState, Proto } from '@signalapp/mock-server';
import type { PrimaryDevice } from '@signalapp/mock-server';
import createDebug from 'debug';
import * as durations from '../../util/durations';
import { generatePni, toUntaggedPni } from '../../types/ServiceId';
import { generatePni } from '../../types/ServiceId';
import { toPniObject } from '../../util/ServiceId';
import { Bootstrap } from '../bootstrap';
import type { App } from '../bootstrap';
import {
@ -54,7 +56,7 @@ describe('pnp/PNI Change', function (this: Mocha.Suite) {
whitelisted: true,
e164: contactA.device.number,
identityKey: contactA.getPublicKey(ServiceIdKind.PNI).serialize(),
pni: toUntaggedPni(contactA.device.pni),
pniBinary: contactA.device.pniRawUuid,
givenName: 'ContactA',
},
ServiceIdKind.PNI
@ -135,17 +137,21 @@ describe('pnp/PNI Change', function (this: Mocha.Suite) {
const state = await phone.expectStorageState('consistency check');
const updated = await phone.setStorageState(
state
.removeRecord(
item =>
item.record.contact?.pni === toUntaggedPni(contactA.device.pni)
)
.removeRecord(item => {
return item.record.contact?.pniBinary?.length
? timingSafeEqual(
item.record.contact.pniBinary,
contactA.device.pniRawUuid
)
: false;
})
.addContact(
contactA,
{
identityState: Proto.ContactRecord.IdentityState.DEFAULT,
whitelisted: true,
e164: contactA.device.number,
pni: toUntaggedPni(updatedPni),
pniBinary: toPniObject(updatedPni).getRawUuidBytes(),
identityKey: contactA.getPublicKey(ServiceIdKind.PNI).serialize(),
},
ServiceIdKind.PNI
@ -232,17 +238,21 @@ describe('pnp/PNI Change', function (this: Mocha.Suite) {
const state = await phone.expectStorageState('consistency check');
const updated = await phone.setStorageState(
state
.removeRecord(
item =>
item.record.contact?.pni === toUntaggedPni(contactA.device.pni)
)
.removeRecord(item => {
return item.record.contact?.pniBinary?.length
? timingSafeEqual(
item.record.contact.pniBinary,
contactA.device.pniRawUuid
)
: false;
})
.addContact(
contactB,
{
identityState: Proto.ContactRecord.IdentityState.DEFAULT,
whitelisted: true,
e164: contactA.device.number,
pni: toUntaggedPni(contactB.device.pni),
pniBinary: contactB.device.pniRawUuid,
// Key change - different identity key
identityKey: contactB.publicKey.serialize(),
@ -334,17 +344,21 @@ describe('pnp/PNI Change', function (this: Mocha.Suite) {
const state = await phone.expectStorageState('consistency check');
const updated = await phone.setStorageState(
state
.removeRecord(
item =>
item.record.contact?.pni === toUntaggedPni(contactA.device.pni)
)
.removeRecord(item => {
return item.record.contact?.pniBinary?.length
? timingSafeEqual(
item.record.contact.pniBinary,
contactA.device.pniRawUuid
)
: false;
})
.addContact(
contactB,
{
identityState: Proto.ContactRecord.IdentityState.DEFAULT,
whitelisted: true,
e164: contactA.device.number,
pni: toUntaggedPni(contactB.device.pni),
pniBinary: contactB.device.pniRawUuid,
// Note: No identityKey key provided here!
},
@ -465,17 +479,21 @@ describe('pnp/PNI Change', function (this: Mocha.Suite) {
const state = await phone.expectStorageState('consistency check');
const updated = await phone.setStorageState(
state
.removeRecord(
item =>
item.record.contact?.pni === toUntaggedPni(contactA.device.pni)
)
.removeRecord(item => {
return item.record.contact?.pniBinary?.length
? timingSafeEqual(
item.record.contact.pniBinary,
contactA.device.pniRawUuid
)
: false;
})
.addContact(
contactB,
{
identityState: Proto.ContactRecord.IdentityState.DEFAULT,
whitelisted: true,
e164: contactA.device.number,
pni: toUntaggedPni(contactB.device.pni),
pniBinary: contactB.device.pniRawUuid,
// Note: No identityKey key provided here!
},
@ -497,17 +515,21 @@ describe('pnp/PNI Change', function (this: Mocha.Suite) {
const state = await phone.expectStorageState('consistency check');
const updated = await phone.setStorageState(
state
.removeRecord(
item =>
item.record.contact?.pni === toUntaggedPni(contactB.device.pni)
)
.removeRecord(item => {
return item.record.contact?.pniBinary?.length
? timingSafeEqual(
item.record.contact.pniBinary,
contactB.device.pniRawUuid
)
: false;
})
.addContact(
contactB,
{
identityState: Proto.ContactRecord.IdentityState.DEFAULT,
whitelisted: true,
e164: contactA.device.number,
pni: toUntaggedPni(contactA.device.pni),
pniBinary: contactA.device.pniRawUuid,
},
ServiceIdKind.PNI
)

View file

@ -15,7 +15,6 @@ import createDebug from 'debug';
import * as durations from '../../util/durations';
import { uuidToBytes } from '../../util/uuidToBytes';
import { MY_STORY_ID } from '../../types/Stories';
import { isUntaggedPniString, toTaggedPni } from '../../types/ServiceId';
import { Bootstrap } from '../bootstrap';
import type { App } from '../bootstrap';
import {
@ -61,7 +60,6 @@ describe('pnp/PNI Signature', function (this: Mocha.Suite) {
identifier: uuidToBytes(MY_STORY_ID),
isBlockList: true,
name: MY_STORY_ID,
recipientServiceIds: [],
},
},
});
@ -132,9 +130,7 @@ describe('pnp/PNI Signature', function (this: Mocha.Suite) {
});
debug('Open conversation with the stranger');
await leftPane
.locator(`[data-testid="${stranger.toContact().aci}"]`)
.click();
await leftPane.locator(`[data-testid="${stranger.device.aci}"]`).click();
debug('Accept conversation from a stranger');
await acceptConversation(window);
@ -259,7 +255,7 @@ describe('pnp/PNI Signature', function (this: Mocha.Suite) {
debug('Send a PNI sync message');
const timestamp = bootstrap.getTimestamp();
const destinationServiceId = stranger.device.pni;
const destinationServiceIdBinary = stranger.device.pniBinary;
const destinationE164 = stranger.device.number;
const destinationPniIdentityKey = await stranger.device.getIdentityKey(
ServiceIdKind.PNI
@ -271,13 +267,13 @@ describe('pnp/PNI Signature', function (this: Mocha.Suite) {
const content = {
syncMessage: {
sent: {
destinationServiceId,
destinationServiceIdBinary,
destinationE164,
timestamp: Long.fromNumber(timestamp),
message: originalDataMessage,
unidentifiedStatus: [
{
destinationServiceId,
destinationServiceIdBinary,
destinationPniIdentityKey: destinationPniIdentityKey.serialize(),
},
],
@ -367,9 +363,7 @@ describe('pnp/PNI Signature', function (this: Mocha.Suite) {
});
debug('Wait for merge to happen');
await leftPane
.locator(`[data-testid="${stranger.toContact().aci}"]`)
.waitFor();
await leftPane.locator(`[data-testid="${stranger.device.aci}"]`).waitFor();
{
debug('Wait for composition input to clear');
@ -409,13 +403,8 @@ describe('pnp/PNI Signature', function (this: Mocha.Suite) {
);
assert(aciRecord, 'ACI Contact must be in storage service');
assert.strictEqual(aciRecord?.aci, stranger.device.aci);
assert.strictEqual(
aciRecord?.pni &&
isUntaggedPniString(aciRecord?.pni) &&
toTaggedPni(aciRecord?.pni),
stranger.device.pni
);
assert.deepEqual(aciRecord?.aciBinary, stranger.device.aciRawUuid);
assert.deepEqual(aciRecord?.pniBinary, stranger.device.pniRawUuid);
assert.strictEqual(aciRecord?.pniSignatureVerified, true);
// Two outgoing, one incoming

View file

@ -12,7 +12,7 @@ import {
import createDebug from 'debug';
import * as durations from '../../util/durations';
import { generatePni, toUntaggedPni } from '../../types/ServiceId';
import { generatePni } from '../../types/ServiceId';
import { Bootstrap } from '../bootstrap';
import type { App } from '../bootstrap';
@ -93,7 +93,7 @@ describe('pnp/PNI DecryptionError unlink', function (this: Mocha.Suite) {
},
{
timestamp: bootstrap.getTimestamp(),
updatedPni: toUntaggedPni(generatePni()),
updatedPni: generatePni(),
}
)
);
@ -107,7 +107,7 @@ describe('pnp/PNI DecryptionError unlink', function (this: Mocha.Suite) {
},
{
timestamp: bootstrap.getTimestamp(),
updatedPni: toUntaggedPni(desktop.pni),
updatedPni: desktop.pni,
}
)
);

View file

@ -73,7 +73,6 @@ describe('pnp/send gv2 invite', function (this: Mocha.Suite) {
identifier: uuidToBytes(MY_STORY_ID),
isBlockList: true,
name: MY_STORY_ID,
recipientServiceIds: [],
},
},
});

View file

@ -67,7 +67,6 @@ describe('pnp/username', function (this: Mocha.Suite) {
identifier: uuidToBytes(MY_STORY_ID),
isBlockList: true,
name: MY_STORY_ID,
recipientServiceIds: [],
},
},
});
@ -146,10 +145,16 @@ describe('pnp/username', function (this: Mocha.Suite) {
'only one record must be removed'
);
assert.strictEqual(added[0].contact?.aci, usernameContact.device.aci);
assert.deepEqual(
added[0].contact?.aciBinary,
usernameContact.device.aciRawUuid
);
assert.strictEqual(added[0].contact?.username, '');
assert.strictEqual(removed[0].contact?.aci, usernameContact.device.aci);
assert.deepEqual(
removed[0].contact?.aciBinary,
usernameContact.device.aciRawUuid
);
assert.strictEqual(removed[0].contact?.username, USERNAME);
}

View file

@ -46,7 +46,6 @@ describe('story/no-sender-key', function (this: Mocha.Suite) {
identifier: uuidToBytes(MY_STORY_ID),
isBlockList: true,
name: MY_STORY_ID,
recipientServiceIds: [],
},
},
});

View file

@ -9,7 +9,6 @@ import * as durations from '../../util/durations';
import { Bootstrap } from '../bootstrap';
import type { App } from '../bootstrap';
import { ReceiptType } from '../../types/Receipt';
import { toUntaggedPni } from '../../types/ServiceId';
import {
acceptConversation,
typeIntoInput,
@ -57,7 +56,7 @@ describe('challenge/receipts', function (this: Mocha.Suite) {
whitelisted: true,
e164: contact.device.number,
identityKey: contact.getPublicKey(ServiceIdKind.PNI).serialize(),
pni: toUntaggedPni(contact.device.pni),
pniBinary: contact.device.pniRawUuid,
givenName: 'Jamie',
},
ServiceIdKind.PNI
@ -68,7 +67,7 @@ describe('challenge/receipts', function (this: Mocha.Suite) {
whitelisted: true,
e164: contactB.device.number,
identityKey: contactB.getPublicKey(ServiceIdKind.PNI).serialize(),
pni: toUntaggedPni(contactB.device.pni),
pniBinary: contactB.device.pniRawUuid,
givenName: 'Kim',
},
ServiceIdKind.PNI
@ -111,10 +110,8 @@ describe('challenge/receipts', function (this: Mocha.Suite) {
const window = await app.getWindow();
const leftPane = window.locator('#LeftPane');
debug(`Opening conversation with contact (${contact.toContact().aci})`);
await leftPane
.locator(`[data-testid="${contact.toContact().aci}"]`)
.click();
debug(`Opening conversation with contact (${contact.device.aci})`);
await leftPane.locator(`[data-testid="${contact.device.aci}"]`).click();
debug('Accept conversation from contact - does not trigger captcha!');
await acceptConversation(window);
@ -172,10 +169,8 @@ describe('challenge/receipts', function (this: Mocha.Suite) {
timestamp: timestampA,
});
debug(`Opening conversation with ContactA (${contact.toContact().aci})`);
await leftPane
.locator(`[data-testid="${contact.toContact().aci}"]`)
.click();
debug(`Opening conversation with ContactA (${contact.device.aci})`);
await leftPane.locator(`[data-testid="${contact.device.aci}"]`).click();
debug('Accept conversation from ContactA - does not trigger captcha!');
await acceptConversation(window);
@ -186,10 +181,8 @@ describe('challenge/receipts', function (this: Mocha.Suite) {
timestamp: timestampB,
});
debug(`Opening conversation with ContactB (${contact.toContact().aci})`);
await leftPane
.locator(`[data-testid="${contactB.toContact().aci}"]`)
.click();
debug(`Opening conversation with ContactB (${contact.device.aci})`);
await leftPane.locator(`[data-testid="${contactB.device.aci}"]`).click();
debug('Accept conversation from ContactB - does not trigger captcha!');
await acceptConversation(window);
@ -273,10 +266,8 @@ describe('challenge/receipts', function (this: Mocha.Suite) {
const window = await app.getWindow();
const leftPane = window.locator('#LeftPane');
debug(`Opening conversation with contact (${contact.toContact().aci})`);
await leftPane
.locator(`[data-testid="${contact.toContact().aci}"]`)
.click();
debug(`Opening conversation with contact (${contact.device.aci})`);
await leftPane.locator(`[data-testid="${contact.device.aci}"]`).click();
debug('Accept conversation from contact - does not trigger captcha!');
await acceptConversation(window);
@ -342,10 +333,8 @@ describe('challenge/receipts', function (this: Mocha.Suite) {
timestamp,
});
debug(`Opening conversation with Contact B (${contactB.toContact().aci})`);
await leftPane
.locator(`[data-testid="${contactB.toContact().aci}"]`)
.click();
debug(`Opening conversation with Contact B (${contactB.device.aci})`);
await leftPane.locator(`[data-testid="${contactB.device.aci}"]`).click();
debug('Accept conversation from Contact B - does not trigger captcha!');
await acceptConversation(window);

View file

@ -57,7 +57,7 @@ describe('routing', function (this: Mocha.Suite) {
await page.locator('#LeftPane').waitFor();
const token = await page.evaluate(
serviceId => window.SignalCI?.createNotificationToken(serviceId),
friend.toContact().aci
friend.device.aci
);
strictAssert(typeof token === 'string', 'token must be returned');
const conversationUrl = showConversationRoute.toAppUrl({

View file

@ -49,7 +49,7 @@ describe('storage service', function (this: Mocha.Suite) {
});
await leftPane
.locator(`[data-testid="${firstContact.toContact().aci}"]`)
.locator(`[data-testid="${firstContact.device.aci}"]`)
.waitFor({ state: 'hidden' });
await leftPane
@ -74,7 +74,7 @@ describe('storage service', function (this: Mocha.Suite) {
});
await leftPane
.locator(`[data-testid="${firstContact.toContact().aci}"]`)
.locator(`[data-testid="${firstContact.device.aci}"]`)
.waitFor();
await leftPane
@ -89,7 +89,7 @@ describe('storage service', function (this: Mocha.Suite) {
const state = await phone.expectStorageState('consistency check');
await leftPane
.locator(`[data-testid="${firstContact.toContact().aci}"]`)
.locator(`[data-testid="${firstContact.device.aci}"]`)
.click();
const moreButton = conversationStack.locator(

View file

@ -41,7 +41,6 @@ describe('storage service', function (this: Mocha.Suite) {
identifier: uuidToBytes(MY_STORY_ID),
isBlockList: true,
name: MY_STORY_ID,
recipientServiceIds: [],
},
},
});

View file

@ -101,7 +101,6 @@ export async function initStorage(
identifier: uuidToBytes(MY_STORY_ID),
isBlockList: true,
name: MY_STORY_ID,
recipientServiceIds: [],
},
},
});

View file

@ -6,6 +6,7 @@ import { Proto } from '@signalapp/mock-server';
import * as durations from '../../util/durations';
import { generateAci } from '../../types/ServiceId';
import { toAciObject } from '../../util/ServiceId';
import { MAX_READ_KEYS } from '../../services/storageConstants';
import type { App, Bootstrap } from './fixtures';
import { initStorage, debug } from './fixtures';
@ -45,7 +46,7 @@ describe('storage service', function (this: Mocha.Suite) {
debug('wait for first contact to be pinned in the left pane');
await leftPane
.locator(`[data-testid="${firstContact.toContact().aci}"]`)
.locator(`[data-testid="${firstContact.device.aci}"]`)
.waitFor();
{
@ -57,7 +58,7 @@ describe('storage service', function (this: Mocha.Suite) {
type: IdentifierType.CONTACT,
record: {
contact: {
aci: generateAci(),
aciBinary: toAciObject(generateAci()).getRawUuidBytes(),
},
},
});
@ -76,7 +77,7 @@ describe('storage service', function (this: Mocha.Suite) {
debug('wait for last contact to be pinned in the left pane');
await leftPane
.locator(`[data-testid="${lastContact.toContact().aci}"]`)
.locator(`[data-testid="${lastContact.device.aci}"]`)
.waitFor({ timeout: durations.MINUTE });
debug('Verifying the final manifest version');

View file

@ -56,10 +56,8 @@ describe('storage service', function (this: Mocha.Suite) {
const leftPane = window.locator('#LeftPane');
debug('Opening conversation with a stranger');
debug(stranger.toContact().aci);
await leftPane
.locator(`[data-testid="${stranger.toContact().aci}"]`)
.click();
debug(stranger.device.aci);
await leftPane.locator(`[data-testid="${stranger.device.aci}"]`).click();
debug("Verify that we stored stranger's profile key");
const postMessageState = await phone.waitForStorageState({

View file

@ -108,7 +108,7 @@ describe('storage service', function (this: Mocha.Suite) {
debug('pinning contact=%d', i);
const convo = leftPane.locator(
`[data-testid="${contact.toContact().aci}"]`
`[data-testid="${contact.device.aci}"]`
);
await convo.click();

View file

@ -61,7 +61,7 @@ describe('storage service', function (this: Mocha.Suite) {
);
await leftPane
.locator(`[data-testid="${firstContact.toContact().aci}"]`)
.locator(`[data-testid="${firstContact.device.aci}"]`)
.click();
{

View file

@ -4,7 +4,7 @@
import { assert } from 'chai';
import { type WritableDB } from '../../sql/Interface';
import { SignalService as Proto } from '../../protobuf';
import { Migrations as Proto } from '../../protobuf';
import { generateAci } from '../../types/ServiceId';
import { createDB, updateToVersion, insertData, getTableData } from './helpers';

View file

@ -3,16 +3,16 @@
import { Transform } from 'stream';
import { createLogger } from '../logging/log';
import { SignalService as Proto } from '../protobuf';
import protobuf from '../protobuf/wrap';
import { normalizeAci } from '../util/normalizeAci';
import { isAciString } from '../util/isAciString';
import { DurationInSeconds } from '../util/durations';
import { createLogger } from '../logging/log';
import type { ContactAvatarType } from '../types/Avatar';
import type { AttachmentType } from '../types/Attachment';
import type { AciString } from '../types/ServiceId';
import { computeHash } from '../Crypto';
import { dropNull } from '../util/dropNull';
import { fromAciUuidBytesOrString } from '../util/ServiceId';
import { decryptAttachmentV2ToSink } from '../AttachmentCrypto';
import Avatar = Proto.ContactDetails.IAvatar;
@ -30,8 +30,9 @@ type OptionalFields = {
type MessageWithAvatar<Message extends OptionalFields> = Omit<
Message,
'avatar' | 'toJSON'
'avatar' | 'toJSON' | 'aci' | 'aciBinary'
> & {
aci: AciString;
avatar?: ContactAvatarType;
expireTimer?: DurationInSeconds;
expireTimerVersion: number | null;
@ -193,7 +194,7 @@ export class ParseContactsTransform extends Transform {
}
function prepareContact(
proto: Proto.ContactDetails,
{ aci: rawAci, aciBinary, ...proto }: Proto.ContactDetails,
avatar?: ContactAvatarType
): ContactDetailsWithAvatar | undefined {
const expireTimer =
@ -201,15 +202,13 @@ function prepareContact(
? DurationInSeconds.fromSeconds(proto.expireTimer)
: undefined;
// We reject incoming contacts with invalid aci information
if (proto.aci && !isAciString(proto.aci)) {
log.warn('ParseContactsTransform: Dropping contact with invalid aci');
const aci = fromAciUuidBytesOrString(aciBinary, rawAci, 'ContactBuffer.aci');
if (aci == null) {
log.warn('ParseContactsTransform: Dropping contact with invalid aci');
return undefined;
}
const aci = proto.aci ? normalizeAci(proto.aci, 'ContactBuffer.aci') : null;
const result = {
...proto,
expireTimer,

View file

@ -54,7 +54,7 @@ import { DurationInSeconds } from '../util/durations';
import { Address } from '../types/Address';
import { QualifiedAddress } from '../types/QualifiedAddress';
import { normalizeStoryDistributionId } from '../types/StoryDistributionId';
import type { ServiceIdString } from '../types/ServiceId';
import type { ServiceIdString, AciString } from '../types/ServiceId';
import {
fromPniObject,
isPniString,
@ -159,6 +159,12 @@ import { checkOurPniIdentityKey } from '../util/checkOurPniIdentityKey';
import { CallLinkUpdateSyncType } from '../types/CallLink';
import { bytesToUuid } from '../util/uuidToBytes';
import { isBodyTooLong } from '../util/longAttachment';
import {
fromServiceIdBinaryOrString,
fromAciUuidBytes,
fromAciUuidBytesOrString,
fromPniUuidBytesOrUntaggedString,
} from '../util/ServiceId';
const log = createLogger('MessageReceiver');
@ -415,27 +421,24 @@ export default class MessageReceiver
// Proto.Envelope fields
type: decoded.type ?? Proto.Envelope.Type.UNKNOWN,
source: undefined,
sourceServiceId: decoded.sourceServiceId
? normalizeServiceId(
decoded.sourceServiceId,
'MessageReceiver.handleRequest.sourceServiceId'
)
: undefined,
sourceDevice: decoded.sourceDevice ?? 1,
destinationServiceId: decoded.destinationServiceId
? normalizeServiceId(
decoded.destinationServiceId,
'MessageReceiver.handleRequest.destinationServiceId'
)
: ourAci,
updatedPni:
decoded.updatedPni && isUntaggedPniString(decoded.updatedPni)
? normalizePni(
toTaggedPni(decoded.updatedPni),
'MessageReceiver.handleRequest.updatedPni'
)
: undefined,
timestamp: decoded.timestamp?.toNumber() ?? 0,
sourceServiceId: fromServiceIdBinaryOrString(
decoded.sourceServiceIdBinary,
decoded.sourceServiceId,
'MessageReceiver.handleRequest.sourceServiceId'
),
sourceDevice: decoded.sourceDeviceId ?? 1,
destinationServiceId:
fromServiceIdBinaryOrString(
decoded.destinationServiceIdBinary,
decoded.destinationServiceId,
'MessageReceiver.handleRequest.destinationServiceId'
) || ourAci,
updatedPni: fromPniUuidBytesOrUntaggedString(
decoded.updatedPniBinary,
decoded.updatedPni,
'MessageReceiver.handleRequest.updatedPni'
),
timestamp: decoded.clientTimestamp?.toNumber() ?? 0,
content,
serverGuid: decoded.serverGuid ?? getGuid(),
serverTimestamp,
@ -1828,7 +1831,7 @@ export default class MessageReceiver
if (
serviceIdKind === ServiceIdKind.PNI &&
envelope.type !== envelopeTypeEnum.PREKEY_BUNDLE
envelope.type !== envelopeTypeEnum.PREKEY_MESSAGE
) {
log.warn(`innerDecrypt(${logId}): non-PreKey envelope on PNI`);
return undefined;
@ -1850,7 +1853,7 @@ export default class MessageReceiver
wasEncrypted: false,
};
}
if (envelope.type === envelopeTypeEnum.CIPHERTEXT) {
if (envelope.type === envelopeTypeEnum.DOUBLE_RATCHET) {
log.info(`decrypt/${logId}: ciphertext message`);
if (!identifier) {
throw new Error(
@ -1879,7 +1882,7 @@ export default class MessageReceiver
);
return { plaintext, wasEncrypted: true };
}
if (envelope.type === envelopeTypeEnum.PREKEY_BUNDLE) {
if (envelope.type === envelopeTypeEnum.PREKEY_MESSAGE) {
log.info(`decrypt/${logId}: prekey message`);
if (!identifier) {
throw new Error(
@ -3180,9 +3183,11 @@ export default class MessageReceiver
const ev = new ViewOnceOpenSyncEvent(
{
sourceAci: sync.senderAci
? normalizeAci(sync.senderAci, 'handleViewOnceOpen.senderUuid')
: undefined,
sourceAci: fromAciUuidBytesOrString(
sync.senderAciBinary,
sync.senderAci,
'handleViewOnceOpen.senderUuid'
),
timestamp: sync.timestamp?.toNumber(),
},
this.#removeFromCache.bind(this, envelope)
@ -3216,12 +3221,11 @@ export default class MessageReceiver
const ev = new MessageRequestResponseEvent(
{
envelopeId: envelope.id,
threadAci: sync.threadAci
? normalizeAci(
sync.threadAci,
'handleMessageRequestResponse.threadUuid'
)
: undefined,
threadAci: fromAciUuidBytesOrString(
sync.threadAciBinary,
sync.threadAci,
'handleMessageRequestResponse.threadUuid'
),
messageRequestResponseType: sync.type,
groupV2Id: groupV2IdString,
},
@ -3357,16 +3361,20 @@ export default class MessageReceiver
logUnexpectedUrgentValue(envelope, 'readSync');
const reads = read.map(
({ timestamp, senderAci }): ReadSyncEventData => ({
const reads = read.map((data): ReadSyncEventData => {
const { timestamp, senderAci: rawSenderAci, senderAciBinary } = data;
return {
envelopeId: envelope.id,
envelopeTimestamp: envelope.timestamp,
timestamp: timestamp?.toNumber(),
senderAci: senderAci
? normalizeAci(senderAci, 'handleRead.senderAci')
: undefined,
})
);
senderAci: fromAciUuidBytesOrString(
senderAciBinary,
rawSenderAci,
'handleRead.senderAci'
),
};
});
await this.#dispatchAndWait(
logId,
@ -3388,14 +3396,18 @@ export default class MessageReceiver
logUnexpectedUrgentValue(envelope, 'viewSync');
const views = viewed.map(
({ timestamp, senderAci }): ViewSyncEventData => ({
const views = viewed.map((data): ViewSyncEventData => {
const { timestamp, senderAci: rawSenderAci, senderAciBinary } = data;
return {
timestamp: timestamp?.toNumber(),
senderAci: senderAci
? normalizeAci(senderAci, 'handleViewed.senderAci')
: undefined,
})
);
senderAci: fromAciUuidBytesOrString(
senderAciBinary,
rawSenderAci,
'handleViewed.senderAci'
),
};
});
await this.#dispatchAndWait(
logId,
@ -3879,21 +3891,40 @@ export default class MessageReceiver
log.info(`${logId}: New e164 unblocks:`, removed);
await this.#storage.put('blocked', blocked.numbers);
}
if (blocked.acis) {
if (blocked.acisBinary?.length || blocked.acis?.length) {
const previous = this.#storage.get('blocked-uuids', []);
const acis = blocked.acis
.map((aci, index) => {
try {
return normalizeAci(aci, `handleBlocked.acis.${index}`);
} catch (error) {
log.warn(
`${logId}: ACI ${aci} was malformed`,
Errors.toLogFormat(error)
);
return undefined;
}
})
.filter(isNotNil);
let acis: Array<AciString>;
if (blocked.acisBinary?.length) {
acis = blocked.acisBinary
.map((aciBinary, index) => {
try {
return fromAciUuidBytes(aciBinary);
} catch (error) {
log.warn(
`${logId}: ACI ${index} was malformed`,
Errors.toLogFormat(error)
);
return undefined;
}
})
.filter(isNotNil);
} else if (blocked.acis?.length) {
acis = blocked.acis
.map((aci, index) => {
try {
return normalizeAci(aci, `handleBlocked.acis.${index}`);
} catch (error) {
log.warn(
`${logId}: ACI ${aci} was malformed`,
Errors.toLogFormat(error)
);
return undefined;
}
})
.filter(isNotNil);
} else {
throw new Error('No blocked acis');
}
const { added, removed } = diffArraysAsSets(previous, acis);
if (added.length) {
@ -4004,13 +4035,13 @@ export default class MessageReceiver
function envelopeTypeToCiphertextType(type: number | undefined): number {
const { Type } = Proto.Envelope;
if (type === Type.CIPHERTEXT) {
if (type === Type.DOUBLE_RATCHET) {
return CiphertextMessageType.Whisper;
}
if (type === Type.PLAINTEXT_CONTENT) {
return CiphertextMessageType.Plaintext;
}
if (type === Type.PREKEY_BUNDLE) {
if (type === Type.PREKEY_MESSAGE) {
return CiphertextMessageType.PreKey;
}
if (type === Type.SERVER_DELIVERY_RECEIPT) {
@ -4042,25 +4073,26 @@ function processAddressableMessage(
return undefined;
}
const { authorServiceId } = target;
const { authorServiceId: rawAuthorServiceId, authorServiceIdBinary } = target;
const authorServiceId = fromServiceIdBinaryOrString(
authorServiceIdBinary,
rawAuthorServiceId,
logId
);
if (authorServiceId) {
if (isAciString(authorServiceId)) {
return {
type: 'aci' as const,
authorAci: normalizeAci(
authorServiceId,
`${logId}/processAddressableMessage/aci`
),
authorAci: authorServiceId,
sentAt,
};
}
if (isPniString(authorServiceId)) {
return {
type: 'pni' as const,
authorPni: normalizePni(
authorServiceId,
`${logId}/processAddressableMessage/pni`
),
authorPni: authorServiceId,
sentAt,
};
}
@ -4087,19 +4119,30 @@ function processConversationIdentifier(
target: Proto.IConversationIdentifier,
logId: string
): ConversationIdentifier | undefined {
const { threadServiceId, threadGroupId, threadE164 } = target;
const {
threadServiceId: rawThreadServiceId,
threadServiceIdBinary,
threadGroupId,
threadE164,
} = target;
const threadServiceId = fromServiceIdBinaryOrString(
threadServiceIdBinary,
rawThreadServiceId,
logId
);
if (threadServiceId) {
if (isAciString(threadServiceId)) {
return {
type: 'aci' as const,
aci: normalizeAci(threadServiceId, `${logId}/aci`),
aci: threadServiceId,
};
}
if (isPniString(threadServiceId)) {
return {
type: 'pni' as const,
pni: normalizePni(threadServiceId, `${logId}/pni`),
pni: threadServiceId,
};
}
log.error(

View file

@ -72,10 +72,10 @@ type OutgoingMessageOptionsType = SendOptionsType & {
function ciphertextMessageTypeToEnvelopeType(type: number) {
if (type === CiphertextMessageType.PreKey) {
return Proto.Envelope.Type.PREKEY_BUNDLE;
return Proto.Envelope.Type.PREKEY_MESSAGE;
}
if (type === CiphertextMessageType.Whisper) {
return Proto.Envelope.Type.CIPHERTEXT;
return Proto.Envelope.Type.DOUBLE_RATCHET;
}
if (type === CiphertextMessageType.Plaintext) {
return Proto.Envelope.Type.PLAINTEXT_CONTENT;

View file

@ -6,18 +6,12 @@ import pTimeout, { TimeoutError as PTimeoutError } from 'p-timeout';
import { createLogger } from '../logging/log';
import * as Errors from '../types/errors';
import { MAX_DEVICE_NAME_LENGTH } from '../types/InstallScreen';
import {
isUntaggedPniString,
normalizePni,
toTaggedPni,
} from '../types/ServiceId';
import { strictAssert } from '../util/assert';
import { BackOff, FIBONACCI_TIMEOUTS } from '../util/BackOff';
import { SECOND } from '../util/durations';
import { explodePromise } from '../util/explodePromise';
import { drop } from '../util/drop';
import { isLinkAndSyncEnabled } from '../util/isLinkAndSyncEnabled';
import { normalizeAci } from '../util/normalizeAci';
import { normalizeDeviceName } from '../util/normalizeDeviceName';
import { linkDeviceRoute } from '../util/signalRoutes';
import { sleep } from '../util/sleep';
@ -156,10 +150,10 @@ export class Provisioner {
provisioningCode,
aciKeyPair,
pniKeyPair,
aci,
aci: ourAci,
profileKey,
masterKey,
untaggedPni,
pni: ourPni,
userAgent,
readReceipts,
ephemeralBackupKey,
@ -171,7 +165,6 @@ export class Provisioner {
strictAssert(provisioningCode, 'prepareLinkData: missing provisioningCode');
strictAssert(aciKeyPair, 'prepareLinkData: missing aciKeyPair');
strictAssert(pniKeyPair, 'prepareLinkData: missing pniKeyPair');
strictAssert(aci, 'prepareLinkData: missing aci');
strictAssert(
Bytes.isNotEmpty(profileKey),
'prepareLinkData: missing profileKey'
@ -180,16 +173,6 @@ export class Provisioner {
Bytes.isNotEmpty(masterKey) || accountEntropyPool,
'prepareLinkData: missing masterKey or accountEntropyPool'
);
strictAssert(
isUntaggedPniString(untaggedPni),
'prepareLinkData: invalid untaggedPni'
);
const ourAci = normalizeAci(aci, 'provisionMessage.aci');
const ourPni = normalizePni(
toTaggedPni(untaggedPni),
'provisionMessage.pni'
);
return {
type: AccountType.Linked,
@ -363,11 +346,14 @@ export class Provisioner {
'Provisioner.connect: duplicate uuid'
);
const proto = Proto.ProvisioningUuid.decode(body);
strictAssert(proto.uuid, 'Provisioner.connect: expected a UUID');
const proto = Proto.ProvisioningAddress.decode(body);
strictAssert(
proto.address,
'Provisioner.connect: expected a UUID'
);
state = SocketState.WaitingForEnvelope;
uuidPromise.resolve(proto.uuid);
uuidPromise.resolve(proto.address);
request.respond(200, 'OK');
} else if (requestType === ServerRequestType.ProvisioningMessage) {
strictAssert(

View file

@ -3,7 +3,7 @@
/* eslint-disable max-classes-per-file */
import * as client from '@signalapp/libsignal-client';
import { PublicKey, Aci, Pni } from '@signalapp/libsignal-client';
import type { KeyPairType } from './Types.d';
import * as Bytes from '../Bytes';
import {
@ -15,13 +15,23 @@ import { calculateAgreement, createKeyPair, generateKeyPair } from '../Curve';
import { SignalService as Proto } from '../protobuf';
import { strictAssert } from '../util/assert';
import { dropNull } from '../util/dropNull';
import { normalizeAci } from '../util/normalizeAci';
import {
type AciString,
type PniString,
normalizePni,
toTaggedPni,
isUntaggedPniString,
fromAciObject,
fromPniObject,
} from '../types/ServiceId';
export type ProvisionDecryptResult = Readonly<{
aciKeyPair: KeyPairType;
pniKeyPair?: KeyPairType;
number?: string;
aci?: string;
untaggedPni?: string;
aci: AciString;
pni: PniString;
provisioningCode?: string;
userAgent?: string;
readReceipts?: boolean;
@ -57,7 +67,7 @@ class ProvisioningCipherInner {
}
const ecRes = calculateAgreement(
client.PublicKey.deserialize(Buffer.from(masterEphemeral)),
PublicKey.deserialize(Buffer.from(masterEphemeral)),
this.keyPair.privateKey
);
const keys = deriveSecrets(
@ -78,16 +88,36 @@ class ProvisioningCipherInner {
? createKeyPair(pniPrivKey)
: undefined;
const { aci, pni } = provisionMessage;
strictAssert(aci, 'Missing aci in provisioning message');
strictAssert(pni, 'Missing pni in provisioning message');
const {
aci: rawAci,
pni: rawUntaggedPni,
aciBinary,
pniBinary,
} = provisionMessage;
let aci: AciString;
let pni: PniString;
if (Bytes.isNotEmpty(aciBinary) && Bytes.isNotEmpty(pniBinary)) {
aci = fromAciObject(Aci.fromUuidBytes(aciBinary));
pni = fromPniObject(Pni.fromUuidBytes(pniBinary));
} else if (rawAci && rawUntaggedPni) {
strictAssert(
isUntaggedPniString(rawUntaggedPni),
'ProvisioningCipher: invalid untaggedPni'
);
aci = normalizeAci(rawAci, 'provisionMessage.aci');
pni = normalizePni(toTaggedPni(rawUntaggedPni), 'provisionMessage.pni');
} else {
throw new Error('Missing aci/pni in provisioning message');
}
return {
aciKeyPair,
pniKeyPair,
number: dropNull(provisionMessage.number),
aci,
untaggedPni: pni,
pni,
provisioningCode: dropNull(provisionMessage.provisioningCode),
userAgent: dropNull(provisionMessage.userAgent),
readReceipts: provisionMessage.readReceipts ?? false,
@ -107,7 +137,7 @@ class ProvisioningCipherInner {
};
}
getPublicKey(): client.PublicKey {
getPublicKey(): PublicKey {
if (!this.keyPair) {
this.keyPair = generateKeyPair();
}
@ -132,5 +162,5 @@ export default class ProvisioningCipher {
provisionEnvelope: Proto.ProvisionEnvelope
) => ProvisionDecryptResult;
getPublicKey: () => client.PublicKey;
getPublicKey: () => PublicKey;
}

View file

@ -10,7 +10,6 @@ import PQueue from 'p-queue';
import pMap from 'p-map';
import type { PlaintextContent } from '@signalapp/libsignal-client';
import {
Pni,
ProtocolAddress,
SenderKeyDistributionMessage,
} from '@signalapp/libsignal-client';
@ -33,6 +32,7 @@ import {
serviceIdSchema,
isPniString,
} from '../types/ServiceId';
import { toAciObject, toPniObject, toServiceIdObject } from '../util/ServiceId';
import type {
ChallengeType,
GetGroupLogOptionsType,
@ -100,6 +100,7 @@ import {
getProtoForCallHistory,
} from '../util/callDisposition';
import { MAX_MESSAGE_COUNT } from '../util/deleteForMe.types';
import { isProtoBinaryEncodingEnabled } from '../util/isProtoBinaryEncodingEnabled';
import type { GroupSendToken } from '../types/GroupSendEndorsements';
const log = createLogger('SendMessage');
@ -415,6 +416,11 @@ class Message {
proto.reaction.emoji = this.reaction.emoji || null;
proto.reaction.remove = this.reaction.remove || false;
proto.reaction.targetAuthorAci = this.reaction.targetAuthorAci || null;
if (isProtoBinaryEncodingEnabled()) {
proto.reaction.targetAuthorAciBinary = this.reaction.targetAuthorAci
? toAciObject(this.reaction.targetAuthorAci).getRawUuidBytes()
: null;
}
proto.reaction.targetSentTimestamp =
this.reaction.targetTimestamp === undefined
? null
@ -520,6 +526,11 @@ class Message {
quote.id =
this.quote.id === undefined ? null : Long.fromNumber(this.quote.id);
quote.authorAci = this.quote.authorAci || null;
if (isProtoBinaryEncodingEnabled()) {
quote.authorAciBinary = this.quote.authorAci
? toAciObject(this.quote.authorAci).getRawUuidBytes()
: null;
}
quote.text = this.quote.text || null;
quote.attachments = this.quote.attachments.slice() || [];
const bodyRanges = this.quote.bodyRanges || [];
@ -529,6 +540,11 @@ class Message {
bodyRange.length = range.length;
if (BodyRange.isMention(range)) {
bodyRange.mentionAci = range.mentionAci;
if (isProtoBinaryEncodingEnabled()) {
bodyRange.mentionAciBinary = toAciObject(
range.mentionAci
).getRawUuidBytes();
}
} else if (BodyRange.isFormatting(range)) {
bodyRange.style = range.style;
} else {
@ -599,6 +615,11 @@ class Message {
const storyContext = new StoryContext();
if (this.storyContext.authorAci) {
storyContext.authorAci = this.storyContext.authorAci;
if (isProtoBinaryEncodingEnabled()) {
storyContext.authorAciBinary = toAciObject(
this.storyContext.authorAci
).getRawUuidBytes();
}
}
storyContext.sentTimestamp = Long.fromNumber(this.storyContext.timestamp);
@ -637,9 +658,7 @@ function addPniSignatureMessageToProto({
// eslint-disable-next-line no-param-reassign
proto.pniSignatureMessage = {
pni: Pni.parseFromServiceIdString(
pniSignatureMessage.pni
).getRawUuidBytes(),
pni: toPniObject(pniSignatureMessage.pni).getRawUuidBytes(),
signature: pniSignatureMessage.signature,
};
}
@ -1300,6 +1319,10 @@ export default class MessageSender {
}
if (destinationServiceId) {
sentMessage.destinationServiceId = destinationServiceId;
if (isProtoBinaryEncodingEnabled()) {
sentMessage.destinationServiceIdBinary =
toServiceIdObject(destinationServiceId).getServiceIdBinary();
}
}
if (expirationStartTimestamp) {
sentMessage.expirationStartTimestamp = Long.fromNumber(
@ -1330,6 +1353,10 @@ export default class MessageSender {
const serviceId = conv.getServiceId();
if (serviceId) {
status.destinationServiceId = serviceId;
if (isProtoBinaryEncodingEnabled()) {
status.destinationServiceIdBinary =
toServiceIdObject(serviceId).getServiceIdBinary();
}
}
if (isPniString(serviceId)) {
const pniIdentityKey =
@ -1801,7 +1828,11 @@ export default class MessageSender {
const syncMessage = MessageSender.createSyncMessage();
const viewOnceOpen = new Proto.SyncMessage.ViewOnceOpen();
viewOnceOpen.senderAci = senderAci;
if (isProtoBinaryEncodingEnabled()) {
viewOnceOpen.senderAciBinary = toAciObject(senderAci).getRawUuidBytes();
} else {
viewOnceOpen.senderAci = senderAci;
}
viewOnceOpen.timestamp = Long.fromNumber(timestamp);
syncMessage.viewOnceOpen = viewOnceOpen;
@ -1823,7 +1854,7 @@ export default class MessageSender {
static getBlockSync(
options: Readonly<{
e164s: Array<string>;
acis: Array<string>;
acis: Array<AciString>;
groupIds: Array<Uint8Array>;
}>
): SingleProtoJobData {
@ -1833,7 +1864,13 @@ export default class MessageSender {
const blocked = new Proto.SyncMessage.Blocked();
blocked.numbers = options.e164s;
blocked.acis = options.acis;
if (isProtoBinaryEncodingEnabled()) {
blocked.acisBinary = options.acis.map(aci =>
toAciObject(aci).getRawUuidBytes()
);
} else {
blocked.acis = options.acis;
}
blocked.groupIds = options.groupIds;
syncMessage.blocked = blocked;
@ -1867,7 +1904,13 @@ export default class MessageSender {
const response = new Proto.SyncMessage.MessageRequestResponse();
if (options.threadAci !== undefined) {
response.threadAci = options.threadAci;
if (isProtoBinaryEncodingEnabled()) {
response.threadAciBinary = toAciObject(
options.threadAci
).getRawUuidBytes();
} else {
response.threadAci = options.threadAci;
}
}
if (options.groupId) {
response.groupId = options.groupId;
@ -1950,7 +1993,12 @@ export default class MessageSender {
const verified = new Proto.Verified();
verified.state = state;
if (destinationAci) {
verified.destinationAci = destinationAci;
if (isProtoBinaryEncodingEnabled()) {
verified.destinationAciBinary =
toAciObject(destinationAci).getRawUuidBytes();
} else {
verified.destinationAci = destinationAci;
}
}
verified.identityKey = identityKey;
verified.nullMessage = padding;
@ -2501,11 +2549,23 @@ function toAddressableMessage(message: AddressableMessage) {
targetMessage.sentTimestamp = Long.fromNumber(message.sentAt);
if (message.type === 'aci') {
targetMessage.authorServiceId = message.authorAci;
if (isProtoBinaryEncodingEnabled()) {
targetMessage.authorServiceIdBinary = toAciObject(
message.authorAci
).getServiceIdBinary();
} else {
targetMessage.authorServiceId = message.authorAci;
}
} else if (message.type === 'e164') {
targetMessage.authorE164 = message.authorE164;
} else if (message.type === 'pni') {
targetMessage.authorServiceId = message.authorPni;
if (isProtoBinaryEncodingEnabled()) {
targetMessage.authorServiceIdBinary = toPniObject(
message.authorPni
).getServiceIdBinary();
} else {
targetMessage.authorServiceId = message.authorPni;
}
} else {
throw missingCaseError(message);
}
@ -2517,9 +2577,21 @@ function toConversationIdentifier(conversation: ConversationIdentifier) {
const targetConversation = new Proto.ConversationIdentifier();
if (conversation.type === 'aci') {
targetConversation.threadServiceId = conversation.aci;
if (isProtoBinaryEncodingEnabled()) {
targetConversation.threadServiceIdBinary = toAciObject(
conversation.aci
).getServiceIdBinary();
} else {
targetConversation.threadServiceId = conversation.aci;
}
} else if (conversation.type === 'pni') {
targetConversation.threadServiceId = conversation.pni;
if (isProtoBinaryEncodingEnabled()) {
targetConversation.threadServiceIdBinary = toPniObject(
conversation.pni
).getServiceIdBinary();
} else {
targetConversation.threadServiceId = conversation.pni;
}
} else if (conversation.type === 'group') {
targetConversation.threadGroupId = Bytes.fromBase64(conversation.groupId);
} else if (conversation.type === 'e164') {

View file

@ -189,8 +189,6 @@ export type ProcessedBodyRange = RawBodyRange;
export type ProcessedGroupCallUpdate = Proto.DataMessage.IGroupCallUpdate;
export type ProcessedStoryContext = Proto.DataMessage.IStoryContext;
export type ProcessedGiftBadge = {
expiration: number;
id: string | undefined;
@ -199,6 +197,11 @@ export type ProcessedGiftBadge = {
state: GiftBadgeStates;
};
export type ProcessedStoryContext = {
authorAci: AciString | undefined;
sentTimestamp: number;
};
export type ProcessedDataMessage = {
body?: string;
bodyAttachment?: ProcessedAttachment;

View file

@ -7,6 +7,8 @@ import { isNumber } from 'lodash';
import { assertDev, strictAssert } from '../util/assert';
import { dropNull, shallowDropNull } from '../util/dropNull';
import { fromAciUuidBytesOrString } from '../util/ServiceId';
import { getTimestampFromLong } from '../util/timestampLongUtils';
import { SignalService as Proto } from '../protobuf';
import { deriveGroupFields } from '../groups';
import * as Bytes from '../Bytes';
@ -22,6 +24,7 @@ import type {
ProcessedReaction,
ProcessedDelete,
ProcessedGiftBadge,
ProcessedStoryContext,
} from './Types.d';
import { GiftBadgeStates } from '../components/conversation/Message';
import { APPLICATION_OCTET_STREAM, stringToMIMEType } from '../types/MIME';
@ -29,8 +32,6 @@ import { SECOND, DurationInSeconds } from '../util/durations';
import type { AnyPaymentEvent } from '../types/Payment';
import { PaymentEventKind } from '../types/Payment';
import { filterAndClean } from '../types/BodyRange';
import { isAciString } from '../util/isAciString';
import { normalizeAci } from '../util/normalizeAci';
import { bytesToUuid } from '../util/uuidToBytes';
import { createName } from '../util/attachmentPath';
import { partitionBodyAndNormalAttachments } from '../types/Attachment';
@ -168,14 +169,16 @@ export function processQuote(
return undefined;
}
const { authorAci } = quote;
if (!isAciString(authorAci)) {
throw new Error('quote.authorAci is not an ACI string');
}
const { authorAci: rawAuthorAci, authorAciBinary } = quote;
const authorAci = fromAciUuidBytesOrString(
authorAciBinary,
rawAuthorAci,
'Quote.authorAci'
);
return {
id: quote.id?.toNumber(),
authorAci: normalizeAci(authorAci, 'Quote.authorAci'),
authorAci,
text: dropNull(quote.text),
attachments: (quote.attachments ?? []).slice(0, 1).map(attachment => {
return {
@ -191,6 +194,30 @@ export function processQuote(
};
}
export function processStoryContext(
storyContext?: Proto.DataMessage.IStoryContext | null
): ProcessedStoryContext | undefined {
if (!storyContext) {
return undefined;
}
const {
authorAci: rawAuthorAci,
authorAciBinary,
sentTimestamp,
} = storyContext;
const authorAci = fromAciUuidBytesOrString(
authorAciBinary,
rawAuthorAci,
'StoryContext.authorAci'
);
return {
authorAci,
sentTimestamp: getTimestampFromLong(sentTimestamp),
};
}
export function processContact(
contact?: ReadonlyArray<Proto.DataMessage.IContact> | null
): ReadonlyArray<ProcessedContact> | undefined {
@ -266,15 +293,18 @@ export function processReaction(
return undefined;
}
const { targetAuthorAci } = reaction;
if (!isAciString(targetAuthorAci)) {
throw new Error('reaction.targetAuthorAci is not an ACI string');
}
const { targetAuthorAci: rawTargetAuthorAci, targetAuthorAciBinary } =
reaction;
const targetAuthorAci = fromAciUuidBytesOrString(
targetAuthorAciBinary,
rawTargetAuthorAci,
'Reaction.targetAuthorAci'
);
return {
emoji: dropNull(reaction.emoji),
remove: Boolean(reaction.remove),
targetAuthorAci: normalizeAci(targetAuthorAci, 'Reaction.targetAuthorAci'),
targetAuthorAci,
targetTimestamp: reaction.targetSentTimestamp?.toNumber(),
};
}
@ -380,7 +410,7 @@ export function processDataMessage(
delete: processDelete(message.delete),
bodyRanges: filterAndClean(message.bodyRanges),
groupCallUpdate: dropNull(message.groupCallUpdate),
storyContext: dropNull(message.storyContext),
storyContext: processStoryContext(message.storyContext),
giftBadge: processGiftBadge(message.giftBadge),
};

View file

@ -3,11 +3,12 @@
import type { SignalService as Proto } from '../protobuf';
import type { ServiceIdString } from '../types/ServiceId';
import { normalizeServiceId } from '../types/ServiceId';
import { fromServiceIdBinaryOrString } from '../util/ServiceId';
import type { ProcessedSent, ProcessedSyncMessage } from './Types.d';
type ProtoServiceId = Readonly<{
destinationServiceId?: string | null;
destinationServiceIdBinary?: Uint8Array | null;
}>;
function processProtoWithDestinationServiceId<Input extends ProtoServiceId>(
@ -15,14 +16,20 @@ function processProtoWithDestinationServiceId<Input extends ProtoServiceId>(
): Omit<Input, keyof ProtoServiceId> & {
destinationServiceId?: ServiceIdString;
} {
const { destinationServiceId, ...remaining } = input;
const {
destinationServiceId: rawDestinationServiceId,
destinationServiceIdBinary,
...remaining
} = input;
return {
...remaining,
destinationServiceId: destinationServiceId
? normalizeServiceId(destinationServiceId, 'processSyncMessage')
: undefined,
destinationServiceId: fromServiceIdBinaryOrString(
destinationServiceIdBinary,
rawDestinationServiceId,
'processSyncMessage'
),
};
}
@ -34,7 +41,8 @@ function processSent(
}
const {
destinationServiceId,
destinationServiceId: rawDestinationServiceId,
destinationServiceIdBinary,
unidentifiedStatus,
storyMessageRecipients,
...remaining
@ -43,9 +51,11 @@ function processSent(
return {
...remaining,
destinationServiceId: destinationServiceId
? normalizeServiceId(destinationServiceId, 'processSent')
: undefined,
destinationServiceId: fromServiceIdBinaryOrString(
destinationServiceIdBinary,
rawDestinationServiceId,
'processSent'
),
unidentifiedStatus: unidentifiedStatus
? unidentifiedStatus.map(processProtoWithDestinationServiceId)
: undefined,

View file

@ -3,6 +3,17 @@
import { Aci, Pni, ServiceId } from '@signalapp/libsignal-client';
import type { AciString, PniString, ServiceIdString } from '../types/ServiceId';
import {
normalizeServiceId,
normalizePni,
isUntaggedPniString,
toTaggedPni,
fromServiceIdObject,
fromAciObject,
fromPniObject,
} from '../types/ServiceId';
import * as Bytes from '../Bytes';
import { normalizeAci } from './normalizeAci';
export function toServiceIdObject(serviceId: ServiceIdString): ServiceId {
return ServiceId.parseFromServiceIdString(serviceId);
@ -15,3 +26,86 @@ export function toAciObject(aci: AciString): Aci {
export function toPniObject(pni: PniString): Pni {
return Pni.parseFromServiceIdString(pni);
}
export function fromServiceIdBinaryOrString(
bytes: Uint8Array,
fallback: string | undefined | null,
context: string
): ServiceIdString;
export function fromServiceIdBinaryOrString(
bytes: Uint8Array | undefined | null,
fallback: string | undefined | null,
context: string
): ServiceIdString | undefined;
export function fromServiceIdBinaryOrString(
bytes: Uint8Array | undefined | null,
fallback: string | undefined | null,
context: string
): ServiceIdString | undefined {
if (Bytes.isNotEmpty(bytes)) {
return fromServiceIdObject(
ServiceId.parseFromServiceIdBinary(Buffer.from(bytes))
);
}
if (fallback) {
return normalizeServiceId(fallback, context);
}
return undefined;
}
export function fromAciUuidBytes(bytes: Uint8Array): AciString;
export function fromAciUuidBytes(
bytes: Uint8Array | undefined | null
): AciString | undefined;
export function fromAciUuidBytes(
bytes: Uint8Array | undefined | null
): AciString | undefined {
if (Bytes.isNotEmpty(bytes)) {
return fromAciObject(Aci.fromUuidBytes(Buffer.from(bytes)));
}
return undefined;
}
export function fromAciUuidBytesOrString(
bytes: Uint8Array,
fallback: string | undefined | null,
context: string
): AciString;
export function fromAciUuidBytesOrString(
bytes: Uint8Array | undefined | null,
fallback: string | undefined | null,
context: string
): AciString | undefined;
export function fromAciUuidBytesOrString(
bytes: Uint8Array | undefined | null,
fallback: string | undefined | null,
context: string
): AciString | undefined {
if (Bytes.isNotEmpty(bytes)) {
return fromAciUuidBytes(bytes);
}
if (fallback) {
return normalizeAci(fallback, context);
}
return undefined;
}
export function fromPniUuidBytesOrUntaggedString(
bytes: Uint8Array | undefined | null,
fallback: string | undefined | null,
context: string
): PniString | undefined {
if (Bytes.isNotEmpty(bytes)) {
return fromPniObject(Pni.fromUuidBytes(Buffer.from(bytes)));
}
if (fallback && isUntaggedPniString(fallback)) {
return normalizePni(toTaggedPni(fallback), context);
}
return undefined;
}

View file

@ -4,6 +4,7 @@
import * as Bytes from '../Bytes';
import { SignalService as Proto } from '../protobuf';
import { fromServiceIdBinaryOrString } from './ServiceId';
import PinnedConversation = Proto.AccountRecord.IPinnedConversation;
@ -22,10 +23,24 @@ export function arePinnedConversationsEqual(
localPinnedConversation;
if (contact) {
return (
remotePinnedConversation.contact &&
contact.serviceId === remotePinnedConversation.contact.serviceId
const { contact: remoteContact } = remotePinnedConversation;
if (!remoteContact) {
return false;
}
const serviceId = fromServiceIdBinaryOrString(
contact.serviceIdBinary,
contact.serviceId,
`arePinnedConversationsEqual(${index}).local`
);
const remoteServiceId = fromServiceIdBinaryOrString(
remoteContact.serviceIdBinary,
remoteContact.serviceId,
`arePinnedConversationsEqual(${index}).remote`
);
return serviceId === remoteServiceId;
}
if (groupMasterKey && groupMasterKey.length) {

View file

@ -31,7 +31,7 @@ export function checkFirstEnvelope(incoming: IncomingWebSocketRequest): void {
}
const decoded = Proto.Envelope.decode(plaintext);
const newEnvelopeTimestamp = decoded.timestamp?.toNumber();
const newEnvelopeTimestamp = decoded.clientTimestamp?.toNumber();
if (!isNumber(newEnvelopeTimestamp)) {
log.warn('timestamp is not a number!');
return;

View file

@ -5,33 +5,32 @@ import type {
ReadonlyMessageAttributesType,
MessageAttributesType,
} from '../model-types.d';
import type { SignalService as Proto } from '../protobuf';
import type { AciString } from '../types/ServiceId';
import { type AciString } from '../types/ServiceId';
import { type ProcessedStoryContext } from '../textsecure/Types.d';
import { DataReader } from '../sql/Client';
import { createLogger } from '../logging/log';
import { normalizeAci } from './normalizeAci';
import { getAuthorId } from '../messages/helpers';
import { getTimestampFromLong } from './timestampLongUtils';
const log = createLogger('findStoryMessage');
export async function findStoryMessages(
conversationId: string,
storyContext?: Proto.DataMessage.IStoryContext
storyContext?: ProcessedStoryContext
): Promise<Array<MessageAttributesType>> {
if (!storyContext) {
return [];
}
const { authorAci: rawAuthorAci, sentTimestamp } = storyContext;
const { authorAci, sentTimestamp: sentAt } = storyContext;
if (!rawAuthorAci || !sentTimestamp) {
if (!sentAt) {
return [];
}
const authorAci = normalizeAci(rawAuthorAci, 'findStoryMessage');
if (authorAci == null) {
return [];
}
const sentAt = getTimestampFromLong(sentTimestamp);
const ourConversationId =
window.ConversationController.getOurConversationIdOrThrow();

View file

@ -0,0 +1,13 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { isTestOrMockEnvironment } from '../environment';
export function isProtoBinaryEncodingEnabled(): boolean {
if (isTestOrMockEnvironment()) {
return true;
}
// TODO: https://signalmessenger.atlassian.net/browse/DESKTOP-8938
return false;
}

View file

@ -4,7 +4,6 @@
import { isEqual } from 'lodash';
import { DataReader } from '../sql/Client';
import type { StoryRecipientUpdateEvent } from '../textsecure/messageReceiverEvents';
import { normalizeServiceId } from '../types/ServiceId';
import { normalizeStoryDistributionId } from '../types/StoryDistributionId';
import { createLogger } from '../logging/log';
import { SendStatus } from '../messages/MessageSendState';
@ -13,6 +12,7 @@ import { isStory } from '../state/selectors/message';
import { queueUpdateMessage } from './messageBatcher';
import { isMe } from './whatTypeOfConversation';
import { drop } from './drop';
import { fromServiceIdBinaryOrString } from './ServiceId';
import { handleDeleteForEveryone } from './deleteForEveryone';
import { MessageModel } from '../models/messages';
@ -61,15 +61,22 @@ export async function onStoryRecipientUpdate(
Set<string>
>();
data.storyMessageRecipients.forEach(item => {
const { destinationServiceId: recipientServiceId } = item;
const {
destinationServiceId: rawDestinationServiceId,
destinationServiceIdBinary,
} = item;
if (!recipientServiceId) {
const recipientServiceId = fromServiceIdBinaryOrString(
destinationServiceIdBinary,
rawDestinationServiceId,
`${logId}.recipientServiceId`
);
if (recipientServiceId == null) {
return;
}
const convo = window.ConversationController.get(
normalizeServiceId(recipientServiceId, `${logId}.recipientServiceId`)
);
const convo = window.ConversationController.get(recipientServiceId);
if (!convo || !item.distributionListIds) {
return;