js: include armored Key in import callback
[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 false, 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                         me.getArmor().then(function(){
198                             resolve(me);
199                         }, function(error){
200                             reject(error);
201                         });
202                     }, function(error){
203                         reject(error);
204                     });
205                 } else {
206                     reject(gpgme_error('KEY_NOKEY'));
207                 }
208             }, function (error) {
209                 reject(gpgme_error('GNUPG_ERROR'), error);
210             });
211         });
212     }
213
214     /**
215      * Query the armored block of the Key directly from gnupg. Please note that
216      * this will not get you any export of the secret/private parts of a Key
217      * @returns {Promise<String|GPGME_Error>}
218      * @async
219      */
220     getArmor(){
221         let me = this;
222         return new Promise(function(resolve, reject) {
223             if (!me._data.fingerprint){
224                 reject(gpgme_error('KEY_INVALID'));
225             }
226             let msg = createMessage('export');
227             msg.setParameter('armor', true);
228             msg.setParameter('keys', me._data.fingerprint);
229             msg.post().then(function(result){
230                 me._data.armored = result.data;
231                 resolve(result.data);
232             }, function(error){
233                 reject(error);
234             });
235         });
236     }
237
238     /**
239      * Find out if the Key includes a secret part. Note that this is a rather
240      * nonperformant operation, as it needs to query gnupg twice. If you want
241      * this inforrmation about more than a few Keys, it may be advisable to run
242      * {@link Keyring.getKeys} instead.
243      * @returns {Promise<Boolean|GPGME_Error>} True if a secret/private Key is
244      * available in the gnupg Keyring
245      * @async
246      */
247     getHasSecret(){
248         let me = this;
249         return new Promise(function(resolve, reject) {
250             if (!me._data.fingerprint){
251                 reject(gpgme_error('KEY_INVALID'));
252             }
253             let msg = createMessage('keylist');
254             msg.setParameter('keys', me._data.fingerprint);
255             msg.setParameter('secret', true);
256             msg.post().then(function(result){
257                 me._data.hasSecret = null;
258                 if (
259                     result.keys &&
260                     result.keys.length === 1 &&
261                     result.keys[0].secret === true
262                 ) {
263                     me._data.hasSecret = true;
264                     resolve(true);
265                 } else {
266                     me._data.hasSecret = false;
267                     resolve(false);
268                 }
269             }, function(error){
270                 reject(error);
271             });
272         });
273     }
274
275     /**
276      * Convenience functions to be directly used as properties of the Key
277      * Notice that these rely on cached info and may be outdated. Use the async
278      * get(property, false) if you need the most current info
279      */
280
281     /**
282      * Property for the export of armored Key. If the armored Key is not
283      * cached, it returns an {@link GPGME_Error} with code 'KEY_NO_INIT'.
284      * Running {@link refreshKey} may help in this case.
285      * @returns {String|GPGME_Error} The armored public Key block.
286      */
287     get armored(){
288         return this.get('armored', true);
289     }
290
291     /**
292      * Property indicating if the Key possesses a private/secret part. If this
293      * information is not yet cached, it returns an {@link GPGME_Error} with
294      * code 'KEY_NO_INIT'.  Running {@link refreshKey} may help in this case.
295      * @returns {Boolean} If the Key has a secret subkey.
296      */
297     get hasSecret(){
298         return this.get('hasSecret', true);
299     }
300
301     /**
302      * Deletes the (public) Key from the GPG Keyring. Note that a deletion of a
303      * secret key is not supported by the native backend.
304      * @returns {Promise<Boolean|GPGME_Error>} Success if key was deleted,
305      * rejects with a GPG error otherwise.
306      */
307     delete(){
308         let me = this;
309         return new Promise(function(resolve, reject){
310             if (!me._data.fingerprint){
311                 reject(gpgme_error('KEY_INVALID'));
312             }
313             let msg = createMessage('delete');
314             msg.setParameter('key', me._data.fingerprint);
315             msg.post().then(function(result){
316                 resolve(result.success);
317             }, function(error){
318                 reject(error);
319             });
320         });
321     }
322 }
323
324 /**
325  * Representing a subkey of a Key.
326  * @class
327  * @protected
328  */
329 class GPGME_Subkey {
330
331     /**
332      * Initializes with the json data sent by gpgme-json
333      * @param {Object} data
334      * @private
335      */
336     constructor(data){
337         let keys = Object.keys(data);
338         for (let i=0; i< keys.length; i++) {
339             this.setProperty(keys[i], data[keys[i]]);
340         }
341     }
342
343     /**
344      * Validates a subkey property against {@link validSubKeyProperties} and
345      * sets it if validation is successful
346      * @param {String} property
347      * @param {*} value
348      * @param private
349      */
350     setProperty(property, value){
351         if (!this._data){
352             this._data = {};
353         }
354         if (validSubKeyProperties.hasOwnProperty(property)){
355             if (validSubKeyProperties[property](value) === true) {
356                 if (property === 'timestamp' || property === 'expires'){
357                     this._data[property] = new Date(value * 1000);
358                 } else {
359                     this._data[property] = value;
360                 }
361             }
362         }
363     }
364
365     /**
366      * Fetches any information about this subkey
367      * @param {String} property Information to request
368      * @returns {String | Number | Date}
369      */
370     get(property) {
371         if (this._data.hasOwnProperty(property)){
372             return (this._data[property]);
373         }
374     }
375 }
376
377 /**
378  * Representing user attributes associated with a Key or subkey
379  * @class
380  * @protected
381  */
382 class GPGME_UserId {
383
384     /**
385      * Initializes with the json data sent by gpgme-json
386      * @param {Object} data
387      * @private
388      */
389     constructor(data){
390         let keys = Object.keys(data);
391         for (let i=0; i< keys.length; i++) {
392             this.setProperty(keys[i], data[keys[i]]);
393         }
394     }
395     /**
396      * Validates a subkey property against {@link validUserIdProperties} and
397      * sets it if validation is successful
398      * @param {String} property
399      * @param {*} value
400      * @param private
401      */
402     setProperty(property, value){
403         if (!this._data){
404             this._data = {};
405         }
406         if (validUserIdProperties.hasOwnProperty(property)){
407             if (validUserIdProperties[property](value) === true) {
408                 if (property === 'last_update'){
409                     this._data[property] = new Date(value*1000);
410                 } else {
411                     this._data[property] = value;
412                 }
413             }
414         }
415
416     }
417
418     /**
419      * Fetches information about the user
420      * @param {String} property Information to request
421      * @returns {String | Number}
422      */
423     get(property) {
424         if (this._data.hasOwnProperty(property)){
425             return (this._data[property]);
426         }
427     }
428 }
429
430 /**
431  * Validation definition for userIds. Each valid userId property is represented
432  * as a key- Value pair, with their value being a validation function to check
433  * against
434  * @protected
435  * @const
436  */
437 const validUserIdProperties = {
438     'revoked': function(value){
439         return typeof(value) === 'boolean';
440     },
441     'invalid':  function(value){
442         return typeof(value) === 'boolean';
443     },
444     'uid': function(value){
445         if (typeof(value) === 'string' || value === ''){
446             return true;
447         }
448         return false;
449     },
450     'validity': function(value){
451         if (typeof(value) === 'string'){
452             return true;
453         }
454         return false;
455     },
456     'name': function(value){
457         if (typeof(value) === 'string' || value === ''){
458             return true;
459         }
460         return false;
461     },
462     'email': function(value){
463         if (typeof(value) === 'string' || value === ''){
464             return true;
465         }
466         return false;
467     },
468     'address': function(value){
469         if (typeof(value) === 'string' || value === ''){
470             return true;
471         }
472         return false;
473     },
474     'comment': function(value){
475         if (typeof(value) === 'string' || value === ''){
476             return true;
477         }
478         return false;
479     },
480     'origin':  function(value){
481         return Number.isInteger(value);
482     },
483     'last_update':  function(value){
484         return Number.isInteger(value);
485     }
486 };
487
488 /**
489  * Validation definition for subKeys. Each valid userId property is represented
490  * as a key-value pair, with the value being a validation function
491  * @protected
492  * @const
493  */
494 const validSubKeyProperties = {
495     'invalid': function(value){
496         return typeof(value) === 'boolean';
497     },
498     'can_encrypt': function(value){
499         return typeof(value) === 'boolean';
500     },
501     'can_sign': function(value){
502         return typeof(value) === 'boolean';
503     },
504     'can_certify':  function(value){
505         return typeof(value) === 'boolean';
506     },
507     'can_authenticate':  function(value){
508         return typeof(value) === 'boolean';
509     },
510     'secret': function(value){
511         return typeof(value) === 'boolean';
512     },
513     'is_qualified': function(value){
514         return typeof(value) === 'boolean';
515     },
516     'is_cardkey':  function(value){
517         return typeof(value) === 'boolean';
518     },
519     'is_de_vs':  function(value){
520         return typeof(value) === 'boolean';
521     },
522     'pubkey_algo_name': function(value){
523         return typeof(value) === 'string';
524         // TODO: check against list of known?['']
525     },
526     'pubkey_algo_string': function(value){
527         return typeof(value) === 'string';
528         // TODO: check against list of known?['']
529     },
530     'keyid': function(value){
531         return isLongId(value);
532     },
533     'pubkey_algo': function(value) {
534         return (Number.isInteger(value) && value >= 0);
535     },
536     'length': function(value){
537         return (Number.isInteger(value) && value > 0);
538     },
539     'timestamp': function(value){
540         return (Number.isInteger(value) && value > 0);
541     },
542     'expires': function(value){
543         return (Number.isInteger(value) && value > 0);
544     }
545 };
546
547 /**
548  * Validation definition for Keys. Each valid Key property is represented
549  * as a key-value pair, with their value being a validation function
550  * @protected
551  * @const
552  */
553 const validKeyProperties = {
554     'fingerprint': function(value){
555         return isFingerprint(value);
556     },
557     'armored': function(value){
558         return typeof(value === 'string');
559     },
560     'revoked': function(value){
561         return typeof(value) === 'boolean';
562     },
563     'expired': function(value){
564         return typeof(value) === 'boolean';
565     },
566     'disabled': function(value){
567         return typeof(value) === 'boolean';
568     },
569     'invalid': function(value){
570         return typeof(value) === 'boolean';
571     },
572     'can_encrypt': function(value){
573         return typeof(value) === 'boolean';
574     },
575     'can_sign': function(value){
576         return typeof(value) === 'boolean';
577     },
578     'can_certify': function(value){
579         return typeof(value) === 'boolean';
580     },
581     'can_authenticate': function(value){
582         return typeof(value) === 'boolean';
583     },
584     'secret': function(value){
585         return typeof(value) === 'boolean';
586     },
587     'is_qualified': function(value){
588         return typeof(value) === 'boolean';
589     },
590     'protocol': function(value){
591         return typeof(value) === 'string';
592         //TODO check for implemented ones
593     },
594     'issuer_serial': function(value){
595         return typeof(value) === 'string';
596     },
597     'issuer_name': function(value){
598         return typeof(value) === 'string';
599     },
600     'chain_id': function(value){
601         return typeof(value) === 'string';
602     },
603     'owner_trust': function(value){
604         return typeof(value) === 'string';
605     },
606     'last_update': function(value){
607         return (Number.isInteger(value));
608         //TODO undefined/null possible?
609     },
610     'origin': function(value){
611         return (Number.isInteger(value));
612     },
613     'subkeys': function(value){
614         return (Array.isArray(value));
615     },
616     'userids': function(value){
617         return (Array.isArray(value));
618     },
619     'tofu': function(value){
620         return (Array.isArray(value));
621     },
622     'hasSecret': function(value){
623         return typeof(value) === 'boolean';
624     }
625
626 };