Include and process destinationPniIdentityKey
This commit is contained in:
parent
711e321d16
commit
e031d136a1
11 changed files with 238 additions and 214 deletions
|
@ -209,7 +209,7 @@
|
||||||
"@formatjs/intl": "2.6.7",
|
"@formatjs/intl": "2.6.7",
|
||||||
"@indutny/rezip-electron": "1.3.1",
|
"@indutny/rezip-electron": "1.3.1",
|
||||||
"@mixer/parallel-prettier": "2.0.3",
|
"@mixer/parallel-prettier": "2.0.3",
|
||||||
"@signalapp/mock-server": "6.0.0",
|
"@signalapp/mock-server": "6.1.0",
|
||||||
"@storybook/addon-a11y": "7.4.5",
|
"@storybook/addon-a11y": "7.4.5",
|
||||||
"@storybook/addon-actions": "7.4.5",
|
"@storybook/addon-actions": "7.4.5",
|
||||||
"@storybook/addon-controls": "7.4.5",
|
"@storybook/addon-controls": "7.4.5",
|
||||||
|
|
|
@ -450,9 +450,11 @@ message Verified {
|
||||||
message SyncMessage {
|
message SyncMessage {
|
||||||
message Sent {
|
message Sent {
|
||||||
message UnidentifiedDeliveryStatus {
|
message UnidentifiedDeliveryStatus {
|
||||||
optional string destination = 1;
|
optional string destination = 1;
|
||||||
optional string destinationServiceId = 3;
|
optional string destinationServiceId = 3;
|
||||||
optional bool unidentified = 2;
|
optional bool unidentified = 2;
|
||||||
|
reserved /* destinationPni */ 4;
|
||||||
|
optional bytes destinationPniIdentityKey = 5; // Only set for PNI destinations
|
||||||
}
|
}
|
||||||
|
|
||||||
message StoryMessageRecipient {
|
message StoryMessageRecipient {
|
||||||
|
|
|
@ -136,6 +136,11 @@ export type SessionTransactionOptions = Readonly<{
|
||||||
zone?: Zone;
|
zone?: Zone;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
export type SaveIdentityOptions = Readonly<{
|
||||||
|
zone?: Zone;
|
||||||
|
noOverwrite?: boolean;
|
||||||
|
}>;
|
||||||
|
|
||||||
export type VerifyAlternateIdentityOptionsType = Readonly<{
|
export type VerifyAlternateIdentityOptionsType = Readonly<{
|
||||||
aci: AciString;
|
aci: AciString;
|
||||||
pni: PniString;
|
pni: PniString;
|
||||||
|
@ -2049,7 +2054,7 @@ export class SignalProtocolStore extends EventEmitter {
|
||||||
encodedAddress: Address,
|
encodedAddress: Address,
|
||||||
publicKey: Uint8Array,
|
publicKey: Uint8Array,
|
||||||
nonblockingApproval = false,
|
nonblockingApproval = false,
|
||||||
{ zone = GLOBAL_ZONE }: SessionTransactionOptions = {}
|
{ zone = GLOBAL_ZONE, noOverwrite = false }: SaveIdentityOptions = {}
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
if (!this.identityKeys) {
|
if (!this.identityKeys) {
|
||||||
throw new Error('saveIdentity: this.identityKeys not yet cached!');
|
throw new Error('saveIdentity: this.identityKeys not yet cached!');
|
||||||
|
@ -2100,6 +2105,10 @@ export class SignalProtocolStore extends EventEmitter {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (noOverwrite) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const identityKeyChanged = !constantTimeEqual(
|
const identityKeyChanged = !constantTimeEqual(
|
||||||
identityRecord.publicKey,
|
identityRecord.publicKey,
|
||||||
publicKey
|
publicKey
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { isNumber, throttle, groupBy } from 'lodash';
|
||||||
import { render } from 'react-dom';
|
import { render } from 'react-dom';
|
||||||
import { batch as batchDispatch } from 'react-redux';
|
import { batch as batchDispatch } from 'react-redux';
|
||||||
import PQueue from 'p-queue';
|
import PQueue from 'p-queue';
|
||||||
|
import pMap from 'p-map';
|
||||||
import { v4 as generateUuid } from 'uuid';
|
import { v4 as generateUuid } from 'uuid';
|
||||||
|
|
||||||
import * as Registration from './util/registration';
|
import * as Registration from './util/registration';
|
||||||
|
@ -52,6 +53,7 @@ import { GROUP_CREDENTIALS_KEY } from './services/groupCredentialFetcher';
|
||||||
import * as KeyboardLayout from './services/keyboardLayout';
|
import * as KeyboardLayout from './services/keyboardLayout';
|
||||||
import * as StorageService from './services/storage';
|
import * as StorageService from './services/storage';
|
||||||
import { usernameIntegrity } from './services/usernameIntegrity';
|
import { usernameIntegrity } from './services/usernameIntegrity';
|
||||||
|
import { updateIdentityKey } from './services/profiles';
|
||||||
import { RoutineProfileRefresher } from './routineProfileRefresh';
|
import { RoutineProfileRefresher } from './routineProfileRefresh';
|
||||||
import { isOlderThan } from './util/timestamp';
|
import { isOlderThan } from './util/timestamp';
|
||||||
import { isValidReactionEmoji } from './reactions/isValidReactionEmoji';
|
import { isValidReactionEmoji } from './reactions/isValidReactionEmoji';
|
||||||
|
@ -2536,46 +2538,74 @@ export async function startApp(): Promise<void> {
|
||||||
return confirm();
|
return confirm();
|
||||||
}
|
}
|
||||||
|
|
||||||
function createSentMessage(
|
async function createSentMessage(
|
||||||
data: SentEventData,
|
data: SentEventData,
|
||||||
descriptor: MessageDescriptor
|
descriptor: MessageDescriptor
|
||||||
) {
|
) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const timestamp = data.timestamp || now;
|
const timestamp = data.timestamp || now;
|
||||||
|
const logId = `createSentMessage(${timestamp})`;
|
||||||
|
|
||||||
const ourId = window.ConversationController.getOurConversationIdOrThrow();
|
const ourId = window.ConversationController.getOurConversationIdOrThrow();
|
||||||
|
|
||||||
const { unidentifiedStatus = [] } = data;
|
const { unidentifiedStatus = [] } = data;
|
||||||
|
|
||||||
const sendStateByConversationId: SendStateByConversationId =
|
const sendStateByConversationId: SendStateByConversationId = {
|
||||||
unidentifiedStatus.reduce(
|
[ourId]: {
|
||||||
(
|
status: SendStatus.Sent,
|
||||||
result: SendStateByConversationId,
|
updatedAt: timestamp,
|
||||||
{ destinationServiceId, destination, isAllowedToReplyToStory }
|
},
|
||||||
) => {
|
};
|
||||||
const conversation = window.ConversationController.get(
|
|
||||||
destinationServiceId || destination
|
|
||||||
);
|
|
||||||
if (!conversation || conversation.id === ourId) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
for (const {
|
||||||
...result,
|
destinationServiceId,
|
||||||
[conversation.id]: {
|
destination,
|
||||||
isAllowedToReplyToStory,
|
isAllowedToReplyToStory,
|
||||||
status: SendStatus.Sent,
|
} of unidentifiedStatus) {
|
||||||
updatedAt: timestamp,
|
const conversation = window.ConversationController.get(
|
||||||
},
|
destinationServiceId || destination
|
||||||
};
|
|
||||||
},
|
|
||||||
{
|
|
||||||
[ourId]: {
|
|
||||||
status: SendStatus.Sent,
|
|
||||||
updatedAt: timestamp,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
if (!conversation || conversation.id === ourId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendStateByConversationId[conversation.id] = {
|
||||||
|
isAllowedToReplyToStory,
|
||||||
|
status: SendStatus.Sent,
|
||||||
|
updatedAt: timestamp,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await pMap(
|
||||||
|
unidentifiedStatus,
|
||||||
|
async ({ destinationServiceId, destinationPniIdentityKey }) => {
|
||||||
|
if (!Bytes.isNotEmpty(destinationPniIdentityKey)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isPniString(destinationServiceId)) {
|
||||||
|
log.warn(
|
||||||
|
`${logId}: received an destinationPniIdentityKey for ` +
|
||||||
|
`an invalid PNI: ${destinationServiceId}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const changed = await updateIdentityKey(
|
||||||
|
destinationPniIdentityKey,
|
||||||
|
destinationServiceId,
|
||||||
|
{
|
||||||
|
noOverwrite: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (changed) {
|
||||||
|
log.info(
|
||||||
|
`${logId}: Updated identity key for ${destinationServiceId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ concurrency: 10 }
|
||||||
|
);
|
||||||
|
|
||||||
let unidentifiedDeliveries: Array<string> = [];
|
let unidentifiedDeliveries: Array<string> = [];
|
||||||
if (unidentifiedStatus.length) {
|
if (unidentifiedStatus.length) {
|
||||||
|
@ -2720,7 +2750,7 @@ export async function startApp(): Promise<void> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = createSentMessage(data, messageDescriptor);
|
const message = await createSentMessage(data, messageDescriptor);
|
||||||
|
|
||||||
if (data.message.reaction) {
|
if (data.message.reaction) {
|
||||||
strictAssert(
|
strictAssert(
|
||||||
|
|
|
@ -2027,9 +2027,12 @@ export class ConversationModel extends window.Backbone
|
||||||
incrementSentMessageCount({ dry = false }: { dry?: boolean } = {}):
|
incrementSentMessageCount({ dry = false }: { dry?: boolean } = {}):
|
||||||
| Partial<ConversationAttributesType>
|
| Partial<ConversationAttributesType>
|
||||||
| undefined {
|
| undefined {
|
||||||
|
const needsTitleTransition =
|
||||||
|
hasNumberTitle(this.attributes) || hasUsernameTitle(this.attributes);
|
||||||
const update = {
|
const update = {
|
||||||
messageCount: (this.get('messageCount') || 0) + 1,
|
messageCount: (this.get('messageCount') || 0) + 1,
|
||||||
sentMessageCount: (this.get('sentMessageCount') || 0) + 1,
|
sentMessageCount: (this.get('sentMessageCount') || 0) + 1,
|
||||||
|
...(needsTitleTransition ? { needsTitleTransition: true } : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (dry) {
|
if (dry) {
|
||||||
|
@ -3719,13 +3722,10 @@ export class ConversationModel extends window.Backbone
|
||||||
};
|
};
|
||||||
|
|
||||||
const isEditMessage = Boolean(message.get('editHistory'));
|
const isEditMessage = Boolean(message.get('editHistory'));
|
||||||
const needsTitleTransition =
|
|
||||||
hasNumberTitle(this.attributes) || hasUsernameTitle(this.attributes);
|
|
||||||
|
|
||||||
this.set({
|
this.set({
|
||||||
...draftProperties,
|
...draftProperties,
|
||||||
...(enabledProfileSharing ? { profileSharing: true } : {}),
|
...(enabledProfileSharing ? { profileSharing: true } : {}),
|
||||||
...(needsTitleTransition ? { needsTitleTransition: true } : {}),
|
|
||||||
...(dontAddMessage
|
...(dontAddMessage
|
||||||
? {}
|
? {}
|
||||||
: this.incrementSentMessageCount({ dry: true })),
|
: this.incrementSentMessageCount({ dry: true })),
|
||||||
|
|
|
@ -354,7 +354,8 @@ async function doGetProfile(c: ConversationModel): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (profile.identityKey) {
|
if (profile.identityKey) {
|
||||||
await updateIdentityKey(profile.identityKey, serviceId);
|
const identityKeyBytes = Bytes.fromBase64(profile.identityKey);
|
||||||
|
await updateIdentityKey(identityKeyBytes, serviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update accessKey to prevent race conditions. Since we run asynchronous
|
// Update accessKey to prevent race conditions. Since we run asynchronous
|
||||||
|
@ -596,19 +597,24 @@ async function doGetProfile(c: ConversationModel): Promise<void> {
|
||||||
window.Signal.Data.updateConversation(c.attributes);
|
window.Signal.Data.updateConversation(c.attributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UpdateIdentityKeyOptionsType = Readonly<{
|
||||||
|
noOverwrite?: boolean;
|
||||||
|
}>;
|
||||||
|
|
||||||
export async function updateIdentityKey(
|
export async function updateIdentityKey(
|
||||||
identityKey: string,
|
identityKey: Uint8Array,
|
||||||
serviceId: ServiceIdString
|
serviceId: ServiceIdString,
|
||||||
): Promise<void> {
|
{ noOverwrite = false }: UpdateIdentityKeyOptionsType = {}
|
||||||
if (!identityKey) {
|
): Promise<boolean> {
|
||||||
return;
|
if (!Bytes.isNotEmpty(identityKey)) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const identityKeyBytes = Bytes.fromBase64(identityKey);
|
|
||||||
const changed = await window.textsecure.storage.protocol.saveIdentity(
|
const changed = await window.textsecure.storage.protocol.saveIdentity(
|
||||||
new Address(serviceId, 1),
|
new Address(serviceId, 1),
|
||||||
identityKeyBytes,
|
identityKey,
|
||||||
false
|
false,
|
||||||
|
{ noOverwrite }
|
||||||
);
|
);
|
||||||
if (changed) {
|
if (changed) {
|
||||||
log.info(`updateIdentityKey(${serviceId}): changed`);
|
log.info(`updateIdentityKey(${serviceId}): changed`);
|
||||||
|
@ -619,4 +625,6 @@ export async function updateIdentityKey(
|
||||||
new QualifiedAddress(ourAci, new Address(serviceId, 1))
|
new QualifiedAddress(ourAci, new Address(serviceId, 1))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return changed;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,94 +0,0 @@
|
||||||
// Copyright 2023 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import { assert } from 'chai';
|
|
||||||
import createDebug from 'debug';
|
|
||||||
import Long from 'long';
|
|
||||||
|
|
||||||
import type { App } from '../playwright';
|
|
||||||
import * as durations from '../../util/durations';
|
|
||||||
import { Bootstrap } from '../bootstrap';
|
|
||||||
|
|
||||||
export const debug = createDebug('mock:test:sendSync');
|
|
||||||
|
|
||||||
describe('sendSync', function (this: Mocha.Suite) {
|
|
||||||
this.timeout(durations.MINUTE);
|
|
||||||
|
|
||||||
let bootstrap: Bootstrap;
|
|
||||||
let app: App;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
bootstrap = new Bootstrap();
|
|
||||||
await bootstrap.init();
|
|
||||||
app = await bootstrap.link();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async function (this: Mocha.Context) {
|
|
||||||
if (!bootstrap) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await bootstrap.maybeSaveLogs(this.currentTest, app);
|
|
||||||
await app.close();
|
|
||||||
await bootstrap.teardown();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('creates conversation for sendSync to PNI', async () => {
|
|
||||||
const { desktop, phone, server } = bootstrap;
|
|
||||||
|
|
||||||
debug('Creating stranger');
|
|
||||||
const STRANGER_NAME = 'Mysterious Stranger';
|
|
||||||
const stranger = await server.createPrimaryDevice({
|
|
||||||
profileName: STRANGER_NAME,
|
|
||||||
});
|
|
||||||
|
|
||||||
const timestamp = Date.now();
|
|
||||||
const messageText = 'hey there, just reaching out';
|
|
||||||
const destinationServiceId = stranger.device.pni;
|
|
||||||
const destination = stranger.device.number;
|
|
||||||
const originalDataMessage = {
|
|
||||||
body: messageText,
|
|
||||||
timestamp: Long.fromNumber(timestamp),
|
|
||||||
};
|
|
||||||
const content = {
|
|
||||||
syncMessage: {
|
|
||||||
sent: {
|
|
||||||
destinationServiceId,
|
|
||||||
destination,
|
|
||||||
timestamp: Long.fromNumber(timestamp),
|
|
||||||
message: originalDataMessage,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const sendOptions = {
|
|
||||||
timestamp,
|
|
||||||
};
|
|
||||||
await phone.sendRaw(desktop, content, sendOptions);
|
|
||||||
|
|
||||||
const page = await app.getWindow();
|
|
||||||
const leftPane = page.locator('#LeftPane');
|
|
||||||
|
|
||||||
debug('checking left pane for conversation');
|
|
||||||
const strangerName = await leftPane
|
|
||||||
.locator(
|
|
||||||
'.module-conversation-list__item--contact-or-conversation .module-contact-name'
|
|
||||||
)
|
|
||||||
.first()
|
|
||||||
.innerText();
|
|
||||||
|
|
||||||
assert.equal(
|
|
||||||
strangerName.slice(-4),
|
|
||||||
destination?.slice(-4),
|
|
||||||
'no profile, just phone number'
|
|
||||||
);
|
|
||||||
|
|
||||||
debug('opening conversation');
|
|
||||||
await leftPane
|
|
||||||
.locator('.module-conversation-list__item--contact-or-conversation')
|
|
||||||
.first()
|
|
||||||
.click();
|
|
||||||
|
|
||||||
debug('checking for latest message');
|
|
||||||
await page.locator(`.module-message__text >> "${messageText}"`).waitFor();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -2,6 +2,7 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
|
import Long from 'long';
|
||||||
import { Pni } from '@signalapp/libsignal-client';
|
import { Pni } from '@signalapp/libsignal-client';
|
||||||
import {
|
import {
|
||||||
ServiceIdKind,
|
ServiceIdKind,
|
||||||
|
@ -9,7 +10,6 @@ import {
|
||||||
ReceiptType,
|
ReceiptType,
|
||||||
StorageState,
|
StorageState,
|
||||||
} from '@signalapp/mock-server';
|
} from '@signalapp/mock-server';
|
||||||
import type { PrimaryDevice } from '@signalapp/mock-server';
|
|
||||||
import createDebug from 'debug';
|
import createDebug from 'debug';
|
||||||
|
|
||||||
import * as durations from '../../util/durations';
|
import * as durations from '../../util/durations';
|
||||||
|
@ -33,17 +33,12 @@ describe('pnp/PNI Signature', function (this: Mocha.Suite) {
|
||||||
|
|
||||||
let bootstrap: Bootstrap;
|
let bootstrap: Bootstrap;
|
||||||
let app: App;
|
let app: App;
|
||||||
let pniContact: PrimaryDevice;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
bootstrap = new Bootstrap();
|
bootstrap = new Bootstrap({ contactCount: 0 });
|
||||||
await bootstrap.init();
|
await bootstrap.init();
|
||||||
|
|
||||||
const { server, phone } = bootstrap;
|
const { phone } = bootstrap;
|
||||||
|
|
||||||
pniContact = await server.createPrimaryDevice({
|
|
||||||
profileName: 'ACI Contact',
|
|
||||||
});
|
|
||||||
|
|
||||||
let state = StorageState.getEmpty();
|
let state = StorageState.getEmpty();
|
||||||
|
|
||||||
|
@ -52,21 +47,6 @@ describe('pnp/PNI Signature', function (this: Mocha.Suite) {
|
||||||
e164: phone.device.number,
|
e164: phone.device.number,
|
||||||
});
|
});
|
||||||
|
|
||||||
state = state.addContact(
|
|
||||||
pniContact,
|
|
||||||
{
|
|
||||||
whitelisted: true,
|
|
||||||
serviceE164: pniContact.device.number,
|
|
||||||
identityKey: pniContact.getPublicKey(ServiceIdKind.PNI).serialize(),
|
|
||||||
givenName: undefined,
|
|
||||||
familyName: undefined,
|
|
||||||
},
|
|
||||||
ServiceIdKind.PNI
|
|
||||||
);
|
|
||||||
|
|
||||||
// Just to make PNI Contact visible in the left pane
|
|
||||||
state = state.pin(pniContact, ServiceIdKind.PNI);
|
|
||||||
|
|
||||||
// Add my story
|
// Add my story
|
||||||
state = state.addRecord({
|
state = state.addRecord({
|
||||||
type: IdentifierType.STORY_DISTRIBUTION_LIST,
|
type: IdentifierType.STORY_DISTRIBUTION_LIST,
|
||||||
|
@ -140,14 +120,14 @@ describe('pnp/PNI Signature', function (this: Mocha.Suite) {
|
||||||
assert.isTrue(isValid, `Invalid pni signature from ${source}`);
|
assert.isTrue(isValid, `Invalid pni signature from ${source}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
debug('sending a message to our PNI');
|
debug('Send a message to our PNI');
|
||||||
await stranger.sendText(desktop, 'A message to PNI', {
|
await stranger.sendText(desktop, 'A message to PNI', {
|
||||||
serviceIdKind: ServiceIdKind.PNI,
|
serviceIdKind: ServiceIdKind.PNI,
|
||||||
withProfileKey: true,
|
withProfileKey: true,
|
||||||
timestamp: bootstrap.getTimestamp(),
|
timestamp: bootstrap.getTimestamp(),
|
||||||
});
|
});
|
||||||
|
|
||||||
debug('opening conversation with the stranger');
|
debug('Open conversation with the stranger');
|
||||||
await leftPane
|
await leftPane
|
||||||
.locator(`[data-testid="${stranger.toContact().aci}"]`)
|
.locator(`[data-testid="${stranger.toContact().aci}"]`)
|
||||||
.click();
|
.click();
|
||||||
|
@ -157,7 +137,7 @@ describe('pnp/PNI Signature', function (this: Mocha.Suite) {
|
||||||
.locator('.module-message-request-actions button >> "Accept"')
|
.locator('.module-message-request-actions button >> "Accept"')
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
debug('Waiting for a pniSignatureMessage');
|
debug('Wait for a pniSignatureMessage');
|
||||||
{
|
{
|
||||||
const { source, content } = await stranger.waitForMessage();
|
const { source, content } = await stranger.waitForMessage();
|
||||||
|
|
||||||
|
@ -171,7 +151,7 @@ describe('pnp/PNI Signature', function (this: Mocha.Suite) {
|
||||||
await compositionInput.type('first');
|
await compositionInput.type('first');
|
||||||
await compositionInput.press('Enter');
|
await compositionInput.press('Enter');
|
||||||
}
|
}
|
||||||
debug('Waiting for the first message with pni signature');
|
debug('Wait for the first message with pni signature');
|
||||||
{
|
{
|
||||||
const { source, content, body, dataMessage } =
|
const { source, content, body, dataMessage } =
|
||||||
await stranger.waitForMessage();
|
await stranger.waitForMessage();
|
||||||
|
@ -185,7 +165,7 @@ describe('pnp/PNI Signature', function (this: Mocha.Suite) {
|
||||||
checkPniSignature(content.pniSignatureMessage, 'first message');
|
checkPniSignature(content.pniSignatureMessage, 'first message');
|
||||||
|
|
||||||
const receiptTimestamp = bootstrap.getTimestamp();
|
const receiptTimestamp = bootstrap.getTimestamp();
|
||||||
debug('Sending unencrypted receipt', receiptTimestamp);
|
debug('Send unencrypted receipt', receiptTimestamp);
|
||||||
|
|
||||||
await stranger.sendUnencryptedReceipt(desktop, {
|
await stranger.sendUnencryptedReceipt(desktop, {
|
||||||
messageTimestamp: dataMessage.timestamp?.toNumber() ?? 0,
|
messageTimestamp: dataMessage.timestamp?.toNumber() ?? 0,
|
||||||
|
@ -199,7 +179,7 @@ describe('pnp/PNI Signature', function (this: Mocha.Suite) {
|
||||||
await compositionInput.type('second');
|
await compositionInput.type('second');
|
||||||
await compositionInput.press('Enter');
|
await compositionInput.press('Enter');
|
||||||
}
|
}
|
||||||
debug('Waiting for the second message with pni signature');
|
debug('Wait for the second message with pni signature');
|
||||||
{
|
{
|
||||||
const { source, content, body, dataMessage } =
|
const { source, content, body, dataMessage } =
|
||||||
await stranger.waitForMessage();
|
await stranger.waitForMessage();
|
||||||
|
@ -213,7 +193,7 @@ describe('pnp/PNI Signature', function (this: Mocha.Suite) {
|
||||||
checkPniSignature(content.pniSignatureMessage, 'second message');
|
checkPniSignature(content.pniSignatureMessage, 'second message');
|
||||||
|
|
||||||
const receiptTimestamp = bootstrap.getTimestamp();
|
const receiptTimestamp = bootstrap.getTimestamp();
|
||||||
debug('Sending encrypted receipt', receiptTimestamp);
|
debug('Send encrypted receipt', receiptTimestamp);
|
||||||
|
|
||||||
await stranger.sendReceipt(desktop, {
|
await stranger.sendReceipt(desktop, {
|
||||||
type: ReceiptType.Delivery,
|
type: ReceiptType.Delivery,
|
||||||
|
@ -233,7 +213,7 @@ describe('pnp/PNI Signature', function (this: Mocha.Suite) {
|
||||||
await compositionInput.type('third');
|
await compositionInput.type('third');
|
||||||
await compositionInput.press('Enter');
|
await compositionInput.press('Enter');
|
||||||
}
|
}
|
||||||
debug('Waiting for the third message without pni signature');
|
debug('Wait for the third message without pni signature');
|
||||||
{
|
{
|
||||||
const { source, content, body } = await stranger.waitForMessage();
|
const { source, content, body } = await stranger.waitForMessage();
|
||||||
|
|
||||||
|
@ -262,55 +242,126 @@ describe('pnp/PNI Signature', function (this: Mocha.Suite) {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be received by Desktop and trigger contact merge', async () => {
|
it('should be received by Desktop and trigger contact merge', async () => {
|
||||||
const { desktop, phone } = bootstrap;
|
const { desktop, phone, server } = bootstrap;
|
||||||
|
|
||||||
const window = await app.getWindow();
|
const window = await app.getWindow();
|
||||||
|
|
||||||
const leftPane = window.locator('#LeftPane');
|
const leftPane = window.locator('#LeftPane');
|
||||||
|
|
||||||
debug('opening conversation with the pni contact');
|
debug('Capture storage service state before messaging');
|
||||||
await leftPane
|
let state = await phone.expectStorageState('state before messaging');
|
||||||
.locator('.module-conversation-list__item--contact-or-conversation')
|
|
||||||
.first()
|
|
||||||
.click();
|
|
||||||
|
|
||||||
debug('Enter a PNI message text');
|
debug('Create stranger');
|
||||||
|
const STRANGER_NAME = 'Mysterious Stranger';
|
||||||
|
const stranger = await server.createPrimaryDevice({
|
||||||
|
profileName: STRANGER_NAME,
|
||||||
|
});
|
||||||
|
|
||||||
|
debug('Send a PNI sync message');
|
||||||
|
const timestamp = bootstrap.getTimestamp();
|
||||||
|
const destinationServiceId = stranger.device.pni;
|
||||||
|
const destination = stranger.device.number;
|
||||||
|
const destinationPniIdentityKey = await stranger.device.getIdentityKey(
|
||||||
|
ServiceIdKind.PNI
|
||||||
|
);
|
||||||
|
const originalDataMessage = {
|
||||||
|
body: 'Hello PNI',
|
||||||
|
timestamp: Long.fromNumber(timestamp),
|
||||||
|
};
|
||||||
|
const content = {
|
||||||
|
syncMessage: {
|
||||||
|
sent: {
|
||||||
|
destinationServiceId,
|
||||||
|
destination,
|
||||||
|
timestamp: Long.fromNumber(timestamp),
|
||||||
|
message: originalDataMessage,
|
||||||
|
unidentifiedStatus: [
|
||||||
|
{
|
||||||
|
destinationServiceId,
|
||||||
|
destination,
|
||||||
|
destinationPniIdentityKey: destinationPniIdentityKey.serialize(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const sendOptions = {
|
||||||
|
timestamp,
|
||||||
|
};
|
||||||
|
await phone.sendRaw(desktop, content, sendOptions);
|
||||||
|
|
||||||
|
debug('Wait for updated storage service state with PNI contact');
|
||||||
{
|
{
|
||||||
const compositionInput = await app.waitForEnabledComposer();
|
const newState = await phone.waitForStorageState({
|
||||||
|
after: state,
|
||||||
|
});
|
||||||
|
|
||||||
await compositionInput.type('Hello PNI');
|
const aciRecord = newState.getContact(stranger, ServiceIdKind.ACI);
|
||||||
await compositionInput.press('Enter');
|
assert.isUndefined(aciRecord, 'ACI contact must not be created');
|
||||||
|
|
||||||
|
const pniRecord = newState.getContact(stranger, ServiceIdKind.PNI);
|
||||||
|
assert.deepEqual(
|
||||||
|
pniRecord?.identityKey,
|
||||||
|
destinationPniIdentityKey.serialize(),
|
||||||
|
'PNI contact must have correct identity key'
|
||||||
|
);
|
||||||
|
|
||||||
|
state = newState;
|
||||||
}
|
}
|
||||||
|
|
||||||
debug('Waiting for a PNI message');
|
debug('Open conversation with the pni contact');
|
||||||
{
|
const contactElem = leftPane.locator(
|
||||||
const { source, body, serviceIdKind } = await pniContact.waitForMessage();
|
`[data-testid="${stranger.device.pni}"]`
|
||||||
|
);
|
||||||
|
await contactElem.click();
|
||||||
|
|
||||||
assert.strictEqual(source, desktop, 'PNI message has valid source');
|
debug('Verify that left pane shows phone number');
|
||||||
assert.strictEqual(body, 'Hello PNI', 'PNI message has valid body');
|
{
|
||||||
assert.strictEqual(
|
const strangerName = await contactElem
|
||||||
serviceIdKind,
|
.locator('.module-contact-name')
|
||||||
ServiceIdKind.PNI,
|
.first()
|
||||||
'PNI message has valid destination'
|
.innerText();
|
||||||
|
assert.equal(
|
||||||
|
strangerName.slice(-4),
|
||||||
|
destination?.slice(-4),
|
||||||
|
'no profile, just phone number'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
debug('Capture storage service state before merging');
|
debug('Verify that we are in MR state');
|
||||||
const state = await phone.expectStorageState('state before merge');
|
const conversationStack = window.locator('.Inbox__conversation-stack');
|
||||||
|
await conversationStack
|
||||||
|
.locator('.module-message-request-actions button >> "Continue"')
|
||||||
|
.waitFor();
|
||||||
|
|
||||||
debug('Enter a draft text without hitting enter');
|
debug('Clear message request state on phone');
|
||||||
{
|
{
|
||||||
const compositionInput = await app.waitForEnabledComposer();
|
const newState = state.updateContact(
|
||||||
|
stranger,
|
||||||
|
{
|
||||||
|
whitelisted: true,
|
||||||
|
},
|
||||||
|
ServiceIdKind.PNI
|
||||||
|
);
|
||||||
|
|
||||||
await compositionInput.type('Draft text');
|
await phone.setStorageState(newState, state);
|
||||||
|
await phone.sendFetchStorage({
|
||||||
|
timestamp: bootstrap.getTimestamp(),
|
||||||
|
});
|
||||||
|
state = newState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debug('Wait for MR state to disappear');
|
||||||
|
await conversationStack
|
||||||
|
.locator('.module-message-request-actions button >> "Continue"')
|
||||||
|
.waitFor({ state: 'hidden' });
|
||||||
|
|
||||||
debug('Send back the response with profile key and pni signature');
|
debug('Send back the response with profile key and pni signature');
|
||||||
|
|
||||||
const ourKey = await desktop.popSingleUseKey();
|
const ourKey = await desktop.popSingleUseKey();
|
||||||
await pniContact.addSingleUseKey(desktop, ourKey);
|
await stranger.addSingleUseKey(desktop, ourKey);
|
||||||
|
|
||||||
await pniContact.sendText(desktop, 'Hello Desktop!', {
|
await stranger.sendText(desktop, 'Hello Desktop!', {
|
||||||
timestamp: bootstrap.getTimestamp(),
|
timestamp: bootstrap.getTimestamp(),
|
||||||
withPniSignature: true,
|
withPniSignature: true,
|
||||||
withProfileKey: true,
|
withProfileKey: true,
|
||||||
|
@ -318,7 +369,7 @@ describe('pnp/PNI Signature', function (this: Mocha.Suite) {
|
||||||
|
|
||||||
debug('Wait for merge to happen');
|
debug('Wait for merge to happen');
|
||||||
await leftPane
|
await leftPane
|
||||||
.locator(`[data-testid="${pniContact.toContact().aci}"]`)
|
.locator(`[data-testid="${stranger.toContact().aci}"]`)
|
||||||
.waitFor();
|
.waitFor();
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -330,9 +381,9 @@ describe('pnp/PNI Signature', function (this: Mocha.Suite) {
|
||||||
await compositionInput.press('Enter');
|
await compositionInput.press('Enter');
|
||||||
}
|
}
|
||||||
|
|
||||||
debug('Waiting for a ACI message');
|
debug('Wait for a ACI message');
|
||||||
{
|
{
|
||||||
const { source, body, serviceIdKind } = await pniContact.waitForMessage();
|
const { source, body, serviceIdKind } = await stranger.waitForMessage();
|
||||||
|
|
||||||
assert.strictEqual(source, desktop, 'ACI message has valid source');
|
assert.strictEqual(source, desktop, 'ACI message has valid source');
|
||||||
assert.strictEqual(body, 'Hello ACI', 'ACI message has valid body');
|
assert.strictEqual(body, 'Hello ACI', 'ACI message has valid body');
|
||||||
|
@ -350,8 +401,8 @@ describe('pnp/PNI Signature', function (this: Mocha.Suite) {
|
||||||
after: state,
|
after: state,
|
||||||
});
|
});
|
||||||
|
|
||||||
const pniRecord = newState.getContact(pniContact, ServiceIdKind.PNI);
|
const pniRecord = newState.getContact(stranger, ServiceIdKind.PNI);
|
||||||
const aciRecord = newState.getContact(pniContact, ServiceIdKind.ACI);
|
const aciRecord = newState.getContact(stranger, ServiceIdKind.ACI);
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
aciRecord,
|
aciRecord,
|
||||||
pniRecord,
|
pniRecord,
|
||||||
|
@ -359,12 +410,12 @@ describe('pnp/PNI Signature', function (this: Mocha.Suite) {
|
||||||
);
|
);
|
||||||
assert(aciRecord, 'ACI Contact must be in storage service');
|
assert(aciRecord, 'ACI Contact must be in storage service');
|
||||||
|
|
||||||
assert.strictEqual(aciRecord?.aci, pniContact.device.aci);
|
assert.strictEqual(aciRecord?.aci, stranger.device.aci);
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
aciRecord?.pni &&
|
aciRecord?.pni &&
|
||||||
isUntaggedPniString(aciRecord?.pni) &&
|
isUntaggedPniString(aciRecord?.pni) &&
|
||||||
toTaggedPni(aciRecord?.pni),
|
toTaggedPni(aciRecord?.pni),
|
||||||
pniContact.device.pni
|
stranger.device.pni
|
||||||
);
|
);
|
||||||
assert.strictEqual(aciRecord?.pniSignatureVerified, true);
|
assert.strictEqual(aciRecord?.pniSignatureVerified, true);
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import Long from 'long';
|
import Long from 'long';
|
||||||
import PQueue from 'p-queue';
|
import PQueue from 'p-queue';
|
||||||
|
import pMap from 'p-map';
|
||||||
import type { PlaintextContent } from '@signalapp/libsignal-client';
|
import type { PlaintextContent } from '@signalapp/libsignal-client';
|
||||||
import {
|
import {
|
||||||
Pni,
|
Pni,
|
||||||
|
@ -26,7 +27,11 @@ import type {
|
||||||
UploadedAttachmentType,
|
UploadedAttachmentType,
|
||||||
} from '../types/Attachment';
|
} from '../types/Attachment';
|
||||||
import type { AciString, ServiceIdString } from '../types/ServiceId';
|
import type { AciString, ServiceIdString } from '../types/ServiceId';
|
||||||
import { ServiceIdKind, serviceIdSchema } from '../types/ServiceId';
|
import {
|
||||||
|
ServiceIdKind,
|
||||||
|
serviceIdSchema,
|
||||||
|
isPniString,
|
||||||
|
} from '../types/ServiceId';
|
||||||
import type {
|
import type {
|
||||||
ChallengeType,
|
ChallengeType,
|
||||||
GetGroupLogOptionsType,
|
GetGroupLogOptionsType,
|
||||||
|
@ -63,7 +68,7 @@ import type {
|
||||||
LinkPreviewImage,
|
LinkPreviewImage,
|
||||||
LinkPreviewMetadata,
|
LinkPreviewMetadata,
|
||||||
} from '../linkPreviews/linkPreviewFetch';
|
} from '../linkPreviews/linkPreviewFetch';
|
||||||
import { concat, isEmpty, map } from '../util/iterables';
|
import { concat, isEmpty } 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 type { DurationInSeconds } from '../util/durations';
|
import type { DurationInSeconds } from '../util/durations';
|
||||||
|
@ -1267,8 +1272,9 @@ export default class MessageSender {
|
||||||
// Though this field has 'unidentified' in the name, it should have entries for each
|
// Though this field has 'unidentified' in the name, it should have entries for each
|
||||||
// number we sent to.
|
// number we sent to.
|
||||||
if (!isEmpty(conversationIdsSentTo)) {
|
if (!isEmpty(conversationIdsSentTo)) {
|
||||||
sentMessage.unidentifiedStatus = [
|
sentMessage.unidentifiedStatus = await pMap(
|
||||||
...map(conversationIdsSentTo, conversationId => {
|
conversationIdsSentTo,
|
||||||
|
async conversationId => {
|
||||||
const status =
|
const status =
|
||||||
new Proto.SyncMessage.Sent.UnidentifiedDeliveryStatus();
|
new Proto.SyncMessage.Sent.UnidentifiedDeliveryStatus();
|
||||||
const conv = window.ConversationController.get(conversationId);
|
const conv = window.ConversationController.get(conversationId);
|
||||||
|
@ -1281,12 +1287,22 @@ export default class MessageSender {
|
||||||
if (serviceId) {
|
if (serviceId) {
|
||||||
status.destinationServiceId = serviceId;
|
status.destinationServiceId = serviceId;
|
||||||
}
|
}
|
||||||
|
if (isPniString(serviceId)) {
|
||||||
|
const pniIdentityKey =
|
||||||
|
await window.textsecure.storage.protocol.loadIdentityKey(
|
||||||
|
serviceId
|
||||||
|
);
|
||||||
|
if (pniIdentityKey) {
|
||||||
|
status.destinationPniIdentityKey = pniIdentityKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
status.unidentified =
|
status.unidentified =
|
||||||
conversationIdsWithSealedSender.has(conversationId);
|
conversationIdsWithSealedSender.has(conversationId);
|
||||||
return status;
|
return status;
|
||||||
}),
|
},
|
||||||
];
|
{ concurrency: 10 }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const syncMessage = MessageSender.createSyncMessage();
|
const syncMessage = MessageSender.createSyncMessage();
|
||||||
|
|
|
@ -5,6 +5,7 @@ import * as log from '../logging/log';
|
||||||
import { isNotNil } from './isNotNil';
|
import { isNotNil } from './isNotNil';
|
||||||
import { updateIdentityKey } from '../services/profiles';
|
import { updateIdentityKey } from '../services/profiles';
|
||||||
import type { ServiceIdString } from '../types/ServiceId';
|
import type { ServiceIdString } from '../types/ServiceId';
|
||||||
|
import * as Bytes from '../Bytes';
|
||||||
|
|
||||||
export async function verifyStoryListMembers(
|
export async function verifyStoryListMembers(
|
||||||
serviceIds: Array<ServiceIdString>
|
serviceIds: Array<ServiceIdString>
|
||||||
|
@ -49,7 +50,8 @@ export async function verifyStoryListMembers(
|
||||||
verifiedServiceIds.delete(serviceId);
|
verifiedServiceIds.delete(serviceId);
|
||||||
|
|
||||||
if (identityKey) {
|
if (identityKey) {
|
||||||
await updateIdentityKey(identityKey, serviceId);
|
const identityKeyBytes = Bytes.fromBase64(identityKey);
|
||||||
|
await updateIdentityKey(identityKeyBytes, serviceId);
|
||||||
} else {
|
} else {
|
||||||
await window.ConversationController.get(serviceId)?.getProfiles();
|
await window.ConversationController.get(serviceId)?.getProfiles();
|
||||||
}
|
}
|
||||||
|
|
|
@ -4001,10 +4001,10 @@
|
||||||
type-fest "^3.5.0"
|
type-fest "^3.5.0"
|
||||||
uuid "^8.3.0"
|
uuid "^8.3.0"
|
||||||
|
|
||||||
"@signalapp/mock-server@6.0.0":
|
"@signalapp/mock-server@6.1.0":
|
||||||
version "6.0.0"
|
version "6.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@signalapp/mock-server/-/mock-server-6.0.0.tgz#a67e18b5cb928749c379c219c775a412ad5c181b"
|
resolved "https://registry.yarnpkg.com/@signalapp/mock-server/-/mock-server-6.1.0.tgz#5cb26ed475ca74c74800d9abff4c0c7954c59f54"
|
||||||
integrity sha512-hzKqCQ8A0xSScn9bztwZnjdizI15wTuEjj/uwmzWylzsPxbcXkOOL+db9O0uTOKbDIl6nJFrsUFqQ8R6LC8TAg==
|
integrity sha512-SBfN61aRqhtH7hHsdVl3waS1HFI5RCBcQJJ3o5yHXTx1xnXvp1VCjSQ85Vlg57+0IihBPZqqYxyEuNN+5Fnp8g==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@signalapp/libsignal-client" "^0.39.2"
|
"@signalapp/libsignal-client" "^0.39.2"
|
||||||
debug "^4.3.2"
|
debug "^4.3.2"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue