Imported Upstream version 1.12.0
[platform/upstream/gpgme.git] / lang / js / src / Connection.js
1 /* gpgme.js - Javascript integration for gpgme
2  * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik
3  *
4  * This file is part of GPGME.
5  *
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.
10  *
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.
15  *
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+
19  *
20  * Author(s):
21  *     Maximilian Krambach <mkrambach@intevation.de>
22  */
23
24 /* global chrome */
25
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';
30
31 /**
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
36  * contexts.
37  * @class
38  * @private
39  */
40 export class Connection{
41
42     constructor (){
43         this._connection = chrome.runtime.connectNative('gpgmejson');
44     }
45
46     /**
47      * Immediately closes an open port.
48      */
49     disconnect () {
50         if (this._connection){
51             this._connection.disconnect();
52             this._connection = null;
53         }
54     }
55
56
57     /**
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:
62     * <pre>
63     * {
64     *          "protocol":     "OpenPGP",
65     *          "fname":        "/usr/bin/gpg",
66     *          "version":      "2.2.6",
67     *          "req_version":  "1.4.0",
68     *          "homedir":      "default"
69     * }
70     * </pre>
71     */
72
73     /**
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
79      * backend
80      * @async
81      */
82     checkConnection (details = true, timeout = 1000){
83         if (typeof timeout !== 'number' && timeout <= 0) {
84             timeout = 1000;
85         }
86         const msg = createMessage('version');
87         if (details === true) {
88             return this.post(msg);
89         } else {
90             let me = this;
91             return new Promise(function (resolve) {
92                 Promise.race([
93                     me.post(msg),
94                     new Promise(function (resolve, reject){
95                         setTimeout(function (){
96                             reject(gpgme_error('CONN_TIMEOUT'));
97                         }, timeout);
98                     })
99                 ]).then(function (){ // success
100                     resolve(true);
101                 }, function (){ // failure
102                     resolve(false);
103                 });
104             });
105         }
106     }
107
108     /**
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}.
112      *
113      * @param {GPGME_Message} message
114      * @returns {Promise<*>} The collected answer, depending on the messages'
115      * operation
116      * @private
117      * @async
118      */
119     post (message){
120         if (!message || !(message instanceof GPGME_Message)){
121             this.disconnect();
122             return Promise.reject(gpgme_error(
123                 'PARAM_WRONG', 'Connection.post'));
124         }
125         if (message.isComplete() !== true){
126             this.disconnect();
127             return Promise.reject(gpgme_error('MSG_INCOMPLETE'));
128         }
129         let chunksize = message.chunksize;
130         const me = this;
131         return new Promise(function (resolve, reject){
132             let answer = new Answer(message);
133             let listener = function (msg) {
134                 if (!msg){
135                     me._connection.onMessage.removeListener(listener);
136                     me._connection.disconnect();
137                     reject(gpgme_error('CONN_EMPTY_GPG_ANSWER'));
138                 } else {
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);
144                     } else {
145                         if (msg.more === true){
146                             me._connection.postMessage({
147                                 'op': 'getmore',
148                                 'chunksize': chunksize
149                             });
150                         } else {
151                             me._connection.onMessage.removeListener(listener);
152                             me._connection.disconnect();
153                             const message = answer.getMessage();
154                             if (message instanceof Error){
155                                 reject(message);
156                             } else {
157                                 resolve(message);
158                             }
159                         }
160                     }
161                 }
162             };
163             me._connection.onMessage.addListener(listener);
164             if (permittedOperations[message.operation].pinentry){
165                 return me._connection.postMessage(message.message);
166             } else {
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'));
173                         }, 5000);
174                     }
175                 ]).then(function (result){
176                     return result;
177                 }, function (reject){
178                     if (!(reject instanceof Error)) {
179                         me._connection.disconnect();
180                         return gpgme_error('GNUPG_ERROR', reject);
181                     } else {
182                         return reject;
183                     }
184                 });
185             }
186         });
187     }
188 }
189
190
191 /**
192  * A class for answer objects, checking and processing the return messages of
193  * the nativeMessaging communication.
194  * @private
195  */
196 class Answer{
197
198     /**
199      * @param {GPGME_Message} message
200      */
201     constructor (message){
202         this._operation = message.operation;
203         this._expected = message.expected;
204         this._response_b64 = null;
205     }
206
207     get operation (){
208         return this._operation;
209     }
210
211     get expected (){
212         return this._expected;
213     }
214
215     /**
216      * Adds incoming base64 encoded data to the existing response
217      * @param {*} msg base64 encoded data.
218      * @returns {Boolean}
219      *
220      * @private
221      */
222     collect (msg){
223         if (typeof (msg) !== 'object' || !msg.hasOwnProperty('response')) {
224             return gpgme_error('CONN_UNEXPECTED_ANSWER');
225         }
226         if (!this._response_b64){
227             this._response_b64 = msg.response;
228             return true;
229         } else {
230             this._response_b64 += msg.response;
231             return true;
232         }
233     }
234     /**
235      * Decodes and verifies the base64 encoded answer data. Verified against
236      * {@link permittedOperations}.
237      * @returns {Object} The readable gpnupg answer
238      */
239     getMessage (){
240         if (this._response_b64 === null){
241             return gpgme_error('CONN_UNEXPECTED_ANSWER');
242         }
243         let _decodedResponse = JSON.parse(atob(this._response_b64));
244         let _response = {
245             format: 'ascii'
246         };
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');
251         }
252         for (let i= 0; i < messageKeys.length; i++){
253             let key = messageKeys[i];
254             switch (key) {
255             case 'type': {
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');
261                 }
262                 break;
263             }
264             case 'base64': {
265                 break;
266             }
267             case 'msg': {
268                 if (_decodedResponse.type === 'error'){
269                     return (gpgme_error('GNUPG_ERROR', _decodedResponse.msg));
270                 }
271                 break;
272             }
273             default: {
274                 let answerType = null;
275                 if (poa.payload && poa.payload.hasOwnProperty(key)){
276                     answerType = 'p';
277                 } else if (poa.info && poa.info.hasOwnProperty(key)){
278                     answerType = 'i';
279                 }
280                 if (answerType !== 'p' && answerType !== 'i'){
281                     return gpgme_error('CONN_UNEXPECTED_ANSWER');
282                 }
283
284                 if (answerType === 'i') {
285                     if ( typeof (_decodedResponse[key]) !== poa.info[key] ){
286                         return gpgme_error('CONN_UNEXPECTED_ANSWER');
287                     }
288                     _response[key] = decode(_decodedResponse[key]);
289
290                 } else if (answerType === 'p') {
291                     if (_decodedResponse.base64 === true
292                         && poa.payload[key] === 'string'
293                     ) {
294                         if (this.expected === 'uint8'){
295                             _response[key] = atobArray(_decodedResponse[key]);
296                             _response.format = 'uint8';
297
298                         } else if (this.expected === 'base64'){
299                             _response[key] = _decodedResponse[key];
300                             _response.format = 'base64';
301
302                         } else { // no 'expected'
303                             _response[key] = Utf8ArrayToStr(
304                                 atobArray(_decodedResponse[key]));
305                             _response.format = 'string';
306                         }
307                     } else if (poa.payload[key] === 'string') {
308                         _response[key] = _decodedResponse[key];
309                     } else {
310                         // fallthrough, should not be reached
311                         // (payload is always string)
312                         return gpgme_error('CONN_UNEXPECTED_ANSWER');
313                     }
314                 }
315                 break;
316             } }
317         }
318         return _response;
319     }
320 }