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", |     "@babel/preset-typescript": "7.17.12", | ||||||
|     "@electron/fuses": "1.5.0", |     "@electron/fuses": "1.5.0", | ||||||
|     "@mixer/parallel-prettier": "2.0.1", |     "@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-a11y": "6.5.6", | ||||||
|     "@storybook/addon-actions": "6.5.6", |     "@storybook/addon-actions": "6.5.6", | ||||||
|     "@storybook/addon-controls": "6.5.6", |     "@storybook/addon-controls": "6.5.6", | ||||||
|  |  | ||||||
|  | @ -39,15 +39,16 @@ message Envelope { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| message Content { | message Content { | ||||||
|   optional DataMessage    dataMessage                  = 1; |   optional DataMessage         dataMessage                  = 1; | ||||||
|   optional SyncMessage    syncMessage                  = 2; |   optional SyncMessage         syncMessage                  = 2; | ||||||
|   optional CallingMessage callingMessage               = 3; |   optional CallingMessage      callingMessage               = 3; | ||||||
|   optional NullMessage    nullMessage                  = 4; |   optional NullMessage         nullMessage                  = 4; | ||||||
|   optional ReceiptMessage receiptMessage               = 5; |   optional ReceiptMessage      receiptMessage               = 5; | ||||||
|   optional TypingMessage  typingMessage                = 6; |   optional TypingMessage       typingMessage                = 6; | ||||||
|   optional bytes          senderKeyDistributionMessage = 7; |   optional bytes               senderKeyDistributionMessage = 7; | ||||||
|   optional bytes          decryptionErrorMessage       = 8; |   optional bytes               decryptionErrorMessage       = 8; | ||||||
|   optional StoryMessage   storyMessage                 = 9; |   optional StoryMessage        storyMessage                 = 9; | ||||||
|  |   optional PniSignatureMessage pniSignatureMessage          = 10; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Everything in CallingMessage must be kept in sync with RingRTC (ringrtc-node). | // Everything in CallingMessage must be kept in sync with RingRTC (ringrtc-node). | ||||||
|  | @ -627,3 +628,9 @@ message GroupDetails { | ||||||
|   optional bool   blocked         = 8; |   optional bool   blocked         = 8; | ||||||
|   optional uint32 inboxPosition   = 10; |   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> { |   private async doLoad(): Promise<void> { | ||||||
|     log.info('ConversationController: starting initial fetch'); |     log.info('ConversationController: starting initial fetch'); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -111,7 +111,7 @@ export class IdentityKeys extends IdentityKeyStore { | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async getIdentityKey(): Promise<PrivateKey> { |   async getIdentityKey(): Promise<PrivateKey> { | ||||||
|     const keyPair = await window.textsecure.storage.protocol.getIdentityKeyPair( |     const keyPair = window.textsecure.storage.protocol.getIdentityKeyPair( | ||||||
|       this.ourUuid |       this.ourUuid | ||||||
|     ); |     ); | ||||||
|     if (!keyPair) { |     if (!keyPair) { | ||||||
|  |  | ||||||
|  | @ -33,6 +33,7 @@ import type { | ||||||
|   KeyPairType, |   KeyPairType, | ||||||
|   OuterSignedPrekeyType, |   OuterSignedPrekeyType, | ||||||
|   PniKeyMaterialType, |   PniKeyMaterialType, | ||||||
|  |   PniSignatureMessageType, | ||||||
|   PreKeyIdType, |   PreKeyIdType, | ||||||
|   PreKeyType, |   PreKeyType, | ||||||
|   SenderKeyIdType, |   SenderKeyIdType, | ||||||
|  | @ -108,9 +109,15 @@ type MapFields = | ||||||
|   | 'sessions' |   | 'sessions' | ||||||
|   | 'signedPreKeys'; |   | 'signedPreKeys'; | ||||||
| 
 | 
 | ||||||
| export type SessionTransactionOptions = { | export type SessionTransactionOptions = Readonly<{ | ||||||
|   readonly zone?: Zone; |   zone?: Zone; | ||||||
| }; | }>; | ||||||
|  | 
 | ||||||
|  | export type VerifyAlternateIdentityOptionsType = Readonly<{ | ||||||
|  |   aci: UUID; | ||||||
|  |   pni: UUID; | ||||||
|  |   signature: Uint8Array; | ||||||
|  | }>; | ||||||
| 
 | 
 | ||||||
| export const GLOBAL_ZONE = new Zone('GLOBAL_ZONE'); | export const GLOBAL_ZONE = new Zone('GLOBAL_ZONE'); | ||||||
| 
 | 
 | ||||||
|  | @ -213,6 +220,8 @@ export class SignalProtocolStore extends EventsMixin { | ||||||
| 
 | 
 | ||||||
|   private ourRegistrationIds = new Map<UUIDStringType, number>(); |   private ourRegistrationIds = new Map<UUIDStringType, number>(); | ||||||
| 
 | 
 | ||||||
|  |   private cachedPniSignatureMessage: PniSignatureMessageType | undefined; | ||||||
|  | 
 | ||||||
|   identityKeys?: Map< |   identityKeys?: Map< | ||||||
|     IdentityKeyIdType, |     IdentityKeyIdType, | ||||||
|     CacheEntryType<IdentityKeyType, PublicKey> |     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()); |     return this.ourIdentityKeys.get(ourUuid.toString()); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | @ -999,7 +1008,7 @@ export class SignalProtocolStore extends EventsMixin { | ||||||
| 
 | 
 | ||||||
|     const ourUuid = new UUID(session.ourUuid); |     const ourUuid = new UUID(session.ourUuid); | ||||||
| 
 | 
 | ||||||
|     const keyPair = await this.getIdentityKeyPair(ourUuid); |     const keyPair = this.getIdentityKeyPair(ourUuid); | ||||||
|     if (!keyPair) { |     if (!keyPair) { | ||||||
|       throw new Error('_maybeMigrateSession: No identity key for ourself!'); |       throw new Error('_maybeMigrateSession: No identity key for ourself!'); | ||||||
|     } |     } | ||||||
|  | @ -2049,6 +2058,69 @@ export class SignalProtocolStore extends EventsMixin { | ||||||
|     await window.storage.fetch(); |     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> { |   private _getAllSessions(): Array<SessionCacheEntry> { | ||||||
|     const union = new Map<string, SessionCacheEntry>(); |     const union = new Map<string, SessionCacheEntry>(); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -55,7 +55,7 @@ import { RoutineProfileRefresher } from './routineProfileRefresh'; | ||||||
| import { isMoreRecentThan, isOlderThan, toDayMillis } from './util/timestamp'; | import { isMoreRecentThan, isOlderThan, toDayMillis } from './util/timestamp'; | ||||||
| import { isValidReactionEmoji } from './reactions/isValidReactionEmoji'; | import { isValidReactionEmoji } from './reactions/isValidReactionEmoji'; | ||||||
| import type { ConversationModel } from './models/conversations'; | import type { ConversationModel } from './models/conversations'; | ||||||
| import { getContact } from './messages/helpers'; | import { getContact, isIncoming } from './messages/helpers'; | ||||||
| import { migrateMessageData } from './messages/migrateMessageData'; | import { migrateMessageData } from './messages/migrateMessageData'; | ||||||
| import { createBatcher } from './util/batcher'; | import { createBatcher } from './util/batcher'; | ||||||
| import { updateConversationsWithUuidLookup } from './updateConversationsWithUuidLookup'; | import { updateConversationsWithUuidLookup } from './updateConversationsWithUuidLookup'; | ||||||
|  | @ -102,7 +102,6 @@ import { BackOff, FIBONACCI_TIMEOUTS } from './util/BackOff'; | ||||||
| import { AppViewType } from './state/ducks/app'; | import { AppViewType } from './state/ducks/app'; | ||||||
| import type { BadgesStateType } from './state/ducks/badges'; | import type { BadgesStateType } from './state/ducks/badges'; | ||||||
| import { badgeImageFileDownloader } from './badges/badgeImageFileDownloader'; | import { badgeImageFileDownloader } from './badges/badgeImageFileDownloader'; | ||||||
| import { isIncoming } from './state/selectors/message'; |  | ||||||
| import { actionCreators } from './state/actions'; | import { actionCreators } from './state/actions'; | ||||||
| import { Deletes } from './messageModifiers/Deletes'; | import { Deletes } from './messageModifiers/Deletes'; | ||||||
| import { | import { | ||||||
|  | @ -2948,7 +2947,9 @@ export async function startApp(): Promise<void> { | ||||||
| 
 | 
 | ||||||
|     const messageDescriptor = getMessageDescriptor({ |     const messageDescriptor = getMessageDescriptor({ | ||||||
|       confirm, |       confirm, | ||||||
|       ...data, |       message: data.message, | ||||||
|  |       source: data.source, | ||||||
|  |       sourceUuid: data.sourceUuid, | ||||||
|       // 'message' event: for 1:1 converations, the conversation is same as sender
 |       // 'message' event: for 1:1 converations, the conversation is same as sender
 | ||||||
|       destination: data.source, |       destination: data.source, | ||||||
|       destinationUuid: data.sourceUuid, |       destinationUuid: data.sourceUuid, | ||||||
|  | @ -2967,19 +2968,28 @@ export async function startApp(): Promise<void> { | ||||||
| 
 | 
 | ||||||
|     const message = initIncomingMessage(data, messageDescriptor); |     const message = initIncomingMessage(data, messageDescriptor); | ||||||
| 
 | 
 | ||||||
|     if ( |     if (isIncoming(message.attributes)) { | ||||||
|       isIncoming(message.attributes) && |  | ||||||
|       !message.get('unidentifiedDeliveryReceived') |  | ||||||
|     ) { |  | ||||||
|       const sender = getContact(message.attributes); |       const sender = getContact(message.attributes); | ||||||
|  |       strictAssert(sender, 'MessageModel has no sender'); | ||||||
| 
 | 
 | ||||||
|       if (!sender) { |       const uuidKind = window.textsecure.storage.user.getOurUuidKind( | ||||||
|         throw new Error('MessageModel has no sender.'); |         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(() => { |       if (!message.get('unidentifiedDeliveryReceived')) { | ||||||
|         respondWithProfileKeyBatcher.add(sender); |         profileKeyResponseQueue.add(() => { | ||||||
|       }); |           respondWithProfileKeyBatcher.add(sender); | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (data.message.reaction) { |     if (data.message.reaction) { | ||||||
|  | @ -3731,8 +3741,14 @@ export async function startApp(): Promise<void> { | ||||||
|     logTitle: string; |     logTitle: string; | ||||||
|     type: MessageReceiptType.Read | MessageReceiptType.View; |     type: MessageReceiptType.Read | MessageReceiptType.View; | ||||||
|   }>): void { |   }>): void { | ||||||
|     const { envelopeTimestamp, timestamp, source, sourceUuid, sourceDevice } = |     const { | ||||||
|       event.receipt; |       envelopeTimestamp, | ||||||
|  |       timestamp, | ||||||
|  |       source, | ||||||
|  |       sourceUuid, | ||||||
|  |       sourceDevice, | ||||||
|  |       wasSentEncrypted, | ||||||
|  |     } = event.receipt; | ||||||
|     const sourceConversation = window.ConversationController.maybeMergeContacts( |     const sourceConversation = window.ConversationController.maybeMergeContacts( | ||||||
|       { |       { | ||||||
|         aci: sourceUuid, |         aci: sourceUuid, | ||||||
|  | @ -3770,6 +3786,7 @@ export async function startApp(): Promise<void> { | ||||||
|       sourceUuid, |       sourceUuid, | ||||||
|       sourceDevice, |       sourceDevice, | ||||||
|       type, |       type, | ||||||
|  |       wasSentEncrypted, | ||||||
|     }; |     }; | ||||||
|     const receipt = MessageReceipts.getSingleton().add(attributes); |     const receipt = MessageReceipts.getSingleton().add(attributes); | ||||||
| 
 | 
 | ||||||
|  | @ -3856,8 +3873,14 @@ export async function startApp(): Promise<void> { | ||||||
| 
 | 
 | ||||||
|   function onDeliveryReceipt(ev: DeliveryEvent) { |   function onDeliveryReceipt(ev: DeliveryEvent) { | ||||||
|     const { deliveryReceipt } = ev; |     const { deliveryReceipt } = ev; | ||||||
|     const { envelopeTimestamp, sourceUuid, source, sourceDevice, timestamp } = |     const { | ||||||
|       deliveryReceipt; |       envelopeTimestamp, | ||||||
|  |       sourceUuid, | ||||||
|  |       source, | ||||||
|  |       sourceDevice, | ||||||
|  |       timestamp, | ||||||
|  |       wasSentEncrypted, | ||||||
|  |     } = deliveryReceipt; | ||||||
| 
 | 
 | ||||||
|     ev.confirm(); |     ev.confirm(); | ||||||
| 
 | 
 | ||||||
|  | @ -3902,6 +3925,7 @@ export async function startApp(): Promise<void> { | ||||||
|       sourceUuid, |       sourceUuid, | ||||||
|       sourceDevice, |       sourceDevice, | ||||||
|       type: MessageReceiptType.Delivery, |       type: MessageReceiptType.Delivery, | ||||||
|  |       wasSentEncrypted, | ||||||
|     }; |     }; | ||||||
|     const receipt = MessageReceipts.getSingleton().add(attributes); |     const receipt = MessageReceipts.getSingleton().add(attributes); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -188,6 +188,7 @@ export async function sendDeleteForEveryone( | ||||||
|                 profileKey, |                 profileKey, | ||||||
|                 options: sendOptions, |                 options: sendOptions, | ||||||
|                 urgent: true, |                 urgent: true, | ||||||
|  |                 includePniSignatureMessage: true, | ||||||
|               }), |               }), | ||||||
|             sendType, |             sendType, | ||||||
|             timestamp, |             timestamp, | ||||||
|  |  | ||||||
|  | @ -82,6 +82,7 @@ export async function sendDirectExpirationTimerUpdate( | ||||||
|     profileKey, |     profileKey, | ||||||
|     recipients: conversation.getRecipients(), |     recipients: conversation.getRecipients(), | ||||||
|     timestamp, |     timestamp, | ||||||
|  |     includePniSignatureMessage: true, | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   if (!proto.dataMessage) { |   if (!proto.dataMessage) { | ||||||
|  |  | ||||||
|  | @ -283,6 +283,7 @@ export async function sendNormalMessage( | ||||||
|           storyContext, |           storyContext, | ||||||
|           timestamp: messageTimestamp, |           timestamp: messageTimestamp, | ||||||
|           urgent: true, |           urgent: true, | ||||||
|  |           includePniSignatureMessage: true, | ||||||
|         }); |         }); | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -129,6 +129,7 @@ export async function sendProfileKey( | ||||||
|       profileKey, |       profileKey, | ||||||
|       recipients: conversation.getRecipients(), |       recipients: conversation.getRecipients(), | ||||||
|       timestamp, |       timestamp, | ||||||
|  |       includePniSignatureMessage: true, | ||||||
|     }); |     }); | ||||||
|     sendPromise = messaging.sendIndividualProto({ |     sendPromise = messaging.sendIndividualProto({ | ||||||
|       contentHint, |       contentHint, | ||||||
|  |  | ||||||
|  | @ -240,6 +240,7 @@ export async function sendReaction( | ||||||
|               } |               } | ||||||
|             : undefined, |             : undefined, | ||||||
|           urgent: true, |           urgent: true, | ||||||
|  |           includePniSignatureMessage: true, | ||||||
|         }); |         }); | ||||||
|       } else { |       } else { | ||||||
|         log.info('sending group reaction message'); |         log.info('sending group reaction message'); | ||||||
|  |  | ||||||
|  | @ -39,6 +39,7 @@ export type MessageReceiptAttributesType = { | ||||||
|   sourceConversationId: string; |   sourceConversationId: string; | ||||||
|   sourceDevice: number; |   sourceDevice: number; | ||||||
|   type: MessageReceiptType; |   type: MessageReceiptType; | ||||||
|  |   wasSentEncrypted: boolean; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| class MessageReceiptModel extends Model<MessageReceiptAttributesType> {} | class MessageReceiptModel extends Model<MessageReceiptAttributesType> {} | ||||||
|  | @ -53,7 +54,25 @@ const deleteSentProtoBatcher = createWaitBatcher({ | ||||||
|     log.info( |     log.info( | ||||||
|       `MessageReceipts: Batching ${items.length} sent proto recipients deletes` |       `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 ( |     if ( | ||||||
|       (type === MessageReceiptType.Delivery && |       (type === MessageReceiptType.Delivery && | ||||||
|         wasDeliveredWithSealedSender(sourceConversationId, message)) || |         wasDeliveredWithSealedSender(sourceConversationId, message) && | ||||||
|  |         receipt.get('wasSentEncrypted')) || | ||||||
|       type === MessageReceiptType.Read |       type === MessageReceiptType.Read | ||||||
|     ) { |     ) { | ||||||
|       const recipient = window.ConversationController.get(sourceConversationId); |       const recipient = window.ConversationController.get(sourceConversationId); | ||||||
|  | @ -201,11 +221,17 @@ export class MessageReceipts extends Collection<MessageReceiptModel> { | ||||||
|       const deviceId = receipt.get('sourceDevice'); |       const deviceId = receipt.get('sourceDevice'); | ||||||
| 
 | 
 | ||||||
|       if (recipientUuid && deviceId) { |       if (recipientUuid && deviceId) { | ||||||
|         await deleteSentProtoBatcher.add({ |         await Promise.all([ | ||||||
|           timestamp: messageSentAt, |           deleteSentProtoBatcher.add({ | ||||||
|           recipientUuid, |             timestamp: messageSentAt, | ||||||
|           deviceId, |             recipientUuid, | ||||||
|         }); |             deviceId, | ||||||
|  |           }), | ||||||
|  | 
 | ||||||
|  |           // We want the above call to not be delayed when testing with
 | ||||||
|  |           // CI.
 | ||||||
|  |           window.CI ? deleteSentProtoBatcher.flushAndWait() : Promise.resolve(), | ||||||
|  |         ]); | ||||||
|       } else { |       } else { | ||||||
|         log.warn( |         log.warn( | ||||||
|           `MessageReceipts.onReceipt: Missing uuid or deviceId for deliveredTo ${sourceConversationId}` |           `MessageReceipts.onReceipt: Missing uuid or deviceId for deliveredTo ${sourceConversationId}` | ||||||
|  | @ -249,6 +275,7 @@ export class MessageReceipts extends Collection<MessageReceiptModel> { | ||||||
|             'No message for receipt', |             'No message for receipt', | ||||||
|             type, |             type, | ||||||
|             sourceConversationId, |             sourceConversationId, | ||||||
|  |             sourceUuid, | ||||||
|             messageSentAt |             messageSentAt | ||||||
|           ); |           ); | ||||||
|           return; |           return; | ||||||
|  |  | ||||||
							
								
								
									
										1
									
								
								ts/model-types.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								ts/model-types.d.ts
									
										
									
									
										vendored
									
									
								
							|  | @ -335,6 +335,7 @@ export type ConversationAttributesType = { | ||||||
|   profileLastFetchedAt?: number; |   profileLastFetchedAt?: number; | ||||||
|   pendingUniversalTimer?: string; |   pendingUniversalTimer?: string; | ||||||
|   username?: string; |   username?: string; | ||||||
|  |   shareMyPhoneNumber?: boolean; | ||||||
| 
 | 
 | ||||||
|   // Group-only
 |   // Group-only
 | ||||||
|   groupId?: string; |   groupId?: string; | ||||||
|  |  | ||||||
|  | @ -34,7 +34,10 @@ import type { | ||||||
| } from '../textsecure/SendMessage'; | } from '../textsecure/SendMessage'; | ||||||
| import createTaskWithTimeout from '../textsecure/TaskWithTimeout'; | import createTaskWithTimeout from '../textsecure/TaskWithTimeout'; | ||||||
| import MessageSender from '../textsecure/SendMessage'; | 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 { ConversationType } from '../state/ducks/conversations'; | ||||||
| import type { | import type { | ||||||
|   AvatarColorType, |   AvatarColorType, | ||||||
|  | @ -2023,6 +2026,7 @@ export class ConversationModel extends window.Backbone | ||||||
|             senderE164: m.source, |             senderE164: m.source, | ||||||
|             senderUuid: m.sourceUuid, |             senderUuid: m.sourceUuid, | ||||||
|             timestamp: m.sent_at, |             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; | window.Whisper.Conversation = ConversationModel; | ||||||
|  |  | ||||||
|  | @ -2373,6 +2373,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> { | ||||||
|             senderE164: source, |             senderE164: source, | ||||||
|             senderUuid: sourceUuid, |             senderUuid: sourceUuid, | ||||||
|             timestamp: this.get('sent_at'), |             timestamp: this.get('sent_at'), | ||||||
|  |             isDirectConversation: isDirectConversation(conversation.attributes), | ||||||
|           }); |           }); | ||||||
|         }); |         }); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|  | @ -53,6 +53,7 @@ import type { | ||||||
|   ConversationType, |   ConversationType, | ||||||
|   ConversationMetricsType, |   ConversationMetricsType, | ||||||
|   DeleteSentProtoRecipientOptionsType, |   DeleteSentProtoRecipientOptionsType, | ||||||
|  |   DeleteSentProtoRecipientResultType, | ||||||
|   EmojiType, |   EmojiType, | ||||||
|   GetUnreadByConversationAndMarkReadResultType, |   GetUnreadByConversationAndMarkReadResultType, | ||||||
|   GetConversationRangeCenteredOnMessageResultType, |   GetConversationRangeCenteredOnMessageResultType, | ||||||
|  | @ -952,8 +953,8 @@ async function deleteSentProtoRecipient( | ||||||
|   options: |   options: | ||||||
|     | DeleteSentProtoRecipientOptionsType |     | DeleteSentProtoRecipientOptionsType | ||||||
|     | ReadonlyArray<DeleteSentProtoRecipientOptionsType> |     | ReadonlyArray<DeleteSentProtoRecipientOptionsType> | ||||||
| ): Promise<void> { | ): Promise<DeleteSentProtoRecipientResultType> { | ||||||
|   await channels.deleteSentProtoRecipient(options); |   return channels.deleteSentProtoRecipient(options); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| async function getSentProtoByRecipient(options: { | async function getSentProtoByRecipient(options: { | ||||||
|  |  | ||||||
|  | @ -119,6 +119,7 @@ export type SentProtoType = { | ||||||
|   proto: Uint8Array; |   proto: Uint8Array; | ||||||
|   timestamp: number; |   timestamp: number; | ||||||
|   urgent: boolean; |   urgent: boolean; | ||||||
|  |   hasPniSignatureMessage: boolean; | ||||||
| }; | }; | ||||||
| export type SentProtoWithMessageIdsType = SentProtoType & { | export type SentProtoWithMessageIdsType = SentProtoType & { | ||||||
|   messageIds: Array<string>; |   messageIds: Array<string>; | ||||||
|  | @ -287,6 +288,10 @@ export type DeleteSentProtoRecipientOptionsType = Readonly<{ | ||||||
|   deviceId: number; |   deviceId: number; | ||||||
| }>; | }>; | ||||||
| 
 | 
 | ||||||
|  | export type DeleteSentProtoRecipientResultType = Readonly<{ | ||||||
|  |   successfulPhoneNumberShares: ReadonlyArray<string>; | ||||||
|  | }>; | ||||||
|  | 
 | ||||||
| export type StoryDistributionType = Readonly<{ | export type StoryDistributionType = Readonly<{ | ||||||
|   id: UUIDStringType; |   id: UUIDStringType; | ||||||
|   name: string; |   name: string; | ||||||
|  | @ -381,7 +386,7 @@ export type DataInterface = { | ||||||
|     options: |     options: | ||||||
|       | DeleteSentProtoRecipientOptionsType |       | DeleteSentProtoRecipientOptionsType | ||||||
|       | ReadonlyArray<DeleteSentProtoRecipientOptionsType> |       | ReadonlyArray<DeleteSentProtoRecipientOptionsType> | ||||||
|   ) => Promise<void>; |   ) => Promise<DeleteSentProtoRecipientResultType>; | ||||||
|   getSentProtoByRecipient: (options: { |   getSentProtoByRecipient: (options: { | ||||||
|     now: number; |     now: number; | ||||||
|     recipientUuid: string; |     recipientUuid: string; | ||||||
|  |  | ||||||
|  | @ -76,6 +76,7 @@ import type { | ||||||
|   ConversationMetricsType, |   ConversationMetricsType, | ||||||
|   ConversationType, |   ConversationType, | ||||||
|   DeleteSentProtoRecipientOptionsType, |   DeleteSentProtoRecipientOptionsType, | ||||||
|  |   DeleteSentProtoRecipientResultType, | ||||||
|   EmojiType, |   EmojiType, | ||||||
|   GetConversationRangeCenteredOnMessageResultType, |   GetConversationRangeCenteredOnMessageResultType, | ||||||
|   GetUnreadByConversationAndMarkReadResultType, |   GetUnreadByConversationAndMarkReadResultType, | ||||||
|  | @ -855,17 +856,20 @@ async function insertSentProto( | ||||||
|         contentHint, |         contentHint, | ||||||
|         proto, |         proto, | ||||||
|         timestamp, |         timestamp, | ||||||
|         urgent |         urgent, | ||||||
|  |         hasPniSignatureMessage | ||||||
|       ) VALUES ( |       ) VALUES ( | ||||||
|         $contentHint, |         $contentHint, | ||||||
|         $proto, |         $proto, | ||||||
|         $timestamp, |         $timestamp, | ||||||
|         $urgent |         $urgent, | ||||||
|  |         $hasPniSignatureMessage | ||||||
|       ); |       ); | ||||||
|       ` |       ` | ||||||
|     ).run({ |     ).run({ | ||||||
|       ...proto, |       ...proto, | ||||||
|       urgent: proto.urgent ? 1 : 0, |       urgent: proto.urgent ? 1 : 0, | ||||||
|  |       hasPniSignatureMessage: proto.hasPniSignatureMessage ? 1 : 0, | ||||||
|     }); |     }); | ||||||
|     const id = parseIntOrThrow( |     const id = parseIntOrThrow( | ||||||
|       info.lastInsertRowid, |       info.lastInsertRowid, | ||||||
|  | @ -999,7 +1003,7 @@ async function deleteSentProtoRecipient( | ||||||
|   options: |   options: | ||||||
|     | DeleteSentProtoRecipientOptionsType |     | DeleteSentProtoRecipientOptionsType | ||||||
|     | ReadonlyArray<DeleteSentProtoRecipientOptionsType> |     | ReadonlyArray<DeleteSentProtoRecipientOptionsType> | ||||||
| ): Promise<void> { | ): Promise<DeleteSentProtoRecipientResultType> { | ||||||
|   const db = getInstance(); |   const db = getInstance(); | ||||||
| 
 | 
 | ||||||
|   const items = Array.isArray(options) ? options : [options]; |   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
 |   // Note: we use `pluck` in this function to fetch only the first column of
 | ||||||
|   // returned row.
 |   // returned row.
 | ||||||
| 
 | 
 | ||||||
|   db.transaction(() => { |   return db.transaction(() => { | ||||||
|  |     const successfulPhoneNumberShares = new Array<string>(); | ||||||
|  | 
 | ||||||
|     for (const item of items) { |     for (const item of items) { | ||||||
|       const { timestamp, recipientUuid, deviceId } = item; |       const { timestamp, recipientUuid, deviceId } = item; | ||||||
| 
 | 
 | ||||||
|  | @ -1015,7 +1021,8 @@ async function deleteSentProtoRecipient( | ||||||
|       const rows = prepare( |       const rows = prepare( | ||||||
|         db, |         db, | ||||||
|         ` |         ` | ||||||
|         SELECT sendLogPayloads.id FROM sendLogPayloads |         SELECT sendLogPayloads.id, sendLogPayloads.hasPniSignatureMessage | ||||||
|  |         FROM sendLogPayloads | ||||||
|         INNER JOIN sendLogRecipients |         INNER JOIN sendLogRecipients | ||||||
|           ON sendLogRecipients.payloadId = sendLogPayloads.id |           ON sendLogRecipients.payloadId = sendLogPayloads.id | ||||||
|         WHERE |         WHERE | ||||||
|  | @ -1032,10 +1039,9 @@ async function deleteSentProtoRecipient( | ||||||
|           'deleteSentProtoRecipient: More than one payload matches ' + |           'deleteSentProtoRecipient: More than one payload matches ' + | ||||||
|             `recipient and timestamp ${timestamp}. Using the first.` |             `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.
 |       // 2. Delete the recipient/device combination in question.
 | ||||||
|       prepare( |       prepare( | ||||||
|  | @ -1050,32 +1056,61 @@ async function deleteSentProtoRecipient( | ||||||
|       ).run({ id, recipientUuid, deviceId }); |       ).run({ id, recipientUuid, deviceId }); | ||||||
| 
 | 
 | ||||||
|       // 3. See how many more recipient devices there were for this payload.
 |       // 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, |         db, | ||||||
|         'SELECT count(*) FROM sendLogRecipients WHERE payloadId = $id;' |         'SELECT count(*) FROM sendLogRecipients WHERE payloadId = $id;' | ||||||
|       ) |       ) | ||||||
|         .pluck(true) |         .pluck(true) | ||||||
|         .get({ id }); |         .get({ id }); | ||||||
| 
 | 
 | ||||||
|       if (!isNumber(remaining)) { |       strictAssert( | ||||||
|         throw new Error( |         isNumber(remainingTotal), | ||||||
|           'deleteSentProtoRecipient: select count() returned non-number!' |         'deleteSentProtoRecipient: select count() returned non-number!' | ||||||
|         ); |       ); | ||||||
|       } |  | ||||||
| 
 | 
 | ||||||
|       if (remaining > 0) { |       if (remainingTotal > 0) { | ||||||
|         continue; |         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( |       logger.info( | ||||||
|         'deleteSentProtoRecipient: ' + |         'deleteSentProtoRecipient: ' + | ||||||
|           `Deleting proto payload for timestamp ${timestamp}` |           `Deleting proto payload for timestamp ${timestamp}` | ||||||
|       ); |       ); | ||||||
|  | 
 | ||||||
|       prepare(db, 'DELETE FROM sendLogPayloads WHERE id = $id;').run({ |       prepare(db, 'DELETE FROM sendLogPayloads WHERE id = $id;').run({ | ||||||
|         id, |         id, | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     return { successfulPhoneNumberShares }; | ||||||
|   })(); |   })(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -1122,6 +1157,9 @@ async function getSentProtoByRecipient({ | ||||||
|   return { |   return { | ||||||
|     ...row, |     ...row, | ||||||
|     urgent: isNumber(row.urgent) ? Boolean(row.urgent) : true, |     urgent: isNumber(row.urgent) ? Boolean(row.urgent) : true, | ||||||
|  |     hasPniSignatureMessage: isNumber(row.hasPniSignatureMessage) | ||||||
|  |       ? Boolean(row.hasPniSignatureMessage) | ||||||
|  |       : true, | ||||||
|     messageIds: messageIds ? messageIds.split(',') : [], |     messageIds: messageIds ? messageIds.split(',') : [], | ||||||
|   }; |   }; | ||||||
| } | } | ||||||
|  | @ -1136,6 +1174,9 @@ async function getAllSentProtos(): Promise<Array<SentProtoType>> { | ||||||
|   return rows.map(row => ({ |   return rows.map(row => ({ | ||||||
|     ...row, |     ...row, | ||||||
|     urgent: isNumber(row.urgent) ? Boolean(row.urgent) : true, |     urgent: isNumber(row.urgent) ? Boolean(row.urgent) : true, | ||||||
|  |     hasPniSignatureMessage: isNumber(row.hasPniSignatureMessage) | ||||||
|  |       ? Boolean(row.hasPniSignatureMessage) | ||||||
|  |       : true, | ||||||
|   })); |   })); | ||||||
| } | } | ||||||
| async function _getAllSentProtoRecipients(): Promise< | 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 updateToSchemaVersion63 from './63-add-urgent-to-unprocessed'; | ||||||
| import updateToSchemaVersion64 from './64-uuid-column-for-pre-keys'; | import updateToSchemaVersion64 from './64-uuid-column-for-pre-keys'; | ||||||
| import updateToSchemaVersion65 from './65-add-storage-id-to-stickers'; | import updateToSchemaVersion65 from './65-add-storage-id-to-stickers'; | ||||||
|  | import updateToSchemaVersion66 from './66-add-pni-signature-to-sent-protos'; | ||||||
| 
 | 
 | ||||||
| function updateToSchemaVersion1( | function updateToSchemaVersion1( | ||||||
|   currentVersion: number, |   currentVersion: number, | ||||||
|  | @ -1945,6 +1946,7 @@ export const SCHEMA_VERSIONS = [ | ||||||
|   updateToSchemaVersion63, |   updateToSchemaVersion63, | ||||||
|   updateToSchemaVersion64, |   updateToSchemaVersion64, | ||||||
|   updateToSchemaVersion65, |   updateToSchemaVersion65, | ||||||
|  |   updateToSchemaVersion66, | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
| export function updateSchema(db: Database, logger: LoggerType): void { | export function updateSchema(db: Database, logger: LoggerType): void { | ||||||
|  |  | ||||||
|  | @ -410,6 +410,7 @@ function markStoryRead( | ||||||
|       senderE164: message.attributes.source, |       senderE164: message.attributes.source, | ||||||
|       senderUuid: message.attributes.sourceUuid, |       senderUuid: message.attributes.sourceUuid, | ||||||
|       timestamp: message.attributes.sent_at, |       timestamp: message.attributes.sent_at, | ||||||
|  |       isDirectConversation: false, | ||||||
|     }; |     }; | ||||||
|     const viewSyncs: Array<SyncType> = [viewedReceipt]; |     const viewSyncs: Array<SyncType> = [viewedReceipt]; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -166,7 +166,7 @@ describe('SignalProtocolStore', () => { | ||||||
|   describe('getIdentityKeyPair', () => { |   describe('getIdentityKeyPair', () => { | ||||||
|     it('retrieves my identity key', async () => { |     it('retrieves my identity key', async () => { | ||||||
|       await store.hydrateCaches(); |       await store.hydrateCaches(); | ||||||
|       const key = await store.getIdentityKeyPair(ourUuid); |       const key = store.getIdentityKeyPair(ourUuid); | ||||||
|       if (!key) { |       if (!key) { | ||||||
|         throw new Error('Missing key!'); |         throw new Error('Missing key!'); | ||||||
|       } |       } | ||||||
|  | @ -1810,13 +1810,13 @@ describe('SignalProtocolStore', () => { | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|       // Old data has to be removed
 |       // 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.getLocalRegistrationId(oldPni)); | ||||||
|       assert.isUndefined(await store.loadPreKey(oldPni, 2)); |       assert.isUndefined(await store.loadPreKey(oldPni, 2)); | ||||||
|       assert.isUndefined(await store.loadSignedPreKey(oldPni, 3)); |       assert.isUndefined(await store.loadSignedPreKey(oldPni, 3)); | ||||||
| 
 | 
 | ||||||
|       // New data has to be added
 |       // New data has to be added
 | ||||||
|       const storedIdentity = await store.getIdentityKeyPair(newPni); |       const storedIdentity = store.getIdentityKeyPair(newPni); | ||||||
|       if (!storedIdentity) { |       if (!storedIdentity) { | ||||||
|         throw new Error('New identity not found'); |         throw new Error('New identity not found'); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|  | @ -40,6 +40,7 @@ describe('sql/sendLog', () => { | ||||||
|       proto: bytes, |       proto: bytes, | ||||||
|       timestamp, |       timestamp, | ||||||
|       urgent: false, |       urgent: false, | ||||||
|  |       hasPniSignatureMessage: false, | ||||||
|     }; |     }; | ||||||
|     await insertSentProto(proto, { |     await insertSentProto(proto, { | ||||||
|       messageIds: [getUuid()], |       messageIds: [getUuid()], | ||||||
|  | @ -56,6 +57,10 @@ describe('sql/sendLog', () => { | ||||||
|     assert.isTrue(constantTimeEqual(actual.proto, proto.proto)); |     assert.isTrue(constantTimeEqual(actual.proto, proto.proto)); | ||||||
|     assert.strictEqual(actual.timestamp, proto.timestamp); |     assert.strictEqual(actual.timestamp, proto.timestamp); | ||||||
|     assert.strictEqual(actual.urgent, proto.urgent); |     assert.strictEqual(actual.urgent, proto.urgent); | ||||||
|  |     assert.strictEqual( | ||||||
|  |       actual.hasPniSignatureMessage, | ||||||
|  |       proto.hasPniSignatureMessage | ||||||
|  |     ); | ||||||
| 
 | 
 | ||||||
|     await removeAllSentProtos(); |     await removeAllSentProtos(); | ||||||
| 
 | 
 | ||||||
|  | @ -74,6 +79,7 @@ describe('sql/sendLog', () => { | ||||||
|       proto: bytes, |       proto: bytes, | ||||||
|       timestamp, |       timestamp, | ||||||
|       urgent: true, |       urgent: true, | ||||||
|  |       hasPniSignatureMessage: true, | ||||||
|     }; |     }; | ||||||
|     await insertSentProto(proto, { |     await insertSentProto(proto, { | ||||||
|       messageIds: [getUuid(), getUuid()], |       messageIds: [getUuid(), getUuid()], | ||||||
|  | @ -91,6 +97,10 @@ describe('sql/sendLog', () => { | ||||||
|     assert.isTrue(constantTimeEqual(actual.proto, proto.proto)); |     assert.isTrue(constantTimeEqual(actual.proto, proto.proto)); | ||||||
|     assert.strictEqual(actual.timestamp, proto.timestamp); |     assert.strictEqual(actual.timestamp, proto.timestamp); | ||||||
|     assert.strictEqual(actual.urgent, proto.urgent); |     assert.strictEqual(actual.urgent, proto.urgent); | ||||||
|  |     assert.strictEqual( | ||||||
|  |       actual.hasPniSignatureMessage, | ||||||
|  |       proto.hasPniSignatureMessage | ||||||
|  |     ); | ||||||
| 
 | 
 | ||||||
|     assert.lengthOf(await _getAllSentProtoMessageIds(), 2); |     assert.lengthOf(await _getAllSentProtoMessageIds(), 2); | ||||||
|     assert.lengthOf(await _getAllSentProtoRecipients(), 3); |     assert.lengthOf(await _getAllSentProtoRecipients(), 3); | ||||||
|  | @ -127,6 +137,7 @@ describe('sql/sendLog', () => { | ||||||
|       proto: bytes, |       proto: bytes, | ||||||
|       timestamp, |       timestamp, | ||||||
|       urgent: false, |       urgent: false, | ||||||
|  |       hasPniSignatureMessage: false, | ||||||
|     }; |     }; | ||||||
|     await insertSentProto(proto, { |     await insertSentProto(proto, { | ||||||
|       messageIds: [id], |       messageIds: [id], | ||||||
|  | @ -159,12 +170,14 @@ describe('sql/sendLog', () => { | ||||||
|         proto: getRandomBytes(128), |         proto: getRandomBytes(128), | ||||||
|         timestamp, |         timestamp, | ||||||
|         urgent: true, |         urgent: true, | ||||||
|  |         hasPniSignatureMessage: false, | ||||||
|       }; |       }; | ||||||
|       const proto2 = { |       const proto2 = { | ||||||
|         contentHint: 9, |         contentHint: 9, | ||||||
|         proto: getRandomBytes(128), |         proto: getRandomBytes(128), | ||||||
|         timestamp, |         timestamp, | ||||||
|         urgent: false, |         urgent: false, | ||||||
|  |         hasPniSignatureMessage: true, | ||||||
|       }; |       }; | ||||||
| 
 | 
 | ||||||
|       assert.lengthOf(await getAllSentProtos(), 0); |       assert.lengthOf(await getAllSentProtos(), 0); | ||||||
|  | @ -195,6 +208,7 @@ describe('sql/sendLog', () => { | ||||||
|         proto: getRandomBytes(128), |         proto: getRandomBytes(128), | ||||||
|         timestamp, |         timestamp, | ||||||
|         urgent: true, |         urgent: true, | ||||||
|  |         hasPniSignatureMessage: false, | ||||||
|       }; |       }; | ||||||
| 
 | 
 | ||||||
|       assert.lengthOf(await getAllSentProtos(), 0); |       assert.lengthOf(await getAllSentProtos(), 0); | ||||||
|  | @ -234,18 +248,21 @@ describe('sql/sendLog', () => { | ||||||
|         proto: getRandomBytes(128), |         proto: getRandomBytes(128), | ||||||
|         timestamp: timestamp + 10, |         timestamp: timestamp + 10, | ||||||
|         urgent: true, |         urgent: true, | ||||||
|  |         hasPniSignatureMessage: false, | ||||||
|       }; |       }; | ||||||
|       const proto2 = { |       const proto2 = { | ||||||
|         contentHint: 2, |         contentHint: 2, | ||||||
|         proto: getRandomBytes(128), |         proto: getRandomBytes(128), | ||||||
|         timestamp, |         timestamp, | ||||||
|         urgent: true, |         urgent: true, | ||||||
|  |         hasPniSignatureMessage: false, | ||||||
|       }; |       }; | ||||||
|       const proto3 = { |       const proto3 = { | ||||||
|         contentHint: 0, |         contentHint: 0, | ||||||
|         proto: getRandomBytes(128), |         proto: getRandomBytes(128), | ||||||
|         timestamp: timestamp - 15, |         timestamp: timestamp - 15, | ||||||
|         urgent: true, |         urgent: true, | ||||||
|  |         hasPniSignatureMessage: false, | ||||||
|       }; |       }; | ||||||
|       await insertSentProto(proto1, { |       await insertSentProto(proto1, { | ||||||
|         messageIds: [getUuid()], |         messageIds: [getUuid()], | ||||||
|  | @ -298,18 +315,21 @@ describe('sql/sendLog', () => { | ||||||
|         proto: getRandomBytes(128), |         proto: getRandomBytes(128), | ||||||
|         timestamp, |         timestamp, | ||||||
|         urgent: true, |         urgent: true, | ||||||
|  |         hasPniSignatureMessage: false, | ||||||
|       }; |       }; | ||||||
|       const proto2 = { |       const proto2 = { | ||||||
|         contentHint: 1, |         contentHint: 1, | ||||||
|         proto: getRandomBytes(128), |         proto: getRandomBytes(128), | ||||||
|         timestamp: timestamp - 10, |         timestamp: timestamp - 10, | ||||||
|         urgent: true, |         urgent: true, | ||||||
|  |         hasPniSignatureMessage: false, | ||||||
|       }; |       }; | ||||||
|       const proto3 = { |       const proto3 = { | ||||||
|         contentHint: 1, |         contentHint: 1, | ||||||
|         proto: getRandomBytes(128), |         proto: getRandomBytes(128), | ||||||
|         timestamp: timestamp - 20, |         timestamp: timestamp - 20, | ||||||
|         urgent: true, |         urgent: true, | ||||||
|  |         hasPniSignatureMessage: false, | ||||||
|       }; |       }; | ||||||
|       await insertSentProto(proto1, { |       await insertSentProto(proto1, { | ||||||
|         messageIds: [messageId, getUuid()], |         messageIds: [messageId, getUuid()], | ||||||
|  | @ -354,6 +374,7 @@ describe('sql/sendLog', () => { | ||||||
|         proto: getRandomBytes(128), |         proto: getRandomBytes(128), | ||||||
|         timestamp, |         timestamp, | ||||||
|         urgent: true, |         urgent: true, | ||||||
|  |         hasPniSignatureMessage: false, | ||||||
|       }; |       }; | ||||||
|       await insertSentProto(proto, { |       await insertSentProto(proto, { | ||||||
|         messageIds: [getUuid()], |         messageIds: [getUuid()], | ||||||
|  | @ -366,11 +387,12 @@ describe('sql/sendLog', () => { | ||||||
|       assert.lengthOf(await getAllSentProtos(), 1); |       assert.lengthOf(await getAllSentProtos(), 1); | ||||||
|       assert.lengthOf(await _getAllSentProtoRecipients(), 3); |       assert.lengthOf(await _getAllSentProtoRecipients(), 3); | ||||||
| 
 | 
 | ||||||
|       await deleteSentProtoRecipient({ |       const { successfulPhoneNumberShares } = await deleteSentProtoRecipient({ | ||||||
|         timestamp, |         timestamp, | ||||||
|         recipientUuid: recipientUuid1, |         recipientUuid: recipientUuid1, | ||||||
|         deviceId: 1, |         deviceId: 1, | ||||||
|       }); |       }); | ||||||
|  |       assert.lengthOf(successfulPhoneNumberShares, 0); | ||||||
| 
 | 
 | ||||||
|       assert.lengthOf(await getAllSentProtos(), 1); |       assert.lengthOf(await getAllSentProtos(), 1); | ||||||
|       assert.lengthOf(await _getAllSentProtoRecipients(), 2); |       assert.lengthOf(await _getAllSentProtoRecipients(), 2); | ||||||
|  | @ -386,6 +408,7 @@ describe('sql/sendLog', () => { | ||||||
|         proto: getRandomBytes(128), |         proto: getRandomBytes(128), | ||||||
|         timestamp, |         timestamp, | ||||||
|         urgent: true, |         urgent: true, | ||||||
|  |         hasPniSignatureMessage: false, | ||||||
|       }; |       }; | ||||||
|       await insertSentProto(proto, { |       await insertSentProto(proto, { | ||||||
|         messageIds: [getUuid()], |         messageIds: [getUuid()], | ||||||
|  | @ -398,30 +421,99 @@ describe('sql/sendLog', () => { | ||||||
|       assert.lengthOf(await getAllSentProtos(), 1); |       assert.lengthOf(await getAllSentProtos(), 1); | ||||||
|       assert.lengthOf(await _getAllSentProtoRecipients(), 3); |       assert.lengthOf(await _getAllSentProtoRecipients(), 3); | ||||||
| 
 | 
 | ||||||
|       await deleteSentProtoRecipient({ |       { | ||||||
|         timestamp, |         const { successfulPhoneNumberShares } = await deleteSentProtoRecipient({ | ||||||
|         recipientUuid: recipientUuid1, |           timestamp, | ||||||
|         deviceId: 1, |           recipientUuid: recipientUuid1, | ||||||
|       }); |           deviceId: 1, | ||||||
|  |         }); | ||||||
|  |         assert.lengthOf(successfulPhoneNumberShares, 0); | ||||||
|  |       } | ||||||
| 
 | 
 | ||||||
|       assert.lengthOf(await getAllSentProtos(), 1); |       assert.lengthOf(await getAllSentProtos(), 1); | ||||||
|       assert.lengthOf(await _getAllSentProtoRecipients(), 2); |       assert.lengthOf(await _getAllSentProtoRecipients(), 2); | ||||||
| 
 | 
 | ||||||
|       await deleteSentProtoRecipient({ |       { | ||||||
|         timestamp, |         const { successfulPhoneNumberShares } = await deleteSentProtoRecipient({ | ||||||
|         recipientUuid: recipientUuid1, |           timestamp, | ||||||
|         deviceId: 2, |           recipientUuid: recipientUuid1, | ||||||
|       }); |           deviceId: 2, | ||||||
|  |         }); | ||||||
|  |         assert.lengthOf(successfulPhoneNumberShares, 0); | ||||||
|  |       } | ||||||
| 
 | 
 | ||||||
|       assert.lengthOf(await getAllSentProtos(), 1); |       assert.lengthOf(await getAllSentProtos(), 1); | ||||||
|       assert.lengthOf(await _getAllSentProtoRecipients(), 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, |         timestamp, | ||||||
|         recipientUuid: recipientUuid2, |         urgent: true, | ||||||
|         deviceId: 1, |         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 getAllSentProtos(), 0); | ||||||
|       assert.lengthOf(await _getAllSentProtoRecipients(), 0); |       assert.lengthOf(await _getAllSentProtoRecipients(), 0); | ||||||
|     }); |     }); | ||||||
|  | @ -436,6 +528,7 @@ describe('sql/sendLog', () => { | ||||||
|         proto: getRandomBytes(128), |         proto: getRandomBytes(128), | ||||||
|         timestamp, |         timestamp, | ||||||
|         urgent: true, |         urgent: true, | ||||||
|  |         hasPniSignatureMessage: false, | ||||||
|       }; |       }; | ||||||
|       await insertSentProto(proto, { |       await insertSentProto(proto, { | ||||||
|         messageIds: [getUuid()], |         messageIds: [getUuid()], | ||||||
|  | @ -448,7 +541,7 @@ describe('sql/sendLog', () => { | ||||||
|       assert.lengthOf(await getAllSentProtos(), 1); |       assert.lengthOf(await getAllSentProtos(), 1); | ||||||
|       assert.lengthOf(await _getAllSentProtoRecipients(), 3); |       assert.lengthOf(await _getAllSentProtoRecipients(), 3); | ||||||
| 
 | 
 | ||||||
|       await deleteSentProtoRecipient([ |       const { successfulPhoneNumberShares } = await deleteSentProtoRecipient([ | ||||||
|         { |         { | ||||||
|           timestamp, |           timestamp, | ||||||
|           recipientUuid: recipientUuid1, |           recipientUuid: recipientUuid1, | ||||||
|  | @ -465,6 +558,7 @@ describe('sql/sendLog', () => { | ||||||
|           deviceId: 1, |           deviceId: 1, | ||||||
|         }, |         }, | ||||||
|       ]); |       ]); | ||||||
|  |       assert.lengthOf(successfulPhoneNumberShares, 0); | ||||||
| 
 | 
 | ||||||
|       assert.lengthOf(await getAllSentProtos(), 0); |       assert.lengthOf(await getAllSentProtos(), 0); | ||||||
|       assert.lengthOf(await _getAllSentProtoRecipients(), 0); |       assert.lengthOf(await _getAllSentProtoRecipients(), 0); | ||||||
|  | @ -482,6 +576,7 @@ describe('sql/sendLog', () => { | ||||||
|         proto: getRandomBytes(128), |         proto: getRandomBytes(128), | ||||||
|         timestamp, |         timestamp, | ||||||
|         urgent: true, |         urgent: true, | ||||||
|  |         hasPniSignatureMessage: false, | ||||||
|       }; |       }; | ||||||
|       await insertSentProto(proto, { |       await insertSentProto(proto, { | ||||||
|         messageIds, |         messageIds, | ||||||
|  | @ -518,6 +613,7 @@ describe('sql/sendLog', () => { | ||||||
|         proto: getRandomBytes(128), |         proto: getRandomBytes(128), | ||||||
|         timestamp, |         timestamp, | ||||||
|         urgent: true, |         urgent: true, | ||||||
|  |         hasPniSignatureMessage: false, | ||||||
|       }; |       }; | ||||||
|       await insertSentProto(proto, { |       await insertSentProto(proto, { | ||||||
|         messageIds: [], |         messageIds: [], | ||||||
|  | @ -554,6 +650,7 @@ describe('sql/sendLog', () => { | ||||||
|         proto: getRandomBytes(128), |         proto: getRandomBytes(128), | ||||||
|         timestamp, |         timestamp, | ||||||
|         urgent: true, |         urgent: true, | ||||||
|  |         hasPniSignatureMessage: false, | ||||||
|       }; |       }; | ||||||
|       await insertSentProto(proto, { |       await insertSentProto(proto, { | ||||||
|         messageIds: [getUuid()], |         messageIds: [getUuid()], | ||||||
|  | @ -583,6 +680,7 @@ describe('sql/sendLog', () => { | ||||||
|         proto: getRandomBytes(128), |         proto: getRandomBytes(128), | ||||||
|         timestamp, |         timestamp, | ||||||
|         urgent: true, |         urgent: true, | ||||||
|  |         hasPniSignatureMessage: false, | ||||||
|       }; |       }; | ||||||
|       await insertSentProto(proto, { |       await insertSentProto(proto, { | ||||||
|         messageIds: [getUuid()], |         messageIds: [getUuid()], | ||||||
|  | @ -613,6 +711,7 @@ describe('sql/sendLog', () => { | ||||||
|         proto: getRandomBytes(128), |         proto: getRandomBytes(128), | ||||||
|         timestamp, |         timestamp, | ||||||
|         urgent: true, |         urgent: true, | ||||||
|  |         hasPniSignatureMessage: false, | ||||||
|       }; |       }; | ||||||
|       await insertSentProto(proto, { |       await insertSentProto(proto, { | ||||||
|         messageIds: [getUuid()], |         messageIds: [getUuid()], | ||||||
|  |  | ||||||
|  | @ -44,8 +44,7 @@ describe('AccountManager', () => { | ||||||
| 
 | 
 | ||||||
|       window.textsecure.storage.user.getUuid = () => ourUuid; |       window.textsecure.storage.user.getUuid = () => ourUuid; | ||||||
| 
 | 
 | ||||||
|       window.textsecure.storage.protocol.getIdentityKeyPair = async () => |       window.textsecure.storage.protocol.getIdentityKeyPair = () => identityKey; | ||||||
|         identityKey; |  | ||||||
|       window.textsecure.storage.protocol.loadSignedPreKeys = async () => |       window.textsecure.storage.protocol.loadSignedPreKeys = async () => | ||||||
|         signedPreKeys; |         signedPreKeys; | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  | @ -10,7 +10,7 @@ import type { App } from '../bootstrap'; | ||||||
| 
 | 
 | ||||||
| export const debug = createDebug('mock:test:change-number'); | export const debug = createDebug('mock:test:change-number'); | ||||||
| 
 | 
 | ||||||
| describe('change number', function needsName() { | describe('PNP change number', function needsName() { | ||||||
|   this.timeout(durations.MINUTE); |   this.timeout(durations.MINUTE); | ||||||
| 
 | 
 | ||||||
|   let bootstrap: Bootstrap; |   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({ |     pniContact = await server.createPrimaryDevice({ | ||||||
|       profileName: 'My profile is a secret', |       profileName: 'My profile is a secret', | ||||||
|     }); |     }); | ||||||
|     state = state.addContact(pniContact, { |     state = state.addContact( | ||||||
|       identityState: Proto.ContactRecord.IdentityState.VERIFIED, |       pniContact, | ||||||
|       whitelisted: true, |       { | ||||||
|  |         identityState: Proto.ContactRecord.IdentityState.VERIFIED, | ||||||
|  |         whitelisted: true, | ||||||
| 
 | 
 | ||||||
|       identityKey: pniContact.getPublicKey(UUIDKind.PNI).serialize(), |         identityKey: pniContact.getPublicKey(UUIDKind.PNI).serialize(), | ||||||
| 
 | 
 | ||||||
|       // Give PNI as the uuid!
 |         givenName: 'PNI Contact', | ||||||
|       serviceUuid: pniContact.device.pni, |       }, | ||||||
|       givenName: 'PNI Contact', |       UUIDKind.PNI | ||||||
|     }); |     ); | ||||||
| 
 | 
 | ||||||
|     state = state.addRecord({ |     state = state.addRecord({ | ||||||
|       type: IdentifierType.STORY_DISTRIBUTION_LIST, |       type: IdentifierType.STORY_DISTRIBUTION_LIST, | ||||||
|  | @ -107,7 +107,7 @@ export default class AccountManager extends EventTarget { | ||||||
|   async decryptDeviceName(base64: string): Promise<string> { |   async decryptDeviceName(base64: string): Promise<string> { | ||||||
|     const ourUuid = window.textsecure.storage.user.getCheckedUuid(); |     const ourUuid = window.textsecure.storage.user.getCheckedUuid(); | ||||||
|     const identityKey = |     const identityKey = | ||||||
|       await window.textsecure.storage.protocol.getIdentityKeyPair(ourUuid); |       window.textsecure.storage.protocol.getIdentityKeyPair(ourUuid); | ||||||
|     if (!identityKey) { |     if (!identityKey) { | ||||||
|       throw new Error('decryptDeviceName: No identity key pair!'); |       throw new Error('decryptDeviceName: No identity key pair!'); | ||||||
|     } |     } | ||||||
|  | @ -132,7 +132,7 @@ export default class AccountManager extends EventTarget { | ||||||
|     } |     } | ||||||
|     const { storage } = window.textsecure; |     const { storage } = window.textsecure; | ||||||
|     const deviceName = storage.user.getDeviceName(); |     const deviceName = storage.user.getDeviceName(); | ||||||
|     const identityKeyPair = await storage.protocol.getIdentityKeyPair( |     const identityKeyPair = storage.protocol.getIdentityKeyPair( | ||||||
|       storage.user.getCheckedUuid() |       storage.user.getCheckedUuid() | ||||||
|     ); |     ); | ||||||
|     strictAssert( |     strictAssert( | ||||||
|  | @ -362,7 +362,7 @@ export default class AccountManager extends EventTarget { | ||||||
| 
 | 
 | ||||||
|       let identityKey: KeyPairType | undefined; |       let identityKey: KeyPairType | undefined; | ||||||
|       try { |       try { | ||||||
|         identityKey = await store.getIdentityKeyPair(ourUuid); |         identityKey = store.getIdentityKeyPair(ourUuid); | ||||||
|       } catch (error) { |       } catch (error) { | ||||||
|         // We swallow any error here, because we don't want to get into
 |         // We swallow any error here, because we don't want to get into
 | ||||||
|         //   a loop of repeated retries.
 |         //   a loop of repeated retries.
 | ||||||
|  | @ -788,8 +788,7 @@ export default class AccountManager extends EventTarget { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const store = storage.protocol; |     const store = storage.protocol; | ||||||
|     const identityKey = |     const identityKey = maybeIdentityKey ?? store.getIdentityKeyPair(ourUuid); | ||||||
|       maybeIdentityKey ?? (await store.getIdentityKeyPair(ourUuid)); |  | ||||||
|     strictAssert(identityKey, 'generateKeys: No identity key pair!'); |     strictAssert(identityKey, 'generateKeys: No identity key pair!'); | ||||||
| 
 | 
 | ||||||
|     const result: Omit<GeneratedKeysType, 'signedPreKey'> = { |     const result: Omit<GeneratedKeysType, 'signedPreKey'> = { | ||||||
|  |  | ||||||
|  | @ -45,7 +45,7 @@ import { normalizeUuid } from '../util/normalizeUuid'; | ||||||
| import { parseIntOrThrow } from '../util/parseIntOrThrow'; | import { parseIntOrThrow } from '../util/parseIntOrThrow'; | ||||||
| import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary'; | import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary'; | ||||||
| import { Zone } from '../util/Zone'; | import { Zone } from '../util/Zone'; | ||||||
| import { deriveMasterKeyFromGroupV1 } from '../Crypto'; | import { deriveMasterKeyFromGroupV1, bytesToUuid } from '../Crypto'; | ||||||
| import type { DownloadedAttachmentType } from '../types/Attachment'; | import type { DownloadedAttachmentType } from '../types/Attachment'; | ||||||
| import { Address } from '../types/Address'; | import { Address } from '../types/Address'; | ||||||
| import { QualifiedAddress } from '../types/QualifiedAddress'; | import { QualifiedAddress } from '../types/QualifiedAddress'; | ||||||
|  | @ -122,7 +122,8 @@ const GROUPV2_ID_LENGTH = 32; | ||||||
| const RETRY_TIMEOUT = 2 * 60 * 1000; | const RETRY_TIMEOUT = 2 * 60 * 1000; | ||||||
| 
 | 
 | ||||||
| type UnsealedEnvelope = Readonly< | type UnsealedEnvelope = Readonly< | ||||||
|   ProcessedEnvelope & { |   Omit<ProcessedEnvelope, 'sourceUuid'> & { | ||||||
|  |     sourceUuid: UUIDStringType; | ||||||
|     unidentifiedDeliveryReceived?: boolean; |     unidentifiedDeliveryReceived?: boolean; | ||||||
|     contentHint?: number; |     contentHint?: number; | ||||||
|     groupId?: string; |     groupId?: string; | ||||||
|  | @ -133,10 +134,16 @@ type UnsealedEnvelope = Readonly< | ||||||
|   } |   } | ||||||
| >; | >; | ||||||
| 
 | 
 | ||||||
| type DecryptResult = Readonly<{ | type DecryptResult = Readonly< | ||||||
|   envelope: UnsealedEnvelope; |   | { | ||||||
|   plaintext?: Uint8Array; |       envelope: UnsealedEnvelope; | ||||||
| }>; |       plaintext: Uint8Array; | ||||||
|  |     } | ||||||
|  |   | { | ||||||
|  |       envelope?: UnsealedEnvelope; | ||||||
|  |       plaintext?: undefined; | ||||||
|  |     } | ||||||
|  | >; | ||||||
| 
 | 
 | ||||||
| type DecryptSealedSenderResult = Readonly<{ | type DecryptSealedSenderResult = Readonly<{ | ||||||
|   plaintext?: Uint8Array; |   plaintext?: Uint8Array; | ||||||
|  | @ -757,9 +764,9 @@ export default class MessageReceiver | ||||||
|         // Proto.Envelope fields
 |         // Proto.Envelope fields
 | ||||||
|         type: decoded.type, |         type: decoded.type, | ||||||
|         source: item.source, |         source: item.source, | ||||||
|         sourceUuid: decoded.sourceUuid |         sourceUuid: | ||||||
|           ? UUID.cast(decoded.sourceUuid) |           item.sourceUuid || | ||||||
|           : item.sourceUuid, |           (decoded.sourceUuid ? UUID.cast(decoded.sourceUuid) : undefined), | ||||||
|         sourceDevice: decoded.sourceDevice || item.sourceDevice, |         sourceDevice: decoded.sourceDevice || item.sourceDevice, | ||||||
|         destinationUuid: new UUID( |         destinationUuid: new UUID( | ||||||
|           decoded.destinationUuid || item.destinationUuid || ourUuid.toString() |           decoded.destinationUuid || item.destinationUuid || ourUuid.toString() | ||||||
|  | @ -787,10 +794,21 @@ export default class MessageReceiver | ||||||
|           throw new Error('Cached decrypted value was not a string!'); |           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
 |         // Maintain invariant: encrypted queue => decrypted queue
 | ||||||
|         this.addToQueue( |         this.addToQueue( | ||||||
|           async () => { |           async () => { | ||||||
|             this.queueDecryptedEnvelope(envelope, payloadPlaintext); |             this.queueDecryptedEnvelope(decryptedEnvelope, payloadPlaintext); | ||||||
|           }, |           }, | ||||||
|           'queueDecryptedEnvelope', |           'queueDecryptedEnvelope', | ||||||
|           TaskType.Encrypted |           TaskType.Encrypted | ||||||
|  | @ -1088,7 +1106,7 @@ export default class MessageReceiver | ||||||
|             `Rejecting envelope ${getEnvelopeId(envelope)}, ` + |             `Rejecting envelope ${getEnvelopeId(envelope)}, ` + | ||||||
|             `unknown uuid: ${destinationUuid}` |             `unknown uuid: ${destinationUuid}` | ||||||
|         ); |         ); | ||||||
|         return { plaintext: undefined, envelope }; |         return { plaintext: undefined, envelope: undefined }; | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       const unsealedEnvelope = await this.unsealEnvelope( |       const unsealedEnvelope = await this.unsealEnvelope( | ||||||
|  | @ -1099,7 +1117,7 @@ export default class MessageReceiver | ||||||
| 
 | 
 | ||||||
|       // Dropped early
 |       // Dropped early
 | ||||||
|       if (!unsealedEnvelope) { |       if (!unsealedEnvelope) { | ||||||
|         return { plaintext: undefined, envelope }; |         return { plaintext: undefined, envelope: undefined }; | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       logId = getEnvelopeId(unsealedEnvelope); |       logId = getEnvelopeId(unsealedEnvelope); | ||||||
|  | @ -1185,8 +1203,13 @@ export default class MessageReceiver | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (envelope.type !== Proto.Envelope.Type.UNIDENTIFIED_SENDER) { |     if (envelope.type !== Proto.Envelope.Type.UNIDENTIFIED_SENDER) { | ||||||
|  |       strictAssert( | ||||||
|  |         envelope.sourceUuid, | ||||||
|  |         'Unsealed envelope must have source uuid' | ||||||
|  |       ); | ||||||
|       return { |       return { | ||||||
|         ...envelope, |         ...envelope, | ||||||
|  |         sourceUuid: envelope.sourceUuid, | ||||||
|         cipherTextBytes: envelope.content, |         cipherTextBytes: envelope.content, | ||||||
|         cipherTextType: envelopeTypeToCiphertextType(envelope.type), |         cipherTextType: envelopeTypeToCiphertextType(envelope.type), | ||||||
|       }; |       }; | ||||||
|  | @ -1259,6 +1282,10 @@ export default class MessageReceiver | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (envelope.type === Proto.Envelope.Type.RECEIPT) { |     if (envelope.type === Proto.Envelope.Type.RECEIPT) { | ||||||
|  |       strictAssert( | ||||||
|  |         envelope.sourceUuid, | ||||||
|  |         'Unsealed delivery receipt must have sourceUuid' | ||||||
|  |       ); | ||||||
|       await this.onDeliveryReceipt(envelope); |       await this.onDeliveryReceipt(envelope); | ||||||
|       return { plaintext: undefined, envelope }; |       return { plaintext: undefined, envelope }; | ||||||
|     } |     } | ||||||
|  | @ -1291,6 +1318,7 @@ export default class MessageReceiver | ||||||
|     //   sender key to decrypt the next message in the queue!
 |     //   sender key to decrypt the next message in the queue!
 | ||||||
|     let isGroupV2 = false; |     let isGroupV2 = false; | ||||||
| 
 | 
 | ||||||
|  |     let inProgressMessageType = ''; | ||||||
|     try { |     try { | ||||||
|       const content = Proto.Content.decode(plaintext); |       const content = Proto.Content.decode(plaintext); | ||||||
| 
 | 
 | ||||||
|  | @ -1300,6 +1328,7 @@ export default class MessageReceiver | ||||||
|         content.senderKeyDistributionMessage && |         content.senderKeyDistributionMessage && | ||||||
|         Bytes.isNotEmpty(content.senderKeyDistributionMessage) |         Bytes.isNotEmpty(content.senderKeyDistributionMessage) | ||||||
|       ) { |       ) { | ||||||
|  |         inProgressMessageType = 'sender key distribution'; | ||||||
|         await this.handleSenderKeyDistributionMessage( |         await this.handleSenderKeyDistributionMessage( | ||||||
|           stores, |           stores, | ||||||
|           envelope, |           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
 |       // Some sync messages have to be fully processed in the middle of
 | ||||||
|       // decryption queue since subsequent envelopes use their key material.
 |       // decryption queue since subsequent envelopes use their key material.
 | ||||||
|       const { syncMessage } = content; |       const { syncMessage } = content; | ||||||
|       if (syncMessage?.pniIdentity) { |       if (syncMessage?.pniIdentity) { | ||||||
|  |         inProgressMessageType = 'pni identity'; | ||||||
|         await this.handlePNIIdentity(envelope, syncMessage.pniIdentity); |         await this.handlePNIIdentity(envelope, syncMessage.pniIdentity); | ||||||
|         return { plaintext: undefined, envelope }; |         return { plaintext: undefined, envelope }; | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       if (syncMessage?.pniChangeNumber) { |       if (syncMessage?.pniChangeNumber) { | ||||||
|  |         inProgressMessageType = 'pni change number'; | ||||||
|         await this.handlePNIChangeNumber(envelope, syncMessage.pniChangeNumber); |         await this.handlePNIChangeNumber(envelope, syncMessage.pniChangeNumber); | ||||||
|         return { plaintext: undefined, envelope }; |         return { plaintext: undefined, envelope }; | ||||||
|       } |       } | ||||||
|  | 
 | ||||||
|  |       inProgressMessageType = ''; | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       log.error( |       log.error( | ||||||
|         'MessageReceiver.decryptEnvelope: Failed to process sender ' + |         'MessageReceiver.decryptEnvelope: ' + | ||||||
|           `key distribution message: ${Errors.toLogFormat(error)}` |           `Failed to process ${inProgressMessageType} ` + | ||||||
|  |           `message: ${Errors.toLogFormat(error)}` | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -1412,6 +1454,7 @@ export default class MessageReceiver | ||||||
|           source: envelope.source, |           source: envelope.source, | ||||||
|           sourceUuid: envelope.sourceUuid, |           sourceUuid: envelope.sourceUuid, | ||||||
|           sourceDevice: envelope.sourceDevice, |           sourceDevice: envelope.sourceDevice, | ||||||
|  |           wasSentEncrypted: false, | ||||||
|         }, |         }, | ||||||
|         this.removeFromCache.bind(this, envelope) |         this.removeFromCache.bind(this, envelope) | ||||||
|       ) |       ) | ||||||
|  | @ -1549,7 +1592,7 @@ export default class MessageReceiver | ||||||
| 
 | 
 | ||||||
|   private async innerDecrypt( |   private async innerDecrypt( | ||||||
|     stores: LockedStores, |     stores: LockedStores, | ||||||
|     envelope: ProcessedEnvelope, |     envelope: UnsealedEnvelope, | ||||||
|     ciphertext: Uint8Array, |     ciphertext: Uint8Array, | ||||||
|     uuidKind: UUIDKind |     uuidKind: UUIDKind | ||||||
|   ): Promise<Uint8Array | undefined> { |   ): Promise<Uint8Array | undefined> { | ||||||
|  | @ -2014,6 +2057,7 @@ export default class MessageReceiver | ||||||
|         source: envelope.source, |         source: envelope.source, | ||||||
|         sourceUuid: envelope.sourceUuid, |         sourceUuid: envelope.sourceUuid, | ||||||
|         sourceDevice: envelope.sourceDevice, |         sourceDevice: envelope.sourceDevice, | ||||||
|  |         destinationUuid: envelope.destinationUuid.toString(), | ||||||
|         timestamp: envelope.timestamp, |         timestamp: envelope.timestamp, | ||||||
|         serverGuid: envelope.serverGuid, |         serverGuid: envelope.serverGuid, | ||||||
|         serverTimestamp: envelope.serverTimestamp, |         serverTimestamp: envelope.serverTimestamp, | ||||||
|  | @ -2138,6 +2182,7 @@ export default class MessageReceiver | ||||||
|         source: envelope.source, |         source: envelope.source, | ||||||
|         sourceUuid: envelope.sourceUuid, |         sourceUuid: envelope.sourceUuid, | ||||||
|         sourceDevice: envelope.sourceDevice, |         sourceDevice: envelope.sourceDevice, | ||||||
|  |         destinationUuid: envelope.destinationUuid.toString(), | ||||||
|         timestamp: envelope.timestamp, |         timestamp: envelope.timestamp, | ||||||
|         serverGuid: envelope.serverGuid, |         serverGuid: envelope.serverGuid, | ||||||
|         serverTimestamp: envelope.serverTimestamp, |         serverTimestamp: envelope.serverTimestamp, | ||||||
|  | @ -2154,8 +2199,8 @@ export default class MessageReceiver | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private async maybeUpdateTimestamp( |   private async maybeUpdateTimestamp( | ||||||
|     envelope: ProcessedEnvelope |     envelope: UnsealedEnvelope | ||||||
|   ): Promise<ProcessedEnvelope> { |   ): Promise<UnsealedEnvelope> { | ||||||
|     const { retryPlaceholders } = window.Signal.Services; |     const { retryPlaceholders } = window.Signal.Services; | ||||||
|     if (!retryPlaceholders) { |     if (!retryPlaceholders) { | ||||||
|       log.warn('maybeUpdateTimestamp: retry placeholders not available!'); |       log.warn('maybeUpdateTimestamp: retry placeholders not available!'); | ||||||
|  | @ -2209,7 +2254,7 @@ export default class MessageReceiver | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private async innerHandleContentMessage( |   private async innerHandleContentMessage( | ||||||
|     incomingEnvelope: ProcessedEnvelope, |     incomingEnvelope: UnsealedEnvelope, | ||||||
|     plaintext: Uint8Array |     plaintext: Uint8Array | ||||||
|   ): Promise<void> { |   ): Promise<void> { | ||||||
|     const content = Proto.Content.decode(plaintext); |     const content = Proto.Content.decode(plaintext); | ||||||
|  | @ -2311,7 +2356,7 @@ export default class MessageReceiver | ||||||
| 
 | 
 | ||||||
|   private async handleSenderKeyDistributionMessage( |   private async handleSenderKeyDistributionMessage( | ||||||
|     stores: LockedStores, |     stores: LockedStores, | ||||||
|     envelope: ProcessedEnvelope, |     envelope: UnsealedEnvelope, | ||||||
|     distributionMessage: Uint8Array |     distributionMessage: Uint8Array | ||||||
|   ): Promise<void> { |   ): Promise<void> { | ||||||
|     const envelopeId = getEnvelopeId(envelope); |     const envelopeId = getEnvelopeId(envelope); | ||||||
|  | @ -2324,11 +2369,6 @@ export default class MessageReceiver | ||||||
| 
 | 
 | ||||||
|     const identifier = envelope.sourceUuid; |     const identifier = envelope.sourceUuid; | ||||||
|     const { sourceDevice } = envelope; |     const { sourceDevice } = envelope; | ||||||
|     if (!identifier) { |  | ||||||
|       throw new Error( |  | ||||||
|         `handleSenderKeyDistributionMessage: No identifier for envelope ${envelopeId}` |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
|     if (!isNumber(sourceDevice)) { |     if (!isNumber(sourceDevice)) { | ||||||
|       throw new Error( |       throw new Error( | ||||||
|         `handleSenderKeyDistributionMessage: Missing sourceDevice for envelope ${envelopeId}` |         `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( |   private async handleCallingMessage( | ||||||
|     envelope: ProcessedEnvelope, |     envelope: UnsealedEnvelope, | ||||||
|     callingMessage: Proto.ICallingMessage |     callingMessage: Proto.ICallingMessage | ||||||
|   ): Promise<void> { |   ): Promise<void> { | ||||||
|     logUnexpectedUrgentValue(envelope, 'callingMessage'); |     logUnexpectedUrgentValue(envelope, 'callingMessage'); | ||||||
|  | @ -2372,7 +2448,7 @@ export default class MessageReceiver | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private async handleReceiptMessage( |   private async handleReceiptMessage( | ||||||
|     envelope: ProcessedEnvelope, |     envelope: UnsealedEnvelope, | ||||||
|     receiptMessage: Proto.IReceiptMessage |     receiptMessage: Proto.IReceiptMessage | ||||||
|   ): Promise<void> { |   ): Promise<void> { | ||||||
|     strictAssert(receiptMessage.timestamp, 'Receipt message without timestamp'); |     strictAssert(receiptMessage.timestamp, 'Receipt message without timestamp'); | ||||||
|  | @ -2409,6 +2485,7 @@ export default class MessageReceiver | ||||||
|             source: envelope.source, |             source: envelope.source, | ||||||
|             sourceUuid: envelope.sourceUuid, |             sourceUuid: envelope.sourceUuid, | ||||||
|             sourceDevice: envelope.sourceDevice, |             sourceDevice: envelope.sourceDevice, | ||||||
|  |             wasSentEncrypted: true, | ||||||
|           }, |           }, | ||||||
|           this.removeFromCache.bind(this, envelope) |           this.removeFromCache.bind(this, envelope) | ||||||
|         ); |         ); | ||||||
|  | @ -2418,7 +2495,7 @@ export default class MessageReceiver | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private async handleTypingMessage( |   private async handleTypingMessage( | ||||||
|     envelope: ProcessedEnvelope, |     envelope: UnsealedEnvelope, | ||||||
|     typingMessage: Proto.ITypingMessage |     typingMessage: Proto.ITypingMessage | ||||||
|   ): Promise<void> { |   ): Promise<void> { | ||||||
|     this.removeFromCache(envelope); |     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)); |     log.info('MessageReceiver.handleNullMessage', getEnvelopeId(envelope)); | ||||||
| 
 | 
 | ||||||
|     logUnexpectedUrgentValue(envelope, 'nullMessage'); |     logUnexpectedUrgentValue(envelope, 'nullMessage'); | ||||||
|  | @ -2591,7 +2668,7 @@ export default class MessageReceiver | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private async handleSyncMessage( |   private async handleSyncMessage( | ||||||
|     envelope: ProcessedEnvelope, |     envelope: UnsealedEnvelope, | ||||||
|     syncMessage: ProcessedSyncMessage |     syncMessage: ProcessedSyncMessage | ||||||
|   ): Promise<void> { |   ): Promise<void> { | ||||||
|     const ourNumber = this.storage.user.getNumber(); |     const ourNumber = this.storage.user.getNumber(); | ||||||
|  |  | ||||||
|  | @ -196,9 +196,13 @@ export default class OutgoingMessage { | ||||||
|       const contentProto = this.getContentProtoBytes(); |       const contentProto = this.getContentProtoBytes(); | ||||||
|       const { timestamp, contentHint, recipients, urgent } = this; |       const { timestamp, contentHint, recipients, urgent } = this; | ||||||
|       let dataMessage: Uint8Array | undefined; |       let dataMessage: Uint8Array | undefined; | ||||||
|  |       let hasPniSignatureMessage = false; | ||||||
| 
 | 
 | ||||||
|       if (proto instanceof Proto.Content && proto.dataMessage) { |       if (proto instanceof Proto.Content) { | ||||||
|         dataMessage = Proto.DataMessage.encode(proto.dataMessage).finish(); |         if (proto.dataMessage) { | ||||||
|  |           dataMessage = Proto.DataMessage.encode(proto.dataMessage).finish(); | ||||||
|  |         } | ||||||
|  |         hasPniSignatureMessage = Boolean(proto.pniSignatureMessage); | ||||||
|       } else if (proto instanceof Proto.DataMessage) { |       } else if (proto instanceof Proto.DataMessage) { | ||||||
|         dataMessage = Proto.DataMessage.encode(proto).finish(); |         dataMessage = Proto.DataMessage.encode(proto).finish(); | ||||||
|       } |       } | ||||||
|  | @ -215,6 +219,7 @@ export default class OutgoingMessage { | ||||||
|         contentProto, |         contentProto, | ||||||
|         timestamp, |         timestamp, | ||||||
|         urgent, |         urgent, | ||||||
|  |         hasPniSignatureMessage, | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  | @ -15,8 +15,9 @@ import { | ||||||
| } from '@signalapp/libsignal-client'; | } from '@signalapp/libsignal-client'; | ||||||
| 
 | 
 | ||||||
| import type { QuotedMessageType } from '../model-types.d'; | import type { QuotedMessageType } from '../model-types.d'; | ||||||
|  | import type { ConversationModel } from '../models/conversations'; | ||||||
| import { GLOBAL_ZONE } from '../SignalProtocolStore'; | import { GLOBAL_ZONE } from '../SignalProtocolStore'; | ||||||
| import { assert } from '../util/assert'; | import { assert, strictAssert } from '../util/assert'; | ||||||
| import { parseIntOrThrow } from '../util/parseIntOrThrow'; | import { parseIntOrThrow } from '../util/parseIntOrThrow'; | ||||||
| import { Address } from '../types/Address'; | import { Address } from '../types/Address'; | ||||||
| import { QualifiedAddress } from '../types/QualifiedAddress'; | import { QualifiedAddress } from '../types/QualifiedAddress'; | ||||||
|  | @ -65,6 +66,7 @@ import type { | ||||||
| import { concat, isEmpty, map } from '../util/iterables'; | import { concat, isEmpty, map } from '../util/iterables'; | ||||||
| import type { SendTypesType } from '../util/handleMessageSend'; | import type { SendTypesType } from '../util/handleMessageSend'; | ||||||
| import { shouldSaveProto, sendTypesEnum } from '../util/handleMessageSend'; | import { shouldSaveProto, sendTypesEnum } from '../util/handleMessageSend'; | ||||||
|  | import { uuidToBytes } from '../util/uuidToBytes'; | ||||||
| import { SignalService as Proto } from '../protobuf'; | import { SignalService as Proto } from '../protobuf'; | ||||||
| import * as log from '../logging/log'; | import * as log from '../logging/log'; | ||||||
| import type { Avatar, EmbeddedContactType } from '../types/EmbeddedContact'; | import type { Avatar, EmbeddedContactType } from '../types/EmbeddedContact'; | ||||||
|  | @ -574,11 +576,43 @@ class Message { | ||||||
|     return proto; |     return proto; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   encode() { |   encode(): Uint8Array { | ||||||
|     return Proto.DataMessage.encode(this.toProto()).finish(); |     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 { | export default class MessageSender { | ||||||
|   pendingMessages: { |   pendingMessages: { | ||||||
|     [id: string]: PQueue; |     [id: string]: PQueue; | ||||||
|  | @ -944,7 +978,10 @@ export default class MessageSender { | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async getContentMessage( |   async getContentMessage( | ||||||
|     options: Readonly<MessageOptionsType> |     options: Readonly<MessageOptionsType> & | ||||||
|  |       Readonly<{ | ||||||
|  |         includePniSignatureMessage?: boolean; | ||||||
|  |       }> | ||||||
|   ): Promise<Proto.Content> { |   ): Promise<Proto.Content> { | ||||||
|     const message = await this.getHydratedMessage(options); |     const message = await this.getHydratedMessage(options); | ||||||
|     const dataMessage = message.toProto(); |     const dataMessage = message.toProto(); | ||||||
|  | @ -952,6 +989,24 @@ export default class MessageSender { | ||||||
|     const contentMessage = new Proto.Content(); |     const contentMessage = new Proto.Content(); | ||||||
|     contentMessage.dataMessage = dataMessage; |     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; |     return contentMessage; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | @ -1001,6 +1056,14 @@ export default class MessageSender { | ||||||
|     const contentMessage = new Proto.Content(); |     const contentMessage = new Proto.Content(); | ||||||
|     contentMessage.typingMessage = typingMessage; |     contentMessage.typingMessage = typingMessage; | ||||||
| 
 | 
 | ||||||
|  |     if (recipientId) { | ||||||
|  |       addPniSignatureMessageToProto({ | ||||||
|  |         conversation: window.ConversationController.get(recipientId), | ||||||
|  |         proto: contentMessage, | ||||||
|  |         reason: `getTypingContentMessage(${finalTimestamp})`, | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     return contentMessage; |     return contentMessage; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | @ -1100,14 +1163,19 @@ export default class MessageSender { | ||||||
|     groupId, |     groupId, | ||||||
|     options, |     options, | ||||||
|     urgent, |     urgent, | ||||||
|  |     includePniSignatureMessage, | ||||||
|   }: Readonly<{ |   }: Readonly<{ | ||||||
|     messageOptions: MessageOptionsType; |     messageOptions: MessageOptionsType; | ||||||
|     contentHint: number; |     contentHint: number; | ||||||
|     groupId: string | undefined; |     groupId: string | undefined; | ||||||
|     options?: SendOptionsType; |     options?: SendOptionsType; | ||||||
|     urgent: boolean; |     urgent: boolean; | ||||||
|  |     includePniSignatureMessage?: boolean; | ||||||
|   }>): Promise<CallbackResultType> { |   }>): Promise<CallbackResultType> { | ||||||
|     const message = await this.getHydratedMessage(messageOptions); |     const proto = await this.getContentMessage({ | ||||||
|  |       ...messageOptions, | ||||||
|  |       includePniSignatureMessage, | ||||||
|  |     }); | ||||||
| 
 | 
 | ||||||
|     return new Promise((resolve, reject) => { |     return new Promise((resolve, reject) => { | ||||||
|       this.sendMessageProto({ |       this.sendMessageProto({ | ||||||
|  | @ -1121,9 +1189,9 @@ export default class MessageSender { | ||||||
|         contentHint, |         contentHint, | ||||||
|         groupId, |         groupId, | ||||||
|         options, |         options, | ||||||
|         proto: message.toProto(), |         proto, | ||||||
|         recipients: message.recipients || [], |         recipients: messageOptions.recipients || [], | ||||||
|         timestamp: message.timestamp, |         timestamp: messageOptions.timestamp, | ||||||
|         urgent, |         urgent, | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|  | @ -1276,6 +1344,7 @@ export default class MessageSender { | ||||||
|     storyContext, |     storyContext, | ||||||
|     timestamp, |     timestamp, | ||||||
|     urgent, |     urgent, | ||||||
|  |     includePniSignatureMessage, | ||||||
|   }: Readonly<{ |   }: Readonly<{ | ||||||
|     attachments: ReadonlyArray<AttachmentType> | undefined; |     attachments: ReadonlyArray<AttachmentType> | undefined; | ||||||
|     contact?: Array<ContactWithHydratedAvatar>; |     contact?: Array<ContactWithHydratedAvatar>; | ||||||
|  | @ -1294,6 +1363,7 @@ export default class MessageSender { | ||||||
|     storyContext?: StoryContextType; |     storyContext?: StoryContextType; | ||||||
|     timestamp: number; |     timestamp: number; | ||||||
|     urgent: boolean; |     urgent: boolean; | ||||||
|  |     includePniSignatureMessage?: boolean; | ||||||
|   }>): Promise<CallbackResultType> { |   }>): Promise<CallbackResultType> { | ||||||
|     return this.sendMessage({ |     return this.sendMessage({ | ||||||
|       messageOptions: { |       messageOptions: { | ||||||
|  | @ -1315,6 +1385,7 @@ export default class MessageSender { | ||||||
|       groupId, |       groupId, | ||||||
|       options, |       options, | ||||||
|       urgent, |       urgent, | ||||||
|  |       includePniSignatureMessage, | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | @ -1886,6 +1957,14 @@ export default class MessageSender { | ||||||
|     const contentMessage = new Proto.Content(); |     const contentMessage = new Proto.Content(); | ||||||
|     contentMessage.callingMessage = callingMessage; |     contentMessage.callingMessage = callingMessage; | ||||||
| 
 | 
 | ||||||
|  |     const conversation = window.ConversationController.get(recipientId); | ||||||
|  | 
 | ||||||
|  |     addPniSignatureMessageToProto({ | ||||||
|  |       conversation, | ||||||
|  |       proto: contentMessage, | ||||||
|  |       reason: `sendCallingMessage(${finalTimestamp})`, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|     const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; |     const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; | ||||||
| 
 | 
 | ||||||
|     return this.sendMessageProtoAndWait({ |     return this.sendMessageProtoAndWait({ | ||||||
|  | @ -1904,6 +1983,7 @@ export default class MessageSender { | ||||||
|       senderE164?: string; |       senderE164?: string; | ||||||
|       senderUuid?: string; |       senderUuid?: string; | ||||||
|       timestamps: Array<number>; |       timestamps: Array<number>; | ||||||
|  |       isDirectConversation: boolean; | ||||||
|       options?: Readonly<SendOptionsType>; |       options?: Readonly<SendOptionsType>; | ||||||
|     }> |     }> | ||||||
|   ): Promise<CallbackResultType> { |   ): Promise<CallbackResultType> { | ||||||
|  | @ -1918,6 +1998,7 @@ export default class MessageSender { | ||||||
|       senderE164?: string; |       senderE164?: string; | ||||||
|       senderUuid?: string; |       senderUuid?: string; | ||||||
|       timestamps: Array<number>; |       timestamps: Array<number>; | ||||||
|  |       isDirectConversation: boolean; | ||||||
|       options?: Readonly<SendOptionsType>; |       options?: Readonly<SendOptionsType>; | ||||||
|     }> |     }> | ||||||
|   ): Promise<CallbackResultType> { |   ): Promise<CallbackResultType> { | ||||||
|  | @ -1932,6 +2013,7 @@ export default class MessageSender { | ||||||
|       senderE164?: string; |       senderE164?: string; | ||||||
|       senderUuid?: string; |       senderUuid?: string; | ||||||
|       timestamps: Array<number>; |       timestamps: Array<number>; | ||||||
|  |       isDirectConversation: boolean; | ||||||
|       options?: Readonly<SendOptionsType>; |       options?: Readonly<SendOptionsType>; | ||||||
|     }> |     }> | ||||||
|   ): Promise<CallbackResultType> { |   ): Promise<CallbackResultType> { | ||||||
|  | @ -1946,12 +2028,14 @@ export default class MessageSender { | ||||||
|     senderUuid, |     senderUuid, | ||||||
|     timestamps, |     timestamps, | ||||||
|     type, |     type, | ||||||
|  |     isDirectConversation, | ||||||
|     options, |     options, | ||||||
|   }: Readonly<{ |   }: Readonly<{ | ||||||
|     senderE164?: string; |     senderE164?: string; | ||||||
|     senderUuid?: string; |     senderUuid?: string; | ||||||
|     timestamps: Array<number>; |     timestamps: Array<number>; | ||||||
|     type: Proto.ReceiptMessage.Type; |     type: Proto.ReceiptMessage.Type; | ||||||
|  |     isDirectConversation: boolean; | ||||||
|     options?: Readonly<SendOptionsType>; |     options?: Readonly<SendOptionsType>; | ||||||
|   }>): Promise<CallbackResultType> { |   }>): Promise<CallbackResultType> { | ||||||
|     if (!senderUuid && !senderE164) { |     if (!senderUuid && !senderE164) { | ||||||
|  | @ -1960,21 +2044,35 @@ export default class MessageSender { | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     const timestamp = Date.now(); | ||||||
|  | 
 | ||||||
|     const receiptMessage = new Proto.ReceiptMessage(); |     const receiptMessage = new Proto.ReceiptMessage(); | ||||||
|     receiptMessage.type = type; |     receiptMessage.type = type; | ||||||
|     receiptMessage.timestamp = timestamps.map(timestamp => |     receiptMessage.timestamp = timestamps.map(receiptTimestamp => | ||||||
|       Long.fromNumber(timestamp) |       Long.fromNumber(receiptTimestamp) | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     const contentMessage = new Proto.Content(); |     const contentMessage = new Proto.Content(); | ||||||
|     contentMessage.receiptMessage = receiptMessage; |     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; |     const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; | ||||||
| 
 | 
 | ||||||
|     return this.sendIndividualProto({ |     return this.sendIndividualProto({ | ||||||
|       identifier: senderUuid || senderE164, |       identifier: senderUuid || senderE164, | ||||||
|       proto: contentMessage, |       proto: contentMessage, | ||||||
|       timestamp: Date.now(), |       timestamp, | ||||||
|       contentHint: ContentHint.RESENDABLE, |       contentHint: ContentHint.RESENDABLE, | ||||||
|       options, |       options, | ||||||
|       urgent: false, |       urgent: false, | ||||||
|  | @ -2052,6 +2150,7 @@ export default class MessageSender { | ||||||
|     sendType, |     sendType, | ||||||
|     timestamp, |     timestamp, | ||||||
|     urgent, |     urgent, | ||||||
|  |     hasPniSignatureMessage, | ||||||
|   }: Readonly<{ |   }: Readonly<{ | ||||||
|     contentHint: number; |     contentHint: number; | ||||||
|     messageId?: string; |     messageId?: string; | ||||||
|  | @ -2059,6 +2158,7 @@ export default class MessageSender { | ||||||
|     sendType: SendTypesType; |     sendType: SendTypesType; | ||||||
|     timestamp: number; |     timestamp: number; | ||||||
|     urgent: boolean; |     urgent: boolean; | ||||||
|  |     hasPniSignatureMessage: boolean; | ||||||
|   }>): SendLogCallbackType { |   }>): SendLogCallbackType { | ||||||
|     let initialSavePromise: Promise<number>; |     let initialSavePromise: Promise<number>; | ||||||
| 
 | 
 | ||||||
|  | @ -2095,6 +2195,7 @@ export default class MessageSender { | ||||||
|             proto, |             proto, | ||||||
|             timestamp, |             timestamp, | ||||||
|             urgent, |             urgent, | ||||||
|  |             hasPniSignatureMessage, | ||||||
|           }, |           }, | ||||||
|           { |           { | ||||||
|             recipients: { [recipientUuid]: deviceIds }, |             recipients: { [recipientUuid]: deviceIds }, | ||||||
|  | @ -2270,6 +2371,7 @@ export default class MessageSender { | ||||||
|             sendType: 'senderKeyDistributionMessage', |             sendType: 'senderKeyDistributionMessage', | ||||||
|             timestamp, |             timestamp, | ||||||
|             urgent, |             urgent, | ||||||
|  |             hasPniSignatureMessage: false, | ||||||
|           }) |           }) | ||||||
|         : undefined; |         : undefined; | ||||||
| 
 | 
 | ||||||
|  | @ -2313,6 +2415,7 @@ export default class MessageSender { | ||||||
|             sendType: 'legacyGroupChange', |             sendType: 'legacyGroupChange', | ||||||
|             timestamp, |             timestamp, | ||||||
|             urgent: false, |             urgent: false, | ||||||
|  |             hasPniSignatureMessage: false, | ||||||
|           }) |           }) | ||||||
|         : undefined; |         : undefined; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										6
									
								
								ts/textsecure/Types.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								ts/textsecure/Types.d.ts
									
										
									
									
										vendored
									
									
								
							|  | @ -267,6 +267,7 @@ export interface CallbackResultType { | ||||||
|   timestamp?: number; |   timestamp?: number; | ||||||
|   recipients?: Record<string, Array<number>>; |   recipients?: Record<string, Array<number>>; | ||||||
|   urgent?: boolean; |   urgent?: boolean; | ||||||
|  |   hasPniSignatureMessage?: boolean; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface IRequestHandler { | export interface IRequestHandler { | ||||||
|  | @ -278,3 +279,8 @@ export type PniKeyMaterialType = Readonly<{ | ||||||
|   signedPreKey: Uint8Array; |   signedPreKey: Uint8Array; | ||||||
|   registrationId: number; |   registrationId: number; | ||||||
| }>; | }>; | ||||||
|  | 
 | ||||||
|  | export type PniSignatureMessageType = Readonly<{ | ||||||
|  |   pni: UUIDStringType; | ||||||
|  |   signature: Uint8Array; | ||||||
|  | }>; | ||||||
|  |  | ||||||
|  | @ -132,6 +132,7 @@ export type DeliveryEventData = Readonly<{ | ||||||
|   source?: string; |   source?: string; | ||||||
|   sourceUuid?: UUIDStringType; |   sourceUuid?: UUIDStringType; | ||||||
|   sourceDevice?: number; |   sourceDevice?: number; | ||||||
|  |   wasSentEncrypted: boolean; | ||||||
| }>; | }>; | ||||||
| 
 | 
 | ||||||
| export class DeliveryEvent extends ConfirmableEvent { | export class DeliveryEvent extends ConfirmableEvent { | ||||||
|  | @ -220,8 +221,9 @@ export class ProfileKeyUpdateEvent extends ConfirmableEvent { | ||||||
| 
 | 
 | ||||||
| export type MessageEventData = Readonly<{ | export type MessageEventData = Readonly<{ | ||||||
|   source?: string; |   source?: string; | ||||||
|   sourceUuid?: UUIDStringType; |   sourceUuid: UUIDStringType; | ||||||
|   sourceDevice?: number; |   sourceDevice?: number; | ||||||
|  |   destinationUuid: UUIDStringType; | ||||||
|   timestamp: number; |   timestamp: number; | ||||||
|   serverGuid?: string; |   serverGuid?: string; | ||||||
|   serverTimestamp?: number; |   serverTimestamp?: number; | ||||||
|  | @ -246,6 +248,7 @@ export type ReadOrViewEventData = Readonly<{ | ||||||
|   source?: string; |   source?: string; | ||||||
|   sourceUuid?: UUIDStringType; |   sourceUuid?: UUIDStringType; | ||||||
|   sourceDevice?: number; |   sourceDevice?: number; | ||||||
|  |   wasSentEncrypted: true; | ||||||
| }>; | }>; | ||||||
| 
 | 
 | ||||||
| export class ReadEvent extends ConfirmableEvent { | export class ReadEvent extends ConfirmableEvent { | ||||||
|  |  | ||||||
|  | @ -8,6 +8,7 @@ export const receiptSchema = z.object({ | ||||||
|   senderE164: z.string().optional(), |   senderE164: z.string().optional(), | ||||||
|   senderUuid: z.string().optional(), |   senderUuid: z.string().optional(), | ||||||
|   timestamp: z.number(), |   timestamp: z.number(), | ||||||
|  |   isDirectConversation: z.boolean().optional(), | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| export enum ReceiptType { | export enum ReceiptType { | ||||||
|  |  | ||||||
|  | @ -10,13 +10,8 @@ import * as Bytes from '../Bytes'; | ||||||
| import { getRandomBytes } from '../Crypto'; | import { getRandomBytes } from '../Crypto'; | ||||||
| import { getConversationMembers } from './getConversationMembers'; | import { getConversationMembers } from './getConversationMembers'; | ||||||
| import { isDirectConversation, isMe } from './whatTypeOfConversation'; | import { isDirectConversation, isMe } from './whatTypeOfConversation'; | ||||||
| import { isInSystemContacts } from './isInSystemContacts'; |  | ||||||
| import { missingCaseError } from './missingCaseError'; |  | ||||||
| import { senderCertificateService } from '../services/senderCertificate'; | import { senderCertificateService } from '../services/senderCertificate'; | ||||||
| import { | import { shouldSharePhoneNumberWith } from './phoneNumberSharingMode'; | ||||||
|   PhoneNumberSharingMode, |  | ||||||
|   parsePhoneNumberSharingMode, |  | ||||||
| } from './phoneNumberSharingMode'; |  | ||||||
| import type { SerializedCertificateType } from '../textsecure/OutgoingMessage'; | import type { SerializedCertificateType } from '../textsecure/OutgoingMessage'; | ||||||
| import { SenderCertificateMode } from '../textsecure/OutgoingMessage'; | import { SenderCertificateMode } from '../textsecure/OutgoingMessage'; | ||||||
| import { isNotNil } from './isNotNil'; | import { isNotNil } from './isNotNil'; | ||||||
|  | @ -146,25 +141,11 @@ function getSenderCertificateForDirectConversation( | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   const phoneNumberSharingMode = parsePhoneNumberSharingMode( |  | ||||||
|     window.storage.get('phoneNumberSharingMode') |  | ||||||
|   ); |  | ||||||
| 
 |  | ||||||
|   let certificateMode: SenderCertificateMode; |   let certificateMode: SenderCertificateMode; | ||||||
|   switch (phoneNumberSharingMode) { |   if (shouldSharePhoneNumberWith(conversationAttrs)) { | ||||||
|     case PhoneNumberSharingMode.Everybody: |     certificateMode = SenderCertificateMode.WithE164; | ||||||
|       certificateMode = SenderCertificateMode.WithE164; |   } else { | ||||||
|       break; |     certificateMode = SenderCertificateMode.WithoutE164; | ||||||
|     case PhoneNumberSharingMode.ContactsOnly: |  | ||||||
|       certificateMode = isInSystemContacts(conversationAttrs) |  | ||||||
|         ? SenderCertificateMode.WithE164 |  | ||||||
|         : SenderCertificateMode.WithoutE164; |  | ||||||
|       break; |  | ||||||
|     case PhoneNumberSharingMode.Nobody: |  | ||||||
|       certificateMode = SenderCertificateMode.WithoutE164; |  | ||||||
|       break; |  | ||||||
|     default: |  | ||||||
|       throw missingCaseError(phoneNumberSharingMode); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   return senderCertificateService.get(certificateMode); |   return senderCertificateService.get(certificateMode); | ||||||
|  |  | ||||||
|  | @ -236,7 +236,14 @@ async function maybeSaveToSendLog( | ||||||
|     sendType: SendTypesType; |     sendType: SendTypesType; | ||||||
|   } |   } | ||||||
| ): Promise<void> { | ): Promise<void> { | ||||||
|   const { contentHint, contentProto, recipients, timestamp, urgent } = result; |   const { | ||||||
|  |     contentHint, | ||||||
|  |     contentProto, | ||||||
|  |     recipients, | ||||||
|  |     timestamp, | ||||||
|  |     urgent, | ||||||
|  |     hasPniSignatureMessage, | ||||||
|  |   } = result; | ||||||
| 
 | 
 | ||||||
|   if (!shouldSaveProto(sendType)) { |   if (!shouldSaveProto(sendType)) { | ||||||
|     return; |     return; | ||||||
|  | @ -268,6 +275,7 @@ async function maybeSaveToSendLog( | ||||||
|       proto: Buffer.from(contentProto), |       proto: Buffer.from(contentProto), | ||||||
|       contentHint, |       contentHint, | ||||||
|       urgent: isBoolean(urgent) ? urgent : true, |       urgent: isBoolean(urgent) ? urgent : true, | ||||||
|  |       hasPniSignatureMessage: Boolean(hasPniSignatureMessage), | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       messageIds, |       messageIds, | ||||||
|  |  | ||||||
|  | @ -10,7 +10,7 @@ import { readSyncJobQueue } from '../jobs/readSyncJobQueue'; | ||||||
| import { notificationService } from '../services/notifications'; | import { notificationService } from '../services/notifications'; | ||||||
| import { expiringMessagesDeletionService } from '../services/expiringMessagesDeletion'; | import { expiringMessagesDeletionService } from '../services/expiringMessagesDeletion'; | ||||||
| import { tapToViewMessagesDeletionService } from '../services/tapToViewMessagesDeletionService'; | import { tapToViewMessagesDeletionService } from '../services/tapToViewMessagesDeletionService'; | ||||||
| import { isGroup } from './whatTypeOfConversation'; | import { isGroup, isDirectConversation } from './whatTypeOfConversation'; | ||||||
| import * as log from '../logging/log'; | import * as log from '../logging/log'; | ||||||
| import { getConversationIdForLogging } from './idForLogging'; | import { getConversationIdForLogging } from './idForLogging'; | ||||||
| import { ReadStatus } from '../messages/MessageReadStatus'; | import { ReadStatus } from '../messages/MessageReadStatus'; | ||||||
|  | @ -94,6 +94,7 @@ export async function markConversationRead( | ||||||
|         uuid: messageSyncData.sourceUuid, |         uuid: messageSyncData.sourceUuid, | ||||||
|       })?.id, |       })?.id, | ||||||
|       timestamp: messageSyncData.sent_at, |       timestamp: messageSyncData.sent_at, | ||||||
|  |       isDirectConversation: isDirectConversation(conversationAttrs), | ||||||
|       hasErrors: message ? hasErrors(message.attributes) : false, |       hasErrors: message ? hasErrors(message.attributes) : false, | ||||||
|     }; |     }; | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|  | @ -1,7 +1,12 @@ | ||||||
| // Copyright 2021 Signal Messenger, LLC
 | // Copyright 2021 Signal Messenger, LLC
 | ||||||
| // SPDX-License-Identifier: AGPL-3.0-only
 | // SPDX-License-Identifier: AGPL-3.0-only
 | ||||||
| 
 | 
 | ||||||
|  | import type { ConversationAttributesType } from '../model-types.d'; | ||||||
|  | 
 | ||||||
| import { makeEnumParser } from './enum'; | 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.
 | // These strings are saved to disk, so be careful when changing them.
 | ||||||
| export enum PhoneNumberSharingMode { | export enum PhoneNumberSharingMode { | ||||||
|  | @ -14,3 +19,26 @@ export const parsePhoneNumberSharingMode = makeEnumParser( | ||||||
|   PhoneNumberSharingMode, |   PhoneNumberSharingMode, | ||||||
|   PhoneNumberSharingMode.Everybody |   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 => { |         map(batches, async batch => { | ||||||
|           const timestamps = batch.map(receipt => receipt.timestamp); |           const timestamps = batch.map(receipt => receipt.timestamp); | ||||||
|           const messageIds = batch.map(receipt => receipt.messageId); |           const messageIds = batch.map(receipt => receipt.messageId); | ||||||
|  |           const isDirectConversation = batch.some( | ||||||
|  |             receipt => receipt.isDirectConversation | ||||||
|  |           ); | ||||||
| 
 | 
 | ||||||
|           await handleMessageSend( |           await handleMessageSend( | ||||||
|             messaging[methodName]({ |             messaging[methodName]({ | ||||||
|               senderE164: sender.get('e164'), |               senderE164: sender.get('e164'), | ||||||
|               senderUuid: sender.get('uuid'), |               senderUuid: sender.get('uuid'), | ||||||
|  |               isDirectConversation, | ||||||
|               timestamps, |               timestamps, | ||||||
|               options: sendOptions, |               options: sendOptions, | ||||||
|             }), |             }), | ||||||
|  |  | ||||||
|  | @ -225,6 +225,7 @@ export async function sendContentMessageToGroup({ | ||||||
|     sendType, |     sendType, | ||||||
|     timestamp, |     timestamp, | ||||||
|     urgent, |     urgent, | ||||||
|  |     hasPniSignatureMessage: false, | ||||||
|   }); |   }); | ||||||
|   const groupId = sendTarget.isGroupV2() ? sendTarget.getGroupId() : undefined; |   const groupId = sendTarget.isGroupV2() ? sendTarget.getGroupId() : undefined; | ||||||
|   return window.textsecure.messaging.sendGroupProto({ |   return window.textsecure.messaging.sendGroupProto({ | ||||||
|  | @ -544,6 +545,7 @@ export async function sendToGroupViaSenderKey(options: { | ||||||
|           proto: Buffer.from(Proto.Content.encode(contentMessage).finish()), |           proto: Buffer.from(Proto.Content.encode(contentMessage).finish()), | ||||||
|           timestamp, |           timestamp, | ||||||
|           urgent, |           urgent, | ||||||
|  |           hasPniSignatureMessage: false, | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|           recipients: senderKeyRecipientsWithDevices, |           recipients: senderKeyRecipientsWithDevices, | ||||||
|  |  | ||||||
|  | @ -794,6 +794,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> { | ||||||
|             senderE164, |             senderE164, | ||||||
|             senderUuid, |             senderUuid, | ||||||
|             timestamp, |             timestamp, | ||||||
|  |             isDirectConversation: isDirectConversation(this.model.attributes), | ||||||
|           }, |           }, | ||||||
|         }); |         }); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|  | @ -1753,10 +1753,10 @@ | ||||||
|     node-gyp-build "^4.2.3" |     node-gyp-build "^4.2.3" | ||||||
|     uuid "^8.3.0" |     uuid "^8.3.0" | ||||||
| 
 | 
 | ||||||
| "@signalapp/mock-server@2.4.1": | "@signalapp/mock-server@2.6.0": | ||||||
|   version "2.4.1" |   version "2.6.0" | ||||||
|   resolved "https://registry.yarnpkg.com/@signalapp/mock-server/-/mock-server-2.4.1.tgz#74db72514319acea828803747082ec8403a4ab04" |   resolved "https://registry.yarnpkg.com/@signalapp/mock-server/-/mock-server-2.6.0.tgz#64277abd5ad5a540c0ae7e98d0347b420d69acfd" | ||||||
|   integrity sha512-TaTIVjHRWtLTJVYuG7GsVdcWeC/OEuRXmlyfp9FGxygvrJncsWG1pCq3YZEHrisAnWJl/Hcogg97lDkUvtjRJA== |   integrity sha512-EYI52E0ZwtNO0tt7V7PZJ5vs5Yy/nReHZMWovfHqcdG3iurwxq4/YIbz0fP4HylpoiJLbZ1cVzY7A8A3IAlrLQ== | ||||||
|   dependencies: |   dependencies: | ||||||
|     "@signalapp/libsignal-client" "^0.19.2" |     "@signalapp/libsignal-client" "^0.19.2" | ||||||
|     debug "^4.3.2" |     debug "^4.3.2" | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Fedor Indutny
				Fedor Indutny