Kaynağa Gözat

More work on decrypting messages

JC Brand 7 yıl önce
ebeveyn
işleme
f2c283c907
7 değiştirilmiş dosya ile 390 ekleme ve 164 silme
  1. 173 66
      dist/converse.js
  2. 39 10
      spec/omemo.js
  3. 17 13
      src/converse-chatboxes.js
  4. 10 6
      src/converse-mam.js
  5. 140 67
      src/converse-omemo.js
  6. 7 2
      src/utils/core.js
  7. 4 0
      tests/mock.js

+ 173 - 66
dist/converse.js

@@ -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,

+ 39 - 10
spec/omemo.js

@@ -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();
             });
         }));

+ 17 - 13
src/converse-chatboxes.js

@@ -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 () {

+ 10 - 6
src/converse-mam.js

@@ -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);
+                    });
                 }
             },
 

+ 140 - 67
src/converse-omemo.js

@@ -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));
             }

+ 7 - 2
src/utils/core.js

@@ -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,

+ 4 - 0
tests/mock.js

@@ -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 () {