Use untagged pnis in storage service

This commit is contained in:
Fedor Indutny 2023-09-28 01:14:55 +02:00 committed by GitHub
parent 283ef57779
commit eb7942dd1e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 530 additions and 62 deletions

View file

@ -195,7 +195,7 @@
"@electron/fuses": "1.5.0", "@electron/fuses": "1.5.0",
"@formatjs/intl": "2.6.7", "@formatjs/intl": "2.6.7",
"@mixer/parallel-prettier": "2.0.3", "@mixer/parallel-prettier": "2.0.3",
"@signalapp/mock-server": "4.1.1", "@signalapp/mock-server": "4.1.2",
"@storybook/addon-a11y": "6.5.6", "@storybook/addon-a11y": "6.5.6",
"@storybook/addon-actions": "6.5.6", "@storybook/addon-actions": "6.5.6",
"@storybook/addon-controls": "6.5.6", "@storybook/addon-controls": "6.5.6",

View file

@ -49,6 +49,9 @@ import {
normalizeServiceId, normalizeServiceId,
normalizePni, normalizePni,
ServiceIdKind, ServiceIdKind,
isUntaggedPniString,
toUntaggedPni,
toTaggedPni,
} from '../types/ServiceId'; } from '../types/ServiceId';
import { normalizeAci } from '../util/normalizeAci'; import { normalizeAci } from '../util/normalizeAci';
import * as Stickers from '../types/Stickers'; import * as Stickers from '../types/Stickers';
@ -171,7 +174,7 @@ export async function toContactRecord(
} }
const pni = conversation.getPni(); const pni = conversation.getPni();
if (pni && RemoteConfig.isEnabled('desktop.pnp')) { if (pni && RemoteConfig.isEnabled('desktop.pnp')) {
contactRecord.pni = pni; contactRecord.pni = toUntaggedPni(pni);
} }
const profileKey = conversation.get('profileKey'); const profileKey = conversation.get('profileKey');
if (profileKey) { if (profileKey) {
@ -972,9 +975,14 @@ export async function mergeContactRecord(
aci: originalContactRecord.aci aci: originalContactRecord.aci
? normalizeAci(originalContactRecord.aci, 'ContactRecord.aci') ? normalizeAci(originalContactRecord.aci, 'ContactRecord.aci')
: undefined, : undefined,
pni: originalContactRecord.pni pni:
? normalizePni(originalContactRecord.pni, 'ContactRecord.pni') originalContactRecord.pni &&
: undefined, isUntaggedPniString(originalContactRecord.pni)
? normalizePni(
toTaggedPni(originalContactRecord.pni),
'ContactRecord.pni'
)
: undefined,
}; };
const isPniSupported = RemoteConfig.isEnabled('desktop.pnp'); const isPniSupported = RemoteConfig.isEnabled('desktop.pnp');

View file

@ -0,0 +1,265 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { Database } from '@signalapp/better-sqlite3';
import type { LoggerType } from '../../types/Logging';
import type {
ServiceIdString,
AciString,
PniString,
} from '../../types/ServiceId';
import { normalizePni } from '../../types/ServiceId';
import { normalizeAci } from '../../util/normalizeAci';
import type { JSONWithUnknownFields } from '../../types/Util';
export const version = 960;
export function updateToSchemaVersion960(
currentVersion: number,
db: Database,
logger: LoggerType
): void {
if (currentVersion >= 960) {
return;
}
db.transaction(() => {
const ourServiceIds = migratePni(db, logger);
if (!ourServiceIds) {
logger.info('updateToSchemaVersion960: not running, pni is normalized');
return;
}
// Migrate JSON fields
db.prepare(
`
UPDATE conversations
SET json = json_set(json, '$.pni', $pni)
WHERE serviceId IS $aci
`
).run({
aci: ourServiceIds.aci,
pni: ourServiceIds.pni,
});
migratePreKeys(db, 'preKeys', ourServiceIds, logger);
migratePreKeys(db, 'signedPreKeys', ourServiceIds, logger);
migratePreKeys(db, 'kyberPreKeys', ourServiceIds, logger);
db.pragma('user_version = 960');
})();
logger.info('updateToSchemaVersion960: success!');
}
//
// migratePni checks if `pni` needs normalization:
//
// * If yes - return legacy and updated pni
// * It no - return undefined
//
type OurServiceIds = Readonly<{
aci: AciString;
legacyPni: string;
pni: PniString;
}>;
function migratePni(
db: Database,
logger: LoggerType
): OurServiceIds | undefined {
// Get our ACI and PNI
const uuidIdJson = db
.prepare(
`
SELECT json
FROM items
WHERE id IS 'uuid_id'
`
)
.pluck()
.get();
const pniJson = db
.prepare(
`
SELECT json
FROM items
WHERE id IS 'pni'
`
)
.pluck()
.get();
let aci: string | undefined;
try {
[aci] = JSON.parse(uuidIdJson).value.split('.', 2);
} catch (error) {
if (uuidIdJson) {
logger.warn(
'updateToSchemaVersion960: failed to parse uuid_id item',
error
);
} else {
logger.info('updateToSchemaVersion960: Our ACI not found');
}
}
if (!aci) {
return undefined;
}
let legacyPni: string | undefined;
try {
legacyPni = JSON.parse(pniJson).value;
} catch (error) {
if (pniJson) {
logger.warn('updateToSchemaVersion960: failed to parse pni item', error);
} else {
logger.info('updateToSchemaVersion960: Our PNI not found');
}
}
if (!legacyPni) {
return undefined;
}
const pni = prefixPni(legacyPni, 'pni', logger);
if (!pni || pni === legacyPni) {
return undefined;
}
const maps: Array<{ id: string; json: string }> = db
.prepare(
`
SELECT id, json
FROM items
WHERE id IN ('identityKeyMap', 'registrationIdMap');
`
)
.all();
const updateStmt = db.prepare(
'UPDATE items SET json = $json WHERE id IS $id'
);
updateStmt.run({
id: 'pni',
json: JSON.stringify({ id: 'pni', value: pni }),
});
for (const { id, json } of maps) {
try {
const data: { id: string; value: Record<string, unknown> } =
JSON.parse(json);
const pniValue = data.value[legacyPni];
if (pniValue) {
delete data.value[legacyPni];
data.value[pni] = pniValue;
}
updateStmt.run({ id, json: JSON.stringify(data) });
} catch (error) {
logger.warn(
`updateToSchemaVersion960: failed to parse ${id} item`,
error
);
}
}
return {
aci: normalizeAci(aci, 'uuid_id', logger),
pni,
legacyPni,
};
}
// migratePreKeys does the following:
//
// 1. Update `ourServiceId` to prefixed PNI
// 2. Update `id` to use new `ourServiceId` value
// (the schema is `${ourServiceId}:${keyId}`)
//
function migratePreKeys(
db: Database,
table: string,
{ legacyPni, pni }: OurServiceIds,
logger: LoggerType
): void {
const preKeys = db
.prepare(`SELECT id, json FROM ${table} WHERE ourServiceId IS $legacyPni`)
.all({ legacyPni });
const updateStmt = db.prepare(`
UPDATE ${table}
SET id = $newId, json = $newJson
WHERE id = $id
`);
logger.info(`updateToSchemaVersion960: updating ${preKeys.length} ${table}`);
for (const { id, json } of preKeys) {
const match = id.match(/^(.*):(.*)$/);
if (!match) {
logger.warn(`updateToSchemaVersion960: invalid ${table} id ${id}`);
continue;
}
let legacyData: JSONWithUnknownFields<Record<string, unknown>>;
try {
legacyData = JSON.parse(json);
} catch (error) {
logger.warn(
`updateToSchemaVersion960: failed to parse ${table} ${id}`,
error
);
continue;
}
const [, ourServiceId, keyId] = match;
if (ourServiceId !== legacyPni) {
logger.warn(
'updateToSchemaVersion960: unexpected ourServiceId',
ourServiceId,
legacyPni
);
continue;
}
const newId = `${pni}:${keyId}`;
const newData: JSONWithUnknownFields<{
id: string;
ourServiceId: ServiceIdString;
}> = {
...legacyData,
id: newId,
ourServiceId: pni,
};
updateStmt.run({
id,
newId,
newJson: JSON.stringify(newData),
});
}
}
//
// Various utility methods below.
//
function prefixPni(
legacyPni: string | null | undefined,
context: string,
logger: LoggerType
): PniString | undefined {
if (legacyPni == null) {
return undefined;
}
if (legacyPni.toLowerCase().startsWith('pni:')) {
return normalizePni(legacyPni, context, logger);
}
return normalizePni(`PNI:${legacyPni}`, context, logger);
}

View file

@ -70,10 +70,11 @@ import updateToSchemaVersion91 from './91-clean-keys';
import { updateToSchemaVersion920 } from './920-clean-more-keys'; import { updateToSchemaVersion920 } from './920-clean-more-keys';
import { updateToSchemaVersion930 } from './930-fts5-secure-delete'; import { updateToSchemaVersion930 } from './930-fts5-secure-delete';
import { updateToSchemaVersion940 } from './940-fts5-revert'; import { updateToSchemaVersion940 } from './940-fts5-revert';
import { updateToSchemaVersion950 } from './950-fts5-secure-delete';
import { import {
version as MAX_VERSION, version as MAX_VERSION,
updateToSchemaVersion950, updateToSchemaVersion960,
} from './950-fts5-secure-delete'; } from './960-untag-pni';
function updateToSchemaVersion1( function updateToSchemaVersion1(
currentVersion: number, currentVersion: number,
@ -2011,6 +2012,7 @@ export const SCHEMA_VERSIONS = [
updateToSchemaVersion930, updateToSchemaVersion930,
updateToSchemaVersion940, updateToSchemaVersion940,
updateToSchemaVersion950, updateToSchemaVersion950,
updateToSchemaVersion960,
]; ];
export class DBVersionFromFutureError extends Error { export class DBVersionFromFutureError extends Error {

View file

@ -9,6 +9,7 @@ import Long from 'long';
import * as durations from '../../util/durations'; import * as durations from '../../util/durations';
import { uuidToBytes } from '../../util/uuidToBytes'; import { uuidToBytes } from '../../util/uuidToBytes';
import { toUntaggedPni } from '../../types/ServiceId';
import { MY_STORY_ID } from '../../types/Stories'; import { MY_STORY_ID } from '../../types/Stories';
import { Bootstrap } from '../bootstrap'; import { Bootstrap } from '../bootstrap';
import type { App } from '../bootstrap'; import type { App } from '../bootstrap';
@ -392,7 +393,7 @@ describe('pnp/merge', function needsName() {
aciContacts += 1; aciContacts += 1;
assert.strictEqual(pni, ''); assert.strictEqual(pni, '');
assert.strictEqual(serviceE164, ''); assert.strictEqual(serviceE164, '');
} else if (pni === pniContact.device.pni) { } else if (pni === toUntaggedPni(pniContact.device.pni)) {
pniContacts += 1; pniContacts += 1;
assert.strictEqual(aci, ''); assert.strictEqual(aci, '');
assert.strictEqual(serviceE164, pniContact.device.number); assert.strictEqual(serviceE164, pniContact.device.number);
@ -401,7 +402,10 @@ describe('pnp/merge', function needsName() {
assert.strictEqual(aciContacts, 1); assert.strictEqual(aciContacts, 1);
assert.strictEqual(pniContacts, 1); assert.strictEqual(pniContacts, 1);
assert.strictEqual(removed[0].contact?.pni, pniContact.device.pni); assert.strictEqual(
removed[0].contact?.pni,
toUntaggedPni(pniContact.device.pni)
);
assert.strictEqual(removed[0].contact?.aci, pniContact.device.aci); assert.strictEqual(removed[0].contact?.aci, pniContact.device.aci);
// Pin PNI so that it appears in the left pane // Pin PNI so that it appears in the left pane

View file

@ -7,7 +7,7 @@ import type { PrimaryDevice } from '@signalapp/mock-server';
import createDebug from 'debug'; import createDebug from 'debug';
import * as durations from '../../util/durations'; import * as durations from '../../util/durations';
import { generatePni } from '../../types/ServiceId'; import { generatePni, toUntaggedPni } from '../../types/ServiceId';
import { Bootstrap } from '../bootstrap'; import { Bootstrap } from '../bootstrap';
import type { App } from '../bootstrap'; import type { App } from '../bootstrap';
@ -48,7 +48,7 @@ describe('pnp/PNI Change', function needsName() {
whitelisted: true, whitelisted: true,
serviceE164: contactA.device.number, serviceE164: contactA.device.number,
identityKey: contactA.getPublicKey(ServiceIdKind.PNI).serialize(), identityKey: contactA.getPublicKey(ServiceIdKind.PNI).serialize(),
pni: contactA.device.getServiceIdByKind(ServiceIdKind.PNI), pni: toUntaggedPni(contactA.device.pni),
givenName: 'ContactA', givenName: 'ContactA',
}, },
ServiceIdKind.PNI ServiceIdKind.PNI
@ -132,8 +132,7 @@ describe('pnp/PNI Change', function needsName() {
state state
.removeRecord( .removeRecord(
item => item =>
item.record.contact?.pni === item.record.contact?.pni === toUntaggedPni(contactA.device.pni)
contactA.device.getServiceIdByKind(ServiceIdKind.PNI)
) )
.addContact( .addContact(
contactA, contactA,
@ -141,8 +140,7 @@ describe('pnp/PNI Change', function needsName() {
identityState: Proto.ContactRecord.IdentityState.DEFAULT, identityState: Proto.ContactRecord.IdentityState.DEFAULT,
whitelisted: true, whitelisted: true,
serviceE164: contactA.device.number, serviceE164: contactA.device.number,
aci: updatedPni, pni: toUntaggedPni(updatedPni),
pni: updatedPni,
identityKey: contactA.getPublicKey(ServiceIdKind.PNI).serialize(), identityKey: contactA.getPublicKey(ServiceIdKind.PNI).serialize(),
}, },
ServiceIdKind.PNI ServiceIdKind.PNI
@ -232,8 +230,7 @@ describe('pnp/PNI Change', function needsName() {
state state
.removeRecord( .removeRecord(
item => item =>
item.record.contact?.pni === item.record.contact?.pni === toUntaggedPni(contactA.device.pni)
contactA.device.getServiceIdByKind(ServiceIdKind.PNI)
) )
.addContact( .addContact(
contactB, contactB,
@ -241,7 +238,7 @@ describe('pnp/PNI Change', function needsName() {
identityState: Proto.ContactRecord.IdentityState.DEFAULT, identityState: Proto.ContactRecord.IdentityState.DEFAULT,
whitelisted: true, whitelisted: true,
serviceE164: contactA.device.number, serviceE164: contactA.device.number,
pni: contactB.device.getServiceIdByKind(ServiceIdKind.PNI), pni: toUntaggedPni(contactB.device.pni),
// Key change - different identity key // Key change - different identity key
identityKey: contactB.publicKey.serialize(), identityKey: contactB.publicKey.serialize(),
@ -336,8 +333,7 @@ describe('pnp/PNI Change', function needsName() {
state state
.removeRecord( .removeRecord(
item => item =>
item.record.contact?.pni === item.record.contact?.pni === toUntaggedPni(contactA.device.pni)
contactA.device.getServiceIdByKind(ServiceIdKind.PNI)
) )
.addContact( .addContact(
contactB, contactB,
@ -345,7 +341,7 @@ describe('pnp/PNI Change', function needsName() {
identityState: Proto.ContactRecord.IdentityState.DEFAULT, identityState: Proto.ContactRecord.IdentityState.DEFAULT,
whitelisted: true, whitelisted: true,
serviceE164: contactA.device.number, serviceE164: contactA.device.number,
pni: contactB.device.getServiceIdByKind(ServiceIdKind.PNI), pni: toUntaggedPni(contactB.device.pni),
// Note: No identityKey key provided here! // Note: No identityKey key provided here!
}, },
@ -470,8 +466,7 @@ describe('pnp/PNI Change', function needsName() {
state state
.removeRecord( .removeRecord(
item => item =>
item.record.contact?.pni === item.record.contact?.pni === toUntaggedPni(contactA.device.pni)
contactA.device.getServiceIdByKind(ServiceIdKind.PNI)
) )
.addContact( .addContact(
contactB, contactB,
@ -479,7 +474,7 @@ describe('pnp/PNI Change', function needsName() {
identityState: Proto.ContactRecord.IdentityState.DEFAULT, identityState: Proto.ContactRecord.IdentityState.DEFAULT,
whitelisted: true, whitelisted: true,
serviceE164: contactA.device.number, serviceE164: contactA.device.number,
pni: contactB.device.getServiceIdByKind(ServiceIdKind.PNI), pni: toUntaggedPni(contactB.device.pni),
// Note: No identityKey key provided here! // Note: No identityKey key provided here!
}, },
@ -503,8 +498,7 @@ describe('pnp/PNI Change', function needsName() {
state state
.removeRecord( .removeRecord(
item => item =>
item.record.contact?.pni === item.record.contact?.pni === toUntaggedPni(contactB.device.pni)
contactB.device.getServiceIdByKind(ServiceIdKind.PNI)
) )
.addContact( .addContact(
contactB, contactB,
@ -512,7 +506,7 @@ describe('pnp/PNI Change', function needsName() {
identityState: Proto.ContactRecord.IdentityState.DEFAULT, identityState: Proto.ContactRecord.IdentityState.DEFAULT,
whitelisted: true, whitelisted: true,
serviceE164: contactA.device.number, serviceE164: contactA.device.number,
pni: contactA.device.getServiceIdByKind(ServiceIdKind.PNI), pni: toUntaggedPni(contactA.device.pni),
}, },
ServiceIdKind.PNI ServiceIdKind.PNI
) )

View file

@ -15,6 +15,7 @@ import createDebug from 'debug';
import * as durations from '../../util/durations'; import * as durations from '../../util/durations';
import { uuidToBytes } from '../../util/uuidToBytes'; import { uuidToBytes } from '../../util/uuidToBytes';
import { MY_STORY_ID } from '../../types/Stories'; import { MY_STORY_ID } from '../../types/Stories';
import { isUntaggedPniString, toTaggedPni } from '../../types/ServiceId';
import { Bootstrap } from '../bootstrap'; import { Bootstrap } from '../bootstrap';
import type { App } from '../bootstrap'; import type { App } from '../bootstrap';
@ -345,17 +346,22 @@ describe('pnp/PNI Signature', function needsName() {
after: state, after: state,
}); });
const pni = newState.getContact(pniContact, ServiceIdKind.PNI); const pniRecord = newState.getContact(pniContact, ServiceIdKind.PNI);
const aci = newState.getContact(pniContact, ServiceIdKind.ACI); const aciRecord = newState.getContact(pniContact, ServiceIdKind.ACI);
assert.strictEqual( assert.strictEqual(
aci, aciRecord,
pni, pniRecord,
'ACI Contact must be the same as PNI Contact storage service' 'ACI Contact must be the same as PNI Contact storage service'
); );
assert(aci, 'ACI Contact must be in storage service'); assert(aciRecord, 'ACI Contact must be in storage service');
assert.strictEqual(aci?.aci, pniContact.device.aci); assert.strictEqual(aciRecord?.aci, pniContact.device.aci);
assert.strictEqual(aci?.pni, pniContact.device.pni); assert.strictEqual(
aciRecord?.pni &&
isUntaggedPniString(aciRecord?.pni) &&
toTaggedPni(aciRecord?.pni),
pniContact.device.pni
);
// Two outgoing, one incoming // Two outgoing, one incoming
const messages = window.locator('.module-message__text'); const messages = window.locator('.module-message__text');

View file

@ -12,7 +12,7 @@ import {
import createDebug from 'debug'; import createDebug from 'debug';
import * as durations from '../../util/durations'; import * as durations from '../../util/durations';
import { generatePni } from '../../types/ServiceId'; import { generatePni, toUntaggedPni } from '../../types/ServiceId';
import { Bootstrap } from '../bootstrap'; import { Bootstrap } from '../bootstrap';
import type { App } from '../bootstrap'; import type { App } from '../bootstrap';
@ -92,7 +92,10 @@ describe('pnp/PNI DecryptionError unlink', function needsName() {
pniChangeNumber, pniChangeNumber,
}, },
}, },
{ timestamp: bootstrap.getTimestamp(), updatedPni: generatePni() } {
timestamp: bootstrap.getTimestamp(),
updatedPni: toUntaggedPni(generatePni()),
}
) )
); );
sendPromises.push( sendPromises.push(
@ -103,7 +106,10 @@ describe('pnp/PNI DecryptionError unlink', function needsName() {
pniChangeNumber, pniChangeNumber,
}, },
}, },
{ timestamp: bootstrap.getTimestamp(), updatedPni: desktop.pni } {
timestamp: bootstrap.getTimestamp(),
updatedPni: toUntaggedPni(desktop.pni),
}
) )
); );

View file

@ -9,6 +9,7 @@ import * as durations from '../../util/durations';
import { Bootstrap } from '../bootstrap'; import { Bootstrap } from '../bootstrap';
import type { App } from '../bootstrap'; import type { App } from '../bootstrap';
import { ReceiptType } from '../../types/Receipt'; import { ReceiptType } from '../../types/Receipt';
import { toUntaggedPni } from '../../types/ServiceId';
export const debug = createDebug('mock:test:challenge:receipts'); export const debug = createDebug('mock:test:challenge:receipts');
@ -49,7 +50,7 @@ describe('challenge/receipts', function challengeReceiptsTest() {
whitelisted: true, whitelisted: true,
serviceE164: contact.device.number, serviceE164: contact.device.number,
identityKey: contact.getPublicKey(ServiceIdKind.PNI).serialize(), identityKey: contact.getPublicKey(ServiceIdKind.PNI).serialize(),
pni: contact.device.getServiceIdByKind(ServiceIdKind.PNI), pni: toUntaggedPni(contact.device.pni),
givenName: 'Jamie', givenName: 'Jamie',
}, },
ServiceIdKind.PNI ServiceIdKind.PNI

View file

@ -0,0 +1,175 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import type { Database } from '@signalapp/better-sqlite3';
import SQL from '@signalapp/better-sqlite3';
import { v4 as generateGuid } from 'uuid';
import { updateToVersion, insertData, getTableData } from './helpers';
const CONVO_ID = generateGuid();
const OUR_ACI = generateGuid();
const OUR_UNPREFIXED_PNI = generateGuid();
const OUR_PREFIXED_PNI = `PNI:${OUR_UNPREFIXED_PNI}`;
describe('SQL/updateToSchemaVersion960', () => {
let db: Database;
beforeEach(() => {
db = new SQL(':memory:');
updateToVersion(db, 950);
insertData(db, 'items', [
{
id: 'uuid_id',
json: {
id: 'uuid_id',
value: `${OUR_ACI}.1`,
},
},
{
id: 'pni',
json: {
id: 'pni',
value: OUR_UNPREFIXED_PNI,
},
},
]);
});
afterEach(() => {
db.close();
});
it('should migrate our conversation', () => {
insertData(db, 'conversations', [
{
id: CONVO_ID,
type: 'direct',
serviceId: OUR_ACI,
json: {
id: CONVO_ID,
serviceId: OUR_ACI,
pni: OUR_UNPREFIXED_PNI,
},
},
]);
updateToVersion(db, 960);
assert.deepStrictEqual(getTableData(db, 'conversations'), [
{
id: CONVO_ID,
type: 'direct',
serviceId: OUR_ACI,
json: {
id: CONVO_ID,
serviceId: OUR_ACI,
pni: OUR_PREFIXED_PNI,
},
},
]);
});
it('should migrate items', () => {
insertData(db, 'items', [
{
id: 'registrationIdMap',
json: {
id: 'registrationIdMap',
value: {
[OUR_ACI]: 123,
[OUR_UNPREFIXED_PNI]: 456,
},
},
},
{
id: 'identityKeyMap',
json: {
id: 'identityKeyMap',
value: {
[OUR_ACI]: {},
[OUR_UNPREFIXED_PNI]: {},
},
},
},
]);
updateToVersion(db, 960);
assert.deepStrictEqual(getTableData(db, 'items'), [
{
id: 'uuid_id',
json: {
id: 'uuid_id',
value: `${OUR_ACI}.1`,
},
},
{
id: 'pni',
json: {
id: 'pni',
value: OUR_PREFIXED_PNI,
},
},
{
id: 'registrationIdMap',
json: {
id: 'registrationIdMap',
value: {
[OUR_ACI]: 123,
[OUR_PREFIXED_PNI]: 456,
},
},
},
{
id: 'identityKeyMap',
json: {
id: 'identityKeyMap',
value: {
[OUR_ACI]: {},
[OUR_PREFIXED_PNI]: {},
},
},
},
]);
});
for (const table of ['preKeys', 'signedPreKeys', 'kyberPreKeys']) {
// eslint-disable-next-line no-loop-func
it(`should migrate ${table}`, () => {
insertData(db, table, [
{
id: `${OUR_ACI}:123`,
json: {
id: `${OUR_ACI}:123`,
ourServiceId: OUR_ACI,
},
},
{
id: `${OUR_UNPREFIXED_PNI}:456`,
json: {
id: `${OUR_UNPREFIXED_PNI}:456`,
ourServiceId: OUR_UNPREFIXED_PNI,
},
},
]);
updateToVersion(db, 960);
assert.deepStrictEqual(getTableData(db, table), [
{
id: `${OUR_ACI}:123`,
json: {
id: `${OUR_ACI}:123`,
ourServiceId: OUR_ACI,
},
ourServiceId: OUR_ACI,
},
{
id: `${OUR_PREFIXED_PNI}:456`,
json: {
id: `${OUR_PREFIXED_PNI}:456`,
ourServiceId: OUR_PREFIXED_PNI,
},
ourServiceId: OUR_PREFIXED_PNI,
},
]);
});
}
});

View file

@ -401,7 +401,7 @@ export default class AccountManager extends EventTarget {
!provisionMessage.pniKeyPair || !provisionMessage.pniKeyPair ||
!provisionMessage.profileKey || !provisionMessage.profileKey ||
!provisionMessage.aci || !provisionMessage.aci ||
!isUntaggedPniString(provisionMessage.pni) !isUntaggedPniString(provisionMessage.untaggedPni)
) { ) {
throw new Error( throw new Error(
'AccountManager.registerSecondDevice: Provision message was missing key data' 'AccountManager.registerSecondDevice: Provision message was missing key data'
@ -410,7 +410,7 @@ export default class AccountManager extends EventTarget {
const ourAci = normalizeAci(provisionMessage.aci, 'provisionMessage.aci'); const ourAci = normalizeAci(provisionMessage.aci, 'provisionMessage.aci');
const ourPni = normalizePni( const ourPni = normalizePni(
toTaggedPni(provisionMessage.pni), toTaggedPni(provisionMessage.untaggedPni),
'provisionMessage.pni' 'provisionMessage.pni'
); );

View file

@ -59,8 +59,10 @@ import {
normalizeServiceId, normalizeServiceId,
normalizePni, normalizePni,
isPniString, isPniString,
isUntaggedPniString,
isServiceIdString, isServiceIdString,
fromPniObject, fromPniObject,
toTaggedPni,
} from '../types/ServiceId'; } from '../types/ServiceId';
import { normalizeAci } from '../util/normalizeAci'; import { normalizeAci } from '../util/normalizeAci';
import { isAciString } from '../util/isAciString'; import { isAciString } from '../util/isAciString';
@ -421,12 +423,13 @@ export default class MessageReceiver
'MessageReceiver.handleRequest.destinationServiceId' 'MessageReceiver.handleRequest.destinationServiceId'
) )
: ourAci, : ourAci,
updatedPni: decoded.updatedPni updatedPni:
? normalizePni( decoded.updatedPni && isUntaggedPniString(decoded.updatedPni)
decoded.updatedPni, ? normalizePni(
'MessageReceiver.handleRequest.updatedPni' toTaggedPni(decoded.updatedPni),
) 'MessageReceiver.handleRequest.updatedPni'
: undefined, )
: undefined,
timestamp: decoded.timestamp?.toNumber(), timestamp: decoded.timestamp?.toNumber(),
content: dropNull(decoded.content), content: dropNull(decoded.content),
serverGuid: decoded.serverGuid, serverGuid: decoded.serverGuid,
@ -878,8 +881,11 @@ export default class MessageReceiver
decoded.destinationServiceId || item.destinationServiceId || ourAci, decoded.destinationServiceId || item.destinationServiceId || ourAci,
'CachedEnvelope.destinationServiceId' 'CachedEnvelope.destinationServiceId'
), ),
updatedPni: decoded.updatedPni updatedPni: isUntaggedPniString(decoded.updatedPni)
? normalizePni(decoded.updatedPni, 'CachedEnvelope.updatedPni') ? normalizePni(
toTaggedPni(decoded.updatedPni),
'CachedEnvelope.updatedPni'
)
: undefined, : undefined,
timestamp: decoded.timestamp?.toNumber(), timestamp: decoded.timestamp?.toNumber(),
content: dropNull(decoded.content), content: dropNull(decoded.content),

View file

@ -12,17 +12,14 @@ import {
} from '../Crypto'; } from '../Crypto';
import { calculateAgreement, createKeyPair, generateKeyPair } from '../Curve'; import { calculateAgreement, createKeyPair, generateKeyPair } from '../Curve';
import { SignalService as Proto } from '../protobuf'; import { SignalService as Proto } from '../protobuf';
import type { PniString, AciString } from '../types/ServiceId';
import { normalizePni } from '../types/ServiceId';
import { normalizeAci } from '../util/normalizeAci';
import { strictAssert } from '../util/assert'; import { strictAssert } from '../util/assert';
type ProvisionDecryptResult = { type ProvisionDecryptResult = {
aciKeyPair: KeyPairType; aciKeyPair: KeyPairType;
pniKeyPair?: KeyPairType; pniKeyPair?: KeyPairType;
number?: string; number?: string;
aci?: AciString; aci?: string;
pni?: PniString; untaggedPni?: string;
provisioningCode?: string; provisioningCode?: string;
userAgent?: string; userAgent?: string;
readReceipts?: boolean; readReceipts?: boolean;
@ -75,13 +72,14 @@ class ProvisioningCipherInner {
const { aci, pni } = provisionMessage; const { aci, pni } = provisionMessage;
strictAssert(aci, 'Missing aci in provisioning message'); strictAssert(aci, 'Missing aci in provisioning message');
strictAssert(pni, 'Missing pni in provisioning message');
const ret: ProvisionDecryptResult = { const ret: ProvisionDecryptResult = {
aciKeyPair, aciKeyPair,
pniKeyPair, pniKeyPair,
number: provisionMessage.number, number: provisionMessage.number,
aci: normalizeAci(aci, 'ProvisionMessage.aci'), aci,
pni: pni ? normalizePni(pni, 'ProvisionMessage.pni') : undefined, untaggedPni: pni,
provisioningCode: provisionMessage.provisioningCode, provisioningCode: provisionMessage.provisioningCode,
userAgent: provisionMessage.userAgent, userAgent: provisionMessage.userAgent,
readReceipts: provisionMessage.readReceipts, readReceipts: provisionMessage.readReceipts,

View file

@ -49,6 +49,10 @@ export function toTaggedPni(untagged: UntaggedPniString): PniString {
return `PNI:${untagged}` as PniString; return `PNI:${untagged}` as PniString;
} }
export function toUntaggedPni(pni: PniString): UntaggedPniString {
return pni.replace(/^PNI:/i, '') as UntaggedPniString;
}
export function normalizeServiceId( export function normalizeServiceId(
rawServiceId: string, rawServiceId: string,
context: string, context: string,
@ -106,10 +110,9 @@ export function normalizePni(
} }
const result = rawPni.toLowerCase().replace(/^pni:/, 'PNI:'); const result = rawPni.toLowerCase().replace(/^pni:/, 'PNI:');
if (!isPniString(result)) { if (!isPniString(result)) {
logger.warn( logger.warn(
`Normalizing invalid serviceId: ${rawPni} to ${result} in context "${context}"` `Normalizing invalid pni: ${rawPni} to ${result} in context "${context}"`
); );
// Cast anyway we don't want to throw here // Cast anyway we don't want to throw here

View file

@ -31,7 +31,7 @@ export function normalizeAci(
if (!isAciString(result)) { if (!isAciString(result)) {
logger.warn( logger.warn(
`Normalizing invalid serviceId: ${rawAci} to ${result} in context "${context}"` `Normalizing invalid aci: ${rawAci} to ${result} in context "${context}"`
); );
// Cast anyway we don't want to throw here // Cast anyway we don't want to throw here

View file

@ -3379,10 +3379,10 @@
node-gyp-build "^4.2.3" node-gyp-build "^4.2.3"
uuid "^8.3.0" uuid "^8.3.0"
"@signalapp/mock-server@4.1.1": "@signalapp/mock-server@4.1.2":
version "4.1.1" version "4.1.2"
resolved "https://registry.yarnpkg.com/@signalapp/mock-server/-/mock-server-4.1.1.tgz#563a31a30cbefcb6c443a8fe7c77d9f20d3920db" resolved "https://registry.yarnpkg.com/@signalapp/mock-server/-/mock-server-4.1.2.tgz#3f497d4cc5cc6613d2a860173ee1d9cee24ce9cd"
integrity sha512-u+8BJK3Nl1Daw/I1J5ki4LtB99NvwSCUassEcTllWQppSg0wU0nxOwlDedMseyUvIhtUIePu2/nmysT1E3jRiw== integrity sha512-vOFJ8bVQdhII6ZGc34wurxJZ9roeoq4ch0VeorImcyavL5p7d9VbNwpWyOA/VAlfTaUgaiXegVmzK3t52lCQTw==
dependencies: dependencies:
"@signalapp/libsignal-client" "^0.30.2" "@signalapp/libsignal-client" "^0.30.2"
debug "^4.3.2" debug "^4.3.2"