js: Keyring listing keys
authorMaximilian Krambach <maximilian.krambach@intevation.de>
Mon, 28 May 2018 15:26:56 +0000 (17:26 +0200)
committerMaximilian Krambach <maximilian.krambach@intevation.de>
Mon, 28 May 2018 15:26:56 +0000 (17:26 +0200)
--

* implementing Keyring methods:

  - Keyring.getKeys: has an additional option that retrieves the armor
    and secret state once at the beginning. This is power hungry, but
    allows for Keys to be used directly (without querying gpgme-json
    each call)
  * permittedOperations.js: reflect recent changes in the native
    counterpart, adding more options
  * Key: adding two methods for retrieving the armored Key block and
    for finding out if the Key includes a secret subkey.

lang/js/BrowserTestExtension/testkey2.pub [new file with mode: 0644]
lang/js/src/Key.js
lang/js/src/Keyring.js
lang/js/src/permittedOperations.js
lang/js/unittest_inputvalues.js
lang/js/unittests.js

diff --git a/lang/js/BrowserTestExtension/testkey2.pub b/lang/js/BrowserTestExtension/testkey2.pub
new file mode 100644 (file)
index 0000000..557bd5b
--- /dev/null
@@ -0,0 +1,30 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQENBFsMHecBCACqdJgqa+CeNYwPCK+MpOwAV6uFVjDyO2LmOs6+XfDWRBU/Zjtz
+8zdYNKSbLjkWN4ujV5aiyA7MtEofszzYLEoKUt1wiDScHMpW8qmEFDvl9g26MeAV
+rTno9D5KodHvEIs8wnrqBs8ix0WLbh6J1Dtt8HQgIbN+v3gaRQrgBFe6z2ZYpHHx
+ZfOu3iFKlm2WE/NekRkvvFIo3ApGvRhGIYw6JMmugBlo7s5xosJK0I9dkPGlEEtt
+aF1RkcMj8sWG9vHAXcjlGgFfXSN9YLppydXpkuZGm4+gjLB2a3rbQCZVFnxCyG4O
+ybjkP8Jw6Udm89bK2ucYFfjdrmYn/nJqRxeNABEBAAG0I1Rlc3QgTm9Qcml2S2V5
+IDxub2JvZHlAZXhhbXBsZS5vcmc+iQFOBBMBCAA4FiEE4Fmh4IZtMa4TEXCITZou
+EzBBU9EFAlsMHecCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQTZouEzBB
+U9F+qwf/SHj4uRnTWgyJ71FBxQDYCBq3jbi6e7hMkRPbJyJdnPIMAb2p0PJjBgjW
+0pp4+kDPZans3UDHbma1u/SFI4/y6isJiK94Bk5xp5YliLGnUceTjgDFe6lBhfQ1
+zVWZC/NF3tPgbziIxXQTNt34nS+9dbV/QFDLW0POcN7C0jR/hgkBjMEH2PezWhSj
+mL/yLfLfUYAoxVpXjfC5aPJKqw0tR7m5ibznjCphE+FUMRg8EOmJcg6soeJ5QspU
+k2dPN3+Y0zCTNRgAHEI+yIQbM6pio6v2c+UCtT1QhW4xSI38/kcEG8QiM55r1TUy
+FcWAY5n5t1nNZtMxxse3LqEon3rKiLkBDQRbDB3nAQgAqfAjSjcngERtM+ZYOwN0
+QF2v2FuEuMe8mhju7Met7SN2zGv1LnjhTNshEa9IABEfjZirE2Tqx4xCWDwDedK4
+u1ToFvcnuAMnq2O47Sh+eTypsf6WPFtPBWf6ctKY31hFXjgoyDBULBvl43XU/D9C
+Mt7nsKDPYHVrrnge/qWPYVcb+cO0sSwNImMcwQSdTQ3VBq7MeNS9ZeBcXi+XCjhN
+kjNum2AQqpkHHDQV7871yQ8RIILvZSSfkLb0/SNDU+bGaw2G3lcyKdIfZi2EWWZT
+oCbH38I/+LV7nAEe4zFpHwW8X0Dkx2aLgxe6UszDH9L3eGhTLpJhOSiaanG+zZKm
++QARAQABiQE2BBgBCAAgFiEE4Fmh4IZtMa4TEXCITZouEzBBU9EFAlsMHecCGwwA
+CgkQTZouEzBBU9H5TQgAolWvIsez/WW8N2tmZEnX0LOFNB+1S4L4X983njwNdoVI
+w19pbj+8RIHF/H9kcPGi7jK96gvlykQn3uez/95D2AiRFW5KYdOouFisKgHpv8Ay
+BrhclHv11yK+X/0iTD0scYaG7np5162xLkaxSO9hsz2fGv20RKaXCWkI69fWw0BR
+XlI5pZh2YFei2ZhH/tIMIW65h3w0gtgaZBBdpZTOOW4zvghyN+0MSObqkI1BvUJu
+caDFI4d6ZTmp5SY+pZyktZ4bg/vMH5VFxdIKgbLx9uVeTvOupvbAW0TNulYGUBQE
+nm+S0zr3W18t64e4sS3oHse8zCqo1iiImpba6F1Oaw==
+=y6DD
+-----END PGP PUBLIC KEY BLOCK-----
index 13c9954..f2a16b4 100644 (file)
@@ -77,7 +77,7 @@ export class GPGME_Key {
      * @returns {GPGME_Key|GPGME_Error} The Key object itself after values have
      * been set
      */
-    setKeydata(data){
+    setKeyData(data){
         if (this._data === undefined) {
             this._data = {};
         }
@@ -161,11 +161,17 @@ export class GPGME_Key {
         if (cached === false) {
             let me = this;
             return new Promise(function(resolve, reject) {
-                me.refreshKey().then(function(key){
-                    resolve(key.get(property, true));
-                }, function(error){
-                    reject(error);
-                });
+                if (property === 'armor'){
+                    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 (!this._data.hasOwnProperty(property)){
@@ -188,10 +194,9 @@ export class GPGME_Key {
             let msg = createMessage('keylist');
             msg.setParameter('sigs', true);
             msg.setParameter('keys', me._data.fingerprint);
-            console.log(msg);
             msg.post().then(function(result){
                 if (result.keys.length === 1){
-                    me.setKeydata(result.keys[0]);
+                    me.setKeyData(result.keys[0]);
                     resolve(me);
                 } else {
                     reject(gpgme_error('KEY_NOKEY'));
@@ -202,25 +207,78 @@ export class GPGME_Key {
         });
     }
 
-    //TODO:
     /**
      * 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.
-     * Async, returns Promise<String>
+     * @returns {Promise<String>}
      */
-    // getArmor(){ TODO }
-    //
+     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.armor = result.data;
+                resolve(result.data);
+            }, function(error){
+                reject(error);
+            });
+        });
+    }
 
-    // get hasSecret(){TODO} // confusing difference to Key.get('secret')!
-    // getHasSecret(){TODO async version}
+    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){
+            })
+        });
+    }
 }
 
 /**
index 9abb9ec..7e13dfe 100644 (file)
@@ -19,7 +19,7 @@
  */
 
 import {createMessage} from './Message'
