diff --git a/lang/js/src/Helpers.js b/lang/js/src/Helpers.js index ea056fff..fd0e7200 100644 --- a/lang/js/src/Helpers.js +++ b/lang/js/src/Helpers.js @@ -1,103 +1,103 @@ /* 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)){ + 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 */ 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 6d3cf17d..075a190e 100644 --- a/lang/js/src/Key.js +++ b/lang/js/src/Key.js @@ -1,242 +1,244 @@ /* 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 { gpgme_error } from './Errors' import { createMessage } from './Message'; import { permittedOperations } from './permittedOperations'; import { Connection } from './Connection'; export function createKey(fingerprint, parent){ if (!isFingerprint(fingerprint)){ - return gpgme_error('KEY_INVALID'); + 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'); } } 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){ 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; } } get fingerprint(){ if (!this._fingerprint){ return gpgme_error('KEY_INVALID'); } return this._fingerprint; } /** * hasSecret returns true if a secret subkey is included in this Key */ 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'); } get canSign(){ return this.checkKey('can_sign'); } get canCertify(){ return this.checkKey('can_certify'); } get canAuthenticate(){ return this.checkKey('can_authenticate'); } get isQualified(){ return this.checkKey('is_qualified'); } get armored(){ let msg = createMessage ('export_key'); msg.setParameter('armor', true); if (msg instanceof Error){ return gpgme_error('KEY_INVALID'); } 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 } /** * TODO returns true if this is the default key used to sign */ get isDefault(){ throw('NOT_YET_IMPLEMENTED'); } /** * get the Key's subkeys as GPGME_Key objects * @returns {Array} */ get subkeys(){ return this.checkKey('subkeys').then(function(result){ // TBD expecting a list of fingerprints if (!Array.isArray(result)){ result = [result]; } let resultset = []; for (let i=0; i < result.length; i++){ let subkey = new GPGME_Key(result[i], this.connection); if (subkey instanceof GPGME_Key){ resultset.push(subkey); } } return Promise.resolve(resultset); }, function(error){ //TODO this.checkKey fails }); } /** * creation time stamp of the key * @returns {Date|null} TBD */ get timestamp(){ return this.checkKey('timestamp'); //TODO GPGME: -1 if the timestamp is invalid, and 0 if it is not available. } /** * The expiration timestamp of this key TBD * @returns {Date|null} TBD */ get expires(){ return this.checkKey('expires'); // TODO convert to Date; check for 0 } /** * getter name TBD * @returns {String|Array} The user ids associated with this key */ get userIds(){ return this.checkKey('uids'); } /** * @returns {String} The public key algorithm supported by this subkey */ get pubkey_algo(){ return this.checkKey('pubkey_algo'); } /** * 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'); } 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'); }); } }; \ No newline at end of file diff --git a/lang/js/src/permittedOperations.js b/lang/js/src/permittedOperations.js index 274e037e..59597aaf 100644 --- a/lang/js/src/permittedOperations.js +++ b/lang/js/src/permittedOperations.js @@ -1,176 +1,217 @@ /* 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', 'Uint8Array'] } }, 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', 'Uint8Array'] } }, optional: { 'protocol': { allowed: ['string'], allowed_data: ['cms', 'openpgp'] }, 'chunksize': { allowed: ['number'], }, 'base64': { allowed: ['boolean'] } }, answer: { type: ['plaintext'], data: ['data'], params: ['base64', 'mime'], - infos: [] // pending. Info about signatures and validity - //signature: [{Key Fingerprint, valid boolean}] + infos: [] // TODO pending. Info about signatures and validity + //{ + //signatures: [{ + //Key : Fingerprint, + //valid: + // }] } }, - /** - keyinfo: { // querying the Key's information. - required: ['fingerprint'], - anser: { + /** 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'], + 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:{ - optional: ['with-secret', 'pattern'], + required: {}; + optional: { + 'with-secret':{ + allowed: ['boolean'] + },{ + 'pattern': { + allowed: ['string'] + } + }, answer: { - type: ['TBD'], //Array of fingerprints? - infos: ['TBD'] //the property with infos + type: ['TBD'], + infos: ['TBD'] + // keys: Array Fingerprints representing the results }, */ /** importkey: { - required: ['keyarmored'], + required: { + 'keyarmored': { + allowed: ['string'] + } + }, answer: { type: ['TBD'], - infos: [''], // for each key if import was a success, if it was an update + infos: ['TBD'], + // for each key if import was a success, + // and if it was an update of preexisting key } }, */ /** deletekey: { - required: ['fingerprint'], + 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: [''] //success:true? in gpgme, an error NO_ERROR is returned + infos: [''] + // TBD (optional) Some kind of 'ok' if delete was successful. } } */ /** - *get armored secret different treatment from keyinfo! - */ - - /** - * TBD key modification requests? + *TBD get armored secret different treatment from keyinfo! + * TBD key modification? + * encryptsign: TBD + * verify: TBD */ } diff --git a/lang/js/unittests.js b/lang/js/unittests.js index 0a1b4b48..6c0d1890 100644 --- a/lang/js/unittests.js +++ b/lang/js/unittests.js @@ -1,321 +1,321 @@ /* 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 { 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; let delayed = function(){ expect(conn0.isConnected).to.be.true; expect(conn0.connect).to.be.a('function'); expect(conn0.disconnect).to.be.a('function'); expect(conn0.post).to.be.a('function'); done(); }; setTimeout(delayed, 5); }); it('Disconnecting', function(done) { let conn0 = new Connection; let delayed = function(){ conn0.disconnect(); // TODO fails! expect(conn0.isConnected).to.be.false; done(); }; setTimeout(delayed, 5); }); // broken // it('Connect info still only available after a delay', function(done){ // // if false, all delayed connections can be refactored // let conn0 = new Connection; // expect(conn0.isConnected).to.be.undefined; // // // }) }); describe('Error Object handling', function(){ 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(){ + 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); console.log(test0); 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); expect(key).to.be.an.instanceof(GPGME_Key); expect(key.connection).to.be.an.instanceof(Connection); // TODO not implemented yet: Further Key functionality }); it('Key can use the connection', function(){ let conn = new Connection; let key = createKey(hp.validFingerprint, conn); expect(key.connection.isConnected).to.be.true; key.connection.disconnect(); expect(key.connection.isConnected).to.be.false; }); 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'); } }); it('bad GPGME_Key returns Error if 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'); } }); }); 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'); expect(keyring.getKeys).to.be.an.instanceof(Error); expect(keyring.getkeys.code).to.equal('CONN_NO_CONNECT'); }); //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 mandatoy 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('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'); } }); }); mocha.run(); } export default {unittests}; \ No newline at end of file