f2a16b4265f0e600893a177c400b083f4c631c79
[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
97         let booleans = ['expired', 'disabled','invalid','can_encrypt',
98             'can_sign','can_certify','can_authenticate','secret',
99             'is_qualified'];
100         for (let b =0; b < booleans.length; b++) {
101             if (
102                 !data.hasOwnProperty(booleans[b]) ||
103                 typeof(data[booleans[b]]) !== 'boolean'
104             ){
105                 return gpgme_error('KEY_INVALID');
106             }
107             this._data[booleans[b]] = data[booleans[b]];
108         }
109         if (typeof(data.protocol) !== 'string'){
110             return gpgme_error('KEY_INVALID');
111         }
112         // TODO check valid protocols?
113         this._data.protocol = data.protocol;
114
115         if (typeof(data.owner_trust) !== 'string'){
116             return gpgme_error('KEY_INVALID');
117         }
118         // TODO check valid values?
119         this._data.owner_trust = data.owner_trust;
120
121         // TODO: what about origin ?
122         if (!Number.isInteger(data.last_update)){
123             return gpgme_error('KEY_INVALID');
124         }
125         this._data.last_update = data.last_update;
126
127         this._data.subkeys = [];
128         if (data.hasOwnProperty('subkeys')){
129             if (!Array.isArray(data.subkeys)){
130                 return gpgme_error('KEY_INVALID');
131             }
132             for (let i=0; i< data.subkeys.length; i++) {
133                 this._data.subkeys.push(
134                     new GPGME_Subkey(data.subkeys[i]));
135             }
136         }
137
138         this._data.userids = [];
139         if (data.hasOwnProperty('userids')){
140             if (!Array.isArray(data.userids)){
141                 return gpgme_error('KEY_INVALID');
142             }
143             for (let i=0; i< data.userids.length; i++) {
144                 this._data.userids.push(
145                     new GPGME_UserId(data.userids[i]));
146             }
147         }
148         return this;
149     }
150
151     /**
152      * Query any property of the Key list
153      * (TODO: armor not in here, might be unexpected)
154      * @param {String} property Key property to be retreived
155      * @param {*} cached (optional) if false, the data will be directly queried
156      * from gnupg.
157      *  @returns {*|Promise<*>} the value, or if not cached, a Promise
158      * resolving on the value
159      */
160     get(property, cached=true) {
161         if (cached === false) {
162             let me = this;
163             return new Promise(function(resolve, reject) {
164                 if (property === 'armor'){
165                     resolve(me.getArmor());
166                 } else if (property === 'hasSecret'){
167                     resolve(me.getHasSecret());
168                 } else {
169                     me.refreshKey().then(function(key){
170                         resolve(key.get(property, true));
171                     }, function(error){
172                         reject(error);
173                     });
174                 }
175             });
176          } else {
177             if (!this._data.hasOwnProperty(property)){
178                 return gpgme_error('PARAM_WRONG');
179             } else {
180                 return (this._data[property]);
181             }
182         }
183     }
184
185     /**
186      * Reloads the Key from gnupg
187      */
188     refreshKey() {
189         let me = this;
190         return new Promise(function(resolve, reject) {
191             if (!me._data.fingerprint){
192                 reject(gpgme_error('KEY_INVALID'));
193             }
194             let msg = createMessage('keylist');
195             msg.setParameter('sigs', true);
196             msg.setParameter('keys', me._data.fingerprint);
197             msg.post().then(function(result){
198                 if (result.keys.length === 1){
199                     me.setKeyData(result.keys[0]);
200                     resolve(me);
201                 } else {
202                     reject(gpgme_error('KEY_NOKEY'));
203                 }
204             }, function (error) {
205                 reject(gpgme_error('GNUPG_ERROR'), error);
206             })
207         });
208     }
209
210     /**
211      * Get the armored block of the non- secret parts of the Key.
212      * @returns {String} the armored Key block.
213      * Notice that this may be outdated cached info. Use the async getArmor if
214      * you need the most current info
215      */
216
217     // get armor(){ TODO }
218
219     /**
220      * Query the armored block of the non- secret parts of the Key directly
221      * from gpg.
222      * @returns {Promise<String>}
223      */
224      getArmor(){
225         let me = this;
226         return new Promise(function(resolve, reject) {
227             if (!me._data.fingerprint){
228                 reject(gpgme_error('KEY_INVALID'));
229             }
230             let msg = createMessage('export');
231             msg.setParameter('armor', true);
232             msg.setParameter('keys', me._data.fingerprint);
233             msg.post().then(function(result){
234                 me._data.armor = result.data;
235                 resolve(result.data);
236             }, function(error){
237                 reject(error);
238             });
239         });
240     }
241
242     getHasSecret(){
243         let me = this;
244         return new Promise(function(resolve, reject) {
245             if (!me._data.fingerprint){
246                 reject(gpgme_error('KEY_INVALID'));
247             }
248             let msg = createMessage('keylist');
249             msg.setParameter('keys', me._data.fingerprint);
250             msg.setParameter('secret', true);
251             msg.post().then(function(result){
252                 me._data.hasSecret = null;
253                 if (result.keys === undefined || result.keys.length < 1) {
254                     me._data.hasSecret = false;
255                     resolve(false);
256                 }
257                 else if (result.keys.length === 1){
258                     let key = result.keys[0];
259                     if (!key.subkeys){
260                         me._data.hasSecret = false;
261                         resolve(false);
262                     } else {
263                         for (let i=0; i < key.subkeys.length; i++) {
264                             if (key.subkeys[i].secret === true) {
265                                 me._data.hasSecret = true;
266                                 resolve(true);
267                                 break;
268                             }
269                             if (i === (key.subkeys.length -1)) {
270                                 me._data.hasSecret = false;
271                                 resolve(false);
272                             }
273                         }
274                     }
275                 } else {
276                     reject(gpgme_error('CONN_UNEXPECTED_ANSWER'))
277                 }
278             }, function(error){
279             })
280         });
281     }
282 }
283
284 /**
285  * The subkeys of a Key. Currently, they cannot be refreshed separately
286  */
287 class GPGME_Subkey {
288
289     constructor(data){
290         let keys = Object.keys(data);
291         for (let i=0; i< keys.length; i++) {
292             this.setProperty(keys[i], data[keys[i]]);
293         }
294     }
295
296     setProperty(property, value){
297         if (!this._data){
298             this._data = {};
299         }
300         if (validSubKeyProperties.hasOwnProperty(property)){
301             if (validSubKeyProperties[property](value) === true) {
302                 this._data[property] = value;
303             }
304         }
305     }
306
307     /**
308      *
309      * @param {String} property Information to request
310      * @returns {String | Number}
311      * TODO: date properties are numbers with Date in seconds
312      */
313     get(property) {
314         if (this._data.hasOwnProperty(property)){
315             return (this._data[property]);
316         }
317     }
318 }
319
320 class GPGME_UserId {
321
322     constructor(data){
323         let keys = Object.keys(data);
324         for (let i=0; i< keys.length; i++) {
325             this.setProperty(keys[i], data[keys[i]]);
326         }
327     }
328
329     setProperty(property, value){
330         if (!this._data){
331             this._data = {};
332         }
333         if (validUserIdProperties.hasOwnProperty(property)){
334             if (validUserIdProperties[property](value) === true) {
335                 this._data[property] = value;
336             }
337         }
338     }
339
340     /**
341      *
342      * @param {String} property Information to request
343      * @returns {String | Number}
344      * TODO: date properties are numbers with Date in seconds
345      */
346     get(property) {
347         if (this._data.hasOwnProperty(property)){
348             return (this._data[property]);
349         }
350     }
351 }
352
353 const validUserIdProperties = {
354     'revoked': function(value){
355         return typeof(value) === 'boolean';
356     },
357     'invalid':  function(value){
358         return typeof(value) === 'boolean';
359     },
360     'uid': function(value){
361         if (typeof(value) === 'string' || value === ''){
362             return true;;
363         }
364         return false;
365     },
366     'validity': function(value){
367         if (typeof(value) === 'string'){
368             return true;;
369         }
370         return false;
371     },
372     'name': function(value){
373         if (typeof(value) === 'string' || value === ''){
374         return true;;
375         }
376         return false;
377     },
378     'email': function(value){
379         if (typeof(value) === 'string' || value === ''){
380             return true;;
381         }
382         return false;
383     },
384     'address': function(value){
385         if (typeof(value) === 'string' || value === ''){
386             return true;;
387         }
388         return false;
389     },
390     'comment': function(value){
391         if (typeof(value) === 'string' || value === ''){
392             return true;;
393         }
394         return false;
395     },
396     'origin':  function(value){
397         return Number.isInteger(value);
398     },
399     'last_update':  function(value){
400         return Number.isInteger(value);
401     }
402 };
403
404 const validSubKeyProperties = {
405     'invalid': function(value){
406         return typeof(value) === 'boolean';
407     },
408     'can_encrypt': function(value){
409         return typeof(value) === 'boolean';
410     },
411     'can_sign': function(value){
412         return typeof(value) === 'boolean';
413     },
414     'can_certify':  function(value){
415         return typeof(value) === 'boolean';
416     },
417     'can_authenticate':  function(value){
418         return typeof(value) === 'boolean';
419     },
420     'secret': function(value){
421         return typeof(value) === 'boolean';
422     },
423     'is_qualified': function(value){
424         return typeof(value) === 'boolean';
425     },
426     'is_cardkey':  function(value){
427         return typeof(value) === 'boolean';
428     },
429     'is_de_vs':  function(value){
430         return typeof(value) === 'boolean';
431     },
432     'pubkey_algo_name': function(value){
433             return typeof(value) === 'string';
434             // TODO: check against list of known?['']
435     },
436     'pubkey_algo_string': function(value){
437         return typeof(value) === 'string';
438         // TODO: check against list of known?['']
439     },
440     'keyid': function(value){
441         return isLongId(value);
442     },
443     'pubkey_algo': function(value) {
444         return (Number.isInteger(value) && value >= 0);
445     },
446     'length': function(value){
447         return (Number.isInteger(value) && value > 0);
448     },
449     'timestamp': function(value){
450         return (Number.isInteger(value) && value > 0);
451     },
452     'expires': function(value){
453         return (Number.isInteger(value) && value > 0);
454     }
455 }