Send and receive PniSignatureMessage
This commit is contained in:
		
					parent
					
						
							
								95be24e8f7
							
						
					
				
			
			
				commit
				
					
						00cfd92dd0
					
				
			
		
					 43 changed files with 1082 additions and 164 deletions
				
			
		| 
						 | 
				
			
			@ -191,7 +191,7 @@
 | 
			
		|||
    "@babel/preset-typescript": "7.17.12",
 | 
			
		||||
    "@electron/fuses": "1.5.0",
 | 
			
		||||
    "@mixer/parallel-prettier": "2.0.1",
 | 
			
		||||
    "@signalapp/mock-server": "2.4.1",
 | 
			
		||||
    "@signalapp/mock-server": "2.6.0",
 | 
			
		||||
    "@storybook/addon-a11y": "6.5.6",
 | 
			
		||||
    "@storybook/addon-actions": "6.5.6",
 | 
			
		||||
    "@storybook/addon-controls": "6.5.6",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -39,15 +39,16 @@ message Envelope {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
message Content {
 | 
			
		||||
  optional DataMessage    dataMessage                  = 1;
 | 
			
		||||
  optional SyncMessage    syncMessage                  = 2;
 | 
			
		||||
  optional CallingMessage callingMessage               = 3;
 | 
			
		||||
  optional NullMessage    nullMessage                  = 4;
 | 
			
		||||
  optional ReceiptMessage receiptMessage               = 5;
 | 
			
		||||
  optional TypingMessage  typingMessage                = 6;
 | 
			
		||||
  optional bytes          senderKeyDistributionMessage = 7;
 | 
			
		||||
  optional bytes          decryptionErrorMessage       = 8;
 | 
			
		||||
  optional StoryMessage   storyMessage                 = 9;
 | 
			
		||||
  optional DataMessage         dataMessage                  = 1;
 | 
			
		||||
  optional SyncMessage         syncMessage                  = 2;
 | 
			
		||||
  optional CallingMessage      callingMessage               = 3;
 | 
			
		||||
  optional NullMessage         nullMessage                  = 4;
 | 
			
		||||
  optional ReceiptMessage      receiptMessage               = 5;
 | 
			
		||||
  optional TypingMessage       typingMessage                = 6;
 | 
			
		||||
  optional bytes               senderKeyDistributionMessage = 7;
 | 
			
		||||
  optional bytes               decryptionErrorMessage       = 8;
 | 
			
		||||
  optional StoryMessage        storyMessage                 = 9;
 | 
			
		||||
  optional PniSignatureMessage pniSignatureMessage          = 10;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Everything in CallingMessage must be kept in sync with RingRTC (ringrtc-node).
 | 
			
		||||
| 
						 | 
				
			
			@ -627,3 +628,9 @@ message GroupDetails {
 | 
			
		|||
  optional bool   blocked         = 8;
 | 
			
		||||
  optional uint32 inboxPosition   = 10;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message PniSignatureMessage {
 | 
			
		||||
  optional bytes pni = 1;
 | 
			
		||||
  // Signature *by* the PNI identity key *of* the ACI identity key
 | 
			
		||||
  optional bytes signature = 2;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1097,6 +1097,28 @@ export class ConversationController {
 | 
			
		|||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // For testing
 | 
			
		||||
  async _forgetE164(e164: string): Promise<void> {
 | 
			
		||||
    const { server } = window.textsecure;
 | 
			
		||||
    strictAssert(server, 'Server must be initialized');
 | 
			
		||||
    const { [e164]: pni } = await server.getUuidsForE164s([e164]);
 | 
			
		||||
 | 
			
		||||
    log.info(`ConversationController: forgetting e164=${e164} pni=${pni}`);
 | 
			
		||||
 | 
			
		||||
    const convos = [this.get(e164), this.get(pni)];
 | 
			
		||||
 | 
			
		||||
    for (const convo of convos) {
 | 
			
		||||
      if (!convo) {
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // eslint-disable-next-line no-await-in-loop
 | 
			
		||||
      await removeConversation(convo.id);
 | 
			
		||||
      this._conversations.remove(convo);
 | 
			
		||||
      this._conversations.resetLookups();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async doLoad(): Promise<void> {
 | 
			
		||||
    log.info('ConversationController: starting initial fetch');
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -111,7 +111,7 @@ export class IdentityKeys extends IdentityKeyStore {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  async getIdentityKey(): Promise<PrivateKey> {
 | 
			
		||||
    const keyPair = await window.textsecure.storage.protocol.getIdentityKeyPair(
 | 
			
		||||
    const keyPair = window.textsecure.storage.protocol.getIdentityKeyPair(
 | 
			
		||||
      this.ourUuid
 | 
			
		||||
    );
 | 
			
		||||
    if (!keyPair) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -33,6 +33,7 @@ import type {
 | 
			
		|||
  KeyPairType,
 | 
			
		||||
  OuterSignedPrekeyType,
 | 
			
		||||
  PniKeyMaterialType,
 | 
			
		||||
  PniSignatureMessageType,
 | 
			
		||||
  PreKeyIdType,
 | 
			
		||||
  PreKeyType,
 | 
			
		||||
  SenderKeyIdType,
 | 
			
		||||
| 
						 | 
				
			
			@ -108,9 +109,15 @@ type MapFields =
 | 
			
		|||
  | 'sessions'
 | 
			
		||||
  | 'signedPreKeys';
 | 
			
		||||
 | 
			
		||||
export type SessionTransactionOptions = {
 | 
			
		||||
  readonly zone?: Zone;
 | 
			
		||||
};
 | 
			
		||||
export type SessionTransactionOptions = Readonly<{
 | 
			
		||||
  zone?: Zone;
 | 
			
		||||
}>;
 | 
			
		||||
 | 
			
		||||
export type VerifyAlternateIdentityOptionsType = Readonly<{
 | 
			
		||||
  aci: UUID;
 | 
			
		||||
  pni: UUID;
 | 
			
		||||
  signature: Uint8Array;
 | 
			
		||||
}>;
 | 
			
		||||
 | 
			
		||||
export const GLOBAL_ZONE = new Zone('GLOBAL_ZONE');
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -213,6 +220,8 @@ export class SignalProtocolStore extends EventsMixin {
 | 
			
		|||
 | 
			
		||||
  private ourRegistrationIds = new Map<UUIDStringType, number>();
 | 
			
		||||
 | 
			
		||||
  private cachedPniSignatureMessage: PniSignatureMessageType | undefined;
 | 
			
		||||
 | 
			
		||||
  identityKeys?: Map<
 | 
			
		||||
    IdentityKeyIdType,
 | 
			
		||||
    CacheEntryType<IdentityKeyType, PublicKey>
 | 
			
		||||
| 
						 | 
				
			
			@ -301,7 +310,7 @@ export class SignalProtocolStore extends EventsMixin {
 | 
			
		|||
    ]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async getIdentityKeyPair(ourUuid: UUID): Promise<KeyPairType | undefined> {
 | 
			
		||||
  getIdentityKeyPair(ourUuid: UUID): KeyPairType | undefined {
 | 
			
		||||
    return this.ourIdentityKeys.get(ourUuid.toString());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -999,7 +1008,7 @@ export class SignalProtocolStore extends EventsMixin {
 | 
			
		|||
 | 
			
		||||
    const ourUuid = new UUID(session.ourUuid);
 | 
			
		||||
 | 
			
		||||
    const keyPair = await this.getIdentityKeyPair(ourUuid);
 | 
			
		||||
    const keyPair = this.getIdentityKeyPair(ourUuid);
 | 
			
		||||
    if (!keyPair) {
 | 
			
		||||
      throw new Error('_maybeMigrateSession: No identity key for ourself!');
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -2049,6 +2058,69 @@ export class SignalProtocolStore extends EventsMixin {
 | 
			
		|||
    await window.storage.fetch();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  signAlternateIdentity(): PniSignatureMessageType | undefined {
 | 
			
		||||
    const ourACI = window.textsecure.storage.user.getCheckedUuid(UUIDKind.ACI);
 | 
			
		||||
    const ourPNI = window.textsecure.storage.user.getUuid(UUIDKind.PNI);
 | 
			
		||||
    if (!ourPNI) {
 | 
			
		||||
      log.error('signAlternateIdentity: No local pni');
 | 
			
		||||
      return undefined;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this.cachedPniSignatureMessage?.pni === ourPNI.toString()) {
 | 
			
		||||
      return this.cachedPniSignatureMessage;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const aciKeyPair = this.getIdentityKeyPair(ourACI);
 | 
			
		||||
    const pniKeyPair = this.getIdentityKeyPair(ourPNI);
 | 
			
		||||
    if (!aciKeyPair) {
 | 
			
		||||
      log.error('signAlternateIdentity: No local ACI key pair');
 | 
			
		||||
      return undefined;
 | 
			
		||||
    }
 | 
			
		||||
    if (!pniKeyPair) {
 | 
			
		||||
      log.error('signAlternateIdentity: No local PNI key pair');
 | 
			
		||||
      return undefined;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const pniIdentity = new IdentityKeyPair(
 | 
			
		||||
      PublicKey.deserialize(Buffer.from(pniKeyPair.pubKey)),
 | 
			
		||||
      PrivateKey.deserialize(Buffer.from(pniKeyPair.privKey))
 | 
			
		||||
    );
 | 
			
		||||
    const aciPubKey = PublicKey.deserialize(Buffer.from(aciKeyPair.pubKey));
 | 
			
		||||
    this.cachedPniSignatureMessage = {
 | 
			
		||||
      pni: ourPNI.toString(),
 | 
			
		||||
      signature: pniIdentity.signAlternateIdentity(aciPubKey),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return this.cachedPniSignatureMessage;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async verifyAlternateIdentity({
 | 
			
		||||
    aci,
 | 
			
		||||
    pni,
 | 
			
		||||
    signature,
 | 
			
		||||
  }: VerifyAlternateIdentityOptionsType): Promise<boolean> {
 | 
			
		||||
    const logId = `SignalProtocolStore.verifyAlternateIdentity(${aci}, ${pni})`;
 | 
			
		||||
    const aciPublicKeyBytes = await this.loadIdentityKey(aci);
 | 
			
		||||
    if (!aciPublicKeyBytes) {
 | 
			
		||||
      log.warn(`${logId}: no ACI public key`);
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const pniPublicKeyBytes = await this.loadIdentityKey(pni);
 | 
			
		||||
    if (!pniPublicKeyBytes) {
 | 
			
		||||
      log.warn(`${logId}: no PNI public key`);
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const aciPublicKey = PublicKey.deserialize(Buffer.from(aciPublicKeyBytes));
 | 
			
		||||
    const pniPublicKey = PublicKey.deserialize(Buffer.from(pniPublicKeyBytes));
 | 
			
		||||
 | 
			
		||||
    return pniPublicKey.verifyAlternateIdentity(
 | 
			
		||||
      aciPublicKey,
 | 
			
		||||
      Buffer.from(signature)
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _getAllSessions(): Array<SessionCacheEntry> {
 | 
			
		||||
    const union = new Map<string, SessionCacheEntry>();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -55,7 +55,7 @@ import { RoutineProfileRefresher } from './routineProfileRefresh';
 | 
			
		|||
import { isMoreRecentThan, isOlderThan, toDayMillis } from './util/timestamp';
 | 
			
		||||
import { isValidReactionEmoji } from './reactions/isValidReactionEmoji';
 | 
			
		||||
import type { ConversationModel } from './models/conversations';
 | 
			
		||||
import { getContact } from './messages/helpers';
 | 
			
		||||
import { getContact, isIncoming } from './messages/helpers';
 | 
			
		||||
import { migrateMessageData } from './messages/migrateMessageData';
 | 
			
		||||
import { createBatcher } from './util/batcher';
 | 
			
		||||
import { updateConversationsWithUuidLookup } from './updateConversationsWithUuidLookup';
 | 
			
		||||
| 
						 | 
				
			
			@ -102,7 +102,6 @@ import { BackOff, FIBONACCI_TIMEOUTS } from './util/BackOff';
 | 
			
		|||
import { AppViewType } from './state/ducks/app';
 | 
			
		||||
import type { BadgesStateType } from './state/ducks/badges';
 | 
			
		||||
import { badgeImageFileDownloader } from './badges/badgeImageFileDownloader';
 | 
			
		||||
import { isIncoming } from './state/selectors/message';
 | 
			
		||||
import { actionCreators } from './state/actions';
 | 
			
		||||
import { Deletes } from './messageModifiers/Deletes';
 | 
			
		||||
import {
 | 
			
		||||
| 
						 | 
				
			
			@ -2948,7 +2947,9 @@ export async function startApp(): Promise<void> {
 | 
			
		|||
 | 
			
		||||
    const messageDescriptor = getMessageDescriptor({
 | 
			
		||||
      confirm,
 | 
			
		||||
      ...data,
 | 
			
		||||
      message: data.message,
 | 
			
		||||
      source: data.source,
 | 
			
		||||
      sourceUuid: data.sourceUuid,
 | 
			
		||||
      // 'message' event: for 1:1 converations, the conversation is same as sender
 | 
			
		||||
      destination: data.source,
 | 
			
		||||
      destinationUuid: data.sourceUuid,
 | 
			
		||||
| 
						 | 
				
			
			@ -2967,19 +2968,28 @@ export async function startApp(): Promise<void> {
 | 
			
		|||
 | 
			
		||||
    const message = initIncomingMessage(data, messageDescriptor);
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
      isIncoming(message.attributes) &&
 | 
			
		||||
      !message.get('unidentifiedDeliveryReceived')
 | 
			
		||||
    ) {
 | 
			
		||||
    if (isIncoming(message.attributes)) {
 | 
			
		||||
      const sender = getContact(message.attributes);
 | 
			
		||||
      strictAssert(sender, 'MessageModel has no sender');
 | 
			
		||||
 | 
			
		||||
      if (!sender) {
 | 
			
		||||
        throw new Error('MessageModel has no sender.');
 | 
			
		||||
      const uuidKind = window.textsecure.storage.user.getOurUuidKind(
 | 
			
		||||
        new UUID(data.destinationUuid)
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      if (uuidKind === UUIDKind.PNI && !sender.get('shareMyPhoneNumber')) {
 | 
			
		||||
        log.info(
 | 
			
		||||
          'onMessageReceived: setting shareMyPhoneNumber ' +
 | 
			
		||||
            `for ${sender.idForLogging()}`
 | 
			
		||||
        );
 | 
			
		||||
        sender.set({ shareMyPhoneNumber: true });
 | 
			
		||||
        window.Signal.Data.updateConversation(sender.attributes);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      profileKeyResponseQueue.add(() => {
 | 
			
		||||
        respondWithProfileKeyBatcher.add(sender);
 | 
			
		||||
      });
 | 
			
		||||
      if (!message.get('unidentifiedDeliveryReceived')) {
 | 
			
		||||
        profileKeyResponseQueue.add(() => {
 | 
			
		||||
          respondWithProfileKeyBatcher.add(sender);
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (data.message.reaction) {
 | 
			
		||||
| 
						 | 
				
			
			@ -3731,8 +3741,14 @@ export async function startApp(): Promise<void> {
 | 
			
		|||
    logTitle: string;
 | 
			
		||||
    type: MessageReceiptType.Read | MessageReceiptType.View;
 | 
			
		||||
  }>): void {
 | 
			
		||||
    const { envelopeTimestamp, timestamp, source, sourceUuid, sourceDevice } =
 | 
			
		||||
      event.receipt;
 | 
			
		||||
    const {
 | 
			
		||||
      envelopeTimestamp,
 | 
			
		||||
      timestamp,
 | 
			
		||||
      source,
 | 
			
		||||
      sourceUuid,
 | 
			
		||||
      sourceDevice,
 | 
			
		||||
      wasSentEncrypted,
 | 
			
		||||
    } = event.receipt;
 | 
			
		||||
    const sourceConversation = window.ConversationController.maybeMergeContacts(
 | 
			
		||||
      {
 | 
			
		||||
        aci: sourceUuid,
 | 
			
		||||
| 
						 | 
				
			
			@ -3770,6 +3786,7 @@ export async function startApp(): Promise<void> {
 | 
			
		|||
      sourceUuid,
 | 
			
		||||
      sourceDevice,
 | 
			
		||||
      type,
 | 
			
		||||
      wasSentEncrypted,
 | 
			
		||||
    };
 | 
			
		||||
    const receipt = MessageReceipts.getSingleton().add(attributes);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -3856,8 +3873,14 @@ export async function startApp(): Promise<void> {
 | 
			
		|||
 | 
			
		||||
  function onDeliveryReceipt(ev: DeliveryEvent) {
 | 
			
		||||
    const { deliveryReceipt } = ev;
 | 
			
		||||
    const { envelopeTimestamp, sourceUuid, source, sourceDevice, timestamp } =
 | 
			
		||||
      deliveryReceipt;
 | 
			
		||||
    const {
 | 
			
		||||
      envelopeTimestamp,
 | 
			
		||||
      sourceUuid,
 | 
			
		||||
      source,
 | 
			
		||||
      sourceDevice,
 | 
			
		||||
      timestamp,
 | 
			
		||||
      wasSentEncrypted,
 | 
			
		||||
    } = deliveryReceipt;
 | 
			
		||||
 | 
			
		||||
    ev.confirm();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -3902,6 +3925,7 @@ export async function startApp(): Promise<void> {
 | 
			
		|||
      sourceUuid,
 | 
			
		||||
      sourceDevice,
 | 
			
		||||
      type: MessageReceiptType.Delivery,
 | 
			
		||||
      wasSentEncrypted,
 | 
			
		||||
    };
 | 
			
		||||
    const receipt = MessageReceipts.getSingleton().add(attributes);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -188,6 +188,7 @@ export async function sendDeleteForEveryone(
 | 
			
		|||
                profileKey,
 | 
			
		||||
                options: sendOptions,
 | 
			
		||||
                urgent: true,
 | 
			
		||||
                includePniSignatureMessage: true,
 | 
			
		||||
              }),
 | 
			
		||||
            sendType,
 | 
			
		||||
            timestamp,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -82,6 +82,7 @@ export async function sendDirectExpirationTimerUpdate(
 | 
			
		|||
    profileKey,
 | 
			
		||||
    recipients: conversation.getRecipients(),
 | 
			
		||||
    timestamp,
 | 
			
		||||
    includePniSignatureMessage: true,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  if (!proto.dataMessage) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -283,6 +283,7 @@ export async function sendNormalMessage(
 | 
			
		|||
          storyContext,
 | 
			
		||||
          timestamp: messageTimestamp,
 | 
			
		||||
          urgent: true,
 | 
			
		||||
          includePniSignatureMessage: true,
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -129,6 +129,7 @@ export async function sendProfileKey(
 | 
			
		|||
      profileKey,
 | 
			
		||||
      recipients: conversation.getRecipients(),
 | 
			
		||||
      timestamp,
 | 
			
		||||
      includePniSignatureMessage: true,
 | 
			
		||||
    });
 | 
			
		||||
    sendPromise = messaging.sendIndividualProto({
 | 
			
		||||
      contentHint,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -240,6 +240,7 @@ export async function sendReaction(
 | 
			
		|||
              }
 | 
			
		||||
            : undefined,
 | 
			
		||||
          urgent: true,
 | 
			
		||||
          includePniSignatureMessage: true,
 | 
			
		||||
        });
 | 
			
		||||
      } else {
 | 
			
		||||
        log.info('sending group reaction message');
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -39,6 +39,7 @@ export type MessageReceiptAttributesType = {
 | 
			
		|||
  sourceConversationId: string;
 | 
			
		||||
  sourceDevice: number;
 | 
			
		||||
  type: MessageReceiptType;
 | 
			
		||||
  wasSentEncrypted: boolean;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class MessageReceiptModel extends Model<MessageReceiptAttributesType> {}
 | 
			
		||||
| 
						 | 
				
			
			@ -53,7 +54,25 @@ const deleteSentProtoBatcher = createWaitBatcher({
 | 
			
		|||
    log.info(
 | 
			
		||||
      `MessageReceipts: Batching ${items.length} sent proto recipients deletes`
 | 
			
		||||
    );
 | 
			
		||||
    await deleteSentProtoRecipient(items);
 | 
			
		||||
    const { successfulPhoneNumberShares } = await deleteSentProtoRecipient(
 | 
			
		||||
      items
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    for (const uuid of successfulPhoneNumberShares) {
 | 
			
		||||
      const convo = window.ConversationController.get(uuid);
 | 
			
		||||
      if (!convo) {
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      log.info(
 | 
			
		||||
        'MessageReceipts: unsetting shareMyPhoneNumber ' +
 | 
			
		||||
          `for ${convo.idForLogging()}`
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      // `deleteSentProtoRecipient` has already updated the database so there
 | 
			
		||||
      // is no need in calling `updateConversation`
 | 
			
		||||
      convo.unset('shareMyPhoneNumber');
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -193,7 +212,8 @@ export class MessageReceipts extends Collection<MessageReceiptModel> {
 | 
			
		|||
 | 
			
		||||
    if (
 | 
			
		||||
      (type === MessageReceiptType.Delivery &&
 | 
			
		||||
        wasDeliveredWithSealedSender(sourceConversationId, message)) ||
 | 
			
		||||
        wasDeliveredWithSealedSender(sourceConversationId, message) &&
 | 
			
		||||
        receipt.get('wasSentEncrypted')) ||
 | 
			
		||||
      type === MessageReceiptType.Read
 | 
			
		||||
    ) {
 | 
			
		||||
      const recipient = window.ConversationController.get(sourceConversationId);
 | 
			
		||||
| 
						 | 
				
			
			@ -201,11 +221,17 @@ export class MessageReceipts extends Collection<MessageReceiptModel> {
 | 
			
		|||
      const deviceId = receipt.get('sourceDevice');
 | 
			
		||||
 | 
			
		||||
      if (recipientUuid && deviceId) {
 | 
			
		||||
        await deleteSentProtoBatcher.add({
 | 
			
		||||
          timestamp: messageSentAt,
 | 
			
		||||
          recipientUuid,
 | 
			
		||||
          deviceId,
 | 
			
		||||
        });
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
          deleteSentProtoBatcher.add({
 | 
			
		||||
            timestamp: messageSentAt,
 | 
			
		||||
            recipientUuid,
 | 
			
		||||
            deviceId,
 | 
			
		||||
          }),
 | 
			
		||||
 | 
			
		||||
          // We want the above call to not be delayed when testing with
 | 
			
		||||
          // CI.
 | 
			
		||||
          window.CI ? deleteSentProtoBatcher.flushAndWait() : Promise.resolve(),
 | 
			
		||||
        ]);
 | 
			
		||||
      } else {
 | 
			
		||||
        log.warn(
 | 
			
		||||
          `MessageReceipts.onReceipt: Missing uuid or deviceId for deliveredTo ${sourceConversationId}`
 | 
			
		||||
| 
						 | 
				
			
			@ -249,6 +275,7 @@ export class MessageReceipts extends Collection<MessageReceiptModel> {
 | 
			
		|||
            'No message for receipt',
 | 
			
		||||
            type,
 | 
			
		||||
            sourceConversationId,
 | 
			
		||||
            sourceUuid,
 | 
			
		||||
            messageSentAt
 | 
			
		||||
          );
 | 
			
		||||
          return;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										1
									
								
								ts/model-types.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								ts/model-types.d.ts
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -335,6 +335,7 @@ export type ConversationAttributesType = {
 | 
			
		|||
  profileLastFetchedAt?: number;
 | 
			
		||||
  pendingUniversalTimer?: string;
 | 
			
		||||
  username?: string;
 | 
			
		||||
  shareMyPhoneNumber?: boolean;
 | 
			
		||||
 | 
			
		||||
  // Group-only
 | 
			
		||||
  groupId?: string;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -34,7 +34,10 @@ import type {
 | 
			
		|||
} from '../textsecure/SendMessage';
 | 
			
		||||
import createTaskWithTimeout from '../textsecure/TaskWithTimeout';
 | 
			
		||||
import MessageSender from '../textsecure/SendMessage';
 | 
			
		||||
import type { CallbackResultType } from '../textsecure/Types.d';
 | 
			
		||||
import type {
 | 
			
		||||
  CallbackResultType,
 | 
			
		||||
  PniSignatureMessageType,
 | 
			
		||||
} from '../textsecure/Types.d';
 | 
			
		||||
import type { ConversationType } from '../state/ducks/conversations';
 | 
			
		||||
import type {
 | 
			
		||||
  AvatarColorType,
 | 
			
		||||
| 
						 | 
				
			
			@ -2023,6 +2026,7 @@ export class ConversationModel extends window.Backbone
 | 
			
		|||
            senderE164: m.source,
 | 
			
		||||
            senderUuid: m.sourceUuid,
 | 
			
		||||
            timestamp: m.sent_at,
 | 
			
		||||
            isDirectConversation: isDirectConversation(this.attributes),
 | 
			
		||||
          }))
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
| 
						 | 
				
			
			@ -5377,6 +5381,13 @@ export class ConversationModel extends window.Backbone
 | 
			
		|||
      );
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getPniSignatureMessage(): PniSignatureMessageType | undefined {
 | 
			
		||||
    if (!this.get('shareMyPhoneNumber')) {
 | 
			
		||||
      return undefined;
 | 
			
		||||
    }
 | 
			
		||||
    return window.textsecure.storage.protocol.signAlternateIdentity();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
window.Whisper.Conversation = ConversationModel;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2373,6 +2373,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
 | 
			
		|||
            senderE164: source,
 | 
			
		||||
            senderUuid: sourceUuid,
 | 
			
		||||
            timestamp: this.get('sent_at'),
 | 
			
		||||
            isDirectConversation: isDirectConversation(conversation.attributes),
 | 
			
		||||
          });
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -53,6 +53,7 @@ import type {
 | 
			
		|||
  ConversationType,
 | 
			
		||||
  ConversationMetricsType,
 | 
			
		||||
  DeleteSentProtoRecipientOptionsType,
 | 
			
		||||
  DeleteSentProtoRecipientResultType,
 | 
			
		||||
  EmojiType,
 | 
			
		||||
  GetUnreadByConversationAndMarkReadResultType,
 | 
			
		||||
  GetConversationRangeCenteredOnMessageResultType,
 | 
			
		||||
| 
						 | 
				
			
			@ -952,8 +953,8 @@ async function deleteSentProtoRecipient(
 | 
			
		|||
  options:
 | 
			
		||||
    | DeleteSentProtoRecipientOptionsType
 | 
			
		||||
    | ReadonlyArray<DeleteSentProtoRecipientOptionsType>
 | 
			
		||||
): Promise<void> {
 | 
			
		||||
  await channels.deleteSentProtoRecipient(options);
 | 
			
		||||
): Promise<DeleteSentProtoRecipientResultType> {
 | 
			
		||||
  return channels.deleteSentProtoRecipient(options);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function getSentProtoByRecipient(options: {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -119,6 +119,7 @@ export type SentProtoType = {
 | 
			
		|||
  proto: Uint8Array;
 | 
			
		||||
  timestamp: number;
 | 
			
		||||
  urgent: boolean;
 | 
			
		||||
  hasPniSignatureMessage: boolean;
 | 
			
		||||
};
 | 
			
		||||
export type SentProtoWithMessageIdsType = SentProtoType & {
 | 
			
		||||
  messageIds: Array<string>;
 | 
			
		||||
| 
						 | 
				
			
			@ -287,6 +288,10 @@ export type DeleteSentProtoRecipientOptionsType = Readonly<{
 | 
			
		|||
  deviceId: number;
 | 
			
		||||
}>;
 | 
			
		||||
 | 
			
		||||
export type DeleteSentProtoRecipientResultType = Readonly<{
 | 
			
		||||
  successfulPhoneNumberShares: ReadonlyArray<string>;
 | 
			
		||||
}>;
 | 
			
		||||
 | 
			
		||||
export type StoryDistributionType = Readonly<{
 | 
			
		||||
  id: UUIDStringType;
 | 
			
		||||
  name: string;
 | 
			
		||||
| 
						 | 
				
			
			@ -381,7 +386,7 @@ export type DataInterface = {
 | 
			
		|||
    options:
 | 
			
		||||
      | DeleteSentProtoRecipientOptionsType
 | 
			
		||||
      | ReadonlyArray<DeleteSentProtoRecipientOptionsType>
 | 
			
		||||
  ) => Promise<void>;
 | 
			
		||||
  ) => Promise<DeleteSentProtoRecipientResultType>;
 | 
			
		||||
  getSentProtoByRecipient: (options: {
 | 
			
		||||
    now: number;
 | 
			
		||||
    recipientUuid: string;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -76,6 +76,7 @@ import type {
 | 
			
		|||
  ConversationMetricsType,
 | 
			
		||||
  ConversationType,
 | 
			
		||||
  DeleteSentProtoRecipientOptionsType,
 | 
			
		||||
  DeleteSentProtoRecipientResultType,
 | 
			
		||||
  EmojiType,
 | 
			
		||||
  GetConversationRangeCenteredOnMessageResultType,
 | 
			
		||||
  GetUnreadByConversationAndMarkReadResultType,
 | 
			
		||||
| 
						 | 
				
			
			@ -855,17 +856,20 @@ async function insertSentProto(
 | 
			
		|||
        contentHint,
 | 
			
		||||
        proto,
 | 
			
		||||
        timestamp,
 | 
			
		||||
        urgent
 | 
			
		||||
        urgent,
 | 
			
		||||
        hasPniSignatureMessage
 | 
			
		||||
      ) VALUES (
 | 
			
		||||
        $contentHint,
 | 
			
		||||
        $proto,
 | 
			
		||||
        $timestamp,
 | 
			
		||||
        $urgent
 | 
			
		||||
        $urgent,
 | 
			
		||||
        $hasPniSignatureMessage
 | 
			
		||||
      );
 | 
			
		||||
      `
 | 
			
		||||
    ).run({
 | 
			
		||||
      ...proto,
 | 
			
		||||
      urgent: proto.urgent ? 1 : 0,
 | 
			
		||||
      hasPniSignatureMessage: proto.hasPniSignatureMessage ? 1 : 0,
 | 
			
		||||
    });
 | 
			
		||||
    const id = parseIntOrThrow(
 | 
			
		||||
      info.lastInsertRowid,
 | 
			
		||||
| 
						 | 
				
			
			@ -999,7 +1003,7 @@ async function deleteSentProtoRecipient(
 | 
			
		|||
  options:
 | 
			
		||||
    | DeleteSentProtoRecipientOptionsType
 | 
			
		||||
    | ReadonlyArray<DeleteSentProtoRecipientOptionsType>
 | 
			
		||||
): Promise<void> {
 | 
			
		||||
): Promise<DeleteSentProtoRecipientResultType> {
 | 
			
		||||
  const db = getInstance();
 | 
			
		||||
 | 
			
		||||
  const items = Array.isArray(options) ? options : [options];
 | 
			
		||||
| 
						 | 
				
			
			@ -1007,7 +1011,9 @@ async function deleteSentProtoRecipient(
 | 
			
		|||
  // Note: we use `pluck` in this function to fetch only the first column of
 | 
			
		||||
  // returned row.
 | 
			
		||||
 | 
			
		||||
  db.transaction(() => {
 | 
			
		||||
  return db.transaction(() => {
 | 
			
		||||
    const successfulPhoneNumberShares = new Array<string>();
 | 
			
		||||
 | 
			
		||||
    for (const item of items) {
 | 
			
		||||
      const { timestamp, recipientUuid, deviceId } = item;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1015,7 +1021,8 @@ async function deleteSentProtoRecipient(
 | 
			
		|||
      const rows = prepare(
 | 
			
		||||
        db,
 | 
			
		||||
        `
 | 
			
		||||
        SELECT sendLogPayloads.id FROM sendLogPayloads
 | 
			
		||||
        SELECT sendLogPayloads.id, sendLogPayloads.hasPniSignatureMessage
 | 
			
		||||
        FROM sendLogPayloads
 | 
			
		||||
        INNER JOIN sendLogRecipients
 | 
			
		||||
          ON sendLogRecipients.payloadId = sendLogPayloads.id
 | 
			
		||||
        WHERE
 | 
			
		||||
| 
						 | 
				
			
			@ -1032,10 +1039,9 @@ async function deleteSentProtoRecipient(
 | 
			
		|||
          'deleteSentProtoRecipient: More than one payload matches ' +
 | 
			
		||||
            `recipient and timestamp ${timestamp}. Using the first.`
 | 
			
		||||
        );
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const { id } = rows[0];
 | 
			
		||||
      const { id, hasPniSignatureMessage } = rows[0];
 | 
			
		||||
 | 
			
		||||
      // 2. Delete the recipient/device combination in question.
 | 
			
		||||
      prepare(
 | 
			
		||||
| 
						 | 
				
			
			@ -1050,32 +1056,61 @@ async function deleteSentProtoRecipient(
 | 
			
		|||
      ).run({ id, recipientUuid, deviceId });
 | 
			
		||||
 | 
			
		||||
      // 3. See how many more recipient devices there were for this payload.
 | 
			
		||||
      const remaining = prepare(
 | 
			
		||||
      const remainingDevices = prepare(
 | 
			
		||||
        db,
 | 
			
		||||
        `
 | 
			
		||||
        SELECT count(*) FROM sendLogRecipients
 | 
			
		||||
        WHERE payloadId = $id AND recipientUuid = $recipientUuid;
 | 
			
		||||
        `
 | 
			
		||||
      )
 | 
			
		||||
        .pluck(true)
 | 
			
		||||
        .get({ id, recipientUuid });
 | 
			
		||||
 | 
			
		||||
      // 4. If there are no remaining devices for this recipient and we included
 | 
			
		||||
      //    the pni signature in the proto - return the recipient to the caller.
 | 
			
		||||
      if (remainingDevices === 0 && hasPniSignatureMessage) {
 | 
			
		||||
        logger.info(
 | 
			
		||||
          'deleteSentProtoRecipient: ' +
 | 
			
		||||
            `Successfully shared phone number with ${recipientUuid} ` +
 | 
			
		||||
            `through message ${timestamp}`
 | 
			
		||||
        );
 | 
			
		||||
        successfulPhoneNumberShares.push(recipientUuid);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      strictAssert(
 | 
			
		||||
        isNumber(remainingDevices),
 | 
			
		||||
        'deleteSentProtoRecipient: select count() returned non-number!'
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      // 5. See how many more recipients there were for this payload.
 | 
			
		||||
      const remainingTotal = prepare(
 | 
			
		||||
        db,
 | 
			
		||||
        'SELECT count(*) FROM sendLogRecipients WHERE payloadId = $id;'
 | 
			
		||||
      )
 | 
			
		||||
        .pluck(true)
 | 
			
		||||
        .get({ id });
 | 
			
		||||
 | 
			
		||||
      if (!isNumber(remaining)) {
 | 
			
		||||
        throw new Error(
 | 
			
		||||
          'deleteSentProtoRecipient: select count() returned non-number!'
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      strictAssert(
 | 
			
		||||
        isNumber(remainingTotal),
 | 
			
		||||
        'deleteSentProtoRecipient: select count() returned non-number!'
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      if (remaining > 0) {
 | 
			
		||||
      if (remainingTotal > 0) {
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // 4. Delete the entire payload if there are no more recipients left.
 | 
			
		||||
      // 6. Delete the entire payload if there are no more recipients left.
 | 
			
		||||
      logger.info(
 | 
			
		||||
        'deleteSentProtoRecipient: ' +
 | 
			
		||||
          `Deleting proto payload for timestamp ${timestamp}`
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      prepare(db, 'DELETE FROM sendLogPayloads WHERE id = $id;').run({
 | 
			
		||||
        id,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return { successfulPhoneNumberShares };
 | 
			
		||||
  })();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1122,6 +1157,9 @@ async function getSentProtoByRecipient({
 | 
			
		|||
  return {
 | 
			
		||||
    ...row,
 | 
			
		||||
    urgent: isNumber(row.urgent) ? Boolean(row.urgent) : true,
 | 
			
		||||
    hasPniSignatureMessage: isNumber(row.hasPniSignatureMessage)
 | 
			
		||||
      ? Boolean(row.hasPniSignatureMessage)
 | 
			
		||||
      : true,
 | 
			
		||||
    messageIds: messageIds ? messageIds.split(',') : [],
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1136,6 +1174,9 @@ async function getAllSentProtos(): Promise<Array<SentProtoType>> {
 | 
			
		|||
  return rows.map(row => ({
 | 
			
		||||
    ...row,
 | 
			
		||||
    urgent: isNumber(row.urgent) ? Boolean(row.urgent) : true,
 | 
			
		||||
    hasPniSignatureMessage: isNumber(row.hasPniSignatureMessage)
 | 
			
		||||
      ? Boolean(row.hasPniSignatureMessage)
 | 
			
		||||
      : true,
 | 
			
		||||
  }));
 | 
			
		||||
}
 | 
			
		||||
async function _getAllSentProtoRecipients(): Promise<
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										29
									
								
								ts/sql/migrations/66-add-pni-signature-to-sent-protos.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								ts/sql/migrations/66-add-pni-signature-to-sent-protos.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,29 @@
 | 
			
		|||
// Copyright 2022 Signal Messenger, LLC
 | 
			
		||||
// SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
import type { Database } from 'better-sqlite3';
 | 
			
		||||
 | 
			
		||||
import type { LoggerType } from '../../types/Logging';
 | 
			
		||||
 | 
			
		||||
export default function updateToSchemaVersion66(
 | 
			
		||||
  currentVersion: number,
 | 
			
		||||
  db: Database,
 | 
			
		||||
  logger: LoggerType
 | 
			
		||||
): void {
 | 
			
		||||
  if (currentVersion >= 66) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  db.transaction(() => {
 | 
			
		||||
    db.exec(
 | 
			
		||||
      `
 | 
			
		||||
      ALTER TABLE sendLogPayloads
 | 
			
		||||
      ADD COLUMN hasPniSignatureMessage INTEGER DEFAULT 0 NOT NULL;
 | 
			
		||||
      `
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    db.pragma('user_version = 66');
 | 
			
		||||
  })();
 | 
			
		||||
 | 
			
		||||
  logger.info('updateToSchemaVersion66: success!');
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -41,6 +41,7 @@ import updateToSchemaVersion62 from './62-add-urgent-to-send-log';
 | 
			
		|||
import updateToSchemaVersion63 from './63-add-urgent-to-unprocessed';
 | 
			
		||||
import updateToSchemaVersion64 from './64-uuid-column-for-pre-keys';
 | 
			
		||||
import updateToSchemaVersion65 from './65-add-storage-id-to-stickers';
 | 
			
		||||
import updateToSchemaVersion66 from './66-add-pni-signature-to-sent-protos';
 | 
			
		||||
 | 
			
		||||
function updateToSchemaVersion1(
 | 
			
		||||
  currentVersion: number,
 | 
			
		||||
| 
						 | 
				
			
			@ -1945,6 +1946,7 @@ export const SCHEMA_VERSIONS = [
 | 
			
		|||
  updateToSchemaVersion63,
 | 
			
		||||
  updateToSchemaVersion64,
 | 
			
		||||
  updateToSchemaVersion65,
 | 
			
		||||
  updateToSchemaVersion66,
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
export function updateSchema(db: Database, logger: LoggerType): void {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -410,6 +410,7 @@ function markStoryRead(
 | 
			
		|||
      senderE164: message.attributes.source,
 | 
			
		||||
      senderUuid: message.attributes.sourceUuid,
 | 
			
		||||
      timestamp: message.attributes.sent_at,
 | 
			
		||||
      isDirectConversation: false,
 | 
			
		||||
    };
 | 
			
		||||
    const viewSyncs: Array<SyncType> = [viewedReceipt];
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -166,7 +166,7 @@ describe('SignalProtocolStore', () => {
 | 
			
		|||
  describe('getIdentityKeyPair', () => {
 | 
			
		||||
    it('retrieves my identity key', async () => {
 | 
			
		||||
      await store.hydrateCaches();
 | 
			
		||||
      const key = await store.getIdentityKeyPair(ourUuid);
 | 
			
		||||
      const key = store.getIdentityKeyPair(ourUuid);
 | 
			
		||||
      if (!key) {
 | 
			
		||||
        throw new Error('Missing key!');
 | 
			
		||||
      }
 | 
			
		||||
| 
						 | 
				
			
			@ -1810,13 +1810,13 @@ describe('SignalProtocolStore', () => {
 | 
			
		|||
      });
 | 
			
		||||
 | 
			
		||||
      // Old data has to be removed
 | 
			
		||||
      assert.isUndefined(await store.getIdentityKeyPair(oldPni));
 | 
			
		||||
      assert.isUndefined(store.getIdentityKeyPair(oldPni));
 | 
			
		||||
      assert.isUndefined(await store.getLocalRegistrationId(oldPni));
 | 
			
		||||
      assert.isUndefined(await store.loadPreKey(oldPni, 2));
 | 
			
		||||
      assert.isUndefined(await store.loadSignedPreKey(oldPni, 3));
 | 
			
		||||
 | 
			
		||||
      // New data has to be added
 | 
			
		||||
      const storedIdentity = await store.getIdentityKeyPair(newPni);
 | 
			
		||||
      const storedIdentity = store.getIdentityKeyPair(newPni);
 | 
			
		||||
      if (!storedIdentity) {
 | 
			
		||||
        throw new Error('New identity not found');
 | 
			
		||||
      }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -40,6 +40,7 @@ describe('sql/sendLog', () => {
 | 
			
		|||
      proto: bytes,
 | 
			
		||||
      timestamp,
 | 
			
		||||
      urgent: false,
 | 
			
		||||
      hasPniSignatureMessage: false,
 | 
			
		||||
    };
 | 
			
		||||
    await insertSentProto(proto, {
 | 
			
		||||
      messageIds: [getUuid()],
 | 
			
		||||
| 
						 | 
				
			
			@ -56,6 +57,10 @@ describe('sql/sendLog', () => {
 | 
			
		|||
    assert.isTrue(constantTimeEqual(actual.proto, proto.proto));
 | 
			
		||||
    assert.strictEqual(actual.timestamp, proto.timestamp);
 | 
			
		||||
    assert.strictEqual(actual.urgent, proto.urgent);
 | 
			
		||||
    assert.strictEqual(
 | 
			
		||||
      actual.hasPniSignatureMessage,
 | 
			
		||||
      proto.hasPniSignatureMessage
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    await removeAllSentProtos();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -74,6 +79,7 @@ describe('sql/sendLog', () => {
 | 
			
		|||
      proto: bytes,
 | 
			
		||||
      timestamp,
 | 
			
		||||
      urgent: true,
 | 
			
		||||
      hasPniSignatureMessage: true,
 | 
			
		||||
    };
 | 
			
		||||
    await insertSentProto(proto, {
 | 
			
		||||
      messageIds: [getUuid(), getUuid()],
 | 
			
		||||
| 
						 | 
				
			
			@ -91,6 +97,10 @@ describe('sql/sendLog', () => {
 | 
			
		|||
    assert.isTrue(constantTimeEqual(actual.proto, proto.proto));
 | 
			
		||||
    assert.strictEqual(actual.timestamp, proto.timestamp);
 | 
			
		||||
    assert.strictEqual(actual.urgent, proto.urgent);
 | 
			
		||||
    assert.strictEqual(
 | 
			
		||||
      actual.hasPniSignatureMessage,
 | 
			
		||||
      proto.hasPniSignatureMessage
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    assert.lengthOf(await _getAllSentProtoMessageIds(), 2);
 | 
			
		||||
    assert.lengthOf(await _getAllSentProtoRecipients(), 3);
 | 
			
		||||
| 
						 | 
				
			
			@ -127,6 +137,7 @@ describe('sql/sendLog', () => {
 | 
			
		|||
      proto: bytes,
 | 
			
		||||
      timestamp,
 | 
			
		||||
      urgent: false,
 | 
			
		||||
      hasPniSignatureMessage: false,
 | 
			
		||||
    };
 | 
			
		||||
    await insertSentProto(proto, {
 | 
			
		||||
      messageIds: [id],
 | 
			
		||||
| 
						 | 
				
			
			@ -159,12 +170,14 @@ describe('sql/sendLog', () => {
 | 
			
		|||
        proto: getRandomBytes(128),
 | 
			
		||||
        timestamp,
 | 
			
		||||
        urgent: true,
 | 
			
		||||
        hasPniSignatureMessage: false,
 | 
			
		||||
      };
 | 
			
		||||
      const proto2 = {
 | 
			
		||||
        contentHint: 9,
 | 
			
		||||
        proto: getRandomBytes(128),
 | 
			
		||||
        timestamp,
 | 
			
		||||
        urgent: false,
 | 
			
		||||
        hasPniSignatureMessage: true,
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      assert.lengthOf(await getAllSentProtos(), 0);
 | 
			
		||||
| 
						 | 
				
			
			@ -195,6 +208,7 @@ describe('sql/sendLog', () => {
 | 
			
		|||
        proto: getRandomBytes(128),
 | 
			
		||||
        timestamp,
 | 
			
		||||
        urgent: true,
 | 
			
		||||
        hasPniSignatureMessage: false,
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      assert.lengthOf(await getAllSentProtos(), 0);
 | 
			
		||||
| 
						 | 
				
			
			@ -234,18 +248,21 @@ describe('sql/sendLog', () => {
 | 
			
		|||
        proto: getRandomBytes(128),
 | 
			
		||||
        timestamp: timestamp + 10,
 | 
			
		||||
        urgent: true,
 | 
			
		||||
        hasPniSignatureMessage: false,
 | 
			
		||||
      };
 | 
			
		||||
      const proto2 = {
 | 
			
		||||
        contentHint: 2,
 | 
			
		||||
        proto: getRandomBytes(128),
 | 
			
		||||
        timestamp,
 | 
			
		||||
        urgent: true,
 | 
			
		||||
        hasPniSignatureMessage: false,
 | 
			
		||||
      };
 | 
			
		||||
      const proto3 = {
 | 
			
		||||
        contentHint: 0,
 | 
			
		||||
        proto: getRandomBytes(128),
 | 
			
		||||
        timestamp: timestamp - 15,
 | 
			
		||||
        urgent: true,
 | 
			
		||||
        hasPniSignatureMessage: false,
 | 
			
		||||
      };
 | 
			
		||||
      await insertSentProto(proto1, {
 | 
			
		||||
        messageIds: [getUuid()],
 | 
			
		||||
| 
						 | 
				
			
			@ -298,18 +315,21 @@ describe('sql/sendLog', () => {
 | 
			
		|||
        proto: getRandomBytes(128),
 | 
			
		||||
        timestamp,
 | 
			
		||||
        urgent: true,
 | 
			
		||||
        hasPniSignatureMessage: false,
 | 
			
		||||
      };
 | 
			
		||||
      const proto2 = {
 | 
			
		||||
        contentHint: 1,
 | 
			
		||||
        proto: getRandomBytes(128),
 | 
			
		||||
        timestamp: timestamp - 10,
 | 
			
		||||
        urgent: true,
 | 
			
		||||
        hasPniSignatureMessage: false,
 | 
			
		||||
      };
 | 
			
		||||
      const proto3 = {
 | 
			
		||||
        contentHint: 1,
 | 
			
		||||
        proto: getRandomBytes(128),
 | 
			
		||||
        timestamp: timestamp - 20,
 | 
			
		||||
        urgent: true,
 | 
			
		||||
        hasPniSignatureMessage: false,
 | 
			
		||||
      };
 | 
			
		||||
      await insertSentProto(proto1, {
 | 
			
		||||
        messageIds: [messageId, getUuid()],
 | 
			
		||||
| 
						 | 
				
			
			@ -354,6 +374,7 @@ describe('sql/sendLog', () => {
 | 
			
		|||
        proto: getRandomBytes(128),
 | 
			
		||||
        timestamp,
 | 
			
		||||
        urgent: true,
 | 
			
		||||
        hasPniSignatureMessage: false,
 | 
			
		||||
      };
 | 
			
		||||
      await insertSentProto(proto, {
 | 
			
		||||
        messageIds: [getUuid()],
 | 
			
		||||
| 
						 | 
				
			
			@ -366,11 +387,12 @@ describe('sql/sendLog', () => {
 | 
			
		|||
      assert.lengthOf(await getAllSentProtos(), 1);
 | 
			
		||||
      assert.lengthOf(await _getAllSentProtoRecipients(), 3);
 | 
			
		||||
 | 
			
		||||
      await deleteSentProtoRecipient({
 | 
			
		||||
      const { successfulPhoneNumberShares } = await deleteSentProtoRecipient({
 | 
			
		||||
        timestamp,
 | 
			
		||||
        recipientUuid: recipientUuid1,
 | 
			
		||||
        deviceId: 1,
 | 
			
		||||
      });
 | 
			
		||||
      assert.lengthOf(successfulPhoneNumberShares, 0);
 | 
			
		||||
 | 
			
		||||
      assert.lengthOf(await getAllSentProtos(), 1);
 | 
			
		||||
      assert.lengthOf(await _getAllSentProtoRecipients(), 2);
 | 
			
		||||
| 
						 | 
				
			
			@ -386,6 +408,7 @@ describe('sql/sendLog', () => {
 | 
			
		|||
        proto: getRandomBytes(128),
 | 
			
		||||
        timestamp,
 | 
			
		||||
        urgent: true,
 | 
			
		||||
        hasPniSignatureMessage: false,
 | 
			
		||||
      };
 | 
			
		||||
      await insertSentProto(proto, {
 | 
			
		||||
        messageIds: [getUuid()],
 | 
			
		||||
| 
						 | 
				
			
			@ -398,30 +421,99 @@ describe('sql/sendLog', () => {
 | 
			
		|||
      assert.lengthOf(await getAllSentProtos(), 1);
 | 
			
		||||
      assert.lengthOf(await _getAllSentProtoRecipients(), 3);
 | 
			
		||||
 | 
			
		||||
      await deleteSentProtoRecipient({
 | 
			
		||||
        timestamp,
 | 
			
		||||
        recipientUuid: recipientUuid1,
 | 
			
		||||
        deviceId: 1,
 | 
			
		||||
      });
 | 
			
		||||
      {
 | 
			
		||||
        const { successfulPhoneNumberShares } = await deleteSentProtoRecipient({
 | 
			
		||||
          timestamp,
 | 
			
		||||
          recipientUuid: recipientUuid1,
 | 
			
		||||
          deviceId: 1,
 | 
			
		||||
        });
 | 
			
		||||
        assert.lengthOf(successfulPhoneNumberShares, 0);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      assert.lengthOf(await getAllSentProtos(), 1);
 | 
			
		||||
      assert.lengthOf(await _getAllSentProtoRecipients(), 2);
 | 
			
		||||
 | 
			
		||||
      await deleteSentProtoRecipient({
 | 
			
		||||
        timestamp,
 | 
			
		||||
        recipientUuid: recipientUuid1,
 | 
			
		||||
        deviceId: 2,
 | 
			
		||||
      });
 | 
			
		||||
      {
 | 
			
		||||
        const { successfulPhoneNumberShares } = await deleteSentProtoRecipient({
 | 
			
		||||
          timestamp,
 | 
			
		||||
          recipientUuid: recipientUuid1,
 | 
			
		||||
          deviceId: 2,
 | 
			
		||||
        });
 | 
			
		||||
        assert.lengthOf(successfulPhoneNumberShares, 0);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      assert.lengthOf(await getAllSentProtos(), 1);
 | 
			
		||||
      assert.lengthOf(await _getAllSentProtoRecipients(), 1);
 | 
			
		||||
 | 
			
		||||
      await deleteSentProtoRecipient({
 | 
			
		||||
      {
 | 
			
		||||
        const { successfulPhoneNumberShares } = await deleteSentProtoRecipient({
 | 
			
		||||
          timestamp,
 | 
			
		||||
          recipientUuid: recipientUuid2,
 | 
			
		||||
          deviceId: 1,
 | 
			
		||||
        });
 | 
			
		||||
        assert.lengthOf(successfulPhoneNumberShares, 0);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      assert.lengthOf(await getAllSentProtos(), 0);
 | 
			
		||||
      assert.lengthOf(await _getAllSentProtoRecipients(), 0);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('returns deleted recipients when pni signature was sent', async () => {
 | 
			
		||||
      const timestamp = Date.now();
 | 
			
		||||
 | 
			
		||||
      const recipientUuid1 = getUuid();
 | 
			
		||||
      const recipientUuid2 = getUuid();
 | 
			
		||||
      const proto = {
 | 
			
		||||
        contentHint: 1,
 | 
			
		||||
        proto: getRandomBytes(128),
 | 
			
		||||
        timestamp,
 | 
			
		||||
        recipientUuid: recipientUuid2,
 | 
			
		||||
        deviceId: 1,
 | 
			
		||||
        urgent: true,
 | 
			
		||||
        hasPniSignatureMessage: true,
 | 
			
		||||
      };
 | 
			
		||||
      await insertSentProto(proto, {
 | 
			
		||||
        messageIds: [getUuid()],
 | 
			
		||||
        recipients: {
 | 
			
		||||
          [recipientUuid1]: [1, 2],
 | 
			
		||||
          [recipientUuid2]: [1],
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      assert.lengthOf(await getAllSentProtos(), 1);
 | 
			
		||||
      assert.lengthOf(await _getAllSentProtoRecipients(), 3);
 | 
			
		||||
 | 
			
		||||
      {
 | 
			
		||||
        const { successfulPhoneNumberShares } = await deleteSentProtoRecipient({
 | 
			
		||||
          timestamp,
 | 
			
		||||
          recipientUuid: recipientUuid1,
 | 
			
		||||
          deviceId: 1,
 | 
			
		||||
        });
 | 
			
		||||
        assert.lengthOf(successfulPhoneNumberShares, 0);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      assert.lengthOf(await getAllSentProtos(), 1);
 | 
			
		||||
      assert.lengthOf(await _getAllSentProtoRecipients(), 2);
 | 
			
		||||
 | 
			
		||||
      {
 | 
			
		||||
        const { successfulPhoneNumberShares } = await deleteSentProtoRecipient({
 | 
			
		||||
          timestamp,
 | 
			
		||||
          recipientUuid: recipientUuid1,
 | 
			
		||||
          deviceId: 2,
 | 
			
		||||
        });
 | 
			
		||||
        assert.deepStrictEqual(successfulPhoneNumberShares, [recipientUuid1]);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      assert.lengthOf(await getAllSentProtos(), 1);
 | 
			
		||||
      assert.lengthOf(await _getAllSentProtoRecipients(), 1);
 | 
			
		||||
 | 
			
		||||
      {
 | 
			
		||||
        const { successfulPhoneNumberShares } = await deleteSentProtoRecipient({
 | 
			
		||||
          timestamp,
 | 
			
		||||
          recipientUuid: recipientUuid2,
 | 
			
		||||
          deviceId: 1,
 | 
			
		||||
        });
 | 
			
		||||
        assert.deepStrictEqual(successfulPhoneNumberShares, [recipientUuid2]);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      assert.lengthOf(await getAllSentProtos(), 0);
 | 
			
		||||
      assert.lengthOf(await _getAllSentProtoRecipients(), 0);
 | 
			
		||||
    });
 | 
			
		||||
| 
						 | 
				
			
			@ -436,6 +528,7 @@ describe('sql/sendLog', () => {
 | 
			
		|||
        proto: getRandomBytes(128),
 | 
			
		||||
        timestamp,
 | 
			
		||||
        urgent: true,
 | 
			
		||||
        hasPniSignatureMessage: false,
 | 
			
		||||
      };
 | 
			
		||||
      await insertSentProto(proto, {
 | 
			
		||||
        messageIds: [getUuid()],
 | 
			
		||||
| 
						 | 
				
			
			@ -448,7 +541,7 @@ describe('sql/sendLog', () => {
 | 
			
		|||
      assert.lengthOf(await getAllSentProtos(), 1);
 | 
			
		||||
      assert.lengthOf(await _getAllSentProtoRecipients(), 3);
 | 
			
		||||
 | 
			
		||||
      await deleteSentProtoRecipient([
 | 
			
		||||
      const { successfulPhoneNumberShares } = await deleteSentProtoRecipient([
 | 
			
		||||
        {
 | 
			
		||||
          timestamp,
 | 
			
		||||
          recipientUuid: recipientUuid1,
 | 
			
		||||
| 
						 | 
				
			
			@ -465,6 +558,7 @@ describe('sql/sendLog', () => {
 | 
			
		|||
          deviceId: 1,
 | 
			
		||||
        },
 | 
			
		||||
      ]);
 | 
			
		||||
      assert.lengthOf(successfulPhoneNumberShares, 0);
 | 
			
		||||
 | 
			
		||||
      assert.lengthOf(await getAllSentProtos(), 0);
 | 
			
		||||
      assert.lengthOf(await _getAllSentProtoRecipients(), 0);
 | 
			
		||||
| 
						 | 
				
			
			@ -482,6 +576,7 @@ describe('sql/sendLog', () => {
 | 
			
		|||
        proto: getRandomBytes(128),
 | 
			
		||||
        timestamp,
 | 
			
		||||
        urgent: true,
 | 
			
		||||
        hasPniSignatureMessage: false,
 | 
			
		||||
      };
 | 
			
		||||
      await insertSentProto(proto, {
 | 
			
		||||
        messageIds,
 | 
			
		||||
| 
						 | 
				
			
			@ -518,6 +613,7 @@ describe('sql/sendLog', () => {
 | 
			
		|||
        proto: getRandomBytes(128),
 | 
			
		||||
        timestamp,
 | 
			
		||||
        urgent: true,
 | 
			
		||||
        hasPniSignatureMessage: false,
 | 
			
		||||
      };
 | 
			
		||||
      await insertSentProto(proto, {
 | 
			
		||||
        messageIds: [],
 | 
			
		||||
| 
						 | 
				
			
			@ -554,6 +650,7 @@ describe('sql/sendLog', () => {
 | 
			
		|||
        proto: getRandomBytes(128),
 | 
			
		||||
        timestamp,
 | 
			
		||||
        urgent: true,
 | 
			
		||||
        hasPniSignatureMessage: false,
 | 
			
		||||
      };
 | 
			
		||||
      await insertSentProto(proto, {
 | 
			
		||||
        messageIds: [getUuid()],
 | 
			
		||||
| 
						 | 
				
			
			@ -583,6 +680,7 @@ describe('sql/sendLog', () => {
 | 
			
		|||
        proto: getRandomBytes(128),
 | 
			
		||||
        timestamp,
 | 
			
		||||
        urgent: true,
 | 
			
		||||
        hasPniSignatureMessage: false,
 | 
			
		||||
      };
 | 
			
		||||
      await insertSentProto(proto, {
 | 
			
		||||
        messageIds: [getUuid()],
 | 
			
		||||
| 
						 | 
				
			
			@ -613,6 +711,7 @@ describe('sql/sendLog', () => {
 | 
			
		|||
        proto: getRandomBytes(128),
 | 
			
		||||
        timestamp,
 | 
			
		||||
        urgent: true,
 | 
			
		||||
        hasPniSignatureMessage: false,
 | 
			
		||||
      };
 | 
			
		||||
      await insertSentProto(proto, {
 | 
			
		||||
        messageIds: [getUuid()],
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -44,8 +44,7 @@ describe('AccountManager', () => {
 | 
			
		|||
 | 
			
		||||
      window.textsecure.storage.user.getUuid = () => ourUuid;
 | 
			
		||||
 | 
			
		||||
      window.textsecure.storage.protocol.getIdentityKeyPair = async () =>
 | 
			
		||||
        identityKey;
 | 
			
		||||
      window.textsecure.storage.protocol.getIdentityKeyPair = () => identityKey;
 | 
			
		||||
      window.textsecure.storage.protocol.loadSignedPreKeys = async () =>
 | 
			
		||||
        signedPreKeys;
 | 
			
		||||
    });
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,7 +10,7 @@ import type { App } from '../bootstrap';
 | 
			
		|||
 | 
			
		||||
export const debug = createDebug('mock:test:change-number');
 | 
			
		||||
 | 
			
		||||
describe('change number', function needsName() {
 | 
			
		||||
describe('PNP change number', function needsName() {
 | 
			
		||||
  this.timeout(durations.MINUTE);
 | 
			
		||||
 | 
			
		||||
  let bootstrap: Bootstrap;
 | 
			
		||||
							
								
								
									
										350
									
								
								ts/test-mock/pnp/pni_signature_test.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										350
									
								
								ts/test-mock/pnp/pni_signature_test.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,350 @@
 | 
			
		|||
// Copyright 2022 Signal Messenger, LLC
 | 
			
		||||
// SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
import { assert } from 'chai';
 | 
			
		||||
import {
 | 
			
		||||
  UUIDKind,
 | 
			
		||||
  Proto,
 | 
			
		||||
  ReceiptType,
 | 
			
		||||
  StorageState,
 | 
			
		||||
} from '@signalapp/mock-server';
 | 
			
		||||
import type { PrimaryDevice } from '@signalapp/mock-server';
 | 
			
		||||
import createDebug from 'debug';
 | 
			
		||||
 | 
			
		||||
import * as durations from '../../util/durations';
 | 
			
		||||
import { uuidToBytes } from '../../util/uuidToBytes';
 | 
			
		||||
import { MY_STORIES_ID } from '../../types/Stories';
 | 
			
		||||
import { Bootstrap } from '../bootstrap';
 | 
			
		||||
import type { App } from '../bootstrap';
 | 
			
		||||
 | 
			
		||||
export const debug = createDebug('mock:test:pni-signature');
 | 
			
		||||
 | 
			
		||||
const IdentifierType = Proto.ManifestRecord.Identifier.Type;
 | 
			
		||||
 | 
			
		||||
describe('PNI Signature', function needsName() {
 | 
			
		||||
  this.timeout(durations.MINUTE);
 | 
			
		||||
 | 
			
		||||
  let bootstrap: Bootstrap;
 | 
			
		||||
  let app: App;
 | 
			
		||||
  let pniContact: PrimaryDevice;
 | 
			
		||||
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    bootstrap = new Bootstrap();
 | 
			
		||||
    await bootstrap.init();
 | 
			
		||||
 | 
			
		||||
    const { server, phone } = bootstrap;
 | 
			
		||||
 | 
			
		||||
    pniContact = await server.createPrimaryDevice({
 | 
			
		||||
      profileName: 'ACI Contact',
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    let state = StorageState.getEmpty();
 | 
			
		||||
 | 
			
		||||
    state = state.updateAccount({
 | 
			
		||||
      profileKey: phone.profileKey.serialize(),
 | 
			
		||||
      e164: phone.device.number,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    state = state.addContact(
 | 
			
		||||
      pniContact,
 | 
			
		||||
      {
 | 
			
		||||
        identityState: Proto.ContactRecord.IdentityState.VERIFIED,
 | 
			
		||||
        whitelisted: true,
 | 
			
		||||
 | 
			
		||||
        identityKey: pniContact.getPublicKey(UUIDKind.PNI).serialize(),
 | 
			
		||||
 | 
			
		||||
        serviceE164: pniContact.device.number,
 | 
			
		||||
        givenName: 'PNI Contact',
 | 
			
		||||
      },
 | 
			
		||||
      UUIDKind.PNI
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    state = state.addContact(pniContact, {
 | 
			
		||||
      identityState: Proto.ContactRecord.IdentityState.VERIFIED,
 | 
			
		||||
      whitelisted: true,
 | 
			
		||||
 | 
			
		||||
      serviceE164: undefined,
 | 
			
		||||
      identityKey: pniContact.publicKey.serialize(),
 | 
			
		||||
      profileKey: pniContact.profileKey.serialize(),
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Just to make PNI Contact visible in the left pane
 | 
			
		||||
    state = state.pin(pniContact, UUIDKind.PNI);
 | 
			
		||||
 | 
			
		||||
    // Add my story
 | 
			
		||||
    state = state.addRecord({
 | 
			
		||||
      type: IdentifierType.STORY_DISTRIBUTION_LIST,
 | 
			
		||||
      record: {
 | 
			
		||||
        storyDistributionList: {
 | 
			
		||||
          allowsReplies: true,
 | 
			
		||||
          identifier: uuidToBytes(MY_STORIES_ID),
 | 
			
		||||
          isBlockList: true,
 | 
			
		||||
          name: MY_STORIES_ID,
 | 
			
		||||
          recipientUuids: [],
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await phone.setStorageState(state);
 | 
			
		||||
 | 
			
		||||
    app = await bootstrap.link();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  afterEach(async function after() {
 | 
			
		||||
    if (this.currentTest?.state !== 'passed') {
 | 
			
		||||
      await bootstrap.saveLogs();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await app.close();
 | 
			
		||||
    await bootstrap.teardown();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should be sent by Desktop until encrypted delivery receipt', async () => {
 | 
			
		||||
    const { server, desktop } = bootstrap;
 | 
			
		||||
 | 
			
		||||
    const ourPNIKey = await desktop.getIdentityKey(UUIDKind.PNI);
 | 
			
		||||
    const ourACIKey = await desktop.getIdentityKey(UUIDKind.ACI);
 | 
			
		||||
 | 
			
		||||
    const window = await app.getWindow();
 | 
			
		||||
 | 
			
		||||
    const leftPane = window.locator('.left-pane-wrapper');
 | 
			
		||||
    const conversationStack = window.locator('.conversation-stack');
 | 
			
		||||
    const composeArea = window.locator(
 | 
			
		||||
      '.composition-area-wrapper, ' +
 | 
			
		||||
        '.ConversationView__template .react-wrapper'
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    debug('creating a stranger');
 | 
			
		||||
    const stranger = await server.createPrimaryDevice({
 | 
			
		||||
      profileName: 'Mysterious Stranger',
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const ourKey = await desktop.popSingleUseKey(UUIDKind.PNI);
 | 
			
		||||
    await stranger.addSingleUseKey(desktop, ourKey, UUIDKind.PNI);
 | 
			
		||||
 | 
			
		||||
    const checkPniSignature = (
 | 
			
		||||
      message: Proto.IPniSignatureMessage | null | undefined,
 | 
			
		||||
      source: string
 | 
			
		||||
    ) => {
 | 
			
		||||
      if (!message) {
 | 
			
		||||
        throw new Error(
 | 
			
		||||
          `Missing expected pni signature message from ${source}`
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      assert.deepEqual(
 | 
			
		||||
        message.pni,
 | 
			
		||||
        uuidToBytes(desktop.pni),
 | 
			
		||||
        `Incorrect pni in pni signature message from ${source}`
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      const isValid = ourPNIKey.verifyAlternateIdentity(
 | 
			
		||||
        ourACIKey,
 | 
			
		||||
        Buffer.from(message.signature ?? [])
 | 
			
		||||
      );
 | 
			
		||||
      assert.isTrue(isValid, `Invalid pni signature from ${source}`);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    debug('sending a message to our PNI');
 | 
			
		||||
    await stranger.sendText(desktop, 'A message to PNI', {
 | 
			
		||||
      uuidKind: UUIDKind.PNI,
 | 
			
		||||
      withProfileKey: true,
 | 
			
		||||
      timestamp: bootstrap.getTimestamp(),
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    debug('opening conversation with the stranger');
 | 
			
		||||
    await leftPane
 | 
			
		||||
      .locator(
 | 
			
		||||
        '_react=ConversationListItem' +
 | 
			
		||||
          `[title = ${JSON.stringify(stranger.profileName)}]`
 | 
			
		||||
      )
 | 
			
		||||
      .click();
 | 
			
		||||
 | 
			
		||||
    debug('Accept conversation from a stranger');
 | 
			
		||||
    await conversationStack
 | 
			
		||||
      .locator('.module-message-request-actions button >> "Accept"')
 | 
			
		||||
      .click();
 | 
			
		||||
 | 
			
		||||
    debug('Waiting for a pniSignatureMessage');
 | 
			
		||||
    {
 | 
			
		||||
      const { source, content } = await stranger.waitForMessage();
 | 
			
		||||
 | 
			
		||||
      assert.strictEqual(source, desktop, 'initial message has valid source');
 | 
			
		||||
      checkPniSignature(content.pniSignatureMessage, 'initial message');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    debug('Enter first message text');
 | 
			
		||||
    const compositionInput = composeArea.locator('_react=CompositionInput');
 | 
			
		||||
 | 
			
		||||
    await compositionInput.type('first');
 | 
			
		||||
    await compositionInput.press('Enter');
 | 
			
		||||
 | 
			
		||||
    debug('Waiting for the first message with pni signature');
 | 
			
		||||
    {
 | 
			
		||||
      const { source, content, body, dataMessage } =
 | 
			
		||||
        await stranger.waitForMessage();
 | 
			
		||||
 | 
			
		||||
      assert.strictEqual(
 | 
			
		||||
        source,
 | 
			
		||||
        desktop,
 | 
			
		||||
        'first message must have valid source'
 | 
			
		||||
      );
 | 
			
		||||
      assert.strictEqual(body, 'first', 'first message must have valid body');
 | 
			
		||||
      checkPniSignature(content.pniSignatureMessage, 'first message');
 | 
			
		||||
 | 
			
		||||
      const receiptTimestamp = bootstrap.getTimestamp();
 | 
			
		||||
      debug('Sending unencrypted receipt', receiptTimestamp);
 | 
			
		||||
 | 
			
		||||
      await stranger.sendUnencryptedReceipt(desktop, {
 | 
			
		||||
        messageTimestamp: dataMessage.timestamp?.toNumber() ?? 0,
 | 
			
		||||
        timestamp: receiptTimestamp,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    debug('Enter second message text');
 | 
			
		||||
 | 
			
		||||
    await compositionInput.type('second');
 | 
			
		||||
    await compositionInput.press('Enter');
 | 
			
		||||
 | 
			
		||||
    debug('Waiting for the second message with pni signature');
 | 
			
		||||
    {
 | 
			
		||||
      const { source, content, body, dataMessage } =
 | 
			
		||||
        await stranger.waitForMessage();
 | 
			
		||||
 | 
			
		||||
      assert.strictEqual(
 | 
			
		||||
        source,
 | 
			
		||||
        desktop,
 | 
			
		||||
        'second message must have valid source'
 | 
			
		||||
      );
 | 
			
		||||
      assert.strictEqual(body, 'second', 'second message must have valid body');
 | 
			
		||||
      checkPniSignature(content.pniSignatureMessage, 'second message');
 | 
			
		||||
 | 
			
		||||
      const receiptTimestamp = bootstrap.getTimestamp();
 | 
			
		||||
      debug('Sending encrypted receipt', receiptTimestamp);
 | 
			
		||||
 | 
			
		||||
      await stranger.sendReceipt(desktop, {
 | 
			
		||||
        type: ReceiptType.Delivery,
 | 
			
		||||
        messageTimestamps: [dataMessage.timestamp?.toNumber() ?? 0],
 | 
			
		||||
        timestamp: receiptTimestamp,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    debug('Enter third message text');
 | 
			
		||||
 | 
			
		||||
    await compositionInput.type('third');
 | 
			
		||||
    await compositionInput.press('Enter');
 | 
			
		||||
 | 
			
		||||
    debug('Waiting for the third message without pni signature');
 | 
			
		||||
    {
 | 
			
		||||
      const { source, content, body } = await stranger.waitForMessage();
 | 
			
		||||
 | 
			
		||||
      assert.strictEqual(
 | 
			
		||||
        source,
 | 
			
		||||
        desktop,
 | 
			
		||||
        'third message must have valid source'
 | 
			
		||||
      );
 | 
			
		||||
      assert.strictEqual(body, 'third', 'third message must have valid body');
 | 
			
		||||
      assert(
 | 
			
		||||
        !content.pniSignatureMessage,
 | 
			
		||||
        'third message must not have pni signature message'
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should be received by Desktop and trigger contact merge', async () => {
 | 
			
		||||
    const { desktop, phone } = bootstrap;
 | 
			
		||||
 | 
			
		||||
    const window = await app.getWindow();
 | 
			
		||||
 | 
			
		||||
    const leftPane = window.locator('.left-pane-wrapper');
 | 
			
		||||
    const composeArea = window.locator(
 | 
			
		||||
      '.composition-area-wrapper, ' +
 | 
			
		||||
        '.ConversationView__template .react-wrapper'
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    debug('opening conversation with the pni contact');
 | 
			
		||||
    await leftPane
 | 
			
		||||
      .locator('_react=ConversationListItem[title = "PNI Contact"]')
 | 
			
		||||
      .click();
 | 
			
		||||
 | 
			
		||||
    debug('Enter a PNI message text');
 | 
			
		||||
    const compositionInput = composeArea.locator('_react=CompositionInput');
 | 
			
		||||
 | 
			
		||||
    await compositionInput.type('Hello PNI');
 | 
			
		||||
    await compositionInput.press('Enter');
 | 
			
		||||
 | 
			
		||||
    debug('Waiting for a PNI message');
 | 
			
		||||
    {
 | 
			
		||||
      const { source, body, uuidKind } = await pniContact.waitForMessage();
 | 
			
		||||
 | 
			
		||||
      assert.strictEqual(source, desktop, 'PNI message has valid source');
 | 
			
		||||
      assert.strictEqual(body, 'Hello PNI', 'PNI message has valid body');
 | 
			
		||||
      assert.strictEqual(
 | 
			
		||||
        uuidKind,
 | 
			
		||||
        UUIDKind.PNI,
 | 
			
		||||
        'PNI message has valid destination'
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    debug('Capture storage service state before merging');
 | 
			
		||||
    const state = await phone.expectStorageState('state before merge');
 | 
			
		||||
 | 
			
		||||
    debug('Enter a draft text without hitting enter');
 | 
			
		||||
    await compositionInput.type('Draft text');
 | 
			
		||||
 | 
			
		||||
    debug('Send back the response with profile key and pni signature');
 | 
			
		||||
 | 
			
		||||
    const ourKey = await desktop.popSingleUseKey();
 | 
			
		||||
    await pniContact.addSingleUseKey(desktop, ourKey);
 | 
			
		||||
 | 
			
		||||
    await pniContact.sendText(desktop, 'Hello Desktop!', {
 | 
			
		||||
      timestamp: bootstrap.getTimestamp(),
 | 
			
		||||
      withPniSignature: true,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    debug('Wait for merge to happen');
 | 
			
		||||
    await leftPane
 | 
			
		||||
      .locator('_react=ConversationListItem[title = "ACI Contact"]')
 | 
			
		||||
      .waitFor();
 | 
			
		||||
 | 
			
		||||
    debug('Wait for composition input to clear');
 | 
			
		||||
    await composeArea
 | 
			
		||||
      .locator('_react=CompositionInput[draftText = ""]')
 | 
			
		||||
      .waitFor();
 | 
			
		||||
 | 
			
		||||
    debug('Enter an ACI message text');
 | 
			
		||||
    await compositionInput.type('Hello ACI');
 | 
			
		||||
    await compositionInput.press('Enter');
 | 
			
		||||
 | 
			
		||||
    debug('Waiting for a ACI message');
 | 
			
		||||
    {
 | 
			
		||||
      const { source, body, uuidKind } = await pniContact.waitForMessage();
 | 
			
		||||
 | 
			
		||||
      assert.strictEqual(source, desktop, 'ACI message has valid source');
 | 
			
		||||
      assert.strictEqual(body, 'Hello ACI', 'ACI message has valid body');
 | 
			
		||||
      assert.strictEqual(
 | 
			
		||||
        uuidKind,
 | 
			
		||||
        UUIDKind.ACI,
 | 
			
		||||
        'ACI message has valid destination'
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    debug('Verify final state');
 | 
			
		||||
    {
 | 
			
		||||
      const newState = await phone.waitForStorageState({
 | 
			
		||||
        after: state,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      assert.isUndefined(
 | 
			
		||||
        newState.getContact(pniContact, UUIDKind.PNI),
 | 
			
		||||
        'PNI Contact must be removed from storage service'
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      const aci = newState.getContact(pniContact, UUIDKind.ACI);
 | 
			
		||||
      assert(aci, 'ACI Contact must be in storage service');
 | 
			
		||||
 | 
			
		||||
      assert.strictEqual(aci?.serviceUuid, pniContact.device.uuid);
 | 
			
		||||
      assert.strictEqual(aci?.pni, pniContact.device.pni);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -51,16 +51,18 @@ describe('gv2', function needsName() {
 | 
			
		|||
    pniContact = await server.createPrimaryDevice({
 | 
			
		||||
      profileName: 'My profile is a secret',
 | 
			
		||||
    });
 | 
			
		||||
    state = state.addContact(pniContact, {
 | 
			
		||||
      identityState: Proto.ContactRecord.IdentityState.VERIFIED,
 | 
			
		||||
      whitelisted: true,
 | 
			
		||||
    state = state.addContact(
 | 
			
		||||
      pniContact,
 | 
			
		||||
      {
 | 
			
		||||
        identityState: Proto.ContactRecord.IdentityState.VERIFIED,
 | 
			
		||||
        whitelisted: true,
 | 
			
		||||
 | 
			
		||||
      identityKey: pniContact.getPublicKey(UUIDKind.PNI).serialize(),
 | 
			
		||||
        identityKey: pniContact.getPublicKey(UUIDKind.PNI).serialize(),
 | 
			
		||||
 | 
			
		||||
      // Give PNI as the uuid!
 | 
			
		||||
      serviceUuid: pniContact.device.pni,
 | 
			
		||||
      givenName: 'PNI Contact',
 | 
			
		||||
    });
 | 
			
		||||
        givenName: 'PNI Contact',
 | 
			
		||||
      },
 | 
			
		||||
      UUIDKind.PNI
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    state = state.addRecord({
 | 
			
		||||
      type: IdentifierType.STORY_DISTRIBUTION_LIST,
 | 
			
		||||
| 
						 | 
				
			
			@ -107,7 +107,7 @@ export default class AccountManager extends EventTarget {
 | 
			
		|||
  async decryptDeviceName(base64: string): Promise<string> {
 | 
			
		||||
    const ourUuid = window.textsecure.storage.user.getCheckedUuid();
 | 
			
		||||
    const identityKey =
 | 
			
		||||
      await window.textsecure.storage.protocol.getIdentityKeyPair(ourUuid);
 | 
			
		||||
      window.textsecure.storage.protocol.getIdentityKeyPair(ourUuid);
 | 
			
		||||
    if (!identityKey) {
 | 
			
		||||
      throw new Error('decryptDeviceName: No identity key pair!');
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -132,7 +132,7 @@ export default class AccountManager extends EventTarget {
 | 
			
		|||
    }
 | 
			
		||||
    const { storage } = window.textsecure;
 | 
			
		||||
    const deviceName = storage.user.getDeviceName();
 | 
			
		||||
    const identityKeyPair = await storage.protocol.getIdentityKeyPair(
 | 
			
		||||
    const identityKeyPair = storage.protocol.getIdentityKeyPair(
 | 
			
		||||
      storage.user.getCheckedUuid()
 | 
			
		||||
    );
 | 
			
		||||
    strictAssert(
 | 
			
		||||
| 
						 | 
				
			
			@ -362,7 +362,7 @@ export default class AccountManager extends EventTarget {
 | 
			
		|||
 | 
			
		||||
      let identityKey: KeyPairType | undefined;
 | 
			
		||||
      try {
 | 
			
		||||
        identityKey = await store.getIdentityKeyPair(ourUuid);
 | 
			
		||||
        identityKey = store.getIdentityKeyPair(ourUuid);
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        // We swallow any error here, because we don't want to get into
 | 
			
		||||
        //   a loop of repeated retries.
 | 
			
		||||
| 
						 | 
				
			
			@ -788,8 +788,7 @@ export default class AccountManager extends EventTarget {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    const store = storage.protocol;
 | 
			
		||||
    const identityKey =
 | 
			
		||||
      maybeIdentityKey ?? (await store.getIdentityKeyPair(ourUuid));
 | 
			
		||||
    const identityKey = maybeIdentityKey ?? store.getIdentityKeyPair(ourUuid);
 | 
			
		||||
    strictAssert(identityKey, 'generateKeys: No identity key pair!');
 | 
			
		||||
 | 
			
		||||
    const result: Omit<GeneratedKeysType, 'signedPreKey'> = {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -45,7 +45,7 @@ import { normalizeUuid } from '../util/normalizeUuid';
 | 
			
		|||
import { parseIntOrThrow } from '../util/parseIntOrThrow';
 | 
			
		||||
import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary';
 | 
			
		||||
import { Zone } from '../util/Zone';
 | 
			
		||||
import { deriveMasterKeyFromGroupV1 } from '../Crypto';
 | 
			
		||||
import { deriveMasterKeyFromGroupV1, bytesToUuid } from '../Crypto';
 | 
			
		||||
import type { DownloadedAttachmentType } from '../types/Attachment';
 | 
			
		||||
import { Address } from '../types/Address';
 | 
			
		||||
import { QualifiedAddress } from '../types/QualifiedAddress';
 | 
			
		||||
| 
						 | 
				
			
			@ -122,7 +122,8 @@ const GROUPV2_ID_LENGTH = 32;
 | 
			
		|||
const RETRY_TIMEOUT = 2 * 60 * 1000;
 | 
			
		||||
 | 
			
		||||
type UnsealedEnvelope = Readonly<
 | 
			
		||||
  ProcessedEnvelope & {
 | 
			
		||||
  Omit<ProcessedEnvelope, 'sourceUuid'> & {
 | 
			
		||||
    sourceUuid: UUIDStringType;
 | 
			
		||||
    unidentifiedDeliveryReceived?: boolean;
 | 
			
		||||
    contentHint?: number;
 | 
			
		||||
    groupId?: string;
 | 
			
		||||
| 
						 | 
				
			
			@ -133,10 +134,16 @@ type UnsealedEnvelope = Readonly<
 | 
			
		|||
  }
 | 
			
		||||
>;
 | 
			
		||||
 | 
			
		||||
type DecryptResult = Readonly<{
 | 
			
		||||
  envelope: UnsealedEnvelope;
 | 
			
		||||
  plaintext?: Uint8Array;
 | 
			
		||||
}>;
 | 
			
		||||
type DecryptResult = Readonly<
 | 
			
		||||
  | {
 | 
			
		||||
      envelope: UnsealedEnvelope;
 | 
			
		||||
      plaintext: Uint8Array;
 | 
			
		||||
    }
 | 
			
		||||
  | {
 | 
			
		||||
      envelope?: UnsealedEnvelope;
 | 
			
		||||
      plaintext?: undefined;
 | 
			
		||||
    }
 | 
			
		||||
>;
 | 
			
		||||
 | 
			
		||||
type DecryptSealedSenderResult = Readonly<{
 | 
			
		||||
  plaintext?: Uint8Array;
 | 
			
		||||
| 
						 | 
				
			
			@ -757,9 +764,9 @@ export default class MessageReceiver
 | 
			
		|||
        // Proto.Envelope fields
 | 
			
		||||
        type: decoded.type,
 | 
			
		||||
        source: item.source,
 | 
			
		||||
        sourceUuid: decoded.sourceUuid
 | 
			
		||||
          ? UUID.cast(decoded.sourceUuid)
 | 
			
		||||
          : item.sourceUuid,
 | 
			
		||||
        sourceUuid:
 | 
			
		||||
          item.sourceUuid ||
 | 
			
		||||
          (decoded.sourceUuid ? UUID.cast(decoded.sourceUuid) : undefined),
 | 
			
		||||
        sourceDevice: decoded.sourceDevice || item.sourceDevice,
 | 
			
		||||
        destinationUuid: new UUID(
 | 
			
		||||
          decoded.destinationUuid || item.destinationUuid || ourUuid.toString()
 | 
			
		||||
| 
						 | 
				
			
			@ -787,10 +794,21 @@ export default class MessageReceiver
 | 
			
		|||
          throw new Error('Cached decrypted value was not a string!');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        strictAssert(
 | 
			
		||||
          envelope.sourceUuid,
 | 
			
		||||
          'Decrypted envelope must have source uuid'
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Pacify typescript
 | 
			
		||||
        const decryptedEnvelope = {
 | 
			
		||||
          ...envelope,
 | 
			
		||||
          sourceUuid: envelope.sourceUuid,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Maintain invariant: encrypted queue => decrypted queue
 | 
			
		||||
        this.addToQueue(
 | 
			
		||||
          async () => {
 | 
			
		||||
            this.queueDecryptedEnvelope(envelope, payloadPlaintext);
 | 
			
		||||
            this.queueDecryptedEnvelope(decryptedEnvelope, payloadPlaintext);
 | 
			
		||||
          },
 | 
			
		||||
          'queueDecryptedEnvelope',
 | 
			
		||||
          TaskType.Encrypted
 | 
			
		||||
| 
						 | 
				
			
			@ -1088,7 +1106,7 @@ export default class MessageReceiver
 | 
			
		|||
            `Rejecting envelope ${getEnvelopeId(envelope)}, ` +
 | 
			
		||||
            `unknown uuid: ${destinationUuid}`
 | 
			
		||||
        );
 | 
			
		||||
        return { plaintext: undefined, envelope };
 | 
			
		||||
        return { plaintext: undefined, envelope: undefined };
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const unsealedEnvelope = await this.unsealEnvelope(
 | 
			
		||||
| 
						 | 
				
			
			@ -1099,7 +1117,7 @@ export default class MessageReceiver
 | 
			
		|||
 | 
			
		||||
      // Dropped early
 | 
			
		||||
      if (!unsealedEnvelope) {
 | 
			
		||||
        return { plaintext: undefined, envelope };
 | 
			
		||||
        return { plaintext: undefined, envelope: undefined };
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      logId = getEnvelopeId(unsealedEnvelope);
 | 
			
		||||
| 
						 | 
				
			
			@ -1185,8 +1203,13 @@ export default class MessageReceiver
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    if (envelope.type !== Proto.Envelope.Type.UNIDENTIFIED_SENDER) {
 | 
			
		||||
      strictAssert(
 | 
			
		||||
        envelope.sourceUuid,
 | 
			
		||||
        'Unsealed envelope must have source uuid'
 | 
			
		||||
      );
 | 
			
		||||
      return {
 | 
			
		||||
        ...envelope,
 | 
			
		||||
        sourceUuid: envelope.sourceUuid,
 | 
			
		||||
        cipherTextBytes: envelope.content,
 | 
			
		||||
        cipherTextType: envelopeTypeToCiphertextType(envelope.type),
 | 
			
		||||
      };
 | 
			
		||||
| 
						 | 
				
			
			@ -1259,6 +1282,10 @@ export default class MessageReceiver
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    if (envelope.type === Proto.Envelope.Type.RECEIPT) {
 | 
			
		||||
      strictAssert(
 | 
			
		||||
        envelope.sourceUuid,
 | 
			
		||||
        'Unsealed delivery receipt must have sourceUuid'
 | 
			
		||||
      );
 | 
			
		||||
      await this.onDeliveryReceipt(envelope);
 | 
			
		||||
      return { plaintext: undefined, envelope };
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -1291,6 +1318,7 @@ export default class MessageReceiver
 | 
			
		|||
    //   sender key to decrypt the next message in the queue!
 | 
			
		||||
    let isGroupV2 = false;
 | 
			
		||||
 | 
			
		||||
    let inProgressMessageType = '';
 | 
			
		||||
    try {
 | 
			
		||||
      const content = Proto.Content.decode(plaintext);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1300,6 +1328,7 @@ export default class MessageReceiver
 | 
			
		|||
        content.senderKeyDistributionMessage &&
 | 
			
		||||
        Bytes.isNotEmpty(content.senderKeyDistributionMessage)
 | 
			
		||||
      ) {
 | 
			
		||||
        inProgressMessageType = 'sender key distribution';
 | 
			
		||||
        await this.handleSenderKeyDistributionMessage(
 | 
			
		||||
          stores,
 | 
			
		||||
          envelope,
 | 
			
		||||
| 
						 | 
				
			
			@ -1307,22 +1336,35 @@ export default class MessageReceiver
 | 
			
		|||
        );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (content.pniSignatureMessage) {
 | 
			
		||||
        inProgressMessageType = 'pni signature';
 | 
			
		||||
        await this.handlePniSignatureMessage(
 | 
			
		||||
          envelope,
 | 
			
		||||
          content.pniSignatureMessage
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Some sync messages have to be fully processed in the middle of
 | 
			
		||||
      // decryption queue since subsequent envelopes use their key material.
 | 
			
		||||
      const { syncMessage } = content;
 | 
			
		||||
      if (syncMessage?.pniIdentity) {
 | 
			
		||||
        inProgressMessageType = 'pni identity';
 | 
			
		||||
        await this.handlePNIIdentity(envelope, syncMessage.pniIdentity);
 | 
			
		||||
        return { plaintext: undefined, envelope };
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (syncMessage?.pniChangeNumber) {
 | 
			
		||||
        inProgressMessageType = 'pni change number';
 | 
			
		||||
        await this.handlePNIChangeNumber(envelope, syncMessage.pniChangeNumber);
 | 
			
		||||
        return { plaintext: undefined, envelope };
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      inProgressMessageType = '';
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      log.error(
 | 
			
		||||
        'MessageReceiver.decryptEnvelope: Failed to process sender ' +
 | 
			
		||||
          `key distribution message: ${Errors.toLogFormat(error)}`
 | 
			
		||||
        'MessageReceiver.decryptEnvelope: ' +
 | 
			
		||||
          `Failed to process ${inProgressMessageType} ` +
 | 
			
		||||
          `message: ${Errors.toLogFormat(error)}`
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1412,6 +1454,7 @@ export default class MessageReceiver
 | 
			
		|||
          source: envelope.source,
 | 
			
		||||
          sourceUuid: envelope.sourceUuid,
 | 
			
		||||
          sourceDevice: envelope.sourceDevice,
 | 
			
		||||
          wasSentEncrypted: false,
 | 
			
		||||
        },
 | 
			
		||||
        this.removeFromCache.bind(this, envelope)
 | 
			
		||||
      )
 | 
			
		||||
| 
						 | 
				
			
			@ -1549,7 +1592,7 @@ export default class MessageReceiver
 | 
			
		|||
 | 
			
		||||
  private async innerDecrypt(
 | 
			
		||||
    stores: LockedStores,
 | 
			
		||||
    envelope: ProcessedEnvelope,
 | 
			
		||||
    envelope: UnsealedEnvelope,
 | 
			
		||||
    ciphertext: Uint8Array,
 | 
			
		||||
    uuidKind: UUIDKind
 | 
			
		||||
  ): Promise<Uint8Array | undefined> {
 | 
			
		||||
| 
						 | 
				
			
			@ -2014,6 +2057,7 @@ export default class MessageReceiver
 | 
			
		|||
        source: envelope.source,
 | 
			
		||||
        sourceUuid: envelope.sourceUuid,
 | 
			
		||||
        sourceDevice: envelope.sourceDevice,
 | 
			
		||||
        destinationUuid: envelope.destinationUuid.toString(),
 | 
			
		||||
        timestamp: envelope.timestamp,
 | 
			
		||||
        serverGuid: envelope.serverGuid,
 | 
			
		||||
        serverTimestamp: envelope.serverTimestamp,
 | 
			
		||||
| 
						 | 
				
			
			@ -2138,6 +2182,7 @@ export default class MessageReceiver
 | 
			
		|||
        source: envelope.source,
 | 
			
		||||
        sourceUuid: envelope.sourceUuid,
 | 
			
		||||
        sourceDevice: envelope.sourceDevice,
 | 
			
		||||
        destinationUuid: envelope.destinationUuid.toString(),
 | 
			
		||||
        timestamp: envelope.timestamp,
 | 
			
		||||
        serverGuid: envelope.serverGuid,
 | 
			
		||||
        serverTimestamp: envelope.serverTimestamp,
 | 
			
		||||
| 
						 | 
				
			
			@ -2154,8 +2199,8 @@ export default class MessageReceiver
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  private async maybeUpdateTimestamp(
 | 
			
		||||
    envelope: ProcessedEnvelope
 | 
			
		||||
  ): Promise<ProcessedEnvelope> {
 | 
			
		||||
    envelope: UnsealedEnvelope
 | 
			
		||||
  ): Promise<UnsealedEnvelope> {
 | 
			
		||||
    const { retryPlaceholders } = window.Signal.Services;
 | 
			
		||||
    if (!retryPlaceholders) {
 | 
			
		||||
      log.warn('maybeUpdateTimestamp: retry placeholders not available!');
 | 
			
		||||
| 
						 | 
				
			
			@ -2209,7 +2254,7 @@ export default class MessageReceiver
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  private async innerHandleContentMessage(
 | 
			
		||||
    incomingEnvelope: ProcessedEnvelope,
 | 
			
		||||
    incomingEnvelope: UnsealedEnvelope,
 | 
			
		||||
    plaintext: Uint8Array
 | 
			
		||||
  ): Promise<void> {
 | 
			
		||||
    const content = Proto.Content.decode(plaintext);
 | 
			
		||||
| 
						 | 
				
			
			@ -2311,7 +2356,7 @@ export default class MessageReceiver
 | 
			
		|||
 | 
			
		||||
  private async handleSenderKeyDistributionMessage(
 | 
			
		||||
    stores: LockedStores,
 | 
			
		||||
    envelope: ProcessedEnvelope,
 | 
			
		||||
    envelope: UnsealedEnvelope,
 | 
			
		||||
    distributionMessage: Uint8Array
 | 
			
		||||
  ): Promise<void> {
 | 
			
		||||
    const envelopeId = getEnvelopeId(envelope);
 | 
			
		||||
| 
						 | 
				
			
			@ -2324,11 +2369,6 @@ export default class MessageReceiver
 | 
			
		|||
 | 
			
		||||
    const identifier = envelope.sourceUuid;
 | 
			
		||||
    const { sourceDevice } = envelope;
 | 
			
		||||
    if (!identifier) {
 | 
			
		||||
      throw new Error(
 | 
			
		||||
        `handleSenderKeyDistributionMessage: No identifier for envelope ${envelopeId}`
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    if (!isNumber(sourceDevice)) {
 | 
			
		||||
      throw new Error(
 | 
			
		||||
        `handleSenderKeyDistributionMessage: Missing sourceDevice for envelope ${envelopeId}`
 | 
			
		||||
| 
						 | 
				
			
			@ -2358,8 +2398,44 @@ export default class MessageReceiver
 | 
			
		|||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async handlePniSignatureMessage(
 | 
			
		||||
    envelope: UnsealedEnvelope,
 | 
			
		||||
    pniSignatureMessage: Proto.IPniSignatureMessage
 | 
			
		||||
  ): Promise<void> {
 | 
			
		||||
    const envelopeId = getEnvelopeId(envelope);
 | 
			
		||||
    const logId = `handlePniSignatureMessage/${envelopeId}`;
 | 
			
		||||
    log.info(logId);
 | 
			
		||||
 | 
			
		||||
    // Note: we don't call removeFromCache here because this message can be combined
 | 
			
		||||
    //   with a dataMessage, for example. That processing will dictate cache removal.
 | 
			
		||||
 | 
			
		||||
    const aci = envelope.sourceUuid;
 | 
			
		||||
 | 
			
		||||
    const { pni: pniBytes, signature } = pniSignatureMessage;
 | 
			
		||||
    strictAssert(Bytes.isNotEmpty(pniBytes), `${logId}: missing PNI bytes`);
 | 
			
		||||
    const pni = bytesToUuid(pniBytes);
 | 
			
		||||
    strictAssert(pni, `${logId}: missing PNI`);
 | 
			
		||||
    strictAssert(Bytes.isNotEmpty(signature), `${logId}: empty signature`);
 | 
			
		||||
 | 
			
		||||
    const isValid = await this.storage.protocol.verifyAlternateIdentity({
 | 
			
		||||
      aci: new UUID(aci),
 | 
			
		||||
      pni: new UUID(pni),
 | 
			
		||||
      signature,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (isValid) {
 | 
			
		||||
      log.info(`${logId}: merging pni=${pni} aci=${aci}`);
 | 
			
		||||
      window.ConversationController.maybeMergeContacts({
 | 
			
		||||
        pni,
 | 
			
		||||
        aci,
 | 
			
		||||
        e164: window.ConversationController.get(pni)?.get('e164'),
 | 
			
		||||
        reason: logId,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async handleCallingMessage(
 | 
			
		||||
    envelope: ProcessedEnvelope,
 | 
			
		||||
    envelope: UnsealedEnvelope,
 | 
			
		||||
    callingMessage: Proto.ICallingMessage
 | 
			
		||||
  ): Promise<void> {
 | 
			
		||||
    logUnexpectedUrgentValue(envelope, 'callingMessage');
 | 
			
		||||
| 
						 | 
				
			
			@ -2372,7 +2448,7 @@ export default class MessageReceiver
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  private async handleReceiptMessage(
 | 
			
		||||
    envelope: ProcessedEnvelope,
 | 
			
		||||
    envelope: UnsealedEnvelope,
 | 
			
		||||
    receiptMessage: Proto.IReceiptMessage
 | 
			
		||||
  ): Promise<void> {
 | 
			
		||||
    strictAssert(receiptMessage.timestamp, 'Receipt message without timestamp');
 | 
			
		||||
| 
						 | 
				
			
			@ -2409,6 +2485,7 @@ export default class MessageReceiver
 | 
			
		|||
            source: envelope.source,
 | 
			
		||||
            sourceUuid: envelope.sourceUuid,
 | 
			
		||||
            sourceDevice: envelope.sourceDevice,
 | 
			
		||||
            wasSentEncrypted: true,
 | 
			
		||||
          },
 | 
			
		||||
          this.removeFromCache.bind(this, envelope)
 | 
			
		||||
        );
 | 
			
		||||
| 
						 | 
				
			
			@ -2418,7 +2495,7 @@ export default class MessageReceiver
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  private async handleTypingMessage(
 | 
			
		||||
    envelope: ProcessedEnvelope,
 | 
			
		||||
    envelope: UnsealedEnvelope,
 | 
			
		||||
    typingMessage: Proto.ITypingMessage
 | 
			
		||||
  ): Promise<void> {
 | 
			
		||||
    this.removeFromCache(envelope);
 | 
			
		||||
| 
						 | 
				
			
			@ -2475,7 +2552,7 @@ export default class MessageReceiver
 | 
			
		|||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private handleNullMessage(envelope: ProcessedEnvelope): void {
 | 
			
		||||
  private handleNullMessage(envelope: UnsealedEnvelope): void {
 | 
			
		||||
    log.info('MessageReceiver.handleNullMessage', getEnvelopeId(envelope));
 | 
			
		||||
 | 
			
		||||
    logUnexpectedUrgentValue(envelope, 'nullMessage');
 | 
			
		||||
| 
						 | 
				
			
			@ -2591,7 +2668,7 @@ export default class MessageReceiver
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  private async handleSyncMessage(
 | 
			
		||||
    envelope: ProcessedEnvelope,
 | 
			
		||||
    envelope: UnsealedEnvelope,
 | 
			
		||||
    syncMessage: ProcessedSyncMessage
 | 
			
		||||
  ): Promise<void> {
 | 
			
		||||
    const ourNumber = this.storage.user.getNumber();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -196,9 +196,13 @@ export default class OutgoingMessage {
 | 
			
		|||
      const contentProto = this.getContentProtoBytes();
 | 
			
		||||
      const { timestamp, contentHint, recipients, urgent } = this;
 | 
			
		||||
      let dataMessage: Uint8Array | undefined;
 | 
			
		||||
      let hasPniSignatureMessage = false;
 | 
			
		||||
 | 
			
		||||
      if (proto instanceof Proto.Content && proto.dataMessage) {
 | 
			
		||||
        dataMessage = Proto.DataMessage.encode(proto.dataMessage).finish();
 | 
			
		||||
      if (proto instanceof Proto.Content) {
 | 
			
		||||
        if (proto.dataMessage) {
 | 
			
		||||
          dataMessage = Proto.DataMessage.encode(proto.dataMessage).finish();
 | 
			
		||||
        }
 | 
			
		||||
        hasPniSignatureMessage = Boolean(proto.pniSignatureMessage);
 | 
			
		||||
      } else if (proto instanceof Proto.DataMessage) {
 | 
			
		||||
        dataMessage = Proto.DataMessage.encode(proto).finish();
 | 
			
		||||
      }
 | 
			
		||||
| 
						 | 
				
			
			@ -215,6 +219,7 @@ export default class OutgoingMessage {
 | 
			
		|||
        contentProto,
 | 
			
		||||
        timestamp,
 | 
			
		||||
        urgent,
 | 
			
		||||
        hasPniSignatureMessage,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,8 +15,9 @@ import {
 | 
			
		|||
} from '@signalapp/libsignal-client';
 | 
			
		||||
 | 
			
		||||
import type { QuotedMessageType } from '../model-types.d';
 | 
			
		||||
import type { ConversationModel } from '../models/conversations';
 | 
			
		||||
import { GLOBAL_ZONE } from '../SignalProtocolStore';
 | 
			
		||||
import { assert } from '../util/assert';
 | 
			
		||||
import { assert, strictAssert } from '../util/assert';
 | 
			
		||||
import { parseIntOrThrow } from '../util/parseIntOrThrow';
 | 
			
		||||
import { Address } from '../types/Address';
 | 
			
		||||
import { QualifiedAddress } from '../types/QualifiedAddress';
 | 
			
		||||
| 
						 | 
				
			
			@ -65,6 +66,7 @@ import type {
 | 
			
		|||
import { concat, isEmpty, map } from '../util/iterables';
 | 
			
		||||
import type { SendTypesType } from '../util/handleMessageSend';
 | 
			
		||||
import { shouldSaveProto, sendTypesEnum } from '../util/handleMessageSend';
 | 
			
		||||
import { uuidToBytes } from '../util/uuidToBytes';
 | 
			
		||||
import { SignalService as Proto } from '../protobuf';
 | 
			
		||||
import * as log from '../logging/log';
 | 
			
		||||
import type { Avatar, EmbeddedContactType } from '../types/EmbeddedContact';
 | 
			
		||||
| 
						 | 
				
			
			@ -574,11 +576,43 @@ class Message {
 | 
			
		|||
    return proto;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  encode() {
 | 
			
		||||
  encode(): Uint8Array {
 | 
			
		||||
    return Proto.DataMessage.encode(this.toProto()).finish();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type AddPniSignatureMessageToProtoOptionsType = Readonly<{
 | 
			
		||||
  conversation?: ConversationModel;
 | 
			
		||||
  proto: Proto.Content;
 | 
			
		||||
  reason: string;
 | 
			
		||||
}>;
 | 
			
		||||
 | 
			
		||||
function addPniSignatureMessageToProto({
 | 
			
		||||
  conversation,
 | 
			
		||||
  proto,
 | 
			
		||||
  reason,
 | 
			
		||||
}: AddPniSignatureMessageToProtoOptionsType): void {
 | 
			
		||||
  if (!conversation) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const pniSignatureMessage = conversation?.getPniSignatureMessage();
 | 
			
		||||
  if (!pniSignatureMessage) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  log.info(
 | 
			
		||||
    `addPniSignatureMessageToProto(${reason}): ` +
 | 
			
		||||
      `adding pni signature for ${conversation.idForLogging()}`
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  // eslint-disable-next-line no-param-reassign
 | 
			
		||||
  proto.pniSignatureMessage = {
 | 
			
		||||
    pni: uuidToBytes(pniSignatureMessage.pni),
 | 
			
		||||
    signature: pniSignatureMessage.signature,
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default class MessageSender {
 | 
			
		||||
  pendingMessages: {
 | 
			
		||||
    [id: string]: PQueue;
 | 
			
		||||
| 
						 | 
				
			
			@ -944,7 +978,10 @@ export default class MessageSender {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  async getContentMessage(
 | 
			
		||||
    options: Readonly<MessageOptionsType>
 | 
			
		||||
    options: Readonly<MessageOptionsType> &
 | 
			
		||||
      Readonly<{
 | 
			
		||||
        includePniSignatureMessage?: boolean;
 | 
			
		||||
      }>
 | 
			
		||||
  ): Promise<Proto.Content> {
 | 
			
		||||
    const message = await this.getHydratedMessage(options);
 | 
			
		||||
    const dataMessage = message.toProto();
 | 
			
		||||
| 
						 | 
				
			
			@ -952,6 +989,24 @@ export default class MessageSender {
 | 
			
		|||
    const contentMessage = new Proto.Content();
 | 
			
		||||
    contentMessage.dataMessage = dataMessage;
 | 
			
		||||
 | 
			
		||||
    const { includePniSignatureMessage } = options;
 | 
			
		||||
    if (includePniSignatureMessage) {
 | 
			
		||||
      strictAssert(
 | 
			
		||||
        message.recipients.length === 1,
 | 
			
		||||
        'getContentMessage: includePniSignatureMessage is single recipient only'
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      const conversation = window.ConversationController.get(
 | 
			
		||||
        message.recipients[0]
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      addPniSignatureMessageToProto({
 | 
			
		||||
        conversation,
 | 
			
		||||
        proto: contentMessage,
 | 
			
		||||
        reason: `getContentMessage(${message.timestamp})`,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return contentMessage;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1001,6 +1056,14 @@ export default class MessageSender {
 | 
			
		|||
    const contentMessage = new Proto.Content();
 | 
			
		||||
    contentMessage.typingMessage = typingMessage;
 | 
			
		||||
 | 
			
		||||
    if (recipientId) {
 | 
			
		||||
      addPniSignatureMessageToProto({
 | 
			
		||||
        conversation: window.ConversationController.get(recipientId),
 | 
			
		||||
        proto: contentMessage,
 | 
			
		||||
        reason: `getTypingContentMessage(${finalTimestamp})`,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return contentMessage;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1100,14 +1163,19 @@ export default class MessageSender {
 | 
			
		|||
    groupId,
 | 
			
		||||
    options,
 | 
			
		||||
    urgent,
 | 
			
		||||
    includePniSignatureMessage,
 | 
			
		||||
  }: Readonly<{
 | 
			
		||||
    messageOptions: MessageOptionsType;
 | 
			
		||||
    contentHint: number;
 | 
			
		||||
    groupId: string | undefined;
 | 
			
		||||
    options?: SendOptionsType;
 | 
			
		||||
    urgent: boolean;
 | 
			
		||||
    includePniSignatureMessage?: boolean;
 | 
			
		||||
  }>): Promise<CallbackResultType> {
 | 
			
		||||
    const message = await this.getHydratedMessage(messageOptions);
 | 
			
		||||
    const proto = await this.getContentMessage({
 | 
			
		||||
      ...messageOptions,
 | 
			
		||||
      includePniSignatureMessage,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      this.sendMessageProto({
 | 
			
		||||
| 
						 | 
				
			
			@ -1121,9 +1189,9 @@ export default class MessageSender {
 | 
			
		|||
        contentHint,
 | 
			
		||||
        groupId,
 | 
			
		||||
        options,
 | 
			
		||||
        proto: message.toProto(),
 | 
			
		||||
        recipients: message.recipients || [],
 | 
			
		||||
        timestamp: message.timestamp,
 | 
			
		||||
        proto,
 | 
			
		||||
        recipients: messageOptions.recipients || [],
 | 
			
		||||
        timestamp: messageOptions.timestamp,
 | 
			
		||||
        urgent,
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
| 
						 | 
				
			
			@ -1276,6 +1344,7 @@ export default class MessageSender {
 | 
			
		|||
    storyContext,
 | 
			
		||||
    timestamp,
 | 
			
		||||
    urgent,
 | 
			
		||||
    includePniSignatureMessage,
 | 
			
		||||
  }: Readonly<{
 | 
			
		||||
    attachments: ReadonlyArray<AttachmentType> | undefined;
 | 
			
		||||
    contact?: Array<ContactWithHydratedAvatar>;
 | 
			
		||||
| 
						 | 
				
			
			@ -1294,6 +1363,7 @@ export default class MessageSender {
 | 
			
		|||
    storyContext?: StoryContextType;
 | 
			
		||||
    timestamp: number;
 | 
			
		||||
    urgent: boolean;
 | 
			
		||||
    includePniSignatureMessage?: boolean;
 | 
			
		||||
  }>): Promise<CallbackResultType> {
 | 
			
		||||
    return this.sendMessage({
 | 
			
		||||
      messageOptions: {
 | 
			
		||||
| 
						 | 
				
			
			@ -1315,6 +1385,7 @@ export default class MessageSender {
 | 
			
		|||
      groupId,
 | 
			
		||||
      options,
 | 
			
		||||
      urgent,
 | 
			
		||||
      includePniSignatureMessage,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1886,6 +1957,14 @@ export default class MessageSender {
 | 
			
		|||
    const contentMessage = new Proto.Content();
 | 
			
		||||
    contentMessage.callingMessage = callingMessage;
 | 
			
		||||
 | 
			
		||||
    const conversation = window.ConversationController.get(recipientId);
 | 
			
		||||
 | 
			
		||||
    addPniSignatureMessageToProto({
 | 
			
		||||
      conversation,
 | 
			
		||||
      proto: contentMessage,
 | 
			
		||||
      reason: `sendCallingMessage(${finalTimestamp})`,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
 | 
			
		||||
 | 
			
		||||
    return this.sendMessageProtoAndWait({
 | 
			
		||||
| 
						 | 
				
			
			@ -1904,6 +1983,7 @@ export default class MessageSender {
 | 
			
		|||
      senderE164?: string;
 | 
			
		||||
      senderUuid?: string;
 | 
			
		||||
      timestamps: Array<number>;
 | 
			
		||||
      isDirectConversation: boolean;
 | 
			
		||||
      options?: Readonly<SendOptionsType>;
 | 
			
		||||
    }>
 | 
			
		||||
  ): Promise<CallbackResultType> {
 | 
			
		||||
| 
						 | 
				
			
			@ -1918,6 +1998,7 @@ export default class MessageSender {
 | 
			
		|||
      senderE164?: string;
 | 
			
		||||
      senderUuid?: string;
 | 
			
		||||
      timestamps: Array<number>;
 | 
			
		||||
      isDirectConversation: boolean;
 | 
			
		||||
      options?: Readonly<SendOptionsType>;
 | 
			
		||||
    }>
 | 
			
		||||
  ): Promise<CallbackResultType> {
 | 
			
		||||
| 
						 | 
				
			
			@ -1932,6 +2013,7 @@ export default class MessageSender {
 | 
			
		|||
      senderE164?: string;
 | 
			
		||||
      senderUuid?: string;
 | 
			
		||||
      timestamps: Array<number>;
 | 
			
		||||
      isDirectConversation: boolean;
 | 
			
		||||
      options?: Readonly<SendOptionsType>;
 | 
			
		||||
    }>
 | 
			
		||||
  ): Promise<CallbackResultType> {
 | 
			
		||||
| 
						 | 
				
			
			@ -1946,12 +2028,14 @@ export default class MessageSender {
 | 
			
		|||
    senderUuid,
 | 
			
		||||
    timestamps,
 | 
			
		||||
    type,
 | 
			
		||||
    isDirectConversation,
 | 
			
		||||
    options,
 | 
			
		||||
  }: Readonly<{
 | 
			
		||||
    senderE164?: string;
 | 
			
		||||
    senderUuid?: string;
 | 
			
		||||
    timestamps: Array<number>;
 | 
			
		||||
    type: Proto.ReceiptMessage.Type;
 | 
			
		||||
    isDirectConversation: boolean;
 | 
			
		||||
    options?: Readonly<SendOptionsType>;
 | 
			
		||||
  }>): Promise<CallbackResultType> {
 | 
			
		||||
    if (!senderUuid && !senderE164) {
 | 
			
		||||
| 
						 | 
				
			
			@ -1960,21 +2044,35 @@ export default class MessageSender {
 | 
			
		|||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const timestamp = Date.now();
 | 
			
		||||
 | 
			
		||||
    const receiptMessage = new Proto.ReceiptMessage();
 | 
			
		||||
    receiptMessage.type = type;
 | 
			
		||||
    receiptMessage.timestamp = timestamps.map(timestamp =>
 | 
			
		||||
      Long.fromNumber(timestamp)
 | 
			
		||||
    receiptMessage.timestamp = timestamps.map(receiptTimestamp =>
 | 
			
		||||
      Long.fromNumber(receiptTimestamp)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const contentMessage = new Proto.Content();
 | 
			
		||||
    contentMessage.receiptMessage = receiptMessage;
 | 
			
		||||
 | 
			
		||||
    if (isDirectConversation) {
 | 
			
		||||
      const conversation = window.ConversationController.get(
 | 
			
		||||
        senderUuid || senderE164
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      addPniSignatureMessageToProto({
 | 
			
		||||
        conversation,
 | 
			
		||||
        proto: contentMessage,
 | 
			
		||||
        reason: `sendReceiptMessage(${type}, ${timestamp})`,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
 | 
			
		||||
 | 
			
		||||
    return this.sendIndividualProto({
 | 
			
		||||
      identifier: senderUuid || senderE164,
 | 
			
		||||
      proto: contentMessage,
 | 
			
		||||
      timestamp: Date.now(),
 | 
			
		||||
      timestamp,
 | 
			
		||||
      contentHint: ContentHint.RESENDABLE,
 | 
			
		||||
      options,
 | 
			
		||||
      urgent: false,
 | 
			
		||||
| 
						 | 
				
			
			@ -2052,6 +2150,7 @@ export default class MessageSender {
 | 
			
		|||
    sendType,
 | 
			
		||||
    timestamp,
 | 
			
		||||
    urgent,
 | 
			
		||||
    hasPniSignatureMessage,
 | 
			
		||||
  }: Readonly<{
 | 
			
		||||
    contentHint: number;
 | 
			
		||||
    messageId?: string;
 | 
			
		||||
| 
						 | 
				
			
			@ -2059,6 +2158,7 @@ export default class MessageSender {
 | 
			
		|||
    sendType: SendTypesType;
 | 
			
		||||
    timestamp: number;
 | 
			
		||||
    urgent: boolean;
 | 
			
		||||
    hasPniSignatureMessage: boolean;
 | 
			
		||||
  }>): SendLogCallbackType {
 | 
			
		||||
    let initialSavePromise: Promise<number>;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -2095,6 +2195,7 @@ export default class MessageSender {
 | 
			
		|||
            proto,
 | 
			
		||||
            timestamp,
 | 
			
		||||
            urgent,
 | 
			
		||||
            hasPniSignatureMessage,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            recipients: { [recipientUuid]: deviceIds },
 | 
			
		||||
| 
						 | 
				
			
			@ -2270,6 +2371,7 @@ export default class MessageSender {
 | 
			
		|||
            sendType: 'senderKeyDistributionMessage',
 | 
			
		||||
            timestamp,
 | 
			
		||||
            urgent,
 | 
			
		||||
            hasPniSignatureMessage: false,
 | 
			
		||||
          })
 | 
			
		||||
        : undefined;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -2313,6 +2415,7 @@ export default class MessageSender {
 | 
			
		|||
            sendType: 'legacyGroupChange',
 | 
			
		||||
            timestamp,
 | 
			
		||||
            urgent: false,
 | 
			
		||||
            hasPniSignatureMessage: false,
 | 
			
		||||
          })
 | 
			
		||||
        : undefined;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										6
									
								
								ts/textsecure/Types.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								ts/textsecure/Types.d.ts
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -267,6 +267,7 @@ export interface CallbackResultType {
 | 
			
		|||
  timestamp?: number;
 | 
			
		||||
  recipients?: Record<string, Array<number>>;
 | 
			
		||||
  urgent?: boolean;
 | 
			
		||||
  hasPniSignatureMessage?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IRequestHandler {
 | 
			
		||||
| 
						 | 
				
			
			@ -278,3 +279,8 @@ export type PniKeyMaterialType = Readonly<{
 | 
			
		|||
  signedPreKey: Uint8Array;
 | 
			
		||||
  registrationId: number;
 | 
			
		||||
}>;
 | 
			
		||||
 | 
			
		||||
export type PniSignatureMessageType = Readonly<{
 | 
			
		||||
  pni: UUIDStringType;
 | 
			
		||||
  signature: Uint8Array;
 | 
			
		||||
}>;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -132,6 +132,7 @@ export type DeliveryEventData = Readonly<{
 | 
			
		|||
  source?: string;
 | 
			
		||||
  sourceUuid?: UUIDStringType;
 | 
			
		||||
  sourceDevice?: number;
 | 
			
		||||
  wasSentEncrypted: boolean;
 | 
			
		||||
}>;
 | 
			
		||||
 | 
			
		||||
export class DeliveryEvent extends ConfirmableEvent {
 | 
			
		||||
| 
						 | 
				
			
			@ -220,8 +221,9 @@ export class ProfileKeyUpdateEvent extends ConfirmableEvent {
 | 
			
		|||
 | 
			
		||||
export type MessageEventData = Readonly<{
 | 
			
		||||
  source?: string;
 | 
			
		||||
  sourceUuid?: UUIDStringType;
 | 
			
		||||
  sourceUuid: UUIDStringType;
 | 
			
		||||
  sourceDevice?: number;
 | 
			
		||||
  destinationUuid: UUIDStringType;
 | 
			
		||||
  timestamp: number;
 | 
			
		||||
  serverGuid?: string;
 | 
			
		||||
  serverTimestamp?: number;
 | 
			
		||||
| 
						 | 
				
			
			@ -246,6 +248,7 @@ export type ReadOrViewEventData = Readonly<{
 | 
			
		|||
  source?: string;
 | 
			
		||||
  sourceUuid?: UUIDStringType;
 | 
			
		||||
  sourceDevice?: number;
 | 
			
		||||
  wasSentEncrypted: true;
 | 
			
		||||
}>;
 | 
			
		||||
 | 
			
		||||
export class ReadEvent extends ConfirmableEvent {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,6 +8,7 @@ export const receiptSchema = z.object({
 | 
			
		|||
  senderE164: z.string().optional(),
 | 
			
		||||
  senderUuid: z.string().optional(),
 | 
			
		||||
  timestamp: z.number(),
 | 
			
		||||
  isDirectConversation: z.boolean().optional(),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export enum ReceiptType {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,13 +10,8 @@ import * as Bytes from '../Bytes';
 | 
			
		|||
import { getRandomBytes } from '../Crypto';
 | 
			
		||||
import { getConversationMembers } from './getConversationMembers';
 | 
			
		||||
import { isDirectConversation, isMe } from './whatTypeOfConversation';
 | 
			
		||||
import { isInSystemContacts } from './isInSystemContacts';
 | 
			
		||||
import { missingCaseError } from './missingCaseError';
 | 
			
		||||
import { senderCertificateService } from '../services/senderCertificate';
 | 
			
		||||
import {
 | 
			
		||||
  PhoneNumberSharingMode,
 | 
			
		||||
  parsePhoneNumberSharingMode,
 | 
			
		||||
} from './phoneNumberSharingMode';
 | 
			
		||||
import { shouldSharePhoneNumberWith } from './phoneNumberSharingMode';
 | 
			
		||||
import type { SerializedCertificateType } from '../textsecure/OutgoingMessage';
 | 
			
		||||
import { SenderCertificateMode } from '../textsecure/OutgoingMessage';
 | 
			
		||||
import { isNotNil } from './isNotNil';
 | 
			
		||||
| 
						 | 
				
			
			@ -146,25 +141,11 @@ function getSenderCertificateForDirectConversation(
 | 
			
		|||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const phoneNumberSharingMode = parsePhoneNumberSharingMode(
 | 
			
		||||
    window.storage.get('phoneNumberSharingMode')
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  let certificateMode: SenderCertificateMode;
 | 
			
		||||
  switch (phoneNumberSharingMode) {
 | 
			
		||||
    case PhoneNumberSharingMode.Everybody:
 | 
			
		||||
      certificateMode = SenderCertificateMode.WithE164;
 | 
			
		||||
      break;
 | 
			
		||||
    case PhoneNumberSharingMode.ContactsOnly:
 | 
			
		||||
      certificateMode = isInSystemContacts(conversationAttrs)
 | 
			
		||||
        ? SenderCertificateMode.WithE164
 | 
			
		||||
        : SenderCertificateMode.WithoutE164;
 | 
			
		||||
      break;
 | 
			
		||||
    case PhoneNumberSharingMode.Nobody:
 | 
			
		||||
      certificateMode = SenderCertificateMode.WithoutE164;
 | 
			
		||||
      break;
 | 
			
		||||
    default:
 | 
			
		||||
      throw missingCaseError(phoneNumberSharingMode);
 | 
			
		||||
  if (shouldSharePhoneNumberWith(conversationAttrs)) {
 | 
			
		||||
    certificateMode = SenderCertificateMode.WithE164;
 | 
			
		||||
  } else {
 | 
			
		||||
    certificateMode = SenderCertificateMode.WithoutE164;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return senderCertificateService.get(certificateMode);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -236,7 +236,14 @@ async function maybeSaveToSendLog(
 | 
			
		|||
    sendType: SendTypesType;
 | 
			
		||||
  }
 | 
			
		||||
): Promise<void> {
 | 
			
		||||
  const { contentHint, contentProto, recipients, timestamp, urgent } = result;
 | 
			
		||||
  const {
 | 
			
		||||
    contentHint,
 | 
			
		||||
    contentProto,
 | 
			
		||||
    recipients,
 | 
			
		||||
    timestamp,
 | 
			
		||||
    urgent,
 | 
			
		||||
    hasPniSignatureMessage,
 | 
			
		||||
  } = result;
 | 
			
		||||
 | 
			
		||||
  if (!shouldSaveProto(sendType)) {
 | 
			
		||||
    return;
 | 
			
		||||
| 
						 | 
				
			
			@ -268,6 +275,7 @@ async function maybeSaveToSendLog(
 | 
			
		|||
      proto: Buffer.from(contentProto),
 | 
			
		||||
      contentHint,
 | 
			
		||||
      urgent: isBoolean(urgent) ? urgent : true,
 | 
			
		||||
      hasPniSignatureMessage: Boolean(hasPniSignatureMessage),
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      messageIds,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,7 +10,7 @@ import { readSyncJobQueue } from '../jobs/readSyncJobQueue';
 | 
			
		|||
import { notificationService } from '../services/notifications';
 | 
			
		||||
import { expiringMessagesDeletionService } from '../services/expiringMessagesDeletion';
 | 
			
		||||
import { tapToViewMessagesDeletionService } from '../services/tapToViewMessagesDeletionService';
 | 
			
		||||
import { isGroup } from './whatTypeOfConversation';
 | 
			
		||||
import { isGroup, isDirectConversation } from './whatTypeOfConversation';
 | 
			
		||||
import * as log from '../logging/log';
 | 
			
		||||
import { getConversationIdForLogging } from './idForLogging';
 | 
			
		||||
import { ReadStatus } from '../messages/MessageReadStatus';
 | 
			
		||||
| 
						 | 
				
			
			@ -94,6 +94,7 @@ export async function markConversationRead(
 | 
			
		|||
        uuid: messageSyncData.sourceUuid,
 | 
			
		||||
      })?.id,
 | 
			
		||||
      timestamp: messageSyncData.sent_at,
 | 
			
		||||
      isDirectConversation: isDirectConversation(conversationAttrs),
 | 
			
		||||
      hasErrors: message ? hasErrors(message.attributes) : false,
 | 
			
		||||
    };
 | 
			
		||||
  });
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,12 @@
 | 
			
		|||
// Copyright 2021 Signal Messenger, LLC
 | 
			
		||||
// SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
import type { ConversationAttributesType } from '../model-types.d';
 | 
			
		||||
 | 
			
		||||
import { makeEnumParser } from './enum';
 | 
			
		||||
import { isInSystemContacts } from './isInSystemContacts';
 | 
			
		||||
import { missingCaseError } from './missingCaseError';
 | 
			
		||||
import { isDirectConversation, isMe } from './whatTypeOfConversation';
 | 
			
		||||
 | 
			
		||||
// These strings are saved to disk, so be careful when changing them.
 | 
			
		||||
export enum PhoneNumberSharingMode {
 | 
			
		||||
| 
						 | 
				
			
			@ -14,3 +19,26 @@ export const parsePhoneNumberSharingMode = makeEnumParser(
 | 
			
		|||
  PhoneNumberSharingMode,
 | 
			
		||||
  PhoneNumberSharingMode.Everybody
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export const shouldSharePhoneNumberWith = (
 | 
			
		||||
  conversation: ConversationAttributesType
 | 
			
		||||
): boolean => {
 | 
			
		||||
  if (!isDirectConversation(conversation) || isMe(conversation)) {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const phoneNumberSharingMode = parsePhoneNumberSharingMode(
 | 
			
		||||
    window.storage.get('phoneNumberSharingMode')
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  switch (phoneNumberSharingMode) {
 | 
			
		||||
    case PhoneNumberSharingMode.Everybody:
 | 
			
		||||
      return true;
 | 
			
		||||
    case PhoneNumberSharingMode.ContactsOnly:
 | 
			
		||||
      return isInSystemContacts(conversation);
 | 
			
		||||
    case PhoneNumberSharingMode.Nobody:
 | 
			
		||||
      return false;
 | 
			
		||||
    default:
 | 
			
		||||
      throw missingCaseError(phoneNumberSharingMode);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -122,11 +122,15 @@ export async function sendReceipts({
 | 
			
		|||
        map(batches, async batch => {
 | 
			
		||||
          const timestamps = batch.map(receipt => receipt.timestamp);
 | 
			
		||||
          const messageIds = batch.map(receipt => receipt.messageId);
 | 
			
		||||
          const isDirectConversation = batch.some(
 | 
			
		||||
            receipt => receipt.isDirectConversation
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          await handleMessageSend(
 | 
			
		||||
            messaging[methodName]({
 | 
			
		||||
              senderE164: sender.get('e164'),
 | 
			
		||||
              senderUuid: sender.get('uuid'),
 | 
			
		||||
              isDirectConversation,
 | 
			
		||||
              timestamps,
 | 
			
		||||
              options: sendOptions,
 | 
			
		||||
            }),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -225,6 +225,7 @@ export async function sendContentMessageToGroup({
 | 
			
		|||
    sendType,
 | 
			
		||||
    timestamp,
 | 
			
		||||
    urgent,
 | 
			
		||||
    hasPniSignatureMessage: false,
 | 
			
		||||
  });
 | 
			
		||||
  const groupId = sendTarget.isGroupV2() ? sendTarget.getGroupId() : undefined;
 | 
			
		||||
  return window.textsecure.messaging.sendGroupProto({
 | 
			
		||||
| 
						 | 
				
			
			@ -544,6 +545,7 @@ export async function sendToGroupViaSenderKey(options: {
 | 
			
		|||
          proto: Buffer.from(Proto.Content.encode(contentMessage).finish()),
 | 
			
		||||
          timestamp,
 | 
			
		||||
          urgent,
 | 
			
		||||
          hasPniSignatureMessage: false,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          recipients: senderKeyRecipientsWithDevices,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -794,6 +794,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
 | 
			
		|||
            senderE164,
 | 
			
		||||
            senderUuid,
 | 
			
		||||
            timestamp,
 | 
			
		||||
            isDirectConversation: isDirectConversation(this.model.attributes),
 | 
			
		||||
          },
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1753,10 +1753,10 @@
 | 
			
		|||
    node-gyp-build "^4.2.3"
 | 
			
		||||
    uuid "^8.3.0"
 | 
			
		||||
 | 
			
		||||
"@signalapp/mock-server@2.4.1":
 | 
			
		||||
  version "2.4.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@signalapp/mock-server/-/mock-server-2.4.1.tgz#74db72514319acea828803747082ec8403a4ab04"
 | 
			
		||||
  integrity sha512-TaTIVjHRWtLTJVYuG7GsVdcWeC/OEuRXmlyfp9FGxygvrJncsWG1pCq3YZEHrisAnWJl/Hcogg97lDkUvtjRJA==
 | 
			
		||||
"@signalapp/mock-server@2.6.0":
 | 
			
		||||
  version "2.6.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@signalapp/mock-server/-/mock-server-2.6.0.tgz#64277abd5ad5a540c0ae7e98d0347b420d69acfd"
 | 
			
		||||
  integrity sha512-EYI52E0ZwtNO0tt7V7PZJ5vs5Yy/nReHZMWovfHqcdG3iurwxq4/YIbz0fP4HylpoiJLbZ1cVzY7A8A3IAlrLQ==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@signalapp/libsignal-client" "^0.19.2"
 | 
			
		||||
    debug "^4.3.2"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue