parent
f40e4b4dbb
commit
a3593dbc7d
216
dist/converse.js
vendored
216
dist/converse.js
vendored
@ -73263,85 +73263,143 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
|
||||
const _converse = this.__super__._converse;
|
||||
return new Promise((resolve, reject) => {
|
||||
_converse.getDevicesForContact(this.get('jid')).then(devices => {
|
||||
const promises = devices.map(device => device.getBundle());
|
||||
Promise.all(promises).then(() => {
|
||||
this.buildSessions(devices).then(() => resolve(devices)).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
|
||||
}).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
|
||||
}).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
|
||||
}).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
|
||||
},
|
||||
|
||||
buildSessions(devices) {
|
||||
const _converse = this.__super__._converse,
|
||||
device_id = _converse.omemo_store.get('device_id');
|
||||
|
||||
return Promise.all(_.map(devices, device => {
|
||||
const recipient_id = device['id'];
|
||||
const address = new libsignal.SignalProtocolAddress(parseInt(recipient_id, 10), device_id);
|
||||
const sessionBuilder = new libsignal.SessionBuilder(_converse.omemo_store, address);
|
||||
return sessionBuilder.processPreKey({
|
||||
'registrationId': _converse.omemo_store.get('registration_id'),
|
||||
'identityKey': _converse.omemo_store.get('identity_keypair'),
|
||||
'signedPreKey': {
|
||||
'keyId': '',
|
||||
// <Number>,
|
||||
'publicKey': '',
|
||||
// <ArrayBuffer>,
|
||||
'signature': '' // <ArrayBuffer>
|
||||
|
||||
},
|
||||
'preKey': {
|
||||
'keyId': '',
|
||||
// <Number>,
|
||||
'publicKey': '' // <ArrayBuffer>
|
||||
|
||||
}
|
||||
});
|
||||
}));
|
||||
},
|
||||
|
||||
encryptMessage(message) {// TODO:
|
||||
// const { _converse } = this.__super__;
|
||||
// const plaintext = message.get('message');
|
||||
// const address = new libsignal.SignalProtocolAddress(recipientId, deviceId);
|
||||
// return new Promise((resolve, reject) => {
|
||||
// var sessionCipher = new window.libsignal.SessionCipher(_converse.omemo_store, address);
|
||||
// sessionCipher.encrypt(plaintext).then((ciphertext) => {});
|
||||
// });
|
||||
},
|
||||
|
||||
createOMEMOMessageStanza(message, bundles) {
|
||||
const _converse = this.__super__._converse,
|
||||
__ = _converse.__;
|
||||
|
||||
const body = __("This is an OMEMO encrypted message which your client doesn’t seem to support. " + "Find more information on https://conversations.im/omemo");
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.encryptMessage(message).then(payload => {
|
||||
const stanza = $msg({
|
||||
'from': _converse.connection.jid,
|
||||
'to': this.get('jid'),
|
||||
'type': this.get('message_type'),
|
||||
'id': message.get('msgid')
|
||||
}).c('body').t(body).up().c('encrypted').t(payload).c('header').t(payload).up();
|
||||
|
||||
_.forEach(bundles, bundle => {
|
||||
const prekey = bundle.prekeys[Math.random(bundle.prekeys.length)].textContent;
|
||||
stanza('key', {
|
||||
'rid': bundle.identity_key
|
||||
}).t(prekey).up();
|
||||
}); // TODO: set storage hint urn:xmpp:hints
|
||||
|
||||
|
||||
resolve(stanza);
|
||||
Promise.all(devices.map(device => device.getBundle())).then(() => this.buildSessions(devices)).then(() => resolve(devices)).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
|
||||
}).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
|
||||
});
|
||||
},
|
||||
|
||||
buildSession(device) {
|
||||
const _converse = this.__super__._converse;
|
||||
const bundle = device.get('bundle'),
|
||||
address = new libsignal.SignalProtocolAddress(device.get('jid'), device.get('id')),
|
||||
sessionBuilder = new libsignal.SessionBuilder(_converse.omemo_store, address),
|
||||
prekey = device.getRandomPreKey();
|
||||
return sessionBuilder.processPreKey({
|
||||
'registrationId': _converse.omemo_store.get('registration_id'),
|
||||
'identityKey': _converse.omemo_store.get('identity_keypair'),
|
||||
'signedPreKey': {
|
||||
'keyId': bundle.signed_prekey.id,
|
||||
// <Number>
|
||||
'publicKey': u.base64ToArrayBuffer(bundle.signed_prekey.public_key),
|
||||
'signature': u.base64ToArrayBuffer(bundle.signed_prekey.signature)
|
||||
},
|
||||
'preKey': {
|
||||
'keyId': prekey.id,
|
||||
// <Number>
|
||||
'publicKey': u.base64ToArrayBuffer(prekey.key)
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
buildSessions(devices) {
|
||||
return Promise.all(devices.map(device => this.buildSession(device)));
|
||||
},
|
||||
|
||||
encryptMessage(plaintext) {
|
||||
// The client MUST use fresh, randomly generated key/IV pairs
|
||||
// with AES-128 in Galois/Counter Mode (GCM).
|
||||
const TAG_LENGTH = 128,
|
||||
iv = window.crypto.getRandomValues(new window.Uint8Array(16));
|
||||
let key;
|
||||
return window.crypto.subtle.generateKey({
|
||||
'name': "AES-GCM",
|
||||
'length': 256
|
||||
}, true, // extractable
|
||||
["encrypt", "decrypt"] // key usages
|
||||
).then(result => {
|
||||
key = result;
|
||||
const algo = {
|
||||
'name': 'AES-GCM',
|
||||
'iv': iv,
|
||||
'tagLength': TAG_LENGTH
|
||||
};
|
||||
return window.crypto.subtle.encrypt(algo, key, new TextEncoder().encode(plaintext));
|
||||
}).then(ciphertext => {
|
||||
return window.crypto.subtle.exportKey("jwk", key).then(key_str => {
|
||||
return Promise.resolve({
|
||||
'key_str': key_str,
|
||||
'tag': ciphertext.slice(ciphertext.byteLength - (TAG_LENGTH + 7 >> 3)),
|
||||
'iv': iv
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
encryptKey(plaintext, device) {
|
||||
const _converse = this.__super__._converse,
|
||||
address = new libsignal.SignalProtocolAddress(this.get('jid'), device.get('id')),
|
||||
sessionCipher = new window.libsignal.SessionCipher(_converse.omemo_store, address);
|
||||
return sessionCipher.encrypt(plaintext);
|
||||
},
|
||||
|
||||
addKeysToMessageStanza(stanza, devices, payloads) {
|
||||
for (var i in payloads) {
|
||||
if (Object.prototype.hasOwnProperty.call(payloads, i)) {
|
||||
const payload = btoa(JSON.stringify(payloads[i]));
|
||||
const prekey = 3 == parseInt(payloads[i].type, 10);
|
||||
|
||||
if (i == payloads.length - 1) {
|
||||
stanza.c('key', {
|
||||
'rid': devices.get('id')
|
||||
}).t(payload);
|
||||
|
||||
if (prekey) {
|
||||
stanza.attrs({
|
||||
'prekey': prekey
|
||||
});
|
||||
}
|
||||
|
||||
stanza.up().c('iv').t(payloads[0].iv).up().up();
|
||||
} else {
|
||||
stanza.c('key', {
|
||||
prekey: prekey,
|
||||
rid: devices.get('id')
|
||||
}).t(payload).up();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve(stanza);
|
||||
},
|
||||
|
||||
createOMEMOMessageStanza(message, devices) {
|
||||
const _converse = this.__super__._converse,
|
||||
__ = _converse.__;
|
||||
|
||||
const body = __("This is an OMEMO encrypted message which your client doesn’t seem to support. " + "Find more information on https://conversations.im/omemo"); // An encrypted header is added to the message for each device that is supposed to receive it.
|
||||
// These headers simply contain the key that the payload message is encrypted with,
|
||||
// and they are separately encrypted using the session corresponding to the counterpart device.
|
||||
|
||||
|
||||
const stanza = $msg({
|
||||
'from': _converse.connection.jid,
|
||||
'to': this.get('jid'),
|
||||
'type': this.get('message_type'),
|
||||
'id': message.get('msgid')
|
||||
}).c('body').t(body).up().c('encrypted', {
|
||||
'xmlns': Strophe.NS.OMEMO
|
||||
}).c('header', {
|
||||
'sid': _converse.omemo_store.get('device_id')
|
||||
});
|
||||
return this.encryptMessage(message).then(payload => {
|
||||
// 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
|
||||
// devices associated with the contact, the result of this
|
||||
// concatenation is encrypted using the corresponding
|
||||
// long-standing SignalProtocol session.
|
||||
// TODO: need to include own devices here as well (and filter out distrusted devices)
|
||||
const promises = devices.map(device => this.encryptKey(payload.key_str + payload.tag, device));
|
||||
return Promise.all(promises).then(payloads => this.addKeysToMessageStanza(stanza, devices, payloads));
|
||||
});
|
||||
},
|
||||
|
||||
sendMessage(attrs) {
|
||||
const _converse = this.__super__._converse;
|
||||
|
||||
if (this.get('omemo_active')) {
|
||||
const message = this.messages.create(attrs);
|
||||
this.getBundlesAndBuildSessions().then(bundles => this.createOMEMOMessageStanza(message, bundles)).then(stanza => this.sendMessageStanza(stanza));
|
||||
this.getBundlesAndBuildSessions().then(devices => this.createOMEMOMessageStanza(message, devices)).then(stanza => this.sendMessageStanza(stanza)).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
|
||||
} else {
|
||||
return this.__super__.sendMessage.apply(this, arguments);
|
||||
}
|
||||
@ -73643,6 +73701,12 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
|
||||
'trusted': UNDECIDED
|
||||
},
|
||||
|
||||
getRandomPreKey() {
|
||||
// XXX: assumes that the bundle has already been fetched
|
||||
const bundle = this.get('bundle');
|
||||
return bundle.prekeys[u.getRandomInt(bundle.prekeys.length)];
|
||||
},
|
||||
|
||||
fetchBundleFromServer() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const stanza = $iq({
|
||||
@ -73670,7 +73734,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
|
||||
* this device, if the information is not at hand already.
|
||||
*/
|
||||
if (this.get('bundle')) {
|
||||
return Promise.resolve(this.get('bundle').toJSON());
|
||||
return Promise.resolve(this.get('bundle').toJSON(), this);
|
||||
} else {
|
||||
return this.fetchBundleFromServer();
|
||||
}
|
||||
@ -82446,6 +82510,10 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
|
||||
return bytes.buffer;
|
||||
};
|
||||
|
||||
u.getRandomInt = function (max) {
|
||||
return Math.floor(Math.random() * Math.floor(max));
|
||||
};
|
||||
|
||||
u.getUniqueId = function () {
|
||||
return 'xxxxxxxx-xxxx'.replace(/[x]/g, function (c) {
|
||||
var r = Math.random() * 16 | 0,
|
||||
@ -82474,7 +82542,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
|
||||
//
|
||||
// This is the utilities module.
|
||||
//
|
||||
// Copyright (c) 2012-2017, Jan-Carel Brand <jc@opkode.com>
|
||||
// Copyright (c) 2013-2018, Jan-Carel Brand <jc@opkode.com>
|
||||
// Licensed under the Mozilla Public License (MPLv2)
|
||||
//
|
||||
|
||||
|
120
spec/omemo.js
120
spec/omemo.js
@ -10,6 +10,122 @@
|
||||
|
||||
describe("The OMEMO module", function() {
|
||||
|
||||
it("enables encrypted messages to be sent",
|
||||
mock.initConverseWithPromises(
|
||||
null, ['rosterGroupsFetched'], {},
|
||||
function (done, _converse) {
|
||||
|
||||
var sent_stanza;
|
||||
let iq_stanza;
|
||||
test_utils.createContacts(_converse, 'current', 1);
|
||||
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
|
||||
|
||||
// First, fetch own device list
|
||||
return test_utils.waitUntil(() => {
|
||||
return _.filter(
|
||||
_converse.connection.IQ_stanzas,
|
||||
(iq) => {
|
||||
const node = iq.nodeTree.querySelector('iq[to="'+_converse.bare_jid+'"] query[node="eu.siacs.conversations.axolotl.devicelist"]');
|
||||
if (node) { iq_stanza = iq.nodeTree;}
|
||||
return node;
|
||||
}).length;
|
||||
}).then(() => {
|
||||
const stanza = $iq({
|
||||
'from': contact_jid,
|
||||
'id': iq_stanza.getAttribute('id'),
|
||||
'to': _converse.bare_jid,
|
||||
'type': 'result',
|
||||
}).c('query', {
|
||||
'xmlns': 'http://jabber.org/protocol/disco#items',
|
||||
'node': 'eu.siacs.conversations.axolotl.devicelist'
|
||||
}).c('device', {'id': '482886413b977930064a5888b92134fe'}).up()
|
||||
_converse.connection._dataRecv(test_utils.createRequest(stanza));
|
||||
|
||||
_converse.emit('OMEMOInitialized');
|
||||
|
||||
// Check that device list for contact is fetched when chat is opened.
|
||||
test_utils.openChatBoxFor(_converse, contact_jid);
|
||||
return test_utils.waitUntil(() => {
|
||||
return _.filter(
|
||||
_converse.connection.IQ_stanzas,
|
||||
(iq) => {
|
||||
const node = iq.nodeTree.querySelector('iq[to="'+contact_jid+'"] query[node="eu.siacs.conversations.axolotl.devicelist"]');
|
||||
if (node) { iq_stanza = iq.nodeTree; }
|
||||
return node;
|
||||
}).length;
|
||||
});
|
||||
}).then(() => {
|
||||
const stanza = $iq({
|
||||
'from': contact_jid,
|
||||
'id': iq_stanza.getAttribute('id'),
|
||||
'to': _converse.bare_jid,
|
||||
'type': 'result',
|
||||
}).c('query', {
|
||||
'xmlns': 'http://jabber.org/protocol/disco#items',
|
||||
'node': 'eu.siacs.conversations.axolotl.devicelist'
|
||||
}).c('device', {'id': '555'}).up()
|
||||
_converse.connection._dataRecv(test_utils.createRequest(stanza));
|
||||
|
||||
const devicelist = _converse.devicelists.create({'jid': contact_jid});
|
||||
expect(devicelist.devices.length).toBe(1);
|
||||
|
||||
const view = _converse.chatboxviews.get(contact_jid);
|
||||
view.model.set('omemo_active', true);
|
||||
|
||||
const textarea = view.el.querySelector('.chat-textarea');
|
||||
textarea.value = 'This message will be encrypted';
|
||||
view.keyPressed({
|
||||
target: textarea,
|
||||
preventDefault: _.noop,
|
||||
keyCode: 13 // Enter
|
||||
});
|
||||
return test_utils.waitUntil(() => {
|
||||
return _.filter(
|
||||
_converse.connection.IQ_stanzas,
|
||||
(iq) => {
|
||||
const node = iq.nodeTree.querySelector('iq[to="'+contact_jid+'"] items[node="eu.siacs.conversations.axolotl.bundles:555"]');
|
||||
if (node) { iq_stanza = iq.nodeTree; }
|
||||
return node;
|
||||
}).length;
|
||||
});
|
||||
}).then(() => {
|
||||
const stanza = $iq({
|
||||
'from': contact_jid,
|
||||
'id': iq_stanza.getAttribute('id'),
|
||||
'to': _converse.bare_jid,
|
||||
'type': 'result',
|
||||
}).c('pubsub', {
|
||||
'xmlns': 'http://jabber.org/protocol/pubsub'
|
||||
}).c('items', {'node': "eu.siacs.conversations.axolotl.bundles:555"})
|
||||
.c('item')
|
||||
.c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'})
|
||||
.c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t(btoa('1111')).up()
|
||||
.c('signedPreKeySignature').t(btoa('2222')).up()
|
||||
.c('identityKey').t(btoa('3333')).up()
|
||||
.c('prekeys')
|
||||
.c('preKeyPublic', {'preKeyId': '1'}).t(btoa('1001')).up()
|
||||
.c('preKeyPublic', {'preKeyId': '2'}).t(btoa('1002')).up()
|
||||
.c('preKeyPublic', {'preKeyId': '3'}).t(btoa('1003'));
|
||||
|
||||
spyOn(_converse.connection, 'send').and.callFake(stanza => { sent_stanza = stanza });
|
||||
_converse.connection._dataRecv(test_utils.createRequest(stanza));
|
||||
return test_utils.waitUntil(() => sent_stanza);
|
||||
}).then(function () {
|
||||
expect(sent_stanza.toLocaleString()).toBe(
|
||||
`<message from='dummy@localhost/resource' to='max.frankfurter@localhost' `+
|
||||
`type='chat' id='${sent_stanza.nodeTree.getAttribute('id')}' xmlns='jabber:client'>`+
|
||||
`<body>This is an OMEMO encrypted message which your client doesn’t seem to support. Find more information on https://conversations.im/omemo</body>`+
|
||||
`<encrypted xmlns='eu.siacs.conversations.axolotl'>`+
|
||||
`<header sid='123456789'>`+
|
||||
`<key>eyJpdiI6IjEyMzQ1In0=</key>`+
|
||||
`<iv>12345</iv>`+
|
||||
`</header>`+
|
||||
`</encrypted>`+
|
||||
`</message>`);
|
||||
done();
|
||||
});
|
||||
}));
|
||||
|
||||
it("will add processing hints to sent out encrypted <message> stanzas",
|
||||
mock.initConverseWithPromises(
|
||||
null, ['rosterGroupsFetched'], {},
|
||||
@ -24,8 +140,8 @@
|
||||
function (done, _converse) {
|
||||
|
||||
let iq_stanza;
|
||||
test_utils.createContacts(_converse, 'current');
|
||||
const contact_jid = mock.cur_names[3].replace(/ /g,'.').toLowerCase() + '@localhost';
|
||||
test_utils.createContacts(_converse, 'current', 1);
|
||||
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
|
||||
|
||||
test_utils.waitUntil(function () {
|
||||
return _.filter(
|
||||
|
@ -93,86 +93,143 @@
|
||||
return new Promise((resolve, reject) => {
|
||||
_converse.getDevicesForContact(this.get('jid'))
|
||||
.then((devices) => {
|
||||
const promises = devices.map((device) => device.getBundle());
|
||||
Promise.all(promises).then(() => {
|
||||
this.buildSessions(devices)
|
||||
.then(() => resolve(devices))
|
||||
.catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
|
||||
}).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
|
||||
Promise.all(devices.map((device) => device.getBundle()))
|
||||
.then(() => this.buildSessions(devices))
|
||||
.then(() => resolve(devices))
|
||||
.catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
|
||||
}).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
|
||||
}).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
|
||||
});
|
||||
},
|
||||
|
||||
buildSession (device) {
|
||||
const { _converse } = this.__super__;
|
||||
const bundle = device.get('bundle'),
|
||||
address = new libsignal.SignalProtocolAddress(device.get('jid'), device.get('id')),
|
||||
sessionBuilder = new libsignal.SessionBuilder(_converse.omemo_store, address),
|
||||
prekey = device.getRandomPreKey();
|
||||
|
||||
return sessionBuilder.processPreKey({
|
||||
'registrationId': _converse.omemo_store.get('registration_id'),
|
||||
'identityKey': _converse.omemo_store.get('identity_keypair'),
|
||||
'signedPreKey': {
|
||||
'keyId': bundle.signed_prekey.id, // <Number>
|
||||
'publicKey': u.base64ToArrayBuffer(bundle.signed_prekey.public_key),
|
||||
'signature': u.base64ToArrayBuffer(bundle.signed_prekey.signature)
|
||||
},
|
||||
'preKey': {
|
||||
'keyId': prekey.id, // <Number>
|
||||
'publicKey': u.base64ToArrayBuffer(prekey.key),
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
buildSessions (devices) {
|
||||
return Promise.all(devices.map((device) => this.buildSession(device)));
|
||||
},
|
||||
|
||||
encryptMessage (plaintext) {
|
||||
// The client MUST use fresh, randomly generated key/IV pairs
|
||||
// with AES-128 in Galois/Counter Mode (GCM).
|
||||
const TAG_LENGTH = 128,
|
||||
iv = window.crypto.getRandomValues(new window.Uint8Array(16));
|
||||
|
||||
let key;
|
||||
return window.crypto.subtle.generateKey({
|
||||
'name': "AES-GCM",
|
||||
'length': 256
|
||||
},
|
||||
true, // extractable
|
||||
["encrypt", "decrypt"] // key usages
|
||||
).then((result) => {
|
||||
key = result;
|
||||
const algo = {
|
||||
'name': 'AES-GCM',
|
||||
'iv': iv,
|
||||
'tagLength': TAG_LENGTH
|
||||
}
|
||||
return window.crypto.subtle.encrypt(algo, key, new TextEncoder().encode(plaintext));
|
||||
}).then((ciphertext) => {
|
||||
return window.crypto.subtle.exportKey("jwk", key)
|
||||
.then((key_str) => {
|
||||
return Promise.resolve({
|
||||
'key_str': key_str,
|
||||
'tag': ciphertext.slice(ciphertext.byteLength - ((TAG_LENGTH + 7) >> 3)),
|
||||
'iv': iv
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
encryptKey (plaintext, device) {
|
||||
const { _converse } = this.__super__,
|
||||
device_id = _converse.omemo_store.get('device_id');
|
||||
address = new libsignal.SignalProtocolAddress(this.get('jid'), device.get('id')),
|
||||
sessionCipher = new window.libsignal.SessionCipher(_converse.omemo_store, address);
|
||||
|
||||
return Promise.all(_.map(devices, (device) => {
|
||||
const recipient_id = device['id'];
|
||||
const address = new libsignal.SignalProtocolAddress(parseInt(recipient_id, 10), device_id);
|
||||
const sessionBuilder = new libsignal.SessionBuilder(_converse.omemo_store, address);
|
||||
return sessionBuilder.processPreKey({
|
||||
'registrationId': _converse.omemo_store.get('registration_id'),
|
||||
'identityKey': _converse.omemo_store.get('identity_keypair'),
|
||||
'signedPreKey': {
|
||||
'keyId': '', // <Number>,
|
||||
'publicKey': '', // <ArrayBuffer>,
|
||||
'signature': '', // <ArrayBuffer>
|
||||
},
|
||||
'preKey': {
|
||||
'keyId': '', // <Number>,
|
||||
'publicKey': '', // <ArrayBuffer>
|
||||
return sessionCipher.encrypt(plaintext);
|
||||
},
|
||||
|
||||
addKeysToMessageStanza (stanza, devices, payloads) {
|
||||
for (var i in payloads) {
|
||||
if (Object.prototype.hasOwnProperty.call(payloads, i)) {
|
||||
const payload = btoa(JSON.stringify(payloads[i]))
|
||||
const prekey = 3 == parseInt(payloads[i].type, 10)
|
||||
if (i == payloads.length-1) {
|
||||
stanza.c('key', {'rid': devices.get('id') }).t(payload)
|
||||
if (prekey) {
|
||||
stanza.attrs({'prekey': prekey});
|
||||
}
|
||||
stanza.up().c('iv').t(payloads[0].iv).up().up()
|
||||
} else {
|
||||
stanza.c('key', {prekey: prekey, rid: devices.get('id') }).t(payload).up()
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
return Promise.resolve(stanza);
|
||||
},
|
||||
|
||||
encryptMessage (message) {
|
||||
// TODO:
|
||||
// const { _converse } = this.__super__;
|
||||
// const plaintext = message.get('message');
|
||||
// const address = new libsignal.SignalProtocolAddress(recipientId, deviceId);
|
||||
// return new Promise((resolve, reject) => {
|
||||
// var sessionCipher = new window.libsignal.SessionCipher(_converse.omemo_store, address);
|
||||
// sessionCipher.encrypt(plaintext).then((ciphertext) => {});
|
||||
// });
|
||||
},
|
||||
|
||||
createOMEMOMessageStanza (message, bundles) {
|
||||
createOMEMOMessageStanza (message, devices) {
|
||||
const { _converse } = this.__super__, { __ } = _converse;
|
||||
const body = __("This is an OMEMO encrypted message which your client doesn’t seem to support. "+
|
||||
"Find more information on https://conversations.im/omemo");
|
||||
return new Promise((resolve, reject) => {
|
||||
this.encryptMessage(message).then((payload) => {
|
||||
const stanza = $msg({
|
||||
'from': _converse.connection.jid,
|
||||
'to': this.get('jid'),
|
||||
'type': this.get('message_type'),
|
||||
'id': message.get('msgid')
|
||||
}).c('body').t(body).up()
|
||||
.c('encrypted').t(payload)
|
||||
.c('header').t(payload).up()
|
||||
|
||||
_.forEach(bundles, (bundle) => {
|
||||
const prekey = bundle.prekeys[Math.random(bundle.prekeys.length)].textContent;
|
||||
stanza('key', {'rid': bundle.identity_key}).t(prekey).up()
|
||||
});
|
||||
// TODO: set storage hint urn:xmpp:hints
|
||||
resolve(stanza);
|
||||
}).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
|
||||
// An encrypted header is added to the message for each device that is supposed to receive it.
|
||||
// These headers simply contain the key that the payload message is encrypted with,
|
||||
// and they are separately encrypted using the session corresponding to the counterpart device.
|
||||
const stanza = $msg({
|
||||
'from': _converse.connection.jid,
|
||||
'to': this.get('jid'),
|
||||
'type': this.get('message_type'),
|
||||
'id': message.get('msgid')
|
||||
}).c('body').t(body).up()
|
||||
.c('encrypted', {'xmlns': Strophe.NS.OMEMO})
|
||||
.c('header', {'sid': _converse.omemo_store.get('device_id')});
|
||||
|
||||
return this.encryptMessage(message).then((payload) => {
|
||||
// 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
|
||||
// devices associated with the contact, the result of this
|
||||
// concatenation is encrypted using the corresponding
|
||||
// long-standing SignalProtocol session.
|
||||
|
||||
// TODO: need to include own devices here as well (and filter out distrusted devices)
|
||||
const promises = devices.map(device => this.encryptKey(payload.key_str+payload.tag, device));
|
||||
return Promise.all(promises).then((payloads) => this.addKeysToMessageStanza(stanza, devices, payloads));
|
||||
});
|
||||
},
|
||||
|
||||
sendMessage (attrs) {
|
||||
const { _converse } = this.__super__;
|
||||
if (this.get('omemo_active')) {
|
||||
const message = this.messages.create(attrs);
|
||||
this.getBundlesAndBuildSessions()
|
||||
.then((bundles) => this.createOMEMOMessageStanza(message, bundles))
|
||||
.then((stanza) => this.sendMessageStanza(stanza));
|
||||
.then((devices) => this.createOMEMOMessageStanza(message, devices))
|
||||
.then((stanza) => this.sendMessageStanza(stanza))
|
||||
.catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
|
||||
} else {
|
||||
return this.__super__.sendMessage.apply(this, arguments);
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
ChatBoxView: {
|
||||
@ -439,6 +496,12 @@
|
||||
'trusted': UNDECIDED
|
||||
},
|
||||
|
||||
getRandomPreKey () {
|
||||
// XXX: assumes that the bundle has already been fetched
|
||||
const bundle = this.get('bundle');
|
||||
return bundle.prekeys[u.getRandomInt(bundle.prekeys.length)];
|
||||
},
|
||||
|
||||
fetchBundleFromServer () {
|
||||
return new Promise((resolve, reject) => {
|
||||
const stanza = $iq({
|
||||
@ -467,7 +530,7 @@
|
||||
* this device, if the information is not at hand already.
|
||||
*/
|
||||
if (this.get('bundle')) {
|
||||
return Promise.resolve(this.get('bundle').toJSON());
|
||||
return Promise.resolve(this.get('bundle').toJSON(), this);
|
||||
} else {
|
||||
return this.fetchBundleFromServer();
|
||||
}
|
||||
|
@ -883,6 +883,10 @@
|
||||
return bytes.buffer
|
||||
};
|
||||
|
||||
u.getRandomInt = function (max) {
|
||||
return Math.floor(Math.random() * Math.floor(max));
|
||||
};
|
||||
|
||||
u.getUniqueId = function () {
|
||||
return 'xxxxxxxx-xxxx'.replace(/[x]/g, function(c) {
|
||||
var r = Math.random() * 16 | 0,
|
||||
|
@ -3,7 +3,7 @@
|
||||
//
|
||||
// This is the utilities module.
|
||||
//
|
||||
// Copyright (c) 2012-2017, Jan-Carel Brand <jc@opkode.com>
|
||||
// Copyright (c) 2013-2018, Jan-Carel Brand <jc@opkode.com>
|
||||
// Licensed under the Mozilla Public License (MPLv2)
|
||||
//
|
||||
/*global define, escape, Jed */
|
||||
|
@ -8,6 +8,22 @@
|
||||
var $iq = converse.env.$iq;
|
||||
|
||||
window.libsignal = {
|
||||
'SignalProtocolAddress': function (name, device_id) {
|
||||
this.name = name;
|
||||
this.deviceId = device_id;
|
||||
},
|
||||
'SessionCipher': function (storage, remote_address) {
|
||||
this.remoteAddress = remote_address;
|
||||
this.storage = storage;
|
||||
this.encrypt = () => Promise.resolve({
|
||||
'iv': '12345'
|
||||
});
|
||||
},
|
||||
'SessionBuilder': function (storage, remote_address) {
|
||||
this.processPreKey = function () {
|
||||
return Promise.resolve();
|
||||
}
|
||||
},
|
||||
'KeyHelper': {
|
||||
'generateIdentityKeyPair': function () {
|
||||
return Promise.resolve({
|
||||
|
Loading…
Reference in New Issue
Block a user