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