diff --git a/.prettierignore b/.prettierignore index 7b7efc2ae..dc8d24746 100644 --- a/.prettierignore +++ b/.prettierignore @@ -18,6 +18,8 @@ ts/**/*.js components/* dist/* libtextsecure/libsignal-protocol.js +test/fixtures.js +test/blanket_mocha.js /**/*.json /**/*.css diff --git a/libtextsecure/ProvisioningCipher.js b/libtextsecure/ProvisioningCipher.js index 080faf9db..6aebbf6f5 100644 --- a/libtextsecure/ProvisioningCipher.js +++ b/libtextsecure/ProvisioningCipher.js @@ -1,68 +1,85 @@ (function() { -'use strict'; + 'use strict'; -function ProvisioningCipher() {} + function ProvisioningCipher() {} -ProvisioningCipher.prototype = { + ProvisioningCipher.prototype = { decrypt: function(provisionEnvelope) { - var masterEphemeral = provisionEnvelope.publicKey.toArrayBuffer(); - var message = provisionEnvelope.body.toArrayBuffer(); - if (new Uint8Array(message)[0] != 1) { - throw new Error("Bad version number on ProvisioningMessage"); - } + var masterEphemeral = provisionEnvelope.publicKey.toArrayBuffer(); + var message = provisionEnvelope.body.toArrayBuffer(); + if (new Uint8Array(message)[0] != 1) { + throw new Error('Bad version number on ProvisioningMessage'); + } - var iv = message.slice(1, 16 + 1); - var mac = message.slice(message.byteLength - 32, message.byteLength); - var ivAndCiphertext = message.slice(0, message.byteLength - 32); - var ciphertext = message.slice(16 + 1, message.byteLength - 32); + var iv = message.slice(1, 16 + 1); + var mac = message.slice(message.byteLength - 32, message.byteLength); + var ivAndCiphertext = message.slice(0, message.byteLength - 32); + var ciphertext = message.slice(16 + 1, message.byteLength - 32); - return libsignal.Curve.async.calculateAgreement( - masterEphemeral, this.keyPair.privKey - ).then(function(ecRes) { - return libsignal.HKDF.deriveSecrets( - ecRes, new ArrayBuffer(32), "TextSecure Provisioning Message" - ); - }).then(function(keys) { - return libsignal.crypto.verifyMAC(ivAndCiphertext, keys[1], mac, 32).then(function() { - return libsignal.crypto.decrypt(keys[0], ciphertext, iv); + return libsignal.Curve.async + .calculateAgreement(masterEphemeral, this.keyPair.privKey) + .then(function(ecRes) { + return libsignal.HKDF.deriveSecrets( + ecRes, + new ArrayBuffer(32), + 'TextSecure Provisioning Message' + ); + }) + .then(function(keys) { + return libsignal.crypto + .verifyMAC(ivAndCiphertext, keys[1], mac, 32) + .then(function() { + return libsignal.crypto.decrypt(keys[0], ciphertext, iv); }); - }).then(function(plaintext) { - var provisionMessage = textsecure.protobuf.ProvisionMessage.decode(plaintext); - var privKey = provisionMessage.identityKeyPrivate.toArrayBuffer(); + }) + .then(function(plaintext) { + var provisionMessage = textsecure.protobuf.ProvisionMessage.decode( + plaintext + ); + var privKey = provisionMessage.identityKeyPrivate.toArrayBuffer(); - return libsignal.Curve.async.createKeyPair(privKey).then(function(keyPair) { - var ret = { - identityKeyPair : keyPair, - number : provisionMessage.number, - provisioningCode : provisionMessage.provisioningCode, - userAgent : provisionMessage.userAgent, - readReceipts : provisionMessage.readReceipts - }; - if (provisionMessage.profileKey) { - ret.profileKey = provisionMessage.profileKey.toArrayBuffer(); - } - return ret; + return libsignal.Curve.async + .createKeyPair(privKey) + .then(function(keyPair) { + var ret = { + identityKeyPair: keyPair, + number: provisionMessage.number, + provisioningCode: provisionMessage.provisioningCode, + userAgent: provisionMessage.userAgent, + readReceipts: provisionMessage.readReceipts, + }; + if (provisionMessage.profileKey) { + ret.profileKey = provisionMessage.profileKey.toArrayBuffer(); + } + return ret; }); }); }, getPublicKey: function() { - return Promise.resolve().then(function() { - if (!this.keyPair) { - return libsignal.Curve.async.generateKeyPair().then(function(keyPair) { + return Promise.resolve() + .then( + function() { + if (!this.keyPair) { + return libsignal.Curve.async.generateKeyPair().then( + function(keyPair) { this.keyPair = keyPair; - }.bind(this)); - } - }.bind(this)).then(function() { - return this.keyPair.pubKey; - }.bind(this)); - } -}; + }.bind(this) + ); + } + }.bind(this) + ) + .then( + function() { + return this.keyPair.pubKey; + }.bind(this) + ); + }, + }; -libsignal.ProvisioningCipher = function() { + libsignal.ProvisioningCipher = function() { var cipher = new ProvisioningCipher(); - this.decrypt = cipher.decrypt.bind(cipher); + this.decrypt = cipher.decrypt.bind(cipher); this.getPublicKey = cipher.getPublicKey.bind(cipher); -}; - + }; })(); diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index 78148357d..3bd99cda5 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -122,9 +122,10 @@ MessageReceiver.prototype.extend({ this.onEmpty(); } // possible 403 or network issue. Make an request to confirm - return this.server.getDevices(this.number) + return this.server + .getDevices(this.number) .then(this.connect.bind(this)) // No HTTP error? Reconnect - .catch((e) => { + .catch(e => { const event = new Event('error'); event.error = e; return this.dispatchAndWait(event); @@ -146,35 +147,41 @@ MessageReceiver.prototype.extend({ return; } - const promise = textsecure.crypto.decryptWebsocketMessage( - request.body, - this.signalingKey - ).then((plaintext) => { - const envelope = textsecure.protobuf.Envelope.decode(plaintext); - // After this point, decoding errors are not the server's - // fault, and we should handle them gracefully and tell the - // user they received an invalid message + const promise = textsecure.crypto + .decryptWebsocketMessage(request.body, this.signalingKey) + .then(plaintext => { + const envelope = textsecure.protobuf.Envelope.decode(plaintext); + // After this point, decoding errors are not the server's + // fault, and we should handle them gracefully and tell the + // user they received an invalid message - if (this.isBlocked(envelope.source)) { - return request.respond(200, 'OK'); - } + if (this.isBlocked(envelope.source)) { + return request.respond(200, 'OK'); + } - return this.addToCache(envelope, plaintext).then(() => { - request.respond(200, 'OK'); - this.queueEnvelope(envelope); - }, (error) => { - console.log( - 'handleRequest error trying to add message to cache:', - error && error.stack ? error.stack : error + return this.addToCache(envelope, plaintext).then( + () => { + request.respond(200, 'OK'); + this.queueEnvelope(envelope); + }, + error => { + console.log( + 'handleRequest error trying to add message to cache:', + error && error.stack ? error.stack : error + ); + } ); + }) + .catch(e => { + request.respond(500, 'Bad encrypted websocket message'); + console.log( + 'Error handling incoming message:', + e && e.stack ? e.stack : e + ); + const ev = new Event('error'); + ev.error = e; + return this.dispatchAndWait(ev); }); - }).catch((e) => { - request.respond(500, 'Bad encrypted websocket message'); - console.log('Error handling incoming message:', e && e.stack ? e.stack : e); - const ev = new Event('error'); - ev.error = e; - return this.dispatchAndWait(ev); - }); this.incoming.push(promise); }, @@ -203,7 +210,7 @@ MessageReceiver.prototype.extend({ this.incoming = []; const dispatchEmpty = () => { - console.log('MessageReceiver: emitting \'empty\' event'); + console.log("MessageReceiver: emitting 'empty' event"); const ev = new Event('empty'); return this.dispatchAndWait(ev); }; @@ -224,9 +231,10 @@ MessageReceiver.prototype.extend({ const { incoming } = this; this.incoming = []; - const queueDispatch = () => this.addToQueue(() => { - console.log('drained'); - }); + const queueDispatch = () => + this.addToQueue(() => { + console.log('drained'); + }); // This promise will resolve when there are no more messages to be processed. return Promise.all(incoming).then(queueDispatch, queueDispatch); @@ -241,7 +249,7 @@ MessageReceiver.prototype.extend({ this.dispatchEvent(ev); }, queueAllCached() { - return this.getAllFromCache().then((items) => { + return this.getAllFromCache().then(items => { for (let i = 0, max = items.length; i < max; i += 1) { this.queueCached(items[i]); } @@ -273,7 +281,9 @@ MessageReceiver.prototype.extend({ } }, getEnvelopeId(envelope) { - return `${envelope.source}.${envelope.sourceDevice} ${envelope.timestamp.toNumber()}`; + return `${envelope.source}.${ + envelope.sourceDevice + } ${envelope.timestamp.toNumber()}`; }, stringToArrayBuffer(string) { // eslint-disable-next-line new-cap @@ -281,23 +291,28 @@ MessageReceiver.prototype.extend({ }, getAllFromCache() { console.log('getAllFromCache'); - return textsecure.storage.unprocessed.getAll().then((items) => { + return textsecure.storage.unprocessed.getAll().then(items => { console.log('getAllFromCache loaded', items.length, 'saved envelopes'); - return Promise.all(_.map(items, (item) => { - const attempts = 1 + (item.attempts || 0); - if (attempts >= 5) { - console.log('getAllFromCache final attempt for envelope', item.id); - return textsecure.storage.unprocessed.remove(item.id); + return Promise.all( + _.map(items, item => { + const attempts = 1 + (item.attempts || 0); + if (attempts >= 5) { + console.log('getAllFromCache final attempt for envelope', item.id); + return textsecure.storage.unprocessed.remove(item.id); + } + return textsecure.storage.unprocessed.update(item.id, { attempts }); + }) + ).then( + () => items, + error => { + console.log( + 'getAllFromCache error updating items after load:', + error && error.stack ? error.stack : error + ); + return items; } - return textsecure.storage.unprocessed.update(item.id, { attempts }); - })).then(() => items, (error) => { - console.log( - 'getAllFromCache error updating items after load:', - error && error.stack ? error.stack : error - ); - return items; - }); + ); }); }, addToCache(envelope, plaintext) { @@ -332,7 +347,7 @@ MessageReceiver.prototype.extend({ ); const promise = this.addToQueue(taskWithTimeout); - return promise.catch((error) => { + return promise.catch(error => { console.log( 'queueDecryptedEnvelope error handling envelope', id, @@ -346,10 +361,13 @@ MessageReceiver.prototype.extend({ console.log('queueing envelope', id); const task = this.handleEnvelope.bind(this, envelope); - const taskWithTimeout = textsecure.createTaskWithTimeout(task, `queueEnvelope ${id}`); + const taskWithTimeout = textsecure.createTaskWithTimeout( + task, + `queueEnvelope ${id}` + ); const promise = this.addToQueue(taskWithTimeout); - return promise.catch((error) => { + return promise.catch(error => { console.log( 'queueEnvelope error handling envelope', id, @@ -448,46 +466,56 @@ MessageReceiver.prototype.extend({ switch (envelope.type) { case textsecure.protobuf.Envelope.Type.CIPHERTEXT: console.log('message from', this.getEnvelopeId(envelope)); - promise = sessionCipher.decryptWhisperMessage(ciphertext).then(this.unpad); + promise = sessionCipher + .decryptWhisperMessage(ciphertext) + .then(this.unpad); break; case textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE: console.log('prekey message from', this.getEnvelopeId(envelope)); - promise = this.decryptPreKeyWhisperMessage(ciphertext, sessionCipher, address); + promise = this.decryptPreKeyWhisperMessage( + ciphertext, + sessionCipher, + address + ); break; default: promise = Promise.reject(new Error('Unknown message type')); } - return promise.then(plaintext => this.updateCache( - envelope, - plaintext - ).then(() => plaintext, (error) => { - console.log( - 'decrypt failed to save decrypted message contents to cache:', - error && error.stack ? error.stack : error - ); - return plaintext; - })).catch((error) => { - let errorToThrow = error; + return promise + .then(plaintext => + this.updateCache(envelope, plaintext).then( + () => plaintext, + error => { + console.log( + 'decrypt failed to save decrypted message contents to cache:', + error && error.stack ? error.stack : error + ); + return plaintext; + } + ) + ) + .catch(error => { + let errorToThrow = error; - if (error.message === 'Unknown identity key') { - // create an error that the UI will pick up and ask the - // user if they want to re-negotiate - const buffer = dcodeIO.ByteBuffer.wrap(ciphertext); - errorToThrow = new textsecure.IncomingIdentityKeyError( - address.toString(), - buffer.toArrayBuffer(), - error.identityKey - ); - } - const ev = new Event('error'); - ev.error = errorToThrow; - ev.proto = envelope; - ev.confirm = this.removeFromCache.bind(this, envelope); + if (error.message === 'Unknown identity key') { + // create an error that the UI will pick up and ask the + // user if they want to re-negotiate + const buffer = dcodeIO.ByteBuffer.wrap(ciphertext); + errorToThrow = new textsecure.IncomingIdentityKeyError( + address.toString(), + buffer.toArrayBuffer(), + error.identityKey + ); + } + const ev = new Event('error'); + ev.error = errorToThrow; + ev.proto = envelope; + ev.confirm = this.removeFromCache.bind(this, envelope); - const returnError = () => Promise.reject(errorToThrow); - return this.dispatchAndWait(ev).then(returnError, returnError); - }); + const returnError = () => Promise.reject(errorToThrow); + return this.dispatchAndWait(ev).then(returnError, returnError); + }); }, async decryptPreKeyWhisperMessage(ciphertext, sessionCipher, address) { const padded = await sessionCipher.decryptPreKeyWhisperMessage(ciphertext); @@ -508,30 +536,34 @@ MessageReceiver.prototype.extend({ throw e; } }, - handleSentMessage(envelope, destination, timestamp, msg, expirationStartTimestamp) { + handleSentMessage( + envelope, + destination, + timestamp, + msg, + expirationStartTimestamp + ) { let p = Promise.resolve(); // eslint-disable-next-line no-bitwise if (msg.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) { p = this.handleEndSession(destination); } - return p.then(() => this.processDecrypted( - envelope, - msg, - this.number - ).then((message) => { - const ev = new Event('sent'); - ev.confirm = this.removeFromCache.bind(this, envelope); - ev.data = { - destination, - timestamp: timestamp.toNumber(), - device: envelope.sourceDevice, - message, - }; - if (expirationStartTimestamp) { - ev.data.expirationStartTimestamp = expirationStartTimestamp.toNumber(); - } - return this.dispatchAndWait(ev); - })); + return p.then(() => + this.processDecrypted(envelope, msg, this.number).then(message => { + const ev = new Event('sent'); + ev.confirm = this.removeFromCache.bind(this, envelope); + ev.data = { + destination, + timestamp: timestamp.toNumber(), + device: envelope.sourceDevice, + message, + }; + if (expirationStartTimestamp) { + ev.data.expirationStartTimestamp = expirationStartTimestamp.toNumber(); + } + return this.dispatchAndWait(ev); + }) + ); }, handleDataMessage(envelope, msg) { console.log('data message from', this.getEnvelopeId(envelope)); @@ -540,38 +572,34 @@ MessageReceiver.prototype.extend({ if (msg.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) { p = this.handleEndSession(envelope.source); } - return p.then(() => this.processDecrypted( - envelope, - msg, - envelope.source - ).then((message) => { - const ev = new Event('message'); - ev.confirm = this.removeFromCache.bind(this, envelope); - ev.data = { - source: envelope.source, - sourceDevice: envelope.sourceDevice, - timestamp: envelope.timestamp.toNumber(), - receivedAt: envelope.receivedAt, - message, - }; - return this.dispatchAndWait(ev); - })); + return p.then(() => + this.processDecrypted(envelope, msg, envelope.source).then(message => { + const ev = new Event('message'); + ev.confirm = this.removeFromCache.bind(this, envelope); + ev.data = { + source: envelope.source, + sourceDevice: envelope.sourceDevice, + timestamp: envelope.timestamp.toNumber(), + receivedAt: envelope.receivedAt, + message, + }; + return this.dispatchAndWait(ev); + }) + ); }, handleLegacyMessage(envelope) { - return this.decrypt( - envelope, - envelope.legacyMessage - ).then(plaintext => this.innerHandleLegacyMessage(envelope, plaintext)); + return this.decrypt(envelope, envelope.legacyMessage).then(plaintext => + this.innerHandleLegacyMessage(envelope, plaintext) + ); }, innerHandleLegacyMessage(envelope, plaintext) { const message = textsecure.protobuf.DataMessage.decode(plaintext); return this.handleDataMessage(envelope, message); }, handleContentMessage(envelope) { - return this.decrypt( - envelope, - envelope.content - ).then(plaintext => this.innerHandleContentMessage(envelope, plaintext)); + return this.decrypt(envelope, envelope.content).then(plaintext => + this.innerHandleContentMessage(envelope, plaintext) + ); }, innerHandleContentMessage(envelope, plaintext) { const content = textsecure.protobuf.Content.decode(plaintext); @@ -595,7 +623,9 @@ MessageReceiver.prototype.extend({ }, handleReceiptMessage(envelope, receiptMessage) { const results = []; - if (receiptMessage.type === textsecure.protobuf.ReceiptMessage.Type.DELIVERY) { + if ( + receiptMessage.type === textsecure.protobuf.ReceiptMessage.Type.DELIVERY + ) { for (let i = 0; i < receiptMessage.timestamp.length; i += 1) { const ev = new Event('delivery'); ev.confirm = this.removeFromCache.bind(this, envelope); @@ -606,7 +636,9 @@ MessageReceiver.prototype.extend({ }; results.push(this.dispatchAndWait(ev)); } - } else if (receiptMessage.type === textsecure.protobuf.ReceiptMessage.Type.READ) { + } else if ( + receiptMessage.type === textsecure.protobuf.ReceiptMessage.Type.READ + ) { for (let i = 0; i < receiptMessage.timestamp.length; i += 1) { const ev = new Event('read'); ev.confirm = this.removeFromCache.bind(this, envelope); @@ -734,12 +766,13 @@ MessageReceiver.prototype.extend({ let groupDetails = groupBuffer.next(); const promises = []; while (groupDetails !== undefined) { - const getGroupDetails = (details) => { + const getGroupDetails = details => { // eslint-disable-next-line no-param-reassign details.id = details.id.toBinary(); if (details.active) { - return textsecure.storage.groups.getGroup(details.id) - .then((existingGroup) => { + return textsecure.storage.groups + .getGroup(details.id) + .then(existingGroup => { if (existingGroup === undefined) { return textsecure.storage.groups.createNewGroup( details.members, @@ -750,19 +783,22 @@ MessageReceiver.prototype.extend({ details.id, details.members ); - }).then(() => details); + }) + .then(() => details); } return Promise.resolve(details); }; - const promise = getGroupDetails(groupDetails).then((details) => { - const ev = new Event('group'); - ev.confirm = this.removeFromCache.bind(this, envelope); - ev.groupDetails = details; - return this.dispatchAndWait(ev); - }).catch((e) => { - console.log('error processing group', e); - }); + const promise = getGroupDetails(groupDetails) + .then(details => { + const ev = new Event('group'); + ev.confirm = this.removeFromCache.bind(this, envelope); + ev.groupDetails = details; + return this.dispatchAndWait(ev); + }) + .catch(e => { + console.log('error processing group', e); + }); groupDetails = groupBuffer.next(); promises.push(promise); } @@ -803,7 +839,8 @@ MessageReceiver.prototype.extend({ attachment.data = data; } - return this.server.getAttachment(attachment.id) + return this.server + .getAttachment(attachment.id) .then(decryptAttachment) .then(updateAttachment); }, @@ -825,8 +862,14 @@ MessageReceiver.prototype.extend({ // It's most likely that dataMessage will be populated, so we look at it in detail const data = content.dataMessage; - if (data && !data.attachments.length && !data.body && !data.expireTimer && - !data.flags && !data.group) { + if ( + data && + !data.attachments.length && + !data.body && + !data.expireTimer && + !data.flags && + !data.group + ) { return false; } @@ -857,7 +900,7 @@ MessageReceiver.prototype.extend({ ciphertext, sessionCipher, address - ).then((plaintext) => { + ).then(plaintext => { const envelope = { source: number, sourceDevice: device, @@ -901,16 +944,18 @@ MessageReceiver.prototype.extend({ console.log('got end session'); const deviceIds = await textsecure.storage.protocol.getDeviceIds(number); - return Promise.all(deviceIds.map((deviceId) => { - const address = new libsignal.SignalProtocolAddress(number, deviceId); - const sessionCipher = new libsignal.SessionCipher( - textsecure.storage.protocol, - address - ); + return Promise.all( + deviceIds.map(deviceId => { + const address = new libsignal.SignalProtocolAddress(number, deviceId); + const sessionCipher = new libsignal.SessionCipher( + textsecure.storage.protocol, + address + ); - console.log('deleting sessions for', address.toString()); - return sessionCipher.deleteAllSessionsForDevice(); - })); + console.log('deleting sessions for', address.toString()); + return sessionCipher.deleteAllSessionsForDevice(); + }) + ); }, processDecrypted(envelope, decrypted, source) { /* eslint-disable no-bitwise, no-param-reassign */ @@ -928,7 +973,6 @@ MessageReceiver.prototype.extend({ decrypted.expireTimer = 0; } - if (decrypted.flags & FLAGS.END_SESSION) { decrypted.body = null; decrypted.attachments = []; @@ -949,7 +993,9 @@ MessageReceiver.prototype.extend({ if (decrypted.group !== null) { decrypted.group.id = decrypted.group.id.toBinary(); - if (decrypted.group.type === textsecure.protobuf.GroupContext.Type.UPDATE) { + if ( + decrypted.group.type === textsecure.protobuf.GroupContext.Type.UPDATE + ) { if (decrypted.group.avatar !== null) { promises.push(this.handleAttachment(decrypted.group.avatar)); } @@ -957,49 +1003,61 @@ MessageReceiver.prototype.extend({ const storageGroups = textsecure.storage.groups; - promises.push(storageGroups.getNumbers(decrypted.group.id).then((existingGroup) => { - if (existingGroup === undefined) { - if (decrypted.group.type !== textsecure.protobuf.GroupContext.Type.UPDATE) { - decrypted.group.members = [source]; - console.log('Got message for unknown group'); - } - return textsecure.storage.groups.createNewGroup( - decrypted.group.members, - decrypted.group.id - ); - } - const fromIndex = existingGroup.indexOf(source); - - if (fromIndex < 0) { - // TODO: This could be indication of a race... - console.log('Sender was not a member of the group they were sending from'); - } - - switch (decrypted.group.type) { - case textsecure.protobuf.GroupContext.Type.UPDATE: - decrypted.body = null; - decrypted.attachments = []; - return textsecure.storage.groups.updateNumbers( - decrypted.group.id, - decrypted.group.members - ); - case textsecure.protobuf.GroupContext.Type.QUIT: - decrypted.body = null; - decrypted.attachments = []; - if (source === this.number) { - return textsecure.storage.groups.deleteGroup(decrypted.group.id); + promises.push( + storageGroups.getNumbers(decrypted.group.id).then(existingGroup => { + if (existingGroup === undefined) { + if ( + decrypted.group.type !== + textsecure.protobuf.GroupContext.Type.UPDATE + ) { + decrypted.group.members = [source]; + console.log('Got message for unknown group'); } - return textsecure.storage.groups.removeNumber(decrypted.group.id, source); - case textsecure.protobuf.GroupContext.Type.DELIVER: - decrypted.group.name = null; - decrypted.group.members = []; - decrypted.group.avatar = null; - return Promise.resolve(); - default: - this.removeFromCache(envelope); - throw new Error('Unknown group message type'); - } - })); + return textsecure.storage.groups.createNewGroup( + decrypted.group.members, + decrypted.group.id + ); + } + const fromIndex = existingGroup.indexOf(source); + + if (fromIndex < 0) { + // TODO: This could be indication of a race... + console.log( + 'Sender was not a member of the group they were sending from' + ); + } + + switch (decrypted.group.type) { + case textsecure.protobuf.GroupContext.Type.UPDATE: + decrypted.body = null; + decrypted.attachments = []; + return textsecure.storage.groups.updateNumbers( + decrypted.group.id, + decrypted.group.members + ); + case textsecure.protobuf.GroupContext.Type.QUIT: + decrypted.body = null; + decrypted.attachments = []; + if (source === this.number) { + return textsecure.storage.groups.deleteGroup( + decrypted.group.id + ); + } + return textsecure.storage.groups.removeNumber( + decrypted.group.id, + source + ); + case textsecure.protobuf.GroupContext.Type.DELIVER: + decrypted.group.name = null; + decrypted.group.members = []; + decrypted.group.avatar = null; + return Promise.resolve(); + default: + this.removeFromCache(envelope); + throw new Error('Unknown group message type'); + } + }) + ); } for (let i = 0, max = decrypted.attachments.length; i < max; i += 1) { @@ -1021,12 +1079,14 @@ MessageReceiver.prototype.extend({ if (thumbnail) { // We don't want the failure of a thumbnail download to fail the handling of // this message entirely, like we do for full attachments. - promises.push(this.handleAttachment(thumbnail).catch((error) => { - console.log( - 'Problem loading thumbnail for quote', - error && error.stack ? error.stack : error - ); - })); + promises.push( + this.handleAttachment(thumbnail).catch(error => { + console.log( + 'Problem loading thumbnail for quote', + error && error.stack ? error.stack : error + ); + }) + ); } } } @@ -1052,8 +1112,12 @@ textsecure.MessageReceiver = function MessageReceiverWrapper( signalingKey, options ); - this.addEventListener = messageReceiver.addEventListener.bind(messageReceiver); - this.removeEventListener = messageReceiver.removeEventListener.bind(messageReceiver); + this.addEventListener = messageReceiver.addEventListener.bind( + messageReceiver + ); + this.removeEventListener = messageReceiver.removeEventListener.bind( + messageReceiver + ); this.getStatus = messageReceiver.getStatus.bind(messageReceiver); this.close = messageReceiver.close.bind(messageReceiver); messageReceiver.connect(); @@ -1067,4 +1131,3 @@ textsecure.MessageReceiver = function MessageReceiverWrapper( textsecure.MessageReceiver.prototype = { constructor: textsecure.MessageReceiver, }; - diff --git a/libtextsecure/outgoing_message.js b/libtextsecure/outgoing_message.js index b8cfbdf91..3bb949c1b 100644 --- a/libtextsecure/outgoing_message.js +++ b/libtextsecure/outgoing_message.js @@ -1,241 +1,352 @@ -function OutgoingMessage(server, timestamp, numbers, message, silent, callback) { - if (message instanceof textsecure.protobuf.DataMessage) { - var content = new textsecure.protobuf.Content(); - content.dataMessage = message; - message = content; - } - this.server = server; - this.timestamp = timestamp; - this.numbers = numbers; - this.message = message; // ContentMessage proto - this.callback = callback; - this.silent = silent; +function OutgoingMessage( + server, + timestamp, + numbers, + message, + silent, + callback +) { + if (message instanceof textsecure.protobuf.DataMessage) { + var content = new textsecure.protobuf.Content(); + content.dataMessage = message; + message = content; + } + this.server = server; + this.timestamp = timestamp; + this.numbers = numbers; + this.message = message; // ContentMessage proto + this.callback = callback; + this.silent = silent; - this.numbersCompleted = 0; - this.errors = []; - this.successfulNumbers = []; + this.numbersCompleted = 0; + this.errors = []; + this.successfulNumbers = []; } OutgoingMessage.prototype = { - constructor: OutgoingMessage, - numberCompleted: function() { - this.numbersCompleted++; - if (this.numbersCompleted >= this.numbers.length) { - this.callback({successfulNumbers: this.successfulNumbers, errors: this.errors}); - } - }, - registerError: function(number, reason, error) { - if (!error || error.name === 'HTTPError' && error.code !== 404) { - error = new textsecure.OutgoingMessageError(number, this.message.toArrayBuffer(), this.timestamp, error); - } - - error.number = number; - error.reason = reason; - this.errors[this.errors.length] = error; - this.numberCompleted(); - }, - reloadDevicesAndSend: function(number, recurse) { - return function() { - return textsecure.storage.protocol.getDeviceIds(number).then(function(deviceIds) { - if (deviceIds.length == 0) { - return this.registerError(number, "Got empty device list when loading device keys", null); - } - return this.doSendMessage(number, deviceIds, recurse); - }.bind(this)); - }.bind(this); - }, - - getKeysForNumber: function(number, updateDevices) { - var handleResult = function(response) { - return Promise.all(response.devices.map(function(device) { - device.identityKey = response.identityKey; - if (updateDevices === undefined || updateDevices.indexOf(device.deviceId) > -1) { - var address = new libsignal.SignalProtocolAddress(number, device.deviceId); - var builder = new libsignal.SessionBuilder(textsecure.storage.protocol, address); - if (device.registrationId === 0) { - console.log("device registrationId 0!"); - } - return builder.processPreKey(device).catch(function(error) { - if (error.message === "Identity key changed") { - error.timestamp = this.timestamp; - error.originalMessage = this.message.toArrayBuffer(); - error.identityKey = device.identityKey; - } - throw error; - }.bind(this)); - } - }.bind(this))); - }.bind(this); - - if (updateDevices === undefined) { - return this.server.getKeysForNumber(number).then(handleResult); - } else { - var promise = Promise.resolve(); - updateDevices.forEach(function(device) { - promise = promise.then(function() { - return this.server.getKeysForNumber(number, device).then(handleResult).catch(function(e) { - if (e.name === 'HTTPError' && e.code === 404) { - if (device !== 1) { - return this.removeDeviceIdsForNumber(number, [device]); - } else { - throw new textsecure.UnregisteredUserError(number, e); - } - } else { - throw e; - } - }.bind(this)); - }.bind(this)); - }.bind(this)); - - return promise; - } - }, - - transmitMessage: function(number, jsonData, timestamp) { - return this.server.sendMessages(number, jsonData, timestamp, this.silent).catch(function(e) { - if (e.name === 'HTTPError' && (e.code !== 409 && e.code !== 410)) { - // 409 and 410 should bubble and be handled by doSendMessage - // 404 should throw UnregisteredUserError - // all other network errors can be retried later. - if (e.code === 404) { - throw new textsecure.UnregisteredUserError(number, e); - } - throw new textsecure.SendMessageNetworkError(number, jsonData, e, timestamp); - } - throw e; - }); - }, - - getPaddedMessageLength: function(messageLength) { - var messageLengthWithTerminator = messageLength + 1; - var messagePartCount = Math.floor(messageLengthWithTerminator / 160); - - if (messageLengthWithTerminator % 160 !== 0) { - messagePartCount++; - } - - return messagePartCount * 160; - }, - - getPlaintext: function() { - if (!this.plaintext) { - var messageBuffer = this.message.toArrayBuffer(); - this.plaintext = new Uint8Array( - this.getPaddedMessageLength(messageBuffer.byteLength + 1) - 1 - ); - this.plaintext.set(new Uint8Array(messageBuffer)); - this.plaintext[messageBuffer.byteLength] = 0x80; - } - return this.plaintext; - }, - - doSendMessage: function(number, deviceIds, recurse) { - var ciphers = {}; - var plaintext = this.getPlaintext(); - - return Promise.all(deviceIds.map(function(deviceId) { - var address = new libsignal.SignalProtocolAddress(number, deviceId); - - var ourNumber = textsecure.storage.user.getNumber(); - var options = {}; - - // No limit on message keys if we're communicating with our other devices - if (ourNumber === number) { - options.messageKeysLimit = false; - } - - var sessionCipher = new libsignal.SessionCipher(textsecure.storage.protocol, address, options); - ciphers[address.getDeviceId()] = sessionCipher; - return sessionCipher.encrypt(plaintext).then(function(ciphertext) { - return { - type : ciphertext.type, - destinationDeviceId : address.getDeviceId(), - destinationRegistrationId : ciphertext.registrationId, - content : btoa(ciphertext.body) - }; - }); - }.bind(this))).then(function(jsonData) { - return this.transmitMessage(number, jsonData, this.timestamp).then(function() { - this.successfulNumbers[this.successfulNumbers.length] = number; - this.numberCompleted(); - }.bind(this)); - }.bind(this)).catch(function(error) { - if (error instanceof Error && error.name == "HTTPError" && (error.code == 410 || error.code == 409)) { - if (!recurse) - return this.registerError(number, "Hit retry limit attempting to reload device list", error); - - var p; - if (error.code == 409) { - p = this.removeDeviceIdsForNumber(number, error.response.extraDevices); - } else { - p = Promise.all(error.response.staleDevices.map(function(deviceId) { - return ciphers[deviceId].closeOpenSessionForDevice(); - })); - } - - return p.then(function() { - var resetDevices = ((error.code == 410) ? error.response.staleDevices : error.response.missingDevices); - return this.getKeysForNumber(number, resetDevices) - .then(this.reloadDevicesAndSend(number, error.code == 409)); - }.bind(this)); - } else if (error.message === "Identity key changed") { - error.timestamp = this.timestamp; - error.originalMessage = this.message.toArrayBuffer(); - console.log('Got "key changed" error from encrypt - no identityKey for application layer', number, deviceIds) - throw error; - } else { - this.registerError(number, "Failed to create or send message", error); - } - }.bind(this)); - }, - - getStaleDeviceIdsForNumber: function(number) { - return textsecure.storage.protocol.getDeviceIds(number).then(function(deviceIds) { - if (deviceIds.length === 0) { - return [1]; - } - var updateDevices = []; - return Promise.all(deviceIds.map(function(deviceId) { - var address = new libsignal.SignalProtocolAddress(number, deviceId); - var sessionCipher = new libsignal.SessionCipher(textsecure.storage.protocol, address); - return sessionCipher.hasOpenSession().then(function(hasSession) { - if (!hasSession) { - updateDevices.push(deviceId); - } - }); - })).then(function() { - return updateDevices; - }); - }); - }, - - removeDeviceIdsForNumber: function(number, deviceIdsToRemove) { - var promise = Promise.resolve(); - for (var j in deviceIdsToRemove) { - promise = promise.then(function() { - var encodedNumber = number + "." + deviceIdsToRemove[j]; - return textsecure.storage.protocol.removeSession(encodedNumber); - }); - } - return promise; - }, - - sendToNumber: function(number) { - return this.getStaleDeviceIdsForNumber(number).then(function(updateDevices) { - return this.getKeysForNumber(number, updateDevices) - .then(this.reloadDevicesAndSend(number, true)) - .catch(function(error) { - if (error.message === "Identity key changed") { - error = new textsecure.OutgoingIdentityKeyError( - number, error.originalMessage, error.timestamp, error.identityKey - ); - this.registerError(number, "Identity key changed", error); - } else { - this.registerError( - number, "Failed to retrieve new device keys for number " + number, error - ); - } - }.bind(this)); - }.bind(this)); + constructor: OutgoingMessage, + numberCompleted: function() { + this.numbersCompleted++; + if (this.numbersCompleted >= this.numbers.length) { + this.callback({ + successfulNumbers: this.successfulNumbers, + errors: this.errors, + }); } + }, + registerError: function(number, reason, error) { + if (!error || (error.name === 'HTTPError' && error.code !== 404)) { + error = new textsecure.OutgoingMessageError( + number, + this.message.toArrayBuffer(), + this.timestamp, + error + ); + } + + error.number = number; + error.reason = reason; + this.errors[this.errors.length] = error; + this.numberCompleted(); + }, + reloadDevicesAndSend: function(number, recurse) { + return function() { + return textsecure.storage.protocol.getDeviceIds(number).then( + function(deviceIds) { + if (deviceIds.length == 0) { + return this.registerError( + number, + 'Got empty device list when loading device keys', + null + ); + } + return this.doSendMessage(number, deviceIds, recurse); + }.bind(this) + ); + }.bind(this); + }, + + getKeysForNumber: function(number, updateDevices) { + var handleResult = function(response) { + return Promise.all( + response.devices.map( + function(device) { + device.identityKey = response.identityKey; + if ( + updateDevices === undefined || + updateDevices.indexOf(device.deviceId) > -1 + ) { + var address = new libsignal.SignalProtocolAddress( + number, + device.deviceId + ); + var builder = new libsignal.SessionBuilder( + textsecure.storage.protocol, + address + ); + if (device.registrationId === 0) { + console.log('device registrationId 0!'); + } + return builder.processPreKey(device).catch( + function(error) { + if (error.message === 'Identity key changed') { + error.timestamp = this.timestamp; + error.originalMessage = this.message.toArrayBuffer(); + error.identityKey = device.identityKey; + } + throw error; + }.bind(this) + ); + } + }.bind(this) + ) + ); + }.bind(this); + + if (updateDevices === undefined) { + return this.server.getKeysForNumber(number).then(handleResult); + } else { + var promise = Promise.resolve(); + updateDevices.forEach( + function(device) { + promise = promise.then( + function() { + return this.server + .getKeysForNumber(number, device) + .then(handleResult) + .catch( + function(e) { + if (e.name === 'HTTPError' && e.code === 404) { + if (device !== 1) { + return this.removeDeviceIdsForNumber(number, [device]); + } else { + throw new textsecure.UnregisteredUserError(number, e); + } + } else { + throw e; + } + }.bind(this) + ); + }.bind(this) + ); + }.bind(this) + ); + + return promise; + } + }, + + transmitMessage: function(number, jsonData, timestamp) { + return this.server + .sendMessages(number, jsonData, timestamp, this.silent) + .catch(function(e) { + if (e.name === 'HTTPError' && (e.code !== 409 && e.code !== 410)) { + // 409 and 410 should bubble and be handled by doSendMessage + // 404 should throw UnregisteredUserError + // all other network errors can be retried later. + if (e.code === 404) { + throw new textsecure.UnregisteredUserError(number, e); + } + throw new textsecure.SendMessageNetworkError( + number, + jsonData, + e, + timestamp + ); + } + throw e; + }); + }, + + getPaddedMessageLength: function(messageLength) { + var messageLengthWithTerminator = messageLength + 1; + var messagePartCount = Math.floor(messageLengthWithTerminator / 160); + + if (messageLengthWithTerminator % 160 !== 0) { + messagePartCount++; + } + + return messagePartCount * 160; + }, + + getPlaintext: function() { + if (!this.plaintext) { + var messageBuffer = this.message.toArrayBuffer(); + this.plaintext = new Uint8Array( + this.getPaddedMessageLength(messageBuffer.byteLength + 1) - 1 + ); + this.plaintext.set(new Uint8Array(messageBuffer)); + this.plaintext[messageBuffer.byteLength] = 0x80; + } + return this.plaintext; + }, + + doSendMessage: function(number, deviceIds, recurse) { + var ciphers = {}; + var plaintext = this.getPlaintext(); + + return Promise.all( + deviceIds.map( + function(deviceId) { + var address = new libsignal.SignalProtocolAddress(number, deviceId); + + var ourNumber = textsecure.storage.user.getNumber(); + var options = {}; + + // No limit on message keys if we're communicating with our other devices + if (ourNumber === number) { + options.messageKeysLimit = false; + } + + var sessionCipher = new libsignal.SessionCipher( + textsecure.storage.protocol, + address, + options + ); + ciphers[address.getDeviceId()] = sessionCipher; + return sessionCipher.encrypt(plaintext).then(function(ciphertext) { + return { + type: ciphertext.type, + destinationDeviceId: address.getDeviceId(), + destinationRegistrationId: ciphertext.registrationId, + content: btoa(ciphertext.body), + }; + }); + }.bind(this) + ) + ) + .then( + function(jsonData) { + return this.transmitMessage(number, jsonData, this.timestamp).then( + function() { + this.successfulNumbers[this.successfulNumbers.length] = number; + this.numberCompleted(); + }.bind(this) + ); + }.bind(this) + ) + .catch( + function(error) { + if ( + error instanceof Error && + error.name == 'HTTPError' && + (error.code == 410 || error.code == 409) + ) { + if (!recurse) + return this.registerError( + number, + 'Hit retry limit attempting to reload device list', + error + ); + + var p; + if (error.code == 409) { + p = this.removeDeviceIdsForNumber( + number, + error.response.extraDevices + ); + } else { + p = Promise.all( + error.response.staleDevices.map(function(deviceId) { + return ciphers[deviceId].closeOpenSessionForDevice(); + }) + ); + } + + return p.then( + function() { + var resetDevices = + error.code == 410 + ? error.response.staleDevices + : error.response.missingDevices; + return this.getKeysForNumber(number, resetDevices).then( + this.reloadDevicesAndSend(number, error.code == 409) + ); + }.bind(this) + ); + } else if (error.message === 'Identity key changed') { + error.timestamp = this.timestamp; + error.originalMessage = this.message.toArrayBuffer(); + console.log( + 'Got "key changed" error from encrypt - no identityKey for application layer', + number, + deviceIds + ); + throw error; + } else { + this.registerError( + number, + 'Failed to create or send message', + error + ); + } + }.bind(this) + ); + }, + + getStaleDeviceIdsForNumber: function(number) { + return textsecure.storage.protocol + .getDeviceIds(number) + .then(function(deviceIds) { + if (deviceIds.length === 0) { + return [1]; + } + var updateDevices = []; + return Promise.all( + deviceIds.map(function(deviceId) { + var address = new libsignal.SignalProtocolAddress(number, deviceId); + var sessionCipher = new libsignal.SessionCipher( + textsecure.storage.protocol, + address + ); + return sessionCipher.hasOpenSession().then(function(hasSession) { + if (!hasSession) { + updateDevices.push(deviceId); + } + }); + }) + ).then(function() { + return updateDevices; + }); + }); + }, + + removeDeviceIdsForNumber: function(number, deviceIdsToRemove) { + var promise = Promise.resolve(); + for (var j in deviceIdsToRemove) { + promise = promise.then(function() { + var encodedNumber = number + '.' + deviceIdsToRemove[j]; + return textsecure.storage.protocol.removeSession(encodedNumber); + }); + } + return promise; + }, + + sendToNumber: function(number) { + return this.getStaleDeviceIdsForNumber(number).then( + function(updateDevices) { + return this.getKeysForNumber(number, updateDevices) + .then(this.reloadDevicesAndSend(number, true)) + .catch( + function(error) { + if (error.message === 'Identity key changed') { + error = new textsecure.OutgoingIdentityKeyError( + number, + error.originalMessage, + error.timestamp, + error.identityKey + ); + this.registerError(number, 'Identity key changed', error); + } else { + this.registerError( + number, + 'Failed to retrieve new device keys for number ' + number, + error + ); + } + }.bind(this) + ); + }.bind(this) + ); + }, }; diff --git a/libtextsecure/protobufs.js b/libtextsecure/protobufs.js index 9f7088815..4ef228edc 100644 --- a/libtextsecure/protobufs.js +++ b/libtextsecure/protobufs.js @@ -1,29 +1,42 @@ -;(function() { - 'use strict'; - window.textsecure = window.textsecure || {}; - window.textsecure.protobuf = {}; +(function() { + 'use strict'; + window.textsecure = window.textsecure || {}; + window.textsecure.protobuf = {}; - function loadProtoBufs(filename) { - return dcodeIO.ProtoBuf.loadProtoFile({root: window.PROTO_ROOT, file: filename}, function(error, result) { - if (error) { - var text = 'Error loading protos from ' + filename + ' (root: ' + window.PROTO_ROOT + ') ' - + (error && error.stack ? error.stack : error); - console.log(text); - throw error; - } - var protos = result.build('signalservice'); - if (!protos) { - var text = 'Error loading protos from ' + filename + ' (root: ' + window.PROTO_ROOT + ')'; - console.log(text); - throw new Error(text); - } - for (var protoName in protos) { - textsecure.protobuf[protoName] = protos[protoName]; - } - }); - }; + function loadProtoBufs(filename) { + return dcodeIO.ProtoBuf.loadProtoFile( + { root: window.PROTO_ROOT, file: filename }, + function(error, result) { + if (error) { + var text = + 'Error loading protos from ' + + filename + + ' (root: ' + + window.PROTO_ROOT + + ') ' + + (error && error.stack ? error.stack : error); + console.log(text); + throw error; + } + var protos = result.build('signalservice'); + if (!protos) { + var text = + 'Error loading protos from ' + + filename + + ' (root: ' + + window.PROTO_ROOT + + ')'; + console.log(text); + throw new Error(text); + } + for (var protoName in protos) { + textsecure.protobuf[protoName] = protos[protoName]; + } + } + ); + } - loadProtoBufs('SignalService.proto'); - loadProtoBufs('SubProtocol.proto'); - loadProtoBufs('DeviceMessages.proto'); + loadProtoBufs('SignalService.proto'); + loadProtoBufs('SubProtocol.proto'); + loadProtoBufs('DeviceMessages.proto'); })(); diff --git a/libtextsecure/protocol_wrapper.js b/libtextsecure/protocol_wrapper.js index 931089d24..8d12e1be8 100644 --- a/libtextsecure/protocol_wrapper.js +++ b/libtextsecure/protocol_wrapper.js @@ -1,11 +1,11 @@ -;(function() { - 'use strict'; - window.textsecure = window.textsecure || {}; - window.textsecure.storage = window.textsecure.storage || {}; +(function() { + 'use strict'; + window.textsecure = window.textsecure || {}; + window.textsecure.storage = window.textsecure.storage || {}; - textsecure.storage.protocol = new SignalProtocolStore(); + textsecure.storage.protocol = new SignalProtocolStore(); - textsecure.ProvisioningCipher = libsignal.ProvisioningCipher; - textsecure.startWorker = libsignal.worker.startWorker; - textsecure.stopWorker = libsignal.worker.stopWorker; + textsecure.ProvisioningCipher = libsignal.ProvisioningCipher; + textsecure.startWorker = libsignal.worker.startWorker; + textsecure.stopWorker = libsignal.worker.stopWorker; })(); diff --git a/libtextsecure/sendmessage.js b/libtextsecure/sendmessage.js index 0f429daa7..9e465c1e2 100644 --- a/libtextsecure/sendmessage.js +++ b/libtextsecure/sendmessage.js @@ -1,815 +1,997 @@ function stringToArrayBuffer(str) { - if (typeof str !== 'string') { - throw new Error('Passed non-string to stringToArrayBuffer'); - } - var res = new ArrayBuffer(str.length); - var uint = new Uint8Array(res); - for (var i = 0; i < str.length; i++) { - uint[i] = str.charCodeAt(i); - } - return res; + if (typeof str !== 'string') { + throw new Error('Passed non-string to stringToArrayBuffer'); + } + var res = new ArrayBuffer(str.length); + var uint = new Uint8Array(res); + for (var i = 0; i < str.length; i++) { + uint[i] = str.charCodeAt(i); + } + return res; } function Message(options) { - this.body = options.body; - this.attachments = options.attachments || []; - this.quote = options.quote; - this.group = options.group; - this.flags = options.flags; - this.recipients = options.recipients; - this.timestamp = options.timestamp; - this.needsSync = options.needsSync; - this.expireTimer = options.expireTimer; - this.profileKey = options.profileKey; + this.body = options.body; + this.attachments = options.attachments || []; + this.quote = options.quote; + this.group = options.group; + this.flags = options.flags; + this.recipients = options.recipients; + this.timestamp = options.timestamp; + this.needsSync = options.needsSync; + this.expireTimer = options.expireTimer; + this.profileKey = options.profileKey; - if (!(this.recipients instanceof Array) || this.recipients.length < 1) { - throw new Error('Invalid recipient list'); - } + if (!(this.recipients instanceof Array) || this.recipients.length < 1) { + throw new Error('Invalid recipient list'); + } - if (!this.group && this.recipients.length > 1) { - throw new Error('Invalid recipient list for non-group'); - } + if (!this.group && this.recipients.length > 1) { + throw new Error('Invalid recipient list for non-group'); + } - if (typeof this.timestamp !== 'number') { - throw new Error('Invalid timestamp'); - } + if (typeof this.timestamp !== 'number') { + throw new Error('Invalid timestamp'); + } - if (this.expireTimer !== undefined && this.expireTimer !== null) { - if (typeof this.expireTimer !== 'number' || !(this.expireTimer >= 0)) { - throw new Error('Invalid expireTimer'); - } + if (this.expireTimer !== undefined && this.expireTimer !== null) { + if (typeof this.expireTimer !== 'number' || !(this.expireTimer >= 0)) { + throw new Error('Invalid expireTimer'); } + } - if (this.attachments) { - if (!(this.attachments instanceof Array)) { - throw new Error('Invalid message attachments'); - } + if (this.attachments) { + if (!(this.attachments instanceof Array)) { + throw new Error('Invalid message attachments'); } - if (this.flags !== undefined) { - if (typeof this.flags !== 'number') { - throw new Error('Invalid message flags'); - } + } + if (this.flags !== undefined) { + if (typeof this.flags !== 'number') { + throw new Error('Invalid message flags'); } - if (this.isEndSession()) { - if (this.body !== null || this.group !== null || this.attachments.length !== 0) { - throw new Error('Invalid end session message'); - } - } else { - if ( (typeof this.timestamp !== 'number') || - (this.body && typeof this.body !== 'string') ) { - throw new Error('Invalid message body'); - } - if (this.group) { - if ( (typeof this.group.id !== 'string') || - (typeof this.group.type !== 'number') ) { - throw new Error('Invalid group context'); - } - } + } + if (this.isEndSession()) { + if ( + this.body !== null || + this.group !== null || + this.attachments.length !== 0 + ) { + throw new Error('Invalid end session message'); } + } else { + if ( + typeof this.timestamp !== 'number' || + (this.body && typeof this.body !== 'string') + ) { + throw new Error('Invalid message body'); + } + if (this.group) { + if ( + typeof this.group.id !== 'string' || + typeof this.group.type !== 'number' + ) { + throw new Error('Invalid group context'); + } + } + } } Message.prototype = { - constructor: Message, - isEndSession: function() { - return (this.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION); - }, - toProto: function() { - if (this.dataMessage instanceof textsecure.protobuf.DataMessage) { - return this.dataMessage; - } - var proto = new textsecure.protobuf.DataMessage(); - if (this.body) { - proto.body = this.body; - } - proto.attachments = this.attachmentPointers; - if (this.flags) { - proto.flags = this.flags; - } - if (this.group) { - proto.group = new textsecure.protobuf.GroupContext(); - proto.group.id = stringToArrayBuffer(this.group.id); - proto.group.type = this.group.type - } - if (this.quote) { - var QuotedAttachment = textsecure.protobuf.DataMessage.Quote.QuotedAttachment; - var Quote = textsecure.protobuf.DataMessage.Quote; - - proto.quote = new Quote(); - var quote = proto.quote; - - quote.id = this.quote.id; - quote.author = this.quote.author; - quote.text = this.quote.text; - quote.attachments = (this.quote.attachments || []).map(function(attachment) { - var quotedAttachment = new QuotedAttachment(); - - quotedAttachment.contentType = attachment.contentType; - quotedAttachment.fileName = attachment.fileName; - if (attachment.attachmentPointer) { - quotedAttachment.thumbnail = attachment.attachmentPointer; - } - - return quotedAttachment; - }); - } - if (this.expireTimer) { - proto.expireTimer = this.expireTimer; - } - - if (this.profileKey) { - proto.profileKey = this.profileKey; - } - - this.dataMessage = proto; - return proto; - }, - toArrayBuffer: function() { - return this.toProto().toArrayBuffer(); + constructor: Message, + isEndSession: function() { + return this.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION; + }, + toProto: function() { + if (this.dataMessage instanceof textsecure.protobuf.DataMessage) { + return this.dataMessage; } + var proto = new textsecure.protobuf.DataMessage(); + if (this.body) { + proto.body = this.body; + } + proto.attachments = this.attachmentPointers; + if (this.flags) { + proto.flags = this.flags; + } + if (this.group) { + proto.group = new textsecure.protobuf.GroupContext(); + proto.group.id = stringToArrayBuffer(this.group.id); + proto.group.type = this.group.type; + } + if (this.quote) { + var QuotedAttachment = + textsecure.protobuf.DataMessage.Quote.QuotedAttachment; + var Quote = textsecure.protobuf.DataMessage.Quote; + + proto.quote = new Quote(); + var quote = proto.quote; + + quote.id = this.quote.id; + quote.author = this.quote.author; + quote.text = this.quote.text; + quote.attachments = (this.quote.attachments || []).map(function( + attachment + ) { + var quotedAttachment = new QuotedAttachment(); + + quotedAttachment.contentType = attachment.contentType; + quotedAttachment.fileName = attachment.fileName; + if (attachment.attachmentPointer) { + quotedAttachment.thumbnail = attachment.attachmentPointer; + } + + return quotedAttachment; + }); + } + if (this.expireTimer) { + proto.expireTimer = this.expireTimer; + } + + if (this.profileKey) { + proto.profileKey = this.profileKey; + } + + this.dataMessage = proto; + return proto; + }, + toArrayBuffer: function() { + return this.toProto().toArrayBuffer(); + }, }; function MessageSender(url, username, password, cdn_url) { - this.server = new TextSecureServer(url, username, password, cdn_url); - this.pendingMessages = {}; + this.server = new TextSecureServer(url, username, password, cdn_url); + this.pendingMessages = {}; } MessageSender.prototype = { - constructor: MessageSender, + constructor: MessageSender, -// makeAttachmentPointer :: Attachment -> Promise AttachmentPointerProto - makeAttachmentPointer: function(attachment) { - if (typeof attachment !== 'object' || attachment == null) { - return Promise.resolve(undefined); - } + // makeAttachmentPointer :: Attachment -> Promise AttachmentPointerProto + makeAttachmentPointer: function(attachment) { + if (typeof attachment !== 'object' || attachment == null) { + return Promise.resolve(undefined); + } - if (!(attachment.data instanceof ArrayBuffer) && - !ArrayBuffer.isView(attachment.data)) { - return Promise.reject(new TypeError( - '`attachment.data` must be an `ArrayBuffer` or `ArrayBufferView`; got: ' + + if ( + !(attachment.data instanceof ArrayBuffer) && + !ArrayBuffer.isView(attachment.data) + ) { + return Promise.reject( + new TypeError( + '`attachment.data` must be an `ArrayBuffer` or `ArrayBufferView`; got: ' + typeof attachment.data - )); - } + ) + ); + } - var proto = new textsecure.protobuf.AttachmentPointer(); - proto.key = libsignal.crypto.getRandomBytes(64); + var proto = new textsecure.protobuf.AttachmentPointer(); + proto.key = libsignal.crypto.getRandomBytes(64); - var iv = libsignal.crypto.getRandomBytes(16); - return textsecure.crypto.encryptAttachment(attachment.data, proto.key, iv).then(function(result) { - return this.server.putAttachment(result.ciphertext).then(function(id) { - proto.id = id; - proto.contentType = attachment.contentType; - proto.digest = result.digest; - if (attachment.fileName) { - proto.fileName = attachment.fileName; - } - if (attachment.size) { - proto.size = attachment.size; - } - if (attachment.flags) { - proto.flags = attachment.flags; - } - return proto; + var iv = libsignal.crypto.getRandomBytes(16); + return textsecure.crypto + .encryptAttachment(attachment.data, proto.key, iv) + .then( + function(result) { + return this.server + .putAttachment(result.ciphertext) + .then(function(id) { + proto.id = id; + proto.contentType = attachment.contentType; + proto.digest = result.digest; + if (attachment.fileName) { + proto.fileName = attachment.fileName; + } + if (attachment.size) { + proto.size = attachment.size; + } + if (attachment.flags) { + proto.flags = attachment.flags; + } + return proto; }); - }.bind(this)); - }, + }.bind(this) + ); + }, - retransmitMessage: function(number, jsonData, timestamp) { - var outgoing = new OutgoingMessage(this.server); - return outgoing.transmitMessage(number, jsonData, timestamp); - }, + retransmitMessage: function(number, jsonData, timestamp) { + var outgoing = new OutgoingMessage(this.server); + return outgoing.transmitMessage(number, jsonData, timestamp); + }, - validateRetryContentMessage: function(content) { - // We want at least one field set, but not more than one - var count = 0; - count += content.syncMessage ? 1 : 0; - count += content.dataMessage ? 1 : 0; - count += content.callMessage ? 1 : 0; - count += content.nullMessage ? 1 : 0; - if (count !== 1) { - return false; + validateRetryContentMessage: function(content) { + // We want at least one field set, but not more than one + var count = 0; + count += content.syncMessage ? 1 : 0; + count += content.dataMessage ? 1 : 0; + count += content.callMessage ? 1 : 0; + count += content.nullMessage ? 1 : 0; + if (count !== 1) { + return false; + } + + // It's most likely that dataMessage will be populated, so we look at it in detail + var data = content.dataMessage; + if ( + data && + !data.attachments.length && + !data.body && + !data.expireTimer && + !data.flags && + !data.group + ) { + return false; + } + + return true; + }, + + getRetryProto: function(message, timestamp) { + // If message was sent before v0.41.3 was released on Aug 7, then it was most certainly a DataMessage + // + // var d = new Date('2017-08-07T07:00:00.000Z'); + // d.getTime(); + var august7 = 1502089200000; + if (timestamp < august7) { + return textsecure.protobuf.DataMessage.decode(message); + } + + // This is ugly. But we don't know what kind of proto we need to decode... + try { + // Simply decoding as a Content message may throw + var proto = textsecure.protobuf.Content.decode(message); + + // But it might also result in an invalid object, so we try to detect that + if (this.validateRetryContentMessage(proto)) { + return proto; + } + + return textsecure.protobuf.DataMessage.decode(message); + } catch (e) { + // If this call throws, something has really gone wrong, we'll fail to send + return textsecure.protobuf.DataMessage.decode(message); + } + }, + + tryMessageAgain: function(number, encodedMessage, timestamp) { + var proto = this.getRetryProto(encodedMessage, timestamp); + return this.sendIndividualProto(number, proto, timestamp); + }, + + queueJobForNumber: function(number, runJob) { + var taskWithTimeout = textsecure.createTaskWithTimeout( + runJob, + 'queueJobForNumber ' + number + ); + + var runPrevious = this.pendingMessages[number] || Promise.resolve(); + var runCurrent = (this.pendingMessages[number] = runPrevious.then( + taskWithTimeout, + taskWithTimeout + )); + runCurrent.then( + function() { + if (this.pendingMessages[number] === runCurrent) { + delete this.pendingMessages[number]; + } + }.bind(this) + ); + }, + + uploadAttachments: function(message) { + return Promise.all( + message.attachments.map(this.makeAttachmentPointer.bind(this)) + ) + .then(function(attachmentPointers) { + message.attachmentPointers = attachmentPointers; + }) + .catch(function(error) { + if (error instanceof Error && error.name === 'HTTPError') { + throw new textsecure.MessageError(message, error); + } else { + throw error; + } + }); + }, + + uploadThumbnails: function(message) { + var makePointer = this.makeAttachmentPointer.bind(this); + var quote = message.quote; + + if (!quote || !quote.attachments || quote.attachments.length === 0) { + return Promise.resolve(); + } + + return Promise.all( + quote.attachments.map(function(attachment) { + const thumbnail = attachment.thumbnail; + if (!thumbnail) { + return; } - // It's most likely that dataMessage will be populated, so we look at it in detail - var data = content.dataMessage; - if (data && !data.attachments.length && !data.body && !data.expireTimer && !data.flags && !data.group) { - return false; - } - - return true; - }, - - getRetryProto: function(message, timestamp) { - // If message was sent before v0.41.3 was released on Aug 7, then it was most certainly a DataMessage - // - // var d = new Date('2017-08-07T07:00:00.000Z'); - // d.getTime(); - var august7 = 1502089200000; - if (timestamp < august7) { - return textsecure.protobuf.DataMessage.decode(message); - } - - // This is ugly. But we don't know what kind of proto we need to decode... - try { - // Simply decoding as a Content message may throw - var proto = textsecure.protobuf.Content.decode(message); - - // But it might also result in an invalid object, so we try to detect that - if (this.validateRetryContentMessage(proto)) { - return proto; - } - - return textsecure.protobuf.DataMessage.decode(message); - } catch(e) { - // If this call throws, something has really gone wrong, we'll fail to send - return textsecure.protobuf.DataMessage.decode(message); - } - }, - - tryMessageAgain: function(number, encodedMessage, timestamp) { - var proto = this.getRetryProto(encodedMessage, timestamp); - return this.sendIndividualProto(number, proto, timestamp); - }, - - queueJobForNumber: function(number, runJob) { - var taskWithTimeout = textsecure.createTaskWithTimeout(runJob, 'queueJobForNumber ' + number); - - var runPrevious = this.pendingMessages[number] || Promise.resolve(); - var runCurrent = this.pendingMessages[number] = runPrevious.then(taskWithTimeout, taskWithTimeout); - runCurrent.then(function() { - if (this.pendingMessages[number] === runCurrent) { - delete this.pendingMessages[number]; - } - }.bind(this)); - }, - - uploadAttachments: function(message) { - return Promise.all( - message.attachments.map(this.makeAttachmentPointer.bind(this)) - ).then(function(attachmentPointers) { - message.attachmentPointers = attachmentPointers; - }).catch(function(error) { - if (error instanceof Error && error.name === 'HTTPError') { - throw new textsecure.MessageError(message, error); - } else { - throw error; - } + return makePointer(thumbnail).then(function(pointer) { + attachment.attachmentPointer = pointer; }); - }, + }) + ).catch(function(error) { + if (error instanceof Error && error.name === 'HTTPError') { + throw new textsecure.MessageError(message, error); + } else { + throw error; + } + }); + }, - uploadThumbnails: function(message) { - var makePointer = this.makeAttachmentPointer.bind(this); - var quote = message.quote; + sendMessage: function(attrs) { + var message = new Message(attrs); + return Promise.all([ + this.uploadAttachments(message), + this.uploadThumbnails(message), + ]).then( + function() { + return new Promise( + function(resolve, reject) { + this.sendMessageProto( + message.timestamp, + message.recipients, + message.toProto(), + function(res) { + res.dataMessage = message.toArrayBuffer(); + if (res.errors.length > 0) { + reject(res); + } else { + resolve(res); + } + } + ); + }.bind(this) + ); + }.bind(this) + ); + }, + sendMessageProto: function(timestamp, numbers, message, callback, silent) { + var rejections = textsecure.storage.get('signedKeyRotationRejected', 0); + if (rejections > 5) { + throw new textsecure.SignedPreKeyRotationError( + numbers, + message.toArrayBuffer(), + timestamp + ); + } - if (!quote || !quote.attachments || quote.attachments.length === 0) { - return Promise.resolve(); - } + var outgoing = new OutgoingMessage( + this.server, + timestamp, + numbers, + message, + silent, + callback + ); - return Promise.all(quote.attachments.map(function(attachment) { - const thumbnail = attachment.thumbnail; - if (!thumbnail) { - return; - } - - return makePointer(thumbnail).then(function(pointer) { - attachment.attachmentPointer = pointer; - }); - })).catch(function(error) { - if (error instanceof Error && error.name === 'HTTPError') { - throw new textsecure.MessageError(message, error); - } else { - throw error; - } + numbers.forEach( + function(number) { + this.queueJobForNumber(number, function() { + return outgoing.sendToNumber(number); }); - }, + }.bind(this) + ); + }, - sendMessage: function(attrs) { - var message = new Message(attrs); - return Promise.all([ - this.uploadAttachments(message), - this.uploadThumbnails(message), - ]).then(function() { - return new Promise(function(resolve, reject) { - this.sendMessageProto( - message.timestamp, - message.recipients, - message.toProto(), - function(res) { - res.dataMessage = message.toArrayBuffer(); - if (res.errors.length > 0) { - reject(res); - } else { - resolve(res); - } - } - ); - }.bind(this)); - }.bind(this)); - }, - sendMessageProto: function(timestamp, numbers, message, callback, silent) { - var rejections = textsecure.storage.get('signedKeyRotationRejected', 0); - if (rejections > 5) { - throw new textsecure.SignedPreKeyRotationError(numbers, message.toArrayBuffer(), timestamp); - } + retrySendMessageProto: function(numbers, encodedMessage, timestamp) { + 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) { + reject(res); + } else { + resolve(res); + } + }); + }.bind(this) + ); + }, - var outgoing = new OutgoingMessage(this.server, timestamp, numbers, message, silent, callback); + sendIndividualProto: function(number, proto, timestamp, silent) { + return new Promise( + function(resolve, reject) { + var callback = function(res) { + if (res.errors.length > 0) { + reject(res); + } else { + resolve(res); + } + }; + this.sendMessageProto(timestamp, [number], proto, callback, silent); + }.bind(this) + ); + }, - numbers.forEach(function(number) { - this.queueJobForNumber(number, function() { - return outgoing.sendToNumber(number); - }); - }.bind(this)); - }, + createSyncMessage: function() { + var syncMessage = new textsecure.protobuf.SyncMessage(); - retrySendMessageProto: function(numbers, encodedMessage, timestamp) { - 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) { - reject(res); - } else { - resolve(res); - } - }); - }.bind(this)); - }, + // Generate a random int from 1 and 512 + var buffer = libsignal.crypto.getRandomBytes(1); + var paddingLength = (new Uint8Array(buffer)[0] & 0x1ff) + 1; - sendIndividualProto: function(number, proto, timestamp, silent) { - return new Promise(function(resolve, reject) { - var callback = function(res) { - if (res.errors.length > 0) { - reject(res); - } else { - resolve(res); - } - }; - this.sendMessageProto(timestamp, [number], proto, callback, silent); - }.bind(this)); - }, + // Generate a random padding buffer of the chosen size + syncMessage.padding = libsignal.crypto.getRandomBytes(paddingLength); - createSyncMessage: function() { - var syncMessage = new textsecure.protobuf.SyncMessage(); + return syncMessage; + }, - // Generate a random int from 1 and 512 - var buffer = libsignal.crypto.getRandomBytes(1); - var paddingLength = (new Uint8Array(buffer)[0] & 0x1ff) + 1; + sendSyncMessage: function( + encodedDataMessage, + timestamp, + destination, + expirationStartTimestamp + ) { + var myNumber = textsecure.storage.user.getNumber(); + var myDevice = textsecure.storage.user.getDeviceId(); + if (myDevice == 1) { + return Promise.resolve(); + } - // Generate a random padding buffer of the chosen size - syncMessage.padding = libsignal.crypto.getRandomBytes(paddingLength); + var dataMessage = textsecure.protobuf.DataMessage.decode( + encodedDataMessage + ); + var sentMessage = new textsecure.protobuf.SyncMessage.Sent(); + sentMessage.timestamp = timestamp; + sentMessage.message = dataMessage; + if (destination) { + sentMessage.destination = destination; + } + if (expirationStartTimestamp) { + sentMessage.expirationStartTimestamp = expirationStartTimestamp; + } + var syncMessage = this.createSyncMessage(); + syncMessage.sent = sentMessage; + var contentMessage = new textsecure.protobuf.Content(); + contentMessage.syncMessage = syncMessage; - return syncMessage; - }, + var silent = true; + return this.sendIndividualProto( + myNumber, + contentMessage, + Date.now(), + silent + ); + }, - sendSyncMessage: function(encodedDataMessage, timestamp, destination, expirationStartTimestamp) { - var myNumber = textsecure.storage.user.getNumber(); - var myDevice = textsecure.storage.user.getDeviceId(); - if (myDevice == 1) { - return Promise.resolve(); - } + getProfile: function(number) { + return this.server.getProfile(number); + }, + getAvatar: function(path) { + return this.server.getAvatar(path); + }, + + sendRequestConfigurationSyncMessage: function() { + var myNumber = textsecure.storage.user.getNumber(); + var myDevice = textsecure.storage.user.getDeviceId(); + if (myDevice != 1) { + var request = new textsecure.protobuf.SyncMessage.Request(); + request.type = textsecure.protobuf.SyncMessage.Request.Type.CONFIGURATION; + var syncMessage = this.createSyncMessage(); + syncMessage.request = request; + var contentMessage = new textsecure.protobuf.Content(); + contentMessage.syncMessage = syncMessage; + + var silent = true; + return this.sendIndividualProto( + myNumber, + contentMessage, + Date.now(), + silent + ); + } + + return Promise.resolve(); + }, + sendRequestGroupSyncMessage: function() { + var myNumber = textsecure.storage.user.getNumber(); + var myDevice = textsecure.storage.user.getDeviceId(); + if (myDevice != 1) { + var request = new textsecure.protobuf.SyncMessage.Request(); + request.type = textsecure.protobuf.SyncMessage.Request.Type.GROUPS; + var syncMessage = this.createSyncMessage(); + syncMessage.request = request; + var contentMessage = new textsecure.protobuf.Content(); + contentMessage.syncMessage = syncMessage; + + var silent = true; + return this.sendIndividualProto( + myNumber, + contentMessage, + Date.now(), + silent + ); + } + + return Promise.resolve(); + }, + + sendRequestContactSyncMessage: function() { + var myNumber = textsecure.storage.user.getNumber(); + var myDevice = textsecure.storage.user.getDeviceId(); + if (myDevice != 1) { + var request = new textsecure.protobuf.SyncMessage.Request(); + request.type = textsecure.protobuf.SyncMessage.Request.Type.CONTACTS; + var syncMessage = this.createSyncMessage(); + syncMessage.request = request; + var contentMessage = new textsecure.protobuf.Content(); + contentMessage.syncMessage = syncMessage; + + var silent = true; + return this.sendIndividualProto( + myNumber, + contentMessage, + Date.now(), + silent + ); + } + + return Promise.resolve(); + }, + sendReadReceipts: function(sender, timestamps) { + var receiptMessage = new textsecure.protobuf.ReceiptMessage(); + receiptMessage.type = textsecure.protobuf.ReceiptMessage.Type.READ; + receiptMessage.timestamp = timestamps; + + var contentMessage = new textsecure.protobuf.Content(); + contentMessage.receiptMessage = receiptMessage; + + var silent = true; + return this.sendIndividualProto(sender, contentMessage, Date.now(), silent); + }, + syncReadMessages: function(reads) { + var myNumber = textsecure.storage.user.getNumber(); + var myDevice = textsecure.storage.user.getDeviceId(); + if (myDevice != 1) { + var syncMessage = this.createSyncMessage(); + syncMessage.read = []; + for (var i = 0; i < reads.length; ++i) { + var read = new textsecure.protobuf.SyncMessage.Read(); + read.timestamp = reads[i].timestamp; + read.sender = reads[i].sender; + syncMessage.read.push(read); + } + var contentMessage = new textsecure.protobuf.Content(); + contentMessage.syncMessage = syncMessage; + + var silent = true; + return this.sendIndividualProto( + myNumber, + contentMessage, + Date.now(), + silent + ); + } + + return Promise.resolve(); + }, + syncVerification: function(destination, state, identityKey) { + var myNumber = textsecure.storage.user.getNumber(); + var myDevice = textsecure.storage.user.getDeviceId(); + var now = Date.now(); + + if (myDevice == 1) { + 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 dataMessage = textsecure.protobuf.DataMessage.decode(encodedDataMessage); - var sentMessage = new textsecure.protobuf.SyncMessage.Sent(); - sentMessage.timestamp = timestamp; - sentMessage.message = dataMessage; - if (destination) { - sentMessage.destination = destination; - } - if (expirationStartTimestamp) { - sentMessage.expirationStartTimestamp = expirationStartTimestamp; - } var syncMessage = this.createSyncMessage(); - syncMessage.sent = sentMessage; + syncMessage.verified = verified; + var contentMessage = new textsecure.protobuf.Content(); contentMessage.syncMessage = syncMessage; var silent = true; - return this.sendIndividualProto(myNumber, contentMessage, Date.now(), silent); - }, + return this.sendIndividualProto(myNumber, contentMessage, now, silent); + }.bind(this) + ); + }, - getProfile: function(number) { - return this.server.getProfile(number); - }, - getAvatar: function(path) { - return this.server.getAvatar(path); - }, - - sendRequestConfigurationSyncMessage: function() { - var myNumber = textsecure.storage.user.getNumber(); - var myDevice = textsecure.storage.user.getDeviceId(); - if (myDevice != 1) { - var request = new textsecure.protobuf.SyncMessage.Request(); - request.type = textsecure.protobuf.SyncMessage.Request.Type.CONFIGURATION; - var syncMessage = this.createSyncMessage(); - syncMessage.request = request; - var contentMessage = new textsecure.protobuf.Content(); - contentMessage.syncMessage = syncMessage; - - var silent = true; - return this.sendIndividualProto(myNumber, contentMessage, Date.now(), silent); - } - - return Promise.resolve(); - }, - sendRequestGroupSyncMessage: function() { - var myNumber = textsecure.storage.user.getNumber(); - var myDevice = textsecure.storage.user.getDeviceId(); - if (myDevice != 1) { - var request = new textsecure.protobuf.SyncMessage.Request(); - request.type = textsecure.protobuf.SyncMessage.Request.Type.GROUPS; - var syncMessage = this.createSyncMessage(); - syncMessage.request = request; - var contentMessage = new textsecure.protobuf.Content(); - contentMessage.syncMessage = syncMessage; - - var silent = true; - return this.sendIndividualProto(myNumber, contentMessage, Date.now(), silent); - } - - return Promise.resolve(); - }, - - sendRequestContactSyncMessage: function() { - var myNumber = textsecure.storage.user.getNumber(); - var myDevice = textsecure.storage.user.getDeviceId(); - if (myDevice != 1) { - var request = new textsecure.protobuf.SyncMessage.Request(); - request.type = textsecure.protobuf.SyncMessage.Request.Type.CONTACTS; - var syncMessage = this.createSyncMessage(); - syncMessage.request = request; - var contentMessage = new textsecure.protobuf.Content(); - contentMessage.syncMessage = syncMessage; - - var silent = true; - return this.sendIndividualProto(myNumber, contentMessage, Date.now(), silent); - } - - return Promise.resolve(); - }, - sendReadReceipts: function(sender, timestamps) { - var receiptMessage = new textsecure.protobuf.ReceiptMessage(); - receiptMessage.type = textsecure.protobuf.ReceiptMessage.Type.READ; - receiptMessage.timestamp = timestamps; - - var contentMessage = new textsecure.protobuf.Content(); - contentMessage.receiptMessage = receiptMessage; - - var silent = true; - return this.sendIndividualProto(sender, contentMessage, Date.now(), silent); - }, - syncReadMessages: function(reads) { - var myNumber = textsecure.storage.user.getNumber(); - var myDevice = textsecure.storage.user.getDeviceId(); - if (myDevice != 1) { - var syncMessage = this.createSyncMessage(); - syncMessage.read = []; - for (var i = 0; i < reads.length; ++i) { - var read = new textsecure.protobuf.SyncMessage.Read(); - read.timestamp = reads[i].timestamp; - read.sender = reads[i].sender; - syncMessage.read.push(read); - } - var contentMessage = new textsecure.protobuf.Content(); - contentMessage.syncMessage = syncMessage; - - var silent = true; - return this.sendIndividualProto(myNumber, contentMessage, Date.now(), silent); - } - - return Promise.resolve(); - }, - syncVerification: function(destination, state, identityKey) { - var myNumber = textsecure.storage.user.getNumber(); - var myDevice = textsecure.storage.user.getDeviceId(); - var now = Date.now(); - - if (myDevice == 1) { - 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) { - timestamp = timestamp || Date.now(); - var me = textsecure.storage.user.getNumber(); - numbers = numbers.filter(function(number) { return number != me; }); - if (numbers.length === 0) { - return Promise.reject(new Error('No other members in the group')); - } - - return new Promise(function(resolve, reject) { - var silent = true; - var callback = function(res) { - res.dataMessage = proto.toArrayBuffer(); - if (res.errors.length > 0) { - reject(res); - } else { - resolve(res); - } - }.bind(this); - - this.sendMessageProto(timestamp, numbers, proto, callback, silent); - }.bind(this)); - }, - - sendMessageToNumber: function(number, messageText, attachments, quote, timestamp, expireTimer, profileKey) { - return this.sendMessage({ - recipients : [number], - body : messageText, - timestamp : timestamp, - attachments : attachments, - quote : quote, - needsSync : true, - expireTimer : expireTimer, - profileKey : profileKey - }); - }, - - resetSession: function(number, timestamp) { - console.log('resetting secure session'); - var proto = new textsecure.protobuf.DataMessage(); - proto.body = "TERMINATE"; - proto.flags = textsecure.protobuf.DataMessage.Flags.END_SESSION; - - var logError = function(prefix) { - return function(error) { - console.log( - prefix, - error && error.stack ? error.stack : error - ); - throw error; - }; - }; - var deleteAllSessions = function(number) { - return textsecure.storage.protocol.getDeviceIds(number) - .then(function(deviceIds) { - return Promise.all(deviceIds.map(function(deviceId) { - var address = new libsignal.SignalProtocolAddress(number, deviceId); - console.log('deleting sessions for', address.toString()); - var sessionCipher = new libsignal.SessionCipher( - textsecure.storage.protocol, - address - ); - return sessionCipher.deleteAllSessionsForDevice(); - })); - }); - }; - - var sendToContact = deleteAllSessions(number) - .catch(logError('resetSession/deleteAllSessions1 error:')) - .then(function() { - console.log('finished closing local sessions, now sending to contact'); - return this.sendIndividualProto(number, proto, timestamp) - .catch(logError('resetSession/sendToContact error:')) - }.bind(this)) - .then(function() { - return deleteAllSessions(number) - .catch(logError('resetSession/deleteAllSessions2 error:')); - }); - - var buffer = proto.toArrayBuffer(); - var sendSync = this.sendSyncMessage(buffer, timestamp, number) - .catch(logError('resetSession/sendSync error:')); - - return Promise.all([ - sendToContact, - sendSync - ]); - }, - - sendMessageToGroup: function(groupId, messageText, attachments, quote, timestamp, expireTimer, profileKey) { - return textsecure.storage.groups.getNumbers(groupId).then(function(numbers) { - if (numbers === undefined) - return Promise.reject(new Error("Unknown Group")); - - var me = textsecure.storage.user.getNumber(); - numbers = numbers.filter(function(number) { return number != me; }); - if (numbers.length === 0) { - return Promise.reject(new Error('No other members in the group')); - } - - return this.sendMessage({ - recipients : numbers, - body : messageText, - timestamp : timestamp, - attachments : attachments, - quote : quote, - needsSync : true, - expireTimer : expireTimer, - profileKey : profileKey, - group: { - id: groupId, - type: textsecure.protobuf.GroupContext.Type.DELIVER - } - }); - }.bind(this)); - }, - - createGroup: function(numbers, name, avatar) { - var proto = new textsecure.protobuf.DataMessage(); - proto.group = new textsecure.protobuf.GroupContext(); - - return textsecure.storage.groups.createNewGroup(numbers).then(function(group) { - proto.group.id = stringToArrayBuffer(group.id); - var numbers = group.numbers; - - proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE; - proto.group.members = numbers; - proto.group.name = name; - - return this.makeAttachmentPointer(avatar).then(function(attachment) { - proto.group.avatar = attachment; - return this.sendGroupProto(numbers, proto).then(function() { - return proto.group.id; - }); - }.bind(this)); - }.bind(this)); - }, - - updateGroup: function(groupId, name, avatar, numbers) { - var proto = new textsecure.protobuf.DataMessage(); - proto.group = new textsecure.protobuf.GroupContext(); - - proto.group.id = stringToArrayBuffer(groupId); - proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE; - proto.group.name = name; - - return textsecure.storage.groups.addNumbers(groupId, numbers).then(function(numbers) { - if (numbers === undefined) { - return Promise.reject(new Error("Unknown Group")); - } - proto.group.members = numbers; - - return this.makeAttachmentPointer(avatar).then(function(attachment) { - proto.group.avatar = attachment; - return this.sendGroupProto(numbers, proto).then(function() { - return proto.group.id; - }); - }.bind(this)); - }.bind(this)); - }, - - addNumberToGroup: function(groupId, number) { - var proto = new textsecure.protobuf.DataMessage(); - proto.group = new textsecure.protobuf.GroupContext(); - proto.group.id = stringToArrayBuffer(groupId); - proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE; - - return textsecure.storage.groups.addNumbers(groupId, [number]).then(function(numbers) { - if (numbers === undefined) - return Promise.reject(new Error("Unknown Group")); - proto.group.members = numbers; - - return this.sendGroupProto(numbers, proto); - }.bind(this)); - }, - - setGroupName: function(groupId, name) { - var proto = new textsecure.protobuf.DataMessage(); - proto.group = new textsecure.protobuf.GroupContext(); - proto.group.id = stringToArrayBuffer(groupId); - proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE; - proto.group.name = name; - - return textsecure.storage.groups.getNumbers(groupId).then(function(numbers) { - if (numbers === undefined) - return Promise.reject(new Error("Unknown Group")); - proto.group.members = numbers; - - return this.sendGroupProto(numbers, proto); - }.bind(this)); - }, - - setGroupAvatar: function(groupId, avatar) { - var proto = new textsecure.protobuf.DataMessage(); - proto.group = new textsecure.protobuf.GroupContext(); - proto.group.id = stringToArrayBuffer(groupId); - proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE; - - return textsecure.storage.groups.getNumbers(groupId).then(function(numbers) { - if (numbers === undefined) - return Promise.reject(new Error("Unknown Group")); - proto.group.members = numbers; - - return this.makeAttachmentPointer(avatar).then(function(attachment) { - proto.group.avatar = attachment; - return this.sendGroupProto(numbers, proto); - }.bind(this)); - }.bind(this)); - }, - - leaveGroup: function(groupId) { - var proto = new textsecure.protobuf.DataMessage(); - proto.group = new textsecure.protobuf.GroupContext(); - proto.group.id = stringToArrayBuffer(groupId); - proto.group.type = textsecure.protobuf.GroupContext.Type.QUIT; - - return textsecure.storage.groups.getNumbers(groupId).then(function(numbers) { - if (numbers === undefined) - return Promise.reject(new Error("Unknown Group")); - return textsecure.storage.groups.deleteGroup(groupId).then(function() { - return this.sendGroupProto(numbers, proto); - }.bind(this)); - }); - }, - sendExpirationTimerUpdateToGroup: function(groupId, expireTimer, timestamp, profileKey) { - return textsecure.storage.groups.getNumbers(groupId).then(function(numbers) { - if (numbers === undefined) - return Promise.reject(new Error("Unknown Group")); - - var me = textsecure.storage.user.getNumber(); - numbers = numbers.filter(function(number) { return number != me; }); - if (numbers.length === 0) { - return Promise.reject(new Error('No other members in the group')); - } - return this.sendMessage({ - recipients : numbers, - timestamp : timestamp, - needsSync : true, - expireTimer : expireTimer, - profileKey : profileKey, - flags : textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE, - group: { - id: groupId, - type: textsecure.protobuf.GroupContext.Type.DELIVER - } - }); - }.bind(this)); - }, - sendExpirationTimerUpdateToNumber: function(number, expireTimer, timestamp, profileKey) { - var proto = new textsecure.protobuf.DataMessage(); - return this.sendMessage({ - recipients : [number], - timestamp : timestamp, - needsSync : true, - expireTimer : expireTimer, - profileKey : profileKey, - flags : textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE - }); + sendGroupProto: function(numbers, proto, timestamp) { + timestamp = timestamp || Date.now(); + var me = textsecure.storage.user.getNumber(); + numbers = numbers.filter(function(number) { + return number != me; + }); + if (numbers.length === 0) { + return Promise.reject(new Error('No other members in the group')); } + + return new Promise( + function(resolve, reject) { + var silent = true; + var callback = function(res) { + res.dataMessage = proto.toArrayBuffer(); + if (res.errors.length > 0) { + reject(res); + } else { + resolve(res); + } + }.bind(this); + + this.sendMessageProto(timestamp, numbers, proto, callback, silent); + }.bind(this) + ); + }, + + sendMessageToNumber: function( + number, + messageText, + attachments, + quote, + timestamp, + expireTimer, + profileKey + ) { + return this.sendMessage({ + recipients: [number], + body: messageText, + timestamp: timestamp, + attachments: attachments, + quote: quote, + needsSync: true, + expireTimer: expireTimer, + profileKey: profileKey, + }); + }, + + resetSession: function(number, timestamp) { + console.log('resetting secure session'); + var proto = new textsecure.protobuf.DataMessage(); + proto.body = 'TERMINATE'; + proto.flags = textsecure.protobuf.DataMessage.Flags.END_SESSION; + + var logError = function(prefix) { + return function(error) { + console.log(prefix, error && error.stack ? error.stack : error); + throw error; + }; + }; + var deleteAllSessions = function(number) { + return textsecure.storage.protocol + .getDeviceIds(number) + .then(function(deviceIds) { + return Promise.all( + deviceIds.map(function(deviceId) { + var address = new libsignal.SignalProtocolAddress( + number, + deviceId + ); + console.log('deleting sessions for', address.toString()); + var sessionCipher = new libsignal.SessionCipher( + textsecure.storage.protocol, + address + ); + return sessionCipher.deleteAllSessionsForDevice(); + }) + ); + }); + }; + + var sendToContact = deleteAllSessions(number) + .catch(logError('resetSession/deleteAllSessions1 error:')) + .then( + function() { + console.log( + 'finished closing local sessions, now sending to contact' + ); + return this.sendIndividualProto(number, proto, timestamp).catch( + logError('resetSession/sendToContact error:') + ); + }.bind(this) + ) + .then(function() { + return deleteAllSessions(number).catch( + logError('resetSession/deleteAllSessions2 error:') + ); + }); + + var buffer = proto.toArrayBuffer(); + var sendSync = this.sendSyncMessage(buffer, timestamp, number).catch( + logError('resetSession/sendSync error:') + ); + + return Promise.all([sendToContact, sendSync]); + }, + + sendMessageToGroup: function( + groupId, + messageText, + attachments, + quote, + timestamp, + expireTimer, + profileKey + ) { + return textsecure.storage.groups.getNumbers(groupId).then( + function(numbers) { + if (numbers === undefined) + return Promise.reject(new Error('Unknown Group')); + + var me = textsecure.storage.user.getNumber(); + numbers = numbers.filter(function(number) { + return number != me; + }); + if (numbers.length === 0) { + return Promise.reject(new Error('No other members in the group')); + } + + return this.sendMessage({ + recipients: numbers, + body: messageText, + timestamp: timestamp, + attachments: attachments, + quote: quote, + needsSync: true, + expireTimer: expireTimer, + profileKey: profileKey, + group: { + id: groupId, + type: textsecure.protobuf.GroupContext.Type.DELIVER, + }, + }); + }.bind(this) + ); + }, + + createGroup: function(numbers, name, avatar) { + var proto = new textsecure.protobuf.DataMessage(); + proto.group = new textsecure.protobuf.GroupContext(); + + return textsecure.storage.groups.createNewGroup(numbers).then( + function(group) { + proto.group.id = stringToArrayBuffer(group.id); + var numbers = group.numbers; + + proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE; + proto.group.members = numbers; + proto.group.name = name; + + return this.makeAttachmentPointer(avatar).then( + function(attachment) { + proto.group.avatar = attachment; + return this.sendGroupProto(numbers, proto).then(function() { + return proto.group.id; + }); + }.bind(this) + ); + }.bind(this) + ); + }, + + updateGroup: function(groupId, name, avatar, numbers) { + var proto = new textsecure.protobuf.DataMessage(); + proto.group = new textsecure.protobuf.GroupContext(); + + proto.group.id = stringToArrayBuffer(groupId); + proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE; + proto.group.name = name; + + return textsecure.storage.groups.addNumbers(groupId, numbers).then( + function(numbers) { + if (numbers === undefined) { + return Promise.reject(new Error('Unknown Group')); + } + proto.group.members = numbers; + + return this.makeAttachmentPointer(avatar).then( + function(attachment) { + proto.group.avatar = attachment; + return this.sendGroupProto(numbers, proto).then(function() { + return proto.group.id; + }); + }.bind(this) + ); + }.bind(this) + ); + }, + + addNumberToGroup: function(groupId, number) { + var proto = new textsecure.protobuf.DataMessage(); + proto.group = new textsecure.protobuf.GroupContext(); + proto.group.id = stringToArrayBuffer(groupId); + proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE; + + return textsecure.storage.groups.addNumbers(groupId, [number]).then( + function(numbers) { + if (numbers === undefined) + return Promise.reject(new Error('Unknown Group')); + proto.group.members = numbers; + + return this.sendGroupProto(numbers, proto); + }.bind(this) + ); + }, + + setGroupName: function(groupId, name) { + var proto = new textsecure.protobuf.DataMessage(); + proto.group = new textsecure.protobuf.GroupContext(); + proto.group.id = stringToArrayBuffer(groupId); + proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE; + proto.group.name = name; + + return textsecure.storage.groups.getNumbers(groupId).then( + function(numbers) { + if (numbers === undefined) + return Promise.reject(new Error('Unknown Group')); + proto.group.members = numbers; + + return this.sendGroupProto(numbers, proto); + }.bind(this) + ); + }, + + setGroupAvatar: function(groupId, avatar) { + var proto = new textsecure.protobuf.DataMessage(); + proto.group = new textsecure.protobuf.GroupContext(); + proto.group.id = stringToArrayBuffer(groupId); + proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE; + + return textsecure.storage.groups.getNumbers(groupId).then( + function(numbers) { + if (numbers === undefined) + return Promise.reject(new Error('Unknown Group')); + proto.group.members = numbers; + + return this.makeAttachmentPointer(avatar).then( + function(attachment) { + proto.group.avatar = attachment; + return this.sendGroupProto(numbers, proto); + }.bind(this) + ); + }.bind(this) + ); + }, + + leaveGroup: function(groupId) { + var proto = new textsecure.protobuf.DataMessage(); + proto.group = new textsecure.protobuf.GroupContext(); + proto.group.id = stringToArrayBuffer(groupId); + proto.group.type = textsecure.protobuf.GroupContext.Type.QUIT; + + return textsecure.storage.groups + .getNumbers(groupId) + .then(function(numbers) { + if (numbers === undefined) + return Promise.reject(new Error('Unknown Group')); + return textsecure.storage.groups.deleteGroup(groupId).then( + function() { + return this.sendGroupProto(numbers, proto); + }.bind(this) + ); + }); + }, + sendExpirationTimerUpdateToGroup: function( + groupId, + expireTimer, + timestamp, + profileKey + ) { + return textsecure.storage.groups.getNumbers(groupId).then( + function(numbers) { + if (numbers === undefined) + return Promise.reject(new Error('Unknown Group')); + + var me = textsecure.storage.user.getNumber(); + numbers = numbers.filter(function(number) { + return number != me; + }); + if (numbers.length === 0) { + return Promise.reject(new Error('No other members in the group')); + } + return this.sendMessage({ + recipients: numbers, + timestamp: timestamp, + needsSync: true, + expireTimer: expireTimer, + profileKey: profileKey, + flags: textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE, + group: { + id: groupId, + type: textsecure.protobuf.GroupContext.Type.DELIVER, + }, + }); + }.bind(this) + ); + }, + sendExpirationTimerUpdateToNumber: function( + number, + expireTimer, + timestamp, + profileKey + ) { + var proto = new textsecure.protobuf.DataMessage(); + return this.sendMessage({ + recipients: [number], + timestamp: timestamp, + needsSync: true, + expireTimer: expireTimer, + profileKey: profileKey, + flags: textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE, + }); + }, }; window.textsecure = window.textsecure || {}; textsecure.MessageSender = function(url, username, password, cdn_url) { - var sender = new MessageSender(url, username, password, cdn_url); - textsecure.replay.registerFunction(sender.tryMessageAgain.bind(sender), textsecure.replay.Type.ENCRYPT_MESSAGE); - textsecure.replay.registerFunction(sender.retransmitMessage.bind(sender), textsecure.replay.Type.TRANSMIT_MESSAGE); - textsecure.replay.registerFunction(sender.sendMessage.bind(sender), textsecure.replay.Type.REBUILD_MESSAGE); - textsecure.replay.registerFunction(sender.retrySendMessageProto.bind(sender), textsecure.replay.Type.RETRY_SEND_MESSAGE_PROTO); + var sender = new MessageSender(url, username, password, cdn_url); + textsecure.replay.registerFunction( + sender.tryMessageAgain.bind(sender), + textsecure.replay.Type.ENCRYPT_MESSAGE + ); + textsecure.replay.registerFunction( + sender.retransmitMessage.bind(sender), + textsecure.replay.Type.TRANSMIT_MESSAGE + ); + textsecure.replay.registerFunction( + sender.sendMessage.bind(sender), + textsecure.replay.Type.REBUILD_MESSAGE + ); + textsecure.replay.registerFunction( + sender.retrySendMessageProto.bind(sender), + textsecure.replay.Type.RETRY_SEND_MESSAGE_PROTO + ); - this.sendExpirationTimerUpdateToNumber = sender.sendExpirationTimerUpdateToNumber.bind(sender); - this.sendExpirationTimerUpdateToGroup = sender.sendExpirationTimerUpdateToGroup .bind(sender); - this.sendRequestGroupSyncMessage = sender.sendRequestGroupSyncMessage .bind(sender); - this.sendRequestContactSyncMessage = sender.sendRequestContactSyncMessage .bind(sender); - this.sendRequestConfigurationSyncMessage = sender.sendRequestConfigurationSyncMessage.bind(sender); - this.sendMessageToNumber = sender.sendMessageToNumber .bind(sender); - this.resetSession = sender.resetSession .bind(sender); - this.sendMessageToGroup = sender.sendMessageToGroup .bind(sender); - this.createGroup = sender.createGroup .bind(sender); - this.updateGroup = sender.updateGroup .bind(sender); - this.addNumberToGroup = sender.addNumberToGroup .bind(sender); - this.setGroupName = sender.setGroupName .bind(sender); - this.setGroupAvatar = sender.setGroupAvatar .bind(sender); - this.leaveGroup = sender.leaveGroup .bind(sender); - this.sendSyncMessage = sender.sendSyncMessage .bind(sender); - this.getProfile = sender.getProfile .bind(sender); - this.getAvatar = sender.getAvatar .bind(sender); - this.syncReadMessages = sender.syncReadMessages .bind(sender); - this.syncVerification = sender.syncVerification .bind(sender); - this.sendReadReceipts = sender.sendReadReceipts .bind(sender); + this.sendExpirationTimerUpdateToNumber = sender.sendExpirationTimerUpdateToNumber.bind( + sender + ); + this.sendExpirationTimerUpdateToGroup = sender.sendExpirationTimerUpdateToGroup.bind( + sender + ); + this.sendRequestGroupSyncMessage = sender.sendRequestGroupSyncMessage.bind( + sender + ); + this.sendRequestContactSyncMessage = sender.sendRequestContactSyncMessage.bind( + sender + ); + this.sendRequestConfigurationSyncMessage = sender.sendRequestConfigurationSyncMessage.bind( + sender + ); + this.sendMessageToNumber = sender.sendMessageToNumber.bind(sender); + this.resetSession = sender.resetSession.bind(sender); + this.sendMessageToGroup = sender.sendMessageToGroup.bind(sender); + this.createGroup = sender.createGroup.bind(sender); + this.updateGroup = sender.updateGroup.bind(sender); + this.addNumberToGroup = sender.addNumberToGroup.bind(sender); + this.setGroupName = sender.setGroupName.bind(sender); + this.setGroupAvatar = sender.setGroupAvatar.bind(sender); + this.leaveGroup = sender.leaveGroup.bind(sender); + this.sendSyncMessage = sender.sendSyncMessage.bind(sender); + this.getProfile = sender.getProfile.bind(sender); + this.getAvatar = sender.getAvatar.bind(sender); + this.syncReadMessages = sender.syncReadMessages.bind(sender); + this.syncVerification = sender.syncVerification.bind(sender); + this.sendReadReceipts = sender.sendReadReceipts.bind(sender); }; textsecure.MessageSender.prototype = { - constructor: textsecure.MessageSender + constructor: textsecure.MessageSender, }; diff --git a/libtextsecure/storage.js b/libtextsecure/storage.js index 17b952d69..b339e7251 100644 --- a/libtextsecure/storage.js +++ b/libtextsecure/storage.js @@ -1,46 +1,42 @@ 'use strict'; -;(function() { +(function() { + /************************************************ + *** Utilities to store data in local storage *** + ************************************************/ + window.textsecure = window.textsecure || {}; + window.textsecure.storage = window.textsecure.storage || {}; - /************************************************ - *** Utilities to store data in local storage *** - ************************************************/ - window.textsecure = window.textsecure || {}; - window.textsecure.storage = window.textsecure.storage || {}; + // Overrideable storage implementation + window.textsecure.storage.impl = window.textsecure.storage.impl || { + /***************************** + *** Base Storage Routines *** + *****************************/ + put: function(key, value) { + if (value === undefined) throw new Error('Tried to store undefined'); + localStorage.setItem('' + key, textsecure.utils.jsonThing(value)); + }, - // Overrideable storage implementation - window.textsecure.storage.impl = window.textsecure.storage.impl || { - /***************************** - *** Base Storage Routines *** - *****************************/ - put: function(key, value) { - if (value === undefined) - throw new Error("Tried to store undefined"); - localStorage.setItem("" + key, textsecure.utils.jsonThing(value)); - }, + get: function(key, defaultValue) { + var value = localStorage.getItem('' + key); + if (value === null) return defaultValue; + return JSON.parse(value); + }, - get: function(key, defaultValue) { - var value = localStorage.getItem("" + key); - if (value === null) - return defaultValue; - return JSON.parse(value); - }, + remove: function(key) { + localStorage.removeItem('' + key); + }, + }; - remove: function(key) { - localStorage.removeItem("" + key); - }, - }; + window.textsecure.storage.put = function(key, value) { + return textsecure.storage.impl.put(key, value); + }; - window.textsecure.storage.put = function(key, value) { - return textsecure.storage.impl.put(key, value); - }; + window.textsecure.storage.get = function(key, defaultValue) { + return textsecure.storage.impl.get(key, defaultValue); + }; - window.textsecure.storage.get = function(key, defaultValue) { - return textsecure.storage.impl.get(key, defaultValue); - }; - - window.textsecure.storage.remove = function(key) { - return textsecure.storage.impl.remove(key); - }; + window.textsecure.storage.remove = function(key) { + return textsecure.storage.impl.remove(key); + }; })(); - diff --git a/libtextsecure/storage/groups.js b/libtextsecure/storage/groups.js index 9c303d92d..f2d5a23fc 100644 --- a/libtextsecure/storage/groups.js +++ b/libtextsecure/storage/groups.js @@ -1,144 +1,164 @@ -;(function() { - 'use strict'; +(function() { + 'use strict'; - /********************* - *** Group Storage *** - *********************/ - window.textsecure = window.textsecure || {}; - window.textsecure.storage = window.textsecure.storage || {}; + /********************* + *** Group Storage *** + *********************/ + window.textsecure = window.textsecure || {}; + window.textsecure.storage = window.textsecure.storage || {}; - // create a random group id that we haven't seen before. - function generateNewGroupId() { - var groupId = getString(libsignal.crypto.getRandomBytes(16)); - return textsecure.storage.protocol.getGroup(groupId).then(function(group) { - if (group === undefined) { - return groupId; - } else { - console.warn('group id collision'); // probably a bad sign. - return generateNewGroupId(); - } - }); - } + // create a random group id that we haven't seen before. + function generateNewGroupId() { + var groupId = getString(libsignal.crypto.getRandomBytes(16)); + return textsecure.storage.protocol.getGroup(groupId).then(function(group) { + if (group === undefined) { + return groupId; + } else { + console.warn('group id collision'); // probably a bad sign. + return generateNewGroupId(); + } + }); + } - window.textsecure.storage.groups = { - createNewGroup: function(numbers, groupId) { - var groupId = groupId; - return new Promise(function(resolve) { - if (groupId !== undefined) { - resolve(textsecure.storage.protocol.getGroup(groupId).then(function(group) { - if (group !== undefined) { - throw new Error("Tried to recreate group"); - } - })); - } else { - resolve(generateNewGroupId().then(function(newGroupId) { - groupId = newGroupId; - })); - } - }).then(function() { - var me = textsecure.storage.user.getNumber(); - var haveMe = false; - var finalNumbers = []; - for (var i in numbers) { - var number = numbers[i]; - if (!textsecure.utils.isNumberSane(number)) - throw new Error("Invalid number in group"); - if (number == me) - haveMe = true; - if (finalNumbers.indexOf(number) < 0) - finalNumbers.push(number); - } - - if (!haveMe) - finalNumbers.push(me); - - var groupObject = {numbers: finalNumbers, numberRegistrationIds: {}}; - for (var i in finalNumbers) - groupObject.numberRegistrationIds[finalNumbers[i]] = {}; - - return textsecure.storage.protocol.putGroup(groupId, groupObject).then(function() { - return {id: groupId, numbers: finalNumbers}; - }); - }); - }, - - getNumbers: function(groupId) { - return textsecure.storage.protocol.getGroup(groupId).then(function(group) { - if (group === undefined) - return undefined; - - return group.numbers; - }); - }, - - removeNumber: function(groupId, number) { - return textsecure.storage.protocol.getGroup(groupId).then(function(group) { - if (group === undefined) - return undefined; - - var me = textsecure.storage.user.getNumber(); - if (number == me) - throw new Error("Cannot remove ourselves from a group, leave the group instead"); - - var i = group.numbers.indexOf(number); - if (i > -1) { - group.numbers.splice(i, 1); - delete group.numberRegistrationIds[number]; - return textsecure.storage.protocol.putGroup(groupId, group).then(function() { - return group.numbers; - }); - } - - return group.numbers; - }); - }, - - addNumbers: function(groupId, numbers) { - return textsecure.storage.protocol.getGroup(groupId).then(function(group) { - if (group === undefined) - return undefined; - - for (var i in numbers) { - var number = numbers[i]; - if (!textsecure.utils.isNumberSane(number)) - throw new Error("Invalid number in set to add to group"); - if (group.numbers.indexOf(number) < 0) { - group.numbers.push(number); - group.numberRegistrationIds[number] = {}; - } - } - - return textsecure.storage.protocol.putGroup(groupId, group).then(function() { - return group.numbers; - }); - }); - }, - - deleteGroup: function(groupId) { - return textsecure.storage.protocol.removeGroup(groupId); - }, - - getGroup: function(groupId) { - return textsecure.storage.protocol.getGroup(groupId).then(function(group) { - if (group === undefined) - return undefined; - - return { id: groupId, numbers: group.numbers }; - }); - }, - - updateNumbers: function(groupId, numbers) { - return textsecure.storage.protocol.getGroup(groupId).then(function(group) { - if (group === undefined) - throw new Error("Tried to update numbers for unknown group"); - - if (numbers.filter(textsecure.utils.isNumberSane).length < numbers.length) - throw new Error("Invalid number in new group members"); - - var added = numbers.filter(function(number) { return group.numbers.indexOf(number) < 0; }); - - return textsecure.storage.groups.addNumbers(groupId, added); - }); + window.textsecure.storage.groups = { + createNewGroup: function(numbers, groupId) { + var groupId = groupId; + return new Promise(function(resolve) { + if (groupId !== undefined) { + resolve( + textsecure.storage.protocol.getGroup(groupId).then(function(group) { + if (group !== undefined) { + throw new Error('Tried to recreate group'); + } + }) + ); + } else { + resolve( + generateNewGroupId().then(function(newGroupId) { + groupId = newGroupId; + }) + ); } - }; + }).then(function() { + var me = textsecure.storage.user.getNumber(); + var haveMe = false; + var finalNumbers = []; + for (var i in numbers) { + var number = numbers[i]; + if (!textsecure.utils.isNumberSane(number)) + throw new Error('Invalid number in group'); + if (number == me) haveMe = true; + if (finalNumbers.indexOf(number) < 0) finalNumbers.push(number); + } + + if (!haveMe) finalNumbers.push(me); + + var groupObject = { numbers: finalNumbers, numberRegistrationIds: {} }; + for (var i in finalNumbers) + groupObject.numberRegistrationIds[finalNumbers[i]] = {}; + + return textsecure.storage.protocol + .putGroup(groupId, groupObject) + .then(function() { + return { id: groupId, numbers: finalNumbers }; + }); + }); + }, + + getNumbers: function(groupId) { + return textsecure.storage.protocol + .getGroup(groupId) + .then(function(group) { + if (group === undefined) return undefined; + + return group.numbers; + }); + }, + + removeNumber: function(groupId, number) { + return textsecure.storage.protocol + .getGroup(groupId) + .then(function(group) { + if (group === undefined) return undefined; + + var me = textsecure.storage.user.getNumber(); + if (number == me) + throw new Error( + 'Cannot remove ourselves from a group, leave the group instead' + ); + + var i = group.numbers.indexOf(number); + if (i > -1) { + group.numbers.splice(i, 1); + delete group.numberRegistrationIds[number]; + return textsecure.storage.protocol + .putGroup(groupId, group) + .then(function() { + return group.numbers; + }); + } + + return group.numbers; + }); + }, + + addNumbers: function(groupId, numbers) { + return textsecure.storage.protocol + .getGroup(groupId) + .then(function(group) { + if (group === undefined) return undefined; + + for (var i in numbers) { + var number = numbers[i]; + if (!textsecure.utils.isNumberSane(number)) + throw new Error('Invalid number in set to add to group'); + if (group.numbers.indexOf(number) < 0) { + group.numbers.push(number); + group.numberRegistrationIds[number] = {}; + } + } + + return textsecure.storage.protocol + .putGroup(groupId, group) + .then(function() { + return group.numbers; + }); + }); + }, + + deleteGroup: function(groupId) { + return textsecure.storage.protocol.removeGroup(groupId); + }, + + getGroup: function(groupId) { + return textsecure.storage.protocol + .getGroup(groupId) + .then(function(group) { + if (group === undefined) return undefined; + + return { id: groupId, numbers: group.numbers }; + }); + }, + + updateNumbers: function(groupId, numbers) { + return textsecure.storage.protocol + .getGroup(groupId) + .then(function(group) { + if (group === undefined) + throw new Error('Tried to update numbers for unknown group'); + + if ( + numbers.filter(textsecure.utils.isNumberSane).length < + numbers.length + ) + throw new Error('Invalid number in new group members'); + + var added = numbers.filter(function(number) { + return group.numbers.indexOf(number) < 0; + }); + + return textsecure.storage.groups.addNumbers(groupId, added); + }); + }, + }; })(); diff --git a/libtextsecure/storage/unprocessed.js b/libtextsecure/storage/unprocessed.js index 717709050..16dd6a6dc 100644 --- a/libtextsecure/storage/unprocessed.js +++ b/libtextsecure/storage/unprocessed.js @@ -1,24 +1,24 @@ -;(function() { - 'use strict'; +(function() { + 'use strict'; - /***************************************** - *** Not-yet-processed message storage *** - *****************************************/ - window.textsecure = window.textsecure || {}; - window.textsecure.storage = window.textsecure.storage || {}; + /***************************************** + *** Not-yet-processed message storage *** + *****************************************/ + window.textsecure = window.textsecure || {}; + window.textsecure.storage = window.textsecure.storage || {}; - window.textsecure.storage.unprocessed = { - getAll: function() { - return textsecure.storage.protocol.getAllUnprocessed(); - }, - add: function(data) { - return textsecure.storage.protocol.addUnprocessed(data); - }, - update: function(id, updates) { - return textsecure.storage.protocol.updateUnprocessed(id, updates); - }, - remove: function(id) { - return textsecure.storage.protocol.removeUnprocessed(id); - }, - }; + window.textsecure.storage.unprocessed = { + getAll: function() { + return textsecure.storage.protocol.getAllUnprocessed(); + }, + add: function(data) { + return textsecure.storage.protocol.addUnprocessed(data); + }, + update: function(id, updates) { + return textsecure.storage.protocol.updateUnprocessed(id, updates); + }, + remove: function(id) { + return textsecure.storage.protocol.removeUnprocessed(id); + }, + }; })(); diff --git a/libtextsecure/storage/user.js b/libtextsecure/storage/user.js index 3eab84f01..bb8398a95 100644 --- a/libtextsecure/storage/user.js +++ b/libtextsecure/storage/user.js @@ -1,36 +1,34 @@ 'use strict'; -;(function() { - /********************************************* - *** Utilities to store data about the user *** - **********************************************/ - window.textsecure = window.textsecure || {}; - window.textsecure.storage = window.textsecure.storage || {}; +(function() { + /********************************************* + *** Utilities to store data about the user *** + **********************************************/ + window.textsecure = window.textsecure || {}; + window.textsecure.storage = window.textsecure.storage || {}; - window.textsecure.storage.user = { - setNumberAndDeviceId: function(number, deviceId, deviceName) { - textsecure.storage.put("number_id", number + "." + deviceId); - if (deviceName) { - textsecure.storage.put("device_name", deviceName); - } - }, + window.textsecure.storage.user = { + setNumberAndDeviceId: function(number, deviceId, deviceName) { + textsecure.storage.put('number_id', number + '.' + deviceId); + if (deviceName) { + textsecure.storage.put('device_name', deviceName); + } + }, - getNumber: function(key, defaultValue) { - var number_id = textsecure.storage.get("number_id"); - if (number_id === undefined) - return undefined; - return textsecure.utils.unencodeNumber(number_id)[0]; - }, + getNumber: function(key, defaultValue) { + var number_id = textsecure.storage.get('number_id'); + if (number_id === undefined) return undefined; + return textsecure.utils.unencodeNumber(number_id)[0]; + }, - getDeviceId: function(key) { - var number_id = textsecure.storage.get("number_id"); - if (number_id === undefined) - return undefined; - return textsecure.utils.unencodeNumber(number_id)[1]; - }, + getDeviceId: function(key) { + var number_id = textsecure.storage.get('number_id'); + if (number_id === undefined) return undefined; + return textsecure.utils.unencodeNumber(number_id)[1]; + }, - getDeviceName: function(key) { - return textsecure.storage.get("device_name"); - } - }; + getDeviceName: function(key) { + return textsecure.storage.get('device_name'); + }, + }; })(); diff --git a/libtextsecure/stringview.js b/libtextsecure/stringview.js index 269d05c32..7d1e5e4b6 100644 --- a/libtextsecure/stringview.js +++ b/libtextsecure/stringview.js @@ -1,82 +1,93 @@ -;(function() { - "use strict"; +(function() { + 'use strict'; - window.StringView = { - - /* + window.StringView = { + /* * These functions from the Mozilla Developer Network * and have been placed in the public domain. * https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding * https://developer.mozilla.org/en-US/docs/MDN/About#Copyrights_and_licenses */ - b64ToUint6: function(nChr) { - return nChr > 64 && nChr < 91 ? - nChr - 65 - : nChr > 96 && nChr < 123 ? - nChr - 71 - : nChr > 47 && nChr < 58 ? - nChr + 4 - : nChr === 43 ? - 62 - : nChr === 47 ? - 63 - : - 0; - }, + b64ToUint6: function(nChr) { + return nChr > 64 && nChr < 91 + ? nChr - 65 + : nChr > 96 && nChr < 123 + ? nChr - 71 + : nChr > 47 && nChr < 58 + ? nChr + 4 + : nChr === 43 + ? 62 + : nChr === 47 + ? 63 + : 0; + }, - base64ToBytes: function(sBase64, nBlocksSize) { - var - sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ""), nInLen = sB64Enc.length, - nOutLen = nBlocksSize ? Math.ceil((nInLen * 3 + 1 >> 2) / nBlocksSize) * nBlocksSize : nInLen * 3 + 1 >> 2; - var aBBytes = new ArrayBuffer(nOutLen); - var taBytes = new Uint8Array(aBBytes); + base64ToBytes: function(sBase64, nBlocksSize) { + var sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ''), + nInLen = sB64Enc.length, + nOutLen = nBlocksSize + ? Math.ceil(((nInLen * 3 + 1) >> 2) / nBlocksSize) * nBlocksSize + : (nInLen * 3 + 1) >> 2; + var aBBytes = new ArrayBuffer(nOutLen); + var taBytes = new Uint8Array(aBBytes); - for (var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) { - nMod4 = nInIdx & 3; - nUint24 |= StringView.b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << 18 - 6 * nMod4; - if (nMod4 === 3 || nInLen - nInIdx === 1) { - for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) { - taBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255; - } - nUint24 = 0; + for ( + var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; + nInIdx < nInLen; + nInIdx++ + ) { + nMod4 = nInIdx & 3; + nUint24 |= + StringView.b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << (18 - 6 * nMod4); + if (nMod4 === 3 || nInLen - nInIdx === 1) { + for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) { + taBytes[nOutIdx] = (nUint24 >>> ((16 >>> nMod3) & 24)) & 255; } + nUint24 = 0; } - return aBBytes; - }, - - uint6ToB64: function(nUint6) { - return nUint6 < 26 ? - nUint6 + 65 - : nUint6 < 52 ? - nUint6 + 71 - : nUint6 < 62 ? - nUint6 - 4 - : nUint6 === 62 ? - 43 - : nUint6 === 63 ? - 47 - : - 65; - }, - - bytesToBase64: function(aBytes) { - var nMod3, sB64Enc = ""; - for (var nLen = aBytes.length, nUint24 = 0, nIdx = 0; nIdx < nLen; nIdx++) { - nMod3 = nIdx % 3; - if (nIdx > 0 && (nIdx * 4 / 3) % 76 === 0) { sB64Enc += "\r\n"; } - nUint24 |= aBytes[nIdx] << (16 >>> nMod3 & 24); - if (nMod3 === 2 || aBytes.length - nIdx === 1) { - sB64Enc += String.fromCharCode( - StringView.uint6ToB64(nUint24 >>> 18 & 63), - StringView.uint6ToB64(nUint24 >>> 12 & 63), - StringView.uint6ToB64(nUint24 >>> 6 & 63), - StringView.uint6ToB64(nUint24 & 63) - ); - nUint24 = 0; - } - } - return sB64Enc.replace(/A(?=A$|$)/g, "="); } - }; -}()); + return aBBytes; + }, + + uint6ToB64: function(nUint6) { + return nUint6 < 26 + ? nUint6 + 65 + : nUint6 < 52 + ? nUint6 + 71 + : nUint6 < 62 + ? nUint6 - 4 + : nUint6 === 62 + ? 43 + : nUint6 === 63 + ? 47 + : 65; + }, + + bytesToBase64: function(aBytes) { + var nMod3, + sB64Enc = ''; + for ( + var nLen = aBytes.length, nUint24 = 0, nIdx = 0; + nIdx < nLen; + nIdx++ + ) { + nMod3 = nIdx % 3; + if (nIdx > 0 && (nIdx * 4 / 3) % 76 === 0) { + sB64Enc += '\r\n'; + } + nUint24 |= aBytes[nIdx] << ((16 >>> nMod3) & 24); + if (nMod3 === 2 || aBytes.length - nIdx === 1) { + sB64Enc += String.fromCharCode( + StringView.uint6ToB64((nUint24 >>> 18) & 63), + StringView.uint6ToB64((nUint24 >>> 12) & 63), + StringView.uint6ToB64((nUint24 >>> 6) & 63), + StringView.uint6ToB64(nUint24 & 63) + ); + nUint24 = 0; + } + } + return sB64Enc.replace(/A(?=A$|$)/g, '='); + }, + }; +})(); diff --git a/libtextsecure/sync_request.js b/libtextsecure/sync_request.js index c438b7657..5e5bacabf 100644 --- a/libtextsecure/sync_request.js +++ b/libtextsecure/sync_request.js @@ -1,74 +1,82 @@ -;(function () { - 'use strict'; - window.textsecure = window.textsecure || {}; +(function() { + 'use strict'; + window.textsecure = window.textsecure || {}; - function SyncRequest(sender, receiver) { - if (!(sender instanceof textsecure.MessageSender) || !(receiver instanceof textsecure.MessageReceiver)) { - throw new Error('Tried to construct a SyncRequest without MessageSender and MessageReceiver'); - } - this.receiver = receiver; - - this.oncontact = this.onContactSyncComplete.bind(this); - receiver.addEventListener('contactsync', this.oncontact); - - this.ongroup = this.onGroupSyncComplete.bind(this); - receiver.addEventListener('groupsync', this.ongroup); - - console.log('SyncRequest created. Sending contact sync message...'); - sender.sendRequestContactSyncMessage().then(function() { - console.log('SyncRequest now sending group sync messsage...'); - return sender.sendRequestGroupSyncMessage(); - }).catch(function(error) { - console.log( - 'SyncRequest error:', - error && error.stack ? error.stack : error - ); - }); - this.timeout = setTimeout(this.onTimeout.bind(this), 60000); + function SyncRequest(sender, receiver) { + if ( + !(sender instanceof textsecure.MessageSender) || + !(receiver instanceof textsecure.MessageReceiver) + ) { + throw new Error( + 'Tried to construct a SyncRequest without MessageSender and MessageReceiver' + ); } + this.receiver = receiver; - SyncRequest.prototype = new textsecure.EventTarget(); - SyncRequest.prototype.extend({ - constructor: SyncRequest, - onContactSyncComplete: function() { - this.contactSync = true; - this.update(); - }, - onGroupSyncComplete: function() { - this.groupSync = true; - this.update(); - }, - update: function() { - if (this.contactSync && this.groupSync) { - this.dispatchEvent(new Event('success')); - this.cleanup(); - } - }, - onTimeout: function() { - if (this.contactSync || this.groupSync) { - this.dispatchEvent(new Event('success')); - } else { - this.dispatchEvent(new Event('timeout')); - } - this.cleanup(); - }, - cleanup: function() { - clearTimeout(this.timeout); - this.receiver.removeEventListener('contactsync', this.oncontact); - this.receiver.removeEventListener('groupSync', this.ongroup); - delete this.listeners; - } - }); + this.oncontact = this.onContactSyncComplete.bind(this); + receiver.addEventListener('contactsync', this.oncontact); - textsecure.SyncRequest = function(sender, receiver) { - var syncRequest = new SyncRequest(sender, receiver); - this.addEventListener = syncRequest.addEventListener.bind(syncRequest); - this.removeEventListener = syncRequest.removeEventListener.bind(syncRequest); - }; + this.ongroup = this.onGroupSyncComplete.bind(this); + receiver.addEventListener('groupsync', this.ongroup); - textsecure.SyncRequest.prototype = { - constructor: textsecure.SyncRequest - }; + console.log('SyncRequest created. Sending contact sync message...'); + sender + .sendRequestContactSyncMessage() + .then(function() { + console.log('SyncRequest now sending group sync messsage...'); + return sender.sendRequestGroupSyncMessage(); + }) + .catch(function(error) { + console.log( + 'SyncRequest error:', + error && error.stack ? error.stack : error + ); + }); + this.timeout = setTimeout(this.onTimeout.bind(this), 60000); + } + SyncRequest.prototype = new textsecure.EventTarget(); + SyncRequest.prototype.extend({ + constructor: SyncRequest, + onContactSyncComplete: function() { + this.contactSync = true; + this.update(); + }, + onGroupSyncComplete: function() { + this.groupSync = true; + this.update(); + }, + update: function() { + if (this.contactSync && this.groupSync) { + this.dispatchEvent(new Event('success')); + this.cleanup(); + } + }, + onTimeout: function() { + if (this.contactSync || this.groupSync) { + this.dispatchEvent(new Event('success')); + } else { + this.dispatchEvent(new Event('timeout')); + } + this.cleanup(); + }, + cleanup: function() { + clearTimeout(this.timeout); + this.receiver.removeEventListener('contactsync', this.oncontact); + this.receiver.removeEventListener('groupSync', this.ongroup); + delete this.listeners; + }, + }); -}()); + textsecure.SyncRequest = function(sender, receiver) { + var syncRequest = new SyncRequest(sender, receiver); + this.addEventListener = syncRequest.addEventListener.bind(syncRequest); + this.removeEventListener = syncRequest.removeEventListener.bind( + syncRequest + ); + }; + + textsecure.SyncRequest.prototype = { + constructor: textsecure.SyncRequest, + }; +})(); diff --git a/libtextsecure/task_with_timeout.js b/libtextsecure/task_with_timeout.js index fe05b0131..20672375e 100644 --- a/libtextsecure/task_with_timeout.js +++ b/libtextsecure/task_with_timeout.js @@ -1,68 +1,70 @@ -(function () { - window.textsecure = window.textsecure || {}; +(function() { + window.textsecure = window.textsecure || {}; - window.textsecure.createTaskWithTimeout = function(task, id, options) { - options = options || {}; - options.timeout = options.timeout || (1000 * 60 * 2); // two minutes + window.textsecure.createTaskWithTimeout = function(task, id, options) { + options = options || {}; + options.timeout = options.timeout || 1000 * 60 * 2; // two minutes - var errorForStack = new Error('for stack'); - return function() { - return new Promise(function(resolve, reject) { - var complete = false; - var timer = setTimeout(function() { - if (!complete) { - var message = - (id || '') - + ' task did not complete in time. Calling stack: ' - + errorForStack.stack; + var errorForStack = new Error('for stack'); + return function() { + return new Promise(function(resolve, reject) { + var complete = false; + var timer = setTimeout( + function() { + if (!complete) { + var message = + (id || '') + + ' task did not complete in time. Calling stack: ' + + errorForStack.stack; - console.log(message); - return reject(new Error(message)); - } - }.bind(this), options.timeout); - var clearTimer = function() { - try { - var localTimer = timer; - if (localTimer) { - timer = null; - clearTimeout(localTimer); - } - } - catch (error) { - console.log( - id || '', - 'task ran into problem canceling timer. Calling stack:', - errorForStack.stack - ); - } - }; - - var success = function(result) { - clearTimer(); - complete = true; - return resolve(result); - }; - var failure = function(error) { - clearTimer(); - complete = true; - return reject(error); - }; - - var promise; - try { - promise = task(); - } catch(error) { - clearTimer(); - throw error; - } - if (!promise || !promise.then) { - clearTimer(); - complete = true; - return resolve(promise); - } - - return promise.then(success, failure); - }); + console.log(message); + return reject(new Error(message)); + } + }.bind(this), + options.timeout + ); + var clearTimer = function() { + try { + var localTimer = timer; + if (localTimer) { + timer = null; + clearTimeout(localTimer); + } + } catch (error) { + console.log( + id || '', + 'task ran into problem canceling timer. Calling stack:', + errorForStack.stack + ); + } }; + + var success = function(result) { + clearTimer(); + complete = true; + return resolve(result); + }; + var failure = function(error) { + clearTimer(); + complete = true; + return reject(error); + }; + + var promise; + try { + promise = task(); + } catch (error) { + clearTimer(); + throw error; + } + if (!promise || !promise.then) { + clearTimer(); + complete = true; + return resolve(promise); + } + + return promise.then(success, failure); + }); }; + }; })(); diff --git a/libtextsecure/test/_test.js b/libtextsecure/test/_test.js index dfbc95ee2..e0903b56d 100644 --- a/libtextsecure/test/_test.js +++ b/libtextsecure/test/_test.js @@ -1,4 +1,4 @@ -mocha.setup("bdd"); +mocha.setup('bdd'); window.assert = chai.assert; window.PROTO_ROOT = '../../protos'; @@ -27,7 +27,7 @@ window.PROTO_ROOT = '../../protos'; result: false, message: err.message, stack: err.stack, - titles: flattenTitles(test) + titles: flattenTitles(test), }); }); @@ -37,21 +37,21 @@ window.PROTO_ROOT = '../../protos'; SauceReporter.prototype = OriginalReporter.prototype; mocha.reporter(SauceReporter); -}()); +})(); /* * global helpers for tests */ function assertEqualArrayBuffers(ab1, ab2) { assert.deepEqual(new Uint8Array(ab1), new Uint8Array(ab2)); -}; +} function hexToArrayBuffer(str) { var ret = new ArrayBuffer(str.length / 2); var array = new Uint8Array(ret); - for (var i = 0; i < str.length/2; i++) - array[i] = parseInt(str.substr(i*2, 2), 16); + for (var i = 0; i < str.length / 2; i++) + 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 index 309ed52c9..1d25f4a97 100644 --- a/libtextsecure/test/account_manager_test.js +++ b/libtextsecure/test/account_manager_test.js @@ -1,6 +1,6 @@ 'use strict'; -describe("AccountManager", function() { +describe('AccountManager', function() { let accountManager; let originalServer; @@ -35,19 +35,23 @@ describe("AccountManager", function() { 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, - }]; + 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(); @@ -55,27 +59,33 @@ describe("AccountManager", function() { 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, - }]; + 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) { @@ -93,19 +103,24 @@ describe("AccountManager", function() { 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 - }]; + 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) { @@ -123,21 +138,26 @@ describe("AccountManager", function() { 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, - }]; + 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) { diff --git a/libtextsecure/test/contacts_parser_test.js b/libtextsecure/test/contacts_parser_test.js index d47ab94b5..158a93bab 100644 --- a/libtextsecure/test/contacts_parser_test.js +++ b/libtextsecure/test/contacts_parser_test.js @@ -1,19 +1,19 @@ 'use strict'; -describe("ContactBuffer", function() { +describe('ContactBuffer', function() { function getTestBuffer() { var buffer = new dcodeIO.ByteBuffer(); var avatarBuffer = new dcodeIO.ByteBuffer(); var avatarLen = 255; - for (var i=0; i < avatarLen; ++i) { + for (var i = 0; i < avatarLen; ++i) { avatarBuffer.writeUint8(i); } avatarBuffer.limit = avatarBuffer.offset; avatarBuffer.offset = 0; var contactInfo = new textsecure.protobuf.ContactDetails({ - name: "Zero Cool", - number: "+10000000000", - avatar: { contentType: "image/jpeg", length: avatarLen } + name: 'Zero Cool', + number: '+10000000000', + avatar: { contentType: 'image/jpeg', length: avatarLen }, }); var contactInfoBuffer = contactInfo.encode().toArrayBuffer(); @@ -28,21 +28,21 @@ describe("ContactBuffer", function() { return buffer.toArrayBuffer(); } - it("parses an array buffer of contacts", function() { + it('parses an array buffer of contacts', function() { var arrayBuffer = getTestBuffer(); var contactBuffer = new ContactBuffer(arrayBuffer); var contact = contactBuffer.next(); var count = 0; while (contact !== undefined) { count++; - assert.strictEqual(contact.name, "Zero Cool"); - assert.strictEqual(contact.number, "+10000000000"); - assert.strictEqual(contact.avatar.contentType, "image/jpeg"); + assert.strictEqual(contact.name, 'Zero Cool'); + assert.strictEqual(contact.number, '+10000000000'); + assert.strictEqual(contact.avatar.contentType, 'image/jpeg'); assert.strictEqual(contact.avatar.length, 255); assert.strictEqual(contact.avatar.data.byteLength, 255); var avatarBytes = new Uint8Array(contact.avatar.data); - for (var j=0; j < 255; ++j) { - assert.strictEqual(avatarBytes[j],j); + for (var j = 0; j < 255; ++j) { + assert.strictEqual(avatarBytes[j], j); } contact = contactBuffer.next(); } @@ -50,21 +50,21 @@ describe("ContactBuffer", function() { }); }); -describe("GroupBuffer", function() { +describe('GroupBuffer', function() { function getTestBuffer() { var buffer = new dcodeIO.ByteBuffer(); var avatarBuffer = new dcodeIO.ByteBuffer(); var avatarLen = 255; - for (var i=0; i < avatarLen; ++i) { + for (var i = 0; i < avatarLen; ++i) { avatarBuffer.writeUint8(i); } avatarBuffer.limit = avatarBuffer.offset; avatarBuffer.offset = 0; var groupInfo = new textsecure.protobuf.GroupDetails({ id: new Uint8Array([1, 3, 3, 7]).buffer, - name: "Hackers", + name: 'Hackers', members: ['cereal', 'burn', 'phreak', 'joey'], - avatar: { contentType: "image/jpeg", length: avatarLen } + avatar: { contentType: 'image/jpeg', length: avatarLen }, }); var groupInfoBuffer = groupInfo.encode().toArrayBuffer(); @@ -79,22 +79,25 @@ describe("GroupBuffer", function() { return buffer.toArrayBuffer(); } - it("parses an array buffer of groups", function() { + it('parses an array buffer of groups', function() { var arrayBuffer = getTestBuffer(); var groupBuffer = new GroupBuffer(arrayBuffer); var group = groupBuffer.next(); var count = 0; while (group !== undefined) { count++; - assert.strictEqual(group.name, "Hackers"); - assertEqualArrayBuffers(group.id.toArrayBuffer(), new Uint8Array([1,3,3,7]).buffer); + assert.strictEqual(group.name, 'Hackers'); + assertEqualArrayBuffers( + group.id.toArrayBuffer(), + new Uint8Array([1, 3, 3, 7]).buffer + ); assert.sameMembers(group.members, ['cereal', 'burn', 'phreak', 'joey']); - assert.strictEqual(group.avatar.contentType, "image/jpeg"); + assert.strictEqual(group.avatar.contentType, 'image/jpeg'); assert.strictEqual(group.avatar.length, 255); assert.strictEqual(group.avatar.data.byteLength, 255); var avatarBytes = new Uint8Array(group.avatar.data); - for (var j=0; j < 255; ++j) { - assert.strictEqual(avatarBytes[j],j); + for (var j = 0; j < 255; ++j) { + assert.strictEqual(avatarBytes[j], j); } group = groupBuffer.next(); } diff --git a/libtextsecure/test/crypto_test.js b/libtextsecure/test/crypto_test.js index eb8f1025e..1081a0c2c 100644 --- a/libtextsecure/test/crypto_test.js +++ b/libtextsecure/test/crypto_test.js @@ -6,24 +6,38 @@ describe('encrypting and decrypting profile data', function() { var buffer = dcodeIO.ByteBuffer.wrap(name).toArrayBuffer(); var key = libsignal.crypto.getRandomBytes(32); - return textsecure.crypto.encryptProfileName(buffer, key).then(function(encrypted) { - assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12); - return textsecure.crypto.decryptProfileName(encrypted, key).then(function(decrypted) { - assert.strictEqual(dcodeIO.ByteBuffer.wrap(decrypted).toString('utf8'), 'Alice'); + return textsecure.crypto + .encryptProfileName(buffer, key) + .then(function(encrypted) { + assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12); + return textsecure.crypto + .decryptProfileName(encrypted, key) + .then(function(decrypted) { + assert.strictEqual( + dcodeIO.ByteBuffer.wrap(decrypted).toString('utf8'), + 'Alice' + ); + }); }); - }); }); it('works for empty string', function() { var name = dcodeIO.ByteBuffer.wrap('').toArrayBuffer(); var key = libsignal.crypto.getRandomBytes(32); - return textsecure.crypto.encryptProfileName(name.buffer, key).then(function(encrypted) { - assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12); - return textsecure.crypto.decryptProfileName(encrypted, key).then(function(decrypted) { - assert.strictEqual(decrypted.byteLength, 0); - assert.strictEqual(dcodeIO.ByteBuffer.wrap(decrypted).toString('utf8'), ''); + return textsecure.crypto + .encryptProfileName(name.buffer, key) + .then(function(encrypted) { + assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12); + return textsecure.crypto + .decryptProfileName(encrypted, key) + .then(function(decrypted) { + assert.strictEqual(decrypted.byteLength, 0); + assert.strictEqual( + dcodeIO.ByteBuffer.wrap(decrypted).toString('utf8'), + '' + ); + }); }); - }); }); }); describe('encrypting and decrypting profile avatars', function() { @@ -31,24 +45,32 @@ describe('encrypting and decrypting profile data', function() { var buffer = dcodeIO.ByteBuffer.wrap('This is an avatar').toArrayBuffer(); var key = libsignal.crypto.getRandomBytes(32); - return textsecure.crypto.encryptProfile(buffer, key).then(function(encrypted) { - assert(encrypted.byteLength === buffer.byteLength + 16 + 12); - return textsecure.crypto.decryptProfile(encrypted, key).then(function(decrypted) { - assertEqualArrayBuffers(buffer, decrypted) + return textsecure.crypto + .encryptProfile(buffer, key) + .then(function(encrypted) { + assert(encrypted.byteLength === buffer.byteLength + 16 + 12); + return textsecure.crypto + .decryptProfile(encrypted, key) + .then(function(decrypted) { + assertEqualArrayBuffers(buffer, decrypted); + }); }); - }); }); it('throws when decrypting with the wrong key', function() { var buffer = dcodeIO.ByteBuffer.wrap('This is an avatar').toArrayBuffer(); var key = libsignal.crypto.getRandomBytes(32); var bad_key = libsignal.crypto.getRandomBytes(32); - return textsecure.crypto.encryptProfile(buffer, key).then(function(encrypted) { - assert(encrypted.byteLength === buffer.byteLength + 16 + 12); - return textsecure.crypto.decryptProfile(encrypted, bad_key).catch(function(error) { - assert.strictEqual(error.name, 'ProfileDecryptError'); + return textsecure.crypto + .encryptProfile(buffer, key) + .then(function(encrypted) { + assert(encrypted.byteLength === buffer.byteLength + 16 + 12); + return textsecure.crypto + .decryptProfile(encrypted, bad_key) + .catch(function(error) { + assert.strictEqual(error.name, 'ProfileDecryptError'); + }); }); - }); }); }); }); diff --git a/libtextsecure/test/fake_api.js b/libtextsecure/test/fake_api.js index 3f089e9b4..225f8839b 100644 --- a/libtextsecure/test/fake_api.js +++ b/libtextsecure/test/fake_api.js @@ -1,26 +1,29 @@ var getKeysForNumberMap = {}; TextSecureServer.getKeysForNumber = function(number, deviceId) { - var res = getKeysForNumberMap[number]; - if (res !== undefined) { - delete getKeysForNumberMap[number]; - return Promise.resolve(res); - } else - throw new Error("getKeysForNumber of unknown/used number"); + var res = getKeysForNumberMap[number]; + if (res !== undefined) { + delete getKeysForNumberMap[number]; + return Promise.resolve(res); + } else throw new Error('getKeysForNumber of unknown/used number'); }; var messagesSentMap = {}; TextSecureServer.sendMessages = function(destination, messageArray) { - for (i in messageArray) { - var msg = messageArray[i]; - if ((msg.type != 1 && msg.type != 3) || - msg.destinationDeviceId === undefined || - msg.destinationRegistrationId === undefined || - msg.body === undefined || - msg.timestamp == undefined || - msg.relay !== undefined || - msg.destination !== undefined) - throw new Error("Invalid message"); + for (i in messageArray) { + var msg = messageArray[i]; + if ( + (msg.type != 1 && msg.type != 3) || + msg.destinationDeviceId === undefined || + msg.destinationRegistrationId === undefined || + msg.body === undefined || + msg.timestamp == undefined || + msg.relay !== undefined || + msg.destination !== undefined + ) + throw new Error('Invalid message'); - messagesSentMap[destination + "." + messageArray[i].destinationDeviceId] = msg; - } + messagesSentMap[ + destination + '.' + messageArray[i].destinationDeviceId + ] = msg; + } }; diff --git a/libtextsecure/test/generate_keys_test.js b/libtextsecure/test/generate_keys_test.js index 4c985054b..759c8e80e 100644 --- a/libtextsecure/test/generate_keys_test.js +++ b/libtextsecure/test/generate_keys_test.js @@ -1,163 +1,190 @@ 'use strict'; -describe("Key generation", function() { - var count = 10; - this.timeout(count*2000); +describe('Key generation', function() { + var count = 10; + this.timeout(count * 2000); - function validateStoredKeyPair(keyPair) { - /* Ensure the keypair matches the format used internally by libsignal-protocol */ - assert.isObject(keyPair, 'Stored keyPair is not an object'); - assert.instanceOf(keyPair.pubKey, ArrayBuffer); - assert.instanceOf(keyPair.privKey, ArrayBuffer); - assert.strictEqual(keyPair.pubKey.byteLength, 33); - assert.strictEqual(new Uint8Array(keyPair.pubKey)[0], 5); - assert.strictEqual(keyPair.privKey.byteLength, 32); - } - function itStoresPreKey(keyId) { - it('prekey ' + keyId + ' is valid', function(done) { - return textsecure.storage.protocol.loadPreKey(keyId).then(function(keyPair) { - validateStoredKeyPair(keyPair); - }).then(done,done); - }); - } - function itStoresSignedPreKey(keyId) { - it('signed prekey ' + keyId + ' is valid', function(done) { - return textsecure.storage.protocol.loadSignedPreKey(keyId).then(function(keyPair) { - validateStoredKeyPair(keyPair); - }).then(done,done); - }); - } - function validateResultKey(resultKey) { - return textsecure.storage.protocol.loadPreKey(resultKey.keyId).then(function(keyPair) { - assertEqualArrayBuffers(resultKey.publicKey, keyPair.pubKey); - }); - } - function validateResultSignedKey(resultSignedKey) { - return textsecure.storage.protocol.loadSignedPreKey(resultSignedKey.keyId).then(function(keyPair) { - assertEqualArrayBuffers(resultSignedKey.publicKey, keyPair.pubKey); - }); - } - - before(function(done) { - localStorage.clear(); - libsignal.KeyHelper.generateIdentityKeyPair().then(function(keyPair) { - return textsecure.storage.protocol.put('identityKey', keyPair); - }).then(done, done); + function validateStoredKeyPair(keyPair) { + /* Ensure the keypair matches the format used internally by libsignal-protocol */ + assert.isObject(keyPair, 'Stored keyPair is not an object'); + assert.instanceOf(keyPair.pubKey, ArrayBuffer); + assert.instanceOf(keyPair.privKey, ArrayBuffer); + assert.strictEqual(keyPair.pubKey.byteLength, 33); + assert.strictEqual(new Uint8Array(keyPair.pubKey)[0], 5); + assert.strictEqual(keyPair.privKey.byteLength, 32); + } + function itStoresPreKey(keyId) { + it('prekey ' + keyId + ' is valid', function(done) { + return textsecure.storage.protocol + .loadPreKey(keyId) + .then(function(keyPair) { + validateStoredKeyPair(keyPair); + }) + .then(done, done); }); + } + function itStoresSignedPreKey(keyId) { + it('signed prekey ' + keyId + ' is valid', function(done) { + return textsecure.storage.protocol + .loadSignedPreKey(keyId) + .then(function(keyPair) { + validateStoredKeyPair(keyPair); + }) + .then(done, done); + }); + } + function validateResultKey(resultKey) { + return textsecure.storage.protocol + .loadPreKey(resultKey.keyId) + .then(function(keyPair) { + assertEqualArrayBuffers(resultKey.publicKey, keyPair.pubKey); + }); + } + function validateResultSignedKey(resultSignedKey) { + return textsecure.storage.protocol + .loadSignedPreKey(resultSignedKey.keyId) + .then(function(keyPair) { + assertEqualArrayBuffers(resultSignedKey.publicKey, keyPair.pubKey); + }); + } - describe('the first time', function() { - var result; - /* result should have this format + before(function(done) { + localStorage.clear(); + libsignal.KeyHelper.generateIdentityKeyPair() + .then(function(keyPair) { + return textsecure.storage.protocol.put('identityKey', keyPair); + }) + .then(done, done); + }); + + describe('the first time', function() { + var result; + /* result should have this format * { * preKeys: [ { keyId, publicKey }, ... ], * signedPreKey: { keyId, publicKey, signature }, * identityKey: * } */ - before(function(done) { - var accountManager = new textsecure.AccountManager(''); - accountManager.generateKeys(count).then(function(res) { - result = res; - }).then(done,done); - }); - for (var i = 1; i <= count; i++) { - itStoresPreKey(i); - } - itStoresSignedPreKey(1); + before(function(done) { + var accountManager = new textsecure.AccountManager(''); + accountManager + .generateKeys(count) + .then(function(res) { + result = res; + }) + .then(done, done); + }); + for (var i = 1; i <= count; i++) { + itStoresPreKey(i); + } + itStoresSignedPreKey(1); - it('result contains ' + count + ' preKeys', function() { - assert.isArray(result.preKeys); - assert.lengthOf(result.preKeys, count); - for (var i = 0; i < count; i++) { - assert.isObject(result.preKeys[i]); - } - }); - it('result contains the correct keyIds', function() { - for (var i = 0; i < count; i++) { - assert.strictEqual(result.preKeys[i].keyId, i+1); - } - }); - it('result contains the correct public keys', function(done) { - Promise.all(result.preKeys.map(validateResultKey)).then(function() { - done(); - }).catch(done); - }); - it('returns a signed prekey', function(done) { - assert.strictEqual(result.signedPreKey.keyId, 1); - assert.instanceOf(result.signedPreKey.signature, ArrayBuffer); - validateResultSignedKey(result.signedPreKey).then(done,done); - }); + it('result contains ' + count + ' preKeys', function() { + assert.isArray(result.preKeys); + assert.lengthOf(result.preKeys, count); + for (var i = 0; i < count; i++) { + assert.isObject(result.preKeys[i]); + } }); - describe('the second time', function() { - var result; - before(function(done) { - var accountManager = new textsecure.AccountManager(''); - accountManager.generateKeys(count).then(function(res) { - result = res; - }).then(done,done); - }); - for (var i = 1; i <= 2*count; i++) { - itStoresPreKey(i); - } - itStoresSignedPreKey(1); - itStoresSignedPreKey(2); - it('result contains ' + count + ' preKeys', function() { - assert.isArray(result.preKeys); - assert.lengthOf(result.preKeys, count); - for (var i = 0; i < count; i++) { - assert.isObject(result.preKeys[i]); - } - }); - it('result contains the correct keyIds', function() { - for (var i = 1; i <= count; i++) { - assert.strictEqual(result.preKeys[i-1].keyId, i+count); - } - }); - it('result contains the correct public keys', function(done) { - Promise.all(result.preKeys.map(validateResultKey)).then(function() { - done(); - }).catch(done); - }); - it('returns a signed prekey', function(done) { - assert.strictEqual(result.signedPreKey.keyId, 2); - assert.instanceOf(result.signedPreKey.signature, ArrayBuffer); - validateResultSignedKey(result.signedPreKey).then(done,done); - }); + it('result contains the correct keyIds', function() { + for (var i = 0; i < count; i++) { + assert.strictEqual(result.preKeys[i].keyId, i + 1); + } }); - describe('the third time', function() { - var result; - before(function(done) { - var accountManager = new textsecure.AccountManager(''); - accountManager.generateKeys(count).then(function(res) { - result = res; - }).then(done,done); - }); - for (var i = 1; i <= 3*count; i++) { - itStoresPreKey(i); - } - itStoresSignedPreKey(2); - itStoresSignedPreKey(3); - it('result contains ' + count + ' preKeys', function() { - assert.isArray(result.preKeys); - assert.lengthOf(result.preKeys, count); - for (var i = 0; i < count; i++) { - assert.isObject(result.preKeys[i]); - } - }); - it('result contains the correct keyIds', function() { - for (var i = 1; i <= count; i++) { - assert.strictEqual(result.preKeys[i-1].keyId, i+2*count); - } - }); - it('result contains the correct public keys', function(done) { - Promise.all(result.preKeys.map(validateResultKey)).then(function() { - done(); - }).catch(done); - }); - it('result contains a signed prekey', function(done) { - assert.strictEqual(result.signedPreKey.keyId, 3); - assert.instanceOf(result.signedPreKey.signature, ArrayBuffer); - validateResultSignedKey(result.signedPreKey).then(done,done); - }); + it('result contains the correct public keys', function(done) { + Promise.all(result.preKeys.map(validateResultKey)) + .then(function() { + done(); + }) + .catch(done); }); + it('returns a signed prekey', function(done) { + assert.strictEqual(result.signedPreKey.keyId, 1); + assert.instanceOf(result.signedPreKey.signature, ArrayBuffer); + validateResultSignedKey(result.signedPreKey).then(done, done); + }); + }); + describe('the second time', function() { + var result; + before(function(done) { + var accountManager = new textsecure.AccountManager(''); + accountManager + .generateKeys(count) + .then(function(res) { + result = res; + }) + .then(done, done); + }); + for (var i = 1; i <= 2 * count; i++) { + itStoresPreKey(i); + } + itStoresSignedPreKey(1); + itStoresSignedPreKey(2); + it('result contains ' + count + ' preKeys', function() { + assert.isArray(result.preKeys); + assert.lengthOf(result.preKeys, count); + for (var i = 0; i < count; i++) { + assert.isObject(result.preKeys[i]); + } + }); + it('result contains the correct keyIds', function() { + for (var i = 1; i <= count; i++) { + assert.strictEqual(result.preKeys[i - 1].keyId, i + count); + } + }); + it('result contains the correct public keys', function(done) { + Promise.all(result.preKeys.map(validateResultKey)) + .then(function() { + done(); + }) + .catch(done); + }); + it('returns a signed prekey', function(done) { + assert.strictEqual(result.signedPreKey.keyId, 2); + assert.instanceOf(result.signedPreKey.signature, ArrayBuffer); + validateResultSignedKey(result.signedPreKey).then(done, done); + }); + }); + describe('the third time', function() { + var result; + before(function(done) { + var accountManager = new textsecure.AccountManager(''); + accountManager + .generateKeys(count) + .then(function(res) { + result = res; + }) + .then(done, done); + }); + for (var i = 1; i <= 3 * count; i++) { + itStoresPreKey(i); + } + itStoresSignedPreKey(2); + itStoresSignedPreKey(3); + it('result contains ' + count + ' preKeys', function() { + assert.isArray(result.preKeys); + assert.lengthOf(result.preKeys, count); + for (var i = 0; i < count; i++) { + assert.isObject(result.preKeys[i]); + } + }); + it('result contains the correct keyIds', function() { + for (var i = 1; i <= count; i++) { + assert.strictEqual(result.preKeys[i - 1].keyId, i + 2 * count); + } + }); + it('result contains the correct public keys', function(done) { + Promise.all(result.preKeys.map(validateResultKey)) + .then(function() { + done(); + }) + .catch(done); + }); + it('result contains a signed prekey', function(done) { + assert.strictEqual(result.signedPreKey.keyId, 3); + assert.instanceOf(result.signedPreKey.signature, ArrayBuffer); + validateResultSignedKey(result.signedPreKey).then(done, done); + }); + }); }); diff --git a/libtextsecure/test/helpers_test.js b/libtextsecure/test/helpers_test.js index 70b547ac0..c2995ca6d 100644 --- a/libtextsecure/test/helpers_test.js +++ b/libtextsecure/test/helpers_test.js @@ -1,30 +1,32 @@ 'use strict'; -describe("Helpers", function() { - describe("ArrayBuffer->String conversion", function() { - it('works', function() { - var b = new ArrayBuffer(3); - var a = new Uint8Array(b); - a[0] = 0; - a[1] = 255; - a[2] = 128; - assert.equal(getString(b), "\x00\xff\x80"); - }); +describe('Helpers', function() { + describe('ArrayBuffer->String conversion', function() { + it('works', function() { + var b = new ArrayBuffer(3); + var a = new Uint8Array(b); + a[0] = 0; + a[1] = 255; + a[2] = 128; + assert.equal(getString(b), '\x00\xff\x80'); + }); }); - describe("stringToArrayBuffer", function() { - it('returns ArrayBuffer when passed string', function() { - var StaticArrayBufferProto = new ArrayBuffer().__proto__; - var anArrayBuffer = new ArrayBuffer(1); - var typedArray = new Uint8Array(anArrayBuffer); - typedArray[0] = 'a'.charCodeAt(0); - assertEqualArrayBuffers(stringToArrayBuffer('a'), anArrayBuffer); - }); - it('throws an error when passed a non string', function() { - var notStringable = [{}, undefined, null, new ArrayBuffer()]; - notStringable.forEach(function(notString) { - assert.throw(function() { stringToArrayBuffer(notString) }, Error); - }); + describe('stringToArrayBuffer', function() { + it('returns ArrayBuffer when passed string', function() { + var StaticArrayBufferProto = new ArrayBuffer().__proto__; + var anArrayBuffer = new ArrayBuffer(1); + var typedArray = new Uint8Array(anArrayBuffer); + typedArray[0] = 'a'.charCodeAt(0); + assertEqualArrayBuffers(stringToArrayBuffer('a'), anArrayBuffer); + }); + it('throws an error when passed a non string', function() { + var notStringable = [{}, undefined, null, new ArrayBuffer()]; + notStringable.forEach(function(notString) { + assert.throw(function() { + stringToArrayBuffer(notString); + }, Error); }); + }); }); }); diff --git a/libtextsecure/test/in_memory_signal_protocol_store.js b/libtextsecure/test/in_memory_signal_protocol_store.js index cec47e027..a1da3f7ae 100644 --- a/libtextsecure/test/in_memory_signal_protocol_store.js +++ b/libtextsecure/test/in_memory_signal_protocol_store.js @@ -1,145 +1,184 @@ function SignalProtocolStore() { - this.store = {}; + this.store = {}; } SignalProtocolStore.prototype = { - Direction: { SENDING: 1, RECEIVING: 2}, - getIdentityKeyPair: function() { - return Promise.resolve(this.get('identityKey')); - }, - getLocalRegistrationId: function() { - return Promise.resolve(this.get('registrationId')); - }, - put: function(key, value) { - if (key === undefined || value === undefined || key === null || value === null) - throw new Error("Tried to store undefined/null"); - this.store[key] = value; - }, - get: function(key, defaultValue) { - if (key === null || key === undefined) - throw new Error("Tried to get value for undefined/null key"); - if (key in this.store) { - return this.store[key]; - } else { - return defaultValue; - } - }, - remove: function(key) { - if (key === null || key === undefined) - throw new Error("Tried to remove value for undefined/null key"); - delete this.store[key]; - }, + Direction: { SENDING: 1, RECEIVING: 2 }, + getIdentityKeyPair: function() { + return Promise.resolve(this.get('identityKey')); + }, + getLocalRegistrationId: function() { + return Promise.resolve(this.get('registrationId')); + }, + put: function(key, value) { + if ( + key === undefined || + value === undefined || + key === null || + value === null + ) + throw new Error('Tried to store undefined/null'); + this.store[key] = value; + }, + get: function(key, defaultValue) { + if (key === null || key === undefined) + throw new Error('Tried to get value for undefined/null key'); + if (key in this.store) { + return this.store[key]; + } else { + return defaultValue; + } + }, + remove: function(key) { + if (key === null || key === undefined) + throw new Error('Tried to remove value for undefined/null key'); + delete this.store[key]; + }, - isTrustedIdentity: function(identifier, identityKey) { - if (identifier === null || identifier === undefined) { - throw new error("tried to check identity key for undefined/null key"); - } - if (!(identityKey instanceof ArrayBuffer)) { - throw new error("Expected identityKey to be an ArrayBuffer"); - } - var trusted = this.get('identityKey' + identifier); - if (trusted === undefined) { - return Promise.resolve(true); - } - return Promise.resolve(identityKey === trusted); - }, - loadIdentityKey: function(identifier) { - if (identifier === null || identifier === undefined) - throw new Error("Tried to get identity key for undefined/null key"); - return new Promise(function(resolve) { - resolve(this.get('identityKey' + identifier)); - }.bind(this)); - }, - saveIdentity: function(identifier, identityKey) { - if (identifier === null || identifier === undefined) - throw new Error("Tried to put identity key for undefined/null key"); - return new Promise(function(resolve) { - var existing = this.get('identityKey' + identifier); - this.put('identityKey' + identifier, identityKey); - if (existing && existing !== identityKey) { - resolve(true); - } else { - resolve(false); - } - }.bind(this)); - }, - - /* Returns a prekeypair object or undefined */ - loadPreKey: function(keyId) { - return new Promise(function(resolve) { - var res = this.get('25519KeypreKey' + keyId); - resolve(res); - }.bind(this)); - }, - storePreKey: function(keyId, keyPair) { - return new Promise(function(resolve) { - resolve(this.put('25519KeypreKey' + keyId, keyPair)); - }.bind(this)); - }, - removePreKey: function(keyId) { - return new Promise(function(resolve) { - resolve(this.remove('25519KeypreKey' + keyId)); - }.bind(this)); - }, - - /* Returns a signed keypair object or undefined */ - loadSignedPreKey: function(keyId) { - return new Promise(function(resolve) { - var res = this.get('25519KeysignedKey' + keyId); - resolve(res); - }.bind(this)); - }, - loadSignedPreKeys: function() { - return new Promise(function(resolve) { - var res = []; - for (var i in this.store) { - if (i.startsWith('25519KeysignedKey')) { - res.push(this.store[i]); - } - } - resolve(res); - }.bind(this)); - }, - storeSignedPreKey: function(keyId, keyPair) { - return new Promise(function(resolve) { - resolve(this.put('25519KeysignedKey' + keyId, keyPair)); - }.bind(this)); - }, - removeSignedPreKey: function(keyId) { - return new Promise(function(resolve) { - resolve(this.remove('25519KeysignedKey' + keyId)); - }.bind(this)); - }, - - loadSession: function(identifier) { - return new Promise(function(resolve) { - resolve(this.get('session' + identifier)); - }.bind(this)); - }, - storeSession: function(identifier, record) { - return new Promise(function(resolve) { - resolve(this.put('session' + identifier, record)); - }.bind(this)); - }, - removeAllSessions: function(identifier) { - return new Promise(function(resolve) { - for (key in this.store) { - if (key.match(RegExp('^session' + identifier.replace('\+','\\\+') + '.+'))) { - delete this.store[key]; + isTrustedIdentity: function(identifier, identityKey) { + if (identifier === null || identifier === undefined) { + throw new error('tried to check identity key for undefined/null key'); + } + if (!(identityKey instanceof ArrayBuffer)) { + throw new error('Expected identityKey to be an ArrayBuffer'); + } + var trusted = this.get('identityKey' + identifier); + if (trusted === undefined) { + return Promise.resolve(true); + } + return Promise.resolve(identityKey === trusted); + }, + loadIdentityKey: function(identifier) { + if (identifier === null || identifier === undefined) + throw new Error('Tried to get identity key for undefined/null key'); + return new Promise( + function(resolve) { + resolve(this.get('identityKey' + identifier)); + }.bind(this) + ); + }, + saveIdentity: function(identifier, identityKey) { + if (identifier === null || identifier === undefined) + throw new Error('Tried to put identity key for undefined/null key'); + return new Promise( + function(resolve) { + var existing = this.get('identityKey' + identifier); + this.put('identityKey' + identifier, identityKey); + if (existing && existing !== identityKey) { + resolve(true); + } else { + resolve(false); } - } - resolve(); - }.bind(this)); + }.bind(this) + ); + }, + + /* Returns a prekeypair object or undefined */ + loadPreKey: function(keyId) { + return new Promise( + function(resolve) { + var res = this.get('25519KeypreKey' + keyId); + resolve(res); + }.bind(this) + ); + }, + storePreKey: function(keyId, keyPair) { + return new Promise( + function(resolve) { + resolve(this.put('25519KeypreKey' + keyId, keyPair)); + }.bind(this) + ); + }, + removePreKey: function(keyId) { + return new Promise( + function(resolve) { + resolve(this.remove('25519KeypreKey' + keyId)); + }.bind(this) + ); + }, + + /* Returns a signed keypair object or undefined */ + loadSignedPreKey: function(keyId) { + return new Promise( + function(resolve) { + var res = this.get('25519KeysignedKey' + keyId); + resolve(res); + }.bind(this) + ); + }, + loadSignedPreKeys: function() { + return new Promise( + function(resolve) { + var res = []; + for (var i in this.store) { + if (i.startsWith('25519KeysignedKey')) { + res.push(this.store[i]); + } + } + resolve(res); + }.bind(this) + ); + }, + storeSignedPreKey: function(keyId, keyPair) { + return new Promise( + function(resolve) { + resolve(this.put('25519KeysignedKey' + keyId, keyPair)); + }.bind(this) + ); + }, + removeSignedPreKey: function(keyId) { + return new Promise( + function(resolve) { + resolve(this.remove('25519KeysignedKey' + keyId)); + }.bind(this) + ); + }, + + loadSession: function(identifier) { + return new Promise( + function(resolve) { + resolve(this.get('session' + identifier)); + }.bind(this) + ); + }, + storeSession: function(identifier, record) { + return new Promise( + function(resolve) { + resolve(this.put('session' + identifier, record)); + }.bind(this) + ); + }, + removeAllSessions: function(identifier) { + return new Promise( + function(resolve) { + for (key in this.store) { + if ( + key.match( + RegExp('^session' + identifier.replace('+', '\\+') + '.+') + ) + ) { + delete this.store[key]; + } + } + resolve(); + }.bind(this) + ); }, getDeviceIds: function(identifier) { - return new Promise(function(resolve) { - var deviceIds = []; - for (key in this.store) { - if (key.match(RegExp('^session' + identifier.replace('\+','\\\+') + '.+'))) { - deviceIds.push(parseInt(key.split('.')[1])); + return new Promise( + function(resolve) { + var deviceIds = []; + for (key in this.store) { + if ( + key.match( + RegExp('^session' + identifier.replace('+', '\\+') + '.+') + ) + ) { + deviceIds.push(parseInt(key.split('.')[1])); + } } - } - resolve(deviceIds); - }.bind(this)); - } + resolve(deviceIds); + }.bind(this) + ); + }, }; diff --git a/libtextsecure/test/message_receiver_test.js b/libtextsecure/test/message_receiver_test.js index 5f62768c6..bf5577e38 100644 --- a/libtextsecure/test/message_receiver_test.js +++ b/libtextsecure/test/message_receiver_test.js @@ -1,71 +1,99 @@ describe('MessageReceiver', function() { - textsecure.storage.impl = new SignalProtocolStore(); - var WebSocket = window.WebSocket; - var number = '+19999999999'; - var deviceId = 1; - var signalingKey = libsignal.crypto.getRandomBytes(32 + 20); - before(function() { - window.WebSocket = MockSocket; - textsecure.storage.user.setNumberAndDeviceId(number, deviceId, 'name'); - textsecure.storage.put("password", "password"); - textsecure.storage.put("signaling_key", signalingKey); + textsecure.storage.impl = new SignalProtocolStore(); + var WebSocket = window.WebSocket; + var number = '+19999999999'; + var deviceId = 1; + var signalingKey = libsignal.crypto.getRandomBytes(32 + 20); + before(function() { + window.WebSocket = MockSocket; + textsecure.storage.user.setNumberAndDeviceId(number, deviceId, 'name'); + textsecure.storage.put('password', 'password'); + textsecure.storage.put('signaling_key', signalingKey); + }); + after(function() { + window.WebSocket = WebSocket; + }); + + describe('connecting', function() { + var blob = null; + var attrs = { + type: textsecure.protobuf.Envelope.Type.CIPHERTEXT, + source: number, + sourceDevice: deviceId, + timestamp: Date.now(), + }; + var websocketmessage = new textsecure.protobuf.WebSocketMessage({ + type: textsecure.protobuf.WebSocketMessage.Type.REQUEST, + request: { verb: 'PUT', path: '/messages' }, }); - after (function() { window.WebSocket = WebSocket; }); - describe('connecting', function() { - var blob = null; - var attrs = { - type: textsecure.protobuf.Envelope.Type.CIPHERTEXT, - source: number, - sourceDevice: deviceId, - timestamp: Date.now(), - }; - var websocketmessage = new textsecure.protobuf.WebSocketMessage({ - type: textsecure.protobuf.WebSocketMessage.Type.REQUEST, - request: { verb: 'PUT', path: '/messages' } - }); + before(function(done) { + var signal = new textsecure.protobuf.Envelope(attrs).toArrayBuffer(); + var data = new textsecure.protobuf.DataMessage({ body: 'hello' }); - before(function(done) { - var signal = new textsecure.protobuf.Envelope(attrs).toArrayBuffer(); - var data = new textsecure.protobuf.DataMessage({ body: 'hello' }); + var signaling_key = signalingKey; + var aes_key = signaling_key.slice(0, 32); + var mac_key = signaling_key.slice(32, 32 + 20); - var signaling_key = signalingKey; - var aes_key = signaling_key.slice(0, 32); - var mac_key = signaling_key.slice(32, 32 + 20); - - window.crypto.subtle.importKey('raw', aes_key, {name: 'AES-CBC'}, false, ['encrypt']).then(function(key) { - var iv = libsignal.crypto.getRandomBytes(16); - window.crypto.subtle.encrypt({name: 'AES-CBC', iv: new Uint8Array(iv)}, key, signal).then(function(ciphertext) { - window.crypto.subtle.importKey('raw', mac_key, {name: 'HMAC', hash: {name: 'SHA-256'}}, false, ['sign']).then(function(key) { - window.crypto.subtle.sign( {name: 'HMAC', hash: 'SHA-256'}, key, signal).then(function(mac) { - var version = new Uint8Array([1]); - var message = dcodeIO.ByteBuffer.concat([version, iv, ciphertext, mac ]); - websocketmessage.request.body = message.toArrayBuffer(); - console.log(new Uint8Array(message.toArrayBuffer())); - done(); - }); + window.crypto.subtle + .importKey('raw', aes_key, { name: 'AES-CBC' }, false, ['encrypt']) + .then(function(key) { + var iv = libsignal.crypto.getRandomBytes(16); + window.crypto.subtle + .encrypt({ name: 'AES-CBC', iv: new Uint8Array(iv) }, key, signal) + .then(function(ciphertext) { + window.crypto.subtle + .importKey( + 'raw', + mac_key, + { name: 'HMAC', hash: { name: 'SHA-256' } }, + false, + ['sign'] + ) + .then(function(key) { + window.crypto.subtle + .sign({ name: 'HMAC', hash: 'SHA-256' }, key, signal) + .then(function(mac) { + var version = new Uint8Array([1]); + var message = dcodeIO.ByteBuffer.concat([ + version, + iv, + ciphertext, + mac, + ]); + websocketmessage.request.body = message.toArrayBuffer(); + console.log(new Uint8Array(message.toArrayBuffer())); + done(); }); }); }); }); - - it('connects', function(done) { - var mockServer = new MockServer('ws://localhost:8080/v1/websocket/?login='+ encodeURIComponent(number) +'.1&password=password'); - - mockServer.on('connection', function(server) { - server.send(new Blob([ websocketmessage.toArrayBuffer() ])); - }); - - window.addEventListener('textsecure:message', function(ev) { - var signal = ev.proto; - for (var key in attrs) { - assert.strictEqual(attrs[key], signal[key]); - } - assert.strictEqual(signal.message.body, 'hello'); - server.close(); - done(); - }); - var messageReceiver = new textsecure.MessageReceiver('ws://localhost:8080', window); - }); }); + + it('connects', function(done) { + var mockServer = new MockServer( + 'ws://localhost:8080/v1/websocket/?login=' + + encodeURIComponent(number) + + '.1&password=password' + ); + + mockServer.on('connection', function(server) { + server.send(new Blob([websocketmessage.toArrayBuffer()])); + }); + + window.addEventListener('textsecure:message', function(ev) { + var signal = ev.proto; + for (var key in attrs) { + assert.strictEqual(attrs[key], signal[key]); + } + assert.strictEqual(signal.message.body, 'hello'); + server.close(); + done(); + }); + var messageReceiver = new textsecure.MessageReceiver( + 'ws://localhost:8080', + window + ); + }); + }); }); diff --git a/libtextsecure/test/protocol_test.js b/libtextsecure/test/protocol_test.js index 35dd4c144..24ef1c866 100644 --- a/libtextsecure/test/protocol_test.js +++ b/libtextsecure/test/protocol_test.js @@ -1,32 +1,38 @@ 'use strict'; describe('Protocol', function() { + describe('Unencrypted PushMessageProto "decrypt"', function() { + //exclusive + it('works', function(done) { + localStorage.clear(); - describe('Unencrypted PushMessageProto "decrypt"', function() { - //exclusive - it('works', function(done) { - localStorage.clear(); + var text_message = new textsecure.protobuf.DataMessage(); + text_message.body = 'Hi Mom'; + var server_message = { + type: 4, // unencrypted + source: '+19999999999', + timestamp: 42, + message: text_message.encode(), + }; - var text_message = new textsecure.protobuf.DataMessage(); - text_message.body = "Hi Mom"; - var server_message = { - type: 4, // unencrypted - source: "+19999999999", - timestamp: 42, - message: text_message.encode() - }; - - return textsecure.protocol_wrapper.handleEncryptedMessage( - server_message.source, - server_message.source_device, - server_message.type, - server_message.message - ).then(function(message) { - assert.equal(message.body, text_message.body); - assert.equal(message.attachments.length, text_message.attachments.length); - assert.equal(text_message.attachments.length, 0); - }).then(done).catch(done); - }); + return textsecure.protocol_wrapper + .handleEncryptedMessage( + server_message.source, + server_message.source_device, + server_message.type, + server_message.message + ) + .then(function(message) { + assert.equal(message.body, text_message.body); + assert.equal( + message.attachments.length, + text_message.attachments.length + ); + assert.equal(text_message.attachments.length, 0); + }) + .then(done) + .catch(done); }); + }); - // TODO: Use fake_api's hiding of api.sendMessage to test sendmessage.js' maze + // TODO: Use fake_api's hiding of api.sendMessage to test sendmessage.js' maze }); diff --git a/libtextsecure/test/protocol_wrapper_test.js b/libtextsecure/test/protocol_wrapper_test.js index da96ab276..748e81fbf 100644 --- a/libtextsecure/test/protocol_wrapper_test.js +++ b/libtextsecure/test/protocol_wrapper_test.js @@ -1,32 +1,40 @@ 'use strict'; describe('Protocol Wrapper', function() { - var store = textsecure.storage.protocol; - var identifier = '+5558675309'; - var another_identifier = '+5555590210'; - var prekeys, identityKey, testKey; - this.timeout(5000); - before(function(done) { - localStorage.clear(); - libsignal.KeyHelper.generateIdentityKeyPair().then(function(identityKey) { - return textsecure.storage.protocol.saveIdentity(identifier, identityKey); - }).then(function() { - done(); - }); - }); - describe('processPreKey', function() { - it('rejects if the identity key changes', function(done) { - var address = new libsignal.SignalProtocolAddress(identifier, 1); - var builder = new libsignal.SessionBuilder(store, address); - return builder.processPreKey({ - identityKey: textsecure.crypto.getRandomBytes(33), - encodedNumber: address.toString() - }).then(function() { - done(new Error('Allowed to overwrite identity key')); - }).catch(function(e) { - assert.strictEqual(e.message, 'Identity key changed'); - done(); - }); + var store = textsecure.storage.protocol; + var identifier = '+5558675309'; + var another_identifier = '+5555590210'; + var prekeys, identityKey, testKey; + this.timeout(5000); + before(function(done) { + localStorage.clear(); + libsignal.KeyHelper.generateIdentityKeyPair() + .then(function(identityKey) { + return textsecure.storage.protocol.saveIdentity( + identifier, + identityKey + ); + }) + .then(function() { + done(); + }); + }); + describe('processPreKey', function() { + it('rejects if the identity key changes', function(done) { + var address = new libsignal.SignalProtocolAddress(identifier, 1); + var builder = new libsignal.SessionBuilder(store, address); + return builder + .processPreKey({ + identityKey: textsecure.crypto.getRandomBytes(33), + encodedNumber: address.toString(), + }) + .then(function() { + done(new Error('Allowed to overwrite identity key')); + }) + .catch(function(e) { + assert.strictEqual(e.message, 'Identity key changed'); + done(); }); }); + }); }); diff --git a/libtextsecure/test/storage_test.js b/libtextsecure/test/storage_test.js index 7f9ce4ba4..7ce128505 100644 --- a/libtextsecure/test/storage_test.js +++ b/libtextsecure/test/storage_test.js @@ -1,158 +1,200 @@ 'use strict'; -describe("SignalProtocolStore", function() { - before(function() { localStorage.clear(); }); - var store = textsecure.storage.protocol; - var identifier = '+5558675309'; - var another_identifier = '+5555590210'; - var identityKey = { - pubKey: libsignal.crypto.getRandomBytes(33), - privKey: libsignal.crypto.getRandomBytes(32), - }; - var testKey = { - pubKey: libsignal.crypto.getRandomBytes(33), - privKey: libsignal.crypto.getRandomBytes(32), - }; - it('retrieves my registration id', function(done) { - store.put('registrationId', 1337); - store.getLocalRegistrationId().then(function(reg) { - assert.strictEqual(reg, 1337); - }).then(done, done); - }); - it('retrieves my identity key', function(done) { - store.put('identityKey', identityKey); - store.getIdentityKeyPair().then(function(key) { - assertEqualArrayBuffers(key.pubKey, identityKey.pubKey); - assertEqualArrayBuffers(key.privKey, identityKey.privKey); - }).then(done,done); - }); - it('stores identity keys', function(done) { - store.saveIdentity(identifier, testKey.pubKey).then(function() { - return store.loadIdentityKey(identifier).then(function(key) { - assertEqualArrayBuffers(key, testKey.pubKey); - }); - }).then(done,done); - }); - it('returns whether a key is trusted', function(done) { - var newIdentity = libsignal.crypto.getRandomBytes(33); - store.saveIdentity(identifier, testKey.pubKey).then(function() { - store.isTrustedIdentity(identifier, newIdentity).then(function(trusted) { - if (trusted) { - done(new Error('Allowed to overwrite identity key')); - } else { - done(); - } - }).catch(done); +describe('SignalProtocolStore', function() { + before(function() { + localStorage.clear(); + }); + var store = textsecure.storage.protocol; + var identifier = '+5558675309'; + var another_identifier = '+5555590210'; + var identityKey = { + pubKey: libsignal.crypto.getRandomBytes(33), + privKey: libsignal.crypto.getRandomBytes(32), + }; + var testKey = { + pubKey: libsignal.crypto.getRandomBytes(33), + privKey: libsignal.crypto.getRandomBytes(32), + }; + it('retrieves my registration id', function(done) { + store.put('registrationId', 1337); + store + .getLocalRegistrationId() + .then(function(reg) { + assert.strictEqual(reg, 1337); + }) + .then(done, done); + }); + it('retrieves my identity key', function(done) { + store.put('identityKey', identityKey); + store + .getIdentityKeyPair() + .then(function(key) { + assertEqualArrayBuffers(key.pubKey, identityKey.pubKey); + assertEqualArrayBuffers(key.privKey, identityKey.privKey); + }) + .then(done, done); + }); + it('stores identity keys', function(done) { + store + .saveIdentity(identifier, testKey.pubKey) + .then(function() { + return store.loadIdentityKey(identifier).then(function(key) { + assertEqualArrayBuffers(key, testKey.pubKey); }); + }) + .then(done, done); + }); + it('returns whether a key is trusted', function(done) { + var newIdentity = libsignal.crypto.getRandomBytes(33); + store.saveIdentity(identifier, testKey.pubKey).then(function() { + store + .isTrustedIdentity(identifier, newIdentity) + .then(function(trusted) { + if (trusted) { + done(new Error('Allowed to overwrite identity key')); + } else { + done(); + } + }) + .catch(done); }); - it('returns whether a key is untrusted', function(done) { - var newIdentity = libsignal.crypto.getRandomBytes(33); - store.saveIdentity(identifier, testKey.pubKey).then(function() { - store.isTrustedIdentity(identifier, testKey.pubKey).then(function(trusted) { - if (trusted) { - done(); - } else { - done(new Error('Allowed to overwrite identity key')); - } - }).catch(done); + }); + it('returns whether a key is untrusted', function(done) { + var newIdentity = libsignal.crypto.getRandomBytes(33); + store.saveIdentity(identifier, testKey.pubKey).then(function() { + store + .isTrustedIdentity(identifier, testKey.pubKey) + .then(function(trusted) { + if (trusted) { + done(); + } else { + done(new Error('Allowed to overwrite identity key')); + } + }) + .catch(done); + }); + }); + 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('deletes prekeys', function(done) { + before(function(done) { + store.storePreKey(2, testKey).then(done); }); - 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('deletes prekeys', function(done) { - before(function(done) { - store.storePreKey(2, testKey).then(done); + store + .removePreKey(2, testKey) + .then(function() { + return store.loadPreKey(2).then(function(key) { + assert.isUndefined(key); }); - store.removePreKey(2, testKey).then(function() { - return store.loadPreKey(2).then(function(key) { - assert.isUndefined(key); - }); - }).then(done,done); - }); - 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('deletes signed prekeys', function(done) { - before(function(done) { - store.storeSignedPreKey(4, testKey).then(done); + }) + .then(done, done); + }); + 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); }); - store.removeSignedPreKey(4, testKey).then(function() { - return store.loadSignedPreKey(4).then(function(key) { - assert.isUndefined(key); - }); - }).then(done,done); + }) + .then(done, done); + }); + it('deletes signed prekeys', function(done) { + before(function(done) { + store.storeSignedPreKey(4, testKey).then(done); }); - it('stores sessions', function(done) { - var testRecord = "an opaque string"; - var devices = [1, 2, 3].map(function(deviceId) { - return [identifier, deviceId].join('.'); + store + .removeSignedPreKey(4, testKey) + .then(function() { + return store.loadSignedPreKey(4).then(function(key) { + assert.isUndefined(key); }); - var promise = Promise.resolve(); - devices.forEach(function(encodedNumber) { - promise = promise.then(function() { - return store.storeSession(encodedNumber, testRecord + encodedNumber) - }); - }); - promise.then(function() { - return Promise.all(devices.map(store.loadSession.bind(store))).then(function(records) { - for (var i in records) { - assert.strictEqual(records[i], testRecord + devices[i]); - }; - }); - }).then(done,done); + }) + .then(done, done); + }); + it('stores sessions', 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', function(done) { - var testRecord = "an opaque string"; - var devices = [1, 2, 3].map(function(deviceId) { - return [identifier, 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]); - }; - }); - }); - }).then(done,done); + var promise = Promise.resolve(); + devices.forEach(function(encodedNumber) { + promise = promise.then(function() { + return store.storeSession(encodedNumber, testRecord + encodedNumber); + }); }); - 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('.'); - }); - 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]); - }); - }).then(done,done); + promise + .then(function() { + return Promise.all(devices.map(store.loadSession.bind(store))).then( + function(records) { + for (var i in records) { + assert.strictEqual(records[i], testRecord + devices[i]); + } + } + ); + }) + .then(done, done); + }); + 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('returns empty array for a number with no device ids', function(done) { - return store.getDeviceIds('foo').then(function(deviceIds) { - assert.sameMembers(deviceIds,[]); - }).then(done,done); + 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]); + } + } + ); + }); + }) + .then(done, done); + }); + 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('.'); + }); + 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]); + }); + }) + .then(done, done); + }); + it('returns empty array for a number with no device ids', function(done) { + return store + .getDeviceIds('foo') + .then(function(deviceIds) { + assert.sameMembers(deviceIds, []); + }) + .then(done, done); + }); }); diff --git a/libtextsecure/test/task_with_timeout_test.js b/libtextsecure/test/task_with_timeout_test.js index 713d94c58..f98beec0f 100644 --- a/libtextsecure/test/task_with_timeout_test.js +++ b/libtextsecure/test/task_with_timeout_test.js @@ -1,74 +1,80 @@ 'use strict'; describe('createTaskWithTimeout', function() { - it('resolves when promise resolves', function() { - var task = function() { - return Promise.resolve('hi!'); - }; - var taskWithTimeout = textsecure.createTaskWithTimeout(task); + it('resolves when promise resolves', function() { + var task = function() { + return Promise.resolve('hi!'); + }; + var taskWithTimeout = textsecure.createTaskWithTimeout(task); - return taskWithTimeout().then(function(result) { - assert.strictEqual(result, 'hi!') - }); + return taskWithTimeout().then(function(result) { + assert.strictEqual(result, 'hi!'); }); - it('flows error from promise back', function() { - var error = new Error('original'); - var task = function() { - return Promise.reject(error); - }; - var taskWithTimeout = textsecure.createTaskWithTimeout(task); + }); + it('flows error from promise back', function() { + var error = new Error('original'); + var task = function() { + return Promise.reject(error); + }; + var taskWithTimeout = textsecure.createTaskWithTimeout(task); - return taskWithTimeout().catch(function(flowedError) { - assert.strictEqual(error, flowedError); - }); + return taskWithTimeout().catch(function(flowedError) { + assert.strictEqual(error, flowedError); + }); + }); + it('rejects if promise takes too long (this one logs error to console)', function() { + var error = new Error('original'); + var complete = false; + var task = function() { + return new Promise(function(resolve) { + setTimeout(function() { + complete = true; + resolve(); + }, 3000); + }); + }; + var taskWithTimeout = textsecure.createTaskWithTimeout(task, this.name, { + timeout: 10, }); - it('rejects if promise takes too long (this one logs error to console)', function() { - var error = new Error('original'); - var complete = false; - var task = function() { - return new Promise(function(resolve) { - setTimeout(function() { - complete = true; - resolve(); - }, 3000); - }); - }; - var taskWithTimeout = textsecure.createTaskWithTimeout(task, this.name, { - timeout: 10 - }); - return taskWithTimeout().then(function() { - throw new Error('it was not supposed to resolve!'); - }, function() { - assert.strictEqual(complete, false); - }); + return taskWithTimeout().then( + function() { + throw new Error('it was not supposed to resolve!'); + }, + function() { + assert.strictEqual(complete, false); + } + ); + }); + it('resolves if task returns something falsey', function() { + var task = function() {}; + var taskWithTimeout = textsecure.createTaskWithTimeout(task); + return taskWithTimeout(); + }); + it('resolves if task returns a non-promise', function() { + var task = function() { + return 'hi!'; + }; + var taskWithTimeout = textsecure.createTaskWithTimeout(task); + return taskWithTimeout().then(function(result) { + assert.strictEqual(result, 'hi!'); }); - it('resolves if task returns something falsey', function() { - var task = function() {}; - var taskWithTimeout = textsecure.createTaskWithTimeout(task); - return taskWithTimeout(); - }); - it('resolves if task returns a non-promise', function() { - var task = function() { - return 'hi!'; - }; - var taskWithTimeout = textsecure.createTaskWithTimeout(task); - return taskWithTimeout().then(function(result) { - assert.strictEqual(result, 'hi!') - }); - }); - it('rejects if task throws (and does not log about taking too long)', function() { - var error = new Error('Task is throwing!'); - var task = function() { - throw error; - }; - var taskWithTimeout = textsecure.createTaskWithTimeout(task, this.name, { - timeout: 10 - }); - return taskWithTimeout().then(function(result) { - throw new Error('Overall task should reject!') - }, function(flowedError) { - assert.strictEqual(flowedError, error); - }); + }); + it('rejects if task throws (and does not log about taking too long)', function() { + var error = new Error('Task is throwing!'); + var task = function() { + throw error; + }; + var taskWithTimeout = textsecure.createTaskWithTimeout(task, this.name, { + timeout: 10, }); + return taskWithTimeout().then( + function(result) { + throw new Error('Overall task should reject!'); + }, + function(flowedError) { + assert.strictEqual(flowedError, error); + } + ); + }); }); diff --git a/libtextsecure/test/websocket-resources_test.js b/libtextsecure/test/websocket-resources_test.js index 279e423f5..9015fdf9f 100644 --- a/libtextsecure/test/websocket-resources_test.js +++ b/libtextsecure/test/websocket-resources_test.js @@ -1,173 +1,210 @@ -;(function() { - 'use strict'; +(function() { + 'use strict'; - describe('WebSocket-Resource', function() { - describe('requests and responses', function () { - it('receives requests and sends responses', function(done) { - // mock socket - var request_id = '1'; - var socket = { - send: function(data) { - var message = textsecure.protobuf.WebSocketMessage.decode(data); - assert.strictEqual(message.type, textsecure.protobuf.WebSocketMessage.Type.RESPONSE); - assert.strictEqual(message.response.message, 'OK'); - assert.strictEqual(message.response.status, 200); - assert.strictEqual(message.response.id.toString(), request_id); - done(); - }, - addEventListener: function() {}, - }; + describe('WebSocket-Resource', function() { + describe('requests and responses', function() { + it('receives requests and sends responses', function(done) { + // mock socket + var request_id = '1'; + var socket = { + send: function(data) { + var message = textsecure.protobuf.WebSocketMessage.decode(data); + assert.strictEqual( + message.type, + textsecure.protobuf.WebSocketMessage.Type.RESPONSE + ); + assert.strictEqual(message.response.message, 'OK'); + assert.strictEqual(message.response.status, 200); + assert.strictEqual(message.response.id.toString(), request_id); + done(); + }, + addEventListener: function() {}, + }; - // actual test - var resource = new WebSocketResource(socket, { - handleRequest: function (request) { - assert.strictEqual(request.verb, 'PUT'); - assert.strictEqual(request.path, '/some/path'); - assertEqualArrayBuffers(request.body.toArrayBuffer(), new Uint8Array([1,2,3]).buffer); - request.respond(200, 'OK'); - } - }); - - // mock socket request - socket.onmessage({ - data: new Blob([ - new textsecure.protobuf.WebSocketMessage({ - type: textsecure.protobuf.WebSocketMessage.Type.REQUEST, - request: { - id: request_id, - verb: 'PUT', - path: '/some/path', - body: new Uint8Array([1,2,3]).buffer - } - }).encode().toArrayBuffer() - ]) - }); - }); - - it('sends requests and receives responses', function(done) { - // mock socket and request handler - var request_id; - var socket = { - send: function(data) { - var message = textsecure.protobuf.WebSocketMessage.decode(data); - assert.strictEqual(message.type, textsecure.protobuf.WebSocketMessage.Type.REQUEST); - assert.strictEqual(message.request.verb, 'PUT'); - 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 - var resource = new WebSocketResource(socket); - resource.sendRequest({ - verb: 'PUT', - path: '/some/path', - body: new Uint8Array([1,2,3]).buffer, - error: done, - success: function(message, status, request) { - assert.strictEqual(message, 'OK'); - assert.strictEqual(status, 200); - done(); - } - }); - - // mock socket response - socket.onmessage({ - data: new Blob([ - new textsecure.protobuf.WebSocketMessage({ - type: textsecure.protobuf.WebSocketMessage.Type.RESPONSE, - response: { id: request_id, message: 'OK', status: 200 } - }).encode().toArrayBuffer() - ]) - }); - }); + // actual test + var resource = new WebSocketResource(socket, { + handleRequest: function(request) { + assert.strictEqual(request.verb, 'PUT'); + assert.strictEqual(request.path, '/some/path'); + assertEqualArrayBuffers( + request.body.toArrayBuffer(), + new Uint8Array([1, 2, 3]).buffer + ); + request.respond(200, 'OK'); + }, }); - describe('close', function() { - before(function() { window.WebSocket = MockSocket; }); - after (function() { window.WebSocket = WebSocket; }); - it('closes the connection', function(done) { - var mockServer = new MockServer('ws://localhost:8081'); - mockServer.on('connection', function(server) { - server.on('close', done); - }); - var resource = new WebSocketResource(new WebSocket('ws://localhost:8081')); - resource.close(); - }); + // mock socket request + socket.onmessage({ + data: new Blob([ + new textsecure.protobuf.WebSocketMessage({ + type: textsecure.protobuf.WebSocketMessage.Type.REQUEST, + request: { + id: request_id, + verb: 'PUT', + path: '/some/path', + body: new Uint8Array([1, 2, 3]).buffer, + }, + }) + .encode() + .toArrayBuffer(), + ]), + }); + }); + + it('sends requests and receives responses', function(done) { + // mock socket and request handler + var request_id; + var socket = { + send: function(data) { + var message = textsecure.protobuf.WebSocketMessage.decode(data); + assert.strictEqual( + message.type, + textsecure.protobuf.WebSocketMessage.Type.REQUEST + ); + assert.strictEqual(message.request.verb, 'PUT'); + 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 + var resource = new WebSocketResource(socket); + resource.sendRequest({ + verb: 'PUT', + path: '/some/path', + body: new Uint8Array([1, 2, 3]).buffer, + error: done, + success: function(message, status, request) { + assert.strictEqual(message, 'OK'); + assert.strictEqual(status, 200); + done(); + }, }); - describe.skip('with a keepalive config', function() { - before(function() { window.WebSocket = MockSocket; }); - after (function() { window.WebSocket = WebSocket; }); - this.timeout(60000); - it('sends keepalives once a minute', function(done) { - var mockServer = new MockServer('ws://localhost:8081'); - mockServer.on('connection', function(server) { - server.on('message', function(data) { - var message = textsecure.protobuf.WebSocketMessage.decode(data); - assert.strictEqual(message.type, textsecure.protobuf.WebSocketMessage.Type.REQUEST); - assert.strictEqual(message.request.verb, 'GET'); - assert.strictEqual(message.request.path, '/v1/keepalive'); - server.close(); - done(); - }); - }); - new WebSocketResource(new WebSocket('ws://localhost:8081'), { - keepalive: { path: '/v1/keepalive' } - }); - }); - - it('uses / as a default path', function(done) { - var mockServer = new MockServer('ws://localhost:8081'); - mockServer.on('connection', function(server) { - server.on('message', function(data) { - var message = textsecure.protobuf.WebSocketMessage.decode(data); - assert.strictEqual(message.type, textsecure.protobuf.WebSocketMessage.Type.REQUEST); - assert.strictEqual(message.request.verb, 'GET'); - assert.strictEqual(message.request.path, '/'); - server.close(); - done(); - }); - }); - new WebSocketResource(new WebSocket('ws://localhost:8081'), { - keepalive: true - }); - - }); - - it('optionally disconnects if no response', function(done) { - this.timeout(65000); - var mockServer = new MockServer('ws://localhost:8081'); - var socket = new WebSocket('ws://localhost:8081'); - mockServer.on('connection', function(server) { - server.on('close', done); - }); - new WebSocketResource(socket, { keepalive: true }); - }); - - it('allows resetting the keepalive timer', function(done) { - this.timeout(65000); - var mockServer = new MockServer('ws://localhost:8081'); - var socket = new WebSocket('ws://localhost:8081'); - var startTime = Date.now(); - mockServer.on('connection', function(server) { - server.on('message', function(data) { - var message = textsecure.protobuf.WebSocketMessage.decode(data); - assert.strictEqual(message.type, textsecure.protobuf.WebSocketMessage.Type.REQUEST); - assert.strictEqual(message.request.verb, 'GET'); - assert.strictEqual(message.request.path, '/'); - assert(Date.now() > startTime + 60000, 'keepalive time should be longer than a minute'); - server.close(); - done(); - }); - }); - var resource = new WebSocketResource(socket, { keepalive: true }); - setTimeout(function() { - resource.resetKeepAliveTimer() - }, 5000); - }); + // mock socket response + socket.onmessage({ + data: new Blob([ + new textsecure.protobuf.WebSocketMessage({ + type: textsecure.protobuf.WebSocketMessage.Type.RESPONSE, + response: { id: request_id, message: 'OK', status: 200 }, + }) + .encode() + .toArrayBuffer(), + ]), }); + }); }); -}()); + + describe('close', function() { + before(function() { + window.WebSocket = MockSocket; + }); + after(function() { + window.WebSocket = WebSocket; + }); + it('closes the connection', function(done) { + var mockServer = new MockServer('ws://localhost:8081'); + mockServer.on('connection', function(server) { + server.on('close', done); + }); + var resource = new WebSocketResource( + new WebSocket('ws://localhost:8081') + ); + resource.close(); + }); + }); + + describe.skip('with a keepalive config', function() { + before(function() { + window.WebSocket = MockSocket; + }); + after(function() { + window.WebSocket = WebSocket; + }); + this.timeout(60000); + it('sends keepalives once a minute', function(done) { + var mockServer = new MockServer('ws://localhost:8081'); + mockServer.on('connection', function(server) { + server.on('message', function(data) { + var message = textsecure.protobuf.WebSocketMessage.decode(data); + assert.strictEqual( + message.type, + textsecure.protobuf.WebSocketMessage.Type.REQUEST + ); + assert.strictEqual(message.request.verb, 'GET'); + assert.strictEqual(message.request.path, '/v1/keepalive'); + server.close(); + done(); + }); + }); + new WebSocketResource(new WebSocket('ws://localhost:8081'), { + keepalive: { path: '/v1/keepalive' }, + }); + }); + + it('uses / as a default path', function(done) { + var mockServer = new MockServer('ws://localhost:8081'); + mockServer.on('connection', function(server) { + server.on('message', function(data) { + var message = textsecure.protobuf.WebSocketMessage.decode(data); + assert.strictEqual( + message.type, + textsecure.protobuf.WebSocketMessage.Type.REQUEST + ); + assert.strictEqual(message.request.verb, 'GET'); + assert.strictEqual(message.request.path, '/'); + server.close(); + done(); + }); + }); + new WebSocketResource(new WebSocket('ws://localhost:8081'), { + keepalive: true, + }); + }); + + it('optionally disconnects if no response', function(done) { + this.timeout(65000); + var mockServer = new MockServer('ws://localhost:8081'); + var socket = new WebSocket('ws://localhost:8081'); + mockServer.on('connection', function(server) { + server.on('close', done); + }); + new WebSocketResource(socket, { keepalive: true }); + }); + + it('allows resetting the keepalive timer', function(done) { + this.timeout(65000); + var mockServer = new MockServer('ws://localhost:8081'); + var socket = new WebSocket('ws://localhost:8081'); + var startTime = Date.now(); + mockServer.on('connection', function(server) { + server.on('message', function(data) { + var message = textsecure.protobuf.WebSocketMessage.decode(data); + assert.strictEqual( + message.type, + textsecure.protobuf.WebSocketMessage.Type.REQUEST + ); + assert.strictEqual(message.request.verb, 'GET'); + assert.strictEqual(message.request.path, '/'); + assert( + Date.now() > startTime + 60000, + 'keepalive time should be longer than a minute' + ); + server.close(); + done(); + }); + }); + var resource = new WebSocketResource(socket, { keepalive: true }); + setTimeout(function() { + resource.resetKeepAliveTimer(); + }, 5000); + }); + }); + }); +})(); diff --git a/libtextsecure/test/websocket_test.js b/libtextsecure/test/websocket_test.js index 5e242ef3a..f880d2e6b 100644 --- a/libtextsecure/test/websocket_test.js +++ b/libtextsecure/test/websocket_test.js @@ -1,62 +1,64 @@ describe('TextSecureWebSocket', function() { - var RealWebSocket = window.WebSocket; - before(function() { window.WebSocket = MockSocket; }); - after (function() { window.WebSocket = RealWebSocket; }); - it('connects and disconnects', function(done) { - var mockServer = new MockServer('ws://localhost:8080'); - mockServer.on('connection', function(server) { - socket.close(); - server.close(); - done(); - }); - var socket = new TextSecureWebSocket('ws://localhost:8080'); + var RealWebSocket = window.WebSocket; + before(function() { + window.WebSocket = MockSocket; + }); + after(function() { + window.WebSocket = RealWebSocket; + }); + it('connects and disconnects', function(done) { + var mockServer = new MockServer('ws://localhost:8080'); + mockServer.on('connection', function(server) { + socket.close(); + server.close(); + done(); }); + var socket = new TextSecureWebSocket('ws://localhost:8080'); + }); - it('sends and receives', function(done) { - var mockServer = new MockServer('ws://localhost:8080'); - mockServer.on('connection', function(server) { - server.on('message', function(data) { - server.send('ack'); - server.close(); - }); - }); - var socket = new TextSecureWebSocket('ws://localhost:8080'); - socket.onmessage = function(response) { - assert.strictEqual(response.data, 'ack'); - socket.close(); - done(); - }; - socket.send('syn'); - + it('sends and receives', function(done) { + var mockServer = new MockServer('ws://localhost:8080'); + mockServer.on('connection', function(server) { + server.on('message', function(data) { + server.send('ack'); + server.close(); + }); }); + var socket = new TextSecureWebSocket('ws://localhost:8080'); + socket.onmessage = function(response) { + assert.strictEqual(response.data, 'ack'); + socket.close(); + done(); + }; + socket.send('syn'); + }); - it('exposes the socket status', function(done) { - var mockServer = new MockServer('ws://localhost:8082'); - mockServer.on('connection', function(server) { - assert.strictEqual(socket.getStatus(), WebSocket.OPEN); - server.close(); - socket.close(); - }); - var socket = new TextSecureWebSocket('ws://localhost:8082'); - socket.onclose = function() { - assert.strictEqual(socket.getStatus(), WebSocket.CLOSING); - done(); - }; - }); - - it('reconnects', function(done) { - this.timeout(60000); - var mockServer = new MockServer('ws://localhost:8082'); - var socket = new TextSecureWebSocket('ws://localhost:8082'); - socket.onclose = function() { - var mockServer = new MockServer('ws://localhost:8082'); - mockServer.on('connection', function(server) { - socket.close(); - server.close(); - done(); - }); - }; - mockServer.close(); + it('exposes the socket status', function(done) { + var mockServer = new MockServer('ws://localhost:8082'); + mockServer.on('connection', function(server) { + assert.strictEqual(socket.getStatus(), WebSocket.OPEN); + server.close(); + socket.close(); }); + var socket = new TextSecureWebSocket('ws://localhost:8082'); + socket.onclose = function() { + assert.strictEqual(socket.getStatus(), WebSocket.CLOSING); + done(); + }; + }); + it('reconnects', function(done) { + this.timeout(60000); + var mockServer = new MockServer('ws://localhost:8082'); + var socket = new TextSecureWebSocket('ws://localhost:8082'); + socket.onclose = function() { + var mockServer = new MockServer('ws://localhost:8082'); + mockServer.on('connection', function(server) { + socket.close(); + server.close(); + done(); + }); + }; + mockServer.close(); + }); }); diff --git a/libtextsecure/websocket-resources.js b/libtextsecure/websocket-resources.js index 85ee4f41f..bff8942f7 100644 --- a/libtextsecure/websocket-resources.js +++ b/libtextsecure/websocket-resources.js @@ -1,7 +1,7 @@ -;(function(){ - 'use strict'; +(function() { + 'use strict'; - /* + /* * WebSocket-Resources * * Create a request-response interface over websockets using the @@ -23,212 +23,233 @@ * */ - var Request = function(options) { - this.verb = options.verb || options.type; - this.path = options.path || options.url; - this.body = options.body || options.data; - this.success = options.success; - this.error = options.error; - this.id = options.id; + var Request = function(options) { + this.verb = options.verb || options.type; + this.path = options.path || options.url; + this.body = options.body || options.data; + this.success = options.success; + this.error = options.error; + this.id = options.id; - if (this.id === undefined) { - var bits = new Uint32Array(2); - window.crypto.getRandomValues(bits); - this.id = dcodeIO.Long.fromBits(bits[0], bits[1], true); - } - - if (this.body === undefined) { - this.body = null; - } - }; - - var IncomingWebSocketRequest = function(options) { - var request = new Request(options); - var socket = options.socket; - - this.verb = request.verb; - this.path = request.path; - this.body = request.body; - - this.respond = function(status, message) { - socket.send( - new textsecure.protobuf.WebSocketMessage({ - type: textsecure.protobuf.WebSocketMessage.Type.RESPONSE, - response: { id: request.id, message: message, status: status } - }).encode().toArrayBuffer() - ); - }; - }; - - var outgoing = {}; - var OutgoingWebSocketRequest = function(options, socket) { - var request = new Request(options); - outgoing[request.id] = request; - socket.send( - new textsecure.protobuf.WebSocketMessage({ - type: textsecure.protobuf.WebSocketMessage.Type.REQUEST, - request: { - verb : request.verb, - path : request.path, - body : request.body, - id : request.id - } - }).encode().toArrayBuffer() - ); - }; - - window.WebSocketResource = function(socket, opts) { - opts = opts || {}; - var handleRequest = opts.handleRequest; - if (typeof handleRequest !== 'function') { - handleRequest = function(request) { - request.respond(404, 'Not found'); - }; - } - this.sendRequest = function(options) { - return new OutgoingWebSocketRequest(options, socket); - }; - - socket.onmessage = function(socketMessage) { - var blob = socketMessage.data; - var handleArrayBuffer = function(buffer) { - var message = textsecure.protobuf.WebSocketMessage.decode(buffer); - if (message.type === textsecure.protobuf.WebSocketMessage.Type.REQUEST ) { - handleRequest( - new IncomingWebSocketRequest({ - verb : message.request.verb, - path : message.request.path, - body : message.request.body, - id : message.request.id, - socket : socket - }) - ); - } - else if (message.type === textsecure.protobuf.WebSocketMessage.Type.RESPONSE ) { - var response = message.response; - var request = outgoing[response.id]; - if (request) { - request.response = response; - var callback = request.error; - if (response.status >= 200 && response.status < 300) { - callback = request.success; - } - - if (typeof callback === 'function') { - callback(response.message, response.status, request); - } - } else { - throw 'Received response for unknown request ' + message.response.id; - } - } - }; - - if (blob instanceof ArrayBuffer) { - handleArrayBuffer(blob); - } else { - var reader = new FileReader(); - reader.onload = function() { - handleArrayBuffer(reader.result); - }; - reader.readAsArrayBuffer(blob); - } - }; - - if (opts.keepalive) { - this.keepalive = new KeepAlive(this, { - path : opts.keepalive.path, - disconnect : opts.keepalive.disconnect - }); - var resetKeepAliveTimer = this.keepalive.reset.bind(this.keepalive); - socket.addEventListener('open', resetKeepAliveTimer); - socket.addEventListener('message', resetKeepAliveTimer); - socket.addEventListener('close', this.keepalive.stop.bind(this.keepalive)); - } - - socket.addEventListener('close', function() { - this.closed = true; - }.bind(this)) - - this.close = function(code, reason) { - if (this.closed) { - return; - } - - console.log('WebSocketResource.close()'); - if (!code) { - code = 3000; - } - if (this.keepalive) { - this.keepalive.stop(); - } - - socket.close(code, reason); - socket.onmessage = null; - - // On linux the socket can wait a long time to emit its close event if we've - // lost the internet connection. On the order of minutes. This speeds that - // process up. - setTimeout(function() { - if (this.closed) { - return; - } - this.closed = true; - - console.log('Dispatching our own socket close event'); - var ev = new Event('close'); - ev.code = code; - ev.reason = reason; - this.dispatchEvent(ev); - }.bind(this), 1000); - }; - }; - window.WebSocketResource.prototype = new textsecure.EventTarget(); - - - function KeepAlive(websocketResource, opts) { - if (websocketResource instanceof WebSocketResource) { - opts = opts || {}; - this.path = opts.path; - if (this.path === undefined) { - this.path = '/'; - } - this.disconnect = opts.disconnect; - if (this.disconnect === undefined) { - this.disconnect = true; - } - this.wsr = websocketResource; - } else { - throw new TypeError('KeepAlive expected a WebSocketResource'); - } + if (this.id === undefined) { + var bits = new Uint32Array(2); + window.crypto.getRandomValues(bits); + this.id = dcodeIO.Long.fromBits(bits[0], bits[1], true); } - KeepAlive.prototype = { - constructor: KeepAlive, - stop: function() { - clearTimeout(this.keepAliveTimer); - clearTimeout(this.disconnectTimer); - }, - reset: function() { - clearTimeout(this.keepAliveTimer); - clearTimeout(this.disconnectTimer); - this.keepAliveTimer = setTimeout(function() { - if (this.disconnect) { - // automatically disconnect if server doesn't ack - this.disconnectTimer = setTimeout(function() { - clearTimeout(this.keepAliveTimer); - this.wsr.close(3001, 'No response to keepalive request'); - }.bind(this), 1000); - } else { - this.reset(); - } - console.log('Sending a keepalive message'); - this.wsr.sendRequest({ - verb: 'GET', - path: this.path, - success: this.reset.bind(this) - }); - }.bind(this), 55000); + if (this.body === undefined) { + this.body = null; + } + }; + + var IncomingWebSocketRequest = function(options) { + var request = new Request(options); + var socket = options.socket; + + this.verb = request.verb; + this.path = request.path; + this.body = request.body; + + this.respond = function(status, message) { + socket.send( + new textsecure.protobuf.WebSocketMessage({ + type: textsecure.protobuf.WebSocketMessage.Type.RESPONSE, + response: { id: request.id, message: message, status: status }, + }) + .encode() + .toArrayBuffer() + ); + }; + }; + + var outgoing = {}; + var OutgoingWebSocketRequest = function(options, socket) { + var request = new Request(options); + outgoing[request.id] = request; + socket.send( + new textsecure.protobuf.WebSocketMessage({ + type: textsecure.protobuf.WebSocketMessage.Type.REQUEST, + request: { + verb: request.verb, + path: request.path, + body: request.body, + id: request.id, }, + }) + .encode() + .toArrayBuffer() + ); + }; + + window.WebSocketResource = function(socket, opts) { + opts = opts || {}; + var handleRequest = opts.handleRequest; + if (typeof handleRequest !== 'function') { + handleRequest = function(request) { + request.respond(404, 'Not found'); + }; + } + this.sendRequest = function(options) { + return new OutgoingWebSocketRequest(options, socket); }; -}()); + socket.onmessage = function(socketMessage) { + var blob = socketMessage.data; + var handleArrayBuffer = function(buffer) { + var message = textsecure.protobuf.WebSocketMessage.decode(buffer); + if ( + message.type === textsecure.protobuf.WebSocketMessage.Type.REQUEST + ) { + handleRequest( + new IncomingWebSocketRequest({ + verb: message.request.verb, + path: message.request.path, + body: message.request.body, + id: message.request.id, + socket: socket, + }) + ); + } else if ( + message.type === textsecure.protobuf.WebSocketMessage.Type.RESPONSE + ) { + var response = message.response; + var request = outgoing[response.id]; + if (request) { + request.response = response; + var callback = request.error; + if (response.status >= 200 && response.status < 300) { + callback = request.success; + } + + if (typeof callback === 'function') { + callback(response.message, response.status, request); + } + } else { + throw 'Received response for unknown request ' + + message.response.id; + } + } + }; + + if (blob instanceof ArrayBuffer) { + handleArrayBuffer(blob); + } else { + var reader = new FileReader(); + reader.onload = function() { + handleArrayBuffer(reader.result); + }; + reader.readAsArrayBuffer(blob); + } + }; + + if (opts.keepalive) { + this.keepalive = new KeepAlive(this, { + path: opts.keepalive.path, + disconnect: opts.keepalive.disconnect, + }); + var resetKeepAliveTimer = this.keepalive.reset.bind(this.keepalive); + socket.addEventListener('open', resetKeepAliveTimer); + socket.addEventListener('message', resetKeepAliveTimer); + socket.addEventListener( + 'close', + this.keepalive.stop.bind(this.keepalive) + ); + } + + socket.addEventListener( + 'close', + function() { + this.closed = true; + }.bind(this) + ); + + this.close = function(code, reason) { + if (this.closed) { + return; + } + + console.log('WebSocketResource.close()'); + if (!code) { + code = 3000; + } + if (this.keepalive) { + this.keepalive.stop(); + } + + socket.close(code, reason); + socket.onmessage = null; + + // On linux the socket can wait a long time to emit its close event if we've + // lost the internet connection. On the order of minutes. This speeds that + // process up. + setTimeout( + function() { + if (this.closed) { + return; + } + this.closed = true; + + console.log('Dispatching our own socket close event'); + var ev = new Event('close'); + ev.code = code; + ev.reason = reason; + this.dispatchEvent(ev); + }.bind(this), + 1000 + ); + }; + }; + window.WebSocketResource.prototype = new textsecure.EventTarget(); + + function KeepAlive(websocketResource, opts) { + if (websocketResource instanceof WebSocketResource) { + opts = opts || {}; + this.path = opts.path; + if (this.path === undefined) { + this.path = '/'; + } + this.disconnect = opts.disconnect; + if (this.disconnect === undefined) { + this.disconnect = true; + } + this.wsr = websocketResource; + } else { + throw new TypeError('KeepAlive expected a WebSocketResource'); + } + } + + KeepAlive.prototype = { + constructor: KeepAlive, + stop: function() { + clearTimeout(this.keepAliveTimer); + clearTimeout(this.disconnectTimer); + }, + reset: function() { + clearTimeout(this.keepAliveTimer); + clearTimeout(this.disconnectTimer); + this.keepAliveTimer = setTimeout( + function() { + if (this.disconnect) { + // automatically disconnect if server doesn't ack + this.disconnectTimer = setTimeout( + function() { + clearTimeout(this.keepAliveTimer); + this.wsr.close(3001, 'No response to keepalive request'); + }.bind(this), + 1000 + ); + } else { + this.reset(); + } + console.log('Sending a keepalive message'); + this.wsr.sendRequest({ + verb: 'GET', + path: this.path, + success: this.reset.bind(this), + }); + }.bind(this), + 55000 + ); + }, + }; +})();