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