diff --git a/lang/js/BrowserTestExtension/testkey2.pub b/lang/js/BrowserTestExtension/testkey2.pub new file mode 100644 index 00000000..557bd5be --- /dev/null +++ b/lang/js/BrowserTestExtension/testkey2.pub @@ -0,0 +1,30 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFsMHecBCACqdJgqa+CeNYwPCK+MpOwAV6uFVjDyO2LmOs6+XfDWRBU/Zjtz +8zdYNKSbLjkWN4ujV5aiyA7MtEofszzYLEoKUt1wiDScHMpW8qmEFDvl9g26MeAV +rTno9D5KodHvEIs8wnrqBs8ix0WLbh6J1Dtt8HQgIbN+v3gaRQrgBFe6z2ZYpHHx +ZfOu3iFKlm2WE/NekRkvvFIo3ApGvRhGIYw6JMmugBlo7s5xosJK0I9dkPGlEEtt +aF1RkcMj8sWG9vHAXcjlGgFfXSN9YLppydXpkuZGm4+gjLB2a3rbQCZVFnxCyG4O +ybjkP8Jw6Udm89bK2ucYFfjdrmYn/nJqRxeNABEBAAG0I1Rlc3QgTm9Qcml2S2V5 +IDxub2JvZHlAZXhhbXBsZS5vcmc+iQFOBBMBCAA4FiEE4Fmh4IZtMa4TEXCITZou +EzBBU9EFAlsMHecCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQTZouEzBB +U9F+qwf/SHj4uRnTWgyJ71FBxQDYCBq3jbi6e7hMkRPbJyJdnPIMAb2p0PJjBgjW +0pp4+kDPZans3UDHbma1u/SFI4/y6isJiK94Bk5xp5YliLGnUceTjgDFe6lBhfQ1 +zVWZC/NF3tPgbziIxXQTNt34nS+9dbV/QFDLW0POcN7C0jR/hgkBjMEH2PezWhSj +mL/yLfLfUYAoxVpXjfC5aPJKqw0tR7m5ibznjCphE+FUMRg8EOmJcg6soeJ5QspU +k2dPN3+Y0zCTNRgAHEI+yIQbM6pio6v2c+UCtT1QhW4xSI38/kcEG8QiM55r1TUy +FcWAY5n5t1nNZtMxxse3LqEon3rKiLkBDQRbDB3nAQgAqfAjSjcngERtM+ZYOwN0 +QF2v2FuEuMe8mhju7Met7SN2zGv1LnjhTNshEa9IABEfjZirE2Tqx4xCWDwDedK4 +u1ToFvcnuAMnq2O47Sh+eTypsf6WPFtPBWf6ctKY31hFXjgoyDBULBvl43XU/D9C +Mt7nsKDPYHVrrnge/qWPYVcb+cO0sSwNImMcwQSdTQ3VBq7MeNS9ZeBcXi+XCjhN +kjNum2AQqpkHHDQV7871yQ8RIILvZSSfkLb0/SNDU+bGaw2G3lcyKdIfZi2EWWZT +oCbH38I/+LV7nAEe4zFpHwW8X0Dkx2aLgxe6UszDH9L3eGhTLpJhOSiaanG+zZKm ++QARAQABiQE2BBgBCAAgFiEE4Fmh4IZtMa4TEXCITZouEzBBU9EFAlsMHecCGwwA +CgkQTZouEzBBU9H5TQgAolWvIsez/WW8N2tmZEnX0LOFNB+1S4L4X983njwNdoVI +w19pbj+8RIHF/H9kcPGi7jK96gvlykQn3uez/95D2AiRFW5KYdOouFisKgHpv8Ay +BrhclHv11yK+X/0iTD0scYaG7np5162xLkaxSO9hsz2fGv20RKaXCWkI69fWw0BR +XlI5pZh2YFei2ZhH/tIMIW65h3w0gtgaZBBdpZTOOW4zvghyN+0MSObqkI1BvUJu +caDFI4d6ZTmp5SY+pZyktZ4bg/vMH5VFxdIKgbLx9uVeTvOupvbAW0TNulYGUBQE +nm+S0zr3W18t64e4sS3oHse8zCqo1iiImpba6F1Oaw== +=y6DD +-----END PGP PUBLIC KEY BLOCK----- diff --git a/lang/js/src/Key.js b/lang/js/src/Key.js index 13c99542..f2a16b42 100644 --- a/lang/js/src/Key.js +++ b/lang/js/src/Key.js @@ -1,397 +1,455 @@ /* 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, isLongId } from './Helpers' import { gpgme_error } from './Errors' import { createMessage } from './Message'; import { permittedOperations } from './permittedOperations'; /** * Validates the fingerprint. * @param {String} fingerprint */ export function createKey(fingerprint){ if (!isFingerprint(fingerprint)){ return gpgme_error('PARAM_WRONG'); } else return new GPGME_Key(fingerprint); } /** * Representing the Keys as stored in GPG */ export class GPGME_Key { constructor(fingerprint){ this.fingerprint = fingerprint; } set 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._data || !this._data.fingerprint){ return gpgme_error('KEY_INVALID'); } return this._data.fingerprint; } /** * * @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 */ - setKeydata(data){ + 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'); } 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; if (typeof(data.owner_trust) !== 'string'){ return gpgme_error('KEY_INVALID'); } // TODO check valid values? this._data.owner_trust = data.owner_trust; // TODO: what about origin ? if (!Number.isInteger(data.last_update)){ return gpgme_error('KEY_INVALID'); } this._data.last_update = data.last_update; 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])); } } 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])); } } return this; } /** * 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(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); - }); + if (property === 'armor'){ + resolve(me.getArmor()); + } else if (property === 'hasSecret'){ + resolve(me.getHasSecret()); + } else { + 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]); } } } /** * Reloads the Key from gnupg */ refreshKey() { let me = this; return new Promise(function(resolve, reject) { if (!me._data.fingerprint){ reject(gpgme_error('KEY_INVALID')); } let msg = createMessage('keylist'); msg.setParameter('sigs', true); msg.setParameter('keys', me._data.fingerprint); - console.log(msg); msg.post().then(function(result){ if (result.keys.length === 1){ - me.setKeydata(result.keys[0]); + me.setKeyData(result.keys[0]); resolve(me); } else { reject(gpgme_error('KEY_NOKEY')); } }, function (error) { reject(gpgme_error('GNUPG_ERROR'), error); }) }); } - //TODO: /** * 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 armor(){ TODO } /** * Query the armored block of the non- secret parts of the Key directly * from gpg. - * Async, returns Promise + * @returns {Promise} */ - // getArmor(){ TODO } - // + getArmor(){ + let me = this; + return new Promise(function(resolve, reject) { + if (!me._data.fingerprint){ + reject(gpgme_error('KEY_INVALID')); + } + let msg = createMessage('export'); + msg.setParameter('armor', true); + msg.setParameter('keys', me._data.fingerprint); + msg.post().then(function(result){ + me._data.armor = result.data; + resolve(result.data); + }, function(error){ + reject(error); + }); + }); + } - // get hasSecret(){TODO} // confusing difference to Key.get('secret')! - // getHasSecret(){TODO async version} + getHasSecret(){ + let me = this; + return new Promise(function(resolve, reject) { + if (!me._data.fingerprint){ + reject(gpgme_error('KEY_INVALID')); + } + let msg = createMessage('keylist'); + msg.setParameter('keys', me._data.fingerprint); + msg.setParameter('secret', true); + msg.post().then(function(result){ + me._data.hasSecret = null; + if (result.keys === undefined || result.keys.length < 1) { + me._data.hasSecret = false; + resolve(false); + } + else if (result.keys.length === 1){ + let key = result.keys[0]; + if (!key.subkeys){ + me._data.hasSecret = false; + resolve(false); + } else { + for (let i=0; i < key.subkeys.length; i++) { + if (key.subkeys[i].secret === true) { + me._data.hasSecret = true; + resolve(true); + break; + } + if (i === (key.subkeys.length -1)) { + me._data.hasSecret = false; + resolve(false); + } + } + } + } else { + reject(gpgme_error('CONN_UNEXPECTED_ANSWER')) + } + }, function(error){ + }) + }); + } } /** * 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; } } } /** * * @param {String} property Information to request * @returns {String | Number} * TODO: date properties are numbers with Date in seconds */ 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; } } } /** * * @param {String} property Information to request * @returns {String | Number} * TODO: date properties are numbers with Date in seconds */ get(property) { if (this._data.hasOwnProperty(property)){ return (this._data[property]); } } } 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); } }; 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/Keyring.js b/lang/js/src/Keyring.js index 9abb9ec3..7e13dfe2 100644 --- a/lang/js/src/Keyring.js +++ b/lang/js/src/Keyring.js @@ -1,144 +1,81 @@ /* 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 {createMessage} from './Message' -import {GPGME_Key} from './Key' +import {GPGME_Key, createKey} from './Key' import { isFingerprint } from './Helpers'; import { gpgme_error } from './Errors'; export class GPGME_Keyring { constructor(){ } /** - * @param {String} (optional) pattern A pattern to search for, in userIds or KeyIds - * @param {Boolean} (optional) Include listing of secret keys + * @param {String} pattern (optional) pattern A pattern to search for, + * in userIds or KeyIds + * @param {Boolean} prepare_sync (optional, default true) if set to true, + * Key.armor and Key.hasSecret will be called, so they can be used + * inmediately. This allows for full synchronous use. If set to false, + * these will initially only be available as Promises in getArmor() and + * getHasSecret() * @returns {Promise.>} * */ - getKeys(pattern, include_secret){ + getKeys(pattern, prepare_sync){ let me = this; return new Promise(function(resolve, reject) { let msg; - msg = createMessage('listkeys'); + msg = createMessage('keylist'); if (pattern && typeof(pattern) === 'string'){ - msg.setParameter('pattern', pattern); - } - if (include_secret){ - msg.setParameter('with-secret', true); + msg.setParameter('keys', pattern); } + msg.setParameter('sigs', true); //TODO do we need this? msg.post().then(function(result){ - let fpr_list = []; let resultset = []; - if (!Array.isArray(result.keys)){ - //TODO check assumption keys = Array - fpr_list = [result.keys]; - } else { - fpr_list = result.keys; - } - for (let i=0; i < fpr_list.length; i++){ - let newKey = new GPGME_Key(fpr_list[i]); - if (newKey instanceof GPGME_Key){ - resultset.push(newKey); + let promises = []; + // TODO check if result.key is not empty + for (let i=0; i< result.keys.length; i++){ + let k = createKey(result.keys[i].fingerprint, me); + k.setKeyData(result.keys[i]); + if (prepare_sync === true){ + promises.push(k.getArmor()); + promises.push(k.getHasSecret()); } + resultset.push(k); + } + if (promises.length > 0) { + Promise.all(promises).then(function (res){ + resolve(resultset); + }, function(error){ + reject(error); + }); } - resolve(resultset); }, function(error){ reject(error); }); }); } - - /** - * @param {Object} flags subset filter expecting at least one of the - * filters described below. True will filter on the condition, False will - * reverse the filter, if not present or undefined, the filter will not be - * considered. Please note that some combination may not make sense - * @param {Boolean} flags.secret Only Keys containing a secret part. - * @param {Boolean} flags.revoked revoked Keys only - * @param {Boolean} flags.expired Expired Keys only - * @param {String} (optional) pattern A pattern to search for, in userIds or KeyIds - * @returns {Promise Array} - * - */ - getSubset(flags, pattern){ - if (flags === undefined) { - throw('ERR_WRONG_PARAM'); - }; - let secretflag = false; - if (flags.hasOwnProperty(secret) && flags.secret){ - secretflag = true; - } - this.getKeys(pattern, secretflag).then(function(queryset){ - let resultset = []; - for (let i=0; i < queryset.length; i++ ){ - let conditions = []; - let anticonditions = []; - if (secretflag === true){ - conditions.push('hasSecret'); - } else if (secretflag === false){ - anticonditions.push('hasSecret'); - } - /** - if (flags.defaultKey === true){ - conditions.push('isDefault'); - } else if (flags.defaultKey === false){ - anticonditions.push('isDefault'); - } - */ - /** - * if (flags.valid === true){ - anticonditions.push('isInvalid'); - } else if (flags.valid === false){ - conditions.push('isInvalid'); - } - */ - if (flags.revoked === true){ - conditions.push('isRevoked'); - } else if (flags.revoked === false){ - anticonditions.push('isRevoked'); - } - if (flags.expired === true){ - conditions.push('isExpired'); - } else if (flags.expired === false){ - anticonditions.push('isExpired'); - } - let decision = undefined; - for (let con = 0; con < conditions.length; con ++){ - if (queryset[i][conditions[con]] !== true){ - decision = false; - } - } - for (let acon = 0; acon < anticonditions.length; acon ++){ - if (queryset[i][anticonditions[acon]] === true){ - decision = false; - } - } - if (decision !== false){ - resultset.push(queryset[i]); - } - } - return Promise.resolve(resultset); - }, function(error){ - //TODO error handling - }); - } +// TODO: + // deleteKey(key, include_secret=false) + // getKeysArmored(pattern) //just dump all armored keys + // getDefaultKey() Big TODO + // importKeys(armoredKeys) }; diff --git a/lang/js/src/permittedOperations.js b/lang/js/src/permittedOperations.js index 42213ec3..e4f9bd22 100644 --- a/lang/js/src/permittedOperations.js +++ b/lang/js/src/permittedOperations.js @@ -1,278 +1,324 @@ /* 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: { + pinentry: true, //TODO only with signing_keys required: { 'keys': { allowed: ['string'], array_allowed: true }, 'data': { allowed: ['string'] } }, optional: { 'protocol': { allowed: ['string'], allowed_data: ['cms', 'openpgp'] }, - 'chunksize': { + 'signing_keys': { + allowed: ['string'], + array_allowed: true + }, + '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'] - }, }, + '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: - // }] + infos: ['signatures'] } }, 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'] } }, keylist:{ required: {}, optional: { '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'] }, '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: [], + type: ['keys'], data: [], - params: [], + params: ['base64'], infos: ['keys'] } }, - /** - importkey: { + export: { + required: {}, + optional: { + 'protocol': { + allowed: ['string'], + allowed_data: ['cms', 'openpgp'] + }, + 'chunksize': { + allowed: ['number'], + }, + 'keys': { + allowed: ['string'], + array_allowed: true + }, + 'armor': { + allowed: ['boolean'] + }, + 'extern': { + allowed: ['boolean'] + }, + 'minimal': { + allowed: ['boolean'] + }, + 'raw': { + allowed: ['boolean'] + }, + 'pkcs12':{ + allowed: ['boolean'] + } + // secret: not yet implemented + }, + answer: { + type: ['keys'], + data: ['data'], + params: ['base64'] + } + }, + + import: { required: { - 'keyarmored': { + 'data': { allowed: ['string'] } }, + optional: { + 'protocol': { + allowed: ['string'], + allowed_data: ['cms', 'openpgp'] + }, + 'base64': { + allowed: ['boolean'] + }, + }, answer: { - type: ['TBD'], - infos: ['TBD'], - // for each key if import was a success, - // and if it was an update of preexisting key + infos: ['result'], + type: [], + data: [], + params: [] } }, - */ - /** - deletekey: { + delete: { pinentry: true, - required: { - 'fingerprint': { + required:{ + 'key': { + allowed: ['string'] + } + }, + optional: { + 'protocol': { allowed: ['string'], - // array_allowed: TBD Allow several Keys to be deleted at once? + allowed_data: ['cms', 'openpgp'] }, - optional: { - 'TBD' //Flag to delete secret Key ? - } + // 'secret': { not yet implemented + // allowed: ['boolean'] + // } + + }, answer: { - type ['TBD'], - infos: [''] - // TBD (optional) Some kind of 'ok' if delete was successful. + data: [], + params:['success'], + infos: [] } - } - */ - + }, /** *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 ca51f4ae..2a21a6ae 100644 --- a/lang/js/unittest_inputvalues.js +++ b/lang/js/unittest_inputvalues.js @@ -1,51 +1,55 @@ import {Connection} from "./src/Connection"; import {createKey} from "./src/Key"; let conn = new Connection; export const helper_params = { validLongId: '0A0A0A0A0A0A0A0A', validKeys: ['A1E3BC45BDC8E87B74F4392D53B151A1368E50F3', createKey('D41735B91236FDB882048C5A2301635EEFF0CB05', conn), 'EE17AEE730F88F1DE7713C54BBE0A4FF7851650A'], validFingerprint: '9A9A7A7A8A9A9A7A7A8A9A9A7A7A8A9A9A7A7A8A', validFingerprints: ['9A9A7A7A8A9A9A7A7A8A9A9A7A7A8A9A9A7A7A8A', '9AAE7A338A9A9A7A7A8A9A9A7A7A8A9A9A7A7DDA'], invalidLongId: '9A9A7A7A8A9A9A7A7A8A', invalidFingerprints: [{hello:'World'}, ['kekekeke'], new Uint32Array(40)], invalidKeyArray: {curiosity:'uncat'}, invalidKeyArray_OneBad: [ createKey('D41735B91236FDB882048C5A2301635EEFF0CB05', conn), 'E1D18E6E994FA9FE9360Bx0E687B940FEFEB095A', '3AEA7FE4F5F416ED18CEC63DD519450D9C0FAEE5'], invalidErrorCode: 'Please type in all your passwords.', 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}}}}'], } export const key_params = { +// A Key you own (= having a secret Key) in GPG. See testkey.pub/testkey.sec validKeyFingerprint: 'D41735B91236FDB882048C5A2301635EEFF0CB05', +// A Key you do not own (= having no secret Key) in GPG. See testkey2.pub + validFingerprintNoSecret: 'E059A1E0866D31AE131170884D9A2E13304153D1', +// A Key not in your Keyring. This is just a random hex string. 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 9830a2c5..443aa685 100644 --- a/lang/js/unittests.js +++ b/lang/js/unittests.js @@ -1,332 +1,358 @@ /* 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'); 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); key.get('can_authenticate',false).then(function(result){ expect(result).to.be.a('boolean'); done(); }); - }) + }); + + it('Non-cached key async armored Key', function (done){ + let key = createKey(kp.validKeyFingerprint); + key.get('armor', false).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); + key.get('hasSecret', false).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); + expect(key).to.be.an.instanceof(GPGME_Key); + key.get('hasSecret', false).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'); } }); - // 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'); - expect(keyring.getSubset).to.be.a('function'); }); - it('correct initialization', function(){ + it('Loading Keys from Keyring, to be used synchronously', function(done){ let keyring = new GPGME_Keyring; - expect(keyring).to.be.an.instanceof(GPGME_Keyring); - expect(keyring.getKeys).to.be.a('function'); - expect(keyring.getSubset).to.be.a('function'); + 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('armor')).to.be.a('string'); + expect(result[0].get('armor')).to.include( + '-----END PGP PUBLIC KEY BLOCK-----'); + 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