diff --git a/lang/js/src/Connection.js b/lang/js/src/Connection.js index 784929e9..87ec8cf7 100644 --- a/lang/js/src/Connection.js +++ b/lang/js/src/Connection.js @@ -1,180 +1,194 @@ import { GPGME_Message } from "./Message"; /* 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 { GPGMEJS_Error} from "./Errors" +/** + * A Connection handles the nativeMessaging interaction. + */ export class Connection{ - /** - * Opens and closes a port. Thus, it is made sure that the connection can - * be used. - * THIS BEHAVIOUR MAY CHANGE! - * discussion is to keep a port alive as long as the context stays the same - * - * TODO returns nothing, but triggers exceptions if not successfull - */ constructor(){ - this._connection = chrome.runtime.connectNative('gpgmejson'); - if (!this._connection){ - if (chrome.runtime.lastError){ - throw('NO_CONNECT_RLE'); - } else { - throw('NO_CONNECT'); - } - } - this._flags = {}; // TODO general config + this.connect(); } /** - * Immediately closes the open port + * Immediately closes the open port. */ disconnect() { if (this._connection){ this._connection.disconnect(); } } + /** + * Opens a nativeMessaging port. + * returns nothing, but triggers errors if not successfull: + * NO_CONNECT: connection not successfull, chrome.runtime.lastError may be + * available + * ALREADY_CONNECTED: There is already a connection present. + */ + connect(){ + if (this._connection){ + return new GPGMEJS_Error('ALREADY_CONNECTED'); + } + this._connection = chrome.runtime.connectNative('gpgmejson'); + if (!this._connection){ + return new GPGMEJS_Error('NO_CONNECT'); + } + } + + /** + * checks if the connection is established + * TODO: some kind of ping to see if the other side responds as expected? + * @returns {Boolean} + */ + get connected(){ + return this._connection ? true: false; + } + /** * Sends a message and resolves with the answer. * @param {GPGME_Message} message * @returns {Promise} the gnupg answer, or rejection with error - * information - * TODO: better/more consistent error information + * information. */ post(message){ if (!message || !message instanceof GPGME_Message){ - return Promise.reject('ERR_NO_MSG'); + return Promise.reject(new GPGMEJS_Error('WRONGPARAM')); + } + if (message.isComplete !== true){ + return Promise.reject(new GPGMEJS_Error('MSG_INCOMPLETE')); } // let timeout = 5000; //TODO config let me = this; return new Promise(function(resolve, reject){ - let answer = new Answer(message.op); + let answer = new Answer(message.operation); let listener = function(msg) { if (!msg){ me._connection.onMessage.removeListener(listener) - reject('EMPTY_ANSWER'); + reject(new GPGMEJS_Error('EMPTY_GPG_ANSWER')); } else if (msg.type === "error"){ me._connection.onMessage.removeListener(listener) + //TODO: GPGMEJS_Error? reject(msg.msg); } else { answer.add(msg); if (msg.more === true){ me._connection.postMessage({'op': 'getmore'}); } else { me._connection.onMessage.removeListener(listener) resolve(answer.message); } } }; me._connection.onMessage.addListener(listener); - me._connection.postMessage(message); + me._connection.postMessage(message.message); //TBD: needs to be aware if there is a pinentry pending // setTimeout( // function(){ // me.disconnect(); - // reject('TIMEOUT'); + // reject(new GPGMEJS_Error('TIMEOUT', 5000)); // }, timeout); }); } }; /** * 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 return keys + * the nativeMessaging communication. + * @param {String} operation The operation, to look up validity of returning messages */ class Answer{ constructor(operation){ this.operation = operation; } /** - * + * Add the information to the answer * @param {Object} msg The message as received with nativeMessaging - * TODO: "error" and "more" handling are not in here, but in post() */ add(msg){ if (this._response === undefined){ this._response = {}; } let messageKeys = Object.keys(msg); let poa = permittedOperations[this.operation].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){ - console.log( 'unexpected answer type: ' + msg.type); - throw('UNEXPECTED_TYPE'); - + return new GPGMEJS_Error('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] = this._response[key].concat(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]){ - throw('UNEXPECTED_TYPE'); + return new GPGMEJS_Error('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.push(msg[key]); } else { - console.log('unexpected answer parameter: ' + key); - throw('UNEXPECTED_PARAM'); + return new GPGMEJS_Error('UNEXPECTED_ANSWER', key); } break; } } } /** - * Returns the assembled message. TODO: does not care yet if completed. + * @returns {Object} the assembled message. + * TODO: does not care yet if completed. */ get message(){ return this._response; } } diff --git a/lang/js/src/Errors.js b/lang/js/src/Errors.js new file mode 100644 index 00000000..c2356f7c --- /dev/null +++ b/lang/js/src/Errors.js @@ -0,0 +1,148 @@ +/* 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+ + */ + +// This is a preliminary collection of erors and warnings to be thrown and implemented. + +// general idea: if throw , throw the NAME +// return false || 'return' property + +//TODO: Connection.NOCONNECT promise +//connection.timeout: Be aware of pinentry + +export class GPGMEJS_Error { + + constructor(code = 'GENERIC_ERROR', details){ + let config = { //TODO TEMP + debug: 'console', // |'alert' + throw: 'default' // | 'always' | 'never' + }; + let errors = { //TODO: someplace else + //Connection errors + 'ALREADY_CONNECTED':{ + msg: 'The connection was already established. The action would overwrite the context', + throw: true + }, + 'NO_CONNECT': { + msg:'Connection with the nativeMessaging host could not be established.', + throw: true + }, + 'EMPTY_GPG_ANSWER':{ + msg: 'The nativeMesaging answer was empty', + throw: true + }, + 'TIMEOUT': { + msg: 'A timeout was exceeded.', + throw: false + }, + + 'UNEXPECTED_ANSWER': { + msg: 'The answer from gnupg was not as expected', + throw: true + }, + + // Message/Data Errors + + 'NO_KEYS' : { + msg: 'There were no valid keys provided.', + throw: true + }, + 'NOT_A_FPR': { + msg: 'The String is not an accepted fingerprint', + throw: false + }, + 'MSG_INCOMPLETE': { + msg: 'The Message did not match the minimum requirements for the interaction', + throw: true + }, + 'EMPTY_MSG' : { + msg: 'The Message has no data.', + throw: true + }, + 'MSG_NODATA':{ + msg: 'The data sent is empty. This may be unintentional.', + throw: false + }, + 'MSG_OP_PENDING': { + msg: 'There is no operation specified yet. The parameter cannot be set', + throw: false + }, + 'WRONG_OP': { + msg: "The operation requested could not be found", + throw: true + }, + + //generic errors + + 'WRONGPARAM':{ + msg: 'invalid parameter was found', + throw: true + }, + 'WRONGTYPE':{ + msg: 'invalid parameter type was found', + throw: true + }, + 'NOT_IMPLEMENTED': { + msg: 'A openpgpjs parameter was submitted that is not implemented', + throw: true + }, + 'GENERIC_ERROR': { + msg: 'Unspecified error', + throw: true + }, + + // hopefully temporary errors + + 'NOT_YET_IMPLEMENTED': { + msg: 'Support of this is probable, but it is not implemented yet', + throw: false + } + } + if (!errors.hasOwnProperty(code)){ + throw('GENERIC_ERROR'); + } + let msg = code; + if (errors[code].msg !== undefined){ + msg = msg + ': ' + errors[code].msg; + } + if (details){ + msg = msg + ' ' + details; + } + if (config.debug === 'console'){ + console.log(msg); + } else if (config.debug === 'alert'){ + alert(msg); + } + switch (config.throw) { + case 'default': + if (errors[code].throw === true){ + throw(code); + } + break; + case 'always': + throw(code); + break; + + case 'never': + break; + default: + throw('GENERIC_ERROR'); + } + } +} diff --git a/lang/js/src/Helpers.js b/lang/js/src/Helpers.js index eeb7a3c4..922ca06c 100644 --- a/lang/js/src/Helpers.js +++ b/lang/js/src/Helpers.js @@ -1,84 +1,100 @@ +import { GPGMEJS_Error } from "./Errors"; + /* 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+ */ /** * Tries to return an array of fingerprints, either from input fingerprints or * from Key objects - * @param {String|Array} input Input value. + * @param {Key |Array| GPGME_Key | Array|String|Array} input + * @param {Boolean} nocheck if set, an empty result is acceptable * @returns {Array} Array of fingerprints. */ -export function toKeyIdArray(input){ + +export function toKeyIdArray(input, nocheck){ if (!input){ - return []; - // TODO: Warning or error here? Did we expect something or is "nothing" okay? + return (nocheck ===true)? [] : new GPGMEJS_Error('NO_KEYS'); + } + if (!Array.isArray(input)){ + input = [input]; } - if (input instanceof Array){ - let result = []; - for (let i=0; i < input.length; i++){ + 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 { - //TODO error? - console.log('gpgmejs/Helpers.js Warning: '+ - input[i] + - ' is not a valid key fingerprint and will not be used'); + GPGMEJS_Error + } + } 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 { + return new GPGMEJS_Error('WRONGTYPE'); } + } + if (result.length === 0){ + return (nocheck===true)? [] : new GPGMEJS_Error('NO_KEYS'); + } else { return result; - } else if (isFingerprint(input) === true) { - return [input]; } - console.log('gpgmejs/Helpers.js Warning: ' + input + - ' is not a valid key fingerprint and will not be used'); - return []; }; /** * 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 needed anywhere? +/** + * check if the input is a valid Hex string with a length of 16 + */ function isLongId(string){ return hextest(string, 16); }; -//TODO needed anywhere? +// 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 new file mode 100644 index 00000000..d8f16c55 --- /dev/null +++ b/lang/js/src/Key.js @@ -0,0 +1,201 @@ +/* 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 {GPGMEJS_Error} from './Errors' + +export class GPGME_Key { + + constructor(fingerprint){ + if (isFingerprint(fingerprint) === true){ + this._fingerprint = fingerprint; + } else { + return new GPGMEJS_Error('WRONGPARAM', 'Key.js: invalid fingerprint'); + } + } + + get fingerprint(){ + return this._fingerprint; + } + + /** + * hasSecret returns true if a secret subkey is included in this Key + */ + get hasSecret(){ + checkKey(this._fingerprint, 'secret').then( function(result){ + return Promise.resolve(result); + }); + + } + + get isRevoked(){ + return checkKey(this._fingerprint, 'revoked'); + } + + get isExpired(){ + return checkKey(this._fingerprint, 'expired'); + } + + get isDisabled(){ + return checkKey(this._fingerprint, 'disabled'); + } + + get isInvalid(){ + return checkKey(this._fingerprint, 'invalid'); + } + + get canEncrypt(){ + return checkKey(this._fingerprint, 'can_encrypt'); + } + + get canSign(){ + return checkKey(this._fingerprint, 'can_sign'); + } + + get canCertify(){ + return checkKey(this._fingerprint, 'can_certify'); + } + + get canAuthenticate(){ + return checkKey(this._fingerprint, 'can_authenticate'); + } + + get isQualified(){ + return checkKey(this._fingerprint, 'is_qualified'); + } + + get armored(){ + let me = this; + return new Promise(function(resolve, reject){ + let conn = new Connection(); + conn.setFlag('armor', true); + conn.post('export',{'fpr': me._fingerprint}); + }); + // TODO return value not yet checked. Should result in an armored block + // in correct encoding + // TODO openpgpjs also returns secKey if private = true? + } + + /** + * 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 checkKey(this._fingerprint, '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]); + if (subkey instanceof GPGME_Key){ + resultset.push(subkey); + } + } + return Promise.resolve(resultset); + }); + } + + /** + * creation time stamp of the key + * @returns {Date|null} TBD + */ + get timestamp(){ + return checkKey(this._fingerprint, '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 checkKey(this._fingerprint, 'expires'); + // TODO convert to Date; check for 0 + } + + /** + * getter name TBD + * @returns {String|Array} The user ids associated with this key + */ + get userIds(){ + return checkKey(this._fingerprint, 'uids'); + } + + /** + * @returns {String} The public key algorithm supported by this subkey + */ + get pubkey_algo(){ + return checkKey(this._fingerprint, 'pubkey_algo'); + } +}; + +/** + * generic function to query gnupg information on a key. + * @param {*} fingerprint The identifier of the Keyring + * @param {*} property The gpgme-json property to check + * + */ +function checkKey(fingerprint, property){ + return Promise.reject(new GPGMEJS_Error('NOT_YET_IMPLEMENTED')); + + return new Promise(function(resolve, reject){ + if (!isFingerprint(fingerprint)){ + reject('not a fingerprint'); //TBD + } + let conn = new Connection(); + conn.post('getkey',{ // TODO not yet implemented in gpgme + 'fingerprint': this.fingerprint}) + .then(function(result){ + if (property !== undefined){ + if (result.hasOwnProperty(key)){ + resolve(result[property]); + } + else if (property == 'secret'){ + // property undefined means "not true" in case of secret + resolve(false); + } else { + reject('ERR_INVALID_PROPERTY') //TBD + } + } + + + resolve(result); + }, function(error){ + reject(error); + }); + }); +}; \ No newline at end of file diff --git a/lang/js/src/Keyring.js b/lang/js/src/Keyring.js new file mode 100644 index 00000000..52fa7f71 --- /dev/null +++ b/lang/js/src/Keyring.js @@ -0,0 +1,151 @@ +/* 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_Message} from './Message' +import {Connection} from './Connection' +import {GPGME_Key} from './Key' +import { isFingerprint, isLongId } from './Helpers'; + +export class GPGME_Keyring { + constructor(){ + this.reconnect(); + } + + /** + * (Re)-establishes the connection + * TODO TEMP: should we better use the connection of our parent, + * which we do not control? + */ + reconnect(){ + if (!this._connection || ! this._connection instanceof Connection){ + this._connection = new Connection; + } else { + this._connection.disconnect(); + this._connection.connect(); + } + } + + /** + * @param {String} (optional) pattern A pattern to search for, in userIds or KeyIds + * @param {Boolean} (optional) Include listing of secret keys + * @returns {Promise.>} + * + */ + getKeys(pattern, include_secret){ + let msg = new GPGME_Message; + msg.operation = 'listkeys'; + if (pattern && typeof(pattern) === 'string'){ + msg.setParameter('pattern', pattern); + } + if (include_secret){ + msg.setParameter('with-secret', true); + } + + this._connection.post(msg).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); + } + } + return Promise.resolve(resultset); + }); + } + + /** + * @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.defaultKey Only Keys marked as Default Keys + * @param {Boolean} flags.secret Only Keys containing a secret part. + * @param {Boolean} flags.valid Valid Keys only + * @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); + }); + } + +}; diff --git a/lang/js/src/Message.js b/lang/js/src/Message.js index 90b554a1..6a93b6f4 100644 --- a/lang/js/src/Message.js +++ b/lang/js/src/Message.js @@ -1,109 +1,113 @@ /* 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 { permittedOperations } from './permittedOperations' - +import { GPGMEJS_Error } from './Errors' export class GPGME_Message { //TODO getter - constructor(){ + constructor(operation){ + if (operation){ + this.operation(operation); + } } /** * Defines the operation this message will have - * @param {String} operation Mus be defined in permittedOperations + * @param {String} operation Must be defined in permittedOperations * TODO: move to constructor? */ set operation (operation){ if (!operation || typeof(operation) !== 'string'){ - throw('ERR_WRONG_PARAM'); + return new GPGMEJS_Error('WRONGPARAM'); } if (operation in permittedOperations){ if (!this._msg){ this._msg = {}; } this._msg.op = operation; } else { - throw('ERR_NOT_IMPLEMENTED'); + return new GPGMEJS_Error('WRONG_OP'); } } + get operation(){ + return this._msg.op; + } + /** * Sets a parameter for the message. Note that the operation has to be set * first, to be able to check if the parameter is permittted * @param {String} param Parameter to set * @param {any} value Value to set //TODO: Some type checking * @returns {Boolean} If the parameter was set successfully */ setParameter(param,value){ if (!param || typeof(param) !== 'string'){ - throw('ERR_WRONG_PARAM'); + return new GPGMEJS_Error('WRONGPARAM', 'type check failed'); } if (!this._msg || !this._msg.op){ - console.log('There is no operation specified yet. '+ - 'The parameter cannot be set'); - return false; + return new GPGMEJS_Error('MSG_OP_PENDING'); } let po = permittedOperations[this._msg.op]; if (!po){ - throw('LAZY_PROGRAMMER'); - //TODO - return false; + return new GPGMEJS_Error('WRONG_OP', param); } if (po.required.indexOf(param) >= 0 || po.optional.indexOf(param) >= 0){ this._msg[param] = value; return true; } - console.log('' + param + ' is invalid and could not be set'); - return false; + return new GPGMEJS_Error('WRONGPARAM', param); } /** * Check if the message has the minimum requirements to be sent, according * to the definitions in permittedOperations * @returns {Boolean} */ get isComplete(){ if (!this._msg.op){ return false; } let reqParams = permittedOperations[this._msg.op].required; for (let i=0; i < reqParams.length; i++){ - if (!reqParams[i] in this._msg){ + + if (!this._msg.hasOwnProperty(reqParams[i])){ + console.log(reqParams[i] + 'missing'); return false; } } return true; } /** * Returns the prepared message with parameters and completeness checked * @returns {Object|null} Object to be posted to gnupg, or null if * incomplete */ get message(){ if (this.isComplete === true){ return this._msg; } else { return null; } } } \ No newline at end of file diff --git a/lang/js/src/gpgmejs.js b/lang/js/src/gpgmejs.js index 8323ac3b..c23a356b 100644 --- a/lang/js/src/gpgmejs.js +++ b/lang/js/src/gpgmejs.js @@ -1,131 +1,177 @@ /* 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 {Connection} from "./Connection" import {GPGME_Message} from './Message' import {toKeyIdArray} from "./Helpers" +import {GPGMEJS_Error as Error, GPGMEJS_Error} from "./Errors" export class GpgME { /** - * initial check if connection si successfull. Will throw ERR_NO_CONNECT or - * ERR_NO_CONNECT_RLE (if chrome.runtime.lastError is available) if the - * connection fails. - * TODO The connection to the nativeMessaging host will, for now, be closed - * after each interaction. Session management with gpg_agent is TBD. + * initializes GpgME by opening a nativeMessaging port * TODO: add configuration */ - constructor(){ - let conn = new Connection(); - // this.keyring = new Keyring(); TBD - // TODO config, e.g. - this.configuration = { - null_expire_is_never: true - }; - conn.disconnect(); + constructor(configuration = { + null_expire_is_never: false + }){ + this._connection = new Connection; + } + + /** + * refreshes the nativeApp connection + */ + reconnect(){ + if (!this._connection || ! this._connection instanceof Connection){ + this._connection = new Connection; + } else { + this._connection.disconnect(); + this._connection.connect(); + } + } + + /** + * inmediately tries to destroy the nativeMessaging connection. + * TODO: may not be included in final API, as it is redundant. + * For now, it just serves paranoia + */ + disconnect(){ + if (this._connection){ + this._connection.disconnect(); + this._connection = null; + } + } + + /** + * tests the nativeApp connection + */ + get connected(){ + if (!this._connection || ! this._connection instanceof Connection){ + return false; + } + return this._connection.connected; } + /** * @param {String|Uint8Array} data text/data to be encrypted as String/Uint8Array * @param {GPGME_Key|String|Array|Array} publicKeys Keys used to encrypt the message * @param {Boolean} wildcard (optional) If true, recipient information will not be added to the message */ encrypt (data, publicKeys, wildcard=false){ let msg = new GPGME_Message; msg.operation = 'encrypt'; // TODO temporary msg.setParameter('armor', true); msg.setParameter('always-trust', true); let pubkeys = toKeyIdArray(publicKeys); msg.setParameter('keys', pubkeys); putData(msg, data); if (wildcard === true){msg.setParameter('throw-keyids', true); }; - if (msg.isComplete === true) { - let conn = new Connection(); - return (conn.post(msg.message)); - } - else { - return Promise.reject('NO_CONNECT'); - //TODO - } + return (this._connection.post(msg)); } /** * @param {String} data TODO Format: base64? String? Message with the encrypted data * @returns {Promise} decrypted message: data: The decrypted data. This may be base64 encoded. base64: Boolean indicating whether data is base64 encoded. mime: A Boolean indicating whether the data is a MIME object. info: An optional object with extra information. * @async */ decrypt(data){ if (data === undefined){ - throw('ERR_EMPTY_MSG'); + return Promise.reject(new GPGMEJS_Error ('EMPTY_MSG')); } let msg = new GPGME_Message; msg.operation = 'decrypt'; putData(msg, data); - // TODO: needs proper EOL to be decrypted. + return this._connection.post(msg); + + } - if (msg.isComplete === true){ - let conn = new Connection(); - return conn.post(msg.message); + deleteKey(key, delete_secret = false, no_confirm = false){ + return Promise.reject(new GPGMEJS_Error ('NOT_YET_IMPLEMENTED')); + let msg = new GPGME_Message; + msg.operation = 'deletekey'; + let key_arr = toKeyIdArray(key); + if (key_arr.length !== 1){ + throw('TODO'); + //should always be ONE key + } + msg.setParameter('key', key_arr[0]); + if (delete_secret === true){ + msg.setParameter('allow_secret', true); //TBD } - else { - return Promise.reject('NO_CONNECT'); - //TODO + if (no_confirm === true){ //TODO: Do we want this hidden deep in the code? + msg.setParameter('delete_force', true); //TBD } + this._connection.post(msg).then(function(success){ + //TODO: it seems that there is always errors coming back: + }, function(error){ + switch (error.msg){ + case 'ERR_NO_ERROR': + return Promise.resolve('okay'); //TBD + default: + return Promise.reject(new GPGMEJS_Error); + // INV_VALUE, + // GPG_ERR_NO_PUBKEY, + // GPG_ERR_AMBIGUOUS_NAME, + // GPG_ERR_CONFLICT + } + }); } + } /** * Sets the data of the message, converting Uint8Array to base64 and setting * the base64 flag * @param {GPGME_Message} message The message where this data will be set * @param {*} data The data to enter * @param {String} propertyname // TODO unchecked still */ function putData(message, data){ if (!message || !message instanceof GPGME_Message ) { - throw('NO_MESSAGE_OBJECT'); + return new GPGMEJS_Error('WRONGPARAM'); } if (!data){ //TODO Debug only! No data is legitimate console.log('Warning. no data in message'); message.setParameter('data', ''); } else if (data instanceof Uint8Array){ let decoder = new TextDecoder('utf8'); message.setParameter('base64', true); message.setParameter ('data', decoder.decode(data)); } else if (typeof(data) === 'string') { message.setParameter('base64', false); message.setParameter('data', data); } else { - throw('ERR_WRONG_TYPE'); + return new GPGMEJS_Error('WRONGPARAM'); } } \ No newline at end of file diff --git a/lang/js/src/gpgmejs_openpgpjs.js b/lang/js/src/gpgmejs_openpgpjs.js index 1eec4da4..54b9dd45 100644 --- a/lang/js/src/gpgmejs_openpgpjs.js +++ b/lang/js/src/gpgmejs_openpgpjs.js @@ -1,156 +1,201 @@ /* 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+ */ /** * This is a compatibility API to be used as openpgpjs syntax. * Non-implemented options will throw an error if set (not null or undefined) * TODO Some info about differences */ import { GpgME } from "./gpgmejs"; -// import {Keyring} from "./Keyring" TODO - + import {GPGME_Keyring} from "./Keyring" + import { GPGME_Key } from "./Key"; + import { isFingerprint } from "./Helpers" + import { GPGMEJS_Error } from './Errors' export class GpgME_openPGPCompatibility { constructor(){ - this._gpgme = new GpgME; + this._gpgme = new GpgME({ + null_expire_is_never: false + }); + this.Keyring = this.initKeyring(); } /** * Encrypt Message * Supported: * @param {String|Uint8Array} data * @param {Key|Array} publicKeys * @param {Boolean} wildcard * TODO: * @param {Key|Array} privateKeys * @param {String} filename * @param {module:enums.compression} compression * @param {Boolean} armor * @param {Boolean} detached * unsupported: * @param {String|Array} passwords * @param {Object} sessionKey * @param {Signature} signature * @param {Boolean} returnSessionKey * * @returns {Promise} * {data: ASCII armored message, * signature: detached signature if 'detached' is true * } * @async * @static */ encrypt({data = '', publicKeys = '', privateKeys, passwords, sessionKey, filename, compression, armor=true, detached=false, signature=null, returnSessionKey=null, wildcard=false, date=null}) { if (passwords !== undefined || sessionKey !== undefined || signature !== null || returnSessionKey !== null || date !== null){ - throw('NOT_IMPLEMENTED'); + return Promise.reject(new GPMGEJS_Error('NOT_IMPLEMENTED')); } if ( privateKeys || filename || compression || armor === false || detached == true){ - console.log('may be implemented later'); - throw('NOT_YET_IMPLEMENTED'); + return Promise.reject(new GPGMEJS_Error('NOT_YET_IMPLEMENTED')); } return this.GpgME.encrypt(data, translateKeyInput(publicKeys), wildcard); } /** Decrypt Message * supported * TODO: @param {Message} message TODO: for now it accepts an armored string only * Unsupported: * @param {String|Array} passwords * @param {Object|Array} sessionKeys * @param {Date} date * TODO * @param {Key|Array} privateKey * @param {Key|Array} publicKeys * @param {String} format (optional) return data format either as 'utf8' or 'binary' * @param {Signature} signature (optional) detached signature for verification * @returns {Promise} decrypted and verified message in the form: * { data:Uint8Array|String, filename:String, signatures:[{ keyid:String, valid:Boolean }] } * @async * @static */ decrypt({ message, privateKeys, passwords, sessionKeys, publicKeys, format='utf8', signature=null, date}) { if (passwords !== undefined || sessionKeys || date){ - - throw('NOT_IMPLEMENTED'); + return Promise.reject(new GPGMEJS_Error('NOT_IMPLEMENTED')); } if ( privateKeys || publicKeys || format !== 'utf8' || signature ){ - console.log('may be implemented later'); - throw('NOT_YET_IMPLEMENTED'); + return Promise.reject(new GPGMEJS_Error('NOT_YET_IMPLEMENTED')); } return this.GpgME.decrypt(message); // TODO: translate between: // openpgp: // { data:Uint8Array|String, // filename:String, // signatures:[{ keyid:String, valid:Boolean }] } // and gnupg: // data: The decrypted data. This may be base64 encoded. // base64: Boolean indicating whether data is base64 encoded. // mime: A Boolean indicating whether the data is a MIME object. // info: An optional object with extra information. } + initKeyring(){ + return new GPGME_Keyring_openPGPCompatibility; + } } /** - * - * @param {Object | String} Key Either a (presumably openpgp Key) Object with a - * primaryKeyproperty and a method getFingerprint, or a string. - * @returns {String} Unchecked string value claiming to be a fingerprint - * TODO: gpgmejs checks again, so it's okay here. + * Translation layer offering basic Keyring API to be used in Mailvelope. + * It may still be changed/expanded/merged with GPGME_Keyring */ -function translateKeyInput(Key){ - if (!Key){ - return []; +class GPGME_Keyring_openPGPCompatibility { + constructor(){ + this._gpgme_keyring = new GPGME_Keyring; } - if (!Array.isArray(Key)){ - Key = [Key]; + + /** + * Returns a GPGME_Key Object for each Key in the gnupg Keyring. This + * includes keys openpgpjs considers 'private' (usable for signing), with + * the difference that Key.armored will NOT contain any secret information. + * Please also note that a GPGME_Key does not offer full openpgpjs- Key + * compatibility. + * @returns {Array} with the objects offering at least: + * @property {String} armored The armored key block (does not include secret blocks) + * @property {Boolean} hasSecret Indicator if a private/secret key exists + * @property {Boolean} isDefault Indicator if private key exists and is the default key in this keyring + * @property {String} fingerprint The fingerprint identifying this key + * //TODO: Check if IsDefault is also always hasSecret + */ + getPublicKeys(){ + return this._gpgme_keyring.getKeys(null, true); } - let resultslist = []; - for (let i=0; i < Key.length; i++){ - if (typeof(Key[i]) === 'string'){ - resultslist.push(Key); - } else if ( - Key[i].hasOwnProperty(primaryKey) && - Key[i].primaryKey.hasOwnProperty(getFingerprint)){ - resultslist.push(Key[i].primaryKey.getFingerprint()); + + /** + * Returns the Default Key used for crypto operation in gnupg. + * Please note that the armored property does not contained secret key blocks, + * despite secret blocks being part of the key itself. + * @returns {Promise } + */ + getDefaultKey(){ + this._gpgme_keyring.getSubset({defaultKey: true}).then(function(result){ + if (result.length === 1){ + return Promise.resolve(result[0]); + } + else { + // TODO: Can there be "no default key"? + // TODO: Can there be several default keys? + return new GPGMEJS_Error; //TODO + } + }); + } + + /** + * Deletes a Key + * @param {Object} Object identifying key + * @param {String} key.fingerprint - fingerprint of the to be deleted key + * @param {Boolean} key.secret - indicator if private key should be deleted as well + + * @returns {Promise., Error>} TBD: Not sure what is wanted + TODO @throws {Error} error.code = ‘KEY_NOT_EXIST’ - there is no key for the given fingerprint + TODO @throws {Error} error.code = ‘NO_SECRET_KEY’ - secret indicator set, but no secret key exists + */ + deleteKey(key){ + if (typeof(key) !== "object"){ + return Promise.reject(new GPGMEJS_Error('WRONGPARAM')); + } + if ( !key.fingerprint || ! isFingerprint(key.fingerprint)){ + return Promise.reject(new GPGMEJS_Error('WRONGPARAM')); } + let key_to_delete = new GPGME_Key(key.fingerprint); + return key_to_delete.deleteKey(key.secret); } - return resultslist; -} \ No newline at end of file +}