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