diff --git a/lang/js/BrowserTestExtension/tests/startup.js b/lang/js/BrowserTestExtension/tests/startup.js index 1e2784d9..63358aa9 100644 --- a/lang/js/BrowserTestExtension/tests/startup.js +++ b/lang/js/BrowserTestExtension/tests/startup.js @@ -1,48 +1,47 @@ /* 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 */ /* global describe, it, expect, Gpgmejs, inputvalues */ describe('GPGME context', function(){ it('Starting a GpgME instance', function(done){ let prm = Gpgmejs.init(); const input = inputvalues.someInputParameter; prm.then( function(context){ expect(context).to.be.an('object'); expect(context.encrypt).to.be.a('function'); expect(context.decrypt).to.be.a('function'); expect(context.sign).to.be.a('function'); expect(context.verify).to.be.a('function'); context.Keyring = input; expect(context.Keyring).to.be.an('object'); expect(context.Keyring).to.not.equal(input); - expect(context._Keyring).to.equal(context.Keyring); expect(context.Keyring.getKeys).to.be.a('function'); expect(context.Keyring.getDefaultKey).to.be.a('function'); expect(context.Keyring.importKey).to.be.a('function'); expect(context.Keyring.generateKey).to.be.a('function'); done(); }); }); }); \ No newline at end of file diff --git a/lang/js/src/Connection.js b/lang/js/src/Connection.js index 561a5b70..b0105757 100644 --- a/lang/js/src/Connection.js +++ b/lang/js/src/Connection.js @@ -1,280 +1,281 @@ /* 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 */ /* global chrome */ import { permittedOperations } from './permittedOperations'; import { gpgme_error } from './Errors'; import { GPGME_Message, createMessage } from './Message'; /** * A Connection handles the nativeMessaging interaction via a port. As the * protocol only allows up to 1MB of message sent from the nativeApp to the * browser, the connection will stay open until all parts of a communication * are finished. For a new request, a new port will open, to avoid mixing * contexts. * @class */ export class Connection{ constructor(){ let _connection = chrome.runtime.connectNative('gpgmejson'); /** * Immediately closes an open port. */ this.disconnect = function () { if (_connection){ _connection.disconnect(); _connection = null; } }; /** * @typedef {Object} backEndDetails * @property {String} gpgme Version number of gpgme * @property {Array} info Further information about the backend * and the used applications (Example: * { * "protocol": "OpenPGP", * "fname": "/usr/bin/gpg", * "version": "2.2.6", * "req_version": "1.4.0", * "homedir": "default" * } */ /** * Retrieves the information about the backend. * @param {Boolean} details (optional) If set to false, the promise will * just return if a connection was successful. * @returns {Promise|Promise} Details from the * backend * @async */ this.checkConnection = function(details = true){ const msg = createMessage('version'); if (details === true) { return this.post(msg); } else { let me = this; return new Promise(function(resolve) { Promise.race([ me.post(msg), new Promise(function(resolve, reject){ setTimeout(function(){ reject(gpgme_error('CONN_TIMEOUT')); }, 500); }) ]).then(function(){ // success resolve(true); }, function(){ // failure resolve(false); }); }); } }; /** * Sends a {@link GPGME_Message} via tghe nativeMessaging port. It * resolves with the completed answer after all parts have been * received and reassembled, or rejects with an {@link GPGME_Error}. * * @param {GPGME_Message} message * @returns {Promise} The collected answer * @async */ this.post = function (message){ if (!message || !(message instanceof GPGME_Message)){ this.disconnect(); return Promise.reject(gpgme_error( 'PARAM_WRONG', 'Connection.post')); } if (message.isComplete() !== true){ this.disconnect(); return Promise.reject(gpgme_error('MSG_INCOMPLETE')); } let chunksize = message.chunksize; return new Promise(function(resolve, reject){ - let answer = new Answer(message); + let answer = Object.freeze(new Answer(message)); let listener = function(msg) { if (!msg){ _connection.onMessage.removeListener(listener); _connection.disconnect(); reject(gpgme_error('CONN_EMPTY_GPG_ANSWER')); } else { let answer_result = answer.collect(msg); if (answer_result !== true){ _connection.onMessage.removeListener(listener); _connection.disconnect(); reject(answer_result); } else { if (msg.more === true){ _connection.postMessage({ 'op': 'getmore', 'chunksize': chunksize }); } else { _connection.onMessage.removeListener(listener); _connection.disconnect(); const message = answer.getMessage(); if (message instanceof Error){ reject(message); } else { resolve(message); } } } } }; _connection.onMessage.addListener(listener); if (permittedOperations[message.operation].pinentry){ return _connection.postMessage(message.message); } else { return Promise.race([ _connection.postMessage(message.message), function(resolve, reject){ setTimeout(function(){ _connection.disconnect(); reject(gpgme_error('CONN_TIMEOUT')); }, 5000); }]).then(function(result){ return result; }, function(reject){ if(!(reject instanceof Error)) { _connection.disconnect(); return gpgme_error('GNUPG_ERROR', reject); } else { return reject; } }); } }); }; } } /** * A class for answer objects, checking and processing the return messages of * the nativeMessaging communication. * @protected */ class Answer{ /** * @param {GPGME_Message} message */ constructor(message){ const operation = message.operation; - const expect = message.expect; + const expected = message.getExpect(); let response_b64 = null; this.getOperation = function(){ return operation; }; + this.getExpect = function(){ - return expect; + return expected; }; /** * Adds incoming base64 encoded data to the existing response * @param {*} msg base64 encoded data. * @returns {Boolean} * * @private */ this.collect = function (msg){ if (typeof(msg) !== 'object' || !msg.hasOwnProperty('response')) { return gpgme_error('CONN_UNEXPECTED_ANSWER'); } if (response_b64 === null){ response_b64 = msg.response; return true; } else { response_b64 += msg.response; return true; } }; /** * Returns the base64 encoded answer data with the content verified * against {@link permittedOperations}. */ this.getMessage = function (){ if (response_b64 === undefined){ return gpgme_error('CONN_UNEXPECTED_ANSWER'); } let _decodedResponse = JSON.parse(atob(response_b64)); let _response = {}; let messageKeys = Object.keys(_decodedResponse); let poa = permittedOperations[this.getOperation()].answer; if (messageKeys.length === 0){ return gpgme_error('CONN_UNEXPECTED_ANSWER'); } for (let i= 0; i < messageKeys.length; i++){ let key = messageKeys[i]; switch (key) { case 'type': if (_decodedResponse.type === 'error'){ return (gpgme_error('GNUPG_ERROR', decodeURIComponent(escape(_decodedResponse.msg)))); } else if (poa.type.indexOf(_decodedResponse.type) < 0){ return gpgme_error('CONN_UNEXPECTED_ANSWER'); } break; case 'base64': break; case 'msg': if (_decodedResponse.type === 'error'){ return (gpgme_error('GNUPG_ERROR', _decodedResponse.msg)); } break; default: if (!poa.data.hasOwnProperty(key)){ return gpgme_error('CONN_UNEXPECTED_ANSWER'); } if( typeof(_decodedResponse[key]) !== poa.data[key] ){ return gpgme_error('CONN_UNEXPECTED_ANSWER'); } if (_decodedResponse.base64 === true && poa.data[key] === 'string' - && this.getExpect() === undefined + && this.getExpect() !== 'base64' ){ _response[key] = decodeURIComponent( atob(_decodedResponse[key]).split('').map( function(c) { return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); }).join('')); } else { _response[key] = _decodedResponse[key]; } break; } } return _response; }; } } diff --git a/lang/js/src/Errors.js b/lang/js/src/Errors.js index 0cf1af19..39e3a74a 100644 --- a/lang/js/src/Errors.js +++ b/lang/js/src/Errors.js @@ -1,166 +1,166 @@ /* 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 */ /** * Listing of all possible error codes and messages of a {@link GPGME_Error}. */ const err_list = { // Connection 'CONN_NO_CONNECT': { msg:'Connection with the nativeMessaging host could not be' + ' established.', type: 'error' }, 'CONN_EMPTY_GPG_ANSWER':{ msg: 'The nativeMessaging answer was empty.', type: 'error' }, 'CONN_TIMEOUT': { msg: 'A connection timeout was exceeded.', type: 'error' }, 'CONN_UNEXPECTED_ANSWER': { msg: 'The answer from gnupg was not as expected.', type: 'error' }, 'CONN_ALREADY_CONNECTED':{ msg: 'A connection was already established.', type: 'warning' }, // Message/Data 'MSG_INCOMPLETE': { msg: 'The Message did not match the minimum requirements for' + ' the interaction.', type: 'error' }, 'MSG_EMPTY' : { msg: 'The Message is empty.', type: 'error' }, 'MSG_WRONG_OP': { msg: 'The operation requested could not be found', type: 'error' }, 'MSG_NO_KEYS' : { msg: 'There were no valid keys provided.', type: 'warning' }, 'MSG_NOT_A_FPR': { msg: 'The String is not an accepted fingerprint', type: 'warning' }, 'KEY_INVALID': { msg:'Key object is invalid', type: 'error' }, 'KEY_NOKEY': { msg:'This key does not exist in GPG', type: 'error' }, 'KEY_NO_INIT': { msg:'This property has not been retrieved yet from GPG', type: 'error' }, 'KEY_NO_DEFAULT': { msg:'A default key could not be established. Please check yout gpg ' + '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', type: 'error' }, 'PARAM_IGNORED': { msg: 'An parameter was set that has no effect in gpgmejs', type: 'warning' }, 'GENERIC_ERROR': { msg: 'Unspecified error', type: 'error' } }; /** * Checks the given error code and returns an {@link GPGME_Error} error object * with some information about meaning and origin * @param {*} code Error code. Should be in err_list or 'GNUPG_ERROR' * @param {*} info Error message passed through if code is 'GNUPG_ERROR' * @returns {GPGME_Error} */ export function gpgme_error(code = 'GENERIC_ERROR', info){ if (err_list.hasOwnProperty(code)){ if (err_list[code].type === 'error'){ - return new GPGME_Error(code); + return Object.freeze(new GPGME_Error(code)); } if (err_list[code].type === 'warning'){ // eslint-disable-next-line no-console // console.warn(code + ': ' + err_list[code].msg); } return null; } else if (code === 'GNUPG_ERROR'){ - return new GPGME_Error(code, info); + return Object.freeze(new GPGME_Error(code, info)); } else { - return new GPGME_Error('GENERIC_ERROR'); + return Object.freeze(new GPGME_Error('GENERIC_ERROR')); } } /** * An error class with additional info about the origin of the error, as string * @property {String} code Short description of origin and type of the error * @property {String} msg Additional info * @class * @protected * @extends Error */ class GPGME_Error extends Error{ constructor(code = 'GENERIC_ERROR', msg=''){ if (code === 'GNUPG_ERROR' && typeof(msg) === 'string'){ super(msg); } else if (err_list.hasOwnProperty(code)){ if (msg){ super(err_list[code].msg + '--' + msg); } else { super(err_list[code].msg); } } else { super(err_list['GENERIC_ERROR'].msg); } this.getCode = function(){ return code; }; } get code(){ return this.getCode(); } } \ No newline at end of file diff --git a/lang/js/src/Key.js b/lang/js/src/Key.js index d5873a70..f431a283 100644 --- a/lang/js/src/Key.js +++ b/lang/js/src/Key.js @@ -1,608 +1,608 @@ /* 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 */ import { isFingerprint, isLongId } from './Helpers'; import { gpgme_error } from './Errors'; import { createMessage } from './Message'; /** * Validates the given fingerprint and creates a new {@link GPGME_Key} * @param {String} fingerprint * @param {Boolean} async If True, Key properties (except fingerprint) will be * queried from gnupg on each call, making the operation up-to-date, the * answers will be Promises, and the performance will likely suffer * @returns {GPGME_Key|GPGME_Error} */ export function createKey(fingerprint, async = false){ if (!isFingerprint(fingerprint) || typeof(async) !== 'boolean'){ return gpgme_error('PARAM_WRONG'); } - else return new GPGME_Key(fingerprint, async); + else return Object.freeze(new GPGME_Key(fingerprint, async)); } /** * Represents the Keys as stored in the gnupg backend * It allows to query almost all information defined in gpgme Key Objects * Refer to {@link validKeyProperties} for available information, and the gpgme * documentation on their meaning * (https://www.gnupg.org/documentation/manuals/gpgme/Key-objects.html) * * @class */ export class GPGME_Key { constructor(fingerprint, async){ /** * @property {Boolean} If true, most answers will be asynchronous */ this.isAsync = async; let _data = {fingerprint: fingerprint}; this.getFingerprint = function(){ if (!_data.fingerprint || !isFingerprint(_data.fingerprint)){ return gpgme_error('KEY_INVALID'); } return _data.fingerprint; }; /** * Property indicating if the Key possesses a private/secret part. If * this information is not yet cached, it returns an * {@link GPGME_Error} with code 'KEY_NO_INIT'. Running * {@link refreshKey} may help in this case. * @returns {Boolean} If the Key has a secret subkey. */ this.hasSecret= function (){ return this.get('hasSecret', true); }; /** * @param {Object} data Bulk set the data for this key, with an Object * sent by gpgme-json. * @returns {GPGME_Key|GPGME_Error} Itself after values have been set, * an error if something went wrong. * @private */ this.setKeyData = function (data){ if (typeof(data) !== 'object') { return gpgme_error('KEY_INVALID'); } if (!data.fingerprint || data.fingerprint !== _data.fingerprint){ return gpgme_error('KEY_INVALID'); } let keys = Object.keys(data); for (let i=0; i< keys.length; i++){ if (!validKeyProperties.hasOwnProperty(keys[i])){ return gpgme_error('KEY_INVALID'); } //running the defined validation function if (validKeyProperties[keys[i]](data[keys[i]]) !== true ){ return gpgme_error('KEY_INVALID'); } switch (keys[i]){ case 'subkeys': _data.subkeys = []; for (let i=0; i< data.subkeys.length; i++) { - _data.subkeys.push( - new GPGME_Subkey(data.subkeys[i])); + _data.subkeys.push(Object.freeze( + new GPGME_Subkey(data.subkeys[i]))); } break; case 'userids': _data.userids = []; for (let i=0; i< data.userids.length; i++) { - _data.userids.push( - new GPGME_UserId(data.userids[i])); + _data.userids.push(Object.freeze( + new GPGME_UserId(data.userids[i]))); } break; case 'last_update': _data[keys[i]] = new Date( data[keys[i]] * 1000 ); break; default: _data[keys[i]] = data[keys[i]]; } } return this; }; /** * Query any property of the Key listed in {@link validKeyProperties} * @param {String} property property to be retreived * @returns {*|Promise<*>} the value (Boolean, String, Array, Object). * If 'cached' is false, the value will be resolved as a Promise. */ this.get = function(property) { if (this.isAsync === true) { let me = this; return new Promise(function(resolve, reject) { if (property === 'armored'){ resolve(me.getArmor()); } else if (property === 'hasSecret'){ resolve(me.getHasSecret()); } else if (validKeyProperties.hasOwnProperty(property)){ let msg = createMessage('keylist'); msg.setParameter('keys', _data.fingerprint); msg.post().then(function(result){ if (result.keys && result.keys.length === 1 && result.keys[0].hasOwnProperty(property)){ resolve(result.keys[0][property]); } else { reject(gpgme_error('CONN_UNEXPECTED_ANSWER')); } }, function(error){ reject(gpgme_error(error)); }); } else { reject(gpgme_error('PARAM_WRONG')); } }); } else { if (!validKeyProperties.hasOwnProperty(property)){ return gpgme_error('PARAM_WRONG'); } if (!_data.hasOwnProperty(property)){ return gpgme_error('KEY_NO_INIT'); } else { return (_data[property]); } } }; /** * Reloads the Key information from gnupg. This is only useful if you * use the GPGME_Keys cached. Note that this is a performance hungry * operation. If you desire more than a few refreshs, it may be * advisable to run {@link Keyring.getKeys} instead. * @returns {Promise} * @async */ this.refreshKey = function() { let me = this; return new Promise(function(resolve, reject) { if (!_data.fingerprint){ reject(gpgme_error('KEY_INVALID')); } let msg = createMessage('keylist'); msg.setParameter('sigs', true); msg.setParameter('keys', _data.fingerprint); msg.post().then(function(result){ if (result.keys.length === 1){ me.setKeyData(result.keys[0]); me.getHasSecret().then(function(){ me.getArmor().then(function(){ resolve(me); }, function(error){ reject(error); }); }, function(error){ reject(error); }); } else { reject(gpgme_error('KEY_NOKEY')); } }, function (error) { reject(gpgme_error('GNUPG_ERROR'), error); }); }); }; /** * Query the armored block of the Key directly from gnupg. Please note * that this will not get you any export of the secret/private parts of * a Key * @returns {Promise} * @async */ this.getArmor = function(){ return new Promise(function(resolve, reject) { if (!_data.fingerprint){ reject(gpgme_error('KEY_INVALID')); } let msg = createMessage('export'); msg.setParameter('armor', true); msg.setParameter('keys', _data.fingerprint); msg.post().then(function(result){ _data.armored = result.data; resolve(result.data); }, function(error){ reject(error); }); }); }; /** * Find out if the Key includes a secret part. Note that this is a * rather nonperformant operation, as it needs to query gnupg twice. * If you want this inforrmation about more than a few Keys, it may be * advisable to run {@link Keyring.getKeys} instead. * @returns {Promise} True if a secret/private Key * is available in the gnupg Keyring * @async */ this.getHasSecret = function (){ return new Promise(function(resolve, reject) { if (!_data.fingerprint){ reject(gpgme_error('KEY_INVALID')); } let msg = createMessage('keylist'); msg.setParameter('keys', _data.fingerprint); msg.setParameter('secret', true); msg.post().then(function(result){ _data.hasSecret = null; if ( result.keys && result.keys.length === 1 && result.keys[0].secret === true ) { _data.hasSecret = true; resolve(true); } else { _data.hasSecret = false; resolve(false); } }, function(error){ reject(error); }); }); }; /** * Deletes the (public) Key from the GPG Keyring. Note that a deletion * of a secret key is not supported by the native backend. * @returns {Promise} Success if key was deleted, * rejects with a GPG error otherwise. */ this.delete= function (){ return new Promise(function(resolve, reject){ if (!_data.fingerprint){ reject(gpgme_error('KEY_INVALID')); } let msg = createMessage('delete'); msg.setParameter('key', _data.fingerprint); msg.post().then(function(result){ resolve(result.success); }, function(error){ reject(error); }); }); }; } /** * @returns {String} The fingerprint defining this Key */ get fingerprint(){ return this.getFingerprint(); } /** * Property for the export of armored Key. If the armored Key is not * cached, it returns an {@link GPGME_Error} with code 'KEY_NO_INIT'. * Running {@link refreshKey} may help in this case. * @returns {String|GPGME_Error} The armored public Key block. */ get armored(){ return this.get('armored', true); } } /** * Representing a subkey of a Key. * @class * @protected */ class GPGME_Subkey { /** * Initializes with the json data sent by gpgme-json * @param {Object} data * @private */ constructor(data){ let _data = {}; let keys = Object.keys(data); /** * Validates a subkey property against {@link validSubKeyProperties} and * sets it if validation is successful * @param {String} property * @param {*} value * @param private */ const setProperty = function (property, value){ if (validSubKeyProperties.hasOwnProperty(property)){ if (validSubKeyProperties[property](value) === true) { if (property === 'timestamp' || property === 'expires'){ _data[property] = new Date(value * 1000); } else { _data[property] = value; } } } }; for (let i=0; i< keys.length; i++) { setProperty(keys[i], data[keys[i]]); } /** * Fetches any information about this subkey * @param {String} property Information to request * @returns {String | Number | Date} */ this.get = function(property) { if (_data.hasOwnProperty(property)){ return (_data[property]); } }; } } /** * Representing user attributes associated with a Key or subkey * @class * @protected */ class GPGME_UserId { /** * Initializes with the json data sent by gpgme-json * @param {Object} data * @private */ constructor(data){ let _data = {}; let keys = Object.keys(data); const setProperty = function(property, value){ if (validUserIdProperties.hasOwnProperty(property)){ if (validUserIdProperties[property](value) === true) { if (property === 'last_update'){ _data[property] = new Date(value*1000); } else { _data[property] = value; } } } }; for (let i=0; i< keys.length; i++) { setProperty(keys[i], data[keys[i]]); } /** * Validates a subkey property against {@link validUserIdProperties} and * sets it if validation is successful * @param {String} property * @param {*} value * @param private */ /** * Fetches information about the user * @param {String} property Information to request * @returns {String | Number} */ this.get = function (property) { if (_data.hasOwnProperty(property)){ return (_data[property]); } }; } } /** * Validation definition for userIds. Each valid userId property is represented * as a key- Value pair, with their value being a validation function to check * against * @protected * @const */ const validUserIdProperties = { 'revoked': function(value){ return typeof(value) === 'boolean'; }, 'invalid': function(value){ return typeof(value) === 'boolean'; }, 'uid': function(value){ if (typeof(value) === 'string' || value === ''){ return true; } return false; }, 'validity': function(value){ if (typeof(value) === 'string'){ return true; } return false; }, 'name': function(value){ if (typeof(value) === 'string' || value === ''){ return true; } return false; }, 'email': function(value){ if (typeof(value) === 'string' || value === ''){ return true; } return false; }, 'address': function(value){ if (typeof(value) === 'string' || value === ''){ return true; } return false; }, 'comment': function(value){ if (typeof(value) === 'string' || value === ''){ return true; } return false; }, 'origin': function(value){ return Number.isInteger(value); }, 'last_update': function(value){ return Number.isInteger(value); } }; /** * Validation definition for subKeys. Each valid userId property is represented * as a key-value pair, with the value being a validation function * @protected * @const */ const validSubKeyProperties = { 'invalid': function(value){ return typeof(value) === 'boolean'; }, 'can_encrypt': function(value){ return typeof(value) === 'boolean'; }, 'can_sign': function(value){ return typeof(value) === 'boolean'; }, 'can_certify': function(value){ return typeof(value) === 'boolean'; }, 'can_authenticate': function(value){ return typeof(value) === 'boolean'; }, 'secret': function(value){ return typeof(value) === 'boolean'; }, 'is_qualified': function(value){ return typeof(value) === 'boolean'; }, 'is_cardkey': function(value){ return typeof(value) === 'boolean'; }, 'is_de_vs': function(value){ return typeof(value) === 'boolean'; }, 'pubkey_algo_name': function(value){ return typeof(value) === 'string'; // TODO: check against list of known?[''] }, 'pubkey_algo_string': function(value){ return typeof(value) === 'string'; // TODO: check against list of known?[''] }, 'keyid': function(value){ return isLongId(value); }, 'pubkey_algo': function(value) { return (Number.isInteger(value) && value >= 0); }, 'length': function(value){ return (Number.isInteger(value) && value > 0); }, 'timestamp': function(value){ return (Number.isInteger(value) && value > 0); }, 'expires': function(value){ return (Number.isInteger(value) && value > 0); } }; /** * Validation definition for Keys. Each valid Key property is represented * as a key-value pair, with their value being a validation function * @protected * @const */ const validKeyProperties = { 'fingerprint': function(value){ return isFingerprint(value); }, 'armored': function(value){ return typeof(value === 'string'); }, 'revoked': function(value){ return typeof(value) === 'boolean'; }, 'expired': function(value){ return typeof(value) === 'boolean'; }, 'disabled': function(value){ return typeof(value) === 'boolean'; }, 'invalid': function(value){ return typeof(value) === 'boolean'; }, 'can_encrypt': function(value){ return typeof(value) === 'boolean'; }, 'can_sign': function(value){ return typeof(value) === 'boolean'; }, 'can_certify': function(value){ return typeof(value) === 'boolean'; }, 'can_authenticate': function(value){ return typeof(value) === 'boolean'; }, 'secret': function(value){ return typeof(value) === 'boolean'; }, 'is_qualified': function(value){ return typeof(value) === 'boolean'; }, 'protocol': function(value){ return typeof(value) === 'string'; //TODO check for implemented ones }, 'issuer_serial': function(value){ return typeof(value) === 'string'; }, 'issuer_name': function(value){ return typeof(value) === 'string'; }, 'chain_id': function(value){ return typeof(value) === 'string'; }, 'owner_trust': function(value){ return typeof(value) === 'string'; }, 'last_update': function(value){ return (Number.isInteger(value)); //TODO undefined/null possible? }, 'origin': function(value){ return (Number.isInteger(value)); }, 'subkeys': function(value){ return (Array.isArray(value)); }, 'userids': function(value){ return (Array.isArray(value)); }, 'tofu': function(value){ return (Array.isArray(value)); }, 'hasSecret': function(value){ return typeof(value) === 'boolean'; } }; diff --git a/lang/js/src/Message.js b/lang/js/src/Message.js index c0b6ed57..e2c07344 100644 --- a/lang/js/src/Message.js +++ b/lang/js/src/Message.js @@ -1,240 +1,250 @@ /* 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 */ import { permittedOperations } from './permittedOperations'; import { gpgme_error } from './Errors'; import { Connection } from './Connection'; /** * Initializes a message for gnupg, validating the message's purpose with * {@link permittedOperations} first * @param {String} operation * @returns {GPGME_Message|GPGME_Error} The Message object */ export function createMessage(operation){ if (typeof(operation) !== 'string'){ return gpgme_error('PARAM_WRONG'); } if (permittedOperations.hasOwnProperty(operation)){ - return new GPGME_Message(operation); + return Object.freeze(new GPGME_Message(operation)); } else { return gpgme_error('MSG_WRONG_OP'); } } /** * A Message collects, validates and handles all information required to * successfully establish a meaningful communication with gpgme-json via * {@link Connection.post}. The definition on which communication is available * can be found in {@link permittedOperations}. * @class */ export class GPGME_Message { constructor(operation){ let _msg = { op: operation, chunksize: 1023* 1024 }; + let expected = null; this.getOperation = function(){ return _msg.op; }; + this.setExpect = function(value){ + if (value === 'base64'){ + expected = value; + } + }; + this.getExpect = function(){ + return expected; + }; + /** * The maximum size of responses from gpgme in bytes. As of July 2018, * most browsers will only accept answers up to 1 MB of size. * Everything above that threshold will not pass through * nativeMessaging; answers that are larger need to be sent in parts. * The lower limit is set to 10 KB. Messages smaller than the threshold * will not encounter problems, larger messages will be received in * chunks. If the value is not explicitly specified, 1023 KB is used. */ this.setChunksize = function (value){ if ( Number.isInteger(value) && value > 10 * 1024 && value <= 1024 * 1024 ){ _msg.chunksize = value; } }; this.getMsg = function(){ return _msg; }; this.getChunksize= function() { return _msg.chunksize; }; /** * Sets a parameter for the message. It validates with * {@link permittedOperations} * @param {String} param Parameter to set * @param {any} value Value to set * @returns {Boolean} If the parameter was set successfully */ this.setParameter = function ( param,value ){ if (!param || typeof(param) !== 'string'){ return gpgme_error('PARAM_WRONG'); } let po = permittedOperations[_msg.op]; if (!po){ return gpgme_error('MSG_WRONG_OP'); } let poparam = null; if (po.required.hasOwnProperty(param)){ poparam = po.required[param]; } else if (po.optional.hasOwnProperty(param)){ poparam = po.optional[param]; } else { return gpgme_error('PARAM_WRONG'); } // check incoming value for correctness let checktype = function(val){ switch(typeof(val)){ case 'string': if (poparam.allowed.indexOf(typeof(val)) >= 0 && val.length > 0) { return true; } return gpgme_error('PARAM_WRONG'); case 'number': if ( poparam.allowed.indexOf('number') >= 0 && isNaN(value) === false){ return true; } return gpgme_error('PARAM_WRONG'); case 'boolean': if (poparam.allowed.indexOf('boolean') >= 0){ return true; } return gpgme_error('PARAM_WRONG'); case 'object': if (Array.isArray(val)){ if (poparam.array_allowed !== true){ return gpgme_error('PARAM_WRONG'); } for (let i=0; i < val.length; i++){ let res = checktype(val[i]); if (res !== true){ return res; } } if (val.length > 0) { return true; } } else if (val instanceof Uint8Array){ if (poparam.allowed.indexOf('Uint8Array') >= 0){ return true; } return gpgme_error('PARAM_WRONG'); } else { return gpgme_error('PARAM_WRONG'); } break; default: return gpgme_error('PARAM_WRONG'); } }; let typechecked = checktype(value); if (typechecked !== true){ return typechecked; } if (poparam.hasOwnProperty('allowed_data')){ if (poparam.allowed_data.indexOf(value) < 0){ return gpgme_error('PARAM_WRONG'); } } _msg[param] = value; return true; }; /** * Check if the message has the minimum requirements to be sent, that is * all 'required' parameters according to {@link permittedOperations}. * @returns {Boolean} true if message is complete. */ this.isComplete = function(){ if (!_msg.op){ return false; } let reqParams = Object.keys( permittedOperations[_msg.op].required); let msg_params = Object.keys(_msg); for (let i=0; i < reqParams.length; i++){ if (msg_params.indexOf(reqParams[i]) < 0){ return false; } } return true; }; /** * Sends the Message via nativeMessaging and resolves with the answer. * @returns {Promise} * @async */ this.post = function(){ let me = this; return new Promise(function(resolve, reject) { if (me.isComplete() === true) { - let conn = new Connection; + let conn = Object.freeze(new Connection); conn.post(me).then(function(response) { resolve(response); }, function(reason) { reject(reason); }); } else { reject(gpgme_error('MSG_INCOMPLETE')); } }); }; } /** * Returns the prepared message with parameters and completeness checked * @returns {Object|null} Object to be posted to gnupg, or null if * incomplete */ get message(){ if (this.isComplete() === true){ return this.getMsg(); } else { return null; } } get operation(){ return this.getOperation(); } get chunksize(){ return this.getChunksize(); } } diff --git a/lang/js/src/Signature.js b/lang/js/src/Signature.js index 0ee58e94..55131b01 100644 --- a/lang/js/src/Signature.js +++ b/lang/js/src/Signature.js @@ -1,233 +1,233 @@ /* 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 */ import { gpgme_error } from './Errors'; /** * Validates an object containing a signature, as sent by the nativeMessaging * interface * @param {Object} sigObject Object as returned by gpgme-json. The definition * of the expected values are to be found in {@link expKeys}, {@link expSum}, * {@link expNote}. * @returns {GPGME_Signature|GPGME_Error} Signature Object */ export function createSignature(sigObject){ if ( typeof(sigObject) !=='object' || !sigObject.hasOwnProperty('summary') || !sigObject.hasOwnProperty('fingerprint') || !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); + return Object.freeze(new GPGME_Signature(sigObject)); } /** * Representing the details of a signature. The full details as given by * gpgme-json can be read from the _rawSigObject. * * Note to reviewers: This class should be read only except via * {@link createSignature} * @protected * @class */ class GPGME_Signature { constructor(sigObject){ let _rawSigObject = sigObject; this.getFingerprint = function(){ if (!_rawSigObject.fingerprint){ return gpgme_error('SIG_WRONG'); } else { return _rawSigObject.fingerprint; } }; /** * The expiration of this Signature as Javascript date, or null if * signature does not expire * @returns {Date | null} */ this.getExpiration = function(){ if (!_rawSigObject.exp_timestamp){ return null; } return new Date(_rawSigObject.exp_timestamp* 1000); }; /** * The creation date of this Signature in Javascript Date * @returns {Date} */ this.getTimestamp= function (){ return new Date(_rawSigObject.timestamp * 1000); }; /** * The overall validity of the key. If false, errorDetails may contain * additional information. */ this.getValid= function() { if (_rawSigObject.summary.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 */ this.getErrorDetails = function (){ 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 ( _rawSigObject.hasOwnProperty(properties[i]) ){ result[properties[i]] = _rawSigObject[properties[i]]; } } return result; }; } /** * Convenience getter for {@link getFingerprint} */ get fingerprint(){ return this.getFingerprint(); } /** * Convenience getter for {@link getExpiration} */ get expiration(){ return this.getExpiration(); } /** * Convenience getter for {@link getTimeStamp} */ get timestamp(){ return this.getTimestamp(); } /** * Convenience getter for {@link getValid} */ get valid(){ return this.getValid(); } /** * Convenience getter for {@link getErrorDetails} */ get errorDetails(){ return this.getErrorDetails(); } } /** * 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', 'sigsum': 'object' }; /** * 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 720490d6..9154979d 100644 --- a/lang/js/src/gpgmejs.js +++ b/lang/js/src/gpgmejs.js @@ -1,395 +1,392 @@ /* 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 */ import { GPGME_Message, createMessage } from './Message'; import { toKeyIdArray } from './Helpers'; import { gpgme_error } from './Errors'; import { GPGME_Keyring } from './Keyring'; import { createSignature } from './Signature'; /** * @typedef {Object} decrypt_result * @property {String} data The decrypted data * @property {Boolean} base64 indicating whether data is base64 encoded. * @property {Boolean} is_mime (optional) the data claims to be a MIME * object. * @property {String} file_name (optional) the original file name * @property {signatureDetails} signatures Verification details for * signatures */ /** * @typedef {Object} signatureDetails * @property {Boolean} all_valid Summary if all signatures are fully valid * @property {Number} count Number of signatures found * @property {Number} failures Number of invalid signatures * @property {Array} signatures.good All valid signatures * @property {Array} signatures.bad All invalid signatures */ /** * @typedef {Object} encrypt_result The result of an encrypt operation * @property {String} data The encrypted message * @property {Boolean} base64 Indicating whether data is base64 encoded. */ /** * @typedef { GPGME_Key | String | Object } inputKeys * Accepts different identifiers of a gnupg Key that can be parsed by * {@link toKeyIdArray}. Expected inputs are: One or an array of * GPGME_Keys; one or an array of fingerprint strings; one or an array of * openpgpjs Key objects. */ /** * @typedef {Object} signResult The result of a signing operation * @property {String} data The resulting data. Includes the signature in * clearsign mode * @property {String} signature The detached signature (if in detached mode) */ /** @typedef {Object} verifyResult The result of a verification * @property {Boolean} data: The verified data * @property {Boolean} is_mime (optional) the data claims to be a MIME * object. * @property {String} file_name (optional) the original file name * @property {signatureDetails} signatures Verification details for * signatures */ /** * The main entry point for gpgme.js. * @class */ export class GpgME { constructor(){ let _Keyring = null; /** * Sets a new Keyring to be used * @param {GPGME_Keyring} keyring */ this.setKeyring = function(keyring){ if (keyring && keyring instanceof GPGME_Keyring){ _Keyring = keyring; } }; /** * Accesses the {@link GPGME_Keyring}. */ this.getKeyring = function(){ if (!_Keyring){ - _Keyring = new GPGME_Keyring; + _Keyring = Object.freeze(new GPGME_Keyring); } return _Keyring; }; /** * Encrypt (and optionally sign) data * @param {String|Object} data text/data to be encrypted as String. Also * accepts Objects with a getText method * @param {inputKeys} publicKeys * Keys used to encrypt the message * @param {inputKeys} secretKeys (optional) Keys used to sign the * message. If Keys are present, the operation requested is assumed * to be 'encrypt and sign' * @param {Boolean} base64 (optional) The data will be interpreted as * base64 encoded data. * @param {Boolean} armor (optional) Request the output as armored * block. * @param {Boolean} wildcard (optional) If true, recipient information * will not be added to the message. * @param {Object} additional use additional valid gpg options as * defined in {@link permittedOperations} * @returns {Promise} Object containing the encrypted * message and additional info. * @async */ this.encrypt = function (data, publicKeys, secretKeys, base64=false, armor=true, wildcard=false, additional = {} ){ let msg = createMessage('encrypt'); if (msg instanceof Error){ return Promise.reject(msg); } msg.setParameter('armor', armor); msg.setParameter('always-trust', true); if (base64 === true) { msg.setParameter('base64', true); } let pubkeys = toKeyIdArray(publicKeys); msg.setParameter('keys', pubkeys); let sigkeys = toKeyIdArray(secretKeys); if (sigkeys.length > 0) { msg.setParameter('signing_keys', sigkeys); } putData(msg, data); if (wildcard === true){ msg.setParameter('throw-keyids', true); } if (additional){ let additional_Keys = Object.keys(additional); for (let k = 0; k < additional_Keys.length; k++) { msg.setParameter(additional_Keys[k], additional[additional_Keys[k]]); } } if (msg.isComplete() === true){ return msg.post(); } else { return Promise.reject(gpgme_error('MSG_INCOMPLETE')); } }; /** * Decrypts a Message * @param {String|Object} data text/data to be decrypted. Accepts * Strings and Objects with a getText method * @param {Boolean} base64 (optional) false if the data is an armored * block, true if it is base64 encoded binary data * @returns {Promise} Decrypted Message and information * @async */ this.decrypt = function (data, base64=false){ if (data === undefined){ return Promise.reject(gpgme_error('MSG_EMPTY')); } let msg = createMessage('decrypt'); if (msg instanceof Error){ return Promise.reject(msg); } if (base64 === true){ msg.setParameter('base64', true); } putData(msg, data); 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); }); }); }; /** * Sign a Message * @param {String|Object} data text/data to be signed. Accepts Strings * and Objects with a getText method. * @param {inputKeys} keys The key/keys to use for signing * @param {String} mode The signing mode. Currently supported: * 'clearsign':The Message is embedded into the signature; * 'detached': The signature is stored separately * @param {Boolean} base64 input is considered base64 * @returns {Promise} * @async */ this.sign = function (data, keys, mode='clearsign', base64=false) { if (data === undefined){ return Promise.reject(gpgme_error('MSG_EMPTY')); } let key_arr = toKeyIdArray(keys); if (key_arr.length === 0){ return Promise.reject(gpgme_error('MSG_NO_KEYS')); } let msg = createMessage('sign'); msg.setParameter('keys', key_arr); if (base64 === true){ msg.setParameter('base64', true); } msg.setParameter('mode', mode); putData(msg, data); return new Promise(function(resolve,reject) { if (mode ==='detached'){ - msg.expect= 'base64'; + msg.setExpect('base64'); } msg.post().then( function(message) { if (mode === 'clearsign'){ resolve({ data: message.data} ); } else if (mode === 'detached') { resolve({ data: data, signature: message.data }); } }, function(error){ reject(error); }); }); }; /** * 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 * @returns {Promise} *@async */ this.verify= function (data, signature, base64 = false){ let msg = createMessage('verify'); let dt = 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 || !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); }); }); }; } /** * setter for {@link setKeyring}. * @param {GPGME_Keyring} keyring A Keyring to use */ set Keyring(keyring){ this.setKeyring(keyring); } /** * Accesses the {@link GPGME_Keyring}. */ get Keyring(){ - if (!this._Keyring){ - this._Keyring = new GPGME_Keyring; - } - return this._Keyring; + return this.getKeyring(); } } /** * Sets the data of the message, setting flags according on the data type * @param {GPGME_Message} message The message where this data will be set * @param { String| Object } data The data to enter. Expects either a string of * data, or an object with a getText method * @returns {undefined| GPGME_Error} Error if not successful, nothing otherwise * @private */ function putData(message, data){ if (!message || !(message instanceof GPGME_Message)) { return gpgme_error('PARAM_WRONG'); } if (!data){ return gpgme_error('PARAM_WRONG'); } else if (typeof(data) === 'string') { message.setParameter('data', data); } else if ( typeof(data) === 'object' && typeof(data.getText) === 'function' ){ let txt = data.getText(); if (typeof(txt) === 'string'){ message.setParameter('data', txt); } else { return gpgme_error('PARAM_WRONG'); } } else { return gpgme_error('PARAM_WRONG'); } } /** * Parses, validates and converts incoming objects into signatures. * @param {Array} sigs * @returns {signatureDetails} Details about the signatures */ 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(sigObj); } 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 diff --git a/lang/js/src/index.js b/lang/js/src/index.js index dc613fc7..2fed95f9 100644 --- a/lang/js/src/index.js +++ b/lang/js/src/index.js @@ -1,53 +1,53 @@ /* 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 */ import { GpgME } from './gpgmejs'; import { gpgme_error } from './Errors'; import { Connection } from './Connection'; /** * Initializes gpgme.js by testing the nativeMessaging connection once. * @returns {Promise | GPGME_Error} * * @async */ function init(){ return new Promise(function(resolve, reject){ - let connection = new Connection; + let connection = Object.freeze(new Connection); connection.checkConnection(false).then( function(result){ if (result === true) { - resolve(new GpgME()); + resolve(Object.freeze(new GpgME())); } else { reject(gpgme_error('CONN_NO_CONNECT')); } }, function(){ //unspecific connection error. Should not happen reject(gpgme_error('CONN_NO_CONNECT')); }); }); } export default { init: init }; \ No newline at end of file diff --git a/lang/js/unittests.js b/lang/js/unittests.js index 6228993b..3304b1eb 100644 --- a/lang/js/unittests.js +++ b/lang/js/unittests.js @@ -1,393 +1,409 @@ /* 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+ */ import './node_modules/mocha/mocha'; /*global mocha, it, describe*/ import './node_modules/chai/chai';/*global chai*/ import { helper_params as hp } from './unittest_inputvalues'; import { message_params as mp } from './unittest_inputvalues'; import { whatever_params as wp } from './unittest_inputvalues'; import { key_params as kp } from './unittest_inputvalues'; import { Connection } from './src/Connection'; import { gpgme_error } from './src/Errors'; import { toKeyIdArray , isFingerprint } from './src/Helpers'; import { GPGME_Key , createKey } from './src/Key'; import { GPGME_Keyring } from './src/Keyring'; import {GPGME_Message, createMessage} from './src/Message'; mocha.setup('bdd'); const expect = chai.expect; chai.config.includeStack = true; function unittests (){ describe('Connection testing', function(){ it('Connecting', function(done) { let conn0 = new Connection; conn0.checkConnection().then(function(answer) { expect(answer).to.not.be.empty; expect(answer.gpgme).to.not.be.undefined; expect(answer.gpgme).to.be.a('string'); expect(answer.info).to.be.an('Array'); expect(conn0.disconnect).to.be.a('function'); expect(conn0.post).to.be.a('function'); done(); }); }); it('Disconnecting', function(done) { let conn0 = new Connection; conn0.checkConnection(false).then(function(answer) { expect(answer).to.be.true; conn0.disconnect(); conn0.checkConnection(false).then(function(result) { expect(result).to.be.false; done(); }); }); }); }); describe('Error Object handling', function(){ // TODO: new GPGME_Error codes it('check the Timeout error', function(){ let test0 = gpgme_error('CONN_TIMEOUT'); expect(test0).to.be.an.instanceof(Error); expect(test0.code).to.equal('CONN_TIMEOUT'); }); it('Error Object returns generic code if code is not listed', function(){ let test0 = gpgme_error(hp.invalidErrorCode); expect(test0).to.be.an.instanceof(Error); expect(test0.code).to.equal('GENERIC_ERROR'); } ); it('Warnings like PARAM_IGNORED should not return errors', function(){ let test0 = gpgme_error('PARAM_IGNORED'); expect(test0).to.be.null; }); }); describe('Fingerprint checking', function(){ it('isFingerprint(): valid Fingerprint', function(){ let test0 = isFingerprint(hp.validFingerprint); expect(test0).to.be.true; }); it('isFingerprint(): invalid Fingerprints', function(){ for (let i=0; i < hp.invalidFingerprints.length; i++){ let test0 = isFingerprint(hp.invalidFingerprints[i]); expect(test0).to.be.false; } }); }); describe('toKeyIdArray() (converting input to fingerprint)', function(){ it('Correct fingerprint string', function(){ let test0 = toKeyIdArray(hp.validFingerprint); expect(test0).to.be.an('array'); expect(test0).to.include(hp.validFingerprint); }); it('correct GPGME_Key', function(){ expect(hp.validGPGME_Key).to.be.an.instanceof(GPGME_Key); let test0 = toKeyIdArray(hp.validGPGME_Key); expect(test0).to.be.an('array'); expect(test0).to.include(hp.validGPGME_Key.fingerprint); }); it('openpgpjs-like object', function(){ let test0 = toKeyIdArray(hp.valid_openpgplike); expect(test0).to.be.an('array').with.lengthOf(1); expect(test0).to.include( hp.valid_openpgplike.primaryKey.getFingerprint()); }); it('Array of valid inputs', function(){ let test0 = toKeyIdArray(hp.validKeys); expect(test0).to.be.an('array'); expect(test0).to.have.lengthOf(hp.validKeys.length); }); it('Incorrect inputs', function(){ it('valid Long ID', function(){ let test0 = toKeyIdArray(hp.validLongId); expect(test0).to.be.empty; }); it('invalidFingerprint', function(){ let test0 = toKeyIdArray(hp.invalidFingerprint); expect(test0).to.be.empty; }); it('invalidKeyArray', function(){ let test0 = toKeyIdArray(hp.invalidKeyArray); expect(test0).to.be.empty; }); it('Partially invalid array', function(){ let test0 = toKeyIdArray(hp.invalidKeyArray_OneBad); expect(test0).to.be.an('array'); expect(test0).to.have.lengthOf( hp.invalidKeyArray_OneBad.length - 1); }); }); }); describe('GPGME_Key', function(){ it('correct Key initialization', function(){ let key = createKey(kp.validKeyFingerprint); expect(key).to.be.an.instanceof(GPGME_Key); }); it('Key has data after a first refresh', function(done) { let key = createKey(kp.validKeyFingerprint); key.refreshKey().then(function(key2){ expect(key2).to.be.an.instanceof(GPGME_Key); expect(key2.get).to.be.a('function'); for (let i=0; i < kp.validKeyProperties.length; i++) { let prop = key2.get(kp.validKeyProperties[i]); expect(prop).to.not.be.undefined; expect(prop).to.be.a('boolean'); } expect(isFingerprint(key2.get('fingerprint'))).to.be.true; expect( key2.get('fingerprint')).to.equal(kp.validKeyFingerprint); expect( key2.get('fingerprint')).to.equal(key.fingerprint); done(); }); }); it('Non-cached key async data retrieval', function (done){ let key = createKey(kp.validKeyFingerprint, true); key.get('can_authenticate').then(function(result){ expect(result).to.be.a('boolean'); done(); }); }); it('Non-cached key async armored Key', function (done){ let key = createKey(kp.validKeyFingerprint, true); key.get('armored').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, true); key.get('hasSecret').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, true); expect(key).to.be.an.instanceof(GPGME_Key); key.get('hasSecret').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); key.refreshKey().then(function(){}, function(error){ expect(error).to.be.an.instanceof(Error); expect(error.code).to.equal('KEY_NOKEY'); done(); }); }); it('createKey returns error if parameters are wrong', function(){ for (let i=0; i< 4; i++){ let key0 = createKey(wp.four_invalid_params[i]); expect(key0).to.be.an.instanceof(Error); expect(key0.code).to.equal('PARAM_WRONG'); } }); it('malformed GPGME_Key cannot be used', function(){ for (let i=0; i < 4; i++){ let key = new GPGME_Key(wp.four_invalid_params[i]); expect(key.fingerprint).to.be.an.instanceof(Error); expect(key.fingerprint.code).to.equal('KEY_INVALID'); } }); + + it('Overwriting getFingerprint does not work', function(){ + const evilFunction = function(){ + return 'bad Data'; + }; + let key = createKey(kp.validKeyFingerprint, true); + expect(key.fingerprint).to.equal(kp.validKeyFingerprint); + try { + key.getFingerprint = evilFunction; + } + catch(e) { + expect(e).to.be.an.instanceof(TypeError); + } + expect(key.fingerprint).to.equal(kp.validKeyFingerprint); + expect(key.getFingerprint).to.not.equal(evilFunction); + }); // TODO: tests for subkeys // TODO: tests for userids // TODO: some invalid tests for key/keyring }); describe('GPGME_Keyring', function(){ it('correct Keyring initialization', function(){ let keyring = new GPGME_Keyring; expect(keyring).to.be.an.instanceof(GPGME_Keyring); expect(keyring.getKeys).to.be.a('function'); }); it('Loading Keys from Keyring, to be used synchronously', function(done){ let keyring = new GPGME_Keyring; 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('hasSecret')).to.be.a('boolean'); // expect(result[0].get('armored')).to.include( // '-----END PGP PUBLIC KEY BLOCK-----'); done(); }); } ); it('Loading specific Key from Keyring, to be used synchronously', function(done){ let keyring = new GPGME_Keyring; keyring.getKeys(kp.validKeyFingerprint, true).then( function(result){ expect(result).to.be.an('array'); expect(result[0]).to.be.an.instanceof(GPGME_Key); expect(result[0].get('hasSecret')).to.be.a('boolean'); // TODO: preparing sync for armored is still in discussion // expect(result[0].get('armored')).to.be.a('string'); // expect(result[0].get('armored')).to.include( // '-----END PGP PUBLIC KEY BLOCK-----'); done(); } ); } ); it('Querying non-existing Key from Keyring', function(done){ let keyring = new GPGME_Keyring; keyring.getKeys(kp.invalidKeyFingerprint, true).then( function(result){ expect(result).to.be.an('array'); expect(result.length).to.equal(0); done(); } ); }); }); describe('GPGME_Message', function(){ it('creating encrypt Message', function(){ let test0 = createMessage('encrypt'); expect(test0).to.be.an.instanceof(GPGME_Message); expect(test0.isComplete()).to.be.false; }); it('Message is complete after setting mandatory data', function(){ let test0 = createMessage('encrypt'); test0.setParameter('data', mp.valid_encrypt_data); test0.setParameter('keys', hp.validFingerprints); expect(test0.isComplete()).to.be.true; }); it('Message is not complete after mandatory data is empty', function(){ let test0 = createMessage('encrypt'); test0.setParameter('data', ''); test0.setParameter('keys', hp.validFingerprints); expect(test0.isComplete()).to.be.false; }); it('Complete Message contains the data that was set', function(){ let test0 = createMessage('encrypt'); test0.setParameter('data', mp.valid_encrypt_data); test0.setParameter('keys', hp.validFingerprints); expect(test0.message).to.not.be.null; expect(test0.message).to.have.keys('op', 'data', 'keys', 'chunksize'); expect(test0.message.op).to.equal('encrypt'); expect(test0.message.data).to.equal( mp.valid_encrypt_data); }); it ('Not accepting non-allowed operation', function(){ let test0 = createMessage(mp.invalid_op_action); expect(test0).to.be.an.instanceof(Error); expect(test0.code).to.equal('MSG_WRONG_OP'); }); it('Not accepting wrong parameter type', function(){ let test0 = createMessage(mp.invalid_op_type); expect(test0).to.be.an.instanceof(Error); expect(test0.code).to.equal('PARAM_WRONG'); }); it('Not accepting wrong parameter name', function(){ let test0 = createMessage(mp.invalid_param_test.valid_op); for (let i=0; i < mp.invalid_param_test.invalid_param_names.length; i++){ let ret = test0.setParameter( mp.invalid_param_test.invalid_param_names[i], 'Somevalue'); expect(ret).to.be.an.instanceof(Error); expect(ret.code).to.equal('PARAM_WRONG'); } }); it('Not accepting wrong parameter value', function(){ let test0 = createMessage(mp.invalid_param_test.valid_op); for (let j=0; j < mp.invalid_param_test.invalid_values_0.length; j++){ let ret = test0.setParameter( mp.invalid_param_test.validparam_name_0, mp.invalid_param_test.invalid_values_0[j]); expect(ret).to.be.an.instanceof(Error); expect(ret.code).to.equal('PARAM_WRONG'); } }); }); } export default {unittests}; \ No newline at end of file