js: Key handling stubs, Error handling, refactoring
authorMaximilian Krambach <maximilian.krambach@intevation.de>
Mon, 23 Apr 2018 15:18:46 +0000 (17:18 +0200)
committerMaximilian Krambach <maximilian.krambach@intevation.de>
Mon, 23 Apr 2018 15:18:46 +0000 (17:18 +0200)
--

* Error handling: introduced GPGMEJS_Error class that handles errors
  at a more centralized and consistent position
* src/Connection.js:
  The nativeMessaging port now opens per session instead of per
  message. Some methods were added that reflect this change
  - added methods disconnect() and reconnect()
  - added connection status query
* src/gpgmejs.js
  - stub for key deletion
  - error handling
  - high level API for changing connection status
* src/gpgmejs_openpgpjs.js
  - added stubs for Key/Keyring handling according to current
    state of discussion. It is still subject to change
* src/Helpers.js
  - toKeyIdArray creates an array of KeyIds, now accepting
    fingerprints, GPGMEJS_Key objects and openpgp Key objects.
* Key objects (src/Key.js) Querying information about a key
  directly from gnupg. Currently a stub, only the Key.fingerprint is
  functional.
* Keyring queries (src/Keyring.js): Listing and searching keys.
  Currently a stub.

lang/js/src/Connection.js
lang/js/src/Errors.js [new file with mode: 0644]
lang/js/src/Helpers.js
lang/js/src/Key.js [new file with mode: 0644]
lang/js/src/Keyring.js [new file with mode: 0644]
lang/js/src/Message.js
lang/js/src/gpgmejs.js
lang/js/src/gpgmejs_openpgpjs.js

index 784929e..87ec8cf 100644 (file)
@@ -26,31 +26,19 @@ import { GPGME_Message } from "./Message";
  * expected.
  */
 import { permittedOperations } from './permittedOperations'
+import { GPGMEJS_Error} from "./Errors"
 
