From b8b26d3e7935dc1e5b87bcc82771cdd30cf45afc Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Mon, 27 Nov 2017 15:44:44 -0800 Subject: [PATCH 1/9] On group/contact import: don't re-add hidden entries to left pane (#1811) * On contact import: don't re-add hidden contacts to left pane * On group import: don't re-add hidden groups to left pane --- js/background.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/js/background.js b/js/background.js index e22cdcc7c8..3bdabdc89d 100644 --- a/js/background.js +++ b/js/background.js @@ -367,6 +367,14 @@ return ConversationController.getOrCreateAndWait(id, 'private') .then(function(conversation) { return new Promise(function(resolve, reject) { + var activeAt = conversation.get('active_at'); + + // The idea is to make any new contact show up in the left pane. If + // activeAt is null, then this contact has been purposefully hidden. + if (activeAt !== null) { + activeAt = activeAt || Date.now(); + } + if (details.profileKey) { conversation.set({profileKey: details.profileKey}); } @@ -374,7 +382,7 @@ name: details.name, avatar: details.avatar, color: details.color, - active_at: conversation.get('active_at') || Date.now(), + active_at: activeAt, }).then(resolve, reject); }).then(function() { if (details.verified) { @@ -411,7 +419,13 @@ type: 'group', }; if (details.active) { - updates.active_at = Date.now(); + var activeAt = conversation.get('active_at'); + + // The idea is to make any new group show up in the left pane. If + // activeAt is null, then this group has been purposefully hidden. + if (activeAt !== null) { + updates.active_at = activeAt || Date.now(); + } } else { updates.left = true; } From a5923c2177593ebf963fa32d0c5fcbc7692c47b7 Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Thu, 30 Nov 2017 11:55:59 -0800 Subject: [PATCH 2/9] Key rotation: log failures, retry, force on new version (#1833) * Retry failed signed key rotation; start rotation when registered (#1772) * rotateSignedPrekeys: Fix 'res is not defined' error * If the server rejects key rotation, don't retry immediately * Force a signed key rotation on launch of any new version --- js/background.js | 4 +++- js/libtextsecure.js | 19 +++++++++++++------ js/rotate_signed_prekey_listener.js | 13 ++++++++++++- libtextsecure/account_manager.js | 19 +++++++++++++------ 4 files changed, 41 insertions(+), 14 deletions(-) diff --git a/js/background.js b/js/background.js index 3bdabdc89d..36fb406e6c 100644 --- a/js/background.js +++ b/js/background.js @@ -83,6 +83,7 @@ if (!lastVersion || currentVersion !== lastVersion) { console.log('New version detected:', currentVersion); + getAccountManager().rotateSignedPreKey(); } window.dispatchEvent(new Event('storage_ready')); @@ -90,19 +91,20 @@ console.log('listening for registration events'); Whisper.events.on('registration_done', function() { console.log('handling registration event'); + Whisper.RotateSignedPreKeyListener.init(Whisper.events); connect(true); }); var appView = window.owsDesktopApp.appView = new Whisper.AppView({el: $('body')}); Whisper.WallClockListener.init(Whisper.events); - Whisper.RotateSignedPreKeyListener.init(Whisper.events); Whisper.ExpiringMessagesListener.init(Whisper.events); if (Whisper.Import.isIncomplete()) { console.log('Import was interrupted, showing import error screen'); appView.openImporter(); } else if (Whisper.Registration.everDone()) { + Whisper.RotateSignedPreKeyListener.init(Whisper.events); connect(); appView.openInbox({ initialLoadComplete: initialLoadComplete diff --git a/js/libtextsecure.js b/js/libtextsecure.js index b452eac819..14980063a1 100644 --- a/js/libtextsecure.js +++ b/js/libtextsecure.js @@ -38008,13 +38008,20 @@ var TextSecureServer = (function() { return store.storeSignedPreKey(res.keyId, res.keyPair).then(function() { return cleanSignedPreKeys(); }); - }).catch(function(e) { - if (e instanceof Error && e.name == 'HTTPError' && e.code >= 400 && e.code <= 599) { - var rejections = 1 + textsecure.storage.get('signedKeyRotationRejected', 0); - textsecure.storage.put('signedKeyRotationRejected', rejections); - console.log('Signed key rotation rejected count:', rejections); - } }); + }).catch(function(e) { + console.log( + 'rotateSignedPrekey error:', + e && e.stack ? e.stack : e + ); + + if (e instanceof Error && e.name == 'HTTPError' && e.code >= 400 && e.code <= 599) { + var rejections = 1 + textsecure.storage.get('signedKeyRotationRejected', 0); + textsecure.storage.put('signedKeyRotationRejected', rejections); + console.log('Signed key rotation rejected count:', rejections); + } else { + throw e; + } }); }.bind(this)); }, diff --git a/js/rotate_signed_prekey_listener.js b/js/rotate_signed_prekey_listener.js index 43fe6ff827..a52ece8e39 100644 --- a/js/rotate_signed_prekey_listener.js +++ b/js/rotate_signed_prekey_listener.js @@ -17,7 +17,10 @@ function run() { console.log('Rotating signed prekey...'); - getAccountManager().rotateSignedPreKey(); + getAccountManager().rotateSignedPreKey().catch(function() { + console.log('rotateSignedPrekey() failed. Trying again in five seconds'); + setTimeout(runWhenOnline, 5000); + }); scheduleNextRotation(); setTimeoutForNextRun(); } @@ -26,6 +29,7 @@ if (navigator.onLine) { run(); } else { + console.log('We are offline; keys will be rotated when we are next online'); var listener = function() { window.removeEventListener('online', listener); run(); @@ -52,8 +56,15 @@ timeout = setTimeout(runWhenOnline, waitTime); } + var initComplete; Whisper.RotateSignedPreKeyListener = { init: function(events) { + if (initComplete) { + console.log('Rotate signed prekey listener: Already initialized'); + return; + } + initComplete = true; + if (Whisper.Registration.isDone()) { setTimeoutForNextRun(); } diff --git a/libtextsecure/account_manager.js b/libtextsecure/account_manager.js index 61c06a44c6..45c904059f 100644 --- a/libtextsecure/account_manager.js +++ b/libtextsecure/account_manager.js @@ -138,13 +138,20 @@ return store.storeSignedPreKey(res.keyId, res.keyPair).then(function() { return cleanSignedPreKeys(); }); - }).catch(function(e) { - if (e instanceof Error && e.name == 'HTTPError' && e.code >= 400 && e.code <= 599) { - var rejections = 1 + textsecure.storage.get('signedKeyRotationRejected', 0); - textsecure.storage.put('signedKeyRotationRejected', rejections); - console.log('Signed key rotation rejected count:', rejections); - } }); + }).catch(function(e) { + console.log( + 'rotateSignedPrekey error:', + e && e.stack ? e.stack : e + ); + + if (e instanceof Error && e.name == 'HTTPError' && e.code >= 400 && e.code <= 599) { + var rejections = 1 + textsecure.storage.get('signedKeyRotationRejected', 0); + textsecure.storage.put('signedKeyRotationRejected', rejections); + console.log('Signed key rotation rejected count:', rejections); + } else { + throw e; + } }); }.bind(this)); }, From 44da6924f9efd34a83c5cbcdf10f064ac7adcb38 Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Thu, 30 Nov 2017 11:56:29 -0800 Subject: [PATCH 3/9] A variety of logging improvements to track down bugs (#1832) * Log when we get a blocked numbers sync message * Save three old signed keys in addition to the current active * Remove the mystery from all the error-related log messages * Log successful load of signed key - to help debug prekey errors * removeSignedPreKey: Don't hang or crash in error cases * Log on top-level unhandled promise rejection * Remove trailing comma in param list, Electron 1.6 does not like * Harden top-level error handler for strange object shapes --- js/background.js | 3 +-- js/libtextsecure.js | 3 ++- js/logging.js | 6 +++++- js/models/messages.js | 7 +++++-- js/signal_protocol_store.js | 16 +++++++++++++--- libtextsecure/account_manager.js | 2 +- libtextsecure/message_receiver.js | 1 + 7 files changed, 28 insertions(+), 10 deletions(-) diff --git a/js/background.js b/js/background.js index 36fb406e6c..b0aa1268c2 100644 --- a/js/background.js +++ b/js/background.js @@ -559,8 +559,7 @@ function onError(ev) { var error = ev.error; - console.log(error); - console.log(error.stack); + console.log('background onError:', error && error.stack ? error.stack : error); if (error.name === 'HTTPError' && (error.code == 401 || error.code == 403)) { Whisper.Registration.remove(); diff --git a/js/libtextsecure.js b/js/libtextsecure.js index 14980063a1..3ceec56c0d 100644 --- a/js/libtextsecure.js +++ b/js/libtextsecure.js @@ -38049,7 +38049,7 @@ var TextSecureServer = (function() { console.log("Old signed prekey record count: " + oldRecords.length); oldRecords.forEach(function(oldRecord) { - if ( oldRecord.keyId > activeSignedPreKeyId - 3 ) { + if ( oldRecord.keyId >= activeSignedPreKeyId - 3 ) { // keep at least the last 3 signed keys return; } @@ -39151,6 +39151,7 @@ MessageReceiver.prototype.extend({ }.bind(this)); }, handleBlocked: function(envelope, blocked) { + console.log('Setting these numbers as blocked:', blocked.numbers); textsecure.storage.put('blocked', blocked.numbers); }, isBlocked: function(number) { diff --git a/js/logging.js b/js/logging.js index f7a687fe0a..19f26699b2 100644 --- a/js/logging.js +++ b/js/logging.js @@ -161,6 +161,10 @@ window.log = { }; window.onerror = function(message, script, line, col, error) { - window.log.error(error.stack); + const errorInfo = error && error.stack ? error.stack : JSON.stringify(error); + window.log.error('Top-level unhandled error: ' + errorInfo); }; +window.addEventListener('unhandledrejection', function(rejectionEvent) { + window.log.error('Top-level unhandled promise rejection: ' + rejectionEvent.reason); +}); diff --git a/js/models/messages.js b/js/models/messages.js index 76732958c4..7f8d5ddfe6 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -314,8 +314,11 @@ errors = [errors]; } errors.forEach(function(e) { - console.log(e); - console.log(e.reason, e.stack); + console.log( + 'Message.saveErrors:', + e && e.reason ? e.reason : null, + e && e.stack ? e.stack : e + ); }); errors = errors.map(function(e) { if (e.constructor === Error || diff --git a/js/signal_protocol_store.js b/js/signal_protocol_store.js index e3beac5b9e..a744a703f0 100644 --- a/js/signal_protocol_store.js +++ b/js/signal_protocol_store.js @@ -211,7 +211,16 @@ this.trigger('removePreKey'); return new Promise(function(resolve) { - prekey.destroy().then(function() { + var deferred = prekey.destroy(); + if (!deferred) { + return resolve(); + } + + return deferred.then(resolve, function(error) { + console.log( + 'removePreKey error:', + error && error.stack ? error.stack : error + ); resolve(); }); }); @@ -222,6 +231,7 @@ var prekey = new SignedPreKey({id: keyId}); return new Promise(function(resolve) { prekey.fetch().then(function() { + console.log('Successfully loaded prekey:', prekey.get('id')); resolve({ pubKey : prekey.get('publicKey'), privKey : prekey.get('privateKey'), @@ -229,14 +239,14 @@ keyId : prekey.get('id') }); }).fail(function() { - console.log("Failed to load signed prekey:", keyId); + console.log('Failed to load signed prekey:', keyId); resolve(); }); }); }, loadSignedPreKeys: function() { if (arguments.length > 0) { - return Promise.reject(new Error("loadSignedPreKeys takes no arguments")); + return Promise.reject(new Error('loadSignedPreKeys takes no arguments')); } var signedPreKeys = new SignedPreKeyCollection(); return new Promise(function(resolve) { diff --git a/libtextsecure/account_manager.js b/libtextsecure/account_manager.js index 45c904059f..3204809157 100644 --- a/libtextsecure/account_manager.js +++ b/libtextsecure/account_manager.js @@ -179,7 +179,7 @@ console.log("Old signed prekey record count: " + oldRecords.length); oldRecords.forEach(function(oldRecord) { - if ( oldRecord.keyId > activeSignedPreKeyId - 3 ) { + if ( oldRecord.keyId >= activeSignedPreKeyId - 3 ) { // keep at least the last 3 signed keys return; } diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index 3d60f11c89..62e8810871 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -730,6 +730,7 @@ MessageReceiver.prototype.extend({ }.bind(this)); }, handleBlocked: function(envelope, blocked) { + console.log('Setting these numbers as blocked:', blocked.numbers); textsecure.storage.put('blocked', blocked.numbers); }, isBlocked: function(number) { From d9a48478ecd6e65881812e46e7e6b1df103ede03 Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Thu, 30 Nov 2017 11:56:46 -0800 Subject: [PATCH 4/9] Logging for prekey fetches, load of log files (#1836) * Log the files discovered in logPath I've encountered some logs which include very old entries; and my suspicion is that we're not cleaning up old log files properly. * Log prekey fetches (success and failure), just like signed keys * Force log file information into the final web-ready log --- app/logging.js | 11 +++++++++++ js/signal_protocol_store.js | 10 +++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/app/logging.js b/app/logging.js index a7966d2579..1257c24d01 100644 --- a/app/logging.js +++ b/app/logging.js @@ -93,8 +93,19 @@ function fetch(logPath) { return path.join(logPath, file) }); + // creating a manual log entry for the final log result + var now = new Date(); + const fileListEntry = { + level: 30, // INFO + time: now.toJSON(), + msg: 'Loaded this list of log files from logPath: ' + files.join(', '), + }; + return Promise.all(paths.map(fetchLog)).then(function(results) { const data = _.flatten(results); + + data.push(fileListEntry); + return _.sortBy(data, 'time'); }); } diff --git a/js/signal_protocol_store.js b/js/signal_protocol_store.js index a744a703f0..3aacab0ea2 100644 --- a/js/signal_protocol_store.js +++ b/js/signal_protocol_store.js @@ -186,11 +186,15 @@ var prekey = new PreKey({id: keyId}); return new Promise(function(resolve) { prekey.fetch().then(function() { + console.log('Successfully fetched prekey:', keyId); resolve({ - pubKey: prekey.attributes.publicKey, - privKey: prekey.attributes.privateKey + pubKey: prekey.get('publicKey'), + privKey: prekey.get('privateKey'), }); - }).fail(resolve); + }, function() { + console.log('Failed to load prekey:', keyId); + resolve(); + }); }); }, storePreKey: function(keyId, keyPair) { From 6cb8b9963725320731a291c0f44b02bc8c30cff4 Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Thu, 30 Nov 2017 11:58:00 -0800 Subject: [PATCH 5/9] Harden our handling of config.json, verify that window is visible (#1830) * Validate config-provided locatio against available screens * Increase buffer around screen from 10px to 100px * Protect against null mainWindow, fix height/width typo * Properly handle missing x and y * Move to _.isNumber for checking x and y * Use greater than or less than to allow for y = 0, exactly 100px --- main.js | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 65 insertions(+), 7 deletions(-) diff --git a/main.js b/main.js index 7427a70b49..d8f1013e8e 100644 --- a/main.js +++ b/main.js @@ -90,19 +90,73 @@ function captureClicks(window) { window.webContents.on('new-window', handleUrl); } + +const DEFAULT_WIDTH = 800; +const DEFAULT_HEIGHT = 610; +const MIN_WIDTH = 700; +const MIN_HEIGHT = 360; +const BOUNDS_BUFFER = 100; + +function isVisible(window, bounds) { + const boundsX = _.get(bounds, 'x') || 0; + const boundsY = _.get(bounds, 'y') || 0; + const boundsWidth = _.get(bounds, 'width') || DEFAULT_WIDTH; + const boundsHeight = _.get(bounds, 'height') || DEFAULT_HEIGHT; + + // requiring BOUNDS_BUFFER pixels on the left or right side + const rightSideClearOfLeftBound = (window.x + window.width >= boundsX + BOUNDS_BUFFER); + const leftSideClearOfRightBound = (window.x <= boundsX + boundsWidth - BOUNDS_BUFFER); + + // top can't be offscreen, and must show at least BOUNDS_BUFFER pixels at bottom + const topClearOfUpperBound = window.y >= boundsY; + const topClearOfLowerBound = (window.y <= boundsY + boundsHeight - BOUNDS_BUFFER); + + return rightSideClearOfLeftBound + && leftSideClearOfRightBound + && topClearOfUpperBound + && topClearOfLowerBound; +} + function createWindow () { + const screen = electron.screen; const windowOptions = Object.assign({ - width: 800, - height: 610, - minWidth: 700, - minHeight: 360, + width: DEFAULT_WIDTH, + height: DEFAULT_HEIGHT, + minWidth: MIN_WIDTH, + minHeight: MIN_HEIGHT, autoHideMenuBar: false, webPreferences: { nodeIntegration: false, //sandbox: true, preload: path.join(__dirname, 'preload.js') } - }, windowConfig); + }, _.pick(windowConfig, ['maximized', 'autoHideMenuBar', 'width', 'height', 'x', 'y'])); + + if (!_.isNumber(windowOptions.width) || windowOptions.width < MIN_WIDTH) { + windowOptions.width = DEFAULT_WIDTH; + } + if (!_.isNumber(windowOptions.height) || windowOptions.height < MIN_HEIGHT) { + windowOptions.height = DEFAULT_HEIGHT; + } + if (!_.isBoolean(windowOptions.maximized)) { + delete windowOptions.maximized; + } + if (!_.isBoolean(windowOptions.autoHideMenuBar)) { + delete windowOptions.autoHideMenuBar; + } + + const visibleOnAnyScreen = _.some(screen.getAllDisplays(), function(display) { + if (!_.isNumber(windowOptions.x) || !_.isNumber(windowOptions.y)) { + return false; + } + + return isVisible(windowOptions, _.get(display, 'bounds')); + }); + if (!visibleOnAnyScreen) { + console.log('Location reset needed'); + delete windowOptions.x; + delete windowOptions.y; + } if (windowOptions.fullscreen === false) { delete windowOptions.fullscreen; @@ -341,11 +395,15 @@ ipc.on('restart', function(event) { }); ipc.on("set-auto-hide-menu-bar", function(event, autoHide) { - mainWindow.setAutoHideMenuBar(autoHide); + if (mainWindow) { + mainWindow.setAutoHideMenuBar(autoHide); + } }); ipc.on("set-menu-bar-visibility", function(event, visibility) { - mainWindow.setMenuBarVisibility(visibility); + if (mainWindow) { + mainWindow.setMenuBarVisibility(visibility); + } }); ipc.on("close-about", function() { From 21325bc922908ce3284c4392305e2abbfeadf934 Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Fri, 1 Dec 2017 13:31:48 -0800 Subject: [PATCH 6/9] Move sync messages to silent = true (#1843) * Set silent = true for all sync messages * sendmessage.js: Remove brace-less if --- js/libtextsecure.js | 36 ++++++++++++++++++++++++------------ libtextsecure/sendmessage.js | 36 ++++++++++++++++++++++++------------ 2 files changed, 48 insertions(+), 24 deletions(-) diff --git a/js/libtextsecure.js b/js/libtextsecure.js index 3ceec56c0d..220bfb8189 100644 --- a/js/libtextsecure.js +++ b/js/libtextsecure.js @@ -39894,10 +39894,11 @@ MessageSender.prototype = { var proto = textsecure.protobuf.DataMessage.decode(encodedMessage); return new Promise(function(resolve, reject) { this.sendMessageProto(timestamp, numbers, proto, function(res) { - if (res.errors.length > 0) + if (res.errors.length > 0) { reject(res); - else + } else { resolve(res); + } }); }.bind(this)); }, @@ -39949,7 +39950,9 @@ MessageSender.prototype = { syncMessage.sent = sentMessage; var contentMessage = new textsecure.protobuf.Content(); contentMessage.syncMessage = syncMessage; - return this.sendIndividualProto(myNumber, contentMessage, Date.now()); + + var silent = true; + return this.sendIndividualProto(myNumber, contentMessage, Date.now(), silent); }, getProfile: function(number) { @@ -39970,7 +39973,8 @@ MessageSender.prototype = { var contentMessage = new textsecure.protobuf.Content(); contentMessage.syncMessage = syncMessage; - return this.sendIndividualProto(myNumber, contentMessage, Date.now()); + var silent = true; + return this.sendIndividualProto(myNumber, contentMessage, Date.now(), silent); } return Promise.resolve(); @@ -39986,7 +39990,8 @@ MessageSender.prototype = { var contentMessage = new textsecure.protobuf.Content(); contentMessage.syncMessage = syncMessage; - return this.sendIndividualProto(myNumber, contentMessage, Date.now()); + var silent = true; + return this.sendIndividualProto(myNumber, contentMessage, Date.now(), silent); } return Promise.resolve(); @@ -40003,7 +40008,8 @@ MessageSender.prototype = { var contentMessage = new textsecure.protobuf.Content(); contentMessage.syncMessage = syncMessage; - return this.sendIndividualProto(myNumber, contentMessage, Date.now()); + var silent = true; + return this.sendIndividualProto(myNumber, contentMessage, Date.now(), silent); } return Promise.resolve(); @@ -40016,7 +40022,8 @@ MessageSender.prototype = { var contentMessage = new textsecure.protobuf.Content(); contentMessage.receiptMessage = receiptMessage; - return this.sendIndividualProto(sender, contentMessage, Date.now(), true /*silent*/); + var silent = true; + return this.sendIndividualProto(sender, contentMessage, Date.now(), silent); }, syncReadMessages: function(reads) { var myNumber = textsecure.storage.user.getNumber(); @@ -40033,7 +40040,8 @@ MessageSender.prototype = { var contentMessage = new textsecure.protobuf.Content(); contentMessage.syncMessage = syncMessage; - return this.sendIndividualProto(myNumber, contentMessage, Date.now()); + var silent = true; + return this.sendIndividualProto(myNumber, contentMessage, Date.now(), silent); } return Promise.resolve(); @@ -40055,7 +40063,8 @@ MessageSender.prototype = { var contentMessage = new textsecure.protobuf.Content(); contentMessage.nullMessage = nullMessage; - return this.sendIndividualProto(destination, contentMessage, Date.now()).then(function() { + var silent = true; + return this.sendIndividualProto(destination, contentMessage, Date.now(), silent).then(function() { var verified = new textsecure.protobuf.Verified(); verified.state = state; verified.destination = destination; @@ -40068,7 +40077,7 @@ MessageSender.prototype = { var contentMessage = new textsecure.protobuf.Content(); contentMessage.syncMessage = syncMessage; - return this.sendIndividualProto(myNumber, contentMessage, Date.now()); + return this.sendIndividualProto(myNumber, contentMessage, Date.now(), silent); }.bind(this)); } @@ -40084,14 +40093,17 @@ MessageSender.prototype = { } return new Promise(function(resolve, reject) { - this.sendMessageProto(timestamp, numbers, proto, function(res) { + var silent = true; + var callback = function(res) { res.dataMessage = proto.toArrayBuffer(); if (res.errors.length > 0) { reject(res); } else { resolve(res); } - }.bind(this)); + }.bind(this); + + this.sendMessageProto(timestamp, numbers, proto, callback, silent); }.bind(this)); }, diff --git a/libtextsecure/sendmessage.js b/libtextsecure/sendmessage.js index ba55f97bb9..7cc8d92684 100644 --- a/libtextsecure/sendmessage.js +++ b/libtextsecure/sendmessage.js @@ -265,10 +265,11 @@ MessageSender.prototype = { var proto = textsecure.protobuf.DataMessage.decode(encodedMessage); return new Promise(function(resolve, reject) { this.sendMessageProto(timestamp, numbers, proto, function(res) { - if (res.errors.length > 0) + if (res.errors.length > 0) { reject(res); - else + } else { resolve(res); + } }); }.bind(this)); }, @@ -320,7 +321,9 @@ MessageSender.prototype = { syncMessage.sent = sentMessage; var contentMessage = new textsecure.protobuf.Content(); contentMessage.syncMessage = syncMessage; - return this.sendIndividualProto(myNumber, contentMessage, Date.now()); + + var silent = true; + return this.sendIndividualProto(myNumber, contentMessage, Date.now(), silent); }, getProfile: function(number) { @@ -341,7 +344,8 @@ MessageSender.prototype = { var contentMessage = new textsecure.protobuf.Content(); contentMessage.syncMessage = syncMessage; - return this.sendIndividualProto(myNumber, contentMessage, Date.now()); + var silent = true; + return this.sendIndividualProto(myNumber, contentMessage, Date.now(), silent); } return Promise.resolve(); @@ -357,7 +361,8 @@ MessageSender.prototype = { var contentMessage = new textsecure.protobuf.Content(); contentMessage.syncMessage = syncMessage; - return this.sendIndividualProto(myNumber, contentMessage, Date.now()); + var silent = true; + return this.sendIndividualProto(myNumber, contentMessage, Date.now(), silent); } return Promise.resolve(); @@ -374,7 +379,8 @@ MessageSender.prototype = { var contentMessage = new textsecure.protobuf.Content(); contentMessage.syncMessage = syncMessage; - return this.sendIndividualProto(myNumber, contentMessage, Date.now()); + var silent = true; + return this.sendIndividualProto(myNumber, contentMessage, Date.now(), silent); } return Promise.resolve(); @@ -387,7 +393,8 @@ MessageSender.prototype = { var contentMessage = new textsecure.protobuf.Content(); contentMessage.receiptMessage = receiptMessage; - return this.sendIndividualProto(sender, contentMessage, Date.now(), true /*silent*/); + var silent = true; + return this.sendIndividualProto(sender, contentMessage, Date.now(), silent); }, syncReadMessages: function(reads) { var myNumber = textsecure.storage.user.getNumber(); @@ -404,7 +411,8 @@ MessageSender.prototype = { var contentMessage = new textsecure.protobuf.Content(); contentMessage.syncMessage = syncMessage; - return this.sendIndividualProto(myNumber, contentMessage, Date.now()); + var silent = true; + return this.sendIndividualProto(myNumber, contentMessage, Date.now(), silent); } return Promise.resolve(); @@ -426,7 +434,8 @@ MessageSender.prototype = { var contentMessage = new textsecure.protobuf.Content(); contentMessage.nullMessage = nullMessage; - return this.sendIndividualProto(destination, contentMessage, Date.now()).then(function() { + var silent = true; + return this.sendIndividualProto(destination, contentMessage, Date.now(), silent).then(function() { var verified = new textsecure.protobuf.Verified(); verified.state = state; verified.destination = destination; @@ -439,7 +448,7 @@ MessageSender.prototype = { var contentMessage = new textsecure.protobuf.Content(); contentMessage.syncMessage = syncMessage; - return this.sendIndividualProto(myNumber, contentMessage, Date.now()); + return this.sendIndividualProto(myNumber, contentMessage, Date.now(), silent); }.bind(this)); } @@ -455,14 +464,17 @@ MessageSender.prototype = { } return new Promise(function(resolve, reject) { - this.sendMessageProto(timestamp, numbers, proto, function(res) { + var silent = true; + var callback = function(res) { res.dataMessage = proto.toArrayBuffer(); if (res.errors.length > 0) { reject(res); } else { resolve(res); } - }.bind(this)); + }.bind(this); + + this.sendMessageProto(timestamp, numbers, proto, callback, silent); }.bind(this)); }, From c195ba2630a424791a2e0877907daba5a86077ad Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Fri, 1 Dec 2017 13:35:39 -0800 Subject: [PATCH 7/9] Save prekeys optimistically, track confirms, new clean behavior (#1846) * Re-enable libtextsecure unit tests, get passing, run in CI * Save prekeys optimistically, track confirmed, new clean behavior * Eliminate potential conflicts when rotating on startup * Remove last symlink: get libtextsecure tests running on windows --- Gruntfile.js | 135 ++++++++------- config/test-lib.json | 5 + js/background.js | 8 +- js/libtextsecure.js | 110 ++++++++---- js/rotate_signed_prekey_listener.js | 11 +- js/signal_protocol_store.js | 11 +- libtextsecure/account_manager.js | 110 ++++++++---- libtextsecure/test/_test.js | 3 + libtextsecure/test/account_manager_test.js | 156 ++++++++++++++++++ libtextsecure/test/index.html | 13 +- libtextsecure/test/protos | 1 - libtextsecure/test/test.js | 3 + .../test/websocket-resources_test.js | 6 +- main.js | 8 +- 14 files changed, 433 insertions(+), 147 deletions(-) create mode 100644 config/test-lib.json create mode 100644 libtextsecure/test/account_manager_test.js delete mode 120000 libtextsecure/test/protos diff --git a/Gruntfile.js b/Gruntfile.js index 1458fbd6a9..524b2b7068 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -335,71 +335,82 @@ module.exports = function(grunt) { }); }); - grunt.registerTask('unit-tests', 'Run unit tests inside Electron', function() { - var environment = grunt.option('env') || 'test'; - var done = this.async(); - var failure; - - var Application = require('spectron').Application; - var electronBinary = process.platform === 'win32' ? 'electron.cmd' : 'electron'; - var app = new Application({ - path: path.join(__dirname, 'node_modules', '.bin', electronBinary), - args: [path.join(__dirname, 'main.js')], - env: { - NODE_ENV: environment - } - }); - - function getMochaResults() { - return window.mochaResults; + function runTests(environment, cb) { + var failure; + var Application = require('spectron').Application; + var electronBinary = process.platform === 'win32' ? 'electron.cmd' : 'electron'; + var app = new Application({ + path: path.join(__dirname, 'node_modules', '.bin', electronBinary), + args: [path.join(__dirname, 'main.js')], + env: { + NODE_ENV: environment } + }); - app.start().then(function() { - return app.client.waitUntil(function() { - return app.client.execute(getMochaResults).then(function(data) { - return Boolean(data.value); - }); - }, 10000, 'Expected to find window.mochaResults set!'); - }).then(function() { - return app.client.execute(getMochaResults); - }).then(function(data) { - var results = data.value; - if (results.failures > 0) { - console.error(results.reports); - failure = function() { - grunt.fail.fatal('Found ' + results.failures + ' failing unit tests.'); - }; - return app.client.log('browser'); - } else { - grunt.log.ok(results.passes + ' tests passed.'); - } - }).then(function(logs) { - if (logs) { - console.error(); - console.error('Because tests failed, printing browser logs:'); - console.error(logs); - } - }).catch(function (error) { + function getMochaResults() { + return window.mochaResults; + } + + app.start().then(function() { + return app.client.waitUntil(function() { + return app.client.execute(getMochaResults).then(function(data) { + return Boolean(data.value); + }); + }, 10000, 'Expected to find window.mochaResults set!'); + }).then(function() { + return app.client.execute(getMochaResults); + }).then(function(data) { + var results = data.value; + if (results.failures > 0) { + console.error(results.reports); failure = function() { - grunt.fail.fatal('Something went wrong: ' + error.message + ' ' + error.stack); + grunt.fail.fatal('Found ' + results.failures + ' failing unit tests.'); }; - }).then(function () { - // We need to use the failure variable and this early stop to clean up before - // shutting down. Grunt's fail methods are the only way to set the return value, - // but they shut the process down immediately! - return app.stop(); - }).then(function() { - if (failure) { - failure(); - } - done(); - }).catch(function (error) { - console.error('Second-level error:', error.message, error.stack); - if (failure) { - failure(); - } - done(); - }); + return app.client.log('browser'); + } else { + grunt.log.ok(results.passes + ' tests passed.'); + } + }).then(function(logs) { + if (logs) { + console.error(); + console.error('Because tests failed, printing browser logs:'); + console.error(logs); + } + }).catch(function (error) { + failure = function() { + grunt.fail.fatal('Something went wrong: ' + error.message + ' ' + error.stack); + }; + }).then(function () { + // We need to use the failure variable and this early stop to clean up before + // shutting down. Grunt's fail methods are the only way to set the return value, + // but they shut the process down immediately! + return app.stop(); + }).then(function() { + if (failure) { + failure(); + } + cb(); + }).catch(function (error) { + console.error('Second-level error:', error.message, error.stack); + if (failure) { + failure(); + } + cb(); + }); + } + + grunt.registerTask('unit-tests', 'Run unit tests w/Electron', function() { + var environment = grunt.option('env') || 'test'; + var done = this.async(); + + runTests(environment, done); + }); + + grunt.registerTask('lib-unit-tests', 'Run libtextsecure unit tests w/Electron', function() { + var environment = grunt.option('env') || 'test-lib'; + var done = this.async(); + + runTests(environment, done); }); grunt.registerMultiTask('test-release', 'Test packaged releases', function() { @@ -473,7 +484,7 @@ module.exports = function(grunt) { grunt.registerTask('tx', ['exec:tx-pull', 'locale-patch']); grunt.registerTask('dev', ['default', 'watch']); - grunt.registerTask('test', ['jshint', 'jscs', 'unit-tests']); + grunt.registerTask('test', ['jshint', 'jscs', 'unit-tests', 'lib-unit-tests']); grunt.registerTask('copy_dist', ['gitinfo', 'copy:res', 'copy:src']); grunt.registerTask('date', ['gitinfo', 'getExpireTime']); grunt.registerTask('prep-release', ['gitinfo', 'clean-release', 'fetch-release']); diff --git a/config/test-lib.json b/config/test-lib.json new file mode 100644 index 0000000000..d0e5c25d8c --- /dev/null +++ b/config/test-lib.json @@ -0,0 +1,5 @@ +{ + "storageProfile": "test", + "disableAutoUpdate": true, + "openDevTools": false +} diff --git a/js/background.js b/js/background.js index b0aa1268c2..a7c8527922 100644 --- a/js/background.js +++ b/js/background.js @@ -79,11 +79,11 @@ function start() { var currentVersion = window.config.version; var lastVersion = storage.get('version'); + var newVersion = !lastVersion || currentVersion !== lastVersion; storage.put('version', currentVersion); - if (!lastVersion || currentVersion !== lastVersion) { + if (newVersion) { console.log('New version detected:', currentVersion); - getAccountManager().rotateSignedPreKey(); } window.dispatchEvent(new Event('storage_ready')); @@ -91,7 +91,7 @@ console.log('listening for registration events'); Whisper.events.on('registration_done', function() { console.log('handling registration event'); - Whisper.RotateSignedPreKeyListener.init(Whisper.events); + Whisper.RotateSignedPreKeyListener.init(Whisper.events, newVersion); connect(true); }); @@ -104,7 +104,7 @@ console.log('Import was interrupted, showing import error screen'); appView.openImporter(); } else if (Whisper.Registration.everDone()) { - Whisper.RotateSignedPreKeyListener.init(Whisper.events); + Whisper.RotateSignedPreKeyListener.init(Whisper.events, newVersion); connect(); appView.openInbox({ initialLoadComplete: initialLoadComplete diff --git a/js/libtextsecure.js b/js/libtextsecure.js index 220bfb8189..930589e48d 100644 --- a/js/libtextsecure.js +++ b/js/libtextsecure.js @@ -37988,26 +37988,35 @@ var TextSecureServer = (function() { rotateSignedPreKey: function() { return this.queueTask(function() { var signedKeyId = textsecure.storage.get('signedKeyId', 1); - if (typeof signedKeyId != 'number') { throw new Error('Invalid signedKeyId'); } + var store = textsecure.storage.protocol; var server = this.server; var cleanSignedPreKeys = this.cleanSignedPreKeys; + return store.getIdentityKeyPair().then(function(identityKey) { return libsignal.KeyHelper.generateSignedPreKey(identityKey, signedKeyId); }).then(function(res) { - return server.setSignedPreKey({ - keyId : res.keyId, - publicKey : res.keyPair.pubKey, - signature : res.signature + console.log('Saving new signed prekey', res.keyId); + return Promise.all([ + textsecure.storage.put('signedKeyId', signedKeyId + 1), + store.storeSignedPreKey(res.keyId, res.keyPair), + server.setSignedPreKey({ + keyId : res.keyId, + publicKey : res.keyPair.pubKey, + signature : res.signature + }), + ]).then(function() { + var confirmed = true; + console.log('Confirming new signed prekey', res.keyId); + return Promise.all([ + textsecure.storage.remove('signedKeyRotationRejected'), + store.storeSignedPreKey(res.keyId, res.keyPair, confirmed), + ]); }).then(function() { - textsecure.storage.put('signedKeyId', signedKeyId + 1); - textsecure.storage.remove('signedKeyRotationRejected'); - return store.storeSignedPreKey(res.keyId, res.keyPair).then(function() { - return cleanSignedPreKeys(); - }); + return cleanSignedPreKeys(); }); }).catch(function(e) { console.log( @@ -38030,35 +38039,72 @@ var TextSecureServer = (function() { return this.pending = this.pending.then(taskWithTimeout, taskWithTimeout); }, cleanSignedPreKeys: function() { - var nextSignedKeyId = textsecure.storage.get('signedKeyId'); - if (typeof nextSignedKeyId != 'number') { - return Promise.resolve(); - } - var activeSignedPreKeyId = nextSignedKeyId - 1; - + var MINIMUM_KEYS = 3; var store = textsecure.storage.protocol; - return store.loadSignedPreKeys().then(function(allRecords) { - var oldRecords = allRecords.filter(function(record) { - return record.keyId !== activeSignedPreKeyId; - }); - oldRecords.sort(function(a, b) { + return store.loadSignedPreKeys().then(function(allKeys) { + allKeys.sort(function(a, b) { return (a.created_at || 0) - (b.created_at || 0); }); + allKeys.reverse(); // we want the most recent first + var confirmed = allKeys.filter(function(key) { + return key.confirmed; + }); + var unconfirmed = allKeys.filter(function(key) { + return !key.confirmed; + }); - console.log("Active signed prekey: " + activeSignedPreKeyId); - console.log("Old signed prekey record count: " + oldRecords.length); + var recent = allKeys[0] ? allKeys[0].keyId : 'none'; + var recentConfirmed = confirmed[0] ? confirmed[0].keyId : 'none'; + console.log('Most recent signed key: ' + recent); + console.log('Most recent confirmed signed key: ' + recentConfirmed); + console.log( + 'Total signed key count:', + allKeys.length, + '-', + confirmed.length, + 'confirmed' + ); - oldRecords.forEach(function(oldRecord) { - if ( oldRecord.keyId >= activeSignedPreKeyId - 3 ) { - // keep at least the last 3 signed keys + var confirmedCount = confirmed.length; + + // Keep MINIMUM_KEYS confirmed keys, then drop if older than a week + confirmed = confirmed.forEach(function(key, index) { + if (index < MINIMUM_KEYS) { return; } - var created_at = oldRecord.created_at || 0; - var archiveDuration = Date.now() - created_at; - if (archiveDuration > ARCHIVE_AGE) { - console.log("Removing signed prekey record:", - oldRecord.keyId, "with timestamp:", created_at); - store.removeSignedPreKey(oldRecord.keyId); + var created_at = key.created_at || 0; + var age = Date.now() - created_at; + if (age > ARCHIVE_AGE) { + console.log( + 'Removing confirmed signed prekey:', + key.keyId, + 'with timestamp:', + created_at + ); + store.removeSignedPreKey(key.keyId); + confirmedCount--; + } + }); + + var stillNeeded = MINIMUM_KEYS - confirmedCount; + + // If we still don't have enough total keys, we keep as many unconfirmed + // keys as necessary. If not necessary, and over a week old, we drop. + unconfirmed.forEach(function(key, index) { + if (index < stillNeeded) { + return; + } + + var created_at = key.created_at || 0; + var age = Date.now() - created_at; + if (age > ARCHIVE_AGE) { + console.log( + 'Removing unconfirmed signed prekey:', + key.keyId, + 'with timestamp:', + created_at + ); + store.removeSignedPreKey(key.keyId); } }); }); diff --git a/js/rotate_signed_prekey_listener.js b/js/rotate_signed_prekey_listener.js index a52ece8e39..e353e71a79 100644 --- a/js/rotate_signed_prekey_listener.js +++ b/js/rotate_signed_prekey_listener.js @@ -58,20 +58,19 @@ var initComplete; Whisper.RotateSignedPreKeyListener = { - init: function(events) { + init: function(events, newVersion) { if (initComplete) { console.log('Rotate signed prekey listener: Already initialized'); return; } initComplete = true; - if (Whisper.Registration.isDone()) { + if (newVersion) { + runWhenOnline(); + } else { setTimeoutForNextRun(); } - events.on('registration_done', function() { - scheduleNextRotation(); - setTimeoutForNextRun(); - }); + events.on('timetravel', function() { if (Whisper.Registration.isDone()) { setTimeoutForNextRun(); diff --git a/js/signal_protocol_store.js b/js/signal_protocol_store.js index 3aacab0ea2..f9a5403dff 100644 --- a/js/signal_protocol_store.js +++ b/js/signal_protocol_store.js @@ -240,7 +240,8 @@ pubKey : prekey.get('publicKey'), privKey : prekey.get('privateKey'), created_at : prekey.get('created_at'), - keyId : prekey.get('id') + keyId : prekey.get('id'), + confirmed : prekey.get('confirmed'), }); }).fail(function() { console.log('Failed to load signed prekey:', keyId); @@ -260,18 +261,20 @@ pubKey : prekey.get('publicKey'), privKey : prekey.get('privateKey'), created_at : prekey.get('created_at'), - keyId : prekey.get('id') + keyId : prekey.get('id'), + confirmed : prekey.get('confirmed'), }; })); }); }); }, - storeSignedPreKey: function(keyId, keyPair) { + storeSignedPreKey: function(keyId, keyPair, confirmed) { var prekey = new SignedPreKey({ id : keyId, publicKey : keyPair.pubKey, privateKey : keyPair.privKey, - created_at : Date.now() + created_at : Date.now(), + confirmed : Boolean(confirmed), }); return new Promise(function(resolve) { prekey.save().always(function() { diff --git a/libtextsecure/account_manager.js b/libtextsecure/account_manager.js index 3204809157..69f7c2c8a5 100644 --- a/libtextsecure/account_manager.js +++ b/libtextsecure/account_manager.js @@ -118,26 +118,35 @@ rotateSignedPreKey: function() { return this.queueTask(function() { var signedKeyId = textsecure.storage.get('signedKeyId', 1); - if (typeof signedKeyId != 'number') { throw new Error('Invalid signedKeyId'); } + var store = textsecure.storage.protocol; var server = this.server; var cleanSignedPreKeys = this.cleanSignedPreKeys; + return store.getIdentityKeyPair().then(function(identityKey) { return libsignal.KeyHelper.generateSignedPreKey(identityKey, signedKeyId); }).then(function(res) { - return server.setSignedPreKey({ - keyId : res.keyId, - publicKey : res.keyPair.pubKey, - signature : res.signature + console.log('Saving new signed prekey', res.keyId); + return Promise.all([ + textsecure.storage.put('signedKeyId', signedKeyId + 1), + store.storeSignedPreKey(res.keyId, res.keyPair), + server.setSignedPreKey({ + keyId : res.keyId, + publicKey : res.keyPair.pubKey, + signature : res.signature + }), + ]).then(function() { + var confirmed = true; + console.log('Confirming new signed prekey', res.keyId); + return Promise.all([ + textsecure.storage.remove('signedKeyRotationRejected'), + store.storeSignedPreKey(res.keyId, res.keyPair, confirmed), + ]); }).then(function() { - textsecure.storage.put('signedKeyId', signedKeyId + 1); - textsecure.storage.remove('signedKeyRotationRejected'); - return store.storeSignedPreKey(res.keyId, res.keyPair).then(function() { - return cleanSignedPreKeys(); - }); + return cleanSignedPreKeys(); }); }).catch(function(e) { console.log( @@ -160,35 +169,72 @@ return this.pending = this.pending.then(taskWithTimeout, taskWithTimeout); }, cleanSignedPreKeys: function() { - var nextSignedKeyId = textsecure.storage.get('signedKeyId'); - if (typeof nextSignedKeyId != 'number') { - return Promise.resolve(); - } - var activeSignedPreKeyId = nextSignedKeyId - 1; - + var MINIMUM_KEYS = 3; var store = textsecure.storage.protocol; - return store.loadSignedPreKeys().then(function(allRecords) { - var oldRecords = allRecords.filter(function(record) { - return record.keyId !== activeSignedPreKeyId; - }); - oldRecords.sort(function(a, b) { + return store.loadSignedPreKeys().then(function(allKeys) { + allKeys.sort(function(a, b) { return (a.created_at || 0) - (b.created_at || 0); }); + allKeys.reverse(); // we want the most recent first + var confirmed = allKeys.filter(function(key) { + return key.confirmed; + }); + var unconfirmed = allKeys.filter(function(key) { + return !key.confirmed; + }); - console.log("Active signed prekey: " + activeSignedPreKeyId); - console.log("Old signed prekey record count: " + oldRecords.length); + var recent = allKeys[0] ? allKeys[0].keyId : 'none'; + var recentConfirmed = confirmed[0] ? confirmed[0].keyId : 'none'; + console.log('Most recent signed key: ' + recent); + console.log('Most recent confirmed signed key: ' + recentConfirmed); + console.log( + 'Total signed key count:', + allKeys.length, + '-', + confirmed.length, + 'confirmed' + ); - oldRecords.forEach(function(oldRecord) { - if ( oldRecord.keyId >= activeSignedPreKeyId - 3 ) { - // keep at least the last 3 signed keys + var confirmedCount = confirmed.length; + + // Keep MINIMUM_KEYS confirmed keys, then drop if older than a week + confirmed = confirmed.forEach(function(key, index) { + if (index < MINIMUM_KEYS) { return; } - var created_at = oldRecord.created_at || 0; - var archiveDuration = Date.now() - created_at; - if (archiveDuration > ARCHIVE_AGE) { - console.log("Removing signed prekey record:", - oldRecord.keyId, "with timestamp:", created_at); - store.removeSignedPreKey(oldRecord.keyId); + var created_at = key.created_at || 0; + var age = Date.now() - created_at; + if (age > ARCHIVE_AGE) { + console.log( + 'Removing confirmed signed prekey:', + key.keyId, + 'with timestamp:', + created_at + ); + store.removeSignedPreKey(key.keyId); + confirmedCount--; + } + }); + + var stillNeeded = MINIMUM_KEYS - confirmedCount; + + // If we still don't have enough total keys, we keep as many unconfirmed + // keys as necessary. If not necessary, and over a week old, we drop. + unconfirmed.forEach(function(key, index) { + if (index < stillNeeded) { + return; + } + + var created_at = key.created_at || 0; + var age = Date.now() - created_at; + if (age > ARCHIVE_AGE) { + console.log( + 'Removing unconfirmed signed prekey:', + key.keyId, + 'with timestamp:', + created_at + ); + store.removeSignedPreKey(key.keyId); } }); }); diff --git a/libtextsecure/test/_test.js b/libtextsecure/test/_test.js index d4d557b80e..dfbc95ee2a 100644 --- a/libtextsecure/test/_test.js +++ b/libtextsecure/test/_test.js @@ -1,5 +1,6 @@ mocha.setup("bdd"); window.assert = chai.assert; +window.PROTO_ROOT = '../../protos'; (function() { var OriginalReporter = mocha._reporter; @@ -52,3 +53,5 @@ function hexToArrayBuffer(str) { array[i] = parseInt(str.substr(i*2, 2), 16); return ret; }; + +window.MockSocket.prototype.addEventListener = function() {}; diff --git a/libtextsecure/test/account_manager_test.js b/libtextsecure/test/account_manager_test.js new file mode 100644 index 0000000000..309ed52c93 --- /dev/null +++ b/libtextsecure/test/account_manager_test.js @@ -0,0 +1,156 @@ +'use strict'; + +describe("AccountManager", function() { + let accountManager; + let originalServer; + + before(function() { + originalServer = window.TextSecureServer; + window.TextSecureServer = function() {}; + }); + after(function() { + window.TextSecureServer = originalServer; + }); + + beforeEach(function() { + accountManager = new window.textsecure.AccountManager(); + }); + + describe('#cleanSignedPreKeys', function() { + let originalProtocolStorage; + let signedPreKeys; + const DAY = 1000 * 60 * 60 * 24; + + beforeEach(function() { + originalProtocolStorage = window.textsecure.storage.protocol; + window.textsecure.storage.protocol = { + loadSignedPreKeys: function() { + return Promise.resolve(signedPreKeys); + }, + }; + }); + afterEach(function() { + window.textsecure.storage.protocol = originalProtocolStorage; + }); + + it('keeps three confirmed keys even if over a week old', function() { + const now = Date.now(); + signedPreKeys = [{ + keyId: 1, + created_at: now - DAY * 21, + confirmed: true, + }, { + keyId: 2, + created_at: now - DAY * 14, + confirmed: true, + }, { + keyId: 3, + created_at: now - DAY * 18, + confirmed: true, + }]; + + // should be no calls to store.removeSignedPreKey, would cause crash + return accountManager.cleanSignedPreKeys(); + }); + + it('eliminates confirmed keys over a week old, if more than three', function() { + const now = Date.now(); + signedPreKeys = [{ + keyId: 1, + created_at: now - DAY * 21, + confirmed: true, + }, { + keyId: 2, + created_at: now - DAY * 14, + confirmed: true, + }, { + keyId: 3, + created_at: now - DAY * 4, + confirmed: true, + }, { + keyId: 4, + created_at: now - DAY * 18, + confirmed: true, + }, { + keyId: 5, + created_at: now - DAY, + confirmed: true, + }]; + + let count = 0; + window.textsecure.storage.protocol.removeSignedPreKey = function(keyId) { + if (keyId !== 1 && keyId !== 4) { + throw new Error('Wrong keys were eliminated! ' + keyId); + } + + count++; + }; + + return accountManager.cleanSignedPreKeys().then(function() { + assert.strictEqual(count, 2); + }); + }); + + it('keeps at least three unconfirmed keys if no confirmed', function() { + const now = Date.now(); + signedPreKeys = [{ + keyId: 1, + created_at: now - DAY * 14, + }, { + keyId: 2, + created_at: now - DAY * 21, + }, { + keyId: 3, + created_at: now - DAY * 18, + }, { + keyId: 4, + created_at: now - DAY + }]; + + let count = 0; + window.textsecure.storage.protocol.removeSignedPreKey = function(keyId) { + if (keyId !== 2) { + throw new Error('Wrong keys were eliminated! ' + keyId); + } + + count++; + }; + + return accountManager.cleanSignedPreKeys().then(function() { + assert.strictEqual(count, 1); + }); + }); + + it('if some confirmed keys, keeps unconfirmed to addd up to three total', function() { + const now = Date.now(); + signedPreKeys = [{ + keyId: 1, + created_at: now - DAY * 21, + confirmed: true, + }, { + keyId: 2, + created_at: now - DAY * 14, + confirmed: true, + }, { + keyId: 3, + created_at: now - DAY * 12, + }, { + keyId: 4, + created_at: now - DAY * 8, + }]; + + let count = 0; + window.textsecure.storage.protocol.removeSignedPreKey = function(keyId) { + if (keyId !== 3) { + throw new Error('Wrong keys were eliminated! ' + keyId); + } + + count++; + }; + + return accountManager.cleanSignedPreKeys().then(function() { + assert.strictEqual(count, 1); + }); + }); + }); +}); diff --git a/libtextsecure/test/index.html b/libtextsecure/test/index.html index 5c8cd7b880..4064ed9e09 100644 --- a/libtextsecure/test/index.html +++ b/libtextsecure/test/index.html @@ -1,6 +1,7 @@ + libTextSecure test runner @@ -12,7 +13,6 @@ - @@ -23,12 +23,12 @@ + - @@ -42,5 +42,14 @@ + + + + + + + diff --git a/libtextsecure/test/protos b/libtextsecure/test/protos deleted file mode 120000 index 3d021e5976..0000000000 --- a/libtextsecure/test/protos +++ /dev/null @@ -1 +0,0 @@ -../../protos/ \ No newline at end of file diff --git a/libtextsecure/test/test.js b/libtextsecure/test/test.js index bb1cc7a0d2..660b25b2bd 100644 --- a/libtextsecure/test/test.js +++ b/libtextsecure/test/test.js @@ -22054,6 +22054,7 @@ Library.prototype.test = function(obj, type) { }); mocha.setup("bdd"); window.assert = chai.assert; +window.PROTO_ROOT = '../../protos'; (function() { var OriginalReporter = mocha._reporter; @@ -22106,3 +22107,5 @@ function hexToArrayBuffer(str) { array[i] = parseInt(str.substr(i*2, 2), 16); return ret; }; + +window.MockSocket.prototype.addEventListener = function() {}; diff --git a/libtextsecure/test/websocket-resources_test.js b/libtextsecure/test/websocket-resources_test.js index 92755cfbfa..c110cfca21 100644 --- a/libtextsecure/test/websocket-resources_test.js +++ b/libtextsecure/test/websocket-resources_test.js @@ -18,7 +18,8 @@ assert.strictEqual(message.response.status, 200); assert.strictEqual(message.response.id.toString(), request_id); done(); - } + }, + addEventListener: function() {}, }; // actual test @@ -58,7 +59,8 @@ assert.strictEqual(message.request.path, '/some/path'); assertEqualArrayBuffers(message.request.body.toArrayBuffer(), new Uint8Array([1,2,3]).buffer); request_id = message.request.id; - } + }, + addEventListener: function() {}, }; // actual test diff --git a/main.js b/main.js index d8f1013e8e..c087ccd6a6 100644 --- a/main.js +++ b/main.js @@ -207,6 +207,8 @@ function createWindow () { if (config.environment === 'test') { mainWindow.loadURL(prepareURL([__dirname, 'test', 'index.html'])); + } else if (config.environment === 'test-lib') { + mainWindow.loadURL(prepareURL([__dirname, 'libtextsecure', 'test', 'index.html'])); } else { mainWindow.loadURL(prepareURL([__dirname, 'background.html'])); } @@ -225,7 +227,9 @@ function createWindow () { // Emitted when the window is about to be closed. mainWindow.on('close', function (e) { - if (process.platform === 'darwin' && !windowState.shouldQuit() && config.environment !== 'test') { + if (process.platform === 'darwin' && !windowState.shouldQuit() + && config.environment !== 'test' && config.environment !== 'test-lib') { + e.preventDefault(); mainWindow.hide(); } @@ -356,7 +360,7 @@ app.on('before-quit', function() { app.on('window-all-closed', function () { // On OS X it is common for applications and their menu bar // to stay active until the user quits explicitly with Cmd + Q - if (process.platform !== 'darwin' || config.environment === 'test') { + if (process.platform !== 'darwin' || config.environment === 'test' || config.environment === 'test-lib') { app.quit() } }) From 2fdb04872125733b775ac48f9f93e95054a481be Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Mon, 4 Dec 2017 15:28:38 -0800 Subject: [PATCH 8/9] NullMessage sent before verification sync should not be silent (#1857) --- js/libtextsecure.js | 68 ++++++++++++++++--------------- libtextsecure/message_receiver.js | 3 +- libtextsecure/sendmessage.js | 65 +++++++++++++++-------------- 3 files changed, 72 insertions(+), 64 deletions(-) diff --git a/js/libtextsecure.js b/js/libtextsecure.js index 930589e48d..9879d5ec58 100644 --- a/js/libtextsecure.js +++ b/js/libtextsecure.js @@ -39085,8 +39085,7 @@ MessageReceiver.prototype.extend({ console.log('Got SyncMessage Request'); return this.removeFromCache(envelope); } else if (syncMessage.read && syncMessage.read.length) { - console.log('read messages', - 'from', envelope.source + '.' + envelope.sourceDevice); + console.log('read messages from', this.getEnvelopeId(envelope)); return this.handleRead(envelope, syncMessage.read); } else if (syncMessage.verified) { return this.handleVerified(envelope, syncMessage.verified); @@ -40095,39 +40094,44 @@ MessageSender.prototype = { syncVerification: function(destination, state, identityKey) { var myNumber = textsecure.storage.user.getNumber(); var myDevice = textsecure.storage.user.getDeviceId(); - if (myDevice != 1) { - // First send a null message to mask the sync message. - var nullMessage = new textsecure.protobuf.NullMessage(); + var now = Date.now(); - // Generate a random int from 1 and 512 - var buffer = libsignal.crypto.getRandomBytes(1); - var paddingLength = (new Uint8Array(buffer)[0] & 0x1ff) + 1; - - // Generate a random padding buffer of the chosen size - nullMessage.padding = libsignal.crypto.getRandomBytes(paddingLength); - - var contentMessage = new textsecure.protobuf.Content(); - contentMessage.nullMessage = nullMessage; - - var silent = true; - return this.sendIndividualProto(destination, contentMessage, Date.now(), silent).then(function() { - var verified = new textsecure.protobuf.Verified(); - verified.state = state; - verified.destination = destination; - verified.identityKey = identityKey; - verified.nullMessage = nullMessage.padding; - - var syncMessage = this.createSyncMessage(); - syncMessage.verified = verified; - - var contentMessage = new textsecure.protobuf.Content(); - contentMessage.syncMessage = syncMessage; - - return this.sendIndividualProto(myNumber, contentMessage, Date.now(), silent); - }.bind(this)); + if (myDevice === 1) { + return Promise.resolve(); } - return Promise.resolve(); + // First send a null message to mask the sync message. + var nullMessage = new textsecure.protobuf.NullMessage(); + + // Generate a random int from 1 and 512 + var buffer = libsignal.crypto.getRandomBytes(1); + var paddingLength = (new Uint8Array(buffer)[0] & 0x1ff) + 1; + + // Generate a random padding buffer of the chosen size + nullMessage.padding = libsignal.crypto.getRandomBytes(paddingLength); + + var contentMessage = new textsecure.protobuf.Content(); + contentMessage.nullMessage = nullMessage; + + // We want the NullMessage to look like a normal outgoing message; not silent + const promise = this.sendIndividualProto(destination, contentMessage, now); + + return promise.then(function() { + var verified = new textsecure.protobuf.Verified(); + verified.state = state; + verified.destination = destination; + verified.identityKey = identityKey; + verified.nullMessage = nullMessage.padding; + + var syncMessage = this.createSyncMessage(); + syncMessage.verified = verified; + + var contentMessage = new textsecure.protobuf.Content(); + contentMessage.syncMessage = syncMessage; + + var silent = true; + return this.sendIndividualProto(myNumber, contentMessage, now, silent); + }.bind(this)); }, sendGroupProto: function(numbers, proto, timestamp) { diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index 62e8810871..11434168a4 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -618,8 +618,7 @@ MessageReceiver.prototype.extend({ console.log('Got SyncMessage Request'); return this.removeFromCache(envelope); } else if (syncMessage.read && syncMessage.read.length) { - console.log('read messages', - 'from', envelope.source + '.' + envelope.sourceDevice); + console.log('read messages from', this.getEnvelopeId(envelope)); return this.handleRead(envelope, syncMessage.read); } else if (syncMessage.verified) { return this.handleVerified(envelope, syncMessage.verified); diff --git a/libtextsecure/sendmessage.js b/libtextsecure/sendmessage.js index 7cc8d92684..216441f53c 100644 --- a/libtextsecure/sendmessage.js +++ b/libtextsecure/sendmessage.js @@ -420,39 +420,44 @@ MessageSender.prototype = { syncVerification: function(destination, state, identityKey) { var myNumber = textsecure.storage.user.getNumber(); var myDevice = textsecure.storage.user.getDeviceId(); - if (myDevice != 1) { - // First send a null message to mask the sync message. - var nullMessage = new textsecure.protobuf.NullMessage(); + var now = Date.now(); - // Generate a random int from 1 and 512 - var buffer = libsignal.crypto.getRandomBytes(1); - var paddingLength = (new Uint8Array(buffer)[0] & 0x1ff) + 1; - - // Generate a random padding buffer of the chosen size - nullMessage.padding = libsignal.crypto.getRandomBytes(paddingLength); - - var contentMessage = new textsecure.protobuf.Content(); - contentMessage.nullMessage = nullMessage; - - var silent = true; - return this.sendIndividualProto(destination, contentMessage, Date.now(), silent).then(function() { - var verified = new textsecure.protobuf.Verified(); - verified.state = state; - verified.destination = destination; - verified.identityKey = identityKey; - verified.nullMessage = nullMessage.padding; - - var syncMessage = this.createSyncMessage(); - syncMessage.verified = verified; - - var contentMessage = new textsecure.protobuf.Content(); - contentMessage.syncMessage = syncMessage; - - return this.sendIndividualProto(myNumber, contentMessage, Date.now(), silent); - }.bind(this)); + if (myDevice === 1) { + return Promise.resolve(); } - return Promise.resolve(); + // First send a null message to mask the sync message. + var nullMessage = new textsecure.protobuf.NullMessage(); + + // Generate a random int from 1 and 512 + var buffer = libsignal.crypto.getRandomBytes(1); + var paddingLength = (new Uint8Array(buffer)[0] & 0x1ff) + 1; + + // Generate a random padding buffer of the chosen size + nullMessage.padding = libsignal.crypto.getRandomBytes(paddingLength); + + var contentMessage = new textsecure.protobuf.Content(); + contentMessage.nullMessage = nullMessage; + + // We want the NullMessage to look like a normal outgoing message; not silent + const promise = this.sendIndividualProto(destination, contentMessage, now); + + return promise.then(function() { + var verified = new textsecure.protobuf.Verified(); + verified.state = state; + verified.destination = destination; + verified.identityKey = identityKey; + verified.nullMessage = nullMessage.padding; + + var syncMessage = this.createSyncMessage(); + syncMessage.verified = verified; + + var contentMessage = new textsecure.protobuf.Content(); + contentMessage.syncMessage = syncMessage; + + var silent = true; + return this.sendIndividualProto(myNumber, contentMessage, now, silent); + }.bind(this)); }, sendGroupProto: function(numbers, proto, timestamp) { From 1432d9853b0574df7ee9134a87d60fc6cc6127eb Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Mon, 4 Dec 2017 15:54:56 -0800 Subject: [PATCH 9/9] v1.0.40 Fix bug where previously-deleted contacts are re-added to left pane on contact import (#1811) Fix bug where the main window would be created off-screen, inaccessible (#1830) Fix issue where certain sync messages to iOS would result in a new message alert (#1843 and #1857)) Key rotation updates (#1833 and #1846): - Save prekeys optimistically after generation, even if upload appears to fail - Track confirmations from server in prekey - New prekey cleanup behavior: favoring confirmed keys, but always keeping three - Log all failures resulting from signed prekey rotation - Retry rotation if we run into a failure - Force rotation when starting up new version, to fix Chrome interference (https://github.com/WhisperSystems/Signal-Desktop/releases/tag/v0.44.13) Dev: - Verify that saved window location/size data is valid before creating window (#1830) - A variety of logging improvements to help track down bugs (#1832 and #1836) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 465749f3bc..5934975076 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "productName": "Signal", "description": "Private messaging from your desktop", "repository": "https://github.com/WhisperSystems/Signal-Desktop.git", - "version": "1.0.39", + "version": "1.0.40", "license": "GPL-3.0", "author": { "name": "Open Whisper Systems",