Introduce kyber pre key triple table
This commit is contained in:
parent
af55cf4682
commit
658a63cfe6
8 changed files with 178 additions and 15 deletions
|
|
@ -250,10 +250,14 @@ export class KyberPreKeys extends KyberPreKeyStore {
|
||||||
return kyberPreKey;
|
return kyberPreKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
async markKyberPreKeyUsed(id: number): Promise<void> {
|
async markKyberPreKeyUsed(
|
||||||
|
keyId: number,
|
||||||
|
signedPreKeyId: number,
|
||||||
|
baseKey: PublicKey
|
||||||
|
): Promise<void> {
|
||||||
await window.textsecure.storage.protocol.maybeRemoveKyberPreKey(
|
await window.textsecure.storage.protocol.maybeRemoveKyberPreKey(
|
||||||
this.#ourServiceId,
|
this.#ourServiceId,
|
||||||
id,
|
{ keyId, signedPreKeyId, baseKey },
|
||||||
{ zone: this.#zone }
|
{ zone: this.#zone }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -503,7 +503,11 @@ export class SignalProtocolStore extends EventEmitter {
|
||||||
|
|
||||||
async maybeRemoveKyberPreKey(
|
async maybeRemoveKyberPreKey(
|
||||||
ourServiceId: ServiceIdString,
|
ourServiceId: ServiceIdString,
|
||||||
keyId: number,
|
{
|
||||||
|
keyId,
|
||||||
|
signedPreKeyId,
|
||||||
|
baseKey,
|
||||||
|
}: { keyId: number; signedPreKeyId: number; baseKey: PublicKey },
|
||||||
{ zone = GLOBAL_ZONE }: SessionTransactionOptions = {}
|
{ zone = GLOBAL_ZONE }: SessionTransactionOptions = {}
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const id: PreKeyIdType = this.#_getKeyId(ourServiceId, keyId);
|
const id: PreKeyIdType = this.#_getKeyId(ourServiceId, keyId);
|
||||||
|
|
@ -512,14 +516,23 @@ export class SignalProtocolStore extends EventEmitter {
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (entry.fromDB.isLastResort) {
|
if (!entry.fromDB.isLastResort) {
|
||||||
log.info(
|
await this.removeKyberPreKeys(ourServiceId, [keyId], { zone });
|
||||||
`maybeRemoveKyberPreKey: Not removing kyber prekey ${id}; it's a last resort key`
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.removeKyberPreKeys(ourServiceId, [keyId], { zone });
|
log.info(
|
||||||
|
`maybeRemoveKyberPreKey: Not removing kyber prekey ${id}; it's a last resort key`
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await DataWriter.markKyberTripleSeenOrFail({
|
||||||
|
id: `${ourServiceId}:${keyId}`,
|
||||||
|
signedPreKeyId,
|
||||||
|
baseKey: baseKey.serialize(),
|
||||||
|
});
|
||||||
|
if (result === 'fail') {
|
||||||
|
throw new Error(`Duplicate kyber triple ${keyId}:${signedPreKeyId}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeKyberPreKeys(
|
async removeKyberPreKeys(
|
||||||
|
|
|
||||||
|
|
@ -590,6 +590,12 @@ export type MediaItemDBType = Readonly<{
|
||||||
message: MediaItemMessageType;
|
message: MediaItemMessageType;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
export type KyberPreKeyTripleType = Readonly<{
|
||||||
|
id: PreKeyIdType;
|
||||||
|
signedPreKeyId: number;
|
||||||
|
baseKey: Uint8Array;
|
||||||
|
}>;
|
||||||
|
|
||||||
export const MESSAGE_ATTACHMENT_COLUMNS = [
|
export const MESSAGE_ATTACHMENT_COLUMNS = [
|
||||||
'messageId',
|
'messageId',
|
||||||
'conversationId',
|
'conversationId',
|
||||||
|
|
@ -956,6 +962,9 @@ type WritableInterface = {
|
||||||
removeKyberPreKeyById: (id: PreKeyIdType | Array<PreKeyIdType>) => number;
|
removeKyberPreKeyById: (id: PreKeyIdType | Array<PreKeyIdType>) => number;
|
||||||
removeKyberPreKeysByServiceId: (serviceId: ServiceIdString) => void;
|
removeKyberPreKeysByServiceId: (serviceId: ServiceIdString) => void;
|
||||||
removeAllKyberPreKeys: () => number;
|
removeAllKyberPreKeys: () => number;
|
||||||
|
markKyberTripleSeenOrFail: (
|
||||||
|
options: KyberPreKeyTripleType
|
||||||
|
) => 'seen' | 'fail';
|
||||||
|
|
||||||
removePreKeyById: (id: PreKeyIdType | Array<PreKeyIdType>) => number;
|
removePreKeyById: (id: PreKeyIdType | Array<PreKeyIdType>) => number;
|
||||||
removePreKeysByServiceId: (serviceId: ServiceIdString) => void;
|
removePreKeysByServiceId: (serviceId: ServiceIdString) => void;
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,7 @@ import type {
|
||||||
GetUnreadByConversationAndMarkReadResultType,
|
GetUnreadByConversationAndMarkReadResultType,
|
||||||
IdentityKeyIdType,
|
IdentityKeyIdType,
|
||||||
ItemKeyType,
|
ItemKeyType,
|
||||||
|
KyberPreKeyTripleType,
|
||||||
MediaItemDBType,
|
MediaItemDBType,
|
||||||
MessageAttachmentsCursorType,
|
MessageAttachmentsCursorType,
|
||||||
MessageCursorType,
|
MessageCursorType,
|
||||||
|
|
@ -531,6 +532,7 @@ export const DataWriter: ServerWritableInterface = {
|
||||||
bulkAddKyberPreKeys,
|
bulkAddKyberPreKeys,
|
||||||
removeKyberPreKeyById,
|
removeKyberPreKeyById,
|
||||||
removeKyberPreKeysByServiceId,
|
removeKyberPreKeysByServiceId,
|
||||||
|
markKyberTripleSeenOrFail,
|
||||||
removeAllKyberPreKeys,
|
removeAllKyberPreKeys,
|
||||||
|
|
||||||
createOrUpdatePreKey,
|
createOrUpdatePreKey,
|
||||||
|
|
@ -1075,6 +1077,34 @@ function removeKyberPreKeysByServiceId(
|
||||||
serviceId,
|
serviceId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
function markKyberTripleSeenOrFail(
|
||||||
|
db: WritableDB,
|
||||||
|
{ id, signedPreKeyId, baseKey }: KyberPreKeyTripleType
|
||||||
|
): 'seen' | 'fail' {
|
||||||
|
// Notes that `kyberPreKey_triples` has
|
||||||
|
// - Unique constraint on id, signedPreKeyId, baseKey so that we can't insert
|
||||||
|
// two identical rows
|
||||||
|
// - `ON DELETE CASCADE` trigger linked to `kyberPreKeys` table so that we
|
||||||
|
// cleanup the triples whenever we remove the key
|
||||||
|
const [query, parameters] = sql`
|
||||||
|
INSERT OR FAIL INTO kyberPreKey_triples
|
||||||
|
(id, signedPreKeyId, baseKey)
|
||||||
|
VALUES
|
||||||
|
(${id}, ${signedPreKeyId}, ${baseKey});
|
||||||
|
`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
db.prepare(query).run(parameters);
|
||||||
|
return 'seen';
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') {
|
||||||
|
return 'fail';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unexpected error
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
function removeAllKyberPreKeys(db: WritableDB): number {
|
function removeAllKyberPreKeys(db: WritableDB): number {
|
||||||
return removeAllFromTable(db, KYBER_PRE_KEYS_TABLE);
|
return removeAllFromTable(db, KYBER_PRE_KEYS_TABLE);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
15
ts/sql/migrations/1470-kyber-triple.ts
Normal file
15
ts/sql/migrations/1470-kyber-triple.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
// Copyright 2025 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import type { WritableDB } from '../Interface.js';
|
||||||
|
|
||||||
|
export default function updateToSchemaVersion1460(db: WritableDB): void {
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE kyberPreKey_triples (
|
||||||
|
id TEXT NOT NULL REFERENCES kyberPreKeys(id) ON DELETE CASCADE,
|
||||||
|
signedPreKeyId INTEGER NOT NULL,
|
||||||
|
baseKey BLOB NOT NULL,
|
||||||
|
UNIQUE(id, signedPreKeyId, baseKey) ON CONFLICT FAIL
|
||||||
|
) STRICT;
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
@ -122,6 +122,7 @@ import updateToSchemaVersion1430 from './1430-call-links-epoch-id.js';
|
||||||
import updateToSchemaVersion1440 from './1440-chat-folders.js';
|
import updateToSchemaVersion1440 from './1440-chat-folders.js';
|
||||||
import updateToSchemaVersion1450 from './1450-all-media.js';
|
import updateToSchemaVersion1450 from './1450-all-media.js';
|
||||||
import updateToSchemaVersion1460 from './1460-attachment-duration.js';
|
import updateToSchemaVersion1460 from './1460-attachment-duration.js';
|
||||||
|
import updateToSchemaVersion1470 from './1470-kyber-triple.js';
|
||||||
|
|
||||||
import { DataWriter } from '../Server.js';
|
import { DataWriter } from '../Server.js';
|
||||||
|
|
||||||
|
|
@ -1601,6 +1602,7 @@ export const SCHEMA_VERSIONS: ReadonlyArray<SchemaUpdateType> = [
|
||||||
{ version: 1440, update: updateToSchemaVersion1440 },
|
{ version: 1440, update: updateToSchemaVersion1440 },
|
||||||
{ version: 1450, update: updateToSchemaVersion1450 },
|
{ version: 1450, update: updateToSchemaVersion1450 },
|
||||||
{ version: 1460, update: updateToSchemaVersion1460 },
|
{ version: 1460, update: updateToSchemaVersion1460 },
|
||||||
|
{ version: 1470, update: updateToSchemaVersion1470 },
|
||||||
];
|
];
|
||||||
|
|
||||||
export class DBVersionFromFutureError extends Error {
|
export class DBVersionFromFutureError extends Error {
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import {
|
||||||
clampPrivateKey,
|
clampPrivateKey,
|
||||||
setPublicKeyTypeByte,
|
setPublicKeyTypeByte,
|
||||||
generateSignedPreKey,
|
generateSignedPreKey,
|
||||||
|
generateKyberPreKey,
|
||||||
} from '../Curve.js';
|
} from '../Curve.js';
|
||||||
import type { SignalProtocolStore } from '../SignalProtocolStore.js';
|
import type { SignalProtocolStore } from '../SignalProtocolStore.js';
|
||||||
import { GLOBAL_ZONE } from '../SignalProtocolStore.js';
|
import { GLOBAL_ZONE } from '../SignalProtocolStore.js';
|
||||||
|
|
@ -597,12 +598,9 @@ describe('SignalProtocolStore', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
async function testInvalidAttributes() {
|
async function testInvalidAttributes() {
|
||||||
try {
|
await assert.isRejected(
|
||||||
await store.saveIdentityWithAttributes(theirAci, attributes);
|
store.saveIdentityWithAttributes(theirAci, attributes)
|
||||||
throw new Error('saveIdentityWithAttributes should have failed');
|
);
|
||||||
} catch (error) {
|
|
||||||
// good. we expect to fail with invalid attributes.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
it('rejects an invalid publicKey', async () => {
|
it('rejects an invalid publicKey', async () => {
|
||||||
|
|
@ -1637,4 +1635,96 @@ describe('SignalProtocolStore', () => {
|
||||||
// Note: signature is ignored.
|
// Note: signature is ignored.
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('maybeRemoveKyberPreKey', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await store.clearKyberPreKeyStore();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await store.clearKyberPreKeyStore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect duplicate triples', async () => {
|
||||||
|
await store.storeKyberPreKeys(ourAci, [
|
||||||
|
{
|
||||||
|
createdAt: Date.now(),
|
||||||
|
data: generateKyberPreKey(identityKey, 1).serialize(),
|
||||||
|
isConfirmed: true,
|
||||||
|
isLastResort: true,
|
||||||
|
keyId: 1,
|
||||||
|
ourServiceId: ourAci,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await store.maybeRemoveKyberPreKey(ourAci, {
|
||||||
|
keyId: 1,
|
||||||
|
signedPreKeyId: 1,
|
||||||
|
baseKey: testKey.publicKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
await assert.isRejected(
|
||||||
|
store.maybeRemoveKyberPreKey(ourAci, {
|
||||||
|
keyId: 1,
|
||||||
|
signedPreKeyId: 1,
|
||||||
|
baseKey: testKey.publicKey,
|
||||||
|
}),
|
||||||
|
'Duplicate kyber triple 1:1'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore triples for non last resort keys', async () => {
|
||||||
|
await store.storeKyberPreKeys(ourAci, [
|
||||||
|
{
|
||||||
|
createdAt: Date.now(),
|
||||||
|
data: generateKyberPreKey(identityKey, 1).serialize(),
|
||||||
|
isConfirmed: true,
|
||||||
|
isLastResort: false,
|
||||||
|
keyId: 1,
|
||||||
|
ourServiceId: ourAci,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await store.maybeRemoveKyberPreKey(ourAci, {
|
||||||
|
keyId: 1,
|
||||||
|
signedPreKeyId: 1,
|
||||||
|
baseKey: testKey.publicKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
// this should not throw since the key was not last resort
|
||||||
|
await store.maybeRemoveKyberPreKey(ourAci, {
|
||||||
|
keyId: 1,
|
||||||
|
signedPreKeyId: 1,
|
||||||
|
baseKey: testKey.publicKey,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove triples when removing the key', async () => {
|
||||||
|
await store.storeKyberPreKeys(ourAci, [
|
||||||
|
{
|
||||||
|
createdAt: Date.now(),
|
||||||
|
data: generateKyberPreKey(identityKey, 1).serialize(),
|
||||||
|
isConfirmed: true,
|
||||||
|
isLastResort: true,
|
||||||
|
keyId: 1,
|
||||||
|
ourServiceId: ourAci,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await store.maybeRemoveKyberPreKey(ourAci, {
|
||||||
|
keyId: 1,
|
||||||
|
signedPreKeyId: 1,
|
||||||
|
baseKey: testKey.publicKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
await store.removeKyberPreKeys(ourAci, [1]);
|
||||||
|
|
||||||
|
// this should not throw since we removed the key
|
||||||
|
await store.maybeRemoveKyberPreKey(ourAci, {
|
||||||
|
keyId: 1,
|
||||||
|
signedPreKeyId: 1,
|
||||||
|
baseKey: testKey.publicKey,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -363,7 +363,7 @@ export function connectAuthenticatedLibsignal({
|
||||||
const listener: LibsignalWebSocketResourceHolder & ChatServiceListener = {
|
const listener: LibsignalWebSocketResourceHolder & ChatServiceListener = {
|
||||||
resource: undefined,
|
resource: undefined,
|
||||||
onIncomingMessage(
|
onIncomingMessage(
|
||||||
envelope: Buffer,
|
envelope: Uint8Array,
|
||||||
timestamp: number,
|
timestamp: number,
|
||||||
ack: ChatServerMessageAck
|
ack: ChatServerMessageAck
|
||||||
): void {
|
): void {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue