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