diff --git a/lang/js/CHECKLIST b/lang/js/CHECKLIST index fe260187..2e70dff1 100644 --- a/lang/js/CHECKLIST +++ b/lang/js/CHECKLIST @@ -1,30 +1,31 @@ NativeConnection: [X] nativeConnection: successfully sending an encrypt request, receiving an answer [X] nativeConnection successfull on Chromium, chrome and firefox [*] nativeConnection successfull on Windows, macOS, Linux [X] nativeConnection with delayed, multipart (> 1MB) answer [x] Message handling (encrypt, decrypt verify, sign) [x] encrypt, decrypt [x] verify [x] sign [*] Key handling (import/export, modifying, status queries) [x] Import (not importing secret) [x] Export (not exporting secret) - [x] status queries + [*] status queries + [ ] getHasSecret [ ] key generation [ ] modification - [*] Configuration handling + [x] Configuration handling [ ] check for completeness Communication with other implementations [-] option to export SECRET Key into localstore used by e.g. mailvelope? current discussion states that this won't be possible due to security concerns Management: [*] Define the gpgme interface [x] check Permissions (e.g. csp) for the different envs [x] agree on license [*] tests diff --git a/lang/js/DemoExtension/maindemo.js b/lang/js/DemoExtension/maindemo.js index 5cde1ce8..67b811f6 100644 --- a/lang/js/DemoExtension/maindemo.js +++ b/lang/js/DemoExtension/maindemo.js @@ -1,57 +1,67 @@ /* 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 */ /* global document, Gpgmejs */ document.addEventListener('DOMContentLoaded', function() { Gpgmejs.init().then(function(gpgmejs){ document.getElementById('buttonencrypt').addEventListener('click', function(){ let data = document.getElementById('cleartext').value; let keyId = document.getElementById('pubkey').value; gpgmejs.encrypt(data, keyId).then( function(answer){ if (answer.data){ document.getElementById( 'answer').value = answer.data; } }, function(errormsg){ - alert( errormsg.code + ' ' + errormsg.msg); + alert( errormsg.message); }); }); document.getElementById('buttondecrypt').addEventListener('click', function(){ let data = document.getElementById('ciphertext').value; gpgmejs.decrypt(data).then( function(answer){ if (answer.data){ document.getElementById( 'answer').value = answer.data; } }, function(errormsg){ - alert( errormsg.code + ' ' + errormsg.msg); + alert(errormsg.message); }); }); + + document.getElementById('getdefaultkey').addEventListener('click', + function(){ + gpgmejs.Keyring.getDefaultKey().then(function(answer){ + document.getElementById('defaultkey').innerHtml = + answer.fingerprint; + }, function(errormsg){ + alert(errormsg.message); + }); + }); }); }); diff --git a/lang/js/DemoExtension/mainui.html b/lang/js/DemoExtension/mainui.html index 76b8a221..91be7bbc 100644 --- a/lang/js/DemoExtension/mainui.html +++ b/lang/js/DemoExtension/mainui.html @@ -1,33 +1,44 @@



Result data:

