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