Unlink on PNI identity key mismatch

This commit is contained in:
Fedor Indutny 2023-08-17 02:29:39 +02:00 committed by Jamie Kyle
parent ef0a3de636
commit 58aec8b1a3
3 changed files with 191 additions and 0 deletions

View file

@ -0,0 +1,134 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { ServiceIdKind } from '@signalapp/mock-server';
import {
IdentityKeyPair,
PrivateKey,
SignedPreKeyRecord,
KEMKeyPair,
KyberPreKeyRecord,
} from '@signalapp/libsignal-client';
import createDebug from 'debug';
import * as durations from '../../util/durations';
import { generatePni } from '../../types/ServiceId';
import { Bootstrap } from '../bootstrap';
import type { App } from '../bootstrap';
export const debug = createDebug('mock:test:pni-unlink');
describe('pnp/PNI DecryptionError unlink', function needsName() {
this.timeout(durations.MINUTE);
let bootstrap: Bootstrap;
let app: App | undefined;
beforeEach(async () => {
bootstrap = new Bootstrap({
contactCount: 0,
});
await bootstrap.init();
await bootstrap.linkAndClose();
});
afterEach(async function after() {
if (app) {
await bootstrap.maybeSaveLogs(this.currentTest, app);
await app.close();
}
await bootstrap.teardown();
});
it('unlinks desktop if PNI identity is different on server', async () => {
const { desktop, phone, server } = bootstrap;
const badIdentity = IdentityKeyPair.generate();
const signedPreKey = PrivateKey.generate();
const signedPreKeySig = badIdentity.privateKey.sign(
signedPreKey.getPublicKey().serialize()
);
const signedPreKeyRecord = SignedPreKeyRecord.new(
1000,
Date.now(),
signedPreKey.getPublicKey(),
signedPreKey,
signedPreKeySig
);
const kyberPreKey = KEMKeyPair.generate();
const kyberPreKeySig = badIdentity.privateKey.sign(
kyberPreKey.getPublicKey().serialize()
);
const kyberPreKeyRecord = KyberPreKeyRecord.new(
1001,
Date.now(),
kyberPreKey,
kyberPreKeySig
);
debug('corrupting PNI identity key');
const sendPromises = new Array<Promise<unknown>>();
const pniChangeNumber = {
identityKeyPair: badIdentity.serialize(),
registrationId: desktop.getRegistrationId(ServiceIdKind.PNI),
signedPreKey: signedPreKeyRecord.serialize(),
lastResortKyberPreKey: kyberPreKeyRecord.serialize(),
newE164: desktop.number,
};
// The goal of these two sync messages is to update Desktop's PNI identity
// key while keeping the PNI itself the same so that the Desktop wouldn't
// drop the PNI envelope from `sendText()` below.
sendPromises.push(
phone.sendRaw(
desktop,
{
syncMessage: {
pniChangeNumber,
},
},
{ timestamp: bootstrap.getTimestamp(), updatedPni: generatePni() }
)
);
sendPromises.push(
phone.sendRaw(
desktop,
{
syncMessage: {
pniChangeNumber,
},
},
{ timestamp: bootstrap.getTimestamp(), updatedPni: desktop.pni }
)
);
debug('sending a message to our PNI');
const stranger = await server.createPrimaryDevice({
profileName: 'Mysterious Stranger',
});
const ourKey = await desktop.popSingleUseKey(ServiceIdKind.PNI);
await stranger.addSingleUseKey(desktop, ourKey, ServiceIdKind.PNI);
sendPromises.push(
stranger.sendText(desktop, 'A message to PNI', {
serviceIdKind: ServiceIdKind.PNI,
withProfileKey: true,
timestamp: bootstrap.getTimestamp(),
})
);
debug('starting the app to process the queue');
app = await bootstrap.startApp();
await Promise.all(sendPromises);
const window = await app.getWindow();
await window.locator('.LeftPaneDialog__message >> "Unlinked"').waitFor();
});
});

View file

@ -139,6 +139,7 @@ import { inspectUnknownFieldTags } from '../util/inspectProtobufs';
import { incrementMessageCounter } from '../util/incrementMessageCounter';
import { filterAndClean } from '../types/BodyRange';
import { getCallEventForProto } from '../util/callDisposition';
import { checkOurPniIdentityKey } from '../util/checkOurPniIdentityKey';
import { CallLogEvent } from '../types/CallDisposition';
const GROUPV2_ID_LENGTH = 32;
@ -298,6 +299,8 @@ export default class MessageReceiver
private stoppingProcessing?: boolean;
private pniIdentityKeyCheckRequired?: boolean;
constructor({ server, storage, serverTrustRoot }: MessageReceiverOptions) {
super();
@ -739,6 +742,15 @@ export default class MessageReceiver
this.cacheRemoveBatcher.flushAndWait(),
]);
if (this.pniIdentityKeyCheckRequired) {
log.warn(
"MessageReceiver: got 'empty' event, " +
'running scheduled pni identity key check'
);
drop(checkOurPniIdentityKey());
}
this.pniIdentityKeyCheckRequired = false;
log.info("MessageReceiver: emitting 'empty' event");
this.dispatchEvent(new EmptyEvent());
this.isEmptied = true;
@ -2033,6 +2045,16 @@ export default class MessageReceiver
throw error;
}
if (serviceIdKind === ServiceIdKind.PNI) {
log.info(
'MessageReceiver.decrypt: Error on PNI; no further processing; ' +
'queueing pni identity check'
);
this.pniIdentityKeyCheckRequired = true;
this.removeFromCache(envelope);
throw error;
}
const { cipherTextBytes, cipherTextType } = envelope;
const event = new DecryptionErrorEvent(
{
@ -3319,6 +3341,11 @@ export default class MessageReceiver
return;
}
if (this.pniIdentityKeyCheckRequired) {
log.warn('MessageReceiver: canceling pni identity key check');
}
this.pniIdentityKeyCheckRequired = false;
const manager = window.getAccountManager();
await manager.setPni(updatedPni, {
identityKeyPair,

View file

@ -0,0 +1,30 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as log from '../logging/log';
import { constantTimeEqual } from '../Crypto';
import { strictAssert } from './assert';
export async function checkOurPniIdentityKey(): Promise<void> {
const { server } = window.textsecure;
strictAssert(server, 'WebAPI not ready');
const ourPni = window.storage.user.getCheckedPni();
const localKeyPair = await window.storage.protocol.getIdentityKeyPair(ourPni);
if (!localKeyPair) {
log.warn(
`checkOurPniIdentityKey: no local key pair for ${ourPni}, unlinking`
);
window.Whisper.events.trigger('unlinkAndDisconnect');
return;
}
const { identityKey: remoteKey } = await server.getKeysForServiceId(ourPni);
if (!constantTimeEqual(localKeyPair.pubKey, remoteKey)) {
log.warn(
`checkOurPniIdentityKey: local/remote key mismatch for ${ourPni}, unlinking`
);
window.Whisper.events.trigger('unlinkAndDisconnect');
}
}