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