diff --git a/_locales/en/messages.json b/_locales/en/messages.json index a5691dd4d1d0..2fb4f6f6bffa 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -798,11 +798,6 @@ "message": "Are you sure? Clicking 'delete' will permanently remove this message from this device only." }, - "unidentifiedDelivery": { - "message": "Unidentified Delivery", - "description": - "Label shown on the message detail screen for messages sent or received with Unidentified Delivery enabled" - }, "deleteThisMessage": { "message": "Delete this message" }, diff --git a/app/sql.js b/app/sql.js index 6a732e44bba5..fdbb27756d86 100644 --- a/app/sql.js +++ b/app/sql.js @@ -15,6 +15,47 @@ module.exports = { close, removeDB, + createOrUpdateGroup, + getGroupById, + getAllGroupIds, + bulkAddGroups, + removeGroupById, + removeAllGroups, + + createOrUpdateIdentityKey, + getIdentityKeyById, + bulkAddIdentityKeys, + removeIdentityKeyById, + removeAllIdentityKeys, + + createOrUpdatePreKey, + getPreKeyById, + bulkAddPreKeys, + removePreKeyById, + removeAllPreKeys, + + createOrUpdateSignedPreKey, + getSignedPreKeyById, + getAllSignedPreKeys, + bulkAddSignedPreKeys, + removeSignedPreKeyById, + removeAllSignedPreKeys, + + createOrUpdateItem, + getItemById, + getAllItems, + bulkAddItems, + removeItemById, + removeAllItems, + + createOrUpdateSession, + getSessionById, + getSessionsByNumber, + bulkAddSessions, + removeSessionById, + removeSessionsByNumber, + removeAllSessions, + getConversationCount, saveConversation, saveConversations, @@ -51,6 +92,7 @@ module.exports = { removeAllUnprocessed, removeAll, + removeAllConfiguration, getMessagesNeedingUpgrade, getMessagesWithVisualMediaAttachments, @@ -320,12 +362,72 @@ async function updateToSchemaVersion4(currentVersion, instance) { console.log('updateToSchemaVersion4: success!'); } +async function updateToSchemaVersion6(currentVersion, instance) { + if (currentVersion >= 6) { + return; + } + console.log('updateToSchemaVersion6: starting...'); + await instance.run('BEGIN TRANSACTION;'); + + // key-value, ids are strings, one extra column + await instance.run( + `CREATE TABLE sessions( + id STRING PRIMARY KEY ASC, + number STRING, + json TEXT + );` + ); + + await instance.run(`CREATE INDEX sessions_number ON sessions ( + number + ) WHERE number IS NOT NULL;`); + + // key-value, ids are strings + await instance.run( + `CREATE TABLE groups( + id STRING PRIMARY KEY ASC, + json TEXT + );` + ); + await instance.run( + `CREATE TABLE identityKeys( + id STRING PRIMARY KEY ASC, + json TEXT + );` + ); + await instance.run( + `CREATE TABLE items( + id STRING PRIMARY KEY ASC, + json TEXT + );` + ); + + // key-value, ids are integers + await instance.run( + `CREATE TABLE preKeys( + id INTEGER PRIMARY KEY ASC, + json TEXT + );` + ); + await instance.run( + `CREATE TABLE signedPreKeys( + id INTEGER PRIMARY KEY ASC, + json TEXT + );` + ); + + await instance.run('PRAGMA schema_version = 6;'); + await instance.run('COMMIT TRANSACTION;'); + console.log('updateToSchemaVersion6: success!'); +} + const SCHEMA_VERSIONS = [ updateToSchemaVersion1, updateToSchemaVersion2, updateToSchemaVersion3, updateToSchemaVersion4, // version 5 was dropped + updateToSchemaVersion6, ]; async function updateSchema(instance) { @@ -400,6 +502,228 @@ async function removeDB() { rimraf.sync(filePath); } +const GROUPS_TABLE = 'groups'; +async function createOrUpdateGroup(data) { + return createOrUpdate(GROUPS_TABLE, data); +} +async function getGroupById(id) { + return getById(GROUPS_TABLE, id); +} +async function getAllGroupIds() { + const rows = await db.all('SELECT id FROM groups ORDER BY id ASC;'); + return map(rows, row => row.id); +} +async function bulkAddGroups(array) { + return bulkAdd(GROUPS_TABLE, array); +} +async function removeGroupById(id) { + return removeById(GROUPS_TABLE, id); +} +async function removeAllGroups() { + return removeAllFromTable(GROUPS_TABLE); +} + +const IDENTITY_KEYS_TABLE = 'identityKeys'; +async function createOrUpdateIdentityKey(data) { + return createOrUpdate(IDENTITY_KEYS_TABLE, data); +} +async function getIdentityKeyById(id) { + return getById(IDENTITY_KEYS_TABLE, id); +} +async function bulkAddIdentityKeys(array) { + return bulkAdd(IDENTITY_KEYS_TABLE, array); +} +async function removeIdentityKeyById(id) { + return removeById(IDENTITY_KEYS_TABLE, id); +} +async function removeAllIdentityKeys() { + return removeAllFromTable(IDENTITY_KEYS_TABLE); +} + +const PRE_KEYS_TABLE = 'preKeys'; +async function createOrUpdatePreKey(data) { + return createOrUpdate(PRE_KEYS_TABLE, data); +} +async function getPreKeyById(id) { + return getById(PRE_KEYS_TABLE, id); +} +async function bulkAddPreKeys(array) { + return bulkAdd(PRE_KEYS_TABLE, array); +} +async function removePreKeyById(id) { + return removeById(PRE_KEYS_TABLE, id); +} +async function removeAllPreKeys() { + return removeAllFromTable(PRE_KEYS_TABLE); +} + +const SIGNED_PRE_KEYS_TABLE = 'signedPreKeys'; +async function createOrUpdateSignedPreKey(data) { + return createOrUpdate(SIGNED_PRE_KEYS_TABLE, data); +} +async function getSignedPreKeyById(id) { + return getById(SIGNED_PRE_KEYS_TABLE, id); +} +async function getAllSignedPreKeys() { + const rows = await db.all('SELECT json FROM signedPreKeys ORDER BY id ASC;'); + return map(rows, row => jsonToObject(row.json)); +} +async function bulkAddSignedPreKeys(array) { + return bulkAdd(SIGNED_PRE_KEYS_TABLE, array); +} +async function removeSignedPreKeyById(id) { + return removeById(SIGNED_PRE_KEYS_TABLE, id); +} +async function removeAllSignedPreKeys() { + return removeAllFromTable(SIGNED_PRE_KEYS_TABLE); +} + +const ITEMS_TABLE = 'items'; +async function createOrUpdateItem(data) { + return createOrUpdate(ITEMS_TABLE, data); +} +async function getItemById(id) { + return getById(ITEMS_TABLE, id); +} +async function getAllItems() { + const rows = await db.all('SELECT json FROM items ORDER BY id ASC;'); + return map(rows, row => jsonToObject(row.json)); +} +async function bulkAddItems(array) { + return bulkAdd(ITEMS_TABLE, array); +} +async function removeItemById(id) { + return removeById(ITEMS_TABLE, id); +} +async function removeAllItems() { + return removeAllFromTable(ITEMS_TABLE); +} + +const SESSIONS_TABLE = 'sessions'; +async function createOrUpdateSession(data) { + const { id, number } = data; + if (!id) { + throw new Error( + 'createOrUpdateSession: Provided data did not have a truthy id' + ); + } + if (!number) { + throw new Error( + 'createOrUpdateSession: Provided data did not have a truthy number' + ); + } + + await db.run( + `INSERT OR REPLACE INTO sessions ( + id, + number, + json + ) values ( + $id, + $number, + $json + )`, + { + $id: id, + $number: number, + $json: objectToJSON(data), + } + ); +} +async function getSessionById(id) { + return getById(SESSIONS_TABLE, id); +} +async function getSessionsByNumber(number) { + const rows = await db.all('SELECT * FROM sessions WHERE number = $number;', { + $number: number, + }); + return map(rows, row => jsonToObject(row.json)); +} +async function bulkAddSessions(array) { + return bulkAdd(SESSIONS_TABLE, array); +} +async function removeSessionById(id) { + return removeById(SESSIONS_TABLE, id); +} +async function removeSessionsByNumber(number) { + await db.run('DELETE FROM sessions WHERE number = $number;', { + $number: number, + }); +} +async function removeAllSessions() { + return removeAllFromTable(SESSIONS_TABLE); +} + +async function createOrUpdate(table, data) { + const { id } = data; + if (!id) { + throw new Error('createOrUpdate: Provided data did not have a truthy id'); + } + + await db.run( + `INSERT OR REPLACE INTO ${table} ( + id, + json + ) values ( + $id, + $json + )`, + { + $id: id, + $json: objectToJSON(data), + } + ); +} + +async function bulkAdd(table, array) { + let promise; + + db.serialize(() => { + promise = Promise.all([ + db.run('BEGIN TRANSACTION;'), + ...map(array, data => createOrUpdate(table, data)), + db.run('COMMIT TRANSACTION;'), + ]); + }); + + await promise; +} + +async function getById(table, id) { + const row = await db.get(`SELECT * FROM ${table} WHERE id = $id;`, { + $id: id, + }); + + if (!row) { + return null; + } + + return jsonToObject(row.json); +} + +async function removeById(table, id) { + if (!Array.isArray(id)) { + await db.run(`DELETE FROM ${table} WHERE id = $id;`, { $id: id }); + return; + } + + if (!id.length) { + throw new Error('removeById: No ids to delete!'); + } + + // Our node interface doesn't seem to allow you to replace one single ? with an array + await db.run( + `DELETE FROM ${table} WHERE id IN ( ${id.map(() => '?').join(', ')} );`, + id + ); +} + +async function removeAllFromTable(table) { + await db.run(`DELETE FROM ${table};`); +} + +// Conversations + async function getConversationCount() { const row = await db.get('SELECT count(*) from conversations;'); @@ -1007,15 +1331,42 @@ async function removeAllUnprocessed() { await db.run('DELETE FROM unprocessed;'); } +// All data in database async function removeAll() { let promise; db.serialize(() => { promise = Promise.all([ db.run('BEGIN TRANSACTION;'), + db.run('DELETE FROM conversations;'), + db.run('DELETE FROM groups;'), + db.run('DELETE FROM identityKeys;'), + db.run('DELETE FROM items;'), db.run('DELETE FROM messages;'), + db.run('DELETE FROM preKeys;'), + db.run('DELETE FROM sessions;'), + db.run('DELETE FROM signedPreKeys;'), + db.run('DELETE FROM unprocessed;'), + db.run('COMMIT TRANSACTION;'), + ]); + }); + + await promise; +} + +// Anything that isn't user-visible data +async function removeAllConfiguration() { + let promise; + + db.serialize(() => { + promise = Promise.all([ + db.run('BEGIN TRANSACTION;'), + db.run('DELETE FROM identityKeys;'), + db.run('DELETE FROM items;'), + db.run('DELETE FROM preKeys;'), + db.run('DELETE FROM sessions;'), + db.run('DELETE FROM signedPreKeys;'), db.run('DELETE FROM unprocessed;'), - db.run('DELETE from conversations;'), db.run('COMMIT TRANSACTION;'), ]); }); diff --git a/background.html b/background.html index 4c3f3759a626..4df989a3db2b 100644 --- a/background.html +++ b/background.html @@ -590,6 +590,7 @@ + diff --git a/js/background.js b/js/background.js index 223df919f5dd..b9c2ee37dbd6 100644 --- a/js/background.js +++ b/js/background.js @@ -125,16 +125,18 @@ window.setImmediate = window.nodeSetImmediate; const { IdleDetector, MessageDataMigrator } = Signal.Workflow; + const { + mandatoryMessageUpgrade, + migrateAllToSQLCipher, + removeDatabase, + runMigrations, + doesDatabaseExist, + } = Signal.IndexedDB; const { Errors, Message } = window.Signal.Types; const { upgradeMessageSchema, writeNewAttachmentData, deleteAttachmentData, - getCurrentVersion, - } = window.Signal.Migrations; - const { - Migrations0DatabaseWithAttachmentData, - Migrations1DatabaseWithoutAttachmentData, } = window.Signal.Migrations; const { Views } = window.Signal; @@ -184,16 +186,13 @@ }; const cancelInitializationMessage = Views.Initialization.setMessage(); - window.log.info('Start IndexedDB migrations'); - window.log.info('Run migrations on database with attachment data'); - await Migrations0DatabaseWithAttachmentData.run({ - Backbone, - logger: window.log, - }); - - const latestDBVersion2 = await getCurrentVersion(); - Whisper.Database.migrations[0].version = latestDBVersion2; + const isIndexedDBPresent = await doesDatabaseExist(); + if (isIndexedDBPresent) { + window.installStorage(window.legacyStorage); + window.log.info('Start IndexedDB migrations'); + await runMigrations(); + } window.log.info('Storage fetch'); storage.fetch(); @@ -294,121 +293,17 @@ ); } - const MINIMUM_VERSION = 7; - async function upgradeMessages() { - const NUM_MESSAGES_PER_BATCH = 10; - window.log.info( - 'upgradeMessages: Mandatory message schema upgrade started.', - `Target version: ${MINIMUM_VERSION}` - ); + if (isIndexedDBPresent) { + await mandatoryMessageUpgrade({ upgradeMessageSchema }); + await migrateAllToSQLCipher({ writeNewAttachmentData, Views }); + await removeDatabase(); - let isMigrationWithoutIndexComplete = false; - while (!isMigrationWithoutIndexComplete) { - const database = Migrations0DatabaseWithAttachmentData.getDatabase(); - // eslint-disable-next-line no-await-in-loop - const batchWithoutIndex = await MessageDataMigrator.processNextBatchWithoutIndex( - { - databaseName: database.name, - minDatabaseVersion: database.version, - numMessagesPerBatch: NUM_MESSAGES_PER_BATCH, - upgradeMessageSchema, - maxVersion: MINIMUM_VERSION, - BackboneMessage: Whisper.Message, - saveMessage: window.Signal.Data.saveLegacyMessage, - } - ); - window.log.info( - 'upgradeMessages: upgrade without index', - batchWithoutIndex - ); - isMigrationWithoutIndexComplete = batchWithoutIndex.done; - } - window.log.info('upgradeMessages: upgrade without index complete!'); - - let isMigrationWithIndexComplete = false; - while (!isMigrationWithIndexComplete) { - // eslint-disable-next-line no-await-in-loop - const batchWithIndex = await MessageDataMigrator.processNext({ - BackboneMessage: Whisper.Message, - BackboneMessageCollection: Whisper.MessageCollection, - numMessagesPerBatch: NUM_MESSAGES_PER_BATCH, - upgradeMessageSchema, - getMessagesNeedingUpgrade: - window.Signal.Data.getLegacyMessagesNeedingUpgrade, - saveMessage: window.Signal.Data.saveLegacyMessage, - maxVersion: MINIMUM_VERSION, - }); - window.log.info('upgradeMessages: upgrade with index', batchWithIndex); - isMigrationWithIndexComplete = batchWithIndex.done; - } - window.log.info('upgradeMessages: upgrade with index complete!'); - - window.log.info('upgradeMessages: Message schema upgrade complete'); + window.installStorage(window.newStorage); + await window.storage.fetch(); } - await upgradeMessages(); - - const db = await Whisper.Database.open(); - let totalMessages; - try { - totalMessages = await MessageDataMigrator.getNumMessages({ - connection: db, - }); - } catch (error) { - window.log.error( - 'background.getNumMessages error:', - error && error.stack ? error.stack : error - ); - totalMessages = 0; - } - - function showMigrationStatus(current) { - const status = `${current}/${totalMessages}`; - Views.Initialization.setMessage( - window.i18n('migratingToSQLCipher', [status]) - ); - } - - if (totalMessages) { - window.log.info(`About to migrate ${totalMessages} messages`); - showMigrationStatus(0); - } else { - window.log.info('About to migrate non-messages'); - } - - await window.Signal.migrateToSQL({ - db, - clearStores: Whisper.Database.clearStores, - handleDOMException: Whisper.Database.handleDOMException, - arrayBufferToString: textsecure.MessageReceiver.arrayBufferToStringBase64, - countCallback: count => { - window.log.info(`Migration: ${count} messages complete`); - showMigrationStatus(count); - }, - writeNewAttachmentData, - }); - - db.close(); - Views.Initialization.setMessage(window.i18n('optimizingApplication')); - window.log.info('Running cleanup IndexedDB migrations...'); - // Close all previous connections to the database first - await Whisper.Database.close(); - - // Now we clean up IndexedDB database after extracting data from it - await Migrations1DatabaseWithoutAttachmentData.run({ - Backbone, - logger: window.log, - }); - - await Whisper.Database.close(); - - const latestDBVersion = _.last( - Migrations1DatabaseWithoutAttachmentData.migrations - ).version; - Whisper.Database.migrations[0].version = latestDBVersion; - window.log.info('Cleanup: starting...'); const messagesForCleanup = await window.Signal.Data.getOutgoingWithoutExpiresAt( { diff --git a/js/legacy_storage.js b/js/legacy_storage.js new file mode 100644 index 000000000000..5a1f1e2d8c4e --- /dev/null +++ b/js/legacy_storage.js @@ -0,0 +1,92 @@ +/* global Backbone, Whisper */ + +/* eslint-disable more/no-then */ + +// eslint-disable-next-line func-names +(function() { + 'use strict'; + + window.Whisper = window.Whisper || {}; + const Item = Backbone.Model.extend({ + database: Whisper.Database, + storeName: 'items', + }); + const ItemCollection = Backbone.Collection.extend({ + model: Item, + storeName: 'items', + database: Whisper.Database, + }); + + let ready = false; + const items = new ItemCollection(); + items.on('reset', () => { + ready = true; + }); + window.legacyStorage = { + /** *************************** + *** Base Storage Routines *** + **************************** */ + put(key, value) { + if (value === undefined) { + throw new Error('Tried to store undefined'); + } + if (!ready) { + window.log.warn( + 'Called storage.put before storage is ready. key:', + key + ); + } + const item = items.add({ id: key, value }, { merge: true }); + return new Promise((resolve, reject) => { + item.save().then(resolve, reject); + }); + }, + + get(key, defaultValue) { + const item = items.get(`${key}`); + if (!item) { + return defaultValue; + } + return item.get('value'); + }, + + remove(key) { + const item = items.get(`${key}`); + if (item) { + items.remove(item); + return new Promise((resolve, reject) => { + item.destroy().then(resolve, reject); + }); + } + return Promise.resolve(); + }, + + onready(callback) { + if (ready) { + callback(); + } else { + items.on('reset', callback); + } + }, + + fetch() { + return new Promise((resolve, reject) => { + items + .fetch({ reset: true }) + .fail(() => + reject( + new Error( + 'Failed to fetch from storage.' + + ' This may be due to an unexpected database version.' + ) + ) + ) + .always(resolve); + }); + }, + + reset() { + items.reset(); + }, + }; +})(); diff --git a/js/models/conversations.js b/js/models/conversations.js index 28d64d5165a1..df6eb836b3b7 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -47,7 +47,6 @@ ]; Whisper.Conversation = Backbone.Model.extend({ - database: Whisper.Database, storeName: 'conversations', defaults() { return { @@ -1665,8 +1664,6 @@ }); Whisper.ConversationCollection = Backbone.Collection.extend({ - database: Whisper.Database, - storeName: 'conversations', model: Whisper.Conversation, comparator(m) { diff --git a/js/models/messages.js b/js/models/messages.js index a1d9f6c49383..225474714174 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -65,9 +65,6 @@ window.hasSignalAccount = number => window.AccountCache[number]; window.Whisper.Message = Backbone.Model.extend({ - // Keeping this for legacy upgrade pre-migrate to SQLCipher - database: Whisper.Database, - storeName: 'messages', initialize(attributes) { if (_.isObject(attributes)) { this.set( @@ -1418,9 +1415,6 @@ Whisper.MessageCollection = Backbone.Collection.extend({ model: Whisper.Message, - // Keeping this for legacy upgrade pre-migrate to SQLCipher - database: Whisper.Database, - storeName: 'messages', comparator(left, right) { if (left.get('received_at') === right.get('received_at')) { return (left.get('sent_at') || 0) - (right.get('sent_at') || 0); diff --git a/js/modules/backup.js b/js/modules/backup.js index 72ee105bf4a7..7bd0c95cccc5 100644 --- a/js/modules/backup.js +++ b/js/modules/backup.js @@ -207,10 +207,10 @@ function exportContactsAndGroups(db, fileWriter) { }); } -async function importNonMessages(db, parent, options) { +async function importNonMessages(parent, options) { const file = 'db.json'; const string = await readFileAsText(parent, file); - return importFromJsonString(db, string, path.join(parent, file), options); + return importFromJsonString(string, path.join(parent, file), options); } function eliminateClientConfigInBackup(data, targetPath) { @@ -265,7 +265,7 @@ async function importConversationsFromJSON(conversations, options) { ); } -async function importFromJsonString(db, jsonString, targetPath, options) { +async function importFromJsonString(jsonString, targetPath, options) { options = options || {}; _.defaults(options, { forceLightImport: false, @@ -278,136 +278,96 @@ async function importFromJsonString(db, jsonString, targetPath, options) { fullImport: true, }; - return new Promise(async (resolve, reject) => { - const importObject = JSON.parse(jsonString); - delete importObject.debug; + const importObject = JSON.parse(jsonString); + delete importObject.debug; - if (!importObject.sessions || options.forceLightImport) { - result.fullImport = false; + if (!importObject.sessions || options.forceLightImport) { + result.fullImport = false; - delete importObject.items; - delete importObject.signedPreKeys; - delete importObject.preKeys; - delete importObject.identityKeys; - delete importObject.sessions; - delete importObject.unprocessed; - - window.log.info( - 'This is a light import; contacts, groups and messages only' - ); - } - - // We mutate the on-disk backup to prevent the user from importing client - // configuration more than once - that causes lots of encryption errors. - // This of course preserves the true data: conversations and groups. - eliminateClientConfigInBackup(importObject, targetPath); - - const storeNames = _.keys(importObject); - window.log.info('Importing to these stores:', storeNames.join(', ')); - - let finished = false; - const finish = via => { - window.log.info('non-messages import done via', via); - if (finished) { - resolve(result); - } - finished = true; - }; - - // Special-case conversations key here, going to SQLCipher - const { conversations } = importObject; - const remainingStoreNames = _.without( - storeNames, - 'conversations', - 'unprocessed' - ); - try { - await importConversationsFromJSON(conversations, options); - } catch (error) { - reject(error); - } - - // Because the 'are we done?' check below looks at the keys remaining in importObject - delete importObject.conversations; + delete importObject.items; + delete importObject.signedPreKeys; + delete importObject.preKeys; + delete importObject.identityKeys; + delete importObject.sessions; delete importObject.unprocessed; - // The rest go to IndexedDB - const transaction = db.transaction(remainingStoreNames, 'readwrite'); - transaction.onerror = () => { - Whisper.Database.handleDOMException( - 'importFromJsonString transaction error', - transaction.error, - reject - ); - }; - transaction.oncomplete = finish.bind(null, 'transaction complete'); + window.log.info( + 'This is a light import; contacts, groups and messages only' + ); + } - _.each(remainingStoreNames, storeName => { - const items = importObject[storeName]; + // We mutate the on-disk backup to prevent the user from importing client + // configuration more than once - that causes lots of encryption errors. + // This of course preserves the true data: conversations and groups. + eliminateClientConfigInBackup(importObject, targetPath); - window.log.info('Importing items for store', storeName); + const storeNames = _.keys(importObject); + window.log.info('Importing to these stores:', storeNames.join(', ')); - let count = 0; - let skipCount = 0; + // Special-case conversations key here, going to SQLCipher + const { conversations } = importObject; + const remainingStoreNames = _.without( + storeNames, + 'conversations', + 'unprocessed' + ); + await importConversationsFromJSON(conversations, options); - const finishStore = () => { - // added all objects for this store - delete importObject[storeName]; - window.log.info( - 'Done importing to store', - storeName, - 'Total count:', - count, - 'Skipped:', - skipCount + const SAVE_FUNCTIONS = { + groups: window.Signal.Data.createOrUpdateGroup, + identityKeys: window.Signal.Data.createOrUpdateIdentityKey, + items: window.Signal.Data.createOrUpdateItem, + preKeys: window.Signal.Data.createOrUpdatePreKey, + sessions: window.Signal.Data.createOrUpdateSession, + signedPreKeys: window.Signal.Data.createOrUpdateSignedPreKey, + }; + + await Promise.all( + _.map(remainingStoreNames, async storeName => { + const save = SAVE_FUNCTIONS[storeName]; + if (!_.isFunction(save)) { + throw new Error( + `importFromJsonString: Didn't have save function for store ${storeName}` ); - if (_.keys(importObject).length === 0) { - // added all object stores - window.log.info('DB import complete'); - finish('puts scheduled'); - } - }; + } - if (!items || !items.length) { - finishStore(); + window.log.info(`Importing items for store ${storeName}`); + const toImport = importObject[storeName]; + + if (!toImport || !toImport.length) { + window.log.info(`No items in ${storeName} store`); return; } - _.each(items, toAdd => { - toAdd = unstringify(toAdd); + let skipCount = 0; + + for (let i = 0, max = toImport.length; i < max; i += 1) { + const toAdd = unstringify(toImport[i]); const haveGroupAlready = storeName === 'groups' && groupLookup[getGroupKey(toAdd)]; if (haveGroupAlready) { skipCount += 1; - count += 1; - return; + } else { + // eslint-disable-next-line no-await-in-loop + await save(toAdd); } - - const request = transaction.objectStore(storeName).put(toAdd, toAdd.id); - request.onsuccess = () => { - count += 1; - if (count + skipCount >= items.length) { - finishStore(); - } - }; - request.onerror = () => { - Whisper.Database.handleDOMException( - `importFromJsonString request error (store: ${storeName})`, - request.error, - reject - ); - }; - }); - - // We have to check here, because we may have skipped every item, resulting - // in no onsuccess callback at all. - if (skipCount === count) { - finishStore(); } - }); - }); + + window.log.info( + 'Done importing to store', + storeName, + 'Total count:', + toImport.length, + 'Skipped:', + skipCount + ); + }) + ); + + window.log.info('DB import complete'); + return result; } function createDirectory(parent, name) { @@ -1043,11 +1003,11 @@ async function loadAttachments(dir, getName, options) { // TODO: Handle video screenshots, and image/video thumbnails } -function saveMessage(db, message) { - return saveAllMessages(db, [message]); +function saveMessage(message) { + return saveAllMessages([message]); } -async function saveAllMessages(db, rawMessages) { +async function saveAllMessages(rawMessages) { if (rawMessages.length === 0) { return; } @@ -1085,7 +1045,7 @@ async function saveAllMessages(db, rawMessages) { // message, save it, and only then do we move on to the next message. Thus, every // message with attachments needs to be removed from our overall message save with the // filter() call. -async function importConversation(db, dir, options) { +async function importConversation(dir, options) { options = options || {}; _.defaults(options, { messageLookup: {} }); @@ -1141,7 +1101,7 @@ async function importConversation(db, dir, options) { message, key, }); - return saveMessage(db, message); + return saveMessage(message); }; // eslint-disable-next-line more/no-then @@ -1153,7 +1113,7 @@ async function importConversation(db, dir, options) { return true; }); - await saveAllMessages(db, messages); + await saveAllMessages(messages); await promiseChain; window.log.info( @@ -1166,7 +1126,7 @@ async function importConversation(db, dir, options) { ); } -async function importConversations(db, dir, options) { +async function importConversations(dir, options) { const contents = await getDirContents(dir); let promiseChain = Promise.resolve(); @@ -1175,8 +1135,7 @@ async function importConversations(db, dir, options) { return; } - const loadConversation = () => - importConversation(db, conversationDir, options); + const loadConversation = () => importConversation(conversationDir, options); // eslint-disable-next-line more/no-then promiseChain = promiseChain.then(loadConversation); @@ -1211,46 +1170,9 @@ async function loadConversationLookup() { function getGroupKey(group) { return group.id; } -function loadGroupsLookup(db) { - return assembleLookup(db, 'groups', getGroupKey); -} - -function assembleLookup(db, storeName, keyFunction) { - const lookup = Object.create(null); - - return new Promise((resolve, reject) => { - const transaction = db.transaction(storeName, 'readwrite'); - transaction.onerror = () => { - Whisper.Database.handleDOMException( - `assembleLookup(${storeName}) transaction error`, - transaction.error, - reject - ); - }; - transaction.oncomplete = () => { - // not really very useful - fires at unexpected times - }; - - const store = transaction.objectStore(storeName); - const request = store.openCursor(); - request.onerror = () => { - Whisper.Database.handleDOMException( - `assembleLookup(${storeName}) request error`, - request.error, - reject - ); - }; - request.onsuccess = event => { - const cursor = event.target.result; - if (cursor && cursor.value) { - lookup[keyFunction(cursor.value)] = true; - cursor.continue(); - } else { - window.log.info(`Done creating ${storeName} lookup`); - resolve(lookup); - } - }; - }); +async function loadGroupsLookup() { + const array = await window.Signal.Data.getAllGroupIds(); + return fromPairs(map(array, item => [getGroupKey(item), true])); } function getDirectoryForExport() { @@ -1383,11 +1305,10 @@ async function importFromDirectory(directory, options) { options = options || {}; try { - const db = await Whisper.Database.open(); const lookups = await Promise.all([ - loadMessagesLookup(db), - loadConversationLookup(db), - loadGroupsLookup(db), + loadMessagesLookup(), + loadConversationLookup(), + loadGroupsLookup(), ]); const [messageLookup, conversationLookup, groupLookup] = lookups; options = Object.assign({}, options, { @@ -1422,8 +1343,8 @@ async function importFromDirectory(directory, options) { options = Object.assign({}, options, { attachmentsDir, }); - const result = await importNonMessages(db, stagingDir, options); - await importConversations(db, stagingDir, Object.assign({}, options)); + const result = await importNonMessages(stagingDir, options); + await importConversations(stagingDir, Object.assign({}, options)); window.log.info('Done importing from backup!'); return result; @@ -1437,8 +1358,8 @@ async function importFromDirectory(directory, options) { } } - const result = await importNonMessages(db, directory, options); - await importConversations(db, directory, options); + const result = await importNonMessages(directory, options); + await importConversations(directory, options); window.log.info('Done importing!'); return result; diff --git a/js/modules/data.js b/js/modules/data.js index 7d51b9416117..b31c92328926 100644 --- a/js/modules/data.js +++ b/js/modules/data.js @@ -1,10 +1,19 @@ -/* global window, setTimeout */ +/* global window, setTimeout, IDBKeyRange */ const electron = require('electron'); -const { forEach, isFunction, isObject, merge } = require('lodash'); +const { + cloneDeep, + forEach, + get, + isFunction, + isObject, + map, + merge, + set, +} = require('lodash'); -const { deferredToPromise } = require('./deferred_to_promise'); +const { base64ToArrayBuffer, arrayBufferToBase64 } = require('./crypto'); const MessageType = require('./types/message'); const { ipcRenderer } = electron; @@ -13,11 +22,6 @@ const { ipcRenderer } = electron; // any warnings that might be sent to the console in that case. ipcRenderer.setMaxListeners(0); -// calls to search for when finding functions to convert: -// .fetch( -// .save( -// .destroy( - const DATABASE_UPDATE_TIMEOUT = 2 * 60 * 1000; // two minutes const SQL_CHANNEL_KEY = 'sql-channel'; @@ -38,6 +42,47 @@ module.exports = { close, removeDB, + createOrUpdateGroup, + getGroupById, + getAllGroupIds, + bulkAddGroups, + removeGroupById, + removeAllGroups, + + createOrUpdateIdentityKey, + getIdentityKeyById, + bulkAddIdentityKeys, + removeIdentityKeyById, + removeAllIdentityKeys, + + createOrUpdatePreKey, + getPreKeyById, + bulkAddPreKeys, + removePreKeyById, + removeAllPreKeys, + + createOrUpdateSignedPreKey, + getSignedPreKeyById, + getAllSignedPreKeys, + bulkAddSignedPreKeys, + removeSignedPreKeyById, + removeAllSignedPreKeys, + + createOrUpdateItem, + getItemById, + getAllItems, + bulkAddItems, + removeItemById, + removeAllItems, + + createOrUpdateSession, + getSessionById, + getSessionsByNumber, + bulkAddSessions, + removeSessionById, + removeSessionsByNumber, + removeAllSessions, + getConversationCount, saveConversation, saveConversations, @@ -81,6 +126,8 @@ module.exports = { removeAllUnprocessed, removeAll, + removeAllConfiguration, + removeOtherData, cleanupOrphanedAttachments, @@ -229,6 +276,36 @@ forEach(module.exports, fn => { } }); +function keysToArrayBuffer(keys, data) { + const updated = cloneDeep(data); + for (let i = 0, max = keys.length; i < max; i += 1) { + const key = keys[i]; + const value = get(data, key); + + if (value) { + set(updated, key, base64ToArrayBuffer(value)); + } + } + + return updated; +} + +function keysFromArrayBuffer(keys, data) { + const updated = cloneDeep(data); + for (let i = 0, max = keys.length; i < max; i += 1) { + const key = keys[i]; + const value = get(data, key); + + if (value) { + set(updated, key, arrayBufferToBase64(value)); + } + } + + return updated; +} + +// Top-level calls + // Note: will need to restart the app after calling this, to set up afresh async function close() { await channels.close(); @@ -239,6 +316,182 @@ async function removeDB() { await channels.removeDB(); } +// Groups + +async function createOrUpdateGroup(data) { + await channels.createOrUpdateGroup(data); +} +async function getGroupById(id) { + const group = await channels.getGroupById(id); + return group; +} +async function getAllGroupIds() { + const ids = await channels.getAllGroupIds(); + return ids; +} +async function bulkAddGroups(array) { + await channels.bulkAddGroups(array); +} +async function removeGroupById(id) { + await channels.removeGroupById(id); +} +async function removeAllGroups() { + await channels.removeAllGroups(); +} + +// Identity Keys + +const IDENTITY_KEY_KEYS = ['publicKey']; +async function createOrUpdateIdentityKey(data) { + const updated = keysFromArrayBuffer(IDENTITY_KEY_KEYS, data); + await channels.createOrUpdateIdentityKey(updated); +} +async function getIdentityKeyById(id) { + const data = await channels.getIdentityKeyById(id); + return keysToArrayBuffer(IDENTITY_KEY_KEYS, data); +} +async function bulkAddIdentityKeys(array) { + const updated = map(array, data => + keysFromArrayBuffer(IDENTITY_KEY_KEYS, data) + ); + await channels.bulkAddIdentityKeys(updated); +} +async function removeIdentityKeyById(id) { + await channels.removeIdentityKeyById(id); +} +async function removeAllIdentityKeys() { + await channels.removeAllIdentityKeys(); +} + +// Pre Keys + +async function createOrUpdatePreKey(data) { + const updated = keysFromArrayBuffer(PRE_KEY_KEYS, data); + await channels.createOrUpdatePreKey(updated); +} +async function getPreKeyById(id) { + const data = await channels.getPreKeyById(id); + return keysToArrayBuffer(PRE_KEY_KEYS, data); +} +async function bulkAddPreKeys(array) { + const updated = map(array, data => keysFromArrayBuffer(PRE_KEY_KEYS, data)); + await channels.bulkAddPreKeys(updated); +} +async function removePreKeyById(id) { + await channels.removePreKeyById(id); +} +async function removeAllPreKeys() { + await channels.removeAllPreKeys(); +} + +// Signed Pre Keys + +const PRE_KEY_KEYS = ['privateKey', 'publicKey']; +async function createOrUpdateSignedPreKey(data) { + const updated = keysFromArrayBuffer(PRE_KEY_KEYS, data); + await channels.createOrUpdateSignedPreKey(updated); +} +async function getSignedPreKeyById(id) { + const data = await channels.getSignedPreKeyById(id); + return keysToArrayBuffer(PRE_KEY_KEYS, data); +} +async function getAllSignedPreKeys() { + const keys = await channels.getAllSignedPreKeys(); + return keys; +} +async function bulkAddSignedPreKeys(array) { + const updated = map(array, data => keysFromArrayBuffer(PRE_KEY_KEYS, data)); + await channels.bulkAddSignedPreKeys(updated); +} +async function removeSignedPreKeyById(id) { + await channels.removeSignedPreKeyById(id); +} +async function removeAllSignedPreKeys() { + await channels.removeAllSignedPreKeys(); +} + +// Items + +const ITEM_KEYS = { + identityKey: ['value.pubKey', 'value.privKey'], + senderCertificate: [ + 'value.certificate', + 'value.signature', + 'value.serialized', + ], + signaling_key: ['value'], + profileKey: ['value'], +}; +async function createOrUpdateItem(data) { + const { id } = data; + if (!id) { + throw new Error( + 'createOrUpdateItem: Provided data did not have a truthy id' + ); + } + + const keys = ITEM_KEYS[id]; + const updated = Array.isArray(keys) ? keysFromArrayBuffer(keys, data) : data; + + await channels.createOrUpdateItem(updated); +} +async function getItemById(id) { + const keys = ITEM_KEYS[id]; + const data = await channels.getItemById(id); + + return Array.isArray(keys) ? keysToArrayBuffer(keys, data) : data; +} +async function getAllItems() { + const items = await channels.getAllItems(); + return map(items, item => { + const { id } = item; + const keys = ITEM_KEYS[id]; + return Array.isArray(keys) ? keysToArrayBuffer(keys, item) : item; + }); +} +async function bulkAddItems(array) { + const updated = map(array, data => { + const { id } = data; + const keys = ITEM_KEYS[id]; + return Array.isArray(keys) ? keysFromArrayBuffer(keys, data) : data; + }); + await channels.bulkAddItems(updated); +} +async function removeItemById(id) { + await channels.removeItemById(id); +} +async function removeAllItems() { + await channels.removeAllItems(); +} + +// Sessions + +async function createOrUpdateSession(data) { + await channels.createOrUpdateSession(data); +} +async function getSessionById(id) { + const session = await channels.getSessionById(id); + return session; +} +async function getSessionsByNumber(number) { + const sessions = await channels.getSessionsByNumber(number); + return sessions; +} +async function bulkAddSessions(array) { + await channels.bulkAddSessions(array); +} +async function removeSessionById(id) { + await channels.removeSessionById(id); +} +async function removeSessionsByNumber(number) { + await channels.removeSessionsByNumber(number); +} +async function removeAllSessions(id) { + await channels.removeAllSessions(id); +} + +// Conversation + async function getConversationCount() { return channels.getConversationCount(); } @@ -319,6 +572,8 @@ async function searchConversations(query, { ConversationCollection }) { return collection; } +// Message + async function getMessageCount() { return channels.getMessageCount(); } @@ -329,10 +584,41 @@ async function saveMessage(data, { forceSave, Message } = {}) { return id; } -async function saveLegacyMessage(data, { Message }) { - const message = new Message(data); - await deferredToPromise(message.save()); - return message.id; +async function saveLegacyMessage(data) { + const db = await window.Whisper.Database.open(); + try { + await new Promise((resolve, reject) => { + const transaction = db.transaction('messages', 'readwrite'); + + transaction.onerror = () => { + window.Whisper.Database.handleDOMException( + 'saveLegacyMessage transaction error', + transaction.error, + reject + ); + }; + transaction.oncomplete = resolve; + + const store = transaction.objectStore('messages'); + + if (!data.id) { + // eslint-disable-next-line no-param-reassign + data.id = window.getGuid(); + } + + const request = store.put(data, data.id); + request.onsuccess = resolve; + request.onerror = () => { + window.Whisper.Database.handleDOMException( + 'saveLegacyMessage request error', + request.error, + reject + ); + }; + }); + } finally { + db.close(); + } } async function saveMessages(arrayOfMessages, { forceSave } = {}) { @@ -459,6 +745,8 @@ async function getNextExpiringMessage({ MessageCollection }) { return new MessageCollection(messages); } +// Unprocessed + async function getUnprocessedCount() { return channels.getUnprocessedCount(); } @@ -495,10 +783,16 @@ async function removeAllUnprocessed() { await channels.removeAllUnprocessed(); } +// Other + async function removeAll() { await channels.removeAll(); } +async function removeAllConfiguration() { + await channels.removeAllConfiguration(); +} + async function cleanupOrphanedAttachments() { await callChannel(CLEANUP_ORPHANED_ATTACHMENTS_KEY); } @@ -529,28 +823,61 @@ async function callChannel(name) { }); } -// Functions below here return JSON +// Functions below here return plain JSON instead of Backbone Models async function getLegacyMessagesNeedingUpgrade( limit, - { MessageCollection, maxVersion = MessageType.CURRENT_SCHEMA_VERSION } + { maxVersion = MessageType.CURRENT_SCHEMA_VERSION } ) { - const messages = new MessageCollection(); + const db = await window.Whisper.Database.open(); + try { + await new Promise((resolve, reject) => { + const transaction = db.transaction('messages', 'readonly'); + const messages = []; - await deferredToPromise( - messages.fetch({ - limit, - index: { - name: 'schemaVersion', - upper: maxVersion, - excludeUpper: true, - order: 'desc', - }, - }) - ); + transaction.onerror = () => { + window.Whisper.Database.handleDOMException( + 'getLegacyMessagesNeedingUpgrade transaction error', + transaction.error, + reject + ); + }; + transaction.oncomplete = () => { + resolve(messages); + }; - const models = messages.models || []; - return models.map(model => model.toJSON()); + const store = transaction.objectStore('messages'); + const index = store.index('schemaVersion'); + const range = IDBKeyRange.upperBound(maxVersion, true); + + const request = index.openCursor(range); + let count = 0; + + request.onsuccess = event => { + const cursor = event.target.result; + + if (cursor) { + count += 1; + messages.push(cursor.value); + + if (count >= limit) { + return; + } + + cursor.continue(); + } + }; + request.onerror = () => { + window.Whisper.Database.handleDOMException( + 'getLegacyMessagesNeedingUpgrade request error', + request.error, + reject + ); + }; + }); + } finally { + db.close(); + } } async function getMessagesNeedingUpgrade( diff --git a/js/modules/indexeddb.js b/js/modules/indexeddb.js new file mode 100644 index 000000000000..9bdceb5367cd --- /dev/null +++ b/js/modules/indexeddb.js @@ -0,0 +1,168 @@ +/* global window, Whisper, textsecure */ + +const { isFunction } = require('lodash'); + +const MessageDataMigrator = require('./messages_data_migrator'); +const { + run, + getLatestVersion, + getDatabase, +} = require('./migrations/migrations'); + +const MESSAGE_MINIMUM_VERSION = 7; + +module.exports = { + doesDatabaseExist, + mandatoryMessageUpgrade, + MESSAGE_MINIMUM_VERSION, + migrateAllToSQLCipher, + removeDatabase, + runMigrations, +}; + +async function runMigrations() { + window.log.info('Run migrations on database with attachment data'); + await run({ + Backbone: window.Backbone, + logger: window.log, + }); + + Whisper.Database.migrations[0].version = getLatestVersion(); +} + +async function mandatoryMessageUpgrade({ upgradeMessageSchema } = {}) { + if (!isFunction(upgradeMessageSchema)) { + throw new Error( + 'mandatoryMessageUpgrade: upgradeMessageSchema must be a function!' + ); + } + + const NUM_MESSAGES_PER_BATCH = 10; + window.log.info( + 'upgradeMessages: Mandatory message schema upgrade started.', + `Target version: ${MESSAGE_MINIMUM_VERSION}` + ); + + let isMigrationWithoutIndexComplete = false; + while (!isMigrationWithoutIndexComplete) { + const database = getDatabase(); + // eslint-disable-next-line no-await-in-loop + const batchWithoutIndex = await MessageDataMigrator.processNextBatchWithoutIndex( + { + databaseName: database.name, + minDatabaseVersion: database.version, + numMessagesPerBatch: NUM_MESSAGES_PER_BATCH, + upgradeMessageSchema, + maxVersion: MESSAGE_MINIMUM_VERSION, + BackboneMessage: Whisper.Message, + saveMessage: window.Signal.Data.saveLegacyMessage, + } + ); + window.log.info( + 'upgradeMessages: upgrade without index', + batchWithoutIndex + ); + isMigrationWithoutIndexComplete = batchWithoutIndex.done; + } + window.log.info('upgradeMessages: upgrade without index complete!'); + + let isMigrationWithIndexComplete = false; + while (!isMigrationWithIndexComplete) { + // eslint-disable-next-line no-await-in-loop + const batchWithIndex = await MessageDataMigrator.processNext({ + BackboneMessage: Whisper.Message, + BackboneMessageCollection: Whisper.MessageCollection, + numMessagesPerBatch: NUM_MESSAGES_PER_BATCH, + upgradeMessageSchema, + getMessagesNeedingUpgrade: + window.Signal.Data.getLegacyMessagesNeedingUpgrade, + saveMessage: window.Signal.Data.saveLegacyMessage, + maxVersion: MESSAGE_MINIMUM_VERSION, + }); + window.log.info('upgradeMessages: upgrade with index', batchWithIndex); + isMigrationWithIndexComplete = batchWithIndex.done; + } + window.log.info('upgradeMessages: upgrade with index complete!'); + + window.log.info('upgradeMessages: Message schema upgrade complete'); +} + +async function migrateAllToSQLCipher({ writeNewAttachmentData, Views } = {}) { + if (!isFunction(writeNewAttachmentData)) { + throw new Error( + 'migrateAllToSQLCipher: writeNewAttachmentData must be a function' + ); + } + if (!Views) { + throw new Error('migrateAllToSQLCipher: Views must be provided!'); + } + + let totalMessages; + const db = await Whisper.Database.open(); + + function showMigrationStatus(current) { + const status = `${current}/${totalMessages}`; + Views.Initialization.setMessage( + window.i18n('migratingToSQLCipher', [status]) + ); + } + + try { + totalMessages = await MessageDataMigrator.getNumMessages({ + connection: db, + }); + } catch (error) { + window.log.error( + 'background.getNumMessages error:', + error && error.stack ? error.stack : error + ); + totalMessages = 0; + } + + if (totalMessages) { + window.log.info(`About to migrate ${totalMessages} messages`); + showMigrationStatus(0); + } else { + window.log.info('About to migrate non-messages'); + } + + await window.Signal.migrateToSQL({ + db, + clearStores: Whisper.Database.clearStores, + handleDOMException: Whisper.Database.handleDOMException, + arrayBufferToString: textsecure.MessageReceiver.arrayBufferToStringBase64, + countCallback: count => { + window.log.info(`Migration: ${count} messages complete`); + showMigrationStatus(count); + }, + writeNewAttachmentData, + }); + + db.close(); +} + +async function doesDatabaseExist() { + return new Promise((resolve, reject) => { + const { id } = Whisper.Database; + const req = window.indexedDB.open(id); + + let existed = true; + + req.onerror = reject; + req.onsuccess = () => { + req.result.close(); + resolve(existed); + }; + req.onupgradeneeded = () => { + if (req.result.version === 1) { + existed = false; + window.indexedDB.deleteDatabase(id); + } + }; + }); +} + +function removeDatabase() { + window.log.info(`Deleting IndexedDB database '${Whisper.Database.id}'`); + window.indexedDB.deleteDatabase(Whisper.Database.id); +} diff --git a/js/modules/migrate_to_sql.js b/js/modules/migrate_to_sql.js index e64a73889ffc..00dba4df49d0 100644 --- a/js/modules/migrate_to_sql.js +++ b/js/modules/migrate_to_sql.js @@ -2,10 +2,26 @@ const { includes, isFunction, isString, last, map } = require('lodash'); const { + bulkAddGroups, + bulkAddSessions, + bulkAddIdentityKeys, + bulkAddPreKeys, + bulkAddSignedPreKeys, + bulkAddItems, + + removeGroupById, + removeSessionById, + removeIdentityKeyById, + removePreKeyById, + removeSignedPreKeyById, + removeItemById, + saveMessages, _removeMessages, + saveUnprocesseds, removeUnprocessed, + saveConversations, _removeConversations, } = require('./data'); @@ -132,6 +148,8 @@ async function migrateToSQL({ } complete = false; + lastIndex = null; + while (!complete) { // eslint-disable-next-line no-await-in-loop const status = await migrateStoreToSQLite({ @@ -163,6 +181,153 @@ async function migrateToSQL({ window.log.warn('Failed to clear conversations store'); } + complete = false; + lastIndex = null; + + while (!complete) { + // eslint-disable-next-line no-await-in-loop + const status = await migrateStoreToSQLite({ + db, + // eslint-disable-next-line no-loop-func + save: bulkAddGroups, + remove: removeGroupById, + storeName: 'groups', + handleDOMException, + lastIndex, + batchSize: 10, + }); + + ({ complete, lastIndex } = status); + } + window.log.info('migrateToSQL: migrate of groups complete'); + try { + await clearStores(['groups']); + } catch (error) { + window.log.warn('Failed to clear groups store'); + } + + complete = false; + lastIndex = null; + + while (!complete) { + // eslint-disable-next-line no-await-in-loop + const status = await migrateStoreToSQLite({ + db, + // eslint-disable-next-line no-loop-func + save: bulkAddSessions, + remove: removeSessionById, + storeName: 'sessions', + handleDOMException, + lastIndex, + batchSize: 10, + }); + + ({ complete, lastIndex } = status); + } + window.log.info('migrateToSQL: migrate of sessions complete'); + try { + await clearStores(['sessions']); + } catch (error) { + window.log.warn('Failed to clear sessions store'); + } + + complete = false; + lastIndex = null; + + while (!complete) { + // eslint-disable-next-line no-await-in-loop + const status = await migrateStoreToSQLite({ + db, + // eslint-disable-next-line no-loop-func + save: bulkAddIdentityKeys, + remove: removeIdentityKeyById, + storeName: 'identityKeys', + handleDOMException, + lastIndex, + batchSize: 10, + }); + + ({ complete, lastIndex } = status); + } + window.log.info('migrateToSQL: migrate of identityKeys complete'); + try { + await clearStores(['identityKeys']); + } catch (error) { + window.log.warn('Failed to clear identityKeys store'); + } + + complete = false; + lastIndex = null; + + while (!complete) { + // eslint-disable-next-line no-await-in-loop + const status = await migrateStoreToSQLite({ + db, + // eslint-disable-next-line no-loop-func + save: bulkAddPreKeys, + remove: removePreKeyById, + storeName: 'preKeys', + handleDOMException, + lastIndex, + batchSize: 10, + }); + + ({ complete, lastIndex } = status); + } + window.log.info('migrateToSQL: migrate of preKeys complete'); + try { + await clearStores(['preKeys']); + } catch (error) { + window.log.warn('Failed to clear preKeys store'); + } + + complete = false; + lastIndex = null; + + while (!complete) { + // eslint-disable-next-line no-await-in-loop + const status = await migrateStoreToSQLite({ + db, + // eslint-disable-next-line no-loop-func + save: bulkAddSignedPreKeys, + remove: removeSignedPreKeyById, + storeName: 'signedPreKeys', + handleDOMException, + lastIndex, + batchSize: 10, + }); + + ({ complete, lastIndex } = status); + } + window.log.info('migrateToSQL: migrate of signedPreKeys complete'); + try { + await clearStores(['signedPreKeys']); + } catch (error) { + window.log.warn('Failed to clear signedPreKeys store'); + } + + complete = false; + lastIndex = null; + + while (!complete) { + // eslint-disable-next-line no-await-in-loop + const status = await migrateStoreToSQLite({ + db, + // eslint-disable-next-line no-loop-func + save: bulkAddItems, + remove: removeItemById, + storeName: 'items', + handleDOMException, + lastIndex, + batchSize: 10, + }); + + ({ complete, lastIndex } = status); + } + window.log.info('migrateToSQL: migrate of items complete'); + // Note: we don't clear the items store because it contains important metadata which, + // if this process fails, will be crucial to going through this process again. + window.log.info('migrateToSQL: complete'); } diff --git a/js/modules/migrations/get_placeholder_migrations.js b/js/modules/migrations/get_placeholder_migrations.js index 62fe7c677b8d..3377096eae83 100644 --- a/js/modules/migrations/get_placeholder_migrations.js +++ b/js/modules/migrations/get_placeholder_migrations.js @@ -1,13 +1,13 @@ /* global window, Whisper */ -const Migrations0DatabaseWithAttachmentData = require('./migrations_0_database_with_attachment_data'); +const Migrations = require('./migrations'); exports.getPlaceholderMigrations = () => { - const last0MigrationVersion = Migrations0DatabaseWithAttachmentData.getLatestVersion(); + const version = Migrations.getLatestVersion(); return [ { - version: last0MigrationVersion, + version, migrate() { throw new Error( 'Unexpected invocation of placeholder migration!' + diff --git a/js/modules/migrations/migrations_0_database_with_attachment_data.js b/js/modules/migrations/migrations.js similarity index 94% rename from js/modules/migrations/migrations_0_database_with_attachment_data.js rename to js/modules/migrations/migrations.js index 76d70f0955b2..d3da5775924b 100644 --- a/js/modules/migrations/migrations_0_database_with_attachment_data.js +++ b/js/modules/migrations/migrations.js @@ -170,8 +170,19 @@ const migrations = [ migrate(transaction, next) { window.log.info('Migration 19'); + // Empty because we don't want to cause incompatibility with beta users who have + // already run migration 19 when it was object store removal. + + next(); + }, + }, + { + version: 20, + migrate(transaction, next) { + window.log.info('Migration 20'); + // Empty because we don't want to cause incompatibility with users who have already - // run migration 19 when it was the object store removal. + // run migration 20 when it was object store removal. next(); }, diff --git a/js/modules/migrations/migrations_1_database_without_attachment_data.js b/js/modules/migrations/migrations_1_database_without_attachment_data.js deleted file mode 100644 index 2f2154201db6..000000000000 --- a/js/modules/migrations/migrations_1_database_without_attachment_data.js +++ /dev/null @@ -1,84 +0,0 @@ -/* global window */ - -const { last, includes } = require('lodash'); - -const { open } = require('../database'); -const settings = require('../settings'); -const { runMigrations } = require('./run_migrations'); - -// These are cleanup migrations, to be run after migration to SQLCipher -exports.migrations = [ - { - version: 20, - migrate(transaction, next) { - window.log.info('Migration 20'); - - const { db } = transaction; - - // This should be run after things are migrated to SQLCipher - - // We check for existence first, because this removal was present in v1.17.0.beta.1, - // but reverted in v1.17.0-beta.3 - - if (includes(db.objectStoreNames, 'messages')) { - window.log.info('Removing messages store'); - db.deleteObjectStore('messages'); - } - if (includes(db.objectStoreNames, 'unprocessed')) { - window.log.info('Removing unprocessed store'); - db.deleteObjectStore('unprocessed'); - } - if (includes(db.objectStoreNames, 'conversations')) { - window.log.info('Removing conversations store'); - db.deleteObjectStore('conversations'); - } - - next(); - }, - }, -]; - -exports.run = async ({ Backbone, logger } = {}) => { - const database = { - id: 'signal', - nolog: true, - migrations: exports.migrations, - }; - - const { canRun } = await exports.getStatus({ database }); - if (!canRun) { - throw new Error( - 'Cannot run migrations on database without attachment data' - ); - } - - await runMigrations({ - Backbone, - logger, - database, - }); -}; - -exports.getStatus = async ({ database } = {}) => { - const connection = await open(database.id, database.version); - const isAttachmentMigrationComplete = await settings.isAttachmentMigrationComplete( - connection - ); - const hasMigrations = exports.migrations.length > 0; - - const canRun = isAttachmentMigrationComplete && hasMigrations; - return { - isAttachmentMigrationComplete, - hasMigrations, - canRun, - }; -}; - -exports.getLatestVersion = () => { - const lastMigration = last(exports.migrations); - if (!lastMigration) { - return null; - } - - return lastMigration.version; -}; diff --git a/js/modules/migrations/run_migrations.js b/js/modules/migrations/run_migrations.js index 1a60be2491d7..35a84cc4a5ee 100644 --- a/js/modules/migrations/run_migrations.js +++ b/js/modules/migrations/run_migrations.js @@ -52,7 +52,10 @@ exports.runMigrations = async ({ Backbone, database, logger } = {}) => { storeName: 'items', }))(); + // Note: this legacy migration technique is required to bring old clients with + // data in IndexedDB forward into the new world of SQLCipher only. await deferredToPromise(migrationCollection.fetch({ limit: 1 })); + logger.info('Close database connection'); await closeDatabaseConnection({ Backbone }); }; diff --git a/js/modules/signal.js b/js/modules/signal.js index 0d634473216c..27b7321d51b6 100644 --- a/js/modules/signal.js +++ b/js/modules/signal.js @@ -5,6 +5,7 @@ const Crypto = require('./crypto'); const Data = require('./data'); const Database = require('./database'); const Emoji = require('../../ts/util/emoji'); +const IndexedDB = require('./indexeddb'); const Notifications = require('../../ts/notifications'); const OS = require('../../ts/OS'); const Settings = require('./settings'); @@ -63,9 +64,7 @@ const { getPlaceholderMigrations, getCurrentVersion, } = require('./migrations/get_placeholder_migrations'); - -const Migrations0DatabaseWithAttachmentData = require('./migrations/migrations_0_database_with_attachment_data'); -const Migrations1DatabaseWithoutAttachmentData = require('./migrations/migrations_1_database_without_attachment_data'); +const { run } = require('./migrations/migrations'); // Types const AttachmentType = require('./types/attachment'); @@ -132,8 +131,7 @@ function initializeMigrations({ loadAttachmentData, loadQuoteData, loadMessage: MessageType.createAttachmentLoader(loadAttachmentData), - Migrations0DatabaseWithAttachmentData, - Migrations1DatabaseWithoutAttachmentData, + run, upgradeMessageSchema: (message, options = {}) => { const { maxVersion } = options; @@ -225,6 +223,7 @@ exports.setup = (options = {}) => { Data, Database, Emoji, + IndexedDB, Migrations, Notifications, OS, diff --git a/js/signal_protocol_store.js b/js/signal_protocol_store.js index 0dec717d0c08..50d3eebdc63f 100644 --- a/js/signal_protocol_store.js +++ b/js/signal_protocol_store.js @@ -1,14 +1,7 @@ -/* global dcodeIO: false */ -/* global Backbone: false */ -/* global Whisper: false */ -/* global _: false */ -/* global libsignal: false */ -/* global textsecure: false */ -/* global ConversationController: false */ -/* global wrapDeferred: false */ -/* global stringObject: false */ +/* global + dcodeIO, Backbone, _, libsignal, textsecure, ConversationController, stringObject */ -/* eslint-disable more/no-then, no-proto */ +/* eslint-disable no-proto */ // eslint-disable-next-line func-names (function() { @@ -105,30 +98,8 @@ return result === 0; } - const Model = Backbone.Model.extend({ database: Whisper.Database }); - const PreKey = Model.extend({ storeName: 'preKeys' }); - const PreKeyCollection = Backbone.Collection.extend({ - storeName: 'preKeys', - database: Whisper.Database, - model: PreKey, - }); - const SignedPreKey = Model.extend({ storeName: 'signedPreKeys' }); - const SignedPreKeyCollection = Backbone.Collection.extend({ - storeName: 'signedPreKeys', - database: Whisper.Database, - model: SignedPreKey, - }); - const Session = Model.extend({ storeName: 'sessions' }); - const SessionCollection = Backbone.Collection.extend({ - storeName: 'sessions', - database: Whisper.Database, - model: Session, - fetchSessionsForNumber(number) { - return this.fetch({ range: [`${number}.1`, `${number}.:`] }); - }, - }); - const Unprocessed = Model.extend(); - const IdentityRecord = Model.extend({ + const Unprocessed = Backbone.Model.extend(); + const IdentityRecord = Backbone.Model.extend({ storeName: 'identityKeys', validAttributes: [ 'id', @@ -176,322 +147,236 @@ return null; }, }); - const Group = Model.extend({ storeName: 'groups' }); - const Item = Model.extend({ storeName: 'items' }); function SignalProtocolStore() {} SignalProtocolStore.prototype = { constructor: SignalProtocolStore, - getIdentityKeyPair() { - const item = new Item({ id: 'identityKey' }); - return new Promise((resolve, reject) => { - item.fetch().then(() => { - resolve(item.get('value')); - }, reject); - }); + async getIdentityKeyPair() { + const item = await window.Signal.Data.getItemById('identityKey'); + if (item) { + return item.value; + } + + return undefined; }, - getLocalRegistrationId() { - const item = new Item({ id: 'registrationId' }); - return new Promise((resolve, reject) => { - item.fetch().then(() => { - resolve(item.get('value')); - }, reject); - }); + async getLocalRegistrationId() { + const item = await window.Signal.Data.getItemById('registrationId'); + if (item) { + return item.value; + } + + return undefined; }, /* Returns a prekeypair object or undefined */ - loadPreKey(keyId) { - const prekey = new PreKey({ id: keyId }); - return new Promise(resolve => { - prekey.fetch().then( - () => { - window.log.info('Successfully fetched prekey:', keyId); - resolve({ - pubKey: prekey.get('publicKey'), - privKey: prekey.get('privateKey'), - }); - }, - () => { - window.log.error('Failed to fetch prekey:', keyId); - resolve(); - } - ); - }); + async loadPreKey(keyId) { + const key = await window.Signal.Data.getPreKeyById(keyId); + if (key) { + window.log.info('Successfully fetched prekey:', keyId); + return { + pubKey: key.publicKey, + privKey: key.privateKey, + }; + } + + window.log.error('Failed to fetch prekey:', keyId); + return undefined; }, - storePreKey(keyId, keyPair) { - const prekey = new PreKey({ + async storePreKey(keyId, keyPair) { + const data = { id: keyId, publicKey: keyPair.pubKey, privateKey: keyPair.privKey, - }); - return new Promise(resolve => { - prekey.save().always(() => { - resolve(); - }); - }); + }; + + await window.Signal.Data.createOrUpdatePreKey(data); }, - removePreKey(keyId) { - const prekey = new PreKey({ id: keyId }); + async removePreKey(keyId) { + try { + this.trigger('removePreKey'); + } catch (error) { + window.log.error( + 'removePreKey error triggering removePreKey:', + error && error.stack ? error.stack : error + ); + } - this.trigger('removePreKey'); - - return new Promise(resolve => { - const deferred = prekey.destroy(); - if (!deferred) { - return resolve(); - } - - return deferred.then(resolve, error => { - window.log.error( - 'removePreKey error:', - error && error.stack ? error.stack : error - ); - resolve(); - }); - }); + await window.Signal.Data.removePreKeyById(keyId); }, - clearPreKeyStore() { - return new Promise(resolve => { - const preKeys = new PreKeyCollection(); - preKeys.sync('delete', preKeys, {}).always(resolve); - }); + async clearPreKeyStore() { + await window.Signal.Data.removeAllPreKeys(); }, /* Returns a signed keypair object or undefined */ - loadSignedPreKey(keyId) { - const prekey = new SignedPreKey({ id: keyId }); - return new Promise(resolve => { - prekey - .fetch() - .then(() => { - window.log.info( - 'Successfully fetched signed prekey:', - prekey.get('id') - ); - resolve({ - pubKey: prekey.get('publicKey'), - privKey: prekey.get('privateKey'), - created_at: prekey.get('created_at'), - keyId: prekey.get('id'), - confirmed: prekey.get('confirmed'), - }); - }) - .fail(() => { - window.log.error('Failed to fetch signed prekey:', keyId); - resolve(); - }); - }); - }, - loadSignedPreKeys() { - if (arguments.length > 0) { - return Promise.reject( - new Error('loadSignedPreKeys takes no arguments') - ); + async loadSignedPreKey(keyId) { + const key = await window.Signal.Data.getSignedPreKeyById(keyId); + if (key) { + window.log.info('Successfully fetched signed prekey:', key.id); + return { + pubKey: key.publicKey, + privKey: key.privateKey, + created_at: key.created_at, + keyId: key.id, + confirmed: key.confirmed, + }; } - const signedPreKeys = new SignedPreKeyCollection(); - return new Promise(resolve => { - signedPreKeys.fetch().then(() => { - resolve( - signedPreKeys.map(prekey => ({ - pubKey: prekey.get('publicKey'), - privKey: prekey.get('privateKey'), - created_at: prekey.get('created_at'), - keyId: prekey.get('id'), - confirmed: prekey.get('confirmed'), - })) - ); - }); - }); + + window.log.error('Failed to fetch signed prekey:', keyId); + return undefined; }, - storeSignedPreKey(keyId, keyPair, confirmed) { - const prekey = new SignedPreKey({ + async loadSignedPreKeys() { + if (arguments.length > 0) { + throw new Error('loadSignedPreKeys takes no arguments'); + } + + const keys = await window.Signal.Data.getAllSignedPreKeys(); + return keys.map(prekey => ({ + pubKey: prekey.publicKey, + privKey: prekey.privateKey, + created_at: prekey.created_at, + keyId: prekey.id, + confirmed: prekey.confirmed, + })); + }, + async storeSignedPreKey(keyId, keyPair, confirmed) { + const key = { id: keyId, publicKey: keyPair.pubKey, privateKey: keyPair.privKey, created_at: Date.now(), confirmed: Boolean(confirmed), - }); - return new Promise(resolve => { - prekey.save().always(() => { - resolve(); - }); - }); + }; + await window.Signal.Data.createOrUpdateSignedPreKey(key); }, - removeSignedPreKey(keyId) { - const prekey = new SignedPreKey({ id: keyId }); - return new Promise((resolve, reject) => { - const deferred = prekey.destroy(); - if (!deferred) { - return resolve(); - } - - return deferred.then(resolve, reject); - }); + async removeSignedPreKey(keyId) { + await window.Signal.Data.removeSignedPreKeyById(keyId); }, - clearSignedPreKeysStore() { - return new Promise(resolve => { - const signedPreKeys = new SignedPreKeyCollection(); - signedPreKeys.sync('delete', signedPreKeys, {}).always(resolve); - }); + async clearSignedPreKeysStore() { + await window.Signal.Data.removeAllSignedPreKeys(); }, - loadSession(encodedNumber) { + async loadSession(encodedNumber) { if (encodedNumber === null || encodedNumber === undefined) { throw new Error('Tried to get session for undefined/null number'); } - return new Promise(resolve => { - const session = new Session({ id: encodedNumber }); - session.fetch().always(() => { - resolve(session.get('record')); - }); - }); + + const session = await window.Signal.Data.getSessionById(encodedNumber); + if (session) { + return session.record; + } + + return undefined; }, - storeSession(encodedNumber, record) { + async storeSession(encodedNumber, record) { if (encodedNumber === null || encodedNumber === undefined) { throw new Error('Tried to put session for undefined/null number'); } - return new Promise(resolve => { - const number = textsecure.utils.unencodeNumber(encodedNumber)[0]; - const deviceId = parseInt( - textsecure.utils.unencodeNumber(encodedNumber)[1], - 10 - ); + const unencoded = textsecure.utils.unencodeNumber(encodedNumber); + const number = unencoded[0]; + const deviceId = parseInt(unencoded[1], 10); - const session = new Session({ id: encodedNumber }); - session.fetch().always(() => { - session - .save({ - record, - deviceId, - number, - }) - .fail(e => { - window.log.error('Failed to save session', encodedNumber, e); - }) - .always(() => { - resolve(); - }); - }); - }); + const data = { + id: encodedNumber, + number, + deviceId, + record, + }; + + await window.Signal.Data.createOrUpdateSession(data); }, - getDeviceIds(number) { + async getDeviceIds(number) { if (number === null || number === undefined) { throw new Error('Tried to get device ids for undefined/null number'); } - return new Promise(resolve => { - const sessions = new SessionCollection(); - sessions.fetchSessionsForNumber(number).always(() => { - resolve(sessions.pluck('deviceId')); - }); - }); + + const sessions = await window.Signal.Data.getSessionsByNumber(number); + return _.pluck(sessions, 'deviceId'); }, - removeSession(encodedNumber) { + async removeSession(encodedNumber) { window.log.info('deleting session for ', encodedNumber); - return new Promise(resolve => { - const session = new Session({ id: encodedNumber }); - session - .fetch() - .then(() => { - session.destroy().then(resolve); - }) - .fail(resolve); - }); + await window.Signal.Data.removeSessionById(encodedNumber); }, - removeAllSessions(number) { + async removeAllSessions(number) { if (number === null || number === undefined) { throw new Error('Tried to remove sessions for undefined/null number'); } - return new Promise((resolve, reject) => { - const sessions = new SessionCollection(); - sessions.fetchSessionsForNumber(number).always(() => { - const promises = []; - while (sessions.length > 0) { - promises.push( - new Promise((res, rej) => { - sessions - .pop() - .destroy() - .then(res, rej); - }) - ); - } - Promise.all(promises).then(resolve, reject); - }); - }); + + await window.Signal.Data.removeSessionsByNumber(number); }, - archiveSiblingSessions(identifier) { + async archiveSiblingSessions(identifier) { const address = libsignal.SignalProtocolAddress.fromString(identifier); - return this.getDeviceIds(address.getName()).then(deviceIds => { - const siblings = _.without(deviceIds, address.getDeviceId()); - return Promise.all( - siblings.map(deviceId => { - const sibling = new libsignal.SignalProtocolAddress( - address.getName(), - deviceId - ); - window.log.info('closing session for', sibling.toString()); - const sessionCipher = new libsignal.SessionCipher( - textsecure.storage.protocol, - sibling - ); - return sessionCipher.closeOpenSessionForDevice(); - }) - ); - }); - }, - archiveAllSessions(number) { - return this.getDeviceIds(number).then(deviceIds => - Promise.all( - deviceIds.map(deviceId => { - const address = new libsignal.SignalProtocolAddress( - number, - deviceId - ); - window.log.info('closing session for', address.toString()); - const sessionCipher = new libsignal.SessionCipher( - textsecure.storage.protocol, - address - ); - return sessionCipher.closeOpenSessionForDevice(); - }) - ) + + const deviceIds = await this.getDeviceIds(address.getName()); + const siblings = _.without(deviceIds, address.getDeviceId()); + + await Promise.all( + siblings.map(async deviceId => { + const sibling = new libsignal.SignalProtocolAddress( + address.getName(), + deviceId + ); + window.log.info('closing session for', sibling.toString()); + const sessionCipher = new libsignal.SessionCipher( + textsecure.storage.protocol, + sibling + ); + await sessionCipher.closeOpenSessionForDevice(); + }) ); }, - clearSessionStore() { - return new Promise(resolve => { - const sessions = new SessionCollection(); - sessions.sync('delete', sessions, {}).always(resolve); - }); + async archiveAllSessions(number) { + const deviceIds = await this.getDeviceIds(number); + + await Promise.all( + deviceIds.map(async deviceId => { + const address = new libsignal.SignalProtocolAddress(number, deviceId); + window.log.info('closing session for', address.toString()); + const sessionCipher = new libsignal.SessionCipher( + textsecure.storage.protocol, + address + ); + await sessionCipher.closeOpenSessionForDevice(); + }) + ); }, - isTrustedIdentity(identifier, publicKey, direction) { + async clearSessionStore() { + window.Signal.Data.removeAllSessions(); + }, + async isTrustedIdentity(identifier, publicKey, direction) { if (identifier === null || identifier === undefined) { throw new Error('Tried to get identity key for undefined/null key'); } const number = textsecure.utils.unencodeNumber(identifier)[0]; const isOurNumber = number === textsecure.storage.user.getNumber(); - const identityRecord = new IdentityRecord({ id: number }); - return new Promise(resolve => { - identityRecord.fetch().always(resolve); - }).then(() => { - const existing = identityRecord.get('publicKey'); - if (isOurNumber) { - return equalArrayBuffers(existing, publicKey); - } + const identityRecord = await window.Signal.Data.getIdentityKeyById( + number + ); - switch (direction) { - case Direction.SENDING: - return this.isTrustedForSending(publicKey, identityRecord); - case Direction.RECEIVING: - return true; - default: - throw new Error(`Unknown direction: ${direction}`); - } - }); + if (isOurNumber) { + const existing = identityRecord ? identityRecord.publicKey : null; + return equalArrayBuffers(existing, publicKey); + } + + switch (direction) { + case Direction.SENDING: + return this.isTrustedForSending(publicKey, identityRecord); + case Direction.RECEIVING: + return true; + default: + throw new Error(`Unknown direction: ${direction}`); + } }, isTrustedForSending(publicKey, identityRecord) { - const existing = identityRecord.get('publicKey'); + if (!identityRecord) { + window.log.info( + 'isTrustedForSending: No previous record, returning true...' + ); + return true; + } + + const existing = identityRecord.publicKey; if (!existing) { window.log.info('isTrustedForSending: Nothing here, returning true...'); @@ -501,7 +386,7 @@ window.log.info("isTrustedForSending: Identity keys don't match..."); return false; } - if (identityRecord.get('verified') === VerifiedStatus.UNVERIFIED) { + if (identityRecord.verified === VerifiedStatus.UNVERIFIED) { window.log.error('Needs unverified approval!'); return false; } @@ -512,19 +397,22 @@ return true; }, - loadIdentityKey(identifier) { + async loadIdentityKey(identifier) { if (identifier === null || identifier === undefined) { throw new Error('Tried to get identity key for undefined/null key'); } const number = textsecure.utils.unencodeNumber(identifier)[0]; - return new Promise(resolve => { - const identityRecord = new IdentityRecord({ id: number }); - identityRecord.fetch().always(() => { - resolve(identityRecord.get('publicKey')); - }); - }); + const identityRecord = await window.Signal.Data.getIdentityKeyById( + number + ); + + if (identityRecord) { + return identityRecord.publicKey; + } + + return undefined; }, - saveIdentity(identifier, publicKey, nonblockingApproval) { + async saveIdentity(identifier, publicKey, nonblockingApproval) { if (identifier === null || identifier === undefined) { throw new Error('Tried to put identity key for undefined/null key'); } @@ -536,118 +424,124 @@ // eslint-disable-next-line no-param-reassign nonblockingApproval = false; } + const number = textsecure.utils.unencodeNumber(identifier)[0]; - return new Promise((resolve, reject) => { - const identityRecord = new IdentityRecord({ id: number }); - identityRecord.fetch().always(() => { - const oldpublicKey = identityRecord.get('publicKey'); - if (!oldpublicKey) { - // Lookup failed, or the current key was removed, so save this one. - window.log.info('Saving new identity...'); - identityRecord - .save({ - publicKey, - firstUse: true, - timestamp: Date.now(), - verified: VerifiedStatus.DEFAULT, - nonblockingApproval, - }) - .then(() => { - resolve(false); - }, reject); - } else if (!equalArrayBuffers(oldpublicKey, publicKey)) { - window.log.info('Replacing existing identity...'); - const previousStatus = identityRecord.get('verified'); - let verifiedStatus; - if ( - previousStatus === VerifiedStatus.VERIFIED || - previousStatus === VerifiedStatus.UNVERIFIED - ) { - verifiedStatus = VerifiedStatus.UNVERIFIED; - } else { - verifiedStatus = VerifiedStatus.DEFAULT; - } - identityRecord - .save({ - publicKey, - firstUse: false, - timestamp: Date.now(), - verified: verifiedStatus, - nonblockingApproval, - }) - .then(() => { - this.trigger('keychange', number); - this.archiveSiblingSessions(identifier).then(() => { - resolve(true); - }, reject); - }, reject); - } else if (this.isNonBlockingApprovalRequired(identityRecord)) { - window.log.info('Setting approval status...'); - identityRecord - .save({ - nonblockingApproval, - }) - .then(() => { - resolve(false); - }, reject); - } else { - resolve(false); - } + const identityRecord = await window.Signal.Data.getIdentityKeyById( + number + ); + + if (!identityRecord || !identityRecord.publicKey) { + // Lookup failed, or the current key was removed, so save this one. + window.log.info('Saving new identity...'); + await window.Signal.Data.createOrUpdateIdentityKey({ + id: number, + publicKey, + firstUse: true, + timestamp: Date.now(), + verified: VerifiedStatus.DEFAULT, + nonblockingApproval, }); - }); + + return false; + } + + const oldpublicKey = identityRecord.publicKey; + if (!equalArrayBuffers(oldpublicKey, publicKey)) { + window.log.info('Replacing existing identity...'); + const previousStatus = identityRecord.verified; + let verifiedStatus; + if ( + previousStatus === VerifiedStatus.VERIFIED || + previousStatus === VerifiedStatus.UNVERIFIED + ) { + verifiedStatus = VerifiedStatus.UNVERIFIED; + } else { + verifiedStatus = VerifiedStatus.DEFAULT; + } + + await window.Signal.Data.createOrUpdateIdentityKey({ + id: number, + publicKey, + firstUse: false, + timestamp: Date.now(), + verified: verifiedStatus, + nonblockingApproval, + }); + + try { + this.trigger('keychange', number); + } catch (error) { + window.log.error( + 'saveIdentity error triggering keychange:', + error && error.stack ? error.stack : error + ); + } + await this.archiveSiblingSessions(identifier); + + return true; + } else if (this.isNonBlockingApprovalRequired(identityRecord)) { + window.log.info('Setting approval status...'); + + identityRecord.nonblockingApproval = nonblockingApproval; + await window.Signal.Data.createOrUpdateIdentityKey(identityRecord); + + return false; + } + + return false; }, isNonBlockingApprovalRequired(identityRecord) { return ( - !identityRecord.get('firstUse') && - Date.now() - identityRecord.get('timestamp') < TIMESTAMP_THRESHOLD && - !identityRecord.get('nonblockingApproval') + !identityRecord.firstUse && + Date.now() - identityRecord.timestamp < TIMESTAMP_THRESHOLD && + !identityRecord.nonblockingApproval ); }, - saveIdentityWithAttributes(identifier, attributes) { + async saveIdentityWithAttributes(identifier, attributes) { if (identifier === null || identifier === undefined) { throw new Error('Tried to put identity key for undefined/null key'); } + const number = textsecure.utils.unencodeNumber(identifier)[0]; - return new Promise((resolve, reject) => { - const identityRecord = new IdentityRecord({ id: number }); - identityRecord.set(attributes); - if (identityRecord.isValid()) { - // false if invalid attributes - identityRecord.save().then(resolve); - } else { - reject(identityRecord.validationError); - } - }); + const identityRecord = await window.Signal.Data.getIdentityKeyById( + number + ); + + const updates = { + id: number, + ...identityRecord, + ...attributes, + }; + + const model = new IdentityRecord(updates); + if (model.isValid()) { + await window.Signal.Data.createOrUpdateIdentityKey(updates); + } else { + throw model.validationError; + } }, - setApproval(identifier, nonblockingApproval) { + async setApproval(identifier, nonblockingApproval) { if (identifier === null || identifier === undefined) { throw new Error('Tried to set approval for undefined/null identifier'); } if (typeof nonblockingApproval !== 'boolean') { throw new Error('Invalid approval status'); } + const number = textsecure.utils.unencodeNumber(identifier)[0]; - return new Promise((resolve, reject) => { - const identityRecord = new IdentityRecord({ id: number }); - identityRecord.fetch().then(() => { - identityRecord - .save({ - nonblockingApproval, - }) - .then( - () => { - resolve(); - }, - () => { - // catch - reject(new Error(`No identity record for ${number}`)); - } - ); - }); - }); + const identityRecord = await window.Signal.Data.getIdentityKeyById( + number + ); + + if (!identityRecord) { + throw new Error(`No identity record for ${number}`); + } + + identityRecord.nonblockingApproval = nonblockingApproval; + await window.Signal.Data.createOrUpdateIdentityKey(identityRecord); }, - setVerified(identifier, verifiedStatus, publicKey) { - if (identifier === null || identifier === undefined) { + async setVerified(number, verifiedStatus, publicKey) { + if (number === null || number === undefined) { throw new Error('Tried to set verified for undefined/null key'); } if (!validateVerifiedStatus(verifiedStatus)) { @@ -656,56 +550,49 @@ if (arguments.length > 2 && !(publicKey instanceof ArrayBuffer)) { throw new Error('Invalid public key'); } - return new Promise((resolve, reject) => { - const identityRecord = new IdentityRecord({ id: identifier }); - identityRecord.fetch().then( - () => { - if ( - !publicKey || - equalArrayBuffers(identityRecord.get('publicKey'), publicKey) - ) { - identityRecord.set({ verified: verifiedStatus }); - if (identityRecord.isValid()) { - identityRecord.save({}).then(() => { - resolve(); - }, reject); - } else { - reject(identityRecord.validationError); - } - } else { - window.log.info('No identity record for specified publicKey'); - resolve(); - } - }, - () => { - // catch - reject(new Error(`No identity record for ${identifier}`)); - } - ); - }); + const identityRecord = await window.Signal.Data.getIdentityKeyById( + number + ); + if (!identityRecord) { + throw new Error(`No identity record for ${number}`); + } + + if ( + !publicKey || + equalArrayBuffers(identityRecord.publicKey, publicKey) + ) { + identityRecord.verified = verifiedStatus; + + const model = new IdentityRecord(identityRecord); + if (model.isValid()) { + await window.Signal.Data.createOrUpdateIdentityKey(identityRecord); + } else { + throw identityRecord.validationError; + } + } else { + window.log.info('No identity record for specified publicKey'); + } }, - getVerified(identifier) { - if (identifier === null || identifier === undefined) { + async getVerified(number) { + if (number === null || number === undefined) { throw new Error('Tried to set verified for undefined/null key'); } - return new Promise((resolve, reject) => { - const identityRecord = new IdentityRecord({ id: identifier }); - identityRecord.fetch().then( - () => { - const verifiedStatus = identityRecord.get('verified'); - if (validateVerifiedStatus(verifiedStatus)) { - resolve(verifiedStatus); - } else { - resolve(VerifiedStatus.DEFAULT); - } - }, - () => { - // catch - reject(new Error(`No identity record for ${identifier}`)); - } - ); - }); + + const identityRecord = await window.Signal.Data.getIdentityKeyById( + number + ); + + if (!identityRecord) { + throw new Error(`No identity record for ${number}`); + } + + const verifiedStatus = identityRecord.verified; + if (validateVerifiedStatus(verifiedStatus)) { + return verifiedStatus; + } + + return VerifiedStatus.DEFAULT; }, // Resolves to true if a new identity key was saved processContactSyncVerificationState(identifier, verifiedStatus, publicKey) { @@ -721,75 +608,72 @@ // This function encapsulates the non-Java behavior, since the mobile apps don't // currently receive contact syncs and therefore will see a verify sync with // UNVERIFIED status - processUnverifiedMessage(identifier, verifiedStatus, publicKey) { - if (identifier === null || identifier === undefined) { + async processUnverifiedMessage(number, verifiedStatus, publicKey) { + if (number === null || number === undefined) { throw new Error('Tried to set verified for undefined/null key'); } if (publicKey !== undefined && !(publicKey instanceof ArrayBuffer)) { throw new Error('Invalid public key'); } - return new Promise((resolve, reject) => { - const identityRecord = new IdentityRecord({ id: identifier }); - let isPresent = false; - let isEqual = false; - identityRecord - .fetch() - .then(() => { - isPresent = true; - if (publicKey) { - isEqual = equalArrayBuffers( - publicKey, - identityRecord.get('publicKey') - ); - } - }) - .always(() => { - if ( - isPresent && - isEqual && - identityRecord.get('verified') !== VerifiedStatus.UNVERIFIED - ) { - return textsecure.storage.protocol - .setVerified(identifier, verifiedStatus, publicKey) - .then(resolve, reject); - } - if (!isPresent || !isEqual) { - return textsecure.storage.protocol - .saveIdentityWithAttributes(identifier, { - publicKey, - verified: verifiedStatus, - firstUse: false, - timestamp: Date.now(), - nonblockingApproval: true, - }) - .then(() => { - if (isPresent && !isEqual) { - this.trigger('keychange', identifier); - return this.archiveAllSessions(identifier).then( - () => - // true signifies that we overwrote a previous key with a new one - resolve(true), - reject - ); - } + const identityRecord = await window.Signal.Data.getIdentityKeyById( + number + ); + const isPresent = Boolean(identityRecord); + let isEqual = false; - return resolve(); - }, reject); - } + if (isPresent && publicKey) { + isEqual = equalArrayBuffers(publicKey, identityRecord.publicKey); + } - // The situation which could get us here is: - // 1. had a previous key - // 2. new key is the same - // 3. desired new status is same as what we had before - return resolve(); - }); - }); + if ( + isPresent && + isEqual && + identityRecord.verified !== VerifiedStatus.UNVERIFIED + ) { + await textsecure.storage.protocol.setVerified( + number, + verifiedStatus, + publicKey + ); + return false; + } + + if (!isPresent || !isEqual) { + await textsecure.storage.protocol.saveIdentityWithAttributes(number, { + publicKey, + verified: verifiedStatus, + firstUse: false, + timestamp: Date.now(), + nonblockingApproval: true, + }); + + if (isPresent && !isEqual) { + try { + this.trigger('keychange', number); + } catch (error) { + window.log.error( + 'processUnverifiedMessage error triggering keychange:', + error && error.stack ? error.stack : error + ); + } + + await this.archiveAllSessions(number); + + return true; + } + } + + // The situation which could get us here is: + // 1. had a previous key + // 2. new key is the same + // 3. desired new status is same as what we had before + return false; }, // This matches the Java method as of // https://github.com/signalapp/Signal-Android/blob/d0bb68e1378f689e4d10ac6a46014164992ca4e4/src/org/thoughtcrime/securesms/util/IdentityUtil.java#L188 - processVerifiedMessage(identifier, verifiedStatus, publicKey) { - if (identifier === null || identifier === undefined) { + async processVerifiedMessage(number, verifiedStatus, publicKey) { + if (number === null || number === undefined) { throw new Error('Tried to set verified for undefined/null key'); } if (!validateVerifiedStatus(verifiedStatus)) { @@ -798,144 +682,133 @@ if (publicKey !== undefined && !(publicKey instanceof ArrayBuffer)) { throw new Error('Invalid public key'); } - return new Promise((resolve, reject) => { - const identityRecord = new IdentityRecord({ id: identifier }); - let isPresent = false; - let isEqual = false; - identityRecord - .fetch() - .then(() => { - isPresent = true; - if (publicKey) { - isEqual = equalArrayBuffers( - publicKey, - identityRecord.get('publicKey') - ); - } - }) - .always(() => { - if (!isPresent && verifiedStatus === VerifiedStatus.DEFAULT) { - window.log.info('No existing record for default status'); - return resolve(); - } - if ( - isPresent && - isEqual && - identityRecord.get('verified') !== VerifiedStatus.DEFAULT && - verifiedStatus === VerifiedStatus.DEFAULT - ) { - return textsecure.storage.protocol - .setVerified(identifier, verifiedStatus, publicKey) - .then(resolve, reject); - } + const identityRecord = await window.Signal.Data.getIdentityKeyById( + number + ); - if ( - verifiedStatus === VerifiedStatus.VERIFIED && - (!isPresent || - (isPresent && !isEqual) || - (isPresent && - identityRecord.get('verified') !== VerifiedStatus.VERIFIED)) - ) { - return textsecure.storage.protocol - .saveIdentityWithAttributes(identifier, { - publicKey, - verified: verifiedStatus, - firstUse: false, - timestamp: Date.now(), - nonblockingApproval: true, - }) - .then(() => { - if (isPresent && !isEqual) { - this.trigger('keychange', identifier); - return this.archiveAllSessions(identifier).then( - () => - // true signifies that we overwrote a previous key with a new one - resolve(true), - reject - ); - } + const isPresent = Boolean(identityRecord); + let isEqual = false; - return resolve(); - }, reject); - } + if (isPresent && publicKey) { + isEqual = equalArrayBuffers(publicKey, identityRecord.publicKey); + } - // We get here if we got a new key and the status is DEFAULT. If the - // message is out of date, we don't want to lose whatever more-secure - // state we had before. - return resolve(); - }); - }); + if (!isPresent && verifiedStatus === VerifiedStatus.DEFAULT) { + window.log.info('No existing record for default status'); + return false; + } + + if ( + isPresent && + isEqual && + identityRecord.verified !== VerifiedStatus.DEFAULT && + verifiedStatus === VerifiedStatus.DEFAULT + ) { + await textsecure.storage.protocol.setVerified( + number, + verifiedStatus, + publicKey + ); + return false; + } + + if ( + verifiedStatus === VerifiedStatus.VERIFIED && + (!isPresent || + (isPresent && !isEqual) || + (isPresent && identityRecord.verified !== VerifiedStatus.VERIFIED)) + ) { + await textsecure.storage.protocol.saveIdentityWithAttributes(number, { + publicKey, + verified: verifiedStatus, + firstUse: false, + timestamp: Date.now(), + nonblockingApproval: true, + }); + + if (isPresent && !isEqual) { + try { + this.trigger('keychange', number); + } catch (error) { + window.log.error( + 'processVerifiedMessage error triggering keychange:', + error && error.stack ? error.stack : error + ); + } + + await this.archiveAllSessions(number); + + // true signifies that we overwrote a previous key with a new one + return true; + } + } + + // We get here if we got a new key and the status is DEFAULT. If the + // message is out of date, we don't want to lose whatever more-secure + // state we had before. + return false; }, - isUntrusted(identifier) { - if (identifier === null || identifier === undefined) { + async isUntrusted(number) { + if (number === null || number === undefined) { throw new Error('Tried to set verified for undefined/null key'); } - return new Promise((resolve, reject) => { - const identityRecord = new IdentityRecord({ id: identifier }); - identityRecord.fetch().then( - () => { - if ( - Date.now() - identityRecord.get('timestamp') < - TIMESTAMP_THRESHOLD && - !identityRecord.get('nonblockingApproval') && - !identityRecord.get('firstUse') - ) { - resolve(true); - } else { - resolve(false); - } - }, - () => { - // catch - reject(new Error(`No identity record for ${identifier}`)); - } - ); - }); + + const identityRecord = await window.Signal.Data.getIdentityKeyById( + number + ); + + if (!identityRecord) { + throw new Error(`No identity record for ${number}`); + } + + if ( + Date.now() - identityRecord.timestamp < TIMESTAMP_THRESHOLD && + !identityRecord.nonblockingApproval && + !identityRecord.firstUse + ) { + return true; + } + + return false; }, async removeIdentityKey(number) { - const identityRecord = new IdentityRecord({ id: number }); - try { - await wrapDeferred(identityRecord.fetch()); - await wrapDeferred(identityRecord.destroy()); - return textsecure.storage.protocol.removeAllSessions(number); - } catch (error) { - throw new Error('Tried to remove identity for unknown number'); - } + await window.Signal.Data.removeIdentityKeyById(number); + return textsecure.storage.protocol.removeAllSessions(number); }, // Groups - getGroup(groupId) { + async getGroup(groupId) { if (groupId === null || groupId === undefined) { throw new Error('Tried to get group for undefined/null id'); } - return new Promise(resolve => { - const group = new Group({ id: groupId }); - group.fetch().always(() => { - resolve(group.get('data')); - }); - }); + + const group = await window.Signal.Data.getGroupById(groupId); + if (group) { + return group.data; + } + + return undefined; }, - putGroup(groupId, group) { + async putGroup(groupId, group) { if (groupId === null || groupId === undefined) { throw new Error('Tried to put group key for undefined/null id'); } if (group === null || group === undefined) { throw new Error('Tried to put undefined/null group object'); } - const newGroup = new Group({ id: groupId, data: group }); - return new Promise(resolve => { - newGroup.save().always(resolve); - }); + const data = { + id: groupId, + data: group, + }; + await window.Signal.Data.createOrUpdateGroup(data); }, - removeGroup(groupId) { + async removeGroup(groupId) { if (groupId === null || groupId === undefined) { throw new Error('Tried to remove group key for undefined/null id'); } - return new Promise(resolve => { - const group = new Group({ id: groupId }); - group.destroy().always(resolve); - }); + + await window.Signal.Data.removeGroupById(groupId); }, // Not yet processed messages - for resiliency @@ -966,30 +839,19 @@ return window.Signal.Data.removeAllUnprocessed(); }, async removeAllData() { - // First the in-memory caches: - window.storage.reset(); // items store - ConversationController.reset(); // conversations store - await ConversationController.load(); - - // Then, the entire database: - await Whisper.Database.clear(); - await window.Signal.Data.removeAll(); + + window.storage.reset(); + await window.storage.fetch(); + + ConversationController.reset(); + await ConversationController.load(); }, async removeAllConfiguration() { - // First the in-memory cache for the items store: + await window.Signal.Data.removeAllConfiguration(); + window.storage.reset(); - - // Then anything in the database that isn't a message/conversation/group: - await Whisper.Database.clearStores([ - 'items', - 'identityKeys', - 'sessions', - 'signedPreKeys', - 'preKeys', - ]); - - await window.Signal.Data.removeAllUnprocessed(); + await window.storage.fetch(); }, }; _.extend(SignalProtocolStore.prototype, Backbone.Events); diff --git a/js/storage.js b/js/storage.js index 227360dee24c..017eee1b3f71 100644 --- a/js/storage.js +++ b/js/storage.js @@ -1,5 +1,3 @@ -/* global Backbone, Whisper */ - /* eslint-disable more/no-then */ // eslint-disable-next-line func-names @@ -7,89 +5,103 @@ 'use strict'; window.Whisper = window.Whisper || {}; - const Item = Backbone.Model.extend({ - database: Whisper.Database, - storeName: 'items', - }); - const ItemCollection = Backbone.Collection.extend({ - model: Item, - storeName: 'items', - database: Whisper.Database, - }); let ready = false; - const items = new ItemCollection(); - items.on('reset', () => { + let items; + let callbacks = []; + + reset(); + + async function put(key, value) { + if (value === undefined) { + throw new Error('Tried to store undefined'); + } + if (!ready) { + window.log.warn('Called storage.put before storage is ready. key:', key); + } + + const data = { id: key, value }; + + items[key] = data; + await window.Signal.Data.createOrUpdateItem(data); + } + + function get(key, defaultValue) { + if (!ready) { + window.log.warn('Called storage.get before storage is ready. key:', key); + } + + const item = items[key]; + if (!item) { + return defaultValue; + } + + return item.value; + } + + async function remove(key) { + if (!ready) { + window.log.warn('Called storage.get before storage is ready. key:', key); + } + + delete items[key]; + await window.Signal.Data.removeItemById(key); + } + + function onready(callback) { + if (ready) { + callback(); + } else { + callbacks.push(callback); + } + } + + function callListeners() { + if (ready) { + callbacks.forEach(callback => callback()); + callbacks = []; + } + } + + async function fetch() { + this.reset(); + const array = await window.Signal.Data.getAllItems(); + + for (let i = 0, max = array.length; i < max; i += 1) { + const item = array[i]; + const { id } = item; + items[id] = item; + } + ready = true; - }); - window.storage = { - /** *************************** - *** Base Storage Routines *** - **************************** */ - put(key, value) { - if (value === undefined) { - throw new Error('Tried to store undefined'); - } - if (!ready) { - window.log.warn( - 'Called storage.put before storage is ready. key:', - key - ); - } - const item = items.add({ id: key, value }, { merge: true }); - return new Promise((resolve, reject) => { - item.save().then(resolve, reject); - }); - }, + callListeners(); + } - get(key, defaultValue) { - const item = items.get(`${key}`); - if (!item) { - return defaultValue; - } - return item.get('value'); - }, + function reset() { + ready = false; + items = Object.create(null); + } - remove(key) { - const item = items.get(`${key}`); - if (item) { - items.remove(item); - return new Promise((resolve, reject) => { - item.destroy().then(resolve, reject); - }); - } - return Promise.resolve(); - }, - - onready(callback) { - if (ready) { - callback(); - } else { - items.on('reset', callback); - } - }, - - fetch() { - return new Promise((resolve, reject) => { - items - .fetch({ reset: true }) - .fail(() => - reject( - new Error( - 'Failed to fetch from storage.' + - ' This may be due to an unexpected database version.' - ) - ) - ) - .always(resolve); - }); - }, - - reset() { - items.reset(); - }, + const storage = { + fetch, + put, + get, + remove, + onready, + reset, }; + + // Keep a reference to this storage system, since there are scenarios where + // we need to replace it with the legacy storage system for a while. + window.newStorage = storage; + window.textsecure = window.textsecure || {}; window.textsecure.storage = window.textsecure.storage || {}; - window.textsecure.storage.impl = window.storage; + + window.installStorage = newStorage => { + window.storage = newStorage; + window.textsecure.storage.impl = newStorage; + }; + + window.installStorage(storage); })(); diff --git a/js/views/clear_data_view.js b/js/views/clear_data_view.js index ba1e4b541be0..e113f8ed18f1 100644 --- a/js/views/clear_data_view.js +++ b/js/views/clear_data_view.js @@ -8,7 +8,6 @@ 'use strict'; window.Whisper = window.Whisper || {}; - const { Database } = window.Whisper; const { Logs } = window.Signal; const CLEAR_DATA_STEPS = { @@ -33,26 +32,12 @@ this.step = CLEAR_DATA_STEPS.DELETING; this.render(); - try { - await Database.clear(); - await Database.close(); - window.log.info( - 'All database connections closed. Starting database drop.' - ); - await Database.drop(); - } catch (error) { - window.log.error( - 'Something went wrong deleting IndexedDB data then dropping database.' - ); - } - - this.clearAllData(); + await this.clearAllData(); }, async clearAllData() { try { await Logs.deleteAll(); - // SQLCipher await window.Signal.Data.removeAll(); await window.Signal.Data.close(); await window.Signal.Data.removeDB(); diff --git a/js/views/import_view.js b/js/views/import_view.js index 89b5f6dc6331..ee1d88cbbc28 100644 --- a/js/views/import_view.js +++ b/js/views/import_view.js @@ -38,10 +38,7 @@ return storage.put(IMPORT_LOCATION, location); }, reset() { - return Promise.all([ - Whisper.Database.clear(), - window.Signal.Data.removeAll(), - ]); + return window.Signal.Data.removeAll(); }, }; diff --git a/js/views/recipients_input_view.js b/js/views/recipients_input_view.js index 00223092fa3b..af5ea9557865 100644 --- a/js/views/recipients_input_view.js +++ b/js/views/recipients_input_view.js @@ -13,8 +13,6 @@ 'national_number', 'international_number', ], - database: Whisper.Database, - storeName: 'conversations', model: Whisper.Conversation, async fetchContacts() { const models = window.Signal.Data.getAllPrivateConversations({ diff --git a/libtextsecure/storage/groups.js b/libtextsecure/storage/groups.js index 9be9414c1cbb..67460c15e237 100644 --- a/libtextsecure/storage/groups.js +++ b/libtextsecure/storage/groups.js @@ -73,7 +73,9 @@ getNumbers(groupId) { return textsecure.storage.protocol.getGroup(groupId).then(group => { - if (group === undefined) return undefined; + if (!group) { + return undefined; + } return group.numbers; }); diff --git a/test/_test.js b/test/_test.js index 6b97b72c5e5a..c5ad6411f09c 100644 --- a/test/_test.js +++ b/test/_test.js @@ -72,16 +72,8 @@ function deleteDatabase() { before(async () => { await deleteDatabase(); await window.Signal.Data.removeAll(); - - await Signal.Migrations.Migrations0DatabaseWithAttachmentData.run({ - Backbone, - databaseName: Whisper.Database.id, - logger: window.log, - }); }); async function clearDatabase() { - const db = await Whisper.Database.open(); - await Whisper.Database.clear(); await window.Signal.Data.removeAll(); } diff --git a/test/storage_test.js b/test/storage_test.js index 71ef6c4cd6bc..dfd866d54f96 100644 --- a/test/storage_test.js +++ b/test/storage_test.js @@ -1,7 +1,7 @@ 'use strict'; describe('SignalProtocolStore', function() { - var identifier = '+5558675309'; + var number = '+5558675309'; var store; var identityKey; var testKey; @@ -29,187 +29,134 @@ describe('SignalProtocolStore', function() { }); describe('getLocalRegistrationId', function() { - it('retrieves my registration id', function(done) { - store - .getLocalRegistrationId() - .then(function(reg) { - assert.strictEqual(reg, 1337); - }) - .then(done, done); + it('retrieves my registration id', async function() { + const id = await store.getLocalRegistrationId(); + assert.strictEqual(id, 1337); }); }); describe('getIdentityKeyPair', function() { - it('retrieves my identity key', function(done) { - store - .getIdentityKeyPair() - .then(function(key) { - assertEqualArrayBuffers(key.pubKey, identityKey.pubKey); - assertEqualArrayBuffers(key.privKey, identityKey.privKey); - }) - .then(done, done); + it('retrieves my identity key', async function() { + const key = await store.getIdentityKeyPair(); + assertEqualArrayBuffers(key.pubKey, identityKey.pubKey); + assertEqualArrayBuffers(key.privKey, identityKey.privKey); }); }); - var IdentityKeyRecord = Backbone.Model.extend({ - database: Whisper.Database, - storeName: 'identityKeys', - }); describe('saveIdentity', function() { - var record = new IdentityKeyRecord({ id: identifier }); - var address = new libsignal.SignalProtocolAddress(identifier, 1); + var address = new libsignal.SignalProtocolAddress(number, 1); + var identifier = address.toString(); - it('stores identity keys', function(done) { - store - .saveIdentity(address.toString(), testKey.pubKey) - .then(function() { - return store.loadIdentityKey(identifier).then(function(key) { - assertEqualArrayBuffers(key, testKey.pubKey); - }); - }) - .then(done, done); + it('stores identity keys', async function() { + await store.saveIdentity(identifier, testKey.pubKey); + const key = await store.loadIdentityKey(number); + + assertEqualArrayBuffers(key, testKey.pubKey); }); - it('allows key changes', function(done) { + it('allows key changes', async function() { var newIdentity = libsignal.crypto.getRandomBytes(33); - store - .saveIdentity(address.toString(), testKey.pubKey) - .then(function() { - store.saveIdentity(address.toString(), newIdentity).then(function() { - done(); - }); - }) - .catch(done); + await store.saveIdentity(identifier, testKey.pubKey); + await store.saveIdentity(identifier, newIdentity); }); describe('When there is no existing key (first use)', function() { - before(function() { - return store - .removeIdentityKey(identifier) - .then(function() { - return store.saveIdentity(address.toString(), testKey.pubKey); - }) - .then(function() { - return wrapDeferred(record.fetch()); - }); + before(async function() { + await store.removeIdentityKey(number); + await store.saveIdentity(identifier, testKey.pubKey); }); - it('marks the key firstUse', function() { - assert(record.get('firstUse')); + it('marks the key firstUse', async function() { + const identity = await window.Signal.Data.getIdentityKeyById(number); + assert(identity.firstUse); }); - it('sets the timestamp', function() { - assert(record.get('timestamp')); + it('sets the timestamp', async function() { + const identity = await window.Signal.Data.getIdentityKeyById(number); + assert(identity.timestamp); }); - it('sets the verified status to DEFAULT', function() { - assert.strictEqual( - record.get('verified'), - store.VerifiedStatus.DEFAULT - ); + it('sets the verified status to DEFAULT', async function() { + const identity = await window.Signal.Data.getIdentityKeyById(number); + assert.strictEqual(identity.verified, store.VerifiedStatus.DEFAULT); }); }); describe('When there is a different existing key (non first use)', function() { - var newIdentity = libsignal.crypto.getRandomBytes(33); - var oldTimestamp = Date.now(); - before(function(done) { - record - .save({ + const newIdentity = libsignal.crypto.getRandomBytes(33); + const oldTimestamp = Date.now(); + + before(async function() { + await window.Signal.Data.createOrUpdateIdentityKey({ + id: identifier, + publicKey: testKey.pubKey, + firstUse: true, + timestamp: oldTimestamp, + nonblockingApproval: false, + verified: store.VerifiedStatus.DEFAULT, + }); + + await store.saveIdentity(identifier, newIdentity); + }); + it('marks the key not firstUse', async function() { + const identity = await window.Signal.Data.getIdentityKeyById(number); + assert(!identity.firstUse); + }); + it('updates the timestamp', async function() { + const identity = await window.Signal.Data.getIdentityKeyById(number); + assert.notEqual(identity.timestamp, oldTimestamp); + }); + + describe('The previous verified status was DEFAULT', function() { + before(async function() { + await window.Signal.Data.createOrUpdateIdentityKey({ + id: number, publicKey: testKey.pubKey, firstUse: true, timestamp: oldTimestamp, nonblockingApproval: false, verified: store.VerifiedStatus.DEFAULT, - }) - .then(function() { - store - .saveIdentity(address.toString(), newIdentity) - .then(function() { - record.fetch().then(function() { - done(); - }); - }); }); - }); - it('marks the key not firstUse', function() { - assert(!record.get('firstUse')); - }); - it('updates the timestamp', function() { - assert.notEqual(record.get('timestamp'), oldTimestamp); - }); - describe('The previous verified status was DEFAULT', function() { - before(function(done) { - record - .save({ - publicKey: testKey.pubKey, - firstUse: true, - timestamp: oldTimestamp, - nonblockingApproval: false, - verified: store.VerifiedStatus.DEFAULT, - }) - .then(function() { - store - .saveIdentity(address.toString(), newIdentity) - .then(function() { - record.fetch().then(function() { - done(); - }); - }); - }); + await store.saveIdentity(identifier, newIdentity); }); - it('sets the new key to unverified', function() { - assert.strictEqual( - record.get('verified'), - store.VerifiedStatus.DEFAULT - ); + it('sets the new key to default', async function() { + const identity = await window.Signal.Data.getIdentityKeyById(number); + assert.strictEqual(identity.verified, store.VerifiedStatus.DEFAULT); }); }); describe('The previous verified status was VERIFIED', function() { - before(function(done) { - record - .save({ - publicKey: testKey.pubKey, - firstUse: true, - timestamp: oldTimestamp, - nonblockingApproval: false, - verified: store.VerifiedStatus.VERIFIED, - }) - .then(function() { - store - .saveIdentity(address.toString(), newIdentity) - .then(function() { - record.fetch().then(function() { - done(); - }); - }); - }); + before(async function() { + await window.Signal.Data.createOrUpdateIdentityKey({ + id: number, + publicKey: testKey.pubKey, + firstUse: true, + timestamp: oldTimestamp, + nonblockingApproval: false, + verified: store.VerifiedStatus.VERIFIED, + }); + await store.saveIdentity(identifier, newIdentity); }); - it('sets the new key to unverified', function() { + it('sets the new key to unverified', async function() { + const identity = await window.Signal.Data.getIdentityKeyById(number); + assert.strictEqual( - record.get('verified'), + identity.verified, store.VerifiedStatus.UNVERIFIED ); }); }); describe('The previous verified status was UNVERIFIED', function() { - before(function(done) { - record - .save({ - publicKey: testKey.pubKey, - firstUse: true, - timestamp: oldTimestamp, - nonblockingApproval: false, - verified: store.VerifiedStatus.UNVERIFIED, - }) - .then(function() { - store - .saveIdentity(address.toString(), newIdentity) - .then(function() { - record.fetch().then(function() { - done(); - }); - }); - }); + before(async function() { + await window.Signal.Data.createOrUpdateIdentityKey({ + id: number, + publicKey: testKey.pubKey, + firstUse: true, + timestamp: oldTimestamp, + nonblockingApproval: false, + verified: store.VerifiedStatus.UNVERIFIED, + }); + + await store.saveIdentity(identifier, newIdentity); }); - it('sets the new key to unverified', function() { + it('sets the new key to unverified', async function() { + const identity = await window.Signal.Data.getIdentityKeyById(number); assert.strictEqual( - record.get('verified'), + identity.verified, store.VerifiedStatus.UNVERIFIED ); }); @@ -217,61 +164,55 @@ describe('SignalProtocolStore', function() { }); describe('When the key has not changed', function() { var oldTimestamp = Date.now(); - before(function(done) { - record - .save({ - publicKey: testKey.pubKey, - timestamp: oldTimestamp, - nonblockingApproval: false, - verified: store.VerifiedStatus.DEFAULT, - }) - .then(function() { - done(); - }); + before(async function() { + await window.Signal.Data.createOrUpdateIdentityKey({ + id: number, + publicKey: testKey.pubKey, + timestamp: oldTimestamp, + nonblockingApproval: false, + verified: store.VerifiedStatus.DEFAULT, + }); }); describe('If it is marked firstUse', function() { - before(function(done) { - record.save({ firstUse: true }).then(function() { - done(); - }); + before(async function() { + const identity = await window.Signal.Data.getIdentityKeyById(number); + identity.firstUse = true; + await window.Signal.Data.createOrUpdateIdentityKey(identity); }); - it('nothing changes', function(done) { - store - .saveIdentity(address.toString(), testKey.pubKey, true) - .then(function() { - record.fetch().then(function() { - assert(!record.get('nonblockingApproval')); - assert.strictEqual(record.get('timestamp'), oldTimestamp); - done(); - }); - }); + it('nothing changes', async function() { + await store.saveIdentity(identifier, testKey.pubKey, true); + + const identity = await window.Signal.Data.getIdentityKeyById(number); + assert(!identity.nonblockingApproval); + assert.strictEqual(identity.timestamp, oldTimestamp); }); }); describe('If it is not marked firstUse', function() { - before(function(done) { - record.save({ firstUse: false }).then(function() { - done(); - }); + before(async function() { + const identity = await window.Signal.Data.getIdentityKeyById(number); + identity.firstUse = false; + await window.Signal.Data.createOrUpdateIdentityKey(identity); }); describe('If nonblocking approval is required', function() { - var now; - before(function(done) { + let now; + before(async function() { now = Date.now(); - record.save({ timestamp: now }).then(function() { - done(); - }); + const identity = await window.Signal.Data.getIdentityKeyById( + number + ); + identity.timestamp = now; + await window.Signal.Data.createOrUpdateIdentityKey(identity); }); - it('sets non-blocking approval', function(done) { - store - .saveIdentity(address.toString(), testKey.pubKey, true) - .then(function() { - record.fetch().then(function() { - assert.strictEqual(record.get('nonblockingApproval'), true); - assert.strictEqual(record.get('timestamp'), now); - assert.strictEqual(record.get('firstUse'), false); - done(); - }); - }); + it('sets non-blocking approval', async function() { + await store.saveIdentity(identifier, testKey.pubKey, true); + + const identity = await window.Signal.Data.getIdentityKeyById( + number + ); + + assert.strictEqual(identity.nonblockingApproval, true); + assert.strictEqual(identity.timestamp, now); + assert.strictEqual(identity.firstUse, false); }); }); }); @@ -279,12 +220,10 @@ describe('SignalProtocolStore', function() { }); describe('saveIdentityWithAttributes', function() { var now; - var record; var validAttributes; - before(function(done) { + before(async function() { now = Date.now(); - record = new IdentityKeyRecord({ id: identifier }); validAttributes = { publicKey: testKey.pubKey, firstUse: true, @@ -293,39 +232,32 @@ describe('SignalProtocolStore', function() { nonblockingApproval: false, }; - store.removeIdentityKey(identifier).then(function() { - done(); - }); + await store.removeIdentityKey(number); }); describe('with valid attributes', function() { - before(function(done) { - store - .saveIdentityWithAttributes(identifier, validAttributes) - .then(function() { - return new Promise(function(resolve) { - record.fetch().then(resolve); - }); - }) - .then(done, done); + before(async function() { + await store.saveIdentityWithAttributes(number, validAttributes); }); - it('publicKey is saved', function() { - assertEqualArrayBuffers(record.get('publicKey'), testKey.pubKey); + it('publicKey is saved', async function() { + const identity = await window.Signal.Data.getIdentityKeyById(number); + assertEqualArrayBuffers(identity.publicKey, testKey.pubKey); }); - it('firstUse is saved', function() { - assert.strictEqual(record.get('firstUse'), true); + it('firstUse is saved', async function() { + const identity = await window.Signal.Data.getIdentityKeyById(number); + assert.strictEqual(identity.firstUse, true); }); - it('timestamp is saved', function() { - assert.strictEqual(record.get('timestamp'), now); + it('timestamp is saved', async function() { + const identity = await window.Signal.Data.getIdentityKeyById(number); + assert.strictEqual(identity.timestamp, now); }); - it('verified is saved', function() { - assert.strictEqual( - record.get('verified'), - store.VerifiedStatus.VERIFIED - ); + it('verified is saved', async function() { + const identity = await window.Signal.Data.getIdentityKeyById(number); + assert.strictEqual(identity.verified, store.VerifiedStatus.VERIFIED); }); - it('nonblockingApproval is saved', function() { - assert.strictEqual(record.get('nonblockingApproval'), false); + it('nonblockingApproval is saved', async function() { + const identity = await window.Signal.Data.getIdentityKeyById(number); + assert.strictEqual(identity.nonblockingApproval, false); }); }); describe('with invalid attributes', function() { @@ -334,124 +266,94 @@ describe('SignalProtocolStore', function() { attributes = _.clone(validAttributes); }); - function testInvalidAttributes(done) { - store.saveIdentityWithAttributes(identifier, attributes).then( - function() { - done(new Error('saveIdentityWithAttributes should have failed')); - }, - function() { - done(); // good. we expect to fail with invalid attributes. - } - ); + async function testInvalidAttributes() { + try { + await store.saveIdentityWithAttributes(number, attributes); + throw new Error('saveIdentityWithAttributes should have failed'); + } catch (error) { + // good. we expect to fail with invalid attributes. + } } - it('rejects an invalid publicKey', function(done) { + it('rejects an invalid publicKey', async function() { attributes.publicKey = 'a string'; - testInvalidAttributes(done); + await testInvalidAttributes(); }); - it('rejects invalid firstUse', function(done) { + it('rejects invalid firstUse', async function() { attributes.firstUse = 0; - testInvalidAttributes(done); + await testInvalidAttributes(); }); - it('rejects invalid timestamp', function(done) { + it('rejects invalid timestamp', async function() { attributes.timestamp = NaN; - testInvalidAttributes(done); + await testInvalidAttributes(); }); - it('rejects invalid verified', function(done) { + it('rejects invalid verified', async function() { attributes.verified = null; - testInvalidAttributes(done); + await testInvalidAttributes(); }); - it('rejects invalid nonblockingApproval', function(done) { + it('rejects invalid nonblockingApproval', async function() { attributes.nonblockingApproval = 0; - testInvalidAttributes(done); + await testInvalidAttributes(); }); }); }); describe('setApproval', function() { - var record = new IdentityKeyRecord({ id: identifier }); - function fetchRecord() { - return new Promise(function(resolve) { - record.fetch().then(resolve); - }); - } - it('sets nonblockingApproval', function(done) { - store - .setApproval(identifier, true) - .then(fetchRecord) - .then(function() { - assert.strictEqual(record.get('nonblockingApproval'), true); - }) - .then(done, done); + it('sets nonblockingApproval', async function() { + await store.setApproval(number, true); + const identity = await window.Signal.Data.getIdentityKeyById(number); + + assert.strictEqual(identity.nonblockingApproval, true); }); }); describe('setVerified', function() { var record; - function saveRecordDefault() { - record = new IdentityKeyRecord({ - id: identifier, + async function saveRecordDefault() { + await window.Signal.Data.createOrUpdateIdentityKey({ + id: number, publicKey: testKey.pubKey, firstUse: true, timestamp: Date.now(), verified: store.VerifiedStatus.DEFAULT, nonblockingApproval: false, }); - return new Promise(function(resolve, reject) { - record.save().then(resolve, reject); - }); - } - function fetchRecord() { - return new Promise(function(resolve, reject) { - record.fetch().then(resolve, reject); - }); } describe('with no public key argument', function() { before(saveRecordDefault); - it('updates the verified status', function() { - return store - .setVerified(identifier, store.VerifiedStatus.VERIFIED) - .then(fetchRecord) - .then(function() { - assert.strictEqual( - record.get('verified'), - store.VerifiedStatus.VERIFIED - ); - assertEqualArrayBuffers(record.get('publicKey'), testKey.pubKey); - }); + it('updates the verified status', async function() { + await store.setVerified(number, store.VerifiedStatus.VERIFIED); + + const identity = await window.Signal.Data.getIdentityKeyById(number); + assert.strictEqual(identity.verified, store.VerifiedStatus.VERIFIED); + assertEqualArrayBuffers(identity.publicKey, testKey.pubKey); }); }); describe('with the current public key', function() { before(saveRecordDefault); - it('updates the verified status', function() { - return store - .setVerified( - identifier, - store.VerifiedStatus.VERIFIED, - testKey.pubKey - ) - .then(fetchRecord) - .then(function() { - assert.strictEqual( - record.get('verified'), - store.VerifiedStatus.VERIFIED - ); - assertEqualArrayBuffers(record.get('publicKey'), testKey.pubKey); - }); + it('updates the verified status', async function() { + await store.setVerified( + number, + store.VerifiedStatus.VERIFIED, + testKey.pubKey + ); + + const identity = await window.Signal.Data.getIdentityKeyById(number); + assert.strictEqual(identity.verified, store.VerifiedStatus.VERIFIED); + assertEqualArrayBuffers(identity.publicKey, testKey.pubKey); }); }); describe('with a mismatching public key', function() { var newIdentity = libsignal.crypto.getRandomBytes(33); before(saveRecordDefault); - it('does not change the record.', function() { - return store - .setVerified(identifier, store.VerifiedStatus.VERIFIED, newIdentity) - .then(fetchRecord) - .then(function() { - assert.strictEqual( - record.get('verified'), - store.VerifiedStatus.DEFAULT - ); - assertEqualArrayBuffers(record.get('publicKey'), testKey.pubKey); - }); + it('does not change the record.', async function() { + await store.setVerified( + number, + store.VerifiedStatus.VERIFIED, + newIdentity + ); + + const identity = await window.Signal.Data.getIdentityKeyById(number); + assert.strictEqual(identity.verified, store.VerifiedStatus.DEFAULT); + assertEqualArrayBuffers(identity.publicKey, testKey.pubKey); }); }); }); @@ -460,10 +362,6 @@ describe('SignalProtocolStore', function() { var newIdentity = libsignal.crypto.getRandomBytes(33); var keychangeTriggered; - function fetchRecord() { - return wrapDeferred(record.fetch()); - } - beforeEach(function() { keychangeTriggered = 0; store.bind('keychange', function() { @@ -476,366 +374,324 @@ describe('SignalProtocolStore', function() { describe('when the new verified status is DEFAULT', function() { describe('when there is no existing record', function() { - before(function() { - record = new IdentityKeyRecord({ id: identifier }); - return wrapDeferred(record.destroy()); + before(async function() { + await window.Signal.Data.removeIdentityKeyById(number); }); - it('does nothing', function() { - return store - .processContactSyncVerificationState( - identifier, - store.VerifiedStatus.DEFAULT, - newIdentity - ) - .then(fetchRecord) - .then( - function() { - // fetchRecord resolved so there is a record. - // Bad. - throw new Error( - 'processContactSyncVerificationState should not save new records' - ); - }, - function() { - assert.strictEqual(keychangeTriggered, 0); - } + it('does nothing', async function() { + await store.processContactSyncVerificationState( + number, + store.VerifiedStatus.DEFAULT, + newIdentity + ); + + const identity = await window.Signal.Data.getIdentityKeyById(number); + + if (identity) { + // fetchRecord resolved so there is a record. + // Bad. + throw new Error( + 'processContactSyncVerificationState should not save new records' ); + } + + assert.strictEqual(keychangeTriggered, 0); }); }); describe('when the record exists', function() { describe('when the existing key is different', function() { - before(function() { - record = new IdentityKeyRecord({ - id: identifier, + before(async function() { + await window.Signal.Data.createOrUpdateIdentityKey({ + id: number, publicKey: testKey.pubKey, firstUse: true, timestamp: Date.now(), verified: store.VerifiedStatus.VERIFIED, nonblockingApproval: false, }); - return wrapDeferred(record.save()); }); - it('does not save the new identity (because this is a less secure state)', function() { - return store - .processContactSyncVerificationState( - identifier, - store.VerifiedStatus.DEFAULT, - newIdentity - ) - .then(fetchRecord) - .then(function() { - assert.strictEqual( - record.get('verified'), - store.VerifiedStatus.VERIFIED - ); - assertEqualArrayBuffers( - record.get('publicKey'), - testKey.pubKey - ); - assert.strictEqual(keychangeTriggered, 0); - }); + it('does not save the new identity (because this is a less secure state)', async function() { + await store.processContactSyncVerificationState( + number, + store.VerifiedStatus.DEFAULT, + newIdentity + ); + + const identity = await window.Signal.Data.getIdentityKeyById( + number + ); + + assert.strictEqual( + identity.verified, + store.VerifiedStatus.VERIFIED + ); + assertEqualArrayBuffers(identity.publicKey, testKey.pubKey); + assert.strictEqual(keychangeTriggered, 0); }); }); describe('when the existing key is the same but VERIFIED', function() { - before(function() { - record = new IdentityKeyRecord({ - id: identifier, + before(async function() { + await window.Signal.Data.createOrUpdateIdentityKey({ + id: number, publicKey: testKey.pubKey, firstUse: true, timestamp: Date.now(), verified: store.VerifiedStatus.VERIFIED, nonblockingApproval: false, }); - return wrapDeferred(record.save()); }); - it('updates the verified status', function() { - return store - .processContactSyncVerificationState( - identifier, - store.VerifiedStatus.DEFAULT, - testKey.pubKey - ) - .then(fetchRecord) - .then(function() { - assert.strictEqual( - record.get('verified'), - store.VerifiedStatus.DEFAULT - ); - assertEqualArrayBuffers( - record.get('publicKey'), - testKey.pubKey - ); - assert.strictEqual(keychangeTriggered, 0); - }); + it('updates the verified status', async function() { + await store.processContactSyncVerificationState( + number, + store.VerifiedStatus.DEFAULT, + testKey.pubKey + ); + + const identity = await window.Signal.Data.getIdentityKeyById( + number + ); + + assert.strictEqual(identity.verified, store.VerifiedStatus.DEFAULT); + assertEqualArrayBuffers(identity.publicKey, testKey.pubKey); + assert.strictEqual(keychangeTriggered, 0); }); }); describe('when the existing key is the same and already DEFAULT', function() { - before(function() { - record = new IdentityKeyRecord({ - id: identifier, + before(async function() { + await window.Signal.Data.createOrUpdateIdentityKey({ + id: number, publicKey: testKey.pubKey, firstUse: true, timestamp: Date.now(), verified: store.VerifiedStatus.DEFAULT, nonblockingApproval: false, }); - return wrapDeferred(record.save()); }); - it('does not hang', function() { - return store - .processContactSyncVerificationState( - identifier, - store.VerifiedStatus.DEFAULT, - testKey.pubKey - ) - .then(fetchRecord) - .then(function() { - assert.strictEqual(keychangeTriggered, 0); - }); + it('does not hang', async function() { + await store.processContactSyncVerificationState( + number, + store.VerifiedStatus.DEFAULT, + testKey.pubKey + ); + + assert.strictEqual(keychangeTriggered, 0); }); }); }); }); describe('when the new verified status is UNVERIFIED', function() { describe('when there is no existing record', function() { - before(function() { - record = new IdentityKeyRecord({ id: identifier }); - return wrapDeferred(record.destroy()); + before(async function() { + await window.Signal.Data.removeIdentityKeyById(number); }); - it('saves the new identity and marks it verified', function() { - return store - .processContactSyncVerificationState( - identifier, - store.VerifiedStatus.UNVERIFIED, - newIdentity - ) - .then(fetchRecord) - .then(function() { - assert.strictEqual( - record.get('verified'), - store.VerifiedStatus.UNVERIFIED - ); - assertEqualArrayBuffers(record.get('publicKey'), newIdentity); - assert.strictEqual(keychangeTriggered, 0); - }); + it('saves the new identity and marks it verified', async function() { + await store.processContactSyncVerificationState( + number, + store.VerifiedStatus.UNVERIFIED, + newIdentity + ); + + const identity = await window.Signal.Data.getIdentityKeyById(number); + + assert.strictEqual( + identity.verified, + store.VerifiedStatus.UNVERIFIED + ); + assertEqualArrayBuffers(identity.publicKey, newIdentity); + assert.strictEqual(keychangeTriggered, 0); }); }); describe('when the record exists', function() { describe('when the existing key is different', function() { - before(function() { - record = new IdentityKeyRecord({ - id: identifier, + before(async function() { + await window.Signal.Data.createOrUpdateIdentityKey({ + id: number, publicKey: testKey.pubKey, firstUse: true, timestamp: Date.now(), verified: store.VerifiedStatus.VERIFIED, nonblockingApproval: false, }); - return wrapDeferred(record.save()); }); - it('saves the new identity and marks it UNVERIFIED', function() { - return store - .processContactSyncVerificationState( - identifier, - store.VerifiedStatus.UNVERIFIED, - newIdentity - ) - .then(fetchRecord) - .then(function() { - assert.strictEqual( - record.get('verified'), - store.VerifiedStatus.UNVERIFIED - ); - assertEqualArrayBuffers(record.get('publicKey'), newIdentity); - assert.strictEqual(keychangeTriggered, 1); - }); + it('saves the new identity and marks it UNVERIFIED', async function() { + await store.processContactSyncVerificationState( + number, + store.VerifiedStatus.UNVERIFIED, + newIdentity + ); + + const identity = await window.Signal.Data.getIdentityKeyById( + number + ); + + assert.strictEqual( + identity.verified, + store.VerifiedStatus.UNVERIFIED + ); + assertEqualArrayBuffers(identity.publicKey, newIdentity); + assert.strictEqual(keychangeTriggered, 1); }); }); describe('when the key exists and is DEFAULT', function() { - before(function() { - record = new IdentityKeyRecord({ - id: identifier, + before(async function() { + await window.Signal.Data.createOrUpdateIdentityKey({ + id: number, publicKey: testKey.pubKey, firstUse: true, timestamp: Date.now(), verified: store.VerifiedStatus.DEFAULT, nonblockingApproval: false, }); - return wrapDeferred(record.save()); }); - it('updates the verified status', function() { - return store - .processContactSyncVerificationState( - identifier, - store.VerifiedStatus.UNVERIFIED, - testKey.pubKey - ) - .then(fetchRecord) - .then(function() { - assert.strictEqual( - record.get('verified'), - store.VerifiedStatus.UNVERIFIED - ); - assertEqualArrayBuffers( - record.get('publicKey'), - testKey.pubKey - ); - assert.strictEqual(keychangeTriggered, 0); - }); + it('updates the verified status', async function() { + await store.processContactSyncVerificationState( + number, + store.VerifiedStatus.UNVERIFIED, + testKey.pubKey + ); + const identity = await window.Signal.Data.getIdentityKeyById( + number + ); + + assert.strictEqual( + identity.verified, + store.VerifiedStatus.UNVERIFIED + ); + assertEqualArrayBuffers(identity.publicKey, testKey.pubKey); + assert.strictEqual(keychangeTriggered, 0); }); }); describe('when the key exists and is already UNVERIFIED', function() { - before(function() { - record = new IdentityKeyRecord({ - id: identifier, + before(async function() { + await window.Signal.Data.createOrUpdateIdentityKey({ + id: number, publicKey: testKey.pubKey, firstUse: true, timestamp: Date.now(), verified: store.VerifiedStatus.UNVERIFIED, nonblockingApproval: false, }); - return wrapDeferred(record.save()); }); - it('does not hang', function() { - return store - .processContactSyncVerificationState( - identifier, - store.VerifiedStatus.UNVERIFIED, - testKey.pubKey - ) - .then(fetchRecord) - .then(function() { - assert.strictEqual(keychangeTriggered, 0); - }); + it('does not hang', async function() { + await store.processContactSyncVerificationState( + number, + store.VerifiedStatus.UNVERIFIED, + testKey.pubKey + ); + + assert.strictEqual(keychangeTriggered, 0); }); }); }); }); describe('when the new verified status is VERIFIED', function() { describe('when there is no existing record', function() { - before(function() { - record = new IdentityKeyRecord({ id: identifier }); - return new Promise(function(resolve, reject) { - record.destroy().then(resolve, reject); - }); + before(async function() { + await window.Signal.Data.removeIdentityKeyById(number); }); - it('saves the new identity and marks it verified', function() { - return store - .processContactSyncVerificationState( - identifier, - store.VerifiedStatus.VERIFIED, - newIdentity - ) - .then(fetchRecord) - .then(function() { - assert.strictEqual( - record.get('verified'), - store.VerifiedStatus.VERIFIED - ); - assertEqualArrayBuffers(record.get('publicKey'), newIdentity); - assert.strictEqual(keychangeTriggered, 0); - }); + it('saves the new identity and marks it verified', async function() { + await store.processContactSyncVerificationState( + number, + store.VerifiedStatus.VERIFIED, + newIdentity + ); + const identity = await window.Signal.Data.getIdentityKeyById(number); + + assert.strictEqual(identity.verified, store.VerifiedStatus.VERIFIED); + assertEqualArrayBuffers(identity.publicKey, newIdentity); + assert.strictEqual(keychangeTriggered, 0); }); }); describe('when the record exists', function() { describe('when the existing key is different', function() { - before(function() { - record = new IdentityKeyRecord({ - id: identifier, + before(async function() { + await window.Signal.Data.createOrUpdateIdentityKey({ + id: number, publicKey: testKey.pubKey, firstUse: true, timestamp: Date.now(), verified: store.VerifiedStatus.VERIFIED, nonblockingApproval: false, }); - return wrapDeferred(record.save()); }); - it('saves the new identity and marks it VERIFIED', function() { - return store - .processContactSyncVerificationState( - identifier, - store.VerifiedStatus.VERIFIED, - newIdentity - ) - .then(fetchRecord) - .then(function() { - assert.strictEqual( - record.get('verified'), - store.VerifiedStatus.VERIFIED - ); - assertEqualArrayBuffers(record.get('publicKey'), newIdentity); - assert.strictEqual(keychangeTriggered, 1); - }); + it('saves the new identity and marks it VERIFIED', async function() { + await store.processContactSyncVerificationState( + number, + store.VerifiedStatus.VERIFIED, + newIdentity + ); + + const identity = await window.Signal.Data.getIdentityKeyById( + number + ); + + assert.strictEqual( + identity.verified, + store.VerifiedStatus.VERIFIED + ); + assertEqualArrayBuffers(identity.publicKey, newIdentity); + assert.strictEqual(keychangeTriggered, 1); }); }); describe('when the existing key is the same but UNVERIFIED', function() { - before(function() { - record = new IdentityKeyRecord({ - id: identifier, + before(async function() { + await window.Signal.Data.createOrUpdateIdentityKey({ + id: number, publicKey: testKey.pubKey, firstUse: true, timestamp: Date.now(), verified: store.VerifiedStatus.UNVERIFIED, nonblockingApproval: false, }); - return wrapDeferred(record.save()); }); - it('saves the identity and marks it verified', function() { - return store - .processContactSyncVerificationState( - identifier, - store.VerifiedStatus.VERIFIED, - testKey.pubKey - ) - .then(fetchRecord) - .then(function() { - assert.strictEqual( - record.get('verified'), - store.VerifiedStatus.VERIFIED - ); - assertEqualArrayBuffers( - record.get('publicKey'), - testKey.pubKey - ); - assert.strictEqual(keychangeTriggered, 0); - }); + it('saves the identity and marks it verified', async function() { + await store.processContactSyncVerificationState( + number, + store.VerifiedStatus.VERIFIED, + testKey.pubKey + ); + const identity = await window.Signal.Data.getIdentityKeyById( + number + ); + + assert.strictEqual( + identity.verified, + store.VerifiedStatus.VERIFIED + ); + assertEqualArrayBuffers(identity.publicKey, testKey.pubKey); + assert.strictEqual(keychangeTriggered, 0); }); }); describe('when the existing key is the same and already VERIFIED', function() { - before(function() { - record = new IdentityKeyRecord({ - id: identifier, + before(async function() { + await window.Signal.Data.createOrUpdateIdentityKey({ + id: number, publicKey: testKey.pubKey, firstUse: true, timestamp: Date.now(), verified: store.VerifiedStatus.VERIFIED, nonblockingApproval: false, }); - return wrapDeferred(record.save()); }); - it('does not hang', function() { - return store - .processContactSyncVerificationState( - identifier, - store.VerifiedStatus.VERIFIED, - testKey.pubKey - ) - .then(fetchRecord) - .then(function() { - assert.strictEqual(keychangeTriggered, 0); - }); + it('does not hang', async function() { + await store.processContactSyncVerificationState( + number, + store.VerifiedStatus.VERIFIED, + testKey.pubKey + ); + + assert.strictEqual(keychangeTriggered, 0); }); }); }); @@ -843,434 +699,312 @@ describe('SignalProtocolStore', function() { }); describe('isUntrusted', function() { - it('returns false if identity key old enough', function() { - var record = new IdentityKeyRecord({ - id: identifier, + it('returns false if identity key old enough', async function() { + await window.Signal.Data.createOrUpdateIdentityKey({ + id: number, publicKey: testKey.pubKey, timestamp: Date.now() - 10 * 1000 * 60, verified: store.VerifiedStatus.DEFAULT, firstUse: false, nonblockingApproval: false, }); - return wrapDeferred(record.save()) - .then(function() { - return store.isUntrusted(identifier); - }) - .then(function(untrusted) { - assert.strictEqual(untrusted, false); - }); + + const untrusted = await store.isUntrusted(number); + assert.strictEqual(untrusted, false); }); - it('returns false if new but nonblockingApproval is true', function() { - var record = new IdentityKeyRecord({ - id: identifier, + it('returns false if new but nonblockingApproval is true', async function() { + await window.Signal.Data.createOrUpdateIdentityKey({ + id: number, publicKey: testKey.pubKey, timestamp: Date.now(), verified: store.VerifiedStatus.DEFAULT, firstUse: false, nonblockingApproval: true, }); - return wrapDeferred(record.save()) - .then(function() { - return store.isUntrusted(identifier); - }) - .then(function(untrusted) { - assert.strictEqual(untrusted, false); - }); + + const untrusted = await store.isUntrusted(number); + assert.strictEqual(untrusted, false); }); - it('returns false if new but firstUse is true', function() { - var record = new IdentityKeyRecord({ - id: identifier, + it('returns false if new but firstUse is true', async function() { + await window.Signal.Data.createOrUpdateIdentityKey({ + id: number, publicKey: testKey.pubKey, timestamp: Date.now(), verified: store.VerifiedStatus.DEFAULT, firstUse: true, nonblockingApproval: false, }); - return wrapDeferred(record.save()) - .then(function() { - return store.isUntrusted(identifier); - }) - .then(function(untrusted) { - assert.strictEqual(untrusted, false); - }); + + const untrusted = await store.isUntrusted(number); + assert.strictEqual(untrusted, false); }); - it('returns true if new, and no flags are set', function() { - var record = new IdentityKeyRecord({ - id: identifier, + it('returns true if new, and no flags are set', async function() { + await window.Signal.Data.createOrUpdateIdentityKey({ + id: number, publicKey: testKey.pubKey, timestamp: Date.now(), verified: store.VerifiedStatus.DEFAULT, firstUse: false, nonblockingApproval: false, }); - return wrapDeferred(record.save()) - .then(function() { - return store.isUntrusted(identifier); - }) - .then(function(untrusted) { - assert.strictEqual(untrusted, true); - }); + const untrusted = await store.isUntrusted(number); + assert.strictEqual(untrusted, true); }); }); describe('getVerified', function() { - before(function(done) { - store - .setVerified(identifier, store.VerifiedStatus.VERIFIED) - .then(done, done); + before(async function() { + await store.setVerified(number, store.VerifiedStatus.VERIFIED); }); - it('resolves to the verified status', function(done) { - store - .getVerified(identifier) - .then(function(result) { - assert.strictEqual(result, store.VerifiedStatus.VERIFIED); - }) - .then(done, done); + it('resolves to the verified status', async function() { + const result = await store.getVerified(number); + assert.strictEqual(result, store.VerifiedStatus.VERIFIED); }); }); describe('isTrustedIdentity', function() { - var address = new libsignal.SignalProtocolAddress(identifier, 1); - describe('When invalid direction is given', function(done) { - it('should fail', function(done) { - store - .isTrustedIdentity(identifier, testKey.pubKey) - .then(function() { - done(new Error('isTrustedIdentity should have failed')); - }) - .catch(function(e) { - done(); - }); + const address = new libsignal.SignalProtocolAddress(number, 1); + const identifier = address.toString(); + + describe('When invalid direction is given', function() { + it('should fail', async function() { + try { + await store.isTrustedIdentity(number, testKey.pubKey); + throw new Error('isTrustedIdentity should have failed'); + } catch (error) { + // good + } }); }); describe('When direction is RECEIVING', function() { - it('always returns true', function(done) { + it('always returns true', async function() { var newIdentity = libsignal.crypto.getRandomBytes(33); - store.saveIdentity(address.toString(), testKey.pubKey).then(function() { - store - .isTrustedIdentity( - identifier, - newIdentity, - store.Direction.RECEIVING - ) - .then(function(trusted) { - if (trusted) { - done(); - } else { - done(new Error('isTrusted returned false when receiving')); - } - }) - .catch(done); - }); + await store.saveIdentity(identifier, testKey.pubKey); + + const trusted = await store.isTrustedIdentity( + identifier, + newIdentity, + store.Direction.RECEIVING + ); + + if (!trusted) { + throw new Error('isTrusted returned false when receiving'); + } }); }); describe('When direction is SENDING', function() { describe('When there is no existing key (first use)', function() { - before(function(done) { - store.removeIdentityKey(identifier).then(function() { - done(); - }); + before(async function() { + await store.removeIdentityKey(number); }); - it('returns true', function(done) { - var newIdentity = libsignal.crypto.getRandomBytes(33); - store - .isTrustedIdentity(identifier, newIdentity, store.Direction.SENDING) - .then(function(trusted) { - if (trusted) { - done(); - } else { - done(new Error('isTrusted returned false on first use')); - } - }) - .catch(done); + it('returns true', async function() { + const newIdentity = libsignal.crypto.getRandomBytes(33); + const trusted = await store.isTrustedIdentity( + identifier, + newIdentity, + store.Direction.SENDING + ); + if (!trusted) { + throw new Error('isTrusted returned false on first use'); + } }); }); describe('When there is an existing key', function() { - before(function(done) { - store - .saveIdentity(address.toString(), testKey.pubKey) - .then(function() { - done(); - }); + before(async function() { + await store.saveIdentity(identifier, testKey.pubKey); }); describe('When the existing key is different', function() { - it('returns false', function(done) { - var newIdentity = libsignal.crypto.getRandomBytes(33); - store - .isTrustedIdentity( - identifier, - newIdentity, - store.Direction.SENDING - ) - .then(function(trusted) { - if (trusted) { - done(new Error('isTrusted returned true on untrusted key')); - } else { - done(); - } - }) - .catch(done); + it('returns false', async function() { + const newIdentity = libsignal.crypto.getRandomBytes(33); + const trusted = await store.isTrustedIdentity( + identifier, + newIdentity, + store.Direction.SENDING + ); + if (trusted) { + throw new Error('isTrusted returned true on untrusted key'); + } }); }); describe('When the existing key matches the new key', function() { - var newIdentity = libsignal.crypto.getRandomBytes(33); - before(function(done) { - store - .saveIdentity(address.toString(), newIdentity) - .then(function() { - done(); - }); + const newIdentity = libsignal.crypto.getRandomBytes(33); + before(async function() { + await store.saveIdentity(identifier, newIdentity); }); - it('returns false if keys match but we just received this new identiy', function(done) { - store - .isTrustedIdentity( - identifier, - newIdentity, - store.Direction.SENDING - ) - .then(function(trusted) { - if (trusted) { - done(new Error('isTrusted returned true on untrusted key')); - } else { - done(); - } - }) - .catch(done); + it('returns false if keys match but we just received this new identiy', async function() { + const trusted = await store.isTrustedIdentity( + identifier, + newIdentity, + store.Direction.SENDING + ); + + if (trusted) { + throw new Error('isTrusted returned true on untrusted key'); + } }); - it('returns true if we have already approved identity', function(done) { - store - .saveIdentity(address.toString(), newIdentity, true) - .then(function() { - store - .isTrustedIdentity( - identifier, - newIdentity, - store.Direction.SENDING - ) - .then(function(trusted) { - if (trusted) { - done(); - } else { - done( - new Error('isTrusted returned false on an approved key') - ); - } - }) - .catch(done); - }); + it('returns true if we have already approved identity', async function() { + await store.saveIdentity(identifier, newIdentity, true); + + const trusted = await store.isTrustedIdentity( + identifier, + newIdentity, + store.Direction.SENDING + ); + if (!trusted) { + throw new Error('isTrusted returned false on an approved key'); + } }); }); }); }); }); describe('storePreKey', function() { - it('stores prekeys', function(done) { - store - .storePreKey(1, testKey) - .then(function() { - return store.loadPreKey(1).then(function(key) { - assertEqualArrayBuffers(key.pubKey, testKey.pubKey); - assertEqualArrayBuffers(key.privKey, testKey.privKey); - }); - }) - .then(done, done); + it('stores prekeys', async function() { + await store.storePreKey(1, testKey); + const key = await store.loadPreKey(1); + assertEqualArrayBuffers(key.pubKey, testKey.pubKey); + assertEqualArrayBuffers(key.privKey, testKey.privKey); }); }); describe('removePreKey', function() { - before(function(done) { - store.storePreKey(2, testKey).then(done); + before(async function() { + await store.storePreKey(2, testKey); }); - it('deletes prekeys', function(done) { - store - .removePreKey(2, testKey) - .then(function() { - return store.loadPreKey(2).then(function(key) { - assert.isUndefined(key); - }); - }) - .then(done, done); + it('deletes prekeys', async function() { + await store.removePreKey(2, testKey); + + const key = await store.loadPreKey(2); + assert.isUndefined(key); }); }); describe('storeSignedPreKey', function() { - it('stores signed prekeys', function(done) { - store - .storeSignedPreKey(3, testKey) - .then(function() { - return store.loadSignedPreKey(3).then(function(key) { - assertEqualArrayBuffers(key.pubKey, testKey.pubKey); - assertEqualArrayBuffers(key.privKey, testKey.privKey); - }); - }) - .then(done, done); + it('stores signed prekeys', async function() { + await store.storeSignedPreKey(3, testKey); + + const key = await store.loadSignedPreKey(3); + assertEqualArrayBuffers(key.pubKey, testKey.pubKey); + assertEqualArrayBuffers(key.privKey, testKey.privKey); }); }); describe('removeSignedPreKey', function() { - before(function(done) { - store.storeSignedPreKey(4, testKey).then(done); + before(async function() { + await store.storeSignedPreKey(4, testKey); }); - it('deletes signed prekeys', function(done) { - store - .removeSignedPreKey(4, testKey) - .then(function() { - return store.loadSignedPreKey(4).then(function(key) { - assert.isUndefined(key); - }); - }) - .then(done, done); + it('deletes signed prekeys', async function() { + await store.removeSignedPreKey(4, testKey); + + const key = await store.loadSignedPreKey(4); + assert.isUndefined(key); }); }); describe('storeSession', function() { - it('stores sessions', function(done) { - var testRecord = 'an opaque string'; - store - .storeSession(identifier + '.1', testRecord) - .then(function() { - return store.loadSession(identifier + '.1').then(function(record) { - assert.deepEqual(record, testRecord); - }); - }) - .then(done, done); + it('stores sessions', async function() { + const testRecord = 'an opaque string'; + + await store.storeSession(number + '.1', testRecord); + const record = await store.loadSession(number + '.1'); + + assert.deepEqual(record, testRecord); }); }); describe('removeAllSessions', function() { - it('removes all sessions for a number', function(done) { - var testRecord = 'an opaque string'; - var devices = [1, 2, 3].map(function(deviceId) { - return [identifier, deviceId].join('.'); + it('removes all sessions for a number', async function() { + const testRecord = 'an opaque string'; + const devices = [1, 2, 3].map(function(deviceId) { + return [number, deviceId].join('.'); }); - var promise = Promise.resolve(); - devices.forEach(function(encodedNumber) { - promise = promise.then(function() { - return store.storeSession(encodedNumber, testRecord + encodedNumber); - }); - }); - promise - .then(function() { - return store.removeAllSessions(identifier).then(function(record) { - return Promise.all(devices.map(store.loadSession.bind(store))).then( - function(records) { - for (var i in records) { - assert.isUndefined(records[i]); - } - } - ); - }); + + await Promise.all( + devices.map(async function(encodedNumber) { + await store.storeSession(encodedNumber, testRecord + encodedNumber); }) - .then(done, done); + ); + + await store.removeAllSessions(number); + + const records = await Promise.all( + devices.map(store.loadSession.bind(store)) + ); + for (var i in records) { + assert.isUndefined(records[i]); + } }); }); describe('clearSessionStore', function() { - it('clears the session store', function(done) { - var testRecord = 'an opaque string'; - store - .storeSession(identifier + '.1', testRecord) - .then(function() { - return store.clearSessionStore().then(function() { - return store.loadSession(identifier + '.1').then(function(record) { - assert.isUndefined(record); - }); - }); - }) - .then(done, done); + it('clears the session store', async function() { + const testRecord = 'an opaque string'; + await store.storeSession(number + '.1', testRecord); + await store.clearSessionStore(); + + const record = await store.loadSession(number + '.1'); + assert.isUndefined(record); }); }); describe('getDeviceIds', function() { - it('returns deviceIds for a number', function(done) { - var testRecord = 'an opaque string'; - var devices = [1, 2, 3].map(function(deviceId) { - return [identifier, deviceId].join('.'); + it('returns deviceIds for a number', async function() { + const testRecord = 'an opaque string'; + const devices = [1, 2, 3].map(function(deviceId) { + return [number, deviceId].join('.'); }); - var promise = Promise.resolve(); - devices.forEach(function(encodedNumber) { - promise = promise.then(function() { - return store.storeSession(encodedNumber, testRecord + encodedNumber); - }); - }); - promise - .then(function() { - return store.getDeviceIds(identifier).then(function(deviceIds) { - assert.sameMembers(deviceIds, [1, 2, 3]); - }); + + await Promise.all( + devices.map(async function(encodedNumber) { + await store.storeSession(encodedNumber, testRecord + encodedNumber); }) - .then(done, done); + ); + + const deviceIds = await store.getDeviceIds(number); + assert.sameMembers(deviceIds, [1, 2, 3]); }); - it('returns empty array for a number with no device ids', function() { - return store.getDeviceIds('foo').then(function(deviceIds) { - assert.sameMembers(deviceIds, []); - }); + it('returns empty array for a number with no device ids', async function() { + const deviceIds = await store.getDeviceIds('foo'); + assert.sameMembers(deviceIds, []); }); }); describe('Not yet processed messages', function() { - beforeEach(function() { - return store - .getAllUnprocessed() - .then(function(items) { - return Promise.all( - _.map(items, function(item) { - return store.removeUnprocessed(item.id); - }) - ); - }) - .then(function() { - return store.getAllUnprocessed(); - }) - .then(function(items) { - assert.strictEqual(items.length, 0); - }); + beforeEach(async function() { + await store.removeAllUnprocessed(); + const items = await store.getAllUnprocessed(); + assert.strictEqual(items.length, 0); }); - it('adds two and gets them back', function() { - return Promise.all([ + it('adds two and gets them back', async function() { + await Promise.all([ store.addUnprocessed({ id: 2, name: 'second', timestamp: 2 }), store.addUnprocessed({ id: 3, name: 'third', timestamp: 3 }), store.addUnprocessed({ id: 1, name: 'first', timestamp: 1 }), - ]) - .then(function() { - return store.getAllUnprocessed(); - }) - .then(function(items) { - assert.strictEqual(items.length, 3); + ]); - // they are in the proper order because the collection comparator is 'timestamp' - assert.strictEqual(items[0].name, 'first'); - assert.strictEqual(items[1].name, 'second'); - assert.strictEqual(items[2].name, 'third'); - }); + const items = await store.getAllUnprocessed(); + assert.strictEqual(items.length, 3); + + // they are in the proper order because the collection comparator is 'timestamp' + assert.strictEqual(items[0].name, 'first'); + assert.strictEqual(items[1].name, 'second'); + assert.strictEqual(items[2].name, 'third'); }); - it('saveUnprocessed successfully updates item', function() { - var id = 1; - return store - .addUnprocessed({ id: id, name: 'first', timestamp: 1 }) - .then(function() { - return store.saveUnprocessed({ id, name: 'updated', timestamp: 1 }); - }) - .then(function() { - return store.getAllUnprocessed(); - }) - .then(function(items) { - assert.strictEqual(items.length, 1); - assert.strictEqual(items[0].name, 'updated'); - assert.strictEqual(items[0].timestamp, 1); - }); + it('saveUnprocessed successfully updates item', async function() { + const id = 1; + await store.addUnprocessed({ id: id, name: 'first', timestamp: 1 }); + await store.saveUnprocessed({ id, name: 'updated', timestamp: 1 }); + + const items = await store.getAllUnprocessed(); + assert.strictEqual(items.length, 1); + assert.strictEqual(items[0].name, 'updated'); + assert.strictEqual(items[0].timestamp, 1); }); - it('removeUnprocessed successfully deletes item', function() { - var id = 1; - return store - .addUnprocessed({ id: id, name: 'first', timestamp: 1 }) - .then(function() { - return store.removeUnprocessed(id); - }) - .then(function() { - return store.getAllUnprocessed(); - }) - .then(function(items) { - assert.strictEqual(items.length, 0); - }); + it('removeUnprocessed successfully deletes item', async function() { + const id = 1; + await store.addUnprocessed({ id: id, name: 'first', timestamp: 1 }); + await store.removeUnprocessed(id); + + const items = await store.getAllUnprocessed(); + assert.strictEqual(items.length, 0); }); }); }); diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index 2052fb7c047b..c19a2ca276c5 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -164,7 +164,7 @@ "rule": "jQuery-$(", "path": "js/background.js", "line": " if ($('.dark-overlay').length) {", - "lineNumber": 265, + "lineNumber": 264, "reasonCategory": "usageTrusted", "updated": "2018-09-19T21:59:32.770Z", "reasonDetail": "Protected from arbitrary input" @@ -173,7 +173,7 @@ "rule": "jQuery-$(", "path": "js/background.js", "line": " $(document.body).prepend('
');", - "lineNumber": 268, + "lineNumber": 267, "reasonCategory": "usageTrusted", "updated": "2018-09-19T21:59:32.770Z", "reasonDetail": "Protected from arbitrary input" @@ -182,7 +182,7 @@ "rule": "jQuery-prepend(", "path": "js/background.js", "line": " $(document.body).prepend('
');", - "lineNumber": 268, + "lineNumber": 267, "reasonCategory": "usageTrusted", "updated": "2018-09-19T18:13:29.628Z", "reasonDetail": "Hard-coded value" @@ -191,7 +191,7 @@ "rule": "jQuery-$(", "path": "js/background.js", "line": " $('.dark-overlay').on('click', () => $('.dark-overlay').remove());", - "lineNumber": 269, + "lineNumber": 268, "reasonCategory": "usageTrusted", "updated": "2018-09-19T21:59:32.770Z", "reasonDetail": "Protected from arbitrary input" @@ -200,7 +200,7 @@ "rule": "jQuery-$(", "path": "js/background.js", "line": " removeDarkOverlay: () => $('.dark-overlay').remove(),", - "lineNumber": 271, + "lineNumber": 270, "reasonCategory": "usageTrusted", "updated": "2018-09-19T21:59:32.770Z", "reasonDetail": "Protected from arbitrary input" @@ -209,7 +209,7 @@ "rule": "jQuery-$(", "path": "js/background.js", "line": " $('body').append(clearDataView.el);", - "lineNumber": 274, + "lineNumber": 273, "reasonCategory": "usageTrusted", "updated": "2018-09-19T21:59:32.770Z", "reasonDetail": "Protected from arbitrary input" @@ -218,7 +218,7 @@ "rule": "jQuery-append(", "path": "js/background.js", "line": " $('body').append(clearDataView.el);", - "lineNumber": 274, + "lineNumber": 273, "reasonCategory": "usageTrusted", "updated": "2018-09-19T18:13:29.628Z", "reasonDetail": "Interacting with already-existing DOM nodes" @@ -227,7 +227,7 @@ "rule": "jQuery-load(", "path": "js/background.js", "line": " await ConversationController.load();", - "lineNumber": 509, + "lineNumber": 404, "reasonCategory": "falseMatch", "updated": "2018-10-02T21:00:44.007Z" }, @@ -235,7 +235,7 @@ "rule": "jQuery-$(", "path": "js/background.js", "line": " el: $('body'),", - "lineNumber": 572, + "lineNumber": 467, "reasonCategory": "usageTrusted", "updated": "2018-10-16T23:47:48.006Z", "reasonDetail": "Protected from arbitrary input" @@ -244,7 +244,7 @@ "rule": "jQuery-wrap(", "path": "js/background.js", "line": " wrap(", - "lineNumber": 830, + "lineNumber": 725, "reasonCategory": "falseMatch", "updated": "2018-10-18T22:23:00.485Z" }, @@ -252,7 +252,7 @@ "rule": "jQuery-wrap(", "path": "js/background.js", "line": " await wrap(", - "lineNumber": 1320, + "lineNumber": 1215, "reasonCategory": "falseMatch", "updated": "2018-10-26T22:43:23.229Z" }, @@ -303,7 +303,7 @@ "rule": "jQuery-wrap(", "path": "js/models/messages.js", "line": " this.send(wrap(promise));", - "lineNumber": 794, + "lineNumber": 791, "reasonCategory": "falseMatch", "updated": "2018-10-05T23:12:28.961Z" }, @@ -311,7 +311,7 @@ "rule": "jQuery-wrap(", "path": "js/models/messages.js", "line": " return wrap(", - "lineNumber": 996, + "lineNumber": 993, "reasonCategory": "falseMatch", "updated": "2018-10-05T23:12:28.961Z" }, @@ -445,7 +445,7 @@ "rule": "jQuery-load(", "path": "js/signal_protocol_store.js", "line": " await ConversationController.load();", - "lineNumber": 972, + "lineNumber": 848, "reasonCategory": "falseMatch", "updated": "2018-09-15T00:38:04.183Z" }, @@ -1315,7 +1315,7 @@ "rule": "jQuery-load(", "path": "js/views/import_view.js", "line": " return ConversationController.load()", - "lineNumber": 179, + "lineNumber": 176, "reasonCategory": "falseMatch", "updated": "2018-09-15T00:38:04.183Z" }, @@ -1844,7 +1844,7 @@ "rule": "jQuery-$(", "path": "js/views/recipients_input_view.js", "line": " this.$input = this.$('input.search');", - "lineNumber": 71, + "lineNumber": 69, "reasonCategory": "usageTrusted", "updated": "2018-09-19T21:59:32.770Z", "reasonDetail": "Protected from arbitrary input" @@ -1853,7 +1853,7 @@ "rule": "jQuery-$(", "path": "js/views/recipients_input_view.js", "line": " this.$new_contact = this.$('.new-contact');", - "lineNumber": 72, + "lineNumber": 70, "reasonCategory": "usageTrusted", "updated": "2018-09-19T21:59:32.770Z", "reasonDetail": "Protected from arbitrary input" @@ -1862,7 +1862,7 @@ "rule": "jQuery-$(", "path": "js/views/recipients_input_view.js", "line": " el: this.$('.recipients'),", - "lineNumber": 82, + "lineNumber": 80, "reasonCategory": "usageTrusted", "updated": "2018-09-19T21:59:32.770Z", "reasonDetail": "Protected from arbitrary input" @@ -1871,7 +1871,7 @@ "rule": "jQuery-$(", "path": "js/views/recipients_input_view.js", "line": " this.$('.contacts').append(this.typeahead_view.el);", - "lineNumber": 97, + "lineNumber": 95, "reasonCategory": "usageTrusted", "updated": "2018-09-19T21:59:32.770Z", "reasonDetail": "Protected from arbitrary input" @@ -1880,7 +1880,7 @@ "rule": "jQuery-append(", "path": "js/views/recipients_input_view.js", "line": " this.$('.contacts').append(this.typeahead_view.el);", - "lineNumber": 97, + "lineNumber": 95, "reasonCategory": "usageTrusted", "updated": "2018-09-19T18:13:29.628Z", "reasonDetail": "Interacting with already-existing DOM nodes"