Skip to content

Commit 658a63c

Browse files
Introduce kyber pre key triple table
1 parent af55cf4 commit 658a63c

File tree

8 files changed

+178
-15
lines changed

8 files changed

+178
-15
lines changed

ts/LibSignalStores.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,10 +250,14 @@ export class KyberPreKeys extends KyberPreKeyStore {
250250
return kyberPreKey;
251251
}
252252

253-
async markKyberPreKeyUsed(id: number): Promise<void> {
253+
async markKyberPreKeyUsed(
254+
keyId: number,
255+
signedPreKeyId: number,
256+
baseKey: PublicKey
257+
): Promise<void> {
254258
await window.textsecure.storage.protocol.maybeRemoveKyberPreKey(
255259
this.#ourServiceId,
256-
id,
260+
{ keyId, signedPreKeyId, baseKey },
257261
{ zone: this.#zone }
258262
);
259263
}

ts/SignalProtocolStore.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -503,7 +503,11 @@ export class SignalProtocolStore extends EventEmitter {
503503

504504
async maybeRemoveKyberPreKey(
505505
ourServiceId: ServiceIdString,
506-
keyId: number,
506+
{
507+
keyId,
508+
signedPreKeyId,
509+
baseKey,
510+
}: { keyId: number; signedPreKeyId: number; baseKey: PublicKey },
507511
{ zone = GLOBAL_ZONE }: SessionTransactionOptions = {}
508512
): Promise<void> {
509513
const id: PreKeyIdType = this.#_getKeyId(ourServiceId, keyId);
@@ -512,14 +516,23 @@ export class SignalProtocolStore extends EventEmitter {
512516
if (!entry) {
513517
return;
514518
}
515-
if (entry.fromDB.isLastResort) {
516-
log.info(
517-
`maybeRemoveKyberPreKey: Not removing kyber prekey ${id}; it's a last resort key`
518-
);
519+
if (!entry.fromDB.isLastResort) {
520+
await this.removeKyberPreKeys(ourServiceId, [keyId], { zone });
519521
return;
520522
}
521523

522-
await this.removeKyberPreKeys(ourServiceId, [keyId], { zone });
524+
log.info(
525+
`maybeRemoveKyberPreKey: Not removing kyber prekey ${id}; it's a last resort key`
526+
);
527+
528+
const result = await DataWriter.markKyberTripleSeenOrFail({
529+
id: `${ourServiceId}:${keyId}`,
530+
signedPreKeyId,
531+
baseKey: baseKey.serialize(),
532+
});
533+
if (result === 'fail') {
534+
throw new Error(`Duplicate kyber triple ${keyId}:${signedPreKeyId}`);
535+
}
523536
}
524537

525538
async removeKyberPreKeys(

ts/sql/Interface.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,12 @@ export type MediaItemDBType = Readonly<{
590590
message: MediaItemMessageType;
591591
}>;
592592

593+
export type KyberPreKeyTripleType = Readonly<{
594+
id: PreKeyIdType;
595+
signedPreKeyId: number;
596+
baseKey: Uint8Array;
597+
}>;
598+
593599
export const MESSAGE_ATTACHMENT_COLUMNS = [
594600
'messageId',
595601
'conversationId',
@@ -956,6 +962,9 @@ type WritableInterface = {
956962
removeKyberPreKeyById: (id: PreKeyIdType | Array<PreKeyIdType>) => number;
957963
removeKyberPreKeysByServiceId: (serviceId: ServiceIdString) => void;
958964
removeAllKyberPreKeys: () => number;
965+
markKyberTripleSeenOrFail: (
966+
options: KyberPreKeyTripleType
967+
) => 'seen' | 'fail';
959968

960969
removePreKeyById: (id: PreKeyIdType | Array<PreKeyIdType>) => number;
961970
removePreKeysByServiceId: (serviceId: ServiceIdString) => void;

ts/sql/Server.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ import type {
140140
GetUnreadByConversationAndMarkReadResultType,
141141
IdentityKeyIdType,
142142
ItemKeyType,
143+
KyberPreKeyTripleType,
143144
MediaItemDBType,
144145
MessageAttachmentsCursorType,
145146
MessageCursorType,
@@ -531,6 +532,7 @@ export const DataWriter: ServerWritableInterface = {
531532
bulkAddKyberPreKeys,
532533
removeKyberPreKeyById,
533534
removeKyberPreKeysByServiceId,
535+
markKyberTripleSeenOrFail,
534536
removeAllKyberPreKeys,
535537

536538
createOrUpdatePreKey,
@@ -1075,6 +1077,34 @@ function removeKyberPreKeysByServiceId(
10751077
serviceId,
10761078
});
10771079
}
1080+
function markKyberTripleSeenOrFail(
1081+
db: WritableDB,
1082+
{ id, signedPreKeyId, baseKey }: KyberPreKeyTripleType
1083+
): 'seen' | 'fail' {
1084+
// Notes that `kyberPreKey_triples` has
1085+
// - Unique constraint on id, signedPreKeyId, baseKey so that we can't insert
1086+
// two identical rows
1087+
// - `ON DELETE CASCADE` trigger linked to `kyberPreKeys` table so that we
1088+
// cleanup the triples whenever we remove the key
1089+
const [query, parameters] = sql`
1090+
INSERT OR FAIL INTO kyberPreKey_triples
1091+
(id, signedPreKeyId, baseKey)
1092+
VALUES
1093+
(${id}, ${signedPreKeyId}, ${baseKey});
1094+
`;
1095+
1096+
try {
1097+
db.prepare(query).run(parameters);
1098+
return 'seen';
1099+
} catch (error) {
1100+
if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') {
1101+
return 'fail';
1102+
}
1103+
1104+
// Unexpected error
1105+
throw error;
1106+
}
1107+
}
10781108
function removeAllKyberPreKeys(db: WritableDB): number {
10791109
return removeAllFromTable(db, KYBER_PRE_KEYS_TABLE);
10801110
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright 2025 Signal Messenger, LLC
2+
// SPDX-License-Identifier: AGPL-3.0-only
3+
4+
import type { WritableDB } from '../Interface.js';
5+
6+
export default function updateToSchemaVersion1460(db: WritableDB): void {
7+
db.exec(`
8+
CREATE TABLE kyberPreKey_triples (
9+
id TEXT NOT NULL REFERENCES kyberPreKeys(id) ON DELETE CASCADE,
10+
signedPreKeyId INTEGER NOT NULL,
11+
baseKey BLOB NOT NULL,
12+
UNIQUE(id, signedPreKeyId, baseKey) ON CONFLICT FAIL
13+
) STRICT;
14+
`);
15+
}

ts/sql/migrations/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ import updateToSchemaVersion1430 from './1430-call-links-epoch-id.js';
122122
import updateToSchemaVersion1440 from './1440-chat-folders.js';
123123
import updateToSchemaVersion1450 from './1450-all-media.js';
124124
import updateToSchemaVersion1460 from './1460-attachment-duration.js';
125+
import updateToSchemaVersion1470 from './1470-kyber-triple.js';
125126

126127
import { DataWriter } from '../Server.js';
127128

@@ -1601,6 +1602,7 @@ export const SCHEMA_VERSIONS: ReadonlyArray<SchemaUpdateType> = [
16011602
{ version: 1440, update: updateToSchemaVersion1440 },
16021603
{ version: 1450, update: updateToSchemaVersion1450 },
16031604
{ version: 1460, update: updateToSchemaVersion1460 },
1605+
{ version: 1470, update: updateToSchemaVersion1470 },
16041606
];
16051607

16061608
export class DBVersionFromFutureError extends Error {

ts/test-electron/SignalProtocolStore_test.ts

Lines changed: 96 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
clampPrivateKey,
2828
setPublicKeyTypeByte,
2929
generateSignedPreKey,
30+
generateKyberPreKey,
3031
} from '../Curve.js';
3132
import type { SignalProtocolStore } from '../SignalProtocolStore.js';
3233
import { GLOBAL_ZONE } from '../SignalProtocolStore.js';
@@ -597,12 +598,9 @@ describe('SignalProtocolStore', () => {
597598
});
598599

599600
async function testInvalidAttributes() {
600-
try {
601-
await store.saveIdentityWithAttributes(theirAci, attributes);
602-
throw new Error('saveIdentityWithAttributes should have failed');
603-
} catch (error) {
604-
// good. we expect to fail with invalid attributes.
605-
}
601+
await assert.isRejected(
602+
store.saveIdentityWithAttributes(theirAci, attributes)
603+
);
606604
}
607605

608606
it('rejects an invalid publicKey', async () => {
@@ -1637,4 +1635,96 @@ describe('SignalProtocolStore', () => {
16371635
// Note: signature is ignored.
16381636
});
16391637
});
1638+
1639+
describe('maybeRemoveKyberPreKey', () => {
1640+
beforeEach(async () => {
1641+
await store.clearKyberPreKeyStore();
1642+
});
1643+
1644+
afterEach(async () => {
1645+
await store.clearKyberPreKeyStore();
1646+
});
1647+
1648+
it('should detect duplicate triples', async () => {
1649+
await store.storeKyberPreKeys(ourAci, [
1650+
{
1651+
createdAt: Date.now(),
1652+
data: generateKyberPreKey(identityKey, 1).serialize(),
1653+
isConfirmed: true,
1654+
isLastResort: true,
1655+
keyId: 1,
1656+
ourServiceId: ourAci,
1657+
},
1658+
]);
1659+
1660+
await store.maybeRemoveKyberPreKey(ourAci, {
1661+
keyId: 1,
1662+
signedPreKeyId: 1,
1663+
baseKey: testKey.publicKey,
1664+
});
1665+
1666+
await assert.isRejected(
1667+
store.maybeRemoveKyberPreKey(ourAci, {
1668+
keyId: 1,
1669+
signedPreKeyId: 1,
1670+
baseKey: testKey.publicKey,
1671+
}),
1672+
'Duplicate kyber triple 1:1'
1673+
);
1674+
});
1675+
1676+
it('should ignore triples for non last resort keys', async () => {
1677+
await store.storeKyberPreKeys(ourAci, [
1678+
{
1679+
createdAt: Date.now(),
1680+
data: generateKyberPreKey(identityKey, 1).serialize(),
1681+
isConfirmed: true,
1682+
isLastResort: false,
1683+
keyId: 1,
1684+
ourServiceId: ourAci,
1685+
},
1686+
]);
1687+
1688+
await store.maybeRemoveKyberPreKey(ourAci, {
1689+
keyId: 1,
1690+
signedPreKeyId: 1,
1691+
baseKey: testKey.publicKey,
1692+
});
1693+
1694+
// this should not throw since the key was not last resort
1695+
await store.maybeRemoveKyberPreKey(ourAci, {
1696+
keyId: 1,
1697+
signedPreKeyId: 1,
1698+
baseKey: testKey.publicKey,
1699+
});
1700+
});
1701+
1702+
it('should remove triples when removing the key', async () => {
1703+
await store.storeKyberPreKeys(ourAci, [
1704+
{
1705+
createdAt: Date.now(),
1706+
data: generateKyberPreKey(identityKey, 1).serialize(),
1707+
isConfirmed: true,
1708+
isLastResort: true,
1709+
keyId: 1,
1710+
ourServiceId: ourAci,
1711+
},
1712+
]);
1713+
1714+
await store.maybeRemoveKyberPreKey(ourAci, {
1715+
keyId: 1,
1716+
signedPreKeyId: 1,
1717+
baseKey: testKey.publicKey,
1718+
});
1719+
1720+
await store.removeKyberPreKeys(ourAci, [1]);
1721+
1722+
// this should not throw since we removed the key
1723+
await store.maybeRemoveKyberPreKey(ourAci, {
1724+
keyId: 1,
1725+
signedPreKeyId: 1,
1726+
baseKey: testKey.publicKey,
1727+
});
1728+
});
1729+
});
16401730
});

ts/textsecure/WebsocketResources.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ export function connectAuthenticatedLibsignal({
363363
const listener: LibsignalWebSocketResourceHolder & ChatServiceListener = {
364364
resource: undefined,
365365
onIncomingMessage(
366-
envelope: Buffer,
366+
envelope: Uint8Array,
367367
timestamp: number,
368368
ack: ChatServerMessageAck
369369
): void {

0 commit comments

Comments
 (0)