1 /* gpgme.js - Javascript integration for gpgme
2 * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik
4 * This file is part of GPGME.
6 * GPGME is free software; you can redistribute it and/or modify it
7 * under the terms of the GNU Lesser General Public License as
8 * published by the Free Software Foundation; either version 2.1 of
9 * the License, or (at your option) any later version.
11 * GPGME is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public
17 * License along with this program; if not, see <http://www.gnu.org/licenses/>.
18 * SPDX-License-Identifier: LGPL-2.1+
21 * Maximilian Krambach <mkrambach@intevation.de>
26 import { permittedOperations } from './permittedOperations';
27 import { gpgme_error } from './Errors';
28 import { GPGME_Message, createMessage } from './Message';
29 import { decode, atobArray, Utf8ArrayToStr } from './Helpers';
32 * A Connection handles the nativeMessaging interaction via a port. As the
33 * protocol only allows up to 1MB of message sent from the nativeApp to the
34 * browser, the connection will stay open until all parts of a communication
35 * are finished. For a new request, a new port will open, to avoid mixing
40 export class Connection{
43 this._connection = chrome.runtime.connectNative('gpgmejson');
47 * Immediately closes an open port.
50 if (this._connection){
51 this._connection.disconnect();
52 this._connection = null;
58 * @typedef {Object} backEndDetails
59 * @property {String} gpgme Version number of gpgme
60 * @property {Array<Object>} info Further information about the backend
61 * and the used applications (Example:
64 * "protocol": "OpenPGP",
65 * "fname": "/usr/bin/gpg",
67 * "req_version": "1.4.0",
68 * "homedir": "default"
74 * Retrieves the information about the backend.
75 * @param {Boolean} details (optional) If set to false, the promise will
76 * just return if a connection was successful.
77 * @param {Number} timeout (optional)
78 * @returns {Promise<backEndDetails>|Promise<Boolean>} Details from the
82 checkConnection (details = true, timeout = 1000){
83 if (typeof timeout !== 'number' && timeout <= 0) {
86 const msg = createMessage('version');
87 if (details === true) {
88 return this.post(msg);
91 return new Promise(function (resolve) {
94 new Promise(function (resolve, reject){
95 setTimeout(function (){
96 reject(gpgme_error('CONN_TIMEOUT'));
99 ]).then(function (){ // success
101 }, function (){ // failure
109 * Sends a {@link GPGME_Message} via the nativeMessaging port. It
110 * resolves with the completed answer after all parts have been
111 * received and reassembled, or rejects with an {@link GPGME_Error}.
113 * @param {GPGME_Message} message
114 * @returns {Promise<*>} The collected answer, depending on the messages'
120 if (!message || !(message instanceof GPGME_Message)){
122 return Promise.reject(gpgme_error(
123 'PARAM_WRONG', 'Connection.post'));
125 if (message.isComplete() !== true){
127 return Promise.reject(gpgme_error('MSG_INCOMPLETE'));
129 let chunksize = message.chunksize;
131 return new Promise(function (resolve, reject){
132 let answer = new Answer(message);
133 let listener = function (msg) {
135 me._connection.onMessage.removeListener(listener);
136 me._connection.disconnect();
137 reject(gpgme_error('CONN_EMPTY_GPG_ANSWER'));
139 let answer_result = answer.collect(msg);
140 if (answer_result !== true){
141 me._connection.onMessage.removeListener(listener);
142 me._connection.disconnect();
143 reject(answer_result);
145 if (msg.more === true){
146 me._connection.postMessage({
148 'chunksize': chunksize
151 me._connection.onMessage.removeListener(listener);
152 me._connection.disconnect();
153 const message = answer.getMessage();
154 if (message instanceof Error){
163 me._connection.onMessage.addListener(listener);
164 if (permittedOperations[message.operation].pinentry){
165 return me._connection.postMessage(message.message);
167 return Promise.race([
168 me._connection.postMessage(message.message),
169 function (resolve, reject){
170 setTimeout(function (){
171 me._connection.disconnect();
172 reject(gpgme_error('CONN_TIMEOUT'));
175 ]).then(function (result){
177 }, function (reject){
178 if (!(reject instanceof Error)) {
179 me._connection.disconnect();
180 return gpgme_error('GNUPG_ERROR', reject);
192 * A class for answer objects, checking and processing the return messages of
193 * the nativeMessaging communication.
199 * @param {GPGME_Message} message
201 constructor (message){
202 this._operation = message.operation;
203 this._expected = message.expected;
204 this._response_b64 = null;
208 return this._operation;
212 return this._expected;
216 * Adds incoming base64 encoded data to the existing response
217 * @param {*} msg base64 encoded data.
223 if (typeof (msg) !== 'object' || !msg.hasOwnProperty('response')) {
224 return gpgme_error('CONN_UNEXPECTED_ANSWER');
226 if (!this._response_b64){
227 this._response_b64 = msg.response;
230 this._response_b64 += msg.response;
235 * Decodes and verifies the base64 encoded answer data. Verified against
236 * {@link permittedOperations}.
237 * @returns {Object} The readable gpnupg answer
240 if (this._response_b64 === null){
241 return gpgme_error('CONN_UNEXPECTED_ANSWER');
243 let _decodedResponse = JSON.parse(atob(this._response_b64));
247 let messageKeys = Object.keys(_decodedResponse);
248 let poa = permittedOperations[this.operation].answer;
249 if (messageKeys.length === 0){
250 return gpgme_error('CONN_UNEXPECTED_ANSWER');
252 for (let i= 0; i < messageKeys.length; i++){
253 let key = messageKeys[i];
256 if (_decodedResponse.type === 'error'){
257 return (gpgme_error('GNUPG_ERROR',
258 decode(_decodedResponse.msg)));
259 } else if (poa.type.indexOf(_decodedResponse.type) < 0){
260 return gpgme_error('CONN_UNEXPECTED_ANSWER');
268 if (_decodedResponse.type === 'error'){
269 return (gpgme_error('GNUPG_ERROR', _decodedResponse.msg));
274 let answerType = null;
275 if (poa.payload && poa.payload.hasOwnProperty(key)){
277 } else if (poa.info && poa.info.hasOwnProperty(key)){
280 if (answerType !== 'p' && answerType !== 'i'){
281 return gpgme_error('CONN_UNEXPECTED_ANSWER');
284 if (answerType === 'i') {
285 if ( typeof (_decodedResponse[key]) !== poa.info[key] ){
286 return gpgme_error('CONN_UNEXPECTED_ANSWER');
288 _response[key] = decode(_decodedResponse[key]);
290 } else if (answerType === 'p') {
291 if (_decodedResponse.base64 === true
292 && poa.payload[key] === 'string'
294 if (this.expected === 'uint8'){
295 _response[key] = atobArray(_decodedResponse[key]);
296 _response.format = 'uint8';
298 } else if (this.expected === 'base64'){
299 _response[key] = _decodedResponse[key];
300 _response.format = 'base64';
302 } else { // no 'expected'
303 _response[key] = Utf8ArrayToStr(
304 atobArray(_decodedResponse[key]));
305 _response.format = 'string';
307 } else if (poa.payload[key] === 'string') {
308 _response[key] = _decodedResponse[key];
310 // fallthrough, should not be reached
311 // (payload is always string)
312 return gpgme_error('CONN_UNEXPECTED_ANSWER');