diff --git a/lang/js/src/Keyring.js b/lang/js/src/Keyring.js index 7d9b370a..81a047ca 100644 --- a/lang/js/src/Keyring.js +++ b/lang/js/src/Keyring.js @@ -1,426 +1,435 @@ /* gpgme.js - Javascript integration for gpgme * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik * * This file is part of GPGME. * * GPGME is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * GPGME is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this program; if not, see . * SPDX-License-Identifier: LGPL-2.1+ * * Author(s): * Maximilian Krambach */ import { createMessage } from './Message'; import { createKey } from './Key'; import { isFingerprint } from './Helpers'; import { gpgme_error } from './Errors'; /** * This class offers access to the gnupg keyring */ export class GPGME_Keyring { /** * Queries Keys (all Keys or a subset) from gnupg. * * @param {String | Array} pattern (optional) A pattern to * search for in userIds or KeyIds. * @param {Boolean} prepare_sync (optional) if set to true, most data * (with the exception of armored Key blocks) will be cached for the * Keys. This enables direct, synchronous use of these properties for * all keys. It does not check for changes on the backend. The cached * information can be updated with the {@link Key.refresh} method. * @param {Boolean} search (optional) retrieve Keys from external * servers with the method(s) defined in gnupg (e.g. WKD/HKP lookup) * @returns {Promise>} * @static * @async */ getKeys (pattern, prepare_sync=false, search=false){ return new Promise(function (resolve, reject) { let msg = createMessage('keylist'); if (pattern !== undefined && pattern !== null){ msg.setParameter('keys', pattern); } msg.setParameter('sigs', true); if (search === true){ msg.setParameter('locate', true); } msg.post().then(function (result){ let resultset = []; if (result.keys.length === 0){ resolve([]); } else { let secondrequest; if (prepare_sync === true) { secondrequest = function () { let msg2 = createMessage('keylist'); msg2.setParameter('keys', pattern); msg2.setParameter('secret', true); return msg2.post(); }; } else { secondrequest = function () { return Promise.resolve(true); }; } secondrequest().then(function (answer) { for (let i=0; i < result.keys.length; i++){ if (prepare_sync === true){ if (answer && answer.keys) { for (let j=0; j < answer.keys.length; j++ ){ const a = answer.keys[j]; const b = result.keys[i]; if ( a.fingerprint === b.fingerprint ) { if (a.secret === true){ b.hasSecret = true; } else { b.hasSecret = false; } break; } } } } let k = createKey(result.keys[i].fingerprint, !prepare_sync, result.keys[i]); resultset.push(k); } resolve(resultset); }, function (error){ reject(error); }); } }); }); } /** * @typedef {Object} exportResult The result of a getKeysArmored * operation. * @property {String} armored The public Key(s) as armored block. Note * that the result is one armored block, and not a block per key. * @property {Array} secret_fprs (optional) list of * fingerprints for those Keys that also have a secret Key available in * gnupg. The secret key will not be exported, but the fingerprint can * be used in operations needing a secret key. */ /** * Fetches the armored public Key blocks for all Keys matching the * pattern (if no pattern is given, fetches all keys known to gnupg). * @param {String|Array} pattern (optional) The Pattern to * search for * @param {Boolean} with_secret_fpr (optional) also return a list of * fingerprints for the keys that have a secret key available * @returns {Promise} Object containing the * armored Key(s) and additional information. * @static * @async */ getKeysArmored (pattern, with_secret_fpr) { return new Promise(function (resolve, reject) { let msg = createMessage('export'); msg.setParameter('armor', true); if (with_secret_fpr === true) { msg.setParameter('with-sec-fprs', true); } if (pattern !== undefined && pattern !== null){ msg.setParameter('keys', pattern); } msg.post().then(function (answer){ const result = { armored: answer.data }; if (with_secret_fpr === true && answer.hasOwnProperty('sec-fprs') ) { result.secret_fprs = answer['sec-fprs']; } resolve(result); }, function (error){ reject(error); }); }); } /** * Returns the Key used by default in gnupg. * (a.k.a. 'primary Key or 'main key'). * It looks up the gpg configuration if set, or the first key that * contains a secret key. * * @returns {Promise} * @async * @static */ getDefaultKey (prepare_sync = false) { let me = this; return new Promise(function (resolve, reject){ let msg = createMessage('config_opt'); msg.setParameter('component', 'gpg'); msg.setParameter('option', 'default-key'); msg.post().then(function (resp){ if (resp.option !== undefined && resp.option.hasOwnProperty('value') && resp.option.value.length === 1 && resp.option.value[0].hasOwnProperty('string') && typeof (resp.option.value[0].string) === 'string'){ me.getKeys(resp.option.value[0].string, true).then( function (keys){ if (keys.length === 1){ resolve(keys[0]); } else { reject(gpgme_error('KEY_NO_DEFAULT')); } }, function (error){ reject(error); }); } else { let msg = createMessage('keylist'); msg.setParameter('secret', true); msg.post().then(function (result){ if (result.keys.length === 0){ reject(gpgme_error('KEY_NO_DEFAULT')); } else { for (let i=0; i< result.keys.length; i++ ) { if (result.keys[i].invalid === false) { let k = createKey( result.keys[i].fingerprint, !prepare_sync, result.keys[i]); resolve(k); break; } else if (i === result.keys.length - 1){ reject(gpgme_error('KEY_NO_DEFAULT')); } } } }, function (error){ reject(error); }); } }, function (error){ reject(error); }); }); } /** * @typedef {Object} importResult The result of a Key update * @property {Object} summary Numerical summary of the result. See the * feedbackValues variable for available Keys values and the gnupg * documentation. * https://www.gnupg.org/documentation/manuals/gpgme/Importing-Keys.html * for details on their meaning. * @property {Array} Keys Array of Object containing * GPGME_Keys with additional import information * */ /** * @typedef {Object} importedKeyResult * @property {GPGME_Key} key The resulting key * @property {String} status: * 'nochange' if the Key was not changed, * 'newkey' if the Key was imported in gpg, and did not exist * previously, * 'change' if the key existed, but details were updated. For details, * Key.changes is available. * @property {Boolean} changes.userId Changes in userIds * @property {Boolean} changes.signature Changes in signatures * @property {Boolean} changes.subkey Changes in subkeys */ /** * Import an armored Key block into gnupg. Note that this currently * will not succeed on private Key blocks. * @param {String} armored Armored Key block of the Key(s) to be * imported into gnupg * @param {Boolean} prepare_sync prepare the keys for synched use * (see {@link getKeys}). * @returns {Promise} A summary and Keys considered. * @async * @static */ importKey (armored, prepare_sync) { let feedbackValues = ['considered', 'no_user_id', 'imported', 'imported_rsa', 'unchanged', 'new_user_ids', 'new_sub_keys', 'new_signatures', 'new_revocations', 'secret_read', 'secret_imported', 'secret_unchanged', 'skipped_new_keys', 'not_imported', 'skipped_v3_keys']; if (!armored || typeof (armored) !== 'string'){ return Promise.reject(gpgme_error('PARAM_WRONG')); } let me = this; return new Promise(function (resolve, reject){ let msg = createMessage('import'); msg.setParameter('data', armored); msg.post().then(function (response){ let infos = {}; let fprs = []; let summary = {}; for (let i=0; i < feedbackValues.length; i++ ){ summary[feedbackValues[i]] = response.result[feedbackValues[i]]; } if (!response.result.hasOwnProperty('imports') || response.result.imports.length === 0 ){ resolve({ Keys:[],summary: summary }); return; } for (let res=0; res} * @async * @static */ deleteKey (fingerprint){ if (isFingerprint(fingerprint) === true) { let key = createKey(fingerprint); return key.delete(); } else { return Promise.reject(gpgme_error('KEY_INVALID')); } } /** * Generates a new Key pair directly in gpg, and returns a GPGME_Key * representing that Key. Please note that due to security concerns, * secret Keys can not be deleted or exported from inside gpgme.js. * * @param {String} userId The user Id, e.g. 'Foo Bar ' * @param {String} algo (optional) algorithm (and optionally key size) * to be used. See {@link supportedKeyAlgos} below for supported - * values. + * values. If ommitted, 'default' is used. * @param {Date} expires (optional) Expiration date. If not set, * expiration will be set to 'never' + * @param {String} subkey_algo (optional) algorithm of the encryption + * subkey. If ommited the same as algo is used. * * @return {Promise} * @async */ - generateKey (userId, algo = 'default', expires){ + generateKey (userId, algo = 'default', expires, subkey_algo){ if ( typeof (userId) !== 'string' || // eslint-disable-next-line no-use-before-define supportedKeyAlgos.indexOf(algo) < 0 || (expires && !(expires instanceof Date)) ){ return Promise.reject(gpgme_error('PARAM_WRONG')); } + // eslint-disable-next-line no-use-before-define + if (subkey_algo && supportedKeyAlgos.indexOf(subkey_algo) < 0 ){ + return Promise.reject(gpgme_error('PARAM_WRONG')); + } let me = this; return new Promise(function (resolve, reject){ let msg = createMessage('createkey'); msg.setParameter('userid', userId); msg.setParameter('algo', algo ); + if (subkey_algo) { + msg.setParameter('subkey-algo', subkey_algo ); + } if (expires){ const now = new Date(); msg.setParameter('expires', Math.floor((expires - now) /1000)); } else { msg.setParameter('expires', 0); } msg.post().then(function (response){ me.getKeys(response.fingerprint, true).then( // TODO prepare_sync? function (result){ resolve(result); }, function (error){ reject(error); }); }, function (error) { reject(error); }); }); } } /** * List of algorithms supported for key generation. Please refer to the gnupg * documentation for details */ const supportedKeyAlgos = [ 'default', 'rsa', 'rsa2048', 'rsa3072', 'rsa4096', 'dsa', 'dsa2048', 'dsa3072', 'dsa4096', 'elg', 'elg2048', 'elg3072', 'elg4096', 'ed25519', 'cv25519', 'brainpoolP256r1', 'brainpoolP384r1', 'brainpoolP512r1', 'NIST P-256', 'NIST P-384', 'NIST P-521' ]; \ No newline at end of file diff --git a/lang/js/src/permittedOperations.js b/lang/js/src/permittedOperations.js index 48ff7fa7..3142725c 100644 --- a/lang/js/src/permittedOperations.js +++ b/lang/js/src/permittedOperations.js @@ -1,400 +1,403 @@ /* gpgme.js - Javascript integration for gpgme * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik * * This file is part of GPGME. * * GPGME is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * GPGME is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this program; if not, see . * SPDX-License-Identifier: LGPL-2.1+ * * Author(s): * Maximilian Krambach */ /** * @typedef {Object} messageProperty * A message Property is defined by it's key. * @property {Array} allowed Array of allowed types. * Currently accepted values are 'number', 'string', 'boolean'. * @property {Boolean} array_allowed If the value can be an array of types * defined in allowed * @property {Array<*>} allowed_data (optional) restricts to the given values */ /** * Definition of the possible interactions with gpgme-json. * @param {Object} operation Each operation is named by a key and contains * the following properties: * @property {messageProperty} required An object with all required parameters * @property {messageProperty} optional An object with all optional parameters * @property {Boolean} pinentry (optional) If true, a password dialog is * expected, thus a connection tuimeout is not advisable * @property {Object} answer The definition on what to expect as answer, if the * answer is not an error * @property {Array} answer.type the type(s) as reported by gpgme-json. * @property {Object} answer.data key-value combinations of expected properties * of an answer and their type ('boolean', 'string', object) @const */ export const permittedOperations = { encrypt: { pinentry: true, // TODO only with signing_keys required: { 'keys': { allowed: ['string'], array_allowed: true }, 'data': { allowed: ['string'] } }, optional: { 'protocol': { allowed: ['string'], allowed_data: ['cms', 'openpgp'] }, 'signing_keys': { allowed: ['string'], array_allowed: true }, 'base64': { allowed: ['boolean'] }, 'mime': { allowed: ['boolean'] }, 'armor': { allowed: ['boolean'] }, 'always-trust': { allowed: ['boolean'] }, 'no-encrypt-to': { allowed: ['string'], array_allowed: true }, 'no-compress': { allowed: ['boolean'] }, 'throw-keyids': { allowed: ['boolean'] }, 'want-address': { allowed: ['boolean'] }, 'wrap': { allowed: ['boolean'] } }, answer: { type: ['ciphertext'], data: { 'data': 'string', 'base64':'boolean' } } }, decrypt: { pinentry: true, required: { 'data': { allowed: ['string'] } }, optional: { 'protocol': { allowed: ['string'], allowed_data: ['cms', 'openpgp'] }, 'base64': { allowed: ['boolean'] } }, answer: { type: ['plaintext'], data: { 'data': 'string', 'base64': 'boolean', 'mime': 'boolean', 'signatures': 'object', 'info': 'object' } } }, sign: { pinentry: true, required: { 'data': { allowed: ['string'] }, 'keys': { allowed: ['string'], array_allowed: true } }, optional: { 'protocol': { allowed: ['string'], allowed_data: ['cms', 'openpgp'] }, 'sender': { allowed: ['string'], }, 'mode': { allowed: ['string'], allowed_data: ['detached', 'clearsign'] // TODO 'opaque' is not used, but available on native app }, 'base64': { allowed: ['boolean'] }, 'armor': { allowed: ['boolean'] }, }, answer: { type: ['signature', 'ciphertext'], data: { 'data': 'string', 'base64':'boolean' } } }, // note: For the meaning of the optional keylist flags, refer to // https://www.gnupg.org/documentation/manuals/gpgme/Key-Listing-Mode.html keylist:{ required: {}, optional: { 'protocol': { allowed: ['string'], allowed_data: ['cms', 'openpgp'] }, 'secret': { allowed: ['boolean'] }, 'extern': { allowed: ['boolean'] }, 'local':{ allowed: ['boolean'] }, 'locate': { allowed: ['boolean'] }, 'sigs':{ allowed: ['boolean'] }, 'notations':{ allowed: ['boolean'] }, 'tofu': { allowed: ['boolean'] }, 'ephemeral': { allowed: ['boolean'] }, 'validate': { allowed: ['boolean'] }, 'keys': { allowed: ['string'], array_allowed: true } }, answer: { type: ['keys'], data: { 'base64': 'boolean', 'keys': 'object' } } }, export: { required: {}, optional: { 'protocol': { allowed: ['string'], allowed_data: ['cms', 'openpgp'] }, 'keys': { allowed: ['string'], array_allowed: true }, 'armor': { allowed: ['boolean'] }, 'extern': { allowed: ['boolean'] }, 'minimal': { allowed: ['boolean'] }, 'raw': { allowed: ['boolean'] }, 'pkcs12': { allowed: ['boolean'] }, 'with-sec-fprs': { allowed: ['boolean'] } // secret: not yet implemented }, answer: { type: ['keys'], data: { 'data': 'string', 'base64': 'boolean', 'sec-fprs': 'object' } } }, import: { required: { 'data': { allowed: ['string'] } }, optional: { 'protocol': { allowed: ['string'], allowed_data: ['cms', 'openpgp'] }, 'base64': { allowed: ['boolean'] }, }, answer: { type: [], data: { 'result': 'object' } } }, delete: { pinentry: true, required:{ 'key': { allowed: ['string'] } }, optional: { 'protocol': { allowed: ['string'], allowed_data: ['cms', 'openpgp'] }, }, answer: { data: { 'success': 'boolean' } } }, version: { required: {}, optional: {}, answer: { type: [''], data: { 'gpgme': 'string', 'info': 'object' } } }, createkey: { pinentry: true, required: { userid: { allowed: ['string'] } }, optional: { algo: { allowed: ['string'] }, + 'subkey-algo': { + allowed: ['string'] + }, expires: { allowed: ['number'], } }, answer: { type: [''], data: { 'fingerprint': 'string' } } }, verify: { required: { data: { allowed: ['string'] } }, optional: { 'protocol': { allowed: ['string'], allowed_data: ['cms', 'openpgp'] }, 'signature': { allowed: ['string'] }, 'base64':{ allowed: ['boolean'] } }, answer: { type: ['plaintext'], data:{ data: 'string', base64:'boolean', info: 'object' // info.file_name: Optional string of the plaintext file name. // info.is_mime: Boolean if the messages claims it is MIME. // info.signatures: Array of signatures } } }, config_opt: { required: { 'component':{ allowed: ['string'], // allowed_data: ['gpg'] // TODO check all available }, 'option': { allowed: ['string'], // allowed_data: ['default-key'] // TODO check all available } }, optional: {}, answer: { type: [], data: { option: 'object' } } } /** * TBD handling of secrets * TBD key modification? */ };