Now we've got everything prettified!

This commit is contained in:
Scott Nonnenberg 2018-05-02 09:51:22 -07:00
parent 754d65ae2e
commit a0ed993b42
30 changed files with 3562 additions and 2873 deletions

View file

@ -18,6 +18,8 @@ ts/**/*.js
components/* components/*
dist/* dist/*
libtextsecure/libsignal-protocol.js libtextsecure/libsignal-protocol.js
test/fixtures.js
test/blanket_mocha.js
/**/*.json /**/*.json
/**/*.css /**/*.css

View file

@ -8,7 +8,7 @@ ProvisioningCipher.prototype = {
var masterEphemeral = provisionEnvelope.publicKey.toArrayBuffer(); var masterEphemeral = provisionEnvelope.publicKey.toArrayBuffer();
var message = provisionEnvelope.body.toArrayBuffer(); var message = provisionEnvelope.body.toArrayBuffer();
if (new Uint8Array(message)[0] != 1) { if (new Uint8Array(message)[0] != 1) {
throw new Error("Bad version number on ProvisioningMessage"); throw new Error('Bad version number on ProvisioningMessage');
} }
var iv = message.slice(1, 16 + 1); var iv = message.slice(1, 16 + 1);
@ -16,27 +16,37 @@ ProvisioningCipher.prototype = {
var ivAndCiphertext = message.slice(0, message.byteLength - 32); var ivAndCiphertext = message.slice(0, message.byteLength - 32);
var ciphertext = message.slice(16 + 1, message.byteLength - 32); var ciphertext = message.slice(16 + 1, message.byteLength - 32);
return libsignal.Curve.async.calculateAgreement( return libsignal.Curve.async
masterEphemeral, this.keyPair.privKey .calculateAgreement(masterEphemeral, this.keyPair.privKey)
).then(function(ecRes) { .then(function(ecRes) {
return libsignal.HKDF.deriveSecrets( return libsignal.HKDF.deriveSecrets(
ecRes, new ArrayBuffer(32), "TextSecure Provisioning Message" ecRes,
new ArrayBuffer(32),
'TextSecure Provisioning Message'
); );
}).then(function(keys) { })
return libsignal.crypto.verifyMAC(ivAndCiphertext, keys[1], mac, 32).then(function() { .then(function(keys) {
return libsignal.crypto
.verifyMAC(ivAndCiphertext, keys[1], mac, 32)
.then(function() {
return libsignal.crypto.decrypt(keys[0], ciphertext, iv); return libsignal.crypto.decrypt(keys[0], ciphertext, iv);
}); });
}).then(function(plaintext) { })
var provisionMessage = textsecure.protobuf.ProvisionMessage.decode(plaintext); .then(function(plaintext) {
var provisionMessage = textsecure.protobuf.ProvisionMessage.decode(
plaintext
);
var privKey = provisionMessage.identityKeyPrivate.toArrayBuffer(); var privKey = provisionMessage.identityKeyPrivate.toArrayBuffer();
return libsignal.Curve.async.createKeyPair(privKey).then(function(keyPair) { return libsignal.Curve.async
.createKeyPair(privKey)
.then(function(keyPair) {
var ret = { var ret = {
identityKeyPair: keyPair, identityKeyPair: keyPair,
number: provisionMessage.number, number: provisionMessage.number,
provisioningCode: provisionMessage.provisioningCode, provisioningCode: provisionMessage.provisioningCode,
userAgent: provisionMessage.userAgent, userAgent: provisionMessage.userAgent,
readReceipts : provisionMessage.readReceipts readReceipts: provisionMessage.readReceipts,
}; };
if (provisionMessage.profileKey) { if (provisionMessage.profileKey) {
ret.profileKey = provisionMessage.profileKey.toArrayBuffer(); ret.profileKey = provisionMessage.profileKey.toArrayBuffer();
@ -46,16 +56,24 @@ ProvisioningCipher.prototype = {
}); });
}, },
getPublicKey: function() { getPublicKey: function() {
return Promise.resolve().then(function() { return Promise.resolve()
.then(
function() {
if (!this.keyPair) { if (!this.keyPair) {
return libsignal.Curve.async.generateKeyPair().then(function(keyPair) { return libsignal.Curve.async.generateKeyPair().then(
function(keyPair) {
this.keyPair = keyPair; this.keyPair = keyPair;
}.bind(this)); }.bind(this)
);
} }
}.bind(this)).then(function() { }.bind(this)
)
.then(
function() {
return this.keyPair.pubKey; return this.keyPair.pubKey;
}.bind(this)); }.bind(this)
} );
},
}; };
libsignal.ProvisioningCipher = function() { libsignal.ProvisioningCipher = function() {
@ -64,5 +82,4 @@ libsignal.ProvisioningCipher = function() {
this.decrypt = cipher.decrypt.bind(cipher); this.decrypt = cipher.decrypt.bind(cipher);
this.getPublicKey = cipher.getPublicKey.bind(cipher); this.getPublicKey = cipher.getPublicKey.bind(cipher);
}; };
})(); })();

View file

@ -122,9 +122,10 @@ MessageReceiver.prototype.extend({
this.onEmpty(); this.onEmpty();
} }
// possible 403 or network issue. Make an request to confirm // 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 .then(this.connect.bind(this)) // No HTTP error? Reconnect
.catch((e) => { .catch(e => {
const event = new Event('error'); const event = new Event('error');
event.error = e; event.error = e;
return this.dispatchAndWait(event); return this.dispatchAndWait(event);
@ -146,10 +147,9 @@ MessageReceiver.prototype.extend({
return; return;
} }
const promise = textsecure.crypto.decryptWebsocketMessage( const promise = textsecure.crypto
request.body, .decryptWebsocketMessage(request.body, this.signalingKey)
this.signalingKey .then(plaintext => {
).then((plaintext) => {
const envelope = textsecure.protobuf.Envelope.decode(plaintext); const envelope = textsecure.protobuf.Envelope.decode(plaintext);
// After this point, decoding errors are not the server's // After this point, decoding errors are not the server's
// fault, and we should handle them gracefully and tell the // fault, and we should handle them gracefully and tell the
@ -159,18 +159,25 @@ MessageReceiver.prototype.extend({
return request.respond(200, 'OK'); return request.respond(200, 'OK');
} }
return this.addToCache(envelope, plaintext).then(() => { return this.addToCache(envelope, plaintext).then(
() => {
request.respond(200, 'OK'); request.respond(200, 'OK');
this.queueEnvelope(envelope); this.queueEnvelope(envelope);
}, (error) => { },
error => {
console.log( console.log(
'handleRequest error trying to add message to cache:', 'handleRequest error trying to add message to cache:',
error && error.stack ? error.stack : error error && error.stack ? error.stack : error
); );
}); }
}).catch((e) => { );
})
.catch(e => {
request.respond(500, 'Bad encrypted websocket message'); request.respond(500, 'Bad encrypted websocket message');
console.log('Error handling incoming message:', e && e.stack ? e.stack : e); console.log(
'Error handling incoming message:',
e && e.stack ? e.stack : e
);
const ev = new Event('error'); const ev = new Event('error');
ev.error = e; ev.error = e;
return this.dispatchAndWait(ev); return this.dispatchAndWait(ev);
@ -203,7 +210,7 @@ MessageReceiver.prototype.extend({
this.incoming = []; this.incoming = [];
const dispatchEmpty = () => { const dispatchEmpty = () => {
console.log('MessageReceiver: emitting \'empty\' event'); console.log("MessageReceiver: emitting 'empty' event");
const ev = new Event('empty'); const ev = new Event('empty');
return this.dispatchAndWait(ev); return this.dispatchAndWait(ev);
}; };
@ -224,7 +231,8 @@ MessageReceiver.prototype.extend({
const { incoming } = this; const { incoming } = this;
this.incoming = []; this.incoming = [];
const queueDispatch = () => this.addToQueue(() => { const queueDispatch = () =>
this.addToQueue(() => {
console.log('drained'); console.log('drained');
}); });
@ -241,7 +249,7 @@ MessageReceiver.prototype.extend({
this.dispatchEvent(ev); this.dispatchEvent(ev);
}, },
queueAllCached() { queueAllCached() {
return this.getAllFromCache().then((items) => { return this.getAllFromCache().then(items => {
for (let i = 0, max = items.length; i < max; i += 1) { for (let i = 0, max = items.length; i < max; i += 1) {
this.queueCached(items[i]); this.queueCached(items[i]);
} }
@ -273,7 +281,9 @@ MessageReceiver.prototype.extend({
} }
}, },
getEnvelopeId(envelope) { getEnvelopeId(envelope) {
return `${envelope.source}.${envelope.sourceDevice} ${envelope.timestamp.toNumber()}`; return `${envelope.source}.${
envelope.sourceDevice
} ${envelope.timestamp.toNumber()}`;
}, },
stringToArrayBuffer(string) { stringToArrayBuffer(string) {
// eslint-disable-next-line new-cap // eslint-disable-next-line new-cap
@ -281,23 +291,28 @@ MessageReceiver.prototype.extend({
}, },
getAllFromCache() { getAllFromCache() {
console.log('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'); console.log('getAllFromCache loaded', items.length, 'saved envelopes');
return Promise.all(_.map(items, (item) => { return Promise.all(
_.map(items, item => {
const attempts = 1 + (item.attempts || 0); const attempts = 1 + (item.attempts || 0);
if (attempts >= 5) { if (attempts >= 5) {
console.log('getAllFromCache final attempt for envelope', item.id); console.log('getAllFromCache final attempt for envelope', item.id);
return textsecure.storage.unprocessed.remove(item.id); return textsecure.storage.unprocessed.remove(item.id);
} }
return textsecure.storage.unprocessed.update(item.id, { attempts }); return textsecure.storage.unprocessed.update(item.id, { attempts });
})).then(() => items, (error) => { })
).then(
() => items,
error => {
console.log( console.log(
'getAllFromCache error updating items after load:', 'getAllFromCache error updating items after load:',
error && error.stack ? error.stack : error error && error.stack ? error.stack : error
); );
return items; return items;
}); }
);
}); });
}, },
addToCache(envelope, plaintext) { addToCache(envelope, plaintext) {
@ -332,7 +347,7 @@ MessageReceiver.prototype.extend({
); );
const promise = this.addToQueue(taskWithTimeout); const promise = this.addToQueue(taskWithTimeout);
return promise.catch((error) => { return promise.catch(error => {
console.log( console.log(
'queueDecryptedEnvelope error handling envelope', 'queueDecryptedEnvelope error handling envelope',
id, id,
@ -346,10 +361,13 @@ MessageReceiver.prototype.extend({
console.log('queueing envelope', id); console.log('queueing envelope', id);
const task = this.handleEnvelope.bind(this, envelope); 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); const promise = this.addToQueue(taskWithTimeout);
return promise.catch((error) => { return promise.catch(error => {
console.log( console.log(
'queueEnvelope error handling envelope', 'queueEnvelope error handling envelope',
id, id,
@ -448,26 +466,36 @@ MessageReceiver.prototype.extend({
switch (envelope.type) { switch (envelope.type) {
case textsecure.protobuf.Envelope.Type.CIPHERTEXT: case textsecure.protobuf.Envelope.Type.CIPHERTEXT:
console.log('message from', this.getEnvelopeId(envelope)); console.log('message from', this.getEnvelopeId(envelope));
promise = sessionCipher.decryptWhisperMessage(ciphertext).then(this.unpad); promise = sessionCipher
.decryptWhisperMessage(ciphertext)
.then(this.unpad);
break; break;
case textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE: case textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE:
console.log('prekey message from', this.getEnvelopeId(envelope)); console.log('prekey message from', this.getEnvelopeId(envelope));
promise = this.decryptPreKeyWhisperMessage(ciphertext, sessionCipher, address); promise = this.decryptPreKeyWhisperMessage(
ciphertext,
sessionCipher,
address
);
break; break;
default: default:
promise = Promise.reject(new Error('Unknown message type')); promise = Promise.reject(new Error('Unknown message type'));
} }
return promise.then(plaintext => this.updateCache( return promise
envelope, .then(plaintext =>
plaintext this.updateCache(envelope, plaintext).then(
).then(() => plaintext, (error) => { () => plaintext,
error => {
console.log( console.log(
'decrypt failed to save decrypted message contents to cache:', 'decrypt failed to save decrypted message contents to cache:',
error && error.stack ? error.stack : error error && error.stack ? error.stack : error
); );
return plaintext; return plaintext;
})).catch((error) => { }
)
)
.catch(error => {
let errorToThrow = error; let errorToThrow = error;
if (error.message === 'Unknown identity key') { if (error.message === 'Unknown identity key') {
@ -508,17 +536,20 @@ MessageReceiver.prototype.extend({
throw e; throw e;
} }
}, },
handleSentMessage(envelope, destination, timestamp, msg, expirationStartTimestamp) { handleSentMessage(
envelope,
destination,
timestamp,
msg,
expirationStartTimestamp
) {
let p = Promise.resolve(); let p = Promise.resolve();
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
if (msg.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) { if (msg.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) {
p = this.handleEndSession(destination); p = this.handleEndSession(destination);
} }
return p.then(() => this.processDecrypted( return p.then(() =>
envelope, this.processDecrypted(envelope, msg, this.number).then(message => {
msg,
this.number
).then((message) => {
const ev = new Event('sent'); const ev = new Event('sent');
ev.confirm = this.removeFromCache.bind(this, envelope); ev.confirm = this.removeFromCache.bind(this, envelope);
ev.data = { ev.data = {
@ -531,7 +562,8 @@ MessageReceiver.prototype.extend({
ev.data.expirationStartTimestamp = expirationStartTimestamp.toNumber(); ev.data.expirationStartTimestamp = expirationStartTimestamp.toNumber();
} }
return this.dispatchAndWait(ev); return this.dispatchAndWait(ev);
})); })
);
}, },
handleDataMessage(envelope, msg) { handleDataMessage(envelope, msg) {
console.log('data message from', this.getEnvelopeId(envelope)); console.log('data message from', this.getEnvelopeId(envelope));
@ -540,11 +572,8 @@ MessageReceiver.prototype.extend({
if (msg.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) { if (msg.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) {
p = this.handleEndSession(envelope.source); p = this.handleEndSession(envelope.source);
} }
return p.then(() => this.processDecrypted( return p.then(() =>
envelope, this.processDecrypted(envelope, msg, envelope.source).then(message => {
msg,
envelope.source
).then((message) => {
const ev = new Event('message'); const ev = new Event('message');
ev.confirm = this.removeFromCache.bind(this, envelope); ev.confirm = this.removeFromCache.bind(this, envelope);
ev.data = { ev.data = {
@ -555,23 +584,22 @@ MessageReceiver.prototype.extend({
message, message,
}; };
return this.dispatchAndWait(ev); return this.dispatchAndWait(ev);
})); })
);
}, },
handleLegacyMessage(envelope) { handleLegacyMessage(envelope) {
return this.decrypt( return this.decrypt(envelope, envelope.legacyMessage).then(plaintext =>
envelope, this.innerHandleLegacyMessage(envelope, plaintext)
envelope.legacyMessage );
).then(plaintext => this.innerHandleLegacyMessage(envelope, plaintext));
}, },
innerHandleLegacyMessage(envelope, plaintext) { innerHandleLegacyMessage(envelope, plaintext) {
const message = textsecure.protobuf.DataMessage.decode(plaintext); const message = textsecure.protobuf.DataMessage.decode(plaintext);
return this.handleDataMessage(envelope, message); return this.handleDataMessage(envelope, message);
}, },
handleContentMessage(envelope) { handleContentMessage(envelope) {
return this.decrypt( return this.decrypt(envelope, envelope.content).then(plaintext =>
envelope, this.innerHandleContentMessage(envelope, plaintext)
envelope.content );
).then(plaintext => this.innerHandleContentMessage(envelope, plaintext));
}, },
innerHandleContentMessage(envelope, plaintext) { innerHandleContentMessage(envelope, plaintext) {
const content = textsecure.protobuf.Content.decode(plaintext); const content = textsecure.protobuf.Content.decode(plaintext);
@ -595,7 +623,9 @@ MessageReceiver.prototype.extend({
}, },
handleReceiptMessage(envelope, receiptMessage) { handleReceiptMessage(envelope, receiptMessage) {
const results = []; 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) { for (let i = 0; i < receiptMessage.timestamp.length; i += 1) {
const ev = new Event('delivery'); const ev = new Event('delivery');
ev.confirm = this.removeFromCache.bind(this, envelope); ev.confirm = this.removeFromCache.bind(this, envelope);
@ -606,7 +636,9 @@ MessageReceiver.prototype.extend({
}; };
results.push(this.dispatchAndWait(ev)); 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) { for (let i = 0; i < receiptMessage.timestamp.length; i += 1) {
const ev = new Event('read'); const ev = new Event('read');
ev.confirm = this.removeFromCache.bind(this, envelope); ev.confirm = this.removeFromCache.bind(this, envelope);
@ -734,12 +766,13 @@ MessageReceiver.prototype.extend({
let groupDetails = groupBuffer.next(); let groupDetails = groupBuffer.next();
const promises = []; const promises = [];
while (groupDetails !== undefined) { while (groupDetails !== undefined) {
const getGroupDetails = (details) => { const getGroupDetails = details => {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
details.id = details.id.toBinary(); details.id = details.id.toBinary();
if (details.active) { if (details.active) {
return textsecure.storage.groups.getGroup(details.id) return textsecure.storage.groups
.then((existingGroup) => { .getGroup(details.id)
.then(existingGroup => {
if (existingGroup === undefined) { if (existingGroup === undefined) {
return textsecure.storage.groups.createNewGroup( return textsecure.storage.groups.createNewGroup(
details.members, details.members,
@ -750,17 +783,20 @@ MessageReceiver.prototype.extend({
details.id, details.id,
details.members details.members
); );
}).then(() => details); })
.then(() => details);
} }
return Promise.resolve(details); return Promise.resolve(details);
}; };
const promise = getGroupDetails(groupDetails).then((details) => { const promise = getGroupDetails(groupDetails)
.then(details => {
const ev = new Event('group'); const ev = new Event('group');
ev.confirm = this.removeFromCache.bind(this, envelope); ev.confirm = this.removeFromCache.bind(this, envelope);
ev.groupDetails = details; ev.groupDetails = details;
return this.dispatchAndWait(ev); return this.dispatchAndWait(ev);
}).catch((e) => { })
.catch(e => {
console.log('error processing group', e); console.log('error processing group', e);
}); });
groupDetails = groupBuffer.next(); groupDetails = groupBuffer.next();
@ -803,7 +839,8 @@ MessageReceiver.prototype.extend({
attachment.data = data; attachment.data = data;
} }
return this.server.getAttachment(attachment.id) return this.server
.getAttachment(attachment.id)
.then(decryptAttachment) .then(decryptAttachment)
.then(updateAttachment); .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 // It's most likely that dataMessage will be populated, so we look at it in detail
const data = content.dataMessage; const data = content.dataMessage;
if (data && !data.attachments.length && !data.body && !data.expireTimer && if (
!data.flags && !data.group) { data &&
!data.attachments.length &&
!data.body &&
!data.expireTimer &&
!data.flags &&
!data.group
) {
return false; return false;
} }
@ -857,7 +900,7 @@ MessageReceiver.prototype.extend({
ciphertext, ciphertext,
sessionCipher, sessionCipher,
address address
).then((plaintext) => { ).then(plaintext => {
const envelope = { const envelope = {
source: number, source: number,
sourceDevice: device, sourceDevice: device,
@ -901,7 +944,8 @@ MessageReceiver.prototype.extend({
console.log('got end session'); console.log('got end session');
const deviceIds = await textsecure.storage.protocol.getDeviceIds(number); const deviceIds = await textsecure.storage.protocol.getDeviceIds(number);
return Promise.all(deviceIds.map((deviceId) => { return Promise.all(
deviceIds.map(deviceId => {
const address = new libsignal.SignalProtocolAddress(number, deviceId); const address = new libsignal.SignalProtocolAddress(number, deviceId);
const sessionCipher = new libsignal.SessionCipher( const sessionCipher = new libsignal.SessionCipher(
textsecure.storage.protocol, textsecure.storage.protocol,
@ -910,7 +954,8 @@ MessageReceiver.prototype.extend({
console.log('deleting sessions for', address.toString()); console.log('deleting sessions for', address.toString());
return sessionCipher.deleteAllSessionsForDevice(); return sessionCipher.deleteAllSessionsForDevice();
})); })
);
}, },
processDecrypted(envelope, decrypted, source) { processDecrypted(envelope, decrypted, source) {
/* eslint-disable no-bitwise, no-param-reassign */ /* eslint-disable no-bitwise, no-param-reassign */
@ -928,7 +973,6 @@ MessageReceiver.prototype.extend({
decrypted.expireTimer = 0; decrypted.expireTimer = 0;
} }
if (decrypted.flags & FLAGS.END_SESSION) { if (decrypted.flags & FLAGS.END_SESSION) {
decrypted.body = null; decrypted.body = null;
decrypted.attachments = []; decrypted.attachments = [];
@ -949,7 +993,9 @@ MessageReceiver.prototype.extend({
if (decrypted.group !== null) { if (decrypted.group !== null) {
decrypted.group.id = decrypted.group.id.toBinary(); 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) { if (decrypted.group.avatar !== null) {
promises.push(this.handleAttachment(decrypted.group.avatar)); promises.push(this.handleAttachment(decrypted.group.avatar));
} }
@ -957,9 +1003,13 @@ MessageReceiver.prototype.extend({
const storageGroups = textsecure.storage.groups; const storageGroups = textsecure.storage.groups;
promises.push(storageGroups.getNumbers(decrypted.group.id).then((existingGroup) => { promises.push(
storageGroups.getNumbers(decrypted.group.id).then(existingGroup => {
if (existingGroup === undefined) { if (existingGroup === undefined) {
if (decrypted.group.type !== textsecure.protobuf.GroupContext.Type.UPDATE) { if (
decrypted.group.type !==
textsecure.protobuf.GroupContext.Type.UPDATE
) {
decrypted.group.members = [source]; decrypted.group.members = [source];
console.log('Got message for unknown group'); console.log('Got message for unknown group');
} }
@ -972,7 +1022,9 @@ MessageReceiver.prototype.extend({
if (fromIndex < 0) { if (fromIndex < 0) {
// TODO: This could be indication of a race... // TODO: This could be indication of a race...
console.log('Sender was not a member of the group they were sending from'); console.log(
'Sender was not a member of the group they were sending from'
);
} }
switch (decrypted.group.type) { switch (decrypted.group.type) {
@ -987,9 +1039,14 @@ MessageReceiver.prototype.extend({
decrypted.body = null; decrypted.body = null;
decrypted.attachments = []; decrypted.attachments = [];
if (source === this.number) { if (source === this.number) {
return textsecure.storage.groups.deleteGroup(decrypted.group.id); return textsecure.storage.groups.deleteGroup(
decrypted.group.id
);
} }
return textsecure.storage.groups.removeNumber(decrypted.group.id, source); return textsecure.storage.groups.removeNumber(
decrypted.group.id,
source
);
case textsecure.protobuf.GroupContext.Type.DELIVER: case textsecure.protobuf.GroupContext.Type.DELIVER:
decrypted.group.name = null; decrypted.group.name = null;
decrypted.group.members = []; decrypted.group.members = [];
@ -999,7 +1056,8 @@ MessageReceiver.prototype.extend({
this.removeFromCache(envelope); this.removeFromCache(envelope);
throw new Error('Unknown group message type'); throw new Error('Unknown group message type');
} }
})); })
);
} }
for (let i = 0, max = decrypted.attachments.length; i < max; i += 1) { for (let i = 0, max = decrypted.attachments.length; i < max; i += 1) {
@ -1021,12 +1079,14 @@ MessageReceiver.prototype.extend({
if (thumbnail) { if (thumbnail) {
// We don't want the failure of a thumbnail download to fail the handling of // We don't want the failure of a thumbnail download to fail the handling of
// this message entirely, like we do for full attachments. // this message entirely, like we do for full attachments.
promises.push(this.handleAttachment(thumbnail).catch((error) => { promises.push(
this.handleAttachment(thumbnail).catch(error => {
console.log( console.log(
'Problem loading thumbnail for quote', 'Problem loading thumbnail for quote',
error && error.stack ? error.stack : error error && error.stack ? error.stack : error
); );
})); })
);
} }
} }
} }
@ -1052,8 +1112,12 @@ textsecure.MessageReceiver = function MessageReceiverWrapper(
signalingKey, signalingKey,
options options
); );
this.addEventListener = messageReceiver.addEventListener.bind(messageReceiver); this.addEventListener = messageReceiver.addEventListener.bind(
this.removeEventListener = messageReceiver.removeEventListener.bind(messageReceiver); messageReceiver
);
this.removeEventListener = messageReceiver.removeEventListener.bind(
messageReceiver
);
this.getStatus = messageReceiver.getStatus.bind(messageReceiver); this.getStatus = messageReceiver.getStatus.bind(messageReceiver);
this.close = messageReceiver.close.bind(messageReceiver); this.close = messageReceiver.close.bind(messageReceiver);
messageReceiver.connect(); messageReceiver.connect();
@ -1067,4 +1131,3 @@ textsecure.MessageReceiver = function MessageReceiverWrapper(
textsecure.MessageReceiver.prototype = { textsecure.MessageReceiver.prototype = {
constructor: textsecure.MessageReceiver, constructor: textsecure.MessageReceiver,
}; };

View file

@ -1,4 +1,11 @@
function OutgoingMessage(server, timestamp, numbers, message, silent, callback) { function OutgoingMessage(
server,
timestamp,
numbers,
message,
silent,
callback
) {
if (message instanceof textsecure.protobuf.DataMessage) { if (message instanceof textsecure.protobuf.DataMessage) {
var content = new textsecure.protobuf.Content(); var content = new textsecure.protobuf.Content();
content.dataMessage = message; content.dataMessage = message;
@ -21,12 +28,20 @@ OutgoingMessage.prototype = {
numberCompleted: function() { numberCompleted: function() {
this.numbersCompleted++; this.numbersCompleted++;
if (this.numbersCompleted >= this.numbers.length) { if (this.numbersCompleted >= this.numbers.length) {
this.callback({successfulNumbers: this.successfulNumbers, errors: this.errors}); this.callback({
successfulNumbers: this.successfulNumbers,
errors: this.errors,
});
} }
}, },
registerError: function(number, reason, error) { registerError: function(number, reason, error) {
if (!error || error.name === 'HTTPError' && error.code !== 404) { if (!error || (error.name === 'HTTPError' && error.code !== 404)) {
error = new textsecure.OutgoingMessageError(number, this.message.toArrayBuffer(), this.timestamp, error); error = new textsecure.OutgoingMessageError(
number,
this.message.toArrayBuffer(),
this.timestamp,
error
);
} }
error.number = number; error.number = number;
@ -36,44 +51,71 @@ OutgoingMessage.prototype = {
}, },
reloadDevicesAndSend: function(number, recurse) { reloadDevicesAndSend: function(number, recurse) {
return function() { return function() {
return textsecure.storage.protocol.getDeviceIds(number).then(function(deviceIds) { return textsecure.storage.protocol.getDeviceIds(number).then(
function(deviceIds) {
if (deviceIds.length == 0) { if (deviceIds.length == 0) {
return this.registerError(number, "Got empty device list when loading device keys", null); return this.registerError(
number,
'Got empty device list when loading device keys',
null
);
} }
return this.doSendMessage(number, deviceIds, recurse); return this.doSendMessage(number, deviceIds, recurse);
}.bind(this)); }.bind(this)
);
}.bind(this); }.bind(this);
}, },
getKeysForNumber: function(number, updateDevices) { getKeysForNumber: function(number, updateDevices) {
var handleResult = function(response) { var handleResult = function(response) {
return Promise.all(response.devices.map(function(device) { return Promise.all(
response.devices.map(
function(device) {
device.identityKey = response.identityKey; device.identityKey = response.identityKey;
if (updateDevices === undefined || updateDevices.indexOf(device.deviceId) > -1) { if (
var address = new libsignal.SignalProtocolAddress(number, device.deviceId); updateDevices === undefined ||
var builder = new libsignal.SessionBuilder(textsecure.storage.protocol, address); 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) { if (device.registrationId === 0) {
console.log("device registrationId 0!"); console.log('device registrationId 0!');
} }
return builder.processPreKey(device).catch(function(error) { return builder.processPreKey(device).catch(
if (error.message === "Identity key changed") { function(error) {
if (error.message === 'Identity key changed') {
error.timestamp = this.timestamp; error.timestamp = this.timestamp;
error.originalMessage = this.message.toArrayBuffer(); error.originalMessage = this.message.toArrayBuffer();
error.identityKey = device.identityKey; error.identityKey = device.identityKey;
} }
throw error; throw error;
}.bind(this)); }.bind(this)
);
} }
}.bind(this))); }.bind(this)
)
);
}.bind(this); }.bind(this);
if (updateDevices === undefined) { if (updateDevices === undefined) {
return this.server.getKeysForNumber(number).then(handleResult); return this.server.getKeysForNumber(number).then(handleResult);
} else { } else {
var promise = Promise.resolve(); var promise = Promise.resolve();
updateDevices.forEach(function(device) { updateDevices.forEach(
promise = promise.then(function() { function(device) {
return this.server.getKeysForNumber(number, device).then(handleResult).catch(function(e) { promise = promise.then(
function() {
return this.server
.getKeysForNumber(number, device)
.then(handleResult)
.catch(
function(e) {
if (e.name === 'HTTPError' && e.code === 404) { if (e.name === 'HTTPError' && e.code === 404) {
if (device !== 1) { if (device !== 1) {
return this.removeDeviceIdsForNumber(number, [device]); return this.removeDeviceIdsForNumber(number, [device]);
@ -83,16 +125,21 @@ OutgoingMessage.prototype = {
} else { } else {
throw e; throw e;
} }
}.bind(this)); }.bind(this)
}.bind(this)); );
}.bind(this)); }.bind(this)
);
}.bind(this)
);
return promise; return promise;
} }
}, },
transmitMessage: function(number, jsonData, timestamp) { transmitMessage: function(number, jsonData, timestamp) {
return this.server.sendMessages(number, jsonData, timestamp, this.silent).catch(function(e) { return this.server
.sendMessages(number, jsonData, timestamp, this.silent)
.catch(function(e) {
if (e.name === 'HTTPError' && (e.code !== 409 && e.code !== 410)) { if (e.name === 'HTTPError' && (e.code !== 409 && e.code !== 410)) {
// 409 and 410 should bubble and be handled by doSendMessage // 409 and 410 should bubble and be handled by doSendMessage
// 404 should throw UnregisteredUserError // 404 should throw UnregisteredUserError
@ -100,7 +147,12 @@ OutgoingMessage.prototype = {
if (e.code === 404) { if (e.code === 404) {
throw new textsecure.UnregisteredUserError(number, e); throw new textsecure.UnregisteredUserError(number, e);
} }
throw new textsecure.SendMessageNetworkError(number, jsonData, e, timestamp); throw new textsecure.SendMessageNetworkError(
number,
jsonData,
e,
timestamp
);
} }
throw e; throw e;
}); });
@ -133,7 +185,9 @@ OutgoingMessage.prototype = {
var ciphers = {}; var ciphers = {};
var plaintext = this.getPlaintext(); var plaintext = this.getPlaintext();
return Promise.all(deviceIds.map(function(deviceId) { return Promise.all(
deviceIds.map(
function(deviceId) {
var address = new libsignal.SignalProtocolAddress(number, deviceId); var address = new libsignal.SignalProtocolAddress(number, deviceId);
var ourNumber = textsecure.storage.user.getNumber(); var ourNumber = textsecure.storage.user.getNumber();
@ -144,66 +198,114 @@ OutgoingMessage.prototype = {
options.messageKeysLimit = false; options.messageKeysLimit = false;
} }
var sessionCipher = new libsignal.SessionCipher(textsecure.storage.protocol, address, options); var sessionCipher = new libsignal.SessionCipher(
textsecure.storage.protocol,
address,
options
);
ciphers[address.getDeviceId()] = sessionCipher; ciphers[address.getDeviceId()] = sessionCipher;
return sessionCipher.encrypt(plaintext).then(function(ciphertext) { return sessionCipher.encrypt(plaintext).then(function(ciphertext) {
return { return {
type: ciphertext.type, type: ciphertext.type,
destinationDeviceId: address.getDeviceId(), destinationDeviceId: address.getDeviceId(),
destinationRegistrationId: ciphertext.registrationId, destinationRegistrationId: ciphertext.registrationId,
content : btoa(ciphertext.body) content: btoa(ciphertext.body),
}; };
}); });
}.bind(this))).then(function(jsonData) { }.bind(this)
return this.transmitMessage(number, jsonData, this.timestamp).then(function() { )
)
.then(
function(jsonData) {
return this.transmitMessage(number, jsonData, this.timestamp).then(
function() {
this.successfulNumbers[this.successfulNumbers.length] = number; this.successfulNumbers[this.successfulNumbers.length] = number;
this.numberCompleted(); this.numberCompleted();
}.bind(this)); }.bind(this)
}.bind(this)).catch(function(error) { );
if (error instanceof Error && error.name == "HTTPError" && (error.code == 410 || error.code == 409)) { }.bind(this)
)
.catch(
function(error) {
if (
error instanceof Error &&
error.name == 'HTTPError' &&
(error.code == 410 || error.code == 409)
) {
if (!recurse) if (!recurse)
return this.registerError(number, "Hit retry limit attempting to reload device list", error); return this.registerError(
number,
'Hit retry limit attempting to reload device list',
error
);
var p; var p;
if (error.code == 409) { if (error.code == 409) {
p = this.removeDeviceIdsForNumber(number, error.response.extraDevices); p = this.removeDeviceIdsForNumber(
number,
error.response.extraDevices
);
} else { } else {
p = Promise.all(error.response.staleDevices.map(function(deviceId) { p = Promise.all(
error.response.staleDevices.map(function(deviceId) {
return ciphers[deviceId].closeOpenSessionForDevice(); return ciphers[deviceId].closeOpenSessionForDevice();
})); })
);
} }
return p.then(function() { return p.then(
var resetDevices = ((error.code == 410) ? error.response.staleDevices : error.response.missingDevices); function() {
return this.getKeysForNumber(number, resetDevices) var resetDevices =
.then(this.reloadDevicesAndSend(number, error.code == 409)); error.code == 410
}.bind(this)); ? error.response.staleDevices
} else if (error.message === "Identity key changed") { : 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.timestamp = this.timestamp;
error.originalMessage = this.message.toArrayBuffer(); error.originalMessage = this.message.toArrayBuffer();
console.log('Got "key changed" error from encrypt - no identityKey for application layer', number, deviceIds) console.log(
'Got "key changed" error from encrypt - no identityKey for application layer',
number,
deviceIds
);
throw error; throw error;
} else { } else {
this.registerError(number, "Failed to create or send message", error); this.registerError(
number,
'Failed to create or send message',
error
);
} }
}.bind(this)); }.bind(this)
);
}, },
getStaleDeviceIdsForNumber: function(number) { getStaleDeviceIdsForNumber: function(number) {
return textsecure.storage.protocol.getDeviceIds(number).then(function(deviceIds) { return textsecure.storage.protocol
.getDeviceIds(number)
.then(function(deviceIds) {
if (deviceIds.length === 0) { if (deviceIds.length === 0) {
return [1]; return [1];
} }
var updateDevices = []; var updateDevices = [];
return Promise.all(deviceIds.map(function(deviceId) { return Promise.all(
deviceIds.map(function(deviceId) {
var address = new libsignal.SignalProtocolAddress(number, deviceId); var address = new libsignal.SignalProtocolAddress(number, deviceId);
var sessionCipher = new libsignal.SessionCipher(textsecure.storage.protocol, address); var sessionCipher = new libsignal.SessionCipher(
textsecure.storage.protocol,
address
);
return sessionCipher.hasOpenSession().then(function(hasSession) { return sessionCipher.hasOpenSession().then(function(hasSession) {
if (!hasSession) { if (!hasSession) {
updateDevices.push(deviceId); updateDevices.push(deviceId);
} }
}); });
})).then(function() { })
).then(function() {
return updateDevices; return updateDevices;
}); });
}); });
@ -213,7 +315,7 @@ OutgoingMessage.prototype = {
var promise = Promise.resolve(); var promise = Promise.resolve();
for (var j in deviceIdsToRemove) { for (var j in deviceIdsToRemove) {
promise = promise.then(function() { promise = promise.then(function() {
var encodedNumber = number + "." + deviceIdsToRemove[j]; var encodedNumber = number + '.' + deviceIdsToRemove[j];
return textsecure.storage.protocol.removeSession(encodedNumber); return textsecure.storage.protocol.removeSession(encodedNumber);
}); });
} }
@ -221,21 +323,30 @@ OutgoingMessage.prototype = {
}, },
sendToNumber: function(number) { sendToNumber: function(number) {
return this.getStaleDeviceIdsForNumber(number).then(function(updateDevices) { return this.getStaleDeviceIdsForNumber(number).then(
function(updateDevices) {
return this.getKeysForNumber(number, updateDevices) return this.getKeysForNumber(number, updateDevices)
.then(this.reloadDevicesAndSend(number, true)) .then(this.reloadDevicesAndSend(number, true))
.catch(function(error) { .catch(
if (error.message === "Identity key changed") { function(error) {
if (error.message === 'Identity key changed') {
error = new textsecure.OutgoingIdentityKeyError( error = new textsecure.OutgoingIdentityKeyError(
number, error.originalMessage, error.timestamp, error.identityKey number,
error.originalMessage,
error.timestamp,
error.identityKey
); );
this.registerError(number, "Identity key changed", error); this.registerError(number, 'Identity key changed', error);
} else { } else {
this.registerError( this.registerError(
number, "Failed to retrieve new device keys for number " + number, error number,
'Failed to retrieve new device keys for number ' + number,
error
); );
} }
}.bind(this)); }.bind(this)
}.bind(this)); );
} }.bind(this)
);
},
}; };

View file

@ -1,27 +1,40 @@
;(function() { (function() {
'use strict'; 'use strict';
window.textsecure = window.textsecure || {}; window.textsecure = window.textsecure || {};
window.textsecure.protobuf = {}; window.textsecure.protobuf = {};
function loadProtoBufs(filename) { function loadProtoBufs(filename) {
return dcodeIO.ProtoBuf.loadProtoFile({root: window.PROTO_ROOT, file: filename}, function(error, result) { return dcodeIO.ProtoBuf.loadProtoFile(
{ root: window.PROTO_ROOT, file: filename },
function(error, result) {
if (error) { if (error) {
var text = 'Error loading protos from ' + filename + ' (root: ' + window.PROTO_ROOT + ') ' var text =
+ (error && error.stack ? error.stack : error); 'Error loading protos from ' +
filename +
' (root: ' +
window.PROTO_ROOT +
') ' +
(error && error.stack ? error.stack : error);
console.log(text); console.log(text);
throw error; throw error;
} }
var protos = result.build('signalservice'); var protos = result.build('signalservice');
if (!protos) { if (!protos) {
var text = 'Error loading protos from ' + filename + ' (root: ' + window.PROTO_ROOT + ')'; var text =
'Error loading protos from ' +
filename +
' (root: ' +
window.PROTO_ROOT +
')';
console.log(text); console.log(text);
throw new Error(text); throw new Error(text);
} }
for (var protoName in protos) { for (var protoName in protos) {
textsecure.protobuf[protoName] = protos[protoName]; textsecure.protobuf[protoName] = protos[protoName];
} }
}); }
}; );
}
loadProtoBufs('SignalService.proto'); loadProtoBufs('SignalService.proto');
loadProtoBufs('SubProtocol.proto'); loadProtoBufs('SubProtocol.proto');

View file

@ -1,4 +1,4 @@
;(function() { (function() {
'use strict'; 'use strict';
window.textsecure = window.textsecure || {}; window.textsecure = window.textsecure || {};
window.textsecure.storage = window.textsecure.storage || {}; window.textsecure.storage = window.textsecure.storage || {};

View file

@ -51,17 +51,25 @@ function Message(options) {
} }
} }
if (this.isEndSession()) { if (this.isEndSession()) {
if (this.body !== null || this.group !== null || this.attachments.length !== 0) { if (
this.body !== null ||
this.group !== null ||
this.attachments.length !== 0
) {
throw new Error('Invalid end session message'); throw new Error('Invalid end session message');
} }
} else { } else {
if ( (typeof this.timestamp !== 'number') || if (
(this.body && typeof this.body !== 'string') ) { typeof this.timestamp !== 'number' ||
(this.body && typeof this.body !== 'string')
) {
throw new Error('Invalid message body'); throw new Error('Invalid message body');
} }
if (this.group) { if (this.group) {
if ( (typeof this.group.id !== 'string') || if (
(typeof this.group.type !== 'number') ) { typeof this.group.id !== 'string' ||
typeof this.group.type !== 'number'
) {
throw new Error('Invalid group context'); throw new Error('Invalid group context');
} }
} }
@ -71,7 +79,7 @@ function Message(options) {
Message.prototype = { Message.prototype = {
constructor: Message, constructor: Message,
isEndSession: function() { isEndSession: function() {
return (this.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION); return this.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION;
}, },
toProto: function() { toProto: function() {
if (this.dataMessage instanceof textsecure.protobuf.DataMessage) { if (this.dataMessage instanceof textsecure.protobuf.DataMessage) {
@ -88,10 +96,11 @@ Message.prototype = {
if (this.group) { if (this.group) {
proto.group = new textsecure.protobuf.GroupContext(); proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = stringToArrayBuffer(this.group.id); proto.group.id = stringToArrayBuffer(this.group.id);
proto.group.type = this.group.type proto.group.type = this.group.type;
} }
if (this.quote) { if (this.quote) {
var QuotedAttachment = textsecure.protobuf.DataMessage.Quote.QuotedAttachment; var QuotedAttachment =
textsecure.protobuf.DataMessage.Quote.QuotedAttachment;
var Quote = textsecure.protobuf.DataMessage.Quote; var Quote = textsecure.protobuf.DataMessage.Quote;
proto.quote = new Quote(); proto.quote = new Quote();
@ -100,7 +109,9 @@ Message.prototype = {
quote.id = this.quote.id; quote.id = this.quote.id;
quote.author = this.quote.author; quote.author = this.quote.author;
quote.text = this.quote.text; quote.text = this.quote.text;
quote.attachments = (this.quote.attachments || []).map(function(attachment) { quote.attachments = (this.quote.attachments || []).map(function(
attachment
) {
var quotedAttachment = new QuotedAttachment(); var quotedAttachment = new QuotedAttachment();
quotedAttachment.contentType = attachment.contentType; quotedAttachment.contentType = attachment.contentType;
@ -125,7 +136,7 @@ Message.prototype = {
}, },
toArrayBuffer: function() { toArrayBuffer: function() {
return this.toProto().toArrayBuffer(); return this.toProto().toArrayBuffer();
} },
}; };
function MessageSender(url, username, password, cdn_url) { function MessageSender(url, username, password, cdn_url) {
@ -142,20 +153,29 @@ MessageSender.prototype = {
return Promise.resolve(undefined); return Promise.resolve(undefined);
} }
if (!(attachment.data instanceof ArrayBuffer) && if (
!ArrayBuffer.isView(attachment.data)) { !(attachment.data instanceof ArrayBuffer) &&
return Promise.reject(new TypeError( !ArrayBuffer.isView(attachment.data)
) {
return Promise.reject(
new TypeError(
'`attachment.data` must be an `ArrayBuffer` or `ArrayBufferView`; got: ' + '`attachment.data` must be an `ArrayBuffer` or `ArrayBufferView`; got: ' +
typeof attachment.data typeof attachment.data
)); )
);
} }
var proto = new textsecure.protobuf.AttachmentPointer(); var proto = new textsecure.protobuf.AttachmentPointer();
proto.key = libsignal.crypto.getRandomBytes(64); proto.key = libsignal.crypto.getRandomBytes(64);
var iv = libsignal.crypto.getRandomBytes(16); var iv = libsignal.crypto.getRandomBytes(16);
return textsecure.crypto.encryptAttachment(attachment.data, proto.key, iv).then(function(result) { return textsecure.crypto
return this.server.putAttachment(result.ciphertext).then(function(id) { .encryptAttachment(attachment.data, proto.key, iv)
.then(
function(result) {
return this.server
.putAttachment(result.ciphertext)
.then(function(id) {
proto.id = id; proto.id = id;
proto.contentType = attachment.contentType; proto.contentType = attachment.contentType;
proto.digest = result.digest; proto.digest = result.digest;
@ -170,7 +190,8 @@ MessageSender.prototype = {
} }
return proto; return proto;
}); });
}.bind(this)); }.bind(this)
);
}, },
retransmitMessage: function(number, jsonData, timestamp) { retransmitMessage: function(number, jsonData, timestamp) {
@ -191,7 +212,14 @@ MessageSender.prototype = {
// It's most likely that dataMessage will be populated, so we look at it in detail // It's most likely that dataMessage will be populated, so we look at it in detail
var data = content.dataMessage; var 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; return false;
} }
@ -231,23 +259,33 @@ MessageSender.prototype = {
}, },
queueJobForNumber: function(number, runJob) { queueJobForNumber: function(number, runJob) {
var taskWithTimeout = textsecure.createTaskWithTimeout(runJob, 'queueJobForNumber ' + number); var taskWithTimeout = textsecure.createTaskWithTimeout(
runJob,
'queueJobForNumber ' + number
);
var runPrevious = this.pendingMessages[number] || Promise.resolve(); var runPrevious = this.pendingMessages[number] || Promise.resolve();
var runCurrent = this.pendingMessages[number] = runPrevious.then(taskWithTimeout, taskWithTimeout); var runCurrent = (this.pendingMessages[number] = runPrevious.then(
runCurrent.then(function() { taskWithTimeout,
taskWithTimeout
));
runCurrent.then(
function() {
if (this.pendingMessages[number] === runCurrent) { if (this.pendingMessages[number] === runCurrent) {
delete this.pendingMessages[number]; delete this.pendingMessages[number];
} }
}.bind(this)); }.bind(this)
);
}, },
uploadAttachments: function(message) { uploadAttachments: function(message) {
return Promise.all( return Promise.all(
message.attachments.map(this.makeAttachmentPointer.bind(this)) message.attachments.map(this.makeAttachmentPointer.bind(this))
).then(function(attachmentPointers) { )
.then(function(attachmentPointers) {
message.attachmentPointers = attachmentPointers; message.attachmentPointers = attachmentPointers;
}).catch(function(error) { })
.catch(function(error) {
if (error instanceof Error && error.name === 'HTTPError') { if (error instanceof Error && error.name === 'HTTPError') {
throw new textsecure.MessageError(message, error); throw new textsecure.MessageError(message, error);
} else { } else {
@ -264,7 +302,8 @@ MessageSender.prototype = {
return Promise.resolve(); return Promise.resolve();
} }
return Promise.all(quote.attachments.map(function(attachment) { return Promise.all(
quote.attachments.map(function(attachment) {
const thumbnail = attachment.thumbnail; const thumbnail = attachment.thumbnail;
if (!thumbnail) { if (!thumbnail) {
return; return;
@ -273,7 +312,8 @@ MessageSender.prototype = {
return makePointer(thumbnail).then(function(pointer) { return makePointer(thumbnail).then(function(pointer) {
attachment.attachmentPointer = pointer; attachment.attachmentPointer = pointer;
}); });
})).catch(function(error) { })
).catch(function(error) {
if (error instanceof Error && error.name === 'HTTPError') { if (error instanceof Error && error.name === 'HTTPError') {
throw new textsecure.MessageError(message, error); throw new textsecure.MessageError(message, error);
} else { } else {
@ -287,8 +327,10 @@ MessageSender.prototype = {
return Promise.all([ return Promise.all([
this.uploadAttachments(message), this.uploadAttachments(message),
this.uploadThumbnails(message), this.uploadThumbnails(message),
]).then(function() { ]).then(
return new Promise(function(resolve, reject) { function() {
return new Promise(
function(resolve, reject) {
this.sendMessageProto( this.sendMessageProto(
message.timestamp, message.timestamp,
message.recipients, message.recipients,
@ -302,27 +344,43 @@ MessageSender.prototype = {
} }
} }
); );
}.bind(this)); }.bind(this)
}.bind(this)); );
}.bind(this)
);
}, },
sendMessageProto: function(timestamp, numbers, message, callback, silent) { sendMessageProto: function(timestamp, numbers, message, callback, silent) {
var rejections = textsecure.storage.get('signedKeyRotationRejected', 0); var rejections = textsecure.storage.get('signedKeyRotationRejected', 0);
if (rejections > 5) { if (rejections > 5) {
throw new textsecure.SignedPreKeyRotationError(numbers, message.toArrayBuffer(), timestamp); throw new textsecure.SignedPreKeyRotationError(
numbers,
message.toArrayBuffer(),
timestamp
);
} }
var outgoing = new OutgoingMessage(this.server, timestamp, numbers, message, silent, callback); var outgoing = new OutgoingMessage(
this.server,
timestamp,
numbers,
message,
silent,
callback
);
numbers.forEach(function(number) { numbers.forEach(
function(number) {
this.queueJobForNumber(number, function() { this.queueJobForNumber(number, function() {
return outgoing.sendToNumber(number); return outgoing.sendToNumber(number);
}); });
}.bind(this)); }.bind(this)
);
}, },
retrySendMessageProto: function(numbers, encodedMessage, timestamp) { retrySendMessageProto: function(numbers, encodedMessage, timestamp) {
var proto = textsecure.protobuf.DataMessage.decode(encodedMessage); var proto = textsecure.protobuf.DataMessage.decode(encodedMessage);
return new Promise(function(resolve, reject) { return new Promise(
function(resolve, reject) {
this.sendMessageProto(timestamp, numbers, proto, function(res) { this.sendMessageProto(timestamp, numbers, proto, function(res) {
if (res.errors.length > 0) { if (res.errors.length > 0) {
reject(res); reject(res);
@ -330,11 +388,13 @@ MessageSender.prototype = {
resolve(res); resolve(res);
} }
}); });
}.bind(this)); }.bind(this)
);
}, },
sendIndividualProto: function(number, proto, timestamp, silent) { sendIndividualProto: function(number, proto, timestamp, silent) {
return new Promise(function(resolve, reject) { return new Promise(
function(resolve, reject) {
var callback = function(res) { var callback = function(res) {
if (res.errors.length > 0) { if (res.errors.length > 0) {
reject(res); reject(res);
@ -343,7 +403,8 @@ MessageSender.prototype = {
} }
}; };
this.sendMessageProto(timestamp, [number], proto, callback, silent); this.sendMessageProto(timestamp, [number], proto, callback, silent);
}.bind(this)); }.bind(this)
);
}, },
createSyncMessage: function() { createSyncMessage: function() {
@ -359,14 +420,21 @@ MessageSender.prototype = {
return syncMessage; return syncMessage;
}, },
sendSyncMessage: function(encodedDataMessage, timestamp, destination, expirationStartTimestamp) { sendSyncMessage: function(
encodedDataMessage,
timestamp,
destination,
expirationStartTimestamp
) {
var myNumber = textsecure.storage.user.getNumber(); var myNumber = textsecure.storage.user.getNumber();
var myDevice = textsecure.storage.user.getDeviceId(); var myDevice = textsecure.storage.user.getDeviceId();
if (myDevice == 1) { if (myDevice == 1) {
return Promise.resolve(); return Promise.resolve();
} }
var dataMessage = textsecure.protobuf.DataMessage.decode(encodedDataMessage); var dataMessage = textsecure.protobuf.DataMessage.decode(
encodedDataMessage
);
var sentMessage = new textsecure.protobuf.SyncMessage.Sent(); var sentMessage = new textsecure.protobuf.SyncMessage.Sent();
sentMessage.timestamp = timestamp; sentMessage.timestamp = timestamp;
sentMessage.message = dataMessage; sentMessage.message = dataMessage;
@ -382,7 +450,12 @@ MessageSender.prototype = {
contentMessage.syncMessage = syncMessage; contentMessage.syncMessage = syncMessage;
var silent = true; var silent = true;
return this.sendIndividualProto(myNumber, contentMessage, Date.now(), silent); return this.sendIndividualProto(
myNumber,
contentMessage,
Date.now(),
silent
);
}, },
getProfile: function(number) { getProfile: function(number) {
@ -404,7 +477,12 @@ MessageSender.prototype = {
contentMessage.syncMessage = syncMessage; contentMessage.syncMessage = syncMessage;
var silent = true; var silent = true;
return this.sendIndividualProto(myNumber, contentMessage, Date.now(), silent); return this.sendIndividualProto(
myNumber,
contentMessage,
Date.now(),
silent
);
} }
return Promise.resolve(); return Promise.resolve();
@ -421,7 +499,12 @@ MessageSender.prototype = {
contentMessage.syncMessage = syncMessage; contentMessage.syncMessage = syncMessage;
var silent = true; var silent = true;
return this.sendIndividualProto(myNumber, contentMessage, Date.now(), silent); return this.sendIndividualProto(
myNumber,
contentMessage,
Date.now(),
silent
);
} }
return Promise.resolve(); return Promise.resolve();
@ -439,7 +522,12 @@ MessageSender.prototype = {
contentMessage.syncMessage = syncMessage; contentMessage.syncMessage = syncMessage;
var silent = true; var silent = true;
return this.sendIndividualProto(myNumber, contentMessage, Date.now(), silent); return this.sendIndividualProto(
myNumber,
contentMessage,
Date.now(),
silent
);
} }
return Promise.resolve(); return Promise.resolve();
@ -471,7 +559,12 @@ MessageSender.prototype = {
contentMessage.syncMessage = syncMessage; contentMessage.syncMessage = syncMessage;
var silent = true; var silent = true;
return this.sendIndividualProto(myNumber, contentMessage, Date.now(), silent); return this.sendIndividualProto(
myNumber,
contentMessage,
Date.now(),
silent
);
} }
return Promise.resolve(); return Promise.resolve();
@ -501,7 +594,8 @@ MessageSender.prototype = {
// We want the NullMessage to look like a normal outgoing message; not silent // We want the NullMessage to look like a normal outgoing message; not silent
const promise = this.sendIndividualProto(destination, contentMessage, now); const promise = this.sendIndividualProto(destination, contentMessage, now);
return promise.then(function() { return promise.then(
function() {
var verified = new textsecure.protobuf.Verified(); var verified = new textsecure.protobuf.Verified();
verified.state = state; verified.state = state;
verified.destination = destination; verified.destination = destination;
@ -516,18 +610,22 @@ MessageSender.prototype = {
var silent = true; var silent = true;
return this.sendIndividualProto(myNumber, contentMessage, now, silent); return this.sendIndividualProto(myNumber, contentMessage, now, silent);
}.bind(this)); }.bind(this)
);
}, },
sendGroupProto: function(numbers, proto, timestamp) { sendGroupProto: function(numbers, proto, timestamp) {
timestamp = timestamp || Date.now(); timestamp = timestamp || Date.now();
var me = textsecure.storage.user.getNumber(); var me = textsecure.storage.user.getNumber();
numbers = numbers.filter(function(number) { return number != me; }); numbers = numbers.filter(function(number) {
return number != me;
});
if (numbers.length === 0) { if (numbers.length === 0) {
return Promise.reject(new Error('No other members in the group')); return Promise.reject(new Error('No other members in the group'));
} }
return new Promise(function(resolve, reject) { return new Promise(
function(resolve, reject) {
var silent = true; var silent = true;
var callback = function(res) { var callback = function(res) {
res.dataMessage = proto.toArrayBuffer(); res.dataMessage = proto.toArrayBuffer();
@ -539,10 +637,19 @@ MessageSender.prototype = {
}.bind(this); }.bind(this);
this.sendMessageProto(timestamp, numbers, proto, callback, silent); this.sendMessageProto(timestamp, numbers, proto, callback, silent);
}.bind(this)); }.bind(this)
);
}, },
sendMessageToNumber: function(number, messageText, attachments, quote, timestamp, expireTimer, profileKey) { sendMessageToNumber: function(
number,
messageText,
attachments,
quote,
timestamp,
expireTimer,
profileKey
) {
return this.sendMessage({ return this.sendMessage({
recipients: [number], recipients: [number],
body: messageText, body: messageText,
@ -551,69 +658,87 @@ MessageSender.prototype = {
quote: quote, quote: quote,
needsSync: true, needsSync: true,
expireTimer: expireTimer, expireTimer: expireTimer,
profileKey : profileKey profileKey: profileKey,
}); });
}, },
resetSession: function(number, timestamp) { resetSession: function(number, timestamp) {
console.log('resetting secure session'); console.log('resetting secure session');
var proto = new textsecure.protobuf.DataMessage(); var proto = new textsecure.protobuf.DataMessage();
proto.body = "TERMINATE"; proto.body = 'TERMINATE';
proto.flags = textsecure.protobuf.DataMessage.Flags.END_SESSION; proto.flags = textsecure.protobuf.DataMessage.Flags.END_SESSION;
var logError = function(prefix) { var logError = function(prefix) {
return function(error) { return function(error) {
console.log( console.log(prefix, error && error.stack ? error.stack : error);
prefix,
error && error.stack ? error.stack : error
);
throw error; throw error;
}; };
}; };
var deleteAllSessions = function(number) { var deleteAllSessions = function(number) {
return textsecure.storage.protocol.getDeviceIds(number) return textsecure.storage.protocol
.getDeviceIds(number)
.then(function(deviceIds) { .then(function(deviceIds) {
return Promise.all(deviceIds.map(function(deviceId) { return Promise.all(
var address = new libsignal.SignalProtocolAddress(number, deviceId); deviceIds.map(function(deviceId) {
var address = new libsignal.SignalProtocolAddress(
number,
deviceId
);
console.log('deleting sessions for', address.toString()); console.log('deleting sessions for', address.toString());
var sessionCipher = new libsignal.SessionCipher( var sessionCipher = new libsignal.SessionCipher(
textsecure.storage.protocol, textsecure.storage.protocol,
address address
); );
return sessionCipher.deleteAllSessionsForDevice(); return sessionCipher.deleteAllSessionsForDevice();
})); })
);
}); });
}; };
var sendToContact = deleteAllSessions(number) var sendToContact = deleteAllSessions(number)
.catch(logError('resetSession/deleteAllSessions1 error:')) .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() { .then(function() {
console.log('finished closing local sessions, now sending to contact'); return deleteAllSessions(number).catch(
return this.sendIndividualProto(number, proto, timestamp) logError('resetSession/deleteAllSessions2 error:')
.catch(logError('resetSession/sendToContact error:')) );
}.bind(this))
.then(function() {
return deleteAllSessions(number)
.catch(logError('resetSession/deleteAllSessions2 error:'));
}); });
var buffer = proto.toArrayBuffer(); var buffer = proto.toArrayBuffer();
var sendSync = this.sendSyncMessage(buffer, timestamp, number) var sendSync = this.sendSyncMessage(buffer, timestamp, number).catch(
.catch(logError('resetSession/sendSync error:')); logError('resetSession/sendSync error:')
);
return Promise.all([ return Promise.all([sendToContact, sendSync]);
sendToContact,
sendSync
]);
}, },
sendMessageToGroup: function(groupId, messageText, attachments, quote, timestamp, expireTimer, profileKey) { sendMessageToGroup: function(
return textsecure.storage.groups.getNumbers(groupId).then(function(numbers) { groupId,
messageText,
attachments,
quote,
timestamp,
expireTimer,
profileKey
) {
return textsecure.storage.groups.getNumbers(groupId).then(
function(numbers) {
if (numbers === undefined) if (numbers === undefined)
return Promise.reject(new Error("Unknown Group")); return Promise.reject(new Error('Unknown Group'));
var me = textsecure.storage.user.getNumber(); var me = textsecure.storage.user.getNumber();
numbers = numbers.filter(function(number) { return number != me; }); numbers = numbers.filter(function(number) {
return number != me;
});
if (numbers.length === 0) { if (numbers.length === 0) {
return Promise.reject(new Error('No other members in the group')); return Promise.reject(new Error('No other members in the group'));
} }
@ -629,17 +754,19 @@ MessageSender.prototype = {
profileKey: profileKey, profileKey: profileKey,
group: { group: {
id: groupId, id: groupId,
type: textsecure.protobuf.GroupContext.Type.DELIVER type: textsecure.protobuf.GroupContext.Type.DELIVER,
} },
}); });
}.bind(this)); }.bind(this)
);
}, },
createGroup: function(numbers, name, avatar) { createGroup: function(numbers, name, avatar) {
var proto = new textsecure.protobuf.DataMessage(); var proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext(); proto.group = new textsecure.protobuf.GroupContext();
return textsecure.storage.groups.createNewGroup(numbers).then(function(group) { return textsecure.storage.groups.createNewGroup(numbers).then(
function(group) {
proto.group.id = stringToArrayBuffer(group.id); proto.group.id = stringToArrayBuffer(group.id);
var numbers = group.numbers; var numbers = group.numbers;
@ -647,13 +774,16 @@ MessageSender.prototype = {
proto.group.members = numbers; proto.group.members = numbers;
proto.group.name = name; proto.group.name = name;
return this.makeAttachmentPointer(avatar).then(function(attachment) { return this.makeAttachmentPointer(avatar).then(
function(attachment) {
proto.group.avatar = attachment; proto.group.avatar = attachment;
return this.sendGroupProto(numbers, proto).then(function() { return this.sendGroupProto(numbers, proto).then(function() {
return proto.group.id; return proto.group.id;
}); });
}.bind(this)); }.bind(this)
}.bind(this)); );
}.bind(this)
);
}, },
updateGroup: function(groupId, name, avatar, numbers) { updateGroup: function(groupId, name, avatar, numbers) {
@ -664,19 +794,23 @@ MessageSender.prototype = {
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE; proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
proto.group.name = name; proto.group.name = name;
return textsecure.storage.groups.addNumbers(groupId, numbers).then(function(numbers) { return textsecure.storage.groups.addNumbers(groupId, numbers).then(
function(numbers) {
if (numbers === undefined) { if (numbers === undefined) {
return Promise.reject(new Error("Unknown Group")); return Promise.reject(new Error('Unknown Group'));
} }
proto.group.members = numbers; proto.group.members = numbers;
return this.makeAttachmentPointer(avatar).then(function(attachment) { return this.makeAttachmentPointer(avatar).then(
function(attachment) {
proto.group.avatar = attachment; proto.group.avatar = attachment;
return this.sendGroupProto(numbers, proto).then(function() { return this.sendGroupProto(numbers, proto).then(function() {
return proto.group.id; return proto.group.id;
}); });
}.bind(this)); }.bind(this)
}.bind(this)); );
}.bind(this)
);
}, },
addNumberToGroup: function(groupId, number) { addNumberToGroup: function(groupId, number) {
@ -685,13 +819,15 @@ MessageSender.prototype = {
proto.group.id = stringToArrayBuffer(groupId); proto.group.id = stringToArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE; proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
return textsecure.storage.groups.addNumbers(groupId, [number]).then(function(numbers) { return textsecure.storage.groups.addNumbers(groupId, [number]).then(
function(numbers) {
if (numbers === undefined) if (numbers === undefined)
return Promise.reject(new Error("Unknown Group")); return Promise.reject(new Error('Unknown Group'));
proto.group.members = numbers; proto.group.members = numbers;
return this.sendGroupProto(numbers, proto); return this.sendGroupProto(numbers, proto);
}.bind(this)); }.bind(this)
);
}, },
setGroupName: function(groupId, name) { setGroupName: function(groupId, name) {
@ -701,13 +837,15 @@ MessageSender.prototype = {
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE; proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
proto.group.name = name; proto.group.name = name;
return textsecure.storage.groups.getNumbers(groupId).then(function(numbers) { return textsecure.storage.groups.getNumbers(groupId).then(
function(numbers) {
if (numbers === undefined) if (numbers === undefined)
return Promise.reject(new Error("Unknown Group")); return Promise.reject(new Error('Unknown Group'));
proto.group.members = numbers; proto.group.members = numbers;
return this.sendGroupProto(numbers, proto); return this.sendGroupProto(numbers, proto);
}.bind(this)); }.bind(this)
);
}, },
setGroupAvatar: function(groupId, avatar) { setGroupAvatar: function(groupId, avatar) {
@ -716,16 +854,20 @@ MessageSender.prototype = {
proto.group.id = stringToArrayBuffer(groupId); proto.group.id = stringToArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE; proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
return textsecure.storage.groups.getNumbers(groupId).then(function(numbers) { return textsecure.storage.groups.getNumbers(groupId).then(
function(numbers) {
if (numbers === undefined) if (numbers === undefined)
return Promise.reject(new Error("Unknown Group")); return Promise.reject(new Error('Unknown Group'));
proto.group.members = numbers; proto.group.members = numbers;
return this.makeAttachmentPointer(avatar).then(function(attachment) { return this.makeAttachmentPointer(avatar).then(
function(attachment) {
proto.group.avatar = attachment; proto.group.avatar = attachment;
return this.sendGroupProto(numbers, proto); return this.sendGroupProto(numbers, proto);
}.bind(this)); }.bind(this)
}.bind(this)); );
}.bind(this)
);
}, },
leaveGroup: function(groupId) { leaveGroup: function(groupId) {
@ -734,21 +876,33 @@ MessageSender.prototype = {
proto.group.id = stringToArrayBuffer(groupId); proto.group.id = stringToArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.GroupContext.Type.QUIT; proto.group.type = textsecure.protobuf.GroupContext.Type.QUIT;
return textsecure.storage.groups.getNumbers(groupId).then(function(numbers) { return textsecure.storage.groups
.getNumbers(groupId)
.then(function(numbers) {
if (numbers === undefined) if (numbers === undefined)
return Promise.reject(new Error("Unknown Group")); return Promise.reject(new Error('Unknown Group'));
return textsecure.storage.groups.deleteGroup(groupId).then(function() { return textsecure.storage.groups.deleteGroup(groupId).then(
function() {
return this.sendGroupProto(numbers, proto); return this.sendGroupProto(numbers, proto);
}.bind(this)); }.bind(this)
);
}); });
}, },
sendExpirationTimerUpdateToGroup: function(groupId, expireTimer, timestamp, profileKey) { sendExpirationTimerUpdateToGroup: function(
return textsecure.storage.groups.getNumbers(groupId).then(function(numbers) { groupId,
expireTimer,
timestamp,
profileKey
) {
return textsecure.storage.groups.getNumbers(groupId).then(
function(numbers) {
if (numbers === undefined) if (numbers === undefined)
return Promise.reject(new Error("Unknown Group")); return Promise.reject(new Error('Unknown Group'));
var me = textsecure.storage.user.getNumber(); var me = textsecure.storage.user.getNumber();
numbers = numbers.filter(function(number) { return number != me; }); numbers = numbers.filter(function(number) {
return number != me;
});
if (numbers.length === 0) { if (numbers.length === 0) {
return Promise.reject(new Error('No other members in the group')); return Promise.reject(new Error('No other members in the group'));
} }
@ -761,12 +915,18 @@ MessageSender.prototype = {
flags: textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE, flags: textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
group: { group: {
id: groupId, id: groupId,
type: textsecure.protobuf.GroupContext.Type.DELIVER type: textsecure.protobuf.GroupContext.Type.DELIVER,
}
});
}.bind(this));
}, },
sendExpirationTimerUpdateToNumber: function(number, expireTimer, timestamp, profileKey) { });
}.bind(this)
);
},
sendExpirationTimerUpdateToNumber: function(
number,
expireTimer,
timestamp,
profileKey
) {
var proto = new textsecure.protobuf.DataMessage(); var proto = new textsecure.protobuf.DataMessage();
return this.sendMessage({ return this.sendMessage({
recipients: [number], recipients: [number],
@ -774,25 +934,47 @@ MessageSender.prototype = {
needsSync: true, needsSync: true,
expireTimer: expireTimer, expireTimer: expireTimer,
profileKey: profileKey, profileKey: profileKey,
flags : textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE flags: textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
}); });
} },
}; };
window.textsecure = window.textsecure || {}; window.textsecure = window.textsecure || {};
textsecure.MessageSender = function(url, username, password, cdn_url) { textsecure.MessageSender = function(url, username, password, cdn_url) {
var sender = new MessageSender(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(
textsecure.replay.registerFunction(sender.retransmitMessage.bind(sender), textsecure.replay.Type.TRANSMIT_MESSAGE); sender.tryMessageAgain.bind(sender),
textsecure.replay.registerFunction(sender.sendMessage.bind(sender), textsecure.replay.Type.REBUILD_MESSAGE); textsecure.replay.Type.ENCRYPT_MESSAGE
textsecure.replay.registerFunction(sender.retrySendMessageProto.bind(sender), textsecure.replay.Type.RETRY_SEND_MESSAGE_PROTO); );
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.sendExpirationTimerUpdateToNumber = sender.sendExpirationTimerUpdateToNumber.bind(
this.sendExpirationTimerUpdateToGroup = sender.sendExpirationTimerUpdateToGroup .bind(sender); sender
this.sendRequestGroupSyncMessage = sender.sendRequestGroupSyncMessage .bind(sender); );
this.sendRequestContactSyncMessage = sender.sendRequestContactSyncMessage .bind(sender); this.sendExpirationTimerUpdateToGroup = sender.sendExpirationTimerUpdateToGroup.bind(
this.sendRequestConfigurationSyncMessage = sender.sendRequestConfigurationSyncMessage.bind(sender); 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.sendMessageToNumber = sender.sendMessageToNumber.bind(sender);
this.resetSession = sender.resetSession.bind(sender); this.resetSession = sender.resetSession.bind(sender);
this.sendMessageToGroup = sender.sendMessageToGroup.bind(sender); this.sendMessageToGroup = sender.sendMessageToGroup.bind(sender);
@ -811,5 +993,5 @@ textsecure.MessageSender = function(url, username, password, cdn_url) {
}; };
textsecure.MessageSender.prototype = { textsecure.MessageSender.prototype = {
constructor: textsecure.MessageSender constructor: textsecure.MessageSender,
}; };

View file

@ -1,7 +1,6 @@
'use strict'; 'use strict';
;(function() { (function() {
/************************************************ /************************************************
*** Utilities to store data in local storage *** *** Utilities to store data in local storage ***
************************************************/ ************************************************/
@ -14,20 +13,18 @@
*** Base Storage Routines *** *** Base Storage Routines ***
*****************************/ *****************************/
put: function(key, value) { put: function(key, value) {
if (value === undefined) if (value === undefined) throw new Error('Tried to store undefined');
throw new Error("Tried to store undefined"); localStorage.setItem('' + key, textsecure.utils.jsonThing(value));
localStorage.setItem("" + key, textsecure.utils.jsonThing(value));
}, },
get: function(key, defaultValue) { get: function(key, defaultValue) {
var value = localStorage.getItem("" + key); var value = localStorage.getItem('' + key);
if (value === null) if (value === null) return defaultValue;
return defaultValue;
return JSON.parse(value); return JSON.parse(value);
}, },
remove: function(key) { remove: function(key) {
localStorage.removeItem("" + key); localStorage.removeItem('' + key);
}, },
}; };
@ -43,4 +40,3 @@
return textsecure.storage.impl.remove(key); return textsecure.storage.impl.remove(key);
}; };
})(); })();

View file

@ -1,4 +1,4 @@
;(function() { (function() {
'use strict'; 'use strict';
/********************* /*********************
@ -25,15 +25,19 @@
var groupId = groupId; var groupId = groupId;
return new Promise(function(resolve) { return new Promise(function(resolve) {
if (groupId !== undefined) { if (groupId !== undefined) {
resolve(textsecure.storage.protocol.getGroup(groupId).then(function(group) { resolve(
textsecure.storage.protocol.getGroup(groupId).then(function(group) {
if (group !== undefined) { if (group !== undefined) {
throw new Error("Tried to recreate group"); throw new Error('Tried to recreate group');
} }
})); })
);
} else { } else {
resolve(generateNewGroupId().then(function(newGroupId) { resolve(
generateNewGroupId().then(function(newGroupId) {
groupId = newGroupId; groupId = newGroupId;
})); })
);
} }
}).then(function() { }).then(function() {
var me = textsecure.storage.user.getNumber(); var me = textsecure.storage.user.getNumber();
@ -42,49 +46,54 @@
for (var i in numbers) { for (var i in numbers) {
var number = numbers[i]; var number = numbers[i];
if (!textsecure.utils.isNumberSane(number)) if (!textsecure.utils.isNumberSane(number))
throw new Error("Invalid number in group"); throw new Error('Invalid number in group');
if (number == me) if (number == me) haveMe = true;
haveMe = true; if (finalNumbers.indexOf(number) < 0) finalNumbers.push(number);
if (finalNumbers.indexOf(number) < 0)
finalNumbers.push(number);
} }
if (!haveMe) if (!haveMe) finalNumbers.push(me);
finalNumbers.push(me);
var groupObject = { numbers: finalNumbers, numberRegistrationIds: {} }; var groupObject = { numbers: finalNumbers, numberRegistrationIds: {} };
for (var i in finalNumbers) for (var i in finalNumbers)
groupObject.numberRegistrationIds[finalNumbers[i]] = {}; groupObject.numberRegistrationIds[finalNumbers[i]] = {};
return textsecure.storage.protocol.putGroup(groupId, groupObject).then(function() { return textsecure.storage.protocol
.putGroup(groupId, groupObject)
.then(function() {
return { id: groupId, numbers: finalNumbers }; return { id: groupId, numbers: finalNumbers };
}); });
}); });
}, },
getNumbers: function(groupId) { getNumbers: function(groupId) {
return textsecure.storage.protocol.getGroup(groupId).then(function(group) { return textsecure.storage.protocol
if (group === undefined) .getGroup(groupId)
return undefined; .then(function(group) {
if (group === undefined) return undefined;
return group.numbers; return group.numbers;
}); });
}, },
removeNumber: function(groupId, number) { removeNumber: function(groupId, number) {
return textsecure.storage.protocol.getGroup(groupId).then(function(group) { return textsecure.storage.protocol
if (group === undefined) .getGroup(groupId)
return undefined; .then(function(group) {
if (group === undefined) return undefined;
var me = textsecure.storage.user.getNumber(); var me = textsecure.storage.user.getNumber();
if (number == me) if (number == me)
throw new Error("Cannot remove ourselves from a group, leave the group instead"); throw new Error(
'Cannot remove ourselves from a group, leave the group instead'
);
var i = group.numbers.indexOf(number); var i = group.numbers.indexOf(number);
if (i > -1) { if (i > -1) {
group.numbers.splice(i, 1); group.numbers.splice(i, 1);
delete group.numberRegistrationIds[number]; delete group.numberRegistrationIds[number];
return textsecure.storage.protocol.putGroup(groupId, group).then(function() { return textsecure.storage.protocol
.putGroup(groupId, group)
.then(function() {
return group.numbers; return group.numbers;
}); });
} }
@ -94,21 +103,24 @@
}, },
addNumbers: function(groupId, numbers) { addNumbers: function(groupId, numbers) {
return textsecure.storage.protocol.getGroup(groupId).then(function(group) { return textsecure.storage.protocol
if (group === undefined) .getGroup(groupId)
return undefined; .then(function(group) {
if (group === undefined) return undefined;
for (var i in numbers) { for (var i in numbers) {
var number = numbers[i]; var number = numbers[i];
if (!textsecure.utils.isNumberSane(number)) if (!textsecure.utils.isNumberSane(number))
throw new Error("Invalid number in set to add to group"); throw new Error('Invalid number in set to add to group');
if (group.numbers.indexOf(number) < 0) { if (group.numbers.indexOf(number) < 0) {
group.numbers.push(number); group.numbers.push(number);
group.numberRegistrationIds[number] = {}; group.numberRegistrationIds[number] = {};
} }
} }
return textsecure.storage.protocol.putGroup(groupId, group).then(function() { return textsecure.storage.protocol
.putGroup(groupId, group)
.then(function() {
return group.numbers; return group.numbers;
}); });
}); });
@ -119,26 +131,34 @@
}, },
getGroup: function(groupId) { getGroup: function(groupId) {
return textsecure.storage.protocol.getGroup(groupId).then(function(group) { return textsecure.storage.protocol
if (group === undefined) .getGroup(groupId)
return undefined; .then(function(group) {
if (group === undefined) return undefined;
return { id: groupId, numbers: group.numbers }; return { id: groupId, numbers: group.numbers };
}); });
}, },
updateNumbers: function(groupId, numbers) { updateNumbers: function(groupId, numbers) {
return textsecure.storage.protocol.getGroup(groupId).then(function(group) { return textsecure.storage.protocol
.getGroup(groupId)
.then(function(group) {
if (group === undefined) if (group === undefined)
throw new Error("Tried to update numbers for unknown group"); throw new Error('Tried to update numbers for unknown group');
if (numbers.filter(textsecure.utils.isNumberSane).length < numbers.length) if (
throw new Error("Invalid number in new group members"); 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; }); var added = numbers.filter(function(number) {
return group.numbers.indexOf(number) < 0;
});
return textsecure.storage.groups.addNumbers(groupId, added); return textsecure.storage.groups.addNumbers(groupId, added);
}); });
} },
}; };
})(); })();

View file

@ -1,4 +1,4 @@
;(function() { (function() {
'use strict'; 'use strict';
/***************************************** /*****************************************

View file

@ -1,6 +1,6 @@
'use strict'; 'use strict';
;(function() { (function() {
/********************************************* /*********************************************
*** Utilities to store data about the user *** *** Utilities to store data about the user ***
**********************************************/ **********************************************/
@ -9,28 +9,26 @@
window.textsecure.storage.user = { window.textsecure.storage.user = {
setNumberAndDeviceId: function(number, deviceId, deviceName) { setNumberAndDeviceId: function(number, deviceId, deviceName) {
textsecure.storage.put("number_id", number + "." + deviceId); textsecure.storage.put('number_id', number + '.' + deviceId);
if (deviceName) { if (deviceName) {
textsecure.storage.put("device_name", deviceName); textsecure.storage.put('device_name', deviceName);
} }
}, },
getNumber: function(key, defaultValue) { getNumber: function(key, defaultValue) {
var number_id = textsecure.storage.get("number_id"); var number_id = textsecure.storage.get('number_id');
if (number_id === undefined) if (number_id === undefined) return undefined;
return undefined;
return textsecure.utils.unencodeNumber(number_id)[0]; return textsecure.utils.unencodeNumber(number_id)[0];
}, },
getDeviceId: function(key) { getDeviceId: function(key) {
var number_id = textsecure.storage.get("number_id"); var number_id = textsecure.storage.get('number_id');
if (number_id === undefined) if (number_id === undefined) return undefined;
return undefined;
return textsecure.utils.unencodeNumber(number_id)[1]; return textsecure.utils.unencodeNumber(number_id)[1];
}, },
getDeviceName: function(key) { getDeviceName: function(key) {
return textsecure.storage.get("device_name"); return textsecure.storage.get('device_name');
} },
}; };
})(); })();

View file

@ -1,8 +1,7 @@
;(function() { (function() {
"use strict"; 'use strict';
window.StringView = { window.StringView = {
/* /*
* These functions from the Mozilla Developer Network * These functions from the Mozilla Developer Network
* and have been placed in the public domain. * and have been placed in the public domain.
@ -11,33 +10,39 @@
*/ */
b64ToUint6: function(nChr) { b64ToUint6: function(nChr) {
return nChr > 64 && nChr < 91 ? return nChr > 64 && nChr < 91
nChr - 65 ? nChr - 65
: nChr > 96 && nChr < 123 ? : nChr > 96 && nChr < 123
nChr - 71 ? nChr - 71
: nChr > 47 && nChr < 58 ? : nChr > 47 && nChr < 58
nChr + 4 ? nChr + 4
: nChr === 43 ? : nChr === 43
62 ? 62
: nChr === 47 ? : nChr === 47
63 ? 63
: : 0;
0;
}, },
base64ToBytes: function(sBase64, nBlocksSize) { base64ToBytes: function(sBase64, nBlocksSize) {
var var sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ''),
sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ""), nInLen = sB64Enc.length, nInLen = sB64Enc.length,
nOutLen = nBlocksSize ? Math.ceil((nInLen * 3 + 1 >> 2) / nBlocksSize) * nBlocksSize : nInLen * 3 + 1 >> 2; nOutLen = nBlocksSize
? Math.ceil(((nInLen * 3 + 1) >> 2) / nBlocksSize) * nBlocksSize
: (nInLen * 3 + 1) >> 2;
var aBBytes = new ArrayBuffer(nOutLen); var aBBytes = new ArrayBuffer(nOutLen);
var taBytes = new Uint8Array(aBBytes); var taBytes = new Uint8Array(aBBytes);
for (var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) { for (
var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0;
nInIdx < nInLen;
nInIdx++
) {
nMod4 = nInIdx & 3; nMod4 = nInIdx & 3;
nUint24 |= StringView.b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << 18 - 6 * nMod4; nUint24 |=
StringView.b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << (18 - 6 * nMod4);
if (nMod4 === 3 || nInLen - nInIdx === 1) { if (nMod4 === 3 || nInLen - nInIdx === 1) {
for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) { for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) {
taBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255; taBytes[nOutIdx] = (nUint24 >>> ((16 >>> nMod3) & 24)) & 255;
} }
nUint24 = 0; nUint24 = 0;
} }
@ -46,37 +51,43 @@
}, },
uint6ToB64: function(nUint6) { uint6ToB64: function(nUint6) {
return nUint6 < 26 ? return nUint6 < 26
nUint6 + 65 ? nUint6 + 65
: nUint6 < 52 ? : nUint6 < 52
nUint6 + 71 ? nUint6 + 71
: nUint6 < 62 ? : nUint6 < 62
nUint6 - 4 ? nUint6 - 4
: nUint6 === 62 ? : nUint6 === 62
43 ? 43
: nUint6 === 63 ? : nUint6 === 63
47 ? 47
: : 65;
65;
}, },
bytesToBase64: function(aBytes) { bytesToBase64: function(aBytes) {
var nMod3, sB64Enc = ""; var nMod3,
for (var nLen = aBytes.length, nUint24 = 0, nIdx = 0; nIdx < nLen; nIdx++) { sB64Enc = '';
for (
var nLen = aBytes.length, nUint24 = 0, nIdx = 0;
nIdx < nLen;
nIdx++
) {
nMod3 = nIdx % 3; nMod3 = nIdx % 3;
if (nIdx > 0 && (nIdx * 4 / 3) % 76 === 0) { sB64Enc += "\r\n"; } if (nIdx > 0 && (nIdx * 4 / 3) % 76 === 0) {
nUint24 |= aBytes[nIdx] << (16 >>> nMod3 & 24); sB64Enc += '\r\n';
}
nUint24 |= aBytes[nIdx] << ((16 >>> nMod3) & 24);
if (nMod3 === 2 || aBytes.length - nIdx === 1) { if (nMod3 === 2 || aBytes.length - nIdx === 1) {
sB64Enc += String.fromCharCode( sB64Enc += String.fromCharCode(
StringView.uint6ToB64(nUint24 >>> 18 & 63), StringView.uint6ToB64((nUint24 >>> 18) & 63),
StringView.uint6ToB64(nUint24 >>> 12 & 63), StringView.uint6ToB64((nUint24 >>> 12) & 63),
StringView.uint6ToB64(nUint24 >>> 6 & 63), StringView.uint6ToB64((nUint24 >>> 6) & 63),
StringView.uint6ToB64(nUint24 & 63) StringView.uint6ToB64(nUint24 & 63)
); );
nUint24 = 0; nUint24 = 0;
} }
} }
return sB64Enc.replace(/A(?=A$|$)/g, "="); return sB64Enc.replace(/A(?=A$|$)/g, '=');
} },
}; };
}()); })();

View file

@ -1,10 +1,15 @@
;(function () { (function() {
'use strict'; 'use strict';
window.textsecure = window.textsecure || {}; window.textsecure = window.textsecure || {};
function SyncRequest(sender, receiver) { function SyncRequest(sender, receiver) {
if (!(sender instanceof textsecure.MessageSender) || !(receiver instanceof textsecure.MessageReceiver)) { if (
throw new Error('Tried to construct a SyncRequest without MessageSender and MessageReceiver'); !(sender instanceof textsecure.MessageSender) ||
!(receiver instanceof textsecure.MessageReceiver)
) {
throw new Error(
'Tried to construct a SyncRequest without MessageSender and MessageReceiver'
);
} }
this.receiver = receiver; this.receiver = receiver;
@ -15,10 +20,13 @@
receiver.addEventListener('groupsync', this.ongroup); receiver.addEventListener('groupsync', this.ongroup);
console.log('SyncRequest created. Sending contact sync message...'); console.log('SyncRequest created. Sending contact sync message...');
sender.sendRequestContactSyncMessage().then(function() { sender
.sendRequestContactSyncMessage()
.then(function() {
console.log('SyncRequest now sending group sync messsage...'); console.log('SyncRequest now sending group sync messsage...');
return sender.sendRequestGroupSyncMessage(); return sender.sendRequestGroupSyncMessage();
}).catch(function(error) { })
.catch(function(error) {
console.log( console.log(
'SyncRequest error:', 'SyncRequest error:',
error && error.stack ? error.stack : error error && error.stack ? error.stack : error
@ -57,18 +65,18 @@
this.receiver.removeEventListener('contactsync', this.oncontact); this.receiver.removeEventListener('contactsync', this.oncontact);
this.receiver.removeEventListener('groupSync', this.ongroup); this.receiver.removeEventListener('groupSync', this.ongroup);
delete this.listeners; delete this.listeners;
} },
}); });
textsecure.SyncRequest = function(sender, receiver) { textsecure.SyncRequest = function(sender, receiver) {
var syncRequest = new SyncRequest(sender, receiver); var syncRequest = new SyncRequest(sender, receiver);
this.addEventListener = syncRequest.addEventListener.bind(syncRequest); this.addEventListener = syncRequest.addEventListener.bind(syncRequest);
this.removeEventListener = syncRequest.removeEventListener.bind(syncRequest); this.removeEventListener = syncRequest.removeEventListener.bind(
syncRequest
);
}; };
textsecure.SyncRequest.prototype = { textsecure.SyncRequest.prototype = {
constructor: textsecure.SyncRequest constructor: textsecure.SyncRequest,
}; };
})();
}());

View file

@ -3,23 +3,26 @@
window.textsecure.createTaskWithTimeout = function(task, id, options) { window.textsecure.createTaskWithTimeout = function(task, id, options) {
options = options || {}; options = options || {};
options.timeout = options.timeout || (1000 * 60 * 2); // two minutes options.timeout = options.timeout || 1000 * 60 * 2; // two minutes
var errorForStack = new Error('for stack'); var errorForStack = new Error('for stack');
return function() { return function() {
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
var complete = false; var complete = false;
var timer = setTimeout(function() { var timer = setTimeout(
function() {
if (!complete) { if (!complete) {
var message = var message =
(id || '') (id || '') +
+ ' task did not complete in time. Calling stack: ' ' task did not complete in time. Calling stack: ' +
+ errorForStack.stack; errorForStack.stack;
console.log(message); console.log(message);
return reject(new Error(message)); return reject(new Error(message));
} }
}.bind(this), options.timeout); }.bind(this),
options.timeout
);
var clearTimer = function() { var clearTimer = function() {
try { try {
var localTimer = timer; var localTimer = timer;
@ -27,8 +30,7 @@
timer = null; timer = null;
clearTimeout(localTimer); clearTimeout(localTimer);
} }
} } catch (error) {
catch (error) {
console.log( console.log(
id || '', id || '',
'task ran into problem canceling timer. Calling stack:', 'task ran into problem canceling timer. Calling stack:',

View file

@ -1,4 +1,4 @@
mocha.setup("bdd"); mocha.setup('bdd');
window.assert = chai.assert; window.assert = chai.assert;
window.PROTO_ROOT = '../../protos'; window.PROTO_ROOT = '../../protos';
@ -27,7 +27,7 @@ window.PROTO_ROOT = '../../protos';
result: false, result: false,
message: err.message, message: err.message,
stack: err.stack, stack: err.stack,
titles: flattenTitles(test) titles: flattenTitles(test),
}); });
}); });
@ -37,14 +37,14 @@ window.PROTO_ROOT = '../../protos';
SauceReporter.prototype = OriginalReporter.prototype; SauceReporter.prototype = OriginalReporter.prototype;
mocha.reporter(SauceReporter); mocha.reporter(SauceReporter);
}()); })();
/* /*
* global helpers for tests * global helpers for tests
*/ */
function assertEqualArrayBuffers(ab1, ab2) { function assertEqualArrayBuffers(ab1, ab2) {
assert.deepEqual(new Uint8Array(ab1), new Uint8Array(ab2)); assert.deepEqual(new Uint8Array(ab1), new Uint8Array(ab2));
}; }
function hexToArrayBuffer(str) { function hexToArrayBuffer(str) {
var ret = new ArrayBuffer(str.length / 2); var ret = new ArrayBuffer(str.length / 2);
@ -52,6 +52,6 @@ function hexToArrayBuffer(str) {
for (var i = 0; i < str.length / 2; i++) for (var i = 0; i < str.length / 2; i++)
array[i] = parseInt(str.substr(i * 2, 2), 16); array[i] = parseInt(str.substr(i * 2, 2), 16);
return ret; return ret;
}; }
window.MockSocket.prototype.addEventListener = function() {}; window.MockSocket.prototype.addEventListener = function() {};

View file

@ -1,6 +1,6 @@
'use strict'; 'use strict';
describe("AccountManager", function() { describe('AccountManager', function() {
let accountManager; let accountManager;
let originalServer; let originalServer;
@ -35,19 +35,23 @@ describe("AccountManager", function() {
it('keeps three confirmed keys even if over a week old', function() { it('keeps three confirmed keys even if over a week old', function() {
const now = Date.now(); const now = Date.now();
signedPreKeys = [{ signedPreKeys = [
{
keyId: 1, keyId: 1,
created_at: now - DAY * 21, created_at: now - DAY * 21,
confirmed: true, confirmed: true,
}, { },
{
keyId: 2, keyId: 2,
created_at: now - DAY * 14, created_at: now - DAY * 14,
confirmed: true, confirmed: true,
}, { },
{
keyId: 3, keyId: 3,
created_at: now - DAY * 18, created_at: now - DAY * 18,
confirmed: true, confirmed: true,
}]; },
];
// should be no calls to store.removeSignedPreKey, would cause crash // should be no calls to store.removeSignedPreKey, would cause crash
return accountManager.cleanSignedPreKeys(); return accountManager.cleanSignedPreKeys();
@ -55,27 +59,33 @@ describe("AccountManager", function() {
it('eliminates confirmed keys over a week old, if more than three', function() { it('eliminates confirmed keys over a week old, if more than three', function() {
const now = Date.now(); const now = Date.now();
signedPreKeys = [{ signedPreKeys = [
{
keyId: 1, keyId: 1,
created_at: now - DAY * 21, created_at: now - DAY * 21,
confirmed: true, confirmed: true,
}, { },
{
keyId: 2, keyId: 2,
created_at: now - DAY * 14, created_at: now - DAY * 14,
confirmed: true, confirmed: true,
}, { },
{
keyId: 3, keyId: 3,
created_at: now - DAY * 4, created_at: now - DAY * 4,
confirmed: true, confirmed: true,
}, { },
{
keyId: 4, keyId: 4,
created_at: now - DAY * 18, created_at: now - DAY * 18,
confirmed: true, confirmed: true,
}, { },
{
keyId: 5, keyId: 5,
created_at: now - DAY, created_at: now - DAY,
confirmed: true, confirmed: true,
}]; },
];
let count = 0; let count = 0;
window.textsecure.storage.protocol.removeSignedPreKey = function(keyId) { 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() { it('keeps at least three unconfirmed keys if no confirmed', function() {
const now = Date.now(); const now = Date.now();
signedPreKeys = [{ signedPreKeys = [
{
keyId: 1, keyId: 1,
created_at: now - DAY * 14, created_at: now - DAY * 14,
}, { },
{
keyId: 2, keyId: 2,
created_at: now - DAY * 21, created_at: now - DAY * 21,
}, { },
{
keyId: 3, keyId: 3,
created_at: now - DAY * 18, created_at: now - DAY * 18,
}, { },
{
keyId: 4, keyId: 4,
created_at: now - DAY created_at: now - DAY,
}]; },
];
let count = 0; let count = 0;
window.textsecure.storage.protocol.removeSignedPreKey = function(keyId) { 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() { it('if some confirmed keys, keeps unconfirmed to addd up to three total', function() {
const now = Date.now(); const now = Date.now();
signedPreKeys = [{ signedPreKeys = [
{
keyId: 1, keyId: 1,
created_at: now - DAY * 21, created_at: now - DAY * 21,
confirmed: true, confirmed: true,
}, { },
{
keyId: 2, keyId: 2,
created_at: now - DAY * 14, created_at: now - DAY * 14,
confirmed: true, confirmed: true,
}, { },
{
keyId: 3, keyId: 3,
created_at: now - DAY * 12, created_at: now - DAY * 12,
}, { },
{
keyId: 4, keyId: 4,
created_at: now - DAY * 8, created_at: now - DAY * 8,
}]; },
];
let count = 0; let count = 0;
window.textsecure.storage.protocol.removeSignedPreKey = function(keyId) { window.textsecure.storage.protocol.removeSignedPreKey = function(keyId) {

View file

@ -1,6 +1,6 @@
'use strict'; 'use strict';
describe("ContactBuffer", function() { describe('ContactBuffer', function() {
function getTestBuffer() { function getTestBuffer() {
var buffer = new dcodeIO.ByteBuffer(); var buffer = new dcodeIO.ByteBuffer();
var avatarBuffer = new dcodeIO.ByteBuffer(); var avatarBuffer = new dcodeIO.ByteBuffer();
@ -11,9 +11,9 @@ describe("ContactBuffer", function() {
avatarBuffer.limit = avatarBuffer.offset; avatarBuffer.limit = avatarBuffer.offset;
avatarBuffer.offset = 0; avatarBuffer.offset = 0;
var contactInfo = new textsecure.protobuf.ContactDetails({ var contactInfo = new textsecure.protobuf.ContactDetails({
name: "Zero Cool", name: 'Zero Cool',
number: "+10000000000", number: '+10000000000',
avatar: { contentType: "image/jpeg", length: avatarLen } avatar: { contentType: 'image/jpeg', length: avatarLen },
}); });
var contactInfoBuffer = contactInfo.encode().toArrayBuffer(); var contactInfoBuffer = contactInfo.encode().toArrayBuffer();
@ -28,16 +28,16 @@ describe("ContactBuffer", function() {
return buffer.toArrayBuffer(); return buffer.toArrayBuffer();
} }
it("parses an array buffer of contacts", function() { it('parses an array buffer of contacts', function() {
var arrayBuffer = getTestBuffer(); var arrayBuffer = getTestBuffer();
var contactBuffer = new ContactBuffer(arrayBuffer); var contactBuffer = new ContactBuffer(arrayBuffer);
var contact = contactBuffer.next(); var contact = contactBuffer.next();
var count = 0; var count = 0;
while (contact !== undefined) { while (contact !== undefined) {
count++; count++;
assert.strictEqual(contact.name, "Zero Cool"); assert.strictEqual(contact.name, 'Zero Cool');
assert.strictEqual(contact.number, "+10000000000"); assert.strictEqual(contact.number, '+10000000000');
assert.strictEqual(contact.avatar.contentType, "image/jpeg"); assert.strictEqual(contact.avatar.contentType, 'image/jpeg');
assert.strictEqual(contact.avatar.length, 255); assert.strictEqual(contact.avatar.length, 255);
assert.strictEqual(contact.avatar.data.byteLength, 255); assert.strictEqual(contact.avatar.data.byteLength, 255);
var avatarBytes = new Uint8Array(contact.avatar.data); var avatarBytes = new Uint8Array(contact.avatar.data);
@ -50,7 +50,7 @@ describe("ContactBuffer", function() {
}); });
}); });
describe("GroupBuffer", function() { describe('GroupBuffer', function() {
function getTestBuffer() { function getTestBuffer() {
var buffer = new dcodeIO.ByteBuffer(); var buffer = new dcodeIO.ByteBuffer();
var avatarBuffer = new dcodeIO.ByteBuffer(); var avatarBuffer = new dcodeIO.ByteBuffer();
@ -62,9 +62,9 @@ describe("GroupBuffer", function() {
avatarBuffer.offset = 0; avatarBuffer.offset = 0;
var groupInfo = new textsecure.protobuf.GroupDetails({ var groupInfo = new textsecure.protobuf.GroupDetails({
id: new Uint8Array([1, 3, 3, 7]).buffer, id: new Uint8Array([1, 3, 3, 7]).buffer,
name: "Hackers", name: 'Hackers',
members: ['cereal', 'burn', 'phreak', 'joey'], members: ['cereal', 'burn', 'phreak', 'joey'],
avatar: { contentType: "image/jpeg", length: avatarLen } avatar: { contentType: 'image/jpeg', length: avatarLen },
}); });
var groupInfoBuffer = groupInfo.encode().toArrayBuffer(); var groupInfoBuffer = groupInfo.encode().toArrayBuffer();
@ -79,17 +79,20 @@ describe("GroupBuffer", function() {
return buffer.toArrayBuffer(); return buffer.toArrayBuffer();
} }
it("parses an array buffer of groups", function() { it('parses an array buffer of groups', function() {
var arrayBuffer = getTestBuffer(); var arrayBuffer = getTestBuffer();
var groupBuffer = new GroupBuffer(arrayBuffer); var groupBuffer = new GroupBuffer(arrayBuffer);
var group = groupBuffer.next(); var group = groupBuffer.next();
var count = 0; var count = 0;
while (group !== undefined) { while (group !== undefined) {
count++; count++;
assert.strictEqual(group.name, "Hackers"); assert.strictEqual(group.name, 'Hackers');
assertEqualArrayBuffers(group.id.toArrayBuffer(), new Uint8Array([1,3,3,7]).buffer); assertEqualArrayBuffers(
group.id.toArrayBuffer(),
new Uint8Array([1, 3, 3, 7]).buffer
);
assert.sameMembers(group.members, ['cereal', 'burn', 'phreak', 'joey']); 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.length, 255);
assert.strictEqual(group.avatar.data.byteLength, 255); assert.strictEqual(group.avatar.data.byteLength, 255);
var avatarBytes = new Uint8Array(group.avatar.data); var avatarBytes = new Uint8Array(group.avatar.data);

View file

@ -6,10 +6,17 @@ describe('encrypting and decrypting profile data', function() {
var buffer = dcodeIO.ByteBuffer.wrap(name).toArrayBuffer(); var buffer = dcodeIO.ByteBuffer.wrap(name).toArrayBuffer();
var key = libsignal.crypto.getRandomBytes(32); var key = libsignal.crypto.getRandomBytes(32);
return textsecure.crypto.encryptProfileName(buffer, key).then(function(encrypted) { return textsecure.crypto
.encryptProfileName(buffer, key)
.then(function(encrypted) {
assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12); assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12);
return textsecure.crypto.decryptProfileName(encrypted, key).then(function(decrypted) { return textsecure.crypto
assert.strictEqual(dcodeIO.ByteBuffer.wrap(decrypted).toString('utf8'), 'Alice'); .decryptProfileName(encrypted, key)
.then(function(decrypted) {
assert.strictEqual(
dcodeIO.ByteBuffer.wrap(decrypted).toString('utf8'),
'Alice'
);
}); });
}); });
}); });
@ -17,11 +24,18 @@ describe('encrypting and decrypting profile data', function() {
var name = dcodeIO.ByteBuffer.wrap('').toArrayBuffer(); var name = dcodeIO.ByteBuffer.wrap('').toArrayBuffer();
var key = libsignal.crypto.getRandomBytes(32); var key = libsignal.crypto.getRandomBytes(32);
return textsecure.crypto.encryptProfileName(name.buffer, key).then(function(encrypted) { return textsecure.crypto
.encryptProfileName(name.buffer, key)
.then(function(encrypted) {
assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12); assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12);
return textsecure.crypto.decryptProfileName(encrypted, key).then(function(decrypted) { return textsecure.crypto
.decryptProfileName(encrypted, key)
.then(function(decrypted) {
assert.strictEqual(decrypted.byteLength, 0); assert.strictEqual(decrypted.byteLength, 0);
assert.strictEqual(dcodeIO.ByteBuffer.wrap(decrypted).toString('utf8'), ''); assert.strictEqual(
dcodeIO.ByteBuffer.wrap(decrypted).toString('utf8'),
''
);
}); });
}); });
}); });
@ -31,10 +45,14 @@ describe('encrypting and decrypting profile data', function() {
var buffer = dcodeIO.ByteBuffer.wrap('This is an avatar').toArrayBuffer(); var buffer = dcodeIO.ByteBuffer.wrap('This is an avatar').toArrayBuffer();
var key = libsignal.crypto.getRandomBytes(32); var key = libsignal.crypto.getRandomBytes(32);
return textsecure.crypto.encryptProfile(buffer, key).then(function(encrypted) { return textsecure.crypto
.encryptProfile(buffer, key)
.then(function(encrypted) {
assert(encrypted.byteLength === buffer.byteLength + 16 + 12); assert(encrypted.byteLength === buffer.byteLength + 16 + 12);
return textsecure.crypto.decryptProfile(encrypted, key).then(function(decrypted) { return textsecure.crypto
assertEqualArrayBuffers(buffer, decrypted) .decryptProfile(encrypted, key)
.then(function(decrypted) {
assertEqualArrayBuffers(buffer, decrypted);
}); });
}); });
}); });
@ -43,9 +61,13 @@ describe('encrypting and decrypting profile data', function() {
var key = libsignal.crypto.getRandomBytes(32); var key = libsignal.crypto.getRandomBytes(32);
var bad_key = libsignal.crypto.getRandomBytes(32); var bad_key = libsignal.crypto.getRandomBytes(32);
return textsecure.crypto.encryptProfile(buffer, key).then(function(encrypted) { return textsecure.crypto
.encryptProfile(buffer, key)
.then(function(encrypted) {
assert(encrypted.byteLength === buffer.byteLength + 16 + 12); assert(encrypted.byteLength === buffer.byteLength + 16 + 12);
return textsecure.crypto.decryptProfile(encrypted, bad_key).catch(function(error) { return textsecure.crypto
.decryptProfile(encrypted, bad_key)
.catch(function(error) {
assert.strictEqual(error.name, 'ProfileDecryptError'); assert.strictEqual(error.name, 'ProfileDecryptError');
}); });
}); });

View file

@ -4,23 +4,26 @@ TextSecureServer.getKeysForNumber = function(number, deviceId) {
if (res !== undefined) { if (res !== undefined) {
delete getKeysForNumberMap[number]; delete getKeysForNumberMap[number];
return Promise.resolve(res); return Promise.resolve(res);
} else } else throw new Error('getKeysForNumber of unknown/used number');
throw new Error("getKeysForNumber of unknown/used number");
}; };
var messagesSentMap = {}; var messagesSentMap = {};
TextSecureServer.sendMessages = function(destination, messageArray) { TextSecureServer.sendMessages = function(destination, messageArray) {
for (i in messageArray) { for (i in messageArray) {
var msg = messageArray[i]; var msg = messageArray[i];
if ((msg.type != 1 && msg.type != 3) || if (
(msg.type != 1 && msg.type != 3) ||
msg.destinationDeviceId === undefined || msg.destinationDeviceId === undefined ||
msg.destinationRegistrationId === undefined || msg.destinationRegistrationId === undefined ||
msg.body === undefined || msg.body === undefined ||
msg.timestamp == undefined || msg.timestamp == undefined ||
msg.relay !== undefined || msg.relay !== undefined ||
msg.destination !== undefined) msg.destination !== undefined
throw new Error("Invalid message"); )
throw new Error('Invalid message');
messagesSentMap[destination + "." + messageArray[i].destinationDeviceId] = msg; messagesSentMap[
destination + '.' + messageArray[i].destinationDeviceId
] = msg;
} }
}; };

View file

@ -1,6 +1,6 @@
'use strict'; 'use strict';
describe("Key generation", function() { describe('Key generation', function() {
var count = 10; var count = 10;
this.timeout(count * 2000); this.timeout(count * 2000);
@ -15,34 +15,46 @@ describe("Key generation", function() {
} }
function itStoresPreKey(keyId) { function itStoresPreKey(keyId) {
it('prekey ' + keyId + ' is valid', function(done) { it('prekey ' + keyId + ' is valid', function(done) {
return textsecure.storage.protocol.loadPreKey(keyId).then(function(keyPair) { return textsecure.storage.protocol
.loadPreKey(keyId)
.then(function(keyPair) {
validateStoredKeyPair(keyPair); validateStoredKeyPair(keyPair);
}).then(done,done); })
.then(done, done);
}); });
} }
function itStoresSignedPreKey(keyId) { function itStoresSignedPreKey(keyId) {
it('signed prekey ' + keyId + ' is valid', function(done) { it('signed prekey ' + keyId + ' is valid', function(done) {
return textsecure.storage.protocol.loadSignedPreKey(keyId).then(function(keyPair) { return textsecure.storage.protocol
.loadSignedPreKey(keyId)
.then(function(keyPair) {
validateStoredKeyPair(keyPair); validateStoredKeyPair(keyPair);
}).then(done,done); })
.then(done, done);
}); });
} }
function validateResultKey(resultKey) { function validateResultKey(resultKey) {
return textsecure.storage.protocol.loadPreKey(resultKey.keyId).then(function(keyPair) { return textsecure.storage.protocol
.loadPreKey(resultKey.keyId)
.then(function(keyPair) {
assertEqualArrayBuffers(resultKey.publicKey, keyPair.pubKey); assertEqualArrayBuffers(resultKey.publicKey, keyPair.pubKey);
}); });
} }
function validateResultSignedKey(resultSignedKey) { function validateResultSignedKey(resultSignedKey) {
return textsecure.storage.protocol.loadSignedPreKey(resultSignedKey.keyId).then(function(keyPair) { return textsecure.storage.protocol
.loadSignedPreKey(resultSignedKey.keyId)
.then(function(keyPair) {
assertEqualArrayBuffers(resultSignedKey.publicKey, keyPair.pubKey); assertEqualArrayBuffers(resultSignedKey.publicKey, keyPair.pubKey);
}); });
} }
before(function(done) { before(function(done) {
localStorage.clear(); localStorage.clear();
libsignal.KeyHelper.generateIdentityKeyPair().then(function(keyPair) { libsignal.KeyHelper.generateIdentityKeyPair()
.then(function(keyPair) {
return textsecure.storage.protocol.put('identityKey', keyPair); return textsecure.storage.protocol.put('identityKey', keyPair);
}).then(done, done); })
.then(done, done);
}); });
describe('the first time', function() { describe('the first time', function() {
@ -56,9 +68,12 @@ describe("Key generation", function() {
*/ */
before(function(done) { before(function(done) {
var accountManager = new textsecure.AccountManager(''); var accountManager = new textsecure.AccountManager('');
accountManager.generateKeys(count).then(function(res) { accountManager
.generateKeys(count)
.then(function(res) {
result = res; result = res;
}).then(done,done); })
.then(done, done);
}); });
for (var i = 1; i <= count; i++) { for (var i = 1; i <= count; i++) {
itStoresPreKey(i); itStoresPreKey(i);
@ -78,9 +93,11 @@ describe("Key generation", function() {
} }
}); });
it('result contains the correct public keys', function(done) { it('result contains the correct public keys', function(done) {
Promise.all(result.preKeys.map(validateResultKey)).then(function() { Promise.all(result.preKeys.map(validateResultKey))
.then(function() {
done(); done();
}).catch(done); })
.catch(done);
}); });
it('returns a signed prekey', function(done) { it('returns a signed prekey', function(done) {
assert.strictEqual(result.signedPreKey.keyId, 1); assert.strictEqual(result.signedPreKey.keyId, 1);
@ -92,9 +109,12 @@ describe("Key generation", function() {
var result; var result;
before(function(done) { before(function(done) {
var accountManager = new textsecure.AccountManager(''); var accountManager = new textsecure.AccountManager('');
accountManager.generateKeys(count).then(function(res) { accountManager
.generateKeys(count)
.then(function(res) {
result = res; result = res;
}).then(done,done); })
.then(done, done);
}); });
for (var i = 1; i <= 2 * count; i++) { for (var i = 1; i <= 2 * count; i++) {
itStoresPreKey(i); itStoresPreKey(i);
@ -114,9 +134,11 @@ describe("Key generation", function() {
} }
}); });
it('result contains the correct public keys', function(done) { it('result contains the correct public keys', function(done) {
Promise.all(result.preKeys.map(validateResultKey)).then(function() { Promise.all(result.preKeys.map(validateResultKey))
.then(function() {
done(); done();
}).catch(done); })
.catch(done);
}); });
it('returns a signed prekey', function(done) { it('returns a signed prekey', function(done) {
assert.strictEqual(result.signedPreKey.keyId, 2); assert.strictEqual(result.signedPreKey.keyId, 2);
@ -128,9 +150,12 @@ describe("Key generation", function() {
var result; var result;
before(function(done) { before(function(done) {
var accountManager = new textsecure.AccountManager(''); var accountManager = new textsecure.AccountManager('');
accountManager.generateKeys(count).then(function(res) { accountManager
.generateKeys(count)
.then(function(res) {
result = res; result = res;
}).then(done,done); })
.then(done, done);
}); });
for (var i = 1; i <= 3 * count; i++) { for (var i = 1; i <= 3 * count; i++) {
itStoresPreKey(i); itStoresPreKey(i);
@ -150,9 +175,11 @@ describe("Key generation", function() {
} }
}); });
it('result contains the correct public keys', function(done) { it('result contains the correct public keys', function(done) {
Promise.all(result.preKeys.map(validateResultKey)).then(function() { Promise.all(result.preKeys.map(validateResultKey))
.then(function() {
done(); done();
}).catch(done); })
.catch(done);
}); });
it('result contains a signed prekey', function(done) { it('result contains a signed prekey', function(done) {
assert.strictEqual(result.signedPreKey.keyId, 3); assert.strictEqual(result.signedPreKey.keyId, 3);

View file

@ -1,18 +1,18 @@
'use strict'; 'use strict';
describe("Helpers", function() { describe('Helpers', function() {
describe("ArrayBuffer->String conversion", function() { describe('ArrayBuffer->String conversion', function() {
it('works', function() { it('works', function() {
var b = new ArrayBuffer(3); var b = new ArrayBuffer(3);
var a = new Uint8Array(b); var a = new Uint8Array(b);
a[0] = 0; a[0] = 0;
a[1] = 255; a[1] = 255;
a[2] = 128; a[2] = 128;
assert.equal(getString(b), "\x00\xff\x80"); assert.equal(getString(b), '\x00\xff\x80');
}); });
}); });
describe("stringToArrayBuffer", function() { describe('stringToArrayBuffer', function() {
it('returns ArrayBuffer when passed string', function() { it('returns ArrayBuffer when passed string', function() {
var StaticArrayBufferProto = new ArrayBuffer().__proto__; var StaticArrayBufferProto = new ArrayBuffer().__proto__;
var anArrayBuffer = new ArrayBuffer(1); var anArrayBuffer = new ArrayBuffer(1);
@ -23,7 +23,9 @@ describe("Helpers", function() {
it('throws an error when passed a non string', function() { it('throws an error when passed a non string', function() {
var notStringable = [{}, undefined, null, new ArrayBuffer()]; var notStringable = [{}, undefined, null, new ArrayBuffer()];
notStringable.forEach(function(notString) { notStringable.forEach(function(notString) {
assert.throw(function() { stringToArrayBuffer(notString) }, Error); assert.throw(function() {
stringToArrayBuffer(notString);
}, Error);
}); });
}); });
}); });

View file

@ -11,13 +11,18 @@ SignalProtocolStore.prototype = {
return Promise.resolve(this.get('registrationId')); return Promise.resolve(this.get('registrationId'));
}, },
put: function(key, value) { put: function(key, value) {
if (key === undefined || value === undefined || key === null || value === null) if (
throw new Error("Tried to store undefined/null"); key === undefined ||
value === undefined ||
key === null ||
value === null
)
throw new Error('Tried to store undefined/null');
this.store[key] = value; this.store[key] = value;
}, },
get: function(key, defaultValue) { get: function(key, defaultValue) {
if (key === null || key === undefined) if (key === null || key === undefined)
throw new Error("Tried to get value for undefined/null key"); throw new Error('Tried to get value for undefined/null key');
if (key in this.store) { if (key in this.store) {
return this.store[key]; return this.store[key];
} else { } else {
@ -26,16 +31,16 @@ SignalProtocolStore.prototype = {
}, },
remove: function(key) { remove: function(key) {
if (key === null || key === undefined) if (key === null || key === undefined)
throw new Error("Tried to remove value for undefined/null key"); throw new Error('Tried to remove value for undefined/null key');
delete this.store[key]; delete this.store[key];
}, },
isTrustedIdentity: function(identifier, identityKey) { isTrustedIdentity: function(identifier, identityKey) {
if (identifier === null || identifier === undefined) { if (identifier === null || identifier === undefined) {
throw new error("tried to check identity key for undefined/null key"); throw new error('tried to check identity key for undefined/null key');
} }
if (!(identityKey instanceof ArrayBuffer)) { if (!(identityKey instanceof ArrayBuffer)) {
throw new error("Expected identityKey to be an ArrayBuffer"); throw new error('Expected identityKey to be an ArrayBuffer');
} }
var trusted = this.get('identityKey' + identifier); var trusted = this.get('identityKey' + identifier);
if (trusted === undefined) { if (trusted === undefined) {
@ -45,15 +50,18 @@ SignalProtocolStore.prototype = {
}, },
loadIdentityKey: function(identifier) { loadIdentityKey: function(identifier) {
if (identifier === null || identifier === undefined) if (identifier === null || identifier === undefined)
throw new Error("Tried to get identity key for undefined/null key"); throw new Error('Tried to get identity key for undefined/null key');
return new Promise(function(resolve) { return new Promise(
function(resolve) {
resolve(this.get('identityKey' + identifier)); resolve(this.get('identityKey' + identifier));
}.bind(this)); }.bind(this)
);
}, },
saveIdentity: function(identifier, identityKey) { saveIdentity: function(identifier, identityKey) {
if (identifier === null || identifier === undefined) if (identifier === null || identifier === undefined)
throw new Error("Tried to put identity key for undefined/null key"); throw new Error('Tried to put identity key for undefined/null key');
return new Promise(function(resolve) { return new Promise(
function(resolve) {
var existing = this.get('identityKey' + identifier); var existing = this.get('identityKey' + identifier);
this.put('identityKey' + identifier, identityKey); this.put('identityKey' + identifier, identityKey);
if (existing && existing !== identityKey) { if (existing && existing !== identityKey) {
@ -61,36 +69,46 @@ SignalProtocolStore.prototype = {
} else { } else {
resolve(false); resolve(false);
} }
}.bind(this)); }.bind(this)
);
}, },
/* Returns a prekeypair object or undefined */ /* Returns a prekeypair object or undefined */
loadPreKey: function(keyId) { loadPreKey: function(keyId) {
return new Promise(function(resolve) { return new Promise(
function(resolve) {
var res = this.get('25519KeypreKey' + keyId); var res = this.get('25519KeypreKey' + keyId);
resolve(res); resolve(res);
}.bind(this)); }.bind(this)
);
}, },
storePreKey: function(keyId, keyPair) { storePreKey: function(keyId, keyPair) {
return new Promise(function(resolve) { return new Promise(
function(resolve) {
resolve(this.put('25519KeypreKey' + keyId, keyPair)); resolve(this.put('25519KeypreKey' + keyId, keyPair));
}.bind(this)); }.bind(this)
);
}, },
removePreKey: function(keyId) { removePreKey: function(keyId) {
return new Promise(function(resolve) { return new Promise(
function(resolve) {
resolve(this.remove('25519KeypreKey' + keyId)); resolve(this.remove('25519KeypreKey' + keyId));
}.bind(this)); }.bind(this)
);
}, },
/* Returns a signed keypair object or undefined */ /* Returns a signed keypair object or undefined */
loadSignedPreKey: function(keyId) { loadSignedPreKey: function(keyId) {
return new Promise(function(resolve) { return new Promise(
function(resolve) {
var res = this.get('25519KeysignedKey' + keyId); var res = this.get('25519KeysignedKey' + keyId);
resolve(res); resolve(res);
}.bind(this)); }.bind(this)
);
}, },
loadSignedPreKeys: function() { loadSignedPreKeys: function() {
return new Promise(function(resolve) { return new Promise(
function(resolve) {
var res = []; var res = [];
for (var i in this.store) { for (var i in this.store) {
if (i.startsWith('25519KeysignedKey')) { if (i.startsWith('25519KeysignedKey')) {
@ -98,48 +116,69 @@ SignalProtocolStore.prototype = {
} }
} }
resolve(res); resolve(res);
}.bind(this)); }.bind(this)
);
}, },
storeSignedPreKey: function(keyId, keyPair) { storeSignedPreKey: function(keyId, keyPair) {
return new Promise(function(resolve) { return new Promise(
function(resolve) {
resolve(this.put('25519KeysignedKey' + keyId, keyPair)); resolve(this.put('25519KeysignedKey' + keyId, keyPair));
}.bind(this)); }.bind(this)
);
}, },
removeSignedPreKey: function(keyId) { removeSignedPreKey: function(keyId) {
return new Promise(function(resolve) { return new Promise(
function(resolve) {
resolve(this.remove('25519KeysignedKey' + keyId)); resolve(this.remove('25519KeysignedKey' + keyId));
}.bind(this)); }.bind(this)
);
}, },
loadSession: function(identifier) { loadSession: function(identifier) {
return new Promise(function(resolve) { return new Promise(
function(resolve) {
resolve(this.get('session' + identifier)); resolve(this.get('session' + identifier));
}.bind(this)); }.bind(this)
);
}, },
storeSession: function(identifier, record) { storeSession: function(identifier, record) {
return new Promise(function(resolve) { return new Promise(
function(resolve) {
resolve(this.put('session' + identifier, record)); resolve(this.put('session' + identifier, record));
}.bind(this)); }.bind(this)
);
}, },
removeAllSessions: function(identifier) { removeAllSessions: function(identifier) {
return new Promise(function(resolve) { return new Promise(
function(resolve) {
for (key in this.store) { for (key in this.store) {
if (key.match(RegExp('^session' + identifier.replace('\+','\\\+') + '.+'))) { if (
key.match(
RegExp('^session' + identifier.replace('+', '\\+') + '.+')
)
) {
delete this.store[key]; delete this.store[key];
} }
} }
resolve(); resolve();
}.bind(this)); }.bind(this)
);
}, },
getDeviceIds: function(identifier) { getDeviceIds: function(identifier) {
return new Promise(function(resolve) { return new Promise(
function(resolve) {
var deviceIds = []; var deviceIds = [];
for (key in this.store) { for (key in this.store) {
if (key.match(RegExp('^session' + identifier.replace('\+','\\\+') + '.+'))) { if (
key.match(
RegExp('^session' + identifier.replace('+', '\\+') + '.+')
)
) {
deviceIds.push(parseInt(key.split('.')[1])); deviceIds.push(parseInt(key.split('.')[1]));
} }
} }
resolve(deviceIds); resolve(deviceIds);
}.bind(this)); }.bind(this)
} );
},
}; };

View file

@ -7,10 +7,12 @@ describe('MessageReceiver', function() {
before(function() { before(function() {
window.WebSocket = MockSocket; window.WebSocket = MockSocket;
textsecure.storage.user.setNumberAndDeviceId(number, deviceId, 'name'); textsecure.storage.user.setNumberAndDeviceId(number, deviceId, 'name');
textsecure.storage.put("password", "password"); textsecure.storage.put('password', 'password');
textsecure.storage.put("signaling_key", signalingKey); textsecure.storage.put('signaling_key', signalingKey);
});
after(function() {
window.WebSocket = WebSocket;
}); });
after (function() { window.WebSocket = WebSocket; });
describe('connecting', function() { describe('connecting', function() {
var blob = null; var blob = null;
@ -22,7 +24,7 @@ describe('MessageReceiver', function() {
}; };
var websocketmessage = new textsecure.protobuf.WebSocketMessage({ var websocketmessage = new textsecure.protobuf.WebSocketMessage({
type: textsecure.protobuf.WebSocketMessage.Type.REQUEST, type: textsecure.protobuf.WebSocketMessage.Type.REQUEST,
request: { verb: 'PUT', path: '/messages' } request: { verb: 'PUT', path: '/messages' },
}); });
before(function(done) { before(function(done) {
@ -33,13 +35,32 @@ describe('MessageReceiver', function() {
var aes_key = signaling_key.slice(0, 32); var aes_key = signaling_key.slice(0, 32);
var mac_key = signaling_key.slice(32, 32 + 20); var mac_key = signaling_key.slice(32, 32 + 20);
window.crypto.subtle.importKey('raw', aes_key, {name: 'AES-CBC'}, false, ['encrypt']).then(function(key) { window.crypto.subtle
.importKey('raw', aes_key, { name: 'AES-CBC' }, false, ['encrypt'])
.then(function(key) {
var iv = libsignal.crypto.getRandomBytes(16); 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
window.crypto.subtle.importKey('raw', mac_key, {name: 'HMAC', hash: {name: 'SHA-256'}}, false, ['sign']).then(function(key) { .encrypt({ name: 'AES-CBC', iv: new Uint8Array(iv) }, key, signal)
window.crypto.subtle.sign( {name: 'HMAC', hash: 'SHA-256'}, key, signal).then(function(mac) { .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 version = new Uint8Array([1]);
var message = dcodeIO.ByteBuffer.concat([version, iv, ciphertext, mac ]); var message = dcodeIO.ByteBuffer.concat([
version,
iv,
ciphertext,
mac,
]);
websocketmessage.request.body = message.toArrayBuffer(); websocketmessage.request.body = message.toArrayBuffer();
console.log(new Uint8Array(message.toArrayBuffer())); console.log(new Uint8Array(message.toArrayBuffer()));
done(); done();
@ -50,7 +71,11 @@ describe('MessageReceiver', function() {
}); });
it('connects', function(done) { it('connects', function(done) {
var mockServer = new MockServer('ws://localhost:8080/v1/websocket/?login='+ encodeURIComponent(number) +'.1&password=password'); var mockServer = new MockServer(
'ws://localhost:8080/v1/websocket/?login=' +
encodeURIComponent(number) +
'.1&password=password'
);
mockServer.on('connection', function(server) { mockServer.on('connection', function(server) {
server.send(new Blob([websocketmessage.toArrayBuffer()])); server.send(new Blob([websocketmessage.toArrayBuffer()]));
@ -65,7 +90,10 @@ describe('MessageReceiver', function() {
server.close(); server.close();
done(); done();
}); });
var messageReceiver = new textsecure.MessageReceiver('ws://localhost:8080', window); var messageReceiver = new textsecure.MessageReceiver(
'ws://localhost:8080',
window
);
}); });
}); });
}); });

View file

@ -1,30 +1,36 @@
'use strict'; 'use strict';
describe('Protocol', function() { describe('Protocol', function() {
describe('Unencrypted PushMessageProto "decrypt"', function() { describe('Unencrypted PushMessageProto "decrypt"', function() {
//exclusive //exclusive
it('works', function(done) { it('works', function(done) {
localStorage.clear(); localStorage.clear();
var text_message = new textsecure.protobuf.DataMessage(); var text_message = new textsecure.protobuf.DataMessage();
text_message.body = "Hi Mom"; text_message.body = 'Hi Mom';
var server_message = { var server_message = {
type: 4, // unencrypted type: 4, // unencrypted
source: "+19999999999", source: '+19999999999',
timestamp: 42, timestamp: 42,
message: text_message.encode() message: text_message.encode(),
}; };
return textsecure.protocol_wrapper.handleEncryptedMessage( return textsecure.protocol_wrapper
.handleEncryptedMessage(
server_message.source, server_message.source,
server_message.source_device, server_message.source_device,
server_message.type, server_message.type,
server_message.message server_message.message
).then(function(message) { )
.then(function(message) {
assert.equal(message.body, text_message.body); assert.equal(message.body, text_message.body);
assert.equal(message.attachments.length, text_message.attachments.length); assert.equal(
message.attachments.length,
text_message.attachments.length
);
assert.equal(text_message.attachments.length, 0); assert.equal(text_message.attachments.length, 0);
}).then(done).catch(done); })
.then(done)
.catch(done);
}); });
}); });

View file

@ -8,9 +8,14 @@ describe('Protocol Wrapper', function() {
this.timeout(5000); this.timeout(5000);
before(function(done) { before(function(done) {
localStorage.clear(); localStorage.clear();
libsignal.KeyHelper.generateIdentityKeyPair().then(function(identityKey) { libsignal.KeyHelper.generateIdentityKeyPair()
return textsecure.storage.protocol.saveIdentity(identifier, identityKey); .then(function(identityKey) {
}).then(function() { return textsecure.storage.protocol.saveIdentity(
identifier,
identityKey
);
})
.then(function() {
done(); done();
}); });
}); });
@ -18,12 +23,15 @@ describe('Protocol Wrapper', function() {
it('rejects if the identity key changes', function(done) { it('rejects if the identity key changes', function(done) {
var address = new libsignal.SignalProtocolAddress(identifier, 1); var address = new libsignal.SignalProtocolAddress(identifier, 1);
var builder = new libsignal.SessionBuilder(store, address); var builder = new libsignal.SessionBuilder(store, address);
return builder.processPreKey({ return builder
.processPreKey({
identityKey: textsecure.crypto.getRandomBytes(33), identityKey: textsecure.crypto.getRandomBytes(33),
encodedNumber: address.toString() encodedNumber: address.toString(),
}).then(function() { })
.then(function() {
done(new Error('Allowed to overwrite identity key')); done(new Error('Allowed to overwrite identity key'));
}).catch(function(e) { })
.catch(function(e) {
assert.strictEqual(e.message, 'Identity key changed'); assert.strictEqual(e.message, 'Identity key changed');
done(); done();
}); });

View file

@ -1,7 +1,9 @@
'use strict'; 'use strict';
describe("SignalProtocolStore", function() { describe('SignalProtocolStore', function() {
before(function() { localStorage.clear(); }); before(function() {
localStorage.clear();
});
var store = textsecure.storage.protocol; var store = textsecure.storage.protocol;
var identifier = '+5558675309'; var identifier = '+5558675309';
var another_identifier = '+5555590210'; var another_identifier = '+5555590210';
@ -15,144 +17,184 @@ describe("SignalProtocolStore", function() {
}; };
it('retrieves my registration id', function(done) { it('retrieves my registration id', function(done) {
store.put('registrationId', 1337); store.put('registrationId', 1337);
store.getLocalRegistrationId().then(function(reg) { store
.getLocalRegistrationId()
.then(function(reg) {
assert.strictEqual(reg, 1337); assert.strictEqual(reg, 1337);
}).then(done, done); })
.then(done, done);
}); });
it('retrieves my identity key', function(done) { it('retrieves my identity key', function(done) {
store.put('identityKey', identityKey); store.put('identityKey', identityKey);
store.getIdentityKeyPair().then(function(key) { store
.getIdentityKeyPair()
.then(function(key) {
assertEqualArrayBuffers(key.pubKey, identityKey.pubKey); assertEqualArrayBuffers(key.pubKey, identityKey.pubKey);
assertEqualArrayBuffers(key.privKey, identityKey.privKey); assertEqualArrayBuffers(key.privKey, identityKey.privKey);
}).then(done,done); })
.then(done, done);
}); });
it('stores identity keys', function(done) { it('stores identity keys', function(done) {
store.saveIdentity(identifier, testKey.pubKey).then(function() { store
.saveIdentity(identifier, testKey.pubKey)
.then(function() {
return store.loadIdentityKey(identifier).then(function(key) { return store.loadIdentityKey(identifier).then(function(key) {
assertEqualArrayBuffers(key, testKey.pubKey); assertEqualArrayBuffers(key, testKey.pubKey);
}); });
}).then(done,done); })
.then(done, done);
}); });
it('returns whether a key is trusted', function(done) { it('returns whether a key is trusted', function(done) {
var newIdentity = libsignal.crypto.getRandomBytes(33); var newIdentity = libsignal.crypto.getRandomBytes(33);
store.saveIdentity(identifier, testKey.pubKey).then(function() { store.saveIdentity(identifier, testKey.pubKey).then(function() {
store.isTrustedIdentity(identifier, newIdentity).then(function(trusted) { store
.isTrustedIdentity(identifier, newIdentity)
.then(function(trusted) {
if (trusted) { if (trusted) {
done(new Error('Allowed to overwrite identity key')); done(new Error('Allowed to overwrite identity key'));
} else { } else {
done(); done();
} }
}).catch(done); })
.catch(done);
}); });
}); });
it('returns whether a key is untrusted', function(done) { it('returns whether a key is untrusted', function(done) {
var newIdentity = libsignal.crypto.getRandomBytes(33); var newIdentity = libsignal.crypto.getRandomBytes(33);
store.saveIdentity(identifier, testKey.pubKey).then(function() { store.saveIdentity(identifier, testKey.pubKey).then(function() {
store.isTrustedIdentity(identifier, testKey.pubKey).then(function(trusted) { store
.isTrustedIdentity(identifier, testKey.pubKey)
.then(function(trusted) {
if (trusted) { if (trusted) {
done(); done();
} else { } else {
done(new Error('Allowed to overwrite identity key')); done(new Error('Allowed to overwrite identity key'));
} }
}).catch(done); })
.catch(done);
}); });
}); });
it('stores prekeys', function(done) { it('stores prekeys', function(done) {
store.storePreKey(1, testKey).then(function() { store
.storePreKey(1, testKey)
.then(function() {
return store.loadPreKey(1).then(function(key) { return store.loadPreKey(1).then(function(key) {
assertEqualArrayBuffers(key.pubKey, testKey.pubKey); assertEqualArrayBuffers(key.pubKey, testKey.pubKey);
assertEqualArrayBuffers(key.privKey, testKey.privKey); assertEqualArrayBuffers(key.privKey, testKey.privKey);
}); });
}).then(done,done); })
.then(done, done);
}); });
it('deletes prekeys', function(done) { it('deletes prekeys', function(done) {
before(function(done) { before(function(done) {
store.storePreKey(2, testKey).then(done); store.storePreKey(2, testKey).then(done);
}); });
store.removePreKey(2, testKey).then(function() { store
.removePreKey(2, testKey)
.then(function() {
return store.loadPreKey(2).then(function(key) { return store.loadPreKey(2).then(function(key) {
assert.isUndefined(key); assert.isUndefined(key);
}); });
}).then(done,done); })
.then(done, done);
}); });
it('stores signed prekeys', function(done) { it('stores signed prekeys', function(done) {
store.storeSignedPreKey(3, testKey).then(function() { store
.storeSignedPreKey(3, testKey)
.then(function() {
return store.loadSignedPreKey(3).then(function(key) { return store.loadSignedPreKey(3).then(function(key) {
assertEqualArrayBuffers(key.pubKey, testKey.pubKey); assertEqualArrayBuffers(key.pubKey, testKey.pubKey);
assertEqualArrayBuffers(key.privKey, testKey.privKey); assertEqualArrayBuffers(key.privKey, testKey.privKey);
}); });
}).then(done,done); })
.then(done, done);
}); });
it('deletes signed prekeys', function(done) { it('deletes signed prekeys', function(done) {
before(function(done) { before(function(done) {
store.storeSignedPreKey(4, testKey).then(done); store.storeSignedPreKey(4, testKey).then(done);
}); });
store.removeSignedPreKey(4, testKey).then(function() { store
.removeSignedPreKey(4, testKey)
.then(function() {
return store.loadSignedPreKey(4).then(function(key) { return store.loadSignedPreKey(4).then(function(key) {
assert.isUndefined(key); assert.isUndefined(key);
}); });
}).then(done,done); })
.then(done, done);
}); });
it('stores sessions', function(done) { it('stores sessions', function(done) {
var testRecord = "an opaque string"; var testRecord = 'an opaque string';
var devices = [1, 2, 3].map(function(deviceId) { var devices = [1, 2, 3].map(function(deviceId) {
return [identifier, deviceId].join('.'); return [identifier, deviceId].join('.');
}); });
var promise = Promise.resolve(); var promise = Promise.resolve();
devices.forEach(function(encodedNumber) { devices.forEach(function(encodedNumber) {
promise = promise.then(function() { promise = promise.then(function() {
return store.storeSession(encodedNumber, testRecord + encodedNumber) return store.storeSession(encodedNumber, testRecord + encodedNumber);
}); });
}); });
promise.then(function() { promise
return Promise.all(devices.map(store.loadSession.bind(store))).then(function(records) { .then(function() {
return Promise.all(devices.map(store.loadSession.bind(store))).then(
function(records) {
for (var i in records) { for (var i in records) {
assert.strictEqual(records[i], testRecord + devices[i]); assert.strictEqual(records[i], testRecord + devices[i]);
}; }
}); }
}).then(done,done); );
})
.then(done, done);
}); });
it('removes all sessions for a number', function(done) { it('removes all sessions for a number', function(done) {
var testRecord = "an opaque string"; var testRecord = 'an opaque string';
var devices = [1, 2, 3].map(function(deviceId) { var devices = [1, 2, 3].map(function(deviceId) {
return [identifier, deviceId].join('.'); return [identifier, deviceId].join('.');
}); });
var promise = Promise.resolve(); var promise = Promise.resolve();
devices.forEach(function(encodedNumber) { devices.forEach(function(encodedNumber) {
promise = promise.then(function() { promise = promise.then(function() {
return store.storeSession(encodedNumber, testRecord + encodedNumber) return store.storeSession(encodedNumber, testRecord + encodedNumber);
}); });
}); });
promise.then(function() { promise
.then(function() {
return store.removeAllSessions(identifier).then(function(record) { return store.removeAllSessions(identifier).then(function(record) {
return Promise.all(devices.map(store.loadSession.bind(store))).then(function(records) { return Promise.all(devices.map(store.loadSession.bind(store))).then(
function(records) {
for (var i in records) { for (var i in records) {
assert.isUndefined(records[i]); assert.isUndefined(records[i]);
}; }
}
);
}); });
}); })
}).then(done,done); .then(done, done);
}); });
it('returns deviceIds for a number', function(done) { it('returns deviceIds for a number', function(done) {
var testRecord = "an opaque string"; var testRecord = 'an opaque string';
var devices = [1, 2, 3].map(function(deviceId) { var devices = [1, 2, 3].map(function(deviceId) {
return [identifier, deviceId].join('.'); return [identifier, deviceId].join('.');
}); });
var promise = Promise.resolve(); var promise = Promise.resolve();
devices.forEach(function(encodedNumber) { devices.forEach(function(encodedNumber) {
promise = promise.then(function() { promise = promise.then(function() {
return store.storeSession(encodedNumber, testRecord + encodedNumber) return store.storeSession(encodedNumber, testRecord + encodedNumber);
}); });
}); });
promise.then(function() { promise
.then(function() {
return store.getDeviceIds(identifier).then(function(deviceIds) { return store.getDeviceIds(identifier).then(function(deviceIds) {
assert.sameMembers(deviceIds, [1, 2, 3]); assert.sameMembers(deviceIds, [1, 2, 3]);
}); });
}).then(done,done); })
.then(done, done);
}); });
it('returns empty array for a number with no device ids', function(done) { it('returns empty array for a number with no device ids', function(done) {
return store.getDeviceIds('foo').then(function(deviceIds) { return store
.getDeviceIds('foo')
.then(function(deviceIds) {
assert.sameMembers(deviceIds, []); assert.sameMembers(deviceIds, []);
}).then(done,done); })
.then(done, done);
}); });
}); });

View file

@ -8,7 +8,7 @@ describe('createTaskWithTimeout', function() {
var taskWithTimeout = textsecure.createTaskWithTimeout(task); var taskWithTimeout = textsecure.createTaskWithTimeout(task);
return taskWithTimeout().then(function(result) { return taskWithTimeout().then(function(result) {
assert.strictEqual(result, 'hi!') assert.strictEqual(result, 'hi!');
}); });
}); });
it('flows error from promise back', function() { it('flows error from promise back', function() {
@ -34,14 +34,17 @@ describe('createTaskWithTimeout', function() {
}); });
}; };
var taskWithTimeout = textsecure.createTaskWithTimeout(task, this.name, { var taskWithTimeout = textsecure.createTaskWithTimeout(task, this.name, {
timeout: 10 timeout: 10,
}); });
return taskWithTimeout().then(function() { return taskWithTimeout().then(
function() {
throw new Error('it was not supposed to resolve!'); throw new Error('it was not supposed to resolve!');
}, function() { },
function() {
assert.strictEqual(complete, false); assert.strictEqual(complete, false);
}); }
);
}); });
it('resolves if task returns something falsey', function() { it('resolves if task returns something falsey', function() {
var task = function() {}; var task = function() {};
@ -54,7 +57,7 @@ describe('createTaskWithTimeout', function() {
}; };
var taskWithTimeout = textsecure.createTaskWithTimeout(task); var taskWithTimeout = textsecure.createTaskWithTimeout(task);
return taskWithTimeout().then(function(result) { return taskWithTimeout().then(function(result) {
assert.strictEqual(result, 'hi!') assert.strictEqual(result, 'hi!');
}); });
}); });
it('rejects if task throws (and does not log about taking too long)', function() { it('rejects if task throws (and does not log about taking too long)', function() {
@ -63,12 +66,15 @@ describe('createTaskWithTimeout', function() {
throw error; throw error;
}; };
var taskWithTimeout = textsecure.createTaskWithTimeout(task, this.name, { var taskWithTimeout = textsecure.createTaskWithTimeout(task, this.name, {
timeout: 10 timeout: 10,
}); });
return taskWithTimeout().then(function(result) { return taskWithTimeout().then(
throw new Error('Overall task should reject!') function(result) {
}, function(flowedError) { throw new Error('Overall task should reject!');
},
function(flowedError) {
assert.strictEqual(flowedError, error); assert.strictEqual(flowedError, error);
}); }
);
}); });
}); });

View file

@ -1,4 +1,4 @@
;(function() { (function() {
'use strict'; 'use strict';
describe('WebSocket-Resource', function() { describe('WebSocket-Resource', function() {
@ -9,7 +9,10 @@
var socket = { var socket = {
send: function(data) { send: function(data) {
var message = textsecure.protobuf.WebSocketMessage.decode(data); var message = textsecure.protobuf.WebSocketMessage.decode(data);
assert.strictEqual(message.type, textsecure.protobuf.WebSocketMessage.Type.RESPONSE); assert.strictEqual(
message.type,
textsecure.protobuf.WebSocketMessage.Type.RESPONSE
);
assert.strictEqual(message.response.message, 'OK'); assert.strictEqual(message.response.message, 'OK');
assert.strictEqual(message.response.status, 200); assert.strictEqual(message.response.status, 200);
assert.strictEqual(message.response.id.toString(), request_id); assert.strictEqual(message.response.id.toString(), request_id);
@ -23,9 +26,12 @@
handleRequest: function(request) { handleRequest: function(request) {
assert.strictEqual(request.verb, 'PUT'); assert.strictEqual(request.verb, 'PUT');
assert.strictEqual(request.path, '/some/path'); assert.strictEqual(request.path, '/some/path');
assertEqualArrayBuffers(request.body.toArrayBuffer(), new Uint8Array([1,2,3]).buffer); assertEqualArrayBuffers(
request.body.toArrayBuffer(),
new Uint8Array([1, 2, 3]).buffer
);
request.respond(200, 'OK'); request.respond(200, 'OK');
} },
}); });
// mock socket request // mock socket request
@ -37,10 +43,12 @@
id: request_id, id: request_id,
verb: 'PUT', verb: 'PUT',
path: '/some/path', path: '/some/path',
body: new Uint8Array([1,2,3]).buffer body: new Uint8Array([1, 2, 3]).buffer,
} },
}).encode().toArrayBuffer() })
]) .encode()
.toArrayBuffer(),
]),
}); });
}); });
@ -50,10 +58,16 @@
var socket = { var socket = {
send: function(data) { send: function(data) {
var message = textsecure.protobuf.WebSocketMessage.decode(data); var message = textsecure.protobuf.WebSocketMessage.decode(data);
assert.strictEqual(message.type, textsecure.protobuf.WebSocketMessage.Type.REQUEST); assert.strictEqual(
message.type,
textsecure.protobuf.WebSocketMessage.Type.REQUEST
);
assert.strictEqual(message.request.verb, 'PUT'); assert.strictEqual(message.request.verb, 'PUT');
assert.strictEqual(message.request.path, '/some/path'); assert.strictEqual(message.request.path, '/some/path');
assertEqualArrayBuffers(message.request.body.toArrayBuffer(), new Uint8Array([1,2,3]).buffer); assertEqualArrayBuffers(
message.request.body.toArrayBuffer(),
new Uint8Array([1, 2, 3]).buffer
);
request_id = message.request.id; request_id = message.request.id;
}, },
addEventListener: function() {}, addEventListener: function() {},
@ -70,7 +84,7 @@
assert.strictEqual(message, 'OK'); assert.strictEqual(message, 'OK');
assert.strictEqual(status, 200); assert.strictEqual(status, 200);
done(); done();
} },
}); });
// mock socket response // mock socket response
@ -78,36 +92,51 @@
data: new Blob([ data: new Blob([
new textsecure.protobuf.WebSocketMessage({ new textsecure.protobuf.WebSocketMessage({
type: textsecure.protobuf.WebSocketMessage.Type.RESPONSE, type: textsecure.protobuf.WebSocketMessage.Type.RESPONSE,
response: { id: request_id, message: 'OK', status: 200 } response: { id: request_id, message: 'OK', status: 200 },
}).encode().toArrayBuffer() })
]) .encode()
.toArrayBuffer(),
]),
}); });
}); });
}); });
describe('close', function() { describe('close', function() {
before(function() { window.WebSocket = MockSocket; }); before(function() {
after (function() { window.WebSocket = WebSocket; }); window.WebSocket = MockSocket;
});
after(function() {
window.WebSocket = WebSocket;
});
it('closes the connection', function(done) { it('closes the connection', function(done) {
var mockServer = new MockServer('ws://localhost:8081'); var mockServer = new MockServer('ws://localhost:8081');
mockServer.on('connection', function(server) { mockServer.on('connection', function(server) {
server.on('close', done); server.on('close', done);
}); });
var resource = new WebSocketResource(new WebSocket('ws://localhost:8081')); var resource = new WebSocketResource(
new WebSocket('ws://localhost:8081')
);
resource.close(); resource.close();
}); });
}); });
describe.skip('with a keepalive config', function() { describe.skip('with a keepalive config', function() {
before(function() { window.WebSocket = MockSocket; }); before(function() {
after (function() { window.WebSocket = WebSocket; }); window.WebSocket = MockSocket;
});
after(function() {
window.WebSocket = WebSocket;
});
this.timeout(60000); this.timeout(60000);
it('sends keepalives once a minute', function(done) { it('sends keepalives once a minute', function(done) {
var mockServer = new MockServer('ws://localhost:8081'); var mockServer = new MockServer('ws://localhost:8081');
mockServer.on('connection', function(server) { mockServer.on('connection', function(server) {
server.on('message', function(data) { server.on('message', function(data) {
var message = textsecure.protobuf.WebSocketMessage.decode(data); var message = textsecure.protobuf.WebSocketMessage.decode(data);
assert.strictEqual(message.type, textsecure.protobuf.WebSocketMessage.Type.REQUEST); assert.strictEqual(
message.type,
textsecure.protobuf.WebSocketMessage.Type.REQUEST
);
assert.strictEqual(message.request.verb, 'GET'); assert.strictEqual(message.request.verb, 'GET');
assert.strictEqual(message.request.path, '/v1/keepalive'); assert.strictEqual(message.request.path, '/v1/keepalive');
server.close(); server.close();
@ -115,7 +144,7 @@
}); });
}); });
new WebSocketResource(new WebSocket('ws://localhost:8081'), { new WebSocketResource(new WebSocket('ws://localhost:8081'), {
keepalive: { path: '/v1/keepalive' } keepalive: { path: '/v1/keepalive' },
}); });
}); });
@ -124,7 +153,10 @@
mockServer.on('connection', function(server) { mockServer.on('connection', function(server) {
server.on('message', function(data) { server.on('message', function(data) {
var message = textsecure.protobuf.WebSocketMessage.decode(data); var message = textsecure.protobuf.WebSocketMessage.decode(data);
assert.strictEqual(message.type, textsecure.protobuf.WebSocketMessage.Type.REQUEST); assert.strictEqual(
message.type,
textsecure.protobuf.WebSocketMessage.Type.REQUEST
);
assert.strictEqual(message.request.verb, 'GET'); assert.strictEqual(message.request.verb, 'GET');
assert.strictEqual(message.request.path, '/'); assert.strictEqual(message.request.path, '/');
server.close(); server.close();
@ -132,9 +164,8 @@
}); });
}); });
new WebSocketResource(new WebSocket('ws://localhost:8081'), { new WebSocketResource(new WebSocket('ws://localhost:8081'), {
keepalive: true keepalive: true,
}); });
}); });
it('optionally disconnects if no response', function(done) { it('optionally disconnects if no response', function(done) {
@ -155,19 +186,25 @@
mockServer.on('connection', function(server) { mockServer.on('connection', function(server) {
server.on('message', function(data) { server.on('message', function(data) {
var message = textsecure.protobuf.WebSocketMessage.decode(data); var message = textsecure.protobuf.WebSocketMessage.decode(data);
assert.strictEqual(message.type, textsecure.protobuf.WebSocketMessage.Type.REQUEST); assert.strictEqual(
message.type,
textsecure.protobuf.WebSocketMessage.Type.REQUEST
);
assert.strictEqual(message.request.verb, 'GET'); assert.strictEqual(message.request.verb, 'GET');
assert.strictEqual(message.request.path, '/'); assert.strictEqual(message.request.path, '/');
assert(Date.now() > startTime + 60000, 'keepalive time should be longer than a minute'); assert(
Date.now() > startTime + 60000,
'keepalive time should be longer than a minute'
);
server.close(); server.close();
done(); done();
}); });
}); });
var resource = new WebSocketResource(socket, { keepalive: true }); var resource = new WebSocketResource(socket, { keepalive: true });
setTimeout(function() { setTimeout(function() {
resource.resetKeepAliveTimer() resource.resetKeepAliveTimer();
}, 5000); }, 5000);
}); });
}); });
}); });
}()); })();

View file

@ -1,7 +1,11 @@
describe('TextSecureWebSocket', function() { describe('TextSecureWebSocket', function() {
var RealWebSocket = window.WebSocket; var RealWebSocket = window.WebSocket;
before(function() { window.WebSocket = MockSocket; }); before(function() {
after (function() { window.WebSocket = RealWebSocket; }); window.WebSocket = MockSocket;
});
after(function() {
window.WebSocket = RealWebSocket;
});
it('connects and disconnects', function(done) { it('connects and disconnects', function(done) {
var mockServer = new MockServer('ws://localhost:8080'); var mockServer = new MockServer('ws://localhost:8080');
mockServer.on('connection', function(server) { mockServer.on('connection', function(server) {
@ -27,7 +31,6 @@ describe('TextSecureWebSocket', function() {
done(); done();
}; };
socket.send('syn'); socket.send('syn');
}); });
it('exposes the socket status', function(done) { it('exposes the socket status', function(done) {
@ -58,5 +61,4 @@ describe('TextSecureWebSocket', function() {
}; };
mockServer.close(); mockServer.close();
}); });
}); });

View file

@ -1,4 +1,4 @@
;(function(){ (function() {
'use strict'; 'use strict';
/* /*
@ -54,8 +54,10 @@
socket.send( socket.send(
new textsecure.protobuf.WebSocketMessage({ new textsecure.protobuf.WebSocketMessage({
type: textsecure.protobuf.WebSocketMessage.Type.RESPONSE, type: textsecure.protobuf.WebSocketMessage.Type.RESPONSE,
response: { id: request.id, message: message, status: status } response: { id: request.id, message: message, status: status },
}).encode().toArrayBuffer() })
.encode()
.toArrayBuffer()
); );
}; };
}; };
@ -71,9 +73,11 @@
verb: request.verb, verb: request.verb,
path: request.path, path: request.path,
body: request.body, body: request.body,
id : request.id id: request.id,
} },
}).encode().toArrayBuffer() })
.encode()
.toArrayBuffer()
); );
}; };
@ -93,18 +97,21 @@
var blob = socketMessage.data; var blob = socketMessage.data;
var handleArrayBuffer = function(buffer) { var handleArrayBuffer = function(buffer) {
var message = textsecure.protobuf.WebSocketMessage.decode(buffer); var message = textsecure.protobuf.WebSocketMessage.decode(buffer);
if (message.type === textsecure.protobuf.WebSocketMessage.Type.REQUEST ) { if (
message.type === textsecure.protobuf.WebSocketMessage.Type.REQUEST
) {
handleRequest( handleRequest(
new IncomingWebSocketRequest({ new IncomingWebSocketRequest({
verb: message.request.verb, verb: message.request.verb,
path: message.request.path, path: message.request.path,
body: message.request.body, body: message.request.body,
id: message.request.id, id: message.request.id,
socket : socket socket: socket,
}) })
); );
} } else if (
else if (message.type === textsecure.protobuf.WebSocketMessage.Type.RESPONSE ) { message.type === textsecure.protobuf.WebSocketMessage.Type.RESPONSE
) {
var response = message.response; var response = message.response;
var request = outgoing[response.id]; var request = outgoing[response.id];
if (request) { if (request) {
@ -118,7 +125,8 @@
callback(response.message, response.status, request); callback(response.message, response.status, request);
} }
} else { } else {
throw 'Received response for unknown request ' + message.response.id; throw 'Received response for unknown request ' +
message.response.id;
} }
} }
}; };
@ -137,17 +145,23 @@
if (opts.keepalive) { if (opts.keepalive) {
this.keepalive = new KeepAlive(this, { this.keepalive = new KeepAlive(this, {
path: opts.keepalive.path, path: opts.keepalive.path,
disconnect : opts.keepalive.disconnect disconnect: opts.keepalive.disconnect,
}); });
var resetKeepAliveTimer = this.keepalive.reset.bind(this.keepalive); var resetKeepAliveTimer = this.keepalive.reset.bind(this.keepalive);
socket.addEventListener('open', resetKeepAliveTimer); socket.addEventListener('open', resetKeepAliveTimer);
socket.addEventListener('message', resetKeepAliveTimer); socket.addEventListener('message', resetKeepAliveTimer);
socket.addEventListener('close', this.keepalive.stop.bind(this.keepalive)); socket.addEventListener(
'close',
this.keepalive.stop.bind(this.keepalive)
);
} }
socket.addEventListener('close', function() { socket.addEventListener(
'close',
function() {
this.closed = true; this.closed = true;
}.bind(this)) }.bind(this)
);
this.close = function(code, reason) { this.close = function(code, reason) {
if (this.closed) { if (this.closed) {
@ -168,7 +182,8 @@
// On linux the socket can wait a long time to emit its close event if we've // 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 // lost the internet connection. On the order of minutes. This speeds that
// process up. // process up.
setTimeout(function() { setTimeout(
function() {
if (this.closed) { if (this.closed) {
return; return;
} }
@ -179,12 +194,13 @@
ev.code = code; ev.code = code;
ev.reason = reason; ev.reason = reason;
this.dispatchEvent(ev); this.dispatchEvent(ev);
}.bind(this), 1000); }.bind(this),
1000
);
}; };
}; };
window.WebSocketResource.prototype = new textsecure.EventTarget(); window.WebSocketResource.prototype = new textsecure.EventTarget();
function KeepAlive(websocketResource, opts) { function KeepAlive(websocketResource, opts) {
if (websocketResource instanceof WebSocketResource) { if (websocketResource instanceof WebSocketResource) {
opts = opts || {}; opts = opts || {};
@ -211,13 +227,17 @@
reset: function() { reset: function() {
clearTimeout(this.keepAliveTimer); clearTimeout(this.keepAliveTimer);
clearTimeout(this.disconnectTimer); clearTimeout(this.disconnectTimer);
this.keepAliveTimer = setTimeout(function() { this.keepAliveTimer = setTimeout(
function() {
if (this.disconnect) { if (this.disconnect) {
// automatically disconnect if server doesn't ack // automatically disconnect if server doesn't ack
this.disconnectTimer = setTimeout(function() { this.disconnectTimer = setTimeout(
function() {
clearTimeout(this.keepAliveTimer); clearTimeout(this.keepAliveTimer);
this.wsr.close(3001, 'No response to keepalive request'); this.wsr.close(3001, 'No response to keepalive request');
}.bind(this), 1000); }.bind(this),
1000
);
} else { } else {
this.reset(); this.reset();
} }
@ -225,10 +245,11 @@
this.wsr.sendRequest({ this.wsr.sendRequest({
verb: 'GET', verb: 'GET',
path: this.path, path: this.path,
success: this.reset.bind(this) success: this.reset.bind(this),
}); });
}.bind(this), 55000); }.bind(this),
55000
);
}, },
}; };
})();
}());