Introduce Service Id Types

Co-authored-by: Scott Nonnenberg <scott@signal.org>
This commit is contained in:
Fedor Indutny 2023-08-10 18:43:33 +02:00 committed by Jamie Kyle
parent 414c0a58d3
commit 366b875fd2
269 changed files with 5832 additions and 5550 deletions

View file

@ -34,14 +34,12 @@ import {
HTTPError,
} from './Errors';
import type { CallbackResultType, CustomError } from './Types.d';
import { isValidNumber } from '../types/PhoneNumber';
import { Address } from '../types/Address';
import * as Errors from '../types/errors';
import { QualifiedAddress } from '../types/QualifiedAddress';
import { UUID, isValidUuid } from '../types/UUID';
import type { ServiceIdString } from '../types/ServiceId';
import { Sessions, IdentityKeys } from '../LibSignalStores';
import { updateConversationsWithUuidLookup } from '../updateConversationsWithUuidLookup';
import { getKeysForIdentifier } from './getKeysForIdentifier';
import { getKeysForServiceId } from './getKeysForServiceId';
import { SignalService as Proto } from '../protobuf';
import * as log from '../logging/log';
@ -51,7 +49,7 @@ export const enum SenderCertificateMode {
}
export type SendLogCallbackType = (options: {
identifier: string;
serviceId: ServiceIdString;
deviceIds: Array<number>;
}) => Promise<void>;
@ -109,7 +107,7 @@ export default class OutgoingMessage {
timestamp: number;
identifiers: ReadonlyArray<string>;
serviceIds: ReadonlyArray<ServiceIdString>;
message: Proto.Content | PlaintextContent;
@ -117,15 +115,15 @@ export default class OutgoingMessage {
plaintext?: Uint8Array;
identifiersCompleted: number;
serviceIdsCompleted: number;
errors: Array<CustomError>;
successfulIdentifiers: Array<string>;
successfulServiceIds: Array<ServiceIdString>;
failoverIdentifiers: Array<string>;
failoverServiceIds: Array<ServiceIdString>;
unidentifiedDeliveries: Array<string>;
unidentifiedDeliveries: Array<ServiceIdString>;
sendMetadata?: SendMetadataType;
@ -147,7 +145,7 @@ export default class OutgoingMessage {
callback,
contentHint,
groupId,
identifiers,
serviceIds,
message,
options,
sendLogCallback,
@ -159,7 +157,7 @@ export default class OutgoingMessage {
callback: (result: CallbackResultType) => void;
contentHint: number;
groupId: string | undefined;
identifiers: ReadonlyArray<string>;
serviceIds: ReadonlyArray<ServiceIdString>;
message: Proto.Content | Proto.DataMessage | PlaintextContent;
options?: OutgoingMessageOptionsType;
sendLogCallback?: SendLogCallbackType;
@ -178,17 +176,17 @@ export default class OutgoingMessage {
this.server = server;
this.timestamp = timestamp;
this.identifiers = identifiers;
this.serviceIds = serviceIds;
this.contentHint = contentHint;
this.groupId = groupId;
this.callback = callback;
this.story = story;
this.urgent = urgent;
this.identifiersCompleted = 0;
this.serviceIdsCompleted = 0;
this.errors = [];
this.successfulIdentifiers = [];
this.failoverIdentifiers = [];
this.successfulServiceIds = [];
this.failoverServiceIds = [];
this.unidentifiedDeliveries = [];
this.recipients = {};
this.sendLogCallback = sendLogCallback;
@ -198,8 +196,8 @@ export default class OutgoingMessage {
}
numberCompleted(): void {
this.identifiersCompleted += 1;
if (this.identifiersCompleted >= this.identifiers.length) {
this.serviceIdsCompleted += 1;
if (this.serviceIdsCompleted >= this.serviceIds.length) {
const proto = this.message;
const contentProto = this.getContentProtoBytes();
const { timestamp, contentHint, recipients, urgent } = this;
@ -221,8 +219,8 @@ export default class OutgoingMessage {
}
this.callback({
successfulIdentifiers: this.successfulIdentifiers,
failoverIdentifiers: this.failoverIdentifiers,
successfulServiceIds: this.successfulServiceIds,
failoverServiceIds: this.failoverServiceIds,
errors: this.errors,
unidentifiedDeliveries: this.unidentifiedDeliveries,
@ -239,7 +237,7 @@ export default class OutgoingMessage {
}
registerError(
identifier: string,
serviceId: ServiceIdString,
reason: string,
providedError?: Error
): void {
@ -247,9 +245,9 @@ export default class OutgoingMessage {
if (!error || (error instanceof HTTPError && error.code !== 404)) {
if (error && error.code === 428) {
error = new SendMessageChallengeError(identifier, error);
error = new SendMessageChallengeError(serviceId, error);
} else {
error = new OutgoingMessageError(identifier, null, null, error);
error = new OutgoingMessageError(serviceId, null, null, error);
}
}
@ -260,51 +258,51 @@ export default class OutgoingMessage {
}
reloadDevicesAndSend(
identifier: string,
serviceId: ServiceIdString,
recurse?: boolean
): () => Promise<void> {
return async () => {
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
const ourAci = window.textsecure.storage.user.getCheckedAci();
const deviceIds = await window.textsecure.storage.protocol.getDeviceIds({
ourUuid,
identifier,
ourServiceId: ourAci,
serviceId,
});
if (deviceIds.length === 0) {
this.registerError(
identifier,
serviceId,
'reloadDevicesAndSend: Got empty device list when loading device keys',
undefined
);
return undefined;
}
return this.doSendMessage(identifier, deviceIds, recurse);
return this.doSendMessage(serviceId, deviceIds, recurse);
};
}
async getKeysForIdentifier(
identifier: string,
async getKeysForServiceId(
serviceId: ServiceIdString,
updateDevices?: Array<number>
): Promise<void | Array<void | null>> {
): Promise<void> {
const { sendMetadata } = this;
const info =
sendMetadata && sendMetadata[identifier]
? sendMetadata[identifier]
sendMetadata && sendMetadata[serviceId]
? sendMetadata[serviceId]
: { accessKey: undefined };
const { accessKey } = info;
const { accessKeyFailed } = await getKeysForIdentifier(
identifier,
const { accessKeyFailed } = await getKeysForServiceId(
serviceId,
this.server,
updateDevices,
accessKey
);
if (accessKeyFailed && !this.failoverIdentifiers.includes(identifier)) {
this.failoverIdentifiers.push(identifier);
if (accessKeyFailed && !this.failoverServiceIds.includes(serviceId)) {
this.failoverServiceIds.push(serviceId);
}
}
async transmitMessage(
identifier: string,
serviceId: ServiceIdString,
jsonData: ReadonlyArray<MessageType>,
timestamp: number,
{ accessKey }: { accessKey?: string } = {}
@ -312,19 +310,14 @@ export default class OutgoingMessage {
let promise;
if (accessKey) {
promise = this.server.sendMessagesUnauth(
identifier,
jsonData,
timestamp,
{
accessKey,
online: this.online,
story: this.story,
urgent: this.urgent,
}
);
promise = this.server.sendMessagesUnauth(serviceId, jsonData, timestamp, {
accessKey,
online: this.online,
story: this.story,
urgent: this.urgent,
});
} else {
promise = this.server.sendMessages(identifier, jsonData, timestamp, {
promise = this.server.sendMessages(serviceId, jsonData, timestamp, {
online: this.online,
story: this.story,
urgent: this.urgent,
@ -338,12 +331,12 @@ export default class OutgoingMessage {
// 428 should throw SendMessageChallengeError
// all other network errors can be retried later.
if (e.code === 404) {
throw new UnregisteredUserError(identifier, e);
throw new UnregisteredUserError(serviceId, e);
}
if (e.code === 428) {
throw new SendMessageChallengeError(identifier, e);
throw new SendMessageChallengeError(serviceId, e);
}
throw new SendMessageNetworkError(identifier, jsonData, e);
throw new SendMessageNetworkError(serviceId, jsonData, e);
}
throw e;
});
@ -394,12 +387,12 @@ export default class OutgoingMessage {
}
async doSendMessage(
identifier: string,
serviceId: ServiceIdString,
deviceIds: Array<number>,
recurse?: boolean
): Promise<void> {
const { sendMetadata } = this;
const { accessKey, senderCertificate } = sendMetadata?.[identifier] || {};
const { accessKey, senderCertificate } = sendMetadata?.[serviceId] || {};
if (accessKey && !senderCertificate) {
log.warn(
@ -411,12 +404,9 @@ export default class OutgoingMessage {
// We don't send to ourselves unless sealedSender is enabled
const ourNumber = window.textsecure.storage.user.getNumber();
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
const ourAci = window.textsecure.storage.user.getCheckedAci();
const ourDeviceId = window.textsecure.storage.user.getDeviceId();
if (
(identifier === ourNumber || identifier === ourUuid.toString()) &&
!sealedSender
) {
if ((serviceId === ourNumber || serviceId === ourAci) && !sealedSender) {
deviceIds = reject(
deviceIds,
deviceId =>
@ -427,15 +417,14 @@ export default class OutgoingMessage {
);
}
const sessionStore = new Sessions({ ourUuid });
const identityKeyStore = new IdentityKeys({ ourUuid });
const sessionStore = new Sessions({ ourServiceId: ourAci });
const identityKeyStore = new IdentityKeys({ ourServiceId: ourAci });
return Promise.all(
deviceIds.map(async destinationDeviceId => {
const theirUuid = UUID.checkedLookup(identifier);
const address = new QualifiedAddress(
ourUuid,
new Address(theirUuid, destinationDeviceId)
ourAci,
new Address(serviceId, destinationDeviceId)
);
return window.textsecure.storage.protocol.enqueueSessionJob<MessageType>(
@ -443,7 +432,7 @@ export default class OutgoingMessage {
`doSendMessage(${address.toString()})`,
async () => {
const protocolAddress = ProtocolAddress.new(
theirUuid.toString(),
serviceId,
destinationDeviceId
);
@ -517,21 +506,21 @@ export default class OutgoingMessage {
)
.then(async (jsonData: Array<MessageType>) => {
if (sealedSender) {
return this.transmitMessage(identifier, jsonData, this.timestamp, {
return this.transmitMessage(serviceId, jsonData, this.timestamp, {
accessKey,
}).then(
() => {
this.recipients[identifier] = deviceIds;
this.unidentifiedDeliveries.push(identifier);
this.successfulIdentifiers.push(identifier);
this.recipients[serviceId] = deviceIds;
this.unidentifiedDeliveries.push(serviceId);
this.successfulServiceIds.push(serviceId);
this.numberCompleted();
if (this.sendLogCallback) {
void this.sendLogCallback({
identifier,
serviceId,
deviceIds,
});
} else if (this.successfulIdentifiers.length > 1) {
} else if (this.successfulServiceIds.length > 1) {
log.warn(
`OutgoingMessage.doSendMessage: no sendLogCallback provided for message ${this.timestamp}, but multiple recipients`
);
@ -543,18 +532,18 @@ export default class OutgoingMessage {
(error.code === 401 || error.code === 403)
) {
log.warn(
`OutgoingMessage.doSendMessage: Failing over to unsealed send for identifier ${identifier}`
`OutgoingMessage.doSendMessage: Failing over to unsealed send for serviceId ${serviceId}`
);
if (this.failoverIdentifiers.indexOf(identifier) === -1) {
this.failoverIdentifiers.push(identifier);
if (this.failoverServiceIds.indexOf(serviceId) === -1) {
this.failoverServiceIds.push(serviceId);
}
// This ensures that we don't hit this codepath the next time through
if (sendMetadata) {
delete sendMetadata[identifier];
delete sendMetadata[serviceId];
}
return this.doSendMessage(identifier, deviceIds, recurse);
return this.doSendMessage(serviceId, deviceIds, recurse);
}
throw error;
@ -562,18 +551,18 @@ export default class OutgoingMessage {
);
}
return this.transmitMessage(identifier, jsonData, this.timestamp).then(
return this.transmitMessage(serviceId, jsonData, this.timestamp).then(
() => {
this.successfulIdentifiers.push(identifier);
this.recipients[identifier] = deviceIds;
this.successfulServiceIds.push(serviceId);
this.recipients[serviceId] = deviceIds;
this.numberCompleted();
if (this.sendLogCallback) {
void this.sendLogCallback({
identifier,
serviceId,
deviceIds,
});
} else if (this.successfulIdentifiers.length > 1) {
} else if (this.successfulServiceIds.length > 1) {
log.warn(
`OutgoingMessage.doSendMessage: no sendLogCallback provided for message ${this.timestamp}, but multiple recipients`
);
@ -588,7 +577,7 @@ export default class OutgoingMessage {
) {
if (!recurse) {
this.registerError(
identifier,
serviceId,
'Hit retry limit attempting to reload device list',
error
);
@ -602,18 +591,15 @@ export default class OutgoingMessage {
};
let p: Promise<any> = Promise.resolve();
if (error.code === 409) {
p = this.removeDeviceIdsForIdentifier(
identifier,
p = this.removeDeviceIdsForServiceId(
serviceId,
response.extraDevices || []
);
} else {
p = Promise.all(
(response.staleDevices || []).map(async (deviceId: number) => {
await window.textsecure.storage.protocol.archiveSession(
new QualifiedAddress(
ourUuid,
new Address(UUID.checkedLookup(identifier), deviceId)
)
new QualifiedAddress(ourAci, new Address(serviceId, deviceId))
);
})
);
@ -624,10 +610,10 @@ export default class OutgoingMessage {
error.code === 410
? response.staleDevices
: response.missingDevices;
return this.getKeysForIdentifier(identifier, resetDevices).then(
return this.getKeysForServiceId(serviceId, resetDevices).then(
// We continue to retry as long as the error code was 409; the assumption is
// that we'll request new device info and the next request will succeed.
this.reloadDevicesAndSend(identifier, error.code === 409)
this.reloadDevicesAndSend(serviceId, error.code === 409)
);
});
}
@ -637,32 +623,30 @@ export default class OutgoingMessage {
error instanceof LibSignalErrorBase &&
error.code === ErrorCode.UntrustedIdentity
) {
newError = new OutgoingIdentityKeyError(identifier, error);
newError = new OutgoingIdentityKeyError(serviceId, error);
log.error(
'Got "key changed" error from encrypt - no identityKey for application layer',
identifier,
serviceId,
deviceIds
);
log.info('closing all sessions for', identifier);
window.textsecure.storage.protocol
.archiveAllSessions(UUID.checkedLookup(identifier))
.then(
() => {
throw error;
},
innerError => {
log.error(
'doSendMessage: Error closing sessions: ' +
`${Errors.toLogFormat(innerError)}`
);
throw error;
}
);
log.info('closing all sessions for', serviceId);
window.textsecure.storage.protocol.archiveAllSessions(serviceId).then(
() => {
throw error;
},
innerError => {
log.error(
'doSendMessage: Error closing sessions: ' +
`${Errors.toLogFormat(innerError)}`
);
throw error;
}
);
}
this.registerError(
identifier,
serviceId,
'Failed to create or send message',
newError
);
@ -671,87 +655,43 @@ export default class OutgoingMessage {
});
}
async removeDeviceIdsForIdentifier(
identifier: string,
async removeDeviceIdsForServiceId(
serviceId: ServiceIdString,
deviceIdsToRemove: Array<number>
): Promise<void> {
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
const theirUuid = UUID.checkedLookup(identifier);
const ourAci = window.textsecure.storage.user.getCheckedAci();
await Promise.all(
deviceIdsToRemove.map(async deviceId => {
await window.textsecure.storage.protocol.archiveSession(
new QualifiedAddress(ourUuid, new Address(theirUuid, deviceId))
new QualifiedAddress(ourAci, new Address(serviceId, deviceId))
);
})
);
}
async sendToIdentifier(providedIdentifier: string): Promise<void> {
let identifier = providedIdentifier;
async sendToServiceId(serviceId: ServiceIdString): Promise<void> {
try {
if (isValidUuid(identifier)) {
// We're good!
} else if (isValidNumber(identifier)) {
if (!window.textsecure.server) {
throw new Error(
'sendToIdentifier: window.textsecure.server is not available!'
);
}
try {
await updateConversationsWithUuidLookup({
conversationController: window.ConversationController,
conversations: [
window.ConversationController.getOrCreate(identifier, 'private'),
],
server: window.textsecure.server,
});
const uuid =
window.ConversationController.get(identifier)?.get('uuid');
if (!uuid) {
throw new UnregisteredUserError(
identifier,
new HTTPError('User is not registered', {
code: -1,
headers: {},
})
);
}
identifier = uuid;
} catch (error) {
log.error(
`sendToIdentifier: Failed to fetch UUID for identifier ${identifier}`,
Errors.toLogFormat(error)
);
}
} else {
throw new Error(
`sendToIdentifier: identifier ${identifier} was neither a UUID or E164`
);
}
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
const ourAci = window.textsecure.storage.user.getCheckedAci();
const deviceIds = await window.textsecure.storage.protocol.getDeviceIds({
ourUuid,
identifier,
ourServiceId: ourAci,
serviceId,
});
if (deviceIds.length === 0) {
await this.getKeysForIdentifier(identifier);
await this.getKeysForServiceId(serviceId);
}
await this.reloadDevicesAndSend(identifier, true)();
await this.reloadDevicesAndSend(serviceId, true)();
} catch (error) {
if (
error instanceof LibSignalErrorBase &&
error.code === ErrorCode.UntrustedIdentity
) {
const newError = new OutgoingIdentityKeyError(identifier, error);
this.registerError(identifier, 'Untrusted identity', newError);
const newError = new OutgoingIdentityKeyError(serviceId, error);
this.registerError(serviceId, 'Untrusted identity', newError);
} else {
this.registerError(
identifier,
`Failed to retrieve new device keys for identifier ${identifier}`,
serviceId,
`Failed to retrieve new device keys for serviceId ${serviceId}`,
error
);
}