فهرست منبع

Test decryption of incoming OMEMO message

updates #497
JC Brand 7 سال پیش
والد
کامیت
2884549bb3
5فایلهای تغییر یافته به همراه104 افزوده شده و 86 حذف شده
  1. 47 35
      dist/converse.js
  2. 7 12
      spec/omemo.js
  3. 2 1
      src/converse-message-view.js
  4. 47 37
      src/converse-omemo.js
  5. 1 1
      tests/mock.js

+ 47 - 35
dist/converse.js

@@ -68608,7 +68608,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
             msg.querySelector('.chat-msg__media').innerHTML = _.flow(_.partial(u.renderFileURL, _converse), _.partial(u.renderMovieURL, _converse), _.partial(u.renderAudioURL, _converse), _.partial(u.renderImageURL, _converse))(url);
           }
 
-          let text = this.model.get('message');
+          const encrypted = this.model.get('encrypted');
+          let text = encrypted ? this.model.get('plaintext') : this.model.get('message');
 
           if (is_me_message) {
             text = text.replace(/^\/me/, '');
@@ -73420,21 +73421,47 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
           }).then(out => new TextDecoder().decode(out)).catch(e => _converse.log(e.toString(), Strophe.LogLevel.ERROR));
         },
 
+        decryptFromKeyAndTag(key_and_tag, obj) {
+          const aes_data = this.getKeyAndTag(u.arrayBufferToString(key_and_tag));
+          return this.decryptMessage(_.extend(obj, {
+            'key': aes_data.key,
+            'tag': aes_data.tag
+          }));
+        },
+
+        handlePreKeyMessage(attrs) {
+          // TODO
+          const _converse = this.__super__._converse; // 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));
+          return session_cipher.decryptPreKeyWhisperMessage(libsignal_payload.body, 'binary').then(key_and_tag => this.decryptFromKeyAndTag(key_and_tag, attrs.encrypted)).then(f => {// TODO handle new key...
+            // _converse.omemo.publishBundle()
+          });
+        },
+
         decrypt(attrs) {
+          if (attrs.prekey === 'true') {
+            return this.handlePreKeyMessage(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);
+            session_cipher.decryptWhisperMessage(libsignal_payload.body, 'binary').then(key_and_tag => this.decryptFromKeyAndTag(key_and_tag, attrs.encrypted)).then(resolve).catch(reject);
           });
         },
 
-        getEncryptionAttributesfromStanza(encrypted) {
+        getEncryptionAttributesfromStanza(stanza, original_stanza) {
+          const _converse = this.__super__._converse;
+          const encrypted = sizzle(`encrypted[xmlns="${Strophe.NS.OMEMO}"]`, original_stanza).pop();
           return new Promise((resolve, reject) => {
             this.__super__.getMessageAttributesFromStanza.apply(this, arguments).then(attrs => {
               const _converse = this.__super__._converse,
@@ -73446,33 +73473,14 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
                   'device_id': header.getAttribute('sid'),
                   'iv': header.querySelector('iv').textContent,
                   'key': key.textContent,
-                  'payload': _.get(encrypted.querySelector('payload'), 'textContent', null)
+                  'payload': _.get(encrypted.querySelector('payload'), 'textContent', null),
+                  'prekey': key.getAttribute('prekey')
                 };
-
-                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(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);
-                }
+                this.decrypt(attrs).then(plaintext => resolve(_.extend(attrs, {
+                  'plaintext': plaintext
+                }))).catch(reject);
               }
-            });
+            }).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
           });
         },
 
@@ -73482,7 +73490,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
           if (!encrypted) {
             return this.__super__.getMessageAttributesFromStanza.apply(this, arguments);
           } else {
-            return this.getEncryptionAttributesfromStanza(encrypted);
+            return this.getEncryptionAttributesfromStanza(stanza, original_stanza);
           }
         },
 
