Browse Source

Use `raw` key format and not `jwk`. Set correct key size.

JC Brand 6 years ago
parent
commit
6a061b4525
5 changed files with 100 additions and 94 deletions
  1. 46 45
      dist/converse.js
  2. 8 5
      spec/omemo.js
  3. 28 36
      src/converse-omemo.js
  4. 18 7
      src/utils/core.js
  5. 0 1
      tests/mock.js

+ 46 - 45
dist/converse.js

@@ -71600,7 +71600,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
   const TAG_LENGTH = 128;
   const KEY_ALGO = {
     'name': "AES-GCM",
-    'length': 256
+    'length': 128
   };
 
   function parseBundle(bundle_el) {
@@ -71769,25 +71769,8 @@ 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(obj) {
-          const _converse = this.__super__._converse,
-                key_obj = {
-            "alg": "A256GCM",
-            "ext": true,
-            "k": obj.key,
-            "key_ops": ["encrypt", "decrypt"],
-            "kty": "oct"
-          };
-          return crypto.subtle.importKey('jwk', key_obj, KEY_ALGO, true, ['encrypt', 'decrypt']).then(key_obj => {
+          return crypto.subtle.importKey('raw', obj.key, KEY_ALGO, true, ['encrypt', 'decrypt']).then(key_obj => {
             const algo = {
               'name': "AES-GCM",
               'iv': u.base64ToArrayBuffer(obj.iv),
@@ -71810,17 +71793,17 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
 
         decrypt(attrs) {
           const _converse = this.__super__._converse,
-                address = new libsignal.SignalProtocolAddress(attrs.from, parseInt(attrs.encrypted.device_id, 10)),
-                session_cipher = new window.libsignal.SessionCipher(_converse.omemo_store, address); // https://xmpp.org/extensions/xep-0384.html#usecases-receiving
+                session_cipher = this.getSessionCipher(attrs.from, parseInt(attrs.encrypted.device_id, 10)); // https://xmpp.org/extensions/xep-0384.html#usecases-receiving
 
           if (attrs.encrypted.prekey === 'true') {
             let plaintext;
             return session_cipher.decryptPreKeyWhisperMessage(atob(attrs.encrypted.key), 'binary').then(key_and_tag => {
               if (attrs.encrypted.payload) {
-                const aes_data = this.getKeyAndTag(u.arrayBufferToString(key_and_tag));
+                const key = key_and_tag.slice(0, 16),
+                      tag = key_and_tag.slice(16);
                 return this.decryptMessage(_.extend(attrs.encrypted, {
-                  'key': aes_data.key,
-                  'tag': aes_data.tag
+                  'key': key,
+                  'tag': tag
                 }));
               }
 
@@ -71844,10 +71827,11 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
             });
           } else {
             return session_cipher.decryptWhisperMessage(atob(attrs.encrypted.key), 'binary').then(key_and_tag => {
-              const aes_data = this.getKeyAndTag(u.arrayBufferToString(key_and_tag));
+              const key = key_and_tag.slice(0, 16),
+                    tag = key_and_tag.slice(16);
               return this.decryptMessage(_.extend(attrs.encrypted, {
-                'key': aes_data.key,
-                'tag': aes_data.tag
+                'key': key,
+                'tag': tag
               }));
             }).then(plaintext => _.extend(attrs, {
               'plaintext': plaintext
@@ -71911,12 +71895,11 @@ 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)));
+            return window.crypto.subtle.exportKey("raw", key).then(key => {
+              const tag = ciphertext.slice(ciphertext.byteLength - (TAG_LENGTH + 7 >> 3));
               return Promise.resolve({
-                'key': key_obj.k,
-                'tag': tag,
-                'key_and_tag': key_obj.k + tag,
+                'key': key,
+                'key_and_tag': u.appendArrayBuffer(key, tag),
                 'payload': u.arrayBufferToBase64(ciphertext),
                 'iv': u.arrayBufferToBase64(iv)
               });
@@ -71924,11 +71907,18 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
           });
         },
 
+        getSessionCipher(jid, id) {
+          if (!this.session_cipher) {
+            const _converse = this.__super__._converse,
+                  address = new libsignal.SignalProtocolAddress(jid, id);
+            this.session_cipher = new window.libsignal.SessionCipher(_converse.omemo_store, address);
+          }
+
+          return this.session_cipher;
+        },
+
         encryptKey(plaintext, device) {
-          const _converse = this.__super__._converse,
-                address = new libsignal.SignalProtocolAddress(device.get('jid'), device.get('id')),
-                session_cipher = new window.libsignal.SessionCipher(_converse.omemo_store, address);
-          return session_cipher.encrypt(plaintext).then(payload => ({
+          return this.getSessionCipher(device.get('jid'), device.get('id')).encrypt(plaintext).then(payload => ({
             'payload': payload,
             'device': device
           }));
@@ -71994,7 +71984,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_and_tag, device));
+            const promises = devices.filter(device => device.get('trusted') != UNTRUSTED).map(device => this.encryptKey(u.arrayBufferToString(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).up().up();
               stanza.c('store', {
@@ -81292,24 +81282,35 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
     return fp;
   };
 
+  u.appendArrayBuffer = function (buffer1, buffer2) {
+    const tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
+    tmp.set(new Uint8Array(buffer1), 0);
+    tmp.set(new Uint8Array(buffer2), buffer1.byteLength);
+    return tmp.buffer;
+  };
+
   u.arrayBufferToHex = function (ab) {
     // https://stackoverflow.com/questions/40031688/javascript-arraybuffer-to-hex#40031979
     return Array.prototype.map.call(new Uint8Array(ab), x => ('00' + x.toString(16)).slice(-2)).join('');
   };
 
   u.arrayBufferToString = function (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), ''));
+    return new Uint8Array(ab).reduce((data, byte) => data + String.fromCharCode(byte), '');
   };
 
   u.stringToArrayBuffer = function (string) {
-    const enc = new TextEncoder(); // always utf-8
+    const len = string.length,
+          bytes = new Uint8Array(len);
+
+    for (let i = 0; i < len; i++) {
+      bytes[i] = string.charCodeAt(i);
+    }
 
-    return enc.encode(string);
+    return bytes.buffer;
+  };
+
+  u.arrayBufferToBase64 = function (ab) {
+    return btoa(new Uint8Array(ab).reduce((data, byte) => data + String.fromCharCode(byte), ''));
   };
 
   u.base64ToArrayBuffer = function (b64) {

+ 8 - 5
spec/omemo.js

@@ -88,9 +88,9 @@
             .then((v) => {
                 view = v;
                 return view.model.encryptMessage(message);
-            }).then((payload) => {
+            }).then(payload => {
                 return view.model.decryptMessage(payload);
-            }).then((result) => {
+            }).then(result => {
                 expect(result).toBe(message);
                 done();
             });
@@ -199,10 +199,13 @@
 
                 // Test reception of an encrypted message
                 return view.model.encryptMessage('This is an encrypted message from the contact')
-            }).then((obj) => {
+            }).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.
+
+                // u.stringToArrayBuffer(atob(u.arrayBufferToBase64(obj.key_and_tag)));
+                // u.stringToArrayBuffer(u.arrayBufferToString(obj.key_and_tag));
                 const stanza = $msg({
                         'from': contact_jid,
                         'to': _converse.connection.jid,
@@ -211,7 +214,7 @@
                     }).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(btoa(obj.key_and_tag)).up()
+                                .c('key', {'rid':  _converse.omemo_store.get('device_id')}).t(u.arrayBufferToBase64(obj.key_and_tag)).up()
                                 .c('iv').t(obj.iv)
                                 .up().up()
                             .c('payload').t(obj.payload);
@@ -255,7 +258,7 @@
                                 .c('key', {
                                     'prekey': 'true',
                                     'rid':  _converse.omemo_store.get('device_id')
-                                }).t(btoa(obj.key_and_tag)).up()
+                                }).t(u.arrayBufferToBase64(obj.key_and_tag)).up()
                                 .c('iv').t(obj.iv)
                                 .up().up()
                             .c('payload').t(obj.payload);

+ 28 - 36
src/converse-omemo.js

@@ -28,7 +28,7 @@
     const TAG_LENGTH = 128;
     const KEY_ALGO = {
         'name': "AES-GCM",
-        'length': 256
+        'length': 128
     };
 
 
@@ -201,24 +201,9 @@
                     });
                 },
 
-                getKeyAndTag (string) {
-                    return {
-                        'key': string.slice(0, 43), // 256bit key
-                        'tag': string.slice(43, string.length) // rest is tag
-                    }
-                },
-
                 decryptMessage (obj) {
-                    const { _converse } = this.__super__,
-                          key_obj = {
-                              "alg": "A256GCM",
-                              "ext": true,
-                              "k": obj.key,
-                              "key_ops": ["encrypt","decrypt"],
-                              "kty": "oct"
-                          };
-                    return crypto.subtle.importKey('jwk', key_obj, KEY_ALGO, true, ['encrypt','decrypt'])
-                        .then((key_obj) => {
+                    return crypto.subtle.importKey('raw', obj.key, KEY_ALGO, true, ['encrypt','decrypt'])
+                        .then(key_obj => {
                             const algo = {
                                 'name': "AES-GCM",
                                 'iv': u.base64ToArrayBuffer(obj.iv),
@@ -240,8 +225,7 @@
 
                 decrypt (attrs) {
                     const { _converse } = this.__super__,
-                          address  = new libsignal.SignalProtocolAddress(attrs.from, parseInt(attrs.encrypted.device_id, 10)),
-                          session_cipher = new window.libsignal.SessionCipher(_converse.omemo_store, address);
+                          session_cipher = this.getSessionCipher(attrs.from, parseInt(attrs.encrypted.device_id, 10));
 
                     // https://xmpp.org/extensions/xep-0384.html#usecases-receiving
                     if (attrs.encrypted.prekey === 'true') {
@@ -249,8 +233,9 @@
                         return session_cipher.decryptPreKeyWhisperMessage(atob(attrs.encrypted.key), 'binary')
                             .then(key_and_tag => {
                                 if (attrs.encrypted.payload) {
-                                    const aes_data = this.getKeyAndTag(u.arrayBufferToString(key_and_tag));
-                                    return this.decryptMessage(_.extend(attrs.encrypted, {'key': aes_data.key, 'tag': aes_data.tag}));
+                                    const key = key_and_tag.slice(0, 16),
+                                          tag = key_and_tag.slice(16);
+                                    return this.decryptMessage(_.extend(attrs.encrypted, {'key': key, 'tag': tag}));
                                 }
                                 return Promise.resolve();
                             }).then(pt => {
@@ -270,8 +255,9 @@
                     } else {
                         return session_cipher.decryptWhisperMessage(atob(attrs.encrypted.key), 'binary')
                             .then(key_and_tag => {
-                                const aes_data = this.getKeyAndTag(u.arrayBufferToString(key_and_tag));
-                                return this.decryptMessage(_.extend(attrs.encrypted, {'key': aes_data.key, 'tag': aes_data.tag}));
+                                const key = key_and_tag.slice(0, 16),
+                                      tag = key_and_tag.slice(16);
+                                return this.decryptMessage(_.extend(attrs.encrypted, {'key': key, 'tag': tag}));
                             }).then(plaintext => _.extend(attrs, {'plaintext': plaintext}))
                               .catch(e => {
                                   this.reportDecryptionError(e);
@@ -332,13 +318,12 @@
                         }
                         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)));
+                        return window.crypto.subtle.exportKey("raw", key)
+                            .then(key => {
+                                const tag = ciphertext.slice(ciphertext.byteLength - ((TAG_LENGTH + 7) >> 3));
                                 return Promise.resolve({
-                                    'key': key_obj.k,
-                                    'tag': tag,
-                                    'key_and_tag': key_obj.k + tag,
+                                    'key': key,
+                                    'key_and_tag': u.appendArrayBuffer(key, tag),
                                     'payload': u.arrayBufferToBase64(ciphertext),
                                     'iv': u.arrayBufferToBase64(iv)
                                 });
@@ -346,12 +331,19 @@
                     });
                 },
 
-                encryptKey (plaintext, device) {
-                    const { _converse } = this.__super__,
-                          address = new libsignal.SignalProtocolAddress(device.get('jid'), device.get('id')),
-                          session_cipher = new window.libsignal.SessionCipher(_converse.omemo_store, address);
+                getSessionCipher (jid, id) {
+                    if (!this.session_cipher) {
+                        const { _converse } = this.__super__,
+                              address = new libsignal.SignalProtocolAddress(jid, id);
+                        this.session_cipher = new window.libsignal.SessionCipher(_converse.omemo_store, address);
+                    }
+                    return this.session_cipher;
+                },
 
-                    return session_cipher.encrypt(plaintext).then(payload => ({'payload': payload, 'device': device}));
+                encryptKey (plaintext, device) {
+                    return this.getSessionCipher(device.get('jid'), device.get('id'))
+                        .encrypt(plaintext)
+                        .then(payload => ({'payload': payload, 'device': device}));
                 },
 
                 addKeysToMessageStanza (stanza, dicts, iv) {
@@ -406,7 +398,7 @@
                         // long-standing SignalProtocol session.
                         const promises = devices
                             .filter(device => device.get('trusted') != UNTRUSTED)
-                            .map(device => this.encryptKey(obj.key_and_tag, device));
+                            .map(device => this.encryptKey(u.arrayBufferToString(obj.key_and_tag), device));
 
                         return Promise.all(promises)
                             .then(dicts => this.addKeysToMessageStanza(stanza, dicts, obj.iv))

+ 18 - 7
src/utils/core.js

@@ -916,23 +916,34 @@
         return fp;
     };
 
+    u.appendArrayBuffer = function (buffer1, buffer2) {
+        const tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
+        tmp.set(new Uint8Array(buffer1), 0);
+        tmp.set(new Uint8Array(buffer2), buffer1.byteLength);
+        return tmp.buffer;
+    };
+
     u.arrayBufferToHex = function (ab) {
         // https://stackoverflow.com/questions/40031688/javascript-arraybuffer-to-hex#40031979
         return Array.prototype.map.call(new Uint8Array(ab), x => ('00' + x.toString(16)).slice(-2)).join('');
     };
 
     u.arrayBufferToString = function (ab) {
-        const enc = new TextDecoder("utf-8");
-        return enc.decode(ab);
+        return (new Uint8Array(ab)).reduce((data, byte) => data + String.fromCharCode(byte), '');
     };
 
-    u.arrayBufferToBase64 = function (ab) {
-        return btoa((new Uint8Array(ab)).reduce((data, byte) => data + String.fromCharCode(byte), ''));
+    u.stringToArrayBuffer = function (string) {
+        const len = string.length,
+              bytes = new Uint8Array(len);
+
+        for (let i = 0; i < len; i++) {
+            bytes[i] = string.charCodeAt(i)
+        }
+        return bytes.buffer
     };
 
-    u.stringToArrayBuffer = function (string) {
-        const enc = new TextEncoder(); // always utf-8
-        return enc.encode(string);
+    u.arrayBufferToBase64 = function (ab) {
+        return btoa((new Uint8Array(ab)).reduce((data, byte) => data + String.fromCharCode(byte), ''));
     };
 
     u.base64ToArrayBuffer = function (b64) {

+ 0 - 1
tests/mock.js

@@ -22,7 +22,6 @@
                 'registrationId': '1337' 
             });
             this.decryptPreKeyWhisperMessage = (key_and_tag) => {
-                // TODO: remove the prekey
                 return Promise.resolve(u.stringToArrayBuffer(key_and_tag));
             };