js: implement Key handling (1)
[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 import { Connection } from './Connection';
34
35 /**
36  * Validates the fingerprint, and checks for tha availability of a connection.
37  * If both are available, a Key will be returned.
38  * @param {String} fingerprint
39  * @param {Object} parent Either a Connection, or the invoking object with a
40  * Connection (e.g. Keyring)
41  */
42 export function createKey(fingerprint, parent){
43     if (!isFingerprint(fingerprint)){
44         return gpgme_error('PARAM_WRONG');
45     }
46     if ( parent instanceof Connection){
47         return new GPGME_Key(fingerprint, parent);
48     } else if ( parent.hasOwnProperty('connection') &&
49         parent.connection instanceof Connection){
50             return new GPGME_Key(fingerprint, parent.connection);
51     } else {
52         return gpgme_error('PARAM_WRONG');
53     }
54 }
55
56 /**
57  * Representing the Keys as stored in GPG
58  */
59 export class GPGME_Key {
60
61     constructor(fingerprint, connection){
62         this.fingerprint = fingerprint;
63         this.connection = connection;
64     }
65
66     set connection(conn){
67         if (this._connection instanceof Connection) {
68             gpgme_error('CONN_ALREADY_CONNECTED');
69         } else if (conn instanceof Connection ) {
70             this._connection = conn;
71         }
72     }
73
74     get connection(){
75         if (!this._data.fingerprint){
76             return gpgme_error('KEY_INVALID');
77         }
78         if (!this._connection instanceof Connection){
79             return gpgme_error('CONN_NO_CONNECT');
80         } else {
81             return this._connection;
82         }
83     }
84
85     set fingerprint(fpr){
86         if (isFingerprint(fpr) === true) {
87             if (this._data === undefined) {
88                 this._data = {fingerprint:  fpr};
89             } else {
90                 if (this._data.fingerprint === undefined){
91                     this._data.fingerprint = fpr;
92                 }
93             }
94         }
95     }
96
97     get fingerprint(){
98         if (!this._data || !this._data.fingerprint){
99             return gpgme_error('KEY_INVALID');
100         }
101         return this._data.fingerprint;
102     }
103
104     /**
105      *
106      * @param {Object} data Bulk set data for this key, with the Object as sent
107      * by gpgme-json.
108      * @returns {GPGME_Key|GPGME_Error} The Key object itself after values have
109      * been set
110      */
111     setKeydata(data){
112         if (this._data === undefined) {
113             this._data = {};
114         }
115         if (
116             typeof(data) !== 'object') {
117             return gpgme_error('KEY_INVALID');
118         }
119         if (!this._data.fingerprint && isFingerprint(data.fingerprint)){
120             if (data.fingerprint !== this.fingerprint){
121                 return gpgme_error('KEY_INVALID');
122             }
123             this._data.fingerprint = data.fingerprint;
124         } else if (this._data.fingerprint !== data.fingerprint){
125             return gpgme_error('KEY_INVALID');
126         }
127
128         let booleans = ['expired', 'disabled','invalid','can_encrypt',
129             'can_sign','can_certify','can_authenticate','secret',
130             'is_qualified'];
131         for (let b =0; b < booleans.length; b++) {
132             if (
133                 !data.hasOwnProperty(booleans[b]) ||
134                 typeof(data[booleans[b]]) !== 'boolean'
135             ){
136                 return gpgme_error('KEY_INVALID');
137             }
138             this._data[booleans[b]] = data[booleans[b]];
139         }
140         if (typeof(data.protocol) !== 'string'){
141             return gpgme_error('KEY_INVALID');
142         }
143         // TODO check valid protocols?
144         this._data.protocol = data.protocol;
145
146         if (typeof(data.owner_trust) !== 'string'){
147             return gpgme_error('KEY_INVALID');
148         }
149         // TODO check valid values?
150         this._data.owner_trust = data.owner_trust;
151
152         // TODO: what about origin ?
153         if (!Number.isInteger(data.last_update)){
154             return gpgme_error('KEY_INVALID');
155         }
156         this._data.last_update = data.last_update;
157
158         this._data.subkeys = [];
159         if (data.hasOwnProperty('subkeys')){
160             if (!Array.isArray(data.subkeys)){
161                 return gpgme_error('KEY_INVALID');
162             }
163             for (let i=0; i< data.subkeys.length; i++) {
164                 this._data.subkeys.push(
165                     new GPGME_Subkey(data.subkeys[i]));
166             }
167         }
168
169         this._data.userids = [];
170         if (data.hasOwnProperty('userids')){
171             if (!Array.isArray(data.userids)){
172                 return gpgme_error('KEY_INVALID');
173             }
174             for (let i=0; i< data.userids.length; i++) {
175                 this._data.userids.push(
176                     new GPGME_UserId(data.userids[i]));
177             }
178         }
179         return this;
180     }
181
182     /**
183      * Query any property of the Key list
184      * (TODO: armor not in here, might be unexpected)
185      * @param {String} property Key property to be retreived
186      * @param {*} cached (optional) if false, the data will be directly queried
187      * from gnupg.
188      *  @returns {*|Promise<*>} the value, or if not cached, a Promise
189      * resolving on the value
190      */
191     get(property, cached=true) {
192         if (cached === false) {
193             let me = this;
194             return new Promise(function(resolve, reject) {
195                 me.refreshKey().then(function(key){
196                     resolve(key.get(property, true));
197                 }, function(error){
198                     reject(error);
199                 });
200             });
201          } else {
202             if (!this._data.hasOwnProperty(property)){
203                 return gpgme_error('PARAM_WRONG');
204             } else {
205                 return (this._data[property]);
206             }
207         }
208     }
209
210     /**
211      * Reloads the Key from gnupg
212      */
213     refreshKey() {
214         let me = this;
215         return new Promise(function(resolve, reject) {
216             if (!me._data.fingerprint){
217                 reject(gpgme_error('KEY_INVALID'));
218             }
219             let msg = createMessage('keylist');
220             msg.setParameter('sigs', true);
221             msg.setParameter('keys', me._data.fingerprint);
222             me.connection.post(msg).then(function(result){
223                 if (result.keys.length === 1){
224                     me.setKeydata(result.keys[0]);
225                     resolve(me);
226                 } else {
227                     reject(gpgme_error('KEY_NOKEY'));
228                 }
229             }, function (error) {
230                 reject(gpgme_error('GNUPG_ERROR'), error);
231             })
232         });
233     }
234
235     //TODO:
236     /**
237      * Get the armored block of the non- secret parts of the Key.
238      * @returns {String} the armored Key block.
239      * Notice that this may be outdated cached info. Use the async getArmor if
240      * you need the most current info
241      */
242     // get armor(){ TODO }
243
244     /**
245      * Query the armored block of the non- secret parts of the Key directly
246      * from gpg.
247      * Async, returns Promise<String>
248      */
249     // getArmor(){ TODO }
250     //
251
252     // get hasSecret(){TODO} // confusing difference to Key.get('secret')!
253     // getHasSecret(){TODO async version}
254 }
255
256 /**
257  * The subkeys of a Key. Currently, they cannot be refreshed separately
258  */
259 class GPGME_Subkey {
260
261     constructor(data){
262         let keys = Object.keys(data);
263         for (let i=0; i< keys.length; i++) {
264             this.setProperty(keys[i], data[keys[i]]);
265         }
266     }
267
268     setProperty(property, value){
269         if (!this._data){
270             this._data = {};
271         }
272         if (validSubKeyProperties.hasOwnProperty(property)){
273             if (validSubKeyProperties[property](value) === true) {
274                 this._data[property] = value;
275             }
276         }
277     }
278
279     /**
280      *
281      * @param {String} property Information to request
282      * @returns {String | Number}
283      * TODO: date properties are numbers with Date in seconds
284      */
285     get(property) {
286         if (this._data.hasOwnProperty(property)){
287             return (this._data[property]);
288         }
289     }
290 }
291
292 class GPGME_UserId {
293
294     constructor(data){
295         let keys = Object.keys(data);
296         for (let i=0; i< keys.length; i++) {
297             this.setProperty(keys[i], data[keys[i]]);
298         }
299     }
300
301     setProperty(property, value){
302         if (!this._data){
303             this._data = {};
304         }
305         if (validUserIdProperties.hasOwnProperty(property)){
306             if (validUserIdProperties[property](value) === true) {
307                 this._data[property] = value;
308             }
309         }
310     }
311
312     /**
313      *
314      * @param {String} property Information to request
315      * @returns {String | Number}
316      * TODO: date properties are numbers with Date in seconds
317      */
318     get(property) {
319         if (this._data.hasOwnProperty(property)){
320             return (this._data[property]);
321         }
322     }
323 }
324
325 const validUserIdProperties = {
326     'revoked': function(value){
327         return typeof(value) === 'boolean';
328     },
329     'invalid':  function(value){
330         return typeof(value) === 'boolean';
331     },
332     'uid': function(value){
333         if (typeof(value) === 'string' || value === ''){
334             return true;;
335         }
336         return false;
337     },
338     'validity': function(value){
339         if (typeof(value) === 'string'){
340             return true;;
341         }
342         return false;
343     },
344     'name': function(value){
345         if (typeof(value) === 'string' || value === ''){
346         return true;;
347         }
348         return false;
349     },
350     'email': function(value){
351         if (typeof(value) === 'string' || value === ''){
352             return true;;
353         }
354         return false;
355     },
356     'address': function(value){
357         if (typeof(value) === 'string' || value === ''){
358             return true;;
359         }
360         return false;
361     },
362     'comment': function(value){
363         if (typeof(value) === 'string' || value === ''){
364             return true;;
365         }
366         return false;
367     },
368     'origin':  function(value){
369         return Number.isInteger(value);
370     },
371     'last_update':  function(value){
372         return Number.isInteger(value);
373     }
374 };
375
376 const validSubKeyProperties = {
377     'invalid': function(value){
378         return typeof(value) === 'boolean';
379     },
380     'can_encrypt': function(value){
381         return typeof(value) === 'boolean';
382     },
383     'can_sign': function(value){
384         return typeof(value) === 'boolean';
385     },
386     'can_certify':  function(value){
387         return typeof(value) === 'boolean';
388     },
389     'can_authenticate':  function(value){
390         return typeof(value) === 'boolean';
391     },
392     'secret': function(value){
393         return typeof(value) === 'boolean';
394     },
395     'is_qualified': function(value){
396         return typeof(value) === 'boolean';
397     },
398     'is_cardkey':  function(value){
399         return typeof(value) === 'boolean';
400     },
401     'is_de_vs':  function(value){
402         return typeof(value) === 'boolean';
403     },
404     'pubkey_algo_name': function(value){
405             return typeof(value) === 'string';
406             // TODO: check against list of known?['']
407     },
408     'pubkey_algo_string': function(value){
409         return typeof(value) === 'string';
410         // TODO: check against list of known?['']
411     },
412     'keyid': function(value){
413         return isLongId(value);
414     },
415     'pubkey_algo': function(value) {
416         return (Number.isInteger(value) && value >= 0);
417     },
418     'length': function(value){
419         return (Number.isInteger(value) && value > 0);
420     },
421     'timestamp': function(value){
422         return (Number.isInteger(value) && value > 0);
423     },
424     'expires': function(value){
425         return (Number.isInteger(value) && value > 0);
426     }
427 }