diff --git a/lang/js/BrowserTestExtension/tests/signTest.js b/lang/js/BrowserTestExtension/tests/signTest.js new file mode 100644 index 00000000..e3323721 --- /dev/null +++ b/lang/js/BrowserTestExtension/tests/signTest.js @@ -0,0 +1,58 @@ +/* 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+ + */ +describe('Signing', function () { + it('Sign a message', function (done) { + let prm = Gpgmejs.init(); + prm.then(function (context) { + let data = bigString(100); + context.sign( + 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 SIGNATURE'); + expect(answer.data).to.include('END PGP SIGNATURE'); + expect(answer.data).to.include(data); + context.connection.disconnect(); + done(); + }); + }); + }); + it('Detached sign a message', function (done) { + let prm = Gpgmejs.init(); + prm.then(function (context) { + let data = bigString(100); + context.sign( + data, + inputvalues.encrypt.good.fingerprint, + 'detached' + ).then(function (answer) { + expect(answer).to.not.be.empty; + expect(answer.data).to.be.a('string'); + expect(answer.data).to.include(data); + expect(answer.signature).to.be.a('string'); + expect(answer.signature).to.be.a('string'); + context.connection.disconnect(); + done(); + }); + }); + }); + +}); diff --git a/lang/js/BrowserTestExtension/tests/startup.js b/lang/js/BrowserTestExtension/tests/startup.js index 5de70a6b..ebecf4fb 100644 --- a/lang/js/BrowserTestExtension/tests/startup.js +++ b/lang/js/BrowserTestExtension/tests/startup.js @@ -1,54 +1,51 @@ /* 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+ */ describe('GPGME context', function(){ it('Starting a GpgME instance', function(done){ let prm = Gpgmejs.init(); prm.then( function(context){ expect(context.connection).to.not.be.undefined; expect(context).to.be.an('object'); expect(context.connection).to.be.an('object'); expect(context.Keyring).to.be.undefined; expect(context.encrypt).to.be.a('function'); expect(context.decrypt).to.be.a('function'); done(); - }, function(errorr){ - expect(error).to.be.undefined; - done(); }); }); }); describe('GPGME does not start with invalid parameters', function(){ for (let i=0; i < inputvalues.init.invalid_startups.length; i++){ it('Parameter '+ i, function(done){ let prm = Gpgmejs.init(inputvalues.init.invalid_startups[i]); prm.then(function(context){ expect(context).to.be.undefined; done(); }, function(error){ expect(error).to.be.an.instanceof(Error); expect(error.code).to.equal('PARAM_WRONG'); done(); }); }) } }); \ No newline at end of file diff --git a/lang/js/src/Connection.js b/lang/js/src/Connection.js index 9c2a6428..07df5def 100644 --- a/lang/js/src/Connection.js +++ b/lang/js/src/Connection.js @@ -1,241 +1,257 @@ /* 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+ */ /** * A connection port will be opened for each communication between gpgmejs and * gnupg. It should be alive as long as there are additional messages to be * expected. */ import { permittedOperations } from './permittedOperations' import { gpgme_error } from "./Errors" -import { GPGME_Message } from "./Message"; +import { GPGME_Message, createMessage } from "./Message"; /** * A Connection handles the nativeMessaging interaction. */ export class Connection{ constructor(){ this.connect(); - let me = this; } /** - * (Simple) Connection check. - * @returns {Boolean} true if the onDisconnect event has not been fired. - * Please note that the event listener of the port takes some time - * (5 ms seems enough) to react after the port is created. Then this will - * return undefined + * 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" */ - get isConnected(){ - return this._isConnected; + checkConnection(details = true){ + if (details === true) { + return this.post(createMessage('version')); + } else { + let me = this; + return new Promise(function(resolve,reject) { + Promise.race([ + me.post(createMessage('version')), + new Promise(function(resolve, reject){ + setTimeout(function(){ + reject(gpgme_error('CONN_TIMEOUT')); + }, 500); + }) + ]).then(function(result){ + resolve(true); + }, function(reject){ + resolve(false); + }); + }); + } } /** * Immediately closes the open port. */ disconnect() { if (this._connection){ this._connection.disconnect(); + this._connection = null; } } /** * Opens a nativeMessaging port. */ connect(){ - if (this._isConnected === true){ - gpgme_error('CONN_ALREADY_CONNECTED'); - } else { - this._isConnected = true; + if (!this._connection){ this._connection = chrome.runtime.connectNative('gpgmejson'); - let me = this; - this._connection.onDisconnect.addListener( - function(){ - me._isConnected = false; - } - ); } } /** * 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 (!this.isConnected){ - return Promise.reject(gpgme_error('CONN_DISCONNECTED')); + if (!this._connection) { + } if (!message || !message instanceof GPGME_Message){ return Promise.reject(gpgme_error('PARAM_WRONG'), message); } if (message.isComplete !== true){ return Promise.reject(gpgme_error('MSG_INCOMPLETE')); } let me = this; return new Promise(function(resolve, reject){ let answer = new Answer(message); let listener = function(msg) { if (!msg){ me._connection.onMessage.removeListener(listener) reject(gpgme_error('CONN_EMPTY_GPG_ANSWER')); } else if (msg.type === "error"){ me._connection.onMessage.removeListener(listener); reject(gpgme_error('GNUPG_ERROR', msg.msg)); } else { let answer_result = answer.add(msg); if (answer_result !== true){ me._connection.onMessage.removeListener(listener); reject(answer_result); } if (msg.more === true){ me._connection.postMessage({'op': 'getmore'}); } else { me._connection.onMessage.removeListener(listener) 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(){ reject(gpgme_error('CONN_TIMEOUT')); }, 5000); }]).then(function(result){ return result; }, function(reject){ if(!reject instanceof Error) { 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; } /** * 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 = {}; } let messageKeys = Object.keys(msg); 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){ return gpgme_error('CONN_UNEXPECTED_ANSWER'); } break; case 'more': 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]); } } //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] = []; } - this._response.push(msg[key]); + this._response[key].push(msg[key]); } else { 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(function(c) { return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); }).join('')); } } else { msg[keys[i]] = this._response[keys[i]]; } } return msg; } } diff --git a/lang/js/src/Errors.js b/lang/js/src/Errors.js index bfe3a2f4..7e98f319 100644 --- a/lang/js/src/Errors.js +++ b/lang/js/src/Errors.js @@ -1,129 +1,125 @@ /* 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+ */ const err_list = { // Connection 'CONN_NO_CONNECT': { msg:'Connection with the nativeMessaging host could not be' + ' established.', type: 'error' }, - 'CONN_DISCONNECTED': { - msg:'Connection with the nativeMessaging host was lost.', - type: 'error' - }, 'CONN_EMPTY_GPG_ANSWER':{ msg: 'The nativeMessaging answer was empty.', type: 'error' }, 'CONN_TIMEOUT': { msg: 'A connection timeout was exceeded.', type: 'error' }, 'CONN_UNEXPECTED_ANSWER': { msg: 'The answer from gnupg was not as expected.', type: 'error' }, 'CONN_ALREADY_CONNECTED':{ msg: 'A connection was already established.', type: 'warning' }, // Message/Data 'MSG_INCOMPLETE': { msg: 'The Message did not match the minimum requirements for' + ' the interaction.', type: 'error' }, 'MSG_EMPTY' : { msg: 'The Message is empty.', type: 'error' }, 'MSG_WRONG_OP': { msg: 'The operation requested could not be found', type: 'error' }, 'MSG_NO_KEYS' : { msg: 'There were no valid keys provided.', type: 'warning' }, 'MSG_NOT_A_FPR': { msg: 'The String is not an accepted fingerprint', type: 'warning' }, 'KEY_INVALID': { msg:'Key object is invalid', type: 'error' }, // generic 'PARAM_WRONG':{ msg: 'Invalid parameter was found', type: 'error' }, 'PARAM_IGNORED': { msg: 'An parameter was set that has no effect in gpgmejs', type: 'warning' }, 'GENERIC_ERROR': { msg: 'Unspecified error', type: 'error' } }; /** * Checks the given error code and returns an error object with some * information about meaning and origin * @param {*} code Error code. Should be in err_list or 'GNUPG_ERROR' * @param {*} info Error message passed through if code is 'GNUPG_ERROR' */ export function gpgme_error(code = 'GENERIC_ERROR', info){ if (err_list.hasOwnProperty(code)){ if (err_list[code].type === 'error'){ return new GPGME_Error(code); } if (err_list[code].type === 'warning'){ console.warn(code + ': ' + err_list[code].msg); } return null; } else if (code === 'GNUPG_ERROR'){ return new GPGME_Error(code, info); } else { return new GPGME_Error('GENERIC_ERROR'); } } class GPGME_Error extends Error{ constructor(code, msg=''){ if (code === 'GNUPG_ERROR' && typeof(msg) === 'string'){ super(msg); } else if (err_list.hasOwnProperty(code)){ super(err_list[code].msg); } else { super(err_list['GENERIC_ERROR'].msg); } this.code = code || 'GENERIC_ERROR'; } set code(value){ this._code = value; } get code(){ return this._code; } } \ No newline at end of file diff --git a/lang/js/src/Keyring.js b/lang/js/src/Keyring.js index 4596035a..80792f77 100644 --- a/lang/js/src/Keyring.js +++ b/lang/js/src/Keyring.js @@ -1,162 +1,158 @@ /* 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 {createMessage} from './Message' import {GPGME_Key} from './Key' import { isFingerprint } from './Helpers'; import { gpgme_error } from './Errors'; import { Connection } from './Connection'; export class GPGME_Keyring { constructor(connection){ this.connection = connection; } set connection(connection){ if (!this._connection && connection instanceof Connection){ this._connection = connection; } } get connection(){ if (this._connection instanceof Connection){ - if (this._connection.isConnected){ - return this._connection; - } - return gpgme_error('CONN_DISCONNECTED'); + return this._connection; } return gpgme_error('CONN_NO_CONNECT'); } /** * @param {String} (optional) pattern A pattern to search for, in userIds or KeyIds * @param {Boolean} (optional) Include listing of secret keys * @returns {Promise.>} * */ getKeys(pattern, include_secret){ - let msg = createMessage('listkeys'); - if (msg instanceof Error){ - return Promise.reject(msg); - } - if (pattern && typeof(pattern) === 'string'){ - msg.setParameter('pattern', pattern); - } - if (include_secret){ - msg.setParameter('with-secret', true); - } let me = this; - - this.connection.post(msg).then(function(result){ - let fpr_list = []; - let resultset = []; - if (!Array.isArray(result.keys)){ - //TODO check assumption keys = Array - fpr_list = [result.keys]; - } else { - fpr_list = result.keys; + return new Promise(function(resolve, reject) { + let msg; + msg = createMessage('listkeys'); + if (pattern && typeof(pattern) === 'string'){ + msg.setParameter('pattern', pattern); } - for (let i=0; i < fpr_list.length; i++){ - let newKey = new GPGME_Key(fpr_list[i], me._connection); - if (newKey instanceof GPGME_Key){ - resultset.push(newKey); - } + if (include_secret){ + msg.setParameter('with-secret', true); } - return Promise.resolve(resultset); - }, function(error){ - //TODO error handling + me.connection.post(msg).then(function(result){ + let fpr_list = []; + let resultset = []; + if (!Array.isArray(result.keys)){ + //TODO check assumption keys = Array + fpr_list = [result.keys]; + } else { + fpr_list = result.keys; + } + for (let i=0; i < fpr_list.length; i++){ + let newKey = new GPGME_Key(fpr_list[i], me._connection); + if (newKey instanceof GPGME_Key){ + resultset.push(newKey); + } + } + resolve(resultset); + }, function(error){ + reject(error); + }); }); } /** * @param {Object} flags subset filter expecting at least one of the * filters described below. True will filter on the condition, False will * reverse the filter, if not present or undefined, the filter will not be * considered. Please note that some combination may not make sense * @param {Boolean} flags.secret Only Keys containing a secret part. * @param {Boolean} flags.revoked revoked Keys only * @param {Boolean} flags.expired Expired Keys only * @param {String} (optional) pattern A pattern to search for, in userIds or KeyIds * @returns {Promise Array} * */ getSubset(flags, pattern){ if (flags === undefined) { throw('ERR_WRONG_PARAM'); }; let secretflag = false; if (flags.hasOwnProperty(secret) && flags.secret){ secretflag = true; } this.getKeys(pattern, secretflag).then(function(queryset){ let resultset = []; for (let i=0; i < queryset.length; i++ ){ let conditions = []; let anticonditions = []; if (secretflag === true){ conditions.push('hasSecret'); } else if (secretflag === false){ anticonditions.push('hasSecret'); } /** if (flags.defaultKey === true){ conditions.push('isDefault'); } else if (flags.defaultKey === false){ anticonditions.push('isDefault'); } */ /** * if (flags.valid === true){ anticonditions.push('isInvalid'); } else if (flags.valid === false){ conditions.push('isInvalid'); } */ if (flags.revoked === true){ conditions.push('isRevoked'); } else if (flags.revoked === false){ anticonditions.push('isRevoked'); } if (flags.expired === true){ conditions.push('isExpired'); } else if (flags.expired === false){ anticonditions.push('isExpired'); } let decision = undefined; for (let con = 0; con < conditions.length; con ++){ if (queryset[i][conditions[con]] !== true){ decision = false; } } for (let acon = 0; acon < anticonditions.length; acon ++){ if (queryset[i][anticonditions[acon]] === true){ decision = false; } } if (decision !== false){ resultset.push(queryset[i]); } } return Promise.resolve(resultset); }, function(error){ //TODO error handling }); } }; diff --git a/lang/js/src/gpgmejs.js b/lang/js/src/gpgmejs.js index 1e76655e..c182c175 100644 --- a/lang/js/src/gpgmejs.js +++ b/lang/js/src/gpgmejs.js @@ -1,230 +1,224 @@ /* 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 {Connection} from "./Connection" 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(connection){ this.connection = connection; } set connection(conn){ if (this._connection instanceof Connection){ gpgme_error('CONN_ALREADY_CONNECTED'); } else if (conn instanceof Connection){ this._connection = conn; } else { gpgme_error('PARAM_WRONG'); } } get connection(){ - if (this._connection){ - if (this._connection.isConnected === true){ - return this._connection; - } - return undefined; - } - return undefined; + return this._connection; } set Keyring(keyring){ if (keyring && keyring instanceof GPGME_Keyring){ this._Keyring = keyring; } } get Keyring(){ return this._Keyring; } /** * @param {String} data text/data to be encrypted as String * @param {GPGME_Key|String|Array|Array} publicKeys Keys used to encrypt the message * @param {Boolean} wildcard (optional) If true, recipient information will not be added to the message */ encrypt(data, publicKeys, base64=false, wildcard=false){ let msg = createMessage('encrypt'); if (msg instanceof Error){ return Promise.reject(msg) } // TODO temporary msg.setParameter('armor', true); msg.setParameter('always-trust', true); if (base64 === true) { msg.setParameter('base64', true); } let pubkeys = toKeyIdArray(publicKeys); msg.setParameter('keys', pubkeys); putData(msg, data); if (wildcard === true){ msg.setParameter('throw-keyids', true); }; if (msg.isComplete === true){ return this.connection.post(msg); } else { return Promise.reject(gpgme_error('MSG_INCOMPLETE')); } } /** * @param {String} data TODO base64? Message with the encrypted data * @param {Boolean} base64 (optional) Response should stay base64 * @returns {Promise} decrypted message: data: The decrypted data. This may be base64 encoded. base64: Boolean indicating whether data is base64 encoded. mime: A Boolean indicating whether the data is a MIME object. info: An optional object with extra information. * @async */ decrypt(data, base64=false){ 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 this.connection.post(msg); } sign(data, keys, mode='clearsign', base64=false) { //sender 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'; } let me = this; return new Promise(function(resolve,reject) { me.connection.post(msg).then( function(message) { if (mode === 'clearsign'){ resolve({ data: message.data} ); } else if (mode === 'detached') { resolve({ data: data, signature: message.data }); } }, function(error){ reject(error); }) }); } deleteKey(key, delete_secret = false, no_confirm = false){ return Promise.reject(gpgme_error('NOT_YET_IMPLEMENTED')); let msg = createMessage('deletekey'); if (msg instanceof Error){ return Promise.reject(msg); } let key_arr = toKeyIdArray(key); if (key_arr.length !== 1){ return Promise.reject( gpgme_error('GENERIC_ERROR')); // TBD should always be ONE key? } msg.setParameter('key', key_arr[0]); if (delete_secret === true){ msg.setParameter('allow_secret', true); // TBD } if (no_confirm === true){ //TODO: Do we want this hidden deep in the code? msg.setParameter('delete_force', true); // TBD } if (msg.isComplete === true){ this.connection.post(msg).then(function(success){ // TODO: it seems that there is always errors coming back: }, function(error){ switch (error.msg){ case 'ERR_NO_ERROR': return Promise.resolve('okay'); //TBD default: return Promise.reject(gpgme_error('TODO') ); // // INV_VALUE, // GPG_ERR_NO_PUBKEY, // GPG_ERR_AMBIGUOUS_NAME, // GPG_ERR_CONFLICT } }); } else { return Promise.reject(gpgme_error('MSG_INCOMPLETE')); } } } /** * Sets the data of the message * @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/index.js b/lang/js/src/index.js index 8527b3f3..7f969fee 100644 --- a/lang/js/src/index.js +++ b/lang/js/src/index.js @@ -1,86 +1,82 @@ /* 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 { GpgME } from "./gpgmejs"; import { gpgme_error } from "./Errors"; import { Connection } from "./Connection"; import { defaultConf, availableConf } from "./Config"; /** * Initializes a nativeMessaging Connection and returns a GPGMEjs object * @param {Object} config Configuration. See Config.js for available parameters. Still TODO */ function init(config){ let _conf = parseconfiguration(config); if (_conf instanceof Error){ return Promise.reject(_conf); } return new Promise(function(resolve, reject){ let connection = new Connection; - // TODO: Delayed reaction is ugly. We need to listen to the port's - // event listener in isConnected, but in some cases this takes some - // time (<5ms) to disconnect if there is no successfull connection. - let delayedreaction = function(){ - if (connection === undefined) { + connection.checkConnection(false).then( + function(result){ + if (result === true) { + resolve(new GpgME(connection, _conf)); + } else { + reject(gpgme_error('CONN_NO_CONNECT')); + } + }, function(error){ reject(gpgme_error('CONN_NO_CONNECT')); - } - if (connection.isConnected === true){ - resolve(new GpgME(connection, _conf)); - } else { - reject(gpgme_error('CONN_NO_CONNECT')); - } - }; - setTimeout(delayedreaction, 5); + }); }); } function parseconfiguration(rawconfig = {}){ if ( typeof(rawconfig) !== 'object'){ return gpgme_error('PARAM_WRONG'); }; let result_config = {}; let conf_keys = Object.keys(rawconfig); for (let i=0; i < conf_keys.length; i++){ if (availableConf.hasOwnProperty(conf_keys[i])){ let value = rawconfig[conf_keys[i]]; if (availableConf[conf_keys[i]].indexOf(value) < 0){ return gpgme_error('PARAM_WRONG'); } else { result_config[conf_keys[i]] = value; } } else { return gpgme_error('PARAM_WRONG'); } } let default_keys = Object.keys(defaultConf); for (let j=0; j < default_keys.length; j++){ if (!result_config.hasOwnProperty(default_keys[j])){ result_config[default_keys[j]] = defaultConf[default_keys[j]]; } } return result_config; }; export default { init: init } \ No newline at end of file diff --git a/lang/js/unittests.js b/lang/js/unittests.js index c437d599..06b2b23a 100644 --- a/lang/js/unittests.js +++ b/lang/js/unittests.js @@ -1,326 +1,327 @@ /* 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"; import "./node_modules/chai/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 { 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"; import { setTimeout } from "timers"; mocha.setup('bdd'); var expect = chai.expect; chai.config.includeStack = true; function unittests (){ describe('Connection testing', function(){ it('Connecting', function(done) { let conn0 = new Connection; - let delayed = function(){ - expect(conn0.isConnected).to.be.true; - expect(conn0.connect).to.be.a('function'); + 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(); - }; - setTimeout(delayed, 5); + }); }); it('Disconnecting', function(done) { let conn0 = new Connection; - let delayed = function(){ - conn0.disconnect(); // TODO fails! - expect(conn0.isConnected).to.be.false; - done(); - }; - setTimeout(delayed, 5); + 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(); + }); + }); }); - - // broken - // it('Connect info still only available after a delay', function(done){ - // // if false, all delayed connections can be refactored - // let conn0 = new Connection; - // expect(conn0.isConnected).to.be.undefined; - // // - // }) }); describe('Error Object handling', function(){ 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 conn = new Connection; let key = createKey(hp.validFingerprint, conn); expect(key).to.be.an.instanceof(GPGME_Key); expect(key.connection).to.be.an.instanceof(Connection); // TODO not implemented yet: Further Key functionality }); - it('Key can use the connection', function(){ + it('Key can use the connection', function(done){ let conn = new Connection; let key = createKey(hp.validFingerprint, conn); - - expect(key.connection.isConnected).to.be.true; - - key.connection.disconnect(); - expect(key.connection.isConnected).to.be.false; + key.connection.checkConnection(false).then(function(result){ + expect(result).to.be.true; + key.connection.disconnect(); + key.connection.checkConnection(false).then(function(result2){ + expect(result2).to.be.false; + done(); + }); + }); }); it('createKey returns error if parameters are wrong', function(){ let conn = new Connection; for (let i=0; i< 4; i++){ let key0 = createKey(wp.four_invalid_params[i], conn); expect(key0).to.be.an.instanceof(Error); expect(key0.code).to.equal('PARAM_WRONG'); } for (let i=0; i< 4; i++){ let key0 = createKey( hp.validFingerprint, wp.four_invalid_params[i]); expect(key0).to.be.an.instanceof(Error); expect(key0.code).to.equal('PARAM_WRONG'); } }); it('bad GPGME_Key returns Error if used', function(){ let conn = new Connection; for (let i=0; i < 4; i++){ let key = new GPGME_Key(wp.four_invalid_params[i], conn); expect(key.connection).to.be.an.instanceof(Error); expect(key.connection.code).to.equal('KEY_INVALID'); } }); }); describe('GPGME_Keyring', function(){ it('correct initialization', function(){ let conn = new Connection; let keyring = new GPGME_Keyring(conn); expect(keyring).to.be.an.instanceof(GPGME_Keyring); expect(keyring.connection).to.be.an.instanceof(Connection); expect(keyring.getKeys).to.be.a('function'); expect(keyring.getSubset).to.be.a('function'); }); it('Keyring should return errors if not connected', function(){ let keyring = new GPGME_Keyring; - expect(keyring).to.be.an.instanceof(GPGME_Keyring); expect(keyring.connection).to.be.an.instanceof(Error); expect(keyring.connection.code).to.equal('CONN_NO_CONNECT'); - expect(keyring.getKeys).to.be.an.instanceof(Error); - expect(keyring.getkeys.code).to.equal('CONN_NO_CONNECT'); + // not yet implemented: + // keyring.getKeys().then( + // function(result){}, + //function(reject){ + // expect(reject).to.be.an.instanceof(Error); + // done(); }); //TODO not yet implemented: // getKeys(pattern, include_secret) //note: pattern can be null // getSubset(flags, pattern) // available Boolean flags: secret revoked expired }); 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.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