js: add verify and signature parsing
authorMaximilian Krambach <maximilian.krambach@intevation.de>
Thu, 14 Jun 2018 10:15:51 +0000 (12:15 +0200)
committerMaximilian Krambach <maximilian.krambach@intevation.de>
Thu, 14 Jun 2018 10:15:51 +0000 (12:15 +0200)
--

* src/gpgmejs.js:
  - Added verify method
  - Added verification results in decrypt (if signatures are present
    in the message)
  - Added a base64 option to decrypt

* src/Signature.js: Convenience class for verification results. Used
   for e.g. converting timestamps to javascript time, quick overall
   validity checks

* src/Keyring.js: removed debug code

* src/Errors.js add two new Signature errors

lang/js/src/Errors.js
lang/js/src/Keyring.js
lang/js/src/Signature.js [new file with mode: 0644]
lang/js/src/gpgmejs.js

index 73e7438..a8cd8b5 100644 (file)
@@ -83,6 +83,14 @@ const err_list = {
             'configuration',
         type: 'error'
     },
+    'SIG_WRONG': {
+        msg:'A malformed signature was created',
+        type: 'error'
+    },
+    'SIG_NO_SIGS': {
+        msg:'There were no signatures found',
+        type: 'error'
+    },
     // generic
     'PARAM_WRONG':{
         msg: 'Invalid parameter was found',
index e07a593..451f936 100644 (file)
@@ -135,8 +135,6 @@ export class GPGME_Keyring {
                     // and probably performance, too
                     me.getKeys(null,true).then(function(keys){
                         for (let i=0; i < keys.length; i++){
-                            console.log(keys[i]);
-                            console.log(keys[i].get('hasSecret'));
                             if (keys[i].get('hasSecret') === true){
                                 resolve(keys[i]);
                                 break;
diff --git a/lang/js/src/Signature.js b/lang/js/src/Signature.js
new file mode 100644 (file)
index 0000000..d7d0598
--- /dev/null
@@ -0,0 +1,193 @@
+/* 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+
+ *
+ * Author(s):
+ *     Maximilian Krambach <mkrambach@intevation.de>
+ */
+
+/**
+ * Validates a signature object and returns
+ * @param {Object} sigObject Object as returned by gpgme-json. The definition
+ * of the expected values are to be found in the constants 'expKeys', 'expSum',
+ * 'expNote' in this file.
+ * @returns {GPGME_Signature} Signature Object
+ */
+
+import { gpgme_error } from './Errors';
+
+export function createSignature(sigObject){
+    if (
+        typeof(sigObject) !=='object' ||
+        !sigObject.hasOwnProperty('summary') ||
+        !sigObject.hasOwnProperty('fingerpprint') ||
+        !sigObject.hasOwnProperty('timestamp')
+        //TODO check if timestamp is mandatory in specification
+    ){
+        return gpgme_error('SIG_WRONG');
+    }
+    let keys = Object.keys(sigObject);
+    for (let i=0; i< keys.length; i++){
+        if ( typeof(sigObject[keys[i]]) !== expKeys[keys[i]] ){
+            return gpgme_error('SIG_WRONG');
+        }
+    }
+    let sumkeys = Object.keys(sigObject.summary);
+    for (let i=0; i< sumkeys.length; i++){
+        if ( typeof(sigObject.summary[sumkeys[i]]) !== expSum[sumkeys[i]] ){
+            return gpgme_error('SIG_WRONG');
+        }
+    }
+    if (sigObject.hasOwnProperty('notations')){
+        if (!Array.isArray(sigObject.notations)){
+            return gpgme_error('SIG_WRONG');
+        }
+        for (let i=0; i < sigObject.notations.length; i++){
+            let notation = sigObject.notations[i];
+            let notekeys = Object.keys(notation);
+            for (let j=0; j < notekeys.length; j++){
+                if ( typeof(notation[notekeys[j]]) !== expNote[notekeys[j]] ){
+                    return gpgme_error('SIG_WRONG');
+                }
+            }
+        }
+    }
+    return new GPGME_Signature(sigObject);
+}
+
+
+/**
+ * Representing the details of a signature. It is supposed to be read-only. The
+ * full details as given by gpgme-json can be accessed from the _rawSigObject.
+ * )
+ */
+class GPGME_Signature {
+    constructor(sigObject){
+        this._rawSigObject = sigObject;
+    }
+
+    /**
+     * The signatures' fingerprint
+     */
+    get fingerprint(){
+        return this._rawSigObject.fingerprint;
+    }
+
+    /**
+     * The expiration of this Signature as Javascript date, or null if
+     * signature does not expire
+     * @returns {Date | null}
+     */
+    get expiration(){
+        if (!this._rawSigObject.exp_timestamp){
+            return null;
+        }
+        return new Date(this._rawSigObject.exp_timestamp* 1000);
+    }
+
+    /**
+     * The creation date of this Signature in Javascript Date
+     * @returns {Date}
+     */
+    get timestamp(){
+        return new Date(this._rawSigObject.timestamp* 1000);
+    }
+
+    /**
+     * The overall validity of the key. If false, errorDetails may contain
+     * additional information
+     */
+    get valid() {
+        if (this._rawSigObject.valid === true){
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * gives more information on non-valid signatures. Refer to the gpgme docs
+     * https://www.gnupg.org/documentation/manuals/gpgme/Verify.html for
+     * details on the values
+     * @returns {Object} Object with boolean properties
+     */
+    get errorDetails(){
+        let properties = ['revoked', 'key-expired', 'sig-expired',
+            'key-missing', 'crl-missing', 'crl-too-old', 'bad-policy',
+            'sys-error'];
+        let result = {};
+        for (let i=0; i< properties.length; i++){
+            if ( this._rawSigObject.hasOwnProperty(properties[i]) ){
+                result[properties[i]] = this._rawSigObject[properties[i]];
+            }
+        }
+        return result;
+    }
+
+}
+
+/**
+ * Keys and their value's type for the signature Object
+ */
+const expKeys = {
+    'wrong_key_usage': 'boolean',
+    'chain_model': 'boolean',
+    'summary': 'object',
+    'is_de_vs': 'boolean',
+    'status_string':'string',
+    'fingerprint':'string',
+    'validity_string': 'string',
+    'pubkey_algo_name':'string',
+    'hash_algo_name':'string',
+    'pka_address':'string',
+    'status_code':'number',
+    'timestamp':'number',
+    'exp_timestamp':'number',
+    'pka_trust':'number',
+    'validity':'number',
+    'validity_reason':'number',
+    'notations': 'object'
+};
+
+/**
+ * Keys and their value's type for the summary
+ */
+const expSum = {
+    'valid': 'boolean',
+    'green': 'boolean',
+    'red': 'boolean',
+    'revoked': 'boolean',
+    'key-expired': 'boolean',
+    'sig-expired': 'boolean',
+    'key-missing': 'boolean',
+    'crl-missing': 'boolean',
+    'crl-too-old': 'boolean',
+    'bad-policy': 'boolean',
+    'sys-error': 'boolean'
+};
+
+/**
+ * Keys and their value's type for notations objects
+ */
+const expNote = {
+    'human_readable': 'boolean',
+    'critical':'boolean',
+    'name': 'string',
+    'value': 'string',
+    'flags': 'number'
+};
index 7fa7643..a0f7e96 100644 (file)
@@ -26,6 +26,7 @@ import {GPGME_Message, createMessage} from './Message';
 import {toKeyIdArray} from './Helpers';
 import { gpgme_error } from './Errors';
 import { GPGME_Keyring } from './Keyring';
+import { createSignature } from './Signature';
 
 export class GpgME {
     /**
@@ -107,15 +108,28 @@ export class GpgME {
     * Decrypt a Message
     * @param {String|Object} data text/data to be decrypted. Accepts Strings
     *  and Objects with a getText method
-    * @returns {Promise<Object>} decrypted message:
-        data:   The decrypted data.
-        base64: Boolean indicating whether data is base64 encoded.
-        mime:   A Boolean indicating whether the data is a MIME object.
-        signatures: Array of signature Objects TODO not yet implemented.
-            // should be an object that can tell if all signatures are valid.
+    * @param {Boolean} base64 (optional) false if the data is an armored block,
+    *   true if it is base64 encoded binary data
+    * @returns {Promise<Object>} result: Decrypted Message and information
+    * @returns {String} result.data: The decrypted data.
+    * @returns {Boolean} result.base64: indicating whether data is base64
+    *   encoded.
+    * @returns {Boolean} result.is_mime: Indicating whether the data is a MIME
+    *   object.
+    * @returns {String} result.file_name: The optional original file name
+    * @returns {Object} message.signatures Verification details for signatures:
+    * @returns {Boolean} message.signatures.all_valid: true if all signatures
+    *   are valid
+    * @returns {Number} message.signatures.count: Number of signatures found
+    * @returns {Number} message.signatures.failures Number of invalid
+    *   signatures
+    * @returns {Array<Object>} message.signatures.signatures. Two arrays
+    *   (good & bad) of {@link GPGME_Signature} objects, offering further
+    *   information.
+    *
     * @async
     */
-    decrypt(data){
+    decrypt(data, base64=false){
         if (data === undefined){
             return Promise.reject(gpgme_error('MSG_EMPTY'));
         }
@@ -124,8 +138,32 @@ export class GpgME {
         if (msg instanceof Error){
             return Promise.reject(msg);
         }
+        if (base64 === true){
+            msg.setParameter('base64', true);
+        }
         putData(msg, data);
-        return msg.post();
+        if (base64 === true){
+            msg.setParameter('base64', true);
+        }
+        return new Promise(function(resolve, reject){
+            msg.post().then(function(result){
+                let _result = {data: result.data};
+                _result.base64 = result.base64 ? true: false;
+                _result.is_mime = result.mime ? true: false;
+                if (result.file_name){
+                    _result.file_name = result.file_name;
+                }
+                if (
+                    result.hasOwnProperty('signatures') &&
+                    Array.isArray(result.signatures)
+                ) {
+                    _result.signatures = collectSignatures(result.signatures);
+                }
+                resolve(_result);
+            }, function(error){
+                reject(error);
+            });
+        });
     }
 
     /**
@@ -179,6 +217,59 @@ export class GpgME {
             });
         });
     }
+
+    /**
+     * Verifies data.
+     * @param {String|Object} data text/data to be verified. Accepts Strings
+     * and Objects with a gettext method
+     * @param {String} (optional) A detached signature. If not present, opaque
+     * mode is assumed
+     * @param {Boolean} (optional) Data and signature are base64 encoded
+     * // TODO verify if signature really is assumed to be base64
+     * @returns {Promise<Object>} result:
+     * @returns {Boolean} result.data: The verified data
+     * @returns {Boolean} result.is_mime: The message claims it is MIME
+     * @returns {String} result.file_name: The optional filename of the message
+     * @returns {Boolean} result.all_valid: true if all signatures are valid
+     * @returns {Number} result.count: Number of signatures found
+     * @returns {Number} result.failures Number of unsuccessful signatures
+     * @returns {Array<Object>} result.signatures. Two arrays (good & bad) of
+     *      {@link GPGME_Signature} objects, offering further information.
+     */
+    verify(data, signature, base64 = false){
+        let msg = createMessage('verify');
+        let dt = this.putData(msg, data);
+        if (dt instanceof Error){
+            return Promise.reject(dt);
+        }
+        if (signature){
+            if (typeof(signature)!== 'string'){
+                return Promise.reject(gpgme_error('PARAM_WRONG'));
+            } else {
+                msg.setParameter('signature', signature);
+            }
+        }
+        if (base64 === true){
+            msg.setParameter('base64', true);
+        }
+        return new Promise(function(resolve, reject){
+            msg.post().then(function (message){
+                if (!message.info.signatures){
+                    reject(gpgme_error('SIG_NO_SIGS'));
+                } else {
+                    let _result = collectSignatures(message.info.signatures);
+                    _result.is_mime = message.info.is_mime? true: false;
+                    if (message.info.filename){
+                        _result.file_name = message.info.filename;
+                    }
+                    _result.data = message.data;
+                    resolve(_result);
+                }
+            }, function(error){
+                reject(error);
+            });
+        });
+    }
 }
 
 /**
@@ -209,3 +300,34 @@ function putData(message, data){
         return gpgme_error('PARAM_WRONG');
     }
 }
+
+function collectSignatures(sigs){
+    if (!Array.isArray(sigs)){
+        return gpgme_error('SIG_NO_SIGS');
+    }
+    let summary = {
+        all_valid: false,
+        count: sigs.length,
+        failures: 0,
+        signatures: {
+            good: [],
+            bad: [],
+        }
+    };
+    for (let i=0; i< sigs.length; i++){
+        let sigObj = createSignature(sigs[i]);
+        if (sigObj instanceof Error){
+            return gpgme_error('SIG_WRONG');
+        }
+        if (sigObj.valid !== true){
+            summary.failures += 1;
+            summary.signatures.bad.push(sigObj);
+        } else {
+            summary.signatures.good.push(sigObj);
+        }
+    }
+    if (summary.failures === 0){
+        summary.all_valid = true;
+    }
+    return summary;
+}
\ No newline at end of file