From 3c783bd09ce54b0d50dc3bea201e70e4fcbbf6a3 Mon Sep 17 00:00:00 2001 From: Maximilian Krambach Date: Thu, 14 Jun 2018 12:15:51 +0200 Subject: [PATCH] js: add verify and signature parsing -- * 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 | 8 ++ lang/js/src/Keyring.js | 2 - lang/js/src/Signature.js | 193 +++++++++++++++++++++++++++++++++++++++ lang/js/src/gpgmejs.js | 138 ++++++++++++++++++++++++++-- 4 files changed, 331 insertions(+), 10 deletions(-) create mode 100644 lang/js/src/Signature.js diff --git a/lang/js/src/Errors.js b/lang/js/src/Errors.js index 73e74382..a8cd8b56 100644 --- a/lang/js/src/Errors.js +++ b/lang/js/src/Errors.js @@ -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', diff --git a/lang/js/src/Keyring.js b/lang/js/src/Keyring.js index e07a5934..451f936a 100644 --- a/lang/js/src/Keyring.js +++ b/lang/js/src/Keyring.js @@ -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 index 00000000..d7d05983 --- /dev/null +++ b/lang/js/src/Signature.js @@ -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 . + * SPDX-License-Identifier: LGPL-2.1+ + * + * Author(s): + * Maximilian Krambach + */ + +/** + * 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' +}; diff --git a/lang/js/src/gpgmejs.js b/lang/js/src/gpgmejs.js index 7fa7643c..a0f7e968 100644 --- a/lang/js/src/gpgmejs.js +++ b/lang/js/src/gpgmejs.js @@ -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} 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} 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} 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} 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} 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