-import {GPGME_Key} from './Key'
+import {GPGME_Key, createKey} from './Key'
 import { isFingerprint } from './Helpers';
 import { gpgme_error } from './Errors';
 
@@ -28,117 +28,54 @@ export class GPGME_Keyring {
     }
 
     /**
-     * @param {String} (optional) pattern A pattern to search for, in userIds or KeyIds
-     * @param {Boolean} (optional) Include listing of secret keys
+     * @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.<Array<GPGME_Key>>}
      *
      */
-    getKeys(pattern, include_secret){
+    getKeys(pattern, prepare_sync){
         let me = this;
         return new Promise(function(resolve, reject) {
             let msg;
-            msg = createMessage('listkeys');
+            msg = createMessage('keylist');
             if (pattern && typeof(pattern) === 'string'){
-                msg.setParameter('pattern', pattern);
-            }
-            if (include_secret){
-                msg.setParameter('with-secret', true);
+                msg.setParameter('keys', pattern);
             }
+            msg.setParameter('sigs', true); //TODO do we need this?
             msg.post().then(function(result){
-                let fpr_list = [];
                 let resultset = [];
-                if (!Array.isArray(result.keys)){
-                //TODO check assumption keys = Array<String fingerprints>
-                    fpr_list = [result.keys];
-                } else {
-                    fpr_list = result.keys;
-                }
-                for (let i=0; i < fpr_list.length; i++){
-                    let newKey = new GPGME_Key(fpr_list[i]);
-                    if (newKey instanceof GPGME_Key){
-                        resultset.push(newKey);
+                let promises = [];
+                // TODO check if result.key is not empty
+                for (let i=0; i< result.keys.length; i++){
+                    let k = createKey(result.keys[i].fingerprint, me);
+                    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 (res){
+                        resolve(resultset);
+                    }, function(error){
+                        reject(error);
+                    });
                 }
-                resolve(resultset);
             }, function(error){
                 reject(error);
             });
         });
     }
-
-    /**
-     * @param {Object} flags subset filter expecting at least one of the
-     * filters described below. True will filter on the condition, False will
-     * reverse the filter, if not present or undefined, the filter will not be
-     * considered. Please note that some combination may not make sense
-     * @param {Boolean} flags.secret Only Keys containing a secret part.
-     * @param {Boolean} flags.revoked revoked Keys only
-     * @param {Boolean} flags.expired Expired Keys only
-     * @param {String} (optional) pattern A pattern to search for, in userIds or KeyIds
-     * @returns {Promise Array<GPGME_Key>}
-     *
-     */
-    getSubset(flags, pattern){
-        if (flags === undefined) {
-            throw('ERR_WRONG_PARAM');
-        };
-        let secretflag = false;
-        if (flags.hasOwnProperty(secret) && flags.secret){
-            secretflag = true;
-        }
-        this.getKeys(pattern, secretflag).then(function(queryset){
-            let resultset = [];
-            for (let i=0; i < queryset.length; i++ ){
-                let conditions = [];
-                let anticonditions = [];
-                if (secretflag === true){
-                    conditions.push('hasSecret');
-                } else if (secretflag === false){
-                    anticonditions.push('hasSecret');
-                }
-                /**
-                if (flags.defaultKey === true){
-                    conditions.push('isDefault');
-                } else if (flags.defaultKey === false){
-                    anticonditions.push('isDefault');
-                }
-                */
-                /**
-                 * if (flags.valid === true){
-                    anticonditions.push('isInvalid');
-                } else if (flags.valid === false){
-                    conditions.push('isInvalid');
-                }
-                */
-                if (flags.revoked === true){
-                    conditions.push('isRevoked');
-                } else if (flags.revoked === false){
-                    anticonditions.push('isRevoked');
-                }
-                if (flags.expired === true){
-                    conditions.push('isExpired');
-                } else if (flags.expired === false){
-                    anticonditions.push('isExpired');
-                }
-                let decision = undefined;
-                for (let con = 0; con < conditions.length; con ++){
-                    if (queryset[i][conditions[con]] !== true){
-                        decision = false;
-                    }
-                }
-                for (let acon = 0; acon < anticonditions.length; acon ++){
-                    if (queryset[i][anticonditions[acon]] === true){
-                        decision = false;
-                    }
-                }
-                if (decision !== false){
-                    resultset.push(queryset[i]);
-                }
-            }
-            return Promise.resolve(resultset);
-        }, function(error){
-            //TODO error handling
-        });
-    }
+//  TODO:
+    // deleteKey(key, include_secret=false)
+    // getKeysArmored(pattern) //just dump all armored keys
+    // getDefaultKey() Big TODO
+    // importKeys(armoredKeys)
 
 };
index 42213ec..e4f9bd2 100644 (file)
@@ -45,6 +45,7 @@
 
 export const permittedOperations = {
     encrypt: {
+        pinentry: true, //TODO only with signing_keys
         required: {
             'keys': {
                 allowed: ['string'],
@@ -59,38 +60,42 @@ export const permittedOperations = {
                 allowed: ['string'],
                 allowed_data: ['cms', 'openpgp']
             },
-                'chunksize': {
+            'signing_keys': {
+                allowed: ['string'],
+                array_allowed: true
+            },
+            'chunksize': {
                     allowed: ['number']
-                },
-                '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']
-                },
             },
+            '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'],
@@ -122,12 +127,7 @@ export const permittedOperations = {
             type: ['plaintext'],
             data: ['data'],
             params: ['base64', 'mime'],
-            infos: [] // TODO pending. Info about signatures and validity
-                    //{
-                        //signatures: [{
-                            //Key : <String>Fingerprint,
-                            //valid: <Boolean>
-                        // }]
+            infos: ['signatures']
         }
     },
 
@@ -208,61 +208,107 @@ export const permittedOperations = {
             'validate': {
                 allowed: ['boolean']
             },
-            // 'pattern': { TODO
-            //     allowed: ['string']
-            // },
             'keys': {
                 allowed: ['string'],
                 array_allowed: true
             }
         },
         answer: {
-            type: [],
+            type: ['keys'],
             data: [],
-            params: [],
+            params: ['base64'],
             infos: ['keys']
         }
     },
 
