js: throw errors in sync functions
[gpgme.git] / lang / js / src / Key.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 import { isFingerprint, isLongId } from './Helpers';
25 import { gpgme_error } from './Errors';
26 import { createMessage } from './Message';
27
28 /**
29  * Validates the given fingerprint and creates a new {@link GPGME_Key}
30  * @param {String} fingerprint
31  * @param {Boolean} async If True, Key properties (except fingerprint) will be
32  * queried from gnupg on each call, making the operation up-to-date, the
33  * answers will be Promises, and the performance will likely suffer
34  * @param {Object} data additional initial properties this Key will have. Needs
35  * a full object as delivered by gpgme-json
36  * @returns {Object} The verified and updated data
37  */
38 export function createKey (fingerprint, async = false, data){
39     if (!isFingerprint(fingerprint) || typeof (async) !== 'boolean'){
40         throw gpgme_error('PARAM_WRONG');
41     }
42     if (data !== undefined){
43         data = validateKeyData(fingerprint, data);
44     }
45     if (data instanceof Error){
46         throw gpgme_error('KEY_INVALID');
47     } else {
48         return new GPGME_Key(fingerprint, async, data);
49     }
50 }
51
52 /**
53  * Represents the Keys as stored in the gnupg backend
54  * It allows to query almost all information defined in gpgme Key Objects
55  * Refer to {@link validKeyProperties} for available information, and the gpgme
56  * documentation on their meaning
57  * (https://www.gnupg.org/documentation/manuals/gpgme/Key-objects.html)
58  *
59  * @class
60  */
61 class GPGME_Key {
62
63     constructor (fingerprint, async, data){
64
65         /**
66          * @property {Boolean} If true, most answers will be asynchronous
67          */
68         this._async = async;
69
70         this._data = { fingerprint: fingerprint.toUpperCase() };
71         if (data !== undefined
72             && data.fingerprint.toUpperCase() === this._data.fingerprint
73         ) {
74             this._data = data;
75         }
76     }
77
78     /**
79      * Query any property of the Key listed in {@link validKeyProperties}
80      * @param {String} property property to be retreived
81      * @returns {Boolean| String | Date | Array | Object}
82      * the value of the property. If the Key is set to Async, the value
83      * will be fetched from gnupg and resolved as a Promise. If Key is not
84      * async, the armored property is not available (it can still be
85      * retrieved asynchronously by {@link Key.getArmor})
86      */
87     get (property) {
88         if (this._async === true) {
89             switch (property){
90             case 'armored':
91                 return this.getArmor();
92             case 'hasSecret':
93                 return this.getGnupgSecretState();
94             default:
95                 return getGnupgState(this.fingerprint, property);
96             }
97         } else {
98             if (property === 'armored') {
99                 throw gpgme_error('KEY_ASYNC_ONLY');
100             }
101             // eslint-disable-next-line no-use-before-define
102             if (!validKeyProperties.hasOwnProperty(property)){
103                 throw gpgme_error('PARAM_WRONG');
104             } else {
105                 return (this._data[property]);
106             }
107         }
108     }
109
110     /**
111      * Reloads the Key information from gnupg. This is only useful if you
112      * use the GPGME_Keys cached. Note that this is a performance hungry
113      * operation. If you desire more than a few refreshs, it may be
114      * advisable to run {@link Keyring.getKeys} instead.
115      * @returns {Promise<GPGME_Key|GPGME_Error>}
116      * @async
117      */
118     refreshKey () {
119         let me = this;
120         return new Promise(function (resolve, reject) {
121             if (!me._data.fingerprint){
122                 reject(gpgme_error('KEY_INVALID'));
123             }
124             let msg = createMessage('keylist');
125             msg.setParameter('sigs', true);
126             msg.setParameter('keys', me._data.fingerprint);
127             msg.post().then(function (result){
128                 if (result.keys.length === 1){
129                     const newdata = validateKeyData(
130                         me._data.fingerprint, result.keys[0]);
131                     if (newdata instanceof Error){
132                         reject(gpgme_error('KEY_INVALID'));
133                     } else {
134                         me._data = newdata;
135                         me.getGnupgSecretState().then(function (){
136                             me.getArmor().then(function (){
137                                 resolve(me);
138                             }, function (error){
139                                 reject(error);
140                             });
141                         }, function (error){
142                             reject(error);
143                         });
144                     }
145                 } else {
146                     reject(gpgme_error('KEY_NOKEY'));
147                 }
148             }, function (error) {
149                 reject(gpgme_error('GNUPG_ERROR'), error);
150             });
151         });
152     }
153
154     /**
155      * Query the armored block of the Key directly from gnupg. Please note
156      * that this will not get you any export of the secret/private parts of
157      * a Key
158      * @returns {Promise<String|GPGME_Error>}
159      * @async
160      */
161     getArmor () {
162         const me = this;
163         return new Promise(function (resolve, reject) {
164             if (!me._data.fingerprint){
165                 reject(gpgme_error('KEY_INVALID'));
166             }
167             let msg = createMessage('export');
168             msg.setParameter('armor', true);
169             msg.setParameter('keys', me._data.fingerprint);
170             msg.post().then(function (result){
171                 resolve(result.data);
172             }, function (error){
173                 reject(error);
174             });
175         });
176     }
177
178     /**
179      * Find out if the Key is part of a Key pair including public and
180      * private key(s). If you want this information about more than a few
181      * Keys in synchronous mode, it may be advisable to run
182      * {@link Keyring.getKeys} instead, as it performs faster in bulk
183      * querying this state.
184      * @returns {Promise<Boolean|GPGME_Error>} True if a private Key is
185      * available in the gnupg Keyring.
186      * @async
187      */
188     getGnupgSecretState (){
189         const me = this;
190         return new Promise(function (resolve, reject) {
191             if (!me._data.fingerprint){
192                 reject(gpgme_error('KEY_INVALID'));
193             } else {
194                 let msg = createMessage('keylist');
195                 msg.setParameter('keys', me._data.fingerprint);
196                 msg.setParameter('secret', true);
197                 msg.post().then(function (result){
198                     me._data.hasSecret = null;
199                     if (
200                         result.keys &&
201                         result.keys.length === 1 &&
202                         result.keys[0].secret === true
203                     ) {
204                         me._data.hasSecret = true;
205                         resolve(true);
206                     } else {
207                         me._data.hasSecret = false;
208                         resolve(false);
209                     }
210                 }, function (error){
211                     reject(error);
212                 });
213             }
214         });
215     }
216
217     /**
218      * Deletes the (public) Key from the GPG Keyring. Note that a deletion
219      * of a secret key is not supported by the native backend.
220      * @returns {Promise<Boolean|GPGME_Error>} Success if key was deleted,
221      * rejects with a GPG error otherwise.
222      */
223     delete (){
224         const me = this;
225         return new Promise(function (resolve, reject){
226             if (!me._data.fingerprint){
227                 reject(gpgme_error('KEY_INVALID'));
228             }
229             let msg = createMessage('delete');
230             msg.setParameter('key', me._data.fingerprint);
231             msg.post().then(function (result){
232                 resolve(result.success);
233             }, function (error){
234                 reject(error);
235             });
236         });
237     }
238
239     /**
240      * @returns {String} The fingerprint defining this Key. Convenience getter
241      */
242     get fingerprint (){
243         return this._data.fingerprint;
244     }
245 }
246
247 /**
248  * Representing a subkey of a Key.
249  * @class
250  * @protected
251  */
252 class GPGME_Subkey {
253
254     /**
255      * Initializes with the json data sent by gpgme-json
256      * @param {Object} data
257      * @private
258      */
259     constructor (data){
260         this._data = {};
261         let keys = Object.keys(data);
262         const me = this;
263
264         /**
265          * Validates a subkey property against {@link validSubKeyProperties} and
266          * sets it if validation is successful
267          * @param {String} property
268          * @param {*} value
269          * @param private
270          */
271         const setProperty = function (property, value){
272             // eslint-disable-next-line no-use-before-define
273             if (validSubKeyProperties.hasOwnProperty(property)){
274                 // eslint-disable-next-line no-use-before-define
275                 if (validSubKeyProperties[property](value) === true) {
276                     if (property === 'timestamp' || property === 'expires'){
277                         me._data[property] = new Date(value * 1000);
278                     } else {
279                         me._data[property] = value;
280                     }
281                 }
282             }
283         };
284         for (let i=0; i< keys.length; i++) {
285             setProperty(keys[i], data[keys[i]]);
286         }
287     }
288
289     /**
290      * Fetches any information about this subkey
291      * @param {String} property Information to request
292      * @returns {String | Number | Date}
293      */
294     get (property) {
295         if (this._data.hasOwnProperty(property)){
296             return (this._data[property]);
297         }
298     }
299
300 }
301
302 /**
303  * Representing user attributes associated with a Key or subkey
304  * @class
305  * @protected
306  */
307 class GPGME_UserId {
308
309     /**
310      * Initializes with the json data sent by gpgme-json
311      * @param {Object} data
312      * @private
313      */
314     constructor (data){
315         this._data = {};
316         const me = this;
317         let keys = Object.keys(data);
318         const setProperty = function (property, value){
319             // eslint-disable-next-line no-use-before-define
320             if (validUserIdProperties.hasOwnProperty(property)){
321                 // eslint-disable-next-line no-use-before-define
322                 if (validUserIdProperties[property](value) === true) {
323                     if (property === 'last_update'){
324                         me._data[property] = new Date(value*1000);
325                     } else {
326                         me._data[property] = value;
327                     }
328                 }
329             }
330         };
331         for (let i=0; i< keys.length; i++) {
332             setProperty(keys[i], data[keys[i]]);
333         }
334     }
335
336     /**
337      * Fetches information about the user
338      * @param {String} property Information to request
339      * @returns {String | Number}
340      */
341     get (property) {
342         if (this._data.hasOwnProperty(property)){
343             return (this._data[property]);
344         }
345     }
346
347 }
348
349 /**
350  * Validation definition for userIds. Each valid userId property is represented
351  * as a key- Value pair, with their value being a validation function to check
352  * against
353  * @protected
354  * @const
355  */
356 const validUserIdProperties = {
357     'revoked': function (value){
358         return typeof (value) === 'boolean';
359     },
360     'invalid':  function (value){
361         return typeof (value) === 'boolean';
362     },
363     'uid': function (value){
364         if (typeof (value) === 'string' || value === ''){
365             return true;
366         }
367         return false;
368     },
369     'validity': function (value){
370         if (typeof (value) === 'string'){
371             return true;
372         }
373         return false;
374     },
375     'name': function (value){
376         if (typeof (value) === 'string' || value === ''){
377             return true;
378         }
379         return false;
380     },
381     'email': function (value){
382         if (typeof (value) === 'string' || value === ''){
383             return true;
384         }
385         return false;
386     },
387     'address': function (value){
388         if (typeof (value) === 'string' || value === ''){
389             return true;
390         }
391         return false;
392     },
393     'comment': function (value){
394         if (typeof (value) === 'string' || value === ''){
395             return true;
396         }
397         return false;
398     },
399     'origin':  function (value){
400         return Number.isInteger(value);
401     },
402     'last_update':  function (value){
403         return Number.isInteger(value);
404     }
405 };
406
407 /**
408  * Validation definition for subKeys. Each valid userId property is represented
409  * as a key-value pair, with the value being a validation function
410  * @protected
411  * @const
412  */
413 const validSubKeyProperties = {
414     'invalid': function (value){
415         return typeof (value) === 'boolean';
416     },
417     'can_encrypt': function (value){
418         return typeof (value) === 'boolean';
419     },
420     'can_sign': function (value){
421         return typeof (value) === 'boolean';
422     },
423     'can_certify':  function (value){
424         return typeof (value) === 'boolean';
425     },
426     'can_authenticate':  function (value){
427         return typeof (value) === 'boolean';
428     },
429     'secret': function (value){
430         return typeof (value) === 'boolean';
431     },
432     'is_qualified': function (value){
433         return typeof (value) === 'boolean';
434     },
435     'is_cardkey':  function (value){
436         return typeof (value) === 'boolean';
437     },
438     'is_de_vs':  function (value){
439         return typeof (value) === 'boolean';
440     },
441     'pubkey_algo_name': function (value){
442         return typeof (value) === 'string';
443         // TODO: check against list of known?['']
444     },
445     'pubkey_algo_string': function (value){
446         return typeof (value) === 'string';
447         // TODO: check against list of known?['']
448     },
449     'keyid': function (value){
450         return isLongId(value);
451     },
452     'pubkey_algo': function (value) {
453         return (Number.isInteger(value) && value >= 0);
454     },
455     'length': function (value){
456         return (Number.isInteger(value) && value > 0);
457     },
458     'timestamp': function (value){
459         return (Number.isInteger(value) && value > 0);
460     },
461     'expires': function (value){
462         return (Number.isInteger(value) && value > 0);
463     }
464 };
465
466 /**
467  * Validation definition for Keys. Each valid Key property is represented
468  * as a key-value pair, with their value being a validation function. For
469  * details on the meanings, please refer to the gpgme documentation
470  * https://www.gnupg.org/documentation/manuals/gpgme/Key-objects.html#Key-objects
471  * @param {String} fingerprint
472  * @param {Boolean} revoked
473  * @param {Boolean} expired
474  * @param {Boolean} disabled
475  * @param {Boolean} invalid
476  * @param {Boolean} can_encrypt
477  * @param {Boolean} can_sign
478  * @param {Boolean} can_certify
479  * @param {Boolean} can_authenticate
480  * @param {Boolean} secret
481  * @param {Boolean}is_qualified
482  * @param {String} protocol
483  * @param {String} issuer_serial
484  * @param {String} issuer_name
485  * @param {Boolean} chain_id
486  * @param {String} owner_trust
487  * @param {Date} last_update
488  * @param {String} origin
489  * @param {Array<GPGME_Subkey>} subkeys
490  * @param {Array<GPGME_UserId>} userids
491  * @param {Array<String>} tofu
492  * @param {Boolean} hasSecret
493  * @protected
494  * @const
495  */
496 const validKeyProperties = {
497     'fingerprint': function (value){
498         return isFingerprint(value);
499     },
500     'revoked': function (value){
501         return typeof (value) === 'boolean';
502     },
503     'expired': function (value){
504         return typeof (value) === 'boolean';
505     },
506     'disabled': function (value){
507         return typeof (value) === 'boolean';
508     },
509     'invalid': function (value){
510         return typeof (value) === 'boolean';
511     },
512     'can_encrypt': function (value){
513         return typeof (value) === 'boolean';
514     },
515     'can_sign': function (value){
516         return typeof (value) === 'boolean';
517     },
518     'can_certify': function (value){
519         return typeof (value) === 'boolean';
520     },
521     'can_authenticate': function (value){
522         return typeof (value) === 'boolean';
523     },
524     'secret': function (value){
525         return typeof (value) === 'boolean';
526     },
527     'is_qualified': function (value){
528         return typeof (value) === 'boolean';
529     },
530     'protocol': function (value){
531         return typeof (value) === 'string';
532         // TODO check for implemented ones
533     },
534     'issuer_serial': function (value){
535         return typeof (value) === 'string';
536     },
537     'issuer_name': function (value){
538         return typeof (value) === 'string';
539     },
540     'chain_id': function (value){
541         return typeof (value) === 'string';
542     },
543     'owner_trust': function (value){
544         return typeof (value) === 'string';
545     },
546     'last_update': function (value){
547         return (Number.isInteger(value));
548         // TODO undefined/null possible?
549     },
550     'origin': function (value){
551         return (Number.isInteger(value));
552     },
553     'subkeys': function (value){
554         return (Array.isArray(value));
555     },
556     'userids': function (value){
557         return (Array.isArray(value));
558     },
559     'tofu': function (value){
560         return (Array.isArray(value));
561     },
562     'hasSecret': function (value){
563         return typeof (value) === 'boolean';
564     }
565
566 };
567
568 /**
569 * sets the Key data in bulk. It can only be used from inside a Key, either
570 * during construction or on a refresh callback.
571 * @param {Object} key the original internal key data.
572 * @param {Object} data Bulk set the data for this key, with an Object structure
573 * as sent by gpgme-json.
574 * @returns {Object|GPGME_Error} the changed data after values have been set,
575 * an error if something went wrong.
576 * @private
577 */
578 function validateKeyData (fingerprint, data){
579     const key = {};
580     if (!fingerprint || typeof (data) !== 'object' || !data.fingerprint
581      || fingerprint !== data.fingerprint.toUpperCase()
582     ){
583         return gpgme_error('KEY_INVALID');
584     }
585     let props = Object.keys(data);
586     for (let i=0; i< props.length; i++){
587         if (!validKeyProperties.hasOwnProperty(props[i])){
588             return gpgme_error('KEY_INVALID');
589         }
590         // running the defined validation function
591         if (validKeyProperties[props[i]](data[props[i]]) !== true ){
592             return gpgme_error('KEY_INVALID');
593         }
594         switch (props[i]){
595         case 'subkeys':
596             key.subkeys = [];
597             for (let i=0; i< data.subkeys.length; i++) {
598                 key.subkeys.push(
599                     new GPGME_Subkey(data.subkeys[i]));
600             }
601             break;
602         case 'userids':
603             key.userids = [];
604             for (let i=0; i< data.userids.length; i++) {
605                 key.userids.push(
606                     new GPGME_UserId(data.userids[i]));
607             }
608             break;
609         case 'last_update':
610             key[props[i]] = new Date( data[props[i]] * 1000 );
611             break;
612         default:
613             key[props[i]] = data[props[i]];
614         }
615     }
616     return key;
617 }
618
619 /**
620  * Fetches and sets properties from gnupg
621  * @param {String} fingerprint
622  * @param {String} property to search for.
623  * @private
624  * @async
625  */
626 function getGnupgState (fingerprint, property){
627     return new Promise(function (resolve, reject) {
628         if (!isFingerprint(fingerprint)) {
629             reject(gpgme_error('KEY_INVALID'));
630         } else {
631             let msg = createMessage('keylist');
632             msg.setParameter('keys', fingerprint);
633             msg.post().then(function (res){
634                 if (!res.keys || res.keys.length !== 1){
635                     reject(gpgme_error('KEY_INVALID'));
636                 } else {
637                     const key = res.keys[0];
638                     let result;
639                     switch (property){
640                     case 'subkeys':
641                         result = [];
642                         if (key.subkeys.length){
643                             for (let i=0; i < key.subkeys.length; i++) {
644                                 result.push(
645                                     new GPGME_Subkey(key.subkeys[i]));
646                             }
647                         }
648                         resolve(result);
649                         break;
650                     case 'userids':
651                         result = [];
652                         if (key.userids.length){
653                             for (let i=0; i< key.userids.length; i++) {
654                                 result.push(
655                                     new GPGME_UserId(key.userids[i]));
656                             }
657                         }
658                         resolve(result);
659                         break;
660                     case 'last_update':
661                         if (key.last_update === undefined){
662                             reject(gpgme_error('CONN_UNEXPECTED_ANSWER'));
663                         } else if (key.last_update !== null){
664                             resolve(new Date( key.last_update * 1000));
665                         } else {
666                             resolve(null);
667                         }
668                         break;
669                     default:
670                         if (!validKeyProperties.hasOwnProperty(property)){
671                             reject(gpgme_error('PARAM_WRONG'));
672                         } else {
673                             if (key.hasOwnProperty(property)){
674                                 resolve(key[property]);
675                             } else {
676                                 reject(gpgme_error(
677                                     'CONN_UNEXPECTED_ANSWER'));
678                             }
679                         }
680                         break;
681                     }
682                 }
683             }, function (error){
684                 reject(gpgme_error(error));
685             });
686         }
687     });
688 }