js: small documentation fix
[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 is part of a Key pair including public and
183          * private key(s). If you want this information about more than a few
184          * Keys in synchronous mode, it may be advisable to run
185          * {@link Keyring.getKeys} instead, as it performs faster in bulk
186          * querying this state.
187          * @returns {Promise<Boolean|GPGME_Error>} True if a private Key is
188          * available in the gnupg Keyring.
189          * @async
190          */
191         this.getGnupgSecretState = function (){
192             return new Promise(function(resolve, reject) {
193                 if (!_data.fingerprint){
194                     reject(gpgme_error('KEY_INVALID'));
195                 } else {
196                     let msg = createMessage('keylist');
197                     msg.setParameter('keys', _data.fingerprint);
198                     msg.setParameter('secret', true);
199                     msg.post().then(function(result){
200                         _data.hasSecret = null;
201                         if (
202                             result.keys &&
203                             result.keys.length === 1 &&
204                             result.keys[0].secret === true
205                         ) {
206                             _data.hasSecret = true;
207                             resolve(true);
208                         } else {
209                             _data.hasSecret = false;
210                             resolve(false);
211                         }
212                     }, function(error){
213                         reject(error);
214                     });
215                 }
216             });
217         };
218
219         /**
220          * Deletes the (public) Key from the GPG Keyring. Note that a deletion
221          * of a secret key is not supported by the native backend.
222          * @returns {Promise<Boolean|GPGME_Error>} Success if key was deleted,
223          * rejects with a GPG error otherwise.
224          */
225         this.delete= function (){
226             return new Promise(function(resolve, reject){
227                 if (!_data.fingerprint){
228                     reject(gpgme_error('KEY_INVALID'));
229                 }
230                 let msg = createMessage('delete');
231                 msg.setParameter('key', _data.fingerprint);
232                 msg.post().then(function(result){
233                     resolve(result.success);
234                 }, function(error){
235                     reject(error);
236                 });
237             });
238         };
239     }
240
241     /**
242      * @returns {String} The fingerprint defining this Key. Convenience getter
243      */
244     get fingerprint(){
245         return this.getFingerprint();
246     }
247 }
248
249 /**
250  * Representing a subkey of a Key.
251  * @class
252  * @protected
253  */
254 class GPGME_Subkey {
255
256     /**
257      * Initializes with the json data sent by gpgme-json
258      * @param {Object} data
259      * @private
260      */
261     constructor(data){
262         let _data = {};
263         let keys = Object.keys(data);
264
265         /**
266          * Validates a subkey property against {@link validSubKeyProperties} and
267          * sets it if validation is successful
268          * @param {String} property
269          * @param {*} value
270          * @param private
271          */
272         const setProperty = function (property, value){
273             if (validSubKeyProperties.hasOwnProperty(property)){
274                 if (validSubKeyProperties[property](value) === true) {
275                     if (property === 'timestamp' || property === 'expires'){
276                         _data[property] = new Date(value * 1000);
277                     } else {
278                         _data[property] = value;
279                     }
280                 }
281             }
282         };
283         for (let i=0; i< keys.length; i++) {
284             setProperty(keys[i], data[keys[i]]);
285         }
286
287         /**
288          * Fetches any information about this subkey
289          * @param {String} property Information to request
290          * @returns {String | Number | Date}
291          */
292         this.get = function(property) {
293             if (_data.hasOwnProperty(property)){
294                 return (_data[property]);
295             }
296         };
297     }
298 }
299
300 /**
301  * Representing user attributes associated with a Key or subkey
302  * @class
303  * @protected
304  */
305 class GPGME_UserId {
306
307     /**
308      * Initializes with the json data sent by gpgme-json
309      * @param {Object} data
310      * @private
311      */
312     constructor(data){
313         let _data = {};
314         let keys = Object.keys(data);
315         const setProperty = function(property, value){
316             if (validUserIdProperties.hasOwnProperty(property)){
317                 if (validUserIdProperties[property](value) === true) {
318                     if (property === 'last_update'){
319                         _data[property] = new Date(value*1000);
320                     } else {
321                         _data[property] = value;
322                     }
323                 }
324             }
325         };
326         for (let i=0; i< keys.length; i++) {
327             setProperty(keys[i], data[keys[i]]);
328         }
329
330         /**
331          * Fetches information about the user
332          * @param {String} property Information to request
333          * @returns {String | Number}
334          */
335         this.get = function (property) {
336             if (_data.hasOwnProperty(property)){
337                 return (_data[property]);
338             }
339         };
340     }
341 }
342
343 /**
344  * Validation definition for userIds. Each valid userId property is represented
345  * as a key- Value pair, with their value being a validation function to check
346  * against
347  * @protected
348  * @const
349  */
350 const validUserIdProperties = {
351     'revoked': function(value){
352         return typeof(value) === 'boolean';
353     },
354     'invalid':  function(value){
355         return typeof(value) === 'boolean';
356     },
357     'uid': function(value){
358         if (typeof(value) === 'string' || value === ''){
359             return true;
360         }
361         return false;
362     },
363     'validity': function(value){
364         if (typeof(value) === 'string'){
365             return true;
366         }
367         return false;
368     },
369     'name': function(value){
370         if (typeof(value) === 'string' || value === ''){
371             return true;
372         }
373         return false;
374     },
375     'email': function(value){
376         if (typeof(value) === 'string' || value === ''){
377             return true;
378         }
379         return false;
380     },
381     'address': function(value){
382         if (typeof(value) === 'string' || value === ''){
383             return true;
384         }
385         return false;
386     },
387     'comment': function(value){
388         if (typeof(value) === 'string' || value === ''){
389             return true;
390         }
391         return false;
392     },
393     'origin':  function(value){
394         return Number.isInteger(value);
395     },
396     'last_update':  function(value){
397         return Number.isInteger(value);
398     }
399 };
400
401 /**
402  * Validation definition for subKeys. Each valid userId property is represented
403  * as a key-value pair, with the value being a validation function
404  * @protected
405  * @const
406  */
407 const validSubKeyProperties = {
408     'invalid': function(value){
409         return typeof(value) === 'boolean';
410     },
411     'can_encrypt': function(value){
412         return typeof(value) === 'boolean';
413     },
414     'can_sign': function(value){
415         return typeof(value) === 'boolean';
416     },
417     'can_certify':  function(value){
418         return typeof(value) === 'boolean';
419     },
420     'can_authenticate':  function(value){
421         return typeof(value) === 'boolean';
422     },
423     'secret': function(value){
424         return typeof(value) === 'boolean';
425     },
426     'is_qualified': function(value){
427         return typeof(value) === 'boolean';
428     },
429     'is_cardkey':  function(value){
430         return typeof(value) === 'boolean';
431     },
432     'is_de_vs':  function(value){
433         return typeof(value) === 'boolean';
434     },
435     'pubkey_algo_name': function(value){
436         return typeof(value) === 'string';
437         // TODO: check against list of known?['']
438     },
439     'pubkey_algo_string': function(value){
440         return typeof(value) === 'string';
441         // TODO: check against list of known?['']
442     },
443     'keyid': function(value){
444         return isLongId(value);
445     },
446     'pubkey_algo': function(value) {
447         return (Number.isInteger(value) && value >= 0);
448     },
449     'length': function(value){
450         return (Number.isInteger(value) && value > 0);
451     },
452     'timestamp': function(value){
453         return (Number.isInteger(value) && value > 0);
454     },
455     'expires': function(value){
456         return (Number.isInteger(value) && value > 0);
457     }
458 };
459
460 /**
461  * Validation definition for Keys. Each valid Key property is represented
462  * as a key-value pair, with their value being a validation function. For
463  * details on the meanings, please refer to the gpgme documentation
464  * https://www.gnupg.org/documentation/manuals/gpgme/Key-objects.html#Key-objects
465  * @param {String} fingerprint
466  * @param {Boolean} revoked
467  * @param {Boolean} expired
468  * @param {Boolean} disabled
469  * @param {Boolean} invalid
470  * @param {Boolean} can_encrypt
471  * @param {Boolean} can_sign
472  * @param {Boolean} can_certify
473  * @param {Boolean} can_authenticate
474  * @param {Boolean} secret
475  * @param {Boolean}is_qualified
476  * @param {String} protocol
477  * @param {String} issuer_serial
478  * @param {String} issuer_name
479  * @param {Boolean} chain_id
480  * @param {String} owner_trust
481  * @param {Date} last_update
482  * @param {String} origin
483  * @param {Array<GPGME_Subkey>} subkeys
484  * @param {Array<GPGME_UserId>} userids
485  * @param {Array<String>} tofu
486  * @param {Boolean} hasSecret
487  * @protected
488  * @const
489  */
490 const validKeyProperties = {
491     'fingerprint': function(value){
492         return isFingerprint(value);
493     },
494     'revoked': function(value){
495         return typeof(value) === 'boolean';
496     },
497     'expired': function(value){
498         return typeof(value) === 'boolean';
499     },
500     'disabled': function(value){
501         return typeof(value) === 'boolean';
502     },
503     'invalid': function(value){
504         return typeof(value) === 'boolean';
505     },
506     'can_encrypt': function(value){
507         return typeof(value) === 'boolean';
508     },
509     'can_sign': function(value){
510         return typeof(value) === 'boolean';
511     },
512     'can_certify': function(value){
513         return typeof(value) === 'boolean';
514     },
515     'can_authenticate': function(value){
516         return typeof(value) === 'boolean';
517     },
518     'secret': function(value){
519         return typeof(value) === 'boolean';
520     },
521     'is_qualified': function(value){
522         return typeof(value) === 'boolean';
523     },
524     'protocol': function(value){
525         return typeof(value) === 'string';
526         //TODO check for implemented ones
527     },
528     'issuer_serial': function(value){
529         return typeof(value) === 'string';
530     },
531     'issuer_name': function(value){
532         return typeof(value) === 'string';
533     },
534     'chain_id': function(value){
535         return typeof(value) === 'string';
536     },
537     'owner_trust': function(value){
538         return typeof(value) === 'string';
539     },
540     'last_update': function(value){
541         return (Number.isInteger(value));
542         //TODO undefined/null possible?
543     },
544     'origin': function(value){
545         return (Number.isInteger(value));
546     },
547     'subkeys': function(value){
548         return (Array.isArray(value));
549     },
550     'userids': function(value){
551         return (Array.isArray(value));
552     },
553     'tofu': function(value){
554         return (Array.isArray(value));
555     },
556     'hasSecret': function(value){
557         return typeof(value) === 'boolean';
558     }
559
560 };
561
562 /**
563 * sets the Key data in bulk. It can only be used from inside a Key, either
564 * during construction or on a refresh callback.
565 * @param {Object} key the original internal key data.
566 * @param {Object} data Bulk set the data for this key, with an Object structure
567 * as sent by gpgme-json.
568 * @returns {Object|GPGME_Error} the changed data after values have been set,
569 * an error if something went wrong.
570 * @private
571 */
572 function validateKeyData(data){
573     const key = {};
574     if ( typeof(data) !== 'object'
575     || !data.fingerprint){
576         return gpgme_error('KEY_INVALID');
577     }
578     let props = Object.keys(data);
579     for (let i=0; i< props.length; i++){
580         if (!validKeyProperties.hasOwnProperty(props[i])){
581             return gpgme_error('KEY_INVALID');
582         }
583         // running the defined validation function
584         if (validKeyProperties[props[i]](data[props[i]]) !== true ){
585             return gpgme_error('KEY_INVALID');
586         }
587         switch (props[i]){
588         case 'subkeys':
589             key.subkeys = [];
590             for (let i=0; i< data.subkeys.length; i++) {
591                 key.subkeys.push(Object.freeze(
592                     new GPGME_Subkey(data.subkeys[i])));
593             }
594             break;
595         case 'userids':
596             key.userids = [];
597             for (let i=0; i< data.userids.length; i++) {
598                 key.userids.push(Object.freeze(
599                     new GPGME_UserId(data.userids[i])));
600             }
601             break;
602         case 'last_update':
603             key[props[i]] = new Date( data[props[i]] * 1000 );
604             break;
605         default:
606             key[props[i]] = data[props[i]];
607         }
608     }
609     return key;
610 }
611
612 /**
613  * Fetches and sets properties from gnupg
614  * @param {String} fingerprint
615  * @param {String} property to search for.
616  * @private
617  * @async
618  */
619 function getGnupgState (fingerprint, property){
620     return new Promise(function(resolve, reject) {
621         if (!isFingerprint(fingerprint)) {
622             reject(gpgme_error('KEY_INVALID'));
623         } else {
624             let msg = createMessage('keylist');
625             msg.setParameter('keys', fingerprint);
626             msg.post().then(function(result){
627                 if (!result.keys || result.keys.length !== 1){
628                     reject(gpgme_error('KEY_INVALID'));
629                 } else {
630                     const key = result.keys[0];
631                     let result;
632                     switch (property){
633                     case 'subkeys':
634                         result = [];
635                         if (key.subkeys.length){
636                             for (let i=0; i < key.subkeys.length; i++) {
637                                 result.push(Object.freeze(
638                                     new GPGME_Subkey(key.subkeys[i])));
639                             }
640                         }
641                         resolve(result);
642                         break;
643                     case 'userids':
644                         result = [];
645                         if (key.userids.length){
646                             for (let i=0; i< key.userids.length; i++) {
647                                 result.push(Object.freeze(
648                                     new GPGME_UserId(key.userids[i])));
649                             }
650                         }
651                         resolve(result);
652                         break;
653                     case 'last_update':
654                         if (key.last_update === undefined){
655                             reject(gpgme_error('CONN_UNEXPECTED_ANSWER'));
656                         } else if (key.last_update !== null){
657                             resolve(new Date( key.last_update * 1000));
658                         } else {
659                             resolve(null);
660                         }
661                         break;
662                     default:
663                         if (!validKeyProperties.hasOwnProperty(property)){
664                             reject(gpgme_error('PARAM_WRONG'));
665                         } else {
666                             if (key.hasOwnProperty(property)){
667                                 resolve(key[property]);
668                             } else {
669                                 reject(gpgme_error(
670                                     'CONN_UNEXPECTED_ANSWER'));
671                             }
672                         }
673                         break;
674                     }
675                 }
676             }, function(error){
677                 reject(gpgme_error(error));
678             });
679         }
680     });
681 }