js: Treat a connection as a gpgme Context
[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                 me.refreshKey().then(function(key){
165                     resolve(key.get(property, true));
166                 }, function(error){
167                     reject(error);
168                 });
169             });
170          } else {
171             if (!this._data.hasOwnProperty(property)){
172                 return gpgme_error('PARAM_WRONG');
173             } else {
174                 return (this._data[property]);
175             }
176         }
177     }
178
179     /**
180      * Reloads the Key from gnupg
181      */
182     refreshKey() {
183         let me = this;
184         return new Promise(function(resolve, reject) {
185             if (!me._data.fingerprint){
186                 reject(gpgme_error('KEY_INVALID'));
187             }
188             let msg = createMessage('keylist');
189             msg.setParameter('sigs', true);
190             msg.setParameter('keys', me._data.fingerprint);
191             console.log(msg);
192             msg.post().then(function(result){
193                 if (result.keys.length === 1){
194                     me.setKeydata(result.keys[0]);
195                     resolve(me);
196                 } else {
197                     reject(gpgme_error('KEY_NOKEY'));
198                 }
199             }, function (error) {
200                 reject(gpgme_error('GNUPG_ERROR'), error);
201             })
202         });
203     }
204
205     //TODO:
206     /**
207      * Get the armored block of the non- secret parts of the Key.
208      * @returns {String} the armored Key block.
209      * Notice that this may be outdated cached info. Use the async getArmor if
210      * you need the most current info
211      */
212     // get armor(){ TODO }
213
214     /**
215      * Query the armored block of the non- secret parts of the Key directly
216      * from gpg.
217      * Async, returns Promise<String>
218      */
219     // getArmor(){ TODO }
220     //
221
222     // get hasSecret(){TODO} // confusing difference to Key.get('secret')!
223     // getHasSecret(){TODO async version}
224 }
225
226 /**
227  * The subkeys of a Key. Currently, they cannot be refreshed separately
228  */
229 class GPGME_Subkey {
230
231     constructor(data){
232         let keys = Object.keys(data);
233         for (let i=0; i< keys.length; i++) {
234             this.setProperty(keys[i], data[keys[i]]);
235         }
236     }
237
238     setProperty(property, value){
239         if (!this._data){
240             this._data = {};
241         }
242         if (validSubKeyProperties.hasOwnProperty(property)){
243             if (validSubKeyProperties[property](value) === true) {
244                 this._data[property] = value;
245             }
246         }
247     }
248
249     /**
250      *
251      * @param {String} property Information to request
252      * @returns {String | Number}
253      * TODO: date properties are numbers with Date in seconds
254      */
255     get(property) {
256         if (this._data.hasOwnProperty(property)){
257             return (this._data[property]);
258         }
259     }
260 }
261
262 class GPGME_UserId {
263
264     constructor(data){
265         let keys = Object.keys(data);
266         for (let i=0; i< keys.length; i++) {
267             this.setProperty(keys[i], data[keys[i]]);
268         }
269     }
270
271     setProperty(property, value){
272         if (!this._data){
273             this._data = {};
274         }
275         if (validUserIdProperties.hasOwnProperty(property)){
276             if (validUserIdProperties[property](value) === true) {
277                 this._data[property] = value;
278             }
279         }
280     }
281
282     /**
283      *
284      * @param {String} property Information to request
285      * @returns {String | Number}
286      * TODO: date properties are numbers with Date in seconds
287      */
288     get(property) {
289         if (this._data.hasOwnProperty(property)){
290             return (this._data[property]);
291         }
292     }
293 }
294
295 const validUserIdProperties = {
296     'revoked': function(value){
297         return typeof(value) === 'boolean';
298     },
299     'invalid':  function(value){
300         return typeof(value) === 'boolean';
301     },
302     'uid': function(value){
303         if (typeof(value) === 'string' || value === ''){
304             return true;;
305         }
306         return false;
307     },
308     'validity': function(value){
309         if (typeof(value) === 'string'){
310             return true;;
311         }
312         return false;
313     },
314     'name': function(value){
315         if (typeof(value) === 'string' || value === ''){
316         return true;;
317         }
318         return false;
319     },
320     'email': function(value){
321         if (typeof(value) === 'string' || value === ''){
322             return true;;
323         }
324         return false;
325     },
326     'address': function(value){
327         if (typeof(value) === 'string' || value === ''){
328             return true;;
329         }
330         return false;
331     },
332     'comment': function(value){
333         if (typeof(value) === 'string' || value === ''){
334             return true;;
335         }
336         return false;
337     },
338     'origin':  function(value){
339         return Number.isInteger(value);
340     },
341     'last_update':  function(value){
342         return Number.isInteger(value);
343     }
344 };
345
346 const validSubKeyProperties = {
347     'invalid': function(value){
348         return typeof(value) === 'boolean';
349     },
350     'can_encrypt': function(value){
351         return typeof(value) === 'boolean';
352     },
353     'can_sign': function(value){
354         return typeof(value) === 'boolean';
355     },
356     'can_certify':  function(value){
357         return typeof(value) === 'boolean';
358     },
359     'can_authenticate':  function(value){
360         return typeof(value) === 'boolean';
361     },
362     'secret': function(value){
363         return typeof(value) === 'boolean';
364     },
365     'is_qualified': function(value){
366         return typeof(value) === 'boolean';
367     },
368     'is_cardkey':  function(value){
369         return typeof(value) === 'boolean';
370     },
371     'is_de_vs':  function(value){
372         return typeof(value) === 'boolean';
373     },
374     'pubkey_algo_name': function(value){
375             return typeof(value) === 'string';
376             // TODO: check against list of known?['']
377     },
378     'pubkey_algo_string': function(value){
379         return typeof(value) === 'string';
380         // TODO: check against list of known?['']
381     },
382     'keyid': function(value){
383         return isLongId(value);
384     },
385     'pubkey_algo': function(value) {
386         return (Number.isInteger(value) && value >= 0);
387     },
388     'length': function(value){
389         return (Number.isInteger(value) && value > 0);
390     },
391     'timestamp': function(value){
392         return (Number.isInteger(value) && value > 0);
393     },
394     'expires': function(value){
395         return (Number.isInteger(value) && value > 0);
396     }
397 }