Commit 7c43d043 authored by JC Brand's avatar JC Brand

Refactor OMEMO.

- Add hooks to the stanza parsers so that plugins can do additional parsing.
- Change ChatBox instance methods to functions and use them for stanza parsing.
- Move encrypt and decrypt messages to `converse.env.omemo`

Apparently, when receving a 1:1 carbon message, a device was wrongly created
for the contact's device list, instead of our own.
parent fce337e3
......@@ -7307,9 +7307,9 @@
},
"dependencies": {
"dot-prop": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz",
"integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==",
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz",
"integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==",
"dev": true,
"requires": {
"is-obj": "^2.0.0"
......@@ -11651,9 +11651,9 @@
}
},
"git-url-parse": {
"version": "11.1.3",
"resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-11.1.3.tgz",
"integrity": "sha512-GPsfwticcu52WQ+eHp0IYkAyaOASgYdtsQDIt4rUp6GbiNt1P9ddrh3O0kQB0eD4UJZszVqNT3+9Zwcg40fywA==",
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-11.2.0.tgz",
"integrity": "sha512-KPoHZg8v+plarZvto4ruIzzJLFQoRx+sUs5DQSr07By9IBKguVd+e6jwrFR6/TP6xrCJlNV1tPqLO1aREc7O2g==",
"dev": true,
"requires": {
"git-up": "^4.0.0"
......@@ -14712,9 +14712,9 @@
}
},
"node-fetch": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
"integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==",
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==",
"dev": true
},
"node-fetch-npm": {
......@@ -21278,9 +21278,9 @@
}
},
"rxjs": {
"version": "6.6.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz",
"integrity": "sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg==",
"version": "6.6.3",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz",
"integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==",
"dev": true,
"requires": {
"tslib": "^1.9.0"
......
/*global mock, converse */
const { $iq, $pres, $msg, _, Strophe } = converse.env;
const { $iq, $pres, $msg, _, omemo, Strophe } = converse.env;
const u = converse.env.utils;
async function deviceListFetched (_converse, jid) {
......@@ -78,15 +78,12 @@ describe("The OMEMO module", function() {
const message = 'This message will be encrypted'
await mock.waitForRoster(_converse, 'current', 1);
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
const view = await mock.openChatBoxFor(_converse, contact_jid);
const payload = await view.model.encryptMessage(message);
const result = await view.model.decryptMessage(payload);
const payload = await omemo.encryptMessage(message);
const result = await omemo.decryptMessage(payload);
expect(result).toBe(message);
done();
}));
it("enables encrypted messages to be sent and received",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {},
......@@ -182,10 +179,9 @@ describe("The OMEMO module", function() {
`</message>`);
// Test reception of an encrypted message
let obj = await view.model.encryptMessage('This is an encrypted message from the contact')
let obj = await omemo.encryptMessage('This is an encrypted message from the contact')
// XXX: Normally the key will be encrypted via libsignal.
// However, we're mocking libsignal in the tests, so we include
// it as plaintext in the message.
// However, we're mocking libsignal in the tests, so we include it as plaintext in the message.
stanza = $msg({
'from': contact_jid,
'to': _converse.connection.jid,
......@@ -205,7 +201,7 @@ describe("The OMEMO module", function() {
.toBe('This is an encrypted message from the contact');
// #1193 Check for a received message without <body> tag
obj = await view.model.encryptMessage('Another received encrypted message without fallback')
obj = await omemo.encryptMessage('Another received encrypted message without fallback')
stanza = $msg({
'from': contact_jid,
'to': _converse.connection.jid,
......@@ -383,6 +379,9 @@ describe("The OMEMO module", function() {
await u.waitUntil(() => initializedOMEMO(_converse));
await mock.openChatBoxFor(_converse, contact_jid);
let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, contact_jid));
const my_devicelist = _converse.devicelists.get({'jid': _converse.bare_jid});
expect(my_devicelist.devices.length).toBe(2);
const stanza = $iq({
'from': contact_jid,
'id': iq_stanza.getAttribute('id'),
......@@ -395,14 +394,15 @@ describe("The OMEMO module", function() {
.c('device', {'id': '555'});
_converse.connection._dataRecv(mock.createRequest(stanza));
await u.waitUntil(() => _converse.omemo_store);
const devicelist = _converse.devicelists.get({'jid': contact_jid});
await u.waitUntil(() => devicelist.devices.length === 1);
const contact_devicelist = _converse.devicelists.get({'jid': contact_jid});
await u.waitUntil(() => contact_devicelist.devices.length === 1);
const view = _converse.chatboxviews.get(contact_jid);
view.model.set('omemo_active', true);
// Test reception of an encrypted carbon message
const obj = await view.model.encryptMessage('This is an encrypted carbon message from another device of mine')
const obj = await omemo.encryptMessage('This is an encrypted carbon message from another device of mine')
const carbon = u.toStanza(`
<message xmlns="jabber:client" to="romeo@montague.lit/orchard" from="romeo@montague.lit" type="chat">
<sent xmlns="urn:xmpp:carbons:2">
......@@ -440,10 +440,14 @@ describe("The OMEMO module", function() {
expect(view.el.querySelector('.chat-msg__text').textContent.trim())
.toBe('This is an encrypted carbon message from another device of mine');
expect(devicelist.devices.length).toBe(2);
expect(devicelist.devices.at(0).get('id')).toBe('555');
expect(devicelist.devices.at(1).get('id')).toBe('988349631');
expect(devicelist.devices.get('988349631').get('active')).toBe(true);
expect(contact_devicelist.devices.length).toBe(1);
// Check that the new device id has been added to my devices
expect(my_devicelist.devices.length).toBe(3);
expect(my_devicelist.devices.at(0).get('id')).toBe('482886413b977930064a5888b92134fe');
expect(my_devicelist.devices.at(1).get('id')).toBe('123456789');
expect(my_devicelist.devices.at(2).get('id')).toBe('988349631');
expect(my_devicelist.devices.get('988349631').get('active')).toBe(true);
const textarea = view.el.querySelector('.chat-textarea');
textarea.value = 'This is an encrypted message from this device';
......@@ -601,7 +605,7 @@ describe("The OMEMO module", function() {
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await u.waitUntil(() => initializedOMEMO(_converse));
const obj = await _converse.ChatBox.prototype.encryptMessage('This is an encrypted message from the contact');
const obj = await omemo.encryptMessage('This is an encrypted message from the contact');
// XXX: Normally the key will be encrypted via libsignal.
// However, we're mocking libsignal in the tests, so we include
// it as plaintext in the message.
......@@ -631,8 +635,8 @@ describe("The OMEMO module", function() {
return generateMissingPreKeys.apply(_converse.omemo_store, arguments);
});
_converse.connection._dataRecv(mock.createRequest(stanza));
let iq_stanza = await u.waitUntil(() => _converse.chatboxviews.get(contact_jid));
iq_stanza = await deviceListFetched(_converse, contact_jid);
let iq_stanza = await deviceListFetched(_converse, contact_jid);
stanza = $iq({
'from': contact_jid,
'id': iq_stanza.getAttribute('id'),
......@@ -688,7 +692,6 @@ describe("The OMEMO module", function() {
done();
}));
it("updates device lists based on PEP messages",
mock.initConverse(
['rosterGroupsFetched'], {'allow_non_roster_messaging': true},
......
......@@ -40,6 +40,149 @@ class IQError extends Error {
}
}
const omemo = converse.env.omemo = {
async encryptMessage (plaintext) {
// The client MUST use fresh, randomly generated key/IV pairs
// with AES-128 in Galois/Counter Mode (GCM).
// For GCM a 12 byte IV is strongly suggested as other IV lengths
// will require additional calculations. In principle any IV size
// can be used as long as the IV doesn't ever repeat. NIST however
// suggests that only an IV size of 12 bytes needs to be supported
// by implementations.
//
// https://crypto.stackexchange.com/questions/26783/ciphertext-and-tag-size-and-iv-transmission-with-aes-in-gcm-mode
const iv = crypto.getRandomValues(new window.Uint8Array(12)),
key = await crypto.subtle.generateKey(KEY_ALGO, true, ["encrypt", "decrypt"]),
algo = {
'name': 'AES-GCM',
'iv': iv,
'tagLength': TAG_LENGTH
},
encrypted = await crypto.subtle.encrypt(algo, key, u.stringToArrayBuffer(plaintext)),
length = encrypted.byteLength - ((128 + 7) >> 3),
ciphertext = encrypted.slice(0, length),
tag = encrypted.slice(length),
exported_key = await crypto.subtle.exportKey("raw", key);
return {
'key': exported_key,
'tag': tag,
'key_and_tag': u.appendArrayBuffer(exported_key, tag),
'payload': u.arrayBufferToBase64(ciphertext),
'iv': u.arrayBufferToBase64(iv)
};
},
async decryptMessage (obj) {
const key_obj = await crypto.subtle.importKey('raw', obj.key, KEY_ALGO, true, ['encrypt','decrypt']);
const cipher = u.appendArrayBuffer(u.base64ToArrayBuffer(obj.payload), obj.tag);
const algo = {
'name': "AES-GCM",
'iv': u.base64ToArrayBuffer(obj.iv),
'tagLength': TAG_LENGTH
};
return u.arrayBufferToString(await crypto.subtle.decrypt(algo, key_obj, cipher));
}
};
function getSessionCipher (jid, id) {
const address = new libsignal.SignalProtocolAddress(jid, id);
return new window.libsignal.SessionCipher(_converse.omemo_store, address);
}
async function handleDecryptedWhisperMessage (attrs, key_and_tag) {
const encrypted = attrs.encrypted;
const devicelist = _converse.devicelists.getDeviceList(attrs.from);
await devicelist._devices_promise;
let device = devicelist.get(encrypted.device_id);
if (!device) {
device = await devicelist.devices.create({'id': encrypted.device_id, 'jid': attrs.from}, {'promise': true});
}
if (encrypted.payload) {
const key = key_and_tag.slice(0, 16);
const tag = key_and_tag.slice(16);
const result = await omemo.decryptMessage(Object.assign(encrypted, {'key': key, 'tag': tag}));
device.save('active', true);
return result;
}
}
function getDecryptionErrorAttributes (e) {
if (api.settings.get("loglevel") === 'debug') {
return {
'error_text': __("Sorry, could not decrypt a received OMEMO message due to an error.") + ` ${e.name} ${e.message}`,
'error_type': 'Decryption',
'is_ephemeral': true,
'is_error': true,
'type': 'error',
}
} else {
return {};
}
}
async function decryptPrekeyWhisperMessage (attrs) {
const session_cipher = getSessionCipher(attrs.from, parseInt(attrs.encrypted.device_id, 10));
const key = u.base64ToArrayBuffer(attrs.encrypted.key);
let key_and_tag;
try {
key_and_tag = await session_cipher.decryptPreKeyWhisperMessage(key, 'binary');
} catch (e) {
// TODO from the XEP:
// There are various reasons why decryption of an
// OMEMOKeyExchange or an OMEMOAuthenticatedMessage
// could fail. One reason is if the message was
// received twice and already decrypted once, in this
// case the client MUST ignore the decryption failure
// and not show any warnings/errors. In all other cases
// of decryption failure, clients SHOULD respond by
// forcibly doing a new key exchange and sending a new
// OMEMOKeyExchange with a potentially empty SCE
// payload. By building a new session with the original
// sender this way, the invalid session of the original
// sender will get overwritten with this newly created,
// valid session.
log.error(`${e.name} ${e.message}`);
return Object.assign(attrs, getDecryptionErrorAttributes(e));
}
// TODO from the XEP:
// When a client receives the first message for a given
// ratchet key with a counter of 53 or higher, it MUST send
// a heartbeat message. Heartbeat messages are normal OMEMO
// encrypted messages where the SCE payload does not include
// any elements. These heartbeat messages cause the ratchet
// to forward, thus consequent messages will have the
// counter restarted from 0.
try {
const plaintext = await handleDecryptedWhisperMessage(attrs, key_and_tag);
await _converse.omemo_store.generateMissingPreKeys();
await _converse.omemo_store.publishBundle();
if (plaintext) {
return Object.assign(attrs, {'plaintext': plaintext});
} else {
return Object.assign(attrs, {'is_only_key': true});
}
} catch (e) {
log.error(`${e.name} ${e.message}`);
return Object.assign(attrs, getDecryptionErrorAttributes(e));
}
}
async function decryptWhisperMessage (attrs) {
const session_cipher = getSessionCipher(attrs.from, parseInt(attrs.encrypted.device_id, 10));
const key = u.base64ToArrayBuffer(attrs.encrypted.key);
try {
const key_and_tag = await session_cipher.decryptWhisperMessage(key, 'binary')
const plaintext = await handleDecryptedWhisperMessage(attrs, key_and_tag);
return Object.assign(attrs, {'plaintext': plaintext});
} catch (e) {
log.error(`${e.name} ${e.message}`);
return Object.assign(attrs, getDecryptionErrorAttributes(e));
}
}
function addKeysToMessageStanza (stanza, dicts, iv) {
for (const i in dicts) {
......@@ -84,7 +227,6 @@ function parseBundle (bundle_el) {
}
}
async function generateFingerprint (device) {
if (device.get('bundle')?.fingerprint) {
return;
......@@ -107,11 +249,14 @@ function generateDeviceID () {
/* Generates a device ID, making sure that it's unique */
const existing_ids = _converse.devicelists.get(_converse.bare_jid).devices.pluck('id');
let device_id = libsignal.KeyHelper.generateRegistrationId();
// Before publishing a freshly generated device id for the first time,
// a device MUST check whether that device id already exists, and if so, generate a new one.
let i = 0;
while (existing_ids.includes(device_id)) {
device_id = libsignal.KeyHelper.generateRegistrationId();
i++;
if (i == 10) {
if (i === 10) {
throw new Error("Unable to generate a unique device ID");
}
}
......@@ -119,10 +264,13 @@ function generateDeviceID () {
}
async function buildSession (device) {
const address = new libsignal.SignalProtocolAddress(device.get('jid'), device.get('id')),
sessionBuilder = new libsignal.SessionBuilder(_converse.omemo_store, address),
prekey = device.getRandomPreKey(),
bundle = await device.getBundle();
// TODO: check device-get('jid') versus the 'from' attribute which is used
// to build a session when receiving an encrypted message in a MUC.
// https://github.com/conversejs/converse.js/issues/1481#issuecomment-509183431
const address = new libsignal.SignalProtocolAddress(device.get('jid'), device.get('id'));
const sessionBuilder = new libsignal.SessionBuilder(_converse.omemo_store, address);
const prekey = device.getRandomPreKey();
const bundle = await device.getBundle();
return sessionBuilder.processPreKey({
'registrationId': parseInt(device.get('id'), 10),
......@@ -143,7 +291,7 @@ async function getSession (device) {
const address = new libsignal.SignalProtocolAddress(device.get('jid'), device.get('id'));
const session = await _converse.omemo_store.loadSession(address.toString());
if (session) {
return Promise.resolve(session);
return session;
} else {
try {
const session = await buildSession(device);
......@@ -161,11 +309,11 @@ function updateBundleFromStanza (stanza) {
if (!items_el || !items_el.getAttribute('node').startsWith(Strophe.NS.OMEMO_BUNDLES)) {
return;
}
const device_id = items_el.getAttribute('node').split(':')[1],
jid = stanza.getAttribute('from'),
bundle_el = sizzle(`item > bundle`, items_el).pop(),
devicelist = _converse.devicelists.getDeviceList(jid),
device = devicelist.devices.get(device_id) || devicelist.devices.create({'id': device_id, 'jid': jid});
const device_id = items_el.getAttribute('node').split(':')[1];
const jid = stanza.getAttribute('from');
const bundle_el = sizzle(`item > bundle`, items_el).pop();
const devicelist = _converse.devicelists.getDeviceList(jid);
const device = devicelist.devices.get(device_id) || devicelist.devices.create({'id': device_id, 'jid': jid});
device.save({'bundle': parseBundle(bundle_el)});
}
......@@ -260,10 +408,9 @@ async function initOMEMO () {
return;
}
/**
* Triggered once OMEMO support has been initialized
* @event _converse#OMEMOInitialized
* @example _converse.api.listen.on('OMEMOInitialized', () => { ... });
*/
* Triggered once OMEMO support has been initialized
* @event _converse#OMEMOInitialized
* @example _converse.api.listen.on('OMEMOInitialized', () => { ... }); */
api.trigger('OMEMOInitialized');
}
......@@ -298,7 +445,6 @@ async function checkOMEMOSupported (chatbox) {
}
}
function toggleOMEMO (ev) {
ev.stopPropagation();
ev.preventDefault();
......@@ -491,120 +637,8 @@ converse.plugins.add('converse-omemo', {
*/
const OMEMOEnabledChatBox = {
async encryptMessage (plaintext) {
// The client MUST use fresh, randomly generated key/IV pairs
// with AES-128 in Galois/Counter Mode (GCM).
// For GCM a 12 byte IV is strongly suggested as other IV lengths
// will require additional calculations. In principle any IV size
// can be used as long as the IV doesn't ever repeat. NIST however
// suggests that only an IV size of 12 bytes needs to be supported
// by implementations.
//
// https://crypto.stackexchange.com/questions/26783/ciphertext-and-tag-size-and-iv-transmission-with-aes-in-gcm-mode
const iv = crypto.getRandomValues(new window.Uint8Array(12)),
key = await crypto.subtle.generateKey(KEY_ALGO, true, ["encrypt", "decrypt"]),
algo = {
'name': 'AES-GCM',
'iv': iv,
'tagLength': TAG_LENGTH
},
encrypted = await crypto.subtle.encrypt(algo, key, u.stringToArrayBuffer(plaintext)),
length = encrypted.byteLength - ((128 + 7) >> 3),
ciphertext = encrypted.slice(0, length),
tag = encrypted.slice(length),
exported_key = await crypto.subtle.exportKey("raw", key);
return Promise.resolve({
'key': exported_key,
'tag': tag,
'key_and_tag': u.appendArrayBuffer(exported_key, tag),
'payload': u.arrayBufferToBase64(ciphertext),
'iv': u.arrayBufferToBase64(iv)
});
},
async decryptMessage (obj) {
const key_obj = await crypto.subtle.importKey('raw', obj.key, KEY_ALGO, true, ['encrypt','decrypt']);
const cipher = u.appendArrayBuffer(u.base64ToArrayBuffer(obj.payload), obj.tag);
const algo = {
'name': "AES-GCM",
'iv': u.base64ToArrayBuffer(obj.iv),
'tagLength': TAG_LENGTH
};
return u.arrayBufferToString(await crypto.subtle.decrypt(algo, key_obj, cipher));
},
reportDecryptionError (e) {
if (api.settings.get("loglevel") === 'debug') {
this.createMessage({
'message': __("Sorry, could not decrypt a received OMEMO message due to an error.") + ` ${e.name} ${e.message}`,
'type': 'error',
});
}
log.error(`${e.name} ${e.message}`);
},
async handleDecryptedWhisperMessage (attrs, key_and_tag) {
const encrypted = attrs.encrypted;
const devicelist = _converse.devicelists.getDeviceList(this.get('jid'));
await devicelist._devices_promise;
this.save('omemo_supported', true);
let device = devicelist.get(encrypted.device_id);
if (!device) {
device = await devicelist.devices.create({'id': encrypted.device_id, 'jid': attrs.from}, {'promise': true});
}
if (encrypted.payload) {
const key = key_and_tag.slice(0, 16);
const tag = key_and_tag.slice(16);
const result = await this.decryptMessage(Object.assign(encrypted, {'key': key, 'tag': tag}));
device.save('active', true);
return result;
}
},
async decrypt (attrs) {
const session_cipher = this.getSessionCipher(attrs.from, parseInt(attrs.encrypted.device_id, 10));
// https://xmpp.org/extensions/xep-0384.html#usecases-receiving
const key = u.base64ToArrayBuffer(attrs.encrypted.key);
if (attrs.encrypted.prekey === true) {
try {
const key_and_tag = await session_cipher.decryptPreKeyWhisperMessage(key, 'binary');
const plaintext = await this.handleDecryptedWhisperMessage(attrs, key_and_tag);
await _converse.omemo_store.generateMissingPreKeys();
await _converse.omemo_store.publishBundle();
if (plaintext) {
return Object.assign(attrs, {'plaintext': plaintext});
} else {
return Object.assign(attrs, {'is_only_key': true});
}
} catch (e) {
this.reportDecryptionError(e);
return attrs;
}
} else {
try {
const key_and_tag = await session_cipher.decryptWhisperMessage(key, 'binary')
const plaintext = await this.handleDecryptedWhisperMessage(attrs, key_and_tag);
return Object.assign(attrs, {'plaintext': plaintext});
} catch (e) {
this.reportDecryptionError(e);
return attrs;
}
}
},
getSessionCipher (jid, id) {
const address = new libsignal.SignalProtocolAddress(jid, id);
this.session_cipher = new window.libsignal.SessionCipher(_converse.omemo_store, address);
return this.session_cipher;
},
encryptKey (plaintext, device) {
return this.getSessionCipher(device.get('jid'), device.get('id'))
return getSessionCipher(device.get('jid'), device.get('id'))
.encrypt(plaintext)
.then(payload => ({'payload': payload, 'device': device}));
},
......@@ -713,7 +747,7 @@ converse.plugins.add('converse-omemo', {
stanza.c('encrypted', {'xmlns': Strophe.NS.OMEMO})
.c('header', {'sid': _converse.omemo_store.get('device_id')});
return chatbox.encryptMessage(message.get('message')).then(obj => {
return omemo.encryptMessage(message.get('message')).then(obj => {
// The 16 bytes key and the GCM authentication tag (The tag
// SHOULD have at least 128 bit) are concatenated and for each
// intended recipient device, i.e. both own devices as well as
......@@ -916,17 +950,20 @@ converse.plugins.add('converse-omemo', {
device.save('bundle', Object.assign(bundle, {'prekeys': marshalled_keys}));
},
/**
* Generate a the data used by the X3DH key agreement protocol
* that can be used to build a session with a device.
*/
async generateBundle () {
/* The first thing that needs to happen if a client wants to
* start using OMEMO is they need to generate an IdentityKey
* and a Device ID. The IdentityKey is a Curve25519 [6]
* public/private Key pair. The Device ID is a randomly
* generated integer between 1 and 2^31 - 1.
*/
// The first thing that needs to happen if a client wants to
// start using OMEMO is they need to generate an IdentityKey
// and a Device ID. The IdentityKey is a Curve25519 [6]
// public/private Key pair. The Device ID is a randomly
// generated integer between 1 and 2^31 - 1.
const identity_keypair = await libsignal.KeyHelper.generateIdentityKeyPair();
const bundle = {},
identity_key = u.arrayBufferToBase64(identity_keypair.pubKey),
device_id = generateDeviceID();
const bundle = {};
const identity_key = u.arrayBufferToBase64(identity_keypair.pubKey);
const device_id = generateDeviceID();
bundle['identity_key'] = identity_key;
bundle['device_id'] = device_id;
......@@ -1078,6 +1115,16 @@ converse.plugins.add('converse-omemo', {
return this._devices_promise;
},
async getOwnDeviceId () {
let device_id = _converse.omemo_store.get('device_id');
if (!this.devices.findWhere({'id': device_id})) {
// Generate a new bundle if we cannot find our device
await _converse.omemo_store.generateBundle();
device_id = _converse.omemo_store.get('device_id');
}
return device_id;
},
async publishCurrentDevice (device_ids) {
if (this.get('jid') !== _converse.bare_jid) {
return // We only publish for ourselves.
......@@ -1090,14 +1137,7 @@ converse.plugins.add('converse-omemo', {
log.warn('publishCurrentDevice: omemo_store is not defined, likely a timing issue');
return;
}
let device_id = _converse.omemo_store.get('device_id');
if (!this.devices.findWhere({'id': device_id})) {
// Generate a new bundle if we cannot find our device
await _converse.omemo_store.generateBundle();
device_id = _converse.omemo_store.get('device_id');
}
if (!device_ids.includes(device_id)) {
if (!device_ids.includes(await this.getOwnDeviceId())) {
return this.publishDevices();
}
},
......@@ -1125,6 +1165,11 @@ converse.plugins.add('converse-omemo', {
return device_ids;
},
/**
* Send an IQ stanza to the current user's "devices" PEP node to
* ensure that all devices are published for potential chat partners to see.
* See: https://xmpp.org/extensions/xep-0384.html#usecases-announcing
*/
publishDevices () {
const item = $build('item').c('list', {'xmlns': Strophe.NS.OMEMO})
this.devices.filter(d => d.get('active')).forEach(d => item.c('device', {'id': d.get('id')}).up());
......@@ -1160,8 +1205,22 @@ converse.plugins.add('converse-omemo', {
}
});
function parseEncryptedMessage (attrs) {
if (attrs.is_encrypted) {
// https://xmpp.org/extensions/xep-0384.html#usecases-receiving
if (attrs.encrypted.prekey === true) {
return decryptPrekeyWhisperMessage(attrs);
} else {
return decryptWhisperMessage(attrs);
}
} else {
return attrs;
}
}
/******************** Event Handlers ********************/
api.listen.on('parseMessage', parseEncryptedMessage);
api.listen.on('parseMUCMessage', parseEncryptedMessage);
api.waitUntil('chatBoxesInitialized').then(() =>
_converse.chatboxes.on('add', chatbox => {
......@@ -1174,6 +1233,11 @@ converse.plugins.add('converse-omemo', {
);
const onChatInitialized = view => {
view.listenTo(view.model.messages, 'add', (message) => {
if (message.get('is_encrypted') && !message.get('is_error')) {
view.model.save('omemo_supported', true);
}
});
view.listenTo(view.model, 'change:omemo_supported', () => {
if (!view.model.get('omemo_supported') && view.model.get('omemo_active')) {
view.model.set('omemo_active', false);
......@@ -1244,8 +1308,8 @@ converse.plugins.add('converse-omemo', {
*/
'generate': async () => {
// Remove current device
const devicelist = _converse.devicelists.get(_converse.bare_jid),
device_id = _converse.omemo_store.get('device_id');
const devicelist = _converse.devicelists.get(_converse.bare_jid);
const device_id = _converse.omemo_store.get('device_id');
if (device_id) {
const device = devicelist.devices.get(device_id);
_converse.omemo_store.unset(device_id);
......
......@@ -457,8 +457,6 @@ converse.plugins.add('converse-chat', {
attrs.stanza && log.error(attrs.stanza);
return log.error(attrs.message);
}
// TODO: move to OMEMO
attrs = attrs.encrypted ? await this.decrypt(attrs) : attrs;
const message = this.getDuplicateMessage(attrs);
if (message) {
this.updateMessage(message, attrs);
......@@ -1215,7 +1213,7 @@ converse.plugins.add('converse-chat', {
}
const has_body = !!sizzle(`body, encrypted[xmlns="${Strophe.NS.OMEMO}"]`, stanza).length;
const chatbox = await api.chats.get(attrs.contact_jid, {'nickname': attrs.nick }, has_body);
chatbox && await chatbox.queueMessage(attrs);
await chatbox?.queueMessage(attrs);
/**
* Triggered when a message stanza is been received and processed.
* @event _converse#message
......
......@@ -1993,8 +1993,6 @@ converse.plugins.add('converse-muc', {
attrs.stanza && log.error(attrs.stanza);
return log.error(attrs.message);
}
// TODO: move to OMEMO
attrs = attrs.encrypted ? await this.decrypt(attrs) : attrs;
const message = this.getDuplicateMessage(attrs);
if (message) {
return this.updateMessage(message, attrs);
......
......@@ -362,7 +362,6 @@ const st = {
}, {});
},
/**
* Parses a passed in message stanza and returns an object of attributes.
* @method st#parseMessage
......@@ -418,7 +417,6 @@ const st = {
);
}
const is_headline = st.isHeadline(stanza);
const is_server_message = st.isServerMessage(stanza);
let contact, contact_jid;
......@@ -534,7 +532,12 @@ const st = {
// We prefer to use one of the XEP-0359 unique and stable stanza IDs
// as the Model id, to avoid duplicates.
attrs['id'] = attrs['origin_id'] || attrs[`stanza_id ${(attrs.from)}`] || u.getUniqueId();
return attrs;
/**
* *Hook* which allows plugins to add additional parsing
* @event _converse#parseMessage
*/
return api.hook('parseMessage', attrs);
},
/**
......@@ -678,7 +681,11 @@ const st = {
}
// We prefer to use one of the XEP-0359 unique and stable stanza IDs as the Model id, to avoid duplicates.
attrs['id'] = attrs['origin_id'] || attrs[`stanza_id ${(attrs.from_muc || attrs.from)}`] || u.getUniqueId();
return attrs;
/**
* *Hook* which allows plugins to add additional parsing
* @event _converse#parseMUCMessage
*/
return api.hook('parseMUCMessage', attrs);
},
/**
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment