js: code cleanup (eslint)
[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 fingerprint.
30  * @param {String} fingerprint
31  */
32 export function createKey(fingerprint){
33     if (!isFingerprint(fingerprint)){
34         return gpgme_error('PARAM_WRONG');
35     }
36     else return new GPGME_Key(fingerprint);
37 }
38
39 /**
40  * Representing the Keys as stored in GPG
41  * It allows to query almost all information defined in gpgme Key Objects
42  * Refer to validKeyProperties for available information, and the gpgme
43  * documentation on their meaning
44  * (https://www.gnupg.org/documentation/manuals/gpgme/Key-objects.html)
45  *
46  */
47 export class GPGME_Key {
48
49     constructor(fingerprint){
50         this.fingerprint = fingerprint;
51     }
52
53     set fingerprint(fpr){
54         if (isFingerprint(fpr) === true) {
55             if (this._data === undefined) {
56                 this._data = {fingerprint:  fpr};
57             } else {
58                 if (this._data.fingerprint === undefined){
59                     this._data.fingerprint = fpr;
60                 }
61             }
62         }
63     }
64
65     get fingerprint(){
66         if (!this._data || !this._data.fingerprint){
67             return gpgme_error('KEY_INVALID');
68         }
69         return this._data.fingerprint;
70     }
71
72     /**
73      *
74      * @param {Object} data Bulk set data for this key, with the Object as sent
75      * by gpgme-json.
76      * @returns {GPGME_Key|GPGME_Error} The Key object itself after values have
77      * been set
78      */
79     setKeyData(data){
80         if (this._data === undefined) {
81             this._data = {};
82         }
83         if (
84             typeof(data) !== 'object') {
85             return gpgme_error('KEY_INVALID');
86         }
87         if (!this._data.fingerprint && isFingerprint(data.fingerprint)){
88             if (data.fingerprint !== this.fingerprint){
89                 return gpgme_error('KEY_INVALID');
90             }
91             this._data.fingerprint = data.fingerprint;
92         } else if (this._data.fingerprint !== data.fingerprint){
93             return gpgme_error('KEY_INVALID');
94         }
95         let dataKeys = Object.keys(data);
96         for (let i=0; i< dataKeys.length; i++){
97             if (!validKeyProperties.hasOwnProperty(dataKeys[i])){
98                 return gpgme_error('KEY_INVALID');
99             }
100             if (validKeyProperties[dataKeys[i]](data[dataKeys[i]]) !== true ){
101                 return gpgme_error('KEY_INVALID');
102             }
103             switch (dataKeys[i]){
104             case 'subkeys':
105                 this._data.subkeys = [];
106                 for (let i=0; i< data.subkeys.length; i++) {
107                     this._data.subkeys.push(
108                         new GPGME_Subkey(data.subkeys[i]));
109                 }
110                 break;
111             case 'userids':
112                 this._data.userids = [];
113                 for (let i=0; i< data.userids.length; i++) {
114                     this._data.userids.push(
115                         new GPGME_UserId(data.userids[i]));
116                 }
117                 break;
118             default:
119                 this._data[dataKeys[i]] = data[dataKeys[i]];
120             }
121         }
122         return this;
123     }
124
125     /**
126      * Query any property of the Key list
127      * (TODO: armor not in here, might be unexpected)
128      * @param {String} property Key property to be retreived
129      * @param {*} cached (optional) if false, the data will be directly queried
130      * from gnupg.
131      *  @returns {*|Promise<*>} the value, or if not cached, a Promise
132      * resolving on the value
133      */
134     get(property, cached=true) {
135         if (cached === false) {
136             let me = this;
137             return new Promise(function(resolve, reject) {
138                 if (!validKeyProperties.hasOwnProperty(property)){
139                     reject('PARAM_WRONG');
140                 } else if (property === 'armored'){
141                     resolve(me.getArmor());
142                 } else if (property === 'hasSecret'){
143                     resolve(me.getHasSecret());
144                 } else {
145                     me.refreshKey().then(function(key){
146                         resolve(key.get(property, true));
147                     }, function(error){
148                         reject(error);
149                     });
150                 }
151             });
152         } else {
153             if (!validKeyProperties.hasOwnProperty(property)){
154                 return gpgme_error('PARAM_WRONG');
155             }
156             if (!this._data.hasOwnProperty(property)){
157                 return gpgme_error('KEY_NO_INIT');
158             } else {
159                 return (this._data[property]);
160             }
161         }
162     }
163
164     /**
165      * Reloads the Key from gnupg
166      */
167     refreshKey() {
168         let me = this;
169         return new Promise(function(resolve, reject) {
170             if (!me._data.fingerprint){
171                 reject(gpgme_error('KEY_INVALID'));
172             }
173             let msg = createMessage('keylist');
174             msg.setParameter('sigs', true);
175             msg.setParameter('keys', me._data.fingerprint);
176             msg.post().then(function(result){
177                 if (result.keys.length === 1){
178                     me.setKeyData(result.keys[0]);
179                     resolve(me);
180                 } else {
181                     reject(gpgme_error('KEY_NOKEY'));
182                 }
183             }, function (error) {
184                 reject(gpgme_error('GNUPG_ERROR'), error);
185             });
186         });
187     }
188
189     /**
190      * Query the armored block of the non- secret parts of the Key directly
191      * from gpg.
192      * @returns {Promise<String>}
193      */
194     getArmor(){
195         let me = this;
196         return new Promise(function(resolve, reject) {
197             if (!me._data.fingerprint){
198                 reject(gpgme_error('KEY_INVALID'));
199             }
200             let msg = createMessage('export');
201             msg.setParameter('armor', true);
202             msg.setParameter('keys', me._data.fingerprint);
203             msg.post().then(function(result){
204                 me._data.armored = result.data;
205                 resolve(result.data);
206             }, function(error){
207                 reject(error);
208             });
209         });
210     }
211
212     getHasSecret(){
213         let me = this;
214         return new Promise(function(resolve, reject) {
215             if (!me._data.fingerprint){
216                 reject(gpgme_error('KEY_INVALID'));
217             }
218             let msg = createMessage('keylist');
219             msg.setParameter('keys', me._data.fingerprint);
220             msg.setParameter('secret', true);
221             msg.post().then(function(result){
222                 me._data.hasSecret = null;
223                 if (result.keys === undefined || result.keys.length < 1) {
224                     me._data.hasSecret = false;
225                     resolve(false);
226                 }
227                 else if (result.keys.length === 1){
228                     let key = result.keys[0];
229                     if (!key.subkeys){
230                         me._data.hasSecret = false;
231                         resolve(false);
232                     } else {
233                         for (let i=0; i < key.subkeys.length; i++) {
234                             if (key.subkeys[i].secret === true) {
235                                 me._data.hasSecret = true;
236                                 resolve(true);
237                                 break;
238                             }
239                             if (i === (key.subkeys.length -1)) {
240                                 me._data.hasSecret = false;
241                                 resolve(false);
242                             }
243                         }
244                     }
245                 } else {
246                     reject(gpgme_error('CONN_UNEXPECTED_ANSWER'));
247                 }
248             }, function(error){
249                 reject(error);
250             });
251         });
252     }
253
254     /**
255      * Convenience functions to be directly used as properties of the Key
256      * Notice that these rely on cached info and may be outdated. Use the async
257      * get(property, false) if you need the most current info
258      */
259
260     /**
261      * @returns {String} The armored public Key block
262      */
263     get armored(){
264         return this.get('armored', true);
265     }
266
267     /**
268      * @returns {Boolean} If the key is considered a "private Key",
269      * i.e. owns a secret subkey.
270      */
271     get hasSecret(){
272         return this.get('hasSecret', true);
273     }
274
275     /**
276      * Deletes the public Key from the GPG Keyring. Note that a deletion of a
277      * secret key is not supported by the native backend.
278      * @returns {Promise<Boolean>} Success if key was deleted, rejects with a
279      * GPG error otherwise
280      */
281     delete(){
282         let me = this;
283         return new Promise(function(resolve, reject){
284             if (!me._data.fingerprint){
285                 reject(gpgme_error('KEY_INVALID'));
286             }
287             let msg = createMessage('delete');
288             msg.setParameter('key', me._data.fingerprint);
289             msg.post().then(function(result){
290                 resolve(result.success);
291             }, function(error){
292                 reject(error);
293             });
294         });
295     }
296 }
297
298 /**
299  * The subkeys of a Key. Currently, they cannot be refreshed separately
300  */
301 class GPGME_Subkey {
302
303     constructor(data){
304         let keys = Object.keys(data);
305         for (let i=0; i< keys.length; i++) {
306             this.setProperty(keys[i], data[keys[i]]);
307         }
308     }
309
310     setProperty(property, value){
311         if (!this._data){
312             this._data = {};
313         }
314         if (validSubKeyProperties.hasOwnProperty(property)){
315             if (validSubKeyProperties[property](value) === true) {
316                 this._data[property] = value;
317             }
318         }
319     }
320
321     /**
322      *
323      * @param {String} property Information to request
324      * @returns {String | Number}
325      * TODO: date properties are numbers with Date in seconds
326      */
327     get(property) {
328         if (this._data.hasOwnProperty(property)){
329             return (this._data[property]);
330         }
331     }
332 }
333
334 class GPGME_UserId {
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     setProperty(property, value){
344         if (!this._data){
345             this._data = {};
346         }
347         if (validUserIdProperties.hasOwnProperty(property)){
348             if (validUserIdProperties[property](value) === true) {
349                 this._data[property] = value;
350             }
351         }
352     }
353
354     /**
355      *
356      * @param {String} property Information to request
357      * @returns {String | Number}
358      * TODO: date properties are numbers with Date in seconds
359      */
360     get(property) {
361         if (this._data.hasOwnProperty(property)){
362             return (this._data[property]);
363         }
364     }
365 }
366
367 const validUserIdProperties = {
368     'revoked': function(value){
369         return typeof(value) === 'boolean';
370     },
371     'invalid':  function(value){
372         return typeof(value) === 'boolean';
373     },
374     'uid': function(value){
375         if (typeof(value) === 'string' || value === ''){
376             return true;
377         }
378         return false;
379     },
380     'validity': function(value){
381         if (typeof(value) === 'string'){
382             return true;
383         }
384         return false;
385     },
386     'name': function(value){
387         if (typeof(value) === 'string' || value === ''){
388             return true;
389         }
390         return false;
391     },
392     'email': function(value){
393         if (typeof(value) === 'string' || value === ''){
394             return true;
395         }
396         return false;
397     },
398     'address': function(value){
399         if (typeof(value) === 'string' || value === ''){
400             return true;
401         }
402         return false;
403     },
404     'comment': function(value){
405         if (typeof(value) === 'string' || value === ''){
406             return true;
407         }
408         return false;
409     },
410     'origin':  function(value){
411         return Number.isInteger(value);
412     },
413     'last_update':  function(value){
414         return Number.isInteger(value);
415     }
416 };
417
418 const validSubKeyProperties = {
419     'invalid': function(value){
420         return typeof(value) === 'boolean';
421     },
422     'can_encrypt': function(value){
423         return typeof(value) === 'boolean';
424     },
425     'can_sign': function(value){
426         return typeof(value) === 'boolean';
427     },
428     'can_certify':  function(value){
429         return typeof(value) === 'boolean';
430     },
431     'can_authenticate':  function(value){
432         return typeof(value) === 'boolean';
433     },
434     'secret': function(value){
435         return typeof(value) === 'boolean';
436     },
437     'is_qualified': function(value){
438         return typeof(value) === 'boolean';
439     },
440     'is_cardkey':  function(value){
441         return typeof(value) === 'boolean';
442     },
443     'is_de_vs':  function(value){
444         return typeof(value) === 'boolean';
445     },
446     'pubkey_algo_name': function(value){
447         return typeof(value) === 'string';
448         // TODO: check against list of known?['']
449     },
450     'pubkey_algo_string': function(value){
451         return typeof(value) === 'string';
452         // TODO: check against list of known?['']
453     },
454     'keyid': function(value){
455         return isLongId(value);
456     },
457     'pubkey_algo': function(value) {
458         return (Number.isInteger(value) && value >= 0);
459     },
460     'length': function(value){
461         return (Number.isInteger(value) && value > 0);
462     },
463     'timestamp': function(value){
464         return (Number.isInteger(value) && value > 0);
465     },
466     'expires': function(value){
467         return (Number.isInteger(value) && value > 0);
468     }
469 };
470 const validKeyProperties = {
471     //TODO better validation?
472     'fingerprint': function(value){
473         return isFingerprint(value);
474     },
475     'armored': function(value){
476         return typeof(value === 'string');
477     },
478     'revoked': function(value){
479         return typeof(value) === 'boolean';
480     },
481     'expired': function(value){
482         return typeof(value) === 'boolean';
483     },
484     'disabled': function(value){
485         return typeof(value) === 'boolean';
486     },
487     'invalid': function(value){
488         return typeof(value) === 'boolean';
489     },
490     'can_encrypt': function(value){
491         return typeof(value) === 'boolean';
492     },
493     'can_sign': function(value){
494         return typeof(value) === 'boolean';
495     },
496     'can_certify': function(value){
497         return typeof(value) === 'boolean';
498     },
499     'can_authenticate': function(value){
500         return typeof(value) === 'boolean';
501     },
502     'secret': function(value){
503         return typeof(value) === 'boolean';
504     },
505     'is_qualified': function(value){
506         return typeof(value) === 'boolean';
507     },
508     'protocol': function(value){
509         return typeof(value) === 'string';
510         //TODO check for implemented ones
511     },
512     'issuer_serial': function(value){
513         return typeof(value) === 'string';
514     },
515     'issuer_name': function(value){
516         return typeof(value) === 'string';
517     },
518     'chain_id': function(value){
519         return typeof(value) === 'string';
520     },
521     'owner_trust': function(value){
522         return typeof(value) === 'string';
523     },
524     'last_update': function(value){
525         return (Number.isInteger(value));
526         //TODO undefined/null possible?
527     },
528     'origin': function(value){
529         return (Number.isInteger(value));
530     },
531     'subkeys': function(value){
532         return (Array.isArray(value));
533     },
534     'userids': function(value){
535         return (Array.isArray(value));
536     },
537     'tofu': function(value){
538         return (Array.isArray(value));
539     },
540     'hasSecret': function(value){
541         return typeof(value) === 'boolean';
542     }
543
544 };