js: import result feedback
[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 export class GPGME_Keyring {
31     constructor(){
32     }
33
34     /**
35      * @param {String} pattern (optional) pattern A pattern to search for,
36      * in userIds or KeyIds
37      * @param {Boolean} prepare_sync (optional, default true) if set to true,
38      * Key.armor and Key.hasSecret will be called, so they can be used
39      * inmediately. This allows for full synchronous use. If set to false,
40      * these will initially only be available as Promises in getArmor() and
41      * getHasSecret()
42      * @returns {Promise.<Array<GPGME_Key>>}
43      *
44      */
45     getKeys(pattern, prepare_sync){
46         return new Promise(function(resolve, reject) {
47             let msg = createMessage('keylist');
48             if (pattern !== undefined){
49                 msg.setParameter('keys', pattern);
50             }
51             msg.setParameter('sigs', true);
52             msg.post().then(function(result){
53                 let resultset = [];
54                 let promises = [];
55                 if (result.keys.length === 0){
56                     resolve([]);
57                 } else {
58                     for (let i=0; i< result.keys.length; i++){
59                         let k = createKey(result.keys[i].fingerprint);
60                         k.setKeyData(result.keys[i]);
61                         if (prepare_sync === true){
62                             promises.push(k.getArmor());
63                             promises.push(k.getHasSecret());
64                         }
65                         resultset.push(k);
66                     }
67                     if (promises.length > 0) {
68                         Promise.all(promises).then(function() {
69                             resolve(resultset);
70                         }, function(error){
71                             reject(error);
72                         });
73                     } else {
74                         resolve(resultset);
75                     }
76                 }
77             });
78         });
79     }
80
81     /**
82      * Fetches the armored public Key blocks for all Keys matchin the pattern
83      * (if no pattern is given, fetches all known to gnupg)
84      * @param {String|Array<String>} pattern (optional)
85      * @returns {Promise<String>} Armored Key blocks
86      */
87     getKeysArmored(pattern) {
88         return new Promise(function(resolve, reject) {
89             let msg = createMessage('export');
90             msg.setParameter('armor', true);
91             if (pattern !== undefined){
92                 msg.setParameter('keys', pattern);
93             }
94             msg.post().then(function(result){
95                 resolve(result.data);
96             }, function(error){
97                 reject(error);
98             });
99         });
100     }
101
102     /**
103      * Returns the Key to be used by default for signing operations,
104      * looking up the gpg configuration, or returning the first key that
105      * contains a secret key.
106      * @returns {Promise<GPGME_Key>}
107      *
108      * @async
109      * TODO: getHasSecret always returns false at this moment, so this fucntion
110      * still does not fully work as intended.
111      *
112      */
113     getDefaultKey() {
114         let me = this;
115         return new Promise(function(resolve, reject){
116             let msg = createMessage('config_opt');
117             msg.setParameter('component', 'gpg');
118             msg.setParameter('option', 'default-key');
119             msg.post().then(function(response){
120                 if (response.value !== undefined
121                     && response.value.hasOwnProperty('string')
122                     && typeof(response.value.string) === 'string'
123                 ){
124                     me.getKeys(response.value.string,true).then(function(keys){
125                         if(keys.length === 1){
126                             resolve(keys[0]);
127                         } else {
128                             reject(gpgme_error('KEY_NO_DEFAULT'));
129                         }
130                     }, function(error){
131                         reject(error);
132                     });
133                 } else {
134                     // TODO: this is overly 'expensive' in communication
135                     // and probably performance, too
136                     me.getKeys(null,true).then(function(keys){
137                         for (let i=0; i < keys.length; i++){
138                             if (keys[i].get('hasSecret') === true){
139                                 resolve(keys[i]);
140                                 break;
141                             }
142                             if (i === keys.length -1){
143                                 reject(gpgme_error('KEY_NO_DEFAULT'));
144                             }
145                         }
146                     }, function(error){
147                         reject(error);
148                     });
149                 }
150             }, function(error){
151                 reject(error);
152             });
153         });
154     }
155
156     /**
157      *
158      * @param {String} armored Armored Key block of the Key(s) to be imported
159      * into gnupg
160      * @param {Boolean} prepare_sync prepare the keys for synched use
161      * (see getKeys()).
162      *
163      * @returns {Promise<Object>} result: A summary and an array of Keys
164      * considered
165      *
166      * @returns result.summary: Numerical summary of the result. See the
167      * feedbackValues variable for available values and the gnupg documentation
168      * https://www.gnupg.org/documentation/manuals/gpgme/Importing-Keys.html
169      * for details on their meaning.
170      * @returns {Array<Object>} result.Keys: Array of objects containing:
171      * @returns {GPGME_Key} Key.key The resulting key
172      * @returns {String} Key.status:
173      *      'nochange' if the Key was not changed,
174      *      'newkey' if the Key was imported in gpg, and did not exist
175      *         previously,
176      *      'change' if the key existed, but details were updated. For
177      *         details, Key.changes is available.
178      * @returns {Boolean} Key.changes.userId: userIds changed
179      * @returns {Boolean} Key.changes.signature: signatures changed
180      * @returns {Boolean} Key.changes.subkey: subkeys changed
181      */
182     importKey(armored, prepare_sync) {
183         let feedbackValues = ['considered', 'no_user_id', 'imported',
184             'imported_rsa', 'unchanged', 'new_user_ids', 'new_sub_keys',
185             'new_signatures', 'new_revocations', 'secret_read',
186             'secret_imported', 'secret_unchanged', 'skipped_new_keys',
187             'not_imported', 'skipped_v3_keys'];
188         if (!armored || typeof(armored) !== 'string'){
189             return Promise.reject(gpgme_error('PARAM_WRONG'));
190         }
191         let me = this;
192         return new Promise(function(resolve, reject){
193             let msg = createMessage('import');
194             msg.setParameter('data', armored);
195             msg.post().then(function(response){
196                 let infos = {};
197                 let fprs = [];
198                 for (let res=0; res<response.result.imports.length; res++){
199                     let result = response.result.imports[res];
200                     let status = '';
201                     if (result.status === 0){
202                         status = 'nochange';
203                     } else if ((result.status & 1) === 1){
204                         status = 'newkey';
205                     } else {
206                         status = 'change';
207                     }
208                     let changes = {};
209                     changes.userId = (result.status & 2) === 2;
210                     changes.signature = (result.status & 4) === 4;
211                     changes.subkey = (result.status & 8) === 8;
212                     //16 new secret key: not implemented
213
214                     fprs.push(result.fingerprint);
215                     infos[result.fingerprint] = {
216                         changes: changes,
217                         status: status
218                     };
219                 }
220                 let resultset = [];
221                 if (prepare_sync === true){
222                     me.getKeys(fprs, true).then(function(result){
223                         for (let i=0; i < result.length; i++) {
224                             resultset.push({
225                                 key: result[i],
226                                 changes: infos[result[i].fingerprint].changes,
227                                 status: infos[result[i].fingerprint].status
228                             });
229                         }
230                         let summary = {};
231                         for (let i=0; i < feedbackValues.length; i++ ){
232                             summary[feedbackValues[i]] =
233                                 response[feedbackValues[i]];
234                         }
235                         resolve({
236                             Keys:resultset,
237                             summary: summary
238                         });
239                     }, function(error){
240                         reject(error);
241                     });
242                 } else {
243                     for (let i=0; i < fprs.length; i++) {
244                         resultset.push({
245                             key: createKey(fprs[i]),
246                             changes: infos[fprs[i]].changes,
247                             status: infos[fprs[i]].status
248                         });
249                     }
250                     resolve(resultset);
251                 }
252
253             }, function(error){
254                 reject(error);
255             });
256
257
258         });
259
260
261     }
262
263     deleteKey(fingerprint){
264         if (isFingerprint(fingerprint) === true) {
265             let key = createKey(fingerprint);
266             key.delete();
267         }
268     }
269
270     /**
271      * Generates a new Key pair directly in gpg, and returns a GPGME_Key
272      * representing that Key. Please note that due to security concerns, secret
273      * Keys can not be _deleted_ from inside gpgmejs.
274      *
275      * @param {String} userId The user Id, e.g. "Foo Bar <foo@bar.baz>"
276      * @param {*} algo (optional) algorithm to be used. See
277      *      {@link supportedKeyAlgos } below for supported values.
278      * @param {Number} keyLength (optional) TODO
279      * @param {Date} expires (optional) Expiration date. If not set, expiration
280      * will be set to 'never'
281      *
282      * @returns{Promise<Key>}
283      */
284     generateKey(userId, algo = 'default', keyLength, expires){
285         if (
286             typeof(userId) !== 'string' ||
287             supportedKeyAlgos.indexOf(algo) < 0 ||
288             (expires && !(expires instanceof Date))
289             // TODO keylength
290             // TODO check for completeness of algos
291         ){
292             return Promise.reject(gpgme_error('PARAM_WRONG'));
293         }
294         let me = this;
295         return new Promise(function(resolve, reject){
296             let msg = createMessage('createkey');
297             msg.setParameter('userid', userId);
298             msg.setParameter('algo', algo);
299             if (expires){
300                 msg.setParameter('expires',
301                     Math.floor(expires.valueOf()/1000));
302             }
303             // TODO append keylength to algo
304             msg.post().then(function(response){
305                 me.getKeys(response.fingerprint, true).then(
306                     // TODO make prepare_sync (second parameter) optional here.
307                     function(result){
308                         resolve(result);
309                     }, function(error){
310                         reject(error);
311                     });
312             }, function(error) {
313                 reject(error);
314             });
315         });
316     }
317 }
318
319 /**
320  * A list of algorithms supported for key generation.
321  */
322 const supportedKeyAlgos = [
323     'default',
324     'rsa',
325     'dsa',
326     'elg',
327     'ed25519',
328     'cv25519'
329 ];