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/*
dist/*
libtextsecure/libsignal-protocol.js
test/fixtures.js
test/blanket_mocha.js
/**/*.json
/**/*.css

View file

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

View file

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

View file

@ -1,241 +1,352 @@
function OutgoingMessage(server, timestamp, numbers, message, silent, callback) {
if (message instanceof textsecure.protobuf.DataMessage) {
var content = new textsecure.protobuf.Content();
content.dataMessage = message;
message = content;
}
this.server = server;
this.timestamp = timestamp;
this.numbers = numbers;
this.message = message; // ContentMessage proto
this.callback = callback;
this.silent = silent;
function OutgoingMessage(
server,
timestamp,
numbers,
message,
silent,
callback
) {
if (message instanceof textsecure.protobuf.DataMessage) {
var content = new textsecure.protobuf.Content();
content.dataMessage = message;
message = content;
}
this.server = server;
this.timestamp = timestamp;
this.numbers = numbers;
this.message = message; // ContentMessage proto
this.callback = callback;
this.silent = silent;
this.numbersCompleted = 0;
this.errors = [];
this.successfulNumbers = [];
this.numbersCompleted = 0;
this.errors = [];
this.successfulNumbers = [];
}
OutgoingMessage.prototype = {
constructor: OutgoingMessage,
numberCompleted: function() {
this.numbersCompleted++;
if (this.numbersCompleted >= this.numbers.length) {
this.callback({successfulNumbers: this.successfulNumbers, errors: this.errors});
}
},
registerError: function(number, reason, error) {
if (!error || error.name === 'HTTPError' && error.code !== 404) {
error = new textsecure.OutgoingMessageError(number, this.message.toArrayBuffer(), this.timestamp, error);
}
error.number = number;
error.reason = reason;
this.errors[this.errors.length] = error;
this.numberCompleted();
},
reloadDevicesAndSend: function(number, recurse) {
return function() {
return textsecure.storage.protocol.getDeviceIds(number).then(function(deviceIds) {
if (deviceIds.length == 0) {
return this.registerError(number, "Got empty device list when loading device keys", null);
}
return this.doSendMessage(number, deviceIds, recurse);
}.bind(this));
}.bind(this);
},
getKeysForNumber: function(number, updateDevices) {
var handleResult = function(response) {
return Promise.all(response.devices.map(function(device) {
device.identityKey = response.identityKey;
if (updateDevices === undefined || updateDevices.indexOf(device.deviceId) > -1) {
var address = new libsignal.SignalProtocolAddress(number, device.deviceId);
var builder = new libsignal.SessionBuilder(textsecure.storage.protocol, address);
if (device.registrationId === 0) {
console.log("device registrationId 0!");
}
return builder.processPreKey(device).catch(function(error) {
if (error.message === "Identity key changed") {
error.timestamp = this.timestamp;
error.originalMessage = this.message.toArrayBuffer();
error.identityKey = device.identityKey;
}
throw error;
}.bind(this));
}
}.bind(this)));
}.bind(this);
if (updateDevices === undefined) {
return this.server.getKeysForNumber(number).then(handleResult);
} else {
var promise = Promise.resolve();
updateDevices.forEach(function(device) {
promise = promise.then(function() {
return this.server.getKeysForNumber(number, device).then(handleResult).catch(function(e) {
if (e.name === 'HTTPError' && e.code === 404) {
if (device !== 1) {
return this.removeDeviceIdsForNumber(number, [device]);
} else {
throw new textsecure.UnregisteredUserError(number, e);
}
} else {
throw e;
}
}.bind(this));
}.bind(this));
}.bind(this));
return promise;
}
},
transmitMessage: function(number, jsonData, timestamp) {
return this.server.sendMessages(number, jsonData, timestamp, this.silent).catch(function(e) {
if (e.name === 'HTTPError' && (e.code !== 409 && e.code !== 410)) {
// 409 and 410 should bubble and be handled by doSendMessage
// 404 should throw UnregisteredUserError
// all other network errors can be retried later.
if (e.code === 404) {
throw new textsecure.UnregisteredUserError(number, e);
}
throw new textsecure.SendMessageNetworkError(number, jsonData, e, timestamp);
}
throw e;
});
},
getPaddedMessageLength: function(messageLength) {
var messageLengthWithTerminator = messageLength + 1;
var messagePartCount = Math.floor(messageLengthWithTerminator / 160);
if (messageLengthWithTerminator % 160 !== 0) {
messagePartCount++;
}
return messagePartCount * 160;
},
getPlaintext: function() {
if (!this.plaintext) {
var messageBuffer = this.message.toArrayBuffer();
this.plaintext = new Uint8Array(
this.getPaddedMessageLength(messageBuffer.byteLength + 1) - 1
);
this.plaintext.set(new Uint8Array(messageBuffer));
this.plaintext[messageBuffer.byteLength] = 0x80;
}
return this.plaintext;
},
doSendMessage: function(number, deviceIds, recurse) {
var ciphers = {};
var plaintext = this.getPlaintext();
return Promise.all(deviceIds.map(function(deviceId) {
var address = new libsignal.SignalProtocolAddress(number, deviceId);
var ourNumber = textsecure.storage.user.getNumber();
var options = {};
// No limit on message keys if we're communicating with our other devices
if (ourNumber === number) {
options.messageKeysLimit = false;
}
var sessionCipher = new libsignal.SessionCipher(textsecure.storage.protocol, address, options);
ciphers[address.getDeviceId()] = sessionCipher;
return sessionCipher.encrypt(plaintext).then(function(ciphertext) {
return {
type : ciphertext.type,
destinationDeviceId : address.getDeviceId(),
destinationRegistrationId : ciphertext.registrationId,
content : btoa(ciphertext.body)
};
});
}.bind(this))).then(function(jsonData) {
return this.transmitMessage(number, jsonData, this.timestamp).then(function() {
this.successfulNumbers[this.successfulNumbers.length] = number;
this.numberCompleted();
}.bind(this));
}.bind(this)).catch(function(error) {
if (error instanceof Error && error.name == "HTTPError" && (error.code == 410 || error.code == 409)) {
if (!recurse)
return this.registerError(number, "Hit retry limit attempting to reload device list", error);
var p;
if (error.code == 409) {
p = this.removeDeviceIdsForNumber(number, error.response.extraDevices);
} else {
p = Promise.all(error.response.staleDevices.map(function(deviceId) {
return ciphers[deviceId].closeOpenSessionForDevice();
}));
}
return p.then(function() {
var resetDevices = ((error.code == 410) ? error.response.staleDevices : error.response.missingDevices);
return this.getKeysForNumber(number, resetDevices)
.then(this.reloadDevicesAndSend(number, error.code == 409));
}.bind(this));
} else if (error.message === "Identity key changed") {
error.timestamp = this.timestamp;
error.originalMessage = this.message.toArrayBuffer();
console.log('Got "key changed" error from encrypt - no identityKey for application layer', number, deviceIds)
throw error;
} else {
this.registerError(number, "Failed to create or send message", error);
}
}.bind(this));
},
getStaleDeviceIdsForNumber: function(number) {
return textsecure.storage.protocol.getDeviceIds(number).then(function(deviceIds) {
if (deviceIds.length === 0) {
return [1];
}
var updateDevices = [];
return Promise.all(deviceIds.map(function(deviceId) {
var address = new libsignal.SignalProtocolAddress(number, deviceId);
var sessionCipher = new libsignal.SessionCipher(textsecure.storage.protocol, address);
return sessionCipher.hasOpenSession().then(function(hasSession) {
if (!hasSession) {
updateDevices.push(deviceId);
}
});
})).then(function() {
return updateDevices;
});
});
},
removeDeviceIdsForNumber: function(number, deviceIdsToRemove) {
var promise = Promise.resolve();
for (var j in deviceIdsToRemove) {
promise = promise.then(function() {
var encodedNumber = number + "." + deviceIdsToRemove[j];
return textsecure.storage.protocol.removeSession(encodedNumber);
});
}
return promise;
},
sendToNumber: function(number) {
return this.getStaleDeviceIdsForNumber(number).then(function(updateDevices) {
return this.getKeysForNumber(number, updateDevices)
.then(this.reloadDevicesAndSend(number, true))
.catch(function(error) {
if (error.message === "Identity key changed") {
error = new textsecure.OutgoingIdentityKeyError(
number, error.originalMessage, error.timestamp, error.identityKey
);
this.registerError(number, "Identity key changed", error);
} else {
this.registerError(
number, "Failed to retrieve new device keys for number " + number, error
);
}
}.bind(this));
}.bind(this));
constructor: OutgoingMessage,
numberCompleted: function() {
this.numbersCompleted++;
if (this.numbersCompleted >= this.numbers.length) {
this.callback({
successfulNumbers: this.successfulNumbers,
errors: this.errors,
});
}
},
registerError: function(number, reason, error) {
if (!error || (error.name === 'HTTPError' && error.code !== 404)) {
error = new textsecure.OutgoingMessageError(
number,
this.message.toArrayBuffer(),
this.timestamp,
error
);
}
error.number = number;
error.reason = reason;
this.errors[this.errors.length] = error;
this.numberCompleted();
},
reloadDevicesAndSend: function(number, recurse) {
return function() {
return textsecure.storage.protocol.getDeviceIds(number).then(
function(deviceIds) {
if (deviceIds.length == 0) {
return this.registerError(
number,
'Got empty device list when loading device keys',
null
);
}
return this.doSendMessage(number, deviceIds, recurse);
}.bind(this)
);
}.bind(this);
},
getKeysForNumber: function(number, updateDevices) {
var handleResult = function(response) {
return Promise.all(
response.devices.map(
function(device) {
device.identityKey = response.identityKey;
if (
updateDevices === undefined ||
updateDevices.indexOf(device.deviceId) > -1
) {
var address = new libsignal.SignalProtocolAddress(
number,
device.deviceId
);
var builder = new libsignal.SessionBuilder(
textsecure.storage.protocol,
address
);
if (device.registrationId === 0) {
console.log('device registrationId 0!');
}
return builder.processPreKey(device).catch(
function(error) {
if (error.message === 'Identity key changed') {
error.timestamp = this.timestamp;
error.originalMessage = this.message.toArrayBuffer();
error.identityKey = device.identityKey;
}
throw error;
}.bind(this)
);
}
}.bind(this)
)
);
}.bind(this);
if (updateDevices === undefined) {
return this.server.getKeysForNumber(number).then(handleResult);
} else {
var promise = Promise.resolve();
updateDevices.forEach(
function(device) {
promise = promise.then(
function() {
return this.server
.getKeysForNumber(number, device)
.then(handleResult)
.catch(
function(e) {
if (e.name === 'HTTPError' && e.code === 404) {
if (device !== 1) {
return this.removeDeviceIdsForNumber(number, [device]);
} else {
throw new textsecure.UnregisteredUserError(number, e);
}
} else {
throw e;
}
}.bind(this)
);
}.bind(this)
);
}.bind(this)
);
return promise;
}
},
transmitMessage: function(number, jsonData, timestamp) {
return this.server
.sendMessages(number, jsonData, timestamp, this.silent)
.catch(function(e) {
if (e.name === 'HTTPError' && (e.code !== 409 && e.code !== 410)) {
// 409 and 410 should bubble and be handled by doSendMessage
// 404 should throw UnregisteredUserError
// all other network errors can be retried later.
if (e.code === 404) {
throw new textsecure.UnregisteredUserError(number, e);
}
throw new textsecure.SendMessageNetworkError(
number,
jsonData,
e,
timestamp
);
}
throw e;
});
},
getPaddedMessageLength: function(messageLength) {
var messageLengthWithTerminator = messageLength + 1;
var messagePartCount = Math.floor(messageLengthWithTerminator / 160);
if (messageLengthWithTerminator % 160 !== 0) {
messagePartCount++;
}
return messagePartCount * 160;
},
getPlaintext: function() {
if (!this.plaintext) {
var messageBuffer = this.message.toArrayBuffer();
this.plaintext = new Uint8Array(
this.getPaddedMessageLength(messageBuffer.byteLength + 1) - 1
);
this.plaintext.set(new Uint8Array(messageBuffer));
this.plaintext[messageBuffer.byteLength] = 0x80;
}
return this.plaintext;
},
doSendMessage: function(number, deviceIds, recurse) {
var ciphers = {};
var plaintext = this.getPlaintext();
return Promise.all(
deviceIds.map(
function(deviceId) {
var address = new libsignal.SignalProtocolAddress(number, deviceId);
var ourNumber = textsecure.storage.user.getNumber();
var options = {};
// No limit on message keys if we're communicating with our other devices
if (ourNumber === number) {
options.messageKeysLimit = false;
}
var sessionCipher = new libsignal.SessionCipher(
textsecure.storage.protocol,
address,
options
);
ciphers[address.getDeviceId()] = sessionCipher;
return sessionCipher.encrypt(plaintext).then(function(ciphertext) {
return {
type: ciphertext.type,
destinationDeviceId: address.getDeviceId(),
destinationRegistrationId: ciphertext.registrationId,
content: btoa(ciphertext.body),
};
});
}.bind(this)
)
)
.then(
function(jsonData) {
return this.transmitMessage(number, jsonData, this.timestamp).then(
function() {
this.successfulNumbers[this.successfulNumbers.length] = number;
this.numberCompleted();
}.bind(this)
);
}.bind(this)
)
.catch(
function(error) {
if (
error instanceof Error &&
error.name == 'HTTPError' &&
(error.code == 410 || error.code == 409)
) {
if (!recurse)
return this.registerError(
number,
'Hit retry limit attempting to reload device list',
error
);
var p;
if (error.code == 409) {
p = this.removeDeviceIdsForNumber(
number,
error.response.extraDevices
);
} else {
p = Promise.all(
error.response.staleDevices.map(function(deviceId) {
return ciphers[deviceId].closeOpenSessionForDevice();
})
);
}
return p.then(
function() {
var resetDevices =
error.code == 410
? error.response.staleDevices
: error.response.missingDevices;
return this.getKeysForNumber(number, resetDevices).then(
this.reloadDevicesAndSend(number, error.code == 409)
);
}.bind(this)
);
} else if (error.message === 'Identity key changed') {
error.timestamp = this.timestamp;
error.originalMessage = this.message.toArrayBuffer();
console.log(
'Got "key changed" error from encrypt - no identityKey for application layer',
number,
deviceIds
);
throw error;
} else {
this.registerError(
number,
'Failed to create or send message',
error
);
}
}.bind(this)
);
},
getStaleDeviceIdsForNumber: function(number) {
return textsecure.storage.protocol
.getDeviceIds(number)
.then(function(deviceIds) {
if (deviceIds.length === 0) {
return [1];
}
var updateDevices = [];
return Promise.all(
deviceIds.map(function(deviceId) {
var address = new libsignal.SignalProtocolAddress(number, deviceId);
var sessionCipher = new libsignal.SessionCipher(
textsecure.storage.protocol,
address
);
return sessionCipher.hasOpenSession().then(function(hasSession) {
if (!hasSession) {
updateDevices.push(deviceId);
}
});
})
).then(function() {
return updateDevices;
});
});
},
removeDeviceIdsForNumber: function(number, deviceIdsToRemove) {
var promise = Promise.resolve();
for (var j in deviceIdsToRemove) {
promise = promise.then(function() {
var encodedNumber = number + '.' + deviceIdsToRemove[j];
return textsecure.storage.protocol.removeSession(encodedNumber);
});
}
return promise;
},
sendToNumber: function(number) {
return this.getStaleDeviceIdsForNumber(number).then(
function(updateDevices) {
return this.getKeysForNumber(number, updateDevices)
.then(this.reloadDevicesAndSend(number, true))
.catch(
function(error) {
if (error.message === 'Identity key changed') {
error = new textsecure.OutgoingIdentityKeyError(
number,
error.originalMessage,
error.timestamp,
error.identityKey
);
this.registerError(number, 'Identity key changed', error);
} else {
this.registerError(
number,
'Failed to retrieve new device keys for number ' + number,
error
);
}
}.bind(this)
);
}.bind(this)
);
},
};

View file

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

View file

@ -1,11 +1,11 @@
;(function() {
'use strict';
window.textsecure = window.textsecure || {};
window.textsecure.storage = window.textsecure.storage || {};
(function() {
'use strict';
window.textsecure = window.textsecure || {};
window.textsecure.storage = window.textsecure.storage || {};
textsecure.storage.protocol = new SignalProtocolStore();
textsecure.storage.protocol = new SignalProtocolStore();
textsecure.ProvisioningCipher = libsignal.ProvisioningCipher;
textsecure.startWorker = libsignal.worker.startWorker;
textsecure.stopWorker = libsignal.worker.stopWorker;
textsecure.ProvisioningCipher = libsignal.ProvisioningCipher;
textsecure.startWorker = libsignal.worker.startWorker;
textsecure.stopWorker = libsignal.worker.stopWorker;
})();

File diff suppressed because it is too large Load diff

View file

@ -1,46 +1,42 @@
'use strict';
;(function() {
(function() {
/************************************************
*** Utilities to store data in local storage ***
************************************************/
window.textsecure = window.textsecure || {};
window.textsecure.storage = window.textsecure.storage || {};
/************************************************
*** Utilities to store data in local storage ***
************************************************/
window.textsecure = window.textsecure || {};
window.textsecure.storage = window.textsecure.storage || {};
// Overrideable storage implementation
window.textsecure.storage.impl = window.textsecure.storage.impl || {
/*****************************
*** Base Storage Routines ***
*****************************/
put: function(key, value) {
if (value === undefined) throw new Error('Tried to store undefined');
localStorage.setItem('' + key, textsecure.utils.jsonThing(value));
},
// Overrideable storage implementation
window.textsecure.storage.impl = window.textsecure.storage.impl || {
/*****************************
*** Base Storage Routines ***
*****************************/
put: function(key, value) {
if (value === undefined)
throw new Error("Tried to store undefined");
localStorage.setItem("" + key, textsecure.utils.jsonThing(value));
},
get: function(key, defaultValue) {
var value = localStorage.getItem('' + key);
if (value === null) return defaultValue;
return JSON.parse(value);
},
get: function(key, defaultValue) {
var value = localStorage.getItem("" + key);
if (value === null)
return defaultValue;
return JSON.parse(value);
},
remove: function(key) {
localStorage.removeItem('' + key);
},
};
remove: function(key) {
localStorage.removeItem("" + key);
},
};
window.textsecure.storage.put = function(key, value) {
return textsecure.storage.impl.put(key, value);
};
window.textsecure.storage.put = function(key, value) {
return textsecure.storage.impl.put(key, value);
};
window.textsecure.storage.get = function(key, defaultValue) {
return textsecure.storage.impl.get(key, defaultValue);
};
window.textsecure.storage.get = function(key, defaultValue) {
return textsecure.storage.impl.get(key, defaultValue);
};
window.textsecure.storage.remove = function(key) {
return textsecure.storage.impl.remove(key);
};
window.textsecure.storage.remove = function(key) {
return textsecure.storage.impl.remove(key);
};
})();

View file

@ -1,144 +1,164 @@
;(function() {
'use strict';
(function() {
'use strict';
/*********************
*** Group Storage ***
*********************/
window.textsecure = window.textsecure || {};
window.textsecure.storage = window.textsecure.storage || {};
/*********************
*** Group Storage ***
*********************/
window.textsecure = window.textsecure || {};
window.textsecure.storage = window.textsecure.storage || {};
// create a random group id that we haven't seen before.
function generateNewGroupId() {
var groupId = getString(libsignal.crypto.getRandomBytes(16));
return textsecure.storage.protocol.getGroup(groupId).then(function(group) {
if (group === undefined) {
return groupId;
} else {
console.warn('group id collision'); // probably a bad sign.
return generateNewGroupId();
}
});
}
// create a random group id that we haven't seen before.
function generateNewGroupId() {
var groupId = getString(libsignal.crypto.getRandomBytes(16));
return textsecure.storage.protocol.getGroup(groupId).then(function(group) {
if (group === undefined) {
return groupId;
} else {
console.warn('group id collision'); // probably a bad sign.
return generateNewGroupId();
}
});
}
window.textsecure.storage.groups = {
createNewGroup: function(numbers, groupId) {
var groupId = groupId;
return new Promise(function(resolve) {
if (groupId !== undefined) {
resolve(textsecure.storage.protocol.getGroup(groupId).then(function(group) {
if (group !== undefined) {
throw new Error("Tried to recreate group");
}
}));
} else {
resolve(generateNewGroupId().then(function(newGroupId) {
groupId = newGroupId;
}));
}
}).then(function() {
var me = textsecure.storage.user.getNumber();
var haveMe = false;
var finalNumbers = [];
for (var i in numbers) {
var number = numbers[i];
if (!textsecure.utils.isNumberSane(number))
throw new Error("Invalid number in group");
if (number == me)
haveMe = true;
if (finalNumbers.indexOf(number) < 0)
finalNumbers.push(number);
}
if (!haveMe)
finalNumbers.push(me);
var groupObject = {numbers: finalNumbers, numberRegistrationIds: {}};
for (var i in finalNumbers)
groupObject.numberRegistrationIds[finalNumbers[i]] = {};
return textsecure.storage.protocol.putGroup(groupId, groupObject).then(function() {
return {id: groupId, numbers: finalNumbers};
});
});
},
getNumbers: function(groupId) {
return textsecure.storage.protocol.getGroup(groupId).then(function(group) {
if (group === undefined)
return undefined;
return group.numbers;
});
},
removeNumber: function(groupId, number) {
return textsecure.storage.protocol.getGroup(groupId).then(function(group) {
if (group === undefined)
return undefined;
var me = textsecure.storage.user.getNumber();
if (number == me)
throw new Error("Cannot remove ourselves from a group, leave the group instead");
var i = group.numbers.indexOf(number);
if (i > -1) {
group.numbers.splice(i, 1);
delete group.numberRegistrationIds[number];
return textsecure.storage.protocol.putGroup(groupId, group).then(function() {
return group.numbers;
});
}
return group.numbers;
});
},
addNumbers: function(groupId, numbers) {
return textsecure.storage.protocol.getGroup(groupId).then(function(group) {
if (group === undefined)
return undefined;
for (var i in numbers) {
var number = numbers[i];
if (!textsecure.utils.isNumberSane(number))
throw new Error("Invalid number in set to add to group");
if (group.numbers.indexOf(number) < 0) {
group.numbers.push(number);
group.numberRegistrationIds[number] = {};
}
}
return textsecure.storage.protocol.putGroup(groupId, group).then(function() {
return group.numbers;
});
});
},
deleteGroup: function(groupId) {
return textsecure.storage.protocol.removeGroup(groupId);
},
getGroup: function(groupId) {
return textsecure.storage.protocol.getGroup(groupId).then(function(group) {
if (group === undefined)
return undefined;
return { id: groupId, numbers: group.numbers };
});
},
updateNumbers: function(groupId, numbers) {
return textsecure.storage.protocol.getGroup(groupId).then(function(group) {
if (group === undefined)
throw new Error("Tried to update numbers for unknown group");
if (numbers.filter(textsecure.utils.isNumberSane).length < numbers.length)
throw new Error("Invalid number in new group members");
var added = numbers.filter(function(number) { return group.numbers.indexOf(number) < 0; });
return textsecure.storage.groups.addNumbers(groupId, added);
});
window.textsecure.storage.groups = {
createNewGroup: function(numbers, groupId) {
var groupId = groupId;
return new Promise(function(resolve) {
if (groupId !== undefined) {
resolve(
textsecure.storage.protocol.getGroup(groupId).then(function(group) {
if (group !== undefined) {
throw new Error('Tried to recreate group');
}
})
);
} else {
resolve(
generateNewGroupId().then(function(newGroupId) {
groupId = newGroupId;
})
);
}
};
}).then(function() {
var me = textsecure.storage.user.getNumber();
var haveMe = false;
var finalNumbers = [];
for (var i in numbers) {
var number = numbers[i];
if (!textsecure.utils.isNumberSane(number))
throw new Error('Invalid number in group');
if (number == me) haveMe = true;
if (finalNumbers.indexOf(number) < 0) finalNumbers.push(number);
}
if (!haveMe) finalNumbers.push(me);
var groupObject = { numbers: finalNumbers, numberRegistrationIds: {} };
for (var i in finalNumbers)
groupObject.numberRegistrationIds[finalNumbers[i]] = {};
return textsecure.storage.protocol
.putGroup(groupId, groupObject)
.then(function() {
return { id: groupId, numbers: finalNumbers };
});
});
},
getNumbers: function(groupId) {
return textsecure.storage.protocol
.getGroup(groupId)
.then(function(group) {
if (group === undefined) return undefined;
return group.numbers;
});
},
removeNumber: function(groupId, number) {
return textsecure.storage.protocol
.getGroup(groupId)
.then(function(group) {
if (group === undefined) return undefined;
var me = textsecure.storage.user.getNumber();
if (number == me)
throw new Error(
'Cannot remove ourselves from a group, leave the group instead'
);
var i = group.numbers.indexOf(number);
if (i > -1) {
group.numbers.splice(i, 1);
delete group.numberRegistrationIds[number];
return textsecure.storage.protocol
.putGroup(groupId, group)
.then(function() {
return group.numbers;
});
}
return group.numbers;
});
},
addNumbers: function(groupId, numbers) {
return textsecure.storage.protocol
.getGroup(groupId)
.then(function(group) {
if (group === undefined) return undefined;
for (var i in numbers) {
var number = numbers[i];
if (!textsecure.utils.isNumberSane(number))
throw new Error('Invalid number in set to add to group');
if (group.numbers.indexOf(number) < 0) {
group.numbers.push(number);
group.numberRegistrationIds[number] = {};
}
}
return textsecure.storage.protocol
.putGroup(groupId, group)
.then(function() {
return group.numbers;
});
});
},
deleteGroup: function(groupId) {
return textsecure.storage.protocol.removeGroup(groupId);
},
getGroup: function(groupId) {
return textsecure.storage.protocol
.getGroup(groupId)
.then(function(group) {
if (group === undefined) return undefined;
return { id: groupId, numbers: group.numbers };
});
},
updateNumbers: function(groupId, numbers) {
return textsecure.storage.protocol
.getGroup(groupId)
.then(function(group) {
if (group === undefined)
throw new Error('Tried to update numbers for unknown group');
if (
numbers.filter(textsecure.utils.isNumberSane).length <
numbers.length
)
throw new Error('Invalid number in new group members');
var added = numbers.filter(function(number) {
return group.numbers.indexOf(number) < 0;
});
return textsecure.storage.groups.addNumbers(groupId, added);
});
},
};
})();

View file

@ -1,24 +1,24 @@
;(function() {
'use strict';
(function() {
'use strict';
/*****************************************
*** Not-yet-processed message storage ***
*****************************************/
window.textsecure = window.textsecure || {};
window.textsecure.storage = window.textsecure.storage || {};
/*****************************************
*** Not-yet-processed message storage ***
*****************************************/
window.textsecure = window.textsecure || {};
window.textsecure.storage = window.textsecure.storage || {};
window.textsecure.storage.unprocessed = {
getAll: function() {
return textsecure.storage.protocol.getAllUnprocessed();
},
add: function(data) {
return textsecure.storage.protocol.addUnprocessed(data);
},
update: function(id, updates) {
return textsecure.storage.protocol.updateUnprocessed(id, updates);
},
remove: function(id) {
return textsecure.storage.protocol.removeUnprocessed(id);
},
};
window.textsecure.storage.unprocessed = {
getAll: function() {
return textsecure.storage.protocol.getAllUnprocessed();
},
add: function(data) {
return textsecure.storage.protocol.addUnprocessed(data);
},
update: function(id, updates) {
return textsecure.storage.protocol.updateUnprocessed(id, updates);
},
remove: function(id) {
return textsecure.storage.protocol.removeUnprocessed(id);
},
};
})();

View file

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

View file

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

View file

@ -1,74 +1,82 @@
;(function () {
'use strict';
window.textsecure = window.textsecure || {};
(function() {
'use strict';
window.textsecure = window.textsecure || {};
function SyncRequest(sender, receiver) {
if (!(sender instanceof textsecure.MessageSender) || !(receiver instanceof textsecure.MessageReceiver)) {
throw new Error('Tried to construct a SyncRequest without MessageSender and MessageReceiver');
}
this.receiver = receiver;
this.oncontact = this.onContactSyncComplete.bind(this);
receiver.addEventListener('contactsync', this.oncontact);
this.ongroup = this.onGroupSyncComplete.bind(this);
receiver.addEventListener('groupsync', this.ongroup);
console.log('SyncRequest created. Sending contact sync message...');
sender.sendRequestContactSyncMessage().then(function() {
console.log('SyncRequest now sending group sync messsage...');
return sender.sendRequestGroupSyncMessage();
}).catch(function(error) {
console.log(
'SyncRequest error:',
error && error.stack ? error.stack : error
);
});
this.timeout = setTimeout(this.onTimeout.bind(this), 60000);
function SyncRequest(sender, receiver) {
if (
!(sender instanceof textsecure.MessageSender) ||
!(receiver instanceof textsecure.MessageReceiver)
) {
throw new Error(
'Tried to construct a SyncRequest without MessageSender and MessageReceiver'
);
}
this.receiver = receiver;
SyncRequest.prototype = new textsecure.EventTarget();
SyncRequest.prototype.extend({
constructor: SyncRequest,
onContactSyncComplete: function() {
this.contactSync = true;
this.update();
},
onGroupSyncComplete: function() {
this.groupSync = true;
this.update();
},
update: function() {
if (this.contactSync && this.groupSync) {
this.dispatchEvent(new Event('success'));
this.cleanup();
}
},
onTimeout: function() {
if (this.contactSync || this.groupSync) {
this.dispatchEvent(new Event('success'));
} else {
this.dispatchEvent(new Event('timeout'));
}
this.cleanup();
},
cleanup: function() {
clearTimeout(this.timeout);
this.receiver.removeEventListener('contactsync', this.oncontact);
this.receiver.removeEventListener('groupSync', this.ongroup);
delete this.listeners;
}
});
this.oncontact = this.onContactSyncComplete.bind(this);
receiver.addEventListener('contactsync', this.oncontact);
textsecure.SyncRequest = function(sender, receiver) {
var syncRequest = new SyncRequest(sender, receiver);
this.addEventListener = syncRequest.addEventListener.bind(syncRequest);
this.removeEventListener = syncRequest.removeEventListener.bind(syncRequest);
};
this.ongroup = this.onGroupSyncComplete.bind(this);
receiver.addEventListener('groupsync', this.ongroup);
textsecure.SyncRequest.prototype = {
constructor: textsecure.SyncRequest
};
console.log('SyncRequest created. Sending contact sync message...');
sender
.sendRequestContactSyncMessage()
.then(function() {
console.log('SyncRequest now sending group sync messsage...');
return sender.sendRequestGroupSyncMessage();
})
.catch(function(error) {
console.log(
'SyncRequest error:',
error && error.stack ? error.stack : error
);
});
this.timeout = setTimeout(this.onTimeout.bind(this), 60000);
}
SyncRequest.prototype = new textsecure.EventTarget();
SyncRequest.prototype.extend({
constructor: SyncRequest,
onContactSyncComplete: function() {
this.contactSync = true;
this.update();
},
onGroupSyncComplete: function() {
this.groupSync = true;
this.update();
},
update: function() {
if (this.contactSync && this.groupSync) {
this.dispatchEvent(new Event('success'));
this.cleanup();
}
},
onTimeout: function() {
if (this.contactSync || this.groupSync) {
this.dispatchEvent(new Event('success'));
} else {
this.dispatchEvent(new Event('timeout'));
}
this.cleanup();
},
cleanup: function() {
clearTimeout(this.timeout);
this.receiver.removeEventListener('contactsync', this.oncontact);
this.receiver.removeEventListener('groupSync', this.ongroup);
delete this.listeners;
},
});
}());
textsecure.SyncRequest = function(sender, receiver) {
var syncRequest = new SyncRequest(sender, receiver);
this.addEventListener = syncRequest.addEventListener.bind(syncRequest);
this.removeEventListener = syncRequest.removeEventListener.bind(
syncRequest
);
};
textsecure.SyncRequest.prototype = {
constructor: textsecure.SyncRequest,
};
})();

View file

@ -1,68 +1,70 @@
(function () {
window.textsecure = window.textsecure || {};
(function() {
window.textsecure = window.textsecure || {};
window.textsecure.createTaskWithTimeout = function(task, id, options) {
options = options || {};
options.timeout = options.timeout || (1000 * 60 * 2); // two minutes
window.textsecure.createTaskWithTimeout = function(task, id, options) {
options = options || {};
options.timeout = options.timeout || 1000 * 60 * 2; // two minutes
var errorForStack = new Error('for stack');
return function() {
return new Promise(function(resolve, reject) {
var complete = false;
var timer = setTimeout(function() {
if (!complete) {
var message =
(id || '')
+ ' task did not complete in time. Calling stack: '
+ errorForStack.stack;
var errorForStack = new Error('for stack');
return function() {
return new Promise(function(resolve, reject) {
var complete = false;
var timer = setTimeout(
function() {
if (!complete) {
var message =
(id || '') +
' task did not complete in time. Calling stack: ' +
errorForStack.stack;
console.log(message);
return reject(new Error(message));
}
}.bind(this), options.timeout);
var clearTimer = function() {
try {
var localTimer = timer;
if (localTimer) {
timer = null;
clearTimeout(localTimer);
}
}
catch (error) {
console.log(
id || '',
'task ran into problem canceling timer. Calling stack:',
errorForStack.stack
);
}
};
var success = function(result) {
clearTimer();
complete = true;
return resolve(result);
};
var failure = function(error) {
clearTimer();
complete = true;
return reject(error);
};
var promise;
try {
promise = task();
} catch(error) {
clearTimer();
throw error;
}
if (!promise || !promise.then) {
clearTimer();
complete = true;
return resolve(promise);
}
return promise.then(success, failure);
});
console.log(message);
return reject(new Error(message));
}
}.bind(this),
options.timeout
);
var clearTimer = function() {
try {
var localTimer = timer;
if (localTimer) {
timer = null;
clearTimeout(localTimer);
}
} catch (error) {
console.log(
id || '',
'task ran into problem canceling timer. Calling stack:',
errorForStack.stack
);
}
};
var success = function(result) {
clearTimer();
complete = true;
return resolve(result);
};
var failure = function(error) {
clearTimer();
complete = true;
return reject(error);
};
var promise;
try {
promise = task();
} catch (error) {
clearTimer();
throw error;
}
if (!promise || !promise.then) {
clearTimer();
complete = true;
return resolve(promise);
}
return promise.then(success, failure);
});
};
};
})();

View file

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

View file

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

View file

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

View file

@ -6,24 +6,38 @@ describe('encrypting and decrypting profile data', function() {
var buffer = dcodeIO.ByteBuffer.wrap(name).toArrayBuffer();
var key = libsignal.crypto.getRandomBytes(32);
return textsecure.crypto.encryptProfileName(buffer, key).then(function(encrypted) {
assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12);
return textsecure.crypto.decryptProfileName(encrypted, key).then(function(decrypted) {
assert.strictEqual(dcodeIO.ByteBuffer.wrap(decrypted).toString('utf8'), 'Alice');
return textsecure.crypto
.encryptProfileName(buffer, key)
.then(function(encrypted) {
assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12);
return textsecure.crypto
.decryptProfileName(encrypted, key)
.then(function(decrypted) {
assert.strictEqual(
dcodeIO.ByteBuffer.wrap(decrypted).toString('utf8'),
'Alice'
);
});
});
});
});
it('works for empty string', function() {
var name = dcodeIO.ByteBuffer.wrap('').toArrayBuffer();
var key = libsignal.crypto.getRandomBytes(32);
return textsecure.crypto.encryptProfileName(name.buffer, key).then(function(encrypted) {
assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12);
return textsecure.crypto.decryptProfileName(encrypted, key).then(function(decrypted) {
assert.strictEqual(decrypted.byteLength, 0);
assert.strictEqual(dcodeIO.ByteBuffer.wrap(decrypted).toString('utf8'), '');
return textsecure.crypto
.encryptProfileName(name.buffer, key)
.then(function(encrypted) {
assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12);
return textsecure.crypto
.decryptProfileName(encrypted, key)
.then(function(decrypted) {
assert.strictEqual(decrypted.byteLength, 0);
assert.strictEqual(
dcodeIO.ByteBuffer.wrap(decrypted).toString('utf8'),
''
);
});
});
});
});
});
describe('encrypting and decrypting profile avatars', function() {
@ -31,24 +45,32 @@ describe('encrypting and decrypting profile data', function() {
var buffer = dcodeIO.ByteBuffer.wrap('This is an avatar').toArrayBuffer();
var key = libsignal.crypto.getRandomBytes(32);
return textsecure.crypto.encryptProfile(buffer, key).then(function(encrypted) {
assert(encrypted.byteLength === buffer.byteLength + 16 + 12);
return textsecure.crypto.decryptProfile(encrypted, key).then(function(decrypted) {
assertEqualArrayBuffers(buffer, decrypted)
return textsecure.crypto
.encryptProfile(buffer, key)
.then(function(encrypted) {
assert(encrypted.byteLength === buffer.byteLength + 16 + 12);
return textsecure.crypto
.decryptProfile(encrypted, key)
.then(function(decrypted) {
assertEqualArrayBuffers(buffer, decrypted);
});
});
});
});
it('throws when decrypting with the wrong key', function() {
var buffer = dcodeIO.ByteBuffer.wrap('This is an avatar').toArrayBuffer();
var key = libsignal.crypto.getRandomBytes(32);
var bad_key = libsignal.crypto.getRandomBytes(32);
return textsecure.crypto.encryptProfile(buffer, key).then(function(encrypted) {
assert(encrypted.byteLength === buffer.byteLength + 16 + 12);
return textsecure.crypto.decryptProfile(encrypted, bad_key).catch(function(error) {
assert.strictEqual(error.name, 'ProfileDecryptError');
return textsecure.crypto
.encryptProfile(buffer, key)
.then(function(encrypted) {
assert(encrypted.byteLength === buffer.byteLength + 16 + 12);
return textsecure.crypto
.decryptProfile(encrypted, bad_key)
.catch(function(error) {
assert.strictEqual(error.name, 'ProfileDecryptError');
});
});
});
});
});
});

View file

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

View file

@ -1,163 +1,190 @@
'use strict';
describe("Key generation", function() {
var count = 10;
this.timeout(count*2000);
describe('Key generation', function() {
var count = 10;
this.timeout(count * 2000);
function validateStoredKeyPair(keyPair) {
/* Ensure the keypair matches the format used internally by libsignal-protocol */
assert.isObject(keyPair, 'Stored keyPair is not an object');
assert.instanceOf(keyPair.pubKey, ArrayBuffer);
assert.instanceOf(keyPair.privKey, ArrayBuffer);
assert.strictEqual(keyPair.pubKey.byteLength, 33);
assert.strictEqual(new Uint8Array(keyPair.pubKey)[0], 5);
assert.strictEqual(keyPair.privKey.byteLength, 32);
}
function itStoresPreKey(keyId) {
it('prekey ' + keyId + ' is valid', function(done) {
return textsecure.storage.protocol.loadPreKey(keyId).then(function(keyPair) {
validateStoredKeyPair(keyPair);
}).then(done,done);
});
}
function itStoresSignedPreKey(keyId) {
it('signed prekey ' + keyId + ' is valid', function(done) {
return textsecure.storage.protocol.loadSignedPreKey(keyId).then(function(keyPair) {
validateStoredKeyPair(keyPair);
}).then(done,done);
});
}
function validateResultKey(resultKey) {
return textsecure.storage.protocol.loadPreKey(resultKey.keyId).then(function(keyPair) {
assertEqualArrayBuffers(resultKey.publicKey, keyPair.pubKey);
});
}
function validateResultSignedKey(resultSignedKey) {
return textsecure.storage.protocol.loadSignedPreKey(resultSignedKey.keyId).then(function(keyPair) {
assertEqualArrayBuffers(resultSignedKey.publicKey, keyPair.pubKey);
});
}
before(function(done) {
localStorage.clear();
libsignal.KeyHelper.generateIdentityKeyPair().then(function(keyPair) {
return textsecure.storage.protocol.put('identityKey', keyPair);
}).then(done, done);
function validateStoredKeyPair(keyPair) {
/* Ensure the keypair matches the format used internally by libsignal-protocol */
assert.isObject(keyPair, 'Stored keyPair is not an object');
assert.instanceOf(keyPair.pubKey, ArrayBuffer);
assert.instanceOf(keyPair.privKey, ArrayBuffer);
assert.strictEqual(keyPair.pubKey.byteLength, 33);
assert.strictEqual(new Uint8Array(keyPair.pubKey)[0], 5);
assert.strictEqual(keyPair.privKey.byteLength, 32);
}
function itStoresPreKey(keyId) {
it('prekey ' + keyId + ' is valid', function(done) {
return textsecure.storage.protocol
.loadPreKey(keyId)
.then(function(keyPair) {
validateStoredKeyPair(keyPair);
})
.then(done, done);
});
}
function itStoresSignedPreKey(keyId) {
it('signed prekey ' + keyId + ' is valid', function(done) {
return textsecure.storage.protocol
.loadSignedPreKey(keyId)
.then(function(keyPair) {
validateStoredKeyPair(keyPair);
})
.then(done, done);
});
}
function validateResultKey(resultKey) {
return textsecure.storage.protocol
.loadPreKey(resultKey.keyId)
.then(function(keyPair) {
assertEqualArrayBuffers(resultKey.publicKey, keyPair.pubKey);
});
}
function validateResultSignedKey(resultSignedKey) {
return textsecure.storage.protocol
.loadSignedPreKey(resultSignedKey.keyId)
.then(function(keyPair) {
assertEqualArrayBuffers(resultSignedKey.publicKey, keyPair.pubKey);
});
}
describe('the first time', function() {
var result;
/* result should have this format
before(function(done) {
localStorage.clear();
libsignal.KeyHelper.generateIdentityKeyPair()
.then(function(keyPair) {
return textsecure.storage.protocol.put('identityKey', keyPair);
})
.then(done, done);
});
describe('the first time', function() {
var result;
/* result should have this format
* {
* preKeys: [ { keyId, publicKey }, ... ],
* signedPreKey: { keyId, publicKey, signature },
* identityKey: <ArrayBuffer>
* }
*/
before(function(done) {
var accountManager = new textsecure.AccountManager('');
accountManager.generateKeys(count).then(function(res) {
result = res;
}).then(done,done);
});
for (var i = 1; i <= count; i++) {
itStoresPreKey(i);
}
itStoresSignedPreKey(1);
before(function(done) {
var accountManager = new textsecure.AccountManager('');
accountManager
.generateKeys(count)
.then(function(res) {
result = res;
})
.then(done, done);
});
for (var i = 1; i <= count; i++) {
itStoresPreKey(i);
}
itStoresSignedPreKey(1);
it('result contains ' + count + ' preKeys', function() {
assert.isArray(result.preKeys);
assert.lengthOf(result.preKeys, count);
for (var i = 0; i < count; i++) {
assert.isObject(result.preKeys[i]);
}
});
it('result contains the correct keyIds', function() {
for (var i = 0; i < count; i++) {
assert.strictEqual(result.preKeys[i].keyId, i+1);
}
});
it('result contains the correct public keys', function(done) {
Promise.all(result.preKeys.map(validateResultKey)).then(function() {
done();
}).catch(done);
});
it('returns a signed prekey', function(done) {
assert.strictEqual(result.signedPreKey.keyId, 1);
assert.instanceOf(result.signedPreKey.signature, ArrayBuffer);
validateResultSignedKey(result.signedPreKey).then(done,done);
});
it('result contains ' + count + ' preKeys', function() {
assert.isArray(result.preKeys);
assert.lengthOf(result.preKeys, count);
for (var i = 0; i < count; i++) {
assert.isObject(result.preKeys[i]);
}
});
describe('the second time', function() {
var result;
before(function(done) {
var accountManager = new textsecure.AccountManager('');
accountManager.generateKeys(count).then(function(res) {
result = res;
}).then(done,done);
});
for (var i = 1; i <= 2*count; i++) {
itStoresPreKey(i);
}
itStoresSignedPreKey(1);
itStoresSignedPreKey(2);
it('result contains ' + count + ' preKeys', function() {
assert.isArray(result.preKeys);
assert.lengthOf(result.preKeys, count);
for (var i = 0; i < count; i++) {
assert.isObject(result.preKeys[i]);
}
});
it('result contains the correct keyIds', function() {
for (var i = 1; i <= count; i++) {
assert.strictEqual(result.preKeys[i-1].keyId, i+count);
}
});
it('result contains the correct public keys', function(done) {
Promise.all(result.preKeys.map(validateResultKey)).then(function() {
done();
}).catch(done);
});
it('returns a signed prekey', function(done) {
assert.strictEqual(result.signedPreKey.keyId, 2);
assert.instanceOf(result.signedPreKey.signature, ArrayBuffer);
validateResultSignedKey(result.signedPreKey).then(done,done);
});
it('result contains the correct keyIds', function() {
for (var i = 0; i < count; i++) {
assert.strictEqual(result.preKeys[i].keyId, i + 1);
}
});
describe('the third time', function() {
var result;
before(function(done) {
var accountManager = new textsecure.AccountManager('');
accountManager.generateKeys(count).then(function(res) {
result = res;
}).then(done,done);
});
for (var i = 1; i <= 3*count; i++) {
itStoresPreKey(i);
}
itStoresSignedPreKey(2);
itStoresSignedPreKey(3);
it('result contains ' + count + ' preKeys', function() {
assert.isArray(result.preKeys);
assert.lengthOf(result.preKeys, count);
for (var i = 0; i < count; i++) {
assert.isObject(result.preKeys[i]);
}
});
it('result contains the correct keyIds', function() {
for (var i = 1; i <= count; i++) {
assert.strictEqual(result.preKeys[i-1].keyId, i+2*count);
}
});
it('result contains the correct public keys', function(done) {
Promise.all(result.preKeys.map(validateResultKey)).then(function() {
done();
}).catch(done);
});
it('result contains a signed prekey', function(done) {
assert.strictEqual(result.signedPreKey.keyId, 3);
assert.instanceOf(result.signedPreKey.signature, ArrayBuffer);
validateResultSignedKey(result.signedPreKey).then(done,done);
});
it('result contains the correct public keys', function(done) {
Promise.all(result.preKeys.map(validateResultKey))
.then(function() {
done();
})
.catch(done);
});
it('returns a signed prekey', function(done) {
assert.strictEqual(result.signedPreKey.keyId, 1);
assert.instanceOf(result.signedPreKey.signature, ArrayBuffer);
validateResultSignedKey(result.signedPreKey).then(done, done);
});
});
describe('the second time', function() {
var result;
before(function(done) {
var accountManager = new textsecure.AccountManager('');
accountManager
.generateKeys(count)
.then(function(res) {
result = res;
})
.then(done, done);
});
for (var i = 1; i <= 2 * count; i++) {
itStoresPreKey(i);
}
itStoresSignedPreKey(1);
itStoresSignedPreKey(2);
it('result contains ' + count + ' preKeys', function() {
assert.isArray(result.preKeys);
assert.lengthOf(result.preKeys, count);
for (var i = 0; i < count; i++) {
assert.isObject(result.preKeys[i]);
}
});
it('result contains the correct keyIds', function() {
for (var i = 1; i <= count; i++) {
assert.strictEqual(result.preKeys[i - 1].keyId, i + count);
}
});
it('result contains the correct public keys', function(done) {
Promise.all(result.preKeys.map(validateResultKey))
.then(function() {
done();
})
.catch(done);
});
it('returns a signed prekey', function(done) {
assert.strictEqual(result.signedPreKey.keyId, 2);
assert.instanceOf(result.signedPreKey.signature, ArrayBuffer);
validateResultSignedKey(result.signedPreKey).then(done, done);
});
});
describe('the third time', function() {
var result;
before(function(done) {
var accountManager = new textsecure.AccountManager('');
accountManager
.generateKeys(count)
.then(function(res) {
result = res;
})
.then(done, done);
});
for (var i = 1; i <= 3 * count; i++) {
itStoresPreKey(i);
}
itStoresSignedPreKey(2);
itStoresSignedPreKey(3);
it('result contains ' + count + ' preKeys', function() {
assert.isArray(result.preKeys);
assert.lengthOf(result.preKeys, count);
for (var i = 0; i < count; i++) {
assert.isObject(result.preKeys[i]);
}
});
it('result contains the correct keyIds', function() {
for (var i = 1; i <= count; i++) {
assert.strictEqual(result.preKeys[i - 1].keyId, i + 2 * count);
}
});
it('result contains the correct public keys', function(done) {
Promise.all(result.preKeys.map(validateResultKey))
.then(function() {
done();
})
.catch(done);
});
it('result contains a signed prekey', function(done) {
assert.strictEqual(result.signedPreKey.keyId, 3);
assert.instanceOf(result.signedPreKey.signature, ArrayBuffer);
validateResultSignedKey(result.signedPreKey).then(done, done);
});
});
});

View file

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

View file

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

View file

@ -1,71 +1,99 @@
describe('MessageReceiver', function() {
textsecure.storage.impl = new SignalProtocolStore();
var WebSocket = window.WebSocket;
var number = '+19999999999';
var deviceId = 1;
var signalingKey = libsignal.crypto.getRandomBytes(32 + 20);
before(function() {
window.WebSocket = MockSocket;
textsecure.storage.user.setNumberAndDeviceId(number, deviceId, 'name');
textsecure.storage.put("password", "password");
textsecure.storage.put("signaling_key", signalingKey);
textsecure.storage.impl = new SignalProtocolStore();
var WebSocket = window.WebSocket;
var number = '+19999999999';
var deviceId = 1;
var signalingKey = libsignal.crypto.getRandomBytes(32 + 20);
before(function() {
window.WebSocket = MockSocket;
textsecure.storage.user.setNumberAndDeviceId(number, deviceId, 'name');
textsecure.storage.put('password', 'password');
textsecure.storage.put('signaling_key', signalingKey);
});
after(function() {
window.WebSocket = WebSocket;
});
describe('connecting', function() {
var blob = null;
var attrs = {
type: textsecure.protobuf.Envelope.Type.CIPHERTEXT,
source: number,
sourceDevice: deviceId,
timestamp: Date.now(),
};
var websocketmessage = new textsecure.protobuf.WebSocketMessage({
type: textsecure.protobuf.WebSocketMessage.Type.REQUEST,
request: { verb: 'PUT', path: '/messages' },
});
after (function() { window.WebSocket = WebSocket; });
describe('connecting', function() {
var blob = null;
var attrs = {
type: textsecure.protobuf.Envelope.Type.CIPHERTEXT,
source: number,
sourceDevice: deviceId,
timestamp: Date.now(),
};
var websocketmessage = new textsecure.protobuf.WebSocketMessage({
type: textsecure.protobuf.WebSocketMessage.Type.REQUEST,
request: { verb: 'PUT', path: '/messages' }
});
before(function(done) {
var signal = new textsecure.protobuf.Envelope(attrs).toArrayBuffer();
var data = new textsecure.protobuf.DataMessage({ body: 'hello' });
before(function(done) {
var signal = new textsecure.protobuf.Envelope(attrs).toArrayBuffer();
var data = new textsecure.protobuf.DataMessage({ body: 'hello' });
var signaling_key = signalingKey;
var aes_key = signaling_key.slice(0, 32);
var mac_key = signaling_key.slice(32, 32 + 20);
var signaling_key = signalingKey;
var aes_key = signaling_key.slice(0, 32);
var mac_key = signaling_key.slice(32, 32 + 20);
window.crypto.subtle.importKey('raw', aes_key, {name: 'AES-CBC'}, false, ['encrypt']).then(function(key) {
var iv = libsignal.crypto.getRandomBytes(16);
window.crypto.subtle.encrypt({name: 'AES-CBC', iv: new Uint8Array(iv)}, key, signal).then(function(ciphertext) {
window.crypto.subtle.importKey('raw', mac_key, {name: 'HMAC', hash: {name: 'SHA-256'}}, false, ['sign']).then(function(key) {
window.crypto.subtle.sign( {name: 'HMAC', hash: 'SHA-256'}, key, signal).then(function(mac) {
var version = new Uint8Array([1]);
var message = dcodeIO.ByteBuffer.concat([version, iv, ciphertext, mac ]);
websocketmessage.request.body = message.toArrayBuffer();
console.log(new Uint8Array(message.toArrayBuffer()));
done();
});
window.crypto.subtle
.importKey('raw', aes_key, { name: 'AES-CBC' }, false, ['encrypt'])
.then(function(key) {
var iv = libsignal.crypto.getRandomBytes(16);
window.crypto.subtle
.encrypt({ name: 'AES-CBC', iv: new Uint8Array(iv) }, key, signal)
.then(function(ciphertext) {
window.crypto.subtle
.importKey(
'raw',
mac_key,
{ name: 'HMAC', hash: { name: 'SHA-256' } },
false,
['sign']
)
.then(function(key) {
window.crypto.subtle
.sign({ name: 'HMAC', hash: 'SHA-256' }, key, signal)
.then(function(mac) {
var version = new Uint8Array([1]);
var message = dcodeIO.ByteBuffer.concat([
version,
iv,
ciphertext,
mac,
]);
websocketmessage.request.body = message.toArrayBuffer();
console.log(new Uint8Array(message.toArrayBuffer()));
done();
});
});
});
});
it('connects', function(done) {
var mockServer = new MockServer('ws://localhost:8080/v1/websocket/?login='+ encodeURIComponent(number) +'.1&password=password');
mockServer.on('connection', function(server) {
server.send(new Blob([ websocketmessage.toArrayBuffer() ]));
});
window.addEventListener('textsecure:message', function(ev) {
var signal = ev.proto;
for (var key in attrs) {
assert.strictEqual(attrs[key], signal[key]);
}
assert.strictEqual(signal.message.body, 'hello');
server.close();
done();
});
var messageReceiver = new textsecure.MessageReceiver('ws://localhost:8080', window);
});
});
it('connects', function(done) {
var mockServer = new MockServer(
'ws://localhost:8080/v1/websocket/?login=' +
encodeURIComponent(number) +
'.1&password=password'
);
mockServer.on('connection', function(server) {
server.send(new Blob([websocketmessage.toArrayBuffer()]));
});
window.addEventListener('textsecure:message', function(ev) {
var signal = ev.proto;
for (var key in attrs) {
assert.strictEqual(attrs[key], signal[key]);
}
assert.strictEqual(signal.message.body, 'hello');
server.close();
done();
});
var messageReceiver = new textsecure.MessageReceiver(
'ws://localhost:8080',
window
);
});
});
});

View file

@ -1,32 +1,38 @@
'use strict';
describe('Protocol', function() {
describe('Unencrypted PushMessageProto "decrypt"', function() {
//exclusive
it('works', function(done) {
localStorage.clear();
describe('Unencrypted PushMessageProto "decrypt"', function() {
//exclusive
it('works', function(done) {
localStorage.clear();
var text_message = new textsecure.protobuf.DataMessage();
text_message.body = 'Hi Mom';
var server_message = {
type: 4, // unencrypted
source: '+19999999999',
timestamp: 42,
message: text_message.encode(),
};
var text_message = new textsecure.protobuf.DataMessage();
text_message.body = "Hi Mom";
var server_message = {
type: 4, // unencrypted
source: "+19999999999",
timestamp: 42,
message: text_message.encode()
};
return textsecure.protocol_wrapper.handleEncryptedMessage(
server_message.source,
server_message.source_device,
server_message.type,
server_message.message
).then(function(message) {
assert.equal(message.body, text_message.body);
assert.equal(message.attachments.length, text_message.attachments.length);
assert.equal(text_message.attachments.length, 0);
}).then(done).catch(done);
});
return textsecure.protocol_wrapper
.handleEncryptedMessage(
server_message.source,
server_message.source_device,
server_message.type,
server_message.message
)
.then(function(message) {
assert.equal(message.body, text_message.body);
assert.equal(
message.attachments.length,
text_message.attachments.length
);
assert.equal(text_message.attachments.length, 0);
})
.then(done)
.catch(done);
});
});
// TODO: Use fake_api's hiding of api.sendMessage to test sendmessage.js' maze
// TODO: Use fake_api's hiding of api.sendMessage to test sendmessage.js' maze
});

View file

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

View file

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

View file

@ -1,74 +1,80 @@
'use strict';
describe('createTaskWithTimeout', function() {
it('resolves when promise resolves', function() {
var task = function() {
return Promise.resolve('hi!');
};
var taskWithTimeout = textsecure.createTaskWithTimeout(task);
it('resolves when promise resolves', function() {
var task = function() {
return Promise.resolve('hi!');
};
var taskWithTimeout = textsecure.createTaskWithTimeout(task);
return taskWithTimeout().then(function(result) {
assert.strictEqual(result, 'hi!')
});
return taskWithTimeout().then(function(result) {
assert.strictEqual(result, 'hi!');
});
it('flows error from promise back', function() {
var error = new Error('original');
var task = function() {
return Promise.reject(error);
};
var taskWithTimeout = textsecure.createTaskWithTimeout(task);
});
it('flows error from promise back', function() {
var error = new Error('original');
var task = function() {
return Promise.reject(error);
};
var taskWithTimeout = textsecure.createTaskWithTimeout(task);
return taskWithTimeout().catch(function(flowedError) {
assert.strictEqual(error, flowedError);
});
return taskWithTimeout().catch(function(flowedError) {
assert.strictEqual(error, flowedError);
});
});
it('rejects if promise takes too long (this one logs error to console)', function() {
var error = new Error('original');
var complete = false;
var task = function() {
return new Promise(function(resolve) {
setTimeout(function() {
complete = true;
resolve();
}, 3000);
});
};
var taskWithTimeout = textsecure.createTaskWithTimeout(task, this.name, {
timeout: 10,
});
it('rejects if promise takes too long (this one logs error to console)', function() {
var error = new Error('original');
var complete = false;
var task = function() {
return new Promise(function(resolve) {
setTimeout(function() {
complete = true;
resolve();
}, 3000);
});
};
var taskWithTimeout = textsecure.createTaskWithTimeout(task, this.name, {
timeout: 10
});
return taskWithTimeout().then(function() {
throw new Error('it was not supposed to resolve!');
}, function() {
assert.strictEqual(complete, false);
});
return taskWithTimeout().then(
function() {
throw new Error('it was not supposed to resolve!');
},
function() {
assert.strictEqual(complete, false);
}
);
});
it('resolves if task returns something falsey', function() {
var task = function() {};
var taskWithTimeout = textsecure.createTaskWithTimeout(task);
return taskWithTimeout();
});
it('resolves if task returns a non-promise', function() {
var task = function() {
return 'hi!';
};
var taskWithTimeout = textsecure.createTaskWithTimeout(task);
return taskWithTimeout().then(function(result) {
assert.strictEqual(result, 'hi!');
});
it('resolves if task returns something falsey', function() {
var task = function() {};
var taskWithTimeout = textsecure.createTaskWithTimeout(task);
return taskWithTimeout();
});
it('resolves if task returns a non-promise', function() {
var task = function() {
return 'hi!';
};
var taskWithTimeout = textsecure.createTaskWithTimeout(task);
return taskWithTimeout().then(function(result) {
assert.strictEqual(result, 'hi!')
});
});
it('rejects if task throws (and does not log about taking too long)', function() {
var error = new Error('Task is throwing!');
var task = function() {
throw error;
};
var taskWithTimeout = textsecure.createTaskWithTimeout(task, this.name, {
timeout: 10
});
return taskWithTimeout().then(function(result) {
throw new Error('Overall task should reject!')
}, function(flowedError) {
assert.strictEqual(flowedError, error);
});
});
it('rejects if task throws (and does not log about taking too long)', function() {
var error = new Error('Task is throwing!');
var task = function() {
throw error;
};
var taskWithTimeout = textsecure.createTaskWithTimeout(task, this.name, {
timeout: 10,
});
return taskWithTimeout().then(
function(result) {
throw new Error('Overall task should reject!');
},
function(flowedError) {
assert.strictEqual(flowedError, error);
}
);
});
});

View file

@ -1,173 +1,210 @@
;(function() {
'use strict';
(function() {
'use strict';
describe('WebSocket-Resource', function() {
describe('requests and responses', function () {
it('receives requests and sends responses', function(done) {
// mock socket
var request_id = '1';
var socket = {
send: function(data) {
var message = textsecure.protobuf.WebSocketMessage.decode(data);
assert.strictEqual(message.type, textsecure.protobuf.WebSocketMessage.Type.RESPONSE);
assert.strictEqual(message.response.message, 'OK');
assert.strictEqual(message.response.status, 200);
assert.strictEqual(message.response.id.toString(), request_id);
done();
},
addEventListener: function() {},
};
describe('WebSocket-Resource', function() {
describe('requests and responses', function() {
it('receives requests and sends responses', function(done) {
// mock socket
var request_id = '1';
var socket = {
send: function(data) {
var message = textsecure.protobuf.WebSocketMessage.decode(data);
assert.strictEqual(
message.type,
textsecure.protobuf.WebSocketMessage.Type.RESPONSE
);
assert.strictEqual(message.response.message, 'OK');
assert.strictEqual(message.response.status, 200);
assert.strictEqual(message.response.id.toString(), request_id);
done();
},
addEventListener: function() {},
};
// actual test
var resource = new WebSocketResource(socket, {
handleRequest: function (request) {
assert.strictEqual(request.verb, 'PUT');
assert.strictEqual(request.path, '/some/path');
assertEqualArrayBuffers(request.body.toArrayBuffer(), new Uint8Array([1,2,3]).buffer);
request.respond(200, 'OK');
}
});
// mock socket request
socket.onmessage({
data: new Blob([
new textsecure.protobuf.WebSocketMessage({
type: textsecure.protobuf.WebSocketMessage.Type.REQUEST,
request: {
id: request_id,
verb: 'PUT',
path: '/some/path',
body: new Uint8Array([1,2,3]).buffer
}
}).encode().toArrayBuffer()
])
});
});
it('sends requests and receives responses', function(done) {
// mock socket and request handler
var request_id;
var socket = {
send: function(data) {
var message = textsecure.protobuf.WebSocketMessage.decode(data);
assert.strictEqual(message.type, textsecure.protobuf.WebSocketMessage.Type.REQUEST);
assert.strictEqual(message.request.verb, 'PUT');
assert.strictEqual(message.request.path, '/some/path');
assertEqualArrayBuffers(message.request.body.toArrayBuffer(), new Uint8Array([1,2,3]).buffer);
request_id = message.request.id;
},
addEventListener: function() {},
};
// actual test
var resource = new WebSocketResource(socket);
resource.sendRequest({
verb: 'PUT',
path: '/some/path',
body: new Uint8Array([1,2,3]).buffer,
error: done,
success: function(message, status, request) {
assert.strictEqual(message, 'OK');
assert.strictEqual(status, 200);
done();
}
});
// mock socket response
socket.onmessage({
data: new Blob([
new textsecure.protobuf.WebSocketMessage({
type: textsecure.protobuf.WebSocketMessage.Type.RESPONSE,
response: { id: request_id, message: 'OK', status: 200 }
}).encode().toArrayBuffer()
])
});
});
// actual test
var resource = new WebSocketResource(socket, {
handleRequest: function(request) {
assert.strictEqual(request.verb, 'PUT');
assert.strictEqual(request.path, '/some/path');
assertEqualArrayBuffers(
request.body.toArrayBuffer(),
new Uint8Array([1, 2, 3]).buffer
);
request.respond(200, 'OK');
},
});
describe('close', function() {
before(function() { window.WebSocket = MockSocket; });
after (function() { window.WebSocket = WebSocket; });
it('closes the connection', function(done) {
var mockServer = new MockServer('ws://localhost:8081');
mockServer.on('connection', function(server) {
server.on('close', done);
});
var resource = new WebSocketResource(new WebSocket('ws://localhost:8081'));
resource.close();
});
// mock socket request
socket.onmessage({
data: new Blob([
new textsecure.protobuf.WebSocketMessage({
type: textsecure.protobuf.WebSocketMessage.Type.REQUEST,
request: {
id: request_id,
verb: 'PUT',
path: '/some/path',
body: new Uint8Array([1, 2, 3]).buffer,
},
})
.encode()
.toArrayBuffer(),
]),
});
});
it('sends requests and receives responses', function(done) {
// mock socket and request handler
var request_id;
var socket = {
send: function(data) {
var message = textsecure.protobuf.WebSocketMessage.decode(data);
assert.strictEqual(
message.type,
textsecure.protobuf.WebSocketMessage.Type.REQUEST
);
assert.strictEqual(message.request.verb, 'PUT');
assert.strictEqual(message.request.path, '/some/path');
assertEqualArrayBuffers(
message.request.body.toArrayBuffer(),
new Uint8Array([1, 2, 3]).buffer
);
request_id = message.request.id;
},
addEventListener: function() {},
};
// actual test
var resource = new WebSocketResource(socket);
resource.sendRequest({
verb: 'PUT',
path: '/some/path',
body: new Uint8Array([1, 2, 3]).buffer,
error: done,
success: function(message, status, request) {
assert.strictEqual(message, 'OK');
assert.strictEqual(status, 200);
done();
},
});
describe.skip('with a keepalive config', function() {
before(function() { window.WebSocket = MockSocket; });
after (function() { window.WebSocket = WebSocket; });
this.timeout(60000);
it('sends keepalives once a minute', function(done) {
var mockServer = new MockServer('ws://localhost:8081');
mockServer.on('connection', function(server) {
server.on('message', function(data) {
var message = textsecure.protobuf.WebSocketMessage.decode(data);
assert.strictEqual(message.type, textsecure.protobuf.WebSocketMessage.Type.REQUEST);
assert.strictEqual(message.request.verb, 'GET');
assert.strictEqual(message.request.path, '/v1/keepalive');
server.close();
done();
});
});
new WebSocketResource(new WebSocket('ws://localhost:8081'), {
keepalive: { path: '/v1/keepalive' }
});
});
it('uses / as a default path', function(done) {
var mockServer = new MockServer('ws://localhost:8081');
mockServer.on('connection', function(server) {
server.on('message', function(data) {
var message = textsecure.protobuf.WebSocketMessage.decode(data);
assert.strictEqual(message.type, textsecure.protobuf.WebSocketMessage.Type.REQUEST);
assert.strictEqual(message.request.verb, 'GET');
assert.strictEqual(message.request.path, '/');
server.close();
done();
});
});
new WebSocketResource(new WebSocket('ws://localhost:8081'), {
keepalive: true
});
});
it('optionally disconnects if no response', function(done) {
this.timeout(65000);
var mockServer = new MockServer('ws://localhost:8081');
var socket = new WebSocket('ws://localhost:8081');
mockServer.on('connection', function(server) {
server.on('close', done);
});
new WebSocketResource(socket, { keepalive: true });
});
it('allows resetting the keepalive timer', function(done) {
this.timeout(65000);
var mockServer = new MockServer('ws://localhost:8081');
var socket = new WebSocket('ws://localhost:8081');
var startTime = Date.now();
mockServer.on('connection', function(server) {
server.on('message', function(data) {
var message = textsecure.protobuf.WebSocketMessage.decode(data);
assert.strictEqual(message.type, textsecure.protobuf.WebSocketMessage.Type.REQUEST);
assert.strictEqual(message.request.verb, 'GET');
assert.strictEqual(message.request.path, '/');
assert(Date.now() > startTime + 60000, 'keepalive time should be longer than a minute');
server.close();
done();
});
});
var resource = new WebSocketResource(socket, { keepalive: true });
setTimeout(function() {
resource.resetKeepAliveTimer()
}, 5000);
});
// mock socket response
socket.onmessage({
data: new Blob([
new textsecure.protobuf.WebSocketMessage({
type: textsecure.protobuf.WebSocketMessage.Type.RESPONSE,
response: { id: request_id, message: 'OK', status: 200 },
})
.encode()
.toArrayBuffer(),
]),
});
});
});
}());
describe('close', function() {
before(function() {
window.WebSocket = MockSocket;
});
after(function() {
window.WebSocket = WebSocket;
});
it('closes the connection', function(done) {
var mockServer = new MockServer('ws://localhost:8081');
mockServer.on('connection', function(server) {
server.on('close', done);
});
var resource = new WebSocketResource(
new WebSocket('ws://localhost:8081')
);
resource.close();
});
});
describe.skip('with a keepalive config', function() {
before(function() {
window.WebSocket = MockSocket;
});
after(function() {
window.WebSocket = WebSocket;
});
this.timeout(60000);
it('sends keepalives once a minute', function(done) {
var mockServer = new MockServer('ws://localhost:8081');
mockServer.on('connection', function(server) {
server.on('message', function(data) {
var message = textsecure.protobuf.WebSocketMessage.decode(data);
assert.strictEqual(
message.type,
textsecure.protobuf.WebSocketMessage.Type.REQUEST
);
assert.strictEqual(message.request.verb, 'GET');
assert.strictEqual(message.request.path, '/v1/keepalive');
server.close();
done();
});
});
new WebSocketResource(new WebSocket('ws://localhost:8081'), {
keepalive: { path: '/v1/keepalive' },
});
});
it('uses / as a default path', function(done) {
var mockServer = new MockServer('ws://localhost:8081');
mockServer.on('connection', function(server) {
server.on('message', function(data) {
var message = textsecure.protobuf.WebSocketMessage.decode(data);
assert.strictEqual(
message.type,
textsecure.protobuf.WebSocketMessage.Type.REQUEST
);
assert.strictEqual(message.request.verb, 'GET');
assert.strictEqual(message.request.path, '/');
server.close();
done();
});
});
new WebSocketResource(new WebSocket('ws://localhost:8081'), {
keepalive: true,
});
});
it('optionally disconnects if no response', function(done) {
this.timeout(65000);
var mockServer = new MockServer('ws://localhost:8081');
var socket = new WebSocket('ws://localhost:8081');
mockServer.on('connection', function(server) {
server.on('close', done);
});
new WebSocketResource(socket, { keepalive: true });
});
it('allows resetting the keepalive timer', function(done) {
this.timeout(65000);
var mockServer = new MockServer('ws://localhost:8081');
var socket = new WebSocket('ws://localhost:8081');
var startTime = Date.now();
mockServer.on('connection', function(server) {
server.on('message', function(data) {
var message = textsecure.protobuf.WebSocketMessage.decode(data);
assert.strictEqual(
message.type,
textsecure.protobuf.WebSocketMessage.Type.REQUEST
);
assert.strictEqual(message.request.verb, 'GET');
assert.strictEqual(message.request.path, '/');
assert(
Date.now() > startTime + 60000,
'keepalive time should be longer than a minute'
);
server.close();
done();
});
});
var resource = new WebSocketResource(socket, { keepalive: true });
setTimeout(function() {
resource.resetKeepAliveTimer();
}, 5000);
});
});
});
})();

View file

@ -1,62 +1,64 @@
describe('TextSecureWebSocket', function() {
var RealWebSocket = window.WebSocket;
before(function() { window.WebSocket = MockSocket; });
after (function() { window.WebSocket = RealWebSocket; });
it('connects and disconnects', function(done) {
var mockServer = new MockServer('ws://localhost:8080');
mockServer.on('connection', function(server) {
socket.close();
server.close();
done();
});
var socket = new TextSecureWebSocket('ws://localhost:8080');
var RealWebSocket = window.WebSocket;
before(function() {
window.WebSocket = MockSocket;
});
after(function() {
window.WebSocket = RealWebSocket;
});
it('connects and disconnects', function(done) {
var mockServer = new MockServer('ws://localhost:8080');
mockServer.on('connection', function(server) {
socket.close();
server.close();
done();
});
var socket = new TextSecureWebSocket('ws://localhost:8080');
});
it('sends and receives', function(done) {
var mockServer = new MockServer('ws://localhost:8080');
mockServer.on('connection', function(server) {
server.on('message', function(data) {
server.send('ack');
server.close();
});
});
var socket = new TextSecureWebSocket('ws://localhost:8080');
socket.onmessage = function(response) {
assert.strictEqual(response.data, 'ack');
socket.close();
done();
};
socket.send('syn');
it('sends and receives', function(done) {
var mockServer = new MockServer('ws://localhost:8080');
mockServer.on('connection', function(server) {
server.on('message', function(data) {
server.send('ack');
server.close();
});
});
var socket = new TextSecureWebSocket('ws://localhost:8080');
socket.onmessage = function(response) {
assert.strictEqual(response.data, 'ack');
socket.close();
done();
};
socket.send('syn');
});
it('exposes the socket status', function(done) {
var mockServer = new MockServer('ws://localhost:8082');
mockServer.on('connection', function(server) {
assert.strictEqual(socket.getStatus(), WebSocket.OPEN);
server.close();
socket.close();
});
var socket = new TextSecureWebSocket('ws://localhost:8082');
socket.onclose = function() {
assert.strictEqual(socket.getStatus(), WebSocket.CLOSING);
done();
};
});
it('reconnects', function(done) {
this.timeout(60000);
var mockServer = new MockServer('ws://localhost:8082');
var socket = new TextSecureWebSocket('ws://localhost:8082');
socket.onclose = function() {
var mockServer = new MockServer('ws://localhost:8082');
mockServer.on('connection', function(server) {
socket.close();
server.close();
done();
});
};
mockServer.close();
it('exposes the socket status', function(done) {
var mockServer = new MockServer('ws://localhost:8082');
mockServer.on('connection', function(server) {
assert.strictEqual(socket.getStatus(), WebSocket.OPEN);
server.close();
socket.close();
});
var socket = new TextSecureWebSocket('ws://localhost:8082');
socket.onclose = function() {
assert.strictEqual(socket.getStatus(), WebSocket.CLOSING);
done();
};
});
it('reconnects', function(done) {
this.timeout(60000);
var mockServer = new MockServer('ws://localhost:8082');
var socket = new TextSecureWebSocket('ws://localhost:8082');
socket.onclose = function() {
var mockServer = new MockServer('ws://localhost:8082');
mockServer.on('connection', function(server) {
socket.close();
server.close();
done();
});
};
mockServer.close();
});
});

View file

@ -1,7 +1,7 @@
;(function(){
'use strict';
(function() {
'use strict';
/*
/*
* WebSocket-Resources
*
* Create a request-response interface over websockets using the
@ -23,212 +23,233 @@
*
*/
var Request = function(options) {
this.verb = options.verb || options.type;
this.path = options.path || options.url;
this.body = options.body || options.data;
this.success = options.success;
this.error = options.error;
this.id = options.id;
var Request = function(options) {
this.verb = options.verb || options.type;
this.path = options.path || options.url;
this.body = options.body || options.data;
this.success = options.success;
this.error = options.error;
this.id = options.id;
if (this.id === undefined) {
var bits = new Uint32Array(2);
window.crypto.getRandomValues(bits);
this.id = dcodeIO.Long.fromBits(bits[0], bits[1], true);
}
if (this.body === undefined) {
this.body = null;
}
};
var IncomingWebSocketRequest = function(options) {
var request = new Request(options);
var socket = options.socket;
this.verb = request.verb;
this.path = request.path;
this.body = request.body;
this.respond = function(status, message) {
socket.send(
new textsecure.protobuf.WebSocketMessage({
type: textsecure.protobuf.WebSocketMessage.Type.RESPONSE,
response: { id: request.id, message: message, status: status }
}).encode().toArrayBuffer()
);
};
};
var outgoing = {};
var OutgoingWebSocketRequest = function(options, socket) {
var request = new Request(options);
outgoing[request.id] = request;
socket.send(
new textsecure.protobuf.WebSocketMessage({
type: textsecure.protobuf.WebSocketMessage.Type.REQUEST,
request: {
verb : request.verb,
path : request.path,
body : request.body,
id : request.id
}
}).encode().toArrayBuffer()
);
};
window.WebSocketResource = function(socket, opts) {
opts = opts || {};
var handleRequest = opts.handleRequest;
if (typeof handleRequest !== 'function') {
handleRequest = function(request) {
request.respond(404, 'Not found');
};
}
this.sendRequest = function(options) {
return new OutgoingWebSocketRequest(options, socket);
};
socket.onmessage = function(socketMessage) {
var blob = socketMessage.data;
var handleArrayBuffer = function(buffer) {
var message = textsecure.protobuf.WebSocketMessage.decode(buffer);
if (message.type === textsecure.protobuf.WebSocketMessage.Type.REQUEST ) {
handleRequest(
new IncomingWebSocketRequest({
verb : message.request.verb,
path : message.request.path,
body : message.request.body,
id : message.request.id,
socket : socket
})
);
}
else if (message.type === textsecure.protobuf.WebSocketMessage.Type.RESPONSE ) {
var response = message.response;
var request = outgoing[response.id];
if (request) {
request.response = response;
var callback = request.error;
if (response.status >= 200 && response.status < 300) {
callback = request.success;
}
if (typeof callback === 'function') {
callback(response.message, response.status, request);
}
} else {
throw 'Received response for unknown request ' + message.response.id;
}
}
};
if (blob instanceof ArrayBuffer) {
handleArrayBuffer(blob);
} else {
var reader = new FileReader();
reader.onload = function() {
handleArrayBuffer(reader.result);
};
reader.readAsArrayBuffer(blob);
}
};
if (opts.keepalive) {
this.keepalive = new KeepAlive(this, {
path : opts.keepalive.path,
disconnect : opts.keepalive.disconnect
});
var resetKeepAliveTimer = this.keepalive.reset.bind(this.keepalive);
socket.addEventListener('open', resetKeepAliveTimer);
socket.addEventListener('message', resetKeepAliveTimer);
socket.addEventListener('close', this.keepalive.stop.bind(this.keepalive));
}
socket.addEventListener('close', function() {
this.closed = true;
}.bind(this))
this.close = function(code, reason) {
if (this.closed) {
return;
}
console.log('WebSocketResource.close()');
if (!code) {
code = 3000;
}
if (this.keepalive) {
this.keepalive.stop();
}
socket.close(code, reason);
socket.onmessage = null;
// On linux the socket can wait a long time to emit its close event if we've
// lost the internet connection. On the order of minutes. This speeds that
// process up.
setTimeout(function() {
if (this.closed) {
return;
}
this.closed = true;
console.log('Dispatching our own socket close event');
var ev = new Event('close');
ev.code = code;
ev.reason = reason;
this.dispatchEvent(ev);
}.bind(this), 1000);
};
};
window.WebSocketResource.prototype = new textsecure.EventTarget();
function KeepAlive(websocketResource, opts) {
if (websocketResource instanceof WebSocketResource) {
opts = opts || {};
this.path = opts.path;
if (this.path === undefined) {
this.path = '/';
}
this.disconnect = opts.disconnect;
if (this.disconnect === undefined) {
this.disconnect = true;
}
this.wsr = websocketResource;
} else {
throw new TypeError('KeepAlive expected a WebSocketResource');
}
if (this.id === undefined) {
var bits = new Uint32Array(2);
window.crypto.getRandomValues(bits);
this.id = dcodeIO.Long.fromBits(bits[0], bits[1], true);
}
KeepAlive.prototype = {
constructor: KeepAlive,
stop: function() {
clearTimeout(this.keepAliveTimer);
clearTimeout(this.disconnectTimer);
},
reset: function() {
clearTimeout(this.keepAliveTimer);
clearTimeout(this.disconnectTimer);
this.keepAliveTimer = setTimeout(function() {
if (this.disconnect) {
// automatically disconnect if server doesn't ack
this.disconnectTimer = setTimeout(function() {
clearTimeout(this.keepAliveTimer);
this.wsr.close(3001, 'No response to keepalive request');
}.bind(this), 1000);
} else {
this.reset();
}
console.log('Sending a keepalive message');
this.wsr.sendRequest({
verb: 'GET',
path: this.path,
success: this.reset.bind(this)
});
}.bind(this), 55000);
if (this.body === undefined) {
this.body = null;
}
};
var IncomingWebSocketRequest = function(options) {
var request = new Request(options);
var socket = options.socket;
this.verb = request.verb;
this.path = request.path;
this.body = request.body;
this.respond = function(status, message) {
socket.send(
new textsecure.protobuf.WebSocketMessage({
type: textsecure.protobuf.WebSocketMessage.Type.RESPONSE,
response: { id: request.id, message: message, status: status },
})
.encode()
.toArrayBuffer()
);
};
};
var outgoing = {};
var OutgoingWebSocketRequest = function(options, socket) {
var request = new Request(options);
outgoing[request.id] = request;
socket.send(
new textsecure.protobuf.WebSocketMessage({
type: textsecure.protobuf.WebSocketMessage.Type.REQUEST,
request: {
verb: request.verb,
path: request.path,
body: request.body,
id: request.id,
},
})
.encode()
.toArrayBuffer()
);
};
window.WebSocketResource = function(socket, opts) {
opts = opts || {};
var handleRequest = opts.handleRequest;
if (typeof handleRequest !== 'function') {
handleRequest = function(request) {
request.respond(404, 'Not found');
};
}
this.sendRequest = function(options) {
return new OutgoingWebSocketRequest(options, socket);
};
}());
socket.onmessage = function(socketMessage) {
var blob = socketMessage.data;
var handleArrayBuffer = function(buffer) {
var message = textsecure.protobuf.WebSocketMessage.decode(buffer);
if (
message.type === textsecure.protobuf.WebSocketMessage.Type.REQUEST
) {
handleRequest(
new IncomingWebSocketRequest({
verb: message.request.verb,
path: message.request.path,
body: message.request.body,
id: message.request.id,
socket: socket,
})
);
} else if (
message.type === textsecure.protobuf.WebSocketMessage.Type.RESPONSE
) {
var response = message.response;
var request = outgoing[response.id];
if (request) {
request.response = response;
var callback = request.error;
if (response.status >= 200 && response.status < 300) {
callback = request.success;
}
if (typeof callback === 'function') {
callback(response.message, response.status, request);
}
} else {
throw 'Received response for unknown request ' +
message.response.id;
}
}
};
if (blob instanceof ArrayBuffer) {
handleArrayBuffer(blob);
} else {
var reader = new FileReader();
reader.onload = function() {
handleArrayBuffer(reader.result);
};
reader.readAsArrayBuffer(blob);
}
};
if (opts.keepalive) {
this.keepalive = new KeepAlive(this, {
path: opts.keepalive.path,
disconnect: opts.keepalive.disconnect,
});
var resetKeepAliveTimer = this.keepalive.reset.bind(this.keepalive);
socket.addEventListener('open', resetKeepAliveTimer);
socket.addEventListener('message', resetKeepAliveTimer);
socket.addEventListener(
'close',
this.keepalive.stop.bind(this.keepalive)
);
}
socket.addEventListener(
'close',
function() {
this.closed = true;
}.bind(this)
);
this.close = function(code, reason) {
if (this.closed) {
return;
}
console.log('WebSocketResource.close()');
if (!code) {
code = 3000;
}
if (this.keepalive) {
this.keepalive.stop();
}
socket.close(code, reason);
socket.onmessage = null;
// On linux the socket can wait a long time to emit its close event if we've
// lost the internet connection. On the order of minutes. This speeds that
// process up.
setTimeout(
function() {
if (this.closed) {
return;
}
this.closed = true;
console.log('Dispatching our own socket close event');
var ev = new Event('close');
ev.code = code;
ev.reason = reason;
this.dispatchEvent(ev);
}.bind(this),
1000
);
};
};
window.WebSocketResource.prototype = new textsecure.EventTarget();
function KeepAlive(websocketResource, opts) {
if (websocketResource instanceof WebSocketResource) {
opts = opts || {};
this.path = opts.path;
if (this.path === undefined) {
this.path = '/';
}
this.disconnect = opts.disconnect;
if (this.disconnect === undefined) {
this.disconnect = true;
}
this.wsr = websocketResource;
} else {
throw new TypeError('KeepAlive expected a WebSocketResource');
}
}
KeepAlive.prototype = {
constructor: KeepAlive,
stop: function() {
clearTimeout(this.keepAliveTimer);
clearTimeout(this.disconnectTimer);
},
reset: function() {
clearTimeout(this.keepAliveTimer);
clearTimeout(this.disconnectTimer);
this.keepAliveTimer = setTimeout(
function() {
if (this.disconnect) {
// automatically disconnect if server doesn't ack
this.disconnectTimer = setTimeout(
function() {
clearTimeout(this.keepAliveTimer);
this.wsr.close(3001, 'No response to keepalive request');
}.bind(this),
1000
);
} else {
this.reset();
}
console.log('Sending a keepalive message');
this.wsr.sendRequest({
verb: 'GET',
path: this.path,
success: this.reset.bind(this),
});
}.bind(this),
55000
);
},
};
})();