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