@@ -73507,9 +73515,13 @@ 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_obj => {
+              const tag = u.arrayBufferToBase64(ciphertext.slice(ciphertext.byteLength - (TAG_LENGTH + 7 >> 3)));
+              console.log('XXXX: Base64 TAG is ' + tag);
+              console.log('YYY: KEY is ' + key_obj.k);
               return Promise.resolve({
                 'key': key_obj.k,
-                'tag': u.arrayBufferToBase64(ciphertext.slice(ciphertext.byteLength - (TAG_LENGTH + 7 >> 3))),
+                'tag': tag,
+                'key_and_tag': btoa(key_obj.k + tag),
                 'payload': u.arrayBufferToBase64(ciphertext),
                 'iv': u.arrayBufferToBase64(iv)
               });
@@ -73585,7 +73597,7 @@ 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.
-            const promises = devices.filter(device => device.get('trusted') != UNTRUSTED).map(device => this.encryptKey(obj.key + obj.tag, device));
+            const promises = devices.filter(device => device.get('trusted') != UNTRUSTED).map(device => this.encryptKey(obj.key_and_tag, device));
             return Promise.all(promises).then(dicts => this.addKeysToMessageStanza(stanza, dicts, obj.iv)).then(stanza => stanza.c('payload').t(obj.payload)).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
           });
         },

+ 7 - 12
spec/omemo.js

@@ -182,13 +182,13 @@
 
                 // Test reception of an encrypted message
                 return view.model.encryptMessage('This is an encrypted message from the contact')
-            }).then((payload) => {
+            }).then((obj) => {
                 // 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,
+                    'body': obj.key_and_tag,
                     'registrationId': '1337' 
                 }));
                 const stanza = $msg({
@@ -200,21 +200,16 @@
                         .c('encrypted', {'xmlns': Strophe.NS.OMEMO})
                             .c('header', {'sid':  '555'})
                                 .c('key', {'rid':  _converse.omemo_store.get('device_id')}).t(key).up()
-                                .c('iv').t(payload.iv)
+                                .c('iv').t(obj.iv)
                                 .up().up()
-                            .c('payload').t(payload.ciphertext);
+                            .c('payload').t(obj.payload);
                 _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(btoa('1234'));
-                expect(encrypted.key).toBe(btoa('c1ph3R73X7'));
-                expect(encrypted.payload).toBe(btoa('M04R-c1ph3R73X7'));
+                const last_msg = view.model.messages.at(1);
+                expect(view.el.querySelectorAll('.chat-msg__body')[1].textContent.trim())
+                    .toBe('This is an encrypted message from the contact');
                 done();
             });
         }));

+ 2 - 1
src/converse-message-view.js

@@ -158,7 +158,8 @@
                             _.partial(u.renderImageURL, _converse))(url);
                     }
 
-                    let text = this.model.get('message');
+                    const encrypted = this.model.get('encrypted');
+                    let text = encrypted ? this.model.get('plaintext') : this.model.get('message');
                     if (is_me_message) {
                         text = text.replace(/^\/me/, '');
                     }

+ 47 - 37
src/converse-omemo.js

@@ -161,7 +161,35 @@
                           .catch(e => _converse.log(e.toString(), Strophe.LogLevel.ERROR));
                 },
 
+                decryptFromKeyAndTag (key_and_tag, obj) {
+                    const aes_data = this.getKeyAndTag(u.arrayBufferToString(key_and_tag));
+                    return this.decryptMessage(_.extend(obj, {'key': aes_data.key, 'tag': aes_data.tag}));
+                },
+
+                handlePreKeyMessage (attrs) {
+                    // TODO
+                    const { _converse } = this.__super__;
+                    // 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));
+
+                    return session_cipher.decryptPreKeyWhisperMessage(libsignal_payload.body, 'binary')
+                        .then(key_and_tag => this.decryptFromKeyAndTag(key_and_tag, attrs.encrypted))
+                        .then((f) => {
+                            // TODO handle new key...
+                            // _converse.omemo.publishBundle()
+                        });
+                },
+
                 decrypt (attrs) {
+                    if (attrs.prekey === 'true') {
+                        return this.handlePreKeyMessage(attrs)
+                    }
                     const { _converse } = this.__super__,
                           address  = new libsignal.SignalProtocolAddress(attrs.from, attrs.encrypted.device_id),
                           session_cipher = new window.libsignal.SessionCipher(_converse.omemo_store, address),
@@ -169,16 +197,16 @@
 
                     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);
