From 332b4adbcc52ccf337cbc1943d5abef500769e10 Mon Sep 17 00:00:00 2001 From: Maximilian Krambach Date: Wed, 30 May 2018 17:05:54 +0200 Subject: [PATCH] js: more Keyring/Key handling -- * src/Keys.js - made setKeyData more consistent with other methods - added convenience methods (Key.armored, Key.hasSecret) - Added a Key delete function * src/Keyring.js: - added a getkeysArmored which allows for bulk export of public Keys gpgmejs: - removed deleteKey. It is now a method of the Key itself - Encrypt: Added some common options as parameter, and the possibility to set all allowed flags via an additional Object --- lang/js/src/Errors.js | 4 + lang/js/src/Key.js | 210 +++++++++++++++++++++++++++++------------ lang/js/src/Keyring.js | 32 +++++-- lang/js/src/gpgmejs.js | 85 +++++++++-------- 4 files changed, 227 insertions(+), 104 deletions(-) diff --git a/lang/js/src/Errors.js b/lang/js/src/Errors.js index fa8a4efe..3b53eeb4 100644 --- a/lang/js/src/Errors.js +++ b/lang/js/src/Errors.js @@ -71,6 +71,10 @@ const err_list = { 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' + } // generic 'PARAM_WRONG':{ msg: 'Invalid parameter was found', diff --git a/lang/js/src/Key.js b/lang/js/src/Key.js index f2a16b42..454b1912 100644 --- a/lang/js/src/Key.js +++ b/lang/js/src/Key.js @@ -93,56 +93,31 @@ export class GPGME_Key { } else if (this._data.fingerprint !== data.fingerprint){ return gpgme_error('KEY_INVALID'); } - - let booleans = ['expired', 'disabled','invalid','can_encrypt', - 'can_sign','can_certify','can_authenticate','secret', - 'is_qualified']; - for (let b =0; b < booleans.length; b++) { - if ( - !data.hasOwnProperty(booleans[b]) || - typeof(data[booleans[b]]) !== 'boolean' - ){ + let dataKeys = Object.keys(data); + for (let i=0; i< dataKeys.length; i++){ + if (!validKeyProperties.hasOwnProperty(dataKeys[i])){ return gpgme_error('KEY_INVALID'); } - this._data[booleans[b]] = data[booleans[b]]; - } - if (typeof(data.protocol) !== 'string'){ - return gpgme_error('KEY_INVALID'); - } - // TODO check valid protocols? - this._data.protocol = data.protocol; - - if (typeof(data.owner_trust) !== 'string'){ - return gpgme_error('KEY_INVALID'); - } - // TODO check valid values? - this._data.owner_trust = data.owner_trust; - - // TODO: what about origin ? - if (!Number.isInteger(data.last_update)){ - return gpgme_error('KEY_INVALID'); - } - this._data.last_update = data.last_update; - - this._data.subkeys = []; - if (data.hasOwnProperty('subkeys')){ - if (!Array.isArray(data.subkeys)){ + if (validKeyProperties[dataKeys[i]](data[dataKeys[i]]) !== true ){ return gpgme_error('KEY_INVALID'); } - for (let i=0; i< data.subkeys.length; i++) { - this._data.subkeys.push( - new GPGME_Subkey(data.subkeys[i])); - } - } - - this._data.userids = []; - if (data.hasOwnProperty('userids')){ - if (!Array.isArray(data.userids)){ - return gpgme_error('KEY_INVALID'); - } - for (let i=0; i< data.userids.length; i++) { - this._data.userids.push( - new GPGME_UserId(data.userids[i])); + 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; + default: + this._data[dataKeys[i]] = data[dataKeys[i]]; } } return this; @@ -161,7 +136,9 @@ export class GPGME_Key { if (cached === false) { let me = this; return new Promise(function(resolve, reject) { - if (property === 'armor'){ + if (!validKeyProperties.hasOwnProperty(property)){ + reject('PARAM_WRONG'); + } else if (property === 'armored'){ resolve(me.getArmor()); } else if (property === 'hasSecret'){ resolve(me.getHasSecret()); @@ -173,15 +150,23 @@ export class GPGME_Key { }); } }); - } else { - if (!this._data.hasOwnProperty(property)){ + } 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]); } } } + get armored () { + return this.get('armored'); + //TODO exception if empty + } + /** * Reloads the Key from gnupg */ @@ -207,15 +192,6 @@ export class GPGME_Key { }); } - /** - * Get the armored block of the non- secret parts of the Key. - * @returns {String} the armored Key block. - * Notice that this may be outdated cached info. Use the async getArmor if - * you need the most current info - */ - - // get armor(){ TODO } - /** * Query the armored block of the non- secret parts of the Key directly * from gpg. @@ -279,6 +255,49 @@ export class GPGME_Key { }) }); } + + /** + * Convenience function 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 {Boolean} 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); + }) + }); + } } /** @@ -453,3 +472,78 @@ const validSubKeyProperties = { 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 7e13dfe2..9081cbe9 100644 --- a/lang/js/src/Keyring.js +++ b/lang/js/src/Keyring.js @@ -20,7 +20,7 @@ import {createMessage} from './Message' import {GPGME_Key, createKey} from './Key' -import { isFingerprint } from './Helpers'; +import { isFingerprint, toKeyIdArray } from './Helpers'; import { gpgme_error } from './Errors'; export class GPGME_Keyring { @@ -43,10 +43,10 @@ export class GPGME_Keyring { return new Promise(function(resolve, reject) { let msg; msg = createMessage('keylist'); - if (pattern && typeof(pattern) === 'string'){ + if (pattern !== undefined){ msg.setParameter('keys', pattern); } - msg.setParameter('sigs', true); //TODO do we need this? + msg.setParameter('sigs', true); msg.post().then(function(result){ let resultset = []; let promises = []; @@ -72,10 +72,30 @@ export class GPGME_Keyring { }); }); } -// TODO: - // deleteKey(key, include_secret=false) - // getKeysArmored(pattern) //just dump all armored keys + + /** + * 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) { + if (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 // importKeys(armoredKeys) + // generateKey --> TODO (Andre noch anfragen!) }; diff --git a/lang/js/src/gpgmejs.js b/lang/js/src/gpgmejs.js index 88a91a60..39f6a2f0 100644 --- a/lang/js/src/gpgmejs.js +++ b/lang/js/src/gpgmejs.js @@ -46,28 +46,48 @@ export class GpgME { } /** - * @param {String} data text/data to be encrypted as String + * Encrypt (and optionally sign) a Message + * @param {String|Object} data text/data to be encrypted as String. Also accepts Objects with a getText method * @param {GPGME_Key|String|Array|Array} publicKeys Keys used to encrypt the message + * @param {GPGME_Key|String|Array|Array} secretKeys (optional) Keys used to sign the message + * @param {Boolean} base64 (optional) The data is already considered to be in base64 encoding + * @param {Boolean} armor (optional) Request the output as armored block * @param {Boolean} wildcard (optional) If true, recipient information will not be added to the message + * @param {Object} additional use additional gpg options (refer to src/permittedOperations) + * @returns {Promise} Encrypted message: + * data: The encrypted message + * base64: Boolean indicating whether data is base64 encoded. + * @async */ - encrypt(data, publicKeys, base64=false, wildcard=false){ - + encrypt(data, publicKeys, secretKeys, base64=false, armor=true, + wildcard=false, additional = {} + ){ let msg = createMessage('encrypt'); if (msg instanceof Error){ return Promise.reject(msg) } - // TODO temporary - msg.setParameter('armor', true); + msg.setParameter('armor', armor); msg.setParameter('always-trust', true); if (base64 === true) { msg.setParameter('base64', true); } let pubkeys = toKeyIdArray(publicKeys); msg.setParameter('keys', pubkeys); + let sigkeys = toKeyIdArray(secretKeys); + if (sigkeys.length > 0) { + msg.setParameter('signing_keys', sigkeys); + } putData(msg, data); if (wildcard === true){ msg.setParameter('throw-keyids', true); }; + if (additional){ + let additional_Keys = Object.keys(additional); + for (let k = 0; k < additional_Keys.length; k++) { + msg.setParameter(additional_Keys[k], + additional[additional_Keys[k]]); + } + } if (msg.isComplete === true){ return msg.post(); } else { @@ -76,16 +96,17 @@ export class GpgME { } /** - * @param {String} data TODO base64? Message with the encrypted data - * @param {Boolean} base64 (optional) Response should stay base64 + * Decrypt a Message + * @param {String|Object} data text/data to be decrypted. Accepts Strings and Objects with a getText method + * @param {Boolean} base64 (optional) Response is expected to be base64 encoded * @returns {Promise} decrypted message: data: The decrypted data. This may be base64 encoded. base64: Boolean indicating whether data is base64 encoded. mime: A Boolean indicating whether the data is a MIME object. - info: An optional object with extra information. + signatures: Array of signature Objects TODO not yet implemented. + // should be an object that can tell if all signatures are valid etc. * @async */ - decrypt(data, base64=false){ if (data === undefined){ return Promise.reject(gpgme_error('MSG_EMPTY')); @@ -99,10 +120,22 @@ export class GpgME { } putData(msg, data); return msg.post(); - } - sign(data, keys, mode='clearsign', base64=false) { //sender + /** + * Sign a Message + * @param {String|Object} data text/data to be decrypted. Accepts Strings and Objects with a gettext methos + * @param {GPGME_Key|String|Array|Array} keys The key/keys to use for signing + * @param {*} mode The signing mode. Currently supported: + * 'clearsign': (default) The Message is embedded into the signature + * 'detached': The signature is stored separately + * @param {*} base64 input is considered base64 + * @returns {Promise} + * data: The resulting data. In clearsign mode this includes the signature + * signature: The detached signature (if in detached mode) + * @async + */ + sign(data, keys, mode='clearsign', base64=false) { if (data === undefined){ return Promise.reject(gpgme_error('MSG_EMPTY')); } @@ -139,38 +172,10 @@ export class GpgME { }) }); } - - deleteKey(key, delete_secret = false, no_confirm = false){ - return Promise.reject(gpgme_error('NOT_YET_IMPLEMENTED')); - let msg = createMessage('deletekey'); - if (msg instanceof Error){ - return Promise.reject(msg); - } - let key_arr = toKeyIdArray(key); - if (key_arr.length !== 1){ - return Promise.reject( - gpgme_error('GENERIC_ERROR')); - // TBD should always be ONE key? - } - msg.setParameter('key', key_arr[0]); - if (delete_secret === true){ - msg.setParameter('allow_secret', true); - // TBD - } - if (no_confirm === true){ //TODO: Do we want this hidden deep in the code? - msg.setParameter('delete_force', true); - // TBD - } - if (msg.isComplete === true){ - return msg.post(); - } else { - return Promise.reject(gpgme_error('MSG_INCOMPLETE')); - } - } } /** - * Sets the data of the message + * Sets the data of the message, setting flags according on the data type * @param {GPGME_Message} message The message where this data will be set * @param {*} data The data to enter */