js: add with-sec-fprs to getKeysArmored
[gpgme.git] / lang / js / src / Keyring.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
25 import {createMessage} from './Message';
26 import {createKey} from './Key';
27 import { isFingerprint } from './Helpers';
28 import { gpgme_error } from './Errors';
29
30 /**
31  * This class offers access to the gnupg keyring
32  */
33 export class GPGME_Keyring {
34     constructor(){
35     }
36
37     /**
38      * Queries Keys (all Keys or a subset) from gnupg.
39      *
40      * @param {String | Array<String>} pattern (optional) A pattern to search
41      * for in userIds or KeyIds.
42      * @param {Boolean} prepare_sync (optional) if set to true, the 'hasSecret'
43      * and 'armored' properties will be fetched for the Keys as well. These
44      * require additional calls to gnupg, resulting in a performance hungry
45      * operation. Calling them here enables direct, synchronous use of these
46      * properties for all keys, without having to resort to a refresh() first.
47      * @param {Boolean} search (optional) retrieve Keys from external servers
48      * with the method(s) defined in gnupg (e.g. WKD/HKP lookup)
49      * @returns {Promise.<Array<GPGME_Key>|GPGME_Error>}
50      * @static
51      * @async
52      */
53     getKeys(pattern, prepare_sync=false, search=false){
54         return new Promise(function(resolve, reject) {
55             let msg = createMessage('keylist');
56             if (pattern !== undefined){
57                 msg.setParameter('keys', pattern);
58             }
59             msg.setParameter('sigs', true);
60             if (search === true){
61                 msg.setParameter('locate', true);
62             }
63             msg.post().then(function(result){
64                 let resultset = [];
65                 if (result.keys.length === 0){
66                     resolve([]);
67                 } else {
68                     let secondrequest;
69                     if (prepare_sync === true) {
70                         secondrequest = function() {
71                             msg.setParameter('secret', true);
72                             return msg.post();
73                         };
74                     } else {
75                         secondrequest = function() {
76                             return Promise.resolve(true);
77                         };
78                     }
79                     secondrequest().then(function(answer) {
80                         for (let i=0; i < result.keys.length; i++){
81                             if (prepare_sync === true){
82                                 result.keys[i].hasSecret = false;
83                                 if (answer && answer.keys) {
84                                     for (let j=0; j < answer.keys.length; j++ ){
85                                         if (result.keys[i].fingerprint ===
86                                             answer.keys[j].fingerprint
87                                         ) {
88                                             if (answer.keys[j].secret === true){
89                                                 result.keys[i].hasSecret = true;
90                                             }
91                                             break;
92                                         }
93                                     }
94                                     // TODO getArmor() to be used in sync
95                                 }
96                             }
97                             let k = createKey(result.keys[i].fingerprint);
98                             k.setKeyData(result.keys[i]);
99                             resultset.push(k);
100                         }
101                         resolve(resultset);
102                     }, function(error){
103                         reject(error);
104                     });
105                 }
106             });
107         });
108     }
109
110     /**
111      * @typedef {Object} exportResult The result of a getKeysArmored operation.
112      * @property {String} armored The public Key(s) as armored block. Note that
113      * the result is one armored block, and not a block per key.
114      * @property {Array<String>} secret_fprs (optional) list of fingerprints
115      * for those Keys that also have a secret Key available in gnupg. The
116      * secret key will not be exported, but the fingerprint can be used in
117      * operations needing a secret key.
118      */
119
120     /**
121      * Fetches the armored public Key blocks for all Keys matching the pattern
122      * (if no pattern is given, fetches all keys known to gnupg).
123      * @param {String|Array<String>} pattern (optional) The Pattern to search
124      * for
125      * @param {Boolean} with_secret_fpr (optional) also return a list of
126      * fingerprints for the keys that have a secret key available
127      * @returns {Promise<exportResult|GPGME_Error>} Object containing the
128      * armored Key(s) and additional information.
129      * @static
130      * @async
131      */
132     getKeysArmored(pattern, with_secret_fpr) {
133         return new Promise(function(resolve, reject) {
134             let msg = createMessage('export');
135             msg.setParameter('armor', true);
136             if (with_secret_fpr === true) {
137                 msg.setParameter('with-sec-fprs', true);
138             }
139             if (pattern !== undefined){
140                 msg.setParameter('keys', pattern);
141             }
142             msg.post().then(function(answer){
143                 const result = {armored: answer.data};
144                 if (with_secret_fpr === true
145                     && answer.hasOwnProperty('sec-fprs')
146                 ) {
147                     result.secret_fprs = answer['sec-fprs'];
148                 }
149                 resolve(result);
150             }, function(error){
151                 reject(error);
152             });
153         });
154     }
155
156     /**
157      * Returns the Key used by default in gnupg.
158      * (a.k.a. 'primary Key or 'main key').
159      * It looks up the gpg configuration if set, or the first key that contains
160      * a secret key.
161      *
162      * @returns {Promise<GPGME_Key|GPGME_Error>}
163      * @async
164      * @static
165      */
166     getDefaultKey() {
167         let me = this;
168         return new Promise(function(resolve, reject){
169             let msg = createMessage('config_opt');
170             msg.setParameter('component', 'gpg');
171             msg.setParameter('option', 'default-key');
172             msg.post().then(function(response){
173                 if (response.value !== undefined
174                     && response.value.hasOwnProperty('string')
175                     && typeof(response.value.string) === 'string'
176                 ){
177                     me.getKeys(response.value.string,true).then(function(keys){
178                         if(keys.length === 1){
179                             resolve(keys[0]);
180                         } else {
181                             reject(gpgme_error('KEY_NO_DEFAULT'));
182                         }
183                     }, function(error){
184                         reject(error);
185                     });
186                 } else {
187                     // TODO: this is overly 'expensive' in communication
188                     // and probably performance, too
189                     me.getKeys(null,true).then(function(keys){
190                         for (let i=0; i < keys.length; i++){
191                             if (keys[i].get('hasSecret') === true){
192                                 resolve(keys[i]);
193                                 break;
194                             }
195                             if (i === keys.length -1){
196                                 reject(gpgme_error('KEY_NO_DEFAULT'));
197                             }
198                         }
199                     }, function(error){
200                         reject(error);
201                     });
202                 }
203             }, function(error){
204                 reject(error);
205             });
206         });
207     }
208
209     /**
210      * @typedef {Object} importResult The result of a Key update
211      * @property {Object} summary Numerical summary of the result. See the
212      * feedbackValues variable for available Keys values and the gnupg
213      * documentation.
214      * https://www.gnupg.org/documentation/manuals/gpgme/Importing-Keys.html
215      * for details on their meaning.
216      * @property {Array<importedKeyResult>} Keys Array of Object containing
217      * GPGME_Keys with additional import information
218      *
219      */
220
221     /**
222      * @typedef {Object} importedKeyResult
223      * @property {GPGME_Key} key The resulting key
224      * @property {String} status:
225      *  'nochange' if the Key was not changed,
226      *  'newkey' if the Key was imported in gpg, and did not exist previously,
227      *  'change' if the key existed, but details were updated. For details,
228      *    Key.changes is available.
229      * @property {Boolean} changes.userId Changes in userIds
230      * @property {Boolean} changes.signature Changes in signatures
231      * @property {Boolean} changes.subkey Changes in subkeys
232      */
233
234     /**
235      * Import an armored Key block into gnupg. Note that this currently will
236      * not succeed on private Key blocks.
237      * @param {String} armored Armored Key block of the Key(s) to be imported
238      * into gnupg
239      * @param {Boolean} prepare_sync prepare the keys for synched use
240      * (see {@link getKeys}).
241      * @returns {Promise<importResult>} A summary and Keys considered.
242      * @async
243      * @static
244      */
245     importKey(armored, prepare_sync) {
246         let feedbackValues = ['considered', 'no_user_id', 'imported',
247             'imported_rsa', 'unchanged', 'new_user_ids', 'new_sub_keys',
248             'new_signatures', 'new_revocations', 'secret_read',
249             'secret_imported', 'secret_unchanged', 'skipped_new_keys',
250             'not_imported', 'skipped_v3_keys'];
251         if (!armored || typeof(armored) !== 'string'){
252             return Promise.reject(gpgme_error('PARAM_WRONG'));
253         }
254         let me = this;
255         return new Promise(function(resolve, reject){
256             let msg = createMessage('import');
257             msg.setParameter('data', armored);
258             msg.post().then(function(response){
259                 let infos = {};
260                 let fprs = [];
261                 for (let res=0; res<response.result.imports.length; res++){
262                     let result = response.result.imports[res];
263                     let status = '';
264                     if (result.status === 0){
265                         status = 'nochange';
266                     } else if ((result.status & 1) === 1){
267                         status = 'newkey';
268                     } else {
269                         status = 'change';
270                     }
271                     let changes = {};
272                     changes.userId = (result.status & 2) === 2;
273                     changes.signature = (result.status & 4) === 4;
274                     changes.subkey = (result.status & 8) === 8;
275                     //16 new secret key: not implemented
276
277                     fprs.push(result.fingerprint);
278                     infos[result.fingerprint] = {
279                         changes: changes,
280                         status: status
281                     };
282                 }
283                 let resultset = [];
284                 if (prepare_sync === true){
285                     me.getKeys(fprs, true).then(function(result){
286                         for (let i=0; i < result.length; i++) {
287                             resultset.push({
288                                 key: result[i],
289                                 changes: infos[result[i].fingerprint].changes,
290                                 status: infos[result[i].fingerprint].status
291                             });
292                         }
293                         let summary = {};
294                         for (let i=0; i < feedbackValues.length; i++ ){
295                             summary[feedbackValues[i]] =
296                                 response[feedbackValues[i]];
297                         }
298                         resolve({
299                             Keys:resultset,
300                             summary: summary
301                         });
302                     }, function(error){
303                         reject(error);
304                     });
305                 } else {
306                     for (let i=0; i < fprs.length; i++) {
307                         resultset.push({
308                             key: createKey(fprs[i]),
309                             changes: infos[fprs[i]].changes,
310                             status: infos[fprs[i]].status
311                         });
312                     }
313                     resolve(resultset);
314                 }
315
316             }, function(error){
317                 reject(error);
318             });
319
320
321         });
322
323
324     }
325
326     /**
327      * Convenience function for deleting a Key. See {@link Key.delete} for
328      * further information about the return values.
329      * @param {String} fingerprint
330      * @returns {Promise<Boolean|GPGME_Error>}
331      * @async
332      * @static
333      */
334     deleteKey(fingerprint){
335         if (isFingerprint(fingerprint) === true) {
336             let key = createKey(fingerprint);
337             return key.delete();
338         } else {
339             return Promise.reject(gpgme_error('KEY_INVALID'));
340         }
341     }
342
343     /**
344      * Generates a new Key pair directly in gpg, and returns a GPGME_Key
345      * representing that Key. Please note that due to security concerns, secret
346      * Keys can not be deleted or exported from inside gpgme.js.
347      *
348      * @param {String} userId The user Id, e.g. 'Foo Bar <foo@bar.baz>'
349      * @param {String} algo (optional) algorithm (and optionally key size) to
350      * be used. See {@link supportedKeyAlgos} below for supported values.
351      * @param {Date} expires (optional) Expiration date. If not set, expiration
352      * will be set to 'never'
353      *
354      * @return {Promise<Key|GPGME_Error>}
355      * @async
356      */
357     generateKey(userId, algo = 'default', expires){
358         if (
359             typeof(userId) !== 'string' ||
360             supportedKeyAlgos.indexOf(algo) < 0 ||
361             (expires && !(expires instanceof Date))
362         ){
363             return Promise.reject(gpgme_error('PARAM_WRONG'));
364         }
365         let me = this;
366         return new Promise(function(resolve, reject){
367             let msg = createMessage('createkey');
368             msg.setParameter('userid', userId);
369             msg.setParameter('algo', algo );
370             if (expires){
371                 msg.setParameter('expires',
372                     Math.floor(expires.valueOf()/1000));
373             }
374             msg.post().then(function(response){
375                 me.getKeys(response.fingerprint, true).then(
376                     // TODO make prepare_sync (second parameter) optional here.
377                     function(result){
378                         resolve(result);
379                     }, function(error){
380                         reject(error);
381                     });
382             }, function(error) {
383                 reject(error);
384             });
385         });
386     }
387 }
388
389 /**
390  * List of algorithms supported for key generation. Please refer to the gnupg
391  * documentation for details
392  */
393 const supportedKeyAlgos = [
394     'default',
395     'rsa', 'rsa2048', 'rsa3072', 'rsa4096',
396     'dsa', 'dsa2048', 'dsa3072', 'dsa4096',
397     'elg', 'elg2048', 'elg3072', 'elg4096',
398     'ed25519',
399     'cv25519',
400     'brainpoolP256r1', 'brainpoolP384r1', 'brainpoolP512r1',
401     'NIST P-256', 'NIST P-384', 'NIST P-521'
402 ];