+                        .then((key_and_tag) => this.decryptFromKeyAndTag(key_and_tag, attrs.encrypted))
+                        .then(resolve)
+                        .catch(reject);
                     });
                 },
 
-                getEncryptionAttributesfromStanza (encrypted) {
+                getEncryptionAttributesfromStanza (stanza, original_stanza) {
+                    const { _converse } = this.__super__;
+                    const encrypted = sizzle(`encrypted[xmlns="${Strophe.NS.OMEMO}"]`, original_stanza).pop();
+
                     return new Promise((resolve, reject) => {
                         this.__super__.getMessageAttributesFromStanza.apply(this, arguments)
                         .then((attrs) => {
@@ -191,36 +219,14 @@
                                     'device_id': header.getAttribute('sid'),
                                     'iv': header.querySelector('iv').textContent,
                                     'key': key.textContent,
-                                    'payload': _.get(encrypted.querySelector('payload'), 'textContent', null)
+                                    'payload': _.get(encrypted.querySelector('payload'), 'textContent', null),
+                                    'prekey': key.getAttribute('prekey')
                                 }
-                                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(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);
-                                    })
+                                this.decrypt(attrs)
+                                    .then((plaintext) => resolve(_.extend(attrs, {'plaintext': plaintext})))
                                     .catch(reject);
-                                }
                             }
-                        });
+                        }).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
                     });
                 },
 
@@ -229,7 +235,7 @@
                     if (!encrypted) {
                         return this.__super__.getMessageAttributesFromStanza.apply(this, arguments);
                     } else {
-                        return this.getEncryptionAttributesfromStanza(encrypted);
+                        return this.getEncryptionAttributesfromStanza(stanza, original_stanza);
                     }
                 },
 
@@ -257,9 +263,13 @@
                     }).then((ciphertext) => {
                         return window.crypto.subtle.exportKey("jwk", key)
                             .then((key_obj) => {
+                                const tag = u.arrayBufferToBase64(ciphertext.slice(ciphertext.byteLength - ((TAG_LENGTH + 7) >> 3)));
+                                console.log('XXXX: Base64 TAG is '+tag);
+                                console.log('YYY: KEY is '+key_obj.k);
                                 return Promise.resolve({
                                     'key': key_obj.k,
-                                    'tag': u.arrayBufferToBase64(ciphertext.slice(ciphertext.byteLength - ((TAG_LENGTH + 7) >> 3))),
+                                    'tag': tag,
+                                    'key_and_tag': btoa(key_obj.k + tag),
                                     'payload': u.arrayBufferToBase64(ciphertext),
                                     'iv': u.arrayBufferToBase64(iv)
                                 });
@@ -328,7 +338,7 @@
                         // long-standing SignalProtocol session.
                         const promises = devices
                             .filter(device => device.get('trusted') != UNTRUSTED)
-                            .map(device => this.encryptKey(obj.key+obj.tag, device));
+                            .map(device => this.encryptKey(obj.key_and_tag, device));
 
                         return Promise.all(promises)
                             .then((dicts) => this.addKeysToMessageStanza(stanza, dicts, obj.iv))

+ 1 - 1
tests/mock.js

@@ -22,7 +22,7 @@
                 'registrationId': '1337' 
             });
             this.decryptWhisperMessage = (key_and_tag) => {
-                return Promise.resolve(u.stringToArrayBuffer(key_and_tag));
+                return Promise.resolve(u.stringToArrayBuffer(atob(key_and_tag)));
             }
         },
         'SessionBuilder': function (storage, remote_address) {