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:
Scott Nonnenberg 2020-04-13 10:37:29 -07:00
parent 2f2d027161
commit b7d56def82
45 changed files with 5983 additions and 4042 deletions

View File

@ -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',
},

View File

@ -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',

View File

@ -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);
};
})();

View File

@ -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;
})();

View File

@ -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);
},
};
})();

View File

@ -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;
})();

View File

@ -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;
})();

View File

@ -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;
})();

View File

@ -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

View File

@ -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);
})();

View File

@ -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, '=');
},
};
})();

View File

@ -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,
};
})();

View File

@ -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);
});
};
})();

View File

@ -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) {

View File

@ -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);
});
});

View File

@ -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>

View File

@ -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);

View File

@ -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);
},
};
})();

View File

@ -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,

View File

@ -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
View 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>;
}

View File

@ -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
View 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;
}

View 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'));
}
}

View File

@ -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
View 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
View 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);
}
}

View 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
View 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;

View File

@ -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 = {
);
}
}
},
};
}
}

View 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

File diff suppressed because it is too large Load Diff

49
ts/textsecure/Storage.ts Normal file
View 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;

View 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;

View 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;
}

View 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);
});
}

View File

@ -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 = {}

View 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
View 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;

View File

@ -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>;

View File

@ -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
View File

@ -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;

View File

@ -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]+$"
}
],