diff options
Diffstat (limited to 'lang/js/src')
-rw-r--r-- | lang/js/src/Config.js | 31 | ||||
-rw-r--r-- | lang/js/src/Connection.js | 241 | ||||
-rw-r--r-- | lang/js/src/Errors.js | 129 | ||||
-rw-r--r-- | lang/js/src/Helpers.js | 103 | ||||
-rw-r--r-- | lang/js/src/Key.js | 244 | ||||
-rw-r--r-- | lang/js/src/Keyring.js | 162 | ||||
-rw-r--r-- | lang/js/src/Message.js | 196 | ||||
-rw-r--r-- | lang/js/src/gpgmejs.js | 192 | ||||
-rw-r--r-- | lang/js/src/index.js | 86 | ||||
-rw-r--r-- | lang/js/src/permittedOperations.js | 217 |
10 files changed, 1601 insertions, 0 deletions
diff --git a/lang/js/src/Config.js b/lang/js/src/Config.js new file mode 100644 index 00000000..e85bbb82 --- /dev/null +++ b/lang/js/src/Config.js @@ -0,0 +1,31 @@ +/* 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 <http://www.gnu.org/licenses/>. + * SPDX-License-Identifier: LGPL-2.1+ + */ + +export const availableConf = { + null_expire_is_never: [true, false], + // cachedKeys: Some Key info will not be queried on each invocation, + // manual refresh by Key.refresh() + cachedKeys: [true, false] +}; + +export const defaultConf = { + null_expire_is_never: false, + cachedKeys: false +};
\ No newline at end of file diff --git a/lang/js/src/Connection.js b/lang/js/src/Connection.js new file mode 100644 index 00000000..9c2a6428 --- /dev/null +++ b/lang/js/src/Connection.js @@ -0,0 +1,241 @@ +/* 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 <http://www.gnu.org/licenses/>. + * SPDX-License-Identifier: LGPL-2.1+ + */ + +/** + * A connection port will be opened for each communication between gpgmejs and + * gnupg. It should be alive as long as there are additional messages to be + * expected. + */ +import { permittedOperations } from './permittedOperations' +import { gpgme_error } from "./Errors" +import { GPGME_Message } from "./Message"; + +/** + * A Connection handles the nativeMessaging interaction. + */ +export class Connection{ + + constructor(){ + this.connect(); + let me = this; + } + + /** + * (Simple) Connection check. + * @returns {Boolean} true if the onDisconnect event has not been fired. + * Please note that the event listener of the port takes some time + * (5 ms seems enough) to react after the port is created. Then this will + * return undefined + */ + get isConnected(){ + return this._isConnected; + } + + /** + * Immediately closes the open port. + */ + disconnect() { + if (this._connection){ + this._connection.disconnect(); + } + } + + /** + * Opens a nativeMessaging port. + */ + connect(){ + if (this._isConnected === true){ + gpgme_error('CONN_ALREADY_CONNECTED'); + } else { + this._isConnected = true; + this._connection = chrome.runtime.connectNative('gpgmejson'); + let me = this; + this._connection.onDisconnect.addListener( + function(){ + me._isConnected = false; + } + ); + } + } + + /** + * Sends a message and resolves with the answer. + * @param {GPGME_Message} message + * @returns {Promise<Object>} the gnupg answer, or rejection with error + * information. + */ + post(message){ + if (!this.isConnected){ + return Promise.reject(gpgme_error('CONN_DISCONNECTED')); + } + if (!message || !message instanceof GPGME_Message){ + return Promise.reject(gpgme_error('PARAM_WRONG'), message); + } + if (message.isComplete !== true){ + return Promise.reject(gpgme_error('MSG_INCOMPLETE')); + } + let me = this; + return new Promise(function(resolve, reject){ + let answer = new Answer(message); + let listener = function(msg) { + if (!msg){ + me._connection.onMessage.removeListener(listener) + reject(gpgme_error('CONN_EMPTY_GPG_ANSWER')); + } else if (msg.type === "error"){ + me._connection.onMessage.removeListener(listener); + reject(gpgme_error('GNUPG_ERROR', msg.msg)); + } else { + let answer_result = answer.add(msg); + if (answer_result !== true){ + me._connection.onMessage.removeListener(listener); + reject(answer_result); + } + if (msg.more === true){ + me._connection.postMessage({'op': 'getmore'}); + } else { + me._connection.onMessage.removeListener(listener) + resolve(answer.message); + } + } + }; + + me._connection.onMessage.addListener(listener); + if (permittedOperations[message.operation].pinentry){ + return me._connection.postMessage(message.message); + } else { + return Promise.race([ + me._connection.postMessage(message.message), + function(resolve, reject){ + setTimeout(function(){ + reject(gpgme_error('CONN_TIMEOUT')); + }, 5000); + }]).then(function(result){ + return result; + }, function(reject){ + if(!reject instanceof Error) { + return gpgme_error('GNUPG_ERROR', reject); + } else { + return reject; + } + }); + } + }); + } +}; + +/** + * A class for answer objects, checking and processing the return messages of + * the nativeMessaging communication. + * @param {String} operation The operation, to look up validity of returning messages + */ +class Answer{ + + constructor(message){ + this.operation = message.operation; + this.expected = message.expected; + } + + /** + * Add the information to the answer + * @param {Object} msg The message as received with nativeMessaging + * returns true if successfull, gpgme_error otherwise + */ + add(msg){ + if (this._response === undefined){ + this._response = {}; + } + let messageKeys = Object.keys(msg); + let poa = permittedOperations[this.operation].answer; + if (messageKeys.length === 0){ + return gpgme_error('CONN_UNEXPECTED_ANSWER'); + } + for (let i= 0; i < messageKeys.length; i++){ + let key = messageKeys[i]; + switch (key) { + case 'type': + if ( msg.type !== 'error' && poa.type.indexOf(msg.type) < 0){ + return gpgme_error('CONN_UNEXPECTED_ANSWER'); + } + break; + case 'more': + break; + default: + //data should be concatenated + if (poa.data.indexOf(key) >= 0){ + if (!this._response.hasOwnProperty(key)){ + this._response[key] = ''; + } + this._response[key] += msg[key]; + } + //params should not change through the message + else if (poa.params.indexOf(key) >= 0){ + if (!this._response.hasOwnProperty(key)){ + this._response[key] = msg[key]; + } + else if (this._response[key] !== msg[key]){ + return gpgme_error('CONN_UNEXPECTED_ANSWER',msg[key]); + } + } + //infos may be json objects etc. Not yet defined. + // Pushing them into arrays for now + else if (poa.infos.indexOf(key) >= 0){ + if (!this._response.hasOwnProperty(key)){ + this._response[key] = []; + } + this._response.push(msg[key]); + } + else { + return gpgme_error('CONN_UNEXPECTED_ANSWER'); + } + break; + } + } + return true; + } + + /** + * @returns {Object} the assembled message, original data assumed to be + * (javascript-) strings + */ + get message(){ + let keys = Object.keys(this._response); + let msg = {}; + let poa = permittedOperations[this.operation].answer; + for (let i=0; i < keys.length; i++) { + if (poa.data.indexOf(keys[i]) >= 0 + && this._response.base64 === true + ) { + msg[keys[i]] = atob(this._response[keys[i]]); + if (this.expected === 'base64'){ + msg[keys[i]] = this._response[keys[i]]; + } else { + msg[keys[i]] = decodeURIComponent( + atob(this._response[keys[i]]).split('').map(function(c) { + return '%' + + ('00' + c.charCodeAt(0).toString(16)).slice(-2); + }).join('')); + } + } else { + msg[keys[i]] = this._response[keys[i]]; + } + } + return msg; + } +} diff --git a/lang/js/src/Errors.js b/lang/js/src/Errors.js new file mode 100644 index 00000000..bfe3a2f4 --- /dev/null +++ b/lang/js/src/Errors.js @@ -0,0 +1,129 @@ +/* gpgme.js - Javascript integration for gpgme + * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik + * + * This file is part of GPGME. + * + * GPGME is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * GPGME is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, see <http://www.gnu.org/licenses/>. + * SPDX-License-Identifier: LGPL-2.1+ + */ + +const err_list = { + // Connection + 'CONN_NO_CONNECT': { + msg:'Connection with the nativeMessaging host could not be' + + ' established.', + type: 'error' + }, + 'CONN_DISCONNECTED': { + msg:'Connection with the nativeMessaging host was lost.', + type: 'error' + }, + 'CONN_EMPTY_GPG_ANSWER':{ + msg: 'The nativeMessaging answer was empty.', + type: 'error' + }, + 'CONN_TIMEOUT': { + msg: 'A connection timeout was exceeded.', + type: 'error' + }, + 'CONN_UNEXPECTED_ANSWER': { + msg: 'The answer from gnupg was not as expected.', + type: 'error' + }, + 'CONN_ALREADY_CONNECTED':{ + msg: 'A connection was already established.', + type: 'warning' + }, + // Message/Data + 'MSG_INCOMPLETE': { + msg: 'The Message did not match the minimum requirements for' + + ' the interaction.', + type: 'error' + }, + 'MSG_EMPTY' : { + msg: 'The Message is empty.', + type: 'error' + }, + 'MSG_WRONG_OP': { + msg: 'The operation requested could not be found', + type: 'error' + }, + 'MSG_NO_KEYS' : { + msg: 'There were no valid keys provided.', + type: 'warning' + }, + 'MSG_NOT_A_FPR': { + msg: 'The String is not an accepted fingerprint', + type: 'warning' + }, + 'KEY_INVALID': { + msg:'Key object is invalid', + type: 'error' + }, + // generic + 'PARAM_WRONG':{ + msg: 'Invalid parameter was found', + type: 'error' + }, + 'PARAM_IGNORED': { + msg: 'An parameter was set that has no effect in gpgmejs', + type: 'warning' + }, + 'GENERIC_ERROR': { + msg: 'Unspecified error', + type: 'error' + } +}; + +/** + * Checks the given error code and returns an error object with some + * information about meaning and origin + * @param {*} code Error code. Should be in err_list or 'GNUPG_ERROR' + * @param {*} info Error message passed through if code is 'GNUPG_ERROR' + */ +export function gpgme_error(code = 'GENERIC_ERROR', info){ + if (err_list.hasOwnProperty(code)){ + if (err_list[code].type === 'error'){ + return new GPGME_Error(code); + } + if (err_list[code].type === 'warning'){ + console.warn(code + ': ' + err_list[code].msg); + } + return null; + } else if (code === 'GNUPG_ERROR'){ + return new GPGME_Error(code, info); + } + else { + return new GPGME_Error('GENERIC_ERROR'); + } +} + +class GPGME_Error extends Error{ + constructor(code, msg=''){ + if (code === 'GNUPG_ERROR' && typeof(msg) === 'string'){ + super(msg); + } else if (err_list.hasOwnProperty(code)){ + super(err_list[code].msg); + } else { + super(err_list['GENERIC_ERROR'].msg); + } + this.code = code || 'GENERIC_ERROR'; + } + set code(value){ + this._code = value; + } + get code(){ + return this._code; + } +}
\ No newline at end of file diff --git a/lang/js/src/Helpers.js b/lang/js/src/Helpers.js new file mode 100644 index 00000000..fd0e7200 --- /dev/null +++ b/lang/js/src/Helpers.js @@ -0,0 +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 <http://www.gnu.org/licenses/>. + * 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<Key>| GPGME_Key | Array<GPGME_Key>|String|Array<String>} input + * @returns {Array<String>} Array of fingerprints. + */ + +export function toKeyIdArray(input){ + if (!input){ + gpgme_error('MSG_NO_KEYS'); + return []; + } + if (!Array.isArray(input)){ + input = [input]; + } + let result = []; + for (let i=0; i < input.length; i++){ + if (typeof(input[i]) === 'string'){ + if (isFingerprint(input[i]) === true){ + result.push(input[i]); + } else { + gpgme_error('MSG_NOT_A_FPR'); + } + } else if (typeof(input[i]) === 'object'){ + let fpr = ''; + if (input[i] instanceof GPGME_Key){ + fpr = input[i].fingerprint; + } else if (input[i].hasOwnProperty('primaryKey') && + input[i].primaryKey.hasOwnProperty('getFingerprint')){ + fpr = input[i].primaryKey.getFingerprint(); + } + if (isFingerprint(fpr) === true){ + result.push(fpr); + } else { + gpgme_error('MSG_NOT_A_FPR'); + } + } else { + return gpgme_error('PARAM_WRONG'); + } + } + if (result.length === 0){ + gpgme_error('MSG_NO_KEYS'); + return []; + } else { + return result; + } +}; + +/** + * check if values are valid hexadecimal values of a specified length + * @param {*} key input value. + * @param {int} len the expected length of the value + */ +function hextest(key, len){ + if (!key || typeof(key) !== "string"){ + return false; + } + if (key.length !== len){ + return false; + } + let regexp= /^[0-9a-fA-F]*$/i; + return regexp.test(key); +}; + +/** + * check if the input is a valid Hex string with a length of 40 + */ +export function isFingerprint(string){ + return hextest(string, 40); +}; +/** + * TODO no usage; check if the input is a valid Hex string with a length of 16 + */ +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 new file mode 100644 index 00000000..075a190e --- /dev/null +++ b/lang/js/src/Key.js @@ -0,0 +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 <http://www.gnu.org/licenses/>. + * 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('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<GPGME_Key>} + */ + 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<String>} 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/Keyring.js b/lang/js/src/Keyring.js new file mode 100644 index 00000000..4596035a --- /dev/null +++ b/lang/js/src/Keyring.js @@ -0,0 +1,162 @@ +/* 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 <http://www.gnu.org/licenses/>. + * SPDX-License-Identifier: LGPL-2.1+ + */ + +import {createMessage} from './Message' +import {GPGME_Key} from './Key' +import { isFingerprint } from './Helpers'; +import { gpgme_error } from './Errors'; +import { Connection } from './Connection'; + +export class GPGME_Keyring { + constructor(connection){ + this.connection = connection; + } + + set connection(connection){ + if (!this._connection && connection instanceof Connection){ + this._connection = connection; + } + } + get connection(){ + if (this._connection instanceof Connection){ + if (this._connection.isConnected){ + return this._connection; + } + return gpgme_error('CONN_DISCONNECTED'); + } + return gpgme_error('CONN_NO_CONNECT'); + } + + /** + * @param {String} (optional) pattern A pattern to search for, in userIds or KeyIds + * @param {Boolean} (optional) Include listing of secret keys + * @returns {Promise.<Array<GPGME_Key>>} + * + */ + getKeys(pattern, include_secret){ + let msg = createMessage('listkeys'); + if (msg instanceof Error){ + return Promise.reject(msg); + } + if (pattern && typeof(pattern) === 'string'){ + msg.setParameter('pattern', pattern); + } + if (include_secret){ + msg.setParameter('with-secret', true); + } + let me = this; + + this.connection.post(msg).then(function(result){ + let fpr_list = []; + let resultset = []; + if (!Array.isArray(result.keys)){ + //TODO check assumption keys = Array<String fingerprints> + 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], me._connection); + if (newKey instanceof GPGME_Key){ + resultset.push(newKey); + } + } + return Promise.resolve(resultset); + }, function(error){ + //TODO error handling + }); + } + + /** + * @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<GPGME_Key>} + * + */ + 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 + }); + } + +}; diff --git a/lang/js/src/Message.js b/lang/js/src/Message.js new file mode 100644 index 00000000..932212a6 --- /dev/null +++ b/lang/js/src/Message.js @@ -0,0 +1,196 @@ +/* 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 <http://www.gnu.org/licenses/>. + * SPDX-License-Identifier: LGPL-2.1+ + */ +import { permittedOperations } from './permittedOperations' +import { gpgme_error } from './Errors' + +export function createMessage(operation){ + if (typeof(operation) !== 'string'){ + return gpgme_error('PARAM_WRONG'); + } + if (permittedOperations.hasOwnProperty(operation)){ + return new GPGME_Message(operation); + } else { + return gpgme_error('MSG_WRONG_OP'); + } +} + +/** + * Prepares a communication request. It checks operations and parameters in + * ./permittedOperations. + * @param {String} operation + */ +export class GPGME_Message { + //TODO getter + + constructor(operation){ + this.operation = operation; + this._expected = 'string'; + } + + set operation (op){ + if (typeof(op) === "string"){ + if (!this._msg){ + this._msg = {}; + } + if (!this._msg.op & permittedOperations.hasOwnProperty(op)){ + this._msg.op = op; + } + } + } + + get operation(){ + return this._msg.op; + } + + set expected(string){ + if (string === 'base64'){ + this._expected = 'base64'; + } + } + + get expected() { + if (this._expected === "base64"){ + return this._expected; + } + return "string"; + } + + /** + * 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'){ + return gpgme_error('PARAM_WRONG'); + } + let po = permittedOperations[this._msg.op]; + if (!po){ + return gpgme_error('MSG_WRONG_OP'); + } + let poparam = null; + if (po.required.hasOwnProperty(param)){ + poparam = po.required[param]; + } else if (po.optional.hasOwnProperty(param)){ + poparam = po.optional[param]; + } else { + return gpgme_error('PARAM_WRONG'); + } + let checktype = function(val){ + switch(typeof(val)){ + case 'string': + if (poparam.allowed.indexOf(typeof(val)) >= 0 + && val.length > 0) { + return true; + } + return gpgme_error('PARAM_WRONG'); + break; + case 'number': + if ( + poparam.allowed.indexOf('number') >= 0 + && isNaN(value) === false){ + return true; + } + return gpgme_error('PARAM_WRONG'); + break; + case 'boolean': + if (poparam.allowed.indexOf('boolean') >= 0){ + return true; + } + return gpgme_error('PARAM_WRONG'); + break; + case 'object': + if (Array.isArray(val)){ + if (poparam.array_allowed !== true){ + return gpgme_error('PARAM_WRONG'); + } + for (let i=0; i < val.length; i++){ + let res = checktype(val[i]); + if (res !== true){ + return res; + } + } + if (val.length > 0) { + return true; + } + } else if (val instanceof Uint8Array){ + if (poparam.allowed.indexOf('Uint8Array') >= 0){ + return true; + } + return gpgme_error('PARAM_WRONG'); + } else { + return gpgme_error('PARAM_WRONG'); + } + break; + default: + return gpgme_error('PARAM_WRONG'); + } + }; + let typechecked = checktype(value); + if (typechecked !== true){ + return typechecked; + } + if (poparam.hasOwnProperty('allowed_data')){ + if (poparam.allowed_data.indexOf(value) < 0){ + return gpgme_error('PARAM_WRONG'); + } + } + this._msg[param] = value; + return true; + } + + /** + * 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 = Object.keys( + permittedOperations[this._msg.op].required); + let msg_params = Object.keys(this._msg); + for (let i=0; i < reqParams.length; i++){ + if (msg_params.indexOf(reqParams[i]) < 0){ + 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; + } + + } +} diff --git a/lang/js/src/gpgmejs.js b/lang/js/src/gpgmejs.js new file mode 100644 index 00000000..3aa5957a --- /dev/null +++ b/lang/js/src/gpgmejs.js @@ -0,0 +1,192 @@ +/* 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 <http://www.gnu.org/licenses/>. + * SPDX-License-Identifier: LGPL-2.1+ + */ + +import {Connection} from "./Connection" +import {GPGME_Message, createMessage} from './Message' +import {toKeyIdArray} from "./Helpers" +import { gpgme_error } from "./Errors" +import { GPGME_Keyring } from "./Keyring"; + +export class GpgME { + /** + * initializes GpgME by opening a nativeMessaging port + * TODO: add configuration + */ + constructor(connection){ + this.connection = connection; + } + + set connection(conn){ + if (this._connection instanceof Connection){ + gpgme_error('CONN_ALREADY_CONNECTED'); + } else if (conn instanceof Connection){ + this._connection = conn; + } else { + gpgme_error('PARAM_WRONG'); + } + } + + get connection(){ + if (this._connection){ + if (this._connection.isConnected === true){ + return this._connection; + } + return undefined; + } + return undefined; + } + + set Keyring(keyring){ + if (keyring && keyring instanceof GPGME_Keyring){ + this._Keyring = keyring; + } + } + + get Keyring(){ + return this._Keyring; + } + + /** + * @param {String} data text/data to be encrypted as String + * @param {GPGME_Key|String|Array<String>|Array<GPGME_Key>} 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, base64=false, wildcard=false){ + + let msg = createMessage('encrypt'); + if (msg instanceof Error){ + return Promise.reject(msg) + } + // TODO temporary + msg.setParameter('armor', true); + msg.setParameter('always-trust', true); + if (base64 === true) { + msg.setParameter('base64', true); + } + let pubkeys = toKeyIdArray(publicKeys); + msg.setParameter('keys', pubkeys); + putData(msg, data); + if (wildcard === true){ + msg.setParameter('throw-keyids', true); + }; + if (msg.isComplete === true){ + return this.connection.post(msg); + } else { + return Promise.reject(gpgme_error('MSG_INCOMPLETE')); + } + } + + /** + * @param {String} data TODO base64? Message with the encrypted data + * @param {Boolean} base64 (optional) Response should stay base64 + * @returns {Promise<Object>} 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, base64=false){ + if (data === undefined){ + return Promise.reject(gpgme_error('MSG_EMPTY')); + } + let msg = createMessage('decrypt'); + if (base64 === true){ + msg.expected = 'base64'; + } + if (msg instanceof Error){ + return Promise.reject(msg); + } + putData(msg, data); + return this.connection.post(msg); + + } + + deleteKey(key, delete_secret = false, no_confirm = false){ + return Promise.reject(gpgme_error('NOT_YET_IMPLEMENTED')); + let msg = createMessage('deletekey'); + if (msg instanceof Error){ + return Promise.reject(msg); + } + let key_arr = toKeyIdArray(key); + if (key_arr.length !== 1){ + return Promise.reject( + gpgme_error('GENERIC_ERROR')); + // TBD should always be ONE key? + } + msg.setParameter('key', key_arr[0]); + if (delete_secret === true){ + msg.setParameter('allow_secret', true); + // TBD + } + if (no_confirm === true){ //TODO: Do we want this hidden deep in the code? + msg.setParameter('delete_force', true); + // TBD + } + if (msg.isComplete === true){ + 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(gpgme_error('TODO') ); // + // INV_VALUE, + // GPG_ERR_NO_PUBKEY, + // GPG_ERR_AMBIGUOUS_NAME, + // GPG_ERR_CONFLICT + } + }); + } else { + return Promise.reject(gpgme_error('MSG_INCOMPLETE')); + } + } +} + +/** + * Sets the data of the message + * @param {GPGME_Message} message The message where this data will be set + * @param {*} data The data to enter + */ +function putData(message, data){ + if (!message || !message instanceof GPGME_Message ) { + return gpgme_error('PARAM_WRONG'); + } + if (!data){ + return gpgme_error('PARAM_WRONG'); + } else if (typeof(data) === 'string') { + message.setParameter('data', data); + } else if ( + typeof(data) === 'object' && + typeof(data.getText) === 'function' + ){ + let txt = data.getText(); + if (typeof(txt) === 'string'){ + message.setParameter('data', txt); + } else { + return gpgme_error('PARAM_WRONG'); + } + + } else { + return gpgme_error('PARAM_WRONG'); + } +} diff --git a/lang/js/src/index.js b/lang/js/src/index.js new file mode 100644 index 00000000..8527b3f3 --- /dev/null +++ b/lang/js/src/index.js @@ -0,0 +1,86 @@ +/* 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 <http://www.gnu.org/licenses/>. + * SPDX-License-Identifier: LGPL-2.1+ + */ + +import { GpgME } from "./gpgmejs"; +import { gpgme_error } from "./Errors"; +import { Connection } from "./Connection"; +import { defaultConf, availableConf } from "./Config"; + +/** + * Initializes a nativeMessaging Connection and returns a GPGMEjs object + * @param {Object} config Configuration. See Config.js for available parameters. Still TODO + */ +function init(config){ + let _conf = parseconfiguration(config); + if (_conf instanceof Error){ + return Promise.reject(_conf); + } + return new Promise(function(resolve, reject){ + let connection = new Connection; + // TODO: Delayed reaction is ugly. We need to listen to the port's + // event listener in isConnected, but in some cases this takes some + // time (<5ms) to disconnect if there is no successfull connection. + let delayedreaction = function(){ + if (connection === undefined) { + reject(gpgme_error('CONN_NO_CONNECT')); + } + if (connection.isConnected === true){ + resolve(new GpgME(connection, _conf)); + } else { + reject(gpgme_error('CONN_NO_CONNECT')); + } + }; + setTimeout(delayedreaction, 5); + }); +} + +function parseconfiguration(rawconfig = {}){ + if ( typeof(rawconfig) !== 'object'){ + return gpgme_error('PARAM_WRONG'); + }; + let result_config = {}; + let conf_keys = Object.keys(rawconfig); + + for (let i=0; i < conf_keys.length; i++){ + + if (availableConf.hasOwnProperty(conf_keys[i])){ + let value = rawconfig[conf_keys[i]]; + if (availableConf[conf_keys[i]].indexOf(value) < 0){ + return gpgme_error('PARAM_WRONG'); + } else { + result_config[conf_keys[i]] = value; + } + } + else { + return gpgme_error('PARAM_WRONG'); + } + } + let default_keys = Object.keys(defaultConf); + for (let j=0; j < default_keys.length; j++){ + if (!result_config.hasOwnProperty(default_keys[j])){ + result_config[default_keys[j]] = defaultConf[default_keys[j]]; + } + } + return result_config; +}; + +export default { + init: init +}
\ No newline at end of file diff --git a/lang/js/src/permittedOperations.js b/lang/js/src/permittedOperations.js new file mode 100644 index 00000000..da46a1fd --- /dev/null +++ b/lang/js/src/permittedOperations.js @@ -0,0 +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 <http://www.gnu.org/licenses/>. + * SPDX-License-Identifier: LGPL-2.1+ + */ + + /** + * Definition of the possible interactions with gpgme-json. + * operation: <Object> + required: Array<Object> + <String> 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: <Array> If present, restricts to the given value + optional: Array<Object> + 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: <Object> + type: <String< The content type of answer expected + data: Array<String> The payload property of the answer. May be + partial and in need of concatenation + params: Array<String> Information that do not change throughout + the message + infos: Array<*> arbitrary information that may result in a list + } + } + */ + +export const permittedOperations = { + encrypt: { + required: { + 'keys': { + allowed: ['string'], + array_allowed: true + }, + 'data': { + allowed: ['string'] + } + }, + optional: { + 'protocol': { + allowed: ['string'], + allowed_data: ['cms', 'openpgp'] + }, + 'chunksize': { + allowed: ['number'] + }, + 'base64': { + allowed: ['boolean'] + }, + 'mime': { + allowed: ['boolean'] + }, + 'armor': { + allowed: ['boolean'] + }, + 'always-trust': { + allowed: ['boolean'] + }, + 'no-encrypt-to': { + allowed: ['string'], + array_allowed: true + }, + 'no-compress': { + allowed: ['boolean'] + }, + 'throw-keyids': { + allowed: ['boolean'] + }, + 'want-address': { + allowed: ['boolean'] + }, + 'wrap': { + allowed: ['boolean'] + }, + }, + answer: { + type: ['ciphertext'], + data: ['data'], + params: ['base64'], + infos: [] + } + }, + + decrypt: { + pinentry: true, + required: { + 'data': { + allowed: ['string'] + } + }, + optional: { + 'protocol': { + allowed: ['string'], + allowed_data: ['cms', 'openpgp'] + }, + 'chunksize': { + allowed: ['number'], + }, + 'base64': { + allowed: ['boolean'] + } + }, + answer: { + type: ['plaintext'], + data: ['data'], + params: ['base64', 'mime'], + infos: [] // TODO pending. Info about signatures and validity + //{ + //signatures: [{ + //Key : <String>Fingerprint, + //valid: <Boolean> + // }] + } + }, + /** TBD: querying the Key's information (keyinfo) + TBD name: { + required: { + 'fingerprint': { + allowed: ['string'] + }, + }, + answer: { + type: ['TBD'], + data: [], + params: ['hasSecret','isRevoked','isExpired','armored', + 'timestamp','expires','pubkey_algo'], + infos: ['subkeys', 'userIds'] + // {'hasSecret': <Boolean>, + // 'isRevoked': <Boolean>, + // 'isExpired': <Boolean>, + // 'armored': <String>, // armored public Key block + // 'timestamp': <Number>, // + // 'expires': <Number>, + // 'pubkey_algo': TBD // TBD (optional?), + // 'userIds': Array<String>, + // 'subkeys': Array<String> Fingerprints of Subkeys + // } + }*/ + + /** + listkeys:{ + required: {}; + optional: { + 'with-secret':{ + allowed: ['boolean'] + },{ + 'pattern': { + allowed: ['string'] + } + }, + answer: { + type: ['TBD'], + infos: ['TBD'] + // keys: Array<String> Fingerprints representing the results + }, + */ + + /** + importkey: { + required: { + 'keyarmored': { + allowed: ['string'] + } + }, + answer: { + type: ['TBD'], + infos: ['TBD'], + // for each key if import was a success, + // and if it was an update of preexisting key + } + }, + */ + + /** + deletekey: { + pinentry: true, + required: { + 'fingerprint': { + allowed: ['string'], + // array_allowed: TBD Allow several Keys to be deleted at once? + }, + optional: { + 'TBD' //Flag to delete secret Key ? + } + answer: { + type ['TBD'], + infos: [''] + // TBD (optional) Some kind of 'ok' if delete was successful. + } + } + */ + + /** + *TBD get armored secret different treatment from keyinfo! + * TBD key modification? + * encryptsign: TBD + * verify: TBD + */ +} |