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
16
Gruntfile.js
16
Gruntfile.js
|
@ -53,28 +53,12 @@ module.exports = grunt => {
|
||||||
footer: '})();\n',
|
footer: '})();\n',
|
||||||
},
|
},
|
||||||
src: [
|
src: [
|
||||||
'libtextsecure/errors.js',
|
|
||||||
'libtextsecure/libsignal-protocol.js',
|
'libtextsecure/libsignal-protocol.js',
|
||||||
'libtextsecure/protocol_wrapper.js',
|
'libtextsecure/protocol_wrapper.js',
|
||||||
|
|
||||||
'libtextsecure/crypto.js',
|
|
||||||
'libtextsecure/storage.js',
|
|
||||||
'libtextsecure/storage/user.js',
|
'libtextsecure/storage/user.js',
|
||||||
'libtextsecure/storage/groups.js',
|
|
||||||
'libtextsecure/storage/unprocessed.js',
|
'libtextsecure/storage/unprocessed.js',
|
||||||
'libtextsecure/protobufs.js',
|
'libtextsecure/protobufs.js',
|
||||||
'libtextsecure/helpers.js',
|
|
||||||
'libtextsecure/stringview.js',
|
|
||||||
'libtextsecure/event_target.js',
|
|
||||||
'libtextsecure/account_manager.js',
|
|
||||||
'libtextsecure/websocket-resources.js',
|
|
||||||
'libtextsecure/message_receiver.js',
|
|
||||||
'libtextsecure/outgoing_message.js',
|
|
||||||
'libtextsecure/sendmessage.js',
|
|
||||||
'libtextsecure/sync_request.js',
|
|
||||||
'libtextsecure/contacts_parser.js',
|
|
||||||
'libtextsecure/ProvisioningCipher.js',
|
|
||||||
'libtextsecure/task_with_timeout.js',
|
|
||||||
],
|
],
|
||||||
dest: 'js/libtextsecure.js',
|
dest: 'js/libtextsecure.js',
|
||||||
},
|
},
|
||||||
|
|
|
@ -2349,7 +2349,7 @@
|
||||||
sourceUuid: data.sourceUuid,
|
sourceUuid: data.sourceUuid,
|
||||||
sourceDevice: data.sourceDevice,
|
sourceDevice: data.sourceDevice,
|
||||||
sent_at: data.timestamp,
|
sent_at: data.timestamp,
|
||||||
received_at: data.receivedAt || Date.now(),
|
received_at: Date.now(),
|
||||||
conversationId,
|
conversationId,
|
||||||
unidentifiedDeliveryReceived: data.unidentifiedDeliveryReceived,
|
unidentifiedDeliveryReceived: data.unidentifiedDeliveryReceived,
|
||||||
type: 'incoming',
|
type: 'incoming',
|
||||||
|
|
|
@ -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,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 ourIdentityKeyBuffer = util.toArrayBuffer(ourIdentityKey.pubKey);
|
||||||
var theirIdentityKey = util.toArrayBuffer(session.indexInfo.remoteIdentityKey);
|
var theirIdentityKey = util.toArrayBuffer(session.indexInfo.remoteIdentityKey);
|
||||||
var macInput = new Uint8Array(encodedMsg.byteLength + 33*2 + 1);
|
var macInput = new Uint8Array(encodedMsg.byteLength + 33*2 + 1);
|
||||||
|
|
||||||
macInput.set(new Uint8Array(ourIdentityKeyBuffer));
|
macInput.set(new Uint8Array(ourIdentityKeyBuffer));
|
||||||
macInput.set(new Uint8Array(theirIdentityKey), 33);
|
macInput.set(new Uint8Array(theirIdentityKey), 33);
|
||||||
macInput[33*2] = (3 << 4) | 3;
|
macInput[33*2] = (3 << 4) | 3;
|
||||||
|
@ -36512,10 +36511,20 @@ Internal.SessionLock = {};
|
||||||
var jobQueue = {};
|
var jobQueue = {};
|
||||||
|
|
||||||
Internal.SessionLock.queueJobForNumber = function queueJobForNumber(number, runJob) {
|
Internal.SessionLock.queueJobForNumber = function queueJobForNumber(number, runJob) {
|
||||||
|
if (window.PQueue) {
|
||||||
jobQueue[number] = jobQueue[number] || new window.PQueue({ concurrency: 1 });
|
jobQueue[number] = jobQueue[number] || new window.PQueue({ concurrency: 1 });
|
||||||
var queue = jobQueue[number];
|
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 i = 0;
|
||||||
let buf = new Uint8Array(16);
|
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);
|
buf[i++] = parseInt(oct, 16);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
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', () => {
|
describe('ContactBuffer', () => {
|
||||||
function getTestBuffer() {
|
function getTestBuffer() {
|
||||||
const buffer = new dcodeIO.ByteBuffer();
|
const buffer = new dcodeIO.ByteBuffer();
|
||||||
|
@ -10,7 +8,7 @@ describe('ContactBuffer', () => {
|
||||||
}
|
}
|
||||||
avatarBuffer.limit = avatarBuffer.offset;
|
avatarBuffer.limit = avatarBuffer.offset;
|
||||||
avatarBuffer.offset = 0;
|
avatarBuffer.offset = 0;
|
||||||
const contactInfo = new textsecure.protobuf.ContactDetails({
|
const contactInfo = new window.textsecure.protobuf.ContactDetails({
|
||||||
name: 'Zero Cool',
|
name: 'Zero Cool',
|
||||||
number: '+10000000000',
|
number: '+10000000000',
|
||||||
uuid: '7198E1BD-1293-452A-A098-F982FF201902',
|
uuid: '7198E1BD-1293-452A-A098-F982FF201902',
|
||||||
|
@ -31,7 +29,7 @@ describe('ContactBuffer', () => {
|
||||||
|
|
||||||
it('parses an array buffer of contacts', () => {
|
it('parses an array buffer of contacts', () => {
|
||||||
const arrayBuffer = getTestBuffer();
|
const arrayBuffer = getTestBuffer();
|
||||||
const contactBuffer = new ContactBuffer(arrayBuffer);
|
const contactBuffer = new window.textsecure.ContactBuffer(arrayBuffer);
|
||||||
let contact = contactBuffer.next();
|
let contact = contactBuffer.next();
|
||||||
let count = 0;
|
let count = 0;
|
||||||
while (contact !== undefined) {
|
while (contact !== undefined) {
|
||||||
|
@ -62,7 +60,7 @@ describe('GroupBuffer', () => {
|
||||||
}
|
}
|
||||||
avatarBuffer.limit = avatarBuffer.offset;
|
avatarBuffer.limit = avatarBuffer.offset;
|
||||||
avatarBuffer.offset = 0;
|
avatarBuffer.offset = 0;
|
||||||
const groupInfo = new textsecure.protobuf.GroupDetails({
|
const groupInfo = new window.textsecure.protobuf.GroupDetails({
|
||||||
id: new Uint8Array([1, 3, 3, 7]).buffer,
|
id: new Uint8Array([1, 3, 3, 7]).buffer,
|
||||||
name: 'Hackers',
|
name: 'Hackers',
|
||||||
membersE164: ['cereal', 'burn', 'phreak', 'joey'],
|
membersE164: ['cereal', 'burn', 'phreak', 'joey'],
|
||||||
|
@ -89,7 +87,7 @@ describe('GroupBuffer', () => {
|
||||||
|
|
||||||
it('parses an array buffer of groups', () => {
|
it('parses an array buffer of groups', () => {
|
||||||
const arrayBuffer = getTestBuffer();
|
const arrayBuffer = getTestBuffer();
|
||||||
const groupBuffer = new GroupBuffer(arrayBuffer);
|
const groupBuffer = new window.textsecure.GroupBuffer(arrayBuffer);
|
||||||
let group = groupBuffer.next();
|
let group = groupBuffer.next();
|
||||||
let count = 0;
|
let count = 0;
|
||||||
while (group !== undefined) {
|
while (group !== undefined) {
|
||||||
|
|
|
@ -6,7 +6,7 @@ describe('Helpers', () => {
|
||||||
a[0] = 0;
|
a[0] = 0;
|
||||||
a[1] = 255;
|
a[1] = 255;
|
||||||
a[2] = 128;
|
a[2] = 128;
|
||||||
assert.equal(getString(b), '\x00\xff\x80');
|
assert.equal(window.textsecure.utils.getString(b), '\x00\xff\x80');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -15,13 +15,16 @@ describe('Helpers', () => {
|
||||||
const anArrayBuffer = new ArrayBuffer(1);
|
const anArrayBuffer = new ArrayBuffer(1);
|
||||||
const typedArray = new Uint8Array(anArrayBuffer);
|
const typedArray = new Uint8Array(anArrayBuffer);
|
||||||
typedArray[0] = 'a'.charCodeAt(0);
|
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', () => {
|
it('throws an error when passed a non string', () => {
|
||||||
const notStringable = [{}, undefined, null, new ArrayBuffer()];
|
const notStringable = [{}, undefined, null, new ArrayBuffer()];
|
||||||
notStringable.forEach(notString => {
|
notStringable.forEach(notString => {
|
||||||
assert.throw(() => {
|
assert.throw(() => {
|
||||||
stringToArrayBuffer(notString);
|
window.textsecure.utils.stringToArrayBuffer(notString);
|
||||||
}, Error);
|
}, Error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -19,23 +19,10 @@
|
||||||
|
|
||||||
<script type="text/javascript" src="../components.js"></script>
|
<script type="text/javascript" src="../components.js"></script>
|
||||||
<script type="text/javascript" src="../libsignal-protocol.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="../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="../storage/user.js" data-cover></script>
|
||||||
|
|
||||||
<script type="text/javascript" src="../protocol_wrapper.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/libphonenumber-util.js"></script>
|
||||||
<script type="text/javascript" src="../../js/components.js" data-cover></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>
|
<script type="text/javascript" src="../../js/signal_protocol_store.js" data-cover></script>
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
/* global textsecure, WebSocketResource */
|
|
||||||
|
|
||||||
describe('WebSocket-Resource', () => {
|
describe('WebSocket-Resource', () => {
|
||||||
describe('requests and responses', () => {
|
describe('requests and responses', () => {
|
||||||
it('receives requests and sends responses', done => {
|
it('receives requests and sends responses', done => {
|
||||||
|
@ -7,10 +5,12 @@ describe('WebSocket-Resource', () => {
|
||||||
const requestId = '1';
|
const requestId = '1';
|
||||||
const socket = {
|
const socket = {
|
||||||
send(data) {
|
send(data) {
|
||||||
const message = textsecure.protobuf.WebSocketMessage.decode(data);
|
const message = window.textsecure.protobuf.WebSocketMessage.decode(
|
||||||
|
data
|
||||||
|
);
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
message.type,
|
message.type,
|
||||||
textsecure.protobuf.WebSocketMessage.Type.RESPONSE
|
window.textsecure.protobuf.WebSocketMessage.Type.RESPONSE
|
||||||
);
|
);
|
||||||
assert.strictEqual(message.response.message, 'OK');
|
assert.strictEqual(message.response.message, 'OK');
|
||||||
assert.strictEqual(message.response.status, 200);
|
assert.strictEqual(message.response.status, 200);
|
||||||
|
@ -21,7 +21,7 @@ describe('WebSocket-Resource', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// actual test
|
// actual test
|
||||||
this.resource = new WebSocketResource(socket, {
|
this.resource = new window.textsecure.WebSocketResource(socket, {
|
||||||
handleRequest(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');
|
||||||
|
@ -36,8 +36,8 @@ describe('WebSocket-Resource', () => {
|
||||||
// mock socket request
|
// mock socket request
|
||||||
socket.onmessage({
|
socket.onmessage({
|
||||||
data: new Blob([
|
data: new Blob([
|
||||||
new textsecure.protobuf.WebSocketMessage({
|
new window.textsecure.protobuf.WebSocketMessage({
|
||||||
type: textsecure.protobuf.WebSocketMessage.Type.REQUEST,
|
type: window.textsecure.protobuf.WebSocketMessage.Type.REQUEST,
|
||||||
request: {
|
request: {
|
||||||
id: requestId,
|
id: requestId,
|
||||||
verb: 'PUT',
|
verb: 'PUT',
|
||||||
|
@ -56,10 +56,12 @@ describe('WebSocket-Resource', () => {
|
||||||
let requestId;
|
let requestId;
|
||||||
const socket = {
|
const socket = {
|
||||||
send(data) {
|
send(data) {
|
||||||
const message = textsecure.protobuf.WebSocketMessage.decode(data);
|
const message = window.textsecure.protobuf.WebSocketMessage.decode(
|
||||||
|
data
|
||||||
|
);
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
message.type,
|
message.type,
|
||||||
textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
window.textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
||||||
);
|
);
|
||||||
assert.strictEqual(message.request.verb, 'PUT');
|
assert.strictEqual(message.request.verb, 'PUT');
|
||||||
assert.strictEqual(message.request.path, '/some/path');
|
assert.strictEqual(message.request.path, '/some/path');
|
||||||
|
@ -73,7 +75,7 @@ describe('WebSocket-Resource', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// actual test
|
// actual test
|
||||||
const resource = new WebSocketResource(socket);
|
const resource = new window.textsecure.WebSocketResource(socket);
|
||||||
resource.sendRequest({
|
resource.sendRequest({
|
||||||
verb: 'PUT',
|
verb: 'PUT',
|
||||||
path: '/some/path',
|
path: '/some/path',
|
||||||
|
@ -89,8 +91,8 @@ describe('WebSocket-Resource', () => {
|
||||||
// mock socket response
|
// mock socket response
|
||||||
socket.onmessage({
|
socket.onmessage({
|
||||||
data: new Blob([
|
data: new Blob([
|
||||||
new textsecure.protobuf.WebSocketMessage({
|
new window.textsecure.protobuf.WebSocketMessage({
|
||||||
type: textsecure.protobuf.WebSocketMessage.Type.RESPONSE,
|
type: window.textsecure.protobuf.WebSocketMessage.Type.RESPONSE,
|
||||||
response: { id: requestId, message: 'OK', status: 200 },
|
response: { id: requestId, message: 'OK', status: 200 },
|
||||||
})
|
})
|
||||||
.encode()
|
.encode()
|
||||||
|
@ -112,7 +114,7 @@ describe('WebSocket-Resource', () => {
|
||||||
mockServer.on('connection', server => {
|
mockServer.on('connection', server => {
|
||||||
server.on('close', done);
|
server.on('close', done);
|
||||||
});
|
});
|
||||||
const resource = new WebSocketResource(
|
const resource = new window.textsecure.WebSocketResource(
|
||||||
new WebSocket('ws://localhost:8081')
|
new WebSocket('ws://localhost:8081')
|
||||||
);
|
);
|
||||||
resource.close();
|
resource.close();
|
||||||
|
@ -131,10 +133,12 @@ describe('WebSocket-Resource', () => {
|
||||||
const mockServer = new MockServer('ws://localhost:8081');
|
const mockServer = new MockServer('ws://localhost:8081');
|
||||||
mockServer.on('connection', server => {
|
mockServer.on('connection', server => {
|
||||||
server.on('message', data => {
|
server.on('message', data => {
|
||||||
const message = textsecure.protobuf.WebSocketMessage.decode(data);
|
const message = window.textsecure.protobuf.WebSocketMessage.decode(
|
||||||
|
data
|
||||||
|
);
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
message.type,
|
message.type,
|
||||||
textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
window.textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
||||||
);
|
);
|
||||||
assert.strictEqual(message.request.verb, 'GET');
|
assert.strictEqual(message.request.verb, 'GET');
|
||||||
assert.strictEqual(message.request.path, '/v1/keepalive');
|
assert.strictEqual(message.request.path, '/v1/keepalive');
|
||||||
|
@ -142,7 +146,7 @@ describe('WebSocket-Resource', () => {
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
this.resource = new WebSocketResource(
|
this.resource = new window.textsecure.WebSocketResource(
|
||||||
new WebSocket('ws://loc1alhost:8081'),
|
new WebSocket('ws://loc1alhost:8081'),
|
||||||
{
|
{
|
||||||
keepalive: { path: '/v1/keepalive' },
|
keepalive: { path: '/v1/keepalive' },
|
||||||
|
@ -154,10 +158,12 @@ describe('WebSocket-Resource', () => {
|
||||||
const mockServer = new MockServer('ws://localhost:8081');
|
const mockServer = new MockServer('ws://localhost:8081');
|
||||||
mockServer.on('connection', server => {
|
mockServer.on('connection', server => {
|
||||||
server.on('message', data => {
|
server.on('message', data => {
|
||||||
const message = textsecure.protobuf.WebSocketMessage.decode(data);
|
const message = window.textsecure.protobuf.WebSocketMessage.decode(
|
||||||
|
data
|
||||||
|
);
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
message.type,
|
message.type,
|
||||||
textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
window.textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
||||||
);
|
);
|
||||||
assert.strictEqual(message.request.verb, 'GET');
|
assert.strictEqual(message.request.verb, 'GET');
|
||||||
assert.strictEqual(message.request.path, '/');
|
assert.strictEqual(message.request.path, '/');
|
||||||
|
@ -165,7 +171,7 @@ describe('WebSocket-Resource', () => {
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
this.resource = new WebSocketResource(
|
this.resource = new window.textsecure.WebSocketResource(
|
||||||
new WebSocket('ws://localhost:8081'),
|
new WebSocket('ws://localhost:8081'),
|
||||||
{
|
{
|
||||||
keepalive: true,
|
keepalive: true,
|
||||||
|
@ -180,7 +186,9 @@ describe('WebSocket-Resource', () => {
|
||||||
mockServer.on('connection', server => {
|
mockServer.on('connection', server => {
|
||||||
server.on('close', done);
|
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) {
|
it('allows resetting the keepalive timer', function thisNeeded2(done) {
|
||||||
|
@ -190,10 +198,12 @@ describe('WebSocket-Resource', () => {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
mockServer.on('connection', server => {
|
mockServer.on('connection', server => {
|
||||||
server.on('message', data => {
|
server.on('message', data => {
|
||||||
const message = textsecure.protobuf.WebSocketMessage.decode(data);
|
const message = window.textsecure.protobuf.WebSocketMessage.decode(
|
||||||
|
data
|
||||||
|
);
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
message.type,
|
message.type,
|
||||||
textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
window.textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
||||||
);
|
);
|
||||||
assert.strictEqual(message.request.verb, 'GET');
|
assert.strictEqual(message.request.verb, 'GET');
|
||||||
assert.strictEqual(message.request.path, '/');
|
assert.strictEqual(message.request.path, '/');
|
||||||
|
@ -205,7 +215,9 @@ describe('WebSocket-Resource', () => {
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
const resource = new WebSocketResource(socket, { keepalive: true });
|
const resource = new window.textsecure.WebSocketResource(socket, {
|
||||||
|
keepalive: true,
|
||||||
|
});
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
resource.resetKeepAliveTimer();
|
resource.resetKeepAliveTimer();
|
||||||
}, 5000);
|
}, 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);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
})();
|
|
|
@ -222,9 +222,9 @@ try {
|
||||||
|
|
||||||
window.nodeSetImmediate = setImmediate;
|
window.nodeSetImmediate = setImmediate;
|
||||||
|
|
||||||
const { initialize: initializeWebAPI } = require('./ts/WebAPI');
|
window.textsecure = require('./ts/textsecure').default;
|
||||||
|
|
||||||
window.WebAPI = initializeWebAPI({
|
window.WebAPI = window.textsecure.WebAPI.initialize({
|
||||||
url: config.serverUrl,
|
url: config.serverUrl,
|
||||||
cdnUrl: config.cdnUrl,
|
cdnUrl: config.cdnUrl,
|
||||||
certificateAuthority: config.certificateAuthority,
|
certificateAuthority: config.certificateAuthority,
|
||||||
|
|
|
@ -29,7 +29,7 @@ const Signal = require('../js/modules/signal');
|
||||||
|
|
||||||
window.Signal = Signal.setup({});
|
window.Signal = Signal.setup({});
|
||||||
|
|
||||||
const { initialize: initializeWebAPI } = require('../ts/WebAPI');
|
const { initialize: initializeWebAPI } = require('../ts/textsecure/WebAPI');
|
||||||
|
|
||||||
const WebAPI = initializeWebAPI({
|
const WebAPI = initializeWebAPI({
|
||||||
url: config.serverUrl,
|
url: config.serverUrl,
|
||||||
|
|
215
ts/libsignal.d.ts
vendored
Normal file
215
ts/libsignal.d.ts
vendored
Normal file
|
@ -0,0 +1,215 @@
|
||||||
|
export type LibSignalType = {
|
||||||
|
externalCurve?: CurveType;
|
||||||
|
crypto: {
|
||||||
|
encrypt: (
|
||||||
|
key: ArrayBuffer,
|
||||||
|
data: ArrayBuffer,
|
||||||
|
iv: ArrayBuffer
|
||||||
|
) => Promise<ArrayBuffer>;
|
||||||
|
decrypt: (
|
||||||
|
key: ArrayBuffer,
|
||||||
|
data: ArrayBuffer,
|
||||||
|
iv: ArrayBuffer
|
||||||
|
) => Promise<ArrayBuffer>;
|
||||||
|
calculateMAC: (key: ArrayBuffer, data: ArrayBuffer) => Promise<ArrayBuffer>;
|
||||||
|
verifyMAC: (
|
||||||
|
data: ArrayBuffer,
|
||||||
|
key: ArrayBuffer,
|
||||||
|
mac: ArrayBuffer,
|
||||||
|
length: number
|
||||||
|
) => Promise<void>;
|
||||||
|
getRandomBytes: (size: number) => ArrayBuffer;
|
||||||
|
};
|
||||||
|
KeyHelper: {
|
||||||
|
generateIdentityKeyPair: () => Promise<{
|
||||||
|
privKey: ArrayBuffer;
|
||||||
|
pubKey: ArrayBuffer;
|
||||||
|
}>;
|
||||||
|
generateRegistrationId: () => number;
|
||||||
|
generateSignedPreKey: (
|
||||||
|
identityKeyPair: KeyPairType,
|
||||||
|
signedKeyId: number
|
||||||
|
) => Promise<SignedPreKeyType>;
|
||||||
|
generatePreKey: (keyId: number) => Promise<PreKeyType>;
|
||||||
|
};
|
||||||
|
Curve: {
|
||||||
|
generateKeyPair: () => KeyPairType;
|
||||||
|
createKeyPair: (privKey: ArrayBuffer) => KeyPairType;
|
||||||
|
calculateAgreement: (
|
||||||
|
pubKey: ArrayBuffer,
|
||||||
|
privKey: ArrayBuffer
|
||||||
|
) => ArrayBuffer;
|
||||||
|
verifySignature: (
|
||||||
|
pubKey: ArrayBuffer,
|
||||||
|
msg: ArrayBuffer,
|
||||||
|
sig: ArrayBuffer
|
||||||
|
) => void;
|
||||||
|
calculateSignature: (
|
||||||
|
privKey: ArrayBuffer,
|
||||||
|
message: ArrayBuffer
|
||||||
|
) => ArrayBuffer | Promise<ArrayBuffer>;
|
||||||
|
validatePubKeyFormat: (buffer: ArrayBuffer) => ArrayBuffer;
|
||||||
|
async: CurveType;
|
||||||
|
};
|
||||||
|
HKDF: {
|
||||||
|
deriveSecrets: (
|
||||||
|
packKey: ArrayBuffer,
|
||||||
|
salt: ArrayBuffer,
|
||||||
|
// The string is a bit crazy, but ProvisioningCipher currently passes in a string
|
||||||
|
info: ArrayBuffer | string
|
||||||
|
) => Promise<Array<ArrayBuffer>>;
|
||||||
|
};
|
||||||
|
worker: {
|
||||||
|
startWorker: () => void;
|
||||||
|
stopWorker: () => void;
|
||||||
|
};
|
||||||
|
FingerprintGenerator: typeof FingerprintGeneratorClass;
|
||||||
|
SessionBuilder: typeof SessionBuilderClass;
|
||||||
|
SessionCipher: typeof SessionCipherClass;
|
||||||
|
SignalProtocolAddress: typeof SignalProtocolAddressClass;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type KeyPairType = {
|
||||||
|
pubKey: ArrayBuffer;
|
||||||
|
privKey: ArrayBuffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SignedPreKeyType = {
|
||||||
|
keyId: number;
|
||||||
|
keyPair: KeyPairType;
|
||||||
|
signature: ArrayBuffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PreKeyType = {
|
||||||
|
keyId: number;
|
||||||
|
keyPair: KeyPairType;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RecordType = {
|
||||||
|
archiveCurrentState: () => void;
|
||||||
|
deleteAllSessions: () => void;
|
||||||
|
getOpenSession: () => void;
|
||||||
|
getSessionByBaseKey: () => void;
|
||||||
|
getSessions: () => void;
|
||||||
|
haveOpenSession: () => void;
|
||||||
|
promoteState: () => void;
|
||||||
|
serialize: () => void;
|
||||||
|
updateSessionState: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CurveType = {
|
||||||
|
generateKeyPair: () => Promise<KeyPairType>;
|
||||||
|
createKeyPair: (privKey: ArrayBuffer) => Promise<KeyPairType>;
|
||||||
|
calculateAgreement: (
|
||||||
|
pubKey: ArrayBuffer,
|
||||||
|
privKey: ArrayBuffer
|
||||||
|
) => Promise<ArrayBuffer>;
|
||||||
|
verifySignature: (
|
||||||
|
pubKey: ArrayBuffer,
|
||||||
|
msg: ArrayBuffer,
|
||||||
|
sig: ArrayBuffer
|
||||||
|
) => Promise<void>;
|
||||||
|
calculateSignature: (
|
||||||
|
privKey: ArrayBuffer,
|
||||||
|
message: ArrayBuffer
|
||||||
|
) => ArrayBuffer | Promise<ArrayBuffer>;
|
||||||
|
validatePubKeyFormat: (buffer: ArrayBuffer) => ArrayBuffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SessionRecordType = any;
|
||||||
|
|
||||||
|
export type StorageType = {
|
||||||
|
Direction: {
|
||||||
|
SENDING: number;
|
||||||
|
RECEIVING: number;
|
||||||
|
};
|
||||||
|
getIdentityKeyPair: () => Promise<KeyPairType>;
|
||||||
|
getLocalRegistrationId: () => Promise<number>;
|
||||||
|
isTrustedIdentity: () => Promise<void>;
|
||||||
|
loadPreKey: (
|
||||||
|
encodedAddress: string,
|
||||||
|
publicKey: ArrayBuffer | undefined,
|
||||||
|
direction: number
|
||||||
|
) => Promise<void>;
|
||||||
|
loadSession: (encodedAddress: string) => Promise<SessionRecordType>;
|
||||||
|
loadSignedPreKey: (keyId: number) => Promise<SignedPreKeyType>;
|
||||||
|
removePreKey: (keyId: number) => Promise<void>;
|
||||||
|
saveIdentity: (
|
||||||
|
encodedAddress: string,
|
||||||
|
publicKey: ArrayBuffer,
|
||||||
|
nonblockingApproval?: boolean
|
||||||
|
) => Promise<boolean>;
|
||||||
|
storeSession: (
|
||||||
|
encodedAddress: string,
|
||||||
|
record: SessionRecordType
|
||||||
|
) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
declare class FingerprintGeneratorClass {
|
||||||
|
constructor(iterations: number);
|
||||||
|
createFor: (
|
||||||
|
localIdentifier: string,
|
||||||
|
localIdentityKey: ArrayBuffer,
|
||||||
|
remoteIdentifier: string,
|
||||||
|
remoteIdentityKey: ArrayBuffer
|
||||||
|
) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare class SignalProtocolAddressClass {
|
||||||
|
static fromString(encodedAddress: string): SignalProtocolAddressClass;
|
||||||
|
constructor(name: string, deviceId: number);
|
||||||
|
getName: () => string;
|
||||||
|
getDeviceId: () => number;
|
||||||
|
toString: () => string;
|
||||||
|
equals: (other: SignalProtocolAddressClass) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeviceType = {
|
||||||
|
deviceId: number;
|
||||||
|
identityKey: ArrayBuffer;
|
||||||
|
registrationId: number;
|
||||||
|
signedPreKey: {
|
||||||
|
keyId: number;
|
||||||
|
publicKey: ArrayBuffer;
|
||||||
|
signature: ArrayBuffer;
|
||||||
|
};
|
||||||
|
preKey?: {
|
||||||
|
keyId: number;
|
||||||
|
publicKey: ArrayBuffer;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
declare class SessionBuilderClass {
|
||||||
|
constructor(storage: StorageType, remoteAddress: SignalProtocolAddressClass);
|
||||||
|
processPreKey: (device: DeviceType) => Promise<void>;
|
||||||
|
processV3: (record: RecordType, message: any) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare class SessionCipherClass {
|
||||||
|
constructor(
|
||||||
|
storage: StorageType,
|
||||||
|
remoteAddress: SignalProtocolAddressClass,
|
||||||
|
options?: any
|
||||||
|
);
|
||||||
|
closeOpenSessionForDevice: () => Promise<void>;
|
||||||
|
decryptPreKeyWhisperMessage: (
|
||||||
|
buffer: ArrayBuffer,
|
||||||
|
encoding?: string
|
||||||
|
) => Promise<ArrayBuffer>;
|
||||||
|
decryptWhisperMessage: (
|
||||||
|
buffer: ArrayBuffer,
|
||||||
|
encoding?: string
|
||||||
|
) => Promise<ArrayBuffer>;
|
||||||
|
deleteAllSessionsForDevice: () => Promise<void>;
|
||||||
|
encrypt: (
|
||||||
|
buffer: ArrayBuffer | Uint8Array,
|
||||||
|
encoding?: string
|
||||||
|
) => Promise<{
|
||||||
|
type: number;
|
||||||
|
registrationId: number;
|
||||||
|
body: string;
|
||||||
|
}>;
|
||||||
|
getRecord: () => Promise<RecordType>;
|
||||||
|
getRemoteRegistrationId: () => Promise<number>;
|
||||||
|
hasOpenSession: () => Promise<boolean>;
|
||||||
|
}
|
|
@ -1535,6 +1535,7 @@ async function updateToSchemaVersion20(
|
||||||
|
|
||||||
await instance.run('PRAGMA user_version = 20;');
|
await instance.run('PRAGMA user_version = 20;');
|
||||||
await instance.run('COMMIT TRANSACTION;');
|
await instance.run('COMMIT TRANSACTION;');
|
||||||
|
console.log('updateToSchemaVersion20: success!');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await instance.run('ROLLBACK;');
|
await instance.run('ROLLBACK;');
|
||||||
throw error;
|
throw error;
|
||||||
|
|
647
ts/textsecure.d.ts
vendored
Normal file
647
ts/textsecure.d.ts
vendored
Normal file
|
@ -0,0 +1,647 @@
|
||||||
|
import {
|
||||||
|
KeyPairType,
|
||||||
|
SessionRecordType,
|
||||||
|
SignedPreKeyType,
|
||||||
|
StorageType,
|
||||||
|
} from './libsignal.d';
|
||||||
|
import MessageReceiver from './textsecure/MessageReceiver';
|
||||||
|
import EventTarget from './textsecure/EventTarget';
|
||||||
|
import { ByteBufferClass } from './window.d';
|
||||||
|
|
||||||
|
type AttachmentType = any;
|
||||||
|
|
||||||
|
export type UnprocessedType = {
|
||||||
|
attempts: number;
|
||||||
|
decrypted?: string;
|
||||||
|
envelope?: string;
|
||||||
|
id: string;
|
||||||
|
serverTimestamp?: number;
|
||||||
|
source?: string;
|
||||||
|
sourceDevice?: number;
|
||||||
|
sourceUuid?: string;
|
||||||
|
version: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TextSecureType = {
|
||||||
|
createTaskWithTimeout: (
|
||||||
|
task: () => Promise<any>,
|
||||||
|
id?: string,
|
||||||
|
options?: { timeout?: number }
|
||||||
|
) => () => Promise<any>;
|
||||||
|
storage: {
|
||||||
|
user: {
|
||||||
|
getNumber: () => string;
|
||||||
|
getUuid: () => string | undefined;
|
||||||
|
getDeviceId: () => number | string;
|
||||||
|
getDeviceName: () => string;
|
||||||
|
getDeviceNameEncrypted: () => boolean;
|
||||||
|
setDeviceNameEncrypted: () => Promise<void>;
|
||||||
|
getSignalingKey: () => ArrayBuffer;
|
||||||
|
setNumberAndDeviceId: (
|
||||||
|
number: string,
|
||||||
|
deviceId: number,
|
||||||
|
deviceName?: string | null
|
||||||
|
) => Promise<void>;
|
||||||
|
setUuidAndDeviceId: (uuid: string, deviceId: number) => Promise<void>;
|
||||||
|
};
|
||||||
|
unprocessed: {
|
||||||
|
batchAdd: (dataArray: Array<UnprocessedType>) => Promise<void>;
|
||||||
|
remove: (id: string | Array<string>) => Promise<void>;
|
||||||
|
getCount: () => Promise<number>;
|
||||||
|
removeAll: () => Promise<void>;
|
||||||
|
getAll: () => Promise<Array<UnprocessedType>>;
|
||||||
|
updateAttempts: (id: string, attempts: number) => Promise<void>;
|
||||||
|
addDecryptedDataToList: (
|
||||||
|
array: Array<Partial<UnprocessedType>>
|
||||||
|
) => Promise<void>;
|
||||||
|
};
|
||||||
|
get: (key: string, defaultValue?: any) => any;
|
||||||
|
put: (key: string, value: any) => Promise<void>;
|
||||||
|
remove: (key: string | Array<string>) => Promise<void>;
|
||||||
|
protocol: StorageProtocolType;
|
||||||
|
};
|
||||||
|
messaging: {
|
||||||
|
sendStickerPackSync: (
|
||||||
|
operations: Array<{
|
||||||
|
packId: string;
|
||||||
|
packKey: string;
|
||||||
|
installed: boolean;
|
||||||
|
}>,
|
||||||
|
options: Object
|
||||||
|
) => Promise<void>;
|
||||||
|
};
|
||||||
|
protobuf: ProtobufCollectionType;
|
||||||
|
|
||||||
|
EventTarget: typeof EventTarget;
|
||||||
|
MessageReceiver: typeof MessageReceiver;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StoredSignedPreKeyType = SignedPreKeyType & {
|
||||||
|
confirmed?: boolean;
|
||||||
|
created_at: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type StorageProtocolType = StorageType & {
|
||||||
|
VerifiedStatus: {
|
||||||
|
DEFAULT: number;
|
||||||
|
VERIFIED: number;
|
||||||
|
UNVERIFIED: number;
|
||||||
|
};
|
||||||
|
archiveSiblingSessions: (identifier: string) => Promise<void>;
|
||||||
|
removeSession: (identifier: string) => Promise<void>;
|
||||||
|
getDeviceIds: (identifier: string) => Promise<Array<number>>;
|
||||||
|
hydrateCaches: () => Promise<void>;
|
||||||
|
clearPreKeyStore: () => Promise<void>;
|
||||||
|
clearSignedPreKeysStore: () => Promise<void>;
|
||||||
|
clearSessionStore: () => Promise<void>;
|
||||||
|
isTrustedIdentity: () => void;
|
||||||
|
storePreKey: (keyId: number, keyPair: KeyPairType) => Promise<void>;
|
||||||
|
storeSignedPreKey: (
|
||||||
|
keyId: number,
|
||||||
|
keyPair: KeyPairType,
|
||||||
|
confirmed?: boolean
|
||||||
|
) => Promise<void>;
|
||||||
|
loadSignedPreKeys: () => Promise<Array<StoredSignedPreKeyType>>;
|
||||||
|
saveIdentityWithAttributes: (
|
||||||
|
number: string,
|
||||||
|
options: {
|
||||||
|
publicKey: ArrayBuffer;
|
||||||
|
firstUse: boolean;
|
||||||
|
timestamp: number;
|
||||||
|
verified: number;
|
||||||
|
nonblockingApproval: boolean;
|
||||||
|
}
|
||||||
|
) => Promise<void>;
|
||||||
|
removeSignedPreKey: (keyId: number) => Promise<void>;
|
||||||
|
removeAllData: () => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Protobufs
|
||||||
|
|
||||||
|
type ProtobufCollectionType = {
|
||||||
|
AttachmentPointer: typeof AttachmentPointerClass;
|
||||||
|
ContactDetails: typeof ContactDetailsClass;
|
||||||
|
Content: typeof ContentClass;
|
||||||
|
DataMessage: typeof DataMessageClass;
|
||||||
|
DeviceName: typeof DeviceNameClass;
|
||||||
|
Envelope: typeof EnvelopeClass;
|
||||||
|
GroupContext: typeof GroupContextClass;
|
||||||
|
GroupDetails: typeof GroupDetailsClass;
|
||||||
|
NullMessage: typeof NullMessageClass;
|
||||||
|
ProvisioningUuid: typeof ProvisioningUuidClass;
|
||||||
|
ProvisionEnvelope: typeof ProvisionEnvelopeClass;
|
||||||
|
ProvisionMessage: typeof ProvisionMessageClass;
|
||||||
|
ReceiptMessage: typeof ReceiptMessageClass;
|
||||||
|
SyncMessage: typeof SyncMessageClass;
|
||||||
|
TypingMessage: typeof TypingMessageClass;
|
||||||
|
Verified: typeof VerifiedClass;
|
||||||
|
WebSocketMessage: typeof WebSocketMessageClass;
|
||||||
|
WebSocketRequestMessage: typeof WebSocketRequestMessageClass;
|
||||||
|
WebSocketResponseMessage: typeof WebSocketResponseMessageClass;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Note: there are a lot of places in the code that overwrite a field like this
|
||||||
|
// with a type that the app can use. Being more rigorous with these
|
||||||
|
// types would require code changes, out of scope for now.
|
||||||
|
type ProtoBinaryType = any;
|
||||||
|
type ProtoBigNumberType = any;
|
||||||
|
|
||||||
|
export declare class AttachmentPointerClass {
|
||||||
|
static decode: (
|
||||||
|
data: ArrayBuffer | ByteBufferClass,
|
||||||
|
encoding?: string
|
||||||
|
) => AttachmentPointerClass;
|
||||||
|
|
||||||
|
id?: ProtoBigNumberType;
|
||||||
|
contentType?: string;
|
||||||
|
key?: ProtoBinaryType;
|
||||||
|
size?: number;
|
||||||
|
thumbnail?: ProtoBinaryType;
|
||||||
|
digest?: ProtoBinaryType;
|
||||||
|
fileName?: string;
|
||||||
|
flags?: number;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
caption?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare class ContactDetailsClass {
|
||||||
|
static decode: (
|
||||||
|
data: ArrayBuffer | ByteBufferClass,
|
||||||
|
encoding?: string
|
||||||
|
) => ContactDetailsClass;
|
||||||
|
|
||||||
|
number?: string;
|
||||||
|
uuid?: string;
|
||||||
|
name?: string;
|
||||||
|
avatar?: ContactDetailsClass.Avatar;
|
||||||
|
color?: string;
|
||||||
|
verified?: VerifiedClass;
|
||||||
|
profileKey?: ProtoBinaryType;
|
||||||
|
blocked?: boolean;
|
||||||
|
expireTimer?: number;
|
||||||
|
inboxPosition?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: we need to use namespaces to express nested classes in Typescript
|
||||||
|
export declare namespace ContactDetailsClass {
|
||||||
|
class Avatar {
|
||||||
|
contentType?: string;
|
||||||
|
length?: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare class ContentClass {
|
||||||
|
static decode: (
|
||||||
|
data: ArrayBuffer | ByteBufferClass,
|
||||||
|
encoding?: string
|
||||||
|
) => ContentClass;
|
||||||
|
toArrayBuffer: () => ArrayBuffer;
|
||||||
|
|
||||||
|
dataMessage?: DataMessageClass;
|
||||||
|
syncMessage?: SyncMessageClass;
|
||||||
|
callMessage?: any;
|
||||||
|
nullMessage?: NullMessageClass;
|
||||||
|
receiptMessage?: ReceiptMessageClass;
|
||||||
|
typingMessage?: TypingMessageClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare class DataMessageClass {
|
||||||
|
static decode: (
|
||||||
|
data: ArrayBuffer | ByteBufferClass,
|
||||||
|
encoding?: string
|
||||||
|
) => DataMessageClass;
|
||||||
|
toArrayBuffer(): ArrayBuffer;
|
||||||
|
|
||||||
|
body?: string | null;
|
||||||
|
attachments?: Array<AttachmentPointerClass>;
|
||||||
|
group?: GroupContextClass | null;
|
||||||
|
flags?: number;
|
||||||
|
expireTimer?: number;
|
||||||
|
profileKey?: ProtoBinaryType;
|
||||||
|
timestamp?: ProtoBigNumberType;
|
||||||
|
quote?: DataMessageClass.Quote;
|
||||||
|
contact?: Array<DataMessageClass.Contact>;
|
||||||
|
preview?: Array<DataMessageClass.Preview>;
|
||||||
|
sticker?: DataMessageClass.Sticker;
|
||||||
|
requiredProtocolVersion?: number;
|
||||||
|
isViewOnce?: boolean;
|
||||||
|
reaction?: DataMessageClass.Reaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: we need to use namespaces to express nested classes in Typescript
|
||||||
|
export declare namespace DataMessageClass {
|
||||||
|
// Note: deep nesting
|
||||||
|
class Contact {
|
||||||
|
name: any;
|
||||||
|
number: any;
|
||||||
|
email: any;
|
||||||
|
address: any;
|
||||||
|
avatar: any;
|
||||||
|
organization?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Flags {
|
||||||
|
static END_SESSION: number;
|
||||||
|
static EXPIRATION_TIMER_UPDATE: number;
|
||||||
|
static PROFILE_KEY_UPDATE: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Preview {
|
||||||
|
url?: string;
|
||||||
|
title?: string;
|
||||||
|
image?: AttachmentPointerClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProtocolVersion {
|
||||||
|
static INITIAL: number;
|
||||||
|
static MESSAGE_TIMERS: number;
|
||||||
|
static VIEW_ONCE: number;
|
||||||
|
static VIEW_ONCE_VIDEO: number;
|
||||||
|
static REACTIONS: number;
|
||||||
|
static CURRENT: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: deep nesting
|
||||||
|
class Quote {
|
||||||
|
id?: ProtoBigNumberType;
|
||||||
|
author?: string;
|
||||||
|
authorUuid?: string;
|
||||||
|
text?: string;
|
||||||
|
attachments?: Array<DataMessageClass.Quote.QuotedAttachment>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Reaction {
|
||||||
|
emoji?: string;
|
||||||
|
remove?: boolean;
|
||||||
|
targetAuthorE164?: string;
|
||||||
|
targetAuthorUuid?: string;
|
||||||
|
targetTimestamp?: ProtoBigNumberType;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Sticker {
|
||||||
|
packId?: ProtoBinaryType;
|
||||||
|
packKey?: ProtoBinaryType;
|
||||||
|
stickerId?: number;
|
||||||
|
data?: AttachmentPointerClass;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: we need to use namespaces to express nested classes in Typescript
|
||||||
|
export declare namespace DataMessageClass.Quote {
|
||||||
|
class QuotedAttachment {
|
||||||
|
contentType?: string;
|
||||||
|
fileName?: string;
|
||||||
|
thumbnail?: AttachmentPointerClass;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare class DeviceNameClass {
|
||||||
|
static decode: (
|
||||||
|
data: ArrayBuffer | ByteBufferClass,
|
||||||
|
encoding?: string
|
||||||
|
) => DeviceNameClass;
|
||||||
|
encode: () => DeviceNameClass;
|
||||||
|
toArrayBuffer: () => ArrayBuffer;
|
||||||
|
|
||||||
|
ephemeralPublic: ProtoBinaryType;
|
||||||
|
syntheticIv: ProtoBinaryType;
|
||||||
|
ciphertext: ProtoBinaryType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare class EnvelopeClass {
|
||||||
|
static decode: (
|
||||||
|
data: ArrayBuffer | ByteBufferClass,
|
||||||
|
encoding?: string
|
||||||
|
) => EnvelopeClass;
|
||||||
|
|
||||||
|
type?: number;
|
||||||
|
source?: string;
|
||||||
|
sourceUuid?: string;
|
||||||
|
sourceDevice?: number;
|
||||||
|
relay?: string;
|
||||||
|
timestamp?: ProtoBigNumberType;
|
||||||
|
legacyMessage?: ProtoBinaryType;
|
||||||
|
content?: ProtoBinaryType;
|
||||||
|
serverGuid?: string;
|
||||||
|
serverTimestamp?: ProtoBigNumberType;
|
||||||
|
|
||||||
|
// Note: these additional properties are added in the course of processing
|
||||||
|
id: string;
|
||||||
|
unidentifiedDeliveryReceived?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: we need to use namespaces to express nested classes in Typescript
|
||||||
|
export declare namespace EnvelopeClass {
|
||||||
|
class Type {
|
||||||
|
static CIPHERTEXT: number;
|
||||||
|
static PREKEY_BUNDLE: number;
|
||||||
|
static RECEIPT: number;
|
||||||
|
static UNIDENTIFIED_SENDER: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare class GroupContextClass {
|
||||||
|
static decode: (
|
||||||
|
data: ArrayBuffer | ByteBufferClass,
|
||||||
|
encoding?: string
|
||||||
|
) => GroupContextClass;
|
||||||
|
|
||||||
|
id?: ProtoBinaryType;
|
||||||
|
type?: number;
|
||||||
|
name?: string | null;
|
||||||
|
membersE164?: Array<string>;
|
||||||
|
members?: Array<GroupContextClass.Member>;
|
||||||
|
avatar?: AttachmentPointerClass | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: we need to use namespaces to express nested classes in Typescript
|
||||||
|
export declare namespace GroupContextClass {
|
||||||
|
class Member {
|
||||||
|
uuid?: string;
|
||||||
|
e164?: string;
|
||||||
|
}
|
||||||
|
class Type {
|
||||||
|
static UNKNOWN: number;
|
||||||
|
static UPDATE: number;
|
||||||
|
static DELIVER: number;
|
||||||
|
static QUIT: number;
|
||||||
|
static REQUEST_INFO: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare class GroupDetailsClass {
|
||||||
|
static decode: (
|
||||||
|
data: ArrayBuffer | ByteBufferClass,
|
||||||
|
encoding?: string
|
||||||
|
) => GroupDetailsClass;
|
||||||
|
|
||||||
|
id?: ProtoBinaryType;
|
||||||
|
name?: string;
|
||||||
|
membersE164?: Array<string>;
|
||||||
|
members?: Array<GroupDetailsClass.Member>;
|
||||||
|
avatar?: GroupDetailsClass.Avatar;
|
||||||
|
active?: boolean;
|
||||||
|
expireTimer?: number;
|
||||||
|
color?: string;
|
||||||
|
blocked?: boolean;
|
||||||
|
inboxPosition?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: we need to use namespaces to express nested classes in Typescript
|
||||||
|
export declare namespace GroupDetailsClass {
|
||||||
|
class Avatar {
|
||||||
|
contentType?: string;
|
||||||
|
length?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Member {
|
||||||
|
uuid?: string;
|
||||||
|
e164?: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare class NullMessageClass {
|
||||||
|
static decode: (
|
||||||
|
data: ArrayBuffer | ByteBufferClass,
|
||||||
|
encoding?: string
|
||||||
|
) => NullMessageClass;
|
||||||
|
|
||||||
|
padding?: ProtoBinaryType;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare class ProvisioningUuidClass {
|
||||||
|
static decode: (
|
||||||
|
data: ArrayBuffer | ByteBufferClass,
|
||||||
|
encoding?: string
|
||||||
|
) => ProvisioningUuidClass;
|
||||||
|
encode: () => ProvisioningUuidClass;
|
||||||
|
toArrayBuffer: () => ArrayBuffer;
|
||||||
|
|
||||||
|
uuid?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare class ProvisionEnvelopeClass {
|
||||||
|
static decode: (
|
||||||
|
data: ArrayBuffer | ByteBufferClass,
|
||||||
|
encoding?: string
|
||||||
|
) => ProvisionEnvelopeClass;
|
||||||
|
encode: () => ProvisionEnvelopeClass;
|
||||||
|
toArrayBuffer: () => ArrayBuffer;
|
||||||
|
|
||||||
|
publicKey?: ProtoBinaryType;
|
||||||
|
body?: ProtoBinaryType;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare class ProvisionMessageClass {
|
||||||
|
static decode: (
|
||||||
|
data: ArrayBuffer | ByteBufferClass,
|
||||||
|
encoding?: string
|
||||||
|
) => ProvisionMessageClass;
|
||||||
|
encode: () => ProvisionMessageClass;
|
||||||
|
toArrayBuffer: () => ArrayBuffer;
|
||||||
|
|
||||||
|
identityKeyPrivate?: ProtoBinaryType;
|
||||||
|
number?: string;
|
||||||
|
uuid?: string;
|
||||||
|
provisioningCode?: string;
|
||||||
|
userAgent?: string;
|
||||||
|
profileKey?: ProtoBinaryType;
|
||||||
|
readReceipts?: boolean;
|
||||||
|
ProvisioningVersion?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare class ReceiptMessageClass {
|
||||||
|
static decode: (
|
||||||
|
data: ArrayBuffer | ByteBufferClass,
|
||||||
|
encoding?: string
|
||||||
|
) => ReceiptMessageClass;
|
||||||
|
|
||||||
|
type?: number;
|
||||||
|
timestamp?: ProtoBigNumberType;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: we need to use namespaces to express nested classes in Typescript
|
||||||
|
export declare namespace ReceiptMessageClass {
|
||||||
|
class Type {
|
||||||
|
static DELIVERY: number;
|
||||||
|
static READ: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare class SyncMessageClass {
|
||||||
|
static decode: (
|
||||||
|
data: ArrayBuffer | ByteBufferClass,
|
||||||
|
encoding?: string
|
||||||
|
) => SyncMessageClass;
|
||||||
|
|
||||||
|
sent?: SyncMessageClass.Sent;
|
||||||
|
contacts?: SyncMessageClass.Contacts;
|
||||||
|
groups?: SyncMessageClass.Groups;
|
||||||
|
request?: SyncMessageClass.Request;
|
||||||
|
read?: Array<SyncMessageClass.Read>;
|
||||||
|
blocked?: SyncMessageClass.Blocked;
|
||||||
|
verified?: VerifiedClass;
|
||||||
|
configuration?: SyncMessageClass.Configuration;
|
||||||
|
padding?: ProtoBinaryType;
|
||||||
|
stickerPackOperation?: Array<SyncMessageClass.StickerPackOperation>;
|
||||||
|
viewOnceOpen?: SyncMessageClass.ViewOnceOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: we need to use namespaces to express nested classes in Typescript
|
||||||
|
export declare namespace SyncMessageClass {
|
||||||
|
class Configuration {
|
||||||
|
readReceipts?: boolean;
|
||||||
|
unidentifiedDeliveryIndicators?: boolean;
|
||||||
|
typingIndicators?: boolean;
|
||||||
|
linkPreviews?: boolean;
|
||||||
|
}
|
||||||
|
class Contacts {
|
||||||
|
blob?: AttachmentPointerClass;
|
||||||
|
complete?: boolean;
|
||||||
|
}
|
||||||
|
class Groups {
|
||||||
|
blob?: AttachmentPointerClass;
|
||||||
|
}
|
||||||
|
class Blocked {
|
||||||
|
numbers?: Array<string>;
|
||||||
|
uuids?: Array<string>;
|
||||||
|
groupIds?: Array<ProtoBinaryType>;
|
||||||
|
}
|
||||||
|
class Read {
|
||||||
|
sender?: string;
|
||||||
|
senderUuid?: string;
|
||||||
|
timestamp?: ProtoBigNumberType;
|
||||||
|
}
|
||||||
|
class Request {
|
||||||
|
type?: number;
|
||||||
|
}
|
||||||
|
class Sent {
|
||||||
|
destination?: string;
|
||||||
|
destinationUuid?: string;
|
||||||
|
timestamp?: ProtoBigNumberType;
|
||||||
|
message?: DataMessageClass;
|
||||||
|
expirationStartTimestamp?: ProtoBigNumberType;
|
||||||
|
unidentifiedStatus?: Array<
|
||||||
|
SyncMessageClass.Sent.UnidentifiedDeliveryStatus
|
||||||
|
>;
|
||||||
|
isRecipientUpdate?: boolean;
|
||||||
|
}
|
||||||
|
class StickerPackOperation {
|
||||||
|
packId?: ProtoBinaryType;
|
||||||
|
packKey?: ProtoBinaryType;
|
||||||
|
type?: number;
|
||||||
|
}
|
||||||
|
class ViewOnceOpen {
|
||||||
|
sender?: string;
|
||||||
|
senderUuid?: string;
|
||||||
|
timestamp?: ProtoBinaryType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: we need to use namespaces to express nested classes in Typescript
|
||||||
|
export declare namespace SyncMessageClass.Request {
|
||||||
|
class Type {
|
||||||
|
static UNKNOWN: number;
|
||||||
|
static BLOCKED: number;
|
||||||
|
static CONFIGURATION: number;
|
||||||
|
static CONTACTS: number;
|
||||||
|
static GROUPS: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: we need to use namespaces to express nested classes in Typescript
|
||||||
|
export declare namespace SyncMessageClass.Sent {
|
||||||
|
class UnidentifiedDeliveryStatus {
|
||||||
|
destination?: string;
|
||||||
|
destinationUuid?: string;
|
||||||
|
unidentified?: boolean;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: we need to use namespaces to express nested classes in Typescript
|
||||||
|
export declare namespace SyncMessageClass.StickerPackOperation {
|
||||||
|
class Type {
|
||||||
|
static INSTALL: number;
|
||||||
|
static REMOVE: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare class TypingMessageClass {
|
||||||
|
static decode: (
|
||||||
|
data: ArrayBuffer | ByteBufferClass,
|
||||||
|
encoding?: string
|
||||||
|
) => TypingMessageClass;
|
||||||
|
|
||||||
|
timestamp?: ProtoBigNumberType;
|
||||||
|
action?: number;
|
||||||
|
groupId?: ProtoBinaryType;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: we need to use namespaces to express nested classes in Typescript
|
||||||
|
export declare namespace TypingMessageClass {
|
||||||
|
class Action {
|
||||||
|
static STARTED: number;
|
||||||
|
static STOPPED: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare class VerifiedClass {
|
||||||
|
static decode: (
|
||||||
|
data: ArrayBuffer | ByteBufferClass,
|
||||||
|
encoding?: string
|
||||||
|
) => VerifiedClass;
|
||||||
|
|
||||||
|
destination?: string;
|
||||||
|
destinationUuid?: string;
|
||||||
|
identityKey?: ProtoBinaryType;
|
||||||
|
state?: number;
|
||||||
|
nullMessage?: ProtoBinaryType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare class WebSocketMessageClass {
|
||||||
|
constructor(data: any);
|
||||||
|
encode: () => WebSocketMessageClass;
|
||||||
|
toArrayBuffer: () => ArrayBuffer;
|
||||||
|
static decode: (
|
||||||
|
data: ArrayBuffer | ByteBufferClass,
|
||||||
|
encoding?: string
|
||||||
|
) => WebSocketMessageClass;
|
||||||
|
|
||||||
|
type?: number;
|
||||||
|
request?: WebSocketRequestMessageClass;
|
||||||
|
response?: WebSocketResponseMessageClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: we need to use namespaces to express nested classes in Typescript
|
||||||
|
export declare namespace WebSocketMessageClass {
|
||||||
|
class Type {
|
||||||
|
static UNKNOWN: number;
|
||||||
|
static REQUEST: number;
|
||||||
|
static RESPONSE: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare class WebSocketRequestMessageClass {
|
||||||
|
static decode: (
|
||||||
|
data: ArrayBuffer | ByteBufferClass,
|
||||||
|
encoding?: string
|
||||||
|
) => WebSocketRequestMessageClass;
|
||||||
|
verb?: string;
|
||||||
|
path?: string;
|
||||||
|
body?: ProtoBinaryType;
|
||||||
|
headers?: Array<string>;
|
||||||
|
id?: ProtoBigNumberType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare class WebSocketResponseMessageClass {
|
||||||
|
static decode: (
|
||||||
|
data: ArrayBuffer | ByteBufferClass,
|
||||||
|
encoding?: string
|
||||||
|
) => WebSocketResponseMessageClass;
|
||||||
|
id?: ProtoBigNumberType;
|
||||||
|
status?: number;
|
||||||
|
message?: string;
|
||||||
|
headers?: Array<string>;
|
||||||
|
body?: ProtoBinaryType;
|
||||||
|
}
|
699
ts/textsecure/AccountManager.ts
Normal file
699
ts/textsecure/AccountManager.ts
Normal file
|
@ -0,0 +1,699 @@
|
||||||
|
// tslint:disable no-backbone-get-set-outside-model no-default-export no-unnecessary-local-variable
|
||||||
|
|
||||||
|
import EventTarget from './EventTarget';
|
||||||
|
import { WebAPIType } from './WebAPI';
|
||||||
|
import MessageReceiver from './MessageReceiver';
|
||||||
|
import { KeyPairType, SignedPreKeyType } from '../libsignal.d';
|
||||||
|
import utils from './Helpers';
|
||||||
|
import PQueue from 'p-queue';
|
||||||
|
import ProvisioningCipher from './ProvisioningCipher';
|
||||||
|
import WebSocketResource, {
|
||||||
|
IncomingWebSocketRequest,
|
||||||
|
} from './WebsocketResources';
|
||||||
|
|
||||||
|
const ARCHIVE_AGE = 7 * 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
function getIdentifier(id: string) {
|
||||||
|
if (!id || !id.length) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = id.split('.');
|
||||||
|
if (!parts.length) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
type GeneratedKeysType = {
|
||||||
|
preKeys: Array<{
|
||||||
|
keyId: number;
|
||||||
|
publicKey: ArrayBuffer;
|
||||||
|
}>;
|
||||||
|
signedPreKey: {
|
||||||
|
keyId: number;
|
||||||
|
publicKey: ArrayBuffer;
|
||||||
|
signature: ArrayBuffer;
|
||||||
|
keyPair: KeyPairType;
|
||||||
|
};
|
||||||
|
identityKey: ArrayBuffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class AccountManager extends EventTarget {
|
||||||
|
server: WebAPIType;
|
||||||
|
pending: Promise<void>;
|
||||||
|
pendingQueue?: PQueue;
|
||||||
|
|
||||||
|
constructor(username: string, password: string) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.server = window.WebAPI.connect({ username, password });
|
||||||
|
this.pending = Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
async requestVoiceVerification(number: string) {
|
||||||
|
return this.server.requestVerificationVoice(number);
|
||||||
|
}
|
||||||
|
async requestSMSVerification(number: string) {
|
||||||
|
return this.server.requestVerificationSMS(number);
|
||||||
|
}
|
||||||
|
async encryptDeviceName(name: string, providedIdentityKey?: KeyPairType) {
|
||||||
|
if (!name) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const identityKey =
|
||||||
|
providedIdentityKey ||
|
||||||
|
(await window.textsecure.storage.protocol.getIdentityKeyPair());
|
||||||
|
if (!identityKey) {
|
||||||
|
throw new Error('Identity key was not provided and is not in database!');
|
||||||
|
}
|
||||||
|
const encrypted = await window.Signal.Crypto.encryptDeviceName(
|
||||||
|
name,
|
||||||
|
identityKey.pubKey
|
||||||
|
);
|
||||||
|
|
||||||
|
const proto = new window.textsecure.protobuf.DeviceName();
|
||||||
|
proto.ephemeralPublic = encrypted.ephemeralPublic;
|
||||||
|
proto.syntheticIv = encrypted.syntheticIv;
|
||||||
|
proto.ciphertext = encrypted.ciphertext;
|
||||||
|
|
||||||
|
const arrayBuffer = proto.encode().toArrayBuffer();
|
||||||
|
return MessageReceiver.arrayBufferToStringBase64(arrayBuffer);
|
||||||
|
}
|
||||||
|
async decryptDeviceName(base64: string) {
|
||||||
|
const identityKey = await window.textsecure.storage.protocol.getIdentityKeyPair();
|
||||||
|
|
||||||
|
const arrayBuffer = MessageReceiver.stringToArrayBufferBase64(base64);
|
||||||
|
const proto = window.textsecure.protobuf.DeviceName.decode(arrayBuffer);
|
||||||
|
const encrypted = {
|
||||||
|
ephemeralPublic: proto.ephemeralPublic.toArrayBuffer(),
|
||||||
|
syntheticIv: proto.syntheticIv.toArrayBuffer(),
|
||||||
|
ciphertext: proto.ciphertext.toArrayBuffer(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const name = await window.Signal.Crypto.decryptDeviceName(
|
||||||
|
encrypted,
|
||||||
|
identityKey.privKey
|
||||||
|
);
|
||||||
|
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
async maybeUpdateDeviceName() {
|
||||||
|
const isNameEncrypted = window.textsecure.storage.user.getDeviceNameEncrypted();
|
||||||
|
if (isNameEncrypted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const deviceName = window.textsecure.storage.user.getDeviceName();
|
||||||
|
const base64 = await this.encryptDeviceName(deviceName);
|
||||||
|
|
||||||
|
if (base64) {
|
||||||
|
await this.server.updateDeviceName(base64);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async deviceNameIsEncrypted() {
|
||||||
|
await window.textsecure.storage.user.setDeviceNameEncrypted();
|
||||||
|
}
|
||||||
|
async maybeDeleteSignalingKey() {
|
||||||
|
const key = window.textsecure.storage.user.getSignalingKey();
|
||||||
|
if (key) {
|
||||||
|
await this.server.removeSignalingKey();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async registerSingleDevice(number: string, verificationCode: string) {
|
||||||
|
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(async () =>
|
||||||
|
window.libsignal.KeyHelper.generateIdentityKeyPair().then(
|
||||||
|
async identityKeyPair => {
|
||||||
|
const profileKey = window.libsignal.crypto.getRandomBytes(32);
|
||||||
|
const accessKey = await window.Signal.Crypto.deriveAccessKey(
|
||||||
|
profileKey
|
||||||
|
);
|
||||||
|
|
||||||
|
return createAccount(
|
||||||
|
number,
|
||||||
|
verificationCode,
|
||||||
|
identityKeyPair,
|
||||||
|
profileKey,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
{ accessKey }
|
||||||
|
)
|
||||||
|
.then(clearSessionsAndPreKeys)
|
||||||
|
.then(async () => generateKeys())
|
||||||
|
.then(async (keys: GeneratedKeysType) =>
|
||||||
|
registerKeys(keys).then(async () => confirmKeys(keys))
|
||||||
|
)
|
||||||
|
.then(async () => registrationDone({ number }));
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// tslint:disable-next-line max-func-body-length
|
||||||
|
async registerSecondDevice(
|
||||||
|
setProvisioningUrl: Function,
|
||||||
|
confirmNumber: (number?: string) => Promise<string>,
|
||||||
|
progressCallback: Function
|
||||||
|
) {
|
||||||
|
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 ProvisioningCipher();
|
||||||
|
let gotProvisionEnvelope = false;
|
||||||
|
return provisioningCipher.getPublicKey().then(
|
||||||
|
async (pubKey: ArrayBuffer) =>
|
||||||
|
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: IncomingWebSocketRequest) {
|
||||||
|
if (
|
||||||
|
request.path === '/v1/address' &&
|
||||||
|
request.verb === 'PUT' &&
|
||||||
|
request.body
|
||||||
|
) {
|
||||||
|
const proto = window.textsecure.protobuf.ProvisioningUuid.decode(
|
||||||
|
request.body
|
||||||
|
);
|
||||||
|
setProvisioningUrl(
|
||||||
|
[
|
||||||
|
'tsdevice:/?uuid=',
|
||||||
|
proto.uuid,
|
||||||
|
'&pub_key=',
|
||||||
|
encodeURIComponent(btoa(utils.getString(pubKey))),
|
||||||
|
].join('')
|
||||||
|
);
|
||||||
|
request.respond(200, 'OK');
|
||||||
|
} else if (
|
||||||
|
request.path === '/v1/message' &&
|
||||||
|
request.verb === 'PUT' &&
|
||||||
|
request.body
|
||||||
|
) {
|
||||||
|
const envelope = window.textsecure.protobuf.ProvisionEnvelope.decode(
|
||||||
|
request.body,
|
||||||
|
'binary'
|
||||||
|
);
|
||||||
|
request.respond(200, 'OK');
|
||||||
|
gotProvisionEnvelope = true;
|
||||||
|
wsr.close();
|
||||||
|
resolve(
|
||||||
|
provisioningCipher
|
||||||
|
.decrypt(envelope)
|
||||||
|
.then(async provisionMessage =>
|
||||||
|
queueTask(async () =>
|
||||||
|
confirmNumber(provisionMessage.number).then(
|
||||||
|
async deviceName => {
|
||||||
|
if (
|
||||||
|
typeof deviceName !== 'string' ||
|
||||||
|
deviceName.length === 0
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
'AccountManager.registerSecondDevice: Invalid device name'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!provisionMessage.number ||
|
||||||
|
!provisionMessage.provisioningCode ||
|
||||||
|
!provisionMessage.identityKeyPair
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
'AccountManager.registerSecondDevice: Provision message was missing key data'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return createAccount(
|
||||||
|
provisionMessage.number,
|
||||||
|
provisionMessage.provisioningCode,
|
||||||
|
provisionMessage.identityKeyPair,
|
||||||
|
provisionMessage.profileKey,
|
||||||
|
deviceName,
|
||||||
|
provisionMessage.userAgent,
|
||||||
|
provisionMessage.readReceipts,
|
||||||
|
{ uuid: provisionMessage.uuid }
|
||||||
|
)
|
||||||
|
.then(clearSessionsAndPreKeys)
|
||||||
|
.then(generateKeys)
|
||||||
|
.then(async (keys: GeneratedKeysType) =>
|
||||||
|
registerKeys(keys).then(async () =>
|
||||||
|
confirmKeys(keys)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.then(async () =>
|
||||||
|
registrationDone(provisionMessage)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
window.log.error('Unknown websocket message', request.path);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
async refreshPreKeys() {
|
||||||
|
const generateKeys = this.generateKeys.bind(this, 100);
|
||||||
|
const registerKeys = this.server.registerKeys.bind(this.server);
|
||||||
|
|
||||||
|
return this.queueTask(async () =>
|
||||||
|
this.server.getMyKeys().then(async preKeyCount => {
|
||||||
|
window.log.info(`prekey count ${preKeyCount}`);
|
||||||
|
if (preKeyCount < 10) {
|
||||||
|
return generateKeys().then(registerKeys);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
async rotateSignedPreKey() {
|
||||||
|
return this.queueTask(async () => {
|
||||||
|
const signedKeyId = window.textsecure.storage.get('signedKeyId', 1);
|
||||||
|
if (typeof signedKeyId !== 'number') {
|
||||||
|
throw new Error('Invalid signedKeyId');
|
||||||
|
}
|
||||||
|
|
||||||
|
const store = window.textsecure.storage.protocol;
|
||||||
|
const { server, cleanSignedPreKeys } = this;
|
||||||
|
|
||||||
|
return store
|
||||||
|
.getIdentityKeyPair()
|
||||||
|
.then(
|
||||||
|
async (identityKey: KeyPairType) =>
|
||||||
|
window.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.'
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(async (res: SignedPreKeyType | null) => {
|
||||||
|
if (!res) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
window.log.info('Saving new signed prekey', res.keyId);
|
||||||
|
return Promise.all([
|
||||||
|
window.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(async () => {
|
||||||
|
const confirmed = true;
|
||||||
|
window.log.info('Confirming new signed prekey', res.keyId);
|
||||||
|
return Promise.all([
|
||||||
|
window.textsecure.storage.remove('signedKeyRotationRejected'),
|
||||||
|
store.storeSignedPreKey(res.keyId, res.keyPair, confirmed),
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
.then(cleanSignedPreKeys);
|
||||||
|
})
|
||||||
|
.catch(async (e: Error) => {
|
||||||
|
window.log.error(
|
||||||
|
'rotateSignedPrekey error:',
|
||||||
|
e && e.stack ? e.stack : e
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
e instanceof Error &&
|
||||||
|
e.name === 'HTTPError' &&
|
||||||
|
e.code &&
|
||||||
|
e.code >= 400 &&
|
||||||
|
e.code <= 599
|
||||||
|
) {
|
||||||
|
const rejections =
|
||||||
|
// tslint:disable-next-line restrict-plus-operands
|
||||||
|
1 + window.textsecure.storage.get('signedKeyRotationRejected', 0);
|
||||||
|
await window.textsecure.storage.put(
|
||||||
|
'signedKeyRotationRejected',
|
||||||
|
rejections
|
||||||
|
);
|
||||||
|
window.log.error('Signed key rotation rejected count:', rejections);
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async queueTask(task: () => Promise<any>) {
|
||||||
|
this.pendingQueue = this.pendingQueue || new PQueue({ concurrency: 1 });
|
||||||
|
const taskWithTimeout = window.textsecure.createTaskWithTimeout(task);
|
||||||
|
|
||||||
|
return this.pendingQueue.add(taskWithTimeout);
|
||||||
|
}
|
||||||
|
async cleanSignedPreKeys() {
|
||||||
|
const MINIMUM_KEYS = 3;
|
||||||
|
const store = window.textsecure.storage.protocol;
|
||||||
|
return store.loadSignedPreKeys().then(async 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
|
||||||
|
await Promise.all(
|
||||||
|
confirmed.map(async (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()
|
||||||
|
);
|
||||||
|
await 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.
|
||||||
|
await Promise.all(
|
||||||
|
unconfirmed.map(async (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()
|
||||||
|
);
|
||||||
|
await store.removeSignedPreKey(key.keyId);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// tslint:disable max-func-body-length
|
||||||
|
async createAccount(
|
||||||
|
number: string,
|
||||||
|
verificationCode: string,
|
||||||
|
identityKeyPair: KeyPairType,
|
||||||
|
profileKey: ArrayBuffer | undefined,
|
||||||
|
deviceName: string | null,
|
||||||
|
userAgent?: string | null,
|
||||||
|
readReceipts?: boolean | null,
|
||||||
|
options: { accessKey?: ArrayBuffer; uuid?: string } = {}
|
||||||
|
): Promise<void> {
|
||||||
|
const { accessKey } = options;
|
||||||
|
let password = btoa(
|
||||||
|
utils.getString(window.libsignal.crypto.getRandomBytes(16))
|
||||||
|
);
|
||||||
|
password = password.substring(0, password.length - 2);
|
||||||
|
const registrationId = window.libsignal.KeyHelper.generateRegistrationId();
|
||||||
|
|
||||||
|
const previousNumber = getIdentifier(
|
||||||
|
window.textsecure.storage.get('number_id')
|
||||||
|
);
|
||||||
|
const previousUuid = getIdentifier(
|
||||||
|
window.textsecure.storage.get('uuid_id')
|
||||||
|
);
|
||||||
|
|
||||||
|
let encryptedDeviceName;
|
||||||
|
if (deviceName) {
|
||||||
|
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 window.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([
|
||||||
|
window.textsecure.storage.remove('identityKey'),
|
||||||
|
window.textsecure.storage.remove('password'),
|
||||||
|
window.textsecure.storage.remove('registrationId'),
|
||||||
|
window.textsecure.storage.remove('number_id'),
|
||||||
|
window.textsecure.storage.remove('device_name'),
|
||||||
|
window.textsecure.storage.remove('regionCode'),
|
||||||
|
window.textsecure.storage.remove('userAgent'),
|
||||||
|
window.textsecure.storage.remove('profileKey'),
|
||||||
|
window.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 window.textsecure.storage.user.setNumberAndDeviceId(
|
||||||
|
number,
|
||||||
|
response.deviceId || 1,
|
||||||
|
deviceName
|
||||||
|
);
|
||||||
|
|
||||||
|
const setUuid = response.uuid;
|
||||||
|
if (setUuid) {
|
||||||
|
await window.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 window.textsecure.storage.protocol.saveIdentityWithAttributes(
|
||||||
|
number,
|
||||||
|
{
|
||||||
|
publicKey: identityKeyPair.pubKey,
|
||||||
|
firstUse: true,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
verified: window.textsecure.storage.protocol.VerifiedStatus.VERIFIED,
|
||||||
|
nonblockingApproval: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await window.textsecure.storage.put('identityKey', identityKeyPair);
|
||||||
|
await window.textsecure.storage.put('password', password);
|
||||||
|
await window.textsecure.storage.put('registrationId', registrationId);
|
||||||
|
if (profileKey) {
|
||||||
|
await window.textsecure.storage.put('profileKey', profileKey);
|
||||||
|
}
|
||||||
|
if (userAgent) {
|
||||||
|
await window.textsecure.storage.put('userAgent', userAgent);
|
||||||
|
}
|
||||||
|
|
||||||
|
await window.textsecure.storage.put(
|
||||||
|
'read-receipt-setting',
|
||||||
|
Boolean(readReceipts)
|
||||||
|
);
|
||||||
|
|
||||||
|
const regionCode = window.libphonenumber.util.getRegionCodeForNumber(
|
||||||
|
number
|
||||||
|
);
|
||||||
|
await window.textsecure.storage.put('regionCode', regionCode);
|
||||||
|
await window.textsecure.storage.protocol.hydrateCaches();
|
||||||
|
}
|
||||||
|
async clearSessionsAndPreKeys() {
|
||||||
|
const store = window.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: GeneratedKeysType) {
|
||||||
|
const store = window.textsecure.storage.protocol;
|
||||||
|
const key = keys.signedPreKey;
|
||||||
|
const confirmed = true;
|
||||||
|
|
||||||
|
if (!key) {
|
||||||
|
throw new Error('confirmKeys: signedPreKey is null');
|
||||||
|
}
|
||||||
|
|
||||||
|
window.log.info('confirmKeys: confirming key', key.keyId);
|
||||||
|
await store.storeSignedPreKey(key.keyId, key.keyPair, confirmed);
|
||||||
|
}
|
||||||
|
async generateKeys(count: number, providedProgressCallback?: Function) {
|
||||||
|
const progressCallback =
|
||||||
|
typeof providedProgressCallback === 'function'
|
||||||
|
? providedProgressCallback
|
||||||
|
: null;
|
||||||
|
const startId = window.textsecure.storage.get('maxPreKeyId', 1);
|
||||||
|
const signedKeyId = window.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 = window.textsecure.storage.protocol;
|
||||||
|
return store.getIdentityKeyPair().then(async identityKey => {
|
||||||
|
const result: any = {
|
||||||
|
preKeys: [],
|
||||||
|
identityKey: identityKey.pubKey,
|
||||||
|
};
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
for (let keyId = startId; keyId < startId + count; keyId += 1) {
|
||||||
|
promises.push(
|
||||||
|
window.libsignal.KeyHelper.generatePreKey(keyId).then(async res => {
|
||||||
|
await store.storePreKey(res.keyId, res.keyPair);
|
||||||
|
result.preKeys.push({
|
||||||
|
keyId: res.keyId,
|
||||||
|
publicKey: res.keyPair.pubKey,
|
||||||
|
});
|
||||||
|
if (progressCallback) {
|
||||||
|
progressCallback();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
promises.push(
|
||||||
|
window.libsignal.KeyHelper.generateSignedPreKey(
|
||||||
|
identityKey,
|
||||||
|
signedKeyId
|
||||||
|
).then(async res => {
|
||||||
|
await 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,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
promises.push(
|
||||||
|
window.textsecure.storage.put('maxPreKeyId', startId + count)
|
||||||
|
);
|
||||||
|
promises.push(
|
||||||
|
window.textsecure.storage.put('signedKeyId', signedKeyId + 1)
|
||||||
|
);
|
||||||
|
|
||||||
|
return Promise.all(promises).then(async () =>
|
||||||
|
// This is primarily for the signed prekey summary it logs out
|
||||||
|
this.cleanSignedPreKeys().then(() => result as GeneratedKeysType)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async registrationDone({ uuid, number }: { uuid?: string; number?: string }) {
|
||||||
|
window.log.info('registration done');
|
||||||
|
|
||||||
|
const identifier = number || uuid;
|
||||||
|
if (!identifier) {
|
||||||
|
throw new Error('registrationDone: no identifier!');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that we always have a conversation for ourself
|
||||||
|
const conversation = await window.ConversationController.getOrCreateAndWait(
|
||||||
|
identifier,
|
||||||
|
'private'
|
||||||
|
);
|
||||||
|
conversation.updateE164(number);
|
||||||
|
conversation.updateUuid(uuid);
|
||||||
|
|
||||||
|
window.log.info('dispatching registration event');
|
||||||
|
|
||||||
|
this.dispatchEvent(new Event('registration'));
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +1,33 @@
|
||||||
/* global dcodeIO, window, textsecure */
|
import { ByteBufferClass } from '../window.d';
|
||||||
|
import { AttachmentType } from './SendMessage';
|
||||||
|
|
||||||
function ProtoParser(arrayBuffer, protobuf) {
|
type ProtobufConstructorType = {
|
||||||
|
decode: (data: ArrayBuffer) => ProtobufType;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ProtobufType = {
|
||||||
|
avatar?: PackedAttachmentType;
|
||||||
|
profileKey?: any;
|
||||||
|
uuid?: string;
|
||||||
|
members: Array<string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PackedAttachmentType = AttachmentType & {
|
||||||
|
length: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ProtoParser {
|
||||||
|
buffer: ByteBufferClass;
|
||||||
|
protobuf: ProtobufConstructorType;
|
||||||
|
|
||||||
|
constructor(arrayBuffer: ArrayBuffer, protobuf: ProtobufConstructorType) {
|
||||||
this.protobuf = protobuf;
|
this.protobuf = protobuf;
|
||||||
this.buffer = new dcodeIO.ByteBuffer();
|
this.buffer = new window.dcodeIO.ByteBuffer();
|
||||||
this.buffer.append(arrayBuffer);
|
this.buffer.append(arrayBuffer);
|
||||||
this.buffer.offset = 0;
|
this.buffer.offset = 0;
|
||||||
this.buffer.limit = arrayBuffer.byteLength;
|
this.buffer.limit = arrayBuffer.byteLength;
|
||||||
}
|
}
|
||||||
ProtoParser.prototype = {
|
|
||||||
constructor: ProtoParser,
|
|
||||||
next() {
|
next() {
|
||||||
try {
|
try {
|
||||||
if (this.buffer.limit === this.buffer.offset) {
|
if (this.buffer.limit === this.buffer.offset) {
|
||||||
|
@ -18,8 +37,6 @@ ProtoParser.prototype = {
|
||||||
const 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
|
|
||||||
// then remove this toArrayBuffer call.
|
|
||||||
|
|
||||||
const proto = this.protobuf.decode(nextBuffer);
|
const proto = this.protobuf.decode(nextBuffer);
|
||||||
this.buffer.skip(len);
|
this.buffer.skip(len);
|
||||||
|
@ -61,15 +78,17 @@ ProtoParser.prototype = {
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
}
|
||||||
};
|
}
|
||||||
const GroupBuffer = function Constructor(arrayBuffer) {
|
|
||||||
ProtoParser.call(this, arrayBuffer, textsecure.protobuf.GroupDetails);
|
export class GroupBuffer extends ProtoParser {
|
||||||
};
|
constructor(arrayBuffer: ArrayBuffer) {
|
||||||
GroupBuffer.prototype = Object.create(ProtoParser.prototype);
|
super(arrayBuffer, window.textsecure.protobuf.GroupDetails as any);
|
||||||
GroupBuffer.prototype.constructor = GroupBuffer;
|
}
|
||||||
const ContactBuffer = function Constructor(arrayBuffer) {
|
}
|
||||||
ProtoParser.call(this, arrayBuffer, textsecure.protobuf.ContactDetails);
|
|
||||||
};
|
export class ContactBuffer extends ProtoParser {
|
||||||
ContactBuffer.prototype = Object.create(ProtoParser.prototype);
|
constructor(arrayBuffer: ArrayBuffer) {
|
||||||
ContactBuffer.prototype.constructor = ContactBuffer;
|
super(arrayBuffer, window.textsecure.protobuf.ContactDetails as any);
|
||||||
|
}
|
||||||
|
}
|
269
ts/textsecure/Crypto.ts
Normal file
269
ts/textsecure/Crypto.ts
Normal file
|
@ -0,0 +1,269 @@
|
||||||
|
// tslint:disable no-bitwise no-default-export
|
||||||
|
|
||||||
|
import { ByteBufferClass } from '../window.d';
|
||||||
|
|
||||||
|
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: ArrayBuffer, theirDigest: ArrayBuffer) {
|
||||||
|
return window.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: ArrayBuffer) {
|
||||||
|
return window.crypto.subtle.digest({ name: 'SHA-256' }, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Crypto = {
|
||||||
|
// Decrypts message into a raw string
|
||||||
|
async decryptWebsocketMessage(
|
||||||
|
message: ByteBufferClass,
|
||||||
|
signalingKey: ArrayBuffer
|
||||||
|
) {
|
||||||
|
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: ${new Uint8Array(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 window.libsignal.crypto
|
||||||
|
.verifyMAC(ivAndCiphertext, macKey, mac, 10)
|
||||||
|
.then(async () =>
|
||||||
|
window.libsignal.crypto.decrypt(aesKey, ciphertext, iv)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
async decryptAttachment(
|
||||||
|
encryptedBin: ArrayBuffer,
|
||||||
|
keys: ArrayBuffer,
|
||||||
|
theirDigest: ArrayBuffer
|
||||||
|
) {
|
||||||
|
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 window.libsignal.crypto
|
||||||
|
.verifyMAC(ivAndCiphertext, macKey, mac, 32)
|
||||||
|
.then(async () => {
|
||||||
|
if (theirDigest) {
|
||||||
|
return verifyDigest(encryptedBin, theirDigest);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.then(async () =>
|
||||||
|
window.libsignal.crypto.decrypt(aesKey, ciphertext, iv)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
async encryptAttachment(
|
||||||
|
plaintext: ArrayBuffer,
|
||||||
|
keys: ArrayBuffer,
|
||||||
|
iv: ArrayBuffer
|
||||||
|
) {
|
||||||
|
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 window.libsignal.crypto
|
||||||
|
.encrypt(aesKey, plaintext, iv)
|
||||||
|
.then(async ciphertext => {
|
||||||
|
const ivAndCiphertext = new Uint8Array(16 + ciphertext.byteLength);
|
||||||
|
ivAndCiphertext.set(new Uint8Array(iv));
|
||||||
|
ivAndCiphertext.set(new Uint8Array(ciphertext), 16);
|
||||||
|
|
||||||
|
return window.libsignal.crypto
|
||||||
|
.calculateMAC(macKey, ivAndCiphertext.buffer as ArrayBuffer)
|
||||||
|
.then(async mac => {
|
||||||
|
const encryptedBin = new Uint8Array(
|
||||||
|
16 + ciphertext.byteLength + 32
|
||||||
|
);
|
||||||
|
encryptedBin.set(ivAndCiphertext);
|
||||||
|
encryptedBin.set(new Uint8Array(mac), 16 + ciphertext.byteLength);
|
||||||
|
return calculateDigest(encryptedBin.buffer as ArrayBuffer).then(
|
||||||
|
digest => ({
|
||||||
|
ciphertext: encryptedBin.buffer,
|
||||||
|
digest,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async encryptProfile(data: ArrayBuffer, key: ArrayBuffer) {
|
||||||
|
const iv = window.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 window.crypto.subtle
|
||||||
|
.importKey('raw', key, { name: 'AES-GCM' } as any, false, ['encrypt'])
|
||||||
|
.then(async keyForEncryption =>
|
||||||
|
window.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;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
async decryptProfile(data: ArrayBuffer, key: ArrayBuffer) {
|
||||||
|
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 window.crypto.subtle
|
||||||
|
.importKey('raw', key, { name: 'AES-GCM' } as any, false, ['decrypt'])
|
||||||
|
.then(async keyForEncryption =>
|
||||||
|
window.crypto.subtle
|
||||||
|
.decrypt(
|
||||||
|
{ name: 'AES-GCM', iv, tagLength: PROFILE_TAG_LENGTH },
|
||||||
|
keyForEncryption,
|
||||||
|
ciphertext
|
||||||
|
)
|
||||||
|
// Typescript says that there's no .catch() available here
|
||||||
|
// @ts-ignore
|
||||||
|
.catch((e: Error) => {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
async encryptProfileName(name: ArrayBuffer, key: ArrayBuffer) {
|
||||||
|
const padded = new Uint8Array(PROFILE_NAME_PADDED_LENGTH);
|
||||||
|
padded.set(new Uint8Array(name));
|
||||||
|
return Crypto.encryptProfile(padded.buffer as ArrayBuffer, key);
|
||||||
|
},
|
||||||
|
async decryptProfileName(encryptedProfileName: string, key: ArrayBuffer) {
|
||||||
|
const data = window.dcodeIO.ByteBuffer.wrap(
|
||||||
|
encryptedProfileName,
|
||||||
|
'base64'
|
||||||
|
).toArrayBuffer();
|
||||||
|
return 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: window.dcodeIO.ByteBuffer.wrap(padded)
|
||||||
|
.slice(0, givenEnd)
|
||||||
|
.toArrayBuffer(),
|
||||||
|
family: foundFamilyName
|
||||||
|
? window.dcodeIO.ByteBuffer.wrap(padded)
|
||||||
|
.slice(givenEnd + 1, familyEnd)
|
||||||
|
.toArrayBuffer()
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getRandomBytes(size: number) {
|
||||||
|
return window.libsignal.crypto.getRandomBytes(size);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Crypto;
|
164
ts/textsecure/Errors.ts
Normal file
164
ts/textsecure/Errors.ts
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
// tslint:disable max-classes-per-file
|
||||||
|
|
||||||
|
function appendStack(newError: Error, originalError: Error) {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
newError.stack += `\nOriginal stack:\n${originalError.stack}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ReplayableError extends Error {
|
||||||
|
name: string;
|
||||||
|
message: string;
|
||||||
|
functionCode?: number;
|
||||||
|
|
||||||
|
constructor(options: {
|
||||||
|
name?: string;
|
||||||
|
message: string;
|
||||||
|
functionCode?: number;
|
||||||
|
}) {
|
||||||
|
super(options.message);
|
||||||
|
|
||||||
|
this.name = options.name || 'ReplayableError';
|
||||||
|
this.message = 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IncomingIdentityKeyError extends ReplayableError {
|
||||||
|
identifier: string;
|
||||||
|
identityKey: ArrayBuffer;
|
||||||
|
|
||||||
|
// Note: Data to resend message is no longer captured
|
||||||
|
constructor(incomingIdentifier: string, _m: ArrayBuffer, key: ArrayBuffer) {
|
||||||
|
const identifer = incomingIdentifier.split('.')[0];
|
||||||
|
|
||||||
|
super({
|
||||||
|
name: 'IncomingIdentityKeyError',
|
||||||
|
message: `The identity of ${identifer} has changed.`,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.identifier = identifer;
|
||||||
|
this.identityKey = key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OutgoingIdentityKeyError extends ReplayableError {
|
||||||
|
identifier: string;
|
||||||
|
identityKey: ArrayBuffer;
|
||||||
|
|
||||||
|
// Note: Data to resend message is no longer captured
|
||||||
|
constructor(
|
||||||
|
incomingIdentifier: string,
|
||||||
|
_m: ArrayBuffer,
|
||||||
|
_t: number,
|
||||||
|
identityKey: ArrayBuffer
|
||||||
|
) {
|
||||||
|
const identifier = incomingIdentifier.split('.')[0];
|
||||||
|
|
||||||
|
super({
|
||||||
|
name: 'OutgoingIdentityKeyError',
|
||||||
|
message: `The identity of ${identifier} has changed.`,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.identifier = identifier;
|
||||||
|
this.identityKey = identityKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OutgoingMessageError extends ReplayableError {
|
||||||
|
identifier: string;
|
||||||
|
code?: any;
|
||||||
|
|
||||||
|
// Note: Data to resend message is no longer captured
|
||||||
|
constructor(
|
||||||
|
incomingIdentifier: string,
|
||||||
|
_m: ArrayBuffer,
|
||||||
|
_t: number,
|
||||||
|
httpError?: Error
|
||||||
|
) {
|
||||||
|
const identifier = incomingIdentifier.split('.')[0];
|
||||||
|
|
||||||
|
super({
|
||||||
|
name: 'OutgoingMessageError',
|
||||||
|
message: httpError ? httpError.message : 'no http error',
|
||||||
|
});
|
||||||
|
|
||||||
|
this.identifier = identifier;
|
||||||
|
|
||||||
|
if (httpError) {
|
||||||
|
this.code = httpError.code;
|
||||||
|
appendStack(this, httpError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SendMessageNetworkError extends ReplayableError {
|
||||||
|
identifier: string;
|
||||||
|
|
||||||
|
constructor(identifier: string, _m: any, httpError: Error) {
|
||||||
|
super({
|
||||||
|
name: 'SendMessageNetworkError',
|
||||||
|
message: httpError.message,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.identifier = identifier.split('.')[0];
|
||||||
|
this.code = httpError.code;
|
||||||
|
|
||||||
|
appendStack(this, httpError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SignedPreKeyRotationError extends ReplayableError {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
name: 'SignedPreKeyRotationError',
|
||||||
|
message: 'Too many signed prekey rotation failures',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MessageError extends ReplayableError {
|
||||||
|
code?: any;
|
||||||
|
|
||||||
|
constructor(_m: any, httpError: Error) {
|
||||||
|
super({
|
||||||
|
name: 'MessageError',
|
||||||
|
message: httpError.message,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.code = httpError.code;
|
||||||
|
|
||||||
|
appendStack(this, httpError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnregisteredUserError extends Error {
|
||||||
|
identifier: string;
|
||||||
|
code?: any;
|
||||||
|
|
||||||
|
constructor(identifier: string, httpError: Error) {
|
||||||
|
const message = httpError.message;
|
||||||
|
|
||||||
|
super(message);
|
||||||
|
|
||||||
|
this.message = message;
|
||||||
|
this.name = 'UnregisteredUserError';
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
81
ts/textsecure/EventTarget.ts
Normal file
81
ts/textsecure/EventTarget.ts
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
// tslint:disable no-default-export
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Implements EventTarget
|
||||||
|
* https://developer.mozilla.org/en-US/docs/Web/API/EventTarget
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default class EventTarget {
|
||||||
|
listeners?: { [type: string]: Array<Function> };
|
||||||
|
|
||||||
|
dispatchEvent(ev: Event) {
|
||||||
|
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') {
|
||||||
|
const max = listeners.length;
|
||||||
|
for (let i = 0; i < max; i += 1) {
|
||||||
|
const listener = listeners[i];
|
||||||
|
if (typeof listener === 'function') {
|
||||||
|
results.push(listener.call(null, ev));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
addEventListener(eventName: string, callback: Function) {
|
||||||
|
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: string, callback: Function) {
|
||||||
|
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(source: any) {
|
||||||
|
const target = this as any;
|
||||||
|
|
||||||
|
// tslint:disable-next-line forin no-for-in no-default-export
|
||||||
|
for (const prop in source) {
|
||||||
|
target[prop] = source[prop];
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
}
|
98
ts/textsecure/Helpers.ts
Normal file
98
ts/textsecure/Helpers.ts
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
// tslint:disable no-default-export
|
||||||
|
|
||||||
|
import { ByteBufferClass } from '../window.d';
|
||||||
|
|
||||||
|
let ByteBuffer: ByteBufferClass | undefined;
|
||||||
|
const arrayBuffer = new ArrayBuffer(0);
|
||||||
|
const uint8Array = new Uint8Array();
|
||||||
|
|
||||||
|
let StaticByteBufferProto: any;
|
||||||
|
// @ts-ignore
|
||||||
|
const StaticArrayBufferProto = arrayBuffer.__proto__;
|
||||||
|
// @ts-ignore
|
||||||
|
const StaticUint8ArrayProto = uint8Array.__proto__;
|
||||||
|
|
||||||
|
function getString(thing: any): string {
|
||||||
|
// Note: we must make this at runtime because it's loaded in the browser context
|
||||||
|
if (!ByteBuffer) {
|
||||||
|
ByteBuffer = new window.dcodeIO.ByteBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!StaticByteBufferProto) {
|
||||||
|
// @ts-ignore
|
||||||
|
StaticByteBufferProto = ByteBuffer.__proto__;
|
||||||
|
}
|
||||||
|
|
||||||
|
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: any): boolean {
|
||||||
|
return (
|
||||||
|
typeof thing === 'string' ||
|
||||||
|
typeof thing === 'number' ||
|
||||||
|
typeof thing === 'boolean' ||
|
||||||
|
(thing === Object(thing) &&
|
||||||
|
(thing.__proto__ === StaticArrayBufferProto ||
|
||||||
|
thing.__proto__ === StaticUint8ArrayProto ||
|
||||||
|
thing.__proto__ === StaticByteBufferProto))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureStringed(thing: any): any {
|
||||||
|
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: any = {};
|
||||||
|
// tslint:disable-next-line forin no-for-in no-default-export
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringToArrayBuffer(string: string) {
|
||||||
|
if (typeof string !== 'string') {
|
||||||
|
throw new TypeError("'string' must be a string");
|
||||||
|
}
|
||||||
|
|
||||||
|
const array = new Uint8Array(string.length);
|
||||||
|
for (let i = 0; i < string.length; i += 1) {
|
||||||
|
array[i] = string.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return array.buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Number formatting utils
|
||||||
|
const utils = {
|
||||||
|
getString,
|
||||||
|
isNumberSane: (number: string) =>
|
||||||
|
number[0] === '+' && /^[0-9]+$/.test(number.substring(1)),
|
||||||
|
jsonThing: (thing: any) => JSON.stringify(ensureStringed(thing)),
|
||||||
|
stringToArrayBuffer,
|
||||||
|
unencodeNumber: (number: string) => number.split('.'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default utils;
|
File diff suppressed because it is too large
Load diff
|
@ -1,26 +1,66 @@
|
||||||
/* global textsecure, libsignal, window, btoa, _ */
|
// tslint:disable no-default-export
|
||||||
|
|
||||||
/* eslint-disable more/no-then */
|
import { reject } from 'lodash';
|
||||||
|
import { ServerKeysType, WebAPIType } from './WebAPI';
|
||||||
|
import { SignalProtocolAddressClass } from '../libsignal.d';
|
||||||
|
import { ContentClass, DataMessageClass } from '../textsecure.d';
|
||||||
|
import {
|
||||||
|
CallbackResultType,
|
||||||
|
SendMetadataType,
|
||||||
|
SendOptionsType,
|
||||||
|
} from './SendMessage';
|
||||||
|
import {
|
||||||
|
OutgoingIdentityKeyError,
|
||||||
|
OutgoingMessageError,
|
||||||
|
SendMessageNetworkError,
|
||||||
|
UnregisteredUserError,
|
||||||
|
} from './Errors';
|
||||||
|
|
||||||
function OutgoingMessage(
|
type OutgoingMessageOptionsType = SendOptionsType & {
|
||||||
server,
|
online?: boolean;
|
||||||
timestamp,
|
};
|
||||||
identifiers,
|
|
||||||
message,
|
export default class OutgoingMessage {
|
||||||
silent,
|
server: WebAPIType;
|
||||||
callback,
|
timestamp: number;
|
||||||
options = {}
|
identifiers: Array<string>;
|
||||||
) {
|
message: ContentClass;
|
||||||
if (message instanceof textsecure.protobuf.DataMessage) {
|
callback: (result: CallbackResultType) => void;
|
||||||
const content = new textsecure.protobuf.Content();
|
silent?: boolean;
|
||||||
|
plaintext?: Uint8Array;
|
||||||
|
|
||||||
|
identifiersCompleted: number;
|
||||||
|
errors: Array<any>;
|
||||||
|
successfulIdentifiers: Array<any>;
|
||||||
|
failoverIdentifiers: Array<any>;
|
||||||
|
unidentifiedDeliveries: Array<any>;
|
||||||
|
|
||||||
|
sendMetadata?: SendMetadataType;
|
||||||
|
senderCertificate?: ArrayBuffer;
|
||||||
|
senderCertificateWithUuid?: ArrayBuffer;
|
||||||
|
online?: boolean;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
server: WebAPIType,
|
||||||
|
timestamp: number,
|
||||||
|
identifiers: Array<string>,
|
||||||
|
message: ContentClass | DataMessageClass,
|
||||||
|
silent: boolean | undefined,
|
||||||
|
callback: (result: CallbackResultType) => void,
|
||||||
|
options: OutgoingMessageOptionsType = {}
|
||||||
|
) {
|
||||||
|
if (message instanceof window.textsecure.protobuf.DataMessage) {
|
||||||
|
const content = new window.textsecure.protobuf.Content();
|
||||||
content.dataMessage = message;
|
content.dataMessage = message;
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
message = content;
|
this.message = content;
|
||||||
|
} else {
|
||||||
|
this.message = message;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.server = server;
|
this.server = server;
|
||||||
this.timestamp = timestamp;
|
this.timestamp = timestamp;
|
||||||
this.identifiers = identifiers;
|
this.identifiers = identifiers;
|
||||||
this.message = message; // ContentMessage proto
|
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
this.silent = silent;
|
this.silent = silent;
|
||||||
|
|
||||||
|
@ -30,16 +70,17 @@ function OutgoingMessage(
|
||||||
this.failoverIdentifiers = [];
|
this.failoverIdentifiers = [];
|
||||||
this.unidentifiedDeliveries = [];
|
this.unidentifiedDeliveries = [];
|
||||||
|
|
||||||
const { sendMetadata, senderCertificate, senderCertificateWithUuid, online } =
|
const {
|
||||||
options || {};
|
sendMetadata,
|
||||||
|
senderCertificate,
|
||||||
|
senderCertificateWithUuid,
|
||||||
|
online,
|
||||||
|
} = options || ({} as any);
|
||||||
this.sendMetadata = sendMetadata;
|
this.sendMetadata = sendMetadata;
|
||||||
this.senderCertificate = senderCertificate;
|
this.senderCertificate = senderCertificate;
|
||||||
this.senderCertificateWithUuid = senderCertificateWithUuid;
|
this.senderCertificateWithUuid = senderCertificateWithUuid;
|
||||||
this.online = online;
|
this.online = online;
|
||||||
}
|
}
|
||||||
|
|
||||||
OutgoingMessage.prototype = {
|
|
||||||
constructor: OutgoingMessage,
|
|
||||||
numberCompleted() {
|
numberCompleted() {
|
||||||
this.identifiersCompleted += 1;
|
this.identifiersCompleted += 1;
|
||||||
if (this.identifiersCompleted >= this.identifiers.length) {
|
if (this.identifiersCompleted >= this.identifiers.length) {
|
||||||
|
@ -50,11 +91,11 @@ OutgoingMessage.prototype = {
|
||||||
unidentifiedDeliveries: this.unidentifiedDeliveries,
|
unidentifiedDeliveries: this.unidentifiedDeliveries,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
registerError(identifier, reason, error) {
|
registerError(identifier: string, reason: string, error?: Error) {
|
||||||
if (!error || (error.name === 'HTTPError' && error.code !== 404)) {
|
if (!error || (error.name === 'HTTPError' && error.code !== 404)) {
|
||||||
// eslint-disable-next-line no-param-reassign
|
// tslint:disable-next-line no-parameter-reassignment
|
||||||
error = new textsecure.OutgoingMessageError(
|
error = new OutgoingMessageError(
|
||||||
identifier,
|
identifier,
|
||||||
this.message.toArrayBuffer(),
|
this.message.toArrayBuffer(),
|
||||||
this.timestamp,
|
this.timestamp,
|
||||||
|
@ -66,50 +107,57 @@ OutgoingMessage.prototype = {
|
||||||
error.reason = reason;
|
error.reason = reason;
|
||||||
this.errors[this.errors.length] = error;
|
this.errors[this.errors.length] = error;
|
||||||
this.numberCompleted();
|
this.numberCompleted();
|
||||||
},
|
}
|
||||||
reloadDevicesAndSend(identifier, recurse) {
|
reloadDevicesAndSend(
|
||||||
return () =>
|
identifier: string,
|
||||||
textsecure.storage.protocol.getDeviceIds(identifier).then(deviceIds => {
|
recurse?: boolean
|
||||||
|
): () => Promise<void> {
|
||||||
|
return async () =>
|
||||||
|
window.textsecure.storage.protocol
|
||||||
|
.getDeviceIds(identifier)
|
||||||
|
.then(async deviceIds => {
|
||||||
if (deviceIds.length === 0) {
|
if (deviceIds.length === 0) {
|
||||||
return this.registerError(
|
this.registerError(
|
||||||
identifier,
|
identifier,
|
||||||
'Got empty device list when loading device keys',
|
'Got empty device list when loading device keys',
|
||||||
null
|
undefined
|
||||||
);
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
return this.doSendMessage(identifier, deviceIds, recurse);
|
return this.doSendMessage(identifier, deviceIds, recurse);
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
getKeysForIdentifier(identifier, updateDevices) {
|
// tslint:disable-next-line max-func-body-length
|
||||||
const handleResult = response =>
|
async getKeysForIdentifier(identifier: string, updateDevices: Array<number>) {
|
||||||
|
const handleResult = async (response: ServerKeysType) =>
|
||||||
Promise.all(
|
Promise.all(
|
||||||
response.devices.map(device => {
|
response.devices.map(async device => {
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
device.identityKey = response.identityKey;
|
|
||||||
if (
|
if (
|
||||||
updateDevices === undefined ||
|
updateDevices === undefined ||
|
||||||
updateDevices.indexOf(device.deviceId) > -1
|
updateDevices.indexOf(device.deviceId) > -1
|
||||||
) {
|
) {
|
||||||
const address = new libsignal.SignalProtocolAddress(
|
const address = new window.libsignal.SignalProtocolAddress(
|
||||||
identifier,
|
identifier,
|
||||||
device.deviceId
|
device.deviceId
|
||||||
);
|
);
|
||||||
const builder = new libsignal.SessionBuilder(
|
const builder = new window.libsignal.SessionBuilder(
|
||||||
textsecure.storage.protocol,
|
window.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(error => {
|
|
||||||
|
const deviceForProcess = {
|
||||||
|
...device,
|
||||||
|
identityKey: response.identityKey,
|
||||||
|
};
|
||||||
|
return builder.processPreKey(deviceForProcess).catch(error => {
|
||||||
if (error.message === 'Identity key changed') {
|
if (error.message === 'Identity key changed') {
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
error.timestamp = this.timestamp;
|
error.timestamp = this.timestamp;
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
error.originalMessage = this.message.toArrayBuffer();
|
error.originalMessage = this.message.toArrayBuffer();
|
||||||
// eslint-disable-next-line no-param-reassign
|
error.identityKey = response.identityKey;
|
||||||
error.identityKey = device.identityKey;
|
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
|
@ -121,40 +169,40 @@ OutgoingMessage.prototype = {
|
||||||
|
|
||||||
const { sendMetadata } = this;
|
const { sendMetadata } = this;
|
||||||
const info =
|
const info =
|
||||||
sendMetadata && sendMetadata[identifier] ? sendMetadata[identifier] : {};
|
sendMetadata && sendMetadata[identifier]
|
||||||
const { accessKey } = info || {};
|
? sendMetadata[identifier]
|
||||||
|
: { accessKey: undefined };
|
||||||
|
const { accessKey } = info;
|
||||||
|
|
||||||
if (updateDevices === undefined) {
|
if (updateDevices === undefined) {
|
||||||
if (accessKey) {
|
if (accessKey) {
|
||||||
return this.server
|
return this.server
|
||||||
.getKeysForIdentifierUnauth(identifier, '*', { accessKey })
|
.getKeysForIdentifierUnauth(identifier, undefined, { accessKey })
|
||||||
.catch(error => {
|
.catch(async (error: Error) => {
|
||||||
if (error.code === 401 || error.code === 403) {
|
if (error.code === 401 || error.code === 403) {
|
||||||
if (this.failoverIdentifiers.indexOf(identifier) === -1) {
|
if (this.failoverIdentifiers.indexOf(identifier) === -1) {
|
||||||
this.failoverIdentifiers.push(identifier);
|
this.failoverIdentifiers.push(identifier);
|
||||||
}
|
}
|
||||||
return this.server.getKeysForIdentifier(identifier, '*');
|
return this.server.getKeysForIdentifier(identifier);
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
})
|
})
|
||||||
.then(handleResult);
|
.then(handleResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.server
|
return this.server.getKeysForIdentifier(identifier).then(handleResult);
|
||||||
.getKeysForIdentifier(identifier, '*')
|
|
||||||
.then(handleResult);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let promise = Promise.resolve();
|
let promise: Promise<any> = Promise.resolve();
|
||||||
updateDevices.forEach(deviceId => {
|
updateDevices.forEach(deviceId => {
|
||||||
promise = promise.then(() => {
|
promise = promise.then(async () => {
|
||||||
let innerPromise;
|
let innerPromise;
|
||||||
|
|
||||||
if (accessKey) {
|
if (accessKey) {
|
||||||
innerPromise = this.server
|
innerPromise = this.server
|
||||||
.getKeysForIdentifierUnauth(identifier, deviceId, { accessKey })
|
.getKeysForIdentifierUnauth(identifier, deviceId, { accessKey })
|
||||||
.then(handleResult)
|
.then(handleResult)
|
||||||
.catch(error => {
|
.catch(async error => {
|
||||||
if (error.code === 401 || error.code === 403) {
|
if (error.code === 401 || error.code === 403) {
|
||||||
if (this.failoverIdentifiers.indexOf(identifier) === -1) {
|
if (this.failoverIdentifiers.indexOf(identifier) === -1) {
|
||||||
this.failoverIdentifiers.push(identifier);
|
this.failoverIdentifiers.push(identifier);
|
||||||
|
@ -171,12 +219,12 @@ OutgoingMessage.prototype = {
|
||||||
.then(handleResult);
|
.then(handleResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
return innerPromise.catch(e => {
|
return innerPromise.catch(async e => {
|
||||||
if (e.name === 'HTTPError' && e.code === 404) {
|
if (e.name === 'HTTPError' && e.code === 404) {
|
||||||
if (deviceId !== 1) {
|
if (deviceId !== 1) {
|
||||||
return this.removeDeviceIdsForIdentifier(identifier, [deviceId]);
|
return this.removeDeviceIdsForIdentifier(identifier, [deviceId]);
|
||||||
}
|
}
|
||||||
throw new textsecure.UnregisteredUserError(identifier, e);
|
throw new UnregisteredUserError(identifier, e);
|
||||||
} else {
|
} else {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
@ -185,9 +233,14 @@ OutgoingMessage.prototype = {
|
||||||
});
|
});
|
||||||
|
|
||||||
return promise;
|
return promise;
|
||||||
},
|
}
|
||||||
|
|
||||||
transmitMessage(identifier, jsonData, timestamp, { accessKey } = {}) {
|
async transmitMessage(
|
||||||
|
identifier: string,
|
||||||
|
jsonData: Array<any>,
|
||||||
|
timestamp: number,
|
||||||
|
{ accessKey }: { accessKey?: string } = {}
|
||||||
|
) {
|
||||||
let promise;
|
let promise;
|
||||||
|
|
||||||
if (accessKey) {
|
if (accessKey) {
|
||||||
|
@ -215,20 +268,15 @@ OutgoingMessage.prototype = {
|
||||||
// 404 should throw UnregisteredUserError
|
// 404 should throw UnregisteredUserError
|
||||||
// all other network errors can be retried later.
|
// all other network errors can be retried later.
|
||||||
if (e.code === 404) {
|
if (e.code === 404) {
|
||||||
throw new textsecure.UnregisteredUserError(identifier, e);
|
throw new UnregisteredUserError(identifier, e);
|
||||||
}
|
}
|
||||||
throw new textsecure.SendMessageNetworkError(
|
throw new SendMessageNetworkError(identifier, jsonData, e);
|
||||||
identifier,
|
|
||||||
jsonData,
|
|
||||||
e,
|
|
||||||
timestamp
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
getPaddedMessageLength(messageLength) {
|
getPaddedMessageLength(messageLength: number) {
|
||||||
const messageLengthWithTerminator = messageLength + 1;
|
const messageLengthWithTerminator = messageLength + 1;
|
||||||
let messagePartCount = Math.floor(messageLengthWithTerminator / 160);
|
let messagePartCount = Math.floor(messageLengthWithTerminator / 160);
|
||||||
|
|
||||||
|
@ -237,7 +285,7 @@ OutgoingMessage.prototype = {
|
||||||
}
|
}
|
||||||
|
|
||||||
return messagePartCount * 160;
|
return messagePartCount * 160;
|
||||||
},
|
}
|
||||||
|
|
||||||
getPlaintext() {
|
getPlaintext() {
|
||||||
if (!this.plaintext) {
|
if (!this.plaintext) {
|
||||||
|
@ -249,16 +297,29 @@ OutgoingMessage.prototype = {
|
||||||
this.plaintext[messageBuffer.byteLength] = 0x80;
|
this.plaintext[messageBuffer.byteLength] = 0x80;
|
||||||
}
|
}
|
||||||
return this.plaintext;
|
return this.plaintext;
|
||||||
},
|
}
|
||||||
|
|
||||||
doSendMessage(identifier, deviceIds, recurse) {
|
// tslint:disable-next-line max-func-body-length
|
||||||
const ciphers = {};
|
async doSendMessage(
|
||||||
|
identifier: string,
|
||||||
|
deviceIds: Array<number>,
|
||||||
|
recurse?: boolean
|
||||||
|
): Promise<void> {
|
||||||
|
const ciphers: {
|
||||||
|
[key: number]: {
|
||||||
|
closeOpenSessionForDevice: (
|
||||||
|
address: SignalProtocolAddressClass
|
||||||
|
) => Promise<void>;
|
||||||
|
};
|
||||||
|
} = {};
|
||||||
const plaintext = this.getPlaintext();
|
const plaintext = this.getPlaintext();
|
||||||
|
|
||||||
const { sendMetadata } = this;
|
const { sendMetadata } = this;
|
||||||
const info =
|
const info =
|
||||||
sendMetadata && sendMetadata[identifier] ? sendMetadata[identifier] : {};
|
sendMetadata && sendMetadata[identifier]
|
||||||
const { accessKey, useUuidSenderCert } = info || {};
|
? sendMetadata[identifier]
|
||||||
|
: { accessKey: undefined, useUuidSenderCert: undefined };
|
||||||
|
const { accessKey, useUuidSenderCert } = info;
|
||||||
const senderCertificate = useUuidSenderCert
|
const senderCertificate = useUuidSenderCert
|
||||||
? this.senderCertificateWithUuid
|
? this.senderCertificateWithUuid
|
||||||
: this.senderCertificate;
|
: this.senderCertificate;
|
||||||
|
@ -272,27 +333,29 @@ OutgoingMessage.prototype = {
|
||||||
const sealedSender = Boolean(accessKey && senderCertificate);
|
const sealedSender = Boolean(accessKey && senderCertificate);
|
||||||
|
|
||||||
// We don't send to ourselves if unless sealedSender is enabled
|
// We don't send to ourselves if unless sealedSender is enabled
|
||||||
const ourNumber = textsecure.storage.user.getNumber();
|
const ourNumber = window.textsecure.storage.user.getNumber();
|
||||||
const ourUuid = textsecure.storage.user.getUuid();
|
const ourUuid = window.textsecure.storage.user.getUuid();
|
||||||
const ourDeviceId = textsecure.storage.user.getDeviceId();
|
const ourDeviceId = window.textsecure.storage.user.getDeviceId();
|
||||||
if ((identifier === ourNumber || identifier === ourUuid) && !sealedSender) {
|
if ((identifier === ourNumber || identifier === ourUuid) && !sealedSender) {
|
||||||
// eslint-disable-next-line no-param-reassign
|
// tslint:disable-next-line no-parameter-reassignment
|
||||||
deviceIds = _.reject(
|
deviceIds = reject(
|
||||||
deviceIds,
|
deviceIds,
|
||||||
deviceId =>
|
deviceId =>
|
||||||
// because we store our own device ID as a string at least sometimes
|
// because we store our own device ID as a string at least sometimes
|
||||||
deviceId === ourDeviceId || deviceId === parseInt(ourDeviceId, 10)
|
deviceId === ourDeviceId ||
|
||||||
|
(typeof ourDeviceId === 'string' &&
|
||||||
|
deviceId === parseInt(ourDeviceId, 10))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
deviceIds.map(async deviceId => {
|
deviceIds.map(async deviceId => {
|
||||||
const address = new libsignal.SignalProtocolAddress(
|
const address = new window.libsignal.SignalProtocolAddress(
|
||||||
identifier,
|
identifier,
|
||||||
deviceId
|
deviceId
|
||||||
);
|
);
|
||||||
|
|
||||||
const options = {};
|
const options: any = {};
|
||||||
|
|
||||||
// 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 === identifier || ourUuid === identifier) {
|
if (ourNumber === identifier || ourUuid === identifier) {
|
||||||
|
@ -301,7 +364,7 @@ OutgoingMessage.prototype = {
|
||||||
|
|
||||||
if (sealedSender) {
|
if (sealedSender) {
|
||||||
const secretSessionCipher = new window.Signal.Metadata.SecretSessionCipher(
|
const secretSessionCipher = new window.Signal.Metadata.SecretSessionCipher(
|
||||||
textsecure.storage.protocol
|
window.textsecure.storage.protocol
|
||||||
);
|
);
|
||||||
ciphers[address.getDeviceId()] = secretSessionCipher;
|
ciphers[address.getDeviceId()] = secretSessionCipher;
|
||||||
|
|
||||||
|
@ -312,17 +375,16 @@ OutgoingMessage.prototype = {
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER,
|
type: window.textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER,
|
||||||
destinationDeviceId: address.getDeviceId(),
|
destinationDeviceId: address.getDeviceId(),
|
||||||
destinationRegistrationId: await secretSessionCipher.getRemoteRegistrationId(
|
destinationRegistrationId: await secretSessionCipher.getRemoteRegistrationId(
|
||||||
address
|
address
|
||||||
),
|
),
|
||||||
content: window.Signal.Crypto.arrayBufferToBase64(ciphertext),
|
content: window.Signal.Crypto.arrayBufferToBase64(ciphertext),
|
||||||
};
|
};
|
||||||
}
|
} else {
|
||||||
|
const sessionCipher = new window.libsignal.SessionCipher(
|
||||||
const sessionCipher = new libsignal.SessionCipher(
|
window.textsecure.storage.protocol,
|
||||||
textsecure.storage.protocol,
|
|
||||||
address,
|
address,
|
||||||
options
|
options
|
||||||
);
|
);
|
||||||
|
@ -335,9 +397,10 @@ OutgoingMessage.prototype = {
|
||||||
destinationRegistrationId: ciphertext.registrationId,
|
destinationRegistrationId: ciphertext.registrationId,
|
||||||
content: btoa(ciphertext.body),
|
content: btoa(ciphertext.body),
|
||||||
};
|
};
|
||||||
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.then(jsonData => {
|
.then(async jsonData => {
|
||||||
if (sealedSender) {
|
if (sealedSender) {
|
||||||
return this.transmitMessage(identifier, jsonData, this.timestamp, {
|
return this.transmitMessage(identifier, jsonData, this.timestamp, {
|
||||||
accessKey,
|
accessKey,
|
||||||
|
@ -347,18 +410,18 @@ OutgoingMessage.prototype = {
|
||||||
this.successfulIdentifiers.push(identifier);
|
this.successfulIdentifiers.push(identifier);
|
||||||
this.numberCompleted();
|
this.numberCompleted();
|
||||||
},
|
},
|
||||||
error => {
|
async (error: Error) => {
|
||||||
if (error.code === 401 || error.code === 403) {
|
if (error.code === 401 || error.code === 403) {
|
||||||
if (this.failoverIdentifiers.indexOf(identifier) === -1) {
|
if (this.failoverIdentifiers.indexOf(identifier) === -1) {
|
||||||
this.failoverIdentifiers.push(identifier);
|
this.failoverIdentifiers.push(identifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This ensures that we don't hit this codepath the next time through
|
||||||
if (info) {
|
if (info) {
|
||||||
info.accessKey = null;
|
info.accessKey = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set final parameter to true to ensure we don't hit this codepath a
|
return this.doSendMessage(identifier, deviceIds, recurse);
|
||||||
// second time.
|
|
||||||
return this.doSendMessage(identifier, deviceIds, recurse, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
|
@ -373,20 +436,22 @@ OutgoingMessage.prototype = {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(async 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(
|
this.registerError(
|
||||||
identifier,
|
identifier,
|
||||||
'Hit retry limit attempting to reload device list',
|
'Hit retry limit attempting to reload device list',
|
||||||
error
|
error
|
||||||
);
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let p;
|
let p: Promise<any> = Promise.resolve();
|
||||||
if (error.code === 409) {
|
if (error.code === 409) {
|
||||||
p = this.removeDeviceIdsForIdentifier(
|
p = this.removeDeviceIdsForIdentifier(
|
||||||
identifier,
|
identifier,
|
||||||
|
@ -394,15 +459,18 @@ OutgoingMessage.prototype = {
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
p = Promise.all(
|
p = Promise.all(
|
||||||
error.response.staleDevices.map(deviceId =>
|
error.response.staleDevices.map(async (deviceId: number) =>
|
||||||
ciphers[deviceId].closeOpenSessionForDevice(
|
ciphers[deviceId].closeOpenSessionForDevice(
|
||||||
new libsignal.SignalProtocolAddress(identifier, deviceId)
|
new window.libsignal.SignalProtocolAddress(
|
||||||
|
identifier,
|
||||||
|
deviceId
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return p.then(() => {
|
return p.then(async () => {
|
||||||
const resetDevices =
|
const resetDevices =
|
||||||
error.code === 410
|
error.code === 410
|
||||||
? error.response.staleDevices
|
? error.response.staleDevices
|
||||||
|
@ -425,10 +493,13 @@ OutgoingMessage.prototype = {
|
||||||
);
|
);
|
||||||
|
|
||||||
window.log.info('closing all sessions for', identifier);
|
window.log.info('closing all sessions for', identifier);
|
||||||
const address = new libsignal.SignalProtocolAddress(identifier, 1);
|
const address = new window.libsignal.SignalProtocolAddress(
|
||||||
|
identifier,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
const sessionCipher = new libsignal.SessionCipher(
|
const sessionCipher = new window.libsignal.SessionCipher(
|
||||||
textsecure.storage.protocol,
|
window.textsecure.storage.protocol,
|
||||||
address
|
address
|
||||||
);
|
);
|
||||||
window.log.info('closing session for', address.toString());
|
window.log.info('closing session for', address.toString());
|
||||||
|
@ -436,7 +507,7 @@ OutgoingMessage.prototype = {
|
||||||
// Primary device
|
// Primary device
|
||||||
sessionCipher.closeOpenSessionForDevice(),
|
sessionCipher.closeOpenSessionForDevice(),
|
||||||
// The rest of their devices
|
// The rest of their devices
|
||||||
textsecure.storage.protocol.archiveSiblingSessions(
|
window.textsecure.storage.protocol.archiveSiblingSessions(
|
||||||
address.toString()
|
address.toString()
|
||||||
),
|
),
|
||||||
]).then(
|
]).then(
|
||||||
|
@ -457,26 +528,25 @@ OutgoingMessage.prototype = {
|
||||||
'Failed to create or send message',
|
'Failed to create or send message',
|
||||||
error
|
error
|
||||||
);
|
);
|
||||||
return null;
|
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
getStaleDeviceIdsForIdentifier(identifier) {
|
async getStaleDeviceIdsForIdentifier(identifier: string) {
|
||||||
return textsecure.storage.protocol
|
return window.textsecure.storage.protocol
|
||||||
.getDeviceIds(identifier)
|
.getDeviceIds(identifier)
|
||||||
.then(deviceIds => {
|
.then(async deviceIds => {
|
||||||
if (deviceIds.length === 0) {
|
if (deviceIds.length === 0) {
|
||||||
return [1];
|
return [1];
|
||||||
}
|
}
|
||||||
const updateDevices = [];
|
const updateDevices: Array<number> = [];
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
deviceIds.map(deviceId => {
|
deviceIds.map(async deviceId => {
|
||||||
const address = new libsignal.SignalProtocolAddress(
|
const address = new window.libsignal.SignalProtocolAddress(
|
||||||
identifier,
|
identifier,
|
||||||
deviceId
|
deviceId
|
||||||
);
|
);
|
||||||
const sessionCipher = new libsignal.SessionCipher(
|
const sessionCipher = new window.libsignal.SessionCipher(
|
||||||
textsecure.storage.protocol,
|
window.textsecure.storage.protocol,
|
||||||
address
|
address
|
||||||
);
|
);
|
||||||
return sessionCipher.hasOpenSession().then(hasSession => {
|
return sessionCipher.hasOpenSession().then(hasSession => {
|
||||||
|
@ -487,21 +557,24 @@ OutgoingMessage.prototype = {
|
||||||
})
|
})
|
||||||
).then(() => updateDevices);
|
).then(() => updateDevices);
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
removeDeviceIdsForIdentifier(identifier, deviceIdsToRemove) {
|
async removeDeviceIdsForIdentifier(
|
||||||
|
identifier: string,
|
||||||
|
deviceIdsToRemove: Array<number>
|
||||||
|
) {
|
||||||
let promise = Promise.resolve();
|
let promise = Promise.resolve();
|
||||||
// eslint-disable-next-line no-restricted-syntax, guard-for-in
|
// tslint:disable-next-line forin no-for-in no-for-in-array
|
||||||
for (const j in deviceIdsToRemove) {
|
for (const j in deviceIdsToRemove) {
|
||||||
promise = promise.then(() => {
|
promise = promise.then(async () => {
|
||||||
const encodedAddress = `${identifier}.${deviceIdsToRemove[j]}`;
|
const encodedAddress = `${identifier}.${deviceIdsToRemove[j]}`;
|
||||||
return textsecure.storage.protocol.removeSession(encodedAddress);
|
return window.textsecure.storage.protocol.removeSession(encodedAddress);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return promise;
|
return promise;
|
||||||
},
|
}
|
||||||
|
|
||||||
async sendToIdentifier(identifier) {
|
async sendToIdentifier(identifier: string) {
|
||||||
try {
|
try {
|
||||||
const updateDevices = await this.getStaleDeviceIdsForIdentifier(
|
const updateDevices = await this.getStaleDeviceIdsForIdentifier(
|
||||||
identifier
|
identifier
|
||||||
|
@ -510,8 +583,7 @@ OutgoingMessage.prototype = {
|
||||||
await this.reloadDevicesAndSend(identifier, true)();
|
await this.reloadDevicesAndSend(identifier, true)();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.message === 'Identity key changed') {
|
if (error.message === 'Identity key changed') {
|
||||||
// eslint-disable-next-line no-param-reassign
|
const newError = new OutgoingIdentityKeyError(
|
||||||
const newError = new textsecure.OutgoingIdentityKeyError(
|
|
||||||
identifier,
|
identifier,
|
||||||
error.originalMessage,
|
error.originalMessage,
|
||||||
error.timestamp,
|
error.timestamp,
|
||||||
|
@ -526,5 +598,5 @@ OutgoingMessage.prototype = {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
};
|
}
|
111
ts/textsecure/ProvisioningCipher.ts
Normal file
111
ts/textsecure/ProvisioningCipher.ts
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
// tslint:disable no-default-export
|
||||||
|
|
||||||
|
import { KeyPairType } from '../libsignal.d';
|
||||||
|
import { ProvisionEnvelopeClass } from '../textsecure.d';
|
||||||
|
|
||||||
|
type ProvisionDecryptResult = {
|
||||||
|
identityKeyPair: KeyPairType;
|
||||||
|
number?: string;
|
||||||
|
uuid?: string;
|
||||||
|
provisioningCode?: string;
|
||||||
|
userAgent?: string;
|
||||||
|
readReceipts?: boolean;
|
||||||
|
profileKey?: ArrayBuffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ProvisioningCipherInner {
|
||||||
|
keyPair?: KeyPairType;
|
||||||
|
|
||||||
|
async decrypt(
|
||||||
|
provisionEnvelope: ProvisionEnvelopeClass
|
||||||
|
): Promise<ProvisionDecryptResult> {
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (!this.keyPair) {
|
||||||
|
throw new Error('ProvisioningCipher.decrypt: No keypair!');
|
||||||
|
}
|
||||||
|
|
||||||
|
return window.libsignal.Curve.async
|
||||||
|
.calculateAgreement(masterEphemeral, this.keyPair.privKey)
|
||||||
|
.then(async ecRes =>
|
||||||
|
window.libsignal.HKDF.deriveSecrets(
|
||||||
|
ecRes,
|
||||||
|
new ArrayBuffer(32),
|
||||||
|
'TextSecure Provisioning Message'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.then(async keys =>
|
||||||
|
window.libsignal.crypto
|
||||||
|
.verifyMAC(ivAndCiphertext, keys[1], mac, 32)
|
||||||
|
.then(async () =>
|
||||||
|
window.libsignal.crypto.decrypt(keys[0], ciphertext, iv)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.then(async plaintext => {
|
||||||
|
const provisionMessage = window.textsecure.protobuf.ProvisionMessage.decode(
|
||||||
|
plaintext
|
||||||
|
);
|
||||||
|
const privKey = provisionMessage.identityKeyPrivate.toArrayBuffer();
|
||||||
|
|
||||||
|
return window.libsignal.Curve.async
|
||||||
|
.createKeyPair(privKey)
|
||||||
|
.then(keyPair => {
|
||||||
|
const ret: ProvisionDecryptResult = {
|
||||||
|
identityKeyPair: keyPair,
|
||||||
|
number: provisionMessage.number,
|
||||||
|
provisioningCode: provisionMessage.provisioningCode,
|
||||||
|
userAgent: provisionMessage.userAgent,
|
||||||
|
readReceipts: provisionMessage.readReceipts,
|
||||||
|
};
|
||||||
|
if (provisionMessage.profileKey) {
|
||||||
|
ret.profileKey = provisionMessage.profileKey.toArrayBuffer();
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async getPublicKey(): Promise<ArrayBuffer> {
|
||||||
|
return Promise.resolve()
|
||||||
|
.then(async () => {
|
||||||
|
if (!this.keyPair) {
|
||||||
|
return window.libsignal.Curve.async
|
||||||
|
.generateKeyPair()
|
||||||
|
.then(keyPair => {
|
||||||
|
this.keyPair = keyPair;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
if (!this.keyPair) {
|
||||||
|
throw new Error('ProvisioningCipher.decrypt: No keypair!');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.keyPair.pubKey;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ProvisioningCipher {
|
||||||
|
constructor() {
|
||||||
|
const inner = new ProvisioningCipherInner();
|
||||||
|
|
||||||
|
this.decrypt = inner.decrypt.bind(inner);
|
||||||
|
this.getPublicKey = inner.getPublicKey.bind(inner);
|
||||||
|
}
|
||||||
|
|
||||||
|
decrypt: (
|
||||||
|
provisionEnvelope: ProvisionEnvelopeClass
|
||||||
|
) => Promise<ProvisionDecryptResult>;
|
||||||
|
getPublicKey: () => Promise<ArrayBuffer>;
|
||||||
|
}
|
1583
ts/textsecure/SendMessage.ts
Normal file
1583
ts/textsecure/SendMessage.ts
Normal file
File diff suppressed because it is too large
Load diff
49
ts/textsecure/Storage.ts
Normal file
49
ts/textsecure/Storage.ts
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
// tslint:disable no-default-export
|
||||||
|
|
||||||
|
import utils from './Helpers';
|
||||||
|
|
||||||
|
// Default implmentation working with localStorage
|
||||||
|
const localStorageImpl = {
|
||||||
|
put(key: string, value: any) {
|
||||||
|
if (value === undefined) {
|
||||||
|
throw new Error('Tried to store undefined');
|
||||||
|
}
|
||||||
|
localStorage.setItem(`${key}`, utils.jsonThing(value));
|
||||||
|
},
|
||||||
|
|
||||||
|
get(key: string, defaultValue: any) {
|
||||||
|
const value = localStorage.getItem(`${key}`);
|
||||||
|
if (value === null) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
return JSON.parse(value);
|
||||||
|
},
|
||||||
|
|
||||||
|
remove(key: string) {
|
||||||
|
localStorage.removeItem(`${key}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface StorageInterface {
|
||||||
|
put(key: string, value: any): void | Promise<void>;
|
||||||
|
get(key: string, defaultValue: any): any;
|
||||||
|
remove(key: string): void | Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Storage = {
|
||||||
|
impl: localStorageImpl as StorageInterface,
|
||||||
|
|
||||||
|
put(key: string, value: any) {
|
||||||
|
return Storage.impl.put(key, value);
|
||||||
|
},
|
||||||
|
|
||||||
|
get(key: string, defaultValue: any) {
|
||||||
|
return Storage.impl.get(key, defaultValue);
|
||||||
|
},
|
||||||
|
|
||||||
|
remove(key: string) {
|
||||||
|
return Storage.impl.remove(key);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Storage;
|
97
ts/textsecure/StringView.ts
Normal file
97
ts/textsecure/StringView.ts
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
// tslint:disable binary-expression-operand-order no-bitwise no-default-export
|
||||||
|
|
||||||
|
const 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: number) {
|
||||||
|
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: string, nBlocksSize: number) {
|
||||||
|
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;
|
||||||
|
let nOutIdx = 0;
|
||||||
|
let nInIdx = 0;
|
||||||
|
for (let nUint24 = 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: number) {
|
||||||
|
return nUint6 < 26
|
||||||
|
? nUint6 + 65
|
||||||
|
: nUint6 < 52
|
||||||
|
? nUint6 + 71
|
||||||
|
: nUint6 < 62
|
||||||
|
? nUint6 - 4
|
||||||
|
: nUint6 === 62
|
||||||
|
? 43
|
||||||
|
: nUint6 === 63
|
||||||
|
? 47
|
||||||
|
: 65;
|
||||||
|
},
|
||||||
|
|
||||||
|
bytesToBase64(aBytes: Uint8Array) {
|
||||||
|
let nMod3;
|
||||||
|
let sB64Enc = '';
|
||||||
|
let nUint24 = 0;
|
||||||
|
const nLen = aBytes.length;
|
||||||
|
for (let 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, '=');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StringView;
|
100
ts/textsecure/SyncRequest.ts
Normal file
100
ts/textsecure/SyncRequest.ts
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
import EventTarget from './EventTarget';
|
||||||
|
import MessageReceiver from './MessageReceiver';
|
||||||
|
import MessageSender from './SendMessage';
|
||||||
|
|
||||||
|
class SyncRequestInner extends EventTarget {
|
||||||
|
receiver: MessageReceiver;
|
||||||
|
contactSync?: boolean;
|
||||||
|
groupSync?: boolean;
|
||||||
|
timeout: any;
|
||||||
|
oncontact: Function;
|
||||||
|
ongroup: Function;
|
||||||
|
|
||||||
|
constructor(sender: MessageSender, receiver: MessageReceiver) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(sender instanceof MessageSender) ||
|
||||||
|
!(receiver instanceof 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 = window.textsecure.storage.user.getNumber();
|
||||||
|
const { wrap, sendOptions } = window.ConversationController.prepareForSend(
|
||||||
|
ourNumber,
|
||||||
|
{
|
||||||
|
syncMessage: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
window.log.info('SyncRequest created. Sending config sync request...');
|
||||||
|
// tslint:disable
|
||||||
|
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: Error) => {
|
||||||
|
window.log.error(
|
||||||
|
'SyncRequest error:',
|
||||||
|
error && error.stack ? error.stack : error
|
||||||
|
);
|
||||||
|
});
|
||||||
|
this.timeout = setTimeout(this.onTimeout.bind(this), 60000);
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class SyncRequest {
|
||||||
|
constructor(sender: MessageSender, receiver: MessageReceiver) {
|
||||||
|
const inner = new SyncRequestInner(sender, receiver);
|
||||||
|
this.addEventListener = inner.addEventListener.bind(inner);
|
||||||
|
this.removeEventListener = inner.removeEventListener.bind(inner);
|
||||||
|
}
|
||||||
|
|
||||||
|
addEventListener: (name: string, handler: Function) => void;
|
||||||
|
removeEventListener: (name: string, handler: Function) => void;
|
||||||
|
}
|
78
ts/textsecure/TaskWithTimeout.ts
Normal file
78
ts/textsecure/TaskWithTimeout.ts
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
// tslint:disable no-default-export
|
||||||
|
|
||||||
|
export default function createTaskWithTimeout(
|
||||||
|
task: () => Promise<any>,
|
||||||
|
id: string,
|
||||||
|
options: { timeout?: number } = {}
|
||||||
|
) {
|
||||||
|
const timeout = options.timeout || 1000 * 60 * 2; // two minutes
|
||||||
|
|
||||||
|
const errorForStack = new Error('for stack');
|
||||||
|
|
||||||
|
return async () =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
let complete = false;
|
||||||
|
let timer: any = setTimeout(() => {
|
||||||
|
if (!complete) {
|
||||||
|
const message = `${id ||
|
||||||
|
''} task did not complete in time. Calling stack: ${
|
||||||
|
errorForStack.stack
|
||||||
|
}`;
|
||||||
|
|
||||||
|
window.log.error(message);
|
||||||
|
reject(new Error(message));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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: any) => {
|
||||||
|
clearTimer();
|
||||||
|
complete = true;
|
||||||
|
resolve(result);
|
||||||
|
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
const failure = (error: Error) => {
|
||||||
|
clearTimer();
|
||||||
|
complete = true;
|
||||||
|
reject(error);
|
||||||
|
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let promise;
|
||||||
|
try {
|
||||||
|
promise = task();
|
||||||
|
} catch (error) {
|
||||||
|
clearTimer();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
if (!promise || !promise.then) {
|
||||||
|
clearTimer();
|
||||||
|
complete = true;
|
||||||
|
resolve(promise);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise.then(success, failure);
|
||||||
|
});
|
||||||
|
}
|
|
@ -4,8 +4,8 @@ import ProxyAgent from 'proxy-agent';
|
||||||
import { Agent } from 'https';
|
import { Agent } from 'https';
|
||||||
|
|
||||||
import is from '@sindresorhus/is';
|
import is from '@sindresorhus/is';
|
||||||
import { redactPackId } from '../js/modules/stickers';
|
import { redactPackId } from '../../js/modules/stickers';
|
||||||
import { getRandomValue } from './Crypto';
|
import { getRandomValue } from '../Crypto';
|
||||||
|
|
||||||
import PQueue from 'p-queue';
|
import PQueue from 'p-queue';
|
||||||
import { v4 as getGuid } from 'uuid';
|
import { v4 as getGuid } from 'uuid';
|
||||||
|
@ -450,6 +450,7 @@ declare global {
|
||||||
interface Error {
|
interface Error {
|
||||||
code?: number | string;
|
code?: number | string;
|
||||||
response?: any;
|
response?: any;
|
||||||
|
warn?: boolean;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -488,10 +489,6 @@ const URL_CALLS = {
|
||||||
whoami: 'v1/accounts/whoami',
|
whoami: 'v1/accounts/whoami',
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
initialize,
|
|
||||||
};
|
|
||||||
|
|
||||||
type InitializeOptionsType = {
|
type InitializeOptionsType = {
|
||||||
url: string;
|
url: string;
|
||||||
cdnUrl: string;
|
cdnUrl: string;
|
||||||
|
@ -520,16 +517,128 @@ type AjaxOptionsType = {
|
||||||
validateResponse?: any;
|
validateResponse?: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type WebAPIConnectType = {
|
||||||
|
connect: (options: ConnectParametersType) => WebAPIType;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StickerPackManifestType = any;
|
||||||
|
|
||||||
|
export type WebAPIType = {
|
||||||
|
confirmCode: (
|
||||||
|
number: string,
|
||||||
|
code: string,
|
||||||
|
newPassword: string,
|
||||||
|
registrationId: number,
|
||||||
|
deviceName?: string | null,
|
||||||
|
options?: { accessKey?: ArrayBuffer }
|
||||||
|
) => Promise<any>;
|
||||||
|
getAttachment: (id: string) => Promise<any>;
|
||||||
|
getAvatar: (path: string) => Promise<any>;
|
||||||
|
getDevices: () => Promise<any>;
|
||||||
|
getKeysForIdentifier: (
|
||||||
|
identifier: string,
|
||||||
|
deviceId?: number
|
||||||
|
) => Promise<ServerKeysType>;
|
||||||
|
getKeysForIdentifierUnauth: (
|
||||||
|
identifier: string,
|
||||||
|
deviceId?: number,
|
||||||
|
options?: { accessKey?: string }
|
||||||
|
) => Promise<ServerKeysType>;
|
||||||
|
getMessageSocket: () => WebSocket;
|
||||||
|
getMyKeys: () => Promise<number>;
|
||||||
|
getProfile: (identifier: string) => Promise<any>;
|
||||||
|
getProfileUnauth: (
|
||||||
|
identifier: string,
|
||||||
|
options?: { accessKey?: string }
|
||||||
|
) => Promise<any>;
|
||||||
|
getProvisioningSocket: () => WebSocket;
|
||||||
|
getSenderCertificate: (withUuid?: boolean) => Promise<any>;
|
||||||
|
getSticker: (packId: string, stickerId: string) => Promise<any>;
|
||||||
|
getStickerPackManifest: (packId: string) => Promise<StickerPackManifestType>;
|
||||||
|
makeProxiedRequest: (
|
||||||
|
targetUrl: string,
|
||||||
|
options?: ProxiedRequestOptionsType
|
||||||
|
) => Promise<any>;
|
||||||
|
putAttachment: (encryptedBin: ArrayBuffer) => Promise<any>;
|
||||||
|
registerCapabilities: (capabilities: any) => Promise<void>;
|
||||||
|
putStickers: (
|
||||||
|
encryptedManifest: ArrayBuffer,
|
||||||
|
encryptedStickers: Array<ArrayBuffer>,
|
||||||
|
onProgress?: () => void
|
||||||
|
) => Promise<string>;
|
||||||
|
registerKeys: (genKeys: KeysType) => Promise<void>;
|
||||||
|
registerSupportForUnauthenticatedDelivery: () => Promise<any>;
|
||||||
|
removeSignalingKey: () => Promise<void>;
|
||||||
|
requestVerificationSMS: (number: string) => Promise<any>;
|
||||||
|
requestVerificationVoice: (number: string) => Promise<any>;
|
||||||
|
sendMessages: (
|
||||||
|
destination: string,
|
||||||
|
messageArray: Array<MessageType>,
|
||||||
|
timestamp: number,
|
||||||
|
silent?: boolean,
|
||||||
|
online?: boolean
|
||||||
|
) => Promise<void>;
|
||||||
|
sendMessagesUnauth: (
|
||||||
|
destination: string,
|
||||||
|
messageArray: Array<MessageType>,
|
||||||
|
timestamp: number,
|
||||||
|
silent?: boolean,
|
||||||
|
online?: boolean,
|
||||||
|
options?: { accessKey?: string }
|
||||||
|
) => Promise<void>;
|
||||||
|
setSignedPreKey: (signedPreKey: SignedPreKeyType) => Promise<void>;
|
||||||
|
updateDeviceName: (deviceName: string) => Promise<void>;
|
||||||
|
whoami: () => Promise<any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SignedPreKeyType = {
|
||||||
|
keyId: number;
|
||||||
|
publicKey: ArrayBuffer;
|
||||||
|
signature: ArrayBuffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type KeysType = {
|
||||||
|
identityKey: ArrayBuffer;
|
||||||
|
signedPreKey: SignedPreKeyType;
|
||||||
|
preKeys: Array<{
|
||||||
|
keyId: number;
|
||||||
|
publicKey: ArrayBuffer;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ServerKeysType = {
|
||||||
|
devices: Array<{
|
||||||
|
deviceId: number;
|
||||||
|
registrationId: number;
|
||||||
|
signedPreKey: {
|
||||||
|
keyId: number;
|
||||||
|
publicKey: ArrayBuffer;
|
||||||
|
signature: ArrayBuffer;
|
||||||
|
};
|
||||||
|
preKey?: {
|
||||||
|
keyId: number;
|
||||||
|
publicKey: ArrayBuffer;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
identityKey: ArrayBuffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ProxiedRequestOptionsType = {
|
||||||
|
returnArrayBuffer?: boolean;
|
||||||
|
start?: number;
|
||||||
|
end?: number;
|
||||||
|
};
|
||||||
|
|
||||||
// We first set up the data that won't change during this session of the app
|
// We first set up the data that won't change during this session of the app
|
||||||
// tslint:disable-next-line max-func-body-length
|
// tslint:disable-next-line max-func-body-length
|
||||||
function initialize({
|
export function initialize({
|
||||||
url,
|
url,
|
||||||
cdnUrl,
|
cdnUrl,
|
||||||
certificateAuthority,
|
certificateAuthority,
|
||||||
contentProxyUrl,
|
contentProxyUrl,
|
||||||
proxyUrl,
|
proxyUrl,
|
||||||
version,
|
version,
|
||||||
}: InitializeOptionsType) {
|
}: InitializeOptionsType): WebAPIConnectType {
|
||||||
if (!is.string(url)) {
|
if (!is.string(url)) {
|
||||||
throw new Error('WebAPI.initialize: Invalid server url');
|
throw new Error('WebAPI.initialize: Invalid server url');
|
||||||
}
|
}
|
||||||
|
@ -744,9 +853,9 @@ function initialize({
|
||||||
number: string,
|
number: string,
|
||||||
code: string,
|
code: string,
|
||||||
newPassword: string,
|
newPassword: string,
|
||||||
registrationId: string,
|
registrationId: number,
|
||||||
deviceName: string,
|
deviceName?: string | null,
|
||||||
options: { accessKey?: string } = {}
|
options: { accessKey?: ArrayBuffer } = {}
|
||||||
) {
|
) {
|
||||||
const { accessKey } = options;
|
const { accessKey } = options;
|
||||||
const jsonData: any = {
|
const jsonData: any = {
|
||||||
|
@ -812,21 +921,6 @@ function initialize({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
type SignedPreKeyType = {
|
|
||||||
keyId: number;
|
|
||||||
publicKey: ArrayBuffer;
|
|
||||||
signature: ArrayBuffer;
|
|
||||||
};
|
|
||||||
|
|
||||||
type KeysType = {
|
|
||||||
identityKey: ArrayBuffer;
|
|
||||||
signedPreKey: SignedPreKeyType;
|
|
||||||
preKeys: Array<{
|
|
||||||
keyId: number;
|
|
||||||
publicKey: ArrayBuffer;
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
|
|
||||||
type JSONSignedPreKeyType = {
|
type JSONSignedPreKeyType = {
|
||||||
keyId: number;
|
keyId: number;
|
||||||
publicKey: string;
|
publicKey: string;
|
||||||
|
@ -918,24 +1012,7 @@ function initialize({
|
||||||
identityKey: string;
|
identityKey: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ServerKeyType = {
|
function handleKeys(res: ServerKeyResponseType): ServerKeysType {
|
||||||
devices: Array<{
|
|
||||||
deviceId: number;
|
|
||||||
registrationId: number;
|
|
||||||
signedPreKey: {
|
|
||||||
keyId: number;
|
|
||||||
publicKey: ArrayBuffer;
|
|
||||||
signature: ArrayBuffer;
|
|
||||||
};
|
|
||||||
preKey?: {
|
|
||||||
keyId: number;
|
|
||||||
publicKey: ArrayBuffer;
|
|
||||||
};
|
|
||||||
}>;
|
|
||||||
identityKey: ArrayBuffer;
|
|
||||||
};
|
|
||||||
|
|
||||||
function handleKeys(res: ServerKeyResponseType): ServerKeyType {
|
|
||||||
if (!Array.isArray(res.devices)) {
|
if (!Array.isArray(res.devices)) {
|
||||||
throw new Error('Invalid response');
|
throw new Error('Invalid response');
|
||||||
}
|
}
|
||||||
|
@ -984,11 +1061,11 @@ function initialize({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getKeysForIdentifier(identifier: string, deviceId = '*') {
|
async function getKeysForIdentifier(identifier: string, deviceId?: number) {
|
||||||
return _ajax({
|
return _ajax({
|
||||||
call: 'keys',
|
call: 'keys',
|
||||||
httpType: 'GET',
|
httpType: 'GET',
|
||||||
urlParameters: `/${identifier}/${deviceId}`,
|
urlParameters: `/${identifier}/${deviceId || '*'}`,
|
||||||
responseType: 'json',
|
responseType: 'json',
|
||||||
validateResponse: { identityKey: 'string', devices: 'object' },
|
validateResponse: { identityKey: 'string', devices: 'object' },
|
||||||
}).then(handleKeys);
|
}).then(handleKeys);
|
||||||
|
@ -996,13 +1073,13 @@ function initialize({
|
||||||
|
|
||||||
async function getKeysForIdentifierUnauth(
|
async function getKeysForIdentifierUnauth(
|
||||||
identifier: string,
|
identifier: string,
|
||||||
deviceId = '*',
|
deviceId?: number,
|
||||||
{ accessKey }: { accessKey?: string } = {}
|
{ accessKey }: { accessKey?: string } = {}
|
||||||
) {
|
) {
|
||||||
return _ajax({
|
return _ajax({
|
||||||
call: 'keys',
|
call: 'keys',
|
||||||
httpType: 'GET',
|
httpType: 'GET',
|
||||||
urlParameters: `/${identifier}/${deviceId}`,
|
urlParameters: `/${identifier}/${deviceId || '*'}`,
|
||||||
responseType: 'json',
|
responseType: 'json',
|
||||||
validateResponse: { identityKey: 'string', devices: 'object' },
|
validateResponse: { identityKey: 'string', devices: 'object' },
|
||||||
unauthenticated: true,
|
unauthenticated: true,
|
||||||
|
@ -1014,8 +1091,8 @@ function initialize({
|
||||||
destination: string,
|
destination: string,
|
||||||
messageArray: Array<MessageType>,
|
messageArray: Array<MessageType>,
|
||||||
timestamp: number,
|
timestamp: number,
|
||||||
silent: boolean,
|
silent?: boolean,
|
||||||
online: boolean,
|
online?: boolean,
|
||||||
{ accessKey }: { accessKey?: string } = {}
|
{ accessKey }: { accessKey?: string } = {}
|
||||||
) {
|
) {
|
||||||
const jsonData: any = { messages: messageArray, timestamp };
|
const jsonData: any = { messages: messageArray, timestamp };
|
||||||
|
@ -1042,8 +1119,8 @@ function initialize({
|
||||||
destination: string,
|
destination: string,
|
||||||
messageArray: Array<MessageType>,
|
messageArray: Array<MessageType>,
|
||||||
timestamp: number,
|
timestamp: number,
|
||||||
silent: boolean,
|
silent?: boolean,
|
||||||
online: boolean
|
online?: boolean
|
||||||
) {
|
) {
|
||||||
const jsonData: any = { messages: messageArray, timestamp };
|
const jsonData: any = { messages: messageArray, timestamp };
|
||||||
|
|
||||||
|
@ -1262,12 +1339,6 @@ function initialize({
|
||||||
return characters;
|
return characters;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProxiedRequestOptionsType = {
|
|
||||||
returnArrayBuffer?: boolean;
|
|
||||||
start?: number;
|
|
||||||
end?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
async function makeProxiedRequest(
|
async function makeProxiedRequest(
|
||||||
targetUrl: string,
|
targetUrl: string,
|
||||||
options: ProxiedRequestOptionsType = {}
|
options: ProxiedRequestOptionsType = {}
|
304
ts/textsecure/WebsocketResources.ts
Normal file
304
ts/textsecure/WebsocketResources.ts
Normal file
|
@ -0,0 +1,304 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
// tslint:disable max-classes-per-file no-default-export no-unnecessary-class
|
||||||
|
|
||||||
|
import { w3cwebsocket as WebSocket } from 'websocket';
|
||||||
|
import { ByteBufferClass } from '../window.d';
|
||||||
|
|
||||||
|
import EventTarget from './EventTarget';
|
||||||
|
|
||||||
|
class Request {
|
||||||
|
verb: string;
|
||||||
|
path: string;
|
||||||
|
headers: Array<string>;
|
||||||
|
body: ByteBufferClass | null;
|
||||||
|
success: Function;
|
||||||
|
error: Function;
|
||||||
|
id: number;
|
||||||
|
response?: any;
|
||||||
|
|
||||||
|
constructor(options: any) {
|
||||||
|
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 = window.dcodeIO.Long.fromBits(bits[0], bits[1], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.body === undefined) {
|
||||||
|
this.body = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IncomingWebSocketRequest {
|
||||||
|
verb: string;
|
||||||
|
path: string;
|
||||||
|
body: ByteBufferClass | null;
|
||||||
|
headers: Array<string>;
|
||||||
|
respond: (status: number, message: string) => void;
|
||||||
|
|
||||||
|
constructor(options: any) {
|
||||||
|
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 window.textsecure.protobuf.WebSocketMessage({
|
||||||
|
type: window.textsecure.protobuf.WebSocketMessage.Type.RESPONSE,
|
||||||
|
response: { id: request.id, message, status },
|
||||||
|
})
|
||||||
|
.encode()
|
||||||
|
.toArrayBuffer()
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const outgoing: {
|
||||||
|
[id: number]: Request;
|
||||||
|
} = {};
|
||||||
|
class OutgoingWebSocketRequest {
|
||||||
|
constructor(options: any, socket: WebSocket) {
|
||||||
|
const request = new Request(options);
|
||||||
|
outgoing[request.id] = request;
|
||||||
|
socket.send(
|
||||||
|
new window.textsecure.protobuf.WebSocketMessage({
|
||||||
|
type: window.textsecure.protobuf.WebSocketMessage.Type.REQUEST,
|
||||||
|
request: {
|
||||||
|
verb: request.verb,
|
||||||
|
path: request.path,
|
||||||
|
body: request.body,
|
||||||
|
headers: request.headers,
|
||||||
|
id: request.id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.encode()
|
||||||
|
.toArrayBuffer()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class WebSocketResource extends EventTarget {
|
||||||
|
closed?: boolean;
|
||||||
|
close: (code?: number, reason?: string) => void;
|
||||||
|
sendRequest: (options: any) => OutgoingWebSocketRequest;
|
||||||
|
keepalive?: KeepAlive;
|
||||||
|
|
||||||
|
// tslint:disable-next-line max-func-body-length
|
||||||
|
constructor(socket: WebSocket, opts: any = {}) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
let { handleRequest } = opts;
|
||||||
|
if (typeof handleRequest !== 'function') {
|
||||||
|
handleRequest = (request: IncomingWebSocketRequest) => {
|
||||||
|
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: ArrayBuffer) => {
|
||||||
|
const message = window.textsecure.protobuf.WebSocketMessage.decode(
|
||||||
|
buffer
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
message.type ===
|
||||||
|
window.textsecure.protobuf.WebSocketMessage.Type.REQUEST &&
|
||||||
|
message.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 ===
|
||||||
|
window.textsecure.protobuf.WebSocketMessage.Type.RESPONSE &&
|
||||||
|
message.response
|
||||||
|
) {
|
||||||
|
const { response } = message;
|
||||||
|
const request = outgoing[response.id];
|
||||||
|
if (request) {
|
||||||
|
request.response = response;
|
||||||
|
let callback = request.error;
|
||||||
|
if (
|
||||||
|
response.status &&
|
||||||
|
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 as ArrayBuffer);
|
||||||
|
};
|
||||||
|
reader.readAsArrayBuffer(blob as any);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (opts.keepalive) {
|
||||||
|
this.keepalive = new KeepAlive(this, {
|
||||||
|
path: opts.keepalive.path,
|
||||||
|
disconnect: opts.keepalive.disconnect,
|
||||||
|
});
|
||||||
|
const resetKeepAliveTimer = this.keepalive.reset.bind(this.keepalive);
|
||||||
|
|
||||||
|
// websocket type definitions don't include an addEventListener, but it's there. And
|
||||||
|
// We can't use declaration merging on classes:
|
||||||
|
// https://www.typescriptlang.org/docs/handbook/declaration-merging.html#disallowed-merges)
|
||||||
|
// @ts-ignore
|
||||||
|
socket.addEventListener('open', resetKeepAliveTimer);
|
||||||
|
// @ts-ignore
|
||||||
|
socket.addEventListener('message', resetKeepAliveTimer);
|
||||||
|
// @ts-ignore
|
||||||
|
socket.addEventListener(
|
||||||
|
'close',
|
||||||
|
this.keepalive.stop.bind(this.keepalive)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
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);
|
||||||
|
// @ts-ignore
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type KeepAliveOptionsType = {
|
||||||
|
path?: string;
|
||||||
|
disconnect?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
class KeepAlive {
|
||||||
|
keepAliveTimer: any;
|
||||||
|
disconnectTimer: any;
|
||||||
|
path: string;
|
||||||
|
disconnect: boolean;
|
||||||
|
wsr: WebSocketResource;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
websocketResource: WebSocketResource,
|
||||||
|
opts: KeepAliveOptionsType = {}
|
||||||
|
) {
|
||||||
|
if (websocketResource instanceof WebSocketResource) {
|
||||||
|
this.path = opts.path !== undefined ? opts.path : '/';
|
||||||
|
this.disconnect = opts.disconnect !== undefined ? opts.disconnect : true;
|
||||||
|
this.wsr = websocketResource;
|
||||||
|
} else {
|
||||||
|
throw new TypeError('KeepAlive expected a WebSocketResource');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
35
ts/textsecure/index.ts
Normal file
35
ts/textsecure/index.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
// tslint:disable no-default-export
|
||||||
|
|
||||||
|
import EventTarget from './EventTarget';
|
||||||
|
import AccountManager from './AccountManager';
|
||||||
|
import MessageReceiver from './MessageReceiver';
|
||||||
|
import utils from './Helpers';
|
||||||
|
import Crypto from './Crypto';
|
||||||
|
import { ContactBuffer, GroupBuffer } from './ContactsParser';
|
||||||
|
import createTaskWithTimeout from './TaskWithTimeout';
|
||||||
|
import SyncRequest from './SyncRequest';
|
||||||
|
import MessageSender from './SendMessage';
|
||||||
|
import StringView from './StringView';
|
||||||
|
import Storage from './Storage';
|
||||||
|
import * as WebAPI from './WebAPI';
|
||||||
|
import WebSocketResource from './WebsocketResources';
|
||||||
|
|
||||||
|
export const textsecure = {
|
||||||
|
createTaskWithTimeout,
|
||||||
|
crypto: Crypto,
|
||||||
|
utils,
|
||||||
|
storage: Storage,
|
||||||
|
|
||||||
|
AccountManager,
|
||||||
|
ContactBuffer,
|
||||||
|
EventTarget,
|
||||||
|
GroupBuffer,
|
||||||
|
MessageReceiver,
|
||||||
|
MessageSender,
|
||||||
|
SyncRequest,
|
||||||
|
StringView,
|
||||||
|
WebAPI,
|
||||||
|
WebSocketResource,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default textsecure;
|
|
@ -9,13 +9,13 @@ window.waitForAllBatchers = async () => {
|
||||||
await Promise.all(window.batchers.map(item => item.flushAndWait()));
|
await Promise.all(window.batchers.map(item => item.flushAndWait()));
|
||||||
};
|
};
|
||||||
|
|
||||||
type BatcherOptionsType<ItemType> = {
|
export type BatcherOptionsType<ItemType> = {
|
||||||
wait: number;
|
wait: number;
|
||||||
maxSize: number;
|
maxSize: number;
|
||||||
processBatch: (items: Array<ItemType>) => Promise<void>;
|
processBatch: (items: Array<ItemType>) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type BatcherType<ItemType> = {
|
export type BatcherType<ItemType> = {
|
||||||
add: (item: ItemType) => void;
|
add: (item: ItemType) => void;
|
||||||
anyPending: () => boolean;
|
anyPending: () => boolean;
|
||||||
onIdle: () => Promise<void>;
|
onIdle: () => Promise<void>;
|
||||||
|
|
|
@ -1191,134 +1191,6 @@
|
||||||
"updated": "2018-09-15T00:38:04.183Z",
|
"updated": "2018-09-15T00:38:04.183Z",
|
||||||
"reasonDetail": "Getting the value, not setting it"
|
"reasonDetail": "Getting the value, not setting it"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"rule": "jQuery-append(",
|
|
||||||
"path": "libtextsecure/contacts_parser.js",
|
|
||||||
"line": " this.buffer.append(arrayBuffer);",
|
|
||||||
"lineNumber": 6,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "libtextsecure/crypto.js",
|
|
||||||
"line": " const data = dcodeIO.ByteBuffer.wrap(",
|
|
||||||
"lineNumber": 206,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "libtextsecure/crypto.js",
|
|
||||||
"line": " given: dcodeIO.ByteBuffer.wrap(padded)",
|
|
||||||
"lineNumber": 235,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2020-01-10T23:53:06.768Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "libtextsecure/crypto.js",
|
|
||||||
"line": " ? dcodeIO.ByteBuffer.wrap(padded)",
|
|
||||||
"lineNumber": 239,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2020-01-10T23:53:06.768Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "libtextsecure/message_receiver.js",
|
|
||||||
"line": " dcodeIO.ByteBuffer.wrap(string, 'binary').toArrayBuffer();",
|
|
||||||
"lineNumber": 72,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2020-02-14T20:02:37.507Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "libtextsecure/message_receiver.js",
|
|
||||||
"line": " dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('binary');",
|
|
||||||
"lineNumber": 74,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2020-02-14T20:02:37.507Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "libtextsecure/message_receiver.js",
|
|
||||||
"line": " dcodeIO.ByteBuffer.wrap(string, 'base64').toArrayBuffer();",
|
|
||||||
"lineNumber": 76,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2020-02-14T20:02:37.507Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "libtextsecure/message_receiver.js",
|
|
||||||
"line": " dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64');",
|
|
||||||
"lineNumber": 78,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2020-02-14T20:02:37.507Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "libtextsecure/message_receiver.js",
|
|
||||||
"line": " const buffer = dcodeIO.ByteBuffer.wrap(ciphertext);",
|
|
||||||
"lineNumber": 822,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2020-03-20T17:24:11.472Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "libtextsecure/message_receiver.js",
|
|
||||||
"line": " const buffer = dcodeIO.ByteBuffer.wrap(ciphertext);",
|
|
||||||
"lineNumber": 847,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2020-03-20T17:24:11.472Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "libtextsecure/sendmessage.js",
|
|
||||||
"line": " return dcodeIO.ByteBuffer.wrap(string, 'hex').toArrayBuffer();",
|
|
||||||
"lineNumber": 18,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2020-02-14T20:02:37.507Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "libtextsecure/sendmessage.js",
|
|
||||||
"line": " return dcodeIO.ByteBuffer.wrap(string, 'base64').toArrayBuffer();",
|
|
||||||
"lineNumber": 21,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2020-02-14T20:02:37.507Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "libtextsecure/sync_request.js",
|
|
||||||
"line": " wrap(sender.sendRequestConfigurationSyncMessage(sendOptions));",
|
|
||||||
"lineNumber": 33,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2018-11-28T19:48:16.607Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "libtextsecure/sync_request.js",
|
|
||||||
"line": " wrap(sender.sendRequestBlockSyncMessage(sendOptions));",
|
|
||||||
"lineNumber": 36,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2019-12-03T00:28:08.683Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "libtextsecure/sync_request.js",
|
|
||||||
"line": " wrap(sender.sendRequestContactSyncMessage(sendOptions))",
|
|
||||||
"lineNumber": 39,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2019-12-03T00:28:08.683Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "libtextsecure/sync_request.js",
|
|
||||||
"line": " return wrap(sender.sendRequestGroupSyncMessage(sendOptions));",
|
|
||||||
"lineNumber": 42,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2019-12-03T00:28:08.683Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"rule": "DOM-innerHTML",
|
"rule": "DOM-innerHTML",
|
||||||
"path": "node_modules/@electron/get/node_modules/@sindresorhus/is/dist/index.js",
|
"path": "node_modules/@electron/get/node_modules/@sindresorhus/is/dist/index.js",
|
||||||
|
@ -11624,5 +11496,253 @@
|
||||||
"lineNumber": 21,
|
"lineNumber": 21,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2020-02-07T19:52:28.522Z"
|
"updated": "2020-02-07T19:52:28.522Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-append(",
|
||||||
|
"path": "ts/textsecure/ContactsParser.js",
|
||||||
|
"line": " this.buffer.append(arrayBuffer);",
|
||||||
|
"lineNumber": 7,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2020-04-05T23:45:16.746Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-append(",
|
||||||
|
"path": "ts/textsecure/ContactsParser.ts",
|
||||||
|
"line": " this.buffer.append(arrayBuffer);",
|
||||||
|
"lineNumber": 26,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2020-04-05T23:45:16.746Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-wrap(",
|
||||||
|
"path": "ts/textsecure/Crypto.js",
|
||||||
|
"line": " const data = window.dcodeIO.ByteBuffer.wrap(encryptedProfileName, 'base64').toArrayBuffer();",
|
||||||
|
"lineNumber": 157,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2020-04-05T23:45:16.746Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-wrap(",
|
||||||
|
"path": "ts/textsecure/Crypto.js",
|
||||||
|
"line": " given: window.dcodeIO.ByteBuffer.wrap(padded)",
|
||||||
|
"lineNumber": 176,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2020-04-05T23:45:16.746Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-wrap(",
|
||||||
|
"path": "ts/textsecure/Crypto.js",
|
||||||
|
"line": " ? window.dcodeIO.ByteBuffer.wrap(padded)",
|
||||||
|
"lineNumber": 180,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2020-04-05T23:45:16.746Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-wrap(",
|
||||||
|
"path": "ts/textsecure/Crypto.ts",
|
||||||
|
"line": " const data = window.dcodeIO.ByteBuffer.wrap(",
|
||||||
|
"lineNumber": 223,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2020-04-05T23:45:16.746Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-wrap(",
|
||||||
|
"path": "ts/textsecure/Crypto.ts",
|
||||||
|
"line": " given: window.dcodeIO.ByteBuffer.wrap(padded)",
|
||||||
|
"lineNumber": 252,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2020-04-05T23:45:16.746Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-wrap(",
|
||||||
|
"path": "ts/textsecure/Crypto.ts",
|
||||||
|
"line": " ? window.dcodeIO.ByteBuffer.wrap(padded)",
|
||||||
|
"lineNumber": 256,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2020-04-05T23:45:16.746Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-wrap(",
|
||||||
|
"path": "ts/textsecure/MessageReceiver.js",
|
||||||
|
"line": " const buffer = window.dcodeIO.ByteBuffer.wrap(ciphertext);",
|
||||||
|
"lineNumber": 665,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2020-04-05T23:45:16.746Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-wrap(",
|
||||||
|
"path": "ts/textsecure/MessageReceiver.js",
|
||||||
|
"line": " const buffer = window.dcodeIO.ByteBuffer.wrap(ciphertext);",
|
||||||
|
"lineNumber": 685,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2020-04-05T23:45:16.746Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-wrap(",
|
||||||
|
"path": "ts/textsecure/MessageReceiver.js",
|
||||||
|
"line": "MessageReceiverInner.stringToArrayBuffer = (string) => window.dcodeIO.ByteBuffer.wrap(string, 'binary').toArrayBuffer();",
|
||||||
|
"lineNumber": 1253,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2020-04-05T23:45:16.746Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-wrap(",
|
||||||
|
"path": "ts/textsecure/MessageReceiver.js",
|
||||||
|
"line": "MessageReceiverInner.arrayBufferToString = (arrayBuffer) => window.dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('binary');",
|
||||||
|
"lineNumber": 1254,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2020-04-05T23:45:16.746Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-wrap(",
|
||||||
|
"path": "ts/textsecure/MessageReceiver.js",
|
||||||
|
"line": "MessageReceiverInner.arrayBufferToStringBase64 = (arrayBuffer) => window.dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64');",
|
||||||
|
"lineNumber": 1256,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2020-04-05T23:45:16.746Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-wrap(",
|
||||||
|
"path": "ts/textsecure/MessageReceiver.ts",
|
||||||
|
"line": " window.dcodeIO.ByteBuffer.wrap(string, 'binary').toArrayBuffer();",
|
||||||
|
"lineNumber": 179,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2020-04-05T23:45:16.746Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-wrap(",
|
||||||
|
"path": "ts/textsecure/MessageReceiver.ts",
|
||||||
|
"line": " window.dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('binary');",
|
||||||
|
"lineNumber": 181,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2020-04-05T23:45:16.746Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-wrap(",
|
||||||
|
"path": "ts/textsecure/MessageReceiver.ts",
|
||||||
|
"line": " window.dcodeIO.ByteBuffer.wrap(string, 'base64').toArrayBuffer();",
|
||||||
|
"lineNumber": 183,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2020-04-05T23:45:16.746Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-wrap(",
|
||||||
|
"path": "ts/textsecure/MessageReceiver.ts",
|
||||||
|
"line": " window.dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64');",
|
||||||
|
"lineNumber": 185,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2020-04-05T23:45:16.746Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-wrap(",
|
||||||
|
"path": "ts/textsecure/MessageReceiver.ts",
|
||||||
|
"line": " const buffer = window.dcodeIO.ByteBuffer.wrap(ciphertext);",
|
||||||
|
"lineNumber": 987,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2020-04-05T23:45:16.746Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-wrap(",
|
||||||
|
"path": "ts/textsecure/MessageReceiver.ts",
|
||||||
|
"line": " const buffer = window.dcodeIO.ByteBuffer.wrap(ciphertext);",
|
||||||
|
"lineNumber": 1016,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2020-04-05T23:45:16.746Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-wrap(",
|
||||||
|
"path": "ts/textsecure/SendMessage.js",
|
||||||
|
"line": " return window.dcodeIO.ByteBuffer.wrap(string, 'hex').toArrayBuffer();",
|
||||||
|
"lineNumber": 25,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2020-04-05T23:45:16.746Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-wrap(",
|
||||||
|
"path": "ts/textsecure/SendMessage.js",
|
||||||
|
"line": " return window.dcodeIO.ByteBuffer.wrap(string, 'base64').toArrayBuffer();",
|
||||||
|
"lineNumber": 28,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2020-04-05T23:45:16.746Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-wrap(",
|
||||||
|
"path": "ts/textsecure/SendMessage.ts",
|
||||||
|
"line": " return window.dcodeIO.ByteBuffer.wrap(string, 'hex').toArrayBuffer();",
|
||||||
|
"lineNumber": 29,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2020-04-05T23:45:16.746Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-wrap(",
|
||||||
|
"path": "ts/textsecure/SendMessage.ts",
|
||||||
|
"line": " return window.dcodeIO.ByteBuffer.wrap(string, 'base64').toArrayBuffer();",
|
||||||
|
"lineNumber": 32,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2020-04-05T23:45:16.746Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-wrap(",
|
||||||
|
"path": "ts/textsecure/SyncRequest.js",
|
||||||
|
"line": " wrap(sender.sendRequestConfigurationSyncMessage(sendOptions));",
|
||||||
|
"lineNumber": 27,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2020-04-05T23:45:16.746Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-wrap(",
|
||||||
|
"path": "ts/textsecure/SyncRequest.js",
|
||||||
|
"line": " wrap(sender.sendRequestBlockSyncMessage(sendOptions));",
|
||||||
|
"lineNumber": 29,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2020-04-05T23:45:16.746Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-wrap(",
|
||||||
|
"path": "ts/textsecure/SyncRequest.js",
|
||||||
|
"line": " wrap(sender.sendRequestContactSyncMessage(sendOptions))",
|
||||||
|
"lineNumber": 31,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2020-04-05T23:45:16.746Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-wrap(",
|
||||||
|
"path": "ts/textsecure/SyncRequest.js",
|
||||||
|
"line": " return wrap(sender.sendRequestGroupSyncMessage(sendOptions));",
|
||||||
|
"lineNumber": 34,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2020-04-05T23:45:16.746Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-wrap(",
|
||||||
|
"path": "ts/textsecure/SyncRequest.ts",
|
||||||
|
"line": " wrap(sender.sendRequestConfigurationSyncMessage(sendOptions));",
|
||||||
|
"lineNumber": 42,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2020-04-05T23:45:16.746Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-wrap(",
|
||||||
|
"path": "ts/textsecure/SyncRequest.ts",
|
||||||
|
"line": " wrap(sender.sendRequestBlockSyncMessage(sendOptions));",
|
||||||
|
"lineNumber": 45,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2020-04-05T23:45:16.746Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-wrap(",
|
||||||
|
"path": "ts/textsecure/SyncRequest.ts",
|
||||||
|
"line": " wrap(sender.sendRequestContactSyncMessage(sendOptions))",
|
||||||
|
"lineNumber": 48,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2020-04-05T23:45:16.746Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-wrap(",
|
||||||
|
"path": "ts/textsecure/SyncRequest.ts",
|
||||||
|
"line": " return wrap(sender.sendRequestGroupSyncMessage(sendOptions));",
|
||||||
|
"lineNumber": 51,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2020-04-05T23:45:16.746Z"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
137
ts/window.d.ts
vendored
137
ts/window.d.ts
vendored
|
@ -1,17 +1,32 @@
|
||||||
// Captures the globals put in place by preload.js, background.js and others
|
// Captures the globals put in place by preload.js, background.js and others
|
||||||
|
|
||||||
|
import {
|
||||||
|
LibSignalType,
|
||||||
|
SignalProtocolAddressClass,
|
||||||
|
StorageType,
|
||||||
|
} from './libsignal.d';
|
||||||
|
import { TextSecureType } from './textsecure.d';
|
||||||
|
import { WebAPIConnectType } from './textsecure/WebAPI';
|
||||||
|
import * as Crypto from './Crypto';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
dcodeIO: DCodeIOType;
|
dcodeIO: DCodeIOType;
|
||||||
getExpiration: () => string;
|
getExpiration: () => string;
|
||||||
getEnvironment: () => string;
|
getEnvironment: () => string;
|
||||||
getSocketStatus: () => number;
|
getSocketStatus: () => number;
|
||||||
|
libphonenumber: {
|
||||||
|
util: {
|
||||||
|
getRegionCodeForNumber: (number: string) => string;
|
||||||
|
};
|
||||||
|
};
|
||||||
libsignal: LibSignalType;
|
libsignal: LibSignalType;
|
||||||
log: {
|
log: {
|
||||||
info: LoggerType;
|
info: LoggerType;
|
||||||
warn: LoggerType;
|
warn: LoggerType;
|
||||||
error: LoggerType;
|
error: LoggerType;
|
||||||
};
|
};
|
||||||
|
normalizeUuids: (obj: any, paths: Array<string>, context: string) => any;
|
||||||
restart: () => void;
|
restart: () => void;
|
||||||
storage: {
|
storage: {
|
||||||
put: (key: string, value: any) => void;
|
put: (key: string, value: any) => void;
|
||||||
|
@ -20,12 +35,32 @@ declare global {
|
||||||
};
|
};
|
||||||
textsecure: TextSecureType;
|
textsecure: TextSecureType;
|
||||||
|
|
||||||
|
Signal: {
|
||||||
|
Crypto: typeof Crypto;
|
||||||
|
Metadata: {
|
||||||
|
SecretSessionCipher: typeof SecretSessionCipherClass;
|
||||||
|
createCertificateValidator: (
|
||||||
|
trustRoot: ArrayBuffer
|
||||||
|
) => CertificateValidatorType;
|
||||||
|
};
|
||||||
|
};
|
||||||
ConversationController: ConversationControllerType;
|
ConversationController: ConversationControllerType;
|
||||||
|
WebAPI: WebAPIConnectType;
|
||||||
Whisper: WhisperType;
|
Whisper: WhisperType;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ConversationType = {
|
||||||
|
updateE164: (e164?: string) => void;
|
||||||
|
updateUuid: (uuid?: string) => void;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type ConversationControllerType = {
|
export type ConversationControllerType = {
|
||||||
|
getOrCreateAndWait: (
|
||||||
|
identifier: string,
|
||||||
|
type: 'private' | 'group'
|
||||||
|
) => Promise<ConversationType>;
|
||||||
getConversationId: (identifier: string) => string | null;
|
getConversationId: (identifier: string) => string | null;
|
||||||
prepareForSend: (
|
prepareForSend: (
|
||||||
id: string,
|
id: string,
|
||||||
|
@ -34,65 +69,65 @@ export type ConversationControllerType = {
|
||||||
wrap: (promise: Promise<any>) => Promise<void>;
|
wrap: (promise: Promise<any>) => Promise<void>;
|
||||||
sendOptions: Object;
|
sendOptions: Object;
|
||||||
};
|
};
|
||||||
|
get: (
|
||||||
|
identifier: string
|
||||||
|
) => null | {
|
||||||
|
get: (key: string) => any;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DCodeIOType = {
|
export type DCodeIOType = {
|
||||||
ByteBuffer: {
|
ByteBuffer: typeof ByteBufferClass;
|
||||||
wrap: (
|
Long: {
|
||||||
value: any,
|
fromBits: (low: number, high: number, unsigned: boolean) => number;
|
||||||
type?: string
|
};
|
||||||
) => {
|
};
|
||||||
|
|
||||||
|
export class CertificateValidatorType {
|
||||||
|
validate: (cerficate: any, certificateTime: number) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SecretSessionCipherClass {
|
||||||
|
constructor(storage: StorageType);
|
||||||
|
decrypt: (
|
||||||
|
validator: CertificateValidatorType,
|
||||||
|
ciphertext: ArrayBuffer,
|
||||||
|
serverTimestamp: number,
|
||||||
|
me: any
|
||||||
|
) => Promise<{
|
||||||
|
isMe: boolean;
|
||||||
|
sender: SignalProtocolAddressClass;
|
||||||
|
senderUuid: SignalProtocolAddressClass;
|
||||||
|
content: ArrayBuffer;
|
||||||
|
}>;
|
||||||
|
getRemoteRegistrationId: (
|
||||||
|
address: SignalProtocolAddressClass
|
||||||
|
) => Promise<number>;
|
||||||
|
closeOpenSessionForDevice: (
|
||||||
|
address: SignalProtocolAddressClass
|
||||||
|
) => Promise<void>;
|
||||||
|
encrypt: (
|
||||||
|
address: SignalProtocolAddressClass,
|
||||||
|
senderCertificate: any,
|
||||||
|
plaintext: ArrayBuffer | Uint8Array
|
||||||
|
) => Promise<ArrayBuffer>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ByteBufferClass {
|
||||||
|
constructor(value?: any, encoding?: string);
|
||||||
|
static wrap: (value: any, type?: string) => ByteBufferClass;
|
||||||
toString: (type: string) => string;
|
toString: (type: string) => string;
|
||||||
toArrayBuffer: () => ArrayBuffer;
|
toArrayBuffer: () => ArrayBuffer;
|
||||||
};
|
slice: (start: number, end?: number) => ByteBufferClass;
|
||||||
};
|
append: (data: ArrayBuffer) => void;
|
||||||
};
|
limit: number;
|
||||||
|
offset: 0;
|
||||||
export type LibSignalType = {
|
readVarint32: () => number;
|
||||||
KeyHelper: {
|
skip: (length: number) => void;
|
||||||
generateIdentityKeyPair: () => Promise<{
|
}
|
||||||
privKey: ArrayBuffer;
|
|
||||||
pubKey: ArrayBuffer;
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
Curve: {
|
|
||||||
async: {
|
|
||||||
calculateAgreement: (
|
|
||||||
publicKey: ArrayBuffer,
|
|
||||||
privateKey: ArrayBuffer
|
|
||||||
) => Promise<ArrayBuffer>;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
HKDF: {
|
|
||||||
deriveSecrets: (
|
|
||||||
packKey: ArrayBuffer,
|
|
||||||
salt: ArrayBuffer,
|
|
||||||
info: ArrayBuffer
|
|
||||||
) => Promise<Array<ArrayBuffer>>;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type LoggerType = (...args: Array<any>) => void;
|
export type LoggerType = (...args: Array<any>) => void;
|
||||||
|
|
||||||
export type TextSecureType = {
|
|
||||||
storage: {
|
|
||||||
user: {
|
|
||||||
getNumber: () => string;
|
|
||||||
};
|
|
||||||
get: (key: string) => any;
|
|
||||||
};
|
|
||||||
messaging: {
|
|
||||||
sendStickerPackSync: (
|
|
||||||
operations: Array<{
|
|
||||||
packId: string;
|
|
||||||
packKey: string;
|
|
||||||
installed: boolean;
|
|
||||||
}>,
|
|
||||||
options: Object
|
|
||||||
) => Promise<void>;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WhisperType = {
|
export type WhisperType = {
|
||||||
events: {
|
events: {
|
||||||
trigger: (name: string, param1: any, param2: any) => void;
|
trigger: (name: string, param1: any, param2: any) => void;
|
||||||
|
|
|
@ -15,6 +15,12 @@
|
||||||
"import-spacing": false,
|
"import-spacing": false,
|
||||||
"indent": [true, "spaces", 2],
|
"indent": [true, "spaces", 2],
|
||||||
"interface-name": [true, "never-prefix"],
|
"interface-name": [true, "never-prefix"],
|
||||||
|
"member-access": false,
|
||||||
|
"member-ordering": false,
|
||||||
|
"newline-before-return": false,
|
||||||
|
"prefer-for-of": false,
|
||||||
|
"no-this-assignment": false,
|
||||||
|
"binary-expression-operand-order": false,
|
||||||
|
|
||||||
// Allows us to write inline `style`s. Revisit when we have a more sophisticated
|
// Allows us to write inline `style`s. Revisit when we have a more sophisticated
|
||||||
// CSS-in-JS solution:
|
// CSS-in-JS solution:
|
||||||
|
@ -98,6 +104,7 @@
|
||||||
true,
|
true,
|
||||||
{
|
{
|
||||||
"function-regex": "^_?[a-z][\\w\\d]+$",
|
"function-regex": "^_?[a-z][\\w\\d]+$",
|
||||||
|
"method-regex": "^_?[a-z][\\w\\d]+$",
|
||||||
"static-method-regex": "^_?[a-z][\\w\\d]+$"
|
"static-method-regex": "^_?[a-z][\\w\\d]+$"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue