Migrate sessions table to BLOB column
Co-authored-by: Scott Nonnenberg <scott@signal.org>
This commit is contained in:
parent
a4d8ba4899
commit
091580825a
8 changed files with 582 additions and 275 deletions
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
218
ts/sql/migrations/1220-blob-sessions.ts
Normal file
218
ts/sql/migrations/1220-blob-sessions.ts
Normal 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!');
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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ùiT@',
|
||||
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\u000fA\\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÷ç?3Dº\\@0\u0004®+-\br\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®/عRiJI',
|
||||
},
|
||||
{
|
||||
added: 1612211156372,
|
||||
ephemeralKey: '\u0005:[ÛOpd¯ ÂÙç\u0010OÞw{}ý\bw9Àß=\u0014Z',
|
||||
},
|
||||
],
|
||||
'\u00050»\n¨ÊAä\u0006¢Ç´d\u0002\u00129}%î}ΩTc}8¼\u0011n\\': {
|
||||
messageKeys: {},
|
||||
chainKey: {
|
||||
counter: 0,
|
||||
},
|
||||
chainType: 2,
|
||||
},
|
||||
'\u0005^Ä\nòÀ¢\u0000\u000fA\\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÷ç?3Dº\\@0\u0004®+-\br\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·qgó4þ\u0011®U4F\u001cl©\bäô
»ÊÇÆ[',
|
||||
},
|
||||
chainKey: {
|
||||
counter: 5,
|
||||
},
|
||||
chainType: 2,
|
||||
},
|
||||
'\u0005ÞgÅké\u0001\u0013¡ÿûNXÈ(9\u0006¤w®/عRiJI': {
|
||||
messageKeys: {
|
||||
'0': "]'8WÄ\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:[ÛOpd¯ ÂÙç\u0010OÞw{}ý\bw9Àß=\u0014Z': {
|
||||
messageKeys: {
|
||||
'0': '!\u00115\\W~|¯oa2\u001e\u0004V8Ï¡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ÝdSZk\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ùiT@',
|
||||
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\u000fA\\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÷ç?3Dº\\@0\u0004®+-\br\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®/عRiJI',
|
||||
},
|
||||
{
|
||||
added: 1612211156372,
|
||||
ephemeralKey: '\u0005:[ÛOpd¯ ÂÙç\u0010OÞw{}ý\bw9Àß=\u0014Z',
|
||||
},
|
||||
],
|
||||
'\u00050»\n¨ÊAä\u0006¢Ç´d\u0002\u00129}%î}ΩTc}8¼\u0011n\\': {
|
||||
messageKeys: {},
|
||||
chainKey: {
|
||||
counter: 0,
|
||||
},
|
||||
chainType: 2,
|
||||
},
|
||||
'\u0005^Ä\nòÀ¢\u0000\u000fA\\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÷ç?3Dº\\@0\u0004®+-\br\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·qgó4þ\u0011®U4F\u001cl©\bäô
»ÊÇÆ[',
|
||||
},
|
||||
chainKey: {
|
||||
counter: 5,
|
||||
},
|
||||
chainType: 2,
|
||||
},
|
||||
'\u0005ÞgÅké\u0001\u0013¡ÿûNXÈ(9\u0006¤w®/عRiJI': {
|
||||
messageKeys: {
|
||||
'0': "]'8WÄ\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:[ÛOpd¯ ÂÙç\u0010OÞw{}ý\bw9Àß=\u0014Z': {
|
||||
messageKeys: {
|
||||
'0': '!\u00115\\W~|¯oa2\u001e\u0004V8Ï¡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ÝdSZk\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: {
|
||||
|
|
|
@ -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');
|
||||
|
|
153
ts/test-node/sql/migration_1220_test.ts
Normal file
153
ts/test-node/sql/migration_1220_test.ts
Normal 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'), []);
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue