js: change chunksize handling and decoding
authorMaximilian Krambach <maximilian.krambach@intevation.de>
Fri, 8 Jun 2018 15:54:58 +0000 (17:54 +0200)
committerMaximilian Krambach <maximilian.krambach@intevation.de>
Fri, 8 Jun 2018 15:54:58 +0000 (17:54 +0200)
--

* the nativeApp now sends all data in one base64-encoded string, which
  needs reassembly, but in a much easier way now.

* there are some new performance problems now, especially with
  decrypting data

lang/js/BrowserTestExtension/tests/encryptDecryptTest.js
lang/js/BrowserTestExtension/tests/longRunningTests.js
lang/js/src/Connection.js
lang/js/src/Message.js
lang/js/src/gpgmejs.js
lang/js/src/permittedOperations.js
lang/js/unittests.js

index a84be27..bd72c1d 100644 (file)
@@ -152,7 +152,7 @@ describe('Encryption and Decryption', function () {
         let prm = Gpgmejs.init();
         prm.then(function (context) {
             context.encrypt(b64data,
-                inputvalues.encrypt.good.fingerprint).then(
+                inputvalues.encrypt.good.fingerprint, true).then(
                 function (answer) {
                     expect(answer).to.not.be.empty;
                     expect(answer.data).to.be.a('string');
@@ -185,33 +185,7 @@ describe('Encryption and Decryption', function () {
                         'BEGIN PGP MESSAGE');
                     expect(answer.data).to.include(
                         'END PGP MESSAGE');
-                    context.decrypt(answer.data, true).then(
-                        function (result) {
-                            expect(result).to.not.be.empty;
-                            expect(result.data).to.be.a('string');
-                            expect(result.data).to.equal(b64data);
-                            done();
-                        });
-                });
-        });
-    }).timeout(3000);
-
-    it('Random data, input and output as base64', function (done) {
-        let data = bigBoringString(0.0001);
-        let b64data = btoa(data);
-        let prm = Gpgmejs.init();
-        prm.then(function (context) {
-            context.encrypt(b64data,
-                inputvalues.encrypt.good.fingerprint).then(
-                function (answer) {
-                    expect(answer).to.not.be.empty;
-                    expect(answer.data).to.be.a('string');
-
-                    expect(answer.data).to.include(
-                        'BEGIN PGP MESSAGE');
-                    expect(answer.data).to.include(
-                        'END PGP MESSAGE');
-                    context.decrypt(answer.data, true).then(
+                    context.decrypt(answer.data).then(
                         function (result) {
                             expect(result).to.not.be.empty;
                             expect(result.data).to.be.a('string');
@@ -222,5 +196,4 @@ describe('Encryption and Decryption', function () {
         });
     }).timeout(3000);
 
-
 });
index eefe126..e148d1c 100644 (file)
@@ -24,7 +24,7 @@
 /* global bigString, inputvalues */
 
 describe('Long running Encryption/Decryption', function () {
-    for (let i=0; i < 100; i++) {
+    for (let i=0; i < 101; i++) {
         it('Successful encrypt/decrypt completely random data ' +
             (i+1) + '/100', function (done) {
             let prm = Gpgmejs.init();
@@ -43,30 +43,32 @@ describe('Long running Encryption/Decryption', function () {
                             function(result){
                                 expect(result).to.not.be.empty;
                                 expect(result.data).to.be.a('string');
+                                /*
                                 if (result.data.length !== data.length) {
-                                    // console.log('diff: ' +
-                                    // (result.data.length - data.length));
+                                    console.log('diff: ' +
+                                    (result.data.length - data.length));
                                     for (let i=0; i < result.data.length; i++){
                                         if (result.data[i] !== data[i]){
-                                            // console.log('position: ' + i);
-                                            // console.log('result : ' +
-                                            // result.data.charCodeAt(i) +
-                                            // result.data[i-2] +
-                                            // result.data[i-1] +
-                                            // result.data[i] +
-                                            // result.data[i+1] +
-                                            // result.data[i+2]);
-                                            // console.log('original: ' +
-                                            // data.charCodeAt(i) +
-                                            // data[i-2] +
-                                            // data[i-1] +
-                                            // data[i] +
-                                            // data[i+1] +
-                                            // data[i+2]);
+                                            console.log('position: ' + i);
+                                            console.log('result : ' +
+                                            result.data.charCodeAt(i) +
+                                            result.data[i-2] +
+                                            result.data[i-1] +
+                                            result.data[i] +
+                                            result.data[i+1] +
+                                            result.data[i+2]);
+                                            console.log('original: ' +
+                                            data.charCodeAt(i) +
+                                            data[i-2] +
+                                            data[i-1] +
+                                            data[i] +
+                                            data[i+1] +
+                                            data[i+2]);
                                             break;
                                         }
                                     }
                                 }
+                                */
                                 expect(result.data).to.equal(data);
                                 done();
                             });
index e9c0b21..f399b22 100644 (file)
@@ -108,6 +108,7 @@ export class Connection{
             return Promise.reject(gpgme_error('MSG_INCOMPLETE'));
         }
         let me = this;
+        let chunksize = message.chunksize;
         return new Promise(function(resolve, reject){
             let answer = new Answer(message);
             let listener = function(msg) {
@@ -115,22 +116,27 @@ export class Connection{
                     me._connection.onMessage.removeListener(listener);
                     me._connection.disconnect();
                     reject(gpgme_error('CONN_EMPTY_GPG_ANSWER'));
-                } else if (msg.type === 'error'){
-                    me._connection.onMessage.removeListener(listener);
-                    me._connection.disconnect();
-                    reject(gpgme_error('GNUPG_ERROR', msg.msg));
                 } else {
-                    let answer_result = answer.add(msg);
+                    let answer_result = answer.collect(msg);
                     if (answer_result !== true){
                         me._connection.onMessage.removeListener(listener);
                         me._connection.disconnect();
                         reject(answer_result);
-                    } else if (msg.more === true){
-                        me._connection.postMessage({'op': 'getmore'});
                     } else {
-                        me._connection.onMessage.removeListener(listener);
-                        me._connection.disconnect();
-                        resolve(answer.message);
+                        if (msg.more === true){
+                            me._connection.postMessage({
+                                'op': 'getmore',
+                                'chunksize': chunksize
+                            });
+                        } else {
+                            me._connection.onMessage.removeListener(listener);
+                            me._connection.disconnect();
+                            if (answer.message instanceof Error){
+                                reject(answer.message);
+                            } else {
+                                resolve(answer.message);
+                            }
+                        }
                     }
                 }
             };
@@ -170,19 +176,32 @@ class Answer{
 
     constructor(message){
         this.operation = message.operation;
-        this.expected = message.expected;
+        this.expect = message.expect;
     }
 
-    /**
-     * Add the information to the answer
-     * @param {Object} msg The message as received with nativeMessaging
-     * returns true if successfull, gpgme_error otherwise
-     */
-    add(msg){
-        if (this._response === undefined){
-            this._response = {};
+    collect(msg){
+        if (typeof(msg) !== 'object' || !msg.hasOwnProperty('response')) {
+            return gpgme_error('CONN_UNEXPECTED_ANSWER');
+        }
+        if (this._responseb64 === undefined){
+            //this._responseb64 = [msg.response];
+            this._responseb64 = msg.response;
+            return true;
+        } else {
+            //this._responseb64.push(msg.response);
+            this._responseb64 += msg.response;
+            return true;
         }
-        let messageKeys = Object.keys(msg);
+    }
+
+    get message(){
+        if (this._responseb64 === undefined){
+            return gpgme_error('CONN_UNEXPECTED_ANSWER');
+        }
+        // let _decodedResponse = JSON.parse(atob(this._responseb64.join('')));
+        let _decodedResponse = JSON.parse(atob(this._responseb64));
+        let _response = {};
+        let messageKeys = Object.keys(_decodedResponse);
         let poa = permittedOperations[this.operation].answer;
         if (messageKeys.length === 0){
             return gpgme_error('CONN_UNEXPECTED_ANSWER');
@@ -191,80 +210,42 @@ class Answer{
             let key = messageKeys[i];
             switch (key) {
             case 'type':
-                if ( msg.type !== 'error' && poa.type.indexOf(msg.type) < 0){
+                if (_decodedResponse.type === 'error'){
+                    return (gpgme_error('GNUPG_ERROR', _decodedResponse.msg));
+                } else if (poa.type.indexOf(_decodedResponse.type) < 0){
                     return gpgme_error('CONN_UNEXPECTED_ANSWER');
                 }
                 break;
-            case 'more':
+            case 'base64':
                 break;
-            default:
-                //data should be concatenated
-                if (poa.data.indexOf(key) >= 0){
-                    if (!this._response.hasOwnProperty(key)){
-                        this._response[key] = '';
-                    }
-                    this._response[key] += msg[key];
-                }
-                //params should not change through the message
-                else if (poa.params.indexOf(key) >= 0){
-                    if (!this._response.hasOwnProperty(key)){
-                        this._response[key] = msg[key];
-                    }
-                    else if (this._response[key] !== msg[key]){
-                        return gpgme_error('CONN_UNEXPECTED_ANSWER',msg[key]);
-                    }
+            case 'msg':
+                if (_decodedResponse.type === 'error'){
+                    return (gpgme_error('GNUPG_ERROR', _decodedResponse.msg));
                 }
-                //infos may be json objects etc. Not yet defined.
-                // Pushing them into arrays for now
-                else if (poa.infos.indexOf(key) >= 0){
-                    if (!this._response.hasOwnProperty(key)){
-                        this._response[key] = [];
-                    }
-
-                    if (Array.isArray(msg[key])) {
-                        for (let i=0; i< msg[key].length; i++) {
-                            this._response[key].push(msg[key][i]);
-                        }
-                    } else {
-                        this._response[key].push(msg[key]);
-                    }
+                break;
+            default:
+                if (!poa.data.hasOwnProperty(key)){
+                    return gpgme_error('CONN_UNEXPECTED_ANSWER');
                 }
-                else {
+                if( typeof(_decodedResponse[key]) !== poa.data[key] ){
                     return gpgme_error('CONN_UNEXPECTED_ANSWER');
                 }
-                break;
-            }
-        }
-        return true;
-    }
-
-    /**
-     * @returns {Object} the assembled message, original data assumed to be
-     * (javascript-) strings
-     */
-    get message(){
-        let keys = Object.keys(this._response);
-        let msg = {};
-        let poa = permittedOperations[this.operation].answer;
-        for (let i=0; i < keys.length; i++) {
-            if (poa.data.indexOf(keys[i]) >= 0
-                && this._response.base64 === true
-            ) {
-                msg[keys[i]] = atob(this._response[keys[i]]);
-                if (this.expected === 'base64'){
-                    msg[keys[i]] = this._response[keys[i]];
-                } else {
-                    msg[keys[i]] = decodeURIComponent(
-                        atob(this._response[keys[i]]).split('').map(
+                if (_decodedResponse.base64 === true
+                    && poa.data[key] === 'string'
+                    && this.expect === undefined
+                ){
+                    _response[key] = decodeURIComponent(
+                        atob(_decodedResponse[key]).split('').map(
                             function(c) {
                                 return '%' +
-                                ('00' + c.charCodeAt(0).toString(16)).slice(-2);
+                            ('00' + c.charCodeAt(0).toString(16)).slice(-2);
                             }).join(''));
+                } else {
+                    _response[key] = _decodedResponse[key];
                 }
-            } else {
-                msg[keys[i]] = this._response[keys[i]];
+                break;
             }
         }
-        return msg;
+        return _response;
     }
 }
index 0ddda6c..7ccf7ef 100644 (file)
@@ -46,7 +46,6 @@ export class GPGME_Message {
 
     constructor(operation){
         this.operation = operation;
-        this._expected = 'string';
     }
 
     set operation (op){
@@ -59,24 +58,50 @@ export class GPGME_Message {
             }
         }
     }
-
     get operation(){
         return this._msg.op;
     }
 
-    set expected(string){
-        if (string === 'base64'){
-            this._expected = 'base64';
+    /**
+     * Set the maximum size of responses from gpgme in bytes. Values allowed
+     * range from 10kB to 1MB. The lower limit is arbitrary, the upper limit
+     * fixed by browsers' nativeMessaging specifications
+     */
+    set chunksize(value){
+        if (
+            Number.isInteger(value) &&
+            value > 10 * 1024 &&
+            value <= 1024 * 1024
+        ){
+            this._chunksize = value;
+        }
+    }
+    get chunksize(){
+        if (this._chunksize === undefined){
+            return 1024 * 1023;
+        } else {
+            return this._chunksize;
         }
     }
 
-    get expected() {
-        if (this._expected === 'base64'){
-            return this._expected;
+    /**
+     * If expect is set to 'base64', the response is expected to be base64
+     * encoded binary
+     */
+    set expect(value){
+        if (value ==='base64'){
+            this._expect = value;
+        }
+    }
+
+    get expect(){
+        if ( this._expect === 'base64'){
+            return this._expect;
         }
-        return 'string';
+        return undefined;
     }
 
+
     /**
      * Sets a parameter for the message. Note that the operation has to be set
      * first, to be able to check if the parameter is permittted
@@ -188,6 +213,7 @@ export class GPGME_Message {
      */
     get message(){
         if (this.isComplete === true){
+            this._msg.chunksize = this.chunksize;
             return this._msg;
         }
         else {
@@ -201,10 +227,13 @@ export class GPGME_Message {
         return new Promise(function(resolve, reject) {
             if (me.isComplete === true) {
                 let conn  = new Connection;
+                if (me._msg.chunksize === undefined){
+                    me._msg.chunksize = 1023*1024;
+                }
                 conn.post(me).then(function(response) {
                     resolve(response);
                 }, function(reason) {
-                    reject(gpgme_error('GNUPG_ERROR', reason));
+                    reject(reason);
                 });
             }
             else {
index cbad902..09bca7f 100644 (file)
@@ -57,8 +57,8 @@ export class GpgME {
      * Keys used to encrypt the message
      * @param  {GPGME_Key|String|Array<String>|Array<GPGME_Key>} secretKeys
      * (optional) Keys used to sign the message
-     * @param {Boolean} base64 (optional) The data is already considered to be
-     * in base64 encoding
+     * @param {Boolean} base64 (optional) The data will be interpreted as
+     * base64 encoded data
      * @param {Boolean} armor (optional) Request the output as armored block
      * @param {Boolean} wildcard (optional) If true, recipient information will
      *  not be added to the message
@@ -109,24 +109,20 @@ export class GpgME {
     * Decrypt a Message
     * @param {String|Object} data text/data to be decrypted. Accepts Strings
     *  and Objects with a getText method
-    * @param {Boolean} base64 (optional) Response is expected to be base64
-    * encoded
     * @returns {Promise<Object>} decrypted message:
-        data:   The decrypted data.  This may be base64 encoded.
+        data:   The decrypted data.
         base64: Boolean indicating whether data is base64 encoded.
         mime:   A Boolean indicating whether the data is a MIME object.
         signatures: Array of signature Objects TODO not yet implemented.
-            // should be an object that can tell if all signatures are valid .
+            // should be an object that can tell if all signatures are valid.
     * @async
     */
-    decrypt(data, base64=false){
+    decrypt(data){
         if (data === undefined){
             return Promise.reject(gpgme_error('MSG_EMPTY'));
         }
         let msg = createMessage('decrypt');
-        if (base64 === true){
-            msg.expected = 'base64';
-        }
+
         if (msg instanceof Error){
             return Promise.reject(msg);
         }
@@ -165,10 +161,10 @@ export class GpgME {
         }
         msg.setParameter('mode', mode);
         putData(msg, data);
-        if (mode === 'detached') {
-            msg.expected = 'base64';
-        }
         return new Promise(function(resolve,reject) {
+            if (mode ==='detached'){
+                msg.expect= 'base64';
+            }
             msg.post().then( function(message) {
                 if (mode === 'clearsign'){
                     resolve({
index 445a40c..6ac33af 100644 (file)
                 5000 ms would be too short
       answer: <Object>
           type: <String< The content type of answer expected
-          data: Array<String> The payload property of the answer. May be
-                partial and in need of concatenation
-          params: Array<String> Information that do not change throughout
-                the message
-          infos: Array<*> arbitrary information that may result in a list
+          data: <Object>
+            the properties expected and their type, eg: {'data':'string'}
       }
   }
 */
@@ -67,9 +64,6 @@ export const permittedOperations = {
                 allowed: ['string'],
                 array_allowed: true
             },
-            'chunksize': {
-                allowed: ['number']
-            },
             'base64': {
                 allowed: ['boolean']
             },
@@ -101,9 +95,10 @@ export const permittedOperations = {
         },
         answer: {
             type: ['ciphertext'],
-            data: ['data'],
-            params: ['base64'],
-            infos: []
+            data: {
+                'data': 'string',
+                'base64':'boolean'
+            }
         }
     },
 
@@ -119,18 +114,18 @@ export const permittedOperations = {
                 allowed: ['string'],
                 allowed_data: ['cms', 'openpgp']
             },
-            'chunksize': {
-                allowed: ['number'],
-            },
             'base64': {
                 allowed: ['boolean']
             }
         },
         answer: {
             type: ['plaintext'],
-            data: ['data'],
-            params: ['base64', 'mime'],
-            infos: ['signatures']
+            data: {
+                'data': 'string',
+                'base64': 'boolean',
+                'mime': 'boolean',
+                'signatures': 'object'
+            }
         }
     },
 
@@ -149,9 +144,6 @@ export const permittedOperations = {
                 allowed: ['string'],
                 allowed_data: ['cms', 'openpgp']
             },
-            'chunksize': {
-                allowed: ['number'],
-            },
             'sender': {
                 allowed: ['string'],
             },
@@ -169,10 +161,11 @@ export const permittedOperations = {
         },
         answer: {
             type: ['signature', 'ciphertext'],
-            data: ['data'], // Unless armor mode is used a Base64 encoded binary
-            // signature.  In armor mode a string with an armored
-            // OpenPGP or a PEM message.
-            params: ['base64']
+            data: {
+                'data': 'string',
+                'base64':'boolean'
+            }
+
         }
     },
 
@@ -186,9 +179,6 @@ export const permittedOperations = {
                 allowed: ['string'],
                 allowed_data: ['cms', 'openpgp']
             },
-            'chunksize': {
-                allowed: ['number'],
-            },
             'secret': {
                 allowed: ['boolean']
             },
@@ -220,9 +210,10 @@ export const permittedOperations = {
         },
         answer: {
             type: ['keys'],
-            data: [],
-            params: ['base64'],
-            infos: ['keys']
+            data: {
+                'base64': 'boolean',
+                'keys': 'object'
+            }
         }
     },
 
@@ -233,9 +224,6 @@ export const permittedOperations = {
                 allowed: ['string'],
                 allowed_data: ['cms', 'openpgp']
             },
-            'chunksize': {
-                allowed: ['number'],
-            },
             'keys': {
                 allowed: ['string'],
                 array_allowed: true
@@ -259,8 +247,10 @@ export const permittedOperations = {
         },
         answer: {
             type: ['keys'],
-            data: ['data'],
-            params: ['base64']
+            data: {
+                'data': 'string',
+                'base64': 'boolean'
+            }
         }
     },
 
@@ -280,10 +270,10 @@ export const permittedOperations = {
             },
         },
         answer: {
-            infos: ['result'],
             type: [],
-            data: [],
-            params: []
+            data: {
+                'result': 'Object'
+            }
         }
     },
 
@@ -299,15 +289,15 @@ export const permittedOperations = {
                 allowed: ['string'],
                 allowed_data: ['cms', 'openpgp']
             },
-            // 'secret': { not yet implemented
+            // 'secret': { not implemented
             //     allowed: ['boolean']
             // }
 
         },
         answer: {
-            data: [],
-            params:['success'],
-            infos: []
+            data: {
+                'success': 'boolean'
+            }
         }
     },
 
@@ -316,9 +306,10 @@ export const permittedOperations = {
         optional: {},
         answer: {
             type:  [''],
-            data: ['gpgme'],
-            infos: ['info'],
-            params:[]
+            data: {
+                'gpgme': 'string',
+                'info': 'object'
+            }
         }
     }
 
index ce1dd0c..169e8eb 100644 (file)
@@ -339,7 +339,8 @@ function unittests (){
             test0.setParameter('keys', hp.validFingerprints);
 
             expect(test0.message).to.not.be.null;
-            expect(test0.message).to.have.keys('op', 'data', 'keys');
+            expect(test0.message).to.have.keys('op', 'data', 'keys',
+                'chunksize');
             expect(test0.message.op).to.equal('encrypt');
             expect(test0.message.data).to.equal(
                 mp.valid_encrypt_data);