Moves libtextsecure to Typescript
* Starting to work through lint errors * libsignal-protocol: Update changes for primary repo compatibility * Step 1: task_with_timeout rename * Step 2: Apply the changes to TaskWithTimeout.ts * Step 1: All to-be-converted libtextsecure/*.js files moved * Step 2: No Typescript errors! * Get libtextsecure tests passing again * TSLint errors down to 1 * Compilation succeeds, no lint errors or test failures * WebSocketResources - update import for case-sensitive filesystems * Fixes for lint-deps * Remove unnecessary @ts-ignore * Fix inability to message your own contact after link * Add log message for the end of migration 20 * lint fix
This commit is contained in:
parent
2f2d027161
commit
b7d56def82
45 changed files with 5983 additions and 4042 deletions
|
@ -1,78 +0,0 @@
|
|||
/* global libsignal, textsecure */
|
||||
|
||||
/* eslint-disable more/no-then */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
function ProvisioningCipher() {}
|
||||
|
||||
ProvisioningCipher.prototype = {
|
||||
decrypt(provisionEnvelope) {
|
||||
const masterEphemeral = provisionEnvelope.publicKey.toArrayBuffer();
|
||||
const message = provisionEnvelope.body.toArrayBuffer();
|
||||
if (new Uint8Array(message)[0] !== 1) {
|
||||
throw new Error('Bad version number on ProvisioningMessage');
|
||||
}
|
||||
|
||||
const iv = message.slice(1, 16 + 1);
|
||||
const mac = message.slice(message.byteLength - 32, message.byteLength);
|
||||
const ivAndCiphertext = message.slice(0, message.byteLength - 32);
|
||||
const ciphertext = message.slice(16 + 1, message.byteLength - 32);
|
||||
|
||||
return libsignal.Curve.async
|
||||
.calculateAgreement(masterEphemeral, this.keyPair.privKey)
|
||||
.then(ecRes =>
|
||||
libsignal.HKDF.deriveSecrets(
|
||||
ecRes,
|
||||
new ArrayBuffer(32),
|
||||
'TextSecure Provisioning Message'
|
||||
)
|
||||
)
|
||||
.then(keys =>
|
||||
libsignal.crypto
|
||||
.verifyMAC(ivAndCiphertext, keys[1], mac, 32)
|
||||
.then(() => libsignal.crypto.decrypt(keys[0], ciphertext, iv))
|
||||
)
|
||||
.then(plaintext => {
|
||||
const provisionMessage = textsecure.protobuf.ProvisionMessage.decode(
|
||||
plaintext
|
||||
);
|
||||
const privKey = provisionMessage.identityKeyPrivate.toArrayBuffer();
|
||||
|
||||
return libsignal.Curve.async.createKeyPair(privKey).then(keyPair => {
|
||||
const ret = {
|
||||
identityKeyPair: keyPair,
|
||||
number: provisionMessage.number,
|
||||
provisioningCode: provisionMessage.provisioningCode,
|
||||
userAgent: provisionMessage.userAgent,
|
||||
readReceipts: provisionMessage.readReceipts,
|
||||
};
|
||||
if (provisionMessage.profileKey) {
|
||||
ret.profileKey = provisionMessage.profileKey.toArrayBuffer();
|
||||
}
|
||||
return ret;
|
||||
});
|
||||
});
|
||||
},
|
||||
getPublicKey() {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
if (!this.keyPair) {
|
||||
return libsignal.Curve.async.generateKeyPair().then(keyPair => {
|
||||
this.keyPair = keyPair;
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.then(() => this.keyPair.pubKey);
|
||||
},
|
||||
};
|
||||
|
||||
libsignal.ProvisioningCipher = function ProvisioningCipherWrapper() {
|
||||
const cipher = new ProvisioningCipher();
|
||||
|
||||
this.decrypt = cipher.decrypt.bind(cipher);
|
||||
this.getPublicKey = cipher.getPublicKey.bind(cipher);
|
||||
};
|
||||
})();
|
|
@ -1,623 +0,0 @@
|
|||
/* global
|
||||
window,
|
||||
textsecure,
|
||||
libsignal,
|
||||
WebSocketResource,
|
||||
btoa,
|
||||
Signal,
|
||||
getString,
|
||||
libphonenumber,
|
||||
Event,
|
||||
ConversationController
|
||||
*/
|
||||
|
||||
/* eslint-disable more/no-then */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
window.textsecure = window.textsecure || {};
|
||||
|
||||
const ARCHIVE_AGE = 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
function AccountManager(username, password) {
|
||||
this.server = window.WebAPI.connect({ username, password });
|
||||
this.pending = Promise.resolve();
|
||||
}
|
||||
|
||||
function getIdentifier(id) {
|
||||
if (!id || !id.length) {
|
||||
return id;
|
||||
}
|
||||
|
||||
const parts = id.split('.');
|
||||
if (!parts.length) {
|
||||
return id;
|
||||
}
|
||||
|
||||
return parts[0];
|
||||
}
|
||||
|
||||
AccountManager.prototype = new textsecure.EventTarget();
|
||||
AccountManager.prototype.extend({
|
||||
constructor: AccountManager,
|
||||
requestVoiceVerification(number) {
|
||||
return this.server.requestVerificationVoice(number);
|
||||
},
|
||||
requestSMSVerification(number) {
|
||||
return this.server.requestVerificationSMS(number);
|
||||
},
|
||||
async encryptDeviceName(name, providedIdentityKey) {
|
||||
if (!name) {
|
||||
return null;
|
||||
}
|
||||
const identityKey =
|
||||
providedIdentityKey ||
|
||||
(await textsecure.storage.protocol.getIdentityKeyPair());
|
||||
if (!identityKey) {
|
||||
throw new Error(
|
||||
'Identity key was not provided and is not in database!'
|
||||
);
|
||||
}
|
||||
const encrypted = await Signal.Crypto.encryptDeviceName(
|
||||
name,
|
||||
identityKey.pubKey
|
||||
);
|
||||
|
||||
const proto = new textsecure.protobuf.DeviceName();
|
||||
proto.ephemeralPublic = encrypted.ephemeralPublic;
|
||||
proto.syntheticIv = encrypted.syntheticIv;
|
||||
proto.ciphertext = encrypted.ciphertext;
|
||||
|
||||
const arrayBuffer = proto.encode().toArrayBuffer();
|
||||
return Signal.Crypto.arrayBufferToBase64(arrayBuffer);
|
||||
},
|
||||
async decryptDeviceName(base64) {
|
||||
const identityKey = await textsecure.storage.protocol.getIdentityKeyPair();
|
||||
|
||||
const arrayBuffer = Signal.Crypto.base64ToArrayBuffer(base64);
|
||||
const proto = textsecure.protobuf.DeviceName.decode(arrayBuffer);
|
||||
const encrypted = {
|
||||
ephemeralPublic: proto.ephemeralPublic.toArrayBuffer(),
|
||||
syntheticIv: proto.syntheticIv.toArrayBuffer(),
|
||||
ciphertext: proto.ciphertext.toArrayBuffer(),
|
||||
};
|
||||
|
||||
const name = await Signal.Crypto.decryptDeviceName(
|
||||
encrypted,
|
||||
identityKey.privKey
|
||||
);
|
||||
|
||||
return name;
|
||||
},
|
||||
async maybeUpdateDeviceName() {
|
||||
const isNameEncrypted = textsecure.storage.user.getDeviceNameEncrypted();
|
||||
if (isNameEncrypted) {
|
||||
return;
|
||||
}
|
||||
const deviceName = await textsecure.storage.user.getDeviceName();
|
||||
const base64 = await this.encryptDeviceName(deviceName);
|
||||
|
||||
await this.server.updateDeviceName(base64);
|
||||
},
|
||||
async deviceNameIsEncrypted() {
|
||||
await textsecure.storage.user.setDeviceNameEncrypted();
|
||||
},
|
||||
async maybeDeleteSignalingKey() {
|
||||
const key = await textsecure.storage.user.getSignalingKey();
|
||||
if (key) {
|
||||
await this.server.removeSignalingKey();
|
||||
}
|
||||
},
|
||||
registerSingleDevice(number, verificationCode) {
|
||||
const registerKeys = this.server.registerKeys.bind(this.server);
|
||||
const createAccount = this.createAccount.bind(this);
|
||||
const clearSessionsAndPreKeys = this.clearSessionsAndPreKeys.bind(this);
|
||||
const generateKeys = this.generateKeys.bind(this, 100);
|
||||
const confirmKeys = this.confirmKeys.bind(this);
|
||||
const registrationDone = this.registrationDone.bind(this);
|
||||
return this.queueTask(() =>
|
||||
libsignal.KeyHelper.generateIdentityKeyPair().then(
|
||||
async identityKeyPair => {
|
||||
const profileKey = textsecure.crypto.getRandomBytes(32);
|
||||
const accessKey = await window.Signal.Crypto.deriveAccessKey(
|
||||
profileKey
|
||||
);
|
||||
|
||||
return createAccount(
|
||||
number,
|
||||
verificationCode,
|
||||
identityKeyPair,
|
||||
profileKey,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
{ accessKey }
|
||||
)
|
||||
.then(clearSessionsAndPreKeys)
|
||||
.then(generateKeys)
|
||||
.then(keys => registerKeys(keys).then(() => confirmKeys(keys)))
|
||||
.then(() => registrationDone({ number }));
|
||||
}
|
||||
)
|
||||
);
|
||||
},
|
||||
registerSecondDevice(setProvisioningUrl, confirmNumber, progressCallback) {
|
||||
const createAccount = this.createAccount.bind(this);
|
||||
const clearSessionsAndPreKeys = this.clearSessionsAndPreKeys.bind(this);
|
||||
const generateKeys = this.generateKeys.bind(this, 100, progressCallback);
|
||||
const confirmKeys = this.confirmKeys.bind(this);
|
||||
const registrationDone = this.registrationDone.bind(this);
|
||||
const registerKeys = this.server.registerKeys.bind(this.server);
|
||||
const getSocket = this.server.getProvisioningSocket.bind(this.server);
|
||||
const queueTask = this.queueTask.bind(this);
|
||||
const provisioningCipher = new libsignal.ProvisioningCipher();
|
||||
let gotProvisionEnvelope = false;
|
||||
return provisioningCipher.getPublicKey().then(
|
||||
pubKey =>
|
||||
new Promise((resolve, reject) => {
|
||||
const socket = getSocket();
|
||||
socket.onclose = event => {
|
||||
window.log.info('provisioning socket closed. Code:', event.code);
|
||||
if (!gotProvisionEnvelope) {
|
||||
reject(new Error('websocket closed'));
|
||||
}
|
||||
};
|
||||
socket.onopen = () => {
|
||||
window.log.info('provisioning socket open');
|
||||
};
|
||||
const wsr = new WebSocketResource(socket, {
|
||||
keepalive: { path: '/v1/keepalive/provisioning' },
|
||||
handleRequest(request) {
|
||||
if (request.path === '/v1/address' && request.verb === 'PUT') {
|
||||
const proto = textsecure.protobuf.ProvisioningUuid.decode(
|
||||
request.body
|
||||
);
|
||||
setProvisioningUrl(
|
||||
[
|
||||
'tsdevice:/?uuid=',
|
||||
proto.uuid,
|
||||
'&pub_key=',
|
||||
encodeURIComponent(btoa(getString(pubKey))),
|
||||
].join('')
|
||||
);
|
||||
request.respond(200, 'OK');
|
||||
} else if (
|
||||
request.path === '/v1/message' &&
|
||||
request.verb === 'PUT'
|
||||
) {
|
||||
const envelope = textsecure.protobuf.ProvisionEnvelope.decode(
|
||||
request.body,
|
||||
'binary'
|
||||
);
|
||||
request.respond(200, 'OK');
|
||||
gotProvisionEnvelope = true;
|
||||
wsr.close();
|
||||
resolve(
|
||||
provisioningCipher
|
||||
.decrypt(envelope)
|
||||
.then(provisionMessage =>
|
||||
queueTask(() =>
|
||||
confirmNumber(provisionMessage.number).then(
|
||||
deviceName => {
|
||||
if (
|
||||
typeof deviceName !== 'string' ||
|
||||
deviceName.length === 0
|
||||
) {
|
||||
throw new Error('Invalid device name');
|
||||
}
|
||||
return createAccount(
|
||||
provisionMessage.number,
|
||||
provisionMessage.provisioningCode,
|
||||
provisionMessage.identityKeyPair,
|
||||
provisionMessage.profileKey,
|
||||
deviceName,
|
||||
provisionMessage.userAgent,
|
||||
provisionMessage.readReceipts,
|
||||
{ uuid: provisionMessage.uuid }
|
||||
)
|
||||
.then(clearSessionsAndPreKeys)
|
||||
.then(generateKeys)
|
||||
.then(keys =>
|
||||
registerKeys(keys).then(() =>
|
||||
confirmKeys(keys)
|
||||
)
|
||||
)
|
||||
.then(() => registrationDone(provisionMessage));
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
window.log.error('Unknown websocket message', request.path);
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
||||
);
|
||||
},
|
||||
refreshPreKeys() {
|
||||
const generateKeys = this.generateKeys.bind(this, 100);
|
||||
const registerKeys = this.server.registerKeys.bind(this.server);
|
||||
|
||||
return this.queueTask(() =>
|
||||
this.server.getMyKeys().then(preKeyCount => {
|
||||
window.log.info(`prekey count ${preKeyCount}`);
|
||||
if (preKeyCount < 10) {
|
||||
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;
|
||||
|
||||
return store
|
||||
.getIdentityKeyPair()
|
||||
.then(
|
||||
identityKey =>
|
||||
libsignal.KeyHelper.generateSignedPreKey(
|
||||
identityKey,
|
||||
signedKeyId
|
||||
),
|
||||
() => {
|
||||
// We swallow any error here, because we don't want to get into
|
||||
// a loop of repeated retries.
|
||||
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;
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
queueTask(task) {
|
||||
this.pendingQueue =
|
||||
this.pendingQueue || new window.PQueue({ concurrency: 1 });
|
||||
const taskWithTimeout = textsecure.createTaskWithTimeout(task);
|
||||
|
||||
return this.pendingQueue.add(taskWithTimeout);
|
||||
},
|
||||
cleanSignedPreKeys() {
|
||||
const MINIMUM_KEYS = 3;
|
||||
const store = textsecure.storage.protocol;
|
||||
return store.loadSignedPreKeys().then(allKeys => {
|
||||
allKeys.sort((a, b) => (a.created_at || 0) - (b.created_at || 0));
|
||||
allKeys.reverse(); // we want the most recent first
|
||||
const confirmed = allKeys.filter(key => key.confirmed);
|
||||
const unconfirmed = allKeys.filter(key => !key.confirmed);
|
||||
|
||||
const recent = allKeys[0] ? allKeys[0].keyId : 'none';
|
||||
const recentConfirmed = confirmed[0] ? confirmed[0].keyId : 'none';
|
||||
window.log.info(`Most recent signed key: ${recent}`);
|
||||
window.log.info(`Most recent confirmed signed key: ${recentConfirmed}`);
|
||||
window.log.info(
|
||||
'Total signed key count:',
|
||||
allKeys.length,
|
||||
'-',
|
||||
confirmed.length,
|
||||
'confirmed'
|
||||
);
|
||||
|
||||
let confirmedCount = confirmed.length;
|
||||
|
||||
// Keep MINIMUM_KEYS confirmed keys, then drop if older than a week
|
||||
confirmed.forEach((key, index) => {
|
||||
if (index < MINIMUM_KEYS) {
|
||||
return;
|
||||
}
|
||||
const createdAt = key.created_at || 0;
|
||||
const age = Date.now() - createdAt;
|
||||
|
||||
if (age > ARCHIVE_AGE) {
|
||||
window.log.info(
|
||||
'Removing confirmed signed prekey:',
|
||||
key.keyId,
|
||||
'with timestamp:',
|
||||
new Date(createdAt).toJSON()
|
||||
);
|
||||
store.removeSignedPreKey(key.keyId);
|
||||
confirmedCount -= 1;
|
||||
}
|
||||
});
|
||||
|
||||
const stillNeeded = MINIMUM_KEYS - confirmedCount;
|
||||
|
||||
// If we still don't have enough total keys, we keep as many unconfirmed
|
||||
// keys as necessary. If not necessary, and over a week old, we drop.
|
||||
unconfirmed.forEach((key, index) => {
|
||||
if (index < stillNeeded) {
|
||||
return;
|
||||
}
|
||||
|
||||
const createdAt = key.created_at || 0;
|
||||
const age = Date.now() - createdAt;
|
||||
if (age > ARCHIVE_AGE) {
|
||||
window.log.info(
|
||||
'Removing unconfirmed signed prekey:',
|
||||
key.keyId,
|
||||
'with timestamp:',
|
||||
new Date(createdAt).toJSON()
|
||||
);
|
||||
store.removeSignedPreKey(key.keyId);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
async createAccount(
|
||||
number,
|
||||
verificationCode,
|
||||
identityKeyPair,
|
||||
profileKey,
|
||||
deviceName,
|
||||
userAgent,
|
||||
readReceipts,
|
||||
options = {}
|
||||
) {
|
||||
const { accessKey } = options;
|
||||
let password = btoa(getString(libsignal.crypto.getRandomBytes(16)));
|
||||
password = password.substring(0, password.length - 2);
|
||||
const registrationId = libsignal.KeyHelper.generateRegistrationId();
|
||||
|
||||
const previousNumber = getIdentifier(textsecure.storage.get('number_id'));
|
||||
const previousUuid = getIdentifier(textsecure.storage.get('uuid_id'));
|
||||
|
||||
const encryptedDeviceName = await this.encryptDeviceName(
|
||||
deviceName,
|
||||
identityKeyPair
|
||||
);
|
||||
await this.deviceNameIsEncrypted();
|
||||
|
||||
window.log.info(
|
||||
`createAccount: Number is ${number}, password has length: ${
|
||||
password ? password.length : 'none'
|
||||
}`
|
||||
);
|
||||
|
||||
const response = await this.server.confirmCode(
|
||||
number,
|
||||
verificationCode,
|
||||
password,
|
||||
registrationId,
|
||||
encryptedDeviceName,
|
||||
{ accessKey }
|
||||
);
|
||||
|
||||
const numberChanged = previousNumber && previousNumber !== number;
|
||||
const uuidChanged =
|
||||
previousUuid && response.uuid && previousUuid !== response.uuid;
|
||||
|
||||
if (numberChanged || uuidChanged) {
|
||||
if (numberChanged) {
|
||||
window.log.warn(
|
||||
'New number is different from old number; deleting all previous data'
|
||||
);
|
||||
}
|
||||
if (uuidChanged) {
|
||||
window.log.warn(
|
||||
'New uuid is different from old uuid; deleting all previous data'
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await textsecure.storage.protocol.removeAllData();
|
||||
window.log.info('Successfully deleted previous data');
|
||||
} catch (error) {
|
||||
window.log.error(
|
||||
'Something went wrong deleting data from previous number',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
textsecure.storage.remove('identityKey'),
|
||||
textsecure.storage.remove('password'),
|
||||
textsecure.storage.remove('registrationId'),
|
||||
textsecure.storage.remove('number_id'),
|
||||
textsecure.storage.remove('device_name'),
|
||||
textsecure.storage.remove('regionCode'),
|
||||
textsecure.storage.remove('userAgent'),
|
||||
textsecure.storage.remove('profileKey'),
|
||||
textsecure.storage.remove('read-receipts-setting'),
|
||||
]);
|
||||
|
||||
// `setNumberAndDeviceId` and `setUuidAndDeviceId` need to be called
|
||||
// before `saveIdentifyWithAttributes` since `saveIdentityWithAttributes`
|
||||
// indirectly calls `ConversationController.getConverationId()` which
|
||||
// initializes the conversation for the given number (our number) which
|
||||
// calls out to the user storage API to get the stored UUID and number
|
||||
// information.
|
||||
await textsecure.storage.user.setNumberAndDeviceId(
|
||||
number,
|
||||
response.deviceId || 1,
|
||||
deviceName
|
||||
);
|
||||
|
||||
const setUuid = response.uuid;
|
||||
if (setUuid) {
|
||||
await textsecure.storage.user.setUuidAndDeviceId(
|
||||
setUuid,
|
||||
response.deviceId || 1
|
||||
);
|
||||
}
|
||||
|
||||
// update our own identity key, which may have changed
|
||||
// if we're relinking after a reinstall on the master device
|
||||
await textsecure.storage.protocol.saveIdentityWithAttributes(number, {
|
||||
publicKey: identityKeyPair.pubKey,
|
||||
firstUse: true,
|
||||
timestamp: Date.now(),
|
||||
verified: textsecure.storage.protocol.VerifiedStatus.VERIFIED,
|
||||
nonblockingApproval: true,
|
||||
});
|
||||
|
||||
await textsecure.storage.put('identityKey', identityKeyPair);
|
||||
await textsecure.storage.put('password', password);
|
||||
await textsecure.storage.put('registrationId', registrationId);
|
||||
if (profileKey) {
|
||||
await textsecure.storage.put('profileKey', profileKey);
|
||||
}
|
||||
if (userAgent) {
|
||||
await textsecure.storage.put('userAgent', userAgent);
|
||||
}
|
||||
|
||||
await textsecure.storage.put(
|
||||
'read-receipt-setting',
|
||||
Boolean(readReceipts)
|
||||
);
|
||||
|
||||
const regionCode = libphonenumber.util.getRegionCodeForNumber(number);
|
||||
await textsecure.storage.put('regionCode', regionCode);
|
||||
await textsecure.storage.protocol.hydrateCaches();
|
||||
},
|
||||
async clearSessionsAndPreKeys() {
|
||||
const store = textsecure.storage.protocol;
|
||||
|
||||
window.log.info('clearing all sessions, prekeys, and signed prekeys');
|
||||
await Promise.all([
|
||||
store.clearPreKeyStore(),
|
||||
store.clearSignedPreKeysStore(),
|
||||
store.clearSessionStore(),
|
||||
]);
|
||||
},
|
||||
// Takes the same object returned by generateKeys
|
||||
async confirmKeys(keys) {
|
||||
const store = textsecure.storage.protocol;
|
||||
const key = keys.signedPreKey;
|
||||
const confirmed = true;
|
||||
|
||||
window.log.info('confirmKeys: confirming key', key.keyId);
|
||||
await store.storeSignedPreKey(key.keyId, key.keyPair, confirmed);
|
||||
},
|
||||
generateKeys(count, providedProgressCallback) {
|
||||
const progressCallback =
|
||||
typeof providedProgressCallback === 'function'
|
||||
? providedProgressCallback
|
||||
: null;
|
||||
const startId = textsecure.storage.get('maxPreKeyId', 1);
|
||||
const signedKeyId = textsecure.storage.get('signedKeyId', 1);
|
||||
|
||||
if (typeof startId !== 'number') {
|
||||
throw new Error('Invalid maxPreKeyId');
|
||||
}
|
||||
if (typeof signedKeyId !== 'number') {
|
||||
throw new Error('Invalid signedKeyId');
|
||||
}
|
||||
|
||||
const store = textsecure.storage.protocol;
|
||||
return store.getIdentityKeyPair().then(identityKey => {
|
||||
const result = { preKeys: [], identityKey: identityKey.pubKey };
|
||||
const promises = [];
|
||||
|
||||
for (let keyId = startId; keyId < startId + count; keyId += 1) {
|
||||
promises.push(
|
||||
libsignal.KeyHelper.generatePreKey(keyId).then(res => {
|
||||
store.storePreKey(res.keyId, res.keyPair);
|
||||
result.preKeys.push({
|
||||
keyId: res.keyId,
|
||||
publicKey: res.keyPair.pubKey,
|
||||
});
|
||||
if (progressCallback) {
|
||||
progressCallback();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
promises.push(
|
||||
libsignal.KeyHelper.generateSignedPreKey(
|
||||
identityKey,
|
||||
signedKeyId
|
||||
).then(res => {
|
||||
store.storeSignedPreKey(res.keyId, res.keyPair);
|
||||
result.signedPreKey = {
|
||||
keyId: res.keyId,
|
||||
publicKey: res.keyPair.pubKey,
|
||||
signature: res.signature,
|
||||
// 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)
|
||||
);
|
||||
});
|
||||
},
|
||||
async registrationDone({ uuid, number }) {
|
||||
window.log.info('registration done');
|
||||
|
||||
// Ensure that we always have a conversation for ourself
|
||||
const conversation = await ConversationController.getOrCreateAndWait(
|
||||
number || uuid,
|
||||
'private'
|
||||
);
|
||||
conversation.updateE164(number);
|
||||
conversation.updateUuid(uuid);
|
||||
|
||||
window.log.info('dispatching registration event');
|
||||
|
||||
this.dispatchEvent(new Event('registration'));
|
||||
},
|
||||
});
|
||||
textsecure.AccountManager = AccountManager;
|
||||
})();
|
|
@ -1,75 +0,0 @@
|
|||
/* global dcodeIO, window, textsecure */
|
||||
|
||||
function ProtoParser(arrayBuffer, protobuf) {
|
||||
this.protobuf = protobuf;
|
||||
this.buffer = new dcodeIO.ByteBuffer();
|
||||
this.buffer.append(arrayBuffer);
|
||||
this.buffer.offset = 0;
|
||||
this.buffer.limit = arrayBuffer.byteLength;
|
||||
}
|
||||
ProtoParser.prototype = {
|
||||
constructor: ProtoParser,
|
||||
next() {
|
||||
try {
|
||||
if (this.buffer.limit === this.buffer.offset) {
|
||||
return undefined; // eof
|
||||
}
|
||||
const len = this.buffer.readVarint32();
|
||||
const nextBuffer = this.buffer
|
||||
.slice(this.buffer.offset, this.buffer.offset + len)
|
||||
.toArrayBuffer();
|
||||
// TODO: de-dupe ByteBuffer.js includes in libaxo/libts
|
||||
// then remove this toArrayBuffer call.
|
||||
|
||||
const proto = this.protobuf.decode(nextBuffer);
|
||||
this.buffer.skip(len);
|
||||
|
||||
if (proto.avatar) {
|
||||
const attachmentLen = proto.avatar.length;
|
||||
proto.avatar.data = this.buffer
|
||||
.slice(this.buffer.offset, this.buffer.offset + attachmentLen)
|
||||
.toArrayBuffer();
|
||||
this.buffer.skip(attachmentLen);
|
||||
}
|
||||
|
||||
if (proto.profileKey) {
|
||||
proto.profileKey = proto.profileKey.toArrayBuffer();
|
||||
}
|
||||
|
||||
if (proto.uuid) {
|
||||
window.normalizeUuids(
|
||||
proto,
|
||||
['uuid'],
|
||||
'ProtoParser::next (proto.uuid)'
|
||||
);
|
||||
}
|
||||
|
||||
if (proto.members) {
|
||||
window.normalizeUuids(
|
||||
proto,
|
||||
proto.members.map((_member, i) => `members.${i}.uuid`),
|
||||
'ProtoParser::next (proto.members)'
|
||||
);
|
||||
}
|
||||
|
||||
return proto;
|
||||
} catch (error) {
|
||||
window.log.error(
|
||||
'ProtoParser.next error:',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
};
|
||||
const GroupBuffer = function Constructor(arrayBuffer) {
|
||||
ProtoParser.call(this, arrayBuffer, textsecure.protobuf.GroupDetails);
|
||||
};
|
||||
GroupBuffer.prototype = Object.create(ProtoParser.prototype);
|
||||
GroupBuffer.prototype.constructor = GroupBuffer;
|
||||
const ContactBuffer = function Constructor(arrayBuffer) {
|
||||
ProtoParser.call(this, arrayBuffer, textsecure.protobuf.ContactDetails);
|
||||
};
|
||||
ContactBuffer.prototype = Object.create(ProtoParser.prototype);
|
||||
ContactBuffer.prototype.constructor = ContactBuffer;
|
|
@ -1,251 +0,0 @@
|
|||
/* global libsignal, crypto, textsecure, dcodeIO, window */
|
||||
|
||||
/* eslint-disable more/no-then, no-bitwise */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
const { encrypt, decrypt, calculateMAC, verifyMAC } = libsignal.crypto;
|
||||
|
||||
const PROFILE_IV_LENGTH = 12; // bytes
|
||||
const PROFILE_KEY_LENGTH = 32; // bytes
|
||||
const PROFILE_TAG_LENGTH = 128; // bits
|
||||
const PROFILE_NAME_PADDED_LENGTH = 53; // bytes
|
||||
|
||||
function verifyDigest(data, theirDigest) {
|
||||
return crypto.subtle.digest({ name: 'SHA-256' }, data).then(ourDigest => {
|
||||
const a = new Uint8Array(ourDigest);
|
||||
const b = new Uint8Array(theirDigest);
|
||||
let result = 0;
|
||||
for (let i = 0; i < theirDigest.byteLength; i += 1) {
|
||||
result |= a[i] ^ b[i];
|
||||
}
|
||||
if (result !== 0) {
|
||||
throw new Error('Bad digest');
|
||||
}
|
||||
});
|
||||
}
|
||||
function calculateDigest(data) {
|
||||
return crypto.subtle.digest({ name: 'SHA-256' }, data);
|
||||
}
|
||||
|
||||
window.textsecure = window.textsecure || {};
|
||||
window.textsecure.crypto = {
|
||||
// Decrypts message into a raw string
|
||||
decryptWebsocketMessage(message, signalingKey) {
|
||||
const decodedMessage = message.toArrayBuffer();
|
||||
|
||||
if (signalingKey.byteLength !== 52) {
|
||||
throw new Error('Got invalid length signalingKey');
|
||||
}
|
||||
if (decodedMessage.byteLength < 1 + 16 + 10) {
|
||||
throw new Error('Got invalid length message');
|
||||
}
|
||||
if (new Uint8Array(decodedMessage)[0] !== 1) {
|
||||
throw new Error(`Got bad version number: ${decodedMessage[0]}`);
|
||||
}
|
||||
|
||||
const aesKey = signalingKey.slice(0, 32);
|
||||
const macKey = signalingKey.slice(32, 32 + 20);
|
||||
|
||||
const iv = decodedMessage.slice(1, 1 + 16);
|
||||
const ciphertext = decodedMessage.slice(
|
||||
1 + 16,
|
||||
decodedMessage.byteLength - 10
|
||||
);
|
||||
const ivAndCiphertext = decodedMessage.slice(
|
||||
0,
|
||||
decodedMessage.byteLength - 10
|
||||
);
|
||||
const mac = decodedMessage.slice(
|
||||
decodedMessage.byteLength - 10,
|
||||
decodedMessage.byteLength
|
||||
);
|
||||
|
||||
return verifyMAC(ivAndCiphertext, macKey, mac, 10).then(() =>
|
||||
decrypt(aesKey, ciphertext, iv)
|
||||
);
|
||||
},
|
||||
|
||||
decryptAttachment(encryptedBin, keys, theirDigest) {
|
||||
if (keys.byteLength !== 64) {
|
||||
throw new Error('Got invalid length attachment keys');
|
||||
}
|
||||
if (encryptedBin.byteLength < 16 + 32) {
|
||||
throw new Error('Got invalid length attachment');
|
||||
}
|
||||
|
||||
const aesKey = keys.slice(0, 32);
|
||||
const macKey = keys.slice(32, 64);
|
||||
|
||||
const iv = encryptedBin.slice(0, 16);
|
||||
const ciphertext = encryptedBin.slice(16, encryptedBin.byteLength - 32);
|
||||
const ivAndCiphertext = encryptedBin.slice(
|
||||
0,
|
||||
encryptedBin.byteLength - 32
|
||||
);
|
||||
const mac = encryptedBin.slice(
|
||||
encryptedBin.byteLength - 32,
|
||||
encryptedBin.byteLength
|
||||
);
|
||||
|
||||
return verifyMAC(ivAndCiphertext, macKey, mac, 32)
|
||||
.then(() => {
|
||||
if (theirDigest) {
|
||||
return verifyDigest(encryptedBin, theirDigest);
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.then(() => decrypt(aesKey, ciphertext, iv));
|
||||
},
|
||||
|
||||
encryptAttachment(plaintext, keys, iv) {
|
||||
if (
|
||||
!(plaintext instanceof ArrayBuffer) &&
|
||||
!ArrayBuffer.isView(plaintext)
|
||||
) {
|
||||
throw new TypeError(
|
||||
`\`plaintext\` must be an \`ArrayBuffer\` or \`ArrayBufferView\`; got: ${typeof plaintext}`
|
||||
);
|
||||
}
|
||||
|
||||
if (keys.byteLength !== 64) {
|
||||
throw new Error('Got invalid length attachment keys');
|
||||
}
|
||||
if (iv.byteLength !== 16) {
|
||||
throw new Error('Got invalid length attachment iv');
|
||||
}
|
||||
const aesKey = keys.slice(0, 32);
|
||||
const macKey = keys.slice(32, 64);
|
||||
|
||||
return encrypt(aesKey, plaintext, iv).then(ciphertext => {
|
||||
const ivAndCiphertext = new Uint8Array(16 + ciphertext.byteLength);
|
||||
ivAndCiphertext.set(new Uint8Array(iv));
|
||||
ivAndCiphertext.set(new Uint8Array(ciphertext), 16);
|
||||
|
||||
return calculateMAC(macKey, ivAndCiphertext.buffer).then(mac => {
|
||||
const encryptedBin = new Uint8Array(16 + ciphertext.byteLength + 32);
|
||||
encryptedBin.set(ivAndCiphertext);
|
||||
encryptedBin.set(new Uint8Array(mac), 16 + ciphertext.byteLength);
|
||||
return calculateDigest(encryptedBin.buffer).then(digest => ({
|
||||
ciphertext: encryptedBin.buffer,
|
||||
digest,
|
||||
}));
|
||||
});
|
||||
});
|
||||
},
|
||||
encryptProfile(data, key) {
|
||||
const iv = libsignal.crypto.getRandomBytes(PROFILE_IV_LENGTH);
|
||||
if (key.byteLength !== PROFILE_KEY_LENGTH) {
|
||||
throw new Error('Got invalid length profile key');
|
||||
}
|
||||
if (iv.byteLength !== PROFILE_IV_LENGTH) {
|
||||
throw new Error('Got invalid length profile iv');
|
||||
}
|
||||
return crypto.subtle
|
||||
.importKey('raw', key, { name: 'AES-GCM' }, false, ['encrypt'])
|
||||
.then(keyForEncryption =>
|
||||
crypto.subtle
|
||||
.encrypt(
|
||||
{ name: 'AES-GCM', iv, tagLength: PROFILE_TAG_LENGTH },
|
||||
keyForEncryption,
|
||||
data
|
||||
)
|
||||
.then(ciphertext => {
|
||||
const ivAndCiphertext = new Uint8Array(
|
||||
PROFILE_IV_LENGTH + ciphertext.byteLength
|
||||
);
|
||||
ivAndCiphertext.set(new Uint8Array(iv));
|
||||
ivAndCiphertext.set(
|
||||
new Uint8Array(ciphertext),
|
||||
PROFILE_IV_LENGTH
|
||||
);
|
||||
return ivAndCiphertext.buffer;
|
||||
})
|
||||
);
|
||||
},
|
||||
decryptProfile(data, key) {
|
||||
if (data.byteLength < 12 + 16 + 1) {
|
||||
throw new Error(`Got too short input: ${data.byteLength}`);
|
||||
}
|
||||
const iv = data.slice(0, PROFILE_IV_LENGTH);
|
||||
const ciphertext = data.slice(PROFILE_IV_LENGTH, data.byteLength);
|
||||
if (key.byteLength !== PROFILE_KEY_LENGTH) {
|
||||
throw new Error('Got invalid length profile key');
|
||||
}
|
||||
if (iv.byteLength !== PROFILE_IV_LENGTH) {
|
||||
throw new Error('Got invalid length profile iv');
|
||||
}
|
||||
const error = new Error(); // save stack
|
||||
return crypto.subtle
|
||||
.importKey('raw', key, { name: 'AES-GCM' }, false, ['decrypt'])
|
||||
.then(keyForEncryption =>
|
||||
crypto.subtle
|
||||
.decrypt(
|
||||
{ name: 'AES-GCM', iv, tagLength: PROFILE_TAG_LENGTH },
|
||||
keyForEncryption,
|
||||
ciphertext
|
||||
)
|
||||
.catch(e => {
|
||||
if (e.name === 'OperationError') {
|
||||
// bad mac, basically.
|
||||
error.message =
|
||||
'Failed to decrypt profile data. Most likely the profile key has changed.';
|
||||
error.name = 'ProfileDecryptError';
|
||||
throw error;
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
encryptProfileName(name, key) {
|
||||
const padded = new Uint8Array(PROFILE_NAME_PADDED_LENGTH);
|
||||
padded.set(new Uint8Array(name));
|
||||
return textsecure.crypto.encryptProfile(padded.buffer, key);
|
||||
},
|
||||
decryptProfileName(encryptedProfileName, key) {
|
||||
const data = dcodeIO.ByteBuffer.wrap(
|
||||
encryptedProfileName,
|
||||
'base64'
|
||||
).toArrayBuffer();
|
||||
return textsecure.crypto.decryptProfile(data, key).then(decrypted => {
|
||||
const padded = new Uint8Array(decrypted);
|
||||
|
||||
// Given name is the start of the string to the first null character
|
||||
let givenEnd;
|
||||
for (givenEnd = 0; givenEnd < padded.length; givenEnd += 1) {
|
||||
if (padded[givenEnd] === 0x00) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Family name is the next chunk of non-null characters after that first null
|
||||
let familyEnd;
|
||||
for (
|
||||
familyEnd = givenEnd + 1;
|
||||
familyEnd < padded.length;
|
||||
familyEnd += 1
|
||||
) {
|
||||
if (padded[familyEnd] === 0x00) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
const foundFamilyName = familyEnd > givenEnd + 1;
|
||||
|
||||
return {
|
||||
given: dcodeIO.ByteBuffer.wrap(padded)
|
||||
.slice(0, givenEnd)
|
||||
.toArrayBuffer(),
|
||||
family: foundFamilyName
|
||||
? dcodeIO.ByteBuffer.wrap(padded)
|
||||
.slice(givenEnd + 1, familyEnd)
|
||||
.toArrayBuffer()
|
||||
: null,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
getRandomBytes(size) {
|
||||
return libsignal.crypto.getRandomBytes(size);
|
||||
},
|
||||
};
|
||||
})();
|
|
@ -1,144 +0,0 @@
|
|||
/* global window */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
window.textsecure = window.textsecure || {};
|
||||
|
||||
function inherit(Parent, Child) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
Child.prototype = Object.create(Parent.prototype, {
|
||||
constructor: {
|
||||
value: Child,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
function appendStack(newError, originalError) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
newError.stack += `\nOriginal stack:\n${originalError.stack}`;
|
||||
}
|
||||
|
||||
function ReplayableError(options = {}) {
|
||||
this.name = options.name || 'ReplayableError';
|
||||
this.message = options.message;
|
||||
|
||||
Error.call(this, options.message);
|
||||
|
||||
// Maintains proper stack trace, where our error was thrown (only available on V8)
|
||||
// via https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this);
|
||||
}
|
||||
|
||||
this.functionCode = options.functionCode;
|
||||
}
|
||||
inherit(Error, ReplayableError);
|
||||
|
||||
function IncomingIdentityKeyError(identifier, message, key) {
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
this.identifier = identifier.split('.')[0];
|
||||
this.identityKey = key;
|
||||
|
||||
ReplayableError.call(this, {
|
||||
name: 'IncomingIdentityKeyError',
|
||||
message: `The identity of ${this.identifier} has changed.`,
|
||||
});
|
||||
}
|
||||
inherit(ReplayableError, IncomingIdentityKeyError);
|
||||
|
||||
function OutgoingIdentityKeyError(
|
||||
identifier,
|
||||
message,
|
||||
timestamp,
|
||||
identityKey
|
||||
) {
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
this.identifier = identifier.split('.')[0];
|
||||
this.identityKey = identityKey;
|
||||
|
||||
ReplayableError.call(this, {
|
||||
name: 'OutgoingIdentityKeyError',
|
||||
message: `The identity of ${this.identifier} has changed.`,
|
||||
});
|
||||
}
|
||||
inherit(ReplayableError, OutgoingIdentityKeyError);
|
||||
|
||||
function OutgoingMessageError(identifier, message, timestamp, httpError) {
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
this.identifier = identifier.split('.')[0];
|
||||
|
||||
ReplayableError.call(this, {
|
||||
name: 'OutgoingMessageError',
|
||||
message: httpError ? httpError.message : 'no http error',
|
||||
});
|
||||
|
||||
if (httpError) {
|
||||
this.code = httpError.code;
|
||||
appendStack(this, httpError);
|
||||
}
|
||||
}
|
||||
inherit(ReplayableError, OutgoingMessageError);
|
||||
|
||||
function SendMessageNetworkError(identifier, jsonData, httpError) {
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
this.identifier = identifier.split('.')[0];
|
||||
this.code = httpError.code;
|
||||
|
||||
ReplayableError.call(this, {
|
||||
name: 'SendMessageNetworkError',
|
||||
message: httpError.message,
|
||||
});
|
||||
|
||||
appendStack(this, httpError);
|
||||
}
|
||||
inherit(ReplayableError, SendMessageNetworkError);
|
||||
|
||||
function SignedPreKeyRotationError() {
|
||||
ReplayableError.call(this, {
|
||||
name: 'SignedPreKeyRotationError',
|
||||
message: 'Too many signed prekey rotation failures',
|
||||
});
|
||||
}
|
||||
inherit(ReplayableError, SignedPreKeyRotationError);
|
||||
|
||||
function MessageError(message, httpError) {
|
||||
this.code = httpError.code;
|
||||
|
||||
ReplayableError.call(this, {
|
||||
name: 'MessageError',
|
||||
message: httpError.message,
|
||||
});
|
||||
|
||||
appendStack(this, httpError);
|
||||
}
|
||||
inherit(ReplayableError, MessageError);
|
||||
|
||||
function UnregisteredUserError(identifier, httpError) {
|
||||
this.message = httpError.message;
|
||||
this.name = 'UnregisteredUserError';
|
||||
|
||||
Error.call(this, this.message);
|
||||
|
||||
// Maintains proper stack trace, where our error was thrown (only available on V8)
|
||||
// via https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this);
|
||||
}
|
||||
|
||||
this.identifier = identifier;
|
||||
this.code = httpError.code;
|
||||
|
||||
appendStack(this, httpError);
|
||||
}
|
||||
inherit(Error, UnregisteredUserError);
|
||||
|
||||
window.textsecure.UnregisteredUserError = UnregisteredUserError;
|
||||
window.textsecure.SendMessageNetworkError = SendMessageNetworkError;
|
||||
window.textsecure.IncomingIdentityKeyError = IncomingIdentityKeyError;
|
||||
window.textsecure.OutgoingIdentityKeyError = OutgoingIdentityKeyError;
|
||||
window.textsecure.ReplayableError = ReplayableError;
|
||||
window.textsecure.OutgoingMessageError = OutgoingMessageError;
|
||||
window.textsecure.MessageError = MessageError;
|
||||
window.textsecure.SignedPreKeyRotationError = SignedPreKeyRotationError;
|
||||
})();
|
|
@ -1,82 +0,0 @@
|
|||
/* global window, Event, textsecure */
|
||||
|
||||
/*
|
||||
* Implements EventTarget
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/EventTarget
|
||||
*/
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
window.textsecure = window.textsecure || {};
|
||||
|
||||
function EventTarget() {}
|
||||
|
||||
EventTarget.prototype = {
|
||||
constructor: EventTarget,
|
||||
dispatchEvent(ev) {
|
||||
if (!(ev instanceof Event)) {
|
||||
throw new Error('Expects an event');
|
||||
}
|
||||
if (this.listeners === null || typeof this.listeners !== 'object') {
|
||||
this.listeners = {};
|
||||
}
|
||||
const listeners = this.listeners[ev.type];
|
||||
const results = [];
|
||||
if (typeof listeners === 'object') {
|
||||
for (let i = 0, max = listeners.length; i < max; i += 1) {
|
||||
const listener = listeners[i];
|
||||
if (typeof listener === 'function') {
|
||||
results.push(listener.call(null, ev));
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
},
|
||||
addEventListener(eventName, callback) {
|
||||
if (typeof eventName !== 'string') {
|
||||
throw new Error('First argument expects a string');
|
||||
}
|
||||
if (typeof callback !== 'function') {
|
||||
throw new Error('Second argument expects a function');
|
||||
}
|
||||
if (this.listeners === null || typeof this.listeners !== 'object') {
|
||||
this.listeners = {};
|
||||
}
|
||||
let listeners = this.listeners[eventName];
|
||||
if (typeof listeners !== 'object') {
|
||||
listeners = [];
|
||||
}
|
||||
listeners.push(callback);
|
||||
this.listeners[eventName] = listeners;
|
||||
},
|
||||
removeEventListener(eventName, callback) {
|
||||
if (typeof eventName !== 'string') {
|
||||
throw new Error('First argument expects a string');
|
||||
}
|
||||
if (typeof callback !== 'function') {
|
||||
throw new Error('Second argument expects a function');
|
||||
}
|
||||
if (this.listeners === null || typeof this.listeners !== 'object') {
|
||||
this.listeners = {};
|
||||
}
|
||||
const listeners = this.listeners[eventName];
|
||||
if (typeof listeners === 'object') {
|
||||
for (let i = 0; i < listeners.length; i += 1) {
|
||||
if (listeners[i] === callback) {
|
||||
listeners.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.listeners[eventName] = listeners;
|
||||
},
|
||||
extend(obj) {
|
||||
// eslint-disable-next-line no-restricted-syntax, guard-for-in
|
||||
for (const prop in obj) {
|
||||
this[prop] = obj[prop];
|
||||
}
|
||||
return this;
|
||||
},
|
||||
};
|
||||
|
||||
textsecure.EventTarget = EventTarget;
|
||||
})();
|
|
@ -1,70 +0,0 @@
|
|||
/* global window, dcodeIO */
|
||||
|
||||
/* eslint-disable no-proto, no-restricted-syntax, guard-for-in */
|
||||
|
||||
window.textsecure = window.textsecure || {};
|
||||
|
||||
/** *******************************
|
||||
*** Type conversion utilities ***
|
||||
******************************** */
|
||||
// Strings/arrays
|
||||
// TODO: Throw all this shit in favor of consistent types
|
||||
// TODO: Namespace
|
||||
const StaticByteBufferProto = new dcodeIO.ByteBuffer().__proto__;
|
||||
const StaticArrayBufferProto = new ArrayBuffer().__proto__;
|
||||
const StaticUint8ArrayProto = new Uint8Array().__proto__;
|
||||
function getString(thing) {
|
||||
if (thing === Object(thing)) {
|
||||
if (thing.__proto__ === StaticUint8ArrayProto)
|
||||
return String.fromCharCode.apply(null, thing);
|
||||
if (thing.__proto__ === StaticArrayBufferProto)
|
||||
return getString(new Uint8Array(thing));
|
||||
if (thing.__proto__ === StaticByteBufferProto)
|
||||
return thing.toString('binary');
|
||||
}
|
||||
return thing;
|
||||
}
|
||||
|
||||
function getStringable(thing) {
|
||||
return (
|
||||
typeof thing === 'string' ||
|
||||
typeof thing === 'number' ||
|
||||
typeof thing === 'boolean' ||
|
||||
(thing === Object(thing) &&
|
||||
(thing.__proto__ === StaticArrayBufferProto ||
|
||||
thing.__proto__ === StaticUint8ArrayProto ||
|
||||
thing.__proto__ === StaticByteBufferProto))
|
||||
);
|
||||
}
|
||||
|
||||
// Number formatting utils
|
||||
window.textsecure.utils = (() => {
|
||||
const self = {};
|
||||
self.unencodeNumber = number => number.split('.');
|
||||
self.isNumberSane = number =>
|
||||
number[0] === '+' && /^[0-9]+$/.test(number.substring(1));
|
||||
|
||||
/** ************************
|
||||
*** JSON'ing Utilities ***
|
||||
************************* */
|
||||
function ensureStringed(thing) {
|
||||
if (getStringable(thing)) return getString(thing);
|
||||
else if (thing instanceof Array) {
|
||||
const res = [];
|
||||
for (let i = 0; i < thing.length; i += 1)
|
||||
res[i] = ensureStringed(thing[i]);
|
||||
return res;
|
||||
} else if (thing === Object(thing)) {
|
||||
const res = {};
|
||||
for (const key in thing) res[key] = ensureStringed(thing[key]);
|
||||
return res;
|
||||
} else if (thing === null) {
|
||||
return null;
|
||||
}
|
||||
throw new Error(`unsure of how to jsonify object of type ${typeof thing}`);
|
||||
}
|
||||
|
||||
self.jsonThing = thing => JSON.stringify(ensureStringed(thing));
|
||||
|
||||
return self;
|
||||
})();
|
|
@ -36105,7 +36105,6 @@ SessionCipher.prototype = {
|
|||
var ourIdentityKeyBuffer = util.toArrayBuffer(ourIdentityKey.pubKey);
|
||||
var theirIdentityKey = util.toArrayBuffer(session.indexInfo.remoteIdentityKey);
|
||||
var macInput = new Uint8Array(encodedMsg.byteLength + 33*2 + 1);
|
||||
|
||||
macInput.set(new Uint8Array(ourIdentityKeyBuffer));
|
||||
macInput.set(new Uint8Array(theirIdentityKey), 33);
|
||||
macInput[33*2] = (3 << 4) | 3;
|
||||
|
@ -36512,10 +36511,20 @@ Internal.SessionLock = {};
|
|||
var jobQueue = {};
|
||||
|
||||
Internal.SessionLock.queueJobForNumber = function queueJobForNumber(number, runJob) {
|
||||
jobQueue[number] = jobQueue[number] || new window.PQueue({ concurrency: 1 });
|
||||
var queue = jobQueue[number];
|
||||
if (window.PQueue) {
|
||||
jobQueue[number] = jobQueue[number] || new window.PQueue({ concurrency: 1 });
|
||||
var queue = jobQueue[number];
|
||||
return queue.add(runJob);
|
||||
}
|
||||
|
||||
return queue.add(runJob);
|
||||
var runPrevious = jobQueue[number] || Promise.resolve();
|
||||
var runCurrent = jobQueue[number] = runPrevious.then(runJob, runJob);
|
||||
runCurrent.then(function() {
|
||||
if (jobQueue[number] === runCurrent) {
|
||||
delete jobQueue[number];
|
||||
}
|
||||
});
|
||||
return runCurrent;
|
||||
};
|
||||
|
||||
})();
|
||||
|
@ -36555,7 +36564,7 @@ Internal.SessionLock.queueJobForNumber = function queueJobForNumber(number, runJ
|
|||
let i = 0;
|
||||
let buf = new Uint8Array(16);
|
||||
|
||||
uuid.replace(/[0-9A-F]{2}/ig, oct => {
|
||||
uuid.replace(/[0-9A-F]{2}/ig, function(oct) {
|
||||
buf[i++] = parseInt(oct, 16);
|
||||
});
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,530 +0,0 @@
|
|||
/* global textsecure, libsignal, window, btoa, _ */
|
||||
|
||||
/* eslint-disable more/no-then */
|
||||
|
||||
function OutgoingMessage(
|
||||
server,
|
||||
timestamp,
|
||||
identifiers,
|
||||
message,
|
||||
silent,
|
||||
callback,
|
||||
options = {}
|
||||
) {
|
||||
if (message instanceof textsecure.protobuf.DataMessage) {
|
||||
const content = new textsecure.protobuf.Content();
|
||||
content.dataMessage = message;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
message = content;
|
||||
}
|
||||
this.server = server;
|
||||
this.timestamp = timestamp;
|
||||
this.identifiers = identifiers;
|
||||
this.message = message; // ContentMessage proto
|
||||
this.callback = callback;
|
||||
this.silent = silent;
|
||||
|
||||
this.identifiersCompleted = 0;
|
||||
this.errors = [];
|
||||
this.successfulIdentifiers = [];
|
||||
this.failoverIdentifiers = [];
|
||||
this.unidentifiedDeliveries = [];
|
||||
|
||||
const { sendMetadata, senderCertificate, senderCertificateWithUuid, online } =
|
||||
options || {};
|
||||
this.sendMetadata = sendMetadata;
|
||||
this.senderCertificate = senderCertificate;
|
||||
this.senderCertificateWithUuid = senderCertificateWithUuid;
|
||||
this.online = online;
|
||||
}
|
||||
|
||||
OutgoingMessage.prototype = {
|
||||
constructor: OutgoingMessage,
|
||||
numberCompleted() {
|
||||
this.identifiersCompleted += 1;
|
||||
if (this.identifiersCompleted >= this.identifiers.length) {
|
||||
this.callback({
|
||||
successfulIdentifiers: this.successfulIdentifiers,
|
||||
failoverIdentifiers: this.failoverIdentifiers,
|
||||
errors: this.errors,
|
||||
unidentifiedDeliveries: this.unidentifiedDeliveries,
|
||||
});
|
||||
}
|
||||
},
|
||||
registerError(identifier, reason, error) {
|
||||
if (!error || (error.name === 'HTTPError' && error.code !== 404)) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
error = new textsecure.OutgoingMessageError(
|
||||
identifier,
|
||||
this.message.toArrayBuffer(),
|
||||
this.timestamp,
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
error.reason = reason;
|
||||
this.errors[this.errors.length] = error;
|
||||
this.numberCompleted();
|
||||
},
|
||||
reloadDevicesAndSend(identifier, recurse) {
|
||||
return () =>
|
||||
textsecure.storage.protocol.getDeviceIds(identifier).then(deviceIds => {
|
||||
if (deviceIds.length === 0) {
|
||||
return this.registerError(
|
||||
identifier,
|
||||
'Got empty device list when loading device keys',
|
||||
null
|
||||
);
|
||||
}
|
||||
return this.doSendMessage(identifier, deviceIds, recurse);
|
||||
});
|
||||
},
|
||||
|
||||
getKeysForIdentifier(identifier, updateDevices) {
|
||||
const handleResult = response =>
|
||||
Promise.all(
|
||||
response.devices.map(device => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
device.identityKey = response.identityKey;
|
||||
if (
|
||||
updateDevices === undefined ||
|
||||
updateDevices.indexOf(device.deviceId) > -1
|
||||
) {
|
||||
const address = new libsignal.SignalProtocolAddress(
|
||||
identifier,
|
||||
device.deviceId
|
||||
);
|
||||
const builder = new libsignal.SessionBuilder(
|
||||
textsecure.storage.protocol,
|
||||
address
|
||||
);
|
||||
if (device.registrationId === 0) {
|
||||
window.log.info('device registrationId 0!');
|
||||
}
|
||||
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;
|
||||
})
|
||||
);
|
||||
|
||||
const { sendMetadata } = this;
|
||||
const info =
|
||||
sendMetadata && sendMetadata[identifier] ? sendMetadata[identifier] : {};
|
||||
const { accessKey } = info || {};
|
||||
|
||||
if (updateDevices === undefined) {
|
||||
if (accessKey) {
|
||||
return this.server
|
||||
.getKeysForIdentifierUnauth(identifier, '*', { accessKey })
|
||||
.catch(error => {
|
||||
if (error.code === 401 || error.code === 403) {
|
||||
if (this.failoverIdentifiers.indexOf(identifier) === -1) {
|
||||
this.failoverIdentifiers.push(identifier);
|
||||
}
|
||||
return this.server.getKeysForIdentifier(identifier, '*');
|
||||
}
|
||||
throw error;
|
||||
})
|
||||
.then(handleResult);
|
||||
}
|
||||
|
||||
return this.server
|
||||
.getKeysForIdentifier(identifier, '*')
|
||||
.then(handleResult);
|
||||
}
|
||||
|
||||
let promise = Promise.resolve();
|
||||
updateDevices.forEach(deviceId => {
|
||||
promise = promise.then(() => {
|
||||
let innerPromise;
|
||||
|
||||
if (accessKey) {
|
||||
innerPromise = this.server
|
||||
.getKeysForIdentifierUnauth(identifier, deviceId, { accessKey })
|
||||
.then(handleResult)
|
||||
.catch(error => {
|
||||
if (error.code === 401 || error.code === 403) {
|
||||
if (this.failoverIdentifiers.indexOf(identifier) === -1) {
|
||||
this.failoverIdentifiers.push(identifier);
|
||||
}
|
||||
return this.server
|
||||
.getKeysForIdentifier(identifier, deviceId)
|
||||
.then(handleResult);
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
} else {
|
||||
innerPromise = this.server
|
||||
.getKeysForIdentifier(identifier, deviceId)
|
||||
.then(handleResult);
|
||||
}
|
||||
|
||||
return innerPromise.catch(e => {
|
||||
if (e.name === 'HTTPError' && e.code === 404) {
|
||||
if (deviceId !== 1) {
|
||||
return this.removeDeviceIdsForIdentifier(identifier, [deviceId]);
|
||||
}
|
||||
throw new textsecure.UnregisteredUserError(identifier, e);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return promise;
|
||||
},
|
||||
|
||||
transmitMessage(identifier, jsonData, timestamp, { accessKey } = {}) {
|
||||
let promise;
|
||||
|
||||
if (accessKey) {
|
||||
promise = this.server.sendMessagesUnauth(
|
||||
identifier,
|
||||
jsonData,
|
||||
timestamp,
|
||||
this.silent,
|
||||
this.online,
|
||||
{ accessKey }
|
||||
);
|
||||
} else {
|
||||
promise = this.server.sendMessages(
|
||||
identifier,
|
||||
jsonData,
|
||||
timestamp,
|
||||
this.silent,
|
||||
this.online
|
||||
);
|
||||
}
|
||||
|
||||
return promise.catch(e => {
|
||||
if (e.name === 'HTTPError' && e.code !== 409 && e.code !== 410) {
|
||||
// 409 and 410 should bubble and be handled by doSendMessage
|
||||
// 404 should throw UnregisteredUserError
|
||||
// all other network errors can be retried later.
|
||||
if (e.code === 404) {
|
||||
throw new textsecure.UnregisteredUserError(identifier, e);
|
||||
}
|
||||
throw new textsecure.SendMessageNetworkError(
|
||||
identifier,
|
||||
jsonData,
|
||||
e,
|
||||
timestamp
|
||||
);
|
||||
}
|
||||
throw e;
|
||||
});
|
||||
},
|
||||
|
||||
getPaddedMessageLength(messageLength) {
|
||||
const messageLengthWithTerminator = messageLength + 1;
|
||||
let messagePartCount = Math.floor(messageLengthWithTerminator / 160);
|
||||
|
||||
if (messageLengthWithTerminator % 160 !== 0) {
|
||||
messagePartCount += 1;
|
||||
}
|
||||
|
||||
return messagePartCount * 160;
|
||||
},
|
||||
|
||||
getPlaintext() {
|
||||
if (!this.plaintext) {
|
||||
const messageBuffer = this.message.toArrayBuffer();
|
||||
this.plaintext = new Uint8Array(
|
||||
this.getPaddedMessageLength(messageBuffer.byteLength + 1) - 1
|
||||
);
|
||||
this.plaintext.set(new Uint8Array(messageBuffer));
|
||||
this.plaintext[messageBuffer.byteLength] = 0x80;
|
||||
}
|
||||
return this.plaintext;
|
||||
},
|
||||
|
||||
doSendMessage(identifier, deviceIds, recurse) {
|
||||
const ciphers = {};
|
||||
const plaintext = this.getPlaintext();
|
||||
|
||||
const { sendMetadata } = this;
|
||||
const info =
|
||||
sendMetadata && sendMetadata[identifier] ? sendMetadata[identifier] : {};
|
||||
const { accessKey, useUuidSenderCert } = info || {};
|
||||
const senderCertificate = useUuidSenderCert
|
||||
? this.senderCertificateWithUuid
|
||||
: this.senderCertificate;
|
||||
|
||||
if (accessKey && !senderCertificate) {
|
||||
window.log.warn(
|
||||
'OutgoingMessage.doSendMessage: accessKey was provided, but senderCertificate was not'
|
||||
);
|
||||
}
|
||||
|
||||
const sealedSender = Boolean(accessKey && senderCertificate);
|
||||
|
||||
// We don't send to ourselves if unless sealedSender is enabled
|
||||
const ourNumber = textsecure.storage.user.getNumber();
|
||||
const ourUuid = textsecure.storage.user.getUuid();
|
||||
const ourDeviceId = textsecure.storage.user.getDeviceId();
|
||||
if ((identifier === ourNumber || identifier === ourUuid) && !sealedSender) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
deviceIds = _.reject(
|
||||
deviceIds,
|
||||
deviceId =>
|
||||
// because we store our own device ID as a string at least sometimes
|
||||
deviceId === ourDeviceId || deviceId === parseInt(ourDeviceId, 10)
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
deviceIds.map(async deviceId => {
|
||||
const address = new libsignal.SignalProtocolAddress(
|
||||
identifier,
|
||||
deviceId
|
||||
);
|
||||
|
||||
const options = {};
|
||||
|
||||
// No limit on message keys if we're communicating with our other devices
|
||||
if (ourNumber === identifier || ourUuid === identifier) {
|
||||
options.messageKeysLimit = false;
|
||||
}
|
||||
|
||||
if (sealedSender) {
|
||||
const secretSessionCipher = new window.Signal.Metadata.SecretSessionCipher(
|
||||
textsecure.storage.protocol
|
||||
);
|
||||
ciphers[address.getDeviceId()] = secretSessionCipher;
|
||||
|
||||
const ciphertext = await secretSessionCipher.encrypt(
|
||||
address,
|
||||
senderCertificate,
|
||||
plaintext
|
||||
);
|
||||
|
||||
return {
|
||||
type: textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER,
|
||||
destinationDeviceId: address.getDeviceId(),
|
||||
destinationRegistrationId: await secretSessionCipher.getRemoteRegistrationId(
|
||||
address
|
||||
),
|
||||
content: window.Signal.Crypto.arrayBufferToBase64(ciphertext),
|
||||
};
|
||||
}
|
||||
|
||||
const sessionCipher = new libsignal.SessionCipher(
|
||||
textsecure.storage.protocol,
|
||||
address,
|
||||
options
|
||||
);
|
||||
ciphers[address.getDeviceId()] = sessionCipher;
|
||||
|
||||
const ciphertext = await sessionCipher.encrypt(plaintext);
|
||||
return {
|
||||
type: ciphertext.type,
|
||||
destinationDeviceId: address.getDeviceId(),
|
||||
destinationRegistrationId: ciphertext.registrationId,
|
||||
content: btoa(ciphertext.body),
|
||||
};
|
||||
})
|
||||
)
|
||||
.then(jsonData => {
|
||||
if (sealedSender) {
|
||||
return this.transmitMessage(identifier, jsonData, this.timestamp, {
|
||||
accessKey,
|
||||
}).then(
|
||||
() => {
|
||||
this.unidentifiedDeliveries.push(identifier);
|
||||
this.successfulIdentifiers.push(identifier);
|
||||
this.numberCompleted();
|
||||
},
|
||||
error => {
|
||||
if (error.code === 401 || error.code === 403) {
|
||||
if (this.failoverIdentifiers.indexOf(identifier) === -1) {
|
||||
this.failoverIdentifiers.push(identifier);
|
||||
}
|
||||
if (info) {
|
||||
info.accessKey = null;
|
||||
}
|
||||
|
||||
// Set final parameter to true to ensure we don't hit this codepath a
|
||||
// second time.
|
||||
return this.doSendMessage(identifier, deviceIds, recurse, true);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return this.transmitMessage(identifier, jsonData, this.timestamp).then(
|
||||
() => {
|
||||
this.successfulIdentifiers.push(identifier);
|
||||
this.numberCompleted();
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(error => {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
error.name === 'HTTPError' &&
|
||||
(error.code === 410 || error.code === 409)
|
||||
) {
|
||||
if (!recurse)
|
||||
return this.registerError(
|
||||
identifier,
|
||||
'Hit retry limit attempting to reload device list',
|
||||
error
|
||||
);
|
||||
|
||||
let p;
|
||||
if (error.code === 409) {
|
||||
p = this.removeDeviceIdsForIdentifier(
|
||||
identifier,
|
||||
error.response.extraDevices
|
||||
);
|
||||
} else {
|
||||
p = Promise.all(
|
||||
error.response.staleDevices.map(deviceId =>
|
||||
ciphers[deviceId].closeOpenSessionForDevice(
|
||||
new libsignal.SignalProtocolAddress(identifier, deviceId)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return p.then(() => {
|
||||
const resetDevices =
|
||||
error.code === 410
|
||||
? error.response.staleDevices
|
||||
: error.response.missingDevices;
|
||||
return this.getKeysForIdentifier(identifier, resetDevices).then(
|
||||
// We continue to retry as long as the error code was 409; the assumption is
|
||||
// that we'll request new device info and the next request will succeed.
|
||||
this.reloadDevicesAndSend(identifier, 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',
|
||||
identifier,
|
||||
deviceIds
|
||||
);
|
||||
|
||||
window.log.info('closing all sessions for', identifier);
|
||||
const address = new libsignal.SignalProtocolAddress(identifier, 1);
|
||||
|
||||
const sessionCipher = new libsignal.SessionCipher(
|
||||
textsecure.storage.protocol,
|
||||
address
|
||||
);
|
||||
window.log.info('closing session for', address.toString());
|
||||
return Promise.all([
|
||||
// Primary device
|
||||
sessionCipher.closeOpenSessionForDevice(),
|
||||
// The rest of their devices
|
||||
textsecure.storage.protocol.archiveSiblingSessions(
|
||||
address.toString()
|
||||
),
|
||||
]).then(
|
||||
() => {
|
||||
throw error;
|
||||
},
|
||||
innerError => {
|
||||
window.log.error(
|
||||
`doSendMessage: Error closing sessions: ${innerError.stack}`
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
this.registerError(
|
||||
identifier,
|
||||
'Failed to create or send message',
|
||||
error
|
||||
);
|
||||
return null;
|
||||
});
|
||||
},
|
||||
|
||||
getStaleDeviceIdsForIdentifier(identifier) {
|
||||
return textsecure.storage.protocol
|
||||
.getDeviceIds(identifier)
|
||||
.then(deviceIds => {
|
||||
if (deviceIds.length === 0) {
|
||||
return [1];
|
||||
}
|
||||
const updateDevices = [];
|
||||
return Promise.all(
|
||||
deviceIds.map(deviceId => {
|
||||
const address = new libsignal.SignalProtocolAddress(
|
||||
identifier,
|
||||
deviceId
|
||||
);
|
||||
const sessionCipher = new libsignal.SessionCipher(
|
||||
textsecure.storage.protocol,
|
||||
address
|
||||
);
|
||||
return sessionCipher.hasOpenSession().then(hasSession => {
|
||||
if (!hasSession) {
|
||||
updateDevices.push(deviceId);
|
||||
}
|
||||
});
|
||||
})
|
||||
).then(() => updateDevices);
|
||||
});
|
||||
},
|
||||
|
||||
removeDeviceIdsForIdentifier(identifier, deviceIdsToRemove) {
|
||||
let promise = Promise.resolve();
|
||||
// eslint-disable-next-line no-restricted-syntax, guard-for-in
|
||||
for (const j in deviceIdsToRemove) {
|
||||
promise = promise.then(() => {
|
||||
const encodedAddress = `${identifier}.${deviceIdsToRemove[j]}`;
|
||||
return textsecure.storage.protocol.removeSession(encodedAddress);
|
||||
});
|
||||
}
|
||||
return promise;
|
||||
},
|
||||
|
||||
async sendToIdentifier(identifier) {
|
||||
try {
|
||||
const updateDevices = await this.getStaleDeviceIdsForIdentifier(
|
||||
identifier
|
||||
);
|
||||
await this.getKeysForIdentifier(identifier, updateDevices);
|
||||
await this.reloadDevicesAndSend(identifier, true)();
|
||||
} catch (error) {
|
||||
if (error.message === 'Identity key changed') {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
const newError = new textsecure.OutgoingIdentityKeyError(
|
||||
identifier,
|
||||
error.originalMessage,
|
||||
error.timestamp,
|
||||
error.identityKey
|
||||
);
|
||||
this.registerError(identifier, 'Identity key changed', newError);
|
||||
} else {
|
||||
this.registerError(
|
||||
identifier,
|
||||
`Failed to retrieve new device keys for number ${identifier}`,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
File diff suppressed because it is too large
Load diff
|
@ -1,37 +0,0 @@
|
|||
/* global window, textsecure, localStorage */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
/** **********************************************
|
||||
*** Utilities to store data in local storage ***
|
||||
*********************************************** */
|
||||
window.textsecure = window.textsecure || {};
|
||||
window.textsecure.storage = window.textsecure.storage || {};
|
||||
|
||||
// Overrideable storage implementation
|
||||
window.textsecure.storage.impl = window.textsecure.storage.impl || {
|
||||
/** ***************************
|
||||
*** Base Storage Routines ***
|
||||
**************************** */
|
||||
put(key, value) {
|
||||
if (value === undefined) throw new Error('Tried to store undefined');
|
||||
localStorage.setItem(`${key}`, textsecure.utils.jsonThing(value));
|
||||
},
|
||||
|
||||
get(key, defaultValue) {
|
||||
const value = localStorage.getItem(`${key}`);
|
||||
if (value === null) return defaultValue;
|
||||
return JSON.parse(value);
|
||||
},
|
||||
|
||||
remove(key) {
|
||||
localStorage.removeItem(`${key}`);
|
||||
},
|
||||
};
|
||||
|
||||
window.textsecure.storage.put = (key, value) =>
|
||||
textsecure.storage.impl.put(key, value);
|
||||
window.textsecure.storage.get = (key, defaultValue) =>
|
||||
textsecure.storage.impl.get(key, defaultValue);
|
||||
window.textsecure.storage.remove = key => textsecure.storage.impl.remove(key);
|
||||
})();
|
|
@ -1,104 +0,0 @@
|
|||
/* global window, StringView */
|
||||
|
||||
/* eslint-disable no-bitwise, no-nested-ternary */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
window.StringView = {
|
||||
/*
|
||||
* These functions from the Mozilla Developer Network
|
||||
* and have been placed in the public domain.
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding
|
||||
* https://developer.mozilla.org/en-US/docs/MDN/About#Copyrights_and_licenses
|
||||
*/
|
||||
|
||||
// prettier-ignore
|
||||
b64ToUint6(nChr) {
|
||||
return nChr > 64 && nChr < 91
|
||||
? nChr - 65
|
||||
: nChr > 96 && nChr < 123
|
||||
? nChr - 71
|
||||
: nChr > 47 && nChr < 58
|
||||
? nChr + 4
|
||||
: nChr === 43
|
||||
? 62
|
||||
: nChr === 47
|
||||
? 63
|
||||
: 0;
|
||||
},
|
||||
|
||||
base64ToBytes(sBase64, nBlocksSize) {
|
||||
const sB64Enc = sBase64.replace(/[^A-Za-z0-9+/]/g, '');
|
||||
const nInLen = sB64Enc.length;
|
||||
const nOutLen = nBlocksSize
|
||||
? Math.ceil(((nInLen * 3 + 1) >> 2) / nBlocksSize) * nBlocksSize
|
||||
: (nInLen * 3 + 1) >> 2;
|
||||
const aBBytes = new ArrayBuffer(nOutLen);
|
||||
const taBytes = new Uint8Array(aBBytes);
|
||||
|
||||
let nMod3;
|
||||
let nMod4;
|
||||
for (
|
||||
let nUint24 = 0, nOutIdx = 0, nInIdx = 0;
|
||||
nInIdx < nInLen;
|
||||
nInIdx += 1
|
||||
) {
|
||||
nMod4 = nInIdx & 3;
|
||||
nUint24 |=
|
||||
StringView.b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << (18 - 6 * nMod4);
|
||||
if (nMod4 === 3 || nInLen - nInIdx === 1) {
|
||||
for (
|
||||
nMod3 = 0;
|
||||
nMod3 < 3 && nOutIdx < nOutLen;
|
||||
nMod3 += 1, nOutIdx += 1
|
||||
) {
|
||||
taBytes[nOutIdx] = (nUint24 >>> ((16 >>> nMod3) & 24)) & 255;
|
||||
}
|
||||
nUint24 = 0;
|
||||
}
|
||||
}
|
||||
return aBBytes;
|
||||
},
|
||||
|
||||
// prettier-ignore
|
||||
uint6ToB64(nUint6) {
|
||||
return nUint6 < 26
|
||||
? nUint6 + 65
|
||||
: nUint6 < 52
|
||||
? nUint6 + 71
|
||||
: nUint6 < 62
|
||||
? nUint6 - 4
|
||||
: nUint6 === 62
|
||||
? 43
|
||||
: nUint6 === 63
|
||||
? 47
|
||||
: 65;
|
||||
},
|
||||
|
||||
bytesToBase64(aBytes) {
|
||||
let nMod3;
|
||||
let sB64Enc = '';
|
||||
for (
|
||||
let nLen = aBytes.length, nUint24 = 0, nIdx = 0;
|
||||
nIdx < nLen;
|
||||
nIdx += 1
|
||||
) {
|
||||
nMod3 = nIdx % 3;
|
||||
if (nIdx > 0 && ((nIdx * 4) / 3) % 76 === 0) {
|
||||
sB64Enc += '\r\n';
|
||||
}
|
||||
nUint24 |= aBytes[nIdx] << ((16 >>> nMod3) & 24);
|
||||
if (nMod3 === 2 || aBytes.length - nIdx === 1) {
|
||||
sB64Enc += String.fromCharCode(
|
||||
StringView.uint6ToB64((nUint24 >>> 18) & 63),
|
||||
StringView.uint6ToB64((nUint24 >>> 12) & 63),
|
||||
StringView.uint6ToB64((nUint24 >>> 6) & 63),
|
||||
StringView.uint6ToB64(nUint24 & 63)
|
||||
);
|
||||
nUint24 = 0;
|
||||
}
|
||||
}
|
||||
return sB64Enc.replace(/A(?=A$|$)/g, '=');
|
||||
},
|
||||
};
|
||||
})();
|
|
@ -1,97 +0,0 @@
|
|||
/* global Event, textsecure, window, ConversationController */
|
||||
|
||||
/* eslint-disable more/no-then */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
window.textsecure = window.textsecure || {};
|
||||
|
||||
function SyncRequest(sender, receiver) {
|
||||
if (
|
||||
!(sender instanceof textsecure.MessageSender) ||
|
||||
!(receiver instanceof textsecure.MessageReceiver)
|
||||
) {
|
||||
throw new Error(
|
||||
'Tried to construct a SyncRequest without MessageSender and MessageReceiver'
|
||||
);
|
||||
}
|
||||
this.receiver = receiver;
|
||||
|
||||
this.oncontact = this.onContactSyncComplete.bind(this);
|
||||
receiver.addEventListener('contactsync', this.oncontact);
|
||||
|
||||
this.ongroup = this.onGroupSyncComplete.bind(this);
|
||||
receiver.addEventListener('groupsync', this.ongroup);
|
||||
|
||||
const ourNumber = textsecure.storage.user.getNumber();
|
||||
const {
|
||||
wrap,
|
||||
sendOptions,
|
||||
} = ConversationController.prepareForSend(ourNumber, { syncMessage: true });
|
||||
|
||||
window.log.info('SyncRequest created. Sending config sync request...');
|
||||
wrap(sender.sendRequestConfigurationSyncMessage(sendOptions));
|
||||
|
||||
window.log.info('SyncRequest now sending block sync request...');
|
||||
wrap(sender.sendRequestBlockSyncMessage(sendOptions));
|
||||
|
||||
window.log.info('SyncRequest now sending contact sync message...');
|
||||
wrap(sender.sendRequestContactSyncMessage(sendOptions))
|
||||
.then(() => {
|
||||
window.log.info('SyncRequest now sending group sync messsage...');
|
||||
return wrap(sender.sendRequestGroupSyncMessage(sendOptions));
|
||||
})
|
||||
.catch(error => {
|
||||
window.log.error(
|
||||
'SyncRequest error:',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
});
|
||||
this.timeout = setTimeout(this.onTimeout.bind(this), 60000);
|
||||
}
|
||||
|
||||
SyncRequest.prototype = new textsecure.EventTarget();
|
||||
SyncRequest.prototype.extend({
|
||||
constructor: SyncRequest,
|
||||
onContactSyncComplete() {
|
||||
this.contactSync = true;
|
||||
this.update();
|
||||
},
|
||||
onGroupSyncComplete() {
|
||||
this.groupSync = true;
|
||||
this.update();
|
||||
},
|
||||
update() {
|
||||
if (this.contactSync && this.groupSync) {
|
||||
this.dispatchEvent(new Event('success'));
|
||||
this.cleanup();
|
||||
}
|
||||
},
|
||||
onTimeout() {
|
||||
if (this.contactSync || this.groupSync) {
|
||||
this.dispatchEvent(new Event('success'));
|
||||
} else {
|
||||
this.dispatchEvent(new Event('timeout'));
|
||||
}
|
||||
this.cleanup();
|
||||
},
|
||||
cleanup() {
|
||||
clearTimeout(this.timeout);
|
||||
this.receiver.removeEventListener('contactsync', this.oncontact);
|
||||
this.receiver.removeEventListener('groupSync', this.ongroup);
|
||||
delete this.listeners;
|
||||
},
|
||||
});
|
||||
|
||||
textsecure.SyncRequest = function SyncRequestWrapper(sender, receiver) {
|
||||
const syncRequest = new SyncRequest(sender, receiver);
|
||||
this.addEventListener = syncRequest.addEventListener.bind(syncRequest);
|
||||
this.removeEventListener = syncRequest.removeEventListener.bind(
|
||||
syncRequest
|
||||
);
|
||||
};
|
||||
|
||||
textsecure.SyncRequest.prototype = {
|
||||
constructor: textsecure.SyncRequest,
|
||||
};
|
||||
})();
|
|
@ -1,72 +0,0 @@
|
|||
/* global window */
|
||||
|
||||
/* eslint-disable more/no-then */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
window.textsecure = window.textsecure || {};
|
||||
|
||||
window.textsecure.createTaskWithTimeout = (task, id, options = {}) => {
|
||||
const timeout = options.timeout || 1000 * 60 * 2; // two minutes
|
||||
|
||||
const errorForStack = new Error('for stack');
|
||||
return () =>
|
||||
new Promise((resolve, reject) => {
|
||||
let complete = false;
|
||||
let timer = setTimeout(() => {
|
||||
if (!complete) {
|
||||
const message = `${id ||
|
||||
''} task did not complete in time. Calling stack: ${
|
||||
errorForStack.stack
|
||||
}`;
|
||||
|
||||
window.log.error(message);
|
||||
return reject(new Error(message));
|
||||
}
|
||||
|
||||
return null;
|
||||
}, timeout);
|
||||
const clearTimer = () => {
|
||||
try {
|
||||
const localTimer = timer;
|
||||
if (localTimer) {
|
||||
timer = null;
|
||||
clearTimeout(localTimer);
|
||||
}
|
||||
} catch (error) {
|
||||
window.log.error(
|
||||
id || '',
|
||||
'task ran into problem canceling timer. Calling stack:',
|
||||
errorForStack.stack
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const success = result => {
|
||||
clearTimer();
|
||||
complete = true;
|
||||
return resolve(result);
|
||||
};
|
||||
const failure = error => {
|
||||
clearTimer();
|
||||
complete = true;
|
||||
return reject(error);
|
||||
};
|
||||
|
||||
let promise;
|
||||
try {
|
||||
promise = task();
|
||||
} catch (error) {
|
||||
clearTimer();
|
||||
throw error;
|
||||
}
|
||||
if (!promise || !promise.then) {
|
||||
clearTimer();
|
||||
complete = true;
|
||||
return resolve(promise);
|
||||
}
|
||||
|
||||
return promise.then(success, failure);
|
||||
});
|
||||
};
|
||||
})();
|
|
@ -1,5 +1,3 @@
|
|||
/* global ContactBuffer, GroupBuffer, textsecure */
|
||||
|
||||
describe('ContactBuffer', () => {
|
||||
function getTestBuffer() {
|
||||
const buffer = new dcodeIO.ByteBuffer();
|
||||
|
@ -10,7 +8,7 @@ describe('ContactBuffer', () => {
|
|||
}
|
||||
avatarBuffer.limit = avatarBuffer.offset;
|
||||
avatarBuffer.offset = 0;
|
||||
const contactInfo = new textsecure.protobuf.ContactDetails({
|
||||
const contactInfo = new window.textsecure.protobuf.ContactDetails({
|
||||
name: 'Zero Cool',
|
||||
number: '+10000000000',
|
||||
uuid: '7198E1BD-1293-452A-A098-F982FF201902',
|
||||
|
@ -31,7 +29,7 @@ describe('ContactBuffer', () => {
|
|||
|
||||
it('parses an array buffer of contacts', () => {
|
||||
const arrayBuffer = getTestBuffer();
|
||||
const contactBuffer = new ContactBuffer(arrayBuffer);
|
||||
const contactBuffer = new window.textsecure.ContactBuffer(arrayBuffer);
|
||||
let contact = contactBuffer.next();
|
||||
let count = 0;
|
||||
while (contact !== undefined) {
|
||||
|
@ -62,7 +60,7 @@ describe('GroupBuffer', () => {
|
|||
}
|
||||
avatarBuffer.limit = avatarBuffer.offset;
|
||||
avatarBuffer.offset = 0;
|
||||
const groupInfo = new textsecure.protobuf.GroupDetails({
|
||||
const groupInfo = new window.textsecure.protobuf.GroupDetails({
|
||||
id: new Uint8Array([1, 3, 3, 7]).buffer,
|
||||
name: 'Hackers',
|
||||
membersE164: ['cereal', 'burn', 'phreak', 'joey'],
|
||||
|
@ -89,7 +87,7 @@ describe('GroupBuffer', () => {
|
|||
|
||||
it('parses an array buffer of groups', () => {
|
||||
const arrayBuffer = getTestBuffer();
|
||||
const groupBuffer = new GroupBuffer(arrayBuffer);
|
||||
const groupBuffer = new window.textsecure.GroupBuffer(arrayBuffer);
|
||||
let group = groupBuffer.next();
|
||||
let count = 0;
|
||||
while (group !== undefined) {
|
||||
|
|
|
@ -6,7 +6,7 @@ describe('Helpers', () => {
|
|||
a[0] = 0;
|
||||
a[1] = 255;
|
||||
a[2] = 128;
|
||||
assert.equal(getString(b), '\x00\xff\x80');
|
||||
assert.equal(window.textsecure.utils.getString(b), '\x00\xff\x80');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -15,13 +15,16 @@ describe('Helpers', () => {
|
|||
const anArrayBuffer = new ArrayBuffer(1);
|
||||
const typedArray = new Uint8Array(anArrayBuffer);
|
||||
typedArray[0] = 'a'.charCodeAt(0);
|
||||
assertEqualArrayBuffers(stringToArrayBuffer('a'), anArrayBuffer);
|
||||
assertEqualArrayBuffers(
|
||||
window.textsecure.utils.stringToArrayBuffer('a'),
|
||||
anArrayBuffer
|
||||
);
|
||||
});
|
||||
it('throws an error when passed a non string', () => {
|
||||
const notStringable = [{}, undefined, null, new ArrayBuffer()];
|
||||
notStringable.forEach(notString => {
|
||||
assert.throw(() => {
|
||||
stringToArrayBuffer(notString);
|
||||
window.textsecure.utils.stringToArrayBuffer(notString);
|
||||
}, Error);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -19,23 +19,10 @@
|
|||
|
||||
<script type="text/javascript" src="../components.js"></script>
|
||||
<script type="text/javascript" src="../libsignal-protocol.js"></script>
|
||||
<script type="text/javascript" src="../crypto.js"></script>
|
||||
<script type="text/javascript" src="../protobufs.js" data-cover></script>
|
||||
<script type="text/javascript" src="../errors.js" data-cover></script>
|
||||
<script type="text/javascript" src="../storage.js" data-cover></script>
|
||||
|
||||
<script type="text/javascript" src="../event_target.js" data-cover></script>
|
||||
<script type="text/javascript" src="../websocket-resources.js" data-cover></script>
|
||||
<script type="text/javascript" src="../helpers.js" data-cover></script>
|
||||
<script type="text/javascript" src="../stringview.js" data-cover></script>
|
||||
<script type="text/javascript" src="../api.js"></script>
|
||||
<script type="text/javascript" src="../sendmessage.js" data-cover></script>
|
||||
<script type="text/javascript" src="../account_manager.js" data-cover></script>
|
||||
<script type="text/javascript" src="../contacts_parser.js" data-cover></script>
|
||||
<script type="text/javascript" src="../task_with_timeout.js" data-cover></script>
|
||||
<script type="text/javascript" src="../storage/user.js" data-cover></script>
|
||||
|
||||
<script type="text/javascript" src="../protocol_wrapper.js" data-cover></script>
|
||||
|
||||
<script type="text/javascript" src="../../js/libphonenumber-util.js"></script>
|
||||
<script type="text/javascript" src="../../js/components.js" data-cover></script>
|
||||
<script type="text/javascript" src="../../js/signal_protocol_store.js" data-cover></script>
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
/* global textsecure, WebSocketResource */
|
||||
|
||||
describe('WebSocket-Resource', () => {
|
||||
describe('requests and responses', () => {
|
||||
it('receives requests and sends responses', done => {
|
||||
|
@ -7,10 +5,12 @@ describe('WebSocket-Resource', () => {
|
|||
const requestId = '1';
|
||||
const socket = {
|
||||
send(data) {
|
||||
const message = textsecure.protobuf.WebSocketMessage.decode(data);
|
||||
const message = window.textsecure.protobuf.WebSocketMessage.decode(
|
||||
data
|
||||
);
|
||||
assert.strictEqual(
|
||||
message.type,
|
||||
textsecure.protobuf.WebSocketMessage.Type.RESPONSE
|
||||
window.textsecure.protobuf.WebSocketMessage.Type.RESPONSE
|
||||
);
|
||||
assert.strictEqual(message.response.message, 'OK');
|
||||
assert.strictEqual(message.response.status, 200);
|
||||
|
@ -21,7 +21,7 @@ describe('WebSocket-Resource', () => {
|
|||
};
|
||||
|
||||
// actual test
|
||||
this.resource = new WebSocketResource(socket, {
|
||||
this.resource = new window.textsecure.WebSocketResource(socket, {
|
||||
handleRequest(request) {
|
||||
assert.strictEqual(request.verb, 'PUT');
|
||||
assert.strictEqual(request.path, '/some/path');
|
||||
|
@ -36,8 +36,8 @@ describe('WebSocket-Resource', () => {
|
|||
// mock socket request
|
||||
socket.onmessage({
|
||||
data: new Blob([
|
||||
new textsecure.protobuf.WebSocketMessage({
|
||||
type: textsecure.protobuf.WebSocketMessage.Type.REQUEST,
|
||||
new window.textsecure.protobuf.WebSocketMessage({
|
||||
type: window.textsecure.protobuf.WebSocketMessage.Type.REQUEST,
|
||||
request: {
|
||||
id: requestId,
|
||||
verb: 'PUT',
|
||||
|
@ -56,10 +56,12 @@ describe('WebSocket-Resource', () => {
|
|||
let requestId;
|
||||
const socket = {
|
||||
send(data) {
|
||||
const message = textsecure.protobuf.WebSocketMessage.decode(data);
|
||||
const message = window.textsecure.protobuf.WebSocketMessage.decode(
|
||||
data
|
||||
);
|
||||
assert.strictEqual(
|
||||
message.type,
|
||||
textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
||||
window.textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
||||
);
|
||||
assert.strictEqual(message.request.verb, 'PUT');
|
||||
assert.strictEqual(message.request.path, '/some/path');
|
||||
|
@ -73,7 +75,7 @@ describe('WebSocket-Resource', () => {
|
|||
};
|
||||
|
||||
// actual test
|
||||
const resource = new WebSocketResource(socket);
|
||||
const resource = new window.textsecure.WebSocketResource(socket);
|
||||
resource.sendRequest({
|
||||
verb: 'PUT',
|
||||
path: '/some/path',
|
||||
|
@ -89,8 +91,8 @@ describe('WebSocket-Resource', () => {
|
|||
// mock socket response
|
||||
socket.onmessage({
|
||||
data: new Blob([
|
||||
new textsecure.protobuf.WebSocketMessage({
|
||||
type: textsecure.protobuf.WebSocketMessage.Type.RESPONSE,
|
||||
new window.textsecure.protobuf.WebSocketMessage({
|
||||
type: window.textsecure.protobuf.WebSocketMessage.Type.RESPONSE,
|
||||
response: { id: requestId, message: 'OK', status: 200 },
|
||||
})
|
||||
.encode()
|
||||
|
@ -112,7 +114,7 @@ describe('WebSocket-Resource', () => {
|
|||
mockServer.on('connection', server => {
|
||||
server.on('close', done);
|
||||
});
|
||||
const resource = new WebSocketResource(
|
||||
const resource = new window.textsecure.WebSocketResource(
|
||||
new WebSocket('ws://localhost:8081')
|
||||
);
|
||||
resource.close();
|
||||
|
@ -131,10 +133,12 @@ describe('WebSocket-Resource', () => {
|
|||
const mockServer = new MockServer('ws://localhost:8081');
|
||||
mockServer.on('connection', server => {
|
||||
server.on('message', data => {
|
||||
const message = textsecure.protobuf.WebSocketMessage.decode(data);
|
||||
const message = window.textsecure.protobuf.WebSocketMessage.decode(
|
||||
data
|
||||
);
|
||||
assert.strictEqual(
|
||||
message.type,
|
||||
textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
||||
window.textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
||||
);
|
||||
assert.strictEqual(message.request.verb, 'GET');
|
||||
assert.strictEqual(message.request.path, '/v1/keepalive');
|
||||
|
@ -142,7 +146,7 @@ describe('WebSocket-Resource', () => {
|
|||
done();
|
||||
});
|
||||
});
|
||||
this.resource = new WebSocketResource(
|
||||
this.resource = new window.textsecure.WebSocketResource(
|
||||
new WebSocket('ws://loc1alhost:8081'),
|
||||
{
|
||||
keepalive: { path: '/v1/keepalive' },
|
||||
|
@ -154,10 +158,12 @@ describe('WebSocket-Resource', () => {
|
|||
const mockServer = new MockServer('ws://localhost:8081');
|
||||
mockServer.on('connection', server => {
|
||||
server.on('message', data => {
|
||||
const message = textsecure.protobuf.WebSocketMessage.decode(data);
|
||||
const message = window.textsecure.protobuf.WebSocketMessage.decode(
|
||||
data
|
||||
);
|
||||
assert.strictEqual(
|
||||
message.type,
|
||||
textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
||||
window.textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
||||
);
|
||||
assert.strictEqual(message.request.verb, 'GET');
|
||||
assert.strictEqual(message.request.path, '/');
|
||||
|
@ -165,7 +171,7 @@ describe('WebSocket-Resource', () => {
|
|||
done();
|
||||
});
|
||||
});
|
||||
this.resource = new WebSocketResource(
|
||||
this.resource = new window.textsecure.WebSocketResource(
|
||||
new WebSocket('ws://localhost:8081'),
|
||||
{
|
||||
keepalive: true,
|
||||
|
@ -180,7 +186,9 @@ describe('WebSocket-Resource', () => {
|
|||
mockServer.on('connection', server => {
|
||||
server.on('close', done);
|
||||
});
|
||||
this.resource = new WebSocketResource(socket, { keepalive: true });
|
||||
this.resource = new window.textsecure.WebSocketResource(socket, {
|
||||
keepalive: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('allows resetting the keepalive timer', function thisNeeded2(done) {
|
||||
|
@ -190,10 +198,12 @@ describe('WebSocket-Resource', () => {
|
|||
const startTime = Date.now();
|
||||
mockServer.on('connection', server => {
|
||||
server.on('message', data => {
|
||||
const message = textsecure.protobuf.WebSocketMessage.decode(data);
|
||||
const message = window.textsecure.protobuf.WebSocketMessage.decode(
|
||||
data
|
||||
);
|
||||
assert.strictEqual(
|
||||
message.type,
|
||||
textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
||||
window.textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
||||
);
|
||||
assert.strictEqual(message.request.verb, 'GET');
|
||||
assert.strictEqual(message.request.path, '/');
|
||||
|
@ -205,7 +215,9 @@ describe('WebSocket-Resource', () => {
|
|||
done();
|
||||
});
|
||||
});
|
||||
const resource = new WebSocketResource(socket, { keepalive: true });
|
||||
const resource = new window.textsecure.WebSocketResource(socket, {
|
||||
keepalive: true,
|
||||
});
|
||||
setTimeout(() => {
|
||||
resource.resetKeepAliveTimer();
|
||||
}, 5000);
|
||||
|
|
|
@ -1,243 +0,0 @@
|
|||
/* global window, dcodeIO, Event, textsecure, FileReader, WebSocketResource */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
/*
|
||||
* WebSocket-Resources
|
||||
*
|
||||
* Create a request-response interface over websockets using the
|
||||
* WebSocket-Resources sub-protocol[1].
|
||||
*
|
||||
* var client = new WebSocketResource(socket, function(request) {
|
||||
* request.respond(200, 'OK');
|
||||
* });
|
||||
*
|
||||
* client.sendRequest({
|
||||
* verb: 'PUT',
|
||||
* path: '/v1/messages',
|
||||
* body: '{ some: "json" }',
|
||||
* success: function(message, status, request) {...},
|
||||
* error: function(message, status, request) {...}
|
||||
* });
|
||||
*
|
||||
* 1. https://github.com/signalapp/WebSocket-Resources
|
||||
*
|
||||
*/
|
||||
|
||||
const Request = function Request(options) {
|
||||
this.verb = options.verb || options.type;
|
||||
this.path = options.path || options.url;
|
||||
this.headers = options.headers;
|
||||
this.body = options.body || options.data;
|
||||
this.success = options.success;
|
||||
this.error = options.error;
|
||||
this.id = options.id;
|
||||
|
||||
if (this.id === undefined) {
|
||||
const bits = new Uint32Array(2);
|
||||
window.crypto.getRandomValues(bits);
|
||||
this.id = dcodeIO.Long.fromBits(bits[0], bits[1], true);
|
||||
}
|
||||
|
||||
if (this.body === undefined) {
|
||||
this.body = null;
|
||||
}
|
||||
};
|
||||
|
||||
const IncomingWebSocketRequest = function IncomingWebSocketRequest(options) {
|
||||
const request = new Request(options);
|
||||
const { socket } = options;
|
||||
|
||||
this.verb = request.verb;
|
||||
this.path = request.path;
|
||||
this.body = request.body;
|
||||
this.headers = request.headers;
|
||||
|
||||
this.respond = (status, message) => {
|
||||
socket.send(
|
||||
new textsecure.protobuf.WebSocketMessage({
|
||||
type: textsecure.protobuf.WebSocketMessage.Type.RESPONSE,
|
||||
response: { id: request.id, message, status },
|
||||
})
|
||||
.encode()
|
||||
.toArrayBuffer()
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
const outgoing = {};
|
||||
const OutgoingWebSocketRequest = function OutgoingWebSocketRequest(
|
||||
options,
|
||||
socket
|
||||
) {
|
||||
const request = new Request(options);
|
||||
outgoing[request.id] = request;
|
||||
socket.send(
|
||||
new textsecure.protobuf.WebSocketMessage({
|
||||
type: textsecure.protobuf.WebSocketMessage.Type.REQUEST,
|
||||
request: {
|
||||
verb: request.verb,
|
||||
path: request.path,
|
||||
body: request.body,
|
||||
headers: request.headers,
|
||||
id: request.id,
|
||||
},
|
||||
})
|
||||
.encode()
|
||||
.toArrayBuffer()
|
||||
);
|
||||
};
|
||||
|
||||
window.WebSocketResource = function WebSocketResource(socket, opts = {}) {
|
||||
let { handleRequest } = opts;
|
||||
if (typeof handleRequest !== 'function') {
|
||||
handleRequest = request => request.respond(404, 'Not found');
|
||||
}
|
||||
this.sendRequest = options => new OutgoingWebSocketRequest(options, socket);
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
socket.onmessage = socketMessage => {
|
||||
const blob = socketMessage.data;
|
||||
const handleArrayBuffer = buffer => {
|
||||
const message = textsecure.protobuf.WebSocketMessage.decode(buffer);
|
||||
if (
|
||||
message.type === textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
||||
) {
|
||||
handleRequest(
|
||||
new IncomingWebSocketRequest({
|
||||
verb: message.request.verb,
|
||||
path: message.request.path,
|
||||
body: message.request.body,
|
||||
headers: message.request.headers,
|
||||
id: message.request.id,
|
||||
socket,
|
||||
})
|
||||
);
|
||||
} else if (
|
||||
message.type === textsecure.protobuf.WebSocketMessage.Type.RESPONSE
|
||||
) {
|
||||
const { response } = message;
|
||||
const request = outgoing[response.id];
|
||||
if (request) {
|
||||
request.response = response;
|
||||
let callback = request.error;
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
callback = request.success;
|
||||
}
|
||||
|
||||
if (typeof callback === 'function') {
|
||||
callback(response.message, response.status, request);
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
`Received response for unknown request ${message.response.id}`
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (blob instanceof ArrayBuffer) {
|
||||
handleArrayBuffer(blob);
|
||||
} else {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => handleArrayBuffer(reader.result);
|
||||
reader.readAsArrayBuffer(blob);
|
||||
}
|
||||
};
|
||||
|
||||
if (opts.keepalive) {
|
||||
this.keepalive = new KeepAlive(this, {
|
||||
path: opts.keepalive.path,
|
||||
disconnect: opts.keepalive.disconnect,
|
||||
});
|
||||
const resetKeepAliveTimer = this.keepalive.reset.bind(this.keepalive);
|
||||
socket.addEventListener('open', resetKeepAliveTimer);
|
||||
socket.addEventListener('message', resetKeepAliveTimer);
|
||||
socket.addEventListener(
|
||||
'close',
|
||||
this.keepalive.stop.bind(this.keepalive)
|
||||
);
|
||||
}
|
||||
|
||||
socket.addEventListener('close', () => {
|
||||
this.closed = true;
|
||||
});
|
||||
|
||||
this.close = (code = 3000, reason) => {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.log.info('WebSocketResource.close()');
|
||||
if (this.keepalive) {
|
||||
this.keepalive.stop();
|
||||
}
|
||||
|
||||
socket.close(code, reason);
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
socket.onmessage = null;
|
||||
|
||||
// On linux the socket can wait a long time to emit its close event if we've
|
||||
// lost the internet connection. On the order of minutes. This speeds that
|
||||
// process up.
|
||||
setTimeout(() => {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
this.closed = true;
|
||||
|
||||
window.log.warn('Dispatching our own socket close event');
|
||||
const ev = new Event('close');
|
||||
ev.code = code;
|
||||
ev.reason = reason;
|
||||
this.dispatchEvent(ev);
|
||||
}, 5000);
|
||||
};
|
||||
};
|
||||
window.WebSocketResource.prototype = new textsecure.EventTarget();
|
||||
|
||||
function KeepAlive(websocketResource, opts = {}) {
|
||||
if (websocketResource instanceof WebSocketResource) {
|
||||
this.path = opts.path;
|
||||
if (this.path === undefined) {
|
||||
this.path = '/';
|
||||
}
|
||||
this.disconnect = opts.disconnect;
|
||||
if (this.disconnect === undefined) {
|
||||
this.disconnect = true;
|
||||
}
|
||||
this.wsr = websocketResource;
|
||||
} else {
|
||||
throw new TypeError('KeepAlive expected a WebSocketResource');
|
||||
}
|
||||
}
|
||||
|
||||
KeepAlive.prototype = {
|
||||
constructor: KeepAlive,
|
||||
stop() {
|
||||
clearTimeout(this.keepAliveTimer);
|
||||
clearTimeout(this.disconnectTimer);
|
||||
},
|
||||
reset() {
|
||||
clearTimeout(this.keepAliveTimer);
|
||||
clearTimeout(this.disconnectTimer);
|
||||
this.keepAliveTimer = setTimeout(() => {
|
||||
if (this.disconnect) {
|
||||
// automatically disconnect if server doesn't ack
|
||||
this.disconnectTimer = setTimeout(() => {
|
||||
clearTimeout(this.keepAliveTimer);
|
||||
this.wsr.close(3001, 'No response to keepalive request');
|
||||
}, 10000);
|
||||
} else {
|
||||
this.reset();
|
||||
}
|
||||
window.log.info('Sending a keepalive message');
|
||||
this.wsr.sendRequest({
|
||||
verb: 'GET',
|
||||
path: this.path,
|
||||
success: this.reset.bind(this),
|
||||
});
|
||||
}, 55000);
|
||||
},
|
||||
};
|
||||
})();
|
Loading…
Add table
Add a link
Reference in a new issue