-    /**
-    importkey: {
+    export: {
+        required: {},
+        optional: {
+            'protocol': {
+                allowed: ['string'],
+                allowed_data: ['cms', 'openpgp']
+            },
+            'chunksize': {
+                allowed: ['number'],
+            },
+            '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'],
+            params: ['base64']
+        }
+    },
+
+    import: {
         required: {
-            'keyarmored': {
+            'data': {
                 allowed: ['string']
             }
         },
+        optional: {
+            'protocol': {
+                allowed: ['string'],
+                allowed_data: ['cms', 'openpgp']
+            },
+            'base64': {
+                allowed: ['boolean']
+            },
+        },
         answer: {
-            type: ['TBD'],
-            infos: ['TBD'],
-            // for each key if import was a success,
-            // and if it was an update of preexisting key
+            infos: ['result'],
+            type: [],
+            data: [],
+            params: []
         }
     },
-    */
 
-    /**
-    deletekey:  {
+    delete: {
         pinentry: true,
-        required: {
-            'fingerprint': {
+        required:{
+            'key': {
+                allowed: ['string']
+            }
+        },
+        optional: {
+            'protocol': {
                 allowed: ['string'],
-                // array_allowed: TBD Allow several Keys to be deleted at once?
+                allowed_data: ['cms', 'openpgp']
             },
-        optional: {
-            'TBD' //Flag to delete secret Key ?
-        }
+            // 'secret': { not yet implemented
+            //     allowed: ['boolean']
+            // }
+
+        },
         answer: {
-            type ['TBD'],
-            infos: ['']
-                // TBD (optional) Some kind of 'ok' if delete was successful.
+            data: [],
+            params:['success'],
+            infos: []
         }
-    }
-    */
-
+    },
     /**
      *TBD get armored secret different treatment from keyinfo!
      * TBD key modification?
-     * encryptsign: TBD
+
      */
 
     version: {
index ca51f4a..2a21a6a 100644 (file)
@@ -44,7 +44,11 @@ export const whatever_params = {
     four_invalid_params: ['<(((-<', '>°;==;~~', '^^', '{{{{o}}}}'],
 }
 export const key_params = {
+// A Key you own (= having a secret Key) in GPG. See testkey.pub/testkey.sec
     validKeyFingerprint: 'D41735B91236FDB882048C5A2301635EEFF0CB05',
+// A Key you do not own (= having no secret Key) in GPG. See testkey2.pub
+    validFingerprintNoSecret: 'E059A1E0866D31AE131170884D9A2E13304153D1',
+// A Key not in your Keyring. This is just a random hex string.
     invalidKeyFingerprint: 'CDC3A2B2860625CCBFC5AAAAAC6D1B604967FC4A',
     validKeyProperties: ['expired', 'disabled','invalid','can_encrypt',
     'can_sign','can_certify','can_authenticate','secret','is_qualified']
index 9830a2c..443aa68 100644 (file)
@@ -197,7 +197,34 @@ function unittests (){
                 expect(result).to.be.a('boolean');
                 done();
             });
-        })
+        });
+
+        it('Non-cached key async armored Key', function (done){
+            let key = createKey(kp.validKeyFingerprint);
+            key.get('armor', false).then(function(result){
+                expect(result).to.be.a('string');
+                expect(result).to.include('KEY BLOCK-----');
+                done();
+            });
+        });
+
+        it('Non-cached key async hasSecret', function (done){
+            let key = createKey(kp.validKeyFingerprint);
+            key.get('hasSecret', false).then(function(result){
+                expect(result).to.be.a('boolean');
+                done();
+            });
+        });
+
+        it('Non-cached key async hasSecret (no secret in Key)', function (done){
+            let key = createKey(kp.validFingerprintNoSecret);
+            expect(key).to.be.an.instanceof(GPGME_Key);
+            key.get('hasSecret', false).then(function(result){
+                expect(result).to.be.a('boolean');
+                expect(result).to.equal(false);
+                done();
+            });
+        });
 
         it('Querying non-existing Key returns an error', function(done) {
             let key = createKey(kp.invalidKeyFingerprint);
@@ -224,7 +251,6 @@ function unittests (){
                 expect(key.fingerprint.code).to.equal('KEY_INVALID');
             }
         });
