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

@ -38,7 +38,8 @@ import {
generatePreKey,
generateKyberPreKey,
} from '../Curve';
import { UUID, UUIDKind } from '../types/UUID';
import type { ServiceIdString, PniString } from '../types/ServiceId';
import { ServiceIdKind, normalizeAci, normalizePni } from '../types/ServiceId';
import { isMoreRecentThan, isOlderThan } from '../util/timestamp';
import { ourProfileKeyService } from '../services/ourProfileKey';
import { assertDev, strictAssert } from '../util/assert';
@ -49,8 +50,8 @@ import { SignalService as Proto } from '../protobuf';
import * as log from '../logging/log';
import type { StorageAccessType } from '../types/Storage';
type StorageKeyByUuidKind = {
[kind in UUIDKind]: keyof StorageAccessType;
type StorageKeyByServiceIdKind = {
[kind in ServiceIdKind]: keyof StorageAccessType;
};
const DAY = 24 * 60 * 60 * 1000;
@ -59,44 +60,44 @@ const STARTING_KEY_ID = 1;
const PROFILE_KEY_LENGTH = 32;
const KEY_TOO_OLD_THRESHOLD = 14 * DAY;
export const KYBER_KEY_ID_KEY: StorageKeyByUuidKind = {
[UUIDKind.ACI]: 'maxKyberPreKeyId',
[UUIDKind.Unknown]: 'maxKyberPreKeyId',
[UUIDKind.PNI]: 'maxKyberPreKeyIdPNI',
export const KYBER_KEY_ID_KEY: StorageKeyByServiceIdKind = {
[ServiceIdKind.ACI]: 'maxKyberPreKeyId',
[ServiceIdKind.Unknown]: 'maxKyberPreKeyId',
[ServiceIdKind.PNI]: 'maxKyberPreKeyIdPNI',
};
const LAST_RESORT_KEY_ARCHIVE_AGE = 30 * DAY;
const LAST_RESORT_KEY_ROTATION_AGE = DAY * 1.5;
const LAST_RESORT_KEY_MINIMUM = 5;
const LAST_RESORT_KEY_UPDATE_TIME_KEY: StorageKeyByUuidKind = {
[UUIDKind.ACI]: 'lastResortKeyUpdateTime',
[UUIDKind.Unknown]: 'lastResortKeyUpdateTime',
[UUIDKind.PNI]: 'lastResortKeyUpdateTimePNI',
const LAST_RESORT_KEY_UPDATE_TIME_KEY: StorageKeyByServiceIdKind = {
[ServiceIdKind.ACI]: 'lastResortKeyUpdateTime',
[ServiceIdKind.Unknown]: 'lastResortKeyUpdateTime',
[ServiceIdKind.PNI]: 'lastResortKeyUpdateTimePNI',
};
const PRE_KEY_ARCHIVE_AGE = 90 * DAY;
const PRE_KEY_GEN_BATCH_SIZE = 100;
const PRE_KEY_MAX_COUNT = 200;
const PRE_KEY_ID_KEY: StorageKeyByUuidKind = {
[UUIDKind.ACI]: 'maxPreKeyId',
[UUIDKind.Unknown]: 'maxPreKeyId',
[UUIDKind.PNI]: 'maxPreKeyIdPNI',
const PRE_KEY_ID_KEY: StorageKeyByServiceIdKind = {
[ServiceIdKind.ACI]: 'maxPreKeyId',
[ServiceIdKind.Unknown]: 'maxPreKeyId',
[ServiceIdKind.PNI]: 'maxPreKeyIdPNI',
};
const PRE_KEY_MINIMUM = 10;
const SIGNED_PRE_KEY_ARCHIVE_AGE = 30 * DAY;
export const SIGNED_PRE_KEY_ID_KEY: StorageKeyByUuidKind = {
[UUIDKind.ACI]: 'signedKeyId',
[UUIDKind.Unknown]: 'signedKeyId',
[UUIDKind.PNI]: 'signedKeyIdPNI',
export const SIGNED_PRE_KEY_ID_KEY: StorageKeyByServiceIdKind = {
[ServiceIdKind.ACI]: 'signedKeyId',
[ServiceIdKind.Unknown]: 'signedKeyId',
[ServiceIdKind.PNI]: 'signedKeyIdPNI',
};
const SIGNED_PRE_KEY_ROTATION_AGE = DAY * 1.5;
const SIGNED_PRE_KEY_MINIMUM = 5;
const SIGNED_PRE_KEY_UPDATE_TIME_KEY: StorageKeyByUuidKind = {
[UUIDKind.ACI]: 'signedKeyUpdateTime',
[UUIDKind.Unknown]: 'signedKeyUpdateTime',
[UUIDKind.PNI]: 'signedKeyUpdateTimePNI',
const SIGNED_PRE_KEY_UPDATE_TIME_KEY: StorageKeyByServiceIdKind = {
[ServiceIdKind.ACI]: 'signedKeyUpdateTime',
[ServiceIdKind.Unknown]: 'signedKeyUpdateTime',
[ServiceIdKind.PNI]: 'signedKeyUpdateTimePNI',
};
type CreateAccountOptionsType = Readonly<{
@ -111,7 +112,10 @@ type CreateAccountOptionsType = Readonly<{
accessKey?: Uint8Array;
}>;
function getNextKeyId(kind: UUIDKind, keys: StorageKeyByUuidKind): number {
function getNextKeyId(
kind: ServiceIdKind,
keys: StorageKeyByServiceIdKind
): number {
const id = window.storage.get(keys[kind]);
if (isNumber(id)) {
@ -119,8 +123,8 @@ function getNextKeyId(kind: UUIDKind, keys: StorageKeyByUuidKind): number {
}
// For PNI ids, start with existing ACI id
if (kind === UUIDKind.PNI) {
return window.storage.get(keys[UUIDKind.ACI], STARTING_KEY_ID);
if (kind === ServiceIdKind.PNI) {
return window.storage.get(keys[ServiceIdKind.ACI], STARTING_KEY_ID);
}
return STARTING_KEY_ID;
@ -168,9 +172,9 @@ export default class AccountManager extends EventTarget {
}
async decryptDeviceName(base64: string): Promise<string> {
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
const ourAci = window.textsecure.storage.user.getCheckedAci();
const identityKey =
window.textsecure.storage.protocol.getIdentityKeyPair(ourUuid);
window.textsecure.storage.protocol.getIdentityKeyPair(ourAci);
if (!identityKey) {
throw new Error('decryptDeviceName: No identity key pair!');
}
@ -196,7 +200,7 @@ export default class AccountManager extends EventTarget {
const { storage } = window.textsecure;
const deviceName = storage.user.getDeviceName();
const identityKeyPair = storage.protocol.getIdentityKeyPair(
storage.user.getCheckedUuid()
storage.user.getCheckedAci()
);
strictAssert(
identityKeyPair !== undefined,
@ -234,14 +238,14 @@ export default class AccountManager extends EventTarget {
accessKey,
});
const uploadKeys = async (kind: UUIDKind) => {
const uploadKeys = async (kind: ServiceIdKind) => {
const keys = await this._generateKeys(PRE_KEY_GEN_BATCH_SIZE, kind);
await this.server.registerKeys(keys, kind);
await this._confirmKeys(keys, kind);
};
await uploadKeys(UUIDKind.ACI);
await uploadKeys(UUIDKind.PNI);
await uploadKeys(ServiceIdKind.ACI);
await uploadKeys(ServiceIdKind.PNI);
} finally {
this.server.finishRegistration(registrationBaton);
}
@ -344,14 +348,14 @@ export default class AccountManager extends EventTarget {
readReceipts: provisionMessage.readReceipts,
});
const uploadKeys = async (kind: UUIDKind) => {
const uploadKeys = async (kind: ServiceIdKind) => {
const keys = await this._generateKeys(PRE_KEY_GEN_BATCH_SIZE, kind);
try {
await this.server.registerKeys(keys, kind);
await this._confirmKeys(keys, kind);
} catch (error) {
if (kind === UUIDKind.PNI) {
if (kind === ServiceIdKind.PNI) {
log.error(
'Failed to upload PNI prekeys. Moving on',
Errors.toLogFormat(error)
@ -363,9 +367,9 @@ export default class AccountManager extends EventTarget {
}
};
await uploadKeys(UUIDKind.ACI);
await uploadKeys(ServiceIdKind.ACI);
if (provisionMessage.pniKeyPair) {
await uploadKeys(UUIDKind.PNI);
await uploadKeys(ServiceIdKind.PNI);
}
} finally {
this.server.finishRegistration(registrationBaton);
@ -375,12 +379,12 @@ export default class AccountManager extends EventTarget {
});
}
private getIdentityKeyOrThrow(ourUuid: UUID): KeyPairType {
private getIdentityKeyOrThrow(ourServiceId: ServiceIdString): KeyPairType {
const { storage } = window.textsecure;
const store = storage.protocol;
let identityKey: KeyPairType | undefined;
try {
identityKey = store.getIdentityKeyPair(ourUuid);
identityKey = store.getIdentityKeyPair(ourServiceId);
} catch (error) {
const errorText = Errors.toLogFormat(error);
throw new Error(
@ -396,20 +400,20 @@ export default class AccountManager extends EventTarget {
}
private async generateNewPreKeys(
uuidKind: UUIDKind,
serviceIdKind: ServiceIdKind,
count: number
): Promise<Array<UploadPreKeyType>> {
const logId = `AccountManager.generateNewPreKeys(${uuidKind})`;
const logId = `AccountManager.generateNewPreKeys(${serviceIdKind})`;
const { storage } = window.textsecure;
const store = storage.protocol;
const startId = getNextKeyId(uuidKind, PRE_KEY_ID_KEY);
const startId = getNextKeyId(serviceIdKind, PRE_KEY_ID_KEY);
log.info(`${logId}: Generating ${count} new keys starting at ${startId}`);
const ourUuid = storage.user.getCheckedUuid(uuidKind);
const ourServiceId = storage.user.getCheckedServiceId(serviceIdKind);
if (typeof startId !== 'number') {
throw new Error(
`${logId}: Invalid ${PRE_KEY_ID_KEY[uuidKind]} in storage`
`${logId}: Invalid ${PRE_KEY_ID_KEY[serviceIdKind]} in storage`
);
}
@ -419,8 +423,8 @@ export default class AccountManager extends EventTarget {
}
await Promise.all([
store.storePreKeys(ourUuid, toSave),
storage.put(PRE_KEY_ID_KEY[uuidKind], startId + count),
store.storePreKeys(ourServiceId, toSave),
storage.put(PRE_KEY_ID_KEY[serviceIdKind], startId + count),
]);
return toSave.map(key => ({
@ -430,24 +434,24 @@ export default class AccountManager extends EventTarget {
}
private async generateNewKyberPreKeys(
uuidKind: UUIDKind,
serviceIdKind: ServiceIdKind,
count: number
): Promise<Array<UploadKyberPreKeyType>> {
const logId = `AccountManager.generateNewKyberPreKeys(${uuidKind})`;
const logId = `AccountManager.generateNewKyberPreKeys(${serviceIdKind})`;
const { storage } = window.textsecure;
const store = storage.protocol;
const startId = getNextKeyId(uuidKind, KYBER_KEY_ID_KEY);
const startId = getNextKeyId(serviceIdKind, KYBER_KEY_ID_KEY);
log.info(`${logId}: Generating ${count} new keys starting at ${startId}`);
const ourUuid = storage.user.getCheckedUuid(uuidKind);
const ourServiceId = storage.user.getCheckedServiceId(serviceIdKind);
if (typeof startId !== 'number') {
throw new Error(
`${logId}: Invalid ${KYBER_KEY_ID_KEY[uuidKind]} in storage`
`${logId}: Invalid ${KYBER_KEY_ID_KEY[serviceIdKind]} in storage`
);
}
const identityKey = this.getIdentityKeyOrThrow(ourUuid);
const identityKey = this.getIdentityKeyOrThrow(ourServiceId);
const toSave: Array<Omit<KyberPreKeyType, 'id'>> = [];
const toUpload: Array<UploadKyberPreKeyType> = [];
@ -460,7 +464,7 @@ export default class AccountManager extends EventTarget {
isConfirmed: false,
isLastResort: false,
keyId,
ourUuid: ourUuid.toString(),
ourUuid: ourServiceId,
});
toUpload.push({
keyId,
@ -470,18 +474,18 @@ export default class AccountManager extends EventTarget {
}
await Promise.all([
store.storeKyberPreKeys(ourUuid, toSave),
storage.put(KYBER_KEY_ID_KEY[uuidKind], startId + count),
store.storeKyberPreKeys(ourServiceId, toSave),
storage.put(KYBER_KEY_ID_KEY[serviceIdKind], startId + count),
]);
return toUpload;
}
async maybeUpdateKeys(uuidKind: UUIDKind): Promise<void> {
const logId = `maybeUpdateKeys(${uuidKind})`;
async maybeUpdateKeys(serviceIdKind: ServiceIdKind): Promise<void> {
const logId = `maybeUpdateKeys(${serviceIdKind})`;
await this.queueTask(async () => {
const { count: preKeyCount, pqCount: kyberPreKeyCount } =
await this.server.getMyKeyCounts(uuidKind);
await this.server.getMyKeyCounts(serviceIdKind);
let preKeys: Array<UploadPreKeyType> | undefined;
if (preKeyCount < PRE_KEY_MINIMUM) {
@ -489,7 +493,7 @@ export default class AccountManager extends EventTarget {
`${logId}: Server prekey count is ${preKeyCount}, generating a new set`
);
preKeys = await this.generateNewPreKeys(
uuidKind,
serviceIdKind,
PRE_KEY_GEN_BATCH_SIZE
);
}
@ -500,15 +504,15 @@ export default class AccountManager extends EventTarget {
`${logId}: Server kyber prekey count is ${kyberPreKeyCount}, generating a new set`
);
pqPreKeys = await this.generateNewKyberPreKeys(
uuidKind,
serviceIdKind,
PRE_KEY_GEN_BATCH_SIZE
);
}
const pqLastResortPreKey = await this.maybeUpdateLastResortKyberKey(
uuidKind
serviceIdKind
);
const signedPreKey = await this.maybeUpdateSignedPreKey(uuidKind);
const signedPreKey = await this.maybeUpdateSignedPreKey(serviceIdKind);
if (
!preKeys?.length &&
@ -536,8 +540,8 @@ export default class AccountManager extends EventTarget {
log.info(`${logId}: Uploading with ${keySummary.join(', ')}`);
const { storage } = window.textsecure;
const ourUuid = storage.user.getCheckedUuid(uuidKind);
const identityKey = this.getIdentityKeyOrThrow(ourUuid);
const ourServiceId = storage.user.getCheckedServiceId(serviceIdKind);
const identityKey = this.getIdentityKeyOrThrow(ourServiceId);
const toUpload = {
identityKey: identityKey.pubKey,
preKeys,
@ -547,37 +551,37 @@ export default class AccountManager extends EventTarget {
};
try {
await this.server.registerKeys(toUpload, uuidKind);
await this.server.registerKeys(toUpload, serviceIdKind);
} catch (error) {
log.error(`${logId} upload error:`, Errors.toLogFormat(error));
throw error;
}
await this._confirmKeys(toUpload, uuidKind);
await this._confirmKeys(toUpload, serviceIdKind);
const { count: updatedPreKeyCount, pqCount: updatedKyberPreKeyCount } =
await this.server.getMyKeyCounts(uuidKind);
await this.server.getMyKeyCounts(serviceIdKind);
log.info(
`${logId}: Successfully updated; ` +
`server prekey count: ${updatedPreKeyCount}, ` +
`server kyber prekey count: ${updatedKyberPreKeyCount}`
);
await this._cleanSignedPreKeys(uuidKind);
await this._cleanLastResortKeys(uuidKind);
await this._cleanPreKeys(uuidKind);
await this._cleanKyberPreKeys(uuidKind);
await this._cleanSignedPreKeys(serviceIdKind);
await this._cleanLastResortKeys(serviceIdKind);
await this._cleanPreKeys(serviceIdKind);
await this._cleanKyberPreKeys(serviceIdKind);
});
}
areKeysOutOfDate(uuidKind: UUIDKind): boolean {
areKeysOutOfDate(serviceIdKind: ServiceIdKind): boolean {
const signedPreKeyTime = window.storage.get(
SIGNED_PRE_KEY_UPDATE_TIME_KEY[uuidKind],
SIGNED_PRE_KEY_UPDATE_TIME_KEY[serviceIdKind],
0
);
const lastResortKeyTime = window.storage.get(
LAST_RESORT_KEY_UPDATE_TIME_KEY[uuidKind],
LAST_RESORT_KEY_UPDATE_TIME_KEY[serviceIdKind],
0
);
@ -592,20 +596,21 @@ export default class AccountManager extends EventTarget {
}
private async maybeUpdateSignedPreKey(
uuidKind: UUIDKind
serviceIdKind: ServiceIdKind
): Promise<UploadSignedPreKeyType | undefined> {
const logId = `AccountManager.maybeUpdateSignedPreKey(${uuidKind})`;
const logId = `AccountManager.maybeUpdateSignedPreKey(${serviceIdKind})`;
const store = window.textsecure.storage.protocol;
const ourUuid = window.textsecure.storage.user.getCheckedUuid(uuidKind);
const signedKeyId = getNextKeyId(uuidKind, SIGNED_PRE_KEY_ID_KEY);
const ourServiceId =
window.textsecure.storage.user.getCheckedServiceId(serviceIdKind);
const signedKeyId = getNextKeyId(serviceIdKind, SIGNED_PRE_KEY_ID_KEY);
if (typeof signedKeyId !== 'number') {
throw new Error(
`${logId}: Invalid ${SIGNED_PRE_KEY_ID_KEY[uuidKind]} in storage`
`${logId}: Invalid ${SIGNED_PRE_KEY_ID_KEY[serviceIdKind]} in storage`
);
}
const keys = await store.loadSignedPreKeys(ourUuid);
const keys = await store.loadSignedPreKeys(ourServiceId);
const sortedKeys = orderBy(keys, ['created_at'], ['desc']);
const confirmedKeys = sortedKeys.filter(key => key.confirmed);
const mostRecent = confirmedKeys[0];
@ -617,29 +622,29 @@ export default class AccountManager extends EventTarget {
`most recent was created ${lastUpdate}. No need to update.`
);
const existing = window.storage.get(
SIGNED_PRE_KEY_UPDATE_TIME_KEY[uuidKind]
SIGNED_PRE_KEY_UPDATE_TIME_KEY[serviceIdKind]
);
if (lastUpdate && !existing) {
log.warn(`${logId}: Updating last update time to ${lastUpdate}`);
await window.storage.put(
SIGNED_PRE_KEY_UPDATE_TIME_KEY[uuidKind],
SIGNED_PRE_KEY_UPDATE_TIME_KEY[serviceIdKind],
lastUpdate
);
}
return;
}
const identityKey = this.getIdentityKeyOrThrow(ourUuid);
const identityKey = this.getIdentityKeyOrThrow(ourServiceId);
const key = await generateSignedPreKey(identityKey, signedKeyId);
log.info(`${logId}: Saving new signed prekey`, key.keyId);
await Promise.all([
window.textsecure.storage.put(
SIGNED_PRE_KEY_ID_KEY[uuidKind],
SIGNED_PRE_KEY_ID_KEY[serviceIdKind],
signedKeyId + 1
),
store.storeSignedPreKey(ourUuid, key.keyId, key.keyPair),
store.storeSignedPreKey(ourServiceId, key.keyId, key.keyPair),
]);
return {
@ -650,20 +655,21 @@ export default class AccountManager extends EventTarget {
}
private async maybeUpdateLastResortKyberKey(
uuidKind: UUIDKind
serviceIdKind: ServiceIdKind
): Promise<UploadSignedPreKeyType | undefined> {
const logId = `maybeUpdateLastResortKyberKey(${uuidKind})`;
const logId = `maybeUpdateLastResortKyberKey(${serviceIdKind})`;
const store = window.textsecure.storage.protocol;
const ourUuid = window.textsecure.storage.user.getCheckedUuid(uuidKind);
const kyberKeyId = getNextKeyId(uuidKind, KYBER_KEY_ID_KEY);
const ourServiceId =
window.textsecure.storage.user.getCheckedServiceId(serviceIdKind);
const kyberKeyId = getNextKeyId(serviceIdKind, KYBER_KEY_ID_KEY);
if (typeof kyberKeyId !== 'number') {
throw new Error(
`${logId}: Invalid ${KYBER_KEY_ID_KEY[uuidKind]} in storage`
`${logId}: Invalid ${KYBER_KEY_ID_KEY[serviceIdKind]} in storage`
);
}
const keys = store.loadKyberPreKeys(ourUuid, { isLastResort: true });
const keys = store.loadKyberPreKeys(ourServiceId, { isLastResort: true });
const sortedKeys = orderBy(keys, ['createdAt'], ['desc']);
const confirmedKeys = sortedKeys.filter(key => key.isConfirmed);
const mostRecent = confirmedKeys[0];
@ -675,19 +681,19 @@ export default class AccountManager extends EventTarget {
`most recent was created ${lastUpdate}. No need to update.`
);
const existing = window.storage.get(
LAST_RESORT_KEY_UPDATE_TIME_KEY[uuidKind]
LAST_RESORT_KEY_UPDATE_TIME_KEY[serviceIdKind]
);
if (lastUpdate && !existing) {
log.warn(`${logId}: Updating last update time to ${lastUpdate}`);
await window.storage.put(
LAST_RESORT_KEY_UPDATE_TIME_KEY[uuidKind],
LAST_RESORT_KEY_UPDATE_TIME_KEY[serviceIdKind],
lastUpdate
);
}
return;
}
const identityKey = this.getIdentityKeyOrThrow(ourUuid);
const identityKey = this.getIdentityKeyOrThrow(ourServiceId);
const keyId = kyberKeyId;
const record = await generateKyberPreKey(identityKey, keyId);
@ -698,12 +704,15 @@ export default class AccountManager extends EventTarget {
isConfirmed: false,
isLastResort: true,
keyId,
ourUuid: ourUuid.toString(),
ourUuid: ourServiceId,
};
await Promise.all([
window.textsecure.storage.put(KYBER_KEY_ID_KEY[uuidKind], kyberKeyId + 1),
store.storeKyberPreKeys(ourUuid, [key]),
window.textsecure.storage.put(
KYBER_KEY_ID_KEY[serviceIdKind],
kyberKeyId + 1
),
store.storeKyberPreKeys(ourServiceId, [key]),
]);
return {
@ -714,12 +723,13 @@ export default class AccountManager extends EventTarget {
}
// Exposed only for tests
async _cleanSignedPreKeys(uuidKind: UUIDKind): Promise<void> {
const ourUuid = window.textsecure.storage.user.getCheckedUuid(uuidKind);
async _cleanSignedPreKeys(serviceIdKind: ServiceIdKind): Promise<void> {
const ourServiceId =
window.textsecure.storage.user.getCheckedServiceId(serviceIdKind);
const store = window.textsecure.storage.protocol;
const logId = `AccountManager.cleanSignedPreKeys(${uuidKind})`;
const logId = `AccountManager.cleanSignedPreKeys(${serviceIdKind})`;
const allKeys = store.loadSignedPreKeys(ourUuid);
const allKeys = store.loadSignedPreKeys(ourServiceId);
const sortedKeys = orderBy(allKeys, ['created_at'], ['desc']);
const confirmed = sortedKeys.filter(key => key.confirmed);
const unconfirmed = sortedKeys.filter(key => !key.confirmed);
@ -761,17 +771,20 @@ export default class AccountManager extends EventTarget {
});
if (toDelete.length > 0) {
log.info(`${logId}: Removing ${toDelete.length} signed prekeys`);
await store.removeSignedPreKeys(ourUuid, toDelete);
await store.removeSignedPreKeys(ourServiceId, toDelete);
}
}
// Exposed only for tests
async _cleanLastResortKeys(uuidKind: UUIDKind): Promise<void> {
const ourUuid = window.textsecure.storage.user.getCheckedUuid(uuidKind);
async _cleanLastResortKeys(serviceIdKind: ServiceIdKind): Promise<void> {
const ourServiceId =
window.textsecure.storage.user.getCheckedServiceId(serviceIdKind);
const store = window.textsecure.storage.protocol;
const logId = `AccountManager.cleanLastResortKeys(${uuidKind})`;
const logId = `AccountManager.cleanLastResortKeys(${serviceIdKind})`;
const allKeys = store.loadKyberPreKeys(ourUuid, { isLastResort: true });
const allKeys = store.loadKyberPreKeys(ourServiceId, {
isLastResort: true,
});
const sortedKeys = orderBy(allKeys, ['createdAt'], ['desc']);
const confirmed = sortedKeys.filter(key => key.isConfirmed);
const unconfirmed = sortedKeys.filter(key => !key.isConfirmed);
@ -815,16 +828,17 @@ export default class AccountManager extends EventTarget {
});
if (toDelete.length > 0) {
log.info(`${logId}: Removing ${toDelete.length} last resort keys`);
await store.removeKyberPreKeys(ourUuid, toDelete);
await store.removeKyberPreKeys(ourServiceId, toDelete);
}
}
async _cleanPreKeys(uuidKind: UUIDKind): Promise<void> {
const ourUuid = window.textsecure.storage.user.getCheckedUuid(uuidKind);
async _cleanPreKeys(serviceIdKind: ServiceIdKind): Promise<void> {
const ourServiceId =
window.textsecure.storage.user.getCheckedServiceId(serviceIdKind);
const store = window.textsecure.storage.protocol;
const logId = `AccountManager.cleanPreKeys(${uuidKind})`;
const logId = `AccountManager.cleanPreKeys(${serviceIdKind})`;
const preKeys = store.loadPreKeys(ourUuid);
const preKeys = store.loadPreKeys(ourServiceId);
const toDelete: Array<number> = [];
const sortedKeys = orderBy(preKeys, ['createdAt'], ['desc']);
@ -842,16 +856,19 @@ export default class AccountManager extends EventTarget {
log.info(`${logId}: ${sortedKeys.length} total prekeys`);
if (toDelete.length > 0) {
log.info(`${logId}: Removing ${toDelete.length} obsolete prekeys`);
await store.removePreKeys(ourUuid, toDelete);
await store.removePreKeys(ourServiceId, toDelete);
}
}
async _cleanKyberPreKeys(uuidKind: UUIDKind): Promise<void> {
const ourUuid = window.textsecure.storage.user.getCheckedUuid(uuidKind);
async _cleanKyberPreKeys(serviceIdKind: ServiceIdKind): Promise<void> {
const ourServiceId =
window.textsecure.storage.user.getCheckedServiceId(serviceIdKind);
const store = window.textsecure.storage.protocol;
const logId = `AccountManager.cleanKyberPreKeys(${uuidKind})`;
const logId = `AccountManager.cleanKyberPreKeys(${serviceIdKind})`;
const preKeys = store.loadKyberPreKeys(ourUuid, { isLastResort: false });
const preKeys = store.loadKyberPreKeys(ourServiceId, {
isLastResort: false,
});
const toDelete: Array<number> = [];
const sortedKeys = orderBy(preKeys, ['createdAt'], ['desc']);
@ -869,7 +886,7 @@ export default class AccountManager extends EventTarget {
log.info(`${logId}: ${sortedKeys.length} total prekeys`);
if (toDelete.length > 0) {
log.info(`${logId}: Removing ${toDelete.length} kyber keys`);
await store.removeKyberPreKeys(ourUuid, toDelete);
await store.removeKyberPreKeys(ourServiceId, toDelete);
}
}
@ -891,8 +908,8 @@ export default class AccountManager extends EventTarget {
const pniRegistrationId = generateRegistrationId();
const previousNumber = storage.user.getNumber();
const previousACI = storage.user.getUuid(UUIDKind.ACI)?.toString();
const previousPNI = storage.user.getUuid(UUIDKind.PNI)?.toString();
const previousACI = storage.user.getAci();
const previousPNI = storage.user.getPni();
let encryptedDeviceName;
if (deviceName) {
@ -916,10 +933,10 @@ export default class AccountManager extends EventTarget {
accessKey,
});
const ourUuid = UUID.cast(response.uuid);
const ourPni = UUID.cast(response.pni);
const ourAci = normalizeAci(response.uuid, 'createAccount');
const ourPni = normalizePni(response.pni, 'createAccount');
const uuidChanged = previousACI && ourUuid && previousACI !== ourUuid;
const uuidChanged = previousACI && ourAci && previousACI !== ourAci;
// We only consider the number changed if we didn't have a UUID before
const numberChanged =
@ -977,7 +994,7 @@ export default class AccountManager extends EventTarget {
// calls out to the user storage API to get the stored UUID and number
// information.
await storage.user.setCredentials({
uuid: ourUuid,
aci: ourAci,
pni: ourPni,
number,
deviceId: response.deviceId ?? 1,
@ -989,7 +1006,7 @@ export default class AccountManager extends EventTarget {
// database. Your identity, for example, in the saveIdentityWithAttributes call
// below.
const { conversation } = window.ConversationController.maybeMergeContacts({
aci: ourUuid,
aci: ourAci,
pni: ourPni,
e164: number,
reason: 'createAccount',
@ -1009,12 +1026,12 @@ export default class AccountManager extends EventTarget {
// update our own identity key, which may have changed
// if we're relinking after a reinstall on the master device
await Promise.all([
storage.protocol.saveIdentityWithAttributes(new UUID(ourUuid), {
storage.protocol.saveIdentityWithAttributes(ourAci, {
...identityAttrs,
publicKey: aciKeyPair.pubKey,
}),
pniKeyPair
? storage.protocol.saveIdentityWithAttributes(new UUID(ourPni), {
? storage.protocol.saveIdentityWithAttributes(ourPni, {
...identityAttrs,
publicKey: pniKeyPair.pubKey,
})
@ -1023,7 +1040,7 @@ export default class AccountManager extends EventTarget {
const identityKeyMap = {
...(storage.get('identityKeyMap') || {}),
[ourUuid]: aciKeyPair,
[ourAci]: aciKeyPair,
...(pniKeyPair
? {
[ourPni]: pniKeyPair,
@ -1032,7 +1049,7 @@ export default class AccountManager extends EventTarget {
};
const registrationIdMap = {
...(storage.get('registrationIdMap') || {}),
[ourUuid]: registrationId,
[ourAci]: registrationId,
[ourPni]: pniRegistrationId,
};
@ -1055,20 +1072,20 @@ export default class AccountManager extends EventTarget {
// Exposed only for testing
public async _confirmKeys(
keys: UploadKeysType,
uuidKind: UUIDKind
serviceIdKind: ServiceIdKind
): Promise<void> {
const logId = `AccountManager.confirmKeys(${uuidKind})`;
const logId = `AccountManager.confirmKeys(${serviceIdKind})`;
const { storage } = window.textsecure;
const store = storage.protocol;
const ourUuid = storage.user.getCheckedUuid(uuidKind);
const ourServiceId = storage.user.getCheckedServiceId(serviceIdKind);
const updatedAt = Date.now();
const { signedPreKey, pqLastResortPreKey } = keys;
if (signedPreKey) {
log.info(`${logId}: confirming signed prekey key`, signedPreKey.keyId);
await store.confirmSignedPreKey(ourUuid, signedPreKey.keyId);
await store.confirmSignedPreKey(ourServiceId, signedPreKey.keyId);
await window.storage.put(
SIGNED_PRE_KEY_UPDATE_TIME_KEY[uuidKind],
SIGNED_PRE_KEY_UPDATE_TIME_KEY[serviceIdKind],
updatedAt
);
} else {
@ -1080,9 +1097,9 @@ export default class AccountManager extends EventTarget {
`${logId}: confirming last resort key`,
pqLastResortPreKey.keyId
);
await store.confirmKyberPreKey(ourUuid, pqLastResortPreKey.keyId);
await store.confirmKyberPreKey(ourServiceId, pqLastResortPreKey.keyId);
await window.storage.put(
LAST_RESORT_KEY_UPDATE_TIME_KEY[uuidKind],
LAST_RESORT_KEY_UPDATE_TIME_KEY[serviceIdKind],
updatedAt
);
} else {
@ -1093,23 +1110,23 @@ export default class AccountManager extends EventTarget {
// Very similar to maybeUpdateKeys, but will always generate prekeys and doesn't upload
async _generateKeys(
count: number,
uuidKind: UUIDKind,
serviceIdKind: ServiceIdKind,
maybeIdentityKey?: KeyPairType
): Promise<UploadKeysType> {
const logId = `AcountManager.generateKeys(${uuidKind})`;
const logId = `AcountManager.generateKeys(${serviceIdKind})`;
const { storage } = window.textsecure;
const store = storage.protocol;
const ourUuid = storage.user.getCheckedUuid(uuidKind);
const ourUuid = storage.user.getCheckedServiceId(serviceIdKind);
const identityKey = maybeIdentityKey ?? store.getIdentityKeyPair(ourUuid);
strictAssert(identityKey, 'generateKeys: No identity key pair!');
const preKeys = await this.generateNewPreKeys(uuidKind, count);
const pqPreKeys = await this.generateNewKyberPreKeys(uuidKind, count);
const preKeys = await this.generateNewPreKeys(serviceIdKind, count);
const pqPreKeys = await this.generateNewKyberPreKeys(serviceIdKind, count);
const pqLastResortPreKey = await this.maybeUpdateLastResortKyberKey(
uuidKind
serviceIdKind
);
const signedPreKey = await this.maybeUpdateSignedPreKey(uuidKind);
const signedPreKey = await this.maybeUpdateSignedPreKey(serviceIdKind);
log.info(
`${logId}: Generated ` +
@ -1120,10 +1137,10 @@ export default class AccountManager extends EventTarget {
);
// These are primarily for the summaries they log out
await this._cleanPreKeys(uuidKind);
await this._cleanKyberPreKeys(uuidKind);
await this._cleanLastResortKeys(uuidKind);
await this._cleanSignedPreKeys(uuidKind);
await this._cleanPreKeys(serviceIdKind);
await this._cleanKyberPreKeys(serviceIdKind);
await this._cleanLastResortKeys(serviceIdKind);
await this._cleanSignedPreKeys(serviceIdKind);
return {
identityKey: identityKey.pubKey,
@ -1139,11 +1156,14 @@ export default class AccountManager extends EventTarget {
this.dispatchEvent(new Event('registration'));
}
async setPni(pni: string, keyMaterial?: PniKeyMaterialType): Promise<void> {
async setPni(
pni: PniString,
keyMaterial?: PniKeyMaterialType
): Promise<void> {
const logId = `AccountManager.setPni(${pni})`;
const { storage } = window.textsecure;
const oldPni = storage.user.getUuid(UUIDKind.PNI)?.toString();
const oldPni = storage.user.getPni();
if (oldPni === pni && !keyMaterial) {
return;
}
@ -1151,22 +1171,19 @@ export default class AccountManager extends EventTarget {
log.info(`${logId}: updating from ${oldPni}`);
if (oldPni) {
await storage.protocol.removeOurOldPni(new UUID(oldPni));
await storage.protocol.removeOurOldPni(oldPni);
}
await storage.user.setPni(pni);
if (keyMaterial) {
await storage.protocol.updateOurPniKeyMaterial(
new UUID(pni),
keyMaterial
);
await storage.protocol.updateOurPniKeyMaterial(pni, keyMaterial);
// Intentionally not awaiting since this is processed on encrypted queue
// of MessageReceiver.
void this.queueTask(async () => {
try {
await this.maybeUpdateKeys(UUIDKind.PNI);
await this.maybeUpdateKeys(ServiceIdKind.PNI);
} catch (error) {
log.error(
`${logId}: Failed to upload PNI prekeys. Moving on`,

View file

@ -6,7 +6,7 @@
import protobuf from '../protobuf/wrap';
import { SignalService as Proto } from '../protobuf';
import { normalizeUuid } from '../util/normalizeUuid';
import { normalizeAci } from '../types/ServiceId';
import { DurationInSeconds } from '../util/durations';
import * as Errors from '../types/errors';
import * as log from '../logging/log';
@ -120,7 +120,7 @@ export class ContactBuffer extends ParserBase<
return undefined;
}
if (!proto.uuid) {
if (!proto.aci) {
return proto;
}
@ -130,18 +130,18 @@ export class ContactBuffer extends ParserBase<
...proto,
verified:
verified && verified.destinationUuid
verified && verified.destinationAci
? {
...verified,
destinationUuid: normalizeUuid(
verified.destinationUuid,
'ContactBuffer.verified.destinationUuid'
destinationAci: normalizeAci(
verified.destinationAci,
'ContactBuffer.verified.destinationAci'
),
}
: verified,
uuid: normalizeUuid(proto.uuid, 'ContactBuffer.uuid'),
aci: normalizeAci(proto.aci, 'ContactBuffer.aci'),
};
}
}

View file

@ -6,6 +6,7 @@
import type { LibSignalErrorBase } from '@signalapp/libsignal-client';
import { parseRetryAfter } from '../util/parseRetryAfter';
import type { ServiceIdString } from '../types/ServiceId';
import type { CallbackResultType } from './Types.d';
import type { HeaderListType } from './WebAPI';
@ -191,13 +192,13 @@ export class SendMessageChallengeError extends ReplayableError {
}
export class SendMessageProtoError extends Error implements CallbackResultType {
public readonly successfulIdentifiers?: Array<string>;
public readonly successfulServiceIds?: Array<ServiceIdString>;
public readonly failoverIdentifiers?: Array<string>;
public readonly failoverServiceIds?: Array<ServiceIdString>;
public readonly errors?: CallbackResultType['errors'];
public readonly unidentifiedDeliveries?: Array<string>;
public readonly unidentifiedDeliveries?: Array<ServiceIdString>;
public readonly dataMessage: Uint8Array | undefined;
@ -210,13 +211,13 @@ export class SendMessageProtoError extends Error implements CallbackResultType {
public readonly timestamp?: number;
public readonly recipients?: Record<string, Array<number>>;
public readonly recipients?: Record<ServiceIdString, Array<number>>;
public readonly sendIsNotFinal?: boolean;
constructor({
successfulIdentifiers,
failoverIdentifiers,
successfulServiceIds,
failoverServiceIds,
errors,
unidentifiedDeliveries,
dataMessage,
@ -229,8 +230,8 @@ export class SendMessageProtoError extends Error implements CallbackResultType {
}: CallbackResultType) {
super(`SendMessageProtoError: ${SendMessageProtoError.getMessage(errors)}`);
this.successfulIdentifiers = successfulIdentifiers;
this.failoverIdentifiers = failoverIdentifiers;
this.successfulServiceIds = successfulServiceIds;
this.failoverServiceIds = failoverServiceIds;
this.errors = errors;
this.unidentifiedDeliveries = unidentifiedDeliveries;
this.dataMessage = dataMessage;
@ -271,11 +272,11 @@ export class MessageError extends ReplayableError {
}
export class UnregisteredUserError extends Error {
readonly identifier: string;
readonly serviceId: string;
readonly httpError: HTTPError;
constructor(identifier: string, httpError: HTTPError) {
constructor(serviceId: ServiceIdString, httpError: HTTPError) {
const { message } = httpError;
super(message);
@ -289,7 +290,7 @@ export class UnregisteredUserError extends Error {
Error.captureStackTrace(this);
}
this.identifier = identifier;
this.serviceId = serviceId;
this.httpError = httpError;
appendStack(this, httpError);

View file

@ -1,16 +1,16 @@
// Copyright 2017 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { UUID } from '../types/UUID';
import type { ServiceIdString } from '../types/ServiceId';
import type { SignalProtocolStore } from '../SignalProtocolStore';
export function init(signalProtocolStore: SignalProtocolStore): void {
signalProtocolStore.on(
'keychange',
async (uuid: UUID, reason: string): Promise<void> => {
async (serviceId: ServiceIdString, reason: string): Promise<void> => {
const conversation =
await window.ConversationController.getOrCreateAndWait(
uuid.toString(),
serviceId,
'private'
);
void conversation.addKeyChange(reason);

File diff suppressed because it is too large Load diff

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

View file

@ -12,15 +12,16 @@ import {
} from '../Crypto';
import { calculateAgreement, createKeyPair, generateKeyPair } from '../Curve';
import { SignalService as Proto } from '../protobuf';
import type { PniString, AciString } from '../types/ServiceId';
import { normalizeAci, normalizePni } from '../types/ServiceId';
import { strictAssert } from '../util/assert';
import { normalizeUuid } from '../util/normalizeUuid';
type ProvisionDecryptResult = {
aciKeyPair: KeyPairType;
pniKeyPair?: KeyPairType;
number?: string;
aci?: string;
pni?: string;
aci?: AciString;
pni?: PniString;
provisioningCode?: string;
userAgent?: string;
readReceipts?: boolean;
@ -78,8 +79,8 @@ class ProvisioningCipherInner {
aciKeyPair,
pniKeyPair,
number: provisionMessage.number,
aci: normalizeUuid(aci, 'ProvisionMessage.aci'),
pni: pni ? normalizeUuid(pni, 'ProvisionMessage.pni') : undefined,
aci: normalizeAci(aci, 'ProvisionMessage.aci'),
pni: pni ? normalizePni(pni, 'ProvisionMessage.pni') : undefined,
provisioningCode: provisionMessage.provisioningCode,
userAgent: provisionMessage.userAgent,
readReceipts: provisionMessage.readReceipts,

View file

@ -17,7 +17,6 @@ import type { ConversationModel } from '../models/conversations';
import { GLOBAL_ZONE } from '../SignalProtocolStore';
import { assertDev, strictAssert } from '../util/assert';
import { parseIntOrThrow } from '../util/parseIntOrThrow';
import { getTaggedConversationUuid } from '../util/getConversationUuid';
import { Address } from '../types/Address';
import { QualifiedAddress } from '../types/QualifiedAddress';
import { SenderKeys } from '../LibSignalStores';
@ -25,7 +24,8 @@ import type {
TextAttachmentType,
UploadedAttachmentType,
} from '../types/Attachment';
import { type UUID, type TaggedUUIDStringType, UUIDKind } from '../types/UUID';
import type { AciString, ServiceIdString } from '../types/ServiceId';
import { ServiceIdKind } from '../types/ServiceId';
import type {
ChallengeType,
GetGroupLogOptionsType,
@ -81,7 +81,7 @@ import { missingCaseError } from '../util/missingCaseError';
import { drop } from '../util/drop';
export type SendMetadataType = {
[identifier: string]: {
[serviceId: ServiceIdString]: {
accessKey: string;
senderCertificate?: SerializedCertificateType;
};
@ -101,7 +101,7 @@ export type OutgoingQuoteAttachmentType = Readonly<{
export type OutgoingQuoteType = Readonly<{
isGiftBadge?: boolean;
id?: number;
authorUuid?: string;
authorAci?: AciString;
text?: string;
attachments: ReadonlyArray<OutgoingQuoteAttachmentType>;
bodyRanges?: ReadonlyArray<RawBodyRange>;
@ -125,7 +125,7 @@ export type GroupV2InfoType = {
groupChange?: Uint8Array;
masterKey: Uint8Array;
revision: number;
members: ReadonlyArray<string>;
members: ReadonlyArray<ServiceIdString>;
};
type GroupCallUpdateType = {
@ -143,7 +143,7 @@ export type OutgoingStickerType = Readonly<{
export type ReactionType = {
emoji?: string;
remove?: boolean;
targetAuthorUuid?: string;
targetAuthorAci?: AciString;
targetTimestamp?: number;
};
@ -176,7 +176,7 @@ export type MessageOptionsType = {
preview?: ReadonlyArray<OutgoingLinkPreviewType>;
profileKey?: Uint8Array;
quote?: OutgoingQuoteType;
recipients: ReadonlyArray<string>;
recipients: ReadonlyArray<ServiceIdString>;
sticker?: OutgoingStickerType;
reaction?: ReactionType;
deletedForEveryoneTimestamp?: number;
@ -232,7 +232,7 @@ class Message {
quote?: OutgoingQuoteType;
recipients: ReadonlyArray<string>;
recipients: ReadonlyArray<ServiceIdString>;
sticker?: OutgoingStickerType;
@ -377,7 +377,7 @@ class Message {
proto.reaction = new Proto.DataMessage.Reaction();
proto.reaction.emoji = this.reaction.emoji || null;
proto.reaction.remove = this.reaction.remove || false;
proto.reaction.targetAuthorUuid = this.reaction.targetAuthorUuid || null;
proto.reaction.targetAuthorAci = this.reaction.targetAuthorAci || null;
proto.reaction.targetTimestamp =
this.reaction.targetTimestamp === undefined
? null
@ -481,7 +481,7 @@ class Message {
quote.id =
this.quote.id === undefined ? null : Long.fromNumber(this.quote.id);
quote.authorUuid = this.quote.authorUuid || null;
quote.authorAci = this.quote.authorAci || null;
quote.text = this.quote.text || null;
quote.attachments = this.quote.attachments.slice() || [];
const bodyRanges = this.quote.bodyRanges || [];
@ -490,7 +490,7 @@ class Message {
bodyRange.start = range.start;
bodyRange.length = range.length;
if (BodyRange.isMention(range)) {
bodyRange.mentionUuid = range.mentionUuid;
bodyRange.mentionAci = range.mentionUuid;
} else if (BodyRange.isFormatting(range)) {
bodyRange.style = range.style;
} else {
@ -529,7 +529,7 @@ class Message {
return {
start,
length,
mentionUuid: bodyRange.mentionUuid,
mentionAci: bodyRange.mentionUuid,
};
}
if (BodyRange.isFormatting(bodyRange)) {
@ -556,8 +556,8 @@ class Message {
const { StoryContext } = Proto.DataMessage;
const storyContext = new StoryContext();
if (this.storyContext.authorUuid) {
storyContext.authorUuid = this.storyContext.authorUuid;
if (this.storyContext.authorAci) {
storyContext.authorAci = this.storyContext.authorAci;
}
storyContext.sentTimestamp = Long.fromNumber(this.storyContext.timestamp);
@ -610,12 +610,12 @@ export default class MessageSender {
this.pendingMessages = {};
}
async queueJobForIdentifier<T>(
identifier: string,
async queueJobForServiceId<T>(
serviceId: ServiceIdString,
runJob: () => Promise<T>
): Promise<T> {
const { id } = await window.ConversationController.getOrCreateAndWait(
identifier,
serviceId,
'private'
);
this.pendingMessages[id] =
@ -625,7 +625,7 @@ export default class MessageSender {
const taskWithTimeout = createTaskWithTimeout(
runJob,
`queueJobForIdentifier ${identifier} ${id}`
`queueJobForServiceId ${serviceId} ${id}`
);
return queue.add(taskWithTimeout);
@ -808,9 +808,9 @@ export default class MessageSender {
getTypingContentMessage(
options: Readonly<{
recipientId?: string;
recipientId?: ServiceIdString;
groupId?: Uint8Array;
groupMembers: ReadonlyArray<string>;
groupMembers: ReadonlyArray<ServiceIdString>;
isTyping: boolean;
timestamp?: number;
}>
@ -877,19 +877,10 @@ export default class MessageSender {
);
}
const myE164 = window.textsecure.storage.user.getNumber();
const myUuid = window.textsecure.storage.user.getUuid()?.toString();
const myAci = window.textsecure.storage.user.getCheckedAci();
const groupMembers = groupV2?.members || [];
// We should always have a UUID but have this check just in case we don't.
let isNotMe: (recipient: string) => boolean;
if (myUuid) {
isNotMe = r => r !== myE164 && r !== myUuid.toString();
} else {
isNotMe = r => r !== myE164;
}
const blockedIdentifiers = new Set(
concat(
window.storage.blocked.getBlockedUuids(),
@ -898,7 +889,7 @@ export default class MessageSender {
);
const recipients = groupMembers.filter(
recipient => isNotMe(recipient) && !blockedIdentifiers.has(recipient)
recipient => recipient !== myAci && !blockedIdentifiers.has(recipient)
);
return {
@ -997,7 +988,7 @@ export default class MessageSender {
groupId: string | undefined;
options?: SendOptionsType;
proto: Proto.Content | Proto.DataMessage | PlaintextContent;
recipients: ReadonlyArray<string>;
recipients: ReadonlyArray<ServiceIdString>;
sendLogCallback?: SendLogCallbackType;
story?: boolean;
timestamp: number;
@ -1005,12 +996,12 @@ export default class MessageSender {
}>): Promise<void> {
const accountManager = window.getAccountManager();
try {
if (accountManager.areKeysOutOfDate(UUIDKind.ACI)) {
if (accountManager.areKeysOutOfDate(ServiceIdKind.ACI)) {
log.warn(
`sendMessageProto/${timestamp}: Keys are out of date; updating before send`
);
await accountManager.maybeUpdateKeys(UUIDKind.ACI);
if (accountManager.areKeysOutOfDate(UUIDKind.ACI)) {
await accountManager.maybeUpdateKeys(ServiceIdKind.ACI);
if (accountManager.areKeysOutOfDate(ServiceIdKind.ACI)) {
throw new Error('Keys still out of date after update');
}
}
@ -1028,7 +1019,7 @@ export default class MessageSender {
callback,
contentHint,
groupId,
identifiers: recipients,
serviceIds: recipients,
message: proto,
options,
sendLogCallback,
@ -1038,10 +1029,10 @@ export default class MessageSender {
urgent,
});
recipients.forEach(identifier => {
recipients.forEach(serviceId => {
drop(
this.queueJobForIdentifier(identifier, async () =>
outgoing.sendToIdentifier(identifier)
this.queueJobForServiceId(serviceId, async () =>
outgoing.sendToServiceId(serviceId)
)
);
});
@ -1058,7 +1049,7 @@ export default class MessageSender {
story,
}: Readonly<{
timestamp: number;
recipients: Array<string>;
recipients: Array<ServiceIdString>;
proto: Proto.Content | Proto.DataMessage | PlaintextContent;
contentHint: number;
groupId: string | undefined;
@ -1094,7 +1085,7 @@ export default class MessageSender {
async sendIndividualProto({
contentHint,
groupId,
identifier,
serviceId,
options,
proto,
timestamp,
@ -1102,13 +1093,13 @@ export default class MessageSender {
}: Readonly<{
contentHint: number;
groupId?: string;
identifier: string | undefined;
serviceId: ServiceIdString | undefined;
options?: SendOptionsType;
proto: Proto.DataMessage | Proto.Content | PlaintextContent;
timestamp: number;
urgent: boolean;
}>): Promise<CallbackResultType> {
assertDev(identifier, "Identifier can't be undefined");
assertDev(serviceId, "ServiceId can't be undefined");
return new Promise((resolve, reject) => {
const callback = (res: CallbackResultType) => {
if (res && res.errors && res.errors.length > 0) {
@ -1124,7 +1115,7 @@ export default class MessageSender {
groupId,
options,
proto,
recipients: [identifier],
recipients: [serviceId],
timestamp,
urgent,
})
@ -1134,7 +1125,7 @@ export default class MessageSender {
// You might wonder why this takes a groupId. models/messages.resend() can send a group
// message to just one person.
async sendMessageToIdentifier({
async sendMessageToServiceId({
attachments,
bodyRanges,
contact,
@ -1143,7 +1134,7 @@ export default class MessageSender {
editedMessageTimestamp,
expireTimer,
groupId,
identifier,
serviceId,
messageText,
options,
preview,
@ -1165,7 +1156,7 @@ export default class MessageSender {
editedMessageTimestamp?: number;
expireTimer: DurationInSeconds | undefined;
groupId: string | undefined;
identifier: string;
serviceId: ServiceIdString;
messageText: string | undefined;
options?: SendOptionsType;
preview?: ReadonlyArray<OutgoingLinkPreviewType> | undefined;
@ -1192,7 +1183,7 @@ export default class MessageSender {
profileKey,
quote,
reaction,
recipients: [identifier],
recipients: [serviceId],
sticker,
storyContext,
timestamp,
@ -1215,7 +1206,7 @@ export default class MessageSender {
encodedEditMessage,
timestamp,
destination,
destinationUuid,
destinationServiceId,
expirationStartTimestamp,
conversationIdsSentTo = [],
conversationIdsWithSealedSender = new Set(),
@ -1229,7 +1220,7 @@ export default class MessageSender {
encodedEditMessage?: Uint8Array;
timestamp: number;
destination: string | undefined;
destinationUuid: TaggedUUIDStringType | undefined;
destinationServiceId: ServiceIdString | undefined;
expirationStartTimestamp: number | null;
conversationIdsSentTo?: Iterable<string>;
conversationIdsWithSealedSender?: Set<string>;
@ -1239,7 +1230,7 @@ export default class MessageSender {
storyMessage?: Proto.StoryMessage;
storyMessageRecipients?: ReadonlyArray<Proto.SyncMessage.Sent.IStoryMessageRecipient>;
}>): Promise<CallbackResultType> {
const myUuid = window.textsecure.storage.user.getCheckedUuid();
const myAci = window.textsecure.storage.user.getCheckedAci();
const sentMessage = new Proto.SyncMessage.Sent();
sentMessage.timestamp = Long.fromNumber(timestamp);
@ -1254,10 +1245,8 @@ export default class MessageSender {
if (destination) {
sentMessage.destination = destination;
}
if (destinationUuid?.aci) {
sentMessage.destinationAci = destinationUuid.aci;
} else if (destinationUuid?.pni) {
sentMessage.destinationPni = destinationUuid.pni;
if (destinationServiceId) {
sentMessage.destinationServiceId = destinationServiceId;
}
if (expirationStartTimestamp) {
sentMessage.expirationStartTimestamp = Long.fromNumber(
@ -1288,11 +1277,9 @@ export default class MessageSender {
if (e164) {
status.destination = e164;
}
const taggedUuid = getTaggedConversationUuid(conv.attributes);
if (taggedUuid?.aci) {
status.destinationAci = taggedUuid.aci;
} else if (taggedUuid?.pni) {
status.destinationPni = taggedUuid.pni;
const serviceId = conv.getServiceId();
if (serviceId) {
status.destinationServiceId = serviceId;
}
}
status.unidentified =
@ -1310,7 +1297,7 @@ export default class MessageSender {
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
return this.sendIndividualProto({
identifier: myUuid.toString(),
serviceId: myAci,
proto: contentMessage,
timestamp,
contentHint: ContentHint.RESENDABLE,
@ -1320,7 +1307,7 @@ export default class MessageSender {
}
static getRequestBlockSyncMessage(): SingleProtoJobData {
const myUuid = window.textsecure.storage.user.getCheckedUuid();
const myAci = window.textsecure.storage.user.getCheckedAci();
const request = new Proto.SyncMessage.Request();
request.type = Proto.SyncMessage.Request.Type.BLOCKED;
@ -1333,7 +1320,7 @@ export default class MessageSender {
return {
contentHint: ContentHint.RESENDABLE,
identifier: myUuid.toString(),
identifier: myAci,
isSyncMessage: true,
protoBase64: Bytes.toBase64(
Proto.Content.encode(contentMessage).finish()
@ -1344,7 +1331,7 @@ export default class MessageSender {
}
static getRequestConfigurationSyncMessage(): SingleProtoJobData {
const myUuid = window.textsecure.storage.user.getCheckedUuid();
const myAci = window.textsecure.storage.user.getCheckedAci();
const request = new Proto.SyncMessage.Request();
request.type = Proto.SyncMessage.Request.Type.CONFIGURATION;
@ -1357,7 +1344,7 @@ export default class MessageSender {
return {
contentHint: ContentHint.RESENDABLE,
identifier: myUuid.toString(),
identifier: myAci,
isSyncMessage: true,
protoBase64: Bytes.toBase64(
Proto.Content.encode(contentMessage).finish()
@ -1368,7 +1355,7 @@ export default class MessageSender {
}
static getRequestContactSyncMessage(): SingleProtoJobData {
const myUuid = window.textsecure.storage.user.getCheckedUuid();
const myAci = window.textsecure.storage.user.getCheckedAci();
const request = new Proto.SyncMessage.Request();
request.type = Proto.SyncMessage.Request.Type.CONTACTS;
@ -1381,7 +1368,7 @@ export default class MessageSender {
return {
contentHint: ContentHint.RESENDABLE,
identifier: myUuid.toString(),
identifier: myAci,
isSyncMessage: true,
protoBase64: Bytes.toBase64(
Proto.Content.encode(contentMessage).finish()
@ -1392,7 +1379,7 @@ export default class MessageSender {
}
static getFetchManifestSyncMessage(): SingleProtoJobData {
const myUuid = window.textsecure.storage.user.getCheckedUuid();
const myAci = window.textsecure.storage.user.getCheckedAci();
const fetchLatest = new Proto.SyncMessage.FetchLatest();
fetchLatest.type = Proto.SyncMessage.FetchLatest.Type.STORAGE_MANIFEST;
@ -1406,7 +1393,7 @@ export default class MessageSender {
return {
contentHint: ContentHint.RESENDABLE,
identifier: myUuid.toString(),
identifier: myAci,
isSyncMessage: true,
protoBase64: Bytes.toBase64(
Proto.Content.encode(contentMessage).finish()
@ -1417,7 +1404,7 @@ export default class MessageSender {
}
static getFetchLocalProfileSyncMessage(): SingleProtoJobData {
const myUuid = window.textsecure.storage.user.getCheckedUuid();
const myAci = window.textsecure.storage.user.getCheckedAci();
const fetchLatest = new Proto.SyncMessage.FetchLatest();
fetchLatest.type = Proto.SyncMessage.FetchLatest.Type.LOCAL_PROFILE;
@ -1431,7 +1418,7 @@ export default class MessageSender {
return {
contentHint: ContentHint.RESENDABLE,
identifier: myUuid.toString(),
identifier: myAci,
isSyncMessage: true,
protoBase64: Bytes.toBase64(
Proto.Content.encode(contentMessage).finish()
@ -1442,7 +1429,7 @@ export default class MessageSender {
}
static getRequestKeySyncMessage(): SingleProtoJobData {
const myUuid = window.textsecure.storage.user.getCheckedUuid();
const myAci = window.textsecure.storage.user.getCheckedAci();
const request = new Proto.SyncMessage.Request();
request.type = Proto.SyncMessage.Request.Type.KEYS;
@ -1456,7 +1443,7 @@ export default class MessageSender {
return {
contentHint: ContentHint.RESENDABLE,
identifier: myUuid.toString(),
identifier: myAci,
isSyncMessage: true,
protoBase64: Bytes.toBase64(
Proto.Content.encode(contentMessage).finish()
@ -1474,7 +1461,7 @@ export default class MessageSender {
}>,
options?: Readonly<SendOptionsType>
): Promise<CallbackResultType> {
const myUuid = window.textsecure.storage.user.getCheckedUuid();
const myAci = window.textsecure.storage.user.getCheckedAci();
const syncMessage = MessageSender.createSyncMessage();
syncMessage.read = [];
@ -1492,7 +1479,7 @@ export default class MessageSender {
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
return this.sendIndividualProto({
identifier: myUuid.toString(),
serviceId: myAci,
proto: contentMessage,
timestamp: Date.now(),
contentHint: ContentHint.RESENDABLE,
@ -1503,13 +1490,13 @@ export default class MessageSender {
async syncView(
views: ReadonlyArray<{
senderUuid?: string;
senderAci?: AciString;
senderE164?: string;
timestamp: number;
}>,
options?: SendOptionsType
): Promise<CallbackResultType> {
const myUuid = window.textsecure.storage.user.getCheckedUuid();
const myAci = window.textsecure.storage.user.getCheckedAci();
const syncMessage = MessageSender.createSyncMessage();
syncMessage.viewed = views.map(
@ -1525,7 +1512,7 @@ export default class MessageSender {
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
return this.sendIndividualProto({
identifier: myUuid.toString(),
serviceId: myAci,
proto: contentMessage,
timestamp: Date.now(),
contentHint: ContentHint.RESENDABLE,
@ -1536,7 +1523,7 @@ export default class MessageSender {
async syncViewOnceOpen(
viewOnceOpens: ReadonlyArray<{
senderUuid?: string;
senderAci?: AciString;
senderE164?: string;
timestamp: number;
}>,
@ -1547,13 +1534,13 @@ export default class MessageSender {
`syncViewOnceOpen: ${viewOnceOpens.length} opens provided. Can only handle one.`
);
}
const { senderE164, senderUuid, timestamp } = viewOnceOpens[0];
const { senderE164, senderAci, timestamp } = viewOnceOpens[0];
if (!senderUuid) {
throw new Error('syncViewOnceOpen: Missing senderUuid');
if (!senderAci) {
throw new Error('syncViewOnceOpen: Missing senderAci');
}
const myUuid = window.textsecure.storage.user.getCheckedUuid();
const myAci = window.textsecure.storage.user.getCheckedAci();
const syncMessage = MessageSender.createSyncMessage();
@ -1561,7 +1548,7 @@ export default class MessageSender {
if (senderE164 !== undefined) {
viewOnceOpen.sender = senderE164;
}
viewOnceOpen.senderUuid = senderUuid;
viewOnceOpen.senderAci = senderAci;
viewOnceOpen.timestamp = Long.fromNumber(timestamp);
syncMessage.viewOnceOpen = viewOnceOpen;
@ -1571,7 +1558,7 @@ export default class MessageSender {
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
return this.sendIndividualProto({
identifier: myUuid.toString(),
serviceId: myAci,
proto: contentMessage,
timestamp: Date.now(),
contentHint: ContentHint.RESENDABLE,
@ -1583,12 +1570,12 @@ export default class MessageSender {
static getMessageRequestResponseSync(
options: Readonly<{
threadE164?: string;
threadUuid?: string;
threadAci?: AciString;
groupId?: Uint8Array;
type: number;
}>
): SingleProtoJobData {
const myUuid = window.textsecure.storage.user.getCheckedUuid();
const myAci = window.textsecure.storage.user.getCheckedAci();
const syncMessage = MessageSender.createSyncMessage();
@ -1596,8 +1583,8 @@ export default class MessageSender {
if (options.threadE164 !== undefined) {
response.threadE164 = options.threadE164;
}
if (options.threadUuid !== undefined) {
response.threadUuid = options.threadUuid;
if (options.threadAci !== undefined) {
response.threadAci = options.threadAci;
}
if (options.groupId) {
response.groupId = options.groupId;
@ -1612,7 +1599,7 @@ export default class MessageSender {
return {
contentHint: ContentHint.RESENDABLE,
identifier: myUuid.toString(),
identifier: myAci,
isSyncMessage: true,
protoBase64: Bytes.toBase64(
Proto.Content.encode(contentMessage).finish()
@ -1629,7 +1616,7 @@ export default class MessageSender {
installed: boolean;
}>
): SingleProtoJobData {
const myUuid = window.textsecure.storage.user.getCheckedUuid();
const myAci = window.textsecure.storage.user.getCheckedAci();
const ENUM = Proto.SyncMessage.StickerPackOperation.Type;
const packOperations = operations.map(item => {
@ -1653,7 +1640,7 @@ export default class MessageSender {
return {
contentHint: ContentHint.RESENDABLE,
identifier: myUuid.toString(),
identifier: myAci,
isSyncMessage: true,
protoBase64: Bytes.toBase64(
Proto.Content.encode(contentMessage).finish()
@ -1665,13 +1652,13 @@ export default class MessageSender {
static getVerificationSync(
destinationE164: string | undefined,
destinationUuid: string | undefined,
destinationAci: AciString | undefined,
state: number,
identityKey: Readonly<Uint8Array>
): SingleProtoJobData {
const myUuid = window.textsecure.storage.user.getCheckedUuid();
const myAci = window.textsecure.storage.user.getCheckedAci();
if (!destinationE164 && !destinationUuid) {
if (!destinationE164 && !destinationAci) {
throw new Error('syncVerification: Neither e164 nor UUID were provided');
}
@ -1682,8 +1669,8 @@ export default class MessageSender {
if (destinationE164) {
verified.destination = destinationE164;
}
if (destinationUuid) {
verified.destinationUuid = destinationUuid;
if (destinationAci) {
verified.destinationAci = destinationAci;
}
verified.identityKey = identityKey;
verified.nullMessage = padding;
@ -1698,7 +1685,7 @@ export default class MessageSender {
return {
contentHint: ContentHint.RESENDABLE,
identifier: myUuid.toString(),
identifier: myAci,
isSyncMessage: true,
protoBase64: Bytes.toBase64(
Proto.Content.encode(contentMessage).finish()
@ -1711,17 +1698,17 @@ export default class MessageSender {
// Sending messages to contacts
async sendCallingMessage(
recipientId: string,
serviceId: ServiceIdString,
callingMessage: Readonly<Proto.ICallingMessage>,
options?: Readonly<SendOptionsType>
): Promise<CallbackResultType> {
const recipients = [recipientId];
const recipients = [serviceId];
const finalTimestamp = Date.now();
const contentMessage = new Proto.Content();
contentMessage.callingMessage = callingMessage;
const conversation = window.ConversationController.get(recipientId);
const conversation = window.ConversationController.get(serviceId);
addPniSignatureMessageToProto({
conversation,
@ -1744,8 +1731,7 @@ export default class MessageSender {
async sendDeliveryReceipt(
options: Readonly<{
senderE164?: string;
senderUuid?: string;
senderServiceId: ServiceIdString;
timestamps: Array<number>;
isDirectConversation: boolean;
options?: Readonly<SendOptionsType>;
@ -1759,8 +1745,7 @@ export default class MessageSender {
async sendReadReceipt(
options: Readonly<{
senderE164?: string;
senderUuid?: string;
senderServiceId: AciString;
timestamps: Array<number>;
isDirectConversation: boolean;
options?: Readonly<SendOptionsType>;
@ -1774,8 +1759,7 @@ export default class MessageSender {
async sendViewedReceipt(
options: Readonly<{
senderE164?: string;
senderUuid?: string;
senderServiceId: ServiceIdString;
timestamps: Array<number>;
isDirectConversation: boolean;
options?: Readonly<SendOptionsType>;
@ -1788,26 +1772,18 @@ export default class MessageSender {
}
private async sendReceiptMessage({
senderE164,
senderUuid,
senderServiceId,
timestamps,
type,
isDirectConversation,
options,
}: Readonly<{
senderE164?: string;
senderUuid?: string;
senderServiceId: ServiceIdString;
timestamps: Array<number>;
type: Proto.ReceiptMessage.Type;
isDirectConversation: boolean;
options?: Readonly<SendOptionsType>;
}>): Promise<CallbackResultType> {
if (!senderUuid && !senderE164) {
throw new Error(
'sendReceiptMessage: Neither uuid nor e164 was provided!'
);
}
const timestamp = Date.now();
const receiptMessage = new Proto.ReceiptMessage();
@ -1820,9 +1796,7 @@ export default class MessageSender {
contentMessage.receiptMessage = receiptMessage;
if (isDirectConversation) {
const conversation = window.ConversationController.get(
senderUuid || senderE164
);
const conversation = window.ConversationController.get(senderServiceId);
addPniSignatureMessageToProto({
conversation,
@ -1834,7 +1808,7 @@ export default class MessageSender {
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
return this.sendIndividualProto({
identifier: senderUuid || senderE164,
serviceId: senderServiceId,
proto: contentMessage,
timestamp,
contentHint: ContentHint.RESENDABLE,
@ -1882,25 +1856,25 @@ export default class MessageSender {
let initialSavePromise: Promise<number>;
return async ({
identifier,
serviceId,
deviceIds,
}: {
identifier: string;
serviceId: ServiceIdString;
deviceIds: Array<number>;
}) => {
if (!shouldSaveProto(sendType)) {
return;
}
const conversation = window.ConversationController.get(identifier);
const conversation = window.ConversationController.get(serviceId);
if (!conversation) {
log.warn(
`makeSendLogCallback: Unable to find conversation for identifier ${identifier}`
`makeSendLogCallback: Unable to find conversation for serviceId ${serviceId}`
);
return;
}
const recipientUuid = conversation.get('uuid');
if (!recipientUuid) {
const recipientServiceId = conversation.get('uuid');
if (!recipientServiceId) {
log.warn(
`makeSendLogCallback: Conversation ${conversation.idForLogging()} had no UUID`
);
@ -1917,7 +1891,7 @@ export default class MessageSender {
hasPniSignatureMessage,
},
{
recipients: { [recipientUuid]: deviceIds },
recipients: { [recipientServiceId]: deviceIds },
messageIds: messageId ? [messageId] : [],
}
);
@ -1926,7 +1900,7 @@ export default class MessageSender {
const id = await initialSavePromise;
await window.Signal.Data.insertProtoRecipients({
id,
recipientUuid,
recipientServiceId,
deviceIds,
});
}
@ -1949,17 +1923,17 @@ export default class MessageSender {
groupId: string | undefined;
options?: SendOptionsType;
proto: Proto.Content;
recipients: ReadonlyArray<string>;
recipients: ReadonlyArray<ServiceIdString>;
sendLogCallback?: SendLogCallbackType;
story?: boolean;
timestamp: number;
urgent: boolean;
}>): Promise<CallbackResultType> {
const myE164 = window.textsecure.storage.user.getNumber();
const myUuid = window.textsecure.storage.user.getUuid()?.toString();
const identifiers = recipients.filter(id => id !== myE164 && id !== myUuid);
const myAci = window.textsecure.storage.user.getAci();
const serviceIds = recipients.filter(id => id !== myE164 && id !== myAci);
if (identifiers.length === 0) {
if (serviceIds.length === 0) {
const dataMessage = proto.dataMessage
? Proto.DataMessage.encode(proto.dataMessage).finish()
: undefined;
@ -1972,8 +1946,8 @@ export default class MessageSender {
dataMessage,
editMessage,
errors: [],
failoverIdentifiers: [],
successfulIdentifiers: [],
failoverServiceIds: [],
successfulServiceIds: [],
unidentifiedDeliveries: [],
contentHint,
urgent,
@ -1996,7 +1970,7 @@ export default class MessageSender {
groupId,
options,
proto,
recipients: identifiers,
recipients: serviceIds,
sendLogCallback,
story,
timestamp,
@ -2013,26 +1987,26 @@ export default class MessageSender {
timestamp,
}: { throwIfNotInDatabase?: boolean; timestamp: number }
): Promise<Proto.Content> {
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
const ourAci = window.textsecure.storage.user.getCheckedAci();
const ourDeviceId = parseIntOrThrow(
window.textsecure.storage.user.getDeviceId(),
'getSenderKeyDistributionMessage'
);
const protocolAddress = ProtocolAddress.new(
ourUuid.toString(),
ourDeviceId
);
const protocolAddress = ProtocolAddress.new(ourAci, ourDeviceId);
const address = new QualifiedAddress(
ourUuid,
new Address(ourUuid, ourDeviceId)
ourAci,
new Address(ourAci, ourDeviceId)
);
const senderKeyDistributionMessage =
await window.textsecure.storage.protocol.enqueueSenderKeyJob(
address,
async () => {
const senderKeyStore = new SenderKeys({ ourUuid, zone: GLOBAL_ZONE });
const senderKeyStore = new SenderKeys({
ourServiceId: ourAci,
zone: GLOBAL_ZONE,
});
if (throwIfNotInDatabase) {
const key = await senderKeyStore.getSenderKey(
@ -2070,7 +2044,7 @@ export default class MessageSender {
contentHint,
distributionId,
groupId,
identifiers,
serviceIds,
throwIfNotInDatabase,
story,
urgent,
@ -2078,7 +2052,7 @@ export default class MessageSender {
contentHint?: number;
distributionId: string;
groupId: string | undefined;
identifiers: ReadonlyArray<string>;
serviceIds: ReadonlyArray<ServiceIdString>;
throwIfNotInDatabase?: boolean;
story?: boolean;
urgent: boolean;
@ -2096,7 +2070,7 @@ export default class MessageSender {
);
const sendLogCallback =
identifiers.length > 1
serviceIds.length > 1
? this.makeSendLogCallback({
contentHint: contentHint ?? ContentHint.IMPLICIT,
proto: Buffer.from(Proto.Content.encode(contentMessage).finish()),
@ -2112,7 +2086,7 @@ export default class MessageSender {
groupId,
options,
proto: contentMessage,
recipients: identifiers,
recipients: serviceIds,
sendLogCallback,
story,
timestamp,
@ -2126,14 +2100,14 @@ export default class MessageSender {
// directly to window.textsecure.messaging.server.<function>
async getProfile(
uuid: UUID,
serviceId: ServiceIdString,
options: GetProfileOptionsType | GetProfileUnauthOptionsType
): ReturnType<WebAPIType['getProfile']> {
if (options.accessKey !== undefined) {
return this.server.getProfileUnauth(uuid.toString(), options);
return this.server.getProfileUnauth(serviceId, options);
}
return this.server.getProfile(uuid.toString(), options);
return this.server.getProfile(serviceId, options);
}
async getAvatar(path: string): Promise<ReturnType<WebAPIType['getAvatar']>> {

View file

@ -3,7 +3,7 @@
import type { SignalService as Proto } from '../protobuf';
import type { IncomingWebSocketRequest } from './WebsocketResources';
import type { UUID, UUIDStringType, TaggedUUIDStringType } from '../types/UUID';
import type { ServiceIdString, PniString } from '../types/ServiceId';
import type { TextAttachmentType } from '../types/Attachment';
import type { GiftBadgeStates } from '../components/conversation/Message';
import type { MIMEType } from '../types/MIME';
@ -44,7 +44,7 @@ export type WebAPICredentials = {
export type DeviceType = {
id: number;
identifier: string;
serviceId: ServiceIdString;
registrationId: number;
};
@ -88,10 +88,10 @@ export type ProcessedEnvelope = Readonly<{
// Mostly from Proto.Envelope except for null/undefined
type: Proto.Envelope.Type;
source?: string;
sourceUuid?: UUIDStringType;
sourceServiceId?: ServiceIdString;
sourceDevice?: number;
destinationUuid: UUID;
updatedPni?: UUID;
destinationServiceId: ServiceIdString;
updatedPni?: PniString;
timestamp: number;
content?: Uint8Array;
serverGuid: string;
@ -138,7 +138,7 @@ export type ProcessedQuoteAttachment = {
export type ProcessedQuote = {
id?: number;
authorUuid?: string;
authorAci?: string;
text?: string;
attachments: ReadonlyArray<ProcessedQuoteAttachment>;
bodyRanges?: ReadonlyArray<ProcessedBodyRange>;
@ -173,7 +173,7 @@ export type ProcessedSticker = {
export type ProcessedReaction = {
emoji?: string;
remove: boolean;
targetAuthorUuid?: string;
targetAuthorAci?: string;
targetTimestamp?: number;
};
@ -225,7 +225,7 @@ export type ProcessedUnidentifiedDeliveryStatus = Omit<
Proto.SyncMessage.Sent.IUnidentifiedDeliveryStatus,
'destinationAci' | 'destinationPni'
> & {
destinationUuid?: TaggedUUIDStringType;
destinationServiceId?: ServiceIdString;
isAllowedToReplyToStory?: boolean;
};
@ -233,7 +233,7 @@ export type ProcessedStoryMessageRecipient = Omit<
Proto.SyncMessage.Sent.IStoryMessageRecipient,
'destinationAci' | 'destinationPni'
> & {
destinationUuid?: TaggedUUIDStringType;
destinationServiceId?: ServiceIdString;
};
export type ProcessedSent = Omit<
@ -245,7 +245,7 @@ export type ProcessedSent = Omit<
| 'destinationPni'
> & {
destinationId?: string;
destinationUuid?: TaggedUUIDStringType;
destinationServiceId?: ServiceIdString;
unidentifiedStatus?: Array<ProcessedUnidentifiedDeliveryStatus>;
storyMessageRecipients?: Array<ProcessedStoryMessageRecipient>;
};
@ -260,10 +260,10 @@ export type CustomError = Error & {
};
export type CallbackResultType = {
successfulIdentifiers?: Array<string>;
failoverIdentifiers?: Array<string>;
successfulServiceIds?: Array<ServiceIdString>;
failoverServiceIds?: Array<ServiceIdString>;
errors?: Array<CustomError>;
unidentifiedDeliveries?: Array<string>;
unidentifiedDeliveries?: Array<ServiceIdString>;
dataMessage: Uint8Array | undefined;
editMessage: Uint8Array | undefined;
@ -275,7 +275,7 @@ export type CallbackResultType = {
contentHint?: number;
contentProto?: Uint8Array;
timestamp?: number;
recipients?: Record<string, Array<number>>;
recipients?: Record<ServiceIdString, Array<number>>;
urgent?: boolean;
hasPniSignatureMessage?: boolean;
};
@ -292,6 +292,6 @@ export type PniKeyMaterialType = Readonly<{
}>;
export type PniSignatureMessageType = Readonly<{
pni: UUIDStringType;
pni: PniString;
signature: Uint8Array;
}>;

View file

@ -6,7 +6,7 @@ import { isNumber } from 'lodash';
import * as durations from '../util/durations';
import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary';
import * as Registration from '../util/registration';
import { UUIDKind } from '../types/UUID';
import { ServiceIdKind } from '../types/ServiceId';
import * as log from '../logging/log';
import * as Errors from '../types/errors';
@ -56,8 +56,8 @@ export class UpdateKeysListener {
try {
const accountManager = window.getAccountManager();
await accountManager.maybeUpdateKeys(UUIDKind.ACI);
await accountManager.maybeUpdateKeys(UUIDKind.PNI);
await accountManager.maybeUpdateKeys(ServiceIdKind.ACI);
await accountManager.maybeUpdateKeys(ServiceIdKind.PNI);
this.scheduleNextUpdate();
this.setTimeoutForNextRun();

View file

@ -32,8 +32,12 @@ import { createHTTPSAgent } from '../util/createHTTPSAgent';
import type { SocketStatus } from '../types/SocketStatus';
import { toLogFormat } from '../types/errors';
import { isPackIdValid, redactPackId } from '../types/Stickers';
import type { UUID, UUIDStringType } from '../types/UUID';
import { UUIDKind } from '../types/UUID';
import type { ServiceIdString, AciString, PniString } from '../types/ServiceId';
import {
ServiceIdKind,
isAciString,
isServiceIdString,
} from '../types/ServiceId';
import type { DirectoryConfigType } from '../types/RendererConfig';
import * as Bytes from '../Bytes';
import { randomInt } from '../Crypto';
@ -177,8 +181,24 @@ type BytesWithDetailsType = {
response: Response;
};
const aciSchema = z
.string()
.refine(isAciString)
.transform(x => {
assertDev(isAciString(x), 'Refine did not throw!');
return x;
});
const serviceIdSchema = z
.string()
.refine(isServiceIdString)
.transform(x => {
assertDev(isServiceIdString(x), 'Refine did not throw!');
return x;
});
export const multiRecipient200ResponseSchema = z.object({
uuids404: z.array(z.string()).optional(),
uuids404: z.array(serviceIdSchema).optional(),
needsSync: z.boolean().optional(),
});
export type MultiRecipient200ResponseType = z.infer<
@ -187,7 +207,7 @@ export type MultiRecipient200ResponseType = z.infer<
export const multiRecipient409ResponseSchema = z.array(
z.object({
uuid: z.string(),
uuid: serviceIdSchema,
devices: z.object({
missingDevices: z.array(z.number()).optional(),
extraDevices: z.array(z.number()).optional(),
@ -200,7 +220,7 @@ export type MultiRecipient409ResponseType = z.infer<
export const multiRecipient410ResponseSchema = z.array(
z.object({
uuid: z.string(),
uuid: serviceIdSchema,
devices: z.object({
staleDevices: z.array(z.number()).optional(),
}),
@ -708,7 +728,7 @@ export type GetAccountForUsernameOptionsType = Readonly<{
}>;
const getAccountForUsernameResultZod = z.object({
uuid: z.string(),
uuid: aciSchema,
});
export type GetAccountForUsernameResultType = z.infer<
@ -748,14 +768,14 @@ const whoamiResultZod = z.object({
export type WhoamiResultType = z.infer<typeof whoamiResultZod>;
export type ConfirmCodeResultType = Readonly<{
uuid: UUIDStringType;
pni: UUIDStringType;
uuid: AciString;
pni: PniString;
deviceId?: number;
}>;
export type CdsLookupOptionsType = Readonly<{
e164s: ReadonlyArray<string>;
acis?: ReadonlyArray<UUIDStringType>;
acis?: ReadonlyArray<AciString>;
accessKeys?: ReadonlyArray<string>;
returnAcisWithoutUaks?: boolean;
}>;
@ -795,17 +815,22 @@ export type GetGroupCredentialsResultType = Readonly<{
credentials: ReadonlyArray<GroupCredentialType>;
}>;
const verifyAciResponse = z.object({
const verifyServiceIdResponse = z.object({
elements: z.array(
z.object({
aci: z.string(),
uuid: serviceIdSchema,
identityKey: z.string(),
})
),
});
export type VerifyAciRequestType = Array<{ aci: string; fingerprint: string }>;
export type VerifyAciResponseType = z.infer<typeof verifyAciResponse>;
export type VerifyServiceIdRequestType = Array<{
uuid: ServiceIdString;
fingerprint: string;
}>;
export type VerifyServiceIdResponseType = z.infer<
typeof verifyServiceIdResponse
>;
export type ReserveUsernameOptionsType = Readonly<{
hashes: ReadonlyArray<Uint8Array>;
@ -938,29 +963,29 @@ export type WebAPIType = {
credentials: GroupCredentialsType
) => Promise<GroupLogResponseType>;
getIceServers: () => Promise<GetIceServersResultType>;
getKeysForIdentifier: (
identifier: string,
getKeysForServiceId: (
serviceId: ServiceIdString,
deviceId?: number
) => Promise<ServerKeysType>;
getKeysForIdentifierUnauth: (
identifier: string,
getKeysForServiceIdUnauth: (
serviceId: ServiceIdString,
deviceId?: number,
options?: { accessKey?: string }
) => Promise<ServerKeysType>;
getMyKeyCounts: (uuidKind: UUIDKind) => Promise<ServerKeyCountType>;
getMyKeyCounts: (serviceIdKind: ServiceIdKind) => Promise<ServerKeyCountType>;
getOnboardingStoryManifest: () => Promise<{
version: string;
languages: Record<string, Array<string>>;
}>;
getProfile: (
identifier: string,
serviceId: ServiceIdString,
options: GetProfileOptionsType
) => Promise<ProfileType>;
getAccountForUsername: (
options: GetAccountForUsernameOptionsType
) => Promise<GetAccountForUsernameResultType>;
getProfileUnauth: (
identifier: string,
serviceId: ServiceIdString,
options: GetProfileUnauthOptionsType
) => Promise<ProfileType>;
getBadgeImageFile: (imageUrl: string) => Promise<Uint8Array>;
@ -1004,8 +1029,8 @@ export type WebAPIType = {
) => Promise<Proto.IGroupChange>;
modifyStorageRecords: MessageSender['modifyStorageRecords'];
postBatchIdentityCheck: (
elements: VerifyAciRequestType
) => Promise<VerifyAciResponseType>;
elements: VerifyServiceIdRequestType
) => Promise<VerifyServiceIdResponseType>;
putEncryptedAttachment: (encryptedBin: Uint8Array) => Promise<string>;
putProfile: (
jsonData: ProfileRequestDataType
@ -1029,20 +1054,23 @@ export type WebAPIType = {
serverId: string
) => Promise<ResolveUsernameLinkResultType>;
registerCapabilities: (capabilities: CapabilitiesUploadType) => Promise<void>;
registerKeys: (genKeys: UploadKeysType, uuidKind: UUIDKind) => Promise<void>;
registerKeys: (
genKeys: UploadKeysType,
serviceIdKind: ServiceIdKind
) => Promise<void>;
registerSupportForUnauthenticatedDelivery: () => Promise<void>;
reportMessage: (options: ReportMessageOptionsType) => Promise<void>;
requestVerificationSMS: (number: string, token: string) => Promise<void>;
requestVerificationVoice: (number: string, token: string) => Promise<void>;
checkAccountExistence: (uuid: UUID) => Promise<boolean>;
checkAccountExistence: (serviceId: ServiceIdString) => Promise<boolean>;
sendMessages: (
destination: string,
destination: ServiceIdString,
messageArray: ReadonlyArray<MessageType>,
timestamp: number,
options: { online?: boolean; story?: boolean; urgent?: boolean }
) => Promise<void>;
sendMessagesUnauth: (
destination: string,
destination: ServiceIdString,
messageArray: ReadonlyArray<MessageType>,
timestamp: number,
options: {
@ -1335,8 +1363,8 @@ export function initialize({
getGroupLog,
getHasSubscription,
getIceServers,
getKeysForIdentifier,
getKeysForIdentifierUnauth,
getKeysForServiceId,
getKeysForServiceIdUnauth,
getMyKeyCounts,
getOnboardingStoryManifest,
getProfile,
@ -1458,14 +1486,14 @@ export function initialize({
}
}
function uuidKindToQuery(kind: UUIDKind): string {
function serviceIdKindToQuery(kind: ServiceIdKind): string {
let value: string;
if (kind === UUIDKind.ACI) {
if (kind === ServiceIdKind.ACI) {
value = 'aci';
} else if (kind === UUIDKind.PNI) {
} else if (kind === ServiceIdKind.PNI) {
value = 'pni';
} else {
throw new Error(`Unsupported UUIDKind: ${kind}`);
throw new Error(`Unsupported ServiceIdKind: ${kind}`);
}
return `identity=${value}`;
}
@ -1676,7 +1704,9 @@ export function initialize({
});
}
async function postBatchIdentityCheck(elements: VerifyAciRequestType) {
async function postBatchIdentityCheck(
elements: VerifyServiceIdRequestType
) {
const res = await _ajax({
data: JSON.stringify({ elements }),
call: 'batchIdentityCheck',
@ -1684,7 +1714,7 @@ export function initialize({
responseType: 'json',
});
const result = verifyAciResponse.safeParse(res);
const result = verifyServiceIdResponse.safeParse(res);
if (result.success) {
return result.data;
@ -1699,13 +1729,13 @@ export function initialize({
}
function getProfileUrl(
identifier: string,
serviceId: ServiceIdString,
{
profileKeyVersion,
profileKeyCredentialRequest,
}: GetProfileCommonOptionsType
) {
let profileUrl = `/${identifier}`;
let profileUrl = `/${serviceId}`;
if (profileKeyVersion !== undefined) {
profileUrl += `/${profileKeyVersion}`;
if (profileKeyCredentialRequest !== undefined) {
@ -1724,7 +1754,7 @@ export function initialize({
}
async function getProfile(
identifier: string,
serviceId: ServiceIdString,
options: GetProfileOptionsType
) {
const { profileKeyVersion, profileKeyCredentialRequest, userLanguages } =
@ -1733,13 +1763,13 @@ export function initialize({
return (await _ajax({
call: 'profile',
httpType: 'GET',
urlParameters: getProfileUrl(identifier, options),
urlParameters: getProfileUrl(serviceId, options),
headers: {
'Accept-Language': formatAcceptLanguageHeader(userLanguages),
},
responseType: 'json',
redactUrl: _createRedactor(
identifier,
serviceId,
profileKeyVersion,
profileKeyCredentialRequest
),
@ -1781,7 +1811,7 @@ export function initialize({
}
async function getProfileUnauth(
identifier: string,
serviceId: ServiceIdString,
options: GetProfileUnauthOptionsType
) {
const {
@ -1794,7 +1824,7 @@ export function initialize({
return (await _ajax({
call: 'profile',
httpType: 'GET',
urlParameters: getProfileUrl(identifier, options),
urlParameters: getProfileUrl(serviceId, options),
headers: {
'Accept-Language': formatAcceptLanguageHeader(userLanguages),
},
@ -1802,7 +1832,7 @@ export function initialize({
unauthenticated: true,
accessKey,
redactUrl: _createRedactor(
identifier,
serviceId,
profileKeyVersion,
profileKeyCredentialRequest
),
@ -2007,12 +2037,12 @@ export function initialize({
});
}
async function checkAccountExistence(uuid: UUID) {
async function checkAccountExistence(serviceId: ServiceIdString) {
try {
await _ajax({
httpType: 'HEAD',
call: 'accountExistence',
urlParameters: `/${uuid.toString()}`,
urlParameters: `/${serviceId}`,
unauthenticated: true,
accessKey: undefined,
});
@ -2154,7 +2184,10 @@ export function initialize({
signedPreKey?: JSONSignedPreKeyType;
};
async function registerKeys(genKeys: UploadKeysType, uuidKind: UUIDKind) {
async function registerKeys(
genKeys: UploadKeysType,
serviceIdKind: ServiceIdKind
) {
const preKeys = genKeys.preKeys?.map(key => ({
keyId: key.keyId,
publicKey: Bytes.toBase64(key.publicKey),
@ -2209,7 +2242,7 @@ export function initialize({
await _ajax({
isRegistration: true,
call: 'keys',
urlParameters: `?${uuidKindToQuery(uuidKind)}`,
urlParameters: `?${serviceIdKindToQuery(serviceIdKind)}`,
httpType: 'PUT',
jsonData: keys,
});
@ -2226,11 +2259,11 @@ export function initialize({
}
async function getMyKeyCounts(
uuidKind: UUIDKind
serviceIdKind: ServiceIdKind
): Promise<ServerKeyCountType> {
const result = (await _ajax({
call: 'keys',
urlParameters: `?${uuidKindToQuery(uuidKind)}`,
urlParameters: `?${serviceIdKindToQuery(serviceIdKind)}`,
httpType: 'GET',
responseType: 'json',
validateResponse: { count: 'number', pqCount: 'number' },
@ -2325,26 +2358,29 @@ export function initialize({
};
}
async function getKeysForIdentifier(identifier: string, deviceId?: number) {
async function getKeysForServiceId(
serviceId: ServiceIdString,
deviceId?: number
) {
const keys = (await _ajax({
call: 'keys',
httpType: 'GET',
urlParameters: `/${identifier}/${deviceId || '*'}?pq=true`,
urlParameters: `/${serviceId}/${deviceId || '*'}?pq=true`,
responseType: 'json',
validateResponse: { identityKey: 'string', devices: 'object' },
})) as ServerKeyResponseType;
return handleKeys(keys);
}
async function getKeysForIdentifierUnauth(
identifier: string,
async function getKeysForServiceIdUnauth(
serviceId: ServiceIdString,
deviceId?: number,
{ accessKey }: { accessKey?: string } = {}
) {
const keys = (await _ajax({
call: 'keys',
httpType: 'GET',
urlParameters: `/${identifier}/${deviceId || '*'}?pq=true`,
urlParameters: `/${serviceId}/${deviceId || '*'}?pq=true`,
responseType: 'json',
validateResponse: { identityKey: 'string', devices: 'object' },
unauthenticated: true,
@ -2354,7 +2390,7 @@ export function initialize({
}
async function sendMessagesUnauth(
destination: string,
destination: ServiceIdString,
messages: ReadonlyArray<MessageType>,
timestamp: number,
{
@ -2388,7 +2424,7 @@ export function initialize({
}
async function sendMessages(
destination: string,
destination: ServiceIdString,
messages: ReadonlyArray<MessageType>,
timestamp: number,
{

View file

@ -9,8 +9,9 @@ import Long from 'long';
import type { LoggerType } from '../../types/Logging';
import { strictAssert } from '../../util/assert';
import { UUID_BYTE_SIZE } from '../../types/UUID';
import { isAciString, isUntaggedPniString } from '../../types/ServiceId';
import * as Bytes from '../../Bytes';
import { UUID_BYTE_SIZE } from '../../Crypto';
import { uuidToBytes, bytesToUuid } from '../../util/uuidToBytes';
import { SignalService as Proto } from '../../protobuf';
import type {
@ -246,6 +247,14 @@ function decodeSingleResponse(
const e164 = `+${e164Long.toString()}`;
const pni = bytesToUuid(pniBytes);
const aci = bytesToUuid(aciBytes);
strictAssert(
aci === undefined || isAciString(aci),
'CDSI response has invalid ACI'
);
strictAssert(
pni === undefined || isUntaggedPniString(pni),
'CDSI response has invalid PNI'
);
resultMap.set(e164, { pni, aci });
}

View file

@ -1,7 +1,7 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { UUIDStringType } from '../../types/UUID';
import type { AciString, UntaggedPniString } from '../../types/ServiceId';
export type CDSAuthType = Readonly<{
username: string;
@ -9,15 +9,15 @@ export type CDSAuthType = Readonly<{
}>;
export type CDSResponseEntryType = Readonly<{
aci: UUIDStringType | undefined;
pni: UUIDStringType | undefined;
aci: AciString | undefined;
pni: UntaggedPniString | undefined;
}>;
export type CDSResponseType = ReadonlyMap<string, CDSResponseEntryType>;
export type CDSRequestOptionsType = Readonly<{
e164s: ReadonlyArray<string>;
acis: ReadonlyArray<UUIDStringType>;
acis: ReadonlyArray<AciString>;
accessKeys: ReadonlyArray<string>;
returnAcisWithoutUaks?: boolean;
timeout?: number;

View file

@ -19,38 +19,34 @@ import {
import { Sessions, IdentityKeys } from '../LibSignalStores';
import { Address } from '../types/Address';
import { QualifiedAddress } from '../types/QualifiedAddress';
import { UUID } from '../types/UUID';
import type { ServiceIdString } from '../types/ServiceId';
import type { ServerKeysType, WebAPIType } from './WebAPI';
import * as log from '../logging/log';
import { isRecord } from '../util/isRecord';
export async function getKeysForIdentifier(
identifier: string,
export async function getKeysForServiceId(
serviceId: ServiceIdString,
server: WebAPIType,
devicesToUpdate?: Array<number>,
accessKey?: string
): Promise<{ accessKeyFailed?: boolean }> {
try {
const { keys, accessKeyFailed } = await getServerKeys(
identifier,
serviceId,
server,
accessKey
);
await handleServerKeys(identifier, keys, devicesToUpdate);
await handleServerKeys(serviceId, keys, devicesToUpdate);
return {
accessKeyFailed,
};
} catch (error) {
if (error instanceof HTTPError && error.code === 404) {
const theirUuid = UUID.lookup(identifier);
await window.textsecure.storage.protocol.archiveAllSessions(serviceId);
if (theirUuid) {
await window.textsecure.storage.protocol.archiveAllSessions(theirUuid);
}
throw new UnregisteredUserError(identifier, error);
throw new UnregisteredUserError(serviceId, error);
}
throw error;
@ -58,19 +54,19 @@ export async function getKeysForIdentifier(
}
async function getServerKeys(
identifier: string,
serviceId: ServiceIdString,
server: WebAPIType,
accessKey?: string
): Promise<{ accessKeyFailed?: boolean; keys: ServerKeysType }> {
try {
if (!accessKey) {
return {
keys: await server.getKeysForIdentifier(identifier),
keys: await server.getKeysForServiceId(serviceId),
};
}
return {
keys: await server.getKeysForIdentifierUnauth(identifier, undefined, {
keys: await server.getKeysForServiceIdUnauth(serviceId, undefined, {
accessKey,
}),
};
@ -83,7 +79,7 @@ async function getServerKeys(
) {
return {
accessKeyFailed: true,
keys: await server.getKeysForIdentifier(identifier),
keys: await server.getKeysForServiceId(serviceId),
};
}
@ -92,13 +88,13 @@ async function getServerKeys(
}
async function handleServerKeys(
identifier: string,
serviceId: ServiceIdString,
response: ServerKeysType,
devicesToUpdate?: Array<number>
): Promise<void> {
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
const sessionStore = new Sessions({ ourUuid });
const identityKeyStore = new IdentityKeys({ ourUuid });
const ourAci = window.textsecure.storage.user.getCheckedAci();
const sessionStore = new Sessions({ ourServiceId: ourAci });
const identityKeyStore = new IdentityKeys({ ourServiceId: ourAci });
await Promise.all(
response.devices.map(async device => {
@ -113,19 +109,15 @@ async function handleServerKeys(
if (device.registrationId === 0) {
log.info(
`handleServerKeys/${identifier}: Got device registrationId zero!`
`handleServerKeys/${serviceId}: Got device registrationId zero!`
);
}
if (!signedPreKey) {
throw new Error(
`getKeysForIdentifier/${identifier}: Missing signed prekey for deviceId ${deviceId}`
`getKeysForIdentifier/${serviceId}: Missing signed prekey for deviceId ${deviceId}`
);
}
const theirUuid = UUID.checkedLookup(identifier);
const protocolAddress = ProtocolAddress.new(
theirUuid.toString(),
deviceId
);
const protocolAddress = ProtocolAddress.new(serviceId, deviceId);
const preKeyId = preKey?.keyId || null;
const preKeyObject = preKey
? PublicKey.deserialize(Buffer.from(preKey.publicKey))
@ -160,14 +152,14 @@ async function handleServerKeys(
);
const address = new QualifiedAddress(
ourUuid,
new Address(theirUuid, deviceId)
ourAci,
new Address(serviceId, deviceId)
);
try {
await window.textsecure.storage.protocol.enqueueSessionJob(
address,
`handleServerKeys(${identifier})`,
`handleServerKeys(${serviceId})`,
() =>
processPreKeyBundle(
preKeyBundle,
@ -181,7 +173,7 @@ async function handleServerKeys(
error instanceof LibSignalErrorBase &&
error.code === ErrorCode.UntrustedIdentity
) {
throw new OutgoingIdentityKeyError(identifier, error);
throw new OutgoingIdentityKeyError(serviceId, error);
}
throw error;
}

View file

@ -5,7 +5,8 @@
import type { PublicKey } from '@signalapp/libsignal-client';
import type { SignalService as Proto } from '../protobuf';
import type { UUIDStringType, TaggedUUIDStringType } from '../types/UUID';
import type { ServiceIdString, AciString } from '../types/ServiceId';
import type { StoryDistributionIdString } from '../types/StoryDistributionId';
import type {
ProcessedEnvelope,
ProcessedDataMessage,
@ -41,7 +42,7 @@ export type TypingEventData = Readonly<{
export type TypingEventConfig = {
sender?: string;
senderUuid?: string;
senderAci?: AciString;
senderDevice: number;
typing: TypingEventData;
};
@ -49,17 +50,17 @@ export type TypingEventConfig = {
export class TypingEvent extends Event {
public readonly sender?: string;
public readonly senderUuid?: string;
public readonly senderAci?: AciString;
public readonly senderDevice: number;
public readonly typing: TypingEventData;
constructor({ sender, senderUuid, senderDevice, typing }: TypingEventConfig) {
constructor({ sender, senderAci, senderDevice, typing }: TypingEventConfig) {
super('typing');
this.sender = sender;
this.senderUuid = senderUuid;
this.senderAci = senderAci;
this.senderDevice = senderDevice;
this.typing = typing;
}
@ -112,7 +113,7 @@ export type DeliveryEventData = Readonly<{
timestamp: number;
envelopeTimestamp: number;
source?: string;
sourceUuid?: UUIDStringType;
sourceServiceId?: ServiceIdString;
sourceDevice?: number;
wasSentEncrypted: boolean;
}>;
@ -134,7 +135,7 @@ export type DecryptionErrorEventData = Readonly<{
receivedAtCounter: number;
receivedAtDate: number;
senderDevice: number;
senderUuid: string;
senderAci: AciString;
timestamp: number;
}>;
@ -149,7 +150,7 @@ export class DecryptionErrorEvent extends ConfirmableEvent {
export type InvalidPlaintextEventData = Readonly<{
senderDevice: number;
senderUuid: string;
senderAci: AciString;
timestamp: number;
}>;
@ -162,7 +163,7 @@ export class InvalidPlaintextEvent extends Event {
export type RetryRequestEventData = Readonly<{
groupId?: string;
ratchetKey?: PublicKey;
requesterUuid: UUIDStringType;
requesterAci: AciString;
requesterDevice: number;
senderDevice: number;
sentAt: number;
@ -180,7 +181,7 @@ export class RetryRequestEvent extends ConfirmableEvent {
export type SentEventData = Readonly<{
envelopeId: string;
destination?: string;
destinationUuid?: TaggedUUIDStringType;
destinationServiceId?: ServiceIdString;
timestamp?: number;
serverTimestamp?: number;
device: number | undefined;
@ -190,7 +191,7 @@ export type SentEventData = Readonly<{
receivedAtCounter: number;
receivedAtDate: number;
expirationStartTimestamp?: number;
storyDistributionListId?: string;
storyDistributionListId?: StoryDistributionIdString;
}>;
export class SentEvent extends ConfirmableEvent {
@ -201,7 +202,7 @@ export class SentEvent extends ConfirmableEvent {
export type ProfileKeyUpdateData = Readonly<{
source?: string;
sourceUuid?: UUIDStringType;
sourceAci?: AciString;
profileKey: string;
}>;
@ -217,9 +218,9 @@ export class ProfileKeyUpdateEvent extends ConfirmableEvent {
export type MessageEventData = Readonly<{
envelopeId: string;
source?: string;
sourceUuid: UUIDStringType;
sourceAci: AciString;
sourceDevice?: number;
destinationUuid: UUIDStringType;
destinationServiceId: ServiceIdString;
timestamp: number;
serverGuid?: string;
serverTimestamp?: number;
@ -242,7 +243,7 @@ export type ReadOrViewEventData = Readonly<{
timestamp: number;
envelopeTimestamp: number;
source?: string;
sourceUuid?: UUIDStringType;
sourceServiceId?: ServiceIdString;
sourceDevice?: number;
wasSentEncrypted: true;
}>;
@ -276,32 +277,32 @@ export class ConfigurationEvent extends ConfirmableEvent {
export type ViewOnceOpenSyncOptions = {
source?: string;
sourceUuid?: UUIDStringType;
sourceAci?: AciString;
timestamp?: number;
};
export class ViewOnceOpenSyncEvent extends ConfirmableEvent {
public readonly source?: string;
public readonly sourceUuid?: UUIDStringType;
public readonly sourceAci?: AciString;
public readonly timestamp?: number;
constructor(
{ source, sourceUuid, timestamp }: ViewOnceOpenSyncOptions,
{ source, sourceAci, timestamp }: ViewOnceOpenSyncOptions,
confirm: ConfirmCallback
) {
super('viewOnceOpenSync', confirm);
this.source = source;
this.sourceUuid = sourceUuid;
this.sourceAci = sourceAci;
this.timestamp = timestamp;
}
}
export type MessageRequestResponseOptions = {
threadE164?: string;
threadUuid?: string;
threadAci?: AciString;
messageRequestResponseType: Proto.SyncMessage.IMessageRequestResponse['type'];
groupId?: string;
groupV2Id?: string;
@ -310,7 +311,7 @@ export type MessageRequestResponseOptions = {
export class MessageRequestResponseEvent extends ConfirmableEvent {
public readonly threadE164?: string;
public readonly threadUuid?: string;
public readonly threadAci?: AciString;
public readonly messageRequestResponseType?: MessageRequestResponseOptions['messageRequestResponseType'];
@ -321,7 +322,7 @@ export class MessageRequestResponseEvent extends ConfirmableEvent {
constructor(
{
threadE164,
threadUuid,
threadAci,
messageRequestResponseType,
groupId,
groupV2Id,
@ -331,7 +332,7 @@ export class MessageRequestResponseEvent extends ConfirmableEvent {
super('messageRequestResponse', confirm);
this.threadE164 = threadE164;
this.threadUuid = threadUuid;
this.threadAci = threadAci;
this.messageRequestResponseType = messageRequestResponseType;
this.groupId = groupId;
this.groupV2Id = groupV2Id;
@ -376,7 +377,7 @@ export type ReadSyncEventData = Readonly<{
timestamp?: number;
envelopeTimestamp: number;
sender?: string;
senderUuid?: string;
senderAci?: AciString;
}>;
export class ReadSyncEvent extends ConfirmableEvent {
@ -392,7 +393,7 @@ export type ViewSyncEventData = Readonly<{
timestamp?: number;
envelopeTimestamp: number;
senderE164?: string;
senderUuid?: string;
senderAci?: AciString;
}>;
export class ViewSyncEvent extends ConfirmableEvent {
@ -434,7 +435,7 @@ export class CallLogEventSyncEvent extends ConfirmableEvent {
}
export type StoryRecipientUpdateData = Readonly<{
destinationUuid: string;
destinationServiceId: ServiceIdString;
storyMessageRecipients: Array<Proto.SyncMessage.Sent.IStoryMessageRecipient>;
timestamp: number;
}>;

View file

@ -131,7 +131,7 @@ export function processQuote(
return {
id: quote.id?.toNumber(),
authorUuid: dropNull(quote.authorUuid),
authorAci: dropNull(quote.authorAci),
text: dropNull(quote.text),
attachments: (quote.attachments ?? []).map(attachment => {
return {
@ -223,7 +223,7 @@ export function processReaction(
return {
emoji: dropNull(reaction.emoji),
remove: Boolean(reaction.remove),
targetAuthorUuid: dropNull(reaction.targetAuthorUuid),
targetAuthorAci: dropNull(reaction.targetAuthorAci),
targetTimestamp: reaction.targetTimestamp?.toNumber(),
};
}

View file

@ -2,49 +2,27 @@
// SPDX-License-Identifier: AGPL-3.0-only
import type { SignalService as Proto } from '../protobuf';
import { normalizeUuid } from '../util/normalizeUuid';
import type { ServiceIdString } from '../types/ServiceId';
import { normalizeServiceId } from '../types/ServiceId';
import type { ProcessedSent, ProcessedSyncMessage } from './Types.d';
import type { TaggedUUIDStringType } from '../types/UUID';
type ProtoUUIDTriple = Readonly<{
destinationAci?: string | null;
destinationPni?: string | null;
type ProtoServiceId = Readonly<{
destinationServiceId?: string | null;
}>;
function toTaggedUuid({
destinationAci,
destinationPni,
}: ProtoUUIDTriple): TaggedUUIDStringType | undefined {
if (destinationAci) {
return {
aci: normalizeUuid(destinationAci, 'syncMessage.sent.destinationAci'),
pni: undefined,
};
}
if (destinationPni) {
return {
aci: undefined,
pni: normalizeUuid(destinationPni, 'syncMessage.sent.destinationPni'),
};
}
return undefined;
}
function processProtoWithDestinationUuid<Input extends ProtoUUIDTriple>(
function processProtoWithDestinationServiceId<Input extends ProtoServiceId>(
input: Input
): Omit<Input, keyof ProtoUUIDTriple> & {
destinationUuid?: TaggedUUIDStringType;
): Omit<Input, keyof ProtoServiceId> & {
destinationServiceId?: ServiceIdString;
} {
const { destinationAci, destinationPni, ...remaining } = input;
const { destinationServiceId, ...remaining } = input;
return {
...remaining,
destinationUuid: toTaggedUuid({
destinationAci,
destinationPni,
}),
destinationServiceId: destinationServiceId
? normalizeServiceId(destinationServiceId, 'processSyncMessage')
: undefined,
};
}
@ -56,8 +34,7 @@ function processSent(
}
const {
destinationAci,
destinationPni,
destinationServiceId,
unidentifiedStatus,
storyMessageRecipients,
...remaining
@ -66,15 +43,14 @@ function processSent(
return {
...remaining,
destinationUuid: toTaggedUuid({
destinationAci,
destinationPni,
}),
destinationServiceId: destinationServiceId
? normalizeServiceId(destinationServiceId, 'processSent')
: undefined,
unidentifiedStatus: unidentifiedStatus
? unidentifiedStatus.map(processProtoWithDestinationUuid)
? unidentifiedStatus.map(processProtoWithDestinationServiceId)
: undefined,
storyMessageRecipients: storyMessageRecipients
? storyMessageRecipients.map(processProtoWithDestinationUuid)
? storyMessageRecipients.map(processProtoWithDestinationServiceId)
: undefined,
};
}

View file

@ -4,6 +4,7 @@
import { without } from 'lodash';
import type { StorageInterface } from '../../types/Storage.d';
import type { ServiceIdString } from '../../types/ServiceId';
import * as log from '../../logging/log';
const BLOCKED_NUMBERS_ID = 'blocked';
@ -41,15 +42,15 @@ export class Blocked {
await this.storage.put(BLOCKED_NUMBERS_ID, without(numbers, number));
}
public getBlockedUuids(): Array<string> {
return this.storage.get(BLOCKED_UUIDS_ID, new Array<string>());
public getBlockedUuids(): Array<ServiceIdString> {
return this.storage.get(BLOCKED_UUIDS_ID, new Array<ServiceIdString>());
}
public isUuidBlocked(uuid: string): boolean {
public isUuidBlocked(uuid: ServiceIdString): boolean {
return this.getBlockedUuids().includes(uuid);
}
public async addBlockedUuid(uuid: string): Promise<void> {
public async addBlockedUuid(uuid: ServiceIdString): Promise<void> {
const uuids = this.getBlockedUuids();
if (uuids.includes(uuid)) {
return;
@ -59,7 +60,7 @@ export class Blocked {
await this.storage.put(BLOCKED_UUIDS_ID, uuids.concat(uuid));
}
public async removeBlockedUuid(uuid: string): Promise<void> {
public async removeBlockedUuid(uuid: ServiceIdString): Promise<void> {
const numbers = this.getBlockedUuids();
if (!numbers.includes(uuid)) {
return;

View file

@ -5,14 +5,19 @@ import type { WebAPICredentials } from '../Types.d';
import { strictAssert } from '../../util/assert';
import type { StorageInterface } from '../../types/Storage.d';
import { UUID, UUIDKind } from '../../types/UUID';
import type {
AciString,
PniString,
ServiceIdString,
} from '../../types/ServiceId';
import { ServiceIdKind, isAciString, isPniString } from '../../types/ServiceId';
import * as log from '../../logging/log';
import Helpers from '../Helpers';
export type SetCredentialsOptions = {
uuid: string;
pni: string;
aci: AciString;
pni: PniString;
number: string;
deviceId: number;
deviceName?: string;
@ -23,12 +28,12 @@ export class User {
constructor(private readonly storage: StorageInterface) {}
public async setUuidAndDeviceId(
uuid: string,
aci: AciString,
deviceId: number
): Promise<void> {
await this.storage.put('uuid_id', `${uuid}.${deviceId}`);
await this.storage.put('uuid_id', `${aci}.${deviceId}`);
log.info('storage.user: uuid and device id changed');
log.info('storage.user: aci and device id changed');
}
public async setNumber(number: string): Promise<void> {
@ -61,53 +66,78 @@ export class User {
return Helpers.unencodeNumber(numberId)[0];
}
public getUuid(uuidKind = UUIDKind.ACI): UUID | undefined {
if (uuidKind === UUIDKind.PNI) {
const pni = this.storage.get('pni');
if (pni === undefined) {
return undefined;
}
return new UUID(pni);
public getPni(): PniString | undefined {
const pni = this.storage.get('pni');
if (pni === undefined || !isPniString(pni)) {
return undefined;
}
return pni;
}
public getAci(): AciString | undefined {
const uuidId = this.storage.get('uuid_id');
if (!uuidId) {
return undefined;
}
const aci = Helpers.unencodeNumber(uuidId.toLowerCase())[0];
if (!isAciString(aci)) {
return undefined;
}
return aci;
}
public getServiceId(
serviceIdKind: ServiceIdKind
): ServiceIdString | undefined {
if (serviceIdKind === ServiceIdKind.PNI) {
return this.getPni();
}
strictAssert(
uuidKind === UUIDKind.ACI,
`Unsupported uuid kind: ${uuidKind}`
serviceIdKind === ServiceIdKind.ACI,
`Unsupported uuid kind: ${serviceIdKind}`
);
const uuid = this.storage.get('uuid_id');
if (!uuid) {
return undefined;
}
return new UUID(Helpers.unencodeNumber(uuid.toLowerCase())[0]);
return this.getAci();
}
public getCheckedUuid(uuidKind?: UUIDKind): UUID {
const uuid = this.getUuid(uuidKind);
public getCheckedAci(): AciString {
const aci = this.getAci();
strictAssert(aci !== undefined, 'Must have our own ACI');
return aci;
}
public getCheckedPni(): PniString {
const pni = this.getPni();
strictAssert(pni !== undefined, 'Must have our own PNI');
return pni;
}
public getCheckedServiceId(serviceIdKind: ServiceIdKind): ServiceIdString {
const uuid = this.getServiceId(serviceIdKind);
strictAssert(uuid !== undefined, 'Must have our own uuid');
return uuid;
}
public async setPni(pni: string): Promise<void> {
await this.storage.put('pni', UUID.cast(pni));
public async setPni(pni: PniString): Promise<void> {
await this.storage.put('pni', pni);
}
public getOurUuidKind(uuid: UUID): UUIDKind {
const ourUuid = this.getUuid();
if (ourUuid?.toString() === uuid.toString()) {
return UUIDKind.ACI;
public getOurServiceIdKind(serviceId: ServiceIdString): ServiceIdKind {
const ourAci = this.getAci();
if (ourAci === serviceId) {
return ServiceIdKind.ACI;
}
const pni = this.getUuid(UUIDKind.PNI);
if (pni?.toString() === uuid.toString()) {
return UUIDKind.PNI;
const pni = this.getPni();
if (pni === serviceId) {
return ServiceIdKind.PNI;
}
return UUIDKind.Unknown;
return ServiceIdKind.Unknown;
}
public isOurUuid(uuid: UUID): boolean {
return this.getOurUuidKind(uuid) !== UUIDKind.Unknown;
public isOurServiceId(serviceId: ServiceIdString): boolean {
return this.getOurServiceIdKind(serviceId) !== ServiceIdKind.Unknown;
}
public getDeviceId(): number | undefined {
@ -137,11 +167,11 @@ export class User {
public async setCredentials(
credentials: SetCredentialsOptions
): Promise<void> {
const { uuid, pni, number, deviceId, deviceName, password } = credentials;
const { aci, pni, number, deviceId, deviceName, password } = credentials;
await Promise.all([
this.storage.put('number_id', `${number}.${deviceId}`),
this.storage.put('uuid_id', `${uuid}.${deviceId}`),
this.storage.put('uuid_id', `${aci}.${deviceId}`),
this.storage.put('password', password),
this.setPni(pni),
deviceName