js: consistently return uppercase fingerprint
[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         return this.get('armored', true);
307     }
308 }
309
310 /**
311  * Representing a subkey of a Key.
312  * @class
313  * @protected
314  */
315 class GPGME_Subkey {
316
317     /**
318      * Initializes with the json data sent by gpgme-json
319      * @param {Object} data
320      * @private
321      */
322     constructor(data){
323         let _data = {};
324         let keys = Object.keys(data);
325
326         /**
327          * Validates a subkey property against {@link validSubKeyProperties} and
328          * sets it if validation is successful
329          * @param {String} property
330          * @param {*} value
331          * @param private
332          */
333         const setProperty = function (property, value){
334             if (validSubKeyProperties.hasOwnProperty(property)){
335                 if (validSubKeyProperties[property](value) === true) {
336                     if (property === 'timestamp' || property === 'expires'){
337                         _data[property] = new Date(value * 1000);
338                     } else {
339                         _data[property] = value;
340                     }
341                 }
342             }
343         };
344         for (let i=0; i< keys.length; i++) {
345             setProperty(keys[i], data[keys[i]]);
346         }
347
348         /**
349          * Fetches any information about this subkey
350          * @param {String} property Information to request
351          * @returns {String | Number | Date}
352          */
353         this.get = function(property) {
354             if (_data.hasOwnProperty(property)){
355                 return (_data[property]);
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 };