-
         // TODO: tests for subkeys
         // TODO: tests for userids
         // TODO: some invalid tests for key/keyring
@@ -236,19 +262,19 @@ function unittests (){
             let keyring = new GPGME_Keyring;
             expect(keyring).to.be.an.instanceof(GPGME_Keyring);
             expect(keyring.getKeys).to.be.a('function');
-            expect(keyring.getSubset).to.be.a('function');
         });
 
-        it('correct initialization', function(){
+        it('Loading Keys from Keyring, to be used synchronously', function(done){
             let keyring = new GPGME_Keyring;
-            expect(keyring).to.be.an.instanceof(GPGME_Keyring);
-            expect(keyring.getKeys).to.be.a('function');
-            expect(keyring.getSubset).to.be.a('function');
+            keyring.getKeys(null, true).then(function(result){
+                expect(result).to.be.an('array');
+                expect(result[0]).to.be.an.instanceof(GPGME_Key);
+                expect(result[0].get('armor')).to.be.a('string');
+                expect(result[0].get('armor')).to.include(
+                    '-----END PGP PUBLIC KEY BLOCK-----');
+                done();
+            });
         });
-            //TODO not yet implemented:
-            //  getKeys(pattern, include_secret) //note: pattern can be null
-            //  getSubset(flags, pattern)
-                // available Boolean flags: secret revoked expired
     });
 
     describe('GPGME_Message', function(){