Migrate sessions table to BLOB column

Co-authored-by: Scott Nonnenberg <scott@signal.org>
This commit is contained in:
Fedor Indutny 2024-10-02 17:23:00 -07:00 committed by GitHub
parent a4d8ba4899
commit 091580825a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 582 additions and 275 deletions

View file

@ -2,7 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
import PQueue from 'p-queue';
import { isNumber, omit } from 'lodash';
import { omit } from 'lodash';
import { z } from 'zod';
import { EventEmitter } from 'events';
@ -27,10 +27,6 @@ import { isNotNil } from './util/isNotNil';
import { drop } from './util/drop';
import { Zone } from './util/Zone';
import { isMoreRecentThan } from './util/timestamp';
import {
sessionRecordToProtobuf,
sessionStructureToBytes,
} from './util/sessionTranslation';
import type {
DeviceType,
IdentityKeyType,
@ -182,7 +178,7 @@ async function _fillCaches<ID, T extends HasIdType<ID>, HydratedType>(
}
export function hydrateSession(session: SessionType): SessionRecord {
return SessionRecord.deserialize(Buffer.from(session.record, 'base64'));
return SessionRecord.deserialize(Buffer.from(session.record));
}
export function hydratePublicKey(identityKey: IdentityKeyType): PublicKey {
return PublicKey.deserialize(Buffer.from(identityKey.publicKey));
@ -209,9 +205,6 @@ export function hydrateSignedPreKey(
);
}
export function freezeSession(session: SessionRecord): string {
return session.serialize().toString('base64');
}
export function freezePublicKey(publicKey: PublicKey): Uint8Array {
return publicKey.serialize();
}
@ -1333,9 +1326,14 @@ export class SignalProtocolStore extends EventEmitter {
return entry.item;
}
// We'll either just hydrate the item or we'll fully migrate the session
// and save it to the database.
return await this._maybeMigrateSession(entry.fromDB, { zone });
const newItem = {
hydrated: true,
item: hydrateSession(entry.fromDB),
fromDB: entry.fromDB,
};
map.set(id, newItem);
return newItem.item;
} catch (error) {
const errorString = Errors.toLogFormat(error);
log.error(`loadSession: failed to load session ${id}: ${errorString}`);
@ -1359,68 +1357,6 @@ export class SignalProtocolStore extends EventEmitter {
});
}
private async _maybeMigrateSession(
session: SessionType,
{ zone = GLOBAL_ZONE }: SessionTransactionOptions = {}
): Promise<SessionRecord> {
if (!this.sessions) {
throw new Error('_maybeMigrateSession: this.sessions not yet cached!');
}
// Already migrated, hydrate and update cache
if (session.version === 2) {
const item = hydrateSession(session);
const map = this.pendingSessions.has(session.id)
? this.pendingSessions
: this.sessions;
map.set(session.id, {
hydrated: true,
item,
fromDB: session,
});
return item;
}
// Not yet converted, need to translate to new format and save
if (session.version !== undefined) {
throw new Error('_maybeMigrateSession: Unknown session version type!');
}
const { ourServiceId } = session;
const keyPair = this.getIdentityKeyPair(ourServiceId);
if (!keyPair) {
throw new Error('_maybeMigrateSession: No identity key for ourself!');
}
const localRegistrationId = await this.getLocalRegistrationId(ourServiceId);
if (!isNumber(localRegistrationId)) {
throw new Error('_maybeMigrateSession: No registration id for ourself!');
}
const localUserData = {
identityKeyPublic: keyPair.pubKey,
registrationId: localRegistrationId,
};
log.info(`_maybeMigrateSession: Migrating session with id ${session.id}`);
const sessionProto = sessionRecordToProtobuf(
JSON.parse(session.record),
localUserData
);
const record = SessionRecord.deserialize(
Buffer.from(sessionStructureToBytes(sessionProto))
);
await this.storeSession(QualifiedAddress.parse(session.id), record, {
zone,
});
return record;
}
async storeSession(
qualifiedAddress: QualifiedAddress,
record: SessionRecord,
@ -1454,7 +1390,7 @@ export class SignalProtocolStore extends EventEmitter {
conversationId: conversation.id,
serviceId,
deviceId,
record: record.serialize().toString('base64'),
record: record.serialize(),
};
const newSession = {
@ -1533,9 +1469,7 @@ export class SignalProtocolStore extends EventEmitter {
return undefined;
}
const record = await this._maybeMigrateSession(entry.fromDB, {
zone,
});
const record = hydrateSession(entry.fromDB);
if (record.hasCurrentState()) {
return { record, entry };
}
@ -1688,9 +1622,7 @@ export class SignalProtocolStore extends EventEmitter {
addr,
`_archiveSession(${addr.toString()})`,
async () => {
const item = entry.hydrated
? entry.item
: await this._maybeMigrateSession(entry.fromDB, { zone });
const item = entry.hydrated ? entry.item : hydrateSession(entry.fromDB);
if (!item.hasCurrentState()) {
return;

View file

@ -206,8 +206,7 @@ export type SessionType = {
serviceId: ServiceIdString;
conversationId: string;
deviceId: number;
record: string;
version?: number;
record: Uint8Array;
};
export type SessionIdType = SessionType['id'];
export type SignedPreKeyType = {
@ -722,7 +721,6 @@ type WritableInterface = {
sessions: Array<SessionType>;
unprocessed: Array<UnprocessedType>;
}): void;
bulkAddSessions: (array: Array<SessionType>) => void;
removeSessionById: (id: SessionIdType) => number;
removeSessionsByConversation: (conversationId: string) => void;
removeSessionsByServiceId: (serviceId: ServiceIdString) => void;

View file

@ -407,7 +407,6 @@ export const DataWriter: ServerWritableInterface = {
createOrUpdateSession,
createOrUpdateSessions,
commitDecryptResult,
bulkAddSessions,
removeSessionById,
removeSessionsByConversation,
removeSessionsByServiceId,
@ -1443,7 +1442,8 @@ function _getAllSentProtoMessageIds(db: ReadableDB): Array<SentMessageDBType> {
const SESSIONS_TABLE = 'sessions';
function createOrUpdateSession(db: WritableDB, data: SessionType): void {
const { id, conversationId, ourServiceId, serviceId } = data;
const { id, conversationId, ourServiceId, serviceId, deviceId, record } =
data;
if (!id) {
throw new Error(
'createOrUpdateSession: Provided data did not have a truthy id'
@ -1463,13 +1463,15 @@ function createOrUpdateSession(db: WritableDB, data: SessionType): void {
conversationId,
ourServiceId,
serviceId,
json
deviceId,
record
) values (
$id,
$conversationId,
$ourServiceId,
$serviceId,
$json
$deviceId,
$record
)
`
).run({
@ -1477,7 +1479,8 @@ function createOrUpdateSession(db: WritableDB, data: SessionType): void {
conversationId,
ourServiceId,
serviceId,
json: objectToJSON(data),
deviceId,
record,
});
}
@ -1519,9 +1522,6 @@ function commitDecryptResult(
})();
}
function bulkAddSessions(db: WritableDB, array: Array<SessionType>): void {
return bulkAdd(db, SESSIONS_TABLE, array);
}
function removeSessionById(db: WritableDB, id: SessionIdType): number {
return removeById(db, SESSIONS_TABLE, id);
}
@ -1555,7 +1555,7 @@ function removeAllSessions(db: WritableDB): number {
return removeAllFromTable(db, SESSIONS_TABLE);
}
function getAllSessions(db: ReadableDB): Array<SessionType> {
return getAllFromTable(db, SESSIONS_TABLE);
return db.prepare('SELECT * FROM sessions').all();
}
// Conversations

View file

@ -0,0 +1,218 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import assert from 'assert';
import z from 'zod';
import type { Database } from '@signalapp/better-sqlite3';
import type { LoggerType } from '../../types/Logging';
import * as Errors from '../../types/errors';
import {
sessionRecordToProtobuf,
sessionStructureToBytes,
} from '../../util/sessionTranslation';
import { getOwn } from '../../util/getOwn';
import { missingCaseError } from '../../util/missingCaseError';
export const version = 1220;
const identityKeyMapSchema = z.record(
z.string(),
z.object({
privKey: z.string().transform(x => Buffer.from(x, 'base64')),
pubKey: z.string().transform(x => Buffer.from(x, 'base64')),
})
);
const registrationIdMapSchema = z.record(z.string(), z.number());
type PreviousSessionRowType = Readonly<{
id: string;
conversationId: string;
ourServiceId: string;
serviceId: string;
json: string;
}>;
const previousSessionJsonSchema = z.object({
id: z.string(),
ourServiceId: z.string(),
serviceId: z.string(),
conversationId: z.string(),
deviceId: z.number(),
record: z.string(),
version: z.literal(1).or(z.literal(2)),
});
type NextSessionRowType = Readonly<{
id: string;
conversationId: string;
ourServiceId: string;
serviceId: string;
deviceId: number;
record: Buffer;
}>;
function migrateSession(
row: PreviousSessionRowType,
identityKeyMap: z.infer<typeof identityKeyMapSchema>,
registrationIdMap: z.infer<typeof registrationIdMapSchema>,
logger: LoggerType
): NextSessionRowType {
const { id, conversationId, ourServiceId, serviceId, json } = row;
const session = previousSessionJsonSchema.parse(JSON.parse(json));
assert.strictEqual(session.id, id, 'Invalid id');
assert.strictEqual(
session.conversationId,
conversationId,
'Invalid conversationId'
);
assert.strictEqual(
session.ourServiceId,
ourServiceId,
'Invalid ourServiceId,'
);
assert.strictEqual(session.serviceId, serviceId, 'Invalid serviceId');
// Previously migrated session
if (session.version === 2) {
return {
id,
conversationId,
ourServiceId,
serviceId,
deviceId: session.deviceId,
record: Buffer.from(session.record, 'base64'),
};
}
if (session.version === 1) {
const keyPair = getOwn(identityKeyMap, ourServiceId);
if (!keyPair) {
throw new Error('migrateSession: No identity key for ourself!');
}
const localRegistrationId = getOwn(registrationIdMap, ourServiceId);
if (localRegistrationId == null) {
throw new Error('_maybeMigrateSession: No registration id for ourself!');
}
const localUserData = {
identityKeyPublic: keyPair.pubKey,
registrationId: localRegistrationId,
};
logger.info(`migrateSession: Migrating session with id ${id}`);
const sessionProto = sessionRecordToProtobuf(
JSON.parse(session.record),
localUserData
);
return {
id,
conversationId,
ourServiceId,
serviceId,
deviceId: session.deviceId,
record: Buffer.from(sessionStructureToBytes(sessionProto)),
};
}
throw missingCaseError(session.version);
}
export function updateToSchemaVersion1220(
currentVersion: number,
db: Database,
logger: LoggerType
): void {
if (currentVersion >= 1220) {
return;
}
db.transaction(() => {
db.exec(`
ALTER TABLE sessions
RENAME TO old_sessions;
CREATE TABLE sessions (
id TEXT NOT NULL PRIMARY KEY,
ourServiceId TEXT NOT NULL,
serviceId TEXT NOT NULL,
conversationId TEXT NOT NULL,
deviceId INTEGER NOT NULL,
record BLOB NOT NULL
) STRICT;
`);
const getItem = db
.prepare(
`
SELECT json -> '$.value' FROM items WHERE id IS ?
`
)
.pluck();
const identityKeyMapJson = getItem.get('identityKeyMap');
const registrationIdMapJson = getItem.get('registrationIdMap');
// If we don't have private keys - the sessions cannot be used anyway
if (!identityKeyMapJson || !registrationIdMapJson) {
logger.info('updateToSchemaVersion1220: no identity/registration id');
db.exec('DROP TABLE old_sessions');
db.pragma('user_version = 1220');
return;
}
const identityKeyMap = identityKeyMapSchema.parse(
JSON.parse(identityKeyMapJson)
);
const registrationIdMap = registrationIdMapSchema.parse(
JSON.parse(registrationIdMapJson)
);
const getSessionsPage = db.prepare(
'DELETE FROM old_sessions RETURNING * LIMIT 1000'
);
const insertSession = db.prepare(`
INSERT INTO sessions
(id, ourServiceId, serviceId, conversationId, deviceId, record)
VALUES
($id, $ourServiceId, $serviceId, $conversationId, $deviceId, $record)
`);
let migrated = 0;
let failed = 0;
// eslint-disable-next-line no-constant-condition
while (true) {
const rows: Array<PreviousSessionRowType> = getSessionsPage.all();
if (rows.length === 0) {
break;
}
for (const row of rows) {
try {
insertSession.run(
migrateSession(row, identityKeyMap, registrationIdMap, logger)
);
migrated += 1;
} catch (error) {
failed += 1;
logger.error(
'updateToSchemaVersion1220: failed to migrate session',
Errors.toLogFormat(error)
);
}
}
}
logger.info(
`updateToSchemaVersion1220: migrated ${migrated} sessions, ` +
`${failed} failed`
);
db.exec('DROP TABLE old_sessions');
db.pragma('user_version = 1220');
})();
logger.info('updateToSchemaVersion1220: success!');
}

View file

@ -97,10 +97,11 @@ import { updateToSchemaVersion1170 } from './1170-update-call-history-unread-ind
import { updateToSchemaVersion1180 } from './1180-add-attachment-download-source';
import { updateToSchemaVersion1190 } from './1190-call-links-storage';
import { updateToSchemaVersion1200 } from './1200-attachment-download-source-index';
import { updateToSchemaVersion1210 } from './1210-call-history-started-id';
import {
updateToSchemaVersion1210,
updateToSchemaVersion1220,
version as MAX_VERSION,
} from './1210-call-history-started-id';
} from './1220-blob-sessions';
function updateToSchemaVersion1(
currentVersion: number,
@ -2067,6 +2068,7 @@ export const SCHEMA_VERSIONS = [
updateToSchemaVersion1200,
updateToSchemaVersion1210,
updateToSchemaVersion1220,
];
export class DBVersionFromFutureError extends Error {

View file

@ -12,6 +12,185 @@ import { sessionRecordToProtobuf } from '../../util/sessionTranslation';
const getRecordCopy = (record: any): any => JSON.parse(JSON.stringify(record));
export const SESSION_V1_RECORD = {
sessions: {
'\u0005W‡¿“\u0000lÈ\nyª\u000eümB0\u0017j„.Û£³-s\u0016č(O_M': {
registrationId: 4243,
currentRatchet: {
rootKey:
'Ë\u00035/üœÚšg\u0003Xeûú\u0010—\u0000ü\u0002¶»o5\u001cƒ—­¥\u0004Ðÿ«',
lastRemoteEphemeralKey:
'\u0005\n7\u001cmT…b!è\u000eÍ\u0007\u0016m4g³\u0005‘üœIYŒê\b\u0011ÏÎPs',
previousCounter: 2,
ephemeralKeyPair: {
privKey: 'ä—ãÅ«ªŠàøí)ˆá\u0005Á"ŒsJM.¨¡\u0012r(N\f9Ô\b',
pubKey: '\u0005+\u00134–«1\u0000\u0013l *ãKçnºÖó³íTSŸ&Œ{ù ͂>1',
},
},
indexInfo: {
remoteIdentityKey: '\u0005¨¨©üÏäúo፩êO¢çúxr»Æ¿rœ²GžùiT@',
closed: -1,
baseKey: '\u0005W‡¿“\u0000lÈ\nyª\u000eümB0\u0017j„.Û£³-s\u0016č(O_M',
baseKeyType: 2,
},
oldRatchetList: [
{
added: 1605579954962,
ephemeralKey:
'\u00050»­\n¨ÊA‘ä\u0006¢Ç´d\u0002\u00129}%î}ΩTc}8€¼\u0011n\\',
},
{
added: 1605580408250,
ephemeralKey:
'\u0005^Ä\nò›À¢\u0000\u000f­A\\6+Ó\u001a÷&×$¸¬ÑÔ|<qSÖ\u001aÙh',
},
{
added: 1605581155167,
ephemeralKey: '\u0005<\u0017å)œQàFîl29Øƒ\u001c— Ý$·;„udß\u0005I|f\u0006',
},
{
added: 1605638524556,
ephemeralKey: '\u0005¯jõ±ã0wÛPÐÂSÏ´;·&\u0011Â%º¯°“ÝÙþêù8F',
},
{
added: 1606761719753,
ephemeralKey: '\u0005›Î(ð>‘x‚ƒÄÈ?þv~íkx â¬.ðo™òDg\u001eß.\r',
},
{
added: 1606766530935,
ephemeralKey:
'\u0005\u0014@ž½M†,à\bóó™…}¨`i¿\u0000©I\u0001ôG\u001f”:Ù{ó\u0005 ',
},
{
added: 1608326293655,
ephemeralKey: '\u0005µÒ\u0014?È¢+ÑR÷ç?3šƒ\\@0‹†\u0004®+-\bŽr\t',
},
{
added: 1609871105317,
ephemeralKey:
'\u0005„±@íN"Í\u0019HS{$ï\u0017”[Ñ\\\u001a*;>P\u0000\u001f\u000eHNaù)',
},
{
added: 1611707063523,
ephemeralKey: '\u0005Þg”Åkéƒ\u0001\u0013—¡ÿûNXÈ(9\u0006¤’w˜®/عRi‹JI',
},
{
added: 1612211156372,
ephemeralKey: '\u0005:[ÛOˆ–pd¯ ÂÙç\u0010Oއw{}ý\bw–9Àߝ=“\u0014Z',
},
],
'\u00050»­\n¨ÊA‘ä\u0006¢Ç´d\u0002\u00129}%î}ΩTc}8€¼\u0011n\\': {
messageKeys: {},
chainKey: {
counter: 0,
},
chainType: 2,
},
'\u0005^Ä\nò›À¢\u0000\u000f­A\\6+Ó\u001a÷&×$¸¬ÑÔ|<qSÖ\u001aÙh': {
messageKeys: {},
chainKey: {
counter: 2,
},
chainType: 2,
},
'\u0005<\u0017å)œQàFîl29Øƒ\u001c— Ý$·;„udß\u0005I|f\u0006': {
messageKeys: {},
chainKey: {
counter: 1,
},
chainType: 2,
},
'\u0005¯jõ±ã0wÛPÐÂSÏ´;·&\u0011Â%º¯°“ÝÙþêù8F': {
messageKeys: {
'0': 'A/{´{×f(èaøy\\D¾\u0000ÃHÀÁâô$ãŸ\u001d3’Äö°Ù',
'1': ‚ŒFT}dw8Æýª7»ÚÓ\u000f*'Ԛ»7Š£\u0018\u0012ñDá‚",
'2': 'Îï\u0013¨ÁÕÎk\u000eýèȈ÷,¼îû5%ÓUœ¤6_õ¢\u0019ä]',
},
chainKey: {
counter: 3,
},
chainType: 2,
},
'\u0005›Î(ð>‘x‚ƒÄÈ?þv~íkx â¬.ðo™òDg\u001eß.\r': {
messageKeys: {
'4': '©}j›¿Š¼\u0014q\tŠ¥”Á”ñ\u0003: ÷ÞrƒñûÔµ%Æ\u001a',
},
chainKey: {
counter: 6,
},
chainType: 2,
},
'\u0005\u0014@ž½M†,à\bóó™…}¨`i¿\u0000©I\u0001ôG\u001f”:Ù{ó\u0005 ': {
messageKeys: {},
chainKey: {
counter: 0,
},
chainType: 2,
},
'\u0005µÒ\u0014?È¢+ÑR÷ç?3šƒ\\@0‹†\u0004®+-\bŽr\t': {
messageKeys: {},
chainKey: {
counter: 2,
},
chainType: 2,
},
'\u0005„±@íN"Í\u0019HS{$ï\u0017”[Ñ\\\u001a*;>P\u0000\u001f\u000eHNaù)': {
messageKeys: {
'0': "1kÏ\u001cí+«<º‚\b'VÌ!×¼«PÃ[üáy;l'ƒ€€Ž",
'2': 'ö\u00047%L-…Wm)†›\u001d£ääíNô.Ô8…ÃÉ4r´ó^2',
'3': '¨¿¦›7T]\u001c\u001c“à4:x\u0019¿\u0002YÉÀ\u001bâjr¸»¤¢0,*',
'5': '™¥\u0006·q“gó4þ\u0011®ˆU4F\u001cl©\bŒäô…»ÊÇƎ[',
},
chainKey: {
counter: 5,
},
chainType: 2,
},
'\u0005Þg”Åkéƒ\u0001\u0013—¡ÿûNXÈ(9\u0006¤’w˜®/عRi‹JI': {
messageKeys: {
'0': "]'8ŽWÄ\u0007…n˜º­Ö{ÿ7]ôäÄ!é\u000btA@°b¢)\u001ar",
'2': '­ÄfGÇjÖxÅö:×RÔi)M\u0019©IE+¨`þKá—;£Û½',
'3': '¦Õhýø`€Ö“PéPs;\u001e\u000bE}¨¿–õ\u0003uªøå\u00062(×G',
'9': 'Ï^—<‘Õú̃\u0001i´;ït¼\u001aÑ?ï\u0014lãàƸƒ\u001a8“/m',
},
chainKey: {
counter: 11,
},
chainType: 2,
},
'\u0005:[ÛOˆ–pd¯ ÂÙç\u0010Oއw{}ý\bw–9Àߝ=“\u0014Z': {
messageKeys: {
'0': '!\u00115\\W~|¯oa2\u001e\u0004Vž8Ï¡d}\u001b\u001a8^QÖfvÕ"‹',
},
chainKey: {
counter: 1,
},
chainType: 2,
},
'\u0005\n7\u001cmT…b!è\u000eÍ\u0007\u0016m4g³\u0005‘üœIYŒê\b\u0011ÏÎPs': {
messageKeys: {
'0': 'Îgó¯‘2àvñ‘X_õ\u0014–Ç\u0000öl\u001f4J>ŒÐÏ{`-Ü5¦',
'4': 'c¿<µâ¼Xµƒ!Ù¯µ®[—n<žìîúcoå©n\u0013"l]Ð',
},
chainKey: {
counter: 5,
key: 'Z{òÙ8سAÝdSZ†k\n×\u001cô¡\u001b[YÒ¶ƒ\u0016a°\u0004<',
},
chainType: 2,
},
'\u0005+\u00134–«1\u0000\u0013l *ãKçnºÖó³íTSŸ&Œ{ù ͂>1': {
messageKeys: {},
chainKey: {
counter: -1,
key: "èB?7\u000f¯\u001e\u0010¨\u0007}:“?¹\u0010$\\ë~ª\u0000gM0՘'£\u0005",
},
chainType: 1,
},
},
},
version: 'v1',
} as any;
function protoToJSON(value: unknown): unknown {
if (value == null) {
return value;
@ -169,186 +348,7 @@ describe('sessionTranslation', () => {
});
it('Generates expected protobuf with many old receiver chains', () => {
const record: any = {
sessions: {
'\u0005W‡¿“\u0000lÈ\nyª\u000eümB0\u0017j„.Û£³-s\u0016č(O_M': {
registrationId: 4243,
currentRatchet: {
rootKey:
'Ë\u00035/üœÚšg\u0003Xeûú\u0010—\u0000ü\u0002¶»o5\u001cƒ—­¥\u0004Ðÿ«',
lastRemoteEphemeralKey:
'\u0005\n7\u001cmT…b!è\u000eÍ\u0007\u0016m4g³\u0005‘üœIYŒê\b\u0011ÏÎPs',
previousCounter: 2,
ephemeralKeyPair: {
privKey: 'ä—ãÅ«ªŠàøí)ˆá\u0005Á"ŒsJM.¨¡\u0012r(N\f9Ô\b',
pubKey: '\u0005+\u00134–«1\u0000\u0013l *ãKçnºÖó³íTSŸ&Œ{ù ͂>1',
},
},
indexInfo: {
remoteIdentityKey: '\u0005¨¨©üÏäúo፩êO¢çúxr»Æ¿rœ²GžùiT@',
closed: -1,
baseKey: '\u0005W‡¿“\u0000lÈ\nyª\u000eümB0\u0017j„.Û£³-s\u0016č(O_M',
baseKeyType: 2,
},
oldRatchetList: [
{
added: 1605579954962,
ephemeralKey:
'\u00050»­\n¨ÊA‘ä\u0006¢Ç´d\u0002\u00129}%î}ΩTc}8€¼\u0011n\\',
},
{
added: 1605580408250,
ephemeralKey:
'\u0005^Ä\nò›À¢\u0000\u000f­A\\6+Ó\u001a÷&×$¸¬ÑÔ|<qSÖ\u001aÙh',
},
{
added: 1605581155167,
ephemeralKey:
'\u0005<\u0017å)œQàFîl29Øƒ\u001c— Ý$·;„udß\u0005I|f\u0006',
},
{
added: 1605638524556,
ephemeralKey: '\u0005¯jõ±ã0wÛPÐÂSÏ´;·&\u0011Â%º¯°“ÝÙþêù8F',
},
{
added: 1606761719753,
ephemeralKey: '\u0005›Î(ð>‘x‚ƒÄÈ?þv~íkx â¬.ðo™òDg\u001eß.\r',
},
{
added: 1606766530935,
ephemeralKey:
'\u0005\u0014@ž½M†,à\bóó™…}¨`i¿\u0000©I\u0001ôG\u001f”:Ù{ó\u0005 ',
},
{
added: 1608326293655,
ephemeralKey: '\u0005µÒ\u0014?È¢+ÑR÷ç?3šƒ\\@0‹†\u0004®+-\bŽr\t',
},
{
added: 1609871105317,
ephemeralKey:
'\u0005„±@íN"Í\u0019HS{$ï\u0017”[Ñ\\\u001a*;>P\u0000\u001f\u000eHNaù)',
},
{
added: 1611707063523,
ephemeralKey: '\u0005Þg”Åkéƒ\u0001\u0013—¡ÿûNXÈ(9\u0006¤’w˜®/عRi‹JI',
},
{
added: 1612211156372,
ephemeralKey: '\u0005:[ÛOˆ–pd¯ ÂÙç\u0010Oއw{}ý\bw–9Àߝ=“\u0014Z',
},
],
'\u00050»­\n¨ÊA‘ä\u0006¢Ç´d\u0002\u00129}%î}ΩTc}8€¼\u0011n\\': {
messageKeys: {},
chainKey: {
counter: 0,
},
chainType: 2,
},
'\u0005^Ä\nò›À¢\u0000\u000f­A\\6+Ó\u001a÷&×$¸¬ÑÔ|<qSÖ\u001aÙh': {
messageKeys: {},
chainKey: {
counter: 2,
},
chainType: 2,
},
'\u0005<\u0017å)œQàFîl29Øƒ\u001c— Ý$·;„udß\u0005I|f\u0006': {
messageKeys: {},
chainKey: {
counter: 1,
},
chainType: 2,
},
'\u0005¯jõ±ã0wÛPÐÂSÏ´;·&\u0011Â%º¯°“ÝÙþêù8F': {
messageKeys: {
'0': 'A/{´{×f(èaøy\\D¾\u0000ÃHÀÁâô$ãŸ\u001d3’Äö°Ù',
'1': ‚ŒFT}dw8Æýª7»ÚÓ\u000f*'Ԛ»7Š£\u0018\u0012ñDá‚",
'2': 'Îï\u0013¨ÁÕÎk\u000eýèȈ÷,¼îû5%ÓUœ¤6_õ¢\u0019ä]',
},
chainKey: {
counter: 3,
},
chainType: 2,
},
'\u0005›Î(ð>‘x‚ƒÄÈ?þv~íkx â¬.ðo™òDg\u001eß.\r': {
messageKeys: {
'4': '©}j›¿Š¼\u0014q\tŠ¥”Á”ñ\u0003: ÷ÞrƒñûÔµ%Æ\u001a',
},
chainKey: {
counter: 6,
},
chainType: 2,
},
'\u0005\u0014@ž½M†,à\bóó™…}¨`i¿\u0000©I\u0001ôG\u001f”:Ù{ó\u0005 ': {
messageKeys: {},
chainKey: {
counter: 0,
},
chainType: 2,
},
'\u0005µÒ\u0014?È¢+ÑR÷ç?3šƒ\\@0‹†\u0004®+-\bŽr\t': {
messageKeys: {},
chainKey: {
counter: 2,
},
chainType: 2,
},
'\u0005„±@íN"Í\u0019HS{$ï\u0017”[Ñ\\\u001a*;>P\u0000\u001f\u000eHNaù)':
{
messageKeys: {
'0': "1kÏ\u001cí+«<º‚\b'VÌ!×¼«PÃ[üáy;l'ƒ€€Ž",
'2': 'ö\u00047%L-…Wm)†›\u001d£ääíNô.Ô8…ÃÉ4r´ó^2',
'3': '¨¿¦›7T]\u001c\u001c“à4:x\u0019¿\u0002YÉÀ\u001bâjr¸»¤¢0,*',
'5': '™¥\u0006·q“gó4þ\u0011®ˆU4F\u001cl©\bŒäô…»ÊÇƎ[',
},
chainKey: {
counter: 5,
},
chainType: 2,
},
'\u0005Þg”Åkéƒ\u0001\u0013—¡ÿûNXÈ(9\u0006¤’w˜®/عRi‹JI': {
messageKeys: {
'0': "]'8ŽWÄ\u0007…n˜º­Ö{ÿ7]ôäÄ!é\u000btA@°b¢)\u001ar",
'2': '­ÄfGÇjÖxÅö:×RÔi)M\u0019©IE+¨`þKá—;£Û½',
'3': '¦Õhýø`€Ö“PéPs;\u001e\u000bE}¨¿–õ\u0003uªøå\u00062(×G',
'9': 'Ï^—<‘Õú̃\u0001i´;ït¼\u001aÑ?ï\u0014lãàƸƒ\u001a8“/m',
},
chainKey: {
counter: 11,
},
chainType: 2,
},
'\u0005:[ÛOˆ–pd¯ ÂÙç\u0010Oއw{}ý\bw–9Àߝ=“\u0014Z': {
messageKeys: {
'0': '!\u00115\\W~|¯oa2\u001e\u0004Vž8Ï¡d}\u001b\u001a8^QÖfvÕ"‹',
},
chainKey: {
counter: 1,
},
chainType: 2,
},
'\u0005\n7\u001cmT…b!è\u000eÍ\u0007\u0016m4g³\u0005‘üœIYŒê\b\u0011ÏÎPs': {
messageKeys: {
'0': 'Îgó¯‘2àvñ‘X_õ\u0014–Ç\u0000öl\u001f4J>ŒÐÏ{`-Ü5¦',
'4': 'c¿<µâ¼Xµƒ!Ù¯µ®[—n<žìîúcoå©n\u0013"l]Ð',
},
chainKey: {
counter: 5,
key: 'Z{òÙ8سAÝdSZ†k\n×\u001cô¡\u001b[YÒ¶ƒ\u0016a°\u0004<',
},
chainType: 2,
},
'\u0005+\u00134–«1\u0000\u0013l *ãKçnºÖó³íTSŸ&Œ{ù ͂>1': {
messageKeys: {},
chainKey: {
counter: -1,
key: "èB?7\u000f¯\u001e\u0010¨\u0007}:“?¹\u0010$\\ë~ª\u0000gM0՘'£\u0005",
},
chainType: 1,
},
},
},
version: 'v1',
};
const record: any = SESSION_V1_RECORD;
const expected = {
currentSession: {

View file

@ -74,6 +74,10 @@ export function getTableData(db: ReadableDB, table: string): TableRows {
if (value == null) {
continue;
}
if (Buffer.isBuffer(value)) {
result[key] = value.toString('hex');
continue;
}
try {
if (typeof value !== 'string') {
throw new Error('skip');

View file

@ -0,0 +1,153 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import { type WritableDB } from '../../sql/Interface';
import {
sessionRecordToProtobuf,
sessionStructureToBytes,
} from '../../util/sessionTranslation';
import { createDB, updateToVersion, insertData, getTableData } from './helpers';
import { SESSION_V1_RECORD } from '../../test-both/util/sessionTranslation_test';
const MAPS = [
{
id: 'identityKeyMap',
json: {
id: 'identityKeyMap',
value: {
ourAci: {
privKey: 'AAAA',
pubKey: 'AAAA',
},
},
},
},
{
id: 'registrationIdMap',
json: {
id: 'registrationIdMap',
value: {
ourAci: 123,
},
},
},
];
const SESSION_V2 = {
id: 'ourAci:theirAci',
conversationId: 'cid',
ourServiceId: 'ourAci',
serviceId: 'theirAci',
json: {
id: 'ourAci:theirAci',
conversationId: 'cid',
ourServiceId: 'ourAci',
serviceId: 'theirAci',
version: 2,
deviceId: 3,
record: Buffer.from('abc').toString('base64'),
},
};
describe('SQL/updateToSchemaVersion1220', () => {
let db: WritableDB;
afterEach(() => {
db.close();
});
beforeEach(() => {
db = createDB();
updateToVersion(db, 1210);
});
it('drops sessions without identity key/registration id', () => {
insertData(db, 'sessions', [SESSION_V2]);
updateToVersion(db, 1220);
assert.deepStrictEqual(getTableData(db, 'sessions'), []);
});
it('migrates v1 session', () => {
insertData(db, 'items', MAPS);
insertData(db, 'sessions', [
{
id: 'ourAci:theirAci',
conversationId: 'cid',
ourServiceId: 'ourAci',
serviceId: 'theirAci',
json: {
id: 'ourAci:theirAci',
conversationId: 'cid',
ourServiceId: 'ourAci',
serviceId: 'theirAci',
version: 1,
deviceId: 3,
record: JSON.stringify(SESSION_V1_RECORD),
},
},
]);
updateToVersion(db, 1220);
const bytes = sessionStructureToBytes(
sessionRecordToProtobuf(SESSION_V1_RECORD, {
identityKeyPublic: Buffer.from('AAAA', 'base64'),
registrationId: 123,
})
);
assert.deepStrictEqual(getTableData(db, 'sessions'), [
{
id: 'ourAci:theirAci',
conversationId: 'cid',
ourServiceId: 'ourAci',
serviceId: 'theirAci',
deviceId: 3,
record: Buffer.from(bytes).toString('hex'),
},
]);
});
it('migrates v2 session', () => {
insertData(db, 'items', MAPS);
insertData(db, 'sessions', [SESSION_V2]);
updateToVersion(db, 1220);
assert.deepStrictEqual(getTableData(db, 'sessions'), [
{
id: 'ourAci:theirAci',
conversationId: 'cid',
ourServiceId: 'ourAci',
serviceId: 'theirAci',
deviceId: 3,
record: Buffer.from('abc').toString('hex'),
},
]);
});
it('drops invalid sessions', () => {
insertData(db, 'items', MAPS);
insertData(db, 'sessions', [
{
id: 'ourAci:theirAci',
conversationId: 'cid',
ourServiceId: 'ourAci',
serviceId: 'theirAci',
json: {
id: 'ourAci:theirAci2',
conversationId: 'cid',
ourServiceId: 'ourAci',
serviceId: 'theirAci',
version: 1,
deviceId: 3,
record: 'abc',
},
},
]);
updateToVersion(db, 1220);
assert.deepStrictEqual(getTableData(db, 'sessions'), []);
});
});