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_
if (attrs.type === 'groupchat') {
attrs.from = stanza.getAttribute('from');
attrs.nick = Strophe.unescapeNode(Strophe.getResourceFromJid(attrs.from));
if (Strophe.getResourceFromJid(attrs.from) === this.get('nick')) {
attrs.sender = 'me';
} else {
attrs.sender = 'them';
}
attrs.sender = attrs.nick === this.get('nick') ? 'me' : 'them';
} else {
attrs.from = Strophe.getBareJidFromJid(stanza.getAttribute('from'));
......@@ -62628,26 +62623,29 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
attrs.spoiler_hint = spoiler.textContent.length > 0 ? spoiler.textContent : '';
}
return attrs;
return Promise.resolve(attrs);
},
createMessage(message, original_stanza) {
/* Create a Backbone.Message object inside this chat box
* based on the identified message stanza.
*/
const attrs = this.getMessageAttributesFromStanza(message, original_stanza);
const is_csn = u.isOnlyChatStateNotification(attrs);
return new Promise((resolve, reject) => {
this.getMessageAttributesFromStanza(message, original_stanza).then(attrs => {
const is_csn = u.isOnlyChatStateNotification(attrs);
if (is_csn && (attrs.is_delayed || attrs.type === 'groupchat' && Strophe.getResourceFromJid(attrs.from) == this.get('nick'))) {
// XXX: MUC leakage
// No need showing delayed or our own CSN messages
return;
} else if (!is_csn && !attrs.file && !attrs.message && !attrs.oob_url && attrs.type !== 'error') {
// TODO: handle <subject> messages (currently being done by ChatRoom)
return;
} else {
return this.messages.create(attrs);
}
if (is_csn && (attrs.is_delayed || attrs.type === 'groupchat' && Strophe.getResourceFromJid(attrs.from) == this.get('nick'))) {
// XXX: MUC leakage
// No need showing delayed or our own CSN messages
resolve();
} else if (!is_csn && !attrs.file && !attrs.message && !attrs.oob_url && attrs.type !== 'error') {
// TODO: handle <subject> messages (currently being done by ChatRoom)
resolve();
} else {
resolve(this.messages.create(attrs));
}
});
});
},
isHidden() {
......@@ -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.
ChatBox: {
getMessageAttributesFromStanza(message, original_stanza) {
const attrs = this.__super__.getMessageAttributesFromStanza.apply(this, arguments);
const archive_id = getMessageArchiveID(original_stanza);
return new Promise((resolve, reject) => {
this.__super__.getMessageAttributesFromStanza.apply(this, arguments).then(attrs => {
const archive_id = getMessageArchiveID(original_stanza);
if (archive_id) {
attrs.archive_id = archive_id;
}
if (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_
});
},
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) {
return Promise.all(devices.map(device => this.buildSession(device)));
},
......@@ -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));
}).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({
'key_str': key_str,
'tag': ciphertext.slice(ciphertext.byteLength - (TAG_LENGTH + 7 >> 3)),
'iv': iv
'key_str': key_obj.k,
'tag': btoa(ciphertext.slice(ciphertext.byteLength - (TAG_LENGTH + 7 >> 3))),
'ciphertext': btoa(ciphertext),
'iv': btoa(iv)
});
});
});
......@@ -73424,9 +73521,9 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
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);
session_cipher = new window.libsignal.SessionCipher(_converse.omemo_store, address);
return new Promise((resolve, reject) => {
sessionCipher.encrypt(plaintext).then(payload => resolve({
session_cipher.encrypt(plaintext).then(payload => resolve({
'payload': payload,
'device': device
})).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
......@@ -73464,17 +73561,20 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
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 body = __("This is an OMEMO encrypted message which your client doesn’t seem to support. " + "Find more information on https://conversations.im/omemo");
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', {
}).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('header', {
'sid': _converse.omemo_store.get('device_id')
......@@ -73486,9 +73586,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
// 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.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,33 +74027,35 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
_converse.DeviceLists = Backbone.Collection.extend({
model: _converse.DeviceList
});
_converse.omemo = {
publishBundle() {
const store = _converse.omemo_store,
signed_prekey = store.get('signed_prekey');
return new Promise((resolve, reject) => {
const stanza = $iq({
'from': _converse.bare_jid,
'type': 'set'
}).c('pubsub', {
'xmlns': Strophe.NS.PUBSUB
}).c('publish', {
'node': `${Strophe.NS.OMEMO_BUNDLES}:${store.get('device_id')}`
}).c('item').c('bundle', {
'xmlns': Strophe.NS.OMEMO
}).c('signedPreKeyPublic', {
'signedPreKeyId': signed_prekey.keyId
}).t(u.arrayBufferToBase64(signed_prekey.keyPair.pubKey)).up().c('signedPreKeySignature').t(u.arrayBufferToBase64(signed_prekey.signature)).up().c('identityKey').t(u.arrayBufferToBase64(store.get('identity_keypair').pubKey)).up().c('prekeys');
_.forEach(store.get('prekeys').slice(0, _converse.NUM_PREKEYS), prekey => {
stanza.c('preKeyPublic', {
'preKeyId': prekey.keyId
}).t(u.arrayBufferToBase64(prekey.keyPair.pubKey)).up();
});
function publishBundle() {
const store = _converse.omemo_store,
signed_prekey = store.get('signed_prekey');
return new Promise((resolve, reject) => {
const stanza = $iq({
'from': _converse.bare_jid,
'type': 'set'
}).c('pubsub', {
'xmlns': Strophe.NS.PUBSUB
}).c('publish', {
'node': `${Strophe.NS.OMEMO_BUNDLES}:${store.get('device_id')}`
}).c('item').c('bundle', {
'xmlns': Strophe.NS.OMEMO
}).c('signedPreKeyPublic', {
'signedPreKeyId': signed_prekey.keyId
}).t(u.arrayBufferToBase64(signed_prekey.keyPair.pubKey)).up().c('signedPreKeySignature').t(u.arrayBufferToBase64(signed_prekey.signature)).up().c('identityKey').t(u.arrayBufferToBase64(store.get('identity_keypair').pubKey)).up().c('prekeys');
_.forEach(store.get('prekeys').slice(0, _converse.NUM_PREKEYS), prekey => {
stanza.c('preKeyPublic', {
'preKeyId': prekey.keyId
}).t(u.arrayBufferToBase64(prekey.keyPair.pubKey)).up();
_converse.connection.sendIQ(stanza, resolve, reject, _converse.IQ_TIMEOUT);
});
}
_converse.connection.sendIQ(stanza, resolve, reject, _converse.IQ_TIMEOUT);
});
}
};
function fetchDeviceLists() {
return new Promise((resolve, reject) => _converse.devicelists.fetch({
......@@ -74085,7 +74186,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
function initOMEMO() {
_converse.devicelists = new _converse.DeviceLists();
_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());
......@@ -82593,14 +82694,20 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
};
u.arrayBufferToString = function (ab) {
var enc = new TextDecoder("utf-8");
return enc.decode(new Uint8Array(ab));
const enc = new TextDecoder("utf-8");
return enc.decode(ab);
};
u.arrayBufferToBase64 = function (ab) {
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) {
const binary_string = window.atob(b64),
len = binary_string.length,
......@@ -10,13 +10,30 @@
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",
mock.initConverseWithPromises(
null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
function (done, _converse) {
var sent_stanza;
let iq_stanza, view;
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';
......@@ -143,7 +160,7 @@
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 () {
}).then(() => {
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'>`+
......@@ -154,10 +171,21 @@
`<key rid='555'>eyJ0eXBlIjoxLCJib2R5IjoiYzFwaDNSNzNYNyIsInJlZ2lzdHJhdGlvbklkIjoiMTMzNyJ9</key>`+
`<iv>${sent_stanza.nodeTree.querySelector('iv').textContent}</iv>`+
`</header>`+
`<payload>${sent_stanza.nodeTree.querySelector('payload').textContent}</payload>`+
`</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({
'from': contact_jid,
'to': _converse.connection.jid,
......@@ -166,21 +194,22 @@
}).c('body').t('This is a fallback message').up()
.c('encrypted', {'xmlns': Strophe.NS.OMEMO})
.c('header', {'sid': '555'})
.c('key', {'rid': _converse.omemo_store.get('device_id')}).t('c1ph3R73X7').up()
.c('iv').t('1234')
.c('key', {'rid': _converse.omemo_store.get('device_id')}).t(key).up()
.c('iv').t(payload.iv)
.up().up()
.c('payload').t('M04R-c1ph3R73X7');
.c('payload').t(payload.ciphertext);
_converse.connection._dataRecv(test_utils.createRequest(stanza));
return test_utils.waitUntil(() => view.model.messages.length > 1);
}).then(() => {
expect(view.model.messages.length).toBe(2);
const last_msg = view.model.messages.at(1),
encrypted = last_msg.get('encrypted');
expect(encrypted instanceof Object).toBe(true);
expect(encrypted.device_id).toBe('555');
expect(encrypted.iv).toBe('1234');
expect(encrypted.key).toBe('c1ph3R73X7');
expect(encrypted.payload).toBe('M04R-c1ph3R73X7');
expect(encrypted.iv).toBe(btoa('1234'));
expect(encrypted.key).toBe(btoa('c1ph3R73X7'));
expect(encrypted.payload).toBe(btoa('M04R-c1ph3R73X7'));
done();
});
}));
......
......@@ -498,25 +498,29 @@
if (spoiler) {
attrs.spoiler_hint = spoiler.textContent.length > 0 ? spoiler.textContent : '';
}
return attrs;
return Promise.resolve(attrs);
},
createMessage (message, original_stanza) {
/* Create a Backbone.Message object inside this chat box
* based on the identified message stanza.
*/
const attrs = this.getMessageAttributesFromStanza(message, original_stanza);
const is_csn = u.isOnlyChatStateNotification(attrs);
if (is_csn && (attrs.is_delayed || (attrs.type === 'groupchat' && Strophe.getResourceFromJid(attrs.from) == this.get('nick')))) {
// XXX: MUC leakage
// No need showing delayed or our own CSN messages
return;
} else if (!is_csn && !attrs.file && !attrs.message && !attrs.oob_url && attrs.type !== 'error') {
// TODO: handle <subject> messages (currently being done by ChatRoom)
return;
} else {
return this.messages.create(attrs);
}
return new Promise((resolve, reject) => {
this.getMessageAttributesFromStanza(message, original_stanza)
.then((attrs) => {
const is_csn = u.isOnlyChatStateNotification(attrs);
if (is_csn && (attrs.is_delayed || (attrs.type === 'groupchat' && Strophe.getResourceFromJid(attrs.from) == this.get('nick')))) {
// XXX: MUC leakage
// No need showing delayed or our own CSN messages
resolve();
} else if (!is_csn && !attrs.file && !attrs.message && !attrs.oob_url && attrs.type !== 'error') {
// TODO: handle <subject> messages (currently being done by ChatRoom)
resolve();
} else {
resolve(this.messages.create(attrs));
}
});
});
},
isHidden () {
......
......@@ -129,12 +129,16 @@
// New functions which don't exist yet can also be added.
ChatBox: {
getMessageAttributesFromStanza (message, original_stanza) {
const attrs = this.__super__.getMessageAttributesFromStanza.apply(this, arguments);
const archive_id = getMessageArchiveID(original_stanza);
if (archive_id) {
attrs.archive_id = archive_id;
}
return attrs;
return new Promise((resolve, reject) => {
this.__super__.getMessageAttributesFromStanza.apply(this, arguments)
.then((attrs) => {
const archive_id = getMessageArchiveID(original_stanza);
if (archive_id) {
attrs.archive_id = archive_id;
}
resolve(attrs);
}).catch(reject);
});
}
},
......
......@@ -128,38 +128,105 @@
})
},
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__,
attrs = this.__super__.getMessageAttributesFromStanza.apply(this, arguments),
encrypted = sizzle(`encrypted[xmlns="${Strophe.NS.OMEMO}"]`, original_stanza).pop();
if (encrypted) {
const 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') {
// TODO:
// 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.
throw new Error("Not yet implemented");
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__,
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);
}
return attrs;
},
buildSessions (devices) {
......@@ -189,11 +256,12 @@
return window.crypto.subtle.encrypt(algo, key, new TextEncoder().encode(plaintext));
}).then((ciphertext) => {
return window.crypto.subtle.exportKey("jwk", key)
.then((key_str) => {
.then((key_obj) => {
return Promise.resolve({
'key_str': key_str,
'tag': ciphertext.slice(ciphertext.byteLength - ((TAG_LENGTH + 7) >> 3)),
'iv': iv
'key_str': key_obj.k,
'tag': btoa(ciphertext.slice(ciphertext.byteLength - ((TAG_LENGTH + 7) >> 3))),
'ciphertext': btoa(ciphertext),
'iv': btoa(iv)
});
});
});
......@@ -202,10 +270,10 @@
encryptKey (plaintext, device) {
const { _converse } = this.__super__,
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) => {
sessionCipher.encrypt(plaintext)
session_cipher.encrypt(plaintext)
.then(payload => resolve({'payload': payload, 'device': device}))
.catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
});
......@@ -236,15 +304,18 @@
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()
// 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('header', {'sid': _converse.omemo_store.get('device_id')});
......@@ -255,14 +326,13 @@
// 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
.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))
.then((stanza) => stanza.c('payload').t(payload.ciphertext))
.catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
});
},
......@@ -670,33 +740,36 @@
});
function publishBundle () {
const store = _converse.omemo_store,
signed_prekey = store.get('signed_prekey');
_converse.omemo = {
return new Promise((resolve, reject) => {
const stanza = $iq({
'from': _converse.bare_jid,
'type': 'set'
}).c('pubsub', {'xmlns': Strophe.NS.PUBSUB})
.c('publish', {'node': `${Strophe.NS.OMEMO_BUNDLES}:${store.get('device_id')}`})
.c('item')
.c('bundle', {'xmlns': Strophe.NS.OMEMO})
.c('signedPreKeyPublic', {'signedPreKeyId': signed_prekey.keyId})
.t(u.arrayBufferToBase64(signed_prekey.keyPair.pubKey)).up()
.c('signedPreKeySignature')
.t(u.arrayBufferToBase64(signed_prekey.signature)).up()
.c('identityKey')
.t(u.arrayBufferToBase64(store.get('identity_keypair').pubKey)).up()
.c('prekeys');
_.forEach(
store.get('prekeys').slice(0, _converse.NUM_PREKEYS),
(prekey) => {
stanza.c('preKeyPublic', {'preKeyId': prekey.keyId})
.t(u.arrayBufferToBase64(prekey.keyPair.pubKey)).up();
});
_converse.connection.sendIQ(stanza, resolve, reject, _converse.IQ_TIMEOUT);
});
publishBundle () {
const store = _converse.omemo_store,
signed_prekey = store.get('signed_prekey');
return new Promise((resolve, reject) => {
const stanza = $iq({
'from': _converse.bare_jid,
'type': 'set'
}).c('pubsub', {'xmlns': Strophe.NS.PUBSUB})
.c('publish', {'node': `${Strophe.NS.OMEMO_BUNDLES}:${store.get('device_id')}`})
.c('item')
.c('bundle', {'xmlns': Strophe.NS.OMEMO})
.c('signedPreKeyPublic', {'signedPreKeyId': signed_prekey.keyId})
.t(u.arrayBufferToBase64(signed_prekey.keyPair.pubKey)).up()
.c('signedPreKeySignature')
.t(u.arrayBufferToBase64(signed_prekey.signature)).up()
.c('identityKey')
.t(u.arrayBufferToBase64(store.get('identity_keypair').pubKey)).up()
.c('prekeys');
_.forEach(
store.get('prekeys').slice(0, _converse.NUM_PREKEYS),
(prekey) => {
stanza.c('preKeyPublic', {'preKeyId': prekey.keyId})
.t(u.arrayBufferToBase64(prekey.keyPair.pubKey)).up();
});
_converse.connection.sendIQ(stanza, resolve, reject, _converse.IQ_TIMEOUT);
});
}
}
function fetchDeviceLists () {
......@@ -804,7 +877,7 @@
fetchOwnDevices()
.then(() => restoreOMEMOSession())
.then(() => updateOwnDeviceList())
.then(() => publishBundle())
.then(() => _converse.omemo.publishBundle())
.then(() => _converse.emit('OMEMOInitialized'))
.catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
}
......
......@@ -863,8 +863,8 @@
};
u.arrayBufferToString = function (ab) {
var enc = new TextDecoder("utf-8");
return enc.decode(new Uint8Array(ab));
const enc = new TextDecoder("utf-8");
return enc.decode(ab);
};
u.arrayBufferToBase64 = function (ab) {
......@@ -872,6 +872,11 @@
.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) {
const binary_string = window.atob(b64),
len = binary_string.length,
......
......@@ -6,6 +6,7 @@
var Strophe = converse.env.Strophe;
var moment = converse.env.moment;
var $iq = converse.env.$iq;
var u = converse.env.utils;
window.libsignal = {
'SignalProtocolAddress': function (name, device_id) {
......@@ -20,6 +21,9 @@
'body': 'c1ph3R73X7',
'registrationId': '1337'
});
this.decryptWhisperMessage = (key_and_tag) => {
return Promise.resolve(u.stringToArrayBuffer(key_and_tag));
}
},
'SessionBuilder': function (storage, remote_address) {
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