+/**
+ * A Connection handles the nativeMessaging interaction.
+ */
 export class Connection{
 
-    /**
-     * Opens and closes a port. Thus, it is made sure that the connection can
-     * be used.
-     * THIS BEHAVIOUR MAY CHANGE!
-     * discussion is to keep a port alive as long as the context stays the same
-     *
-     * TODO returns nothing, but triggers exceptions if not successfull
-     */
     constructor(){
-        this._connection = chrome.runtime.connectNative('gpgmejson');
-        if (!this._connection){
-            if (chrome.runtime.lastError){
-                throw('NO_CONNECT_RLE');
-            } else {
-                throw('NO_CONNECT');
-            }
-        }
-        this._flags = {}; // TODO general config
+        this.connect();
     }
 
     /**
-     * Immediately closes the open port
+     * Immediately closes the open port.
      */
     disconnect() {
         if (this._connection){
@@ -59,26 +47,55 @@ export class Connection{
     }
 
     /**
+     * Opens a nativeMessaging port.
+     * returns nothing, but triggers errors if not successfull:
+     * NO_CONNECT: connection not successfull, chrome.runtime.lastError may be
+     * available
+     * ALREADY_CONNECTED: There is already a connection present.
+     */
+    connect(){
+        if (this._connection){
+            return new GPGMEJS_Error('ALREADY_CONNECTED');
+        }
+        this._connection = chrome.runtime.connectNative('gpgmejson');
+        if (!this._connection){
+            return new GPGMEJS_Error('NO_CONNECT');
+        }
+    }
+
+    /**
+     * checks if the connection is established
+     * TODO: some kind of ping to see if the other side responds as expected?
+     * @returns {Boolean}
+     */
+    get connected(){
+        return this._connection ? true: false;
+    }
+
+    /**
      * Sends a message and resolves with the answer.
      * @param {GPGME_Message} message
      * @returns {Promise<Object>} the gnupg answer, or rejection with error
-     * information
-     * TODO: better/more consistent error information
+     * information.
      */
     post(message){
         if (!message || !message instanceof GPGME_Message){
-            return Promise.reject('ERR_NO_MSG');
+            return Promise.reject(new GPGMEJS_Error('WRONGPARAM'));
+        }
+        if (message.isComplete !== true){
+            return Promise.reject(new GPGMEJS_Error('MSG_INCOMPLETE'));
         }
         // let timeout = 5000; //TODO config
         let me = this;
         return new Promise(function(resolve, reject){
-            let answer = new Answer(message.op);
+            let answer = new Answer(message.operation);
             let listener = function(msg) {
                 if (!msg){
                     me._connection.onMessage.removeListener(listener)
-                    reject('EMPTY_ANSWER');
+                    reject(new GPGMEJS_Error('EMPTY_GPG_ANSWER'));
                 } else if (msg.type === "error"){
                     me._connection.onMessage.removeListener(listener)
+                    //TODO: GPGMEJS_Error?
                     reject(msg.msg);
                 } else {
                     answer.add(msg);
@@ -92,12 +109,12 @@ export class Connection{
             };
 
             me._connection.onMessage.addListener(listener);
-            me._connection.postMessage(message);
+            me._connection.postMessage(message.message);
             //TBD: needs to be aware if there is a pinentry pending
             // setTimeout(
             //     function(){
             //         me.disconnect();
-            //         reject('TIMEOUT');
+            //         reject(new GPGMEJS_Error('TIMEOUT', 5000));
             //     }, timeout);
         });
      }
@@ -105,8 +122,8 @@ export class Connection{
 
 /**
  * A class for answer objects, checking and processing the return messages of
- * the nativeMessaging communication
- * @param {String} operation The operation, to look up validity of return keys
+ * the nativeMessaging communication.
+ * @param {String} operation The operation, to look up validity of returning messages
  */
 class Answer{
 
@@ -115,9 +132,8 @@ class Answer{
     }
 
     /**
-     *
+     * Add the information to the answer
      * @param {Object} msg The message as received with nativeMessaging
-     * TODO: "error" and "more" handling are not in here, but in post()
      */
     add(msg){
         if (this._response === undefined){
@@ -130,9 +146,7 @@ class Answer{
             switch (key) {
                 case 'type':
                     if ( msg.type !== 'error' && poa.type.indexOf(msg.type) < 0){
-                        console.log( 'unexpected answer type: ' + msg.type);
-                        throw('UNEXPECTED_TYPE');
-
+                        return new GPGMEJS_Error('UNEXPECTED_ANSWER');
                     }
                     break;
                 case 'more':
@@ -151,7 +165,7 @@ class Answer{
                             this._response[key] = msg[key];
                         }
                         else if (this._response[key] !== msg[key]){
-                                throw('UNEXPECTED_TYPE');
+                                return new GPGMEJS_Error('UNEXPECTED_ANSWER',msg[key]);
                         }
                     }
                     //infos may be json objects etc. Not yet defined.
@@ -163,8 +177,7 @@ class Answer{
                         this._response.push(msg[key]);
                     }
                     else {
-                        console.log('unexpected answer parameter: ' + key);
-                        throw('UNEXPECTED_PARAM');
+                        return new GPGMEJS_Error('UNEXPECTED_ANSWER', key);
                     }
                     break;
             }
@@ -172,7 +185,8 @@ class Answer{
     }
 
     /**
-     * Returns the assembled message. TODO: does not care yet if completed.
+     * @returns {Object} the assembled message.
+     * TODO: does not care yet if completed.
      */
     get message(){
         return this._response;
diff --git a/lang/js/src/Errors.js b/lang/js/src/Errors.js
new file mode 100644 (file)
index 0000000..c2356f7
--- /dev/null
@@ -0,0 +1,148 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+// This is a preliminary collection of erors and warnings to be thrown and implemented.
+
+// general idea: if throw , throw the NAME
+// return false || 'return' property
+
+//TODO: Connection.NOCONNECT promise
+//connection.timeout: Be aware of pinentry
+
+export class GPGMEJS_Error {
+
+    constructor(code = 'GENERIC_ERROR', details){
+        let config = { //TODO TEMP
+            debug: 'console', // |'alert'
+            throw: 'default'  // | 'always' | 'never'
+        };
+        let errors = { //TODO: someplace else
+            //Connection errors
+            'ALREADY_CONNECTED':{
+                msg: 'The connection was already established. The action would overwrite the context',
+                throw: true
+            },
+            'NO_CONNECT': {
+                msg:'Connection with the nativeMessaging host could not be established.',
+                throw: true
+            },
+            'EMPTY_GPG_ANSWER':{
+                msg: 'The nativeMesaging answer was empty',
+                throw: true
+            },
+            'TIMEOUT': {
+                msg: 'A timeout was exceeded.',
+                throw: false
+            },
+
+            'UNEXPECTED_ANSWER': {
+                msg: 'The answer from gnupg was not as expected',
+                throw: true
+            },
+
+            // Message/Data Errors
+
+            'NO_KEYS' : {
+                msg: 'There were no valid keys provided.',
+                throw: true
+            },
+            'NOT_A_FPR': {
+                msg: 'The String is not an accepted fingerprint',
+                throw: false
+            },
+            'MSG_INCOMPLETE': {
+                msg: 'The Message did not match the minimum requirements for the interaction',
+                throw: true
+            },
+            'EMPTY_MSG' : {
+                msg: 'The Message has no data.',
+                throw: true
+            },
+            'MSG_NODATA':{
+                msg: 'The data sent is empty. This may be unintentional.',
+                throw: false
+            },
+            'MSG_OP_PENDING': {
+                msg: 'There is no operation specified yet. The parameter cannot be set',
+                throw: false
+            },
+            'WRONG_OP': {
+                msg: "The operation requested could not be found",
+                throw: true
+            },
+
+            //generic errors
+
+            'WRONGPARAM':{
+                msg: 'invalid parameter was found',
+                throw: true
+            },
+            'WRONGTYPE':{
+                msg: 'invalid parameter type was found',
+                throw: true
+            },
+            'NOT_IMPLEMENTED': {
+                msg: 'A openpgpjs parameter was submitted that is not implemented',
+                throw: true
+            },
+            'GENERIC_ERROR': {
+                msg: 'Unspecified error',
+                throw: true
+            },
+
+            // hopefully temporary errors
+
+            'NOT_YET_IMPLEMENTED': {
+                msg: 'Support of this is probable, but it is not implemented yet',
+                throw: false
+            }
+        }
+        if (!errors.hasOwnProperty(code)){
+            throw('GENERIC_ERROR');
+        }
+        let msg = code;
+        if (errors[code].msg !== undefined){
+            msg = msg + ': ' + errors[code].msg;
+        }
+        if (details){
+            msg = msg + ' ' + details;
+        }
+        if (config.debug === 'console'){
+            console.log(msg);
+        } else if (config.debug === 'alert'){
+            alert(msg);
+        }
+        switch (config.throw) {
+            case 'default':
+                if (errors[code].throw === true){
+                    throw(code);
+                }
+                break;
+            case 'always':
+                throw(code);
+                break;
+
+            case 'never':
+                break;
+            default:
+                throw('GENERIC_ERROR');
+        }
+    }
+}
index eeb7a3c..922ca06 100644 (file)
@@ -1,3 +1,5 @@
+import { GPGMEJS_Error } from "./Errors";
+
 /* gpgme.js - Javascript integration for gpgme
  * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik
  *
 /**
  * Tries to return an array of fingerprints, either from input fingerprints or
  * from Key objects
- * @param {String|Array<String>} input Input value.
+ * @param {Key |Array<Key>| GPGME_Key | Array<GPGME_Key>|String|Array<String>} input
+ * @param {Boolean} nocheck if set, an empty result is acceptable
  * @returns {Array<String>} Array of fingerprints.
  */
-export function toKeyIdArray(input){
+
+export function toKeyIdArray(input, nocheck){
     if (!input){
-        return [];
-        // TODO: Warning or error here? Did we expect something or is "nothing" okay?
+        return (nocheck ===true)? [] : new GPGMEJS_Error('NO_KEYS');
+    }
+    if (!Array.isArray(input)){
+        input = [input];
     }
-    if (input instanceof Array){
-        let result = [];
-        for (let i=0; i < input.length; i++){
+    let result = [];
+    for (let i=0; i < input.length; i++){
+        if (typeof(input[i]) === 'string'){
             if (isFingerprint(input[i]) === true){
                 result.push(input[i]);
             } else {
-                //TODO error?
-                console.log('gpgmejs/Helpers.js Warning: '+
-                    input[i] +
-                    ' is not a valid key fingerprint and will not be used');
+                GPGMEJS_Error
+            }
+        } else if (typeof(input[i]) === 'object'){
+            let fpr = '';
+            if (input[i] instanceof GPGME_Key){
+                fpr = input[i].fingerprint;
+            } else if (input[i].hasOwnProperty(primaryKey) &&
+                input[i].primaryKey.hasOwnProperty(getFingerprint)){
+                    fpr = input[i].primaryKey.getFingerprint();
+            }
+            if (isFingerprint(fpr) === true){
+                result.push(fpr);
             }
+        } else {
+            return new GPGMEJS_Error('WRONGTYPE');
         }
+    }
+    if (result.length === 0){
+        return (nocheck===true)? [] : new GPGMEJS_Error('NO_KEYS');
+    } else {
         return result;
-    } else if (isFingerprint(input) === true) {
-        return [input];
     }
-    console.log('gpgmejs/Helpers.js Warning: ' + input +
-                    ' is not a valid key fingerprint and will not be used');
-    return [];
 };
 
 /**
@@ -72,13 +87,14 @@ function hextest(key, len){
 export function isFingerprint(string){
     return hextest(string, 40);
 };
-
-//TODO needed anywhere?
+/**
+ * check if the input is a valid Hex string with a length of 16
+ */
 function isLongId(string){
     return hextest(string, 16);
 };
 
-//TODO needed anywhere?
+// TODO still not needed anywhere
 function isShortId(string){
     return hextest(string, 8);
 };
diff --git a/lang/js/src/Key.js b/lang/js/src/Key.js
new file mode 100644 (file)
index 0000000..d8f16c5
--- /dev/null
@@ -0,0 +1,201 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+/**
+ * The key class allows to query the information defined in gpgme Key Objects
+ * (see https://www.gnupg.org/documentation/manuals/gpgme/Key-objects.html)
+ *
+ * This is a stub, as the gpgme-json side is not yet implemented
+ *
+ */
+
+import {isFingerprint} from './Helpers'
+import {GPGMEJS_Error} from './Errors'
+
+export class GPGME_Key {
+
+    constructor(fingerprint){
+        if (isFingerprint(fingerprint) === true){
+            this._fingerprint = fingerprint;
+        } else {
+            return new GPGMEJS_Error('WRONGPARAM', 'Key.js: invalid fingerprint');
+        }
+    }
+
+    get fingerprint(){
+        return this._fingerprint;
+    }
+
+    /**
+     * hasSecret returns true if a secret subkey is included in this Key
+     */
+    get hasSecret(){
+        checkKey(this._fingerprint, 'secret').then( function(result){
+            return Promise.resolve(result);
+        });
+
+    }
+
+    get isRevoked(){
+        return checkKey(this._fingerprint, 'revoked');
+    }
+
+    get isExpired(){
+        return checkKey(this._fingerprint, 'expired');
+    }
+
+    get isDisabled(){
+        return checkKey(this._fingerprint, 'disabled');
+    }
+
+    get isInvalid(){
+        return checkKey(this._fingerprint, 'invalid');
+    }
+
+    get canEncrypt(){
+        return checkKey(this._fingerprint, 'can_encrypt');
+    }
+
+    get canSign(){
+        return checkKey(this._fingerprint, 'can_sign');
+    }
+
+    get canCertify(){
+        return checkKey(this._fingerprint, 'can_certify');
+    }
+
+    get canAuthenticate(){
+        return checkKey(this._fingerprint, 'can_authenticate');
+    }
+
+    get isQualified(){
+        return checkKey(this._fingerprint, 'is_qualified');
+    }
+
+    get armored(){
+        let me = this;
+        return new Promise(function(resolve, reject){
+            let conn = new Connection();
+            conn.setFlag('armor', true);
+            conn.post('export',{'fpr': me._fingerprint});
+        });
+        // TODO return value not yet checked. Should result in an armored block
+        // in correct encoding
+        // TODO openpgpjs also returns secKey if private = true?
+    }
+
+    /**
+     * TODO returns true if this is the default key used to sign
+     */
+    get isDefault(){
+        throw('NOT_YET_IMPLEMENTED');
+    }
+
+    /**
+     * get the Key's subkeys as GPGME_Key objects
+     * @returns {Array<GPGME_Key>}
+     */
+    get subkeys(){
+        return checkKey(this._fingerprint, 'subkeys').then(function(result){
+            // TBD expecting a list of fingerprints
+            if (!Array.isArray(result)){
+                result = [result];
+            }
+            let resultset = [];
+            for (let i=0; i < result.length; i++){
+                let subkey = new GPGME_Key(result[i]);
+                if (subkey instanceof GPGME_Key){
+                    resultset.push(subkey);
+                }
+            }
+            return Promise.resolve(resultset);
+        });
+    }
+
+    /**
+     * creation time stamp of the key
+     * @returns {Date|null} TBD
+     */
+    get timestamp(){
+        return checkKey(this._fingerprint, 'timestamp');
+        //TODO GPGME: -1 if the timestamp is invalid, and 0 if it is not available.
+    }
+
+    /**
+     * The expiration timestamp of this key TBD
+     *  @returns {Date|null} TBD
+     */
+    get expires(){
+        return checkKey(this._fingerprint, 'expires');
+        // TODO convert to Date; check for 0
+    }
+
+    /**
+     * getter name TBD
+     * @returns {String|Array<String>} The user ids associated with this key
+     */
+    get userIds(){
+        return checkKey(this._fingerprint, 'uids');
+    }
+
+    /**
+     * @returns {String} The public key algorithm supported by this subkey
+     */
+    get pubkey_algo(){
+        return checkKey(this._fingerprint, 'pubkey_algo');
+    }
+};
+
+/**
+ * generic function to query gnupg information on a key.
+ * @param {*} fingerprint The identifier of the Keyring
+ * @param {*} property The gpgme-json property to check
+ *
+ */
+function checkKey(fingerprint, property){
+    return Promise.reject(new GPGMEJS_Error('NOT_YET_IMPLEMENTED'));
+
+    return new Promise(function(resolve, reject){
+        if (!isFingerprint(fingerprint)){
+            reject('not a fingerprint'); //TBD
+        }
+        let conn = new Connection();
+        conn.post('getkey',{ // TODO not yet implemented in gpgme
+            'fingerprint': this.fingerprint})
+        .then(function(result){
+            if (property !== undefined){
+                if (result.hasOwnProperty(key)){
+                    resolve(result[property]);
+                }
+                else if (property == 'secret'){
+                    // property undefined means "not true" in case of secret
+                    resolve(false);
+                } else {
+                    reject('ERR_INVALID_PROPERTY') //TBD
+                }
+            }
+
+
+            resolve(result);
+        }, function(error){
+            reject(error);
+        });
+    });
+};
\ No newline at end of file
diff --git a/lang/js/src/Keyring.js b/lang/js/src/Keyring.js
new file mode 100644 (file)
index 0000000..52fa7f7
--- /dev/null
@@ -0,0 +1,151 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+import {GPGME_Message} from './Message'
+import {Connection} from './Connection'
+import {GPGME_Key} from './Key'
+import { isFingerprint, isLongId } from './Helpers';
+
+export class GPGME_Keyring {
+    constructor(){
+        this.reconnect();
+    }
+
+    /**
+     * (Re)-establishes the connection
+     * TODO TEMP: should we better use the connection of our parent,
+     * which we do not control?
+     */
+    reconnect(){
+        if (!this._connection || ! this._connection instanceof Connection){
+            this._connection = new Connection;
+        } else {
+            this._connection.disconnect();
+            this._connection.connect();
+        }
+    }
+
+    /**
+     * @param {String} (optional) pattern A pattern to search for, in userIds or KeyIds
+     * @param {Boolean} (optional) Include listing of secret keys
+     * @returns {Promise.<Array<GPGME_Key>>}
+     *
+     */
+    getKeys(pattern, include_secret){
+        let msg = new GPGME_Message;
+        msg.operation = 'listkeys';
+        if (pattern && typeof(pattern) === 'string'){
+            msg.setParameter('pattern', pattern);
+        }
+        if (include_secret){
+            msg.setParameter('with-secret', true);
+        }
+
+        this._connection.post(msg).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);
+                }
+            }
+            return Promise.resolve(resultset);
+        });
+    }
+
+    /**
+     * @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.defaultKey Only Keys marked as Default Keys
+     * @param {Boolean} flags.secret Only Keys containing a secret part.
+     * @param {Boolean} flags.valid Valid Keys only
+     * @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);
+        });
+    }
+
+};
index 90b554a..6a93b6f 100644 (file)
  * SPDX-License-Identifier: LGPL-2.1+
  */
 import { permittedOperations } from './permittedOperations'
-
+import { GPGMEJS_Error } from './Errors'
 export class GPGME_Message {
     //TODO getter
 
-    constructor(){
+    constructor(operation){
+        if (operation){
+            this.operation(operation);
+        }
     }
 
     /**
      * Defines the operation this message will have
-     * @param {String} operation Mus be defined in permittedOperations
+     * @param {String} operation Must be defined in permittedOperations
      *  TODO: move to constructor?
      */
     set operation (operation){
         if (!operation || typeof(operation) !== 'string'){
-            throw('ERR_WRONG_PARAM');
+            return new GPGMEJS_Error('WRONGPARAM');
         }
         if (operation in permittedOperations){
             if (!this._msg){
@@ -40,10 +43,14 @@ export class GPGME_Message {
             }
             this._msg.op = operation;
         } else {
-            throw('ERR_NOT_IMPLEMENTED');
+            return new GPGMEJS_Error('WRONG_OP');
         }
     }
 
+    get operation(){
+        return this._msg.op;
+    }
+
     /**
      * Sets a parameter for the message. Note that the operation has to be set
      * first, to be able to check if the parameter is permittted
@@ -53,25 +60,20 @@ export class GPGME_Message {
      */
     setParameter(param,value){
         if (!param || typeof(param) !== 'string'){
-            throw('ERR_WRONG_PARAM');
+            return new GPGMEJS_Error('WRONGPARAM', 'type check failed');
         }
         if (!this._msg || !this._msg.op){
-            console.log('There is no operation specified yet. '+
-                'The parameter cannot be set');
-            return false;
+            return new GPGMEJS_Error('MSG_OP_PENDING');
         }
         let po = permittedOperations[this._msg.op];
         if (!po){
-            throw('LAZY_PROGRAMMER');
-            //TODO
-            return false;
+            return new GPGMEJS_Error('WRONG_OP', param);
         }
         if (po.required.indexOf(param) >= 0 || po.optional.indexOf(param) >= 0){
             this._msg[param] = value;
             return true;
         }
-        console.log('' + param + ' is invalid and could not be set');
-        return false;
+        return new GPGMEJS_Error('WRONGPARAM', param);
     }
 
     /**
@@ -85,7 +87,9 @@ export class GPGME_Message {
         }
         let reqParams = permittedOperations[this._msg.op].required;
         for (let i=0; i < reqParams.length; i++){
-            if (!reqParams[i] in this._msg){
+
+            if (!this._msg.hasOwnProperty(reqParams[i])){
+                console.log(reqParams[i] + 'missing');
                 return false;
             }
         }
index 8323ac3..c23a356 100644 (file)
 import {Connection} from "./Connection"
 import {GPGME_Message} from './Message'
 import {toKeyIdArray} from "./Helpers"
+import {GPGMEJS_Error as Error, GPGMEJS_Error} from "./Errors"
 
 export class GpgME {
     /**
-     * initial check if connection si successfull. Will throw ERR_NO_CONNECT or
-     * ERR_NO_CONNECT_RLE (if chrome.runtime.lastError is available) if the
-     * connection fails.
-     * TODO The connection to the nativeMessaging host will, for now, be closed
-     * after each interaction. Session management with gpg_agent is TBD.
+     * initializes GpgME by opening a nativeMessaging port
      * TODO: add configuration
      */
-    constructor(){
-        let conn = new Connection();
-        // this.keyring = new Keyring(); TBD
-        // TODO config, e.g.
-        this.configuration = {
-            null_expire_is_never: true
-        };
-        conn.disconnect();
+    constructor(configuration = {
+        null_expire_is_never: false
+    }){
+        this._connection = new Connection;
+    }
+
+    /**
+     * refreshes the nativeApp connection
+     */
+    reconnect(){
+        if (!this._connection || ! this._connection instanceof Connection){
+            this._connection = new Connection;
+        } else {
+            this._connection.disconnect();
+            this._connection.connect();
+        }
+    }
+
+    /**
+     * inmediately tries to destroy the nativeMessaging connection.
+     * TODO: may not be included in final API, as it is redundant.
+     * For now, it just serves paranoia
+     */
+    disconnect(){
+        if (this._connection){
+            this._connection.disconnect();
+            this._connection = null;
+        }
+    }
+
+    /**
+     * tests the nativeApp connection
+     */
+    get connected(){
+        if (!this._connection || ! this._connection instanceof Connection){
+            return false;
+        }
+        return this._connection.connected;
     }
 
+
     /**
      * @param {String|Uint8Array} data text/data to be encrypted as String/Uint8Array
      * @param  {GPGME_Key|String|Array<String>|Array<GPGME_Key>} publicKeys Keys used to encrypt the message
@@ -62,14 +90,7 @@ export class GpgME {
         if (wildcard === true){msg.setParameter('throw-keyids', true);
         };
 
-        if (msg.isComplete === true) {
-            let conn = new Connection();
-            return (conn.post(msg.message));
-        }
-        else {
-            return Promise.reject('NO_CONNECT');
-            //TODO
-        }
+        return (this._connection.post(msg));
     }
 
     /**
@@ -85,22 +106,47 @@ export class GpgME {
     decrypt(data){
 
         if (data === undefined){
-            throw('ERR_EMPTY_MSG');
+            return Promise.reject(new GPGMEJS_Error ('EMPTY_MSG'));
         }
         let msg = new GPGME_Message;
         msg.operation = 'decrypt';
         putData(msg, data);
-        // TODO: needs proper EOL to be decrypted.
+        return this._connection.post(msg);
+
+    }
 
-        if (msg.isComplete === true){
-            let conn = new Connection();
-            return conn.post(msg.message);
+    deleteKey(key, delete_secret = false, no_confirm = false){
+        return Promise.reject(new GPGMEJS_Error ('NOT_YET_IMPLEMENTED'));
+        let msg = new GPGME_Message;
+        msg.operation = 'deletekey';
+        let key_arr = toKeyIdArray(key);
+        if (key_arr.length !== 1){
+            throw('TODO');
+            //should always be ONE key
+        }
+        msg.setParameter('key', key_arr[0]);
+        if (delete_secret === true){
+            msg.setParameter('allow_secret', true); //TBD
         }
-        else {
-            return Promise.reject('NO_CONNECT');
-            //TODO
+        if (no_confirm === true){ //TODO: Do we want this hidden deep in the code?
+            msg.setParameter('delete_force', true); //TBD
         }
+        this._connection.post(msg).then(function(success){
+            //TODO: it seems that there is always errors coming back:
+        }, function(error){
+            switch (error.msg){
+            case 'ERR_NO_ERROR':
+                return Promise.resolve('okay'); //TBD
+            default:
+                return Promise.reject(new GPGMEJS_Error);
+                // INV_VALUE,
+                // GPG_ERR_NO_PUBKEY,
+                // GPG_ERR_AMBIGUOUS_NAME,
+                // GPG_ERR_CONFLICT
+            }
+        });
     }
+
 }
 
 /**
@@ -112,7 +158,7 @@ export class GpgME {
  */
 function putData(message, data){
     if (!message || !message instanceof GPGME_Message ) {
-        throw('NO_MESSAGE_OBJECT');
+        return new GPGMEJS_Error('WRONGPARAM');
     }
     if (!data){
         //TODO Debug only! No data is legitimate
@@ -126,6 +172,6 @@ function putData(message, data){
         message.setParameter('base64', false);
         message.setParameter('data', data);
     } else {
-        throw('ERR_WRONG_TYPE');
+        return new GPGMEJS_Error('WRONGPARAM');
     }
 }
\ No newline at end of file
index 1eec4da..54b9dd4 100644 (file)
  */
 
  import { GpgME } from "./gpgmejs";
-// import {Keyring}  from "./Keyring" TODO
-
+ import {GPGME_Keyring}  from "./Keyring"
+ import { GPGME_Key } from "./Key";
+ import { isFingerprint } from "./Helpers"
+ import { GPGMEJS_Error } from './Errors'
 
 export class GpgME_openPGPCompatibility {
 
     constructor(){
-        this._gpgme =  new GpgME;
+        this._gpgme =  new GpgME({
+            null_expire_is_never: false
+        });
+        this.Keyring = this.initKeyring();
     }
 
     /**
@@ -67,15 +72,14 @@ export class GpgME_openPGPCompatibility {
             || signature !== null
             || returnSessionKey !== null
             || date !== null){
-            throw('NOT_IMPLEMENTED');
+            return Promise.reject(new GPMGEJS_Error('NOT_IMPLEMENTED'));
         }
         if ( privateKeys
             || filename
             || compression
             || armor === false
             || detached == true){
-                console.log('may be implemented later');
-                throw('NOT_YET_IMPLEMENTED');
+                return Promise.reject(new GPGMEJS_Error('NOT_YET_IMPLEMENTED'));
         }
         return this.GpgME.encrypt(data, translateKeyInput(publicKeys), wildcard);
     }
@@ -103,16 +107,14 @@ export class GpgME_openPGPCompatibility {
         if (passwords !== undefined
             || sessionKeys
             || date){
-
-            throw('NOT_IMPLEMENTED');
+            return Promise.reject(new GPGMEJS_Error('NOT_IMPLEMENTED'));
         }
         if ( privateKeys
             || publicKeys
             || format !== 'utf8'
             || signature
         ){
-            console.log('may be implemented later');
-            throw('NOT_YET_IMPLEMENTED');
+            return Promise.reject(new GPGMEJS_Error('NOT_YET_IMPLEMENTED'));
         }
         return this.GpgME.decrypt(message);
         // TODO: translate between:
@@ -126,31 +128,74 @@ export class GpgME_openPGPCompatibility {
         // mime:   A Boolean indicating whether the data is a MIME object.
         // info:   An optional object with extra information.
     }
+    initKeyring(){
+        return new GPGME_Keyring_openPGPCompatibility;
+    }
 }
 
 /**
- *
- * @param {Object | String} Key Either a (presumably openpgp Key) Object with a
- *      primaryKeyproperty and a method getFingerprint, or a string.
- * @returns {String} Unchecked string value claiming to be a fingerprint
- *      TODO: gpgmejs checks again, so it's okay here.
+ * Translation layer offering basic Keyring API to be used in Mailvelope.
+ * It may still be changed/expanded/merged with GPGME_Keyring
  */
-function translateKeyInput(Key){
-    if (!Key){
-        return [];
+class GPGME_Keyring_openPGPCompatibility {
+    constructor(){
+        this._gpgme_keyring = new GPGME_Keyring;
     }
-    if (!Array.isArray(Key)){
-        Key = [Key];
+
+    /**
+     * Returns a GPGME_Key Object for each Key in the gnupg Keyring. This
+     * includes keys openpgpjs considers 'private' (usable for signing), with
+     * the difference that Key.armored will NOT contain any secret information.
+     * Please also note that a GPGME_Key does not offer full openpgpjs- Key
+     * compatibility.
+     * @returns {Array<GPGME_Key>} with the objects offering at least:
+     *  @property {String} armored The armored key block (does not include secret blocks)
+     *  @property {Boolean} hasSecret  Indicator if a private/secret key exists
+     *  @property {Boolean} isDefault  Indicator if private key exists and is the default key in this keyring
+     *  @property {String} fingerprint  The fingerprint identifying this key
+     * //TODO: Check if IsDefault is also always hasSecret
+     */
+    getPublicKeys(){
+        return this._gpgme_keyring.getKeys(null, true);
     }
-    let resultslist = [];
-    for (let i=0; i < Key.length; i++){
-        if (typeof(Key[i]) === 'string'){
-            resultslist.push(Key);
-        } else if (
-            Key[i].hasOwnProperty(primaryKey) &&
-            Key[i].primaryKey.hasOwnProperty(getFingerprint)){
-                resultslist.push(Key[i].primaryKey.getFingerprint());
+
+    /**
+     * Returns the Default Key used for crypto operation in gnupg.
+     * Please note that the armored property does not contained secret key blocks,
+     * despite secret blocks being part of the key itself.
+     * @returns {Promise <GPGME_Key>}
+     */
+    getDefaultKey(){
+        this._gpgme_keyring.getSubset({defaultKey: true}).then(function(result){
+            if (result.length === 1){
+                return Promise.resolve(result[0]);
+            }
+            else {
+                // TODO: Can there be "no default key"?
+                // TODO: Can there be several default keys?
+                return new GPGMEJS_Error; //TODO
+            }
+        });
+    }
+
+    /**
+     * Deletes a Key
+     * @param {Object} Object identifying key
+     * @param {String} key.fingerprint - fingerprint of the to be deleted key
+     * @param {Boolean} key.secret - indicator if private key should be deleted as well
+
+     * @returns {Promise.<Array.<undefined>, Error>} TBD: Not sure what is wanted
+     TODO @throws {Error} error.code = ‘KEY_NOT_EXIST’ - there is no key for the given fingerprint
+     TODO @throws {Error} error.code = ‘NO_SECRET_KEY’ - secret indicator set, but no secret key exists
+     */
+    deleteKey(key){
+        if (typeof(key) !== "object"){
+            return Promise.reject(new GPGMEJS_Error('WRONGPARAM'));
+        }
+        if ( !key.fingerprint || ! isFingerprint(key.fingerprint)){
+            return Promise.reject(new GPGMEJS_Error('WRONGPARAM'));
         }
+        let key_to_delete = new GPGME_Key(key.fingerprint);
+        return key_to_delete.deleteKey(key.secret);
     }
-    return resultslist;
-}
\ No newline at end of file
+}