+ +
+ diff --git a/lang/js/src/Errors.js b/lang/js/src/Errors.js index dabf6a5c..73e74382 100644 --- a/lang/js/src/Errors.js +++ b/lang/js/src/Errors.js @@ -1,141 +1,146 @@ /* 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 */ const err_list = { // Connection 'CONN_NO_CONNECT': { msg:'Connection with the nativeMessaging host could not be' + ' established.', type: 'error' }, 'CONN_EMPTY_GPG_ANSWER':{ msg: 'The nativeMessaging answer was empty.', type: 'error' }, 'CONN_TIMEOUT': { msg: 'A connection timeout was exceeded.', type: 'error' }, 'CONN_UNEXPECTED_ANSWER': { msg: 'The answer from gnupg was not as expected.', type: 'error' }, 'CONN_ALREADY_CONNECTED':{ msg: 'A connection was already established.', type: 'warning' }, // Message/Data 'MSG_INCOMPLETE': { msg: 'The Message did not match the minimum requirements for' + ' the interaction.', type: 'error' }, 'MSG_EMPTY' : { msg: 'The Message is empty.', type: 'error' }, 'MSG_WRONG_OP': { msg: 'The operation requested could not be found', type: 'error' }, 'MSG_NO_KEYS' : { msg: 'There were no valid keys provided.', type: 'warning' }, 'MSG_NOT_A_FPR': { msg: 'The String is not an accepted fingerprint', type: 'warning' }, 'KEY_INVALID': { msg:'Key object is invalid', type: 'error' }, 'KEY_NOKEY': { msg:'This key does not exist in GPG', type: 'error' }, 'KEY_NO_INIT': { msg:'This property has not been retrieved yet from GPG', type: 'error' }, + 'KEY_NO_DEFAULT': { + msg:'A default key could not be established. Please check yout gpg ' + + 'configuration', + type: 'error' + }, // generic 'PARAM_WRONG':{ msg: 'Invalid parameter was found', type: 'error' }, 'PARAM_IGNORED': { msg: 'An parameter was set that has no effect in gpgmejs', type: 'warning' }, 'GENERIC_ERROR': { msg: 'Unspecified error', type: 'error' } }; /** * Checks the given error code and returns an error object with some * information about meaning and origin * @param {*} code Error code. Should be in err_list or 'GNUPG_ERROR' * @param {*} info Error message passed through if code is 'GNUPG_ERROR' */ export function gpgme_error(code = 'GENERIC_ERROR', info){ if (err_list.hasOwnProperty(code)){ if (err_list[code].type === 'error'){ return new GPGME_Error(code); } if (err_list[code].type === 'warning'){ // eslint-disable-next-line no-console console.warn(code + ': ' + err_list[code].msg); } return null; } else if (code === 'GNUPG_ERROR'){ return new GPGME_Error(code, info); } else { return new GPGME_Error('GENERIC_ERROR'); } } class GPGME_Error extends Error{ constructor(code, msg=''){ if (code === 'GNUPG_ERROR' && typeof(msg) === 'string'){ super(msg); } else if (err_list.hasOwnProperty(code)){ if (msg){ super(err_list[code].msg + '--' + msg); } else { super(err_list[code].msg); } } else { super(err_list['GENERIC_ERROR'].msg); } this.code = code || 'GENERIC_ERROR'; } set code(value){ this._code = value; } get code(){ return this._code; } } \ No newline at end of file diff --git a/lang/js/src/Key.js b/lang/js/src/Key.js index 5986254e..3e4f1c78 100644 --- a/lang/js/src/Key.js +++ b/lang/js/src/Key.js @@ -1,555 +1,563 @@ /* 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 { isFingerprint, isLongId } from './Helpers'; import { gpgme_error } from './Errors'; import { createMessage } from './Message'; /** * Validates the fingerprint. * @param {String} fingerprint */ export function createKey(fingerprint){ if (!isFingerprint(fingerprint)){ return gpgme_error('PARAM_WRONG'); } else return new GPGME_Key(fingerprint); } /** * Representing the Keys as stored in GPG * It allows to query almost all information defined in gpgme Key Objects * Refer to validKeyProperties for available information, and the gpgme * documentation on their meaning * (https://www.gnupg.org/documentation/manuals/gpgme/Key-objects.html) * */ export class GPGME_Key { constructor(fingerprint){ this.fingerprint = fingerprint; } set fingerprint(fpr){ if (isFingerprint(fpr) === true) { if (this._data === undefined) { this._data = {fingerprint: fpr}; } else { if (this._data.fingerprint === undefined){ this._data.fingerprint = fpr; } } } } get fingerprint(){ if (!this._data || !this._data.fingerprint){ return gpgme_error('KEY_INVALID'); } return this._data.fingerprint; } /** * * @param {Object} data Bulk set data for this key, with the Object as sent * by gpgme-json. * @returns {GPGME_Key|GPGME_Error} The Key object itself after values have * been set */ setKeyData(data){ if (this._data === undefined) { this._data = {}; } if ( typeof(data) !== 'object') { return gpgme_error('KEY_INVALID'); } if (!this._data.fingerprint && isFingerprint(data.fingerprint)){ if (data.fingerprint !== this.fingerprint){ return gpgme_error('KEY_INVALID'); } this._data.fingerprint = data.fingerprint; } else if (this._data.fingerprint !== data.fingerprint){ return gpgme_error('KEY_INVALID'); } let dataKeys = Object.keys(data); for (let i=0; i< dataKeys.length; i++){ if (!validKeyProperties.hasOwnProperty(dataKeys[i])){ return gpgme_error('KEY_INVALID'); } if (validKeyProperties[dataKeys[i]](data[dataKeys[i]]) !== true ){ return gpgme_error('KEY_INVALID'); } switch (dataKeys[i]){ case 'subkeys': this._data.subkeys = []; for (let i=0; i< data.subkeys.length; i++) { this._data.subkeys.push( new GPGME_Subkey(data.subkeys[i])); } break; case 'userids': this._data.userids = []; for (let i=0; i< data.userids.length; i++) { this._data.userids.push( new GPGME_UserId(data.userids[i])); } break; case 'last_update': this._data[dataKeys[i]] = new Date( data[dataKeys[i]] * 1000 ); break; default: this._data[dataKeys[i]] = data[dataKeys[i]]; } } return this; } /** * Query any property of the Key list * @param {String} property Key property to be retreived * @param {*} cached (optional) if false, the data will be directly queried * from gnupg. * @returns {*|Promise<*>} the value, or if not cached, a Promise * resolving on the value */ get(property, cached=true) { if (cached === false) { let me = this; return new Promise(function(resolve, reject) { if (!validKeyProperties.hasOwnProperty(property)){ reject('PARAM_WRONG'); } else if (property === 'armored'){ resolve(me.getArmor()); } else if (property === 'hasSecret'){ resolve(me.getHasSecret()); } else { me.refreshKey().then(function(key){ resolve(key.get(property, true)); }, function(error){ reject(error); }); } }); } else { if (!validKeyProperties.hasOwnProperty(property)){ return gpgme_error('PARAM_WRONG'); } if (!this._data.hasOwnProperty(property)){ return gpgme_error('KEY_NO_INIT'); } else { return (this._data[property]); } } } /** * Reloads the Key from gnupg */ refreshKey() { let me = this; return new Promise(function(resolve, reject) { if (!me._data.fingerprint){ reject(gpgme_error('KEY_INVALID')); } let msg = createMessage('keylist'); msg.setParameter('sigs', true); msg.setParameter('keys', me._data.fingerprint); msg.post().then(function(result){ if (result.keys.length === 1){ me.setKeyData(result.keys[0]); resolve(me); } else { reject(gpgme_error('KEY_NOKEY')); } }, function (error) { reject(gpgme_error('GNUPG_ERROR'), error); }); }); } /** * Query the armored block of the non- secret parts of the Key directly * from gpg. * @returns {Promise} + * @async */ getArmor(){ let me = this; return new Promise(function(resolve, reject) { if (!me._data.fingerprint){ reject(gpgme_error('KEY_INVALID')); } let msg = createMessage('export'); msg.setParameter('armor', true); msg.setParameter('keys', me._data.fingerprint); msg.post().then(function(result){ me._data.armored = result.data; resolve(result.data); }, function(error){ reject(error); }); }); } + /** + * Find out if the Key includes a secret part + * @returns {Promise} + * + * @async + */ + // TODO: Does not work yet, result is always false getHasSecret(){ let me = this; return new Promise(function(resolve, reject) { if (!me._data.fingerprint){ reject(gpgme_error('KEY_INVALID')); } let msg = createMessage('keylist'); msg.setParameter('keys', me._data.fingerprint); msg.setParameter('secret', true); msg.post().then(function(result){ me._data.hasSecret = null; if (result.keys === undefined || result.keys.length < 1) { me._data.hasSecret = false; resolve(false); } else if (result.keys.length === 1){ let key = result.keys[0]; if (!key.subkeys){ me._data.hasSecret = false; resolve(false); } else { for (let i=0; i < key.subkeys.length; i++) { if (key.subkeys[i].secret === true) { me._data.hasSecret = true; resolve(true); break; } if (i === (key.subkeys.length -1)) { me._data.hasSecret = false; resolve(false); } } } } else { reject(gpgme_error('CONN_UNEXPECTED_ANSWER')); } }, function(error){ reject(error); }); }); } /** * Convenience functions to be directly used as properties of the Key * Notice that these rely on cached info and may be outdated. Use the async * get(property, false) if you need the most current info */ /** * @returns {String} The armored public Key block */ get armored(){ return this.get('armored', true); } /** * @returns {Boolean} If the key is considered a "private Key", * i.e. owns a secret subkey. */ get hasSecret(){ return this.get('hasSecret', true); } /** * Deletes the public Key from the GPG Keyring. Note that a deletion of a * secret key is not supported by the native backend. * @returns {Promise} Success if key was deleted, rejects with a * GPG error otherwise */ delete(){ let me = this; return new Promise(function(resolve, reject){ if (!me._data.fingerprint){ reject(gpgme_error('KEY_INVALID')); } let msg = createMessage('delete'); msg.setParameter('key', me._data.fingerprint); msg.post().then(function(result){ resolve(result.success); }, function(error){ reject(error); }); }); } } /** * The subkeys of a Key. Currently, they cannot be refreshed separately */ class GPGME_Subkey { constructor(data){ let keys = Object.keys(data); for (let i=0; i< keys.length; i++) { this.setProperty(keys[i], data[keys[i]]); } } setProperty(property, value){ if (!this._data){ this._data = {}; } if (validSubKeyProperties.hasOwnProperty(property)){ if (validSubKeyProperties[property](value) === true) { if (property === 'timestamp' || property === 'expires'){ this._data[property] = new Date(value * 1000); } else { this._data[property] = value; } } } } /** * * @param {String} property Information to request * @returns {String | Number} * TODO: date properties are numbers with Date in seconds */ get(property) { if (this._data.hasOwnProperty(property)){ return (this._data[property]); } } } class GPGME_UserId { constructor(data){ let keys = Object.keys(data); for (let i=0; i< keys.length; i++) { this.setProperty(keys[i], data[keys[i]]); } } setProperty(property, value){ if (!this._data){ this._data = {}; } if (validUserIdProperties.hasOwnProperty(property)){ if (validUserIdProperties[property](value) === true) { if (property === 'last_update'){ this._data[property] = new Date(value*1000); } else { this._data[property] = value; } } } } /** * * @param {String} property Information to request * @returns {String | Number} * TODO: date properties are numbers with Date in seconds */ get(property) { if (this._data.hasOwnProperty(property)){ return (this._data[property]); } } } const validUserIdProperties = { 'revoked': function(value){ return typeof(value) === 'boolean'; }, 'invalid': function(value){ return typeof(value) === 'boolean'; }, 'uid': function(value){ if (typeof(value) === 'string' || value === ''){ return true; } return false; }, 'validity': function(value){ if (typeof(value) === 'string'){ return true; } return false; }, 'name': function(value){ if (typeof(value) === 'string' || value === ''){ return true; } return false; }, 'email': function(value){ if (typeof(value) === 'string' || value === ''){ return true; } return false; }, 'address': function(value){ if (typeof(value) === 'string' || value === ''){ return true; } return false; }, 'comment': function(value){ if (typeof(value) === 'string' || value === ''){ return true; } return false; }, 'origin': function(value){ return Number.isInteger(value); }, 'last_update': function(value){ return Number.isInteger(value); } }; const validSubKeyProperties = { 'invalid': function(value){ return typeof(value) === 'boolean'; }, 'can_encrypt': function(value){ return typeof(value) === 'boolean'; }, 'can_sign': function(value){ return typeof(value) === 'boolean'; }, 'can_certify': function(value){ return typeof(value) === 'boolean'; }, 'can_authenticate': function(value){ return typeof(value) === 'boolean'; }, 'secret': function(value){ return typeof(value) === 'boolean'; }, 'is_qualified': function(value){ return typeof(value) === 'boolean'; }, 'is_cardkey': function(value){ return typeof(value) === 'boolean'; }, 'is_de_vs': function(value){ return typeof(value) === 'boolean'; }, 'pubkey_algo_name': function(value){ return typeof(value) === 'string'; // TODO: check against list of known?[''] }, 'pubkey_algo_string': function(value){ return typeof(value) === 'string'; // TODO: check against list of known?[''] }, 'keyid': function(value){ return isLongId(value); }, 'pubkey_algo': function(value) { return (Number.isInteger(value) && value >= 0); }, 'length': function(value){ return (Number.isInteger(value) && value > 0); }, 'timestamp': function(value){ return (Number.isInteger(value) && value > 0); }, 'expires': function(value){ return (Number.isInteger(value) && value > 0); } }; const validKeyProperties = { //TODO better validation? 'fingerprint': function(value){ return isFingerprint(value); }, 'armored': function(value){ return typeof(value === 'string'); }, 'revoked': function(value){ return typeof(value) === 'boolean'; }, 'expired': function(value){ return typeof(value) === 'boolean'; }, 'disabled': function(value){ return typeof(value) === 'boolean'; }, 'invalid': function(value){ return typeof(value) === 'boolean'; }, 'can_encrypt': function(value){ return typeof(value) === 'boolean'; }, 'can_sign': function(value){ return typeof(value) === 'boolean'; }, 'can_certify': function(value){ return typeof(value) === 'boolean'; }, 'can_authenticate': function(value){ return typeof(value) === 'boolean'; }, 'secret': function(value){ return typeof(value) === 'boolean'; }, 'is_qualified': function(value){ return typeof(value) === 'boolean'; }, 'protocol': function(value){ return typeof(value) === 'string'; //TODO check for implemented ones }, 'issuer_serial': function(value){ return typeof(value) === 'string'; }, 'issuer_name': function(value){ return typeof(value) === 'string'; }, 'chain_id': function(value){ return typeof(value) === 'string'; }, 'owner_trust': function(value){ return typeof(value) === 'string'; }, 'last_update': function(value){ return (Number.isInteger(value)); //TODO undefined/null possible? }, 'origin': function(value){ return (Number.isInteger(value)); }, 'subkeys': function(value){ return (Array.isArray(value)); }, 'userids': function(value){ return (Array.isArray(value)); }, 'tofu': function(value){ return (Array.isArray(value)); }, 'hasSecret': function(value){ return typeof(value) === 'boolean'; } }; diff --git a/lang/js/src/Keyring.js b/lang/js/src/Keyring.js index 0d4e3c52..e07a5934 100644 --- a/lang/js/src/Keyring.js +++ b/lang/js/src/Keyring.js @@ -1,259 +1,313 @@ /* 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'; export class GPGME_Keyring { constructor(){ } /** * @param {String} pattern (optional) pattern A pattern to search for, * in userIds or KeyIds * @param {Boolean} prepare_sync (optional, default true) if set to true, * Key.armor and Key.hasSecret will be called, so they can be used * inmediately. This allows for full synchronous use. If set to false, * these will initially only be available as Promises in getArmor() and * getHasSecret() * @returns {Promise.>} * */ getKeys(pattern, prepare_sync){ return new Promise(function(resolve, reject) { let msg = createMessage('keylist'); if (pattern !== undefined){ msg.setParameter('keys', pattern); } msg.setParameter('sigs', true); msg.post().then(function(result){ let resultset = []; let promises = []; if (result.keys.length === 0){ resolve([]); } else { for (let i=0; i< result.keys.length; i++){ let k = createKey(result.keys[i].fingerprint); k.setKeyData(result.keys[i]); if (prepare_sync === true){ promises.push(k.getArmor()); promises.push(k.getHasSecret()); } resultset.push(k); } if (promises.length > 0) { Promise.all(promises).then(function() { resolve(resultset); }, function(error){ reject(error); }); } else { resolve(resultset); } } }); }); } /** * Fetches the armored public Key blocks for all Keys matchin the pattern * (if no pattern is given, fetches all known to gnupg) * @param {String|Array} pattern (optional) * @returns {Promise} Armored Key blocks */ getKeysArmored(pattern) { return new Promise(function(resolve, reject) { let msg = createMessage('export'); msg.setParameter('armor', true); if (pattern !== undefined){ msg.setParameter('keys', pattern); } msg.post().then(function(result){ resolve(result.data); }, function(error){ reject(error); }); }); } - // getDefaultKey() Big TODO + /** + * Returns the Key to be used by default for signing operations, + * looking up the gpg configuration, or returning the first key that + * contains a secret key. + * @returns {Promise} + * + * @async + * TODO: getHasSecret always returns false at this moment, so this fucntion + * still does not fully work as intended. + * + */ + getDefaultKey() { + 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(response){ + if (response.value !== undefined + && response.value.hasOwnProperty('string') + && typeof(response.value.string) === 'string' + ){ + me.getKeys(response.value.string,true).then(function(keys){ + if(keys.length === 1){ + resolve(keys[0]); + } else { + reject(gpgme_error('KEY_NO_DEFAULT')); + } + }, function(error){ + reject(error); + }); + } else { + // TODO: this is overly 'expensive' in communication + // and probably performance, too + me.getKeys(null,true).then(function(keys){ + for (let i=0; i < keys.length; i++){ + console.log(keys[i]); + console.log(keys[i].get('hasSecret')); + if (keys[i].get('hasSecret') === true){ + resolve(keys[i]); + break; + } + if (i === keys.length -1){ + reject(gpgme_error('KEY_NO_DEFAULT')); + } + } + }, function(error){ + reject(error); + }); + } + }, function(error){ + reject(error); + }); + }); + } /** * * @param {String} armored Armored Key block of the Kex(s) to be imported * into gnupg * @param {Boolean} prepare_sync prepare the keys for synched use * (see getKeys()). * @returns {Promise>} An array of objects for the Keys * considered: * Key.key : The key itself as a GPGME_Key * Key.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. * Key.changes.userId: userIds changed * Key.changes.signature: signatures changed * Key.changes.subkey: subkeys changed * // TODO: not yet implemented: Information about Keys that failed * (e.g. malformed Keys, secretKeys are not accepted) */ importKey(armored, prepare_sync) { 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 = []; for (let res=0; res < response.result[0].imports.length; res++){ let result = response.result[0].imports[res]; let status = ''; if (result.status === 0){ status = 'nochange'; } else if ((result.status & 1) === 1){ status = 'newkey'; } else { status = 'change'; } let changes = {}; changes.userId = (result.status & 2) === 2; changes.signature = (result.status & 4) === 4; changes.subkey = (result.status & 8) === 8; //16 new secret key: not implemented fprs.push(result.fingerprint); infos[result.fingerprint] = { changes: changes, status: status }; } let resultset = []; if (prepare_sync === true){ me.getKeys(fprs, true).then(function(result){ for (let i=0; i < result.length; i++) { resultset.push({ key: result[i], changes: infos[result[i].fingerprint].changes, status: infos[result[i].fingerprint].status }); } resolve(resultset); }, function(error){ reject(error); }); } else { for (let i=0; i < fprs.length; i++) { resultset.push({ key: createKey(fprs[i]), changes: infos[fprs[i]].changes, status: infos[fprs[i]].status }); } resolve(resultset); } }, function(error){ reject(error); }); }); } deleteKey(fingerprint){ if (isFingerprint(fingerprint) === true) { let key = createKey(fingerprint); key.delete(); } } /** * 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_ from inside gpgmejs. * * @param {String} userId The user Id, e.g. "Foo Bar " * @param {*} algo (optional) algorithm to be used. See * {@link supportedKeyAlgos } below for supported values. * @param {Number} keyLength (optional) TODO * @param {Date} expires (optional) Expiration date. If not set, expiration * will be set to 'never' * * @returns{Promise} */ generateKey(userId, algo = 'default', keyLength, expires){ if ( typeof(userId) !== 'string' || supportedKeyAlgos.indexOf(algo) < 0 || (expires && !(expires instanceof Date)) // TODO keylength // TODO check for completeness of algos ){ 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 (expires){ msg.setParameter('expires', Math.floor(expires.valueOf()/1000)); } // TODO append keylength to algo msg.post().then(function(response){ me.getKeys(response.fingerprint, true).then( // TODO make prepare_sync (second parameter) optional here. function(result){ resolve(result); }, function(error){ reject(error); }); }, function(error) { reject(error); }); }); } } /** * A list of algorithms supported for key generation. */ const supportedKeyAlgos = [ 'default', 'rsa', 'dsa', 'elg', 'ed25519', 'cv25519' ]; \ No newline at end of file diff --git a/lang/js/src/permittedOperations.js b/lang/js/src/permittedOperations.js index 91612ada..044ae4af 100644 --- a/lang/js/src/permittedOperations.js +++ b/lang/js/src/permittedOperations.js @@ -1,341 +1,393 @@ /* 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 */ /** * Definition of the possible interactions with gpgme-json. * operation: required: Array name The name of the property allowed: Array of allowed types. Currently accepted values: ['number', 'string', 'boolean', 'Uint8Array'] array_allowed: Boolean. If the value can be an array of the above allowed_data: If present, restricts to the given value optional: Array see 'required', with these parameters not being mandatory for a complete message pinentry: boolean If a pinentry dialog is expected, and a timeout of 5000 ms would be too short answer: type: the properties expected and their type, eg: {'data':'string'} } } */ 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' } } }, 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'] }, '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'] } // secret: not yet implemented }, answer: { type: ['keys'], data: { 'data': 'string', 'base64': 'boolean' } } }, 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'] }, // 'secret': { not implemented // allowed: ['boolean'] // } }, answer: { data: { 'success': 'boolean' } } }, version: { required: {}, optional: {}, answer: { type: [''], data: { 'gpgme': 'string', 'info': 'object' } } }, createkey: { - pinentry: true, + pinentry: true, required: { userid: { allowed: ['string'] } }, optional: { 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' + // file_name: Optional string of the plaintext file name. + // is_mime: Boolean if the messages claims it is MIME. + // 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? */ };