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