diff options
Diffstat (limited to 'lang/js/src/Connection.js')
-rw-r--r-- | lang/js/src/Connection.js | 320 |
1 files changed, 320 insertions, 0 deletions
diff --git a/lang/js/src/Connection.js b/lang/js/src/Connection.js new file mode 100644 index 0000000..d43d55f --- /dev/null +++ b/lang/js/src/Connection.js @@ -0,0 +1,320 @@ +/* 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 <http://www.gnu.org/licenses/>. + * SPDX-License-Identifier: LGPL-2.1+ + * + * Author(s): + * Maximilian Krambach <mkrambach@intevation.de> + */ + +/* global chrome */ + +import { permittedOperations } from './permittedOperations'; +import { gpgme_error } from './Errors'; +import { GPGME_Message, createMessage } from './Message'; +import { decode, atobArray, Utf8ArrayToStr } from './Helpers'; + +/** + * A Connection handles the nativeMessaging interaction via a port. As the + * protocol only allows up to 1MB of message sent from the nativeApp to the + * browser, the connection will stay open until all parts of a communication + * are finished. For a new request, a new port will open, to avoid mixing + * contexts. + * @class + * @private + */ +export class Connection{ + + constructor (){ + this._connection = chrome.runtime.connectNative('gpgmejson'); + } + + /** + * Immediately closes an open port. + */ + disconnect () { + if (this._connection){ + this._connection.disconnect(); + this._connection = null; + } + } + + + /** + * @typedef {Object} backEndDetails + * @property {String} gpgme Version number of gpgme + * @property {Array<Object>} info Further information about the backend + * and the used applications (Example: + * <pre> + * { + * "protocol": "OpenPGP", + * "fname": "/usr/bin/gpg", + * "version": "2.2.6", + * "req_version": "1.4.0", + * "homedir": "default" + * } + * </pre> + */ + + /** + * Retrieves the information about the backend. + * @param {Boolean} details (optional) If set to false, the promise will + * just return if a connection was successful. + * @param {Number} timeout (optional) + * @returns {Promise<backEndDetails>|Promise<Boolean>} Details from the + * backend + * @async + */ + checkConnection (details = true, timeout = 1000){ + if (typeof timeout !== 'number' && timeout <= 0) { + timeout = 1000; + } + const msg = createMessage('version'); + if (details === true) { + return this.post(msg); + } else { + let me = this; + return new Promise(function (resolve) { + Promise.race([ + me.post(msg), + new Promise(function (resolve, reject){ + setTimeout(function (){ + reject(gpgme_error('CONN_TIMEOUT')); + }, timeout); + }) + ]).then(function (){ // success + resolve(true); + }, function (){ // failure + resolve(false); + }); + }); + } + } + + /** + * Sends a {@link GPGME_Message} via the nativeMessaging port. It + * resolves with the completed answer after all parts have been + * received and reassembled, or rejects with an {@link GPGME_Error}. + * + * @param {GPGME_Message} message + * @returns {Promise<*>} The collected answer, depending on the messages' + * operation + * @private + * @async + */ + 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 chunksize = message.chunksize; + const me = this; + 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 { + 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', + 'chunksize': chunksize + }); + } else { + me._connection.onMessage.removeListener(listener); + me._connection.disconnect(); + const message = answer.getMessage(); + if (message instanceof Error){ + reject(message); + } else { + resolve(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. + * @private + */ +class Answer{ + + /** + * @param {GPGME_Message} message + */ + constructor (message){ + this._operation = message.operation; + this._expected = message.expected; + this._response_b64 = null; + } + + get operation (){ + return this._operation; + } + + get expected (){ + return this._expected; + } + + /** + * Adds incoming base64 encoded data to the existing response + * @param {*} msg base64 encoded data. + * @returns {Boolean} + * + * @private + */ + collect (msg){ + if (typeof (msg) !== 'object' || !msg.hasOwnProperty('response')) { + return gpgme_error('CONN_UNEXPECTED_ANSWER'); + } + if (!this._response_b64){ + this._response_b64 = msg.response; + return true; + } else { + this._response_b64 += msg.response; + return true; + } + } + /** + * Decodes and verifies the base64 encoded answer data. Verified against + * {@link permittedOperations}. + * @returns {Object} The readable gpnupg answer + */ + getMessage (){ + if (this._response_b64 === null){ + return gpgme_error('CONN_UNEXPECTED_ANSWER'); + } + let _decodedResponse = JSON.parse(atob(this._response_b64)); + let _response = { + format: 'ascii' + }; + 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 (_decodedResponse.type === 'error'){ + return (gpgme_error('GNUPG_ERROR', + decode(_decodedResponse.msg))); + } else if (poa.type.indexOf(_decodedResponse.type) < 0){ + return gpgme_error('CONN_UNEXPECTED_ANSWER'); + } + break; + } + case 'base64': { + break; + } + case 'msg': { + if (_decodedResponse.type === 'error'){ + return (gpgme_error('GNUPG_ERROR', _decodedResponse.msg)); + } + break; + } + default: { + let answerType = null; + if (poa.payload && poa.payload.hasOwnProperty(key)){ + answerType = 'p'; + } else if (poa.info && poa.info.hasOwnProperty(key)){ + answerType = 'i'; + } + if (answerType !== 'p' && answerType !== 'i'){ + return gpgme_error('CONN_UNEXPECTED_ANSWER'); + } + + if (answerType === 'i') { + if ( typeof (_decodedResponse[key]) !== poa.info[key] ){ + return gpgme_error('CONN_UNEXPECTED_ANSWER'); + } + _response[key] = decode(_decodedResponse[key]); + + } else if (answerType === 'p') { + if (_decodedResponse.base64 === true + && poa.payload[key] === 'string' + ) { + if (this.expected === 'uint8'){ + _response[key] = atobArray(_decodedResponse[key]); + _response.format = 'uint8'; + + } else if (this.expected === 'base64'){ + _response[key] = _decodedResponse[key]; + _response.format = 'base64'; + + } else { // no 'expected' + _response[key] = Utf8ArrayToStr( + atobArray(_decodedResponse[key])); + _response.format = 'string'; + } + } else if (poa.payload[key] === 'string') { + _response[key] = _decodedResponse[key]; + } else { + // fallthrough, should not be reached + // (payload is always string) + return gpgme_error('CONN_UNEXPECTED_ANSWER'); + } + } + break; + } } + } + return _response; + } +} |