1a5d55748323b593d2f6d7b17ad39c64a5841193
[gpgme.git] / lang / js / src / gpgmejs.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
25 import { GPGME_Message, createMessage } from './Message';
26 import { toKeyIdArray } from './Helpers';
27 import { gpgme_error } from './Errors';
28 import { GPGME_Keyring } from './Keyring';
29 import { createSignature } from './Signature';
30
31 /**
32  * @typedef {Object} decrypt_result
33  * @property {String|Uint8Array} data The decrypted data
34  * @property {String} format Indicating how the data was converted after being
35  * received from gpgme.
36  *      'string': Data was decoded into an utf-8 string,
37  *      'base64': Data was not processed and is a base64 string
38  *      'uint8': data was turned into a Uint8Array
39  * @property {Boolean} is_mime (optional) the data claims to be a MIME
40  * object.
41  * @property {String} file_name (optional) the original file name
42  * @property {signatureDetails} signatures Verification details for
43  * signatures
44  */
45
46 /**
47  * @typedef {Object} signatureDetails
48  * @property {Boolean} all_valid Summary if all signatures are fully valid
49  * @property {Number} count Number of signatures found
50  * @property {Number} failures Number of invalid signatures
51  * @property {Array<GPGME_Signature>} signatures.good All valid signatures
52  * @property {Array<GPGME_Signature>} signatures.bad All invalid signatures
53  */
54
55 /**
56  * @typedef {Object} encrypt_result The result of an encrypt operation
57  * @property {String} data The encrypted message
58  * @property {Boolean} base64 Indicating whether returning payload data is
59  * base64 encoded
60  */
61
62 /**
63  * @typedef { GPGME_Key | String | Object } inputKeys
64  * Accepts different identifiers of a gnupg Key that can be parsed by
65  * {@link toKeyIdArray}. Expected inputs are: One or an array of
66  * GPGME_Keys; one or an array of fingerprint strings; one or an array of
67  * openpgpjs Key objects.
68  */
69
70 /**
71  * @typedef {Object} signResult The result of a signing operation
72  * @property {String} data The resulting data. Includes the signature in
73  *  clearsign mode
74  * @property {String} signature The detached signature (if in detached mode)
75  */
76
77 /** @typedef {Object} verifyResult The result of a verification
78  * @property {Boolean} data: The verified data
79  * @property {Boolean} is_mime (optional) the data claims to be a MIME
80  * object.
81  * @property {String} file_name (optional) the original file name
82  * @property {signatureDetails} signatures Verification details for
83  * signatures
84  */
85
86 /**
87  * The main entry point for gpgme.js.
88  * @class
89  */
90 export class GpgME {
91
92     constructor (){
93         this._Keyring = null;
94     }
95
96     /**
97      * setter for {@link setKeyring}.
98      * @param {GPGME_Keyring} keyring A Keyring to use
99      */
100     set Keyring (keyring){
101         if (keyring && keyring instanceof GPGME_Keyring){
102             this._Keyring = keyring;
103         }
104     }
105     /**
106      * Accesses the {@link GPGME_Keyring}.
107      */
108     get Keyring (){
109         if (!this._Keyring){
110             this._Keyring = new GPGME_Keyring;
111         }
112         return this._Keyring;
113     }
114
115     /**
116      * Encrypt (and optionally sign) data
117      * @param {Object} options
118      * @param {String|Object} options.data text/data to be encrypted as String.
119      * Also accepts Objects with a getText method
120      * @param {inputKeys} options.publicKeys
121      * Keys used to encrypt the message
122      * @param {inputKeys} opions.secretKeys (optional) Keys used to sign the
123      * message. If Keys are present, the  operation requested is assumed
124      * to be 'encrypt and sign'
125      * @param {Boolean} options.base64 (optional) The data will be interpreted
126      * as base64 encoded data.
127      * @param {Boolean} options.armor (optional) Request the output as armored
128      * block.
129      * @param {Boolean} options.wildcard (optional) If true, recipient
130      * information will not be added to the message.
131      * @param {Boolean} always_trust (optional, default true) This assumes that
132      * used keys are fully trusted. If set to false, encryption to a key not
133      * fully trusted in gnupg will fail
134      * @param {Object} additional use additional valid gpg options as
135      * defined in {@link permittedOperations}
136      * @returns {Promise<encrypt_result>} Object containing the encrypted
137      * message and additional info.
138      * @async
139      */
140     encrypt ({ data, publicKeys, secretKeys, base64 = false, armor = true,
141         wildcard, always_trust = true, additional = {} }){
142         if (!data || !publicKeys){
143             return Promise.reject(gpgme_error('MSG_INCOMPLETE'));
144         }
145         let msg = createMessage('encrypt');
146         if (msg instanceof Error){
147             return Promise.reject(msg);
148         }
149         msg.setParameter('armor', armor);
150
151         if (base64 === true) {
152             msg.setParameter('base64', true);
153         }
154         if (always_trust === true) {
155             msg.setParameter('always-trust', true);
156         }
157         let pubkeys = toKeyIdArray(publicKeys);
158         if (!pubkeys.length) {
159             return Promise.reject(gpgme_error('MSG_NO_KEYS'));
160         }
161         msg.setParameter('keys', pubkeys);
162         let sigkeys = toKeyIdArray(secretKeys);
163         if (sigkeys.length > 0) {
164             msg.setParameter('signing_keys', sigkeys);
165         }
166         putData(msg, data);
167         if (wildcard === true){
168             msg.setParameter('throw-keyids', true);
169         }
170         if (additional){
171             let additional_Keys = Object.keys(additional);
172             for (let k = 0; k < additional_Keys.length; k++) {
173                 try {
174                     msg.setParameter(additional_Keys[k],
175                         additional[additional_Keys[k]]);
176                 }
177                 catch (error){
178                     return Promise.reject(error);
179                 }
180             }
181         }
182         if (msg.isComplete() === true){
183             return msg.post();
184         } else {
185             return Promise.reject(gpgme_error('MSG_INCOMPLETE'));
186         }
187     }
188
189     /**
190     * Decrypts a Message
191     * @param {Object} options
192     * @param {String|Object} options.data text/data to be decrypted. Accepts
193     * Strings and Objects with a getText method
194     * @param {Boolean} options.base64 (optional) false if the data is an
195     * armored block, true if it is base64 encoded binary data
196     * @param {String} options.expect (optional) can be set to 'uint8' or
197     * 'base64'. Does no extra decoding on the data, and returns the decoded
198     * data as either Uint8Array or unprocessed(base64 encoded) string.
199     * @returns {Promise<decrypt_result>} Decrypted Message and information
200     * @async
201     */
202     decrypt ({ data, base64, expect }){
203         if (!data){
204             return Promise.reject(gpgme_error('MSG_EMPTY'));
205         }
206         let msg = createMessage('decrypt');
207
208         if (msg instanceof Error){
209             return Promise.reject(msg);
210         }
211         if (base64 === true){
212             msg.setParameter('base64', true);
213         }
214         if (expect === 'base64' || expect === 'uint8'){
215             msg.expected = expect;
216         }
217         putData(msg, data);
218         return new Promise(function (resolve, reject){
219             msg.post().then(function (result){
220                 let _result = { data: result.data };
221                 _result.format = result.format ? result.format : null;
222                 if (result.hasOwnProperty('dec_info')){
223                     _result.is_mime = result.dec_info.is_mime ? true: false;
224                     if (result.dec_info.file_name) {
225                         _result.file_name = result.dec_info.file_name;
226                     }
227                 }
228                 if (!result.file_name) {
229                     _result.file_name = null;
230                 }
231                 if (result.hasOwnProperty('info')
232                     && result.info.hasOwnProperty('signatures')
233                     && Array.isArray(result.info.signatures)
234                 ) {
235                     _result.signatures = collectSignatures(
236                         result.info.signatures);
237                 }
238                 if (_result.signatures instanceof Error){
239                     reject(_result.signatures);
240                 } else {
241                     resolve(_result);
242                 }
243             }, function (error){
244                 reject(error);
245             });
246         });
247     }
248
249     /**
250      * Sign a Message
251      * @param {Object} options Signing options
252      * @param {String|Object} options.data text/data to be signed. Accepts
253      * Strings and Objects with a getText method.
254      * @param {inputKeys} options.keys The key/keys to use for signing
255      * @param {String} options.mode The signing mode. Currently supported:
256      *  'clearsign':The Message is embedded into the signature;
257      *  'detached': The signature is stored separately
258      * @param {Boolean} options.base64 input is considered base64
259      * @returns {Promise<signResult>}
260      * @async
261      */
262     sign ({ data, keys, mode = 'clearsign', base64 }){
263         if (!data){
264             return Promise.reject(gpgme_error('MSG_EMPTY'));
265         }
266         let key_arr = toKeyIdArray(keys);
267         if (key_arr.length === 0){
268             return Promise.reject(gpgme_error('MSG_NO_KEYS'));
269         }
270
271         let msg = createMessage('sign');
272         msg.setParameter('keys', key_arr);
273         if (base64 === true){
274             msg.setParameter('base64', true);
275         }
276         msg.setParameter('mode', mode);
277         putData(msg, data);
278
279         return new Promise(function (resolve,reject) {
280             msg.post().then( function (message) {
281                 if (mode === 'clearsign'){
282                     resolve({
283                         data: message.data }
284                     );
285                 } else if (mode === 'detached') {
286                     resolve({
287                         data: data,
288                         signature: message.data
289                     });
290                 }
291             }, function (error){
292                 reject(error);
293             });
294         });
295     }
296
297     /**
298      * Verifies data.
299      * @param {Object} options
300      * @param {String|Object} options.data text/data to be verified. Accepts
301      * Strings and Objects with a getText method
302      * @param {String} options.signature A detached signature. If not present,
303      * opaque mode is assumed
304      * @param {Boolean} options.base64 Indicating that data and signature are
305      * base64 encoded
306      * @returns {Promise<verifyResult>}
307      *@async
308     */
309     verify ({ data, signature, base64 }){
310         if (!data){
311             return Promise.reject(gpgme_error('PARAM_WRONG'));
312         }
313         let msg = createMessage('verify');
314         let dt = putData(msg, data);
315         if (dt instanceof Error){
316             return Promise.reject(dt);
317         }
318         if (signature){
319             if (typeof signature !== 'string'){
320                 return Promise.reject(gpgme_error('PARAM_WRONG'));
321             } else {
322                 msg.setParameter('signature', signature);
323             }
324         }
325         if (base64 === true){
326             msg.setParameter('base64', true);
327         }
328         return new Promise(function (resolve, reject){
329             msg.post().then(function (message){
330                 if (!message.info || !message.info.signatures){
331                     reject(gpgme_error('SIG_NO_SIGS'));
332                 } else {
333                     let _result = {
334                         signatures: collectSignatures(message.info.signatures)
335                     };
336                     if (_result.signatures instanceof Error){
337                         reject(_result.signatures);
338                     } else {
339                         _result.is_mime = message.info.is_mime? true: false;
340                         if (message.info.filename){
341                             _result.file_name = message.info.filename;
342                         }
343                         _result.data = message.data;
344                         resolve(_result);
345                     }
346                 }
347             }, function (error){
348                 reject(error);
349             });
350         });
351     }
352 }
353
354 /**
355  * Sets the data of the message, setting flags according on the data type
356  * @param {GPGME_Message} message The message where this data will be set
357  * @param { String| Object } data The data to enter. Expects either a string of
358  * data, or an object with a getText method
359  * @returns {undefined| GPGME_Error} Error if not successful, nothing otherwise
360  * @private
361  */
362 function putData (message, data){
363     if (!message || !(message instanceof GPGME_Message)) {
364         return gpgme_error('PARAM_WRONG');
365     }
366     if (!data){
367         return gpgme_error('PARAM_WRONG');
368     } else if (typeof data === 'string') {
369         message.setParameter('data', data);
370     } else if (
371         (typeof data === 'object') &&
372         (typeof data.getText === 'function')
373     ){
374         let txt = data.getText();
375         if (typeof txt === 'string'){
376             message.setParameter('data', txt);
377         } else {
378             return gpgme_error('PARAM_WRONG');
379         }
380
381     } else {
382         return gpgme_error('PARAM_WRONG');
383     }
384 }
385
386 /**
387  * Parses, validates and converts incoming objects into signatures.
388  * @param {Array<Object>} sigs
389  * @returns {signatureDetails} Details about the signatures
390  */
391 function collectSignatures (sigs){
392     if (!Array.isArray(sigs)){
393         return gpgme_error('SIG_NO_SIGS');
394     }
395     let summary = {
396         all_valid: false,
397         count: sigs.length,
398         failures: 0,
399         signatures: {
400             good: [],
401             bad: [],
402         }
403     };
404     for (let i=0; i< sigs.length; i++){
405         let sigObj = createSignature(sigs[i]);
406         if (sigObj instanceof Error) {
407             return gpgme_error('SIG_WRONG');
408         }
409         if (sigObj.valid !== true){
410             summary.failures += 1;
411             summary.signatures.bad.push(sigObj);
412         } else {
413             summary.signatures.good.push(sigObj);
414         }
415     }
416     if (summary.failures === 0){
417         summary.all_valid = true;
418     }
419     return summary;
420 }