Skip to content

Commit c25ba3f

Browse files
Add transaction support to kyber triples
1 parent b2f1639 commit c25ba3f

File tree

3 files changed

+88
-18
lines changed

3 files changed

+88
-18
lines changed

ts/SignalProtocolStore.ts

Lines changed: 72 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
} from '@signalapp/libsignal-client';
2121

2222
import { DataReader, DataWriter } from './sql/Client.js';
23-
import type { ItemType } from './sql/Interface.js';
23+
import type { ItemType, KyberPreKeyTripleType } from './sql/Interface.js';
2424
import * as Bytes from './Bytes.js';
2525
import { constantTimeEqual, sha256 } from './Crypto.js';
2626
import { assertDev, strictAssert } from './util/assert.js';
@@ -213,6 +213,19 @@ export function hydrateSignedPreKey(
213213
);
214214
}
215215

216+
// Format: keyId:signedPreKeyId:baseKey
217+
type KyberTripleCacheKeyPrefixType = `${KyberPreKeyTripleType['id']}:`;
218+
type KyberTripleCacheKeyType =
219+
`${KyberTripleCacheKeyPrefixType}${KyberPreKeyTripleType['signedPreKeyId']}:${string}`;
220+
221+
function getKyberTripleCacheKey({
222+
id,
223+
signedPreKeyId,
224+
baseKey,
225+
}: KyberPreKeyTripleType): KyberTripleCacheKeyType {
226+
return `${id}:${signedPreKeyId}:${Bytes.toHex(baseKey)}`;
227+
}
228+
216229
type SessionCacheEntry = CacheEntryType<SessionType, SessionRecord>;
217230
type SenderKeyCacheEntry = CacheEntryType<SenderKeyType, SenderKeyRecord>;
218231

