Eslintify all of libtextsecure

This commit is contained in:
Scott Nonnenberg 2018-07-21 14:51:20 -07:00
parent 4b3f9e969a
commit 0774ba2903
36 changed files with 1960 additions and 2128 deletions

View file

@ -4,7 +4,7 @@ coverage/**
dist/** dist/**
# these aren't ready yet, pulling files in one-by-one # these aren't ready yet, pulling files in one-by-one
libtextsecure/** libtextsecure/test/*.js
test/*.js test/*.js
test/models/*.js test/models/*.js
test/views/*.js test/views/*.js

View file

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

View file

@ -1,8 +1,21 @@
/* global
window,
textsecure,
libsignal,
WebSocketResource,
btoa,
getString,
libphonenumber,
Event
*/
/* eslint-disable more/no-then */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict';
window.textsecure = window.textsecure || {}; window.textsecure = window.textsecure || {};
var ARCHIVE_AGE = 7 * 24 * 60 * 60 * 1000; const ARCHIVE_AGE = 7 * 24 * 60 * 60 * 1000;
function AccountManager(username, password) { function AccountManager(username, password) {
this.server = window.WebAPI.connect({ username, password }); this.server = window.WebAPI.connect({ username, password });
@ -14,7 +27,7 @@
return numberId; return numberId;
} }
var parts = numberId.split('.'); const parts = numberId.split('.');
if (!parts.length) { if (!parts.length) {
return numberId; return numberId;
} }
@ -25,24 +38,22 @@
AccountManager.prototype = new textsecure.EventTarget(); AccountManager.prototype = new textsecure.EventTarget();
AccountManager.prototype.extend({ AccountManager.prototype.extend({
constructor: AccountManager, constructor: AccountManager,
requestVoiceVerification: function(number) { requestVoiceVerification(number) {
return this.server.requestVerificationVoice(number); return this.server.requestVerificationVoice(number);
}, },
requestSMSVerification: function(number) { requestSMSVerification(number) {
return this.server.requestVerificationSMS(number); return this.server.requestVerificationSMS(number);
}, },
registerSingleDevice: function(number, verificationCode) { registerSingleDevice(number, verificationCode) {
var registerKeys = this.server.registerKeys.bind(this.server); const registerKeys = this.server.registerKeys.bind(this.server);
var createAccount = this.createAccount.bind(this); const createAccount = this.createAccount.bind(this);
var clearSessionsAndPreKeys = this.clearSessionsAndPreKeys.bind(this); const clearSessionsAndPreKeys = this.clearSessionsAndPreKeys.bind(this);
var generateKeys = this.generateKeys.bind(this, 100); const generateKeys = this.generateKeys.bind(this, 100);
var confirmKeys = this.confirmKeys.bind(this); const confirmKeys = this.confirmKeys.bind(this);
var registrationDone = this.registrationDone.bind(this); const registrationDone = this.registrationDone.bind(this);
return this.queueTask(function() { return this.queueTask(() =>
return libsignal.KeyHelper.generateIdentityKeyPair().then(function( libsignal.KeyHelper.generateIdentityKeyPair().then(identityKeyPair => {
identityKeyPair const profileKey = textsecure.crypto.getRandomBytes(32);
) {
var profileKey = textsecure.crypto.getRandomBytes(32);
return createAccount( return createAccount(
number, number,
verificationCode, verificationCode,
@ -51,234 +62,213 @@
) )
.then(clearSessionsAndPreKeys) .then(clearSessionsAndPreKeys)
.then(generateKeys) .then(generateKeys)
.then(function(keys) { .then(keys => registerKeys(keys).then(() => confirmKeys(keys)))
return registerKeys(keys).then(function() {
return confirmKeys(keys);
});
})
.then(registrationDone); .then(registrationDone);
}); })
}); );
}, },
registerSecondDevice: function( registerSecondDevice(setProvisioningUrl, confirmNumber, progressCallback) {
setProvisioningUrl, const createAccount = this.createAccount.bind(this);
confirmNumber, const clearSessionsAndPreKeys = this.clearSessionsAndPreKeys.bind(this);
progressCallback const generateKeys = this.generateKeys.bind(this, 100, progressCallback);
) { const confirmKeys = this.confirmKeys.bind(this);
var createAccount = this.createAccount.bind(this); const registrationDone = this.registrationDone.bind(this);
var clearSessionsAndPreKeys = this.clearSessionsAndPreKeys.bind(this); const registerKeys = this.server.registerKeys.bind(this.server);
var generateKeys = this.generateKeys.bind(this, 100, progressCallback); const getSocket = this.server.getProvisioningSocket.bind(this.server);
var confirmKeys = this.confirmKeys.bind(this); const queueTask = this.queueTask.bind(this);
var registrationDone = this.registrationDone.bind(this); const provisioningCipher = new libsignal.ProvisioningCipher();
var registerKeys = this.server.registerKeys.bind(this.server); let gotProvisionEnvelope = false;
var getSocket = this.server.getProvisioningSocket.bind(this.server); return provisioningCipher.getPublicKey().then(
var queueTask = this.queueTask.bind(this); pubKey =>
var provisioningCipher = new libsignal.ProvisioningCipher(); new Promise((resolve, reject) => {
var gotProvisionEnvelope = false; const socket = getSocket();
return provisioningCipher.getPublicKey().then(function(pubKey) { socket.onclose = event => {
return new Promise(function(resolve, reject) { window.log.info('provisioning socket closed. Code:', event.code);
var socket = getSocket(); if (!gotProvisionEnvelope) {
socket.onclose = function(event) { reject(new Error('websocket closed'));
window.log.info('provisioning socket closed. Code:', event.code); }
if (!gotProvisionEnvelope) { };
reject(new Error('websocket closed')); socket.onopen = () => {
} window.log.info('provisioning socket open');
}; };
socket.onopen = function(e) { const wsr = new WebSocketResource(socket, {
window.log.info('provisioning socket open'); keepalive: { path: '/v1/keepalive/provisioning' },
}; handleRequest(request) {
var wsr = new WebSocketResource(socket, { if (request.path === '/v1/address' && request.verb === 'PUT') {
keepalive: { path: '/v1/keepalive/provisioning' }, const proto = textsecure.protobuf.ProvisioningUuid.decode(
handleRequest: function(request) { request.body
if (request.path === '/v1/address' && request.verb === 'PUT') { );
var proto = textsecure.protobuf.ProvisioningUuid.decode( setProvisioningUrl(
request.body [
); 'tsdevice:/?uuid=',
setProvisioningUrl( proto.uuid,
[ '&pub_key=',
'tsdevice:/?uuid=', encodeURIComponent(btoa(getString(pubKey))),
proto.uuid, ].join('')
'&pub_key=', );
encodeURIComponent(btoa(getString(pubKey))), request.respond(200, 'OK');
].join('') } else if (
); request.path === '/v1/message' &&
request.respond(200, 'OK'); request.verb === 'PUT'
} else if ( ) {
request.path === '/v1/message' && const envelope = textsecure.protobuf.ProvisionEnvelope.decode(
request.verb === 'PUT' request.body,
) { 'binary'
var envelope = textsecure.protobuf.ProvisionEnvelope.decode( );
request.body, request.respond(200, 'OK');
'binary' gotProvisionEnvelope = true;
); wsr.close();
request.respond(200, 'OK'); resolve(
gotProvisionEnvelope = true; provisioningCipher
wsr.close(); .decrypt(envelope)
resolve( .then(provisionMessage =>
provisioningCipher queueTask(() =>
.decrypt(envelope) confirmNumber(provisionMessage.number).then(
.then(function(provisionMessage) { deviceName => {
return queueTask(function() { if (
return confirmNumber(provisionMessage.number).then( typeof deviceName !== 'string' ||
function(deviceName) { deviceName.length === 0
if ( ) {
typeof deviceName !== 'string' || throw new Error('Invalid device name');
deviceName.length === 0 }
) { return createAccount(
throw new Error('Invalid device name'); provisionMessage.number,
provisionMessage.provisioningCode,
provisionMessage.identityKeyPair,
provisionMessage.profileKey,
deviceName,
provisionMessage.userAgent,
provisionMessage.readReceipts
)
.then(clearSessionsAndPreKeys)
.then(generateKeys)
.then(keys =>
registerKeys(keys).then(() =>
confirmKeys(keys)
)
)
.then(registrationDone);
} }
return createAccount( )
provisionMessage.number, )
provisionMessage.provisioningCode, )
provisionMessage.identityKeyPair, );
provisionMessage.profileKey, } else {
deviceName, window.log.error('Unknown websocket message', request.path);
provisionMessage.userAgent, }
provisionMessage.readReceipts },
) });
.then(clearSessionsAndPreKeys) })
.then(generateKeys) );
.then(function(keys) {
return registerKeys(keys).then(function() {
return confirmKeys(keys);
});
})
.then(registrationDone);
}
);
});
})
);
} else {
window.log.error('Unknown websocket message', request.path);
}
},
});
});
});
}, },
refreshPreKeys: function() { refreshPreKeys() {
var generateKeys = this.generateKeys.bind(this, 100); const generateKeys = this.generateKeys.bind(this, 100);
var registerKeys = this.server.registerKeys.bind(this.server); const registerKeys = this.server.registerKeys.bind(this.server);
return this.queueTask( return this.queueTask(() =>
function() { this.server.getMyKeys().then(preKeyCount => {
return this.server.getMyKeys().then(function(preKeyCount) { window.log.info(`prekey count ${preKeyCount}`);
window.log.info('prekey count ' + preKeyCount); if (preKeyCount < 10) {
if (preKeyCount < 10) { return generateKeys().then(registerKeys);
return generateKeys().then(registerKeys); }
return null;
})
);
},
rotateSignedPreKey() {
return this.queueTask(() => {
const signedKeyId = textsecure.storage.get('signedKeyId', 1);
if (typeof signedKeyId !== 'number') {
throw new Error('Invalid signedKeyId');
}
const store = textsecure.storage.protocol;
const { server, cleanSignedPreKeys } = this;
// TODO: harden this against missing identity key? Otherwise, we get
// retries every five seconds.
return store
.getIdentityKeyPair()
.then(
identityKey =>
libsignal.KeyHelper.generateSignedPreKey(
identityKey,
signedKeyId
),
() => {
window.log.error(
'Failed to get identity key. Canceling key rotation.'
);
}
)
.then(res => {
if (!res) {
return null;
}
window.log.info('Saving new signed prekey', res.keyId);
return Promise.all([
textsecure.storage.put('signedKeyId', signedKeyId + 1),
store.storeSignedPreKey(res.keyId, res.keyPair),
server.setSignedPreKey({
keyId: res.keyId,
publicKey: res.keyPair.pubKey,
signature: res.signature,
}),
])
.then(() => {
const confirmed = true;
window.log.info('Confirming new signed prekey', res.keyId);
return Promise.all([
textsecure.storage.remove('signedKeyRotationRejected'),
store.storeSignedPreKey(res.keyId, res.keyPair, confirmed),
]);
})
.then(() => cleanSignedPreKeys());
})
.catch(e => {
window.log.error(
'rotateSignedPrekey error:',
e && e.stack ? e.stack : e
);
if (
e instanceof Error &&
e.name === 'HTTPError' &&
e.code >= 400 &&
e.code <= 599
) {
const rejections =
1 + textsecure.storage.get('signedKeyRotationRejected', 0);
textsecure.storage.put('signedKeyRotationRejected', rejections);
window.log.error(
'Signed key rotation rejected count:',
rejections
);
} else {
throw e;
} }
}); });
}.bind(this) });
);
}, },
rotateSignedPreKey: function() { queueTask(task) {
return this.queueTask( const taskWithTimeout = textsecure.createTaskWithTimeout(task);
function() { this.pending = this.pending.then(taskWithTimeout, taskWithTimeout);
var signedKeyId = textsecure.storage.get('signedKeyId', 1);
if (typeof signedKeyId != 'number') {
throw new Error('Invalid signedKeyId');
}
var store = textsecure.storage.protocol; return this.pending;
var server = this.server;
var cleanSignedPreKeys = this.cleanSignedPreKeys;
// TODO: harden this against missing identity key? Otherwise, we get
// retries every five seconds.
return store
.getIdentityKeyPair()
.then(
function(identityKey) {
return libsignal.KeyHelper.generateSignedPreKey(
identityKey,
signedKeyId
);
},
function(error) {
window.log.error(
'Failed to get identity key. Canceling key rotation.'
);
}
)
.then(function(res) {
if (!res) {
return;
}
window.log.info('Saving new signed prekey', res.keyId);
return Promise.all([
textsecure.storage.put('signedKeyId', signedKeyId + 1),
store.storeSignedPreKey(res.keyId, res.keyPair),
server.setSignedPreKey({
keyId: res.keyId,
publicKey: res.keyPair.pubKey,
signature: res.signature,
}),
])
.then(function() {
var confirmed = true;
window.log.info('Confirming new signed prekey', res.keyId);
return Promise.all([
textsecure.storage.remove('signedKeyRotationRejected'),
store.storeSignedPreKey(res.keyId, res.keyPair, confirmed),
]);
})
.then(function() {
return cleanSignedPreKeys();
});
})
.catch(function(e) {
window.log.error(
'rotateSignedPrekey error:',
e && e.stack ? e.stack : e
);
if (
e instanceof Error &&
e.name == 'HTTPError' &&
e.code >= 400 &&
e.code <= 599
) {
var rejections =
1 + textsecure.storage.get('signedKeyRotationRejected', 0);
textsecure.storage.put('signedKeyRotationRejected', rejections);
window.log.error(
'Signed key rotation rejected count:',
rejections
);
} else {
throw e;
}
});
}.bind(this)
);
}, },
queueTask: function(task) { cleanSignedPreKeys() {
var taskWithTimeout = textsecure.createTaskWithTimeout(task); const MINIMUM_KEYS = 3;
return (this.pending = this.pending.then( const store = textsecure.storage.protocol;
taskWithTimeout, return store.loadSignedPreKeys().then(allKeys => {
taskWithTimeout allKeys.sort((a, b) => (a.created_at || 0) - (b.created_at || 0));
));
},
cleanSignedPreKeys: function() {
var MINIMUM_KEYS = 3;
var store = textsecure.storage.protocol;
return store.loadSignedPreKeys().then(function(allKeys) {
allKeys.sort(function(a, b) {
return (a.created_at || 0) - (b.created_at || 0);
});
allKeys.reverse(); // we want the most recent first allKeys.reverse(); // we want the most recent first
var confirmed = allKeys.filter(function(key) { let confirmed = allKeys.filter(key => key.confirmed);
return key.confirmed; const unconfirmed = allKeys.filter(key => !key.confirmed);
});
var unconfirmed = allKeys.filter(function(key) {
return !key.confirmed;
});
var recent = allKeys[0] ? allKeys[0].keyId : 'none'; const recent = allKeys[0] ? allKeys[0].keyId : 'none';
var recentConfirmed = confirmed[0] ? confirmed[0].keyId : 'none'; const recentConfirmed = confirmed[0] ? confirmed[0].keyId : 'none';
window.log.info('Most recent signed key: ' + recent); window.log.info(`Most recent signed key: ${recent}`);
window.log.info('Most recent confirmed signed key: ' + recentConfirmed); window.log.info(`Most recent confirmed signed key: ${recentConfirmed}`);
window.log.info( window.log.info(
'Total signed key count:', 'Total signed key count:',
allKeys.length, allKeys.length,
@ -287,51 +277,52 @@
'confirmed' 'confirmed'
); );
var confirmedCount = confirmed.length; let confirmedCount = confirmed.length;
// Keep MINIMUM_KEYS confirmed keys, then drop if older than a week // Keep MINIMUM_KEYS confirmed keys, then drop if older than a week
confirmed = confirmed.forEach(function(key, index) { confirmed = confirmed.forEach((key, index) => {
if (index < MINIMUM_KEYS) { if (index < MINIMUM_KEYS) {
return; return;
} }
var created_at = key.created_at || 0; const createdAt = key.created_at || 0;
var age = Date.now() - created_at; const age = Date.now() - createdAt;
if (age > ARCHIVE_AGE) { if (age > ARCHIVE_AGE) {
window.log.info( window.log.info(
'Removing confirmed signed prekey:', 'Removing confirmed signed prekey:',
key.keyId, key.keyId,
'with timestamp:', 'with timestamp:',
created_at createdAt
); );
store.removeSignedPreKey(key.keyId); store.removeSignedPreKey(key.keyId);
confirmedCount--; confirmedCount -= 1;
} }
}); });
var stillNeeded = MINIMUM_KEYS - confirmedCount; const stillNeeded = MINIMUM_KEYS - confirmedCount;
// If we still don't have enough total keys, we keep as many unconfirmed // If we still don't have enough total keys, we keep as many unconfirmed
// keys as necessary. If not necessary, and over a week old, we drop. // keys as necessary. If not necessary, and over a week old, we drop.
unconfirmed.forEach(function(key, index) { unconfirmed.forEach((key, index) => {
if (index < stillNeeded) { if (index < stillNeeded) {
return; return;
} }
var created_at = key.created_at || 0; const createdAt = key.created_at || 0;
var age = Date.now() - created_at; const age = Date.now() - createdAt;
if (age > ARCHIVE_AGE) { if (age > ARCHIVE_AGE) {
window.log.info( window.log.info(
'Removing unconfirmed signed prekey:', 'Removing unconfirmed signed prekey:',
key.keyId, key.keyId,
'with timestamp:', 'with timestamp:',
created_at createdAt
); );
store.removeSignedPreKey(key.keyId); store.removeSignedPreKey(key.keyId);
} }
}); });
}); });
}, },
createAccount: function( createAccount(
number, number,
verificationCode, verificationCode,
identityKeyPair, identityKeyPair,
@ -340,12 +331,12 @@
userAgent, userAgent,
readReceipts readReceipts
) { ) {
var signalingKey = libsignal.crypto.getRandomBytes(32 + 20); const signalingKey = libsignal.crypto.getRandomBytes(32 + 20);
var password = btoa(getString(libsignal.crypto.getRandomBytes(16))); let password = btoa(getString(libsignal.crypto.getRandomBytes(16)));
password = password.substring(0, password.length - 2); password = password.substring(0, password.length - 2);
var registrationId = libsignal.KeyHelper.generateRegistrationId(); const registrationId = libsignal.KeyHelper.generateRegistrationId();
var previousNumber = getNumber(textsecure.storage.get('number_id')); const previousNumber = getNumber(textsecure.storage.get('number_id'));
return this.server return this.server
.confirmCode( .confirmCode(
@ -356,18 +347,18 @@
registrationId, registrationId,
deviceName deviceName
) )
.then(function(response) { .then(response => {
if (previousNumber && previousNumber !== number) { if (previousNumber && previousNumber !== number) {
window.log.warn( window.log.warn(
'New number is different from old number; deleting all previous data' 'New number is different from old number; deleting all previous data'
); );
return textsecure.storage.protocol.removeAllData().then( return textsecure.storage.protocol.removeAllData().then(
function() { () => {
window.log.info('Successfully deleted previous data'); window.log.info('Successfully deleted previous data');
return response; return response;
}, },
function(error) { error => {
window.log.error( window.log.error(
'Something went wrong deleting data from previous number', 'Something went wrong deleting data from previous number',
error && error.stack ? error.stack : error error && error.stack ? error.stack : error
@ -380,60 +371,58 @@
return response; return response;
}) })
.then( .then(response => {
function(response) { textsecure.storage.remove('identityKey');
textsecure.storage.remove('identityKey'); textsecure.storage.remove('signaling_key');
textsecure.storage.remove('signaling_key'); textsecure.storage.remove('password');
textsecure.storage.remove('password'); textsecure.storage.remove('registrationId');
textsecure.storage.remove('registrationId'); textsecure.storage.remove('number_id');
textsecure.storage.remove('number_id'); textsecure.storage.remove('device_name');
textsecure.storage.remove('device_name'); textsecure.storage.remove('regionCode');
textsecure.storage.remove('regionCode'); textsecure.storage.remove('userAgent');
textsecure.storage.remove('userAgent'); textsecure.storage.remove('profileKey');
textsecure.storage.remove('profileKey'); textsecure.storage.remove('read-receipts-setting');
textsecure.storage.remove('read-receipts-setting');
// update our own identity key, which may have changed // update our own identity key, which may have changed
// if we're relinking after a reinstall on the master device // if we're relinking after a reinstall on the master device
textsecure.storage.protocol.saveIdentityWithAttributes(number, { textsecure.storage.protocol.saveIdentityWithAttributes(number, {
id: number, id: number,
publicKey: identityKeyPair.pubKey, publicKey: identityKeyPair.pubKey,
firstUse: true, firstUse: true,
timestamp: Date.now(), timestamp: Date.now(),
verified: textsecure.storage.protocol.VerifiedStatus.VERIFIED, verified: textsecure.storage.protocol.VerifiedStatus.VERIFIED,
nonblockingApproval: true, nonblockingApproval: true,
}); });
textsecure.storage.put('identityKey', identityKeyPair); textsecure.storage.put('identityKey', identityKeyPair);
textsecure.storage.put('signaling_key', signalingKey); textsecure.storage.put('signaling_key', signalingKey);
textsecure.storage.put('password', password); textsecure.storage.put('password', password);
textsecure.storage.put('registrationId', registrationId); textsecure.storage.put('registrationId', registrationId);
if (profileKey) { if (profileKey) {
textsecure.storage.put('profileKey', profileKey); textsecure.storage.put('profileKey', profileKey);
} }
if (userAgent) { if (userAgent) {
textsecure.storage.put('userAgent', userAgent); textsecure.storage.put('userAgent', userAgent);
} }
if (readReceipts) { if (readReceipts) {
textsecure.storage.put('read-receipt-setting', true); textsecure.storage.put('read-receipt-setting', true);
} else { } else {
textsecure.storage.put('read-receipt-setting', false); textsecure.storage.put('read-receipt-setting', false);
} }
textsecure.storage.user.setNumberAndDeviceId( textsecure.storage.user.setNumberAndDeviceId(
number, number,
response.deviceId || 1, response.deviceId || 1,
deviceName deviceName
); );
textsecure.storage.put( textsecure.storage.put(
'regionCode', 'regionCode',
libphonenumber.util.getRegionCodeForNumber(number) libphonenumber.util.getRegionCodeForNumber(number)
); );
}.bind(this) });
);
}, },
clearSessionsAndPreKeys: function() { clearSessionsAndPreKeys() {
var store = textsecure.storage.protocol; const store = textsecure.storage.protocol;
window.log.info('clearing all sessions, prekeys, and signed prekeys'); window.log.info('clearing all sessions, prekeys, and signed prekeys');
return Promise.all([ return Promise.all([
@ -443,79 +432,74 @@
]); ]);
}, },
// Takes the same object returned by generateKeys // Takes the same object returned by generateKeys
confirmKeys: function(keys) { confirmKeys(keys) {
var store = textsecure.storage.protocol; const store = textsecure.storage.protocol;
var key = keys.signedPreKey; const key = keys.signedPreKey;
var confirmed = true; const confirmed = true;
window.log.info('confirmKeys: confirming key', key.keyId); window.log.info('confirmKeys: confirming key', key.keyId);
return store.storeSignedPreKey(key.keyId, key.keyPair, confirmed); return store.storeSignedPreKey(key.keyId, key.keyPair, confirmed);
}, },
generateKeys: function(count, progressCallback) { generateKeys(count, providedProgressCallback) {
if (typeof progressCallback !== 'function') { const progressCallback =
progressCallback = undefined; typeof providedProgressCallback === 'function'
} ? providedProgressCallback
var startId = textsecure.storage.get('maxPreKeyId', 1); : null;
var signedKeyId = textsecure.storage.get('signedKeyId', 1); const startId = textsecure.storage.get('maxPreKeyId', 1);
const signedKeyId = textsecure.storage.get('signedKeyId', 1);
if (typeof startId != 'number') { if (typeof startId !== 'number') {
throw new Error('Invalid maxPreKeyId'); throw new Error('Invalid maxPreKeyId');
} }
if (typeof signedKeyId != 'number') { if (typeof signedKeyId !== 'number') {
throw new Error('Invalid signedKeyId'); throw new Error('Invalid signedKeyId');
} }
var store = textsecure.storage.protocol; const store = textsecure.storage.protocol;
return store.getIdentityKeyPair().then( return store.getIdentityKeyPair().then(identityKey => {
function(identityKey) { const result = { preKeys: [], identityKey: identityKey.pubKey };
var result = { preKeys: [], identityKey: identityKey.pubKey }; const promises = [];
var promises = [];
for (var keyId = startId; keyId < startId + count; ++keyId) {
promises.push(
libsignal.KeyHelper.generatePreKey(keyId).then(function(res) {
store.storePreKey(res.keyId, res.keyPair);
result.preKeys.push({
keyId: res.keyId,
publicKey: res.keyPair.pubKey,
});
if (progressCallback) {
progressCallback();
}
})
);
}
for (let keyId = startId; keyId < startId + count; keyId += 1) {
promises.push( promises.push(
libsignal.KeyHelper.generateSignedPreKey( libsignal.KeyHelper.generatePreKey(keyId).then(res => {
identityKey, store.storePreKey(res.keyId, res.keyPair);
signedKeyId result.preKeys.push({
).then(function(res) {
store.storeSignedPreKey(res.keyId, res.keyPair);
result.signedPreKey = {
keyId: res.keyId, keyId: res.keyId,
publicKey: res.keyPair.pubKey, publicKey: res.keyPair.pubKey,
signature: res.signature, });
// server.registerKeys doesn't use keyPair, confirmKeys does if (progressCallback) {
keyPair: res.keyPair, progressCallback();
}; }
}) })
); );
}
textsecure.storage.put('maxPreKeyId', startId + count); promises.push(
textsecure.storage.put('signedKeyId', signedKeyId + 1); libsignal.KeyHelper.generateSignedPreKey(
return Promise.all(promises).then( identityKey,
function() { signedKeyId
// This is primarily for the signed prekey summary it logs out ).then(res => {
return this.cleanSignedPreKeys().then(function() { store.storeSignedPreKey(res.keyId, res.keyPair);
return result; result.signedPreKey = {
}); keyId: res.keyId,
}.bind(this) publicKey: res.keyPair.pubKey,
); signature: res.signature,
}.bind(this) // server.registerKeys doesn't use keyPair, confirmKeys does
); keyPair: res.keyPair,
};
})
);
textsecure.storage.put('maxPreKeyId', startId + count);
textsecure.storage.put('signedKeyId', signedKeyId + 1);
return Promise.all(promises).then(() =>
// This is primarily for the signed prekey summary it logs out
this.cleanSignedPreKeys().then(() => result)
);
});
}, },
registrationDone: function() { registrationDone() {
window.log.info('registration done'); window.log.info('registration done');
this.dispatchEvent(new Event('registration')); this.dispatchEvent(new Event('registration'));
}, },

View file

@ -1,3 +1,5 @@
/* global dcodeIO, window, textsecure */
function ProtoParser(arrayBuffer, protobuf) { function ProtoParser(arrayBuffer, protobuf) {
this.protobuf = protobuf; this.protobuf = protobuf;
this.buffer = new dcodeIO.ByteBuffer(); this.buffer = new dcodeIO.ByteBuffer();
@ -7,23 +9,23 @@ function ProtoParser(arrayBuffer, protobuf) {
} }
ProtoParser.prototype = { ProtoParser.prototype = {
constructor: ProtoParser, constructor: ProtoParser,
next: function() { next() {
try { try {
if (this.buffer.limit === this.buffer.offset) { if (this.buffer.limit === this.buffer.offset) {
return undefined; // eof return undefined; // eof
} }
var len = this.buffer.readVarint32(); const len = this.buffer.readVarint32();
var nextBuffer = this.buffer const nextBuffer = this.buffer
.slice(this.buffer.offset, this.buffer.offset + len) .slice(this.buffer.offset, this.buffer.offset + len)
.toArrayBuffer(); .toArrayBuffer();
// TODO: de-dupe ByteBuffer.js includes in libaxo/libts // TODO: de-dupe ByteBuffer.js includes in libaxo/libts
// then remove this toArrayBuffer call. // then remove this toArrayBuffer call.
var proto = this.protobuf.decode(nextBuffer); const proto = this.protobuf.decode(nextBuffer);
this.buffer.skip(len); this.buffer.skip(len);
if (proto.avatar) { if (proto.avatar) {
var attachmentLen = proto.avatar.length; const attachmentLen = proto.avatar.length;
proto.avatar.data = this.buffer proto.avatar.data = this.buffer
.slice(this.buffer.offset, this.buffer.offset + attachmentLen) .slice(this.buffer.offset, this.buffer.offset + attachmentLen)
.toArrayBuffer(); .toArrayBuffer();
@ -41,14 +43,16 @@ ProtoParser.prototype = {
error && error.stack ? error.stack : error error && error.stack ? error.stack : error
); );
} }
return null;
}, },
}; };
var GroupBuffer = function(arrayBuffer) { const GroupBuffer = function Constructor(arrayBuffer) {
ProtoParser.call(this, arrayBuffer, textsecure.protobuf.GroupDetails); ProtoParser.call(this, arrayBuffer, textsecure.protobuf.GroupDetails);
}; };
GroupBuffer.prototype = Object.create(ProtoParser.prototype); GroupBuffer.prototype = Object.create(ProtoParser.prototype);
GroupBuffer.prototype.constructor = GroupBuffer; GroupBuffer.prototype.constructor = GroupBuffer;
var ContactBuffer = function(arrayBuffer) { const ContactBuffer = function Constructor(arrayBuffer) {
ProtoParser.call(this, arrayBuffer, textsecure.protobuf.ContactDetails); ProtoParser.call(this, arrayBuffer, textsecure.protobuf.ContactDetails);
}; };
ContactBuffer.prototype = Object.create(ProtoParser.prototype); ContactBuffer.prototype = Object.create(ProtoParser.prototype);

View file

@ -1,30 +1,28 @@
/* global libsignal, crypto, textsecure, dcodeIO, window */
/* eslint-disable more/no-then, no-bitwise */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict'; const { encrypt, decrypt, calculateMAC, verifyMAC } = libsignal.crypto;
var encrypt = libsignal.crypto.encrypt; const PROFILE_IV_LENGTH = 12; // bytes
var decrypt = libsignal.crypto.decrypt; const PROFILE_KEY_LENGTH = 32; // bytes
var calculateMAC = libsignal.crypto.calculateMAC; const PROFILE_TAG_LENGTH = 128; // bits
var verifyMAC = libsignal.crypto.verifyMAC; const PROFILE_NAME_PADDED_LENGTH = 26; // bytes
var PROFILE_IV_LENGTH = 12; // bytes
var PROFILE_KEY_LENGTH = 32; // bytes
var PROFILE_TAG_LENGTH = 128; // bits
var PROFILE_NAME_PADDED_LENGTH = 26; // bytes
function verifyDigest(data, theirDigest) { function verifyDigest(data, theirDigest) {
return crypto.subtle return crypto.subtle.digest({ name: 'SHA-256' }, data).then(ourDigest => {
.digest({ name: 'SHA-256' }, data) const a = new Uint8Array(ourDigest);
.then(function(ourDigest) { const b = new Uint8Array(theirDigest);
var a = new Uint8Array(ourDigest); let result = 0;
var b = new Uint8Array(theirDigest); for (let i = 0; i < theirDigest.byteLength; i += 1) {
var result = 0; result |= a[i] ^ b[i];
for (var i = 0; i < theirDigest.byteLength; ++i) { }
result = result | (a[i] ^ b[i]); if (result !== 0) {
} throw new Error('Bad digest');
if (result !== 0) { }
throw new Error('Bad digest'); });
}
});
} }
function calculateDigest(data) { function calculateDigest(data) {
return crypto.subtle.digest({ name: 'SHA-256' }, data); return crypto.subtle.digest({ name: 'SHA-256' }, data);
@ -33,127 +31,127 @@
window.textsecure = window.textsecure || {}; window.textsecure = window.textsecure || {};
window.textsecure.crypto = { window.textsecure.crypto = {
// Decrypts message into a raw string // Decrypts message into a raw string
decryptWebsocketMessage: function(message, signaling_key) { decryptWebsocketMessage(message, signalingKey) {
var decodedMessage = message.toArrayBuffer(); const decodedMessage = message.toArrayBuffer();
if (signaling_key.byteLength != 52) { if (signalingKey.byteLength !== 52) {
throw new Error('Got invalid length signaling_key'); throw new Error('Got invalid length signalingKey');
} }
if (decodedMessage.byteLength < 1 + 16 + 10) { if (decodedMessage.byteLength < 1 + 16 + 10) {
throw new Error('Got invalid length message'); throw new Error('Got invalid length message');
} }
if (new Uint8Array(decodedMessage)[0] != 1) { if (new Uint8Array(decodedMessage)[0] !== 1) {
throw new Error('Got bad version number: ' + decodedMessage[0]); throw new Error(`Got bad version number: ${decodedMessage[0]}`);
} }
var aes_key = signaling_key.slice(0, 32); const aesKey = signalingKey.slice(0, 32);
var mac_key = signaling_key.slice(32, 32 + 20); const macKey = signalingKey.slice(32, 32 + 20);
var iv = decodedMessage.slice(1, 1 + 16); const iv = decodedMessage.slice(1, 1 + 16);
var ciphertext = decodedMessage.slice( const ciphertext = decodedMessage.slice(
1 + 16, 1 + 16,
decodedMessage.byteLength - 10 decodedMessage.byteLength - 10
); );
var ivAndCiphertext = decodedMessage.slice( const ivAndCiphertext = decodedMessage.slice(
0, 0,
decodedMessage.byteLength - 10 decodedMessage.byteLength - 10
); );
var mac = decodedMessage.slice( const mac = decodedMessage.slice(
decodedMessage.byteLength - 10, decodedMessage.byteLength - 10,
decodedMessage.byteLength decodedMessage.byteLength
); );
return verifyMAC(ivAndCiphertext, mac_key, mac, 10).then(function() { return verifyMAC(ivAndCiphertext, macKey, mac, 10).then(() =>
return decrypt(aes_key, ciphertext, iv); decrypt(aesKey, ciphertext, iv)
}); );
}, },
decryptAttachment: function(encryptedBin, keys, theirDigest) { decryptAttachment(encryptedBin, keys, theirDigest) {
if (keys.byteLength != 64) { if (keys.byteLength !== 64) {
throw new Error('Got invalid length attachment keys'); throw new Error('Got invalid length attachment keys');
} }
if (encryptedBin.byteLength < 16 + 32) { if (encryptedBin.byteLength < 16 + 32) {
throw new Error('Got invalid length attachment'); throw new Error('Got invalid length attachment');
} }
var aes_key = keys.slice(0, 32); const aesKey = keys.slice(0, 32);
var mac_key = keys.slice(32, 64); const macKey = keys.slice(32, 64);
var iv = encryptedBin.slice(0, 16); const iv = encryptedBin.slice(0, 16);
var ciphertext = encryptedBin.slice(16, encryptedBin.byteLength - 32); const ciphertext = encryptedBin.slice(16, encryptedBin.byteLength - 32);
var ivAndCiphertext = encryptedBin.slice(0, encryptedBin.byteLength - 32); const ivAndCiphertext = encryptedBin.slice(
var mac = encryptedBin.slice( 0,
encryptedBin.byteLength - 32
);
const mac = encryptedBin.slice(
encryptedBin.byteLength - 32, encryptedBin.byteLength - 32,
encryptedBin.byteLength encryptedBin.byteLength
); );
return verifyMAC(ivAndCiphertext, mac_key, mac, 32) return verifyMAC(ivAndCiphertext, macKey, mac, 32)
.then(function() { .then(() => {
if (theirDigest !== null) { if (theirDigest !== null) {
return verifyDigest(encryptedBin, theirDigest); return verifyDigest(encryptedBin, theirDigest);
} }
return null;
}) })
.then(function() { .then(() => decrypt(aesKey, ciphertext, iv));
return decrypt(aes_key, ciphertext, iv);
});
}, },
encryptAttachment: function(plaintext, keys, iv) { encryptAttachment(plaintext, keys, iv) {
if ( if (
!(plaintext instanceof ArrayBuffer) && !(plaintext instanceof ArrayBuffer) &&
!ArrayBuffer.isView(plaintext) !ArrayBuffer.isView(plaintext)
) { ) {
throw new TypeError( throw new TypeError(
'`plaintext` must be an `ArrayBuffer` or `ArrayBufferView`; got: ' + `\`plaintext\` must be an \`ArrayBuffer\` or \`ArrayBufferView\`; got: ${typeof plaintext}`
typeof plaintext
); );
} }
if (keys.byteLength != 64) { if (keys.byteLength !== 64) {
throw new Error('Got invalid length attachment keys'); throw new Error('Got invalid length attachment keys');
} }
if (iv.byteLength != 16) { if (iv.byteLength !== 16) {
throw new Error('Got invalid length attachment iv'); throw new Error('Got invalid length attachment iv');
} }
var aes_key = keys.slice(0, 32); const aesKey = keys.slice(0, 32);
var mac_key = keys.slice(32, 64); const macKey = keys.slice(32, 64);
return encrypt(aes_key, plaintext, iv).then(function(ciphertext) { return encrypt(aesKey, plaintext, iv).then(ciphertext => {
var ivAndCiphertext = new Uint8Array(16 + ciphertext.byteLength); const ivAndCiphertext = new Uint8Array(16 + ciphertext.byteLength);
ivAndCiphertext.set(new Uint8Array(iv)); ivAndCiphertext.set(new Uint8Array(iv));
ivAndCiphertext.set(new Uint8Array(ciphertext), 16); ivAndCiphertext.set(new Uint8Array(ciphertext), 16);
return calculateMAC(mac_key, ivAndCiphertext.buffer).then(function( return calculateMAC(macKey, ivAndCiphertext.buffer).then(mac => {
mac const encryptedBin = new Uint8Array(16 + ciphertext.byteLength + 32);
) {
var encryptedBin = new Uint8Array(16 + ciphertext.byteLength + 32);
encryptedBin.set(ivAndCiphertext); encryptedBin.set(ivAndCiphertext);
encryptedBin.set(new Uint8Array(mac), 16 + ciphertext.byteLength); encryptedBin.set(new Uint8Array(mac), 16 + ciphertext.byteLength);
return calculateDigest(encryptedBin.buffer).then(function(digest) { return calculateDigest(encryptedBin.buffer).then(digest => ({
return { ciphertext: encryptedBin.buffer, digest: digest }; ciphertext: encryptedBin.buffer,
}); digest,
}));
}); });
}); });
}, },
encryptProfile: function(data, key) { encryptProfile(data, key) {
var iv = libsignal.crypto.getRandomBytes(PROFILE_IV_LENGTH); const iv = libsignal.crypto.getRandomBytes(PROFILE_IV_LENGTH);
if (key.byteLength != PROFILE_KEY_LENGTH) { if (key.byteLength !== PROFILE_KEY_LENGTH) {
throw new Error('Got invalid length profile key'); throw new Error('Got invalid length profile key');
} }
if (iv.byteLength != PROFILE_IV_LENGTH) { if (iv.byteLength !== PROFILE_IV_LENGTH) {
throw new Error('Got invalid length profile iv'); throw new Error('Got invalid length profile iv');
} }
return crypto.subtle return crypto.subtle
.importKey('raw', key, { name: 'AES-GCM' }, false, ['encrypt']) .importKey('raw', key, { name: 'AES-GCM' }, false, ['encrypt'])
.then(function(key) { .then(keyForEncryption =>
return crypto.subtle crypto.subtle
.encrypt( .encrypt(
{ name: 'AES-GCM', iv: iv, tagLength: PROFILE_TAG_LENGTH }, { name: 'AES-GCM', iv, tagLength: PROFILE_TAG_LENGTH },
key, keyForEncryption,
data data
) )
.then(function(ciphertext) { .then(ciphertext => {
var ivAndCiphertext = new Uint8Array( const ivAndCiphertext = new Uint8Array(
PROFILE_IV_LENGTH + ciphertext.byteLength PROFILE_IV_LENGTH + ciphertext.byteLength
); );
ivAndCiphertext.set(new Uint8Array(iv)); ivAndCiphertext.set(new Uint8Array(iv));
@ -162,32 +160,32 @@
PROFILE_IV_LENGTH PROFILE_IV_LENGTH
); );
return ivAndCiphertext.buffer; return ivAndCiphertext.buffer;
}); })
}); );
}, },
decryptProfile: function(data, key) { decryptProfile(data, key) {
if (data.byteLength < 12 + 16 + 1) { if (data.byteLength < 12 + 16 + 1) {
throw new Error('Got too short input: ' + data.byteLength); throw new Error(`Got too short input: ${data.byteLength}`);
} }
var iv = data.slice(0, PROFILE_IV_LENGTH); const iv = data.slice(0, PROFILE_IV_LENGTH);
var ciphertext = data.slice(PROFILE_IV_LENGTH, data.byteLength); const ciphertext = data.slice(PROFILE_IV_LENGTH, data.byteLength);
if (key.byteLength != PROFILE_KEY_LENGTH) { if (key.byteLength !== PROFILE_KEY_LENGTH) {
throw new Error('Got invalid length profile key'); throw new Error('Got invalid length profile key');
} }
if (iv.byteLength != PROFILE_IV_LENGTH) { if (iv.byteLength !== PROFILE_IV_LENGTH) {
throw new Error('Got invalid length profile iv'); throw new Error('Got invalid length profile iv');
} }
var error = new Error(); // save stack const error = new Error(); // save stack
return crypto.subtle return crypto.subtle
.importKey('raw', key, { name: 'AES-GCM' }, false, ['decrypt']) .importKey('raw', key, { name: 'AES-GCM' }, false, ['decrypt'])
.then(function(key) { .then(keyForEncryption =>
return crypto.subtle crypto.subtle
.decrypt( .decrypt(
{ name: 'AES-GCM', iv: iv, tagLength: PROFILE_TAG_LENGTH }, { name: 'AES-GCM', iv, tagLength: PROFILE_TAG_LENGTH },
key, keyForEncryption,
ciphertext ciphertext
) )
.catch(function(e) { .catch(e => {
if (e.name === 'OperationError') { if (e.name === 'OperationError') {
// bad mac, basically. // bad mac, basically.
error.message = error.message =
@ -195,38 +193,36 @@
error.name = 'ProfileDecryptError'; error.name = 'ProfileDecryptError';
throw error; throw error;
} }
}); })
}); );
}, },
encryptProfileName: function(name, key) { encryptProfileName(name, key) {
var padded = new Uint8Array(PROFILE_NAME_PADDED_LENGTH); const padded = new Uint8Array(PROFILE_NAME_PADDED_LENGTH);
padded.set(new Uint8Array(name)); padded.set(new Uint8Array(name));
return textsecure.crypto.encryptProfile(padded.buffer, key); return textsecure.crypto.encryptProfile(padded.buffer, key);
}, },
decryptProfileName: function(encryptedProfileName, key) { decryptProfileName(encryptedProfileName, key) {
var data = dcodeIO.ByteBuffer.wrap( const data = dcodeIO.ByteBuffer.wrap(
encryptedProfileName, encryptedProfileName,
'base64' 'base64'
).toArrayBuffer(); ).toArrayBuffer();
return textsecure.crypto return textsecure.crypto.decryptProfile(data, key).then(decrypted => {
.decryptProfile(data, key) // unpad
.then(function(decrypted) { const padded = new Uint8Array(decrypted);
// unpad let i;
var name = ''; for (i = padded.length; i > 0; i -= 1) {
var padded = new Uint8Array(decrypted); if (padded[i - 1] !== 0x00) {
for (var i = padded.length; i > 0; i--) { break;
if (padded[i - 1] !== 0x00) {
break;
}
} }
}
return dcodeIO.ByteBuffer.wrap(padded) return dcodeIO.ByteBuffer.wrap(padded)
.slice(0, i) .slice(0, i)
.toArrayBuffer(); .toArrayBuffer();
}); });
}, },
getRandomBytes: function(size) { getRandomBytes(size) {
return libsignal.crypto.getRandomBytes(size); return libsignal.crypto.getRandomBytes(size);
}, },
}; };

View file

@ -1,8 +1,9 @@
(function() { /* global window */
'use strict';
var registeredFunctions = {}; // eslint-disable-next-line func-names
var Type = { (function() {
const registeredFunctions = {};
const Type = {
ENCRYPT_MESSAGE: 1, ENCRYPT_MESSAGE: 1,
INIT_SESSION: 2, INIT_SESSION: 2,
TRANSMIT_MESSAGE: 3, TRANSMIT_MESSAGE: 3,
@ -11,13 +12,14 @@
}; };
window.textsecure = window.textsecure || {}; window.textsecure = window.textsecure || {};
window.textsecure.replay = { window.textsecure.replay = {
Type: Type, Type,
registerFunction: function(func, functionCode) { registerFunction(func, functionCode) {
registeredFunctions[functionCode] = func; registeredFunctions[functionCode] = func;
}, },
}; };
function inherit(Parent, Child) { function inherit(Parent, Child) {
// eslint-disable-next-line no-param-reassign
Child.prototype = Object.create(Parent.prototype, { Child.prototype = Object.create(Parent.prototype, {
constructor: { constructor: {
value: Child, value: Child,
@ -27,11 +29,11 @@
}); });
} }
function appendStack(newError, originalError) { function appendStack(newError, originalError) {
newError.stack += '\nOriginal stack:\n' + originalError.stack; // eslint-disable-next-line no-param-reassign
newError.stack += `\nOriginal stack:\n${originalError.stack}`;
} }
function ReplayableError(options) { function ReplayableError(options = {}) {
options = options || {};
this.name = options.name || 'ReplayableError'; this.name = options.name || 'ReplayableError';
this.message = options.message; this.message = options.message;
@ -48,13 +50,13 @@
} }
inherit(Error, ReplayableError); inherit(Error, ReplayableError);
ReplayableError.prototype.replay = function() { ReplayableError.prototype.replay = function replay(...argumentsAsArray) {
var argumentsAsArray = Array.prototype.slice.call(arguments, 0); const args = this.args.concat(argumentsAsArray);
var args = this.args.concat(argumentsAsArray);
return registeredFunctions[this.functionCode].apply(window, args); return registeredFunctions[this.functionCode].apply(window, args);
}; };
function IncomingIdentityKeyError(number, message, key) { function IncomingIdentityKeyError(number, message, key) {
// eslint-disable-next-line prefer-destructuring
this.number = number.split('.')[0]; this.number = number.split('.')[0];
this.identityKey = key; this.identityKey = key;
@ -62,12 +64,13 @@
functionCode: Type.INIT_SESSION, functionCode: Type.INIT_SESSION,
args: [number, message], args: [number, message],
name: 'IncomingIdentityKeyError', name: 'IncomingIdentityKeyError',
message: 'The identity of ' + this.number + ' has changed.', message: `The identity of ${this.number} has changed.`,
}); });
} }
inherit(ReplayableError, IncomingIdentityKeyError); inherit(ReplayableError, IncomingIdentityKeyError);
function OutgoingIdentityKeyError(number, message, timestamp, identityKey) { function OutgoingIdentityKeyError(number, message, timestamp, identityKey) {
// eslint-disable-next-line prefer-destructuring
this.number = number.split('.')[0]; this.number = number.split('.')[0];
this.identityKey = identityKey; this.identityKey = identityKey;
@ -75,7 +78,7 @@
functionCode: Type.ENCRYPT_MESSAGE, functionCode: Type.ENCRYPT_MESSAGE,
args: [number, message, timestamp], args: [number, message, timestamp],
name: 'OutgoingIdentityKeyError', name: 'OutgoingIdentityKeyError',
message: 'The identity of ' + this.number + ' has changed.', message: `The identity of ${this.number} has changed.`,
}); });
} }
inherit(ReplayableError, OutgoingIdentityKeyError); inherit(ReplayableError, OutgoingIdentityKeyError);

View file

@ -1,27 +1,29 @@
/* global window, Event, textsecure */
/* /*
* Implements EventTarget * Implements EventTarget
* https://developer.mozilla.org/en-US/docs/Web/API/EventTarget * https://developer.mozilla.org/en-US/docs/Web/API/EventTarget
*/ */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict';
window.textsecure = window.textsecure || {}; window.textsecure = window.textsecure || {};
function EventTarget() {} function EventTarget() {}
EventTarget.prototype = { EventTarget.prototype = {
constructor: EventTarget, constructor: EventTarget,
dispatchEvent: function(ev) { dispatchEvent(ev) {
if (!(ev instanceof Event)) { if (!(ev instanceof Event)) {
throw new Error('Expects an event'); throw new Error('Expects an event');
} }
if (this.listeners === null || typeof this.listeners !== 'object') { if (this.listeners === null || typeof this.listeners !== 'object') {
this.listeners = {}; this.listeners = {};
} }
var listeners = this.listeners[ev.type]; const listeners = this.listeners[ev.type];
var results = []; const results = [];
if (typeof listeners === 'object') { if (typeof listeners === 'object') {
for (var i = 0, max = listeners.length; i < max; i += 1) { for (let i = 0, max = listeners.length; i < max; i += 1) {
var listener = listeners[i]; const listener = listeners[i];
if (typeof listener === 'function') { if (typeof listener === 'function') {
results.push(listener.call(null, ev)); results.push(listener.call(null, ev));
} }
@ -29,7 +31,7 @@
} }
return results; return results;
}, },
addEventListener: function(eventName, callback) { addEventListener(eventName, callback) {
if (typeof eventName !== 'string') { if (typeof eventName !== 'string') {
throw new Error('First argument expects a string'); throw new Error('First argument expects a string');
} }
@ -39,14 +41,14 @@
if (this.listeners === null || typeof this.listeners !== 'object') { if (this.listeners === null || typeof this.listeners !== 'object') {
this.listeners = {}; this.listeners = {};
} }
var listeners = this.listeners[eventName]; let listeners = this.listeners[eventName];
if (typeof listeners !== 'object') { if (typeof listeners !== 'object') {
listeners = []; listeners = [];
} }
listeners.push(callback); listeners.push(callback);
this.listeners[eventName] = listeners; this.listeners[eventName] = listeners;
}, },
removeEventListener: function(eventName, callback) { removeEventListener(eventName, callback) {
if (typeof eventName !== 'string') { if (typeof eventName !== 'string') {
throw new Error('First argument expects a string'); throw new Error('First argument expects a string');
} }
@ -56,9 +58,9 @@
if (this.listeners === null || typeof this.listeners !== 'object') { if (this.listeners === null || typeof this.listeners !== 'object') {
this.listeners = {}; this.listeners = {};
} }
var listeners = this.listeners[eventName]; const listeners = this.listeners[eventName];
if (typeof listeners === 'object') { if (typeof listeners === 'object') {
for (var i = 0; i < listeners.length; ++i) { for (let i = 0; i < listeners.length; i += 1) {
if (listeners[i] === callback) { if (listeners[i] === callback) {
listeners.splice(i, 1); listeners.splice(i, 1);
return; return;
@ -67,8 +69,9 @@
} }
this.listeners[eventName] = listeners; this.listeners[eventName] = listeners;
}, },
extend: function(obj) { extend(obj) {
for (var prop in obj) { // eslint-disable-next-line no-restricted-syntax, guard-for-in
for (const prop in obj) {
this[prop] = obj[prop]; this[prop] = obj[prop];
} }
return this; return this;

View file

@ -1,21 +1,25 @@
/* global window, dcodeIO */
/* eslint-disable no-proto, no-restricted-syntax, guard-for-in */
window.textsecure = window.textsecure || {}; window.textsecure = window.textsecure || {};
/********************************* /** *******************************
*** Type conversion utilities *** *** Type conversion utilities ***
*********************************/ ******************************** */
// Strings/arrays // Strings/arrays
//TODO: Throw all this shit in favor of consistent types // TODO: Throw all this shit in favor of consistent types
//TODO: Namespace // TODO: Namespace
var StaticByteBufferProto = new dcodeIO.ByteBuffer().__proto__; const StaticByteBufferProto = new dcodeIO.ByteBuffer().__proto__;
var StaticArrayBufferProto = new ArrayBuffer().__proto__; const StaticArrayBufferProto = new ArrayBuffer().__proto__;
var StaticUint8ArrayProto = new Uint8Array().__proto__; const StaticUint8ArrayProto = new Uint8Array().__proto__;
function getString(thing) { function getString(thing) {
if (thing === Object(thing)) { if (thing === Object(thing)) {
if (thing.__proto__ == StaticUint8ArrayProto) if (thing.__proto__ === StaticUint8ArrayProto)
return String.fromCharCode.apply(null, thing); return String.fromCharCode.apply(null, thing);
if (thing.__proto__ == StaticArrayBufferProto) if (thing.__proto__ === StaticArrayBufferProto)
return getString(new Uint8Array(thing)); return getString(new Uint8Array(thing));
if (thing.__proto__ == StaticByteBufferProto) if (thing.__proto__ === StaticByteBufferProto)
return thing.toString('binary'); return thing.toString('binary');
} }
return thing; return thing;
@ -23,49 +27,44 @@ function getString(thing) {
function getStringable(thing) { function getStringable(thing) {
return ( return (
typeof thing == 'string' || typeof thing === 'string' ||
typeof thing == 'number' || typeof thing === 'number' ||
typeof thing == 'boolean' || typeof thing === 'boolean' ||
(thing === Object(thing) && (thing === Object(thing) &&
(thing.__proto__ == StaticArrayBufferProto || (thing.__proto__ === StaticArrayBufferProto ||
thing.__proto__ == StaticUint8ArrayProto || thing.__proto__ === StaticUint8ArrayProto ||
thing.__proto__ == StaticByteBufferProto)) thing.__proto__ === StaticByteBufferProto))
); );
} }
// Number formatting utils // Number formatting utils
window.textsecure.utils = (function() { window.textsecure.utils = (() => {
var self = {}; const self = {};
self.unencodeNumber = function(number) { self.unencodeNumber = number => number.split('.');
return number.split('.'); self.isNumberSane = number =>
}; number[0] === '+' && /^[0-9]+$/.test(number.substring(1));
self.isNumberSane = function(number) { /** ************************
return number[0] == '+' && /^[0-9]+$/.test(number.substring(1));
};
/**************************
*** JSON'ing Utilities *** *** JSON'ing Utilities ***
**************************/ ************************* */
function ensureStringed(thing) { function ensureStringed(thing) {
if (getStringable(thing)) return getString(thing); if (getStringable(thing)) return getString(thing);
else if (thing instanceof Array) { else if (thing instanceof Array) {
var res = []; const res = [];
for (var i = 0; i < thing.length; i++) res[i] = ensureStringed(thing[i]); for (let i = 0; i < thing.length; i += 1)
res[i] = ensureStringed(thing[i]);
return res; return res;
} else if (thing === Object(thing)) { } else if (thing === Object(thing)) {
var res = {}; const res = {};
for (var key in thing) res[key] = ensureStringed(thing[key]); for (const key in thing) res[key] = ensureStringed(thing[key]);
return res; return res;
} else if (thing === null) { } else if (thing === null) {
return null; return null;
} }
throw new Error('unsure of how to jsonify object of type ' + typeof thing); throw new Error(`unsure of how to jsonify object of type ${typeof thing}`);
} }
self.jsonThing = function(thing) { self.jsonThing = thing => JSON.stringify(ensureStringed(thing));
return JSON.stringify(ensureStringed(thing));
};
return self; return self;
})(); })();

View file

@ -1,4 +1,6 @@
'use strict'; /* global window, postMessage, textsecure, close */
/* eslint-disable more/no-then, no-global-assign, no-restricted-globals, no-unused-vars */
/* /*
* Load this script in a Web Worker to generate new prekeys without * Load this script in a Web Worker to generate new prekeys without
@ -25,34 +27,34 @@
} }
}; };
*/ */
var store = {}; let store = {};
window.textsecure.storage.impl = { window.textsecure.storage.impl = {
/***************************** /** ***************************
*** Override Storage Routines *** *** Override Storage Routines ***
*****************************/ **************************** */
put: function(key, value) { put(key, value) {
if (value === undefined) throw new Error('Tried to store undefined'); if (value === undefined) throw new Error('Tried to store undefined');
store[key] = value; store[key] = value;
postMessage({ method: 'set', key: key, value: value }); postMessage({ method: 'set', key, value });
}, },
get: function(key, defaultValue) { get(key, defaultValue) {
if (key in store) { if (key in store) {
return store[key]; return store[key];
} else {
return defaultValue;
} }
return defaultValue;
}, },
remove: function(key) { remove(key) {
delete store[key]; delete store[key];
postMessage({ method: 'remove', key: key }); postMessage({ method: 'remove', key });
}, },
}; };
onmessage = function(e) { // eslint-disable-next-line no-undef
onmessage = e => {
store = e.data; store = e.data;
textsecure.protocol_wrapper.generateKeys().then(function(keys) { textsecure.protocol_wrapper.generateKeys().then(keys => {
postMessage({ method: 'done', keys: keys }); postMessage({ method: 'done', keys });
close(); close();
}); });
}; };

View file

@ -1,3 +1,7 @@
/* global textsecure, libsignal, window, btoa */
/* eslint-disable more/no-then */
function OutgoingMessage( function OutgoingMessage(
server, server,
timestamp, timestamp,
@ -7,8 +11,9 @@ function OutgoingMessage(
callback callback
) { ) {
if (message instanceof textsecure.protobuf.DataMessage) { if (message instanceof textsecure.protobuf.DataMessage) {
var content = new textsecure.protobuf.Content(); const content = new textsecure.protobuf.Content();
content.dataMessage = message; content.dataMessage = message;
// eslint-disable-next-line no-param-reassign
message = content; message = content;
} }
this.server = server; this.server = server;
@ -25,8 +30,8 @@ function OutgoingMessage(
OutgoingMessage.prototype = { OutgoingMessage.prototype = {
constructor: OutgoingMessage, constructor: OutgoingMessage,
numberCompleted: function() { numberCompleted() {
this.numbersCompleted++; this.numbersCompleted += 1;
if (this.numbersCompleted >= this.numbers.length) { if (this.numbersCompleted >= this.numbers.length) {
this.callback({ this.callback({
successfulNumbers: this.successfulNumbers, successfulNumbers: this.successfulNumbers,
@ -34,8 +39,9 @@ OutgoingMessage.prototype = {
}); });
} }
}, },
registerError: function(number, reason, error) { registerError(number, reason, error) {
if (!error || (error.name === 'HTTPError' && error.code !== 404)) { if (!error || (error.name === 'HTTPError' && error.code !== 404)) {
// eslint-disable-next-line no-param-reassign
error = new textsecure.OutgoingMessageError( error = new textsecure.OutgoingMessageError(
number, number,
this.message.toArrayBuffer(), this.message.toArrayBuffer(),
@ -44,102 +50,94 @@ OutgoingMessage.prototype = {
); );
} }
// eslint-disable-next-line no-param-reassign
error.number = number; error.number = number;
// eslint-disable-next-line no-param-reassign
error.reason = reason; error.reason = reason;
this.errors[this.errors.length] = error; this.errors[this.errors.length] = error;
this.numberCompleted(); this.numberCompleted();
}, },
reloadDevicesAndSend: function(number, recurse) { reloadDevicesAndSend(number, recurse) {
return function() { return () =>
return textsecure.storage.protocol.getDeviceIds(number).then( textsecure.storage.protocol.getDeviceIds(number).then(deviceIds => {
function(deviceIds) { if (deviceIds.length === 0) {
if (deviceIds.length == 0) { return this.registerError(
return this.registerError( number,
number, 'Got empty device list when loading device keys',
'Got empty device list when loading device keys', null
null );
); }
} return this.doSendMessage(number, deviceIds, recurse);
return this.doSendMessage(number, deviceIds, recurse); });
}.bind(this)
);
}.bind(this);
}, },
getKeysForNumber: function(number, updateDevices) { getKeysForNumber(number, updateDevices) {
var handleResult = function(response) { const handleResult = response =>
return Promise.all( Promise.all(
response.devices.map( response.devices.map(device => {
function(device) { // eslint-disable-next-line no-param-reassign
device.identityKey = response.identityKey; device.identityKey = response.identityKey;
if ( if (
updateDevices === undefined || updateDevices === undefined ||
updateDevices.indexOf(device.deviceId) > -1 updateDevices.indexOf(device.deviceId) > -1
) { ) {
var address = new libsignal.SignalProtocolAddress( const address = new libsignal.SignalProtocolAddress(
number, number,
device.deviceId device.deviceId
); );
var builder = new libsignal.SessionBuilder( const builder = new libsignal.SessionBuilder(
textsecure.storage.protocol, textsecure.storage.protocol,
address address
); );
if (device.registrationId === 0) { if (device.registrationId === 0) {
window.log.info('device registrationId 0!'); window.log.info('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) return builder.processPreKey(device).catch(error => {
) if (error.message === 'Identity key changed') {
// eslint-disable-next-line no-param-reassign
error.timestamp = this.timestamp;
// eslint-disable-next-line no-param-reassign
error.originalMessage = this.message.toArrayBuffer();
// eslint-disable-next-line no-param-reassign
error.identityKey = device.identityKey;
}
throw error;
});
}
return null;
})
); );
}.bind(this);
if (updateDevices === undefined) { if (updateDevices === undefined) {
return this.server.getKeysForNumber(number).then(handleResult); 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;
} }
let promise = Promise.resolve();
updateDevices.forEach(device => {
promise = promise.then(() =>
this.server
.getKeysForNumber(number, device)
.then(handleResult)
.catch(e => {
if (e.name === 'HTTPError' && e.code === 404) {
if (device !== 1) {
return this.removeDeviceIdsForNumber(number, [device]);
}
throw new textsecure.UnregisteredUserError(number, e);
} else {
throw e;
}
})
);
});
return promise;
}, },
transmitMessage: function(number, jsonData, timestamp) { transmitMessage(number, jsonData, timestamp) {
return this.server return this.server
.sendMessages(number, jsonData, timestamp, this.silent) .sendMessages(number, jsonData, timestamp, this.silent)
.catch(function(e) { .catch(e => {
if (e.name === 'HTTPError' && (e.code !== 409 && e.code !== 410)) { if (e.name === 'HTTPError' && (e.code !== 409 && e.code !== 410)) {
// 409 and 410 should bubble and be handled by doSendMessage // 409 and 410 should bubble and be handled by doSendMessage
// 404 should throw UnregisteredUserError // 404 should throw UnregisteredUserError
@ -158,20 +156,20 @@ OutgoingMessage.prototype = {
}); });
}, },
getPaddedMessageLength: function(messageLength) { getPaddedMessageLength(messageLength) {
var messageLengthWithTerminator = messageLength + 1; const messageLengthWithTerminator = messageLength + 1;
var messagePartCount = Math.floor(messageLengthWithTerminator / 160); let messagePartCount = Math.floor(messageLengthWithTerminator / 160);
if (messageLengthWithTerminator % 160 !== 0) { if (messageLengthWithTerminator % 160 !== 0) {
messagePartCount++; messagePartCount += 1;
} }
return messagePartCount * 160; return messagePartCount * 160;
}, },
getPlaintext: function() { getPlaintext() {
if (!this.plaintext) { if (!this.plaintext) {
var messageBuffer = this.message.toArrayBuffer(); const messageBuffer = this.message.toArrayBuffer();
this.plaintext = new Uint8Array( this.plaintext = new Uint8Array(
this.getPaddedMessageLength(messageBuffer.byteLength + 1) - 1 this.getPaddedMessageLength(messageBuffer.byteLength + 1) - 1
); );
@ -181,172 +179,154 @@ OutgoingMessage.prototype = {
return this.plaintext; return this.plaintext;
}, },
doSendMessage: function(number, deviceIds, recurse) { doSendMessage(number, deviceIds, recurse) {
var ciphers = {}; const ciphers = {};
var plaintext = this.getPlaintext(); const plaintext = this.getPlaintext();
return Promise.all( return Promise.all(
deviceIds.map( deviceIds.map(deviceId => {
function(deviceId) { const address = new libsignal.SignalProtocolAddress(number, deviceId);
var address = new libsignal.SignalProtocolAddress(number, deviceId);
var ourNumber = textsecure.storage.user.getNumber(); const ourNumber = textsecure.storage.user.getNumber();
var options = {}; const options = {};
// No limit on message keys if we're communicating with our other devices // No limit on message keys if we're communicating with our other devices
if (ourNumber === number) { if (ourNumber === number) {
options.messageKeysLimit = false; options.messageKeysLimit = false;
} }
var sessionCipher = new libsignal.SessionCipher( const sessionCipher = new libsignal.SessionCipher(
textsecure.storage.protocol, textsecure.storage.protocol,
address, address,
options options
); );
ciphers[address.getDeviceId()] = sessionCipher; ciphers[address.getDeviceId()] = sessionCipher;
return sessionCipher.encrypt(plaintext).then(function(ciphertext) { return sessionCipher.encrypt(plaintext).then(ciphertext => ({
return { type: ciphertext.type,
type: ciphertext.type, destinationDeviceId: address.getDeviceId(),
destinationDeviceId: address.getDeviceId(), destinationRegistrationId: ciphertext.registrationId,
destinationRegistrationId: ciphertext.registrationId, content: btoa(ciphertext.body),
content: btoa(ciphertext.body), }));
}; })
});
}.bind(this)
)
) )
.then( .then(jsonData =>
function(jsonData) { this.transmitMessage(number, jsonData, this.timestamp).then(() => {
return this.transmitMessage(number, jsonData, this.timestamp).then( this.successfulNumbers[this.successfulNumbers.length] = number;
function() { this.numberCompleted();
this.successfulNumbers[this.successfulNumbers.length] = number; })
this.numberCompleted();
}.bind(this)
);
}.bind(this)
) )
.catch( .catch(error => {
function(error) { if (
if ( error instanceof Error &&
error instanceof Error && error.name === 'HTTPError' &&
error.name == 'HTTPError' && (error.code === 410 || error.code === 409)
(error.code == 410 || error.code == 409) ) {
) { if (!recurse)
if (!recurse) return this.registerError(
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();
window.log.error(
'Got "key changed" error from encrypt - no identityKey for application layer',
number, number,
deviceIds 'Hit retry limit attempting to reload device list',
);
throw error;
} else {
this.registerError(
number,
'Failed to create or send message',
error error
); );
}
}.bind(this)
);
},
getStaleDeviceIdsForNumber: function(number) { let p;
return textsecure.storage.protocol if (error.code === 409) {
.getDeviceIds(number) p = this.removeDeviceIdsForNumber(
.then(function(deviceIds) { number,
if (deviceIds.length === 0) { error.response.extraDevices
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) { } else {
if (!hasSession) { p = Promise.all(
updateDevices.push(deviceId); error.response.staleDevices.map(deviceId =>
} ciphers[deviceId].closeOpenSessionForDevice()
}); )
}) );
).then(function() { }
return updateDevices;
}); return p.then(() => {
const resetDevices =
error.code === 410
? error.response.staleDevices
: error.response.missingDevices;
return this.getKeysForNumber(number, resetDevices).then(
this.reloadDevicesAndSend(number, error.code === 409)
);
});
} else if (error.message === 'Identity key changed') {
// eslint-disable-next-line no-param-reassign
error.timestamp = this.timestamp;
// eslint-disable-next-line no-param-reassign
error.originalMessage = this.message.toArrayBuffer();
window.log.error(
'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);
}
return null;
}); });
}, },
removeDeviceIdsForNumber: function(number, deviceIdsToRemove) { getStaleDeviceIdsForNumber(number) {
var promise = Promise.resolve(); return textsecure.storage.protocol.getDeviceIds(number).then(deviceIds => {
for (var j in deviceIdsToRemove) { if (deviceIds.length === 0) {
promise = promise.then(function() { return [1];
var encodedNumber = number + '.' + deviceIdsToRemove[j]; }
const updateDevices = [];
return Promise.all(
deviceIds.map(deviceId => {
const address = new libsignal.SignalProtocolAddress(number, deviceId);
const sessionCipher = new libsignal.SessionCipher(
textsecure.storage.protocol,
address
);
return sessionCipher.hasOpenSession().then(hasSession => {
if (!hasSession) {
updateDevices.push(deviceId);
}
});
})
).then(() => updateDevices);
});
},
removeDeviceIdsForNumber(number, deviceIdsToRemove) {
let promise = Promise.resolve();
// eslint-disable-next-line no-restricted-syntax, guard-for-in
for (const j in deviceIdsToRemove) {
promise = promise.then(() => {
const encodedNumber = `${number}.${deviceIdsToRemove[j]}`;
return textsecure.storage.protocol.removeSession(encodedNumber); return textsecure.storage.protocol.removeSession(encodedNumber);
}); });
} }
return promise; return promise;
}, },
sendToNumber: function(number) { sendToNumber(number) {
return this.getStaleDeviceIdsForNumber(number).then( return this.getStaleDeviceIdsForNumber(number).then(updateDevices =>
function(updateDevices) { this.getKeysForNumber(number, updateDevices)
return this.getKeysForNumber(number, updateDevices) .then(this.reloadDevicesAndSend(number, true))
.then(this.reloadDevicesAndSend(number, true)) .catch(error => {
.catch( if (error.message === 'Identity key changed') {
function(error) { // eslint-disable-next-line no-param-reassign
if (error.message === 'Identity key changed') { error = new textsecure.OutgoingIdentityKeyError(
error = new textsecure.OutgoingIdentityKeyError( number,
number, error.originalMessage,
error.originalMessage, error.timestamp,
error.timestamp, error.identityKey
error.identityKey );
); this.registerError(number, 'Identity key changed', error);
this.registerError(number, 'Identity key changed', error); } else {
} else { this.registerError(
this.registerError( number,
number, `Failed to retrieve new device keys for number ${number}`,
'Failed to retrieve new device keys for number ' + number, error
error );
); }
} })
}.bind(this)
);
}.bind(this)
); );
}, },
}; };

View file

@ -1,35 +1,31 @@
/* global window, dcodeIO, textsecure */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict';
window.textsecure = window.textsecure || {}; window.textsecure = window.textsecure || {};
window.textsecure.protobuf = {}; window.textsecure.protobuf = {};
function loadProtoBufs(filename) { function loadProtoBufs(filename) {
return dcodeIO.ProtoBuf.loadProtoFile( return dcodeIO.ProtoBuf.loadProtoFile(
{ root: window.PROTO_ROOT, file: filename }, { root: window.PROTO_ROOT, file: filename },
function(error, result) { (error, result) => {
if (error) { if (error) {
var text = const text = `Error loading protos from ${filename} (root: ${
'Error loading protos from ' + window.PROTO_ROOT
filename + }) ${error && error.stack ? error.stack : error}`;
' (root: ' +
window.PROTO_ROOT +
') ' +
(error && error.stack ? error.stack : error);
window.log.error(text); window.log.error(text);
throw error; throw error;
} }
var protos = result.build('signalservice'); const protos = result.build('signalservice');
if (!protos) { if (!protos) {
var text = const text = `Error loading protos from ${filename} (root: ${
'Error loading protos from ' + window.PROTO_ROOT
filename + })`;
' (root: ' +
window.PROTO_ROOT +
')';
window.log.error(text); window.log.error(text);
throw new Error(text); throw new Error(text);
} }
for (var protoName in protos) { // eslint-disable-next-line no-restricted-syntax, guard-for-in
for (const protoName in protos) {
textsecure.protobuf[protoName] = protos[protoName]; textsecure.protobuf[protoName] = protos[protoName];
} }
} }

View file

@ -1,5 +1,7 @@
/* global window, textsecure, SignalProtocolStore, libsignal */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict';
window.textsecure = window.textsecure || {}; window.textsecure = window.textsecure || {};
window.textsecure.storage = window.textsecure.storage || {}; window.textsecure.storage = window.textsecure.storage || {};

File diff suppressed because it is too large Load diff

View file

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

View file

@ -1,32 +1,33 @@
(function() { /* global window, getString, libsignal, textsecure */
'use strict';
/********************* /* eslint-disable more/no-then */
// eslint-disable-next-line func-names
(function() {
/** *******************
*** Group Storage *** *** Group Storage ***
*********************/ ******************** */
window.textsecure = window.textsecure || {}; window.textsecure = window.textsecure || {};
window.textsecure.storage = window.textsecure.storage || {}; window.textsecure.storage = window.textsecure.storage || {};
// create a random group id that we haven't seen before. // create a random group id that we haven't seen before.
function generateNewGroupId() { function generateNewGroupId() {
var groupId = getString(libsignal.crypto.getRandomBytes(16)); const groupId = getString(libsignal.crypto.getRandomBytes(16));
return textsecure.storage.protocol.getGroup(groupId).then(function(group) { return textsecure.storage.protocol.getGroup(groupId).then(group => {
if (group === undefined) { if (group === undefined) {
return groupId; return groupId;
} else {
console.warn('group id collision'); // probably a bad sign.
return generateNewGroupId();
} }
window.log.warn('group id collision'); // probably a bad sign.
return generateNewGroupId();
}); });
} }
window.textsecure.storage.groups = { window.textsecure.storage.groups = {
createNewGroup: function(numbers, groupId) { createNewGroup(numbers, groupId) {
var groupId = groupId; return new Promise(resolve => {
return new Promise(function(resolve) {
if (groupId !== undefined) { if (groupId !== undefined) {
resolve( resolve(
textsecure.storage.protocol.getGroup(groupId).then(function(group) { textsecure.storage.protocol.getGroup(groupId).then(group => {
if (group !== undefined) { if (group !== undefined) {
throw new Error('Tried to recreate group'); throw new Error('Tried to recreate group');
} }
@ -34,131 +35,124 @@
); );
} else { } else {
resolve( resolve(
generateNewGroupId().then(function(newGroupId) { generateNewGroupId().then(newGroupId => {
// eslint-disable-next-line no-param-reassign
groupId = newGroupId; groupId = newGroupId;
}) })
); );
} }
}).then(function() { }).then(() => {
var me = textsecure.storage.user.getNumber(); const me = textsecure.storage.user.getNumber();
var haveMe = false; let haveMe = false;
var finalNumbers = []; const finalNumbers = [];
for (var i in numbers) { // eslint-disable-next-line no-restricted-syntax, guard-for-in
var number = numbers[i]; for (const i in numbers) {
const number = numbers[i];
if (!textsecure.utils.isNumberSane(number)) if (!textsecure.utils.isNumberSane(number))
throw new Error('Invalid number in group'); throw new Error('Invalid number in group');
if (number == me) haveMe = true; if (number === me) haveMe = true;
if (finalNumbers.indexOf(number) < 0) finalNumbers.push(number); if (finalNumbers.indexOf(number) < 0) finalNumbers.push(number);
} }
if (!haveMe) finalNumbers.push(me); if (!haveMe) finalNumbers.push(me);
var groupObject = { numbers: finalNumbers, numberRegistrationIds: {} }; const groupObject = {
for (var i in finalNumbers) numbers: finalNumbers,
numberRegistrationIds: {},
};
// eslint-disable-next-line no-restricted-syntax, guard-for-in
for (const i in finalNumbers) {
groupObject.numberRegistrationIds[finalNumbers[i]] = {}; groupObject.numberRegistrationIds[finalNumbers[i]] = {};
}
return textsecure.storage.protocol return textsecure.storage.protocol
.putGroup(groupId, groupObject) .putGroup(groupId, groupObject)
.then(function() { .then(() => ({ id: groupId, numbers: finalNumbers }));
return { id: groupId, numbers: finalNumbers };
});
}); });
}, },
getNumbers: function(groupId) { getNumbers(groupId) {
return textsecure.storage.protocol return textsecure.storage.protocol.getGroup(groupId).then(group => {
.getGroup(groupId) if (group === undefined) return undefined;
.then(function(group) {
if (group === undefined) return undefined;
return group.numbers; return group.numbers;
}); });
}, },
removeNumber: function(groupId, number) { removeNumber(groupId, number) {
return textsecure.storage.protocol return textsecure.storage.protocol.getGroup(groupId).then(group => {
.getGroup(groupId) if (group === undefined) return undefined;
.then(function(group) {
if (group === undefined) return undefined;
var me = textsecure.storage.user.getNumber(); const me = textsecure.storage.user.getNumber();
if (number == me) if (number === me)
throw new Error( throw new Error(
'Cannot remove ourselves from a group, leave the group instead' '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] = {};
}
}
const i = group.numbers.indexOf(number);
if (i > -1) {
group.numbers.splice(i, 1);
// eslint-disable-next-line no-param-reassign
delete group.numberRegistrationIds[number];
return textsecure.storage.protocol return textsecure.storage.protocol
.putGroup(groupId, group) .putGroup(groupId, group)
.then(function() { .then(() => group.numbers);
return group.numbers; }
});
}); return group.numbers;
});
}, },
deleteGroup: function(groupId) { addNumbers(groupId, numbers) {
return textsecure.storage.protocol.getGroup(groupId).then(group => {
if (group === undefined) return undefined;
// eslint-disable-next-line no-restricted-syntax, guard-for-in
for (const i in numbers) {
const 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);
// eslint-disable-next-line no-param-reassign
group.numberRegistrationIds[number] = {};
}
}
return textsecure.storage.protocol
.putGroup(groupId, group)
.then(() => group.numbers);
});
},
deleteGroup(groupId) {
return textsecure.storage.protocol.removeGroup(groupId); return textsecure.storage.protocol.removeGroup(groupId);
}, },
getGroup: function(groupId) { getGroup(groupId) {
return textsecure.storage.protocol return textsecure.storage.protocol.getGroup(groupId).then(group => {
.getGroup(groupId) if (group === undefined) return undefined;
.then(function(group) {
if (group === undefined) return undefined;
return { id: groupId, numbers: group.numbers }; return { id: groupId, numbers: group.numbers };
}); });
}, },
updateNumbers: function(groupId, numbers) { updateNumbers(groupId, numbers) {
return textsecure.storage.protocol return textsecure.storage.protocol.getGroup(groupId).then(group => {
.getGroup(groupId) if (group === undefined)
.then(function(group) { throw new Error('Tried to update numbers for unknown group');
if (group === undefined)
throw new Error('Tried to update numbers for unknown group');
if ( if (
numbers.filter(textsecure.utils.isNumberSane).length < numbers.filter(textsecure.utils.isNumberSane).length < numbers.length
numbers.length )
) throw new Error('Invalid number in new group members');
throw new Error('Invalid number in new group members');
var added = numbers.filter(function(number) { const added = numbers.filter(
return group.numbers.indexOf(number) < 0; number => group.numbers.indexOf(number) < 0
}); );
return textsecure.storage.groups.addNumbers(groupId, added); return textsecure.storage.groups.addNumbers(groupId, added);
}); });
}, },
}; };
})(); })();

View file

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

View file

@ -1,33 +1,34 @@
'use strict'; /* global textsecure, window */
// eslint-disable-next-line func-names
(function() { (function() {
/********************************************* /** *******************************************
*** Utilities to store data about the user *** *** Utilities to store data about the user ***
**********************************************/ ********************************************* */
window.textsecure = window.textsecure || {}; window.textsecure = window.textsecure || {};
window.textsecure.storage = window.textsecure.storage || {}; window.textsecure.storage = window.textsecure.storage || {};
window.textsecure.storage.user = { window.textsecure.storage.user = {
setNumberAndDeviceId: function(number, deviceId, deviceName) { setNumberAndDeviceId(number, deviceId, deviceName) {
textsecure.storage.put('number_id', number + '.' + deviceId); textsecure.storage.put('number_id', `${number}.${deviceId}`);
if (deviceName) { if (deviceName) {
textsecure.storage.put('device_name', deviceName); textsecure.storage.put('device_name', deviceName);
} }
}, },
getNumber: function(key, defaultValue) { getNumber() {
var number_id = textsecure.storage.get('number_id'); const numberId = textsecure.storage.get('number_id');
if (number_id === undefined) return undefined; if (numberId === undefined) return undefined;
return textsecure.utils.unencodeNumber(number_id)[0]; return textsecure.utils.unencodeNumber(numberId)[0];
}, },
getDeviceId: function(key) { getDeviceId() {
var number_id = textsecure.storage.get('number_id'); const numberId = textsecure.storage.get('number_id');
if (number_id === undefined) return undefined; if (numberId === undefined) return undefined;
return textsecure.utils.unencodeNumber(number_id)[1]; return textsecure.utils.unencodeNumber(numberId)[1];
}, },
getDeviceName: function(key) { getDeviceName() {
return textsecure.storage.get('device_name'); return textsecure.storage.get('device_name');
}, },
}; };

View file

@ -1,6 +1,9 @@
(function() { /* global window, StringView */
'use strict';
/* eslint-disable no-bitwise, no-nested-ternary */
// eslint-disable-next-line func-names
(function() {
window.StringView = { window.StringView = {
/* /*
* These functions from the Mozilla Developer Network * These functions from the Mozilla Developer Network
@ -9,7 +12,7 @@
* https://developer.mozilla.org/en-US/docs/MDN/About#Copyrights_and_licenses * https://developer.mozilla.org/en-US/docs/MDN/About#Copyrights_and_licenses
*/ */
b64ToUint6: function(nChr) { b64ToUint6(nChr) {
return nChr > 64 && nChr < 91 return nChr > 64 && nChr < 91
? nChr - 65 ? nChr - 65
: nChr > 96 && nChr < 123 : nChr > 96 && nChr < 123
@ -23,25 +26,31 @@
: 0; : 0;
}, },
base64ToBytes: function(sBase64, nBlocksSize) { base64ToBytes(sBase64, nBlocksSize) {
var sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ''), const sB64Enc = sBase64.replace(/[^A-Za-z0-9+/]/g, '');
nInLen = sB64Enc.length, const nInLen = sB64Enc.length;
nOutLen = nBlocksSize const nOutLen = nBlocksSize
? Math.ceil(((nInLen * 3 + 1) >> 2) / nBlocksSize) * nBlocksSize ? Math.ceil(((nInLen * 3 + 1) >> 2) / nBlocksSize) * nBlocksSize
: (nInLen * 3 + 1) >> 2; : (nInLen * 3 + 1) >> 2;
var aBBytes = new ArrayBuffer(nOutLen); const aBBytes = new ArrayBuffer(nOutLen);
var taBytes = new Uint8Array(aBBytes); const taBytes = new Uint8Array(aBBytes);
let nMod3;
let nMod4;
for ( for (
var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; let nUint24 = 0, nOutIdx = 0, nInIdx = 0;
nInIdx < nInLen; nInIdx < nInLen;
nInIdx++ nInIdx += 1
) { ) {
nMod4 = nInIdx & 3; nMod4 = nInIdx & 3;
nUint24 |= nUint24 |=
StringView.b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << (18 - 6 * nMod4); StringView.b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << (18 - 6 * nMod4);
if (nMod4 === 3 || nInLen - nInIdx === 1) { if (nMod4 === 3 || nInLen - nInIdx === 1) {
for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) { for (
nMod3 = 0;
nMod3 < 3 && nOutIdx < nOutLen;
nMod3 += 1, nOutIdx += 1
) {
taBytes[nOutIdx] = (nUint24 >>> ((16 >>> nMod3) & 24)) & 255; taBytes[nOutIdx] = (nUint24 >>> ((16 >>> nMod3) & 24)) & 255;
} }
nUint24 = 0; nUint24 = 0;
@ -50,7 +59,7 @@
return aBBytes; return aBBytes;
}, },
uint6ToB64: function(nUint6) { uint6ToB64(nUint6) {
return nUint6 < 26 return nUint6 < 26
? nUint6 + 65 ? nUint6 + 65
: nUint6 < 52 : nUint6 < 52
@ -64,13 +73,13 @@
: 65; : 65;
}, },
bytesToBase64: function(aBytes) { bytesToBase64(aBytes) {
var nMod3, let nMod3;
sB64Enc = ''; let sB64Enc = '';
for ( for (
var nLen = aBytes.length, nUint24 = 0, nIdx = 0; let nLen = aBytes.length, nUint24 = 0, nIdx = 0;
nIdx < nLen; nIdx < nLen;
nIdx++ nIdx += 1
) { ) {
nMod3 = nIdx % 3; nMod3 = nIdx % 3;
if (nIdx > 0 && (nIdx * 4 / 3) % 76 === 0) { if (nIdx > 0 && (nIdx * 4 / 3) % 76 === 0) {

View file

@ -1,5 +1,9 @@
/* global Event, textsecure, window */
/* eslint-disable more/no-then */
// eslint-disable-next-line func-names
(function() { (function() {
'use strict';
window.textsecure = window.textsecure || {}; window.textsecure = window.textsecure || {};
function SyncRequest(sender, receiver) { function SyncRequest(sender, receiver) {
@ -22,11 +26,11 @@
window.log.info('SyncRequest created. Sending contact sync message...'); window.log.info('SyncRequest created. Sending contact sync message...');
sender sender
.sendRequestContactSyncMessage() .sendRequestContactSyncMessage()
.then(function() { .then(() => {
window.log.info('SyncRequest now sending group sync messsage...'); window.log.info('SyncRequest now sending group sync messsage...');
return sender.sendRequestGroupSyncMessage(); return sender.sendRequestGroupSyncMessage();
}) })
.catch(function(error) { .catch(error => {
window.log.error( window.log.error(
'SyncRequest error:', 'SyncRequest error:',
error && error.stack ? error.stack : error error && error.stack ? error.stack : error
@ -38,21 +42,21 @@
SyncRequest.prototype = new textsecure.EventTarget(); SyncRequest.prototype = new textsecure.EventTarget();
SyncRequest.prototype.extend({ SyncRequest.prototype.extend({
constructor: SyncRequest, constructor: SyncRequest,
onContactSyncComplete: function() { onContactSyncComplete() {
this.contactSync = true; this.contactSync = true;
this.update(); this.update();
}, },
onGroupSyncComplete: function() { onGroupSyncComplete() {
this.groupSync = true; this.groupSync = true;
this.update(); this.update();
}, },
update: function() { update() {
if (this.contactSync && this.groupSync) { if (this.contactSync && this.groupSync) {
this.dispatchEvent(new Event('success')); this.dispatchEvent(new Event('success'));
this.cleanup(); this.cleanup();
} }
}, },
onTimeout: function() { onTimeout() {
if (this.contactSync || this.groupSync) { if (this.contactSync || this.groupSync) {
this.dispatchEvent(new Event('success')); this.dispatchEvent(new Event('success'));
} else { } else {
@ -60,7 +64,7 @@
} }
this.cleanup(); this.cleanup();
}, },
cleanup: function() { cleanup() {
clearTimeout(this.timeout); clearTimeout(this.timeout);
this.receiver.removeEventListener('contactsync', this.oncontact); this.receiver.removeEventListener('contactsync', this.oncontact);
this.receiver.removeEventListener('groupSync', this.ongroup); this.receiver.removeEventListener('groupSync', this.ongroup);
@ -68,8 +72,8 @@
}, },
}); });
textsecure.SyncRequest = function(sender, receiver) { textsecure.SyncRequest = function SyncRequestWrapper(sender, receiver) {
var syncRequest = new SyncRequest(sender, receiver); const syncRequest = new SyncRequest(sender, receiver);
this.addEventListener = syncRequest.addEventListener.bind(syncRequest); this.addEventListener = syncRequest.addEventListener.bind(syncRequest);
this.removeEventListener = syncRequest.removeEventListener.bind( this.removeEventListener = syncRequest.removeEventListener.bind(
syncRequest syncRequest

View file

@ -1,31 +1,34 @@
/* global window */
/* eslint-disable more/no-then */
// eslint-disable-next-line func-names
(function() { (function() {
window.textsecure = window.textsecure || {}; window.textsecure = window.textsecure || {};
window.textsecure.createTaskWithTimeout = function(task, id, options) { window.textsecure.createTaskWithTimeout = (task, id, options = {}) => {
options = options || {}; const timeout = options.timeout || 1000 * 60 * 2; // two minutes
options.timeout = options.timeout || 1000 * 60 * 2; // two minutes
var errorForStack = new Error('for stack'); const errorForStack = new Error('for stack');
return function() { return () =>
return new Promise(function(resolve, reject) { new Promise((resolve, reject) => {
var complete = false; let complete = false;
var timer = setTimeout( let timer = setTimeout(() => {
function() { if (!complete) {
if (!complete) { const message = `${id ||
var message = ''} task did not complete in time. Calling stack: ${
(id || '') + errorForStack.stack
' task did not complete in time. Calling stack: ' + }`;
errorForStack.stack;
window.log.error(message); window.log.error(message);
return reject(new Error(message)); return reject(new Error(message));
} }
}.bind(this),
options.timeout return null;
); }, timeout);
var clearTimer = function() { const clearTimer = () => {
try { try {
var localTimer = timer; const localTimer = timer;
if (localTimer) { if (localTimer) {
timer = null; timer = null;
clearTimeout(localTimer); clearTimeout(localTimer);
@ -39,18 +42,18 @@
} }
}; };
var success = function(result) { const success = result => {
clearTimer(); clearTimer();
complete = true; complete = true;
return resolve(result); return resolve(result);
}; };
var failure = function(error) { const failure = error => {
clearTimer(); clearTimer();
complete = true; complete = true;
return reject(error); return reject(error);
}; };
var promise; let promise;
try { try {
promise = task(); promise = task();
} catch (error) { } catch (error) {
@ -65,6 +68,5 @@
return promise.then(success, failure); return promise.then(success, failure);
}); });
};
}; };
})(); })();

View file

@ -3,19 +3,19 @@ window.assert = chai.assert;
window.PROTO_ROOT = '../../protos'; window.PROTO_ROOT = '../../protos';
(function() { (function() {
var OriginalReporter = mocha._reporter; const OriginalReporter = mocha._reporter;
var SauceReporter = function(runner) { const SauceReporter = function(runner) {
var failedTests = []; const failedTests = [];
runner.on('end', function() { runner.on('end', () => {
window.mochaResults = runner.stats; window.mochaResults = runner.stats;
window.mochaResults.reports = failedTests; window.mochaResults.reports = failedTests;
}); });
runner.on('fail', function(test, err) { runner.on('fail', (test, err) => {
var flattenTitles = function(test) { const flattenTitles = function(test) {
var titles = []; const titles = [];
while (test.parent.title) { while (test.parent.title) {
titles.push(test.parent.title); titles.push(test.parent.title);
test = test.parent; test = test.parent;
@ -47,9 +47,9 @@ function assertEqualArrayBuffers(ab1, ab2) {
} }
function hexToArrayBuffer(str) { function hexToArrayBuffer(str) {
var ret = new ArrayBuffer(str.length / 2); const ret = new ArrayBuffer(str.length / 2);
var array = new Uint8Array(ret); const array = new Uint8Array(ret);
for (var i = 0; i < str.length / 2; i++) for (let i = 0; i < str.length / 2; i++)
array[i] = parseInt(str.substr(i * 2, 2), 16); array[i] = parseInt(str.substr(i * 2, 2), 16);
return ret; return ret;
} }

View file

@ -1,30 +1,28 @@
'use strict'; describe('AccountManager', () => {
describe('AccountManager', function() {
let accountManager; let accountManager;
beforeEach(function() { beforeEach(() => {
accountManager = new window.textsecure.AccountManager(); accountManager = new window.textsecure.AccountManager();
}); });
describe('#cleanSignedPreKeys', function() { describe('#cleanSignedPreKeys', () => {
let originalProtocolStorage; let originalProtocolStorage;
let signedPreKeys; let signedPreKeys;
const DAY = 1000 * 60 * 60 * 24; const DAY = 1000 * 60 * 60 * 24;
beforeEach(function() { beforeEach(() => {
originalProtocolStorage = window.textsecure.storage.protocol; originalProtocolStorage = window.textsecure.storage.protocol;
window.textsecure.storage.protocol = { window.textsecure.storage.protocol = {
loadSignedPreKeys: function() { loadSignedPreKeys() {
return Promise.resolve(signedPreKeys); return Promise.resolve(signedPreKeys);
}, },
}; };
}); });
afterEach(function() { afterEach(() => {
window.textsecure.storage.protocol = originalProtocolStorage; window.textsecure.storage.protocol = originalProtocolStorage;
}); });
it('keeps three confirmed keys even if over a week old', function() { it('keeps three confirmed keys even if over a week old', () => {
const now = Date.now(); const now = Date.now();
signedPreKeys = [ signedPreKeys = [
{ {
@ -48,7 +46,7 @@ describe('AccountManager', function() {
return accountManager.cleanSignedPreKeys(); return accountManager.cleanSignedPreKeys();
}); });
it('eliminates confirmed keys over a week old, if more than three', function() { it('eliminates confirmed keys over a week old, if more than three', () => {
const now = Date.now(); const now = Date.now();
signedPreKeys = [ signedPreKeys = [
{ {
@ -81,18 +79,18 @@ describe('AccountManager', function() {
let count = 0; let count = 0;
window.textsecure.storage.protocol.removeSignedPreKey = function(keyId) { window.textsecure.storage.protocol.removeSignedPreKey = function(keyId) {
if (keyId !== 1 && keyId !== 4) { if (keyId !== 1 && keyId !== 4) {
throw new Error('Wrong keys were eliminated! ' + keyId); throw new Error(`Wrong keys were eliminated! ${keyId}`);
} }
count++; count++;
}; };
return accountManager.cleanSignedPreKeys().then(function() { return accountManager.cleanSignedPreKeys().then(() => {
assert.strictEqual(count, 2); assert.strictEqual(count, 2);
}); });
}); });
it('keeps at least three unconfirmed keys if no confirmed', function() { it('keeps at least three unconfirmed keys if no confirmed', () => {
const now = Date.now(); const now = Date.now();
signedPreKeys = [ signedPreKeys = [
{ {
@ -116,18 +114,18 @@ describe('AccountManager', function() {
let count = 0; let count = 0;
window.textsecure.storage.protocol.removeSignedPreKey = function(keyId) { window.textsecure.storage.protocol.removeSignedPreKey = function(keyId) {
if (keyId !== 2) { if (keyId !== 2) {
throw new Error('Wrong keys were eliminated! ' + keyId); throw new Error(`Wrong keys were eliminated! ${keyId}`);
} }
count++; count++;
}; };
return accountManager.cleanSignedPreKeys().then(function() { return accountManager.cleanSignedPreKeys().then(() => {
assert.strictEqual(count, 1); assert.strictEqual(count, 1);
}); });
}); });
it('if some confirmed keys, keeps unconfirmed to addd up to three total', function() { it('if some confirmed keys, keeps unconfirmed to addd up to three total', () => {
const now = Date.now(); const now = Date.now();
signedPreKeys = [ signedPreKeys = [
{ {
@ -153,13 +151,13 @@ describe('AccountManager', function() {
let count = 0; let count = 0;
window.textsecure.storage.protocol.removeSignedPreKey = function(keyId) { window.textsecure.storage.protocol.removeSignedPreKey = function(keyId) {
if (keyId !== 3) { if (keyId !== 3) {
throw new Error('Wrong keys were eliminated! ' + keyId); throw new Error(`Wrong keys were eliminated! ${keyId}`);
} }
count++; count++;
}; };
return accountManager.cleanSignedPreKeys().then(function() { return accountManager.cleanSignedPreKeys().then(() => {
assert.strictEqual(count, 1); assert.strictEqual(count, 1);
}); });
}); });

View file

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

View file

@ -1,18 +1,18 @@
describe('encrypting and decrypting profile data', function() { describe('encrypting and decrypting profile data', () => {
var NAME_PADDED_LENGTH = 26; const NAME_PADDED_LENGTH = 26;
describe('encrypting and decrypting profile names', function() { describe('encrypting and decrypting profile names', () => {
it('pads, encrypts, decrypts, and unpads a short string', function() { it('pads, encrypts, decrypts, and unpads a short string', () => {
var name = 'Alice'; const name = 'Alice';
var buffer = dcodeIO.ByteBuffer.wrap(name).toArrayBuffer(); const buffer = dcodeIO.ByteBuffer.wrap(name).toArrayBuffer();
var key = libsignal.crypto.getRandomBytes(32); const key = libsignal.crypto.getRandomBytes(32);
return textsecure.crypto return textsecure.crypto
.encryptProfileName(buffer, key) .encryptProfileName(buffer, key)
.then(function(encrypted) { .then(encrypted => {
assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12); assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12);
return textsecure.crypto return textsecure.crypto
.decryptProfileName(encrypted, key) .decryptProfileName(encrypted, key)
.then(function(decrypted) { .then(decrypted => {
assert.strictEqual( assert.strictEqual(
dcodeIO.ByteBuffer.wrap(decrypted).toString('utf8'), dcodeIO.ByteBuffer.wrap(decrypted).toString('utf8'),
'Alice' 'Alice'
@ -20,17 +20,17 @@ describe('encrypting and decrypting profile data', function() {
}); });
}); });
}); });
it('works for empty string', function() { it('works for empty string', () => {
var name = dcodeIO.ByteBuffer.wrap('').toArrayBuffer(); const name = dcodeIO.ByteBuffer.wrap('').toArrayBuffer();
var key = libsignal.crypto.getRandomBytes(32); const key = libsignal.crypto.getRandomBytes(32);
return textsecure.crypto return textsecure.crypto
.encryptProfileName(name.buffer, key) .encryptProfileName(name.buffer, key)
.then(function(encrypted) { .then(encrypted => {
assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12); assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12);
return textsecure.crypto return textsecure.crypto
.decryptProfileName(encrypted, key) .decryptProfileName(encrypted, key)
.then(function(decrypted) { .then(decrypted => {
assert.strictEqual(decrypted.byteLength, 0); assert.strictEqual(decrypted.byteLength, 0);
assert.strictEqual( assert.strictEqual(
dcodeIO.ByteBuffer.wrap(decrypted).toString('utf8'), dcodeIO.ByteBuffer.wrap(decrypted).toString('utf8'),
@ -40,37 +40,37 @@ describe('encrypting and decrypting profile data', function() {
}); });
}); });
}); });
describe('encrypting and decrypting profile avatars', function() { describe('encrypting and decrypting profile avatars', () => {
it('encrypts and decrypts', function() { it('encrypts and decrypts', () => {
var buffer = dcodeIO.ByteBuffer.wrap('This is an avatar').toArrayBuffer(); const buffer = dcodeIO.ByteBuffer.wrap(
var key = libsignal.crypto.getRandomBytes(32); 'This is an avatar'
).toArrayBuffer();
const key = libsignal.crypto.getRandomBytes(32);
return textsecure.crypto return textsecure.crypto.encryptProfile(buffer, key).then(encrypted => {
.encryptProfile(buffer, key) assert(encrypted.byteLength === buffer.byteLength + 16 + 12);
.then(function(encrypted) { return textsecure.crypto
assert(encrypted.byteLength === buffer.byteLength + 16 + 12); .decryptProfile(encrypted, key)
return textsecure.crypto .then(decrypted => {
.decryptProfile(encrypted, key) assertEqualArrayBuffers(buffer, decrypted);
.then(function(decrypted) { });
assertEqualArrayBuffers(buffer, decrypted); });
});
});
}); });
it('throws when decrypting with the wrong key', function() { it('throws when decrypting with the wrong key', () => {
var buffer = dcodeIO.ByteBuffer.wrap('This is an avatar').toArrayBuffer(); const buffer = dcodeIO.ByteBuffer.wrap(
var key = libsignal.crypto.getRandomBytes(32); 'This is an avatar'
var bad_key = libsignal.crypto.getRandomBytes(32); ).toArrayBuffer();
const key = libsignal.crypto.getRandomBytes(32);
const bad_key = libsignal.crypto.getRandomBytes(32);
return textsecure.crypto return textsecure.crypto.encryptProfile(buffer, key).then(encrypted => {
.encryptProfile(buffer, key) assert(encrypted.byteLength === buffer.byteLength + 16 + 12);
.then(function(encrypted) { return textsecure.crypto
assert(encrypted.byteLength === buffer.byteLength + 16 + 12); .decryptProfile(encrypted, bad_key)
return textsecure.crypto .catch(error => {
.decryptProfile(encrypted, bad_key) assert.strictEqual(error.name, 'ProfileDecryptError');
.catch(function(error) { });
assert.strictEqual(error.name, 'ProfileDecryptError'); });
});
});
}); });
}); });
}); });

View file

@ -22,17 +22,18 @@ const fakeAPI = {
// sendMessages: fakeCall, // sendMessages: fakeCall,
setSignedPreKey: fakeCall, setSignedPreKey: fakeCall,
getKeysForNumber: function(number, deviceId) { getKeysForNumber(number, deviceId) {
var res = getKeysForNumberMap[number]; const res = getKeysForNumberMap[number];
if (res !== undefined) { if (res !== undefined) {
delete getKeysForNumberMap[number]; delete getKeysForNumberMap[number];
return Promise.resolve(res); return Promise.resolve(res);
} else throw new Error('getKeysForNumber of unknown/used number'); }
throw new Error('getKeysForNumber of unknown/used number');
}, },
sendMessages: function(destination, messageArray) { sendMessages(destination, messageArray) {
for (i in messageArray) { for (i in messageArray) {
var msg = messageArray[i]; const msg = messageArray[i];
if ( if (
(msg.type != 1 && msg.type != 3) || (msg.type != 1 && msg.type != 3) ||
msg.destinationDeviceId === undefined || msg.destinationDeviceId === undefined ||
@ -45,7 +46,7 @@ const fakeAPI = {
throw new Error('Invalid message'); throw new Error('Invalid message');
messagesSentMap[ messagesSentMap[
destination + '.' + messageArray[i].destinationDeviceId `${destination}.${messageArray[i].destinationDeviceId}`
] = msg; ] = msg;
} }
}, },

View file

@ -1,7 +1,5 @@
'use strict';
describe('Key generation', function() { describe('Key generation', function() {
var count = 10; const count = 10;
this.timeout(count * 2000); this.timeout(count * 2000);
function validateStoredKeyPair(keyPair) { function validateStoredKeyPair(keyPair) {
@ -14,49 +12,41 @@ describe('Key generation', function() {
assert.strictEqual(keyPair.privKey.byteLength, 32); assert.strictEqual(keyPair.privKey.byteLength, 32);
} }
function itStoresPreKey(keyId) { function itStoresPreKey(keyId) {
it('prekey ' + keyId + ' is valid', function() { it(`prekey ${keyId} is valid`, () =>
return textsecure.storage.protocol textsecure.storage.protocol.loadPreKey(keyId).then(keyPair => {
.loadPreKey(keyId) validateStoredKeyPair(keyPair);
.then(function(keyPair) { }));
validateStoredKeyPair(keyPair);
});
});
} }
function itStoresSignedPreKey(keyId) { function itStoresSignedPreKey(keyId) {
it('signed prekey ' + keyId + ' is valid', function() { it(`signed prekey ${keyId} is valid`, () =>
return textsecure.storage.protocol textsecure.storage.protocol.loadSignedPreKey(keyId).then(keyPair => {
.loadSignedPreKey(keyId) validateStoredKeyPair(keyPair);
.then(function(keyPair) { }));
validateStoredKeyPair(keyPair);
});
});
} }
function validateResultKey(resultKey) { function validateResultKey(resultKey) {
return textsecure.storage.protocol return textsecure.storage.protocol
.loadPreKey(resultKey.keyId) .loadPreKey(resultKey.keyId)
.then(function(keyPair) { .then(keyPair => {
assertEqualArrayBuffers(resultKey.publicKey, keyPair.pubKey); assertEqualArrayBuffers(resultKey.publicKey, keyPair.pubKey);
}); });
} }
function validateResultSignedKey(resultSignedKey) { function validateResultSignedKey(resultSignedKey) {
return textsecure.storage.protocol return textsecure.storage.protocol
.loadSignedPreKey(resultSignedKey.keyId) .loadSignedPreKey(resultSignedKey.keyId)
.then(function(keyPair) { .then(keyPair => {
assertEqualArrayBuffers(resultSignedKey.publicKey, keyPair.pubKey); assertEqualArrayBuffers(resultSignedKey.publicKey, keyPair.pubKey);
}); });
} }
before(function() { before(() => {
localStorage.clear(); localStorage.clear();
return libsignal.KeyHelper.generateIdentityKeyPair().then(function( return libsignal.KeyHelper.generateIdentityKeyPair().then(keyPair =>
keyPair textsecure.storage.protocol.put('identityKey', keyPair)
) { );
return textsecure.storage.protocol.put('identityKey', keyPair);
});
}); });
describe('the first time', function() { describe('the first time', () => {
var result; let result;
/* result should have this format /* result should have this format
* { * {
* preKeys: [ { keyId, publicKey }, ... ], * preKeys: [ { keyId, publicKey }, ... ],
@ -64,101 +54,98 @@ describe('Key generation', function() {
* identityKey: <ArrayBuffer> * identityKey: <ArrayBuffer>
* } * }
*/ */
before(function() { before(() => {
var accountManager = new textsecure.AccountManager(''); const accountManager = new textsecure.AccountManager('');
return accountManager.generateKeys(count).then(function(res) { return accountManager.generateKeys(count).then(res => {
result = res; result = res;
}); });
}); });
for (var i = 1; i <= count; i++) { for (let i = 1; i <= count; i++) {
itStoresPreKey(i); itStoresPreKey(i);
} }
itStoresSignedPreKey(1); itStoresSignedPreKey(1);
it('result contains ' + count + ' preKeys', function() { it(`result contains ${count} preKeys`, () => {
assert.isArray(result.preKeys); assert.isArray(result.preKeys);
assert.lengthOf(result.preKeys, count); assert.lengthOf(result.preKeys, count);
for (var i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
assert.isObject(result.preKeys[i]); assert.isObject(result.preKeys[i]);
} }
}); });
it('result contains the correct keyIds', function() { it('result contains the correct keyIds', () => {
for (var i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
assert.strictEqual(result.preKeys[i].keyId, i + 1); assert.strictEqual(result.preKeys[i].keyId, i + 1);
} }
}); });
it('result contains the correct public keys', function() { it('result contains the correct public keys', () =>
return Promise.all(result.preKeys.map(validateResultKey)); Promise.all(result.preKeys.map(validateResultKey)));
}); it('returns a signed prekey', () => {
it('returns a signed prekey', function() {
assert.strictEqual(result.signedPreKey.keyId, 1); assert.strictEqual(result.signedPreKey.keyId, 1);
assert.instanceOf(result.signedPreKey.signature, ArrayBuffer); assert.instanceOf(result.signedPreKey.signature, ArrayBuffer);
return validateResultSignedKey(result.signedPreKey); return validateResultSignedKey(result.signedPreKey);
}); });
}); });
describe('the second time', function() { describe('the second time', () => {
var result; let result;
before(function() { before(() => {
var accountManager = new textsecure.AccountManager(''); const accountManager = new textsecure.AccountManager('');
return accountManager.generateKeys(count).then(function(res) { return accountManager.generateKeys(count).then(res => {
result = res; result = res;
}); });
}); });
for (var i = 1; i <= 2 * count; i++) { for (let i = 1; i <= 2 * count; i++) {
itStoresPreKey(i); itStoresPreKey(i);
} }
itStoresSignedPreKey(1); itStoresSignedPreKey(1);
itStoresSignedPreKey(2); itStoresSignedPreKey(2);
it('result contains ' + count + ' preKeys', function() { it(`result contains ${count} preKeys`, () => {
assert.isArray(result.preKeys); assert.isArray(result.preKeys);
assert.lengthOf(result.preKeys, count); assert.lengthOf(result.preKeys, count);
for (var i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
assert.isObject(result.preKeys[i]); assert.isObject(result.preKeys[i]);
} }
}); });
it('result contains the correct keyIds', function() { it('result contains the correct keyIds', () => {
for (var i = 1; i <= count; i++) { for (let i = 1; i <= count; i++) {
assert.strictEqual(result.preKeys[i - 1].keyId, i + count); assert.strictEqual(result.preKeys[i - 1].keyId, i + count);
} }
}); });
it('result contains the correct public keys', function() { it('result contains the correct public keys', () =>
return Promise.all(result.preKeys.map(validateResultKey)); Promise.all(result.preKeys.map(validateResultKey)));
}); it('returns a signed prekey', () => {
it('returns a signed prekey', function() {
assert.strictEqual(result.signedPreKey.keyId, 2); assert.strictEqual(result.signedPreKey.keyId, 2);
assert.instanceOf(result.signedPreKey.signature, ArrayBuffer); assert.instanceOf(result.signedPreKey.signature, ArrayBuffer);
return validateResultSignedKey(result.signedPreKey); return validateResultSignedKey(result.signedPreKey);
}); });
}); });
describe('the third time', function() { describe('the third time', () => {
var result; let result;
before(function() { before(() => {
var accountManager = new textsecure.AccountManager(''); const accountManager = new textsecure.AccountManager('');
return accountManager.generateKeys(count).then(function(res) { return accountManager.generateKeys(count).then(res => {
result = res; result = res;
}); });
}); });
for (var i = 1; i <= 3 * count; i++) { for (let i = 1; i <= 3 * count; i++) {
itStoresPreKey(i); itStoresPreKey(i);
} }
itStoresSignedPreKey(2); itStoresSignedPreKey(2);
itStoresSignedPreKey(3); itStoresSignedPreKey(3);
it('result contains ' + count + ' preKeys', function() { it(`result contains ${count} preKeys`, () => {
assert.isArray(result.preKeys); assert.isArray(result.preKeys);
assert.lengthOf(result.preKeys, count); assert.lengthOf(result.preKeys, count);
for (var i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
assert.isObject(result.preKeys[i]); assert.isObject(result.preKeys[i]);
} }
}); });
it('result contains the correct keyIds', function() { it('result contains the correct keyIds', () => {
for (var i = 1; i <= count; i++) { for (let i = 1; i <= count; i++) {
assert.strictEqual(result.preKeys[i - 1].keyId, i + 2 * count); assert.strictEqual(result.preKeys[i - 1].keyId, i + 2 * count);
} }
}); });
it('result contains the correct public keys', function() { it('result contains the correct public keys', () =>
return Promise.all(result.preKeys.map(validateResultKey)); Promise.all(result.preKeys.map(validateResultKey)));
}); it('result contains a signed prekey', () => {
it('result contains a signed prekey', function() {
assert.strictEqual(result.signedPreKey.keyId, 3); assert.strictEqual(result.signedPreKey.keyId, 3);
assert.instanceOf(result.signedPreKey.signature, ArrayBuffer); assert.instanceOf(result.signedPreKey.signature, ArrayBuffer);
return validateResultSignedKey(result.signedPreKey); return validateResultSignedKey(result.signedPreKey);

View file

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

View file

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

View file

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

View file

@ -1,13 +1,12 @@
'use strict'; describe('Protocol', () => {
describe('Protocol', function() { describe('Unencrypted PushMessageProto "decrypt"', () => {
describe('Unencrypted PushMessageProto "decrypt"', function() { // exclusive
//exclusive it('works', done => {
it('works', function(done) {
localStorage.clear(); localStorage.clear();
var text_message = new textsecure.protobuf.DataMessage(); const text_message = new textsecure.protobuf.DataMessage();
text_message.body = 'Hi Mom'; text_message.body = 'Hi Mom';
var server_message = { const server_message = {
type: 4, // unencrypted type: 4, // unencrypted
source: '+19999999999', source: '+19999999999',
timestamp: 42, timestamp: 42,
@ -21,7 +20,7 @@ describe('Protocol', function() {
server_message.type, server_message.type,
server_message.message server_message.message
) )
.then(function(message) { .then(message => {
assert.equal(message.body, text_message.body); assert.equal(message.body, text_message.body);
assert.equal( assert.equal(
message.attachments.length, message.attachments.length,

View file

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

View file

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

View file

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

View file

@ -1,14 +1,12 @@
(function() { (function() {
'use strict'; describe('WebSocket-Resource', () => {
describe('requests and responses', () => {
describe('WebSocket-Resource', function() { it('receives requests and sends responses', done => {
describe('requests and responses', function() {
it('receives requests and sends responses', function(done) {
// mock socket // mock socket
var request_id = '1'; const request_id = '1';
var socket = { const socket = {
send: function(data) { send(data) {
var message = textsecure.protobuf.WebSocketMessage.decode(data); const message = textsecure.protobuf.WebSocketMessage.decode(data);
assert.strictEqual( assert.strictEqual(
message.type, message.type,
textsecure.protobuf.WebSocketMessage.Type.RESPONSE textsecure.protobuf.WebSocketMessage.Type.RESPONSE
@ -18,12 +16,12 @@
assert.strictEqual(message.response.id.toString(), request_id); assert.strictEqual(message.response.id.toString(), request_id);
done(); done();
}, },
addEventListener: function() {}, addEventListener() {},
}; };
// actual test // actual test
var resource = new WebSocketResource(socket, { const resource = new WebSocketResource(socket, {
handleRequest: function(request) { handleRequest(request) {
assert.strictEqual(request.verb, 'PUT'); assert.strictEqual(request.verb, 'PUT');
assert.strictEqual(request.path, '/some/path'); assert.strictEqual(request.path, '/some/path');
assertEqualArrayBuffers( assertEqualArrayBuffers(
@ -52,12 +50,12 @@
}); });
}); });
it('sends requests and receives responses', function(done) { it('sends requests and receives responses', done => {
// mock socket and request handler // mock socket and request handler
var request_id; let request_id;
var socket = { const socket = {
send: function(data) { send(data) {
var message = textsecure.protobuf.WebSocketMessage.decode(data); const message = textsecure.protobuf.WebSocketMessage.decode(data);
assert.strictEqual( assert.strictEqual(
message.type, message.type,
textsecure.protobuf.WebSocketMessage.Type.REQUEST textsecure.protobuf.WebSocketMessage.Type.REQUEST
@ -70,17 +68,17 @@
); );
request_id = message.request.id; request_id = message.request.id;
}, },
addEventListener: function() {}, addEventListener() {},
}; };
// actual test // actual test
var resource = new WebSocketResource(socket); const resource = new WebSocketResource(socket);
resource.sendRequest({ resource.sendRequest({
verb: 'PUT', verb: 'PUT',
path: '/some/path', path: '/some/path',
body: new Uint8Array([1, 2, 3]).buffer, body: new Uint8Array([1, 2, 3]).buffer,
error: done, error: done,
success: function(message, status, request) { success(message, status, request) {
assert.strictEqual(message, 'OK'); assert.strictEqual(message, 'OK');
assert.strictEqual(status, 200); assert.strictEqual(status, 200);
done(); done();
@ -101,19 +99,19 @@
}); });
}); });
describe('close', function() { describe('close', () => {
before(function() { before(() => {
window.WebSocket = MockSocket; window.WebSocket = MockSocket;
}); });
after(function() { after(() => {
window.WebSocket = WebSocket; window.WebSocket = WebSocket;
}); });
it('closes the connection', function(done) { it('closes the connection', done => {
var mockServer = new MockServer('ws://localhost:8081'); const mockServer = new MockServer('ws://localhost:8081');
mockServer.on('connection', function(server) { mockServer.on('connection', server => {
server.on('close', done); server.on('close', done);
}); });
var resource = new WebSocketResource( const resource = new WebSocketResource(
new WebSocket('ws://localhost:8081') new WebSocket('ws://localhost:8081')
); );
resource.close(); resource.close();
@ -121,18 +119,18 @@
}); });
describe.skip('with a keepalive config', function() { describe.skip('with a keepalive config', function() {
before(function() { before(() => {
window.WebSocket = MockSocket; window.WebSocket = MockSocket;
}); });
after(function() { after(() => {
window.WebSocket = WebSocket; window.WebSocket = WebSocket;
}); });
this.timeout(60000); this.timeout(60000);
it('sends keepalives once a minute', function(done) { it('sends keepalives once a minute', done => {
var mockServer = new MockServer('ws://localhost:8081'); const mockServer = new MockServer('ws://localhost:8081');
mockServer.on('connection', function(server) { mockServer.on('connection', server => {
server.on('message', function(data) { server.on('message', data => {
var message = textsecure.protobuf.WebSocketMessage.decode(data); const message = textsecure.protobuf.WebSocketMessage.decode(data);
assert.strictEqual( assert.strictEqual(
message.type, message.type,
textsecure.protobuf.WebSocketMessage.Type.REQUEST textsecure.protobuf.WebSocketMessage.Type.REQUEST
@ -148,11 +146,11 @@
}); });
}); });
it('uses / as a default path', function(done) { it('uses / as a default path', done => {
var mockServer = new MockServer('ws://localhost:8081'); const mockServer = new MockServer('ws://localhost:8081');
mockServer.on('connection', function(server) { mockServer.on('connection', server => {
server.on('message', function(data) { server.on('message', data => {
var message = textsecure.protobuf.WebSocketMessage.decode(data); const message = textsecure.protobuf.WebSocketMessage.decode(data);
assert.strictEqual( assert.strictEqual(
message.type, message.type,
textsecure.protobuf.WebSocketMessage.Type.REQUEST textsecure.protobuf.WebSocketMessage.Type.REQUEST
@ -170,9 +168,9 @@
it('optionally disconnects if no response', function(done) { it('optionally disconnects if no response', function(done) {
this.timeout(65000); this.timeout(65000);
var mockServer = new MockServer('ws://localhost:8081'); const mockServer = new MockServer('ws://localhost:8081');
var socket = new WebSocket('ws://localhost:8081'); const socket = new WebSocket('ws://localhost:8081');
mockServer.on('connection', function(server) { mockServer.on('connection', server => {
server.on('close', done); server.on('close', done);
}); });
new WebSocketResource(socket, { keepalive: true }); new WebSocketResource(socket, { keepalive: true });
@ -180,12 +178,12 @@
it('allows resetting the keepalive timer', function(done) { it('allows resetting the keepalive timer', function(done) {
this.timeout(65000); this.timeout(65000);
var mockServer = new MockServer('ws://localhost:8081'); const mockServer = new MockServer('ws://localhost:8081');
var socket = new WebSocket('ws://localhost:8081'); const socket = new WebSocket('ws://localhost:8081');
var startTime = Date.now(); const startTime = Date.now();
mockServer.on('connection', function(server) { mockServer.on('connection', server => {
server.on('message', function(data) { server.on('message', data => {
var message = textsecure.protobuf.WebSocketMessage.decode(data); const message = textsecure.protobuf.WebSocketMessage.decode(data);
assert.strictEqual( assert.strictEqual(
message.type, message.type,
textsecure.protobuf.WebSocketMessage.Type.REQUEST textsecure.protobuf.WebSocketMessage.Type.REQUEST
@ -200,8 +198,8 @@
done(); done();
}); });
}); });
var resource = new WebSocketResource(socket, { keepalive: true }); const resource = new WebSocketResource(socket, { keepalive: true });
setTimeout(function() { setTimeout(() => {
resource.resetKeepAliveTimer(); resource.resetKeepAliveTimer();
}, 5000); }, 5000);
}); });

View file

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

View file

@ -1,6 +1,7 @@
(function() { /* global window, dcodeIO, Event, textsecure, FileReader, WebSocketResource */
'use strict';
// eslint-disable-next-line func-names
(function() {
/* /*
* WebSocket-Resources * WebSocket-Resources
* *
@ -23,7 +24,7 @@
* *
*/ */
var Request = function(options) { const Request = function Request(options) {
this.verb = options.verb || options.type; this.verb = options.verb || options.type;
this.path = options.path || options.url; this.path = options.path || options.url;
this.body = options.body || options.data; this.body = options.body || options.data;
@ -32,7 +33,7 @@
this.id = options.id; this.id = options.id;
if (this.id === undefined) { if (this.id === undefined) {
var bits = new Uint32Array(2); const bits = new Uint32Array(2);
window.crypto.getRandomValues(bits); window.crypto.getRandomValues(bits);
this.id = dcodeIO.Long.fromBits(bits[0], bits[1], true); this.id = dcodeIO.Long.fromBits(bits[0], bits[1], true);
} }
@ -42,19 +43,19 @@
} }
}; };
var IncomingWebSocketRequest = function(options) { const IncomingWebSocketRequest = function IncomingWebSocketRequest(options) {
var request = new Request(options); const request = new Request(options);
var socket = options.socket; const { socket } = options;
this.verb = request.verb; this.verb = request.verb;
this.path = request.path; this.path = request.path;
this.body = request.body; this.body = request.body;
this.respond = function(status, message) { this.respond = (status, message) => {
socket.send( socket.send(
new textsecure.protobuf.WebSocketMessage({ new textsecure.protobuf.WebSocketMessage({
type: textsecure.protobuf.WebSocketMessage.Type.RESPONSE, type: textsecure.protobuf.WebSocketMessage.Type.RESPONSE,
response: { id: request.id, message: message, status: status }, response: { id: request.id, message, status },
}) })
.encode() .encode()
.toArrayBuffer() .toArrayBuffer()
@ -62,9 +63,12 @@
}; };
}; };
var outgoing = {}; const outgoing = {};
var OutgoingWebSocketRequest = function(options, socket) { const OutgoingWebSocketRequest = function OutgoingWebSocketRequest(
var request = new Request(options); options,
socket
) {
const request = new Request(options);
outgoing[request.id] = request; outgoing[request.id] = request;
socket.send( socket.send(
new textsecure.protobuf.WebSocketMessage({ new textsecure.protobuf.WebSocketMessage({
@ -81,22 +85,18 @@
); );
}; };
window.WebSocketResource = function(socket, opts) { window.WebSocketResource = function WebSocketResource(socket, opts = {}) {
opts = opts || {}; let { handleRequest } = opts;
var handleRequest = opts.handleRequest;
if (typeof handleRequest !== 'function') { if (typeof handleRequest !== 'function') {
handleRequest = function(request) { handleRequest = request => request.respond(404, 'Not found');
request.respond(404, 'Not found');
};
} }
this.sendRequest = function(options) { this.sendRequest = options => new OutgoingWebSocketRequest(options, socket);
return new OutgoingWebSocketRequest(options, socket);
};
socket.onmessage = function(socketMessage) { // eslint-disable-next-line no-param-reassign
var blob = socketMessage.data; socket.onmessage = socketMessage => {
var handleArrayBuffer = function(buffer) { const blob = socketMessage.data;
var message = textsecure.protobuf.WebSocketMessage.decode(buffer); const handleArrayBuffer = buffer => {
const message = textsecure.protobuf.WebSocketMessage.decode(buffer);
if ( if (
message.type === textsecure.protobuf.WebSocketMessage.Type.REQUEST message.type === textsecure.protobuf.WebSocketMessage.Type.REQUEST
) { ) {
@ -106,17 +106,17 @@
path: message.request.path, path: message.request.path,
body: message.request.body, body: message.request.body,
id: message.request.id, id: message.request.id,
socket: socket, socket,
}) })
); );
} else if ( } else if (
message.type === textsecure.protobuf.WebSocketMessage.Type.RESPONSE message.type === textsecure.protobuf.WebSocketMessage.Type.RESPONSE
) { ) {
var response = message.response; const { response } = message;
var request = outgoing[response.id]; const request = outgoing[response.id];
if (request) { if (request) {
request.response = response; request.response = response;
var callback = request.error; let callback = request.error;
if (response.status >= 200 && response.status < 300) { if (response.status >= 200 && response.status < 300) {
callback = request.success; callback = request.success;
} }
@ -125,8 +125,9 @@
callback(response.message, response.status, request); callback(response.message, response.status, request);
} }
} else { } else {
throw 'Received response for unknown request ' + throw new Error(
message.response.id; `Received response for unknown request ${message.response.id}`
);
} }
} }
}; };
@ -134,10 +135,8 @@
if (blob instanceof ArrayBuffer) { if (blob instanceof ArrayBuffer) {
handleArrayBuffer(blob); handleArrayBuffer(blob);
} else { } else {
var reader = new FileReader(); const reader = new FileReader();
reader.onload = function() { reader.onload = () => handleArrayBuffer(reader.result);
handleArrayBuffer(reader.result);
};
reader.readAsArrayBuffer(blob); reader.readAsArrayBuffer(blob);
} }
}; };
@ -147,7 +146,7 @@
path: opts.keepalive.path, path: opts.keepalive.path,
disconnect: opts.keepalive.disconnect, disconnect: opts.keepalive.disconnect,
}); });
var resetKeepAliveTimer = this.keepalive.reset.bind(this.keepalive); const resetKeepAliveTimer = this.keepalive.reset.bind(this.keepalive);
socket.addEventListener('open', resetKeepAliveTimer); socket.addEventListener('open', resetKeepAliveTimer);
socket.addEventListener('message', resetKeepAliveTimer); socket.addEventListener('message', resetKeepAliveTimer);
socket.addEventListener( socket.addEventListener(
@ -156,54 +155,45 @@
); );
} }
socket.addEventListener( socket.addEventListener('close', () => {
'close', this.closed = true;
function() { });
this.closed = true;
}.bind(this)
);
this.close = function(code, reason) { this.close = (code = 3000, reason) => {
if (this.closed) { if (this.closed) {
return; return;
} }
window.log.info('WebSocketResource.close()'); window.log.info('WebSocketResource.close()');
if (!code) {
code = 3000;
}
if (this.keepalive) { if (this.keepalive) {
this.keepalive.stop(); this.keepalive.stop();
} }
socket.close(code, reason); socket.close(code, reason);
// eslint-disable-next-line no-param-reassign
socket.onmessage = null; socket.onmessage = null;
// On linux the socket can wait a long time to emit its close event if we've // On linux the socket can wait a long time to emit its close event if we've
// lost the internet connection. On the order of minutes. This speeds that // lost the internet connection. On the order of minutes. This speeds that
// process up. // process up.
setTimeout( setTimeout(() => {
function() { if (this.closed) {
if (this.closed) { return;
return; }
} this.closed = true;
this.closed = true;
window.log.warn('Dispatching our own socket close event'); window.log.warn('Dispatching our own socket close event');
var ev = new Event('close'); const ev = new Event('close');
ev.code = code; ev.code = code;
ev.reason = reason; ev.reason = reason;
this.dispatchEvent(ev); this.dispatchEvent(ev);
}.bind(this), }, 1000);
1000
);
}; };
}; };
window.WebSocketResource.prototype = new textsecure.EventTarget(); window.WebSocketResource.prototype = new textsecure.EventTarget();
function KeepAlive(websocketResource, opts) { function KeepAlive(websocketResource, opts = {}) {
if (websocketResource instanceof WebSocketResource) { if (websocketResource instanceof WebSocketResource) {
opts = opts || {};
this.path = opts.path; this.path = opts.path;
if (this.path === undefined) { if (this.path === undefined) {
this.path = '/'; this.path = '/';
@ -220,36 +210,30 @@
KeepAlive.prototype = { KeepAlive.prototype = {
constructor: KeepAlive, constructor: KeepAlive,
stop: function() { stop() {
clearTimeout(this.keepAliveTimer); clearTimeout(this.keepAliveTimer);
clearTimeout(this.disconnectTimer); clearTimeout(this.disconnectTimer);
}, },
reset: function() { reset() {
clearTimeout(this.keepAliveTimer); clearTimeout(this.keepAliveTimer);
clearTimeout(this.disconnectTimer); clearTimeout(this.disconnectTimer);
this.keepAliveTimer = setTimeout( this.keepAliveTimer = setTimeout(() => {
function() { if (this.disconnect) {
if (this.disconnect) { // automatically disconnect if server doesn't ack
// automatically disconnect if server doesn't ack this.disconnectTimer = setTimeout(() => {
this.disconnectTimer = setTimeout( clearTimeout(this.keepAliveTimer);
function() { this.wsr.close(3001, 'No response to keepalive request');
clearTimeout(this.keepAliveTimer); }, 1000);
this.wsr.close(3001, 'No response to keepalive request'); } else {
}.bind(this), this.reset();
1000 }
); window.log.info('Sending a keepalive message');
} else { this.wsr.sendRequest({
this.reset(); verb: 'GET',
} path: this.path,
window.log.info('Sending a keepalive message'); success: this.reset.bind(this),
this.wsr.sendRequest({ });
verb: 'GET', }, 55000);
path: this.path,
success: this.reset.bind(this),
});
}.bind(this),
55000
);
}, },
}; };
})(); })();