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