Commit f2c283c9 authored by JC Brand's avatar JC Brand

More work on decrypting messages

parent be0eaecf
...@@ -62601,12 +62601,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -62601,12 +62601,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
if (attrs.type === 'groupchat') { if (attrs.type === 'groupchat') {
attrs.from = stanza.getAttribute('from'); attrs.from = stanza.getAttribute('from');
attrs.nick = Strophe.unescapeNode(Strophe.getResourceFromJid(attrs.from)); attrs.nick = Strophe.unescapeNode(Strophe.getResourceFromJid(attrs.from));
attrs.sender = attrs.nick === this.get('nick') ? 'me' : 'them';
if (Strophe.getResourceFromJid(attrs.from) === this.get('nick')) {
attrs.sender = 'me';
} else {
attrs.sender = 'them';
}
} else { } else {
attrs.from = Strophe.getBareJidFromJid(stanza.getAttribute('from')); attrs.from = Strophe.getBareJidFromJid(stanza.getAttribute('from'));
...@@ -62628,26 +62623,29 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -62628,26 +62623,29 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
attrs.spoiler_hint = spoiler.textContent.length > 0 ? spoiler.textContent : ''; attrs.spoiler_hint = spoiler.textContent.length > 0 ? spoiler.textContent : '';
} }
return attrs; return Promise.resolve(attrs);
}, },
createMessage(message, original_stanza) { createMessage(message, original_stanza) {
/* Create a Backbone.Message object inside this chat box /* Create a Backbone.Message object inside this chat box
* based on the identified message stanza. * based on the identified message stanza.
*/ */
const attrs = this.getMessageAttributesFromStanza(message, original_stanza); return new Promise((resolve, reject) => {
this.getMessageAttributesFromStanza(message, original_stanza).then(attrs => {
const is_csn = u.isOnlyChatStateNotification(attrs); const is_csn = u.isOnlyChatStateNotification(attrs);
if (is_csn && (attrs.is_delayed || attrs.type === 'groupchat' && Strophe.getResourceFromJid(attrs.from) == this.get('nick'))) { if (is_csn && (attrs.is_delayed || attrs.type === 'groupchat' && Strophe.getResourceFromJid(attrs.from) == this.get('nick'))) {
// XXX: MUC leakage // XXX: MUC leakage
// No need showing delayed or our own CSN messages // No need showing delayed or our own CSN messages
return; resolve();
} else if (!is_csn && !attrs.file && !attrs.message && !attrs.oob_url && attrs.type !== 'error') { } else if (!is_csn && !attrs.file && !attrs.message && !attrs.oob_url && attrs.type !== 'error') {
// TODO: handle <subject> messages (currently being done by ChatRoom) // TODO: handle <subject> messages (currently being done by ChatRoom)
return; resolve();
} else { } else {
return this.messages.create(attrs); resolve(this.messages.create(attrs));
} }
});
});
}, },
isHidden() { isHidden() {
...@@ -68120,15 +68118,17 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -68120,15 +68118,17 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
// New functions which don't exist yet can also be added. // New functions which don't exist yet can also be added.
ChatBox: { ChatBox: {
getMessageAttributesFromStanza(message, original_stanza) { getMessageAttributesFromStanza(message, original_stanza) {
const attrs = this.__super__.getMessageAttributesFromStanza.apply(this, arguments); return new Promise((resolve, reject) => {
this.__super__.getMessageAttributesFromStanza.apply(this, arguments).then(attrs => {
const archive_id = getMessageArchiveID(original_stanza); const archive_id = getMessageArchiveID(original_stanza);
if (archive_id) { if (archive_id) {
attrs.archive_id = archive_id; attrs.archive_id = archive_id;
} }
return attrs; resolve(attrs);
}).catch(reject);
});
} }
}, },
...@@ -73387,6 +73387,102 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -73387,6 +73387,102 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
}); });
}, },
getKeyAndTag(string) {
return {
'key': string.slice(0, 43),
// 256bit key
'tag': string.slice(43, string.length) // rest is tag
};
},
decryptMessage(key_and_tag, attrs) {
const aes_data = this.getKeyAndTag(u.arrayBufferToString(key_and_tag));
const CryptoKeyObject = {
"alg": "A256GCM",
"ext": true,
"k": aes_data.key,
"key_ops": ["encrypt", "decrypt"],
"kty": "oct"
};
return crypto.subtle.importKey('jwk', CryptoKeyObject, 'AES-GCM', true, ['encrypt', 'decrypt']).then(key_obj => {
return window.crypto.subtle.decrypt({
'name': "AES-GCM",
'iv': u.base64ToArrayBuffer(attrs.iv),
'tagLength': 128
}, key_obj, u.stringToArrayBuffer(attrs.payload));
}).then(out => {
const decoder = new TextDecoder();
return decoder.decode(out);
});
},
decrypt(attrs) {
const _converse = this.__super__._converse,
address = new libsignal.SignalProtocolAddress(attrs.from, attrs.encrypted.device_id),
session_cipher = new window.libsignal.SessionCipher(_converse.omemo_store, address),
libsignal_payload = JSON.parse(atob(attrs.encrypted.key));
return new Promise((resolve, reject) => {
session_cipher.decryptWhisperMessage(libsignal_payload.body, 'binary').then(key_and_tag => this.decryptMessage(key_and_tag, attrs.encrypted)).then(f => {
// TODO handle decrypted messagej
//
resolve(f);
}).catch(reject);
});
},
getEncryptionAttributesfromStanza(encrypted) {
return new Promise((resolve, reject) => {
this.__super__.getMessageAttributesFromStanza.apply(this, arguments).then(attrs => {
const _converse = this.__super__._converse,
header = encrypted.querySelector('header'),
key = sizzle(`key[rid="${_converse.omemo_store.get('device_id')}"]`, encrypted).pop();
if (key) {
attrs['encrypted'] = {
'device_id': header.getAttribute('sid'),
'iv': header.querySelector('iv').textContent,
'key': key.textContent,
'payload': _.get(encrypted.querySelector('payload'), 'textContent', null)
};
if (key.getAttribute('prekey') === 'true') {
// If this is the case, a new session is built from this received element. The client
// SHOULD then republish their bundle information, replacing the used PreKey, such
// that it won't be used again by a different client. If the client already has a session
// with the sender's device, it MUST replace this session with the newly built session.
// The client MUST delete the private key belonging to the PreKey after use.
const address = new libsignal.SignalProtocolAddress(attrs.from, attrs.encrypted.device_id),
session_cipher = new window.libsignal.SessionCipher(_converse.omemo_store, address),
libsignal_payload = JSON.parse(atob(attrs.encrypted.key));
session_cipher.decryptPreKeyWhisperMessage(libsignal_payload.body, 'binary').then(key_and_tag => this.decryptMessage(key_and_tag, attrs.encrypted)).then(f => {
// TODO handle new key...
// _converse.omemo.publishBundle()
resolve(f);
}).catch(reject);
}
if (attrs.encrypted.payload) {
this.decrypt(attrs).then(text => {
attrs.plaintext = text;
resolve(attrs);
}).catch(reject);
}
}
});
});
},
getMessageAttributesFromStanza(stanza, original_stanza) {
const encrypted = sizzle(`encrypted[xmlns="${Strophe.NS.OMEMO}"]`, original_stanza).pop();
if (!encrypted) {
return this.__super__.getMessageAttributesFromStanza.apply(this, arguments);
} else {
return this.getEncryptionAttributesfromStanza(encrypted);
}
},
buildSessions(devices) { buildSessions(devices) {
return Promise.all(devices.map(device => this.buildSession(device))); return Promise.all(devices.map(device => this.buildSession(device)));
}, },
...@@ -73411,11 +73507,12 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -73411,11 +73507,12 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
}; };
return window.crypto.subtle.encrypt(algo, key, new TextEncoder().encode(plaintext)); return window.crypto.subtle.encrypt(algo, key, new TextEncoder().encode(plaintext));
}).then(ciphertext => { }).then(ciphertext => {
return window.crypto.subtle.exportKey("jwk", key).then(key_str => { return window.crypto.subtle.exportKey("jwk", key).then(key_obj => {
return Promise.resolve({ return Promise.resolve({
'key_str': key_str, 'key_str': key_obj.k,
'tag': ciphertext.slice(ciphertext.byteLength - (TAG_LENGTH + 7 >> 3)), 'tag': btoa(ciphertext.slice(ciphertext.byteLength - (TAG_LENGTH + 7 >> 3))),
'iv': iv 'ciphertext': btoa(ciphertext),
'iv': btoa(iv)
}); });
}); });
}); });
...@@ -73424,9 +73521,9 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -73424,9 +73521,9 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
encryptKey(plaintext, device) { encryptKey(plaintext, device) {
const _converse = this.__super__._converse, const _converse = this.__super__._converse,
address = new libsignal.SignalProtocolAddress(this.get('jid'), device.get('id')), address = new libsignal.SignalProtocolAddress(this.get('jid'), device.get('id')),
sessionCipher = new window.libsignal.SessionCipher(_converse.omemo_store, address); session_cipher = new window.libsignal.SessionCipher(_converse.omemo_store, address);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
sessionCipher.encrypt(plaintext).then(payload => resolve({ session_cipher.encrypt(plaintext).then(payload => resolve({
'payload': payload, 'payload': payload,
'device': device 'device': device
})).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR)); })).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
...@@ -73464,17 +73561,20 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -73464,17 +73561,20 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
const _converse = this.__super__._converse, const _converse = this.__super__._converse,
__ = _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. const body = __("This is an OMEMO encrypted message which your client doesn’t seem to support. " + "Find more information on https://conversations.im/omemo");
// 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({ const stanza = $msg({
'from': _converse.connection.jid, 'from': _converse.connection.jid,
'to': this.get('jid'), 'to': this.get('jid'),
'type': this.get('message_type'), 'type': this.get('message_type'),
'id': message.get('msgid') 'id': message.get('msgid')
}).c('body').t(body).up().c('encrypted', { }).c('body').t(body).up() // 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.
.c('encrypted', {
'xmlns': Strophe.NS.OMEMO 'xmlns': Strophe.NS.OMEMO
}).c('header', { }).c('header', {
'sid': _converse.omemo_store.get('device_id') 'sid': _converse.omemo_store.get('device_id')
...@@ -73486,9 +73586,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -73486,9 +73586,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
// devices associated with the contact, the result of this // devices associated with the contact, the result of this
// concatenation is encrypted using the corresponding // concatenation is encrypted using the corresponding
// long-standing SignalProtocol session. // long-standing SignalProtocol session.
// TODO: need to include own devices here as well (and filter out distrusted devices)
const promises = devices.filter(device => device.get('trusted') != UNTRUSTED).map(device => this.encryptKey(payload.key_str + payload.tag, device)); const promises = devices.filter(device => device.get('trusted') != UNTRUSTED).map(device => this.encryptKey(payload.key_str + payload.tag, device));
return Promise.all(promises).then(dicts => this.addKeysToMessageStanza(stanza, dicts, payload.iv)).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR)); return Promise.all(promises).then(dicts => this.addKeysToMessageStanza(stanza, dicts, payload.iv)).then(stanza => stanza.c('payload').t(payload.ciphertext)).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
}); });
}, },
...@@ -73928,8 +74027,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -73928,8 +74027,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
_converse.DeviceLists = Backbone.Collection.extend({ _converse.DeviceLists = Backbone.Collection.extend({
model: _converse.DeviceList model: _converse.DeviceList
}); });
_converse.omemo = {
function publishBundle() { publishBundle() {
const store = _converse.omemo_store, const store = _converse.omemo_store,
signed_prekey = store.get('signed_prekey'); signed_prekey = store.get('signed_prekey');
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
...@@ -73956,6 +74055,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -73956,6 +74055,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
}); });
} }
};
function fetchDeviceLists() { function fetchDeviceLists() {
return new Promise((resolve, reject) => _converse.devicelists.fetch({ return new Promise((resolve, reject) => _converse.devicelists.fetch({
'success': resolve 'success': resolve
...@@ -74085,7 +74186,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -74085,7 +74186,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
function initOMEMO() { function initOMEMO() {
_converse.devicelists = new _converse.DeviceLists(); _converse.devicelists = new _converse.DeviceLists();
_converse.devicelists.browserStorage = new Backbone.BrowserStorage[_converse.storage](b64_sha1(`converse.devicelists-${_converse.bare_jid}`)); _converse.devicelists.browserStorage = new Backbone.BrowserStorage[_converse.storage](b64_sha1(`converse.devicelists-${_converse.bare_jid}`));
fetchOwnDevices().then(() => restoreOMEMOSession()).then(() => updateOwnDeviceList()).then(() => publishBundle()).then(() => _converse.emit('OMEMOInitialized')).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR)); fetchOwnDevices().then(() => restoreOMEMOSession()).then(() => updateOwnDeviceList()).then(() => _converse.omemo.publishBundle()).then(() => _converse.emit('OMEMOInitialized')).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
} }
_converse.api.listen.on('afterTearDown', () => _converse.devices.reset()); _converse.api.listen.on('afterTearDown', () => _converse.devices.reset());
...@@ -82593,14 +82694,20 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -82593,14 +82694,20 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
}; };
u.arrayBufferToString = function (ab) { u.arrayBufferToString = function (ab) {
var enc = new TextDecoder("utf-8"); const enc = new TextDecoder("utf-8");
return enc.decode(new Uint8Array(ab)); return enc.decode(ab);
}; };
u.arrayBufferToBase64 = function (ab) { u.arrayBufferToBase64 = function (ab) {
return btoa(new Uint8Array(ab).reduce((data, byte) => data + String.fromCharCode(byte), '')); return btoa(new Uint8Array(ab).reduce((data, byte) => data + String.fromCharCode(byte), ''));
}; };
u.stringToArrayBuffer = function (string) {
const enc = new TextEncoder(); // always utf-8
return enc.encode(string);
};
u.base64ToArrayBuffer = function (b64) { u.base64ToArrayBuffer = function (b64) {
const binary_string = window.atob(b64), const binary_string = window.atob(b64),
len = binary_string.length, len = binary_string.length,
...@@ -10,13 +10,30 @@ ...@@ -10,13 +10,30 @@
describe("The OMEMO module", function() { describe("The OMEMO module", function() {
it("adds methods for encrypting and decrypting messages via AES GCM",
mock.initConverseWithPromises(
null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
function (done, _converse) {
let iq_stanza, view, sent_stanza;
test_utils.createContacts(_converse, 'current', 1);
_converse.emit('rosterContactsFetched');
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
test_utils.openChatBoxFor(_converse, contact_jid)
.then((view) => view.model.encryptMessage('This message will be encrypted'))
.then((payload) => {
debugger;
return view.model.decryptMessage(payload);
}).then(done);
}));
it("enables encrypted messages to be sent and received", it("enables encrypted messages to be sent and received",
mock.initConverseWithPromises( mock.initConverseWithPromises(
null, ['rosterGroupsFetched', 'chatBoxesFetched'], {}, null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
function (done, _converse) { function (done, _converse) {
var sent_stanza; let iq_stanza, view, sent_stanza;
let iq_stanza, view;
test_utils.createContacts(_converse, 'current', 1); test_utils.createContacts(_converse, 'current', 1);
_converse.emit('rosterContactsFetched'); _converse.emit('rosterContactsFetched');
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost'; const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
...@@ -143,7 +160,7 @@ ...@@ -143,7 +160,7 @@
spyOn(_converse.connection, 'send').and.callFake(stanza => { sent_stanza = stanza }); spyOn(_converse.connection, 'send').and.callFake(stanza => { sent_stanza = stanza });
_converse.connection._dataRecv(test_utils.createRequest(stanza)); _converse.connection._dataRecv(test_utils.createRequest(stanza));
return test_utils.waitUntil(() => sent_stanza); return test_utils.waitUntil(() => sent_stanza);
}).then(function () { }).then(() => {
expect(sent_stanza.toLocaleString()).toBe( expect(sent_stanza.toLocaleString()).toBe(
`<message from='dummy@localhost/resource' to='max.frankfurter@localhost' `+ `<message from='dummy@localhost/resource' to='max.frankfurter@localhost' `+
`type='chat' id='${sent_stanza.nodeTree.getAttribute('id')}' xmlns='jabber:client'>`+ `type='chat' id='${sent_stanza.nodeTree.getAttribute('id')}' xmlns='jabber:client'>`+
...@@ -154,10 +171,21 @@ ...@@ -154,10 +171,21 @@
`<key rid='555'>eyJ0eXBlIjoxLCJib2R5IjoiYzFwaDNSNzNYNyIsInJlZ2lzdHJhdGlvbklkIjoiMTMzNyJ9</key>`+ `<key rid='555'>eyJ0eXBlIjoxLCJib2R5IjoiYzFwaDNSNzNYNyIsInJlZ2lzdHJhdGlvbklkIjoiMTMzNyJ9</key>`+
`<iv>${sent_stanza.nodeTree.querySelector('iv').textContent}</iv>`+ `<iv>${sent_stanza.nodeTree.querySelector('iv').textContent}</iv>`+
`</header>`+ `</header>`+
`<payload>${sent_stanza.nodeTree.querySelector('payload').textContent}</payload>`+
`</encrypted>`+ `</encrypted>`+
`</message>`); `</message>`);
// Test reception of an encrypted message // Test reception of an encrypted message
return view.model.encryptMessage('This is an encrypted message from the contact')
}).then((payload) => {
// 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.
const key = btoa(JSON.stringify({
'type': 1,
'body': payload.key_str+payload.tag,
'registrationId': '1337'
}));
const stanza = $msg({ const stanza = $msg({
'from': contact_jid, 'from': contact_jid,
'to': _converse.connection.jid, 'to': _converse.connection.jid,
...@@ -166,21 +194,22 @@ ...@@ -166,21 +194,22 @@
}).c('body').t('This is a fallback message').up() }).c('body').t('This is a fallback message').up()
.c('encrypted', {'xmlns': Strophe.NS.OMEMO}) .c('encrypted', {'xmlns': Strophe.NS.OMEMO})
.c('header', {'sid': '555'}) .c('header', {'sid': '555'})
.c('key', {'rid': _converse.omemo_store.get('device_id')}).t('c1ph3R73X7').up() .c('key', {'rid': _converse.omemo_store.get('device_id')}).t(key).up()
.c('iv').t('1234') .c('iv').t(payload.iv)
.up().up() .up().up()
.c('payload').t('M04R-c1ph3R73X7'); .c('payload').t(payload.ciphertext);
_converse.connection._dataRecv(test_utils.createRequest(stanza)); _converse.connection._dataRecv(test_utils.createRequest(stanza));
return test_utils.waitUntil(() => view.model.messages.length > 1);
}).then(() => {
expect(view.model.messages.length).toBe(2); expect(view.model.messages.length).toBe(2);
const last_msg = view.model.messages.at(1), const last_msg = view.model.messages.at(1),
encrypted = last_msg.get('encrypted'); encrypted = last_msg.get('encrypted');
expect(encrypted instanceof Object).toBe(true); expect(encrypted instanceof Object).toBe(true);
expect(encrypted.device_id).toBe('555'); expect(encrypted.device_id).toBe('555');
expect(encrypted.iv).toBe('1234'); expect(encrypted.iv).toBe(btoa('1234'));
expect(encrypted.key).toBe('c1ph3R73X7'); expect(encrypted.key).toBe(btoa('c1ph3R73X7'));
expect(encrypted.payload).toBe('M04R-c1ph3R73X7'); expect(encrypted.payload).toBe(btoa('M04R-c1ph3R73X7'));
done(); done();
}); });
})); }));
......
...@@ -498,25 +498,29 @@ ...@@ -498,25 +498,29 @@
if (spoiler) { if (spoiler) {
attrs.spoiler_hint = spoiler.textContent.length > 0 ? spoiler.textContent : ''; attrs.spoiler_hint = spoiler.textContent.length > 0 ? spoiler.textContent : '';
} }
return attrs; return Promise.resolve(attrs);
}, },
createMessage (message, original_stanza) { createMessage (message, original_stanza) {
/* Create a Backbone.Message object inside this chat box /* Create a Backbone.Message object inside this chat box
* based on the identified message stanza. * based on the identified message stanza.
*/ */
const attrs = this.getMessageAttributesFromStanza(message, original_stanza); return new Promise((resolve, reject) => {
this.getMessageAttributesFromStanza(message, original_stanza)
.then((attrs) => {
const is_csn = u.isOnlyChatStateNotification(attrs); const is_csn = u.isOnlyChatStateNotification(attrs);
if (is_csn && (attrs.is_delayed || (attrs.type === 'groupchat' && Strophe.getResourceFromJid(attrs.from) == this.get('nick')))) { if (is_csn && (attrs.is_delayed || (attrs.type === 'groupchat' && Strophe.getResourceFromJid(attrs.from) == this.get('nick')))) {
// XXX: MUC leakage // XXX: MUC leakage
// No need showing delayed or our own CSN messages // No need showing delayed or our own CSN messages
return; resolve();
} else if (!is_csn && !attrs.file && !attrs.message && !attrs.oob_url && attrs.type !== 'error') { } else if (!is_csn && !attrs.file && !attrs.message && !attrs.oob_url && attrs.type !== 'error') {
// TODO: handle <subject> messages (currently being done by ChatRoom) // TODO: handle <subject> messages (currently being done by ChatRoom)
return; resolve();
} else { } else {
return this.messages.create(attrs); resolve(this.messages.create(attrs));
} }
});
});
}, },
isHidden () { isHidden () {
......
...@@ -129,12 +129,16 @@ ...@@ -129,12 +129,16 @@
// New functions which don't exist yet can also be added. // New functions which don't exist yet can also be added.
ChatBox: { ChatBox: {
getMessageAttributesFromStanza (message, original_stanza) { getMessageAttributesFromStanza (message, original_stanza) {
const attrs = this.__super__.getMessageAttributesFromStanza.apply(this, arguments); return new Promise((resolve, reject) => {
this.__super__.getMessageAttributesFromStanza.apply(this, arguments)
.then((attrs) => {
const archive_id = getMessageArchiveID(original_stanza); const archive_id = getMessageArchiveID(original_stanza);
if (archive_id) { if (archive_id) {
attrs.archive_id = archive_id; attrs.archive_id = archive_id;
} }
return attrs; resolve(attrs);
}).catch(reject);
});
} }
}, },
......
...@@ -128,14 +128,60 @@ ...@@ -128,14 +128,60 @@
}) })
}, },
getMessageAttributesFromStanza (stanza, original_stanza) { getKeyAndTag (string) {
return {
'key': string.slice(0, 43), // 256bit key
'tag': string.slice(43, string.length) // rest is tag
}
},
decryptMessage (key_and_tag, attrs) {
const aes_data = this.getKeyAndTag(u.arrayBufferToString(key_and_tag));
const CryptoKeyObject = {
"alg": "A256GCM",
"ext": true,
"k": aes_data.key,
"key_ops": ["encrypt","decrypt"],
"kty": "oct"
}
return crypto.subtle.importKey('jwk', CryptoKeyObject, 'AES-GCM', true, ['encrypt','decrypt'])
.then((key_obj) => {
return window.crypto.subtle.decrypt(
{'name': "AES-GCM", 'iv': u.base64ToArrayBuffer(attrs.iv), 'tagLength': 128},
key_obj,
u.stringToArrayBuffer(attrs.payload)
);
}).then((out) => {
const decoder = new TextDecoder()
return decoder.decode(out)
})
},
decrypt (attrs) {
const { _converse } = this.__super__, const { _converse } = this.__super__,
attrs = this.__super__.getMessageAttributesFromStanza.apply(this, arguments), address = new libsignal.SignalProtocolAddress(attrs.from, attrs.encrypted.device_id),
encrypted = sizzle(`encrypted[xmlns="${Strophe.NS.OMEMO}"]`, original_stanza).pop(); session_cipher = new window.libsignal.SessionCipher(_converse.omemo_store, address),
libsignal_payload = JSON.parse(atob(attrs.encrypted.key));
if (encrypted) { return new Promise((resolve, reject) => {
const header = encrypted.querySelector('header'), session_cipher.decryptWhisperMessage(libsignal_payload.body, 'binary')
.then((key_and_tag) => this.decryptMessage(key_and_tag, attrs.encrypted))
.then((f) => {
// TODO handle decrypted messagej
//
resolve(f);
}).catch(reject);
});
},
getEncryptionAttributesfromStanza (encrypted) {
return new Promise((resolve, reject) => {
this.__super__.getMessageAttributesFromStanza.apply(this, arguments)
.then((attrs) => {
const { _converse } = this.__super__,
header = encrypted.querySelector('header'),
key = sizzle(`key[rid="${_converse.omemo_store.get('device_id')}"]`, encrypted).pop(); key = sizzle(`key[rid="${_converse.omemo_store.get('device_id')}"]`, encrypted).pop();
if (key) { if (key) {
attrs['encrypted'] = { attrs['encrypted'] = {
'device_id': header.getAttribute('sid'), 'device_id': header.getAttribute('sid'),
...@@ -144,22 +190,43 @@ ...@@ -144,22 +190,43 @@
'payload': _.get(encrypted.querySelector('payload'), 'textContent', null) 'payload': _.get(encrypted.querySelector('payload'), 'textContent', null)
} }
if (key.getAttribute('prekey') === 'true') { if (key.getAttribute('prekey') === 'true') {
// TODO: // If this is the case, a new session is built from this received element. The client
// If this is the case, a new session is built // SHOULD then republish their bundle information, replacing the used PreKey, such
// from this received element. The client // that it won't be used again by a different client. If the client already has a session
// SHOULD then republish their bundle // with the sender's device, it MUST replace this session with the newly built session.
// information, replacing the used PreKey, such // The client MUST delete the private key belonging to the PreKey after use.
// that it won't be used again by a different const address = new libsignal.SignalProtocolAddress(attrs.from, attrs.encrypted.device_id),
// client. If the client already has a session session_cipher = new window.libsignal.SessionCipher(_converse.omemo_store, address),
// with the sender's device, it MUST replace libsignal_payload = JSON.parse(atob(attrs.encrypted.key));
// this session with the newly built session.
// The client MUST delete the private key session_cipher.decryptPreKeyWhisperMessage(libsignal_payload.body, 'binary')
// belonging to the PreKey after use. .then(key_and_tag => this.decryptMessage(key_and_tag, attrs.encrypted))
throw new Error("Not yet implemented"); .then((f) => {
// TODO handle new key...
// _converse.omemo.publishBundle()
resolve(f);
}).catch(reject);
} }
if (attrs.encrypted.payload) {
this.decrypt(attrs)
.then((text) => {
attrs.plaintext = text
resolve(attrs);
})
.catch(reject);
} }
} }
return attrs; });
});
},
getMessageAttributesFromStanza (stanza, original_stanza) {
const encrypted = sizzle(`encrypted[xmlns="${Strophe.NS.OMEMO}"]`, original_stanza).pop();
if (!encrypted) {
return this.__super__.getMessageAttributesFromStanza.apply(this, arguments);
} else {
return this.getEncryptionAttributesfromStanza(encrypted);
}
}, },
buildSessions (devices) { buildSessions (devices) {
...@@ -189,11 +256,12 @@ ...@@ -189,11 +256,12 @@
return window.crypto.subtle.encrypt(algo, key, new TextEncoder().encode(plaintext)); return window.crypto.subtle.encrypt(algo, key, new TextEncoder().encode(plaintext));
}).then((ciphertext) => { }).then((ciphertext) => {
return window.crypto.subtle.exportKey("jwk", key) return window.crypto.subtle.exportKey("jwk", key)
.then((key_str) => { .then((key_obj) => {
return Promise.resolve({ return Promise.resolve({
'key_str': key_str, 'key_str': key_obj.k,
'tag': ciphertext.slice(ciphertext.byteLength - ((TAG_LENGTH + 7) >> 3)), 'tag': btoa(ciphertext.slice(ciphertext.byteLength - ((TAG_LENGTH + 7) >> 3))),
'iv': iv 'ciphertext': btoa(ciphertext),
'iv': btoa(iv)
}); });
}); });
}); });
...@@ -202,10 +270,10 @@ ...@@ -202,10 +270,10 @@
encryptKey (plaintext, device) { encryptKey (plaintext, device) {
const { _converse } = this.__super__, const { _converse } = this.__super__,
address = new libsignal.SignalProtocolAddress(this.get('jid'), device.get('id')), address = new libsignal.SignalProtocolAddress(this.get('jid'), device.get('id')),
sessionCipher = new window.libsignal.SessionCipher(_converse.omemo_store, address); session_cipher = new window.libsignal.SessionCipher(_converse.omemo_store, address);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
sessionCipher.encrypt(plaintext) session_cipher.encrypt(plaintext)
.then(payload => resolve({'payload': payload, 'device': device})) .then(payload => resolve({'payload': payload, 'device': device}))
.catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR)); .catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
}); });
...@@ -236,15 +304,18 @@ ...@@ -236,15 +304,18 @@
const body = __("This is an OMEMO encrypted message which your client doesn’t seem to support. "+ const body = __("This is an OMEMO encrypted message which your client doesn’t seem to support. "+
"Find more information on https://conversations.im/omemo"); "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({ const stanza = $msg({
'from': _converse.connection.jid, 'from': _converse.connection.jid,
'to': this.get('jid'), 'to': this.get('jid'),
'type': this.get('message_type'), 'type': this.get('message_type'),
'id': message.get('msgid') 'id': message.get('msgid')
}).c('body').t(body).up() }).c('body').t(body).up()
// 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.
.c('encrypted', {'xmlns': Strophe.NS.OMEMO}) .c('encrypted', {'xmlns': Strophe.NS.OMEMO})
.c('header', {'sid': _converse.omemo_store.get('device_id')}); .c('header', {'sid': _converse.omemo_store.get('device_id')});
...@@ -255,14 +326,13 @@ ...@@ -255,14 +326,13 @@
// devices associated with the contact, the result of this // devices associated with the contact, the result of this
// concatenation is encrypted using the corresponding // concatenation is encrypted using the corresponding
// long-standing SignalProtocol session. // long-standing SignalProtocol session.
// TODO: need to include own devices here as well (and filter out distrusted devices)
const promises = devices const promises = devices
.filter(device => device.get('trusted') != UNTRUSTED) .filter(device => device.get('trusted') != UNTRUSTED)
.map(device => this.encryptKey(payload.key_str+payload.tag, device)); .map(device => this.encryptKey(payload.key_str+payload.tag, device));
return Promise.all(promises) return Promise.all(promises)
.then((dicts) => this.addKeysToMessageStanza(stanza, dicts, payload.iv)) .then((dicts) => this.addKeysToMessageStanza(stanza, dicts, payload.iv))
.then((stanza) => stanza.c('payload').t(payload.ciphertext))
.catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR)); .catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
}); });
}, },
...@@ -670,7 +740,9 @@ ...@@ -670,7 +740,9 @@
}); });
function publishBundle () { _converse.omemo = {
publishBundle () {
const store = _converse.omemo_store, const store = _converse.omemo_store,
signed_prekey = store.get('signed_prekey'); signed_prekey = store.get('signed_prekey');
...@@ -698,6 +770,7 @@ ...@@ -698,6 +770,7 @@
_converse.connection.sendIQ(stanza, resolve, reject, _converse.IQ_TIMEOUT); _converse.connection.sendIQ(stanza, resolve, reject, _converse.IQ_TIMEOUT);
}); });
} }
}
function fetchDeviceLists () { function fetchDeviceLists () {
return new Promise((resolve, reject) => _converse.devicelists.fetch({'success': resolve})); return new Promise((resolve, reject) => _converse.devicelists.fetch({'success': resolve}));
...@@ -804,7 +877,7 @@ ...@@ -804,7 +877,7 @@
fetchOwnDevices() fetchOwnDevices()
.then(() => restoreOMEMOSession()) .then(() => restoreOMEMOSession())
.then(() => updateOwnDeviceList()) .then(() => updateOwnDeviceList())
.then(() => publishBundle()) .then(() => _converse.omemo.publishBundle())
.then(() => _converse.emit('OMEMOInitialized')) .then(() => _converse.emit('OMEMOInitialized'))
.catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR)); .catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
} }
......
...@@ -863,8 +863,8 @@ ...@@ -863,8 +863,8 @@
}; };
u.arrayBufferToString = function (ab) { u.arrayBufferToString = function (ab) {
var enc = new TextDecoder("utf-8"); const enc = new TextDecoder("utf-8");
return enc.decode(new Uint8Array(ab)); return enc.decode(ab);
}; };
u.arrayBufferToBase64 = function (ab) { u.arrayBufferToBase64 = function (ab) {
...@@ -872,6 +872,11 @@ ...@@ -872,6 +872,11 @@
.reduce((data, byte) => data + String.fromCharCode(byte), '')); .reduce((data, byte) => data + String.fromCharCode(byte), ''));
}; };
u.stringToArrayBuffer = function (string) {
const enc = new TextEncoder(); // always utf-8
return enc.encode(string);
};
u.base64ToArrayBuffer = function (b64) { u.base64ToArrayBuffer = function (b64) {
const binary_string = window.atob(b64), const binary_string = window.atob(b64),
len = binary_string.length, len = binary_string.length,
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
var Strophe = converse.env.Strophe; var Strophe = converse.env.Strophe;
var moment = converse.env.moment; var moment = converse.env.moment;
var $iq = converse.env.$iq; var $iq = converse.env.$iq;
var u = converse.env.utils;
window.libsignal = { window.libsignal = {
'SignalProtocolAddress': function (name, device_id) { 'SignalProtocolAddress': function (name, device_id) {
...@@ -20,6 +21,9 @@ ...@@ -20,6 +21,9 @@
'body': 'c1ph3R73X7', 'body': 'c1ph3R73X7',
'registrationId': '1337' 'registrationId': '1337'
}); });
this.decryptWhisperMessage = (key_and_tag) => {
return Promise.resolve(u.stringToArrayBuffer(key_and_tag));
}
}, },
'SessionBuilder': function (storage, remote_address) { 'SessionBuilder': function (storage, remote_address) {
this.processPreKey = function () { this.processPreKey = function () {
......
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