You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
session-desktop/libloki/libloki-protocol.js

150 lines
5.7 KiB
JavaScript

/* global window, libsignal, textsecure, OutgoingMessage */
// eslint-disable-next-line func-names
(function() {
window.libloki = window.libloki || {};
class FallBackDecryptionError extends Error {}
const IV_LENGTH = 16;
FallBackSessionCipher = function (address) {
this.identityKeyString = address.getName();
this.pubKey = StringView.hexToArrayBuffer(address.getName());
this.encrypt = async (plaintext) => {
const myKeyPair = await textsecure.storage.protocol.getIdentityKeyPair();
const myPrivateKey = myKeyPair.privKey;
const symmetricKey = libsignal.Curve.calculateAgreement(this.pubKey, myPrivateKey);
const iv = libsignal.crypto.getRandomBytes(IV_LENGTH);
const ciphertext = await libsignal.crypto.encrypt(symmetricKey, plaintext, iv);
const ivAndCiphertext = new Uint8Array(
iv.byteLength + ciphertext.byteLength
);
ivAndCiphertext.set(new Uint8Array(iv));
ivAndCiphertext.set(
new Uint8Array(ciphertext),
iv.byteLength
);
return {
type : 6, //friend request
body : ivAndCiphertext,
registrationId : null
};
},
this.decrypt = async (ivAndCiphertext) => {
const iv = ivAndCiphertext.slice(0, IV_LENGTH);
const cipherText = ivAndCiphertext.slice(IV_LENGTH);
const myKeyPair = await textsecure.storage.protocol.getIdentityKeyPair();
const myPrivateKey = myKeyPair.privKey;
const symmetricKey = libsignal.Curve.calculateAgreement(this.pubKey, myPrivateKey);
try {
return await libsignal.crypto.decrypt(symmetricKey, cipherText, iv);
} catch(e) {
throw new FallBackDecryptionError('Could not decrypt message from ' + this.identityKeyString + ' using FallBack encryption.')
}
}
}
getPreKeyBundleForNumber = async function(pubKey) {
const myKeyPair = await textsecure.storage.protocol.getIdentityKeyPair();
const identityKey = myKeyPair.pubKey;
// Retrieve ids. The ids stored are always the latest generated + 1
const signedKeyId = textsecure.storage.get('signedKeyId', 1) - 1;
const [signedKey, preKey] = await Promise.all([
textsecure.storage.protocol.loadSignedPreKey(signedKeyId),
new Promise(async (resolve, reject) => {
// retrieve existing prekey if we already generated one for that recipient
const storedPreKey = await textsecure.storage.protocol.loadPreKeyForContactIdentityKeyString(pubKey);
if (storedPreKey) {
resolve({ pubKey: storedPreKey.pubKey, keyId: storedPreKey.keyId });
} else {
// generate and store new prekey
const preKeyId = textsecure.storage.get('maxPreKeyId', 1);
textsecure.storage.put('maxPreKeyId', preKeyId + 1);
const preKey = await libsignal.KeyHelper.generatePreKey(preKeyId);
await textsecure.storage.protocol.storePreKey(preKey.keyId, preKey.keyPair, pubKey);
resolve({ pubKey: preKey.keyPair.pubKey, keyId: preKeyId });
}
})
]);
const preKeyMessage = new textsecure.protobuf.PreKeyBundleMessage({
identityKey: new Uint8Array(identityKey),
deviceId: 1, // TODO: fetch from somewhere
preKeyId: preKey.keyId,
signedKeyId,
preKey: new Uint8Array(preKey.pubKey),
signedKey: new Uint8Array(signedKey.pubKey),
signature: new Uint8Array(signedKey.signature),
});
return preKeyMessage;
}
savePreKeyBundleForNumber = async function({ pubKey, preKeyId, preKey, signedKeyId, signedKey, signature }) {
const signedKeyPromise = new Promise(async (resolve) => {
const existingSignedKeys = await textsecure.storage.protocol.loadContactSignedPreKeys({ identityKeyString: pubKey, keyId: signedKeyId });
if (!existingSignedKeys || (existingSignedKeys instanceof Array && existingSignedKeys.length == 0))
{
const signedPreKey = {
keyId: signedKeyId,
publicKey: signedKey,
signature,
};
await textsecure.storage.protocol.storeContactSignedPreKey(pubKey, signedPreKey);
}
resolve();
});
const preKeyPromise = new Promise(async (resolve) => {
const existingPreKeys = textsecure.storage.protocol.loadContactPreKeys({ identityKeyString: pubKey, keyId: preKeyId });
if (!existingPreKeys || (existingPreKeys instanceof Array && existingPreKeys.length == 0))
{
const preKeyObject = {
publicKey: preKey,
keyId: preKeyId,
}
await textsecure.storage.protocol.storeContactPreKey(pubKey, preKeyObject);
}
resolve();
});
await Promise.all([signedKeyPromise, preKeyPromise]);
}
sendEmptyMessageWithPreKeys = async function(pubKey) {
// empty content message
const content = new textsecure.protobuf.Content();
// will be called once the transmission succeeded or failed
const callback = res => {
if (res.errors.length > 0) {
res.errors.forEach(error => console.error(error));
} else {
console.log('empty message sent successfully');
}
};
// send an empty message. The logic in ougoing_message will attach the prekeys.
const outgoingMessage = new textsecure.OutgoingMessage(
null, //server
Date.now(), //timestamp,
[pubKey], //numbers
content, //message
true, //silent
callback, //callback
);
await outgoingMessage.sendToNumber(pubKey);
}
window.libloki.FallBackSessionCipher = FallBackSessionCipher;
window.libloki.getPreKeyBundleForNumber = getPreKeyBundleForNumber;
window.libloki.FallBackDecryptionError = FallBackDecryptionError;
window.libloki.savePreKeyBundleForNumber = savePreKeyBundleForNumber;
window.libloki.sendEmptyMessageWithPreKeys = sendEmptyMessageWithPreKeys;
})();