diff --git a/lang/js/src/Connection.js b/lang/js/src/Connection.js index 8bc3d42a..4270be58 100644 --- a/lang/js/src/Connection.js +++ b/lang/js/src/Connection.js @@ -1,209 +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+ */ /** * 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 { GPGMEJS_Error } from "./Errors" import { GPGME_Message } 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 */ get isConnected(){ return this._isConnected; } /** * Immediately closes the open port. */ disconnect() { if (this._connection){ this._connection.disconnect(); } } /** * Opens a nativeMessaging port. */ connect(){ if (this._isConnected === true){ GPGMEJS_Error('CONN_ALREADY_CONNECTED'); } else { this._isConnected = true; 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(GPGMEJS_Error('CONN_NO_CONNECT')); } if (!message || !message instanceof GPGME_Message){ return Promise.reject(GPGMEJS_Error('PARAM_WRONG'), message); } if (message.isComplete !== true){ return Promise.reject(GPGMEJS_Error('MSG_INCOMPLETE')); } let me = this; return new Promise(function(resolve, reject){ let answer = new Answer(message.operation); let listener = function(msg) { if (!msg){ me._connection.onMessage.removeListener(listener) reject(GPGMEJS_Error('CONN_EMPTY_GPG_ANSWER')); } else if (msg.type === "error"){ me._connection.onMessage.removeListener(listener) reject( {code: 'GNUPG_ERROR', msg: msg.msg} ); } else { let answer_result = answer.add(msg); if (answer_result !== true){ 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); - me._connection.postMessage(message.message); - - //TBD: needs to be aware if there is a pinentry pending - // setTimeout( - // function(){ - // me.disconnect(); - // reject(GPGMEJS_Error('CONN_TIMEOUT')); - // }, timeout); + let timeout = new Promise(function(resolve, reject){ + setTimeout(function(){ + reject(GPGMEJS_Error('CONN_TIMEOUT')); + }, 5000); + }); + if (permittedOperations[message.operation].pinentry){ + return me._connection.postMessage(message.message); + } else { + return Promise.race([timeout, + me._connection.postMessage(message.message) + ]); + } }); } }; /** * 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(operation){ this.operation = operation; } /** * Add the information to the answer * @param {Object} msg The message as received with nativeMessaging * returns true if successfull, GPGMEJS_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 GPGMEJS_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 GPGMEJS_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] = this._response[key].concat(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 GPGMEJS_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]); } else { return GPGMEJS_Error('CONN_UNEXPECTED_ANSWER', key); } break; } } return true; } /** * @returns {Object} the assembled message. * TODO: does not care yet if completed. */ get message(){ return this._response; } } diff --git a/lang/js/src/Errors.js b/lang/js/src/Errors.js index c49bfe21..04b13e10 100644 --- a/lang/js/src/Errors.js +++ b/lang/js/src/Errors.js @@ -1,116 +1,116 @@ /* 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+ */ /** * Checks the given error code and returns some information about it's meaning * @param {String} code The error code * @returns {Object} An object containing string properties code and msg * TODO: error-like objects with the code 'GNUPG_ERROR' are errors sent * directly by gnupg as answer in Connection.post() */ export function GPGMEJS_Error(code = 'GENERIC_ERROR'){ if (!typeof(code) === 'string'){ code = 'GENERIC_ERROR'; } let errors = { //TODO: someplace else // Connection 'CONN_NO_CONNECT': { msg:'Connection with the nativeMessaging host could not be' + ' established.', 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: 'warn' }, // 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_OP_PENDING': { msg: 'There is no operation specified yet. The parameter cannot' + ' be set', type: 'warning' }, 'MSG_WRONG_OP': { msg: 'The operation requested could not be found', type: 'warning' }, 'MSG_NO_KEYS' : { msg: 'There were no valid keys provided.', type: 'warn' }, 'MSG_NOT_A_FPR': { msg: 'The String is not an accepted fingerprint', type: 'warn' }, // generic 'PARAM_WRONG':{ msg: 'invalid parameter was found', type: 'error' }, 'NOT_IMPLEMENTED': { msg: 'A openpgpjs parameter was submitted that is not implemented', type: 'error' }, 'NOT_YET_IMPLEMENTED': { msg: 'Support of this is probable, but it is not implemented yet', type: 'error' }, 'GENERIC_ERROR': { msg: 'Unspecified error', type: 'error' }, } if (code === 'TODO'){ alert('TODO_Error!'); } if (errors.hasOwnProperty(code)){ code = 'GENERIC_ERROR'; } - if (error.type === 'error'){ + if (errors.type === 'error'){ return {code: 'code', msg: errors[code].msg }; } - if (error.type === 'warning'){ + if (errors.type === 'warning'){ console.log(code + ': ' + error[code].msg); } return undefined; } diff --git a/lang/js/src/permittedOperations.js b/lang/js/src/permittedOperations.js index 3c11b8e0..892f4f2e 100644 --- a/lang/js/src/permittedOperations.js +++ b/lang/js/src/permittedOperations.js @@ -1,75 +1,78 @@ /* 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+ */ /** * Definition of the possible interactions with gpgme-json. * operation: required: Array optional: Array + 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 change } } */ export const permittedOperations = { encrypt: { required: ['keys', 'data'], optional: [ 'protocol', 'chunksize', 'base64', 'mime', 'armor', 'always-trust', 'no-encrypt-to', 'no-compress', 'throw-keyids', 'want-address', 'wrap' ], answer: { type: ['ciphertext'], data: ['data'], params: ['base64'], infos: [] } }, decrypt: { + pinentry: true, required: ['data'], optional: [ 'protocol', 'chunksize', 'base64' ], answer: { type: ['plaintext'], data: ['data'], params: ['base64', 'mime'], infos: ['info'] } } } diff --git a/lang/js/testapplication.js b/lang/js/testapplication.js index f47299e8..b2cb4c23 100644 --- a/lang/js/testapplication.js +++ b/lang/js/testapplication.js @@ -1,55 +1,55 @@ /* 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+ * */ document.addEventListener('DOMContentLoaded', function() { Gpgmejs.init().then(function(gpgmejs){ document.getElementById("buttonencrypt").addEventListener("click", function(){ let data = document.getElementById('cleartext').value; let keyId = document.getElementById('pubkey').value; gpgmejs.encrypt(data, keyId).then( function(answer){ console.log(answer); if (answer.data){ console.log(answer.data); document.getElementById('answer').value = answer.data; } }, function(errormsg){ - alert('Error: '+ errormsg); + alert( errormsg.code + ' ' + errormsg.msg); }); }); document.getElementById("buttondecrypt").addEventListener("click", function(){ let data = document.getElementById("ciphertext").value; gpgmejs.decrypt(data).then( function(answer){ console.log(answer); if (answer.data){ document.getElementById('answer').value = answer.data; } }, function(errormsg){ - alert('Error: '+ errormsg); + alert( errormsg.code + ' ' + errormsg.msg); }); }); }, function(error){console.log(error)}); });