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