@@ -254,6 +267,8 @@ export class SignalProtocolStore extends EventEmitter {
254267
CacheEntryType<SignedPreKeyType, SignedPreKeyRecord>
255268
>;
256269

270+
readonly #kyberTriples = new Set<KyberTripleCacheKeyType>();
271+
257272
senderKeyQueues = new Map<QualifiedAddressStringType, PQueue>();
258273

259274
sessionQueues = new Map<SessionIdType, PQueue>();
@@ -267,6 +282,10 @@ export class SignalProtocolStore extends EventEmitter {
267282
#pendingSessions = new Map<SessionIdType, SessionCacheEntry>();
268283
#pendingSenderKeys = new Map<SenderKeyIdType, SenderKeyCacheEntry>();
269284
#pendingUnprocessed = new Map<string, UnprocessedType>();
285+
#pendingKyberTriples = new Map<
286+
KyberTripleCacheKeyType,
287+
KyberPreKeyTripleType
288+
>();
270289

271290
async hydrateCaches(): Promise<void> {
272291
await Promise.all([
@@ -310,6 +329,15 @@ export class SignalProtocolStore extends EventEmitter {
310329
this.#ourRegistrationIds.set(serviceId, map.value[serviceId]);
311330
}
312331
})(),
332+
(async () => {
333+
this.#kyberTriples.clear();
334+
335+
const triples = await DataReader.getAllKyberTriples();
336+
337+
for (const t of triples) {
338+
this.#kyberTriples.add(getKyberTripleCacheKey(t));
339+
}
340+
})(),
313341
_fillCaches<string, IdentityKeyType, PublicKey>(
314342
this,
315343
'identityKeys',
@@ -525,14 +553,30 @@ export class SignalProtocolStore extends EventEmitter {
525553
`maybeRemoveKyberPreKey: Not removing kyber prekey ${id}; it's a last resort key`
526554
);
527555

528-
const result = await DataWriter.markKyberTripleSeenOrFail({
529-
id: `${ourServiceId}:${keyId}`,
530-
signedPreKeyId,
531-
baseKey: baseKey.serialize(),
556+
await this.withZone(zone, 'maybeRemoveKyberPreKey', async () => {
557+
const triple: KyberPreKeyTripleType = {
558+
id: `${ourServiceId}:${keyId}`,
559+
signedPreKeyId,
560+
baseKey: baseKey.serialize(),
561+
};
562+
563+
const cacheKey = getKyberTripleCacheKey(triple);
564+
565+
// Note: we don't have to check for `#pendingKyberPreKeysToRemove` since
566+
// it makes the key in question inaccessible to begin with.
567+
if (
568+
this.#kyberTriples.has(cacheKey) ||
569+
this.#pendingKyberTriples.has(cacheKey)
570+
) {
571+
throw new Error(`Duplicate kyber triple ${keyId}:${signedPreKeyId}`);
572+
}
573+
574+
this.#pendingKyberTriples.set(cacheKey, triple);
575+
576+
if (!zone.supportsPendingKyberPreKeysToRemove()) {
577+
await this.#commitZoneChanges('removeKyberPreKeys');
578+
}
532579
});
533-
if (result === 'fail') {
534-
throw new Error(`Duplicate kyber triple ${keyId}:${signedPreKeyId}`);
535-
}
536580
}
537581

538582
async removeKyberPreKeys(
@@ -1169,13 +1213,15 @@ export class SignalProtocolStore extends EventEmitter {
11691213
const pendingSenderKeys = this.#pendingSenderKeys;
11701214
const pendingSessions = this.#pendingSessions;
11711215
const pendingUnprocessed = this.#pendingUnprocessed;
1216+
const pendingKyberTriples = this.#pendingKyberTriples;
11721217

11731218
if (
11741219
pendingKyberPreKeysToRemove.size === 0 &&
11751220
pendingPreKeysToRemove.size === 0 &&
11761221
pendingSenderKeys.size === 0 &&
11771222
pendingSessions.size === 0 &&
1178-
pendingUnprocessed.size === 0
1223+
pendingUnprocessed.size === 0 &&
1224+
pendingKyberTriples.size === 0
11791225
) {
11801226
return;
11811227
}
@@ -1186,14 +1232,16 @@ export class SignalProtocolStore extends EventEmitter {
11861232
`pending preKeysToRemove ${pendingKyberPreKeysToRemove.size}, ` +
11871233
`pending senderKeys ${pendingSenderKeys.size}, ` +
11881234
`pending sessions ${pendingSessions.size}, ` +
1189-
`pending unprocessed ${pendingUnprocessed.size}`
1235+
`pending unprocessed ${pendingUnprocessed.size}, ` +
1236+
`pending kyberTriples ${pendingKyberTriples.size}`
11901237
);
11911238

11921239
this.#pendingKyberPreKeysToRemove = new Set();
11931240
this.#pendingPreKeysToRemove = new Set();
11941241
this.#pendingSenderKeys = new Map();
11951242
this.#pendingSessions = new Map();
11961243
this.#pendingUnprocessed = new Map();
1244+
this.#pendingKyberTriples = new Map();
11971245

11981246
// Commit both sender keys, sessions and unprocessed in the same database transaction
11991247
// to unroll both on error.
@@ -1207,17 +1255,30 @@ export class SignalProtocolStore extends EventEmitter {
12071255
({ fromDB }) => fromDB
12081256
),
12091257
unprocessed: Array.from(pendingUnprocessed.values()),
1258+
kyberTriples: Array.from(pendingKyberTriples.values()),
12101259
});
12111260

12121261
// Apply changes to in-memory storage after successful DB write.
12131262

1263+
for (const cacheKey of pendingKyberTriples.keys()) {
1264+
this.#kyberTriples.add(cacheKey);
1265+
}
1266+
12141267
const { kyberPreKeys } = this;
12151268
assertDev(
12161269
kyberPreKeys !== undefined,
12171270
"Can't commit unhydrated kyberPreKeys storage"
12181271
);
1219-
pendingKyberPreKeysToRemove.forEach(value => {
1272+
pendingKyberPreKeysToRemove.forEach((value: PreKeyIdType) => {
12201273
kyberPreKeys.delete(value);
1274+
1275+
// Remove all cached kyber triples for this key.
1276+
const prefix: KyberTripleCacheKeyPrefixType = `${value}:`;
1277+
for (const key of this.#kyberTriples.keys()) {
1278+
if (key.startsWith(prefix)) {
1279+
this.#kyberTriples.delete(key);
1280+
}
1281+
}
12211282
});
12221283
if (kyberPreKeys.size < LOW_KEYS_THRESHOLD) {
12231284
this.#emitLowKeys(`removeKyberPreKeys@${kyberPreKeys.size}`);

ts/sql/Interface.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,8 @@ type ReadableInterface = {
738738

739739
getAllSessions: () => Array<SessionType>;
740740

741+
getAllKyberTriples: () => Array<KyberPreKeyTripleType>;
742+
741743
getConversationCount: () => number;
742744
getConversationById: (id: string) => ConversationType | undefined;
743745

@@ -962,9 +964,6 @@ type WritableInterface = {
962964
removeKyberPreKeyById: (id: PreKeyIdType | Array<PreKeyIdType>) => number;
963965
removeKyberPreKeysByServiceId: (serviceId: ServiceIdString) => void;
964966
removeAllKyberPreKeys: () => number;
965-
markKyberTripleSeenOrFail: (
966-
options: KyberPreKeyTripleType
967-
) => 'seen' | 'fail';
968967

969968
removePreKeyById: (id: PreKeyIdType | Array<PreKeyIdType>) => number;
970969
removePreKeysByServiceId: (serviceId: ServiceIdString) => void;
@@ -1017,6 +1016,7 @@ type WritableInterface = {
10171016
senderKeys: Array<SenderKeyType>;
10181017
sessions: Array<SessionType>;
10191018
unprocessed: Array<UnprocessedType>;
1019+
kyberTriples: Array<KyberPreKeyTripleType>;
10201020
}): void;
10211021
removeSessionById: (id: SessionIdType) => number;
10221022
removeSessionsByConversation: (conversationId: string) => void;

ts/sql/Server.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,8 @@ export const DataReader: ServerReadableInterface = {
397397

398398
getAllSessions,
399399

400+
getAllKyberTriples,
401+
400402
getConversationCount,
401403
getConversationById,
402404

@@ -532,7 +534,6 @@ export const DataWriter: ServerWritableInterface = {
532534
bulkAddKyberPreKeys,
533535
removeKyberPreKeyById,
534536
removeKyberPreKeysByServiceId,
535-
markKyberTripleSeenOrFail,
536537
removeAllKyberPreKeys,
537538

538539
createOrUpdatePreKey,
@@ -1080,7 +1081,7 @@ function removeKyberPreKeysByServiceId(
10801081
function markKyberTripleSeenOrFail(
10811082
db: WritableDB,
10821083
{ id, signedPreKeyId, baseKey }: KyberPreKeyTripleType
1083-
): 'seen' | 'fail' {
1084+
): void {
10841085
// Notes that `kyberPreKey_triples` has
10851086
// - Unique constraint on id, signedPreKeyId, baseKey so that we can't insert
10861087
// two identical rows
@@ -1095,10 +1096,9 @@ function markKyberTripleSeenOrFail(
10951096

10961097
try {
10971098
db.prepare(query).run(parameters);
1098-
return 'seen';
10991099
} catch (error) {
11001100
if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') {
1101-
return 'fail';
1101+
throw new Error(`Duplicate kyber triple ${id}:${signedPreKeyId}`);
11021102
}
11031103

11041104
// Unexpected error
@@ -1699,12 +1699,14 @@ function commitDecryptResult(
16991699
senderKeys,
17001700
sessions,
17011701
unprocessed,
1702+
kyberTriples,
17021703
}: {
17031704
kyberPreKeysToRemove: Array<PreKeyIdType>;
17041705
preKeysToRemove: Array<PreKeyIdType>;
17051706
senderKeys: Array<SenderKeyType>;
17061707
sessions: Array<SessionType>;
17071708
unprocessed: Array<UnprocessedType>;
1709+
kyberTriples: Array<KyberPreKeyTripleType>;
17081710
}
17091711
): void {
17101712
db.transaction(() => {
@@ -1745,6 +1747,10 @@ function commitDecryptResult(
17451747
for (const item of unprocessed) {
17461748
saveUnprocessed(db, item);
17471749
}
1750+
1751+
for (const item of kyberTriples) {
1752+
markKyberTripleSeenOrFail(db, item);
1753+
}
17481754
})();
17491755
}
17501756

@@ -1783,6 +1789,9 @@ function removeAllSessions(db: WritableDB): number {
17831789
function getAllSessions(db: ReadableDB): Array<SessionType> {
17841790
return db.prepare('SELECT * FROM sessions').all();
17851791
}
1792+
function getAllKyberTriples(db: ReadableDB): Array<KyberPreKeyTripleType> {
1793+
return db.prepare('SELECT * FROM kyberPreKey_triples').all();
1794+
}
17861795
// Conversations
17871796

17881797
function getConversationCount(db: ReadableDB): number {

0 commit comments

Comments
 (0)