Include ACI+Access Keys pairs with CDSI requests

This commit is contained in:
Fedor Indutny 2022-08-18 13:44:53 -07:00 committed by GitHub
parent 13046dc020
commit 757af2cbbe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 145 additions and 144 deletions

View File

@ -9,7 +9,7 @@
"directoryV2PublicKey": null,
"directoryV2CodeHashes": null,
"directoryV3Url": "https://cdsi.staging.signal.org",
"directoryV3MRENCLAVE": "7b75dd6e862decef9b37132d54be082441917a7790e82fe44f9cf653de03a75f",
"directoryV3MRENCLAVE": "ddc7b9b1cbcc932e24b9905e26c4ecbea3f9b7effd033f9e96488c2e8449f64e",
"cdn": {
"0": "https://cdn-staging.signal.org",
"2": "https://cdn2-staging.signal.org"

View File

@ -27,6 +27,7 @@ import { QualifiedAddress } from './types/QualifiedAddress';
import { sleep } from './util/sleep';
import { isNotNil } from './util/isNotNil';
import { MINUTE, SECOND } from './util/durations';
import { getUuidsForE164s } from './util/getUuidsForE164s';
type ConvoMatchType =
| {
@ -1104,7 +1105,9 @@ export class ConversationController {
async _forgetE164(e164: string): Promise<void> {
const { server } = window.textsecure;
strictAssert(server, 'Server must be initialized');
const { [e164]: pni } = await server.getUuidsForE164s([e164]);
const uuidMap = await getUuidsForE164s(server, [e164]);
const pni = uuidMap.get(e164)?.pni;
log.info(`ConversationController: forgetting e164=${e164} pni=${pni}`);

View File

@ -2128,15 +2128,16 @@ export async function startApp(): Promise<void> {
!c.isEverUnregistered()
)
);
strictAssert(window.textsecure.server, 'server must be initialized');
await updateConversationsWithUuidLookup({
conversationController: window.ConversationController,
conversations: lonelyE164Conversations,
messaging: window.textsecure.messaging,
server: window.textsecure.server,
});
} catch (error) {
log.error(
'connect: Error fetching UUIDs for lonely e164s:',
error && error.stack ? error.stack : error
Errors.toLogFormat(error)
);
}
}

View File

@ -1033,8 +1033,8 @@ export class ConversationModel extends window.Backbone
}
async fetchSMSOnlyUUID(): Promise<void> {
const { messaging } = window.textsecure;
if (!messaging) {
const { server } = window.textsecure;
if (!server) {
return;
}
if (!this.isSMSOnly()) {
@ -1053,7 +1053,7 @@ export class ConversationModel extends window.Backbone
await updateConversationsWithUuidLookup({
conversationController: window.ConversationController,
conversations: [this],
messaging,
server,
});
} finally {
// No redux update here

View File

@ -837,10 +837,6 @@ async function removeAllSignedPreKeys(): Promise<void> {
// Items
const ITEM_SPECS: Partial<Record<ItemKeyType, ObjectMappingSpecType>> = {
senderCertificate: ['value.serialized'],
senderCertificateNoE164: ['value.serialized'],
subscriberId: ['value'],
profileKey: ['value'],
identityKeyMap: {
key: 'value',
valueSpec: {
@ -848,6 +844,10 @@ const ITEM_SPECS: Partial<Record<ItemKeyType, ObjectMappingSpecType>> = {
valueSpec: ['privKey', 'pubKey'],
},
},
profileKey: ['value'],
senderCertificate: ['value.serialized'],
senderCertificateNoE164: ['value.serialized'],
subscriberId: ['value'],
};
async function createOrUpdateItem<K extends ItemKeyType>(
data: ItemType<K>

View File

@ -8,6 +8,7 @@ import * as log from '../../logging/log';
import type { StateType as RootStateType } from '../reducer';
import type { UUIDStringType } from '../../types/UUID';
import { getUuidsForE164s } from '../../util/getUuidsForE164s';
import type { NoopActionType } from './noop';
@ -44,7 +45,8 @@ function checkForAccount(
AccountUpdateActionType | NoopActionType
> {
return async (dispatch, getState) => {
if (!window.textsecure.messaging) {
const { server } = window.textsecure;
if (!server) {
dispatch({
type: 'NOOP',
payload: null,
@ -77,16 +79,24 @@ function checkForAccount(
type: 'NOOP',
payload: null,
});
return;
}
let uuid: UUIDStringType | undefined;
log.info(`checkForAccount: looking ${phoneNumber} up on server`);
try {
const uuidLookup = await window.textsecure.messaging.getUuidsForE164s([
phoneNumber,
]);
uuid = uuidLookup[phoneNumber] || undefined;
const uuidLookup = await getUuidsForE164s(server, [phoneNumber]);
const maybePair = uuidLookup.get(phoneNumber);
if (maybePair) {
uuid = window.ConversationController.maybeMergeContacts({
aci: maybePair.aci,
pni: maybePair.pni,
e164: phoneNumber,
reason: 'checkForAccount',
})?.get('uuid');
}
} catch (error) {
log.error('checkForAccount:', Errors.toLogFormat(error));
}

View File

@ -7,7 +7,7 @@ import { assert } from 'chai';
import sinon from 'sinon';
import { ConversationModel } from '../models/conversations';
import type { ConversationAttributesType } from '../model-types.d';
import type SendMessage from '../textsecure/SendMessage';
import type { WebAPIType } from '../textsecure/WebAPI';
import { UUID } from '../types/UUID';
import { updateConversationsWithUuidLookup } from '../updateConversationsWithUuidLookup';
@ -137,22 +137,19 @@ describe('updateConversationsWithUuidLookup', () => {
let sinonSandbox: sinon.SinonSandbox;
let fakeGetUuidsForE164s: sinon.SinonStub;
let fakeCdsLookup: sinon.SinonStub;
let fakeCheckAccountExistence: sinon.SinonStub;
let fakeMessaging: Pick<
SendMessage,
'getUuidsForE164s' | 'checkAccountExistence'
>;
let fakeServer: Pick<WebAPIType, 'cdsLookup' | 'checkAccountExistence'>;
beforeEach(() => {
sinonSandbox = sinon.createSandbox();
sinonSandbox.stub(window.Signal.Data, 'updateConversation');
fakeGetUuidsForE164s = sinonSandbox.stub().resolves({});
fakeCdsLookup = sinonSandbox.stub().resolves(new Map());
fakeCheckAccountExistence = sinonSandbox.stub().resolves(false);
fakeMessaging = {
getUuidsForE164s: fakeGetUuidsForE164s,
fakeServer = {
cdsLookup: fakeCdsLookup,
checkAccountExistence: fakeCheckAccountExistence,
};
});
@ -165,10 +162,10 @@ describe('updateConversationsWithUuidLookup', () => {
await updateConversationsWithUuidLookup({
conversationController: new FakeConversationController(),
conversations: [],
messaging: fakeMessaging,
server: fakeServer,
});
sinon.assert.notCalled(fakeMessaging.getUuidsForE164s as sinon.SinonStub);
sinon.assert.notCalled(fakeServer.cdsLookup as sinon.SinonStub);
});
it('does nothing when called with an array of conversations that lack E164s', async () => {
@ -178,10 +175,10 @@ describe('updateConversationsWithUuidLookup', () => {
createConversation(),
createConversation({ uuid: UUID.generate().toString() }),
],
messaging: fakeMessaging,
server: fakeServer,
});
sinon.assert.notCalled(fakeMessaging.getUuidsForE164s as sinon.SinonStub);
sinon.assert.notCalled(fakeServer.cdsLookup as sinon.SinonStub);
});
it('updates conversations with their UUID', async () => {
@ -194,10 +191,12 @@ describe('updateConversationsWithUuidLookup', () => {
const uuid1 = UUID.generate().toString();
const uuid2 = UUID.generate().toString();
fakeGetUuidsForE164s.resolves({
'+13215559876': uuid1,
'+16545559876': uuid2,
});
fakeCdsLookup.resolves(
new Map([
['+13215559876', { aci: uuid1, pni: undefined }],
['+16545559876', { aci: uuid2, pni: undefined }],
])
);
await updateConversationsWithUuidLookup({
conversationController: new FakeConversationController([
@ -205,7 +204,7 @@ describe('updateConversationsWithUuidLookup', () => {
conversation2,
]),
conversations: [conversation1, conversation2],
messaging: fakeMessaging,
server: fakeServer,
});
assert.strictEqual(conversation1.get('uuid'), uuid1);
@ -219,12 +218,10 @@ describe('updateConversationsWithUuidLookup', () => {
'Test was not set up correctly'
);
fakeGetUuidsForE164s.resolves({ '+13215559876': null });
await updateConversationsWithUuidLookup({
conversationController: new FakeConversationController([conversation]),
conversations: [conversation],
messaging: fakeMessaging,
server: fakeServer,
});
assert.approximately(
@ -245,13 +242,12 @@ describe('updateConversationsWithUuidLookup', () => {
'Test was not set up correctly'
);
fakeGetUuidsForE164s.resolves({ '+13215559876': null });
fakeCheckAccountExistence.resolves(true);
await updateConversationsWithUuidLookup({
conversationController: new FakeConversationController([conversation]),
conversations: [conversation],
messaging: fakeMessaging,
server: fakeServer,
});
assert.strictEqual(conversation.get('uuid'), existingUuid);
@ -269,13 +265,12 @@ describe('updateConversationsWithUuidLookup', () => {
'Test was not set up correctly'
);
fakeGetUuidsForE164s.resolves({ '+13215559876': null });
fakeCheckAccountExistence.resolves(false);
await updateConversationsWithUuidLookup({
conversationController: new FakeConversationController([conversation]),
conversations: [conversation],
messaging: fakeMessaging,
server: fakeServer,
});
assert.isUndefined(conversation.get('uuid'));

View File

@ -671,9 +671,9 @@ export default class OutgoingMessage {
if (isValidUuid(identifier)) {
// We're good!
} else if (isValidNumber(identifier)) {
if (!window.textsecure.messaging) {
if (!window.textsecure.server) {
throw new Error(
'sendToIdentifier: window.textsecure.messaging is not available!'
'sendToIdentifier: window.textsecure.server is not available!'
);
}
@ -683,7 +683,7 @@ export default class OutgoingMessage {
conversations: [
window.ConversationController.getOrCreate(identifier, 'private'),
],
messaging: window.textsecure.messaging,
server: window.textsecure.server,
});
const uuid =

View File

@ -5,7 +5,6 @@
/* eslint-disable max-classes-per-file */
import { z } from 'zod';
import type { Dictionary } from 'lodash';
import Long from 'long';
import PQueue from 'p-queue';
import type { PlaintextContent } from '@signalapp/libsignal-client';
@ -25,7 +24,7 @@ import { SenderKeys } from '../LibSignalStores';
import type { LinkPreviewType } from '../types/message/LinkPreviews';
import { MIMETypeToString } from '../types/MIME';
import type * as Attachment from '../types/Attachment';
import type { UUID, UUIDStringType } from '../types/UUID';
import type { UUID } from '../types/UUID';
import type {
ChallengeType,
GetGroupLogOptionsType,
@ -49,7 +48,6 @@ import type {
SendLogCallbackType,
} from './OutgoingMessage';
import OutgoingMessage from './OutgoingMessage';
import type { CDSResponseType } from './cds/Types.d';
import * as Bytes from '../Bytes';
import { getRandomBytes, getZeroes, encryptAttachment } from '../Crypto';
import {
@ -2447,34 +2445,12 @@ export default class MessageSender {
return this.server.getProfile(uuid.toString(), options);
}
async checkAccountExistence(uuid: UUID): Promise<boolean> {
return this.server.checkAccountExistence(uuid);
}
async getProfileForUsername(
username: string
): ReturnType<WebAPIType['getProfileForUsername']> {
return this.server.getProfileForUsername(username);
}
async getUuidsForE164s(
numbers: ReadonlyArray<string>
): Promise<Dictionary<UUIDStringType | null>> {
return this.server.getUuidsForE164s(numbers);
}
async getUuidsForE164sV2(
e164s: ReadonlyArray<string>,
acis: ReadonlyArray<UUIDStringType>,
accessKeys: ReadonlyArray<string>
): Promise<CDSResponseType> {
return this.server.getUuidsForE164sV2({
e164s,
acis,
accessKeys,
});
}
async getAvatar(path: string): Promise<ReturnType<WebAPIType['getAvatar']>> {
return this.server.getAvatar(path);
}

View File

@ -11,7 +11,6 @@ import type { Response } from 'node-fetch';
import fetch from 'node-fetch';
import ProxyAgent from 'proxy-agent';
import { Agent } from 'https';
import type { Dictionary } from 'lodash';
import { escapeRegExp, isNumber } from 'lodash';
import is from '@sindresorhus/is';
import PQueue from 'p-queue';
@ -767,10 +766,10 @@ export type ConfirmCodeResultType = Readonly<{
deviceId?: number;
}>;
export type GetUuidsForE164sV2OptionsType = Readonly<{
export type CdsLookupOptionsType = Readonly<{
e164s: ReadonlyArray<string>;
acis: ReadonlyArray<UUIDStringType>;
accessKeys: ReadonlyArray<string>;
acis?: ReadonlyArray<UUIDStringType>;
accessKeys?: ReadonlyArray<string>;
}>;
type GetProfileCommonOptionsType = Readonly<
@ -812,6 +811,7 @@ export type GetGroupCredentialsResultType = Readonly<{
export type WebAPIType = {
startRegistration(): unknown;
finishRegistration(baton: unknown): void;
cdsLookup: (options: CdsLookupOptionsType) => Promise<CDSResponseType>;
confirmCode: (
number: string,
code: string,
@ -880,12 +880,6 @@ export type WebAPIType = {
getStorageCredentials: MessageSender['getStorageCredentials'];
getStorageManifest: MessageSender['getStorageManifest'];
getStorageRecords: MessageSender['getStorageRecords'];
getUuidsForE164s: (
e164s: ReadonlyArray<string>
) => Promise<Dictionary<UUIDStringType | null>>;
getUuidsForE164sV2: (
options: GetUuidsForE164sV2OptionsType
) => Promise<CDSResponseType>;
fetchLinkPreviewMetadata: (
href: string,
abortSignal: AbortSignal
@ -1251,6 +1245,7 @@ export function initialize({
unregisterRequestHandler,
authenticate,
logout,
cdsLookup,
checkAccountExistence,
confirmCode,
createGroup,
@ -1285,8 +1280,6 @@ export function initialize({
getStorageCredentials,
getStorageManifest,
getStorageRecords,
getUuidsForE164s,
getUuidsForE164sV2,
makeProxiedRequest,
makeSfuRequest,
modifyGroup,
@ -2858,25 +2851,11 @@ export function initialize({
return socketManager.getProvisioningResource(handler);
}
async function getUuidsForE164s(
e164s: ReadonlyArray<string>
): Promise<Dictionary<UUIDStringType | null>> {
const map = await cds.request({
e164s,
});
const result: Dictionary<UUIDStringType | null> = {};
for (const [key, value] of map) {
result[key] = value.pni ?? value.aci ?? null;
}
return result;
}
async function getUuidsForE164sV2({
async function cdsLookup({
e164s,
acis,
accessKeys,
}: GetUuidsForE164sV2OptionsType): Promise<CDSResponseType> {
acis = [],
accessKeys = [],
}: CdsLookupOptionsType): Promise<CDSResponseType> {
return cds.request({
e164s,
acis,

View File

@ -85,28 +85,20 @@ export abstract class CDSSocketBase<
const aciUakPairs = new Array<Uint8Array>();
let version: 1 | 2;
if (acis) {
strictAssert(accessKeys, 'accessKeys are required when acis are present');
const version = 2;
strictAssert(
acis.length === accessKeys.length,
`Number of ACIs ${acis.length} is different ` +
`from number of access keys ${accessKeys.length}`
);
strictAssert(
acis.length === accessKeys.length,
`Number of ACIs ${acis.length} is different ` +
`from number of access keys ${accessKeys.length}`
for (let i = 0; i < acis.length; i += 1) {
aciUakPairs.push(
Bytes.concatenate([
uuidToBytes(acis[i]),
Bytes.fromBase64(accessKeys[i]),
])
);
version = 2;
for (let i = 0; i < acis.length; i += 1) {
aciUakPairs.push(
Bytes.concatenate([
uuidToBytes(acis[i]),
Bytes.fromBase64(accessKeys[i]),
])
);
}
} else {
version = 1;
}
const request = Proto.CDSClientRequest.encode({

View File

@ -17,7 +17,6 @@ import {
} from '../../Crypto';
import { calculateAgreement, generateKeyPair } from '../../Curve';
import * as Bytes from '../../Bytes';
import { strictAssert } from '../../util/assert';
import { UUID } from '../../types/UUID';
import type { CDSBaseOptionsType } from './CDSBase';
import { CDSBase } from './CDSBase';
@ -125,11 +124,7 @@ function getSgxConstants() {
export class LegacyCDS extends CDSBase<LegacyCDSOptionsType> {
public override async request({
e164s,
acis,
accessKeys,
}: CDSRequestOptionsType): Promise<CDSResponseType> {
strictAssert(!acis && !accessKeys, 'LegacyCDS does not support PNP');
const directoryAuth = await this.getAuth();
const attestationResult = await this.putAttestation(directoryAuth);

View File

@ -17,7 +17,7 @@ export type CDSResponseType = ReadonlyMap<string, CDSResponseEntryType>;
export type CDSRequestOptionsType = Readonly<{
e164s: ReadonlyArray<string>;
acis?: ReadonlyArray<UUIDStringType>;
accessKeys?: ReadonlyArray<string>;
acis: ReadonlyArray<UUIDStringType>;
accessKeys: ReadonlyArray<string>;
timeout?: number;
}>;

View File

@ -3,22 +3,22 @@
import type { ConversationController } from './ConversationController';
import type { ConversationModel } from './models/conversations';
import type SendMessage from './textsecure/SendMessage';
import type { WebAPIType } from './textsecure/WebAPI';
import { assert } from './util/assert';
import { getOwn } from './util/getOwn';
import { isNotNil } from './util/isNotNil';
import { getUuidsForE164s } from './util/getUuidsForE164s';
export async function updateConversationsWithUuidLookup({
conversationController,
conversations,
messaging,
server,
}: Readonly<{
conversationController: Pick<
ConversationController,
'maybeMergeContacts' | 'get'
>;
conversations: ReadonlyArray<ConversationModel>;
messaging: Pick<SendMessage, 'getUuidsForE164s' | 'checkAccountExistence'>;
server: Pick<WebAPIType, 'cdsLookup' | 'checkAccountExistence'>;
}>): Promise<void> {
const e164s = conversations
.map(conversation => conversation.get('e164'))
@ -27,7 +27,7 @@ export async function updateConversationsWithUuidLookup({
return;
}
const serverLookup = await messaging.getUuidsForE164s(e164s);
const serverLookup = await getUuidsForE164s(server, e164s);
await Promise.all(
conversations.map(async conversation => {
@ -38,11 +38,12 @@ export async function updateConversationsWithUuidLookup({
let finalConversation: ConversationModel;
const uuidFromServer = getOwn(serverLookup, e164);
if (uuidFromServer) {
const pairFromServer = serverLookup.get(e164);
if (pairFromServer) {
const maybeFinalConversation =
conversationController.maybeMergeContacts({
aci: uuidFromServer,
aci: pairFromServer.aci,
pni: pairFromServer.pni,
e164,
reason: 'updateConversationsWithUuidLookup',
});
@ -59,10 +60,8 @@ export async function updateConversationsWithUuidLookup({
// they can't be looked up by a phone number. Check that uuid still exists,
// and if not - drop it.
let finalUuid = finalConversation.getUuid();
if (!uuidFromServer && finalUuid) {
const doesAccountExist = await messaging.checkAccountExistence(
finalUuid
);
if (!pairFromServer && finalUuid) {
const doesAccountExist = await server.checkAccountExistence(finalUuid);
if (!doesAccountExist) {
finalConversation.updateUuid(undefined);
finalUuid = undefined;

View File

@ -0,0 +1,45 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { CDSResponseType } from '../textsecure/cds/Types.d';
import type { WebAPIType } from '../textsecure/WebAPI';
import type { UUIDStringType } from '../types/UUID';
import * as log from '../logging/log';
import { isDirectConversation, isMe } from './whatTypeOfConversation';
export async function getUuidsForE164s(
server: Pick<WebAPIType, 'cdsLookup'>,
e164s: ReadonlyArray<string>
): Promise<CDSResponseType> {
// Note: these have no relationship to supplied e164s. We just provide
// all available information to the server so that it could return as many
// ACI+PNI+E164 matches as possible.
const acis = new Array<UUIDStringType>();
const accessKeys = new Array<string>();
for (const convo of window.ConversationController.getAll()) {
if (!isDirectConversation(convo.attributes) || isMe(convo.attributes)) {
continue;
}
const aci = convo.getUuid();
if (!aci) {
continue;
}
convo.deriveAccessKeyIfNeeded();
const accessKey = convo.get('accessKey');
if (!accessKey) {
continue;
}
acis.push(aci.toString());
accessKeys.push(accessKey);
}
log.info(
`getUuidsForE164s(${e164s}): acis=${acis.length} ` +
`accessKeys=${accessKeys.length}`
);
return server.cdsLookup({ e164s, acis, accessKeys });
}

View File

@ -12,6 +12,7 @@ import { downloadAttachment } from './downloadAttachment';
import { generateSecurityNumber } from './safetyNumber';
import { getStringForProfileChange } from './getStringForProfileChange';
import { getTextWithMentions } from './getTextWithMentions';
import { getUuidsForE164s } from './getUuidsForE164s';
import { getUserAgent } from './getUserAgent';
import { hasExpired } from './hasExpired';
import {
@ -81,4 +82,5 @@ export {
toWebSafeBase64,
zkgroup,
expirationTimer,
getUuidsForE164s,
};

View File

@ -13,6 +13,7 @@ import { HTTPError } from '../textsecure/Errors';
import { showToast } from './showToast';
import { strictAssert } from './assert';
import type { UUIDFetchStateKeyType } from './uuidFetchState';
import { getUuidsForE164s } from './getUuidsForE164s';
export type LookupConversationWithoutUuidActionsType = Readonly<{
lookupConversationWithoutUuid: typeof lookupConversationWithoutUuid;
@ -62,19 +63,22 @@ export async function lookupConversationWithoutUuid(
const { showUserNotFoundModal, setIsFetchingUUID } = options;
setIsFetchingUUID(identifier, true);
const { messaging } = window.textsecure;
if (!messaging) {
throw new Error('messaging is not available!');
const { server } = window.textsecure;
if (!server) {
throw new Error('server is not available!');
}
try {
let conversationId: string | undefined;
if (options.type === 'e164') {
const serverLookup = await messaging.getUuidsForE164s([options.e164]);
const serverLookup = await getUuidsForE164s(server, [options.e164]);
if (serverLookup[options.e164]) {
const maybePair = serverLookup.get(options.e164);
if (maybePair) {
const convo = window.ConversationController.maybeMergeContacts({
aci: serverLookup[options.e164] || undefined,
aci: maybePair.aci,
pni: maybePair.pni,
e164: options.e164,
reason: 'startNewConversationWithoutUuid(e164)',
});