diff --git a/lang/js/src/Connection.js b/lang/js/src/Connection.js index 07df5def..3b442622 100644 --- a/lang/js/src/Connection.js +++ b/lang/js/src/Connection.js @@ -1,257 +1,263 @@ /* 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+ */ /** * A connection port will be opened for each communication between gpgmejs and * gnupg. It should be alive as long as there are additional messages to be * expected. */ import { permittedOperations } from './permittedOperations' import { gpgme_error } from "./Errors" import { GPGME_Message, createMessage } from "./Message"; /** * A Connection handles the nativeMessaging interaction. */ export class Connection{ constructor(){ this.connect(); } /** * Retrieves the information about the backend. * @param {Boolean} details (optional) If set to false, the promise will * just return a connection status * @returns {Promise} * {String} The property 'gpgme': Version number of gpgme * {Array} 'info' Further information about the backends. * Example: * "protocol": "OpenPGP", * "fname": "/usr/bin/gpg", * "version": "2.2.6", * "req_version": "1.4.0", * "homedir": "default" */ checkConnection(details = true){ if (details === true) { return this.post(createMessage('version')); } else { let me = this; return new Promise(function(resolve,reject) { Promise.race([ me.post(createMessage('version')), new Promise(function(resolve, reject){ setTimeout(function(){ reject(gpgme_error('CONN_TIMEOUT')); }, 500); }) ]).then(function(result){ resolve(true); }, function(reject){ resolve(false); }); }); } } /** * Immediately closes the open port. */ disconnect() { if (this._connection){ this._connection.disconnect(); this._connection = null; } } /** * Opens a nativeMessaging port. */ connect(){ if (!this._connection){ this._connection = chrome.runtime.connectNative('gpgmejson'); } } /** * Sends a message and resolves with the answer. * @param {GPGME_Message} message * @returns {Promise} the gnupg answer, or rejection with error * information. */ post(message){ if (!this._connection) { } if (!message || !message instanceof GPGME_Message){ return Promise.reject(gpgme_error('PARAM_WRONG'), message); } if (message.isComplete !== true){ return Promise.reject(gpgme_error('MSG_INCOMPLETE')); } let me = this; return new Promise(function(resolve, reject){ let answer = new Answer(message); let listener = function(msg) { if (!msg){ me._connection.onMessage.removeListener(listener) reject(gpgme_error('CONN_EMPTY_GPG_ANSWER')); } else if (msg.type === "error"){ me._connection.onMessage.removeListener(listener); reject(gpgme_error('GNUPG_ERROR', msg.msg)); } else { let answer_result = answer.add(msg); if (answer_result !== true){ me._connection.onMessage.removeListener(listener); reject(answer_result); } if (msg.more === true){ me._connection.postMessage({'op': 'getmore'}); } else { me._connection.onMessage.removeListener(listener) resolve(answer.message); } } }; me._connection.onMessage.addListener(listener); if (permittedOperations[message.operation].pinentry){ return me._connection.postMessage(message.message); } else { return Promise.race([ me._connection.postMessage(message.message), function(resolve, reject){ setTimeout(function(){ reject(gpgme_error('CONN_TIMEOUT')); }, 5000); }]).then(function(result){ return result; }, function(reject){ if(!reject instanceof Error) { return gpgme_error('GNUPG_ERROR', reject); } else { return reject; } }); } }); } }; /** * 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 returning messages */ class Answer{ constructor(message){ this.operation = message.operation; this.expected = message.expected; } /** * Add the information to the answer * @param {Object} msg The message as received with nativeMessaging * returns true if successfull, gpgme_error otherwise */ add(msg){ if (this._response === undefined){ this._response = {}; } let messageKeys = Object.keys(msg); let poa = permittedOperations[this.operation].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 ( msg.type !== 'error' && poa.type.indexOf(msg.type) < 0){ return gpgme_error('CONN_UNEXPECTED_ANSWER'); } break; case 'more': break; default: //data should be concatenated if (poa.data.indexOf(key) >= 0){ if (!this._response.hasOwnProperty(key)){ this._response[key] = ''; } this._response[key] += msg[key]; } //params should not change through the message else if (poa.params.indexOf(key) >= 0){ if (!this._response.hasOwnProperty(key)){ this._response[key] = msg[key]; } else if (this._response[key] !== msg[key]){ return gpgme_error('CONN_UNEXPECTED_ANSWER',msg[key]); } } //infos may be json objects etc. Not yet defined. // Pushing them into arrays for now else if (poa.infos.indexOf(key) >= 0){ if (!this._response.hasOwnProperty(key)){ this._response[key] = []; } - this._response[key].push(msg[key]); + if (Array.isArray(msg[key])) { + for (let i=0; i< msg[key].length; i++) { + this._response[key].push(msg[key][i]); + } + } else { + this._response[key].push(msg[key][i]); + } } else { return gpgme_error('CONN_UNEXPECTED_ANSWER'); } break; } } return true; } /** * @returns {Object} the assembled message, original data assumed to be * (javascript-) strings */ get message(){ let keys = Object.keys(this._response); let msg = {}; let poa = permittedOperations[this.operation].answer; for (let i=0; i < keys.length; i++) { if (poa.data.indexOf(keys[i]) >= 0 && this._response.base64 === true ) { msg[keys[i]] = atob(this._response[keys[i]]); if (this.expected === 'base64'){ msg[keys[i]] = this._response[keys[i]]; } else { msg[keys[i]] = decodeURIComponent( atob(this._response[keys[i]]).split('').map(function(c) { return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); }).join('')); } } else { msg[keys[i]] = this._response[keys[i]]; } } return msg; } } diff --git a/lang/js/src/Errors.js b/lang/js/src/Errors.js index 7e98f319..fa8a4efe 100644 --- a/lang/js/src/Errors.js +++ b/lang/js/src/Errors.js @@ -1,125 +1,129 @@ /* 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+ */ 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' + }, // 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 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' */ export function gpgme_error(code = 'GENERIC_ERROR', info){ if (err_list.hasOwnProperty(code)){ if (err_list[code].type === 'error'){ return new GPGME_Error(code); } if (err_list[code].type === 'warning'){ console.warn(code + ': ' + err_list[code].msg); } return null; } else if (code === 'GNUPG_ERROR'){ return new GPGME_Error(code, info); } else { return new GPGME_Error('GENERIC_ERROR'); } } class GPGME_Error extends Error{ constructor(code, msg=''){ if (code === 'GNUPG_ERROR' && typeof(msg) === 'string'){ super(msg); } else if (err_list.hasOwnProperty(code)){ super(err_list[code].msg); } else { super(err_list['GENERIC_ERROR'].msg); } this.code = code || 'GENERIC_ERROR'; } set code(value){ this._code = value; } get code(){ return this._code; } } \ No newline at end of file diff --git a/lang/js/src/Helpers.js b/lang/js/src/Helpers.js index fd0e7200..b26f40fb 100644 --- a/lang/js/src/Helpers.js +++ b/lang/js/src/Helpers.js @@ -1,103 +1,104 @@ /* 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 { gpgme_error } from "./Errors"; import { GPGME_Key } from "./Key"; /** * Tries to return an array of fingerprints, either from input fingerprints or * from Key objects * @param {Key |Array| GPGME_Key | Array|String|Array} input * @returns {Array} Array of fingerprints. */ export function toKeyIdArray(input){ if (!input){ gpgme_error('MSG_NO_KEYS'); return []; } if (!Array.isArray(input)){ input = [input]; } 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 { gpgme_error('MSG_NOT_A_FPR'); } } 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 { gpgme_error('MSG_NOT_A_FPR'); } } else { return gpgme_error('PARAM_WRONG'); } } if (result.length === 0){ gpgme_error('MSG_NO_KEYS'); return []; } else { return result; } }; /** * check if values are valid hexadecimal values of a specified length * @param {*} key input value. * @param {int} len the expected length of the value */ function hextest(key, len){ if (!key || typeof(key) !== "string"){ return false; } if (key.length !== len){ return false; } let regexp= /^[0-9a-fA-F]*$/i; return regexp.test(key); }; /** * check if the input is a valid Hex string with a length of 40 */ export function isFingerprint(string){ return hextest(string, 40); }; + /** - * TODO no usage; check if the input is a valid Hex string with a length of 16 + * check if the input is a valid Hex string with a length of 16 */ -function isLongId(string){ +export function isLongId(string){ return hextest(string, 16); }; // 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 index 075a190e..7d3d82b1 100644 --- a/lang/js/src/Key.js +++ b/lang/js/src/Key.js @@ -1,244 +1,427 @@ /* 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+ */ /** * 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 { isFingerprint, isLongId } from './Helpers' import { gpgme_error } from './Errors' import { createMessage } from './Message'; import { permittedOperations } from './permittedOperations'; import { Connection } from './Connection'; - +/** + * Validates the fingerprint, and checks for tha availability of a connection. + * If both are available, a Key will be returned. + * @param {String} fingerprint + * @param {Object} parent Either a Connection, or the invoking object with a + * Connection (e.g. Keyring) + */ export function createKey(fingerprint, parent){ if (!isFingerprint(fingerprint)){ return gpgme_error('PARAM_WRONG'); } if ( parent instanceof Connection){ return new GPGME_Key(fingerprint, parent); } else if ( parent.hasOwnProperty('connection') && parent.connection instanceof Connection){ return new GPGME_Key(fingerprint, parent.connection); } else { return gpgme_error('PARAM_WRONG'); } } +/** + * Representing the Keys as stored in GPG + */ export class GPGME_Key { constructor(fingerprint, connection){ this.fingerprint = fingerprint; this.connection = connection; } set connection(conn){ if (this._connection instanceof Connection) { gpgme_error('CONN_ALREADY_CONNECTED'); } else if (conn instanceof Connection ) { this._connection = conn; } } get connection(){ - if (!this._fingerprint){ + if (!this._data.fingerprint){ return gpgme_error('KEY_INVALID'); } if (!this._connection instanceof Connection){ return gpgme_error('CONN_NO_CONNECT'); } else { return this._connection; } } set fingerprint(fpr){ - if (isFingerprint(fpr) === true && !this._fingerprint){ - this._fingerprint = fpr; + if (isFingerprint(fpr) === true) { + if (this._data === undefined) { + this._data = {fingerprint: fpr}; + } else { + if (this._data.fingerprint === undefined){ + this._data.fingerprint = fpr; + } + } } } get fingerprint(){ - if (!this._fingerprint){ + if (!this._data || !this._data.fingerprint){ return gpgme_error('KEY_INVALID'); } - return this._fingerprint; + return this._data.fingerprint; } /** - * hasSecret returns true if a secret subkey is included in this Key + * + * @param {Object} data Bulk set data for this key, with the Object as sent + * by gpgme-json. + * @returns {GPGME_Key|GPGME_Error} The Key object itself after values have + * been set */ - get hasSecret(){ - return this.checkKey('secret'); - } - - get isRevoked(){ - return this.checkKey('revoked'); - } - - get isExpired(){ - return this.checkKey('expired'); - } - - get isDisabled(){ - return this.checkKey('disabled'); - } - - get isInvalid(){ - return this.checkKey('invalid'); - } - - get canEncrypt(){ - return this.checkKey('can_encrypt'); - } + setKeydata(data){ + if (this._data === undefined) { + this._data = {}; + } + if ( + typeof(data) !== 'object') { + return gpgme_error('KEY_INVALID'); + } + if (!this._data.fingerprint && isFingerprint(data.fingerprint)){ + if (data.fingerprint !== this.fingerprint){ + return gpgme_error('KEY_INVALID'); + } + this._data.fingerprint = data.fingerprint; + } else if (this._data.fingerprint !== data.fingerprint){ + return gpgme_error('KEY_INVALID'); + } - get canSign(){ - return this.checkKey('can_sign'); - } + let booleans = ['expired', 'disabled','invalid','can_encrypt', + 'can_sign','can_certify','can_authenticate','secret', + 'is_qualified']; + for (let b =0; b < booleans.length; b++) { + if ( + !data.hasOwnProperty(booleans[b]) || + typeof(data[booleans[b]]) !== 'boolean' + ){ + return gpgme_error('KEY_INVALID'); + } + this._data[booleans[b]] = data[booleans[b]]; + } + if (typeof(data.protocol) !== 'string'){ + return gpgme_error('KEY_INVALID'); + } + // TODO check valid protocols? + this._data.protocol = data.protocol; - get canCertify(){ - return this.checkKey('can_certify'); - } + if (typeof(data.owner_trust) !== 'string'){ + return gpgme_error('KEY_INVALID'); + } + // TODO check valid values? + this._data.owner_trust = data.owner_trust; - get canAuthenticate(){ - return this.checkKey('can_authenticate'); - } + // TODO: what about origin ? + if (!Number.isInteger(data.last_update)){ + return gpgme_error('KEY_INVALID'); + } + this._data.last_update = data.last_update; - get isQualified(){ - return this.checkKey('is_qualified'); - } + this._data.subkeys = []; + if (data.hasOwnProperty('subkeys')){ + if (!Array.isArray(data.subkeys)){ + return gpgme_error('KEY_INVALID'); + } + for (let i=0; i< data.subkeys.length; i++) { + this._data.subkeys.push( + new GPGME_Subkey(data.subkeys[i])); + } + } - get armored(){ - let msg = createMessage ('export_key'); - msg.setParameter('armor', true); - if (msg instanceof Error){ - return gpgme_error('KEY_INVALID'); + this._data.userids = []; + if (data.hasOwnProperty('userids')){ + if (!Array.isArray(data.userids)){ + return gpgme_error('KEY_INVALID'); + } + for (let i=0; i< data.userids.length; i++) { + this._data.userids.push( + new GPGME_UserId(data.userids[i])); + } } - this.connection.post(msg).then(function(result){ - return result.data; - }); - // TODO return value not yet checked. Should result in an armored block - // in correct encoding + return this; } /** - * TODO returns true if this is the default key used to sign + * Query any property of the Key list + * (TODO: armor not in here, might be unexpected) + * @param {String} property Key property to be retreived + * @param {*} cached (optional) if false, the data will be directly queried + * from gnupg. + * @returns {*|Promise<*>} the value, or if not cached, a Promise + * resolving on the value */ - get isDefault(){ - throw('NOT_YET_IMPLEMENTED'); + get(property, cached=true) { + if (cached === false) { + let me = this; + return new Promise(function(resolve, reject) { + me.refreshKey().then(function(key){ + resolve(key.get(property, true)); + }, function(error){ + reject(error); + }); + }); + } else { + if (!this._data.hasOwnProperty(property)){ + return gpgme_error('PARAM_WRONG'); + } else { + return (this._data[property]); + } + } } /** - * get the Key's subkeys as GPGME_Key objects - * @returns {Array} + * Reloads the Key from gnupg */ - get subkeys(){ - return this.checkKey('subkeys').then(function(result){ - // TBD expecting a list of fingerprints - if (!Array.isArray(result)){ - result = [result]; + refreshKey() { + let me = this; + return new Promise(function(resolve, reject) { + if (!me._data.fingerprint){ + reject(gpgme_error('KEY_INVALID')); } - let resultset = []; - for (let i=0; i < result.length; i++){ - let subkey = new GPGME_Key(result[i], this.connection); - if (subkey instanceof GPGME_Key){ - resultset.push(subkey); + let msg = createMessage('keylist'); + msg.setParameter('sigs', true); + msg.setParameter('keys', me._data.fingerprint); + me.connection.post(msg).then(function(result){ + if (result.keys.length === 1){ + me.setKeydata(result.keys[0]); + resolve(me); + } else { + reject(gpgme_error('KEY_NOKEY')); } - } - return Promise.resolve(resultset); - }, function(error){ - //TODO this.checkKey fails + }, function (error) { + reject(gpgme_error('GNUPG_ERROR'), error); + }) }); } + //TODO: /** - * creation time stamp of the key - * @returns {Date|null} TBD + * Get the armored block of the non- secret parts of the Key. + * @returns {String} the armored Key block. + * Notice that this may be outdated cached info. Use the async getArmor if + * you need the most current info */ - get timestamp(){ - return this.checkKey('timestamp'); - //TODO GPGME: -1 if the timestamp is invalid, and 0 if it is not available. - } + // get armor(){ TODO } /** - * The expiration timestamp of this key TBD - * @returns {Date|null} TBD + * Query the armored block of the non- secret parts of the Key directly + * from gpg. + * Async, returns Promise */ - get expires(){ - return this.checkKey('expires'); - // TODO convert to Date; check for 0 + // getArmor(){ TODO } + // + + // get hasSecret(){TODO} // confusing difference to Key.get('secret')! + // getHasSecret(){TODO async version} +} + +/** + * The subkeys of a Key. Currently, they cannot be refreshed separately + */ +class GPGME_Subkey { + + constructor(data){ + let keys = Object.keys(data); + for (let i=0; i< keys.length; i++) { + this.setProperty(keys[i], data[keys[i]]); + } + } + + setProperty(property, value){ + if (!this._data){ + this._data = {}; + } + if (validSubKeyProperties.hasOwnProperty(property)){ + if (validSubKeyProperties[property](value) === true) { + this._data[property] = value; + } + } } /** - * getter name TBD - * @returns {String|Array} The user ids associated with this key + * + * @param {String} property Information to request + * @returns {String | Number} + * TODO: date properties are numbers with Date in seconds */ - get userIds(){ - return this.checkKey('uids'); + get(property) { + if (this._data.hasOwnProperty(property)){ + return (this._data[property]); + } + } +} + +class GPGME_UserId { + + constructor(data){ + let keys = Object.keys(data); + for (let i=0; i< keys.length; i++) { + this.setProperty(keys[i], data[keys[i]]); + } + } + + setProperty(property, value){ + if (!this._data){ + this._data = {}; + } + if (validUserIdProperties.hasOwnProperty(property)){ + if (validUserIdProperties[property](value) === true) { + this._data[property] = value; + } + } } /** - * @returns {String} The public key algorithm supported by this subkey + * + * @param {String} property Information to request + * @returns {String | Number} + * TODO: date properties are numbers with Date in seconds */ - get pubkey_algo(){ - return this.checkKey('pubkey_algo'); + get(property) { + if (this._data.hasOwnProperty(property)){ + return (this._data[property]); + } } +} - /** - * generic function to query gnupg information on a key. - * @param {*} property The gpgme-json property to check. - * TODO: check if Promise.then(return) - */ - checkKey(property){ - if (!this._fingerprint){ - return gpgme_error('KEY_INVALID'); +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 gpgme_error('NOT_YET_IMPLEMENTED'); - // TODO: async is not what is to be ecpected from Key information :( - if (!property || typeof(property) !== 'string' || - !permittedOperations['keyinfo'].hasOwnProperty(property)){ - return gpgme_error('PARAM_WRONG'); - } - let msg = createMessage ('keyinfo'); - if (msg instanceof Error){ - return gpgme_error('PARAM_WRONG'); - } - msg.setParameter('fingerprint', this.fingerprint); - this.connection.post(msg).then(function(result, error){ - if (error){ - return gpgme_error('GNUPG_ERROR',error.msg); - } else if (result.hasOwnProperty(property)){ - return result[property]; - } - else if (property == 'secret'){ - // TBD property undefined means "not true" in case of secret? - return false; - } else { - return gpgme_error('CONN_UNEXPECTED_ANSWER'); - } - }, function(error){ - return gpgme_error('GENERIC_ERROR'); - }); + 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); } -}; \ No newline at end of file +}; + +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); + } +} diff --git a/lang/js/src/permittedOperations.js b/lang/js/src/permittedOperations.js index aa02a8bc..42213ec3 100644 --- a/lang/js/src/permittedOperations.js +++ b/lang/js/src/permittedOperations.js @@ -1,259 +1,278 @@ /* 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+ */ /** * Definition of the possible interactions with gpgme-json. * operation: required: Array name The name of the property allowed: Array of allowed types. Currently accepted values: ['number', 'string', 'boolean', 'Uint8Array'] array_allowed: Boolean. If the value can be an array of the above allowed_data: If present, restricts to the given value optional: Array see 'required', with these parameters not being mandatory for a complete message pinentry: boolean If a pinentry dialog is expected, and a timeout of 5000 ms would be too short answer: type: The payload property of the answer. May be partial and in need of concatenation params: Array Information that do not change throughout the message infos: Array<*> arbitrary information that may result in a list } } */ export const permittedOperations = { encrypt: { required: { 'keys': { allowed: ['string'], array_allowed: true }, 'data': { allowed: ['string'] } }, optional: { 'protocol': { allowed: ['string'], allowed_data: ['cms', 'openpgp'] }, 'chunksize': { allowed: ['number'] }, 'base64': { allowed: ['boolean'] }, 'mime': { allowed: ['boolean'] }, 'armor': { allowed: ['boolean'] }, 'always-trust': { allowed: ['boolean'] }, 'no-encrypt-to': { allowed: ['string'], array_allowed: true }, 'no-compress': { allowed: ['boolean'] }, 'throw-keyids': { allowed: ['boolean'] }, 'want-address': { allowed: ['boolean'] }, 'wrap': { allowed: ['boolean'] }, }, answer: { type: ['ciphertext'], data: ['data'], params: ['base64'], infos: [] } }, decrypt: { pinentry: true, required: { 'data': { allowed: ['string'] } }, optional: { 'protocol': { allowed: ['string'], allowed_data: ['cms', 'openpgp'] }, 'chunksize': { allowed: ['number'], }, 'base64': { allowed: ['boolean'] } }, answer: { type: ['plaintext'], data: ['data'], params: ['base64', 'mime'], infos: [] // TODO pending. Info about signatures and validity //{ //signatures: [{ //Key : Fingerprint, //valid: // }] } }, sign: { pinentry: true, required: { 'data': { allowed: ['string']}, 'keys': { allowed: ['string'], array_allowed: true } }, optional: { 'protocol': { allowed: ['string'], allowed_data: ['cms', 'openpgp'] }, 'chunksize': { allowed: ['number'], }, 'sender': { allowed: ['string'], }, 'mode': { allowed: ['string'], allowed_data: ['detached', 'clearsign'] // TODO 'opaque' not used }, 'base64': { allowed: ['boolean'] }, 'armor': { allowed: ['boolean'] }, }, answer: { type: ['signature', 'ciphertext'], data: ['data'], // Unless armor mode is used a Base64 encoded binary // signature. In armor mode a string with an armored // OpenPGP or a PEM message. params: ['base64'] } }, - - /** TBD: querying the Key's information (keyinfo) - TBD name: { - required: { - 'fingerprint': { - allowed: ['string'] - }, - }, - answer: { - type: ['TBD'], - data: [], - params: ['hasSecret','isRevoked','isExpired','armored', - 'timestamp','expires','pubkey_algo'], - infos: ['subkeys', 'userIds'] - // {'hasSecret': , - // 'isRevoked': , - // 'isExpired': , - // 'armored': , // armored public Key block - // 'timestamp': , // - // 'expires': , - // 'pubkey_algo': TBD // TBD (optional?), - // 'userIds': Array, - // 'subkeys': Array Fingerprints of Subkeys - // } - }*/ - - /** - listkeys:{ - required: {}; + keylist:{ + required: {}, optional: { - 'with-secret':{ + 'protocol': { + allowed: ['string'], + allowed_data: ['cms', 'openpgp'] + }, + 'chunksize': { + allowed: ['number'], + }, + // note: For the meaning of the flags, refer to + // https://www.gnupg.org/documentation/manuals/gpgme/Key-Listing-Mode.html + 'secret': { allowed: ['boolean'] - },{ - 'pattern': { - allowed: ['string'] + }, + 'extern': { + allowed: ['boolean'] + }, + 'local':{ + allowed: ['boolean'] + }, + 'sigs':{ + allowed: ['boolean'] + }, + 'notations':{ + allowed: ['boolean'] + }, + 'tofu': { + allowed: ['boolean'] + }, + 'ephemeral': { + allowed: ['boolean'] + }, + 'validate': { + allowed: ['boolean'] + }, + // 'pattern': { TODO + // allowed: ['string'] + // }, + 'keys': { + allowed: ['string'], + array_allowed: true } }, - answer: { - type: ['TBD'], - infos: ['TBD'] - // keys: Array Fingerprints representing the results + answer: { + type: [], + data: [], + params: [], + infos: ['keys'] + } }, - */ /** importkey: { required: { 'keyarmored': { allowed: ['string'] } }, answer: { type: ['TBD'], infos: ['TBD'], // for each key if import was a success, // and if it was an update of preexisting key } }, */ /** deletekey: { pinentry: true, required: { 'fingerprint': { allowed: ['string'], // array_allowed: TBD Allow several Keys to be deleted at once? }, optional: { 'TBD' //Flag to delete secret Key ? } answer: { type ['TBD'], infos: [''] // TBD (optional) Some kind of 'ok' if delete was successful. } } */ /** *TBD get armored secret different treatment from keyinfo! * TBD key modification? * encryptsign: TBD */ + + version: { + required: {}, + optional: {}, + answer: { + type: [''], + data: ['gpgme'], + infos: ['info'], + params:[] + } + } } diff --git a/lang/js/unittest_inputvalues.js b/lang/js/unittest_inputvalues.js index 3450afd2..ca51f4ae 100644 --- a/lang/js/unittest_inputvalues.js +++ b/lang/js/unittest_inputvalues.js @@ -1,45 +1,51 @@ import {Connection} from "./src/Connection"; import {createKey} from "./src/Key"; let conn = new Connection; export const helper_params = { validLongId: '0A0A0A0A0A0A0A0A', validKeys: ['A1E3BC45BDC8E87B74F4392D53B151A1368E50F3', - createKey('ADDBC303B6D31026F5EB4591A27EABDF283121BB', conn), + createKey('D41735B91236FDB882048C5A2301635EEFF0CB05', conn), 'EE17AEE730F88F1DE7713C54BBE0A4FF7851650A'], validFingerprint: '9A9A7A7A8A9A9A7A7A8A9A9A7A7A8A9A9A7A7A8A', validFingerprints: ['9A9A7A7A8A9A9A7A7A8A9A9A7A7A8A9A9A7A7A8A', '9AAE7A338A9A9A7A7A8A9A9A7A7A8A9A9A7A7DDA'], invalidLongId: '9A9A7A7A8A9A9A7A7A8A', invalidFingerprints: [{hello:'World'}, ['kekekeke'], new Uint32Array(40)], invalidKeyArray: {curiosity:'uncat'}, invalidKeyArray_OneBad: [ - createKey('12AE9F3E41B33BF77DF52B6BE8EE1992D7909B08', conn), + createKey('D41735B91236FDB882048C5A2301635EEFF0CB05', conn), 'E1D18E6E994FA9FE9360Bx0E687B940FEFEB095A', '3AEA7FE4F5F416ED18CEC63DD519450D9C0FAEE5'], invalidErrorCode: 'Please type in all your passwords.', - validGPGME_Key: createKey('ADDBC303B6D31026F5EB4591A27EABDF283121BB', conn), + validGPGME_Key: createKey('D41735B91236FDB882048C5A2301635EEFF0CB05', conn), valid_openpgplike: { primaryKey: { getFingerprint: function(){ return '85DE2A8BA5A5AB3A8A7BE2000B8AED24D7534BC2';} } } } export const message_params = { invalid_op_action : 'dance', invalid_op_type : [234, 34, '<>'], valid_encrypt_data: "مرحبا بالعالم", invalid_param_test: { valid_op: 'encrypt', invalid_param_names: [22,'dance', {}], validparam_name_0: 'mime', invalid_values_0: [2134, 'All your passwords', createKey('12AE9F3E41B33BF77DF52B6BE8EE1992D7909B08', conn), null] } } export const whatever_params = { - four_invalid_params: ['<(((-<', '>°;==;~~', '^^', '{{{{o}}}}'] + four_invalid_params: ['<(((-<', '>°;==;~~', '^^', '{{{{o}}}}'], +} +export const key_params = { + validKeyFingerprint: 'D41735B91236FDB882048C5A2301635EEFF0CB05', + invalidKeyFingerprint: 'CDC3A2B2860625CCBFC5AAAAAC6D1B604967FC4A', + validKeyProperties: ['expired', 'disabled','invalid','can_encrypt', + 'can_sign','can_certify','can_authenticate','secret','is_qualified'] } diff --git a/lang/js/unittests.js b/lang/js/unittests.js index 06b2b23a..bb06309d 100644 --- a/lang/js/unittests.js +++ b/lang/js/unittests.js @@ -1,327 +1,378 @@ /* 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"; import "./node_modules/chai/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"; import { setTimeout } from "timers"; mocha.setup('bdd'); var 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'); + conn0.disconnect(); 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 conn = new Connection; - let key = createKey(hp.validFingerprint, conn); - + let key = createKey(kp.validKeyFingerprint, conn); expect(key).to.be.an.instanceof(GPGME_Key); expect(key.connection).to.be.an.instanceof(Connection); - // TODO not implemented yet: Further Key functionality + conn.disconnect(); + }); + it('Key has data after a first refresh', function(done) { + let conn = new Connection; + let key = createKey(kp.validKeyFingerprint, conn); + 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); + conn.disconnect(); + done(); + }); }); + it('Non-cached key async data retrieval', function (done){ + let conn = new Connection; + let key = createKey(kp.validKeyFingerprint, conn); + key.get('can_authenticate',false).then(function(result){ + expect(result).to.be.a('boolean'); + conn.disconnect(); + done(); + }); + }) + + it('Querying non-existing Key returns an error', function(done) { + let conn = new Connection; + let key = createKey(kp.invalidKeyFingerprint, conn); + key.refreshKey().then(function(){}, + function(error){ + expect(error).to.be.an.instanceof(Error); + expect(error.code).to.equal('KEY_NOKEY'); + conn.disconnect(); + done(); + }); + }); + + it('Key can use the connection', function(done){ let conn = new Connection; let key = createKey(hp.validFingerprint, conn); key.connection.checkConnection(false).then(function(result){ expect(result).to.be.true; key.connection.disconnect(); key.connection.checkConnection(false).then(function(result2){ expect(result2).to.be.false; + conn.disconnect(); done(); }); }); }); it('createKey returns error if parameters are wrong', function(){ let conn = new Connection; for (let i=0; i< 4; i++){ let key0 = createKey(wp.four_invalid_params[i], conn); expect(key0).to.be.an.instanceof(Error); expect(key0.code).to.equal('PARAM_WRONG'); } for (let i=0; i< 4; i++){ let key0 = createKey( hp.validFingerprint, wp.four_invalid_params[i]); expect(key0).to.be.an.instanceof(Error); expect(key0.code).to.equal('PARAM_WRONG'); } + conn.disconnect(); }); - it('bad GPGME_Key returns Error if used', function(){ + + it('malformed GPGME_Key cannot be used', function(){ let conn = new Connection; for (let i=0; i < 4; i++){ let key = new GPGME_Key(wp.four_invalid_params[i], conn); - - expect(key.connection).to.be.an.instanceof(Error); - expect(key.connection.code).to.equal('KEY_INVALID'); + expect(key.fingerprint).to.be.an.instanceof(Error); + expect(key.fingerprint.code).to.equal('KEY_INVALID'); } + conn.disconnect(); }); + + // TODO: tests for subkeys + // TODO: tests for userids + // TODO: some invalid tests for key/keyring }); describe('GPGME_Keyring', function(){ it('correct initialization', function(){ let conn = new Connection; let keyring = new GPGME_Keyring(conn); expect(keyring).to.be.an.instanceof(GPGME_Keyring); expect(keyring.connection).to.be.an.instanceof(Connection); expect(keyring.getKeys).to.be.a('function'); expect(keyring.getSubset).to.be.a('function'); }); it('Keyring should return errors if not connected', function(){ let keyring = new GPGME_Keyring; expect(keyring).to.be.an.instanceof(GPGME_Keyring); expect(keyring.connection).to.be.an.instanceof(Error); expect(keyring.connection.code).to.equal('CONN_NO_CONNECT'); // not yet implemented: // keyring.getKeys().then( // function(result){}, //function(reject){ // expect(reject).to.be.an.instanceof(Error); // done(); }); //TODO not yet implemented: // getKeys(pattern, include_secret) //note: pattern can be null // getSubset(flags, pattern) // available Boolean flags: secret revoked expired }); describe('GPGME_Message', function(){ 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'); 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