js: more Keyring/Key handling
authorMaximilian Krambach <maximilian.krambach@intevation.de>
Wed, 30 May 2018 15:05:54 +0000 (17:05 +0200)
committerMaximilian Krambach <maximilian.krambach@intevation.de>
Wed, 30 May 2018 15:05:54 +0000 (17:05 +0200)
--

* 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
lang/js/src/Key.js
lang/js/src/Keyring.js
lang/js/src/gpgmejs.js

index fa8a4ef..3b53eeb 100644 (file)
@@ -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',
index f2a16b4..454b191 100644 (file)
@@ -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
      */
@@ -208,15 +193,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.
      * @returns {Promise<String>}
@@ -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';
+    }
+
+}
index 7e13dfe..9081cbe 100644 (file)
@@ -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<String>} pattern (optional)
+     * @returns {Promise<String>} 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!)
 
 };
index 88a91a6..39f6a2f 100644 (file)
@@ -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<String>|Array<GPGME_Key>} publicKeys Keys used to encrypt the message
+     * @param  {GPGME_Key|String|Array<String>|Array<GPGME_Key>} 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<Object>} 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<Object>} 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<String>|Array<GPGME_Key>} 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<Object>}
+     *    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
  */