Procházet zdrojové kódy

Working example of AES-GCM encryption and decryption

with key import and export.

updates #497
JC Brand před 7 roky
rodič
revize
713f49453f
4 změnil soubory, kde provedl 72 přidání a 67 odebrání
  1. 1 0
      .eslintrc.json
  2. 26 27
      dist/converse.js
  3. 10 5
      spec/omemo.js
  4. 35 35
      src/converse-omemo.js

+ 1 - 0
.eslintrc.json

@@ -9,6 +9,7 @@
     "plugins": ["lodash"],
     "plugins": ["lodash"],
     "extends": ["eslint:recommended", "plugin:lodash/canonical"],
     "extends": ["eslint:recommended", "plugin:lodash/canonical"],
     "globals": {
     "globals": {
+        "Uint8Array": true,
         "Promise": true,
         "Promise": true,
         "converse": true,
         "converse": true,
         "define": true,
         "define": true,

+ 26 - 27
dist/converse.js

@@ -73292,6 +73292,11 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
   const UNDECIDED = 0;
   const UNDECIDED = 0;
   const TRUSTED = 1;
   const TRUSTED = 1;
   const UNTRUSTED = -1;
   const UNTRUSTED = -1;
+  const TAG_LENGTH = 128;
+  const KEY_ALGO = {
+    'name': "AES-GCM",
+    'length': 256
+  };
 
 
   function parseBundle(bundle_el) {
   function parseBundle(bundle_el) {
     /* Given an XML element representing a user's OMEMO bundle, parse it
     /* Given an XML element representing a user's OMEMO bundle, parse it
@@ -73396,25 +73401,23 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
           };
           };
         },
         },
 
 
-        decryptMessage(key_and_tag, attrs) {
-          const aes_data = this.getKeyAndTag(u.arrayBufferToString(key_and_tag));
-          const CryptoKeyObject = {
+        decryptMessage(obj) {
+          const _converse = this.__super__._converse,
+                key_obj = {
             "alg": "A256GCM",
             "alg": "A256GCM",
             "ext": true,
             "ext": true,
-            "k": aes_data.key,
+            "k": obj.key,
             "key_ops": ["encrypt", "decrypt"],
             "key_ops": ["encrypt", "decrypt"],
             "kty": "oct"
             "kty": "oct"
           };
           };
-          return crypto.subtle.importKey('jwk', CryptoKeyObject, 'AES-GCM', true, ['encrypt', 'decrypt']).then(key_obj => {
-            return window.crypto.subtle.decrypt({
+          return crypto.subtle.importKey('jwk', key_obj, KEY_ALGO, true, ['encrypt', 'decrypt']).then(key_obj => {
+            const algo = {
               'name': "AES-GCM",
               '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);
-          });
+              'iv': u.base64ToArrayBuffer(obj.iv),
+              'tagLength': TAG_LENGTH
+            };
+            return window.crypto.subtle.decrypt(algo, key_obj, u.base64ToArrayBuffer(obj.payload));
+          }).then(out => new TextDecoder().decode(out)).catch(e => _converse.log(e.toString(), Strophe.LogLevel.ERROR));
         },
         },
 
 
         decrypt(attrs) {
         decrypt(attrs) {
@@ -73455,7 +73458,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
                   const address = new libsignal.SignalProtocolAddress(attrs.from, attrs.encrypted.device_id),
                   const address = new libsignal.SignalProtocolAddress(attrs.from, attrs.encrypted.device_id),
                         session_cipher = new window.libsignal.SessionCipher(_converse.omemo_store, address),
                         session_cipher = new window.libsignal.SessionCipher(_converse.omemo_store, address),
                         libsignal_payload = JSON.parse(atob(attrs.encrypted.key));
                         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 => {
+                  session_cipher.decryptPreKeyWhisperMessage(libsignal_payload.body, 'binary').then(key_and_tag => this.decryptMessage(attrs.encrypted)).then(f => {
                     // TODO handle new key...
                     // TODO handle new key...
                     // _converse.omemo.publishBundle()
                     // _converse.omemo.publishBundle()
                     resolve(f);
                     resolve(f);
@@ -73490,13 +73493,9 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
         encryptMessage(plaintext) {
         encryptMessage(plaintext) {
           // The client MUST use fresh, randomly generated key/IV pairs
           // The client MUST use fresh, randomly generated key/IV pairs
           // with AES-128 in Galois/Counter Mode (GCM).
           // with AES-128 in Galois/Counter Mode (GCM).
-          const TAG_LENGTH = 128,
-                iv = window.crypto.getRandomValues(new window.Uint8Array(16));
+          const iv = window.crypto.getRandomValues(new window.Uint8Array(16));
           let key;
           let key;
-          return window.crypto.subtle.generateKey({
-            'name': "AES-GCM",
-            'length': 256
-          }, true, // extractable
+          return window.crypto.subtle.generateKey(KEY_ALGO, true, // extractable
           ["encrypt", "decrypt"] // key usages
           ["encrypt", "decrypt"] // key usages
           ).then(result => {
           ).then(result => {
             key = result;
             key = result;
@@ -73509,10 +73508,10 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
           }).then(ciphertext => {
           }).then(ciphertext => {
             return window.crypto.subtle.exportKey("jwk", key).then(key_obj => {
             return window.crypto.subtle.exportKey("jwk", key).then(key_obj => {
               return Promise.resolve({
               return Promise.resolve({
-                'key_str': key_obj.k,
-                'tag': btoa(ciphertext.slice(ciphertext.byteLength - (TAG_LENGTH + 7 >> 3))),
-                'ciphertext': btoa(ciphertext),
-                'iv': btoa(iv)
+                'key': key_obj.k,
+                'tag': u.arrayBufferToBase64(ciphertext.slice(ciphertext.byteLength - (TAG_LENGTH + 7 >> 3))),
+                'payload': u.arrayBufferToBase64(ciphertext),
+                'iv': u.arrayBufferToBase64(iv)
               });
               });
             });
             });
           });
           });
@@ -73579,15 +73578,15 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
           }).c('header', {
           }).c('header', {
             'sid': _converse.omemo_store.get('device_id')
             'sid': _converse.omemo_store.get('device_id')
           });
           });
-          return this.encryptMessage(message).then(payload => {
+          return this.encryptMessage(message).then(obj => {
             // The 16 bytes key and the GCM authentication tag (The tag
             // The 16 bytes key and the GCM authentication tag (The tag
             // SHOULD have at least 128 bit) are concatenated and for each
             // SHOULD have at least 128 bit) are concatenated and for each
             // intended recipient device, i.e. both own devices as well as
             // intended recipient device, i.e. both own devices as well as
             // devices associated with the contact, the result of this
             // devices associated with the contact, the result of this
             // concatenation is encrypted using the corresponding
             // concatenation is encrypted using the corresponding
             // long-standing SignalProtocol session.
             // long-standing SignalProtocol session.
-            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));
+            const promises = devices.filter(device => device.get('trusted') != UNTRUSTED).map(device => this.encryptKey(obj.key + obj.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));
           });
           });
         },
         },
 
 

+ 10 - 5
spec/omemo.js

@@ -15,16 +15,21 @@
                     null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
                     null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
                     function (done, _converse) {
                     function (done, _converse) {
 
 
-            let iq_stanza, view, sent_stanza;
+            const message = 'This message will be encrypted'
+            let view;
             test_utils.createContacts(_converse, 'current', 1);
             test_utils.createContacts(_converse, 'current', 1);
             _converse.emit('rosterContactsFetched');
             _converse.emit('rosterContactsFetched');
             const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
             const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
             test_utils.openChatBoxFor(_converse, contact_jid)
             test_utils.openChatBoxFor(_converse, contact_jid)
-            .then((view) => view.model.encryptMessage('This message will be encrypted'))
-            .then((payload) => {
-                debugger;
+            .then((v) => {
+                view = v;
+                return view.model.encryptMessage(message);
+            }).then((payload) => {
                 return view.model.decryptMessage(payload);
                 return view.model.decryptMessage(payload);
-            }).then(done);
+            }).then((result) => {
+                expect(result).toBe(message);
+                done();
+            });
         }));
         }));
 
 
 
 

+ 35 - 35
src/converse-omemo.js

@@ -25,6 +25,11 @@
     const UNDECIDED = 0;
     const UNDECIDED = 0;
     const TRUSTED = 1;
     const TRUSTED = 1;
     const UNTRUSTED = -1;
     const UNTRUSTED = -1;
+    const TAG_LENGTH = 128;
+    const KEY_ALGO = {
+        'name': "AES-GCM",
+        'length': 256
+    };
 
 
 
 
     function parseBundle (bundle_el) {
     function parseBundle (bundle_el) {
@@ -135,26 +140,25 @@
                     }
                     }
                 },
                 },
 
 
-                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'])
+                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) => {
                         .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)
-                        })
+                            const algo = {
+                                'name': "AES-GCM",
+                                'iv': u.base64ToArrayBuffer(obj.iv),
+                                'tagLength': TAG_LENGTH 
+                            }
+                            return window.crypto.subtle.decrypt(algo, key_obj, u.base64ToArrayBuffer(obj.payload));
+                        }).then(out => (new TextDecoder()).decode(out))
+                          .catch(e => _converse.log(e.toString(), Strophe.LogLevel.ERROR));
                 },
                 },
 
 
                 decrypt (attrs) {
                 decrypt (attrs) {
@@ -200,7 +204,7 @@
                                           libsignal_payload = JSON.parse(atob(attrs.encrypted.key));
                                           libsignal_payload = JSON.parse(atob(attrs.encrypted.key));
 
 
                                     session_cipher.decryptPreKeyWhisperMessage(libsignal_payload.body, 'binary')
                                     session_cipher.decryptPreKeyWhisperMessage(libsignal_payload.body, 'binary')
-                                    .then(key_and_tag => this.decryptMessage(key_and_tag, attrs.encrypted))
+                                    .then(key_and_tag => this.decryptMessage(attrs.encrypted))
                                     .then((f) => {
                                     .then((f) => {
                                         // TODO handle new key...
                                         // TODO handle new key...
                                         // _converse.omemo.publishBundle()
                                         // _converse.omemo.publishBundle()
@@ -236,14 +240,10 @@
                 encryptMessage (plaintext) {
                 encryptMessage (plaintext) {
                     // The client MUST use fresh, randomly generated key/IV pairs
                     // The client MUST use fresh, randomly generated key/IV pairs
                     // with AES-128 in Galois/Counter Mode (GCM).
                     // with AES-128 in Galois/Counter Mode (GCM).
-                    const TAG_LENGTH = 128,
-                          iv = window.crypto.getRandomValues(new window.Uint8Array(16));
-
+                    const iv = window.crypto.getRandomValues(new window.Uint8Array(16));
                     let key;
                     let key;
-                    return window.crypto.subtle.generateKey({
-                            'name': "AES-GCM",
-                            'length': 256
-                        },
+                    return window.crypto.subtle.generateKey(
+                        KEY_ALGO,
                         true, // extractable
                         true, // extractable
                         ["encrypt", "decrypt"] // key usages
                         ["encrypt", "decrypt"] // key usages
                     ).then((result) => {
                     ).then((result) => {
@@ -258,10 +258,10 @@
                         return window.crypto.subtle.exportKey("jwk", key)
                         return window.crypto.subtle.exportKey("jwk", key)
                             .then((key_obj) => {
                             .then((key_obj) => {
                                 return Promise.resolve({
                                 return Promise.resolve({
-                                    'key_str': key_obj.k,
-                                    'tag': btoa(ciphertext.slice(ciphertext.byteLength - ((TAG_LENGTH + 7) >> 3))),
-                                    'ciphertext': btoa(ciphertext),
-                                    'iv': btoa(iv)
+                                    'key': key_obj.k,
+                                    'tag': u.arrayBufferToBase64(ciphertext.slice(ciphertext.byteLength - ((TAG_LENGTH + 7) >> 3))),
+                                    'payload': u.arrayBufferToBase64(ciphertext),
+                                    'iv': u.arrayBufferToBase64(iv)
                                 });
                                 });
                             });
                             });
                     });
                     });
@@ -319,7 +319,7 @@
                             .c('encrypted', {'xmlns': Strophe.NS.OMEMO})
                             .c('encrypted', {'xmlns': Strophe.NS.OMEMO})
                                 .c('header', {'sid':  _converse.omemo_store.get('device_id')});
                                 .c('header', {'sid':  _converse.omemo_store.get('device_id')});
 
 
-                    return this.encryptMessage(message).then((payload) => {
+                    return this.encryptMessage(message).then((obj) => {
                         // The 16 bytes key and the GCM authentication tag (The tag
                         // The 16 bytes key and the GCM authentication tag (The tag
                         // SHOULD have at least 128 bit) are concatenated and for each
                         // SHOULD have at least 128 bit) are concatenated and for each
                         // intended recipient device, i.e. both own devices as well as
                         // intended recipient device, i.e. both own devices as well as
@@ -328,11 +328,11 @@
                         // long-standing SignalProtocol session.
                         // long-standing SignalProtocol session.
                         const promises = devices
                         const promises = devices
                             .filter(device => device.get('trusted') != UNTRUSTED)
                             .filter(device => device.get('trusted') != UNTRUSTED)
-                            .map(device => this.encryptKey(payload.key_str+payload.tag, device));
+                            .map(device => this.encryptKey(obj.key+obj.tag, device));
 
 
                         return Promise.all(promises)
                         return Promise.all(promises)
-                            .then((dicts) => this.addKeysToMessageStanza(stanza, dicts, payload.iv))
-                            .then((stanza) => stanza.c('payload').t(payload.ciphertext))
+                            .then((dicts) => this.addKeysToMessageStanza(stanza, dicts, obj.iv))
+                            .then((stanza) => stanza.c('payload').t(obj.payload))
                             .catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
                             .catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
                     });
                     });
                 },
                 },