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',
|
||||
},
|
||||
src: [
|
||||
'libtextsecure/errors.js',
|
||||
'libtextsecure/libsignal-protocol.js',
|
||||
'libtextsecure/protocol_wrapper.js',
|
||||
|
||||
'libtextsecure/crypto.js',
|
||||
'libtextsecure/storage.js',
|
||||
'libtextsecure/storage/user.js',
|
||||
'libtextsecure/storage/groups.js',
|
||||
'libtextsecure/storage/unprocessed.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',
|
||||
},
|
||||
|
|
|
@ -2349,7 +2349,7 @@
|
|||
sourceUuid: data.sourceUuid,
|
||||
sourceDevice: data.sourceDevice,
|
||||
sent_at: data.timestamp,
|
||||
received_at: data.receivedAt || Date.now(),
|
||||
received_at: Date.now(),
|
||||
conversationId,
|
||||
unidentifiedDeliveryReceived: data.unidentifiedDeliveryReceived,
|
||||
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 theirIdentityKey = util.toArrayBuffer(session.indexInfo.remoteIdentityKey);
|
||||
var macInput = new Uint8Array(encodedMsg.byteLength + 33*2 + 1);
|
||||
|
||||
macInput.set(new Uint8Array(ourIdentityKeyBuffer));
|
||||
macInput.set(new Uint8Array(theirIdentityKey), 33);
|
||||
macInput[33*2] = (3 << 4) | 3;
|
||||
|
@ -36512,10 +36511,20 @@ Internal.SessionLock = {};
|
|||
var jobQueue = {};
|
||||
|
||||
Internal.SessionLock.queueJobForNumber = function queueJobForNumber(number, runJob) {
|
||||
jobQueue[number] = jobQueue[number] || new window.PQueue({ concurrency: 1 });
|
||||
var queue = jobQueue[number];
|
||||
if (window.PQueue) {
|
||||
jobQueue[number] = jobQueue[number] || new window.PQueue({ concurrency: 1 });
|
||||
var queue = jobQueue[number];
|
||||
return queue.add(runJob);
|
||||
}
|
||||
|
||||
return queue.add(runJob);
|
||||
var runPrevious = jobQueue[number] || Promise.resolve();
|
||||
var runCurrent = jobQueue[number] = runPrevious.then(runJob, runJob);
|
||||
runCurrent.then(function() {
|
||||
if (jobQueue[number] === runCurrent) {
|
||||
delete jobQueue[number];
|
||||
}
|
||||
});
|
||||
return runCurrent;
|
||||
};
|
||||
|
||||
})();
|
||||
|
@ -36555,7 +36564,7 @@ Internal.SessionLock.queueJobForNumber = function queueJobForNumber(number, runJ
|
|||
let i = 0;
|
||||
let buf = new Uint8Array(16);
|
||||
|
||||
uuid.replace(/[0-9A-F]{2}/ig, oct => {
|
||||
uuid.replace(/[0-9A-F]{2}/ig, function(oct) {
|
||||
buf[i++] = parseInt(oct, 16);
|
||||
});
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,37 +0,0 @@
|
|||
/* global window, textsecure, localStorage */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
/** **********************************************
|
||||
*** Utilities to store data in local storage ***
|
||||
*********************************************** */
|
||||
window.textsecure = window.textsecure || {};
|
||||
window.textsecure.storage = window.textsecure.storage || {};
|
||||
|
||||
// Overrideable storage implementation
|
||||
window.textsecure.storage.impl = window.textsecure.storage.impl || {
|
||||
/** ***************************
|
||||
*** Base Storage Routines ***
|
||||
**************************** */
|
||||
put(key, value) {
|
||||
if (value === undefined) throw new Error('Tried to store undefined');
|
||||
localStorage.setItem(`${key}`, textsecure.utils.jsonThing(value));
|
||||
},
|
||||
|
||||
get(key, defaultValue) {
|
||||
const value = localStorage.getItem(`${key}`);
|
||||
if (value === null) return defaultValue;
|
||||
return JSON.parse(value);
|
||||
},
|
||||
|
||||
remove(key) {
|
||||
localStorage.removeItem(`${key}`);
|
||||
},
|
||||
};
|
||||
|
||||
window.textsecure.storage.put = (key, value) =>
|
||||
textsecure.storage.impl.put(key, value);
|
||||
window.textsecure.storage.get = (key, defaultValue) =>
|
||||
textsecure.storage.impl.get(key, defaultValue);
|
||||
window.textsecure.storage.remove = key => textsecure.storage.impl.remove(key);
|
||||
})();
|
|
@ -1,104 +0,0 @@
|
|||
/* global window, StringView */
|
||||
|
||||
/* eslint-disable no-bitwise, no-nested-ternary */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
window.StringView = {
|
||||
/*
|
||||
* These functions from the Mozilla Developer Network
|
||||
* and have been placed in the public domain.
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding
|
||||
* https://developer.mozilla.org/en-US/docs/MDN/About#Copyrights_and_licenses
|
||||
*/
|
||||
|
||||
// prettier-ignore
|
||||
b64ToUint6(nChr) {
|
||||
return nChr > 64 && nChr < 91
|
||||
? nChr - 65
|
||||
: nChr > 96 && nChr < 123
|
||||
? nChr - 71
|
||||
: nChr > 47 && nChr < 58
|
||||
? nChr + 4
|
||||
: nChr === 43
|
||||
? 62
|
||||
: nChr === 47
|
||||
? 63
|
||||
: 0;
|
||||
},
|
||||
|
||||
base64ToBytes(sBase64, nBlocksSize) {
|
||||
const sB64Enc = sBase64.replace(/[^A-Za-z0-9+/]/g, '');
|
||||
const nInLen = sB64Enc.length;
|
||||
const nOutLen = nBlocksSize
|
||||
? Math.ceil(((nInLen * 3 + 1) >> 2) / nBlocksSize) * nBlocksSize
|
||||
: (nInLen * 3 + 1) >> 2;
|
||||
const aBBytes = new ArrayBuffer(nOutLen);
|
||||
const taBytes = new Uint8Array(aBBytes);
|
||||
|
||||
let nMod3;
|
||||
let nMod4;
|
||||
for (
|
||||
let nUint24 = 0, nOutIdx = 0, nInIdx = 0;
|
||||
nInIdx < nInLen;
|
||||
nInIdx += 1
|
||||
) {
|
||||
nMod4 = nInIdx & 3;
|
||||
nUint24 |=
|
||||
StringView.b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << (18 - 6 * nMod4);
|
||||
if (nMod4 === 3 || nInLen - nInIdx === 1) {
|
||||
for (
|
||||
nMod3 = 0;
|
||||
nMod3 < 3 && nOutIdx < nOutLen;
|
||||
nMod3 += 1, nOutIdx += 1
|
||||
) {
|
||||
taBytes[nOutIdx] = (nUint24 >>> ((16 >>> nMod3) & 24)) & 255;
|
||||
}
|
||||
nUint24 = 0;
|
||||
}
|
||||
}
|
||||
return aBBytes;
|
||||
},
|
||||
|
||||
// prettier-ignore
|
||||
uint6ToB64(nUint6) {
|
||||
return nUint6 < 26
|
||||
? nUint6 + 65
|
||||
: nUint6 < 52
|
||||
? nUint6 + 71
|
||||
: nUint6 < 62
|
||||
? nUint6 - 4
|
||||
: nUint6 === 62
|
||||
? 43
|
||||
: nUint6 === 63
|
||||
? 47
|
||||
: 65;
|
||||
},
|
||||
|
||||
bytesToBase64(aBytes) {
|
||||
let nMod3;
|
||||
let sB64Enc = '';
|
||||
for (
|
||||
let nLen = aBytes.length, nUint24 = 0, nIdx = 0;
|
||||
nIdx < nLen;
|
||||
nIdx += 1
|
||||
) {
|
||||
nMod3 = nIdx % 3;
|
||||
if (nIdx > 0 && ((nIdx * 4) / 3) % 76 === 0) {
|
||||
sB64Enc += '\r\n';
|
||||
}
|
||||
nUint24 |= aBytes[nIdx] << ((16 >>> nMod3) & 24);
|
||||
if (nMod3 === 2 || aBytes.length - nIdx === 1) {
|
||||
sB64Enc += String.fromCharCode(
|
||||
StringView.uint6ToB64((nUint24 >>> 18) & 63),
|
||||
StringView.uint6ToB64((nUint24 >>> 12) & 63),
|
||||
StringView.uint6ToB64((nUint24 >>> 6) & 63),
|
||||
StringView.uint6ToB64(nUint24 & 63)
|
||||
);
|
||||
nUint24 = 0;
|
||||
}
|
||||
}
|
||||
return sB64Enc.replace(/A(?=A$|$)/g, '=');
|
||||
},
|
||||
};
|
||||
})();
|
|
@ -1,97 +0,0 @@
|
|||
/* global Event, textsecure, window, ConversationController */
|
||||
|
||||
/* eslint-disable more/no-then */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
window.textsecure = window.textsecure || {};
|
||||
|
||||
function SyncRequest(sender, receiver) {
|
||||
if (
|
||||
!(sender instanceof textsecure.MessageSender) ||
|
||||
!(receiver instanceof textsecure.MessageReceiver)
|
||||
) {
|
||||
throw new Error(
|
||||
'Tried to construct a SyncRequest without MessageSender and MessageReceiver'
|
||||
);
|
||||
}
|
||||
this.receiver = receiver;
|
||||
|
||||
this.oncontact = this.onContactSyncComplete.bind(this);
|
||||
receiver.addEventListener('contactsync', this.oncontact);
|
||||
|
||||
this.ongroup = this.onGroupSyncComplete.bind(this);
|
||||
receiver.addEventListener('groupsync', this.ongroup);
|
||||
|
||||
const ourNumber = textsecure.storage.user.getNumber();
|
||||
const {
|
||||
wrap,
|
||||
sendOptions,
|
||||
} = ConversationController.prepareForSend(ourNumber, { syncMessage: true });
|
||||
|
||||
window.log.info('SyncRequest created. Sending config sync request...');
|
||||
wrap(sender.sendRequestConfigurationSyncMessage(sendOptions));
|
||||
|
||||
window.log.info('SyncRequest now sending block sync request...');
|
||||
wrap(sender.sendRequestBlockSyncMessage(sendOptions));
|
||||
|
||||
window.log.info('SyncRequest now sending contact sync message...');
|
||||
wrap(sender.sendRequestContactSyncMessage(sendOptions))
|
||||
.then(() => {
|
||||
window.log.info('SyncRequest now sending group sync messsage...');
|
||||
return wrap(sender.sendRequestGroupSyncMessage(sendOptions));
|
||||
})
|
||||
.catch(error => {
|
||||
window.log.error(
|
||||
'SyncRequest error:',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
});
|
||||
this.timeout = setTimeout(this.onTimeout.bind(this), 60000);
|
||||
}
|
||||
|
||||
SyncRequest.prototype = new textsecure.EventTarget();
|
||||
SyncRequest.prototype.extend({
|
||||
constructor: SyncRequest,
|
||||
onContactSyncComplete() {
|
||||
this.contactSync = true;
|
||||
this.update();
|
||||
},
|
||||
onGroupSyncComplete() {
|
||||
this.groupSync = true;
|
||||
this.update();
|
||||
},
|
||||
update() {
|
||||
if (this.contactSync && this.groupSync) {
|
||||
this.dispatchEvent(new Event('success'));
|
||||
this.cleanup();
|
||||
}
|
||||
},
|
||||
onTimeout() {
|
||||
if (this.contactSync || this.groupSync) {
|
||||
this.dispatchEvent(new Event('success'));
|
||||
} else {
|
||||
this.dispatchEvent(new Event('timeout'));
|
||||
}
|
||||
this.cleanup();
|
||||
},
|
||||
cleanup() {
|
||||
clearTimeout(this.timeout);
|
||||
this.receiver.removeEventListener('contactsync', this.oncontact);
|
||||
this.receiver.removeEventListener('groupSync', this.ongroup);
|
||||
delete this.listeners;
|
||||
},
|
||||
});
|
||||
|
||||
textsecure.SyncRequest = function SyncRequestWrapper(sender, receiver) {
|
||||
const syncRequest = new SyncRequest(sender, receiver);
|
||||
this.addEventListener = syncRequest.addEventListener.bind(syncRequest);
|
||||
this.removeEventListener = syncRequest.removeEventListener.bind(
|
||||
syncRequest
|
||||
);
|
||||
};
|
||||
|
||||
textsecure.SyncRequest.prototype = {
|
||||
constructor: textsecure.SyncRequest,
|
||||
};
|
||||
})();
|
|
@ -1,72 +0,0 @@
|
|||
/* global window */
|
||||
|
||||
/* eslint-disable more/no-then */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
window.textsecure = window.textsecure || {};
|
||||
|
||||
window.textsecure.createTaskWithTimeout = (task, id, options = {}) => {
|
||||
const timeout = options.timeout || 1000 * 60 * 2; // two minutes
|
||||
|
||||
const errorForStack = new Error('for stack');
|
||||
return () =>
|
||||
new Promise((resolve, reject) => {
|
||||
let complete = false;
|
||||
let timer = setTimeout(() => {
|
||||
if (!complete) {
|
||||
const message = `${id ||
|
||||
''} task did not complete in time. Calling stack: ${
|
||||
errorForStack.stack
|
||||
}`;
|
||||
|
||||
window.log.error(message);
|
||||
return reject(new Error(message));
|
||||
}
|
||||
|
||||
return null;
|
||||
}, timeout);
|
||||
const clearTimer = () => {
|
||||
try {
|
||||
const localTimer = timer;
|
||||
if (localTimer) {
|
||||
timer = null;
|
||||
clearTimeout(localTimer);
|
||||
}
|
||||
} catch (error) {
|
||||
window.log.error(
|
||||
id || '',
|
||||
'task ran into problem canceling timer. Calling stack:',
|
||||
errorForStack.stack
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const success = result => {
|
||||
clearTimer();
|
||||
complete = true;
|
||||
return resolve(result);
|
||||
};
|
||||
const failure = error => {
|
||||
clearTimer();
|
||||
complete = true;
|
||||
return reject(error);
|
||||
};
|
||||
|
||||
let promise;
|
||||
try {
|
||||
promise = task();
|
||||
} catch (error) {
|
||||
clearTimer();
|
||||
throw error;
|
||||
}
|
||||
if (!promise || !promise.then) {
|
||||
clearTimer();
|
||||
complete = true;
|
||||
return resolve(promise);
|
||||
}
|
||||
|
||||
return promise.then(success, failure);
|
||||
});
|
||||
};
|
||||
})();
|
|
@ -1,5 +1,3 @@
|
|||
/* global ContactBuffer, GroupBuffer, textsecure */
|
||||
|
||||
describe('ContactBuffer', () => {
|
||||
function getTestBuffer() {
|
||||
const buffer = new dcodeIO.ByteBuffer();
|
||||
|
@ -10,7 +8,7 @@ describe('ContactBuffer', () => {
|
|||
}
|
||||
avatarBuffer.limit = avatarBuffer.offset;
|
||||
avatarBuffer.offset = 0;
|
||||
const contactInfo = new textsecure.protobuf.ContactDetails({
|
||||
const contactInfo = new window.textsecure.protobuf.ContactDetails({
|
||||
name: 'Zero Cool',
|
||||
number: '+10000000000',
|
||||
uuid: '7198E1BD-1293-452A-A098-F982FF201902',
|
||||
|
@ -31,7 +29,7 @@ describe('ContactBuffer', () => {
|
|||
|
||||
it('parses an array buffer of contacts', () => {
|
||||
const arrayBuffer = getTestBuffer();
|
||||
const contactBuffer = new ContactBuffer(arrayBuffer);
|
||||
const contactBuffer = new window.textsecure.ContactBuffer(arrayBuffer);
|
||||
let contact = contactBuffer.next();
|
||||
let count = 0;
|
||||
while (contact !== undefined) {
|
||||
|
@ -62,7 +60,7 @@ describe('GroupBuffer', () => {
|
|||
}
|
||||
avatarBuffer.limit = avatarBuffer.offset;
|
||||
avatarBuffer.offset = 0;
|
||||
const groupInfo = new textsecure.protobuf.GroupDetails({
|
||||
const groupInfo = new window.textsecure.protobuf.GroupDetails({
|
||||
id: new Uint8Array([1, 3, 3, 7]).buffer,
|
||||
name: 'Hackers',
|
||||
membersE164: ['cereal', 'burn', 'phreak', 'joey'],
|
||||
|
@ -89,7 +87,7 @@ describe('GroupBuffer', () => {
|
|||
|
||||
it('parses an array buffer of groups', () => {
|
||||
const arrayBuffer = getTestBuffer();
|
||||
const groupBuffer = new GroupBuffer(arrayBuffer);
|
||||
const groupBuffer = new window.textsecure.GroupBuffer(arrayBuffer);
|
||||
let group = groupBuffer.next();
|
||||
let count = 0;
|
||||
while (group !== undefined) {
|
||||
|
|
|
@ -6,7 +6,7 @@ describe('Helpers', () => {
|
|||
a[0] = 0;
|
||||
a[1] = 255;
|
||||
a[2] = 128;
|
||||
assert.equal(getString(b), '\x00\xff\x80');
|
||||
assert.equal(window.textsecure.utils.getString(b), '\x00\xff\x80');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -15,13 +15,16 @@ describe('Helpers', () => {
|
|||
const anArrayBuffer = new ArrayBuffer(1);
|
||||
const typedArray = new Uint8Array(anArrayBuffer);
|
||||
typedArray[0] = 'a'.charCodeAt(0);
|
||||
assertEqualArrayBuffers(stringToArrayBuffer('a'), anArrayBuffer);
|
||||
assertEqualArrayBuffers(
|
||||
window.textsecure.utils.stringToArrayBuffer('a'),
|
||||
anArrayBuffer
|
||||
);
|
||||
});
|
||||
it('throws an error when passed a non string', () => {
|
||||
const notStringable = [{}, undefined, null, new ArrayBuffer()];
|
||||
notStringable.forEach(notString => {
|
||||
assert.throw(() => {
|
||||
stringToArrayBuffer(notString);
|
||||
window.textsecure.utils.stringToArrayBuffer(notString);
|
||||
}, Error);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -19,23 +19,10 @@
|
|||
|
||||
<script type="text/javascript" src="../components.js"></script>
|
||||
<script type="text/javascript" src="../libsignal-protocol.js"></script>
|
||||
<script type="text/javascript" src="../crypto.js"></script>
|
||||
<script type="text/javascript" src="../protobufs.js" data-cover></script>
|
||||
<script type="text/javascript" src="../errors.js" data-cover></script>
|
||||
<script type="text/javascript" src="../storage.js" data-cover></script>
|
||||
|
||||
<script type="text/javascript" src="../event_target.js" data-cover></script>
|
||||
<script type="text/javascript" src="../websocket-resources.js" data-cover></script>
|
||||
<script type="text/javascript" src="../helpers.js" data-cover></script>
|
||||
<script type="text/javascript" src="../stringview.js" data-cover></script>
|
||||
<script type="text/javascript" src="../api.js"></script>
|
||||
<script type="text/javascript" src="../sendmessage.js" data-cover></script>
|
||||
<script type="text/javascript" src="../account_manager.js" data-cover></script>
|
||||
<script type="text/javascript" src="../contacts_parser.js" data-cover></script>
|
||||
<script type="text/javascript" src="../task_with_timeout.js" data-cover></script>
|
||||
<script type="text/javascript" src="../storage/user.js" data-cover></script>
|
||||
|
||||
<script type="text/javascript" src="../protocol_wrapper.js" data-cover></script>
|
||||
|
||||
<script type="text/javascript" src="../../js/libphonenumber-util.js"></script>
|
||||
<script type="text/javascript" src="../../js/components.js" data-cover></script>
|
||||
<script type="text/javascript" src="../../js/signal_protocol_store.js" data-cover></script>
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
/* global textsecure, WebSocketResource */
|
||||
|
||||
describe('WebSocket-Resource', () => {
|
||||
describe('requests and responses', () => {
|
||||
it('receives requests and sends responses', done => {
|
||||
|
@ -7,10 +5,12 @@ describe('WebSocket-Resource', () => {
|
|||
const requestId = '1';
|
||||
const socket = {
|
||||
send(data) {
|
||||
const message = textsecure.protobuf.WebSocketMessage.decode(data);
|
||||
const message = window.textsecure.protobuf.WebSocketMessage.decode(
|
||||
data
|
||||
);
|
||||
assert.strictEqual(
|
||||
message.type,
|
||||
textsecure.protobuf.WebSocketMessage.Type.RESPONSE
|
||||
window.textsecure.protobuf.WebSocketMessage.Type.RESPONSE
|
||||
);
|
||||
assert.strictEqual(message.response.message, 'OK');
|
||||
assert.strictEqual(message.response.status, 200);
|
||||
|
@ -21,7 +21,7 @@ describe('WebSocket-Resource', () => {
|
|||
};
|
||||
|
||||
// actual test
|
||||
this.resource = new WebSocketResource(socket, {
|
||||
this.resource = new window.textsecure.WebSocketResource(socket, {
|
||||
handleRequest(request) {
|
||||
assert.strictEqual(request.verb, 'PUT');
|
||||
assert.strictEqual(request.path, '/some/path');
|
||||
|
@ -36,8 +36,8 @@ describe('WebSocket-Resource', () => {
|
|||
// mock socket request
|
||||
socket.onmessage({
|
||||
data: new Blob([
|
||||
new textsecure.protobuf.WebSocketMessage({
|
||||
type: textsecure.protobuf.WebSocketMessage.Type.REQUEST,
|
||||
new window.textsecure.protobuf.WebSocketMessage({
|
||||
type: window.textsecure.protobuf.WebSocketMessage.Type.REQUEST,
|
||||
request: {
|
||||
id: requestId,
|
||||
verb: 'PUT',
|
||||
|
@ -56,10 +56,12 @@ describe('WebSocket-Resource', () => {
|
|||
let requestId;
|
||||
const socket = {
|
||||
send(data) {
|
||||
const message = textsecure.protobuf.WebSocketMessage.decode(data);
|
||||
const message = window.textsecure.protobuf.WebSocketMessage.decode(
|
||||
data
|
||||
);
|
||||
assert.strictEqual(
|
||||
message.type,
|
||||
textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
||||
window.textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
||||
);
|
||||
assert.strictEqual(message.request.verb, 'PUT');
|
||||
assert.strictEqual(message.request.path, '/some/path');
|
||||
|
@ -73,7 +75,7 @@ describe('WebSocket-Resource', () => {
|
|||
};
|
||||
|
||||
// actual test
|
||||
const resource = new WebSocketResource(socket);
|
||||
const resource = new window.textsecure.WebSocketResource(socket);
|
||||
resource.sendRequest({
|
||||
verb: 'PUT',
|
||||
path: '/some/path',
|
||||
|
@ -89,8 +91,8 @@ describe('WebSocket-Resource', () => {
|
|||
// mock socket response
|
||||
socket.onmessage({
|
||||
data: new Blob([
|
||||
new textsecure.protobuf.WebSocketMessage({
|
||||
type: textsecure.protobuf.WebSocketMessage.Type.RESPONSE,
|
||||
new window.textsecure.protobuf.WebSocketMessage({
|
||||
type: window.textsecure.protobuf.WebSocketMessage.Type.RESPONSE,
|
||||
response: { id: requestId, message: 'OK', status: 200 },
|
||||
})
|
||||
.encode()
|
||||
|
@ -112,7 +114,7 @@ describe('WebSocket-Resource', () => {
|
|||
mockServer.on('connection', server => {
|
||||
server.on('close', done);
|
||||
});
|
||||
const resource = new WebSocketResource(
|
||||
const resource = new window.textsecure.WebSocketResource(
|
||||
new WebSocket('ws://localhost:8081')
|
||||
);
|
||||
resource.close();
|
||||
|
@ -131,10 +133,12 @@ describe('WebSocket-Resource', () => {
|
|||
const mockServer = new MockServer('ws://localhost:8081');
|
||||
mockServer.on('connection', server => {
|
||||
server.on('message', data => {
|
||||
const message = textsecure.protobuf.WebSocketMessage.decode(data);
|
||||
const message = window.textsecure.protobuf.WebSocketMessage.decode(
|
||||
data
|
||||
);
|
||||
assert.strictEqual(
|
||||
message.type,
|
||||
textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
||||
window.textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
||||
);
|
||||
assert.strictEqual(message.request.verb, 'GET');
|
||||
assert.strictEqual(message.request.path, '/v1/keepalive');
|
||||
|
@ -142,7 +146,7 @@ describe('WebSocket-Resource', () => {
|
|||
done();
|
||||
});
|
||||
});
|
||||
this.resource = new WebSocketResource(
|
||||
this.resource = new window.textsecure.WebSocketResource(
|
||||
new WebSocket('ws://loc1alhost:8081'),
|
||||
{
|
||||
keepalive: { path: '/v1/keepalive' },
|
||||
|
@ -154,10 +158,12 @@ describe('WebSocket-Resource', () => {
|
|||
const mockServer = new MockServer('ws://localhost:8081');
|
||||
mockServer.on('connection', server => {
|
||||
server.on('message', data => {
|
||||
const message = textsecure.protobuf.WebSocketMessage.decode(data);
|
||||
const message = window.textsecure.protobuf.WebSocketMessage.decode(
|
||||
data
|
||||
);
|
||||
assert.strictEqual(
|
||||
message.type,
|
||||
textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
||||
window.textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
||||
);
|
||||
assert.strictEqual(message.request.verb, 'GET');
|
||||
assert.strictEqual(message.request.path, '/');
|
||||
|
@ -165,7 +171,7 @@ describe('WebSocket-Resource', () => {
|
|||
done();
|
||||
});
|
||||
});
|
||||
this.resource = new WebSocketResource(
|
||||
this.resource = new window.textsecure.WebSocketResource(
|
||||
new WebSocket('ws://localhost:8081'),
|
||||
{
|
||||
keepalive: true,
|
||||
|
@ -180,7 +186,9 @@ describe('WebSocket-Resource', () => {
|
|||
mockServer.on('connection', server => {
|
||||
server.on('close', done);
|
||||
});
|
||||
this.resource = new WebSocketResource(socket, { keepalive: true });
|
||||
this.resource = new window.textsecure.WebSocketResource(socket, {
|
||||
keepalive: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('allows resetting the keepalive timer', function thisNeeded2(done) {
|
||||
|
@ -190,10 +198,12 @@ describe('WebSocket-Resource', () => {
|
|||
const startTime = Date.now();
|
||||
mockServer.on('connection', server => {
|
||||
server.on('message', data => {
|
||||
const message = textsecure.protobuf.WebSocketMessage.decode(data);
|
||||
const message = window.textsecure.protobuf.WebSocketMessage.decode(
|
||||
data
|
||||
);
|
||||
assert.strictEqual(
|
||||
message.type,
|
||||
textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
||||
window.textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
||||
);
|
||||
assert.strictEqual(message.request.verb, 'GET');
|
||||
assert.strictEqual(message.request.path, '/');
|
||||
|
@ -205,7 +215,9 @@ describe('WebSocket-Resource', () => {
|
|||
done();
|
||||
});
|
||||
});
|
||||
const resource = new WebSocketResource(socket, { keepalive: true });
|
||||
const resource = new window.textsecure.WebSocketResource(socket, {
|
||||
keepalive: true,
|
||||
});
|
||||
setTimeout(() => {
|
||||
resource.resetKeepAliveTimer();
|
||||
}, 5000);
|
||||
|
|
|
@ -1,243 +0,0 @@
|
|||
/* global window, dcodeIO, Event, textsecure, FileReader, WebSocketResource */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
/*
|
||||
* WebSocket-Resources
|
||||
*
|
||||
* Create a request-response interface over websockets using the
|
||||
* WebSocket-Resources sub-protocol[1].
|
||||
*
|
||||
* var client = new WebSocketResource(socket, function(request) {
|
||||
* request.respond(200, 'OK');
|
||||
* });
|
||||
*
|
||||
* client.sendRequest({
|
||||
* verb: 'PUT',
|
||||
* path: '/v1/messages',
|
||||
* body: '{ some: "json" }',
|
||||
* success: function(message, status, request) {...},
|
||||
* error: function(message, status, request) {...}
|
||||
* });
|
||||
*
|
||||
* 1. https://github.com/signalapp/WebSocket-Resources
|
||||
*
|
||||
*/
|
||||
|
||||
const Request = function Request(options) {
|
||||
this.verb = options.verb || options.type;
|
||||
this.path = options.path || options.url;
|
||||
this.headers = options.headers;
|
||||
this.body = options.body || options.data;
|
||||
this.success = options.success;
|
||||
this.error = options.error;
|
||||
this.id = options.id;
|
||||
|
||||
if (this.id === undefined) {
|
||||
const bits = new Uint32Array(2);
|
||||
window.crypto.getRandomValues(bits);
|
||||
this.id = dcodeIO.Long.fromBits(bits[0], bits[1], true);
|
||||
}
|
||||
|
||||
if (this.body === undefined) {
|
||||
this.body = null;
|
||||
}
|
||||
};
|
||||
|
||||
const IncomingWebSocketRequest = function IncomingWebSocketRequest(options) {
|
||||
const request = new Request(options);
|
||||
const { socket } = options;
|
||||
|
||||
this.verb = request.verb;
|
||||
this.path = request.path;
|
||||
this.body = request.body;
|
||||
this.headers = request.headers;
|
||||
|
||||
this.respond = (status, message) => {
|
||||
socket.send(
|
||||
new textsecure.protobuf.WebSocketMessage({
|
||||
type: textsecure.protobuf.WebSocketMessage.Type.RESPONSE,
|
||||
response: { id: request.id, message, status },
|
||||
})
|
||||
.encode()
|
||||
.toArrayBuffer()
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
const outgoing = {};
|
||||
const OutgoingWebSocketRequest = function OutgoingWebSocketRequest(
|
||||
options,
|
||||
socket
|
||||
) {
|
||||
const request = new Request(options);
|
||||
outgoing[request.id] = request;
|
||||
socket.send(
|
||||
new textsecure.protobuf.WebSocketMessage({
|
||||
type: textsecure.protobuf.WebSocketMessage.Type.REQUEST,
|
||||
request: {
|
||||
verb: request.verb,
|
||||
path: request.path,
|
||||
body: request.body,
|
||||
headers: request.headers,
|
||||
id: request.id,
|
||||
},
|
||||
})
|
||||
.encode()
|
||||
.toArrayBuffer()
|
||||
);
|
||||
};
|
||||
|
||||
window.WebSocketResource = function WebSocketResource(socket, opts = {}) {
|
||||
let { handleRequest } = opts;
|
||||
if (typeof handleRequest !== 'function') {
|
||||
handleRequest = request => request.respond(404, 'Not found');
|
||||
}
|
||||
this.sendRequest = options => new OutgoingWebSocketRequest(options, socket);
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
socket.onmessage = socketMessage => {
|
||||
const blob = socketMessage.data;
|
||||
const handleArrayBuffer = buffer => {
|
||||
const message = textsecure.protobuf.WebSocketMessage.decode(buffer);
|
||||
if (
|
||||
message.type === textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
||||
) {
|
||||
handleRequest(
|
||||
new IncomingWebSocketRequest({
|
||||
verb: message.request.verb,
|
||||
path: message.request.path,
|
||||
body: message.request.body,
|
||||
headers: message.request.headers,
|
||||
id: message.request.id,
|
||||
socket,
|
||||
})
|
||||
);
|
||||
} else if (
|
||||
message.type === textsecure.protobuf.WebSocketMessage.Type.RESPONSE
|
||||
) {
|
||||
const { response } = message;
|
||||
const request = outgoing[response.id];
|
||||
if (request) {
|
||||
request.response = response;
|
||||
let callback = request.error;
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
callback = request.success;
|
||||
}
|
||||
|
||||
if (typeof callback === 'function') {
|
||||
callback(response.message, response.status, request);
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
`Received response for unknown request ${message.response.id}`
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (blob instanceof ArrayBuffer) {
|
||||
handleArrayBuffer(blob);
|
||||
} else {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => handleArrayBuffer(reader.result);
|
||||
reader.readAsArrayBuffer(blob);
|
||||
}
|
||||
};
|
||||
|
||||
if (opts.keepalive) {
|
||||
this.keepalive = new KeepAlive(this, {
|
||||
path: opts.keepalive.path,
|
||||
disconnect: opts.keepalive.disconnect,
|
||||
});
|
||||
const resetKeepAliveTimer = this.keepalive.reset.bind(this.keepalive);
|
||||
socket.addEventListener('open', resetKeepAliveTimer);
|
||||
socket.addEventListener('message', resetKeepAliveTimer);
|
||||
socket.addEventListener(
|
||||
'close',
|
||||
this.keepalive.stop.bind(this.keepalive)
|
||||
);
|
||||
}
|
||||
|
||||
socket.addEventListener('close', () => {
|
||||
this.closed = true;
|
||||
});
|
||||
|
||||
this.close = (code = 3000, reason) => {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.log.info('WebSocketResource.close()');
|
||||
if (this.keepalive) {
|
||||
this.keepalive.stop();
|
||||
}
|
||||
|
||||
socket.close(code, reason);
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
socket.onmessage = null;
|
||||
|
||||
// On linux the socket can wait a long time to emit its close event if we've
|
||||
// lost the internet connection. On the order of minutes. This speeds that
|
||||
// process up.
|
||||
setTimeout(() => {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
this.closed = true;
|
||||
|
||||
window.log.warn('Dispatching our own socket close event');
|
||||
const ev = new Event('close');
|
||||
ev.code = code;
|
||||
ev.reason = reason;
|
||||
this.dispatchEvent(ev);
|
||||
}, 5000);
|
||||
};
|
||||
};
|
||||
window.WebSocketResource.prototype = new textsecure.EventTarget();
|
||||
|
||||
function KeepAlive(websocketResource, opts = {}) {
|
||||
if (websocketResource instanceof WebSocketResource) {
|
||||
this.path = opts.path;
|
||||
if (this.path === undefined) {
|
||||
this.path = '/';
|
||||
}
|
||||
this.disconnect = opts.disconnect;
|
||||
if (this.disconnect === undefined) {
|
||||
this.disconnect = true;
|
||||
}
|
||||
this.wsr = websocketResource;
|
||||
} else {
|
||||
throw new TypeError('KeepAlive expected a WebSocketResource');
|
||||
}
|
||||
}
|
||||
|
||||
KeepAlive.prototype = {
|
||||
constructor: KeepAlive,
|
||||
stop() {
|
||||
clearTimeout(this.keepAliveTimer);
|
||||
clearTimeout(this.disconnectTimer);
|
||||
},
|
||||
reset() {
|
||||
clearTimeout(this.keepAliveTimer);
|
||||
clearTimeout(this.disconnectTimer);
|
||||
this.keepAliveTimer = setTimeout(() => {
|
||||
if (this.disconnect) {
|
||||
// automatically disconnect if server doesn't ack
|
||||
this.disconnectTimer = setTimeout(() => {
|
||||
clearTimeout(this.keepAliveTimer);
|
||||
this.wsr.close(3001, 'No response to keepalive request');
|
||||
}, 10000);
|
||||
} else {
|
||||
this.reset();
|
||||
}
|
||||
window.log.info('Sending a keepalive message');
|
||||
this.wsr.sendRequest({
|
||||
verb: 'GET',
|
||||
path: this.path,
|
||||
success: this.reset.bind(this),
|
||||
});
|
||||
}, 55000);
|
||||
},
|
||||
};
|
||||
})();
|
|
@ -222,9 +222,9 @@ try {
|
|||
|
||||
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,
|
||||
cdnUrl: config.cdnUrl,
|
||||
certificateAuthority: config.certificateAuthority,
|
||||
|
|
|
@ -29,7 +29,7 @@ const Signal = require('../js/modules/signal');
|
|||
|
||||
window.Signal = Signal.setup({});
|
||||
|
||||
const { initialize: initializeWebAPI } = require('../ts/WebAPI');
|
||||
const { initialize: initializeWebAPI } = require('../ts/textsecure/WebAPI');
|
||||
|
||||
const WebAPI = initializeWebAPI({
|
||||
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('COMMIT TRANSACTION;');
|
||||
console.log('updateToSchemaVersion20: success!');
|
||||
} catch (error) {
|
||||
await instance.run('ROLLBACK;');
|
||||
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';
|
||||
|
||||
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.buffer = new window.dcodeIO.ByteBuffer();
|
||||
this.buffer.append(arrayBuffer);
|
||||
this.buffer.offset = 0;
|
||||
this.buffer.limit = arrayBuffer.byteLength;
|
||||
}
|
||||
|
||||
function ProtoParser(arrayBuffer, protobuf) {
|
||||
this.protobuf = protobuf;
|
||||
this.buffer = new dcodeIO.ByteBuffer();
|
||||
this.buffer.append(arrayBuffer);
|
||||
this.buffer.offset = 0;
|
||||
this.buffer.limit = arrayBuffer.byteLength;
|
||||
}
|
||||
ProtoParser.prototype = {
|
||||
constructor: ProtoParser,
|
||||
next() {
|
||||
try {
|
||||
if (this.buffer.limit === this.buffer.offset) {
|
||||
|
@ -18,8 +37,6 @@ ProtoParser.prototype = {
|
|||
const nextBuffer = this.buffer
|
||||
.slice(this.buffer.offset, this.buffer.offset + len)
|
||||
.toArrayBuffer();
|
||||
// TODO: de-dupe ByteBuffer.js includes in libaxo/libts
|
||||
// then remove this toArrayBuffer call.
|
||||
|
||||
const proto = this.protobuf.decode(nextBuffer);
|
||||
this.buffer.skip(len);
|
||||
|
@ -61,15 +78,17 @@ ProtoParser.prototype = {
|
|||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
};
|
||||
const GroupBuffer = function Constructor(arrayBuffer) {
|
||||
ProtoParser.call(this, arrayBuffer, textsecure.protobuf.GroupDetails);
|
||||
};
|
||||
GroupBuffer.prototype = Object.create(ProtoParser.prototype);
|
||||
GroupBuffer.prototype.constructor = GroupBuffer;
|
||||
const ContactBuffer = function Constructor(arrayBuffer) {
|
||||
ProtoParser.call(this, arrayBuffer, textsecure.protobuf.ContactDetails);
|
||||
};
|
||||
ContactBuffer.prototype = Object.create(ProtoParser.prototype);
|
||||
ContactBuffer.prototype.constructor = ContactBuffer;
|
||||
}
|
||||
}
|
||||
|
||||
export class GroupBuffer extends ProtoParser {
|
||||
constructor(arrayBuffer: ArrayBuffer) {
|
||||
super(arrayBuffer, window.textsecure.protobuf.GroupDetails as any);
|
||||
}
|
||||
}
|
||||
|
||||
export class ContactBuffer extends ProtoParser {
|
||||
constructor(arrayBuffer: ArrayBuffer) {
|
||||
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,45 +1,86 @@
|
|||
/* 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(
|
||||
server,
|
||||
timestamp,
|
||||
identifiers,
|
||||
message,
|
||||
silent,
|
||||
callback,
|
||||
options = {}
|
||||
) {
|
||||
if (message instanceof textsecure.protobuf.DataMessage) {
|
||||
const content = new textsecure.protobuf.Content();
|
||||
content.dataMessage = message;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
message = content;
|
||||
type OutgoingMessageOptionsType = SendOptionsType & {
|
||||
online?: boolean;
|
||||
};
|
||||
|
||||
export default class OutgoingMessage {
|
||||
server: WebAPIType;
|
||||
timestamp: number;
|
||||
identifiers: Array<string>;
|
||||
message: ContentClass;
|
||||
callback: (result: CallbackResultType) => void;
|
||||
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;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
this.message = content;
|
||||
} else {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
this.server = server;
|
||||
this.timestamp = timestamp;
|
||||
this.identifiers = identifiers;
|
||||
this.callback = callback;
|
||||
this.silent = silent;
|
||||
|
||||
this.identifiersCompleted = 0;
|
||||
this.errors = [];
|
||||
this.successfulIdentifiers = [];
|
||||
this.failoverIdentifiers = [];
|
||||
this.unidentifiedDeliveries = [];
|
||||
|
||||
const {
|
||||
sendMetadata,
|
||||
senderCertificate,
|
||||
senderCertificateWithUuid,
|
||||
online,
|
||||
} = options || ({} as any);
|
||||
this.sendMetadata = sendMetadata;
|
||||
this.senderCertificate = senderCertificate;
|
||||
this.senderCertificateWithUuid = senderCertificateWithUuid;
|
||||
this.online = online;
|
||||
}
|
||||
this.server = server;
|
||||
this.timestamp = timestamp;
|
||||
this.identifiers = identifiers;
|
||||
this.message = message; // ContentMessage proto
|
||||
this.callback = callback;
|
||||
this.silent = silent;
|
||||
|
||||
this.identifiersCompleted = 0;
|
||||
this.errors = [];
|
||||
this.successfulIdentifiers = [];
|
||||
this.failoverIdentifiers = [];
|
||||
this.unidentifiedDeliveries = [];
|
||||
|
||||
const { sendMetadata, senderCertificate, senderCertificateWithUuid, online } =
|
||||
options || {};
|
||||
this.sendMetadata = sendMetadata;
|
||||
this.senderCertificate = senderCertificate;
|
||||
this.senderCertificateWithUuid = senderCertificateWithUuid;
|
||||
this.online = online;
|
||||
}
|
||||
|
||||
OutgoingMessage.prototype = {
|
||||
constructor: OutgoingMessage,
|
||||
numberCompleted() {
|
||||
this.identifiersCompleted += 1;
|
||||
if (this.identifiersCompleted >= this.identifiers.length) {
|
||||
|
@ -50,11 +91,11 @@ OutgoingMessage.prototype = {
|
|||
unidentifiedDeliveries: this.unidentifiedDeliveries,
|
||||
});
|
||||
}
|
||||
},
|
||||
registerError(identifier, reason, error) {
|
||||
}
|
||||
registerError(identifier: string, reason: string, error?: Error) {
|
||||
if (!error || (error.name === 'HTTPError' && error.code !== 404)) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
error = new textsecure.OutgoingMessageError(
|
||||
// tslint:disable-next-line no-parameter-reassignment
|
||||
error = new OutgoingMessageError(
|
||||
identifier,
|
||||
this.message.toArrayBuffer(),
|
||||
this.timestamp,
|
||||
|
@ -66,50 +107,57 @@ OutgoingMessage.prototype = {
|
|||
error.reason = reason;
|
||||
this.errors[this.errors.length] = error;
|
||||
this.numberCompleted();
|
||||
},
|
||||
reloadDevicesAndSend(identifier, recurse) {
|
||||
return () =>
|
||||
textsecure.storage.protocol.getDeviceIds(identifier).then(deviceIds => {
|
||||
if (deviceIds.length === 0) {
|
||||
return this.registerError(
|
||||
identifier,
|
||||
'Got empty device list when loading device keys',
|
||||
null
|
||||
);
|
||||
}
|
||||
return this.doSendMessage(identifier, deviceIds, recurse);
|
||||
});
|
||||
},
|
||||
}
|
||||
reloadDevicesAndSend(
|
||||
identifier: string,
|
||||
recurse?: boolean
|
||||
): () => Promise<void> {
|
||||
return async () =>
|
||||
window.textsecure.storage.protocol
|
||||
.getDeviceIds(identifier)
|
||||
.then(async deviceIds => {
|
||||
if (deviceIds.length === 0) {
|
||||
this.registerError(
|
||||
identifier,
|
||||
'Got empty device list when loading device keys',
|
||||
undefined
|
||||
);
|
||||
return;
|
||||
}
|
||||
return this.doSendMessage(identifier, deviceIds, recurse);
|
||||
});
|
||||
}
|
||||
|
||||
getKeysForIdentifier(identifier, updateDevices) {
|
||||
const handleResult = response =>
|
||||
// tslint:disable-next-line max-func-body-length
|
||||
async getKeysForIdentifier(identifier: string, updateDevices: Array<number>) {
|
||||
const handleResult = async (response: ServerKeysType) =>
|
||||
Promise.all(
|
||||
response.devices.map(device => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
device.identityKey = response.identityKey;
|
||||
response.devices.map(async device => {
|
||||
if (
|
||||
updateDevices === undefined ||
|
||||
updateDevices.indexOf(device.deviceId) > -1
|
||||
) {
|
||||
const address = new libsignal.SignalProtocolAddress(
|
||||
const address = new window.libsignal.SignalProtocolAddress(
|
||||
identifier,
|
||||
device.deviceId
|
||||
);
|
||||
const builder = new libsignal.SessionBuilder(
|
||||
textsecure.storage.protocol,
|
||||
const builder = new window.libsignal.SessionBuilder(
|
||||
window.textsecure.storage.protocol,
|
||||
address
|
||||
);
|
||||
if (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') {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
error.timestamp = this.timestamp;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
error.originalMessage = this.message.toArrayBuffer();
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
error.identityKey = device.identityKey;
|
||||
error.identityKey = response.identityKey;
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
|
@ -121,40 +169,40 @@ OutgoingMessage.prototype = {
|
|||
|
||||
const { sendMetadata } = this;
|
||||
const info =
|
||||
sendMetadata && sendMetadata[identifier] ? sendMetadata[identifier] : {};
|
||||
const { accessKey } = info || {};
|
||||
sendMetadata && sendMetadata[identifier]
|
||||
? sendMetadata[identifier]
|
||||
: { accessKey: undefined };
|
||||
const { accessKey } = info;
|
||||
|
||||
if (updateDevices === undefined) {
|
||||
if (accessKey) {
|
||||
return this.server
|
||||
.getKeysForIdentifierUnauth(identifier, '*', { accessKey })
|
||||
.catch(error => {
|
||||
.getKeysForIdentifierUnauth(identifier, undefined, { accessKey })
|
||||
.catch(async (error: Error) => {
|
||||
if (error.code === 401 || error.code === 403) {
|
||||
if (this.failoverIdentifiers.indexOf(identifier) === -1) {
|
||||
this.failoverIdentifiers.push(identifier);
|
||||
}
|
||||
return this.server.getKeysForIdentifier(identifier, '*');
|
||||
return this.server.getKeysForIdentifier(identifier);
|
||||
}
|
||||
throw error;
|
||||
})
|
||||
.then(handleResult);
|
||||
}
|
||||
|
||||
return this.server
|
||||
.getKeysForIdentifier(identifier, '*')
|
||||
.then(handleResult);
|
||||
return this.server.getKeysForIdentifier(identifier).then(handleResult);
|
||||
}
|
||||
|
||||
let promise = Promise.resolve();
|
||||
let promise: Promise<any> = Promise.resolve();
|
||||
updateDevices.forEach(deviceId => {
|
||||
promise = promise.then(() => {
|
||||
promise = promise.then(async () => {
|
||||
let innerPromise;
|
||||
|
||||
if (accessKey) {
|
||||
innerPromise = this.server
|
||||
.getKeysForIdentifierUnauth(identifier, deviceId, { accessKey })
|
||||
.then(handleResult)
|
||||
.catch(error => {
|
||||
.catch(async error => {
|
||||
if (error.code === 401 || error.code === 403) {
|
||||
if (this.failoverIdentifiers.indexOf(identifier) === -1) {
|
||||
this.failoverIdentifiers.push(identifier);
|
||||
|
@ -171,12 +219,12 @@ OutgoingMessage.prototype = {
|
|||
.then(handleResult);
|
||||
}
|
||||
|
||||
return innerPromise.catch(e => {
|
||||
return innerPromise.catch(async e => {
|
||||
if (e.name === 'HTTPError' && e.code === 404) {
|
||||
if (deviceId !== 1) {
|
||||
return this.removeDeviceIdsForIdentifier(identifier, [deviceId]);
|
||||
}
|
||||
throw new textsecure.UnregisteredUserError(identifier, e);
|
||||
throw new UnregisteredUserError(identifier, e);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
|
@ -185,9 +233,14 @@ OutgoingMessage.prototype = {
|
|||
});
|
||||
|
||||
return promise;
|
||||
},
|
||||
}
|
||||
|
||||
transmitMessage(identifier, jsonData, timestamp, { accessKey } = {}) {
|
||||
async transmitMessage(
|
||||
identifier: string,
|
||||
jsonData: Array<any>,
|
||||
timestamp: number,
|
||||
{ accessKey }: { accessKey?: string } = {}
|
||||
) {
|
||||
let promise;
|
||||
|
||||
if (accessKey) {
|
||||
|
@ -215,20 +268,15 @@ OutgoingMessage.prototype = {
|
|||
// 404 should throw UnregisteredUserError
|
||||
// all other network errors can be retried later.
|
||||
if (e.code === 404) {
|
||||
throw new textsecure.UnregisteredUserError(identifier, e);
|
||||
throw new UnregisteredUserError(identifier, e);
|
||||
}
|
||||
throw new textsecure.SendMessageNetworkError(
|
||||
identifier,
|
||||
jsonData,
|
||||
e,
|
||||
timestamp
|
||||
);
|
||||
throw new SendMessageNetworkError(identifier, jsonData, e);
|
||||
}
|
||||
throw e;
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
getPaddedMessageLength(messageLength) {
|
||||
getPaddedMessageLength(messageLength: number) {
|
||||
const messageLengthWithTerminator = messageLength + 1;
|
||||
let messagePartCount = Math.floor(messageLengthWithTerminator / 160);
|
||||
|
||||
|
@ -237,7 +285,7 @@ OutgoingMessage.prototype = {
|
|||
}
|
||||
|
||||
return messagePartCount * 160;
|
||||
},
|
||||
}
|
||||
|
||||
getPlaintext() {
|
||||
if (!this.plaintext) {
|
||||
|
@ -249,16 +297,29 @@ OutgoingMessage.prototype = {
|
|||
this.plaintext[messageBuffer.byteLength] = 0x80;
|
||||
}
|
||||
return this.plaintext;
|
||||
},
|
||||
}
|
||||
|
||||
doSendMessage(identifier, deviceIds, recurse) {
|
||||
const ciphers = {};
|
||||
// tslint:disable-next-line max-func-body-length
|
||||
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 { sendMetadata } = this;
|
||||
const info =
|
||||
sendMetadata && sendMetadata[identifier] ? sendMetadata[identifier] : {};
|
||||
const { accessKey, useUuidSenderCert } = info || {};
|
||||
sendMetadata && sendMetadata[identifier]
|
||||
? sendMetadata[identifier]
|
||||
: { accessKey: undefined, useUuidSenderCert: undefined };
|
||||
const { accessKey, useUuidSenderCert } = info;
|
||||
const senderCertificate = useUuidSenderCert
|
||||
? this.senderCertificateWithUuid
|
||||
: this.senderCertificate;
|
||||
|
@ -272,27 +333,29 @@ OutgoingMessage.prototype = {
|
|||
const sealedSender = Boolean(accessKey && senderCertificate);
|
||||
|
||||
// We don't send to ourselves if unless sealedSender is enabled
|
||||
const ourNumber = textsecure.storage.user.getNumber();
|
||||
const ourUuid = textsecure.storage.user.getUuid();
|
||||
const ourDeviceId = textsecure.storage.user.getDeviceId();
|
||||
const ourNumber = window.textsecure.storage.user.getNumber();
|
||||
const ourUuid = window.textsecure.storage.user.getUuid();
|
||||
const ourDeviceId = window.textsecure.storage.user.getDeviceId();
|
||||
if ((identifier === ourNumber || identifier === ourUuid) && !sealedSender) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
deviceIds = _.reject(
|
||||
// tslint:disable-next-line no-parameter-reassignment
|
||||
deviceIds = reject(
|
||||
deviceIds,
|
||||
deviceId =>
|
||||
// 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(
|
||||
deviceIds.map(async deviceId => {
|
||||
const address = new libsignal.SignalProtocolAddress(
|
||||
const address = new window.libsignal.SignalProtocolAddress(
|
||||
identifier,
|
||||
deviceId
|
||||
);
|
||||
|
||||
const options = {};
|
||||
const options: any = {};
|
||||
|
||||
// No limit on message keys if we're communicating with our other devices
|
||||
if (ourNumber === identifier || ourUuid === identifier) {
|
||||
|
@ -301,7 +364,7 @@ OutgoingMessage.prototype = {
|
|||
|
||||
if (sealedSender) {
|
||||
const secretSessionCipher = new window.Signal.Metadata.SecretSessionCipher(
|
||||
textsecure.storage.protocol
|
||||
window.textsecure.storage.protocol
|
||||
);
|
||||
ciphers[address.getDeviceId()] = secretSessionCipher;
|
||||
|
||||
|
@ -312,32 +375,32 @@ OutgoingMessage.prototype = {
|
|||
);
|
||||
|
||||
return {
|
||||
type: textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER,
|
||||
type: window.textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER,
|
||||
destinationDeviceId: address.getDeviceId(),
|
||||
destinationRegistrationId: await secretSessionCipher.getRemoteRegistrationId(
|
||||
address
|
||||
),
|
||||
content: window.Signal.Crypto.arrayBufferToBase64(ciphertext),
|
||||
};
|
||||
} else {
|
||||
const sessionCipher = new window.libsignal.SessionCipher(
|
||||
window.textsecure.storage.protocol,
|
||||
address,
|
||||
options
|
||||
);
|
||||
ciphers[address.getDeviceId()] = sessionCipher;
|
||||
|
||||
const ciphertext = await sessionCipher.encrypt(plaintext);
|
||||
return {
|
||||
type: ciphertext.type,
|
||||
destinationDeviceId: address.getDeviceId(),
|
||||
destinationRegistrationId: ciphertext.registrationId,
|
||||
content: btoa(ciphertext.body),
|
||||
};
|
||||
}
|
||||
|
||||
const sessionCipher = new libsignal.SessionCipher(
|
||||
textsecure.storage.protocol,
|
||||
address,
|
||||
options
|
||||
);
|
||||
ciphers[address.getDeviceId()] = sessionCipher;
|
||||
|
||||
const ciphertext = await sessionCipher.encrypt(plaintext);
|
||||
return {
|
||||
type: ciphertext.type,
|
||||
destinationDeviceId: address.getDeviceId(),
|
||||
destinationRegistrationId: ciphertext.registrationId,
|
||||
content: btoa(ciphertext.body),
|
||||
};
|
||||
})
|
||||
)
|
||||
.then(jsonData => {
|
||||
.then(async jsonData => {
|
||||
if (sealedSender) {
|
||||
return this.transmitMessage(identifier, jsonData, this.timestamp, {
|
||||
accessKey,
|
||||
|
@ -347,18 +410,18 @@ OutgoingMessage.prototype = {
|
|||
this.successfulIdentifiers.push(identifier);
|
||||
this.numberCompleted();
|
||||
},
|
||||
error => {
|
||||
async (error: Error) => {
|
||||
if (error.code === 401 || error.code === 403) {
|
||||
if (this.failoverIdentifiers.indexOf(identifier) === -1) {
|
||||
this.failoverIdentifiers.push(identifier);
|
||||
}
|
||||
|
||||
// This ensures that we don't hit this codepath the next time through
|
||||
if (info) {
|
||||
info.accessKey = null;
|
||||
info.accessKey = undefined;
|
||||
}
|
||||
|
||||
// Set final parameter to true to ensure we don't hit this codepath a
|
||||
// second time.
|
||||
return this.doSendMessage(identifier, deviceIds, recurse, true);
|
||||
return this.doSendMessage(identifier, deviceIds, recurse);
|
||||
}
|
||||
|
||||
throw error;
|
||||
|
@ -373,20 +436,22 @@ OutgoingMessage.prototype = {
|
|||
}
|
||||
);
|
||||
})
|
||||
.catch(error => {
|
||||
.catch(async error => {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
error.name === 'HTTPError' &&
|
||||
(error.code === 410 || error.code === 409)
|
||||
) {
|
||||
if (!recurse)
|
||||
return this.registerError(
|
||||
if (!recurse) {
|
||||
this.registerError(
|
||||
identifier,
|
||||
'Hit retry limit attempting to reload device list',
|
||||
error
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let p;
|
||||
let p: Promise<any> = Promise.resolve();
|
||||
if (error.code === 409) {
|
||||
p = this.removeDeviceIdsForIdentifier(
|
||||
identifier,
|
||||
|
@ -394,15 +459,18 @@ OutgoingMessage.prototype = {
|
|||
);
|
||||
} else {
|
||||
p = Promise.all(
|
||||
error.response.staleDevices.map(deviceId =>
|
||||
error.response.staleDevices.map(async (deviceId: number) =>
|
||||
ciphers[deviceId].closeOpenSessionForDevice(
|
||||
new libsignal.SignalProtocolAddress(identifier, deviceId)
|
||||
new window.libsignal.SignalProtocolAddress(
|
||||
identifier,
|
||||
deviceId
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return p.then(() => {
|
||||
return p.then(async () => {
|
||||
const resetDevices =
|
||||
error.code === 410
|
||||
? error.response.staleDevices
|
||||
|
@ -425,10 +493,13 @@ OutgoingMessage.prototype = {
|
|||
);
|
||||
|
||||
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(
|
||||
textsecure.storage.protocol,
|
||||
const sessionCipher = new window.libsignal.SessionCipher(
|
||||
window.textsecure.storage.protocol,
|
||||
address
|
||||
);
|
||||
window.log.info('closing session for', address.toString());
|
||||
|
@ -436,7 +507,7 @@ OutgoingMessage.prototype = {
|
|||
// Primary device
|
||||
sessionCipher.closeOpenSessionForDevice(),
|
||||
// The rest of their devices
|
||||
textsecure.storage.protocol.archiveSiblingSessions(
|
||||
window.textsecure.storage.protocol.archiveSiblingSessions(
|
||||
address.toString()
|
||||
),
|
||||
]).then(
|
||||
|
@ -457,26 +528,25 @@ OutgoingMessage.prototype = {
|
|||
'Failed to create or send message',
|
||||
error
|
||||
);
|
||||
return null;
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
getStaleDeviceIdsForIdentifier(identifier) {
|
||||
return textsecure.storage.protocol
|
||||
async getStaleDeviceIdsForIdentifier(identifier: string) {
|
||||
return window.textsecure.storage.protocol
|
||||
.getDeviceIds(identifier)
|
||||
.then(deviceIds => {
|
||||
.then(async deviceIds => {
|
||||
if (deviceIds.length === 0) {
|
||||
return [1];
|
||||
}
|
||||
const updateDevices = [];
|
||||
const updateDevices: Array<number> = [];
|
||||
return Promise.all(
|
||||
deviceIds.map(deviceId => {
|
||||
const address = new libsignal.SignalProtocolAddress(
|
||||
deviceIds.map(async deviceId => {
|
||||
const address = new window.libsignal.SignalProtocolAddress(
|
||||
identifier,
|
||||
deviceId
|
||||
);
|
||||
const sessionCipher = new libsignal.SessionCipher(
|
||||
textsecure.storage.protocol,
|
||||
const sessionCipher = new window.libsignal.SessionCipher(
|
||||
window.textsecure.storage.protocol,
|
||||
address
|
||||
);
|
||||
return sessionCipher.hasOpenSession().then(hasSession => {
|
||||
|
@ -487,21 +557,24 @@ OutgoingMessage.prototype = {
|
|||
})
|
||||
).then(() => updateDevices);
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
removeDeviceIdsForIdentifier(identifier, deviceIdsToRemove) {
|
||||
async removeDeviceIdsForIdentifier(
|
||||
identifier: string,
|
||||
deviceIdsToRemove: Array<number>
|
||||
) {
|
||||
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) {
|
||||
promise = promise.then(() => {
|
||||
promise = promise.then(async () => {
|
||||
const encodedAddress = `${identifier}.${deviceIdsToRemove[j]}`;
|
||||
return textsecure.storage.protocol.removeSession(encodedAddress);
|
||||
return window.textsecure.storage.protocol.removeSession(encodedAddress);
|
||||
});
|
||||
}
|
||||
return promise;
|
||||
},
|
||||
}
|
||||
|
||||
async sendToIdentifier(identifier) {
|
||||
async sendToIdentifier(identifier: string) {
|
||||
try {
|
||||
const updateDevices = await this.getStaleDeviceIdsForIdentifier(
|
||||
identifier
|
||||
|
@ -510,8 +583,7 @@ OutgoingMessage.prototype = {
|
|||
await this.reloadDevicesAndSend(identifier, true)();
|
||||
} catch (error) {
|
||||
if (error.message === 'Identity key changed') {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
const newError = new textsecure.OutgoingIdentityKeyError(
|
||||
const newError = new OutgoingIdentityKeyError(
|
||||
identifier,
|
||||
error.originalMessage,
|
||||
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 is from '@sindresorhus/is';
|
||||
import { redactPackId } from '../js/modules/stickers';
|
||||
import { getRandomValue } from './Crypto';
|
||||
import { redactPackId } from '../../js/modules/stickers';
|
||||
import { getRandomValue } from '../Crypto';
|
||||
|
||||
import PQueue from 'p-queue';
|
||||
import { v4 as getGuid } from 'uuid';
|
||||
|
@ -450,6 +450,7 @@ declare global {
|
|||
interface Error {
|
||||
code?: number | string;
|
||||
response?: any;
|
||||
warn?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -488,10 +489,6 @@ const URL_CALLS = {
|
|||
whoami: 'v1/accounts/whoami',
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
initialize,
|
||||
};
|
||||
|
||||
type InitializeOptionsType = {
|
||||
url: string;
|
||||
cdnUrl: string;
|
||||
|
@ -520,16 +517,128 @@ type AjaxOptionsType = {
|
|||
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
|
||||
// tslint:disable-next-line max-func-body-length
|
||||
function initialize({
|
||||
export function initialize({
|
||||
url,
|
||||
cdnUrl,
|
||||
certificateAuthority,
|
||||
contentProxyUrl,
|
||||
proxyUrl,
|
||||
version,
|
||||
}: InitializeOptionsType) {
|
||||
}: InitializeOptionsType): WebAPIConnectType {
|
||||
if (!is.string(url)) {
|
||||
throw new Error('WebAPI.initialize: Invalid server url');
|
||||
}
|
||||
|
@ -744,9 +853,9 @@ function initialize({
|
|||
number: string,
|
||||
code: string,
|
||||
newPassword: string,
|
||||
registrationId: string,
|
||||
deviceName: string,
|
||||
options: { accessKey?: string } = {}
|
||||
registrationId: number,
|
||||
deviceName?: string | null,
|
||||
options: { accessKey?: ArrayBuffer } = {}
|
||||
) {
|
||||
const { accessKey } = options;
|
||||
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 = {
|
||||
keyId: number;
|
||||
publicKey: string;
|
||||
|
@ -918,24 +1012,7 @@ function initialize({
|
|||
identityKey: string;
|
||||
};
|
||||
|
||||
type ServerKeyType = {
|
||||
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 {
|
||||
function handleKeys(res: ServerKeyResponseType): ServerKeysType {
|
||||
if (!Array.isArray(res.devices)) {
|
||||
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({
|
||||
call: 'keys',
|
||||
httpType: 'GET',
|
||||
urlParameters: `/${identifier}/${deviceId}`,
|
||||
urlParameters: `/${identifier}/${deviceId || '*'}`,
|
||||
responseType: 'json',
|
||||
validateResponse: { identityKey: 'string', devices: 'object' },
|
||||
}).then(handleKeys);
|
||||
|
@ -996,13 +1073,13 @@ function initialize({
|
|||
|
||||
async function getKeysForIdentifierUnauth(
|
||||
identifier: string,
|
||||
deviceId = '*',
|
||||
deviceId?: number,
|
||||
{ accessKey }: { accessKey?: string } = {}
|
||||
) {
|
||||
return _ajax({
|
||||
call: 'keys',
|
||||
httpType: 'GET',
|
||||
urlParameters: `/${identifier}/${deviceId}`,
|
||||
urlParameters: `/${identifier}/${deviceId || '*'}`,
|
||||
responseType: 'json',
|
||||
validateResponse: { identityKey: 'string', devices: 'object' },
|
||||
unauthenticated: true,
|
||||
|
@ -1014,8 +1091,8 @@ function initialize({
|
|||
destination: string,
|
||||
messageArray: Array<MessageType>,
|
||||
timestamp: number,
|
||||
silent: boolean,
|
||||
online: boolean,
|
||||
silent?: boolean,
|
||||
online?: boolean,
|
||||
{ accessKey }: { accessKey?: string } = {}
|
||||
) {
|
||||
const jsonData: any = { messages: messageArray, timestamp };
|
||||
|
@ -1042,8 +1119,8 @@ function initialize({
|
|||
destination: string,
|
||||
messageArray: Array<MessageType>,
|
||||
timestamp: number,
|
||||
silent: boolean,
|
||||
online: boolean
|
||||
silent?: boolean,
|
||||
online?: boolean
|
||||
) {
|
||||
const jsonData: any = { messages: messageArray, timestamp };
|
||||
|
||||
|
@ -1262,12 +1339,6 @@ function initialize({
|
|||
return characters;
|
||||
}
|
||||
|
||||
type ProxiedRequestOptionsType = {
|
||||
returnArrayBuffer?: boolean;
|
||||
start?: number;
|
||||
end?: number;
|
||||
};
|
||||
|
||||
async function makeProxiedRequest(
|
||||
targetUrl: string,
|
||||
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()));
|
||||
};
|
||||
|
||||
type BatcherOptionsType<ItemType> = {
|
||||
export type BatcherOptionsType<ItemType> = {
|
||||
wait: number;
|
||||
maxSize: number;
|
||||
processBatch: (items: Array<ItemType>) => Promise<void>;
|
||||
};
|
||||
|
||||
type BatcherType<ItemType> = {
|
||||
export type BatcherType<ItemType> = {
|
||||
add: (item: ItemType) => void;
|
||||
anyPending: () => boolean;
|
||||
onIdle: () => Promise<void>;
|
||||
|
|
|
@ -1191,134 +1191,6 @@
|
|||
"updated": "2018-09-15T00:38:04.183Z",
|
||||
"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",
|
||||
"path": "node_modules/@electron/get/node_modules/@sindresorhus/is/dist/index.js",
|
||||
|
@ -11624,5 +11496,253 @@
|
|||
"lineNumber": 21,
|
||||
"reasonCategory": "falseMatch",
|
||||
"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"
|
||||
}
|
||||
]
|
||||
|
|
135
ts/window.d.ts
vendored
135
ts/window.d.ts
vendored
|
@ -1,17 +1,32 @@
|
|||
// 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 {
|
||||
interface Window {
|
||||
dcodeIO: DCodeIOType;
|
||||
getExpiration: () => string;
|
||||
getEnvironment: () => string;
|
||||
getSocketStatus: () => number;
|
||||
libphonenumber: {
|
||||
util: {
|
||||
getRegionCodeForNumber: (number: string) => string;
|
||||
};
|
||||
};
|
||||
libsignal: LibSignalType;
|
||||
log: {
|
||||
info: LoggerType;
|
||||
warn: LoggerType;
|
||||
error: LoggerType;
|
||||
};
|
||||
normalizeUuids: (obj: any, paths: Array<string>, context: string) => any;
|
||||
restart: () => void;
|
||||
storage: {
|
||||
put: (key: string, value: any) => void;
|
||||
|
@ -20,12 +35,32 @@ declare global {
|
|||
};
|
||||
textsecure: TextSecureType;
|
||||
|
||||
Signal: {
|
||||
Crypto: typeof Crypto;
|
||||
Metadata: {
|
||||
SecretSessionCipher: typeof SecretSessionCipherClass;
|
||||
createCertificateValidator: (
|
||||
trustRoot: ArrayBuffer
|
||||
) => CertificateValidatorType;
|
||||
};
|
||||
};
|
||||
ConversationController: ConversationControllerType;
|
||||
WebAPI: WebAPIConnectType;
|
||||
Whisper: WhisperType;
|
||||
}
|
||||
}
|
||||
|
||||
export type ConversationType = {
|
||||
updateE164: (e164?: string) => void;
|
||||
updateUuid: (uuid?: string) => void;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type ConversationControllerType = {
|
||||
getOrCreateAndWait: (
|
||||
identifier: string,
|
||||
type: 'private' | 'group'
|
||||
) => Promise<ConversationType>;
|
||||
getConversationId: (identifier: string) => string | null;
|
||||
prepareForSend: (
|
||||
id: string,
|
||||
|
@ -34,65 +69,65 @@ export type ConversationControllerType = {
|
|||
wrap: (promise: Promise<any>) => Promise<void>;
|
||||
sendOptions: Object;
|
||||
};
|
||||
get: (
|
||||
identifier: string
|
||||
) => null | {
|
||||
get: (key: string) => any;
|
||||
};
|
||||
};
|
||||
|
||||
export type DCodeIOType = {
|
||||
ByteBuffer: {
|
||||
wrap: (
|
||||
value: any,
|
||||
type?: string
|
||||
) => {
|
||||
toString: (type: string) => string;
|
||||
toArrayBuffer: () => ArrayBuffer;
|
||||
};
|
||||
ByteBuffer: typeof ByteBufferClass;
|
||||
Long: {
|
||||
fromBits: (low: number, high: number, unsigned: boolean) => number;
|
||||
};
|
||||
};
|
||||
|
||||
export type LibSignalType = {
|
||||
KeyHelper: {
|
||||
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 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;
|
||||
toArrayBuffer: () => ArrayBuffer;
|
||||
slice: (start: number, end?: number) => ByteBufferClass;
|
||||
append: (data: ArrayBuffer) => void;
|
||||
limit: number;
|
||||
offset: 0;
|
||||
readVarint32: () => number;
|
||||
skip: (length: number) => 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 = {
|
||||
events: {
|
||||
trigger: (name: string, param1: any, param2: any) => void;
|
||||
|
|
|
@ -15,6 +15,12 @@
|
|||
"import-spacing": false,
|
||||
"indent": [true, "spaces", 2],
|
||||
"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
|
||||
// CSS-in-JS solution:
|
||||
|
@ -98,6 +104,7 @@
|
|||
true,
|
||||
{
|
||||
"function-regex": "^_?[a-z][\\w\\d]+$",
|
||||
"method-regex": "^_?[a-z][\\w\\d]+$",
|
||||
"static-method-regex": "^_?[a-z][\\w\\d]+$"
|
||||
}
|
||||
],
|
||||
|
|
Loading…
Reference in a new issue