diff --git a/package.json b/package.json index 5095098921..40c7fdff7e 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index adfdf6660b..cda67461ea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/protos/DeviceMessages.proto b/protos/DeviceMessages.proto index ae2e44bacb..2fde938e19 100644 --- a/protos/DeviceMessages.proto +++ b/protos/DeviceMessages.proto @@ -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; } diff --git a/protos/Migrations.proto b/protos/Migrations.proto new file mode 100644 index 0000000000..ab8e092b99 --- /dev/null +++ b/protos/Migrations.proto @@ -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 +} diff --git a/protos/SignalService.proto b/protos/SignalService.proto index 4963955836..15b09dd97c 100644 --- a/protos/SignalService.proto +++ b/protos/SignalService.proto @@ -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) } } diff --git a/protos/SignalStorage.proto b/protos/SignalStorage.proto index f19c3fea4f..1eafd913c4 100644 --- a/protos/SignalStorage.proto +++ b/protos/SignalStorage.proto @@ -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 { diff --git a/ts/protobuf/index.ts b/ts/protobuf/index.ts index ef1e0e5978..9e319adca1 100644 --- a/ts/protobuf/index.ts +++ b/ts/protobuf/index.ts @@ -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 }; diff --git a/ts/services/contactSync.ts b/ts/services/contactSync.ts index 1d626837b1..7d6760b4f5 100644 --- a/ts/services/contactSync.ts +++ b/ts/services/contactSync.ts @@ -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, }); diff --git a/ts/services/storage.ts b/ts/services/storage.ts index aef4b82146..7404a4c12c 100644 --- a/ts/services/storage.ts +++ b/ts/services/storage.ts @@ -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; } diff --git a/ts/services/storageRecordOps.ts b/ts/services/storageRecordOps.ts index 76acd992f4..0b34193e3a 100644 --- a/ts/services/storageRecordOps.ts +++ b/ts/services/storageRecordOps.ts @@ -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 = ( - storyDistributionListRecord.recipientServiceIds || [] - ).map(id => normalizeServiceId(id, 'mergeStoryDistributionListRecord')); + let remoteListMembers: Array; + + 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'); diff --git a/ts/sql/migrations/1280-blob-unprocessed.ts b/ts/sql/migrations/1280-blob-unprocessed.ts index 95eb0d4504..c844a6cb0e 100644 --- a/ts/sql/migrations/1280-blob-unprocessed.ts +++ b/ts/sql/migrations/1280-blob-unprocessed.ts @@ -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'; diff --git a/ts/test-both/processDataMessage_test.ts b/ts/test-both/processDataMessage_test.ts index bc39de0907..a6d917500e 100644 --- a/ts/test-both/processDataMessage_test.ts +++ b/ts/test-both/processDataMessage_test.ts @@ -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, diff --git a/ts/test-electron/ContactsParser_test.ts b/ts/test-electron/ContactsParser_test.ts index 3d68bd0bab..f10b05eab0 100644 --- a/ts/test-electron/ContactsParser_test.ts +++ b/ts/test-electron/ContactsParser_test.ts @@ -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 { 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; diff --git a/ts/test-electron/MessageReceiver_test.ts b/ts/test-electron/MessageReceiver_test.ts index 151740e20a..8c18f9eceb 100644 --- a/ts/test-electron/MessageReceiver_test.ts +++ b/ts/test-electron/MessageReceiver_test.ts @@ -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(); diff --git a/ts/test-mock/backups/backups_test.ts b/ts/test-mock/backups/backups_test.ts index 3aac1756ad..7346c9b352 100644 --- a/ts/test-mock/backups/backups_test.ts +++ b/ts/test-mock/backups/backups_test.ts @@ -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'); diff --git a/ts/test-mock/benchmarks/convo_open_bench.ts b/ts/test-mock/benchmarks/convo_open_bench.ts index 4d1be3aa2a..d500bd47f8 100644 --- a/ts/test-mock/benchmarks/convo_open_bench.ts +++ b/ts/test-mock/benchmarks/convo_open_bench.ts @@ -62,9 +62,7 @@ Bootstrap.benchmark(async (bootstrap: Bootstrap): Promise => { const openConvo = async (contact: PrimaryDevice): Promise => { 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(); }; diff --git a/ts/test-mock/benchmarks/group_send_bench.ts b/ts/test-mock/benchmarks/group_send_bench.ts index 22ea00cf44..5d610775b9 100644 --- a/ts/test-mock/benchmarks/group_send_bench.ts +++ b/ts/test-mock/benchmarks/group_send_bench.ts @@ -126,39 +126,49 @@ Bootstrap.benchmark(async (bootstrap: Bootstrap): Promise => { } 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'); { diff --git a/ts/test-mock/benchmarks/send_bench.ts b/ts/test-mock/benchmarks/send_bench.ts index c08e2bea36..861d8e8b7b 100644 --- a/ts/test-mock/benchmarks/send_bench.ts +++ b/ts/test-mock/benchmarks/send_bench.ts @@ -68,7 +68,7 @@ Bootstrap.benchmark(async (bootstrap: Bootstrap): Promise => { { 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(); } diff --git a/ts/test-mock/benchmarks/storage_sync_bench.ts b/ts/test-mock/benchmarks/storage_sync_bench.ts index ee4377f61c..8df1e4ceb7 100644 --- a/ts/test-mock/benchmarks/storage_sync_bench.ts +++ b/ts/test-mock/benchmarks/storage_sync_bench.ts @@ -49,9 +49,7 @@ Bootstrap.benchmark(async (bootstrap: Bootstrap): Promise => { 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; diff --git a/ts/test-mock/helpers.ts b/ts/test-mock/helpers.ts index b105313df0..aa5627701a 100644 --- a/ts/test-mock/helpers.ts +++ b/ts/test-mock/helpers.ts @@ -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), }, }, diff --git a/ts/test-mock/messaging/backfill_test.ts b/ts/test-mock/messaging/backfill_test.ts index bb526e12e8..469226cf27 100644 --- a/ts/test-mock/messaging/backfill_test.ts +++ b/ts/test-mock/messaging/backfill_test.ts @@ -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: [ { diff --git a/ts/test-mock/messaging/edit_test.ts b/ts/test-mock/messaging/edit_test.ts index de60e7bd70..bb28ebac43 100644 --- a/ts/test-mock/messaging/edit_test.ts +++ b/ts/test-mock/messaging/edit_test.ts @@ -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'); diff --git a/ts/test-mock/messaging/expire_timer_version_test.ts b/ts/test-mock/messaging/expire_timer_version_test.ts index 5cba492456..1552a377fb 100644 --- a/ts/test-mock/messaging/expire_timer_version_test.ts +++ b/ts/test-mock/messaging/expire_timer_version_test.ts @@ -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, }, ], }, diff --git a/ts/test-mock/messaging/readSync_test.ts b/ts/test-mock/messaging/readSync_test.ts index 84eaedda42..1056243fd2 100644 --- a/ts/test-mock/messaging/readSync_test.ts +++ b/ts/test-mock/messaging/readSync_test.ts @@ -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, })), }, diff --git a/ts/test-mock/messaging/safety_number_test.ts b/ts/test-mock/messaging/safety_number_test.ts index 680b5d1c2f..b61719398d 100644 --- a/ts/test-mock/messaging/safety_number_test.ts +++ b/ts/test-mock/messaging/safety_number_test.ts @@ -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], }, }, }); diff --git a/ts/test-mock/messaging/sendSync_test.ts b/ts/test-mock/messaging/sendSync_test.ts index 328f0608dc..0d79e21902 100644 --- a/ts/test-mock/messaging/sendSync_test.ts +++ b/ts/test-mock/messaging/sendSync_test.ts @@ -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, })), }, diff --git a/ts/test-mock/messaging/stories_test.ts b/ts/test-mock/messaging/stories_test.ts index 125567e1fc..ef81dfd3df 100644 --- a/ts/test-mock/messaging/stories_test.ts +++ b/ts/test-mock/messaging/stories_test.ts @@ -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: { diff --git a/ts/test-mock/messaging/unknown_contact_test.ts b/ts/test-mock/messaging/unknown_contact_test.ts index eea7cf7ed0..d4e4b34678 100644 --- a/ts/test-mock/messaging/unknown_contact_test.ts +++ b/ts/test-mock/messaging/unknown_contact_test.ts @@ -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, }, }, }); diff --git a/ts/test-mock/network/libsignal_test.ts b/ts/test-mock/network/libsignal_test.ts index dd20f7948c..cd40e4bb9e 100644 --- a/ts/test-mock/network/libsignal_test.ts +++ b/ts/test-mock/network/libsignal_test.ts @@ -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(); } diff --git a/ts/test-mock/pnp/calling_test.ts b/ts/test-mock/pnp/calling_test.ts index 52f8b4a1df..ab09c464fe 100644 --- a/ts/test-mock/pnp/calling_test.ts +++ b/ts/test-mock/pnp/calling_test.ts @@ -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); diff --git a/ts/test-mock/pnp/change_number_test.ts b/ts/test-mock/pnp/change_number_test.ts index 546b968784..e30df0788a 100644 --- a/ts/test-mock/pnp/change_number_test.ts +++ b/ts/test-mock/pnp/change_number_test.ts @@ -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'); }); diff --git a/ts/test-mock/pnp/merge_test.ts b/ts/test-mock/pnp/merge_test.ts index 62fdcf627c..11435b8990 100644 --- a/ts/test-mock/pnp/merge_test.ts +++ b/ts/test-mock/pnp/merge_test.ts @@ -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, }, ], diff --git a/ts/test-mock/pnp/phone_discovery_test.ts b/ts/test-mock/pnp/phone_discovery_test.ts index 38d44b88a8..0f0747e345 100644 --- a/ts/test-mock/pnp/phone_discovery_test.ts +++ b/ts/test-mock/pnp/phone_discovery_test.ts @@ -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({ diff --git a/ts/test-mock/pnp/pni_change_test.ts b/ts/test-mock/pnp/pni_change_test.ts index fecb3688bd..3bc3f7801b 100644 --- a/ts/test-mock/pnp/pni_change_test.ts +++ b/ts/test-mock/pnp/pni_change_test.ts @@ -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 ) diff --git a/ts/test-mock/pnp/pni_signature_test.ts b/ts/test-mock/pnp/pni_signature_test.ts index 0713460d7b..82b94ef1e4 100644 --- a/ts/test-mock/pnp/pni_signature_test.ts +++ b/ts/test-mock/pnp/pni_signature_test.ts @@ -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 diff --git a/ts/test-mock/pnp/pni_unlink_test.ts b/ts/test-mock/pnp/pni_unlink_test.ts index 7e44eb6b19..6855815a6b 100644 --- a/ts/test-mock/pnp/pni_unlink_test.ts +++ b/ts/test-mock/pnp/pni_unlink_test.ts @@ -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, } ) ); diff --git a/ts/test-mock/pnp/send_gv2_invite_test.ts b/ts/test-mock/pnp/send_gv2_invite_test.ts index a1cc1e479d..b7602ed34d 100644 --- a/ts/test-mock/pnp/send_gv2_invite_test.ts +++ b/ts/test-mock/pnp/send_gv2_invite_test.ts @@ -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: [], }, }, }); diff --git a/ts/test-mock/pnp/username_test.ts b/ts/test-mock/pnp/username_test.ts index 36bbddc6ad..a2094e295c 100644 --- a/ts/test-mock/pnp/username_test.ts +++ b/ts/test-mock/pnp/username_test.ts @@ -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); } diff --git a/ts/test-mock/rate-limit/story_test.ts b/ts/test-mock/rate-limit/story_test.ts index 9119928976..8b89a475b6 100644 --- a/ts/test-mock/rate-limit/story_test.ts +++ b/ts/test-mock/rate-limit/story_test.ts @@ -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: [], }, }, }); diff --git a/ts/test-mock/rate-limit/viewed_test.ts b/ts/test-mock/rate-limit/viewed_test.ts index 9ca8e56bce..1922397493 100644 --- a/ts/test-mock/rate-limit/viewed_test.ts +++ b/ts/test-mock/rate-limit/viewed_test.ts @@ -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); diff --git a/ts/test-mock/routing/routing_test.ts b/ts/test-mock/routing/routing_test.ts index 4f325ab92e..2ccff69b9c 100644 --- a/ts/test-mock/routing/routing_test.ts +++ b/ts/test-mock/routing/routing_test.ts @@ -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({ diff --git a/ts/test-mock/storage/archive_test.ts b/ts/test-mock/storage/archive_test.ts index 8c1bc1da59..f71d87a8ef 100644 --- a/ts/test-mock/storage/archive_test.ts +++ b/ts/test-mock/storage/archive_test.ts @@ -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( diff --git a/ts/test-mock/storage/call_links_test.ts b/ts/test-mock/storage/call_links_test.ts index 7ff4569948..a222349a15 100644 --- a/ts/test-mock/storage/call_links_test.ts +++ b/ts/test-mock/storage/call_links_test.ts @@ -41,7 +41,6 @@ describe('storage service', function (this: Mocha.Suite) { identifier: uuidToBytes(MY_STORY_ID), isBlockList: true, name: MY_STORY_ID, - recipientServiceIds: [], }, }, }); diff --git a/ts/test-mock/storage/fixtures.ts b/ts/test-mock/storage/fixtures.ts index 36d46b5321..972507026e 100644 --- a/ts/test-mock/storage/fixtures.ts +++ b/ts/test-mock/storage/fixtures.ts @@ -101,7 +101,6 @@ export async function initStorage( identifier: uuidToBytes(MY_STORY_ID), isBlockList: true, name: MY_STORY_ID, - recipientServiceIds: [], }, }, }); diff --git a/ts/test-mock/storage/max_read_keys_test.ts b/ts/test-mock/storage/max_read_keys_test.ts index 57242e1b6f..73098c9d40 100644 --- a/ts/test-mock/storage/max_read_keys_test.ts +++ b/ts/test-mock/storage/max_read_keys_test.ts @@ -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'); diff --git a/ts/test-mock/storage/message_request_test.ts b/ts/test-mock/storage/message_request_test.ts index 1bd416c8b1..3072dfe1b9 100644 --- a/ts/test-mock/storage/message_request_test.ts +++ b/ts/test-mock/storage/message_request_test.ts @@ -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({ diff --git a/ts/test-mock/storage/pin_unpin_test.ts b/ts/test-mock/storage/pin_unpin_test.ts index 95f452775b..6fd44abdb2 100644 --- a/ts/test-mock/storage/pin_unpin_test.ts +++ b/ts/test-mock/storage/pin_unpin_test.ts @@ -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(); diff --git a/ts/test-mock/storage/sticker_test.ts b/ts/test-mock/storage/sticker_test.ts index c8c67bfd75..f104d0b7e0 100644 --- a/ts/test-mock/storage/sticker_test.ts +++ b/ts/test-mock/storage/sticker_test.ts @@ -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(); { diff --git a/ts/test-node/sql/migration_1280_test.ts b/ts/test-node/sql/migration_1280_test.ts index 4a8910efdf..b8a2f230f0 100644 --- a/ts/test-node/sql/migration_1280_test.ts +++ b/ts/test-node/sql/migration_1280_test.ts @@ -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'; diff --git a/ts/textsecure/ContactsParser.ts b/ts/textsecure/ContactsParser.ts index abb686c4d4..38c0a7c58a 100644 --- a/ts/textsecure/ContactsParser.ts +++ b/ts/textsecure/ContactsParser.ts @@ -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 = 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, diff --git a/ts/textsecure/MessageReceiver.ts b/ts/textsecure/MessageReceiver.ts index f9e615922f..80f94e7e2f 100644 --- a/ts/textsecure/MessageReceiver.ts +++ b/ts/textsecure/MessageReceiver.ts @@ -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; + 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( diff --git a/ts/textsecure/OutgoingMessage.ts b/ts/textsecure/OutgoingMessage.ts index 9eab99f8c0..a90710806b 100644 --- a/ts/textsecure/OutgoingMessage.ts +++ b/ts/textsecure/OutgoingMessage.ts @@ -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; diff --git a/ts/textsecure/Provisioner.ts b/ts/textsecure/Provisioner.ts index d77c7bd81b..18ffeb090b 100644 --- a/ts/textsecure/Provisioner.ts +++ b/ts/textsecure/Provisioner.ts @@ -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( diff --git a/ts/textsecure/ProvisioningCipher.ts b/ts/textsecure/ProvisioningCipher.ts index c6870f78e3..f05426652d 100644 --- a/ts/textsecure/ProvisioningCipher.ts +++ b/ts/textsecure/ProvisioningCipher.ts @@ -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; } diff --git a/ts/textsecure/SendMessage.ts b/ts/textsecure/SendMessage.ts index 5cb6fa9fc8..7f90304703 100644 --- a/ts/textsecure/SendMessage.ts +++ b/ts/textsecure/SendMessage.ts @@ -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; - acis: Array; + acis: Array; groupIds: Array; }> ): 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') { diff --git a/ts/textsecure/Types.d.ts b/ts/textsecure/Types.d.ts index e70369a1fb..8bd4b6f1d1 100644 --- a/ts/textsecure/Types.d.ts +++ b/ts/textsecure/Types.d.ts @@ -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; diff --git a/ts/textsecure/processDataMessage.ts b/ts/textsecure/processDataMessage.ts index dcb16ddaab..600dbfbe70 100644 --- a/ts/textsecure/processDataMessage.ts +++ b/ts/textsecure/processDataMessage.ts @@ -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 | null ): ReadonlyArray | 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), }; diff --git a/ts/textsecure/processSyncMessage.ts b/ts/textsecure/processSyncMessage.ts index 77db19c97d..ffa19b9466 100644 --- a/ts/textsecure/processSyncMessage.ts +++ b/ts/textsecure/processSyncMessage.ts @@ -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( @@ -15,14 +16,20 @@ function processProtoWithDestinationServiceId( ): Omit & { 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, diff --git a/ts/util/ServiceId.ts b/ts/util/ServiceId.ts index 3fdc6aaf99..0ebd08aae1 100644 --- a/ts/util/ServiceId.ts +++ b/ts/util/ServiceId.ts @@ -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; +} diff --git a/ts/util/arePinnedConversationsEqual.ts b/ts/util/arePinnedConversationsEqual.ts index 6e32919ada..f814402f22 100644 --- a/ts/util/arePinnedConversationsEqual.ts +++ b/ts/util/arePinnedConversationsEqual.ts @@ -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) { diff --git a/ts/util/checkFirstEnvelope.ts b/ts/util/checkFirstEnvelope.ts index 127f5488a0..dfd93aabcd 100644 --- a/ts/util/checkFirstEnvelope.ts +++ b/ts/util/checkFirstEnvelope.ts @@ -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; diff --git a/ts/util/findStoryMessage.ts b/ts/util/findStoryMessage.ts index 5c969fdb0a..83c33c0772 100644 --- a/ts/util/findStoryMessage.ts +++ b/ts/util/findStoryMessage.ts @@ -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> { 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(); diff --git a/ts/util/isProtoBinaryEncodingEnabled.ts b/ts/util/isProtoBinaryEncodingEnabled.ts new file mode 100644 index 0000000000..9083e6f645 --- /dev/null +++ b/ts/util/isProtoBinaryEncodingEnabled.ts @@ -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; +} diff --git a/ts/util/onStoryRecipientUpdate.ts b/ts/util/onStoryRecipientUpdate.ts index 5ff80d57cd..10319b294f 100644 --- a/ts/util/onStoryRecipientUpdate.ts +++ b/ts/util/onStoryRecipientUpdate.ts @@ -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 >(); 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;