Send and receive PniSignatureMessage

This commit is contained in:
Fedor Indutny 2022-08-15 14:53:33 -07:00 committed by GitHub
parent 95be24e8f7
commit 00cfd92dd0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 1082 additions and 164 deletions

View File

@ -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",

View File

@ -48,6 +48,7 @@ message Content {
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;
}

View File

@ -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');

View File

@ -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) {

View File

@ -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>();

View File

@ -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,20 +2968,29 @@ 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);
} }
if (!message.get('unidentifiedDeliveryReceived')) {
profileKeyResponseQueue.add(() => { profileKeyResponseQueue.add(() => {
respondWithProfileKeyBatcher.add(sender); respondWithProfileKeyBatcher.add(sender);
}); });
} }
}
if (data.message.reaction) { if (data.message.reaction) {
strictAssert( strictAssert(
@ -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);

View File

@ -188,6 +188,7 @@ export async function sendDeleteForEveryone(
profileKey, profileKey,
options: sendOptions, options: sendOptions,
urgent: true, urgent: true,
includePniSignatureMessage: true,
}), }),
sendType, sendType,
timestamp, timestamp,

View File

@ -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) {

View File

@ -283,6 +283,7 @@ export async function sendNormalMessage(
storyContext, storyContext,
timestamp: messageTimestamp, timestamp: messageTimestamp,
urgent: true, urgent: true,
includePniSignatureMessage: true,
}); });
} }

View File

@ -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,

View File

@ -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');

View File

@ -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([
deleteSentProtoBatcher.add({
timestamp: messageSentAt, timestamp: messageSentAt,
recipientUuid, recipientUuid,
deviceId, 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
View File

@ -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;

View File

@ -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;

View File

@ -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),
}); });
}); });
} }

View File

@ -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: {

View File

@ -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;

View File

@ -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<

View 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!');
}

View File

@ -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 {

View File

@ -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];

View File

@ -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');
} }

View File

@ -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,29 +421,98 @@ 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);
await deleteSentProtoRecipient({ {
const { successfulPhoneNumberShares } = await deleteSentProtoRecipient({
timestamp, timestamp,
recipientUuid: recipientUuid1, recipientUuid: recipientUuid1,
deviceId: 2, 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, timestamp,
recipientUuid: recipientUuid2, recipientUuid: recipientUuid2,
deviceId: 1, 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,
urgent: true,
hasPniSignatureMessage: true,
};
await insertSentProto(proto, {
messageIds: [getUuid()],
recipients: {
[recipientUuid1]: [1, 2],
[recipientUuid2]: [1],
},
});
assert.lengthOf(await getAllSentProtos(), 1);
assert.lengthOf(await _getAllSentProtoRecipients(), 3);
{
const { successfulPhoneNumberShares } = await deleteSentProtoRecipient({
timestamp,
recipientUuid: recipientUuid1,
deviceId: 1,
});
assert.lengthOf(successfulPhoneNumberShares, 0);
}
assert.lengthOf(await getAllSentProtos(), 1);
assert.lengthOf(await _getAllSentProtoRecipients(), 2);
{
const { successfulPhoneNumberShares } = await deleteSentProtoRecipient({
timestamp,
recipientUuid: recipientUuid1,
deviceId: 2,
});
assert.deepStrictEqual(successfulPhoneNumberShares, [recipientUuid1]);
}
assert.lengthOf(await getAllSentProtos(), 1);
assert.lengthOf(await _getAllSentProtoRecipients(), 1);
{
const { successfulPhoneNumberShares } = await deleteSentProtoRecipient({
timestamp,
recipientUuid: recipientUuid2,
deviceId: 1,
});
assert.deepStrictEqual(successfulPhoneNumberShares, [recipientUuid2]);
}
assert.lengthOf(await getAllSentProtos(), 0); assert.lengthOf(await 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()],

View File

@ -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;
}); });

View File

@ -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;

View 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);
}
});
});

View File

@ -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(
pniContact,
{
identityState: Proto.ContactRecord.IdentityState.VERIFIED, identityState: Proto.ContactRecord.IdentityState.VERIFIED,
whitelisted: true, whitelisted: true,
identityKey: pniContact.getPublicKey(UUIDKind.PNI).serialize(), identityKey: pniContact.getPublicKey(UUIDKind.PNI).serialize(),
// Give PNI as the uuid!
serviceUuid: pniContact.device.pni,
givenName: 'PNI Contact', givenName: 'PNI Contact',
}); },
UUIDKind.PNI
);
state = state.addRecord({ state = state.addRecord({
type: IdentifierType.STORY_DISTRIBUTION_LIST, type: IdentifierType.STORY_DISTRIBUTION_LIST,

View File

@ -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'> = {

View File

@ -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; envelope: UnsealedEnvelope;
plaintext?: Uint8Array; 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();

View File

@ -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) {
if (proto.dataMessage) {
dataMessage = Proto.DataMessage.encode(proto.dataMessage).finish(); 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,
}); });
} }
} }

View File

@ -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;

View File

@ -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;
}>;

View File

@ -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 {

View File

@ -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 {

View File

@ -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;
break; } else {
case PhoneNumberSharingMode.ContactsOnly:
certificateMode = isInSystemContacts(conversationAttrs)
? SenderCertificateMode.WithE164
: SenderCertificateMode.WithoutE164;
break;
case PhoneNumberSharingMode.Nobody:
certificateMode = SenderCertificateMode.WithoutE164; certificateMode = SenderCertificateMode.WithoutE164;
break;
default:
throw missingCaseError(phoneNumberSharingMode);
} }
return senderCertificateService.get(certificateMode); return senderCertificateService.get(certificateMode);

View File

@ -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,

View File

@ -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,
}; };
}); });

View File

@ -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);
}
};

View File

@ -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,
}), }),

View File

@ -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,

View File

@ -794,6 +794,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
senderE164, senderE164,
senderUuid, senderUuid,
timestamp, timestamp,
isDirectConversation: isDirectConversation(this.model.attributes),
}, },
}); });
} }

View File

@ -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"