"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SqliteCryptoStorageProvider = void 0; const Database = require("better-sqlite3"); /** * Sqlite crypto storage provider. Requires `better-sqlite3` package to be installed. * @category Storage providers */ class SqliteCryptoStorageProvider { /** * Creates a new Sqlite storage provider. * @param {string} path The file path to store the database at. Use ":memory:" to * store the database entirely in memory, or an empty string to do the equivalent * on the disk. */ constructor(path) { this.db = new Database(path); this.db.exec("CREATE TABLE IF NOT EXISTS kv (name TEXT PRIMARY KEY NOT NULL, value TEXT NOT NULL)"); this.db.exec("CREATE TABLE IF NOT EXISTS rooms (room_id TEXT PRIMARY KEY NOT NULL, config TEXT NOT NULL)"); this.db.exec("CREATE TABLE IF NOT EXISTS users (user_id TEXT PRIMARY KEY NOT NULL, outdated TINYINT NOT NULL)"); this.db.exec("CREATE TABLE IF NOT EXISTS user_devices (user_id TEXT NOT NULL, device_id TEXT NOT NULL, device TEXT NOT NULL, active TINYINT NOT NULL, PRIMARY KEY (user_id, device_id))"); this.db.exec("CREATE TABLE IF NOT EXISTS outbound_group_sessions (session_id TEXT NOT NULL, room_id TEXT NOT NULL, current TINYINT NOT NULL, pickled TEXT NOT NULL, uses_left NUMBER NOT NULL, expires_ts NUMBER NOT NULL, PRIMARY KEY (session_id, room_id))"); this.db.exec("CREATE TABLE IF NOT EXISTS sent_outbound_group_sessions (session_id TEXT NOT NULL, room_id TEXT NOT NULL, session_index INT NOT NULL, user_id TEXT NOT NULL, device_id TEXT NOT NULL, PRIMARY KEY (session_id, room_id, user_id, device_id, session_index))"); this.db.exec("CREATE TABLE IF NOT EXISTS olm_sessions (user_id TEXT NOT NULL, device_id TEXT NOT NULL, session_id TEXT NOT NULL, last_decryption_ts NUMBER NOT NULL, pickled TEXT NOT NULL, PRIMARY KEY (user_id, device_id, session_id))"); this.db.exec("CREATE TABLE IF NOT EXISTS inbound_group_sessions (session_id TEXT NOT NULL, room_id TEXT NOT NULL, user_id TEXT NOT NULL, device_id TEXT NOT NULL, pickled TEXT NOT NULL, PRIMARY KEY (session_id, room_id, user_id, device_id))"); this.db.exec("CREATE TABLE IF NOT EXISTS decrypted_event_metadata (room_id TEXT NOT NULL, event_id TEXT NOT NULL, session_id TEXT NOT NULL, message_index INT NOT NULL, PRIMARY KEY (room_id, event_id))"); this.db.exec("CREATE INDEX IF NOT EXISTS idx_decrypted_event_metadata_by_message_index ON decrypted_event_metadata (room_id, session_id, message_index)"); this.kvUpsert = this.db.prepare("INSERT INTO kv (name, value) VALUES (@name, @value) ON CONFLICT (name) DO UPDATE SET value = @value"); this.kvSelect = this.db.prepare("SELECT name, value FROM kv WHERE name = @name"); this.roomUpsert = this.db.prepare("INSERT INTO rooms (room_id, config) VALUES (@roomId, @config) ON CONFLICT (room_id) DO UPDATE SET config = @config"); this.roomSelect = this.db.prepare("SELECT room_id, config FROM rooms WHERE room_id = @roomId"); this.userUpsert = this.db.prepare("INSERT INTO users (user_id, outdated) VALUES (@userId, @outdated) ON CONFLICT (user_id) DO UPDATE SET outdated = @outdated"); this.userSelect = this.db.prepare("SELECT user_id, outdated FROM users WHERE user_id = @userId"); this.userDeviceUpsert = this.db.prepare("INSERT INTO user_devices (user_id, device_id, device, active) VALUES (@userId, @deviceId, @device, @active) ON CONFLICT (user_id, device_id) DO UPDATE SET device = @device, active = @active"); this.userDevicesDelete = this.db.prepare("UPDATE user_devices SET active = 0 WHERE user_id = @userId"); this.userDevicesSelect = this.db.prepare("SELECT user_id, device_id, device, active FROM user_devices WHERE user_id = @userId"); this.userActiveDevicesSelect = this.db.prepare("SELECT user_id, device_id, device, active FROM user_devices WHERE user_id = @userId AND active = 1"); this.userActiveDeviceSelect = this.db.prepare("SELECT user_id, device_id, device, active FROM user_devices WHERE user_id = @userId AND device_id = @deviceId AND active = 1"); this.obGroupSessionUpsert = this.db.prepare("INSERT INTO outbound_group_sessions (session_id, room_id, current, pickled, uses_left, expires_ts) VALUES (@sessionId, @roomId, @current, @pickled, @usesLeft, @expiresTs) ON CONFLICT (session_id, room_id) DO UPDATE SET pickled = @pickled, current = @current, uses_left = @usesLeft, expires_ts = @expiresTs"); this.obGroupSessionSelect = this.db.prepare("SELECT session_id, room_id, current, pickled, uses_left, expires_ts FROM outbound_group_sessions WHERE session_id = @sessionId AND room_id = @roomId"); this.obGroupCurrentSessionSelect = this.db.prepare("SELECT session_id, room_id, current, pickled, uses_left, expires_ts FROM outbound_group_sessions WHERE room_id = @roomId AND current = 1"); this.obGroupSessionMarkAllInactive = this.db.prepare("UPDATE outbound_group_sessions SET current = 0 WHERE room_id = @roomId"); this.obSentGroupSessionUpsert = this.db.prepare("INSERT INTO sent_outbound_group_sessions (session_id, room_id, session_index, user_id, device_id) VALUES (@sessionId, @roomId, @sessionIndex, @userId, @deviceId) ON CONFLICT (session_id, room_id, user_id, device_id, session_index) DO NOTHING"); this.obSentSelectLastSent = this.db.prepare("SELECT session_id, room_id, session_index, user_id, device_id FROM sent_outbound_group_sessions WHERE user_id = @userId AND device_id = @deviceId AND room_id = @roomId"); this.olmSessionUpsert = this.db.prepare("INSERT INTO olm_sessions (user_id, device_id, session_id, last_decryption_ts, pickled) VALUES (@userId, @deviceId, @sessionId, @lastDecryptionTs, @pickled) ON CONFLICT (user_id, device_id, session_id) DO UPDATE SET last_decryption_ts = @lastDecryptionTs, pickled = @pickled"); this.olmSessionCurrentSelect = this.db.prepare("SELECT user_id, device_id, session_id, last_decryption_ts, pickled FROM olm_sessions WHERE user_id = @userId AND device_id = @deviceId ORDER BY last_decryption_ts DESC LIMIT 1"); this.olmSessionSelect = this.db.prepare("SELECT user_id, device_id, session_id, last_decryption_ts, pickled FROM olm_sessions WHERE user_id = @userId AND device_id = @deviceId"); this.ibGroupSessionUpsert = this.db.prepare("INSERT INTO inbound_group_sessions (session_id, room_id, user_id, device_id, pickled) VALUES (@sessionId, @roomId, @userId, @deviceId, @pickled) ON CONFLICT (session_id, room_id, user_id, device_id) DO UPDATE SET pickled = @pickled"); this.ibGroupSessionSelect = this.db.prepare("SELECT session_id, room_id, user_id, device_id, pickled FROM inbound_group_sessions WHERE session_id = @sessionId AND room_id = @roomId AND user_id = @userId AND device_id = @deviceId"); this.deMetadataUpsert = this.db.prepare("INSERT INTO decrypted_event_metadata (room_id, event_id, session_id, message_index) VALUES (@roomId, @eventId, @sessionId, @messageIndex) ON CONFLICT (room_id, event_id) DO UPDATE SET message_index = @messageIndex, session_id = @sessionId"); this.deMetadataSelect = this.db.prepare("SELECT room_id, event_id, session_id, message_index FROM decrypted_event_metadata WHERE room_id = @roomId AND session_id = @sessionId AND message_index = @messageIndex LIMIT 1"); } setDeviceId(deviceId) { return __awaiter(this, void 0, void 0, function* () { this.kvUpsert.run({ name: 'deviceId', value: deviceId, }); }); } getDeviceId() { return __awaiter(this, void 0, void 0, function* () { const row = this.kvSelect.get({ name: 'deviceId' }); return row === null || row === void 0 ? void 0 : row.value; }); } setPickleKey(pickleKey) { return __awaiter(this, void 0, void 0, function* () { this.kvUpsert.run({ name: 'pickleKey', value: pickleKey, }); }); } getPickleKey() { return __awaiter(this, void 0, void 0, function* () { const row = this.kvSelect.get({ name: 'pickleKey' }); return row === null || row === void 0 ? void 0 : row.value; }); } setPickledAccount(pickled) { return __awaiter(this, void 0, void 0, function* () { this.kvUpsert.run({ name: 'pickled', value: pickled, }); }); } getPickledAccount() { return __awaiter(this, void 0, void 0, function* () { const row = this.kvSelect.get({ name: 'pickled' }); return row === null || row === void 0 ? void 0 : row.value; }); } storeRoom(roomId, config) { return __awaiter(this, void 0, void 0, function* () { this.roomUpsert.run({ roomId: roomId, config: JSON.stringify(config), }); }); } getRoom(roomId) { return __awaiter(this, void 0, void 0, function* () { const row = this.roomSelect.get({ roomId: roomId }); const val = row === null || row === void 0 ? void 0 : row.config; return val ? JSON.parse(val) : null; }); } setActiveUserDevices(userId, devices) { return __awaiter(this, void 0, void 0, function* () { this.db.transaction(() => { this.userUpsert.run({ userId: userId, outdated: 0 }); this.userDevicesDelete.run({ userId: userId }); for (const device of devices) { this.userDeviceUpsert.run({ userId: userId, deviceId: device.device_id, device: JSON.stringify(device), active: 1 }); } })(); }); } getActiveUserDevices(userId) { return __awaiter(this, void 0, void 0, function* () { const results = this.userActiveDevicesSelect.all({ userId: userId }); if (!results) return []; return results.map(d => JSON.parse(d.device)); }); } getActiveUserDevice(userId, deviceId) { return __awaiter(this, void 0, void 0, function* () { const result = this.userActiveDeviceSelect.get({ userId: userId, deviceId: deviceId }); if (!result) return null; return JSON.parse(result.device); }); } getAllUserDevices(userId) { return __awaiter(this, void 0, void 0, function* () { const results = this.userDevicesSelect.all({ userId: userId }); if (!results) return []; return results.map(d => Object.assign({}, JSON.parse(d.device), { unsigned: { bsdkIsActive: d.active === 1 } })); }); } flagUsersOutdated(userIds) { return __awaiter(this, void 0, void 0, function* () { this.db.transaction(() => { for (const userId of userIds) { this.userUpsert.run({ userId: userId, outdated: 1 }); } })(); }); } isUserOutdated(userId) { return __awaiter(this, void 0, void 0, function* () { const user = this.userSelect.get({ userId: userId }); return user ? Boolean(user.outdated) : true; }); } storeOutboundGroupSession(session) { return __awaiter(this, void 0, void 0, function* () { this.db.transaction(() => { if (session.isCurrent) { this.obGroupSessionMarkAllInactive.run({ roomId: session.roomId, }); } this.obGroupSessionUpsert.run({ sessionId: session.sessionId, roomId: session.roomId, pickled: session.pickled, current: session.isCurrent ? 1 : 0, usesLeft: session.usesLeft, expiresTs: session.expiresTs, }); })(); }); } getOutboundGroupSession(sessionId, roomId) { return __awaiter(this, void 0, void 0, function* () { const result = this.obGroupSessionSelect.get({ sessionId: sessionId, roomId: roomId }); if (result) { return { sessionId: result.session_id, roomId: result.room_id, pickled: result.pickled, isCurrent: result.current === 1, usesLeft: result.uses_left, expiresTs: result.expires_ts, }; } return null; }); } getCurrentOutboundGroupSession(roomId) { return __awaiter(this, void 0, void 0, function* () { const result = this.obGroupCurrentSessionSelect.get({ roomId: roomId }); if (result) { return { sessionId: result.session_id, roomId: result.room_id, pickled: result.pickled, isCurrent: result.current === 1, usesLeft: result.uses_left, expiresTs: result.expires_ts, }; } return null; }); } storeSentOutboundGroupSession(session, index, device) { return __awaiter(this, void 0, void 0, function* () { this.obSentGroupSessionUpsert.run({ sessionId: session.sessionId, roomId: session.roomId, sessionIndex: index, userId: device.user_id, deviceId: device.device_id, }); }); } getLastSentOutboundGroupSession(userId, deviceId, roomId) { return __awaiter(this, void 0, void 0, function* () { const result = this.obSentSelectLastSent.get({ userId: userId, deviceId: deviceId, roomId: roomId }); if (result) { return { sessionId: result.session_id, index: result.session_index }; } return null; }); } storeOlmSession(userId, deviceId, session) { return __awaiter(this, void 0, void 0, function* () { this.olmSessionUpsert.run({ userId: userId, deviceId: deviceId, sessionId: session.sessionId, lastDecryptionTs: session.lastDecryptionTs, pickled: session.pickled, }); }); } getCurrentOlmSession(userId, deviceId) { return __awaiter(this, void 0, void 0, function* () { const result = this.olmSessionCurrentSelect.get({ userId: userId, deviceId: deviceId }); if (!result) return null; return { sessionId: result.session_id, pickled: result.pickled, lastDecryptionTs: result.last_decryption_ts, }; }); } getOlmSessions(userId, deviceId) { return __awaiter(this, void 0, void 0, function* () { const result = this.olmSessionSelect.all({ userId: userId, deviceId: deviceId, }); return (result || []).map(r => ({ sessionId: r.session_id, pickled: r.pickled, lastDecryptionTs: r.last_decryption_ts, })); }); } storeInboundGroupSession(session) { return __awaiter(this, void 0, void 0, function* () { this.ibGroupSessionUpsert.run({ sessionId: session.sessionId, roomId: session.roomId, userId: session.senderUserId, deviceId: session.senderDeviceId, pickled: session.pickled, }); }); } getInboundGroupSession(senderUserId, senderDeviceId, roomId, sessionId) { return __awaiter(this, void 0, void 0, function* () { const result = this.ibGroupSessionSelect.get({ sessionId: sessionId, roomId: roomId, userId: senderUserId, deviceId: senderDeviceId, }); if (result) { return { sessionId: result.session_id, roomId: result.room_id, senderUserId: result.user_id, senderDeviceId: result.device_id, pickled: result.pickled, }; } return null; }); } setMessageIndexForEvent(roomId, eventId, sessionId, messageIndex) { return __awaiter(this, void 0, void 0, function* () { this.deMetadataUpsert.run({ roomId: roomId, eventId: eventId, sessionId: sessionId, messageIndex: messageIndex, }); }); } getEventForMessageIndex(roomId, sessionId, messageIndex) { return __awaiter(this, void 0, void 0, function* () { const result = this.deMetadataSelect.get({ roomId: roomId, sessionId: sessionId, messageIndex: messageIndex }); return result === null || result === void 0 ? void 0 : result.event_id; }); } /** * Closes the crypto store. Primarily for testing purposes. */ close() { return __awaiter(this, void 0, void 0, function* () { this.db.close(); }); } } exports.SqliteCryptoStorageProvider = SqliteCryptoStorageProvider;