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