3480d811a5a9dd5c385712979e9d129c779f947c
[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
21 /**
22  * A connection port will be opened for each communication between gpgmejs and
23  * gnupg. It should be alive as long as there are additional messages to be
24  * expected.
25  */
26 import { permittedOperations } from './permittedOperations'
27 import { gpgme_error } from "./Errors"
28 import { GPGME_Message, createMessage } from "./Message";
29
30 /**
31  * A Connection handles the nativeMessaging interaction.
32  */
33 export class Connection{
34
35     constructor(){
36         this.connect();
37     }
38
39     /**
40      * Retrieves the information about the backend.
41      * @param {Boolean} details (optional) If set to false, the promise will
42      *  just return a connection status
43      * @returns {Promise<Object>}
44      *      {String} The property 'gpgme': Version number of gpgme
45      *      {Array<Object>} 'info' Further information about the backends.
46      *      Example:
47      *          "protocol":     "OpenPGP",
48      *          "fname":        "/usr/bin/gpg",
49      *          "version":      "2.2.6",
50      *          "req_version":  "1.4.0",
51      *          "homedir":      "default"
52      */
53     checkConnection(details = true){
54         if (details === true) {
55             return this.post(createMessage('version'));
56         } else {
57             let me = this;
58             return new Promise(function(resolve,reject) {
59                 Promise.race([
60                     me.post(createMessage('version')),
61                     new Promise(function(resolve, reject){
62                         setTimeout(function(){
63                             reject(gpgme_error('CONN_TIMEOUT'));
64                         }, 500);
65                     })
66                 ]).then(function(result){
67                         resolve(true);
68                 }, function(reject){
69                     resolve(false);
70                 });
71             });
72         }
73     }
74
75     /**
76      * Immediately closes the open port.
77      */
78     disconnect() {
79         if (this._connection){
80             this._connection.disconnect();
81             this._connection = null;
82         }
83     }
84
85     /**
86      * Opens a nativeMessaging port.
87      */
88     connect(){
89         if (!this._connection){
90             this._connection = chrome.runtime.connectNative('gpgmejson');
91         }
92     }
93
94     /**
95      * Sends a message and resolves with the answer.
96      * @param {GPGME_Message} message
97      * @returns {Promise<Object>} the gnupg answer, or rejection with error
98      * information.
99      */
100     post(message){
101         if (!this._connection) {
102
103         }
104         if (!message || !message instanceof GPGME_Message){
105             this.disconnect();
106             return Promise.reject(gpgme_error('PARAM_WRONG'), message);
107         }
108         if (message.isComplete !== true){
109             this.disconnect();
110             return Promise.reject(gpgme_error('MSG_INCOMPLETE'));
111         }
112         let me = this;
113         return new Promise(function(resolve, reject){
114             let answer = new Answer(message);
115             let listener = function(msg) {
116                 if (!msg){
117                     me._connection.onMessage.removeListener(listener)
118                     me._connection.disconnect();
119                     reject(gpgme_error('CONN_EMPTY_GPG_ANSWER'));
120                 } else if (msg.type === "error"){
121                     me._connection.onMessage.removeListener(listener);
122                     me._connection.disconnect();
123                     reject(gpgme_error('GNUPG_ERROR', msg.msg));
124                 } else {
125                     let answer_result = answer.add(msg);
126                     if (answer_result !== true){
127                         me._connection.onMessage.removeListener(listener);
128                         me._connection.disconnect();
129                         reject(answer_result);
130                     } else if (msg.more === true){
131                         me._connection.postMessage({'op': 'getmore'});
132                     } else {
133                         me._connection.onMessage.removeListener(listener)
134                         me._connection.disconnect();
135                         resolve(answer.message);
136                     }
137                 }
138             };
139             me._connection.onMessage.addListener(listener);
140             if (permittedOperations[message.operation].pinentry){
141                 return me._connection.postMessage(message.message);
142             } else {
143                 return Promise.race([
144                     me._connection.postMessage(message.message),
145                     function(resolve, reject){
146                         setTimeout(function(){
147                             me._connection.disconnect();
148                             reject(gpgme_error('CONN_TIMEOUT'));
149                         }, 5000);
150                     }]).then(function(result){
151                         return result;
152                 }, function(reject){
153                     if(!reject instanceof Error) {
154                         me._connection.disconnect();
155                         return gpgme_error('GNUPG_ERROR', reject);
156                     } else {
157                         return reject;
158                     }
159                 });
160             }
161         });
162      }
163 };
164
165 /**
166  * A class for answer objects, checking and processing the return messages of
167  * the nativeMessaging communication.
168  * @param {String} operation The operation, to look up validity of returning messages
169  */
170 class Answer{
171
172     constructor(message){
173         this.operation = message.operation;
174         this.expected = message.expected;
175     }
176
177     /**
178      * Add the information to the answer
179      * @param {Object} msg The message as received with nativeMessaging
180      * returns true if successfull, gpgme_error otherwise
181      */
182     add(msg){
183         if (this._response === undefined){
184             this._response = {};
185         }
186         let messageKeys = Object.keys(msg);
187         let poa = permittedOperations[this.operation].answer;
188         if (messageKeys.length === 0){
189             return gpgme_error('CONN_UNEXPECTED_ANSWER');
190         }
191         for (let i= 0; i < messageKeys.length; i++){
192             let key = messageKeys[i];
193             switch (key) {
194                 case 'type':
195                     if ( msg.type !== 'error' && poa.type.indexOf(msg.type) < 0){
196                         return gpgme_error('CONN_UNEXPECTED_ANSWER');
197                     }
198                     break;
199                 case 'more':
200                     break;
201                 default:
202                     //data should be concatenated
203                     if (poa.data.indexOf(key) >= 0){
204                         if (!this._response.hasOwnProperty(key)){
205                             this._response[key] = '';
206                         }
207                         this._response[key] += msg[key];
208                     }
209                     //params should not change through the message
210                     else if (poa.params.indexOf(key) >= 0){
211                         if (!this._response.hasOwnProperty(key)){
212                             this._response[key] = msg[key];
213                         }
214                         else if (this._response[key] !== msg[key]){
215                                 return gpgme_error('CONN_UNEXPECTED_ANSWER',msg[key]);
216                         }
217                     }
218                     //infos may be json objects etc. Not yet defined.
219                     // Pushing them into arrays for now
220                     else if (poa.infos.indexOf(key) >= 0){
221                         if (!this._response.hasOwnProperty(key)){
222                             this._response[key] = [];
223                         }
224                         if (Array.isArray(msg[key])) {
225                             for (let i=0; i< msg[key].length; i++) {
226                                 this._response[key].push(msg[key][i]);
227                             }
228                         } else {
229                             this._response[key].push(msg[key][i]);
230                         }
231                     }
232                     else {
233                         return gpgme_error('CONN_UNEXPECTED_ANSWER');
234                     }
235                     break;
236             }
237         }
238         return true;
239     }
240
241     /**
242      * @returns {Object} the assembled message, original data assumed to be
243      * (javascript-) strings
244      */
245     get message(){
246         let keys = Object.keys(this._response);
247         let msg = {};
248         let poa = permittedOperations[this.operation].answer;
249         for (let i=0; i < keys.length; i++) {
250             if (poa.data.indexOf(keys[i]) >= 0
251                 && this._response.base64 === true
252             ) {
253                 msg[keys[i]] = atob(this._response[keys[i]]);
254                 if (this.expected === 'base64'){
255                     msg[keys[i]] = this._response[keys[i]];
256                 } else {
257                     msg[keys[i]] = decodeURIComponent(
258                         atob(this._response[keys[i]]).split('').map(function(c) {
259                             return '%' +
260                                 ('00' + c.charCodeAt(0).toString(16)).slice(-2);
261                         }).join(''));
262                 }
263             } else {
264                 msg[keys[i]] = this._response[keys[i]];
265             }
266         }
267         return msg;
268     }
269 }