diff --git a/lang/js/BrowserTestExtension/tests/encryptDecryptTest.js b/lang/js/BrowserTestExtension/tests/encryptDecryptTest.js index a84be27c..bd72c1d2 100644 --- a/lang/js/BrowserTestExtension/tests/encryptDecryptTest.js +++ b/lang/js/BrowserTestExtension/tests/encryptDecryptTest.js @@ -1,226 +1,199 @@ /* gpgme.js - Javascript integration for gpgme * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik * * This file is part of GPGME. * * GPGME is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * GPGME is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this program; if not, see . * SPDX-License-Identifier: LGPL-2.1+ * * Author(s): * Maximilian Krambach */ /* global describe, it, expect, Gpgmejs */ /* global inputvalues, encryptedData, bigString, bigBoringString */ describe('Encryption and Decryption', function () { it('Successful encrypt and decrypt simple string', function (done) { let prm = Gpgmejs.init(); prm.then(function (context) { context.encrypt( inputvalues.encrypt.good.data, 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).then(function (result) { expect(result).to.not.be.empty; expect(result.data).to.be.a('string'); expect(result.data).to.equal( inputvalues.encrypt.good.data); done(); }); }); }); }); it('Decrypt simple non-ascii', function (done) { let prm = Gpgmejs.init(); prm.then(function (context) { let data = encryptedData; context.decrypt(data).then( function (result) { expect(result).to.not.be.empty; expect(result.data).to.be.a('string'); expect(result.data).to.equal( '¡Äußerste µ€ før ñoquis@hóme! Добрый день\n'); done(); }); }); }).timeout(3000); it('Roundtrip does not destroy trailing whitespace', function (done) { let prm = Gpgmejs.init(); prm.then(function (context) { let data = 'Keks. \rKeks \n Keks \r\n'; context.encrypt(data, 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).then( function (result) { expect(result).to.not.be.empty; expect(result.data).to.be.a('string'); expect(result.data).to.equal(data); done(); }); }); }); }).timeout(5000); for (let j = 0; j < inputvalues.encrypt.good.data_nonascii_32.length; j++){ it('Roundtrip with >1MB non-ascii input meeting default chunksize (' + (j + 1) + '/' + inputvalues.encrypt.good.data_nonascii_32.length + ')', function (done) { let input = inputvalues.encrypt.good.data_nonascii_32[j]; expect(input).to.have.length(32); let prm = Gpgmejs.init(); prm.then(function (context) { let data = ''; for (let i=0; i < 34 * 1024; i++){ data += input; } context.encrypt(data, 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).then( function (result) { expect(result).to.not.be.empty; expect(result.data).to.be.a('string'); expect(result.data).to.equal(data); done(); }); }); }); }).timeout(3000); } it('Random data, as string', function (done) { let data = bigString(1000); let prm = Gpgmejs.init(); prm.then(function (context) { context.encrypt(data, 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).then( function (result) { expect(result).to.not.be.empty; expect(result.data).to.be.a('string'); expect(result.data).to.equal(data); done(); }); }); }); }).timeout(3000); it('Data, input as base64', function (done) { let data = inputvalues.encrypt.good.data; let b64data = btoa(data); 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'); expect(answer.data).to.include( 'BEGIN PGP MESSAGE'); expect(answer.data).to.include( 'END PGP MESSAGE'); context.decrypt(answer.data).then( function (result) { expect(result).to.not.be.empty; expect(result.data).to.be.a('string'); expect(data).to.equal(data); done(); }); }); }); }).timeout(3000); it('Random data, input as base64', function (done) { let data = bigBoringString(0.001); let b64data = btoa(data); let prm = Gpgmejs.init(); prm.then(function (context) { context.encrypt(b64data, inputvalues.encrypt.good.fingerprint, true).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( - 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'); expect(result.data).to.equal(b64data); done(); }); }); }); }).timeout(3000); - }); diff --git a/lang/js/BrowserTestExtension/tests/longRunningTests.js b/lang/js/BrowserTestExtension/tests/longRunningTests.js index eefe126d..e148d1cf 100644 --- a/lang/js/BrowserTestExtension/tests/longRunningTests.js +++ b/lang/js/BrowserTestExtension/tests/longRunningTests.js @@ -1,78 +1,80 @@ /* gpgme.js - Javascript integration for gpgme * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik * * This file is part of GPGME. * * GPGME is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * GPGME is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this program; if not, see . * SPDX-License-Identifier: LGPL-2.1+ * * Author(s): * Maximilian Krambach */ /* global describe, it, expect, Gpgmejs */ /* 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(); let data = bigString(2*1024*1024); prm.then(function (context) { context.encrypt(data, 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).then( 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(); }); }); }); }).timeout(8000); } }); diff --git a/lang/js/src/Connection.js b/lang/js/src/Connection.js index e9c0b213..f399b22b 100644 --- a/lang/js/src/Connection.js +++ b/lang/js/src/Connection.js @@ -1,270 +1,251 @@ /* gpgme.js - Javascript integration for gpgme * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik * * This file is part of GPGME. * * GPGME is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * GPGME is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this program; if not, see . * SPDX-License-Identifier: LGPL-2.1+ * * Author(s): * Maximilian Krambach */ /* global chrome */ import { permittedOperations } from './permittedOperations'; import { gpgme_error } from './Errors'; import { GPGME_Message, createMessage } from './Message'; /** * A Connection handles the nativeMessaging interaction. */ export class Connection{ constructor(){ this.connect(); } /** * Retrieves the information about the backend. * @param {Boolean} details (optional) If set to false, the promise will * just return a connection status * @returns {Promise} * {String} The property 'gpgme': Version number of gpgme * {Array} 'info' Further information about the backends. * Example: * "protocol": "OpenPGP", * "fname": "/usr/bin/gpg", * "version": "2.2.6", * "req_version": "1.4.0", * "homedir": "default" */ checkConnection(details = true){ if (details === true) { return this.post(createMessage('version')); } else { let me = this; return new Promise(function(resolve) { Promise.race([ me.post(createMessage('version')), new Promise(function(resolve, reject){ setTimeout(function(){ reject(gpgme_error('CONN_TIMEOUT')); }, 500); }) ]).then(function(){ // success resolve(true); }, function(){ // failure resolve(false); }); }); } } /** * Immediately closes the open port. */ disconnect() { if (this._connection){ this._connection.disconnect(); this._connection = null; } } /** * Opens a nativeMessaging port. */ connect(){ if (!this._connection){ this._connection = chrome.runtime.connectNative('gpgmejson'); } } /** * Sends a message and resolves with the answer. * @param {GPGME_Message} message * @returns {Promise} the gnupg answer, or rejection with error * information. */ post(message){ if (!message || !(message instanceof GPGME_Message)){ this.disconnect(); return Promise.reject(gpgme_error( 'PARAM_WRONG', 'Connection.post')); } if (message.isComplete !== true){ this.disconnect(); 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) { if (!msg){ 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); + } + } } } }; me._connection.onMessage.addListener(listener); if (permittedOperations[message.operation].pinentry){ return me._connection.postMessage(message.message); } else { return Promise.race([ me._connection.postMessage(message.message), function(resolve, reject){ setTimeout(function(){ me._connection.disconnect(); reject(gpgme_error('CONN_TIMEOUT')); }, 5000); }]).then(function(result){ return result; }, function(reject){ if(!(reject instanceof Error)) { me._connection.disconnect(); return gpgme_error('GNUPG_ERROR', reject); } else { return reject; } }); } }); } } /** * A class for answer objects, checking and processing the return messages of * the nativeMessaging communication. * @param {String} operation The operation, to look up validity of returning * messages */ 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'); } for (let i= 0; i < messageKeys.length; i++){ 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; } } diff --git a/lang/js/src/Message.js b/lang/js/src/Message.js index 0ddda6c4..7ccf7efc 100644 --- a/lang/js/src/Message.js +++ b/lang/js/src/Message.js @@ -1,215 +1,244 @@ /* gpgme.js - Javascript integration for gpgme * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik * * This file is part of GPGME. * * GPGME is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * GPGME is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this program; if not, see . * SPDX-License-Identifier: LGPL-2.1+ * * Author(s): * Maximilian Krambach */ import { permittedOperations } from './permittedOperations'; import { gpgme_error } from './Errors'; import { Connection } from './Connection'; export function createMessage(operation){ if (typeof(operation) !== 'string'){ return gpgme_error('PARAM_WRONG'); } if (permittedOperations.hasOwnProperty(operation)){ return new GPGME_Message(operation); } else { return gpgme_error('MSG_WRONG_OP'); } } /** * Prepares a communication request. It checks operations and parameters in * ./permittedOperations. * @param {String} operation */ export class GPGME_Message { //TODO getter constructor(operation){ this.operation = operation; - this._expected = 'string'; } set operation (op){ if (typeof(op) === 'string'){ if (!this._msg){ this._msg = {}; } if (!this._msg.op & permittedOperations.hasOwnProperty(op)){ this._msg.op = op; } } } - 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 * @param {String} param Parameter to set * @param {any} value Value to set //TODO: Some type checking * @returns {Boolean} If the parameter was set successfully */ setParameter(param,value){ if (!param || typeof(param) !== 'string'){ return gpgme_error('PARAM_WRONG'); } let po = permittedOperations[this._msg.op]; if (!po){ return gpgme_error('MSG_WRONG_OP'); } let poparam = null; if (po.required.hasOwnProperty(param)){ poparam = po.required[param]; } else if (po.optional.hasOwnProperty(param)){ poparam = po.optional[param]; } else { return gpgme_error('PARAM_WRONG'); } let checktype = function(val){ switch(typeof(val)){ case 'string': if (poparam.allowed.indexOf(typeof(val)) >= 0 && val.length > 0) { return true; } return gpgme_error('PARAM_WRONG'); case 'number': if ( poparam.allowed.indexOf('number') >= 0 && isNaN(value) === false){ return true; } return gpgme_error('PARAM_WRONG'); case 'boolean': if (poparam.allowed.indexOf('boolean') >= 0){ return true; } return gpgme_error('PARAM_WRONG'); case 'object': if (Array.isArray(val)){ if (poparam.array_allowed !== true){ return gpgme_error('PARAM_WRONG'); } for (let i=0; i < val.length; i++){ let res = checktype(val[i]); if (res !== true){ return res; } } if (val.length > 0) { return true; } } else if (val instanceof Uint8Array){ if (poparam.allowed.indexOf('Uint8Array') >= 0){ return true; } return gpgme_error('PARAM_WRONG'); } else { return gpgme_error('PARAM_WRONG'); } break; default: return gpgme_error('PARAM_WRONG'); } }; let typechecked = checktype(value); if (typechecked !== true){ return typechecked; } if (poparam.hasOwnProperty('allowed_data')){ if (poparam.allowed_data.indexOf(value) < 0){ return gpgme_error('PARAM_WRONG'); } } this._msg[param] = value; return true; } /** * Check if the message has the minimum requirements to be sent, according * to the definitions in permittedOperations * @returns {Boolean} */ get isComplete(){ if (!this._msg.op){ return false; } let reqParams = Object.keys( permittedOperations[this._msg.op].required); let msg_params = Object.keys(this._msg); for (let i=0; i < reqParams.length; i++){ if (msg_params.indexOf(reqParams[i]) < 0){ return false; } } return true; } /** * Returns the prepared message with parameters and completeness checked * @returns {Object|null} Object to be posted to gnupg, or null if * incomplete */ get message(){ if (this.isComplete === true){ + this._msg.chunksize = this.chunksize; return this._msg; } else { return null; } } post(){ let me = this; 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 { reject(gpgme_error('MSG_INCOMPLETE')); } }); } } diff --git a/lang/js/src/gpgmejs.js b/lang/js/src/gpgmejs.js index cbad9021..09bca7f9 100644 --- a/lang/js/src/gpgmejs.js +++ b/lang/js/src/gpgmejs.js @@ -1,217 +1,213 @@ /* gpgme.js - Javascript integration for gpgme * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik * * This file is part of GPGME. * * GPGME is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * GPGME is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this program; if not, see . * SPDX-License-Identifier: LGPL-2.1+ * * Author(s): * Maximilian Krambach */ import {GPGME_Message, createMessage} from './Message'; import {toKeyIdArray} from './Helpers'; import { gpgme_error } from './Errors'; import { GPGME_Keyring } from './Keyring'; export class GpgME { /** * initializes GpgME by opening a nativeMessaging port * TODO: add configuration */ constructor(config){ //TODO config not parsed this._config = config; } set Keyring(keyring){ if (keyring && keyring instanceof GPGME_Keyring){ this._Keyring = keyring; } } get Keyring(){ if (!this._Keyring){ this._Keyring = new GPGME_Keyring; } return this._Keyring; } /** * Encrypt (and optionally sign) a Message * @param {String|Object} data text/data to be encrypted as String. Also * accepts Objects with a getText method * @param {GPGME_Key|String|Array|Array} publicKeys * Keys used to encrypt the message * @param {GPGME_Key|String|Array|Array} 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 * @param {Object} additional use additional gpg options * (refer to src/permittedOperations) * @returns {Promise} Encrypted message: * data: The encrypted message * base64: Boolean indicating whether data is base64 encoded. * @async */ encrypt(data, publicKeys, secretKeys, base64=false, armor=true, wildcard=false, additional = {} ){ let msg = createMessage('encrypt'); if (msg instanceof Error){ return Promise.reject(msg); } msg.setParameter('armor', armor); msg.setParameter('always-trust', true); if (base64 === true) { msg.setParameter('base64', true); } let pubkeys = toKeyIdArray(publicKeys); msg.setParameter('keys', pubkeys); let sigkeys = toKeyIdArray(secretKeys); if (sigkeys.length > 0) { msg.setParameter('signing_keys', sigkeys); } putData(msg, data); if (wildcard === true){ msg.setParameter('throw-keyids', true); } if (additional){ let additional_Keys = Object.keys(additional); for (let k = 0; k < additional_Keys.length; k++) { msg.setParameter(additional_Keys[k], additional[additional_Keys[k]]); } } if (msg.isComplete === true){ return msg.post(); } else { return Promise.reject(gpgme_error('MSG_INCOMPLETE')); } } /** * 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} 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); } putData(msg, data); return msg.post(); } /** * Sign a Message * @param {String|Object} data text/data to be decrypted. Accepts Strings * and Objects with a gettext methos * @param {GPGME_Key|String|Array|Array} keys The * key/keys to use for signing * @param {*} mode The signing mode. Currently supported: * 'clearsign': (default) The Message is embedded into the signature * 'detached': The signature is stored separately * @param {*} base64 input is considered base64 * @returns {Promise} * data: The resulting data. Includes the signature in clearsign mode * signature: The detached signature (if in detached mode) * @async */ sign(data, keys, mode='clearsign', base64=false) { if (data === undefined){ return Promise.reject(gpgme_error('MSG_EMPTY')); } let key_arr = toKeyIdArray(keys); if (key_arr.length === 0){ return Promise.reject(gpgme_error('MSG_NO_KEYS')); } let msg = createMessage('sign'); msg.setParameter('keys', key_arr); if (base64 === true){ msg.setParameter('base64', true); } 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({ data: message.data} ); } else if (mode === 'detached') { resolve({ data: data, signature: message.data }); } }, function(error){ reject(error); }); }); } } /** * Sets the data of the message, setting flags according on the data type * @param {GPGME_Message} message The message where this data will be set * @param {*} data The data to enter */ function putData(message, data){ if (!message || !(message instanceof GPGME_Message) ) { return gpgme_error('PARAM_WRONG'); } if (!data){ return gpgme_error('PARAM_WRONG'); } else if (typeof(data) === 'string') { message.setParameter('data', data); } else if ( typeof(data) === 'object' && typeof(data.getText) === 'function' ){ let txt = data.getText(); if (typeof(txt) === 'string'){ message.setParameter('data', txt); } else { return gpgme_error('PARAM_WRONG'); } } else { return gpgme_error('PARAM_WRONG'); } } diff --git a/lang/js/src/permittedOperations.js b/lang/js/src/permittedOperations.js index 445a40cc..6ac33af9 100644 --- a/lang/js/src/permittedOperations.js +++ b/lang/js/src/permittedOperations.js @@ -1,331 +1,322 @@ /* gpgme.js - Javascript integration for gpgme * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik * * This file is part of GPGME. * * GPGME is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * GPGME is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this program; if not, see . * SPDX-License-Identifier: LGPL-2.1+ * * Author(s): * Maximilian Krambach */ /** * Definition of the possible interactions with gpgme-json. * operation: required: Array name The name of the property allowed: Array of allowed types. Currently accepted values: ['number', 'string', 'boolean', 'Uint8Array'] array_allowed: Boolean. If the value can be an array of the above allowed_data: If present, restricts to the given value optional: Array see 'required', with these parameters not being mandatory for a complete message pinentry: boolean If a pinentry dialog is expected, and a timeout of 5000 ms would be too short answer: type: The payload property of the answer. May be - partial and in need of concatenation - params: Array Information that do not change throughout - the message - infos: Array<*> arbitrary information that may result in a list + data: + the properties expected and their type, eg: {'data':'string'} } } */ export const permittedOperations = { encrypt: { pinentry: true, //TODO only with signing_keys required: { 'keys': { allowed: ['string'], array_allowed: true }, 'data': { allowed: ['string'] } }, optional: { 'protocol': { allowed: ['string'], allowed_data: ['cms', 'openpgp'] }, 'signing_keys': { allowed: ['string'], array_allowed: true }, - 'chunksize': { - allowed: ['number'] - }, 'base64': { allowed: ['boolean'] }, 'mime': { allowed: ['boolean'] }, 'armor': { allowed: ['boolean'] }, 'always-trust': { allowed: ['boolean'] }, 'no-encrypt-to': { allowed: ['string'], array_allowed: true }, 'no-compress': { allowed: ['boolean'] }, 'throw-keyids': { allowed: ['boolean'] }, 'want-address': { allowed: ['boolean'] }, 'wrap': { allowed: ['boolean'] } }, answer: { type: ['ciphertext'], - data: ['data'], - params: ['base64'], - infos: [] + data: { + 'data': 'string', + 'base64':'boolean' + } } }, decrypt: { pinentry: true, required: { 'data': { allowed: ['string'] } }, optional: { 'protocol': { 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' + } } }, sign: { pinentry: true, required: { 'data': { allowed: ['string']}, 'keys': { allowed: ['string'], array_allowed: true } }, optional: { 'protocol': { allowed: ['string'], allowed_data: ['cms', 'openpgp'] }, - 'chunksize': { - allowed: ['number'], - }, 'sender': { allowed: ['string'], }, 'mode': { allowed: ['string'], allowed_data: ['detached', 'clearsign'] // TODO 'opaque' is not used, but available on native app }, 'base64': { allowed: ['boolean'] }, 'armor': { allowed: ['boolean'] }, }, 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' + } + } }, // note: For the meaning of the optional keylist flags, refer to // https://www.gnupg.org/documentation/manuals/gpgme/Key-Listing-Mode.html keylist:{ required: {}, optional: { 'protocol': { allowed: ['string'], allowed_data: ['cms', 'openpgp'] }, - 'chunksize': { - allowed: ['number'], - }, 'secret': { allowed: ['boolean'] }, 'extern': { allowed: ['boolean'] }, 'local':{ allowed: ['boolean'] }, 'sigs':{ allowed: ['boolean'] }, 'notations':{ allowed: ['boolean'] }, 'tofu': { allowed: ['boolean'] }, 'ephemeral': { allowed: ['boolean'] }, 'validate': { allowed: ['boolean'] }, 'keys': { allowed: ['string'], array_allowed: true } }, answer: { type: ['keys'], - data: [], - params: ['base64'], - infos: ['keys'] + data: { + 'base64': 'boolean', + 'keys': 'object' + } } }, export: { required: {}, optional: { 'protocol': { allowed: ['string'], allowed_data: ['cms', 'openpgp'] }, - 'chunksize': { - allowed: ['number'], - }, 'keys': { allowed: ['string'], array_allowed: true }, 'armor': { allowed: ['boolean'] }, 'extern': { allowed: ['boolean'] }, 'minimal': { allowed: ['boolean'] }, 'raw': { allowed: ['boolean'] }, 'pkcs12':{ allowed: ['boolean'] } // secret: not yet implemented }, answer: { type: ['keys'], - data: ['data'], - params: ['base64'] + data: { + 'data': 'string', + 'base64': 'boolean' + } } }, import: { required: { 'data': { allowed: ['string'] } }, optional: { 'protocol': { allowed: ['string'], allowed_data: ['cms', 'openpgp'] }, 'base64': { allowed: ['boolean'] }, }, answer: { - infos: ['result'], type: [], - data: [], - params: [] + data: { + 'result': 'Object' + } } }, delete: { pinentry: true, required:{ 'key': { allowed: ['string'] } }, optional: { 'protocol': { allowed: ['string'], allowed_data: ['cms', 'openpgp'] }, - // 'secret': { not yet implemented + // 'secret': { not implemented // allowed: ['boolean'] // } }, answer: { - data: [], - params:['success'], - infos: [] + data: { + 'success': 'boolean' + } } }, version: { required: {}, optional: {}, answer: { type: [''], - data: ['gpgme'], - infos: ['info'], - params:[] + data: { + 'gpgme': 'string', + 'info': 'object' + } } } /** * TBD handling of secrets * TBD key modification? * TBD: key generation */ }; diff --git a/lang/js/unittests.js b/lang/js/unittests.js index ce1dd0c3..169e8ebc 100644 --- a/lang/js/unittests.js +++ b/lang/js/unittests.js @@ -1,390 +1,391 @@ /* gpgme.js - Javascript integration for gpgme * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik * * This file is part of GPGME. * * GPGME is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * GPGME is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this program; if not, see . * SPDX-License-Identifier: LGPL-2.1+ */ import './node_modules/mocha/mocha'; /*global mocha, it, describe*/ import './node_modules/chai/chai';/*global chai*/ import { helper_params as hp } from './unittest_inputvalues'; import { message_params as mp } from './unittest_inputvalues'; import { whatever_params as wp } from './unittest_inputvalues'; import { key_params as kp } from './unittest_inputvalues'; import { Connection } from './src/Connection'; import { gpgme_error } from './src/Errors'; import { toKeyIdArray , isFingerprint } from './src/Helpers'; import { GPGME_Key , createKey } from './src/Key'; import { GPGME_Keyring } from './src/Keyring'; import {GPGME_Message, createMessage} from './src/Message'; mocha.setup('bdd'); const expect = chai.expect; chai.config.includeStack = true; function unittests (){ describe('Connection testing', function(){ it('Connecting', function(done) { let conn0 = new Connection; conn0.checkConnection().then(function(answer) { expect(answer).to.not.be.empty; expect(answer.gpgme).to.not.be.undefined; expect(answer.gpgme).to.be.a('string'); expect(answer.info).to.be.an('Array'); expect(conn0.disconnect).to.be.a('function'); expect(conn0.post).to.be.a('function'); done(); }); }); it('Disconnecting', function(done) { let conn0 = new Connection; conn0.checkConnection(false).then(function(answer) { expect(answer).to.be.true; conn0.disconnect(); conn0.checkConnection(false).then(function(result) { expect(result).to.be.false; done(); }); }); }); }); describe('Error Object handling', function(){ // TODO: new GPGME_Error codes it('check the Timeout error', function(){ let test0 = gpgme_error('CONN_TIMEOUT'); expect(test0).to.be.an.instanceof(Error); expect(test0.code).to.equal('CONN_TIMEOUT'); }); it('Error Object returns generic code if code is not listed', function(){ let test0 = gpgme_error(hp.invalidErrorCode); expect(test0).to.be.an.instanceof(Error); expect(test0.code).to.equal('GENERIC_ERROR'); } ); it('Warnings like PARAM_IGNORED should not return errors', function(){ let test0 = gpgme_error('PARAM_IGNORED'); expect(test0).to.be.null; }); }); describe('Fingerprint checking', function(){ it('isFingerprint(): valid Fingerprint', function(){ let test0 = isFingerprint(hp.validFingerprint); expect(test0).to.be.true; }); it('isFingerprint(): invalid Fingerprints', function(){ for (let i=0; i < hp.invalidFingerprints.length; i++){ let test0 = isFingerprint(hp.invalidFingerprints[i]); expect(test0).to.be.false; } }); }); describe('toKeyIdArray() (converting input to fingerprint)', function(){ it('Correct fingerprint string', function(){ let test0 = toKeyIdArray(hp.validFingerprint); expect(test0).to.be.an('array'); expect(test0).to.include(hp.validFingerprint); }); it('correct GPGME_Key', function(){ expect(hp.validGPGME_Key).to.be.an.instanceof(GPGME_Key); let test0 = toKeyIdArray(hp.validGPGME_Key); expect(test0).to.be.an('array'); expect(test0).to.include(hp.validGPGME_Key.fingerprint); }); it('openpgpjs-like object', function(){ let test0 = toKeyIdArray(hp.valid_openpgplike); expect(test0).to.be.an('array').with.lengthOf(1); expect(test0).to.include( hp.valid_openpgplike.primaryKey.getFingerprint()); }); it('Array of valid inputs', function(){ let test0 = toKeyIdArray(hp.validKeys); expect(test0).to.be.an('array'); expect(test0).to.have.lengthOf(hp.validKeys.length); }); it('Incorrect inputs', function(){ it('valid Long ID', function(){ let test0 = toKeyIdArray(hp.validLongId); expect(test0).to.be.empty; }); it('invalidFingerprint', function(){ let test0 = toKeyIdArray(hp.invalidFingerprint); expect(test0).to.be.empty; }); it('invalidKeyArray', function(){ let test0 = toKeyIdArray(hp.invalidKeyArray); expect(test0).to.be.empty; }); it('Partially invalid array', function(){ let test0 = toKeyIdArray(hp.invalidKeyArray_OneBad); expect(test0).to.be.an('array'); expect(test0).to.have.lengthOf( hp.invalidKeyArray_OneBad.length - 1); }); }); }); describe('GPGME_Key', function(){ it('correct Key initialization', function(){ let key = createKey(kp.validKeyFingerprint); expect(key).to.be.an.instanceof(GPGME_Key); }); it('Key has data after a first refresh', function(done) { let key = createKey(kp.validKeyFingerprint); key.refreshKey().then(function(key2){ expect(key2).to.be.an.instanceof(GPGME_Key); expect(key2.get).to.be.a('function'); for (let i=0; i < kp.validKeyProperties.length; i++) { let prop = key2.get(kp.validKeyProperties[i]); expect(prop).to.not.be.undefined; expect(prop).to.be.a('boolean'); } expect(isFingerprint(key2.get('fingerprint'))).to.be.true; expect( key2.get('fingerprint')).to.equal(kp.validKeyFingerprint); expect( key2.get('fingerprint')).to.equal(key.fingerprint); done(); }); }); it('Non-cached key async data retrieval', function (done){ let key = createKey(kp.validKeyFingerprint); key.get('can_authenticate',false).then(function(result){ expect(result).to.be.a('boolean'); done(); }); }); it('Non-cached key async armored Key', function (done){ let key = createKey(kp.validKeyFingerprint); key.get('armored', false).then(function(result){ expect(result).to.be.a('string'); expect(result).to.include('KEY BLOCK-----'); done(); }); }); it('Non-cached key async hasSecret', function (done){ let key = createKey(kp.validKeyFingerprint); key.get('hasSecret', false).then(function(result){ expect(result).to.be.a('boolean'); done(); }); }); it('Non-cached key async hasSecret (no secret in Key)', function (done){ let key = createKey(kp.validFingerprintNoSecret); expect(key).to.be.an.instanceof(GPGME_Key); key.get('hasSecret', false).then(function(result){ expect(result).to.be.a('boolean'); expect(result).to.equal(false); done(); }); }); it('Querying non-existing Key returns an error', function(done) { let key = createKey(kp.invalidKeyFingerprint); key.refreshKey().then(function(){}, function(error){ expect(error).to.be.an.instanceof(Error); expect(error.code).to.equal('KEY_NOKEY'); done(); }); }); it('createKey returns error if parameters are wrong', function(){ for (let i=0; i< 4; i++){ let key0 = createKey(wp.four_invalid_params[i]); expect(key0).to.be.an.instanceof(Error); expect(key0.code).to.equal('PARAM_WRONG'); } }); it('malformed GPGME_Key cannot be used', function(){ for (let i=0; i < 4; i++){ let key = new GPGME_Key(wp.four_invalid_params[i]); expect(key.fingerprint).to.be.an.instanceof(Error); expect(key.fingerprint.code).to.equal('KEY_INVALID'); } }); // TODO: tests for subkeys // TODO: tests for userids // TODO: some invalid tests for key/keyring }); describe('GPGME_Keyring', function(){ it('correct Keyring initialization', function(){ let keyring = new GPGME_Keyring; expect(keyring).to.be.an.instanceof(GPGME_Keyring); expect(keyring.getKeys).to.be.a('function'); }); it('Loading Keys from Keyring, to be used synchronously', function(done){ let keyring = new GPGME_Keyring; keyring.getKeys(null, true).then(function(result){ expect(result).to.be.an('array'); expect(result[0]).to.be.an.instanceof(GPGME_Key); expect(result[0].get('armored')).to.be.a('string'); expect(result[0].get('armored')).to.include( '-----END PGP PUBLIC KEY BLOCK-----'); done(); }); } ); it('Loading specific Key from Keyring, to be used synchronously', function(done){ let keyring = new GPGME_Keyring; keyring.getKeys(kp.validKeyFingerprint, true).then( function(result){ expect(result).to.be.an('array'); expect(result[0]).to.be.an.instanceof(GPGME_Key); expect(result[0].get('armored')).to.be.a('string'); expect(result[0].get('armored')).to.include( '-----END PGP PUBLIC KEY BLOCK-----'); done(); } ); } ); it('Querying non-existing Key from Keyring', function(done){ let keyring = new GPGME_Keyring; keyring.getKeys(kp.invalidKeyFingerprint, true).then( function(result){ expect(result).to.be.an('array'); expect(result.length).to.equal(0); done(); } ); }); }); describe('GPGME_Message', function(){ it('creating encrypt Message', function(){ let test0 = createMessage('encrypt'); expect(test0).to.be.an.instanceof(GPGME_Message); expect(test0.isComplete).to.be.false; }); it('Message is complete after setting mandatory data', function(){ let test0 = createMessage('encrypt'); test0.setParameter('data', mp.valid_encrypt_data); test0.setParameter('keys', hp.validFingerprints); expect(test0.isComplete).to.be.true; }); it('Message is not complete after mandatory data is empty', function(){ let test0 = createMessage('encrypt'); test0.setParameter('data', ''); test0.setParameter('keys', hp.validFingerprints); expect(test0.isComplete).to.be.false; }); it('Complete Message contains the data that was set', function(){ let test0 = createMessage('encrypt'); test0.setParameter('data', mp.valid_encrypt_data); 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); }); it ('Not accepting non-allowed operation', function(){ let test0 = createMessage(mp.invalid_op_action); expect(test0).to.be.an.instanceof(Error); expect(test0.code).to.equal('MSG_WRONG_OP'); }); it('Not accepting wrong parameter type', function(){ let test0 = createMessage(mp.invalid_op_type); expect(test0).to.be.an.instanceof(Error); expect(test0.code).to.equal('PARAM_WRONG'); }); it('Not accepting wrong parameter name', function(){ let test0 = createMessage(mp.invalid_param_test.valid_op); for (let i=0; i < mp.invalid_param_test.invalid_param_names.length; i++){ let ret = test0.setParameter( mp.invalid_param_test.invalid_param_names[i], 'Somevalue'); expect(ret).to.be.an.instanceof(Error); expect(ret.code).to.equal('PARAM_WRONG'); } }); it('Not accepting wrong parameter value', function(){ let test0 = createMessage(mp.invalid_param_test.valid_op); for (let j=0; j < mp.invalid_param_test.invalid_values_0.length; j++){ let ret = test0.setParameter( mp.invalid_param_test.validparam_name_0, mp.invalid_param_test.invalid_values_0[j]); expect(ret).to.be.an.instanceof(Error); expect(ret.code).to.equal('PARAM_WRONG'); } }); }); } export default {unittests}; \ No newline at end of file