diff options
Diffstat (limited to 'lang/js/src')
-rw-r--r-- | lang/js/src/Connection.js | 283 | ||||
-rw-r--r-- | lang/js/src/Errors.js | 169 | ||||
-rw-r--r-- | lang/js/src/Helpers.js | 137 | ||||
-rw-r--r-- | lang/js/src/Key.js | 688 | ||||
-rw-r--r-- | lang/js/src/Keyring.js | 435 | ||||
-rw-r--r-- | lang/js/src/Makefile.am | 30 | ||||
-rw-r--r-- | lang/js/src/Message.js | 239 | ||||
-rw-r--r-- | lang/js/src/Signature.js | 200 | ||||
-rw-r--r-- | lang/js/src/gpgmejs.js | 391 | ||||
-rw-r--r-- | lang/js/src/index.js | 52 | ||||
-rw-r--r-- | lang/js/src/permittedOperations.js | 403 |
11 files changed, 3027 insertions, 0 deletions
diff --git a/lang/js/src/Connection.js b/lang/js/src/Connection.js new file mode 100644 index 00000000..928ac681 --- /dev/null +++ b/lang/js/src/Connection.js @@ -0,0 +1,283 @@ +/* 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+ + * + * Author(s): + * Maximilian Krambach <[email protected]> + */ + +/* global chrome */ + +import { permittedOperations } from './permittedOperations'; +import { gpgme_error } from './Errors'; +import { GPGME_Message, createMessage } from './Message'; +import { decode } from './Helpers'; + +/** + * A Connection handles the nativeMessaging interaction via a port. As the + * protocol only allows up to 1MB of message sent from the nativeApp to the + * browser, the connection will stay open until all parts of a communication + * are finished. For a new request, a new port will open, to avoid mixing + * contexts. + * @class + */ +export class Connection{ + + constructor (){ + this._connection = chrome.runtime.connectNative('gpgmejson'); + } + + /** + * Immediately closes an open port. + */ + disconnect () { + if (this._connection){ + this._connection.disconnect(); + this._connection = null; + } + } + + + /** + * @typedef {Object} backEndDetails + * @property {String} gpgme Version number of gpgme + * @property {Array<Object>} info Further information about the backend + * and the used applications (Example: + * { + * "protocol": "OpenPGP", + * "fname": "/usr/bin/gpg", + * "version": "2.2.6", + * "req_version": "1.4.0", + * "homedir": "default" + * } + */ + + /** + * Retrieves the information about the backend. + * @param {Boolean} details (optional) If set to false, the promise will + * just return if a connection was successful. + * @returns {Promise<backEndDetails>|Promise<Boolean>} Details from the + * backend + * @async + */ + checkConnection (details = true){ + const msg = createMessage('version'); + if (details === true) { + return this.post(msg); + } else { + let me = this; + return new Promise(function (resolve) { + Promise.race([ + me.post(msg), + new Promise(function (resolve, reject){ + setTimeout(function (){ + reject(gpgme_error('CONN_TIMEOUT')); + }, 500); + }) + ]).then(function (){ // success + resolve(true); + }, function (){ // failure + resolve(false); + }); + }); + } + } + + /** + * Sends a {@link GPGME_Message} via tghe nativeMessaging port. It + * resolves with the completed answer after all parts have been + * received and reassembled, or rejects with an {@link GPGME_Error}. + * + * @param {GPGME_Message} message + * @returns {Promise<Object>} The collected answer + * @async + */ + post (message){ + if (!message || !(message instanceof GPGME_Message)){ + this.disconnect(); + return Promise.reject(gpgme_error( + 'PARAM_WRONG', 'Connection.post')); + } + if (message.isComplete() !== true){ + this.disconnect(); + return Promise.reject(gpgme_error('MSG_INCOMPLETE')); + } + let chunksize = message.chunksize; + const me = this; + return new Promise(function (resolve, reject){ + let answer = new Answer(message); + let listener = function (msg) { + if (!msg){ + me._connection.onMessage.removeListener(listener); + me._connection.disconnect(); + reject(gpgme_error('CONN_EMPTY_GPG_ANSWER')); + } else { + let answer_result = answer.collect(msg); + if (answer_result !== true){ + me._connection.onMessage.removeListener(listener); + me._connection.disconnect(); + reject(answer_result); + } else { + if (msg.more === true){ + me._connection.postMessage({ + 'op': 'getmore', + 'chunksize': chunksize + }); + } else { + me._connection.onMessage.removeListener(listener); + me._connection.disconnect(); + const message = answer.getMessage(); + if (message instanceof Error){ + reject(message); + } else { + resolve(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 (){ + me._connection.disconnect(); + reject(gpgme_error('CONN_TIMEOUT')); + }, 5000); + } + ]).then(function (result){ + return result; + }, function (reject){ + if (!(reject instanceof Error)) { + me._connection.disconnect(); + return gpgme_error('GNUPG_ERROR', reject); + } else { + return reject; + } + }); + } + }); + } +} + + +/** + * A class for answer objects, checking and processing the return messages of + * the nativeMessaging communication. + * @protected + */ +class Answer{ + + /** + * @param {GPGME_Message} message + */ + constructor (message){ + this._operation = message.operation; + this._expected = message.expected; + this._response_b64 = null; + } + + get operation (){ + return this._operation; + } + + get expected (){ + return this._expected; + } + + /** + * Adds incoming base64 encoded data to the existing response + * @param {*} msg base64 encoded data. + * @returns {Boolean} + * + * @private + */ + collect (msg){ + if (typeof (msg) !== 'object' || !msg.hasOwnProperty('response')) { + return gpgme_error('CONN_UNEXPECTED_ANSWER'); + } + if (!this._response_b64){ + this._response_b64 = msg.response; + return true; + } else { + this._response_b64 += msg.response; + return true; + } + } + /** + * Returns the base64 encoded answer data with the content verified + * against {@link permittedOperations}. + */ + getMessage (){ + if (this._response_b64 === null){ + return gpgme_error('CONN_UNEXPECTED_ANSWER'); + } + let _decodedResponse = JSON.parse(atob(this._response_b64)); + let _response = {}; + let messageKeys = Object.keys(_decodedResponse); + 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 (_decodedResponse.type === 'error'){ + return (gpgme_error('GNUPG_ERROR', + decode(_decodedResponse.msg))); + } else if (poa.type.indexOf(_decodedResponse.type) < 0){ + return gpgme_error('CONN_UNEXPECTED_ANSWER'); + } + break; + case 'base64': + break; + case 'msg': + if (_decodedResponse.type === 'error'){ + return (gpgme_error('GNUPG_ERROR', _decodedResponse.msg)); + } + break; + default: + if (!poa.data.hasOwnProperty(key)){ + return gpgme_error('CONN_UNEXPECTED_ANSWER'); + } + if ( typeof (_decodedResponse[key]) !== poa.data[key] ){ + return gpgme_error('CONN_UNEXPECTED_ANSWER'); + } + if (_decodedResponse.base64 === true + && poa.data[key] === 'string' + && this.expected !== 'base64' + ){ + _response[key] = decodeURIComponent( + atob(_decodedResponse[key]).split('').map( + function (c) { + return '%' + + ('00' + c.charCodeAt(0).toString(16)).slice(-2); + }).join('')); + } else { + _response[key] = decode(_decodedResponse[key]); + } + break; + } + } + return _response; + } +} diff --git a/lang/js/src/Errors.js b/lang/js/src/Errors.js new file mode 100644 index 00000000..73418028 --- /dev/null +++ b/lang/js/src/Errors.js @@ -0,0 +1,169 @@ +/* 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+ + * + * Author(s): + * Maximilian Krambach <[email protected]> + */ + +/** + * Listing of all possible error codes and messages of a {@link GPGME_Error}. + */ +export const err_list = { + // Connection + 'CONN_NO_CONNECT': { + msg:'Connection with the nativeMessaging host could not be' + + ' established.', + type: 'error' + }, + 'CONN_EMPTY_GPG_ANSWER':{ + msg: 'The nativeMessaging answer was empty.', + type: 'error' + }, + 'CONN_TIMEOUT': { + msg: 'A connection timeout was exceeded.', + type: 'error' + }, + 'CONN_UNEXPECTED_ANSWER': { + msg: 'The answer from gnupg was not as expected.', + type: 'error' + }, + 'CONN_ALREADY_CONNECTED':{ + msg: 'A connection was already established.', + type: 'warning' + }, + // Message/Data + 'MSG_INCOMPLETE': { + msg: 'The Message did not match the minimum requirements for' + + ' the interaction.', + type: 'error' + }, + 'MSG_EMPTY' : { + msg: 'The Message is empty.', + type: 'error' + }, + 'MSG_WRONG_OP': { + msg: 'The operation requested could not be found', + type: 'error' + }, + 'MSG_NO_KEYS' : { + msg: 'There were no valid keys provided.', + type: 'warning' + }, + 'MSG_NOT_A_FPR': { + msg: 'The String is not an accepted fingerprint', + type: 'warning' + }, + 'KEY_INVALID': { + msg:'Key object is invalid', + type: 'error' + }, + 'KEY_NOKEY': { + msg:'This key does not exist in GPG', + type: 'error' + }, + 'KEY_NO_INIT': { + msg:'This property has not been retrieved yet from GPG', + type: 'error' + }, + 'KEY_ASYNC_ONLY': { + msg: 'This property cannot be used in synchronous calls', + type: 'error' + }, + 'KEY_NO_DEFAULT': { + msg:'A default key could not be established. Please check yout gpg ' + + 'configuration', + type: 'error' + }, + 'SIG_WRONG': { + msg:'A malformed signature was created', + type: 'error' + }, + 'SIG_NO_SIGS': { + msg:'There were no signatures found', + 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 {@link GPGME_Error} 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' + * @returns {GPGME_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'){ + // eslint-disable-next-line no-console + // 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'); + } +} + +/** + * An error class with additional info about the origin of the error, as string + * @property {String} code Short description of origin and type of the error + * @property {String} msg Additional info + * @class + * @protected + * @extends Error + */ +class GPGME_Error extends Error{ + constructor (code = 'GENERIC_ERROR', msg=''){ + + if (code === 'GNUPG_ERROR' && typeof (msg) === 'string'){ + super(msg); + } else if (err_list.hasOwnProperty(code)){ + if (msg){ + super(err_list[code].msg + '--' + msg); + } else { + super(err_list[code].msg); + } + } else { + super(err_list['GENERIC_ERROR'].msg); + } + this._code = code; + } + + 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..ba4277ab --- /dev/null +++ b/lang/js/src/Helpers.js @@ -0,0 +1,137 @@ +/* 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+ + * + * Author(s): + * Maximilian Krambach <[email protected]> + */ + +import { gpgme_error } from './Errors'; + +/** + * Tries to return an array of fingerprints, either from input fingerprints or + * from Key objects (openpgp Keys or GPGME_Keys are both accepted). + * + * @param {Object | Array<Object> | String | Array<String>} input + * @returns {Array<String>} Array of fingerprints, or an empty array + */ +export function toKeyIdArray (input){ + if (!input){ + 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 { + // MSG_NOT_A_FPR is just a console warning if warning enabled + // in src/Errors.js + gpgme_error('MSG_NOT_A_FPR'); + } + } else if (typeof (input[i]) === 'object'){ + let fpr = ''; + if (input[i].hasOwnProperty('fingerprint')){ + 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){ + return []; + } else { + return result; + } +} + +/** + * Check if values are valid hexadecimal values of a specified length + * @param {String} key input value. + * @param {int} len the expected length of the value + * @returns {Boolean} true if value passes test + * @private + */ +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 Fingerprint + * (Hex string with a length of 40 characters) + * @param {String} value to check + * @returns {Boolean} true if value passes test + */ +export function isFingerprint (value){ + return hextest(value, 40); +} + +/** + * check if the input is a valid gnupg long ID (Hex string with a length of 16 + * characters) + * @param {String} value to check + * @returns {Boolean} true if value passes test + */ +export function isLongId (value){ + return hextest(value, 16); +} + +/** + * Recursively decodes input (utf8) to output (utf-16; javascript) strings + * @param {Object | Array | String} property + */ +export function decode (property){ + if (typeof property === 'string'){ + return decodeURIComponent(escape(property)); + } else if (Array.isArray(property)){ + let res = []; + for (let arr=0; arr < property.length; arr++){ + res.push(decode(property[arr])); + } + return res; + } else if (typeof property === 'object'){ + const keys = Object.keys(property); + if (keys.length){ + let res = {}; + for (let k=0; k < keys.length; k++ ){ + res[keys[k]] = decode(property[keys[k]]); + } + return res; + } + return property; + } + return property; +}
\ No newline at end of file diff --git a/lang/js/src/Key.js b/lang/js/src/Key.js new file mode 100644 index 00000000..d0f87eda --- /dev/null +++ b/lang/js/src/Key.js @@ -0,0 +1,688 @@ +/* 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+ + * + * Author(s): + * Maximilian Krambach <[email protected]> + */ + +import { isFingerprint, isLongId } from './Helpers'; +import { gpgme_error } from './Errors'; +import { createMessage } from './Message'; + +/** + * Validates the given fingerprint and creates a new {@link GPGME_Key} + * @param {String} fingerprint + * @param {Boolean} async If True, Key properties (except fingerprint) will be + * queried from gnupg on each call, making the operation up-to-date, the + * answers will be Promises, and the performance will likely suffer + * @param {Object} data additional initial properties this Key will have. Needs + * a full object as delivered by gpgme-json + * @returns {Object} The verified and updated data + */ +export function createKey (fingerprint, async = false, data){ + if (!isFingerprint(fingerprint) || typeof (async) !== 'boolean'){ + throw gpgme_error('PARAM_WRONG'); + } + if (data !== undefined){ + data = validateKeyData(fingerprint, data); + } + if (data instanceof Error){ + throw gpgme_error('KEY_INVALID'); + } else { + return new GPGME_Key(fingerprint, async, data); + } +} + +/** + * Represents the Keys as stored in the gnupg backend + * It allows to query almost all information defined in gpgme Key Objects + * Refer to {@link validKeyProperties} for available information, and the gpgme + * documentation on their meaning + * (https://www.gnupg.org/documentation/manuals/gpgme/Key-objects.html) + * + * @class + */ +class GPGME_Key { + + constructor (fingerprint, async, data){ + + /** + * @property {Boolean} If true, most answers will be asynchronous + */ + this._async = async; + + this._data = { fingerprint: fingerprint.toUpperCase() }; + if (data !== undefined + && data.fingerprint.toUpperCase() === this._data.fingerprint + ) { + this._data = data; + } + } + + /** + * Query any property of the Key listed in {@link validKeyProperties} + * @param {String} property property to be retreived + * @returns {Boolean| String | Date | Array | Object} + * the value of the property. If the Key is set to Async, the value + * will be fetched from gnupg and resolved as a Promise. If Key is not + * async, the armored property is not available (it can still be + * retrieved asynchronously by {@link Key.getArmor}) + */ + get (property) { + if (this._async === true) { + switch (property){ + case 'armored': + return this.getArmor(); + case 'hasSecret': + return this.getGnupgSecretState(); + default: + return getGnupgState(this.fingerprint, property); + } + } else { + if (property === 'armored') { + throw gpgme_error('KEY_ASYNC_ONLY'); + } + // eslint-disable-next-line no-use-before-define + if (!validKeyProperties.hasOwnProperty(property)){ + throw gpgme_error('PARAM_WRONG'); + } else { + return (this._data[property]); + } + } + } + + /** + * Reloads the Key information from gnupg. This is only useful if you + * use the GPGME_Keys cached. Note that this is a performance hungry + * operation. If you desire more than a few refreshs, it may be + * advisable to run {@link Keyring.getKeys} instead. + * @returns {Promise<GPGME_Key|GPGME_Error>} + * @async + */ + 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); + msg.post().then(function (result){ + if (result.keys.length === 1){ + const newdata = validateKeyData( + me._data.fingerprint, result.keys[0]); + if (newdata instanceof Error){ + reject(gpgme_error('KEY_INVALID')); + } else { + me._data = newdata; + me.getGnupgSecretState().then(function (){ + me.getArmor().then(function (){ + resolve(me); + }, function (error){ + reject(error); + }); + }, function (error){ + reject(error); + }); + } + } else { + reject(gpgme_error('KEY_NOKEY')); + } + }, function (error) { + reject(gpgme_error('GNUPG_ERROR'), error); + }); + }); + } + + /** + * Query the armored block of the Key directly from gnupg. Please note + * that this will not get you any export of the secret/private parts of + * a Key + * @returns {Promise<String|GPGME_Error>} + * @async + */ + getArmor () { + const 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){ + resolve(result.data); + }, function (error){ + reject(error); + }); + }); + } + + /** + * Find out if the Key is part of a Key pair including public and + * private key(s). If you want this information about more than a few + * Keys in synchronous mode, it may be advisable to run + * {@link Keyring.getKeys} instead, as it performs faster in bulk + * querying this state. + * @returns {Promise<Boolean|GPGME_Error>} True if a private Key is + * available in the gnupg Keyring. + * @async + */ + getGnupgSecretState (){ + const me = this; + return new Promise(function (resolve, reject) { + if (!me._data.fingerprint){ + reject(gpgme_error('KEY_INVALID')); + } else { + 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 && + result.keys.length === 1 && + result.keys[0].secret === true + ) { + me._data.hasSecret = true; + resolve(true); + } else { + me._data.hasSecret = false; + resolve(false); + } + }, function (error){ + reject(error); + }); + } + }); + } + + /** + * Deletes the (public) Key from the GPG Keyring. Note that a deletion + * of a secret key is not supported by the native backend. + * @returns {Promise<Boolean|GPGME_Error>} Success if key was deleted, + * rejects with a GPG error otherwise. + */ + delete (){ + const me = this; + return new Promise(function (resolve, reject){ + if (!me._data.fingerprint){ + reject(gpgme_error('KEY_INVALID')); + } + let msg = createMessage('delete'); + msg.setParameter('key', me._data.fingerprint); + msg.post().then(function (result){ + resolve(result.success); + }, function (error){ + reject(error); + }); + }); + } + + /** + * @returns {String} The fingerprint defining this Key. Convenience getter + */ + get fingerprint (){ + return this._data.fingerprint; + } +} + +/** + * Representing a subkey of a Key. + * @class + * @protected + */ +class GPGME_Subkey { + + /** + * Initializes with the json data sent by gpgme-json + * @param {Object} data + * @private + */ + constructor (data){ + this._data = {}; + let keys = Object.keys(data); + const me = this; + + /** + * Validates a subkey property against {@link validSubKeyProperties} and + * sets it if validation is successful + * @param {String} property + * @param {*} value + * @param private + */ + const setProperty = function (property, value){ + // eslint-disable-next-line no-use-before-define + if (validSubKeyProperties.hasOwnProperty(property)){ + // eslint-disable-next-line no-use-before-define + if (validSubKeyProperties[property](value) === true) { + if (property === 'timestamp' || property === 'expires'){ + me._data[property] = new Date(value * 1000); + } else { + me._data[property] = value; + } + } + } + }; + for (let i=0; i< keys.length; i++) { + setProperty(keys[i], data[keys[i]]); + } + } + + /** + * Fetches any information about this subkey + * @param {String} property Information to request + * @returns {String | Number | Date} + */ + get (property) { + if (this._data.hasOwnProperty(property)){ + return (this._data[property]); + } + } + +} + +/** + * Representing user attributes associated with a Key or subkey + * @class + * @protected + */ +class GPGME_UserId { + + /** + * Initializes with the json data sent by gpgme-json + * @param {Object} data + * @private + */ + constructor (data){ + this._data = {}; + const me = this; + let keys = Object.keys(data); + const setProperty = function (property, value){ + // eslint-disable-next-line no-use-before-define + if (validUserIdProperties.hasOwnProperty(property)){ + // eslint-disable-next-line no-use-before-define + if (validUserIdProperties[property](value) === true) { + if (property === 'last_update'){ + me._data[property] = new Date(value*1000); + } else { + me._data[property] = value; + } + } + } + }; + for (let i=0; i< keys.length; i++) { + setProperty(keys[i], data[keys[i]]); + } + } + + /** + * Fetches information about the user + * @param {String} property Information to request + * @returns {String | Number} + */ + get (property) { + if (this._data.hasOwnProperty(property)){ + return (this._data[property]); + } + } + +} + +/** + * Validation definition for userIds. Each valid userId property is represented + * as a key- Value pair, with their value being a validation function to check + * against + * @protected + * @const + */ +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); + } +}; + +/** + * Validation definition for subKeys. Each valid userId property is represented + * as a key-value pair, with the value being a validation function + * @protected + * @const + */ +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); + } +}; + +/** + * Validation definition for Keys. Each valid Key property is represented + * as a key-value pair, with their value being a validation function. For + * details on the meanings, please refer to the gpgme documentation + * https://www.gnupg.org/documentation/manuals/gpgme/Key-objects.html#Key-objects + * @param {String} fingerprint + * @param {Boolean} revoked + * @param {Boolean} expired + * @param {Boolean} disabled + * @param {Boolean} invalid + * @param {Boolean} can_encrypt + * @param {Boolean} can_sign + * @param {Boolean} can_certify + * @param {Boolean} can_authenticate + * @param {Boolean} secret + * @param {Boolean}is_qualified + * @param {String} protocol + * @param {String} issuer_serial + * @param {String} issuer_name + * @param {Boolean} chain_id + * @param {String} owner_trust + * @param {Date} last_update + * @param {String} origin + * @param {Array<GPGME_Subkey>} subkeys + * @param {Array<GPGME_UserId>} userids + * @param {Array<String>} tofu + * @param {Boolean} hasSecret + * @protected + * @const + */ +const validKeyProperties = { + 'fingerprint': function (value){ + return isFingerprint(value); + }, + 'revoked': function (value){ + return typeof (value) === 'boolean'; + }, + 'expired': function (value){ + return typeof (value) === 'boolean'; + }, + 'disabled': function (value){ + return typeof (value) === 'boolean'; + }, + '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'; + }, + 'protocol': function (value){ + return typeof (value) === 'string'; + // TODO check for implemented ones + }, + 'issuer_serial': function (value){ + return typeof (value) === 'string'; + }, + 'issuer_name': function (value){ + return typeof (value) === 'string'; + }, + 'chain_id': function (value){ + return typeof (value) === 'string'; + }, + 'owner_trust': function (value){ + return typeof (value) === 'string'; + }, + 'last_update': function (value){ + return (Number.isInteger(value)); + // TODO undefined/null possible? + }, + 'origin': function (value){ + return (Number.isInteger(value)); + }, + 'subkeys': function (value){ + return (Array.isArray(value)); + }, + 'userids': function (value){ + return (Array.isArray(value)); + }, + 'tofu': function (value){ + return (Array.isArray(value)); + }, + 'hasSecret': function (value){ + return typeof (value) === 'boolean'; + } + +}; + +/** +* sets the Key data in bulk. It can only be used from inside a Key, either +* during construction or on a refresh callback. +* @param {Object} key the original internal key data. +* @param {Object} data Bulk set the data for this key, with an Object structure +* as sent by gpgme-json. +* @returns {Object|GPGME_Error} the changed data after values have been set, +* an error if something went wrong. +* @private +*/ +function validateKeyData (fingerprint, data){ + const key = {}; + if (!fingerprint || typeof (data) !== 'object' || !data.fingerprint + || fingerprint !== data.fingerprint.toUpperCase() + ){ + return gpgme_error('KEY_INVALID'); + } + let props = Object.keys(data); + for (let i=0; i< props.length; i++){ + if (!validKeyProperties.hasOwnProperty(props[i])){ + return gpgme_error('KEY_INVALID'); + } + // running the defined validation function + if (validKeyProperties[props[i]](data[props[i]]) !== true ){ + return gpgme_error('KEY_INVALID'); + } + switch (props[i]){ + case 'subkeys': + key.subkeys = []; + for (let i=0; i< data.subkeys.length; i++) { + key.subkeys.push( + new GPGME_Subkey(data.subkeys[i])); + } + break; + case 'userids': + key.userids = []; + for (let i=0; i< data.userids.length; i++) { + key.userids.push( + new GPGME_UserId(data.userids[i])); + } + break; + case 'last_update': + key[props[i]] = new Date( data[props[i]] * 1000 ); + break; + default: + key[props[i]] = data[props[i]]; + } + } + return key; +} + +/** + * Fetches and sets properties from gnupg + * @param {String} fingerprint + * @param {String} property to search for. + * @private + * @async + */ +function getGnupgState (fingerprint, property){ + return new Promise(function (resolve, reject) { + if (!isFingerprint(fingerprint)) { + reject(gpgme_error('KEY_INVALID')); + } else { + let msg = createMessage('keylist'); + msg.setParameter('keys', fingerprint); + msg.post().then(function (res){ + if (!res.keys || res.keys.length !== 1){ + reject(gpgme_error('KEY_INVALID')); + } else { + const key = res.keys[0]; + let result; + switch (property){ + case 'subkeys': + result = []; + if (key.subkeys.length){ + for (let i=0; i < key.subkeys.length; i++) { + result.push( + new GPGME_Subkey(key.subkeys[i])); + } + } + resolve(result); + break; + case 'userids': + result = []; + if (key.userids.length){ + for (let i=0; i< key.userids.length; i++) { + result.push( + new GPGME_UserId(key.userids[i])); + } + } + resolve(result); + break; + case 'last_update': + if (key.last_update === undefined){ + reject(gpgme_error('CONN_UNEXPECTED_ANSWER')); + } else if (key.last_update !== null){ + resolve(new Date( key.last_update * 1000)); + } else { + resolve(null); + } + break; + default: + if (!validKeyProperties.hasOwnProperty(property)){ + reject(gpgme_error('PARAM_WRONG')); + } else { + if (key.hasOwnProperty(property)){ + resolve(key[property]); + } else { + reject(gpgme_error( + 'CONN_UNEXPECTED_ANSWER')); + } + } + break; + } + } + }, function (error){ + reject(gpgme_error(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..cb053ba1 --- /dev/null +++ b/lang/js/src/Keyring.js @@ -0,0 +1,435 @@ +/* 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+ + * + * Author(s): + * Maximilian Krambach <[email protected]> + */ + + +import { createMessage } from './Message'; +import { createKey } from './Key'; +import { isFingerprint } from './Helpers'; +import { gpgme_error } from './Errors'; + +/** + * This class offers access to the gnupg keyring + */ +export class GPGME_Keyring { + + /** + * Queries Keys (all Keys or a subset) from gnupg. + * + * @param {String | Array<String>} pattern (optional) A pattern to + * search for in userIds or KeyIds. + * @param {Boolean} prepare_sync (optional) if set to true, most data + * (with the exception of armored Key blocks) will be cached for the + * Keys. This enables direct, synchronous use of these properties for + * all keys. It does not check for changes on the backend. The cached + * information can be updated with the {@link Key.refresh} method. + * @param {Boolean} search (optional) retrieve Keys from external + * servers with the method(s) defined in gnupg (e.g. WKD/HKP lookup) + * @returns {Promise<Array<GPGME_Key>>} + * @static + * @async + */ + getKeys (pattern, prepare_sync=false, search=false){ + return new Promise(function (resolve, reject) { + let msg = createMessage('keylist'); + if (pattern !== undefined && pattern !== null){ + msg.setParameter('keys', pattern); + } + msg.setParameter('sigs', true); + if (search === true){ + msg.setParameter('locate', true); + } + msg.post().then(function (result){ + let resultset = []; + if (result.keys.length === 0){ + resolve([]); + } else { + let secondrequest; + if (prepare_sync === true) { + secondrequest = function () { + let msg2 = createMessage('keylist'); + if (pattern){ + msg2.setParameter('keys', pattern); + } + msg2.setParameter('secret', true); + return msg2.post(); + }; + } else { + secondrequest = function () { + return Promise.resolve(true); + }; + } + secondrequest().then(function (answer) { + for (let i=0; i < result.keys.length; i++){ + if (prepare_sync === true){ + if (answer && answer.keys) { + for (let j=0; + j < answer.keys.length; j++ ){ + const a = answer.keys[j]; + const b = result.keys[i]; + if ( + a.fingerprint === b.fingerprint + ) { + if (a.secret === true){ + b.hasSecret = true; + } else { + b.hasSecret = false; + } + break; + } + } + } + } + let k = createKey(result.keys[i].fingerprint, + !prepare_sync, result.keys[i]); + resultset.push(k); + } + resolve(resultset); + }, function (error){ + reject(error); + }); + } + }); + }); + } + + /** + * @typedef {Object} exportResult The result of a getKeysArmored + * operation. + * @property {String} armored The public Key(s) as armored block. Note + * that the result is one armored block, and not a block per key. + * @property {Array<String>} secret_fprs (optional) list of + * fingerprints for those Keys that also have a secret Key available in + * gnupg. The secret key will not be exported, but the fingerprint can + * be used in operations needing a secret key. + */ + + /** + * Fetches the armored public Key blocks for all Keys matching the + * pattern (if no pattern is given, fetches all keys known to gnupg). + * @param {String|Array<String>} pattern (optional) The Pattern to + * search for + * @param {Boolean} with_secret_fpr (optional) also return a list of + * fingerprints for the keys that have a secret key available + * @returns {Promise<exportResult|GPGME_Error>} Object containing the + * armored Key(s) and additional information. + * @static + * @async + */ + getKeysArmored (pattern, with_secret_fpr) { + return new Promise(function (resolve, reject) { + let msg = createMessage('export'); + msg.setParameter('armor', true); + if (with_secret_fpr === true) { + msg.setParameter('with-sec-fprs', true); + } + if (pattern !== undefined && pattern !== null){ + msg.setParameter('keys', pattern); + } + msg.post().then(function (answer){ + const result = { armored: answer.data }; + if (with_secret_fpr === true + && answer.hasOwnProperty('sec-fprs') + ) { + result.secret_fprs = answer['sec-fprs']; + } + resolve(result); + }, function (error){ + reject(error); + }); + }); + } + + /** + * Returns the Key used by default in gnupg. + * (a.k.a. 'primary Key or 'main key'). + * It looks up the gpg configuration if set, or the first key that + * contains a secret key. + * + * @returns {Promise<GPGME_Key|GPGME_Error>} + * @async + * @static + */ + getDefaultKey (prepare_sync = false) { + let me = this; + return new Promise(function (resolve, reject){ + let msg = createMessage('config_opt'); + msg.setParameter('component', 'gpg'); + msg.setParameter('option', 'default-key'); + msg.post().then(function (resp){ + if (resp.option !== undefined + && resp.option.hasOwnProperty('value') + && resp.option.value.length === 1 + && resp.option.value[0].hasOwnProperty('string') + && typeof (resp.option.value[0].string) === 'string'){ + me.getKeys(resp.option.value[0].string, true).then( + function (keys){ + if (keys.length === 1){ + resolve(keys[0]); + } else { + reject(gpgme_error('KEY_NO_DEFAULT')); + } + }, function (error){ + reject(error); + }); + } else { + let msg = createMessage('keylist'); + msg.setParameter('secret', true); + msg.post().then(function (result){ + if (result.keys.length === 0){ + reject(gpgme_error('KEY_NO_DEFAULT')); + } else { + for (let i=0; i< result.keys.length; i++ ) { + if (result.keys[i].invalid === false) { + let k = createKey( + result.keys[i].fingerprint, + !prepare_sync, + result.keys[i]); + resolve(k); + break; + } else if (i === result.keys.length - 1){ + reject(gpgme_error('KEY_NO_DEFAULT')); + } + } + } + }, function (error){ + reject(error); + }); + } + }, function (error){ + reject(error); + }); + }); + } + + /** + * @typedef {Object} importResult The result of a Key update + * @property {Object} summary Numerical summary of the result. See the + * feedbackValues variable for available Keys values and the gnupg + * documentation. + * https://www.gnupg.org/documentation/manuals/gpgme/Importing-Keys.html + * for details on their meaning. + * @property {Array<importedKeyResult>} Keys Array of Object containing + * GPGME_Keys with additional import information + * + */ + + /** + * @typedef {Object} importedKeyResult + * @property {GPGME_Key} key The resulting key + * @property {String} status: + * 'nochange' if the Key was not changed, + * 'newkey' if the Key was imported in gpg, and did not exist + * previously, + * 'change' if the key existed, but details were updated. For details, + * Key.changes is available. + * @property {Boolean} changes.userId Changes in userIds + * @property {Boolean} changes.signature Changes in signatures + * @property {Boolean} changes.subkey Changes in subkeys + */ + + /** + * Import an armored Key block into gnupg. Note that this currently + * will not succeed on private Key blocks. + * @param {String} armored Armored Key block of the Key(s) to be + * imported into gnupg + * @param {Boolean} prepare_sync prepare the keys for synched use + * (see {@link getKeys}). + * @returns {Promise<importResult>} A summary and Keys considered. + * @async + * @static + */ + importKey (armored, prepare_sync) { + let feedbackValues = ['considered', 'no_user_id', 'imported', + 'imported_rsa', 'unchanged', 'new_user_ids', 'new_sub_keys', + 'new_signatures', 'new_revocations', 'secret_read', + 'secret_imported', 'secret_unchanged', 'skipped_new_keys', + 'not_imported', 'skipped_v3_keys']; + if (!armored || typeof (armored) !== 'string'){ + return Promise.reject(gpgme_error('PARAM_WRONG')); + } + let me = this; + return new Promise(function (resolve, reject){ + let msg = createMessage('import'); + msg.setParameter('data', armored); + msg.post().then(function (response){ + let infos = {}; + let fprs = []; + let summary = {}; + for (let i=0; i < feedbackValues.length; i++ ){ + summary[feedbackValues[i]] = + response.result[feedbackValues[i]]; + } + if (!response.result.hasOwnProperty('imports') || + response.result.imports.length === 0 + ){ + resolve({ Keys:[],summary: summary }); + return; + } + for (let res=0; res<response.result.imports.length; res++){ + let result = response.result.imports[res]; + let status = ''; + if (result.status === 0){ + status = 'nochange'; + } else if ((result.status & 1) === 1){ + status = 'newkey'; + } else { + status = 'change'; + } + let changes = {}; + changes.userId = (result.status & 2) === 2; + changes.signature = (result.status & 4) === 4; + changes.subkey = (result.status & 8) === 8; + // 16 new secret key: not implemented + + fprs.push(result.fingerprint); + infos[result.fingerprint] = { + changes: changes, + status: status + }; + } + let resultset = []; + if (prepare_sync === true){ + me.getKeys(fprs, true).then(function (result){ + for (let i=0; i < result.length; i++) { + resultset.push({ + key: result[i], + changes: + infos[result[i].fingerprint].changes, + status: infos[result[i].fingerprint].status + }); + } + resolve({ Keys:resultset,summary: summary }); + }, function (error){ + reject(error); + }); + } else { + for (let i=0; i < fprs.length; i++) { + resultset.push({ + key: createKey(fprs[i]), + changes: infos[fprs[i]].changes, + status: infos[fprs[i]].status + }); + } + resolve({ Keys:resultset,summary:summary }); + } + + }, function (error){ + reject(error); + }); + + + }); + + + } + + /** + * Convenience function for deleting a Key. See {@link Key.delete} for + * further information about the return values. + * @param {String} fingerprint + * @returns {Promise<Boolean|GPGME_Error>} + * @async + * @static + */ + deleteKey (fingerprint){ + if (isFingerprint(fingerprint) === true) { + let key = createKey(fingerprint); + return key.delete(); + } else { + return Promise.reject(gpgme_error('KEY_INVALID')); + } + } + + /** + * Generates a new Key pair directly in gpg, and returns a GPGME_Key + * representing that Key. Please note that due to security concerns, + * secret Keys can not be deleted or exported from inside gpgme.js. + * + * @param {String} userId The user Id, e.g. 'Foo Bar <[email protected]>' + * @param {String} algo (optional) algorithm (and optionally key size) + * to be used. See {@link supportedKeyAlgos} below for supported + * values. If ommitted, 'default' is used. + * @param {Number} expires (optional) Expiration time in seconds from now. + * If not set or set to 0, expiration will be 'never' + * @param {String} subkey_algo (optional) algorithm of the encryption + * subkey. If ommited the same as algo is used. + * + * @return {Promise<Key|GPGME_Error>} + * @async + */ + generateKey (userId, algo = 'default', expires, subkey_algo){ + if ( + typeof (userId) !== 'string' || + // eslint-disable-next-line no-use-before-define + supportedKeyAlgos.indexOf(algo) < 0 || + (expires && !( Number.isInteger(expires) || expires < 0 )) + ){ + return Promise.reject(gpgme_error('PARAM_WRONG')); + } + // eslint-disable-next-line no-use-before-define + if (subkey_algo && supportedKeyAlgos.indexOf(subkey_algo) < 0 ){ + return Promise.reject(gpgme_error('PARAM_WRONG')); + } + let me = this; + return new Promise(function (resolve, reject){ + let msg = createMessage('createkey'); + msg.setParameter('userid', userId); + msg.setParameter('algo', algo ); + if (subkey_algo) { + msg.setParameter('subkey-algo', subkey_algo ); + } + if (expires){ + msg.setParameter('expires', expires); + } else { + msg.setParameter('expires', 0); + } + msg.post().then(function (response){ + me.getKeys(response.fingerprint, true).then( + // TODO prepare_sync? + function (result){ + resolve(result); + }, function (error){ + reject(error); + }); + }, function (error) { + reject(error); + }); + }); + } +} + + +/** + * List of algorithms supported for key generation. Please refer to the gnupg + * documentation for details + */ +const supportedKeyAlgos = [ + 'default', + 'rsa', 'rsa2048', 'rsa3072', 'rsa4096', + 'dsa', 'dsa2048', 'dsa3072', 'dsa4096', + 'elg', 'elg2048', 'elg3072', 'elg4096', + 'ed25519', + 'cv25519', + 'brainpoolP256r1', 'brainpoolP384r1', 'brainpoolP512r1', + 'NIST P-256', 'NIST P-384', 'NIST P-521' +];
\ No newline at end of file diff --git a/lang/js/src/Makefile.am b/lang/js/src/Makefile.am new file mode 100644 index 00000000..dc58fd31 --- /dev/null +++ b/lang/js/src/Makefile.am @@ -0,0 +1,30 @@ +# Makefile.am for gpgme.js. +# Copyright (C) 2018 Intevation GmbH +# +# This file is part of GPGME. +# +# gpgme.js is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# gpgme.js 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 General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA +# 02111-1307, USA + +EXTRA_DIST = Connection.js \ + Errors.js \ + gpgmejs.js \ + Helpers.js \ + index.js \ + Key.js \ + Keyring.js \ + Message.js \ + permittedOperations.js \ + Signature.js diff --git a/lang/js/src/Message.js b/lang/js/src/Message.js new file mode 100644 index 00000000..b83caf6d --- /dev/null +++ b/lang/js/src/Message.js @@ -0,0 +1,239 @@ +/* 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+ + * + * Author(s): + * Maximilian Krambach <[email protected]> + */ + +import { permittedOperations } from './permittedOperations'; +import { gpgme_error } from './Errors'; +import { Connection } from './Connection'; + +/** + * Initializes a message for gnupg, validating the message's purpose with + * {@link permittedOperations} first + * @param {String} operation + * @returns {GPGME_Message} The Message object + */ +export function createMessage (operation){ + if (typeof (operation) !== 'string'){ + throw gpgme_error('PARAM_WRONG'); + } + if (permittedOperations.hasOwnProperty(operation)){ + return new GPGME_Message(operation); + } else { + throw gpgme_error('MSG_WRONG_OP'); + } +} + +/** + * A Message collects, validates and handles all information required to + * successfully establish a meaningful communication with gpgme-json via + * {@link Connection.post}. The definition on which communication is available + * can be found in {@link permittedOperations}. + * @class + */ +export class GPGME_Message { + + constructor (operation){ + this._msg = { + op: operation, + chunksize: 1023* 1024 + }; + this._expected = null; + } + + get operation (){ + return this._msg.op; + } + + set expected (value){ + if (value === 'base64'){ + this._expected = value; + } + } + + get expected () { + return this._expected; + } + /** + * The maximum size of responses from gpgme in bytes. As of July 2018, + * most browsers will only accept answers up to 1 MB of size. + * Everything above that threshold will not pass through + * nativeMessaging; answers that are larger need to be sent in parts. + * The lower limit is set to 10 KB. Messages smaller than the threshold + * will not encounter problems, larger messages will be received in + * chunks. If the value is not explicitly specified, 1023 KB is used. + */ + set chunksize (value){ + if ( + Number.isInteger(value) && + value > 10 * 1024 && + value <= 1024 * 1024 + ){ + this._msg.chunksize = value; + } + } + + get chunksize (){ + return this._msg.chunksize; + } + + /** + * 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; + } + } + + /** + * Sets a parameter for the message. It validates with + * {@link permittedOperations} + * @param {String} param Parameter to set + * @param {any} value Value to set + * @returns {Boolean} If the parameter was set successfully + */ + setParameter ( param,value ){ + if (!param || typeof (param) !== 'string'){ + throw gpgme_error('PARAM_WRONG'); + } + let po = permittedOperations[this._msg.op]; + if (!po){ + throw 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 { + throw gpgme_error('PARAM_WRONG'); + } + // check incoming value for correctness + let checktype = function (val){ + switch (typeof (val)){ + case 'string': + if (poparam.allowed.indexOf(typeof (val)) >= 0 + && val.length > 0) { + return true; + } + throw gpgme_error('PARAM_WRONG'); + case 'number': + if ( + poparam.allowed.indexOf('number') >= 0 + && isNaN(value) === false){ + return true; + } + throw gpgme_error('PARAM_WRONG'); + + case 'boolean': + if (poparam.allowed.indexOf('boolean') >= 0){ + return true; + } + throw gpgme_error('PARAM_WRONG'); + case 'object': + if (Array.isArray(val)){ + if (poparam.array_allowed !== true){ + throw 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; + } + throw gpgme_error('PARAM_WRONG'); + } else { + throw gpgme_error('PARAM_WRONG'); + } + break; + default: + throw 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, that is + * all 'required' parameters according to {@link permittedOperations}. + * @returns {Boolean} true if message is complete. + */ + 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){ + return false; + } + } + return true; + } + /** + * Sends the Message via nativeMessaging and resolves with the answer. + * @returns {Promise<Object|GPGME_Error>} + * @async + */ + post (){ + let me = this; + return new Promise(function (resolve, reject) { + if (me.isComplete() === true) { + + let conn = new Connection; + conn.post(me).then(function (response) { + resolve(response); + }, function (reason) { + reject(reason); + }); + } + else { + reject(gpgme_error('MSG_INCOMPLETE')); + } + }); + } + +} diff --git a/lang/js/src/Signature.js b/lang/js/src/Signature.js new file mode 100644 index 00000000..a6539048 --- /dev/null +++ b/lang/js/src/Signature.js @@ -0,0 +1,200 @@ +/* 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+ + * + * Author(s): + * Maximilian Krambach <[email protected]> + */ +import { gpgme_error } from './Errors'; + +/** + * Validates an object containing a signature, as sent by the nativeMessaging + * interface + * @param {Object} sigObject Object as returned by gpgme-json. The definition + * of the expected values are to be found in {@link expKeys}, {@link expSum}, + * {@link expNote}. + * @returns {GPGME_Signature|GPGME_Error} Signature Object + */ +export function createSignature (sigObject){ + if ( + typeof (sigObject) !=='object' || + !sigObject.hasOwnProperty('summary') || + !sigObject.hasOwnProperty('fingerprint') || + !sigObject.hasOwnProperty('timestamp') + // TODO check if timestamp is mandatory in specification + ){ + return gpgme_error('SIG_WRONG'); + } + let keys = Object.keys(sigObject); + for (let i=0; i< keys.length; i++){ + // eslint-disable-next-line no-use-before-define + if ( typeof (sigObject[keys[i]]) !== expKeys[keys[i]] ){ + return gpgme_error('SIG_WRONG'); + } + } + let sumkeys = Object.keys(sigObject.summary); + for (let i=0; i< sumkeys.length; i++){ + // eslint-disable-next-line no-use-before-define + if ( typeof (sigObject.summary[sumkeys[i]]) !== expSum[sumkeys[i]] ){ + return gpgme_error('SIG_WRONG'); + } + } + if (sigObject.hasOwnProperty('notations')){ + if (!Array.isArray(sigObject.notations)){ + return gpgme_error('SIG_WRONG'); + } + for (let i=0; i < sigObject.notations.length; i++){ + let notation = sigObject.notations[i]; + let notekeys = Object.keys(notation); + for (let j=0; j < notekeys.length; j++){ + // eslint-disable-next-line no-use-before-define + if ( typeof (notation[notekeys[j]]) !== expNote[notekeys[j]] ){ + return gpgme_error('SIG_WRONG'); + } + } + } + } + return new GPGME_Signature(sigObject); +} + + +/** + * Representing the details of a signature. The full details as given by + * gpgme-json can be read from the _rawSigObject. + * + * Note to reviewers: This class should be read only except via + * {@link createSignature} + * @protected + * @class + */ +class GPGME_Signature { + + constructor (sigObject){ + this._rawSigObject = sigObject; + } + get fingerprint (){ + if (!this._rawSigObject.fingerprint){ + return gpgme_error('SIG_WRONG'); + } else { + return this._rawSigObject.fingerprint; + } + } + + /** + * The expiration of this Signature as Javascript date, or null if + * signature does not expire + * @returns {Date | null} + */ + get expiration (){ + if (!this._rawSigObject.exp_timestamp){ + return null; + } + return new Date(this._rawSigObject.exp_timestamp* 1000); + } + + /** + * The creation date of this Signature in Javascript Date + * @returns {Date} + */ + get timestamp (){ + return new Date(this._rawSigObject.timestamp * 1000); + } + + /** + * The overall validity of the key. If false, errorDetails may contain + * additional information. + */ + get valid () { + if (this._rawSigObject.summary.valid === true){ + return true; + } else { + return false; + } + } + + /** + * gives more information on non-valid signatures. Refer to the gpgme + * docs https://www.gnupg.org/documentation/manuals/gpgme/Verify.html + * for details on the values. + * @returns {Object} Object with boolean properties + */ + get errorDetails (){ + let properties = ['revoked', 'key-expired', 'sig-expired', + 'key-missing', 'crl-missing', 'crl-too-old', 'bad-policy', + 'sys-error']; + let result = {}; + for (let i=0; i< properties.length; i++){ + if ( this._rawSigObject.hasOwnProperty(properties[i]) ){ + result[properties[i]] = this._rawSigObject[properties[i]]; + } + } + return result; + } +} + +/** + * Keys and their value's type for the signature Object + */ +const expKeys = { + 'wrong_key_usage': 'boolean', + 'chain_model': 'boolean', + 'summary': 'object', + 'is_de_vs': 'boolean', + 'status_string':'string', + 'fingerprint':'string', + 'validity_string': 'string', + 'pubkey_algo_name':'string', + 'hash_algo_name':'string', + 'pka_address':'string', + 'status_code':'number', + 'timestamp':'number', + 'exp_timestamp':'number', + 'pka_trust':'number', + 'validity':'number', + 'validity_reason':'number', + 'notations': 'object' +}; + +/** + * Keys and their value's type for the summary + */ +const expSum = { + 'valid': 'boolean', + 'green': 'boolean', + 'red': 'boolean', + 'revoked': 'boolean', + 'key-expired': 'boolean', + 'sig-expired': 'boolean', + 'key-missing': 'boolean', + 'crl-missing': 'boolean', + 'crl-too-old': 'boolean', + 'bad-policy': 'boolean', + 'sys-error': 'boolean', + 'sigsum': 'object' +}; + +/** + * Keys and their value's type for notations objects + */ +const expNote = { + 'human_readable': 'boolean', + 'critical':'boolean', + 'name': 'string', + 'value': 'string', + 'flags': 'number' +}; diff --git a/lang/js/src/gpgmejs.js b/lang/js/src/gpgmejs.js new file mode 100644 index 00000000..7692298f --- /dev/null +++ b/lang/js/src/gpgmejs.js @@ -0,0 +1,391 @@ +/* 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+ + * + * Author(s): + * Maximilian Krambach <[email protected]> + */ + + +import { GPGME_Message, createMessage } from './Message'; +import { toKeyIdArray } from './Helpers'; +import { gpgme_error } from './Errors'; +import { GPGME_Keyring } from './Keyring'; +import { createSignature } from './Signature'; + +/** + * @typedef {Object} decrypt_result + * @property {String} data The decrypted data + * @property {Boolean} base64 indicating whether data is base64 encoded. + * @property {Boolean} is_mime (optional) the data claims to be a MIME + * object. + * @property {String} file_name (optional) the original file name + * @property {signatureDetails} signatures Verification details for + * signatures + */ + +/** + * @typedef {Object} signatureDetails + * @property {Boolean} all_valid Summary if all signatures are fully valid + * @property {Number} count Number of signatures found + * @property {Number} failures Number of invalid signatures + * @property {Array<GPGME_Signature>} signatures.good All valid signatures + * @property {Array<GPGME_Signature>} signatures.bad All invalid signatures + */ + +/** + * @typedef {Object} encrypt_result The result of an encrypt operation + * @property {String} data The encrypted message + * @property {Boolean} base64 Indicating whether data is base64 encoded. + */ + +/** + * @typedef { GPGME_Key | String | Object } inputKeys + * Accepts different identifiers of a gnupg Key that can be parsed by + * {@link toKeyIdArray}. Expected inputs are: One or an array of + * GPGME_Keys; one or an array of fingerprint strings; one or an array of + * openpgpjs Key objects. + */ + +/** + * @typedef {Object} signResult The result of a signing operation + * @property {String} data The resulting data. Includes the signature in + * clearsign mode + * @property {String} signature The detached signature (if in detached mode) + */ + +/** @typedef {Object} verifyResult The result of a verification + * @property {Boolean} data: The verified data + * @property {Boolean} is_mime (optional) the data claims to be a MIME + * object. + * @property {String} file_name (optional) the original file name + * @property {signatureDetails} signatures Verification details for + * signatures + */ + +/** + * The main entry point for gpgme.js. + * @class + */ +export class GpgME { + + constructor (){ + this._Keyring = null; + } + + /** + * setter for {@link setKeyring}. + * @param {GPGME_Keyring} keyring A Keyring to use + */ + set Keyring (keyring){ + if (keyring && keyring instanceof GPGME_Keyring){ + this._Keyring = keyring; + } + } + /** + * Accesses the {@link GPGME_Keyring}. + */ + get Keyring (){ + if (!this._Keyring){ + this._Keyring = new GPGME_Keyring; + } + return this._Keyring; + } + + /** + * Encrypt (and optionally sign) data + * @param {String|Object} data text/data to be encrypted as String. Also + * accepts Objects with a getText method + * @param {inputKeys} publicKeys + * Keys used to encrypt the message + * @param {inputKeys} secretKeys (optional) Keys used to sign the + * message. If Keys are present, the operation requested is assumed + * to be 'encrypt and sign' + * @param {Boolean} base64 (optional) The data will be interpreted as + * base64 encoded data. + * @param {Boolean} armor (optional) Request the output as armored + * block. + * @param {Boolean} wildcard (optional) If true, recipient information + * will not be added to the message. + * @param {Object} additional use additional valid gpg options as + * defined in {@link permittedOperations} + * @returns {Promise<encrypt_result>} Object containing the encrypted + * message and additional info. + * @async + */ + encrypt (data, publicKeys, secretKeys, base64=false, armor=true, + wildcard=false, additional = {}){ + let msg = createMessage('encrypt'); + if (msg instanceof Error){ + return Promise.reject(msg); + } + msg.setParameter('armor', armor); + msg.setParameter('always-trust', true); + if (base64 === true) { + msg.setParameter('base64', true); + } + let pubkeys = toKeyIdArray(publicKeys); + msg.setParameter('keys', pubkeys); + let sigkeys = toKeyIdArray(secretKeys); + if (sigkeys.length > 0) { + msg.setParameter('signing_keys', sigkeys); + } + putData(msg, data); + if (wildcard === true){ + msg.setParameter('throw-keyids', true); + } + if (additional){ + let additional_Keys = Object.keys(additional); + for (let k = 0; k < additional_Keys.length; k++) { + try { + msg.setParameter(additional_Keys[k], + additional[additional_Keys[k]]); + } + catch (error){ + return Promise.reject(error); + } + } + } + if (msg.isComplete() === true){ + return msg.post(); + } else { + return Promise.reject(gpgme_error('MSG_INCOMPLETE')); + } + } + + /** + * Decrypts a Message + * @param {String|Object} data text/data to be decrypted. Accepts + * Strings and Objects with a getText method + * @param {Boolean} base64 (optional) false if the data is an armored + * block, true if it is base64 encoded binary data + * @returns {Promise<decrypt_result>} Decrypted Message and information + * @async + */ + decrypt (data, base64=false){ + if (data === undefined){ + return Promise.reject(gpgme_error('MSG_EMPTY')); + } + let msg = createMessage('decrypt'); + + if (msg instanceof Error){ + return Promise.reject(msg); + } + if (base64 === true){ + msg.setParameter('base64', true); + } + putData(msg, data); + return new Promise(function (resolve, reject){ + msg.post().then(function (result){ + let _result = { data: result.data }; + _result.base64 = result.base64 ? true: false; + if (result.hasOwnProperty('dec_info')){ + _result.is_mime = result.dec_info.is_mime ? true: false; + if (result.dec_info.file_name) { + _result.file_name = result.dec_info.file_name; + } + } + if (!result.file_name) { + _result.file_name = null; + } + if (result.hasOwnProperty('info') + && result.info.hasOwnProperty('signatures') + && Array.isArray(result.info.signatures) + ) { + _result.signatures = collectSignatures( + result.info.signatures); + } + if (_result.signatures instanceof Error){ + reject(_result.signatures); + } else { + resolve(_result); + } + }, function (error){ + reject(error); + }); + }); + } + + /** + * Sign a Message + * @param {String|Object} data text/data to be signed. Accepts Strings + * and Objects with a getText method. + * @param {inputKeys} keys The key/keys to use for signing + * @param {String} mode The signing mode. Currently supported: + * 'clearsign':The Message is embedded into the signature; + * 'detached': The signature is stored separately + * @param {Boolean} base64 input is considered base64 + * @returns {Promise<signResult>} + * @async + */ + sign (data, keys, mode='clearsign', base64=false) { + if (data === undefined){ + return Promise.reject(gpgme_error('MSG_EMPTY')); + } + let key_arr = toKeyIdArray(keys); + if (key_arr.length === 0){ + return Promise.reject(gpgme_error('MSG_NO_KEYS')); + } + let msg = createMessage('sign'); + + msg.setParameter('keys', key_arr); + if (base64 === true){ + msg.setParameter('base64', true); + } + msg.setParameter('mode', mode); + putData(msg, data); + return new Promise(function (resolve,reject) { + if (mode ==='detached'){ + msg.expected ='base64'; + } + msg.post().then( function (message) { + if (mode === 'clearsign'){ + resolve({ + data: message.data } + ); + } else if (mode === 'detached') { + resolve({ + data: data, + signature: message.data + }); + } + }, function (error){ + reject(error); + }); + }); + } + + /** + * Verifies data. + * @param {String|Object} data text/data to be verified. Accepts Strings + * and Objects with a getText method + * @param {String} (optional) A detached signature. If not present, + * opaque mode is assumed + * @param {Boolean} (optional) Data and signature are base64 encoded + * @returns {Promise<verifyResult>} + *@async + */ + verify (data, signature, base64 = false){ + let msg = createMessage('verify'); + let dt = putData(msg, data); + if (dt instanceof Error){ + return Promise.reject(dt); + } + if (signature){ + if (typeof (signature)!== 'string'){ + return Promise.reject(gpgme_error('PARAM_WRONG')); + } else { + msg.setParameter('signature', signature); + } + } + if (base64 === true){ + msg.setParameter('base64', true); + } + return new Promise(function (resolve, reject){ + msg.post().then(function (message){ + if (!message.info || !message.info.signatures){ + reject(gpgme_error('SIG_NO_SIGS')); + } else { + let _result = { + signatures: collectSignatures(message.info.signatures) + }; + if (_result.signatures instanceof Error){ + reject(_result.signatures); + } else { + _result.is_mime = message.info.is_mime? true: false; + if (message.info.filename){ + _result.file_name = message.info.filename; + } + _result.data = message.data; + resolve(_result); + } + } + }, function (error){ + reject(error); + }); + }); + } +} + +/** + * Sets the data of the message, setting flags according on the data type + * @param {GPGME_Message} message The message where this data will be set + * @param { String| Object } data The data to enter. Expects either a string of + * data, or an object with a getText method + * @returns {undefined| GPGME_Error} Error if not successful, nothing otherwise + * @private + */ +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'); + } +} + +/** + * Parses, validates and converts incoming objects into signatures. + * @param {Array<Object>} sigs + * @returns {signatureDetails} Details about the signatures + */ +function collectSignatures (sigs){ + if (!Array.isArray(sigs)){ + return gpgme_error('SIG_NO_SIGS'); + } + let summary = { + all_valid: false, + count: sigs.length, + failures: 0, + signatures: { + good: [], + bad: [], + } + }; + for (let i=0; i< sigs.length; i++){ + let sigObj = createSignature(sigs[i]); + if (sigObj instanceof Error) { + return gpgme_error('SIG_WRONG'); + } + if (sigObj.valid !== true){ + summary.failures += 1; + summary.signatures.bad.push(sigObj); + } else { + summary.signatures.good.push(sigObj); + } + } + if (summary.failures === 0){ + summary.all_valid = true; + } + return summary; +}
\ No newline at end of file diff --git a/lang/js/src/index.js b/lang/js/src/index.js new file mode 100644 index 00000000..cf6e2d03 --- /dev/null +++ b/lang/js/src/index.js @@ -0,0 +1,52 @@ +/* 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+ + * + * Author(s): + * Maximilian Krambach <[email protected]> + */ + + +import { GpgME } from './gpgmejs'; +import { gpgme_error } from './Errors'; +import { Connection } from './Connection'; + +/** + * Initializes gpgme.js by testing the nativeMessaging connection once. + * @returns {Promise<GpgME> | GPGME_Error} + * + * @async + */ +function init (){ + return new Promise(function (resolve, reject){ + const connection = new Connection; + connection.checkConnection(false).then( + function (result){ + if (result === true) { + resolve(new GpgME()); + } else { + reject(gpgme_error('CONN_NO_CONNECT')); + } + }, function (){ // unspecific connection error. Should not happen + reject(gpgme_error('CONN_NO_CONNECT')); + }); + }); +} + +const exportvalue = { init:init }; +export default exportvalue;
\ 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..6c05fc6c --- /dev/null +++ b/lang/js/src/permittedOperations.js @@ -0,0 +1,403 @@ +/* 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+ + * + * Author(s): + * Maximilian Krambach <[email protected]> + */ + +/** + * @typedef {Object} messageProperty + * A message Property is defined by it's key. + * @property {Array<String>} allowed Array of allowed types. + * Currently accepted values are 'number', 'string', 'boolean'. + * @property {Boolean} array_allowed If the value can be an array of types + * defined in allowed + * @property {Array<*>} allowed_data (optional) restricts to the given values + */ + +/** + * Definition of the possible interactions with gpgme-json. + * @param {Object} operation Each operation is named by a key and contains + * the following properties: + * @property {messageProperty} required An object with all required parameters + * @property {messageProperty} optional An object with all optional parameters + * @property {Boolean} pinentry (optional) If true, a password dialog is + * expected, thus a connection tuimeout is not advisable + * @property {Object} answer The definition on what to expect as answer, if the + * answer is not an error + * @property {Array<String>} answer.type the type(s) as reported by gpgme-json. + * @property {Object} answer.data key-value combinations of expected properties + * of an answer and their type ('boolean', 'string', object) + @const +*/ +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'] + }, + 'signing_keys': { + allowed: ['string'], + array_allowed: true + }, + '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': 'string', + 'base64':'boolean' + } + } + }, + + decrypt: { + pinentry: true, + required: { + 'data': { + allowed: ['string'] + } + }, + optional: { + 'protocol': { + allowed: ['string'], + allowed_data: ['cms', 'openpgp'] + }, + 'base64': { + allowed: ['boolean'] + } + }, + answer: { + type: ['plaintext'], + data: { + 'data': 'string', + 'base64': 'boolean', + 'mime': 'boolean', + 'info': 'object', + 'dec_info': 'object' + } + } + }, + + sign: { + pinentry: true, + required: { + 'data': { + allowed: ['string'] }, + 'keys': { + allowed: ['string'], + array_allowed: true + } + }, + optional: { + 'protocol': { + allowed: ['string'], + allowed_data: ['cms', 'openpgp'] + }, + 'sender': { + allowed: ['string'], + }, + 'mode': { + allowed: ['string'], + allowed_data: ['detached', 'clearsign'] + // TODO 'opaque' is not used, but available on native app + }, + 'base64': { + allowed: ['boolean'] + }, + 'armor': { + allowed: ['boolean'] + }, + }, + answer: { + type: ['signature', 'ciphertext'], + data: { + 'data': 'string', + 'base64':'boolean' + } + + } + }, + + // note: For the meaning of the optional keylist flags, refer to + // https://www.gnupg.org/documentation/manuals/gpgme/Key-Listing-Mode.html + keylist:{ + required: {}, + + optional: { + 'protocol': { + allowed: ['string'], + allowed_data: ['cms', 'openpgp'] + }, + 'secret': { + allowed: ['boolean'] + }, + 'extern': { + allowed: ['boolean'] + }, + 'local':{ + allowed: ['boolean'] + }, + 'locate': { + allowed: ['boolean'] + }, + 'sigs':{ + allowed: ['boolean'] + }, + 'notations':{ + allowed: ['boolean'] + }, + 'tofu': { + allowed: ['boolean'] + }, + 'ephemeral': { + allowed: ['boolean'] + }, + 'validate': { + allowed: ['boolean'] + }, + 'keys': { + allowed: ['string'], + array_allowed: true + } + }, + answer: { + type: ['keys'], + data: { + 'base64': 'boolean', + 'keys': 'object' + } + } + }, + + export: { + required: {}, + optional: { + 'protocol': { + allowed: ['string'], + allowed_data: ['cms', 'openpgp'] + }, + 'keys': { + allowed: ['string'], + array_allowed: true + }, + 'armor': { + allowed: ['boolean'] + }, + 'extern': { + allowed: ['boolean'] + }, + 'minimal': { + allowed: ['boolean'] + }, + 'raw': { + allowed: ['boolean'] + }, + 'pkcs12': { + allowed: ['boolean'] + }, + 'with-sec-fprs': { + allowed: ['boolean'] + } + // secret: not yet implemented + }, + answer: { + type: ['keys'], + data: { + 'data': 'string', + 'base64': 'boolean', + 'sec-fprs': 'object' + } + } + }, + + import: { + required: { + 'data': { + allowed: ['string'] + } + }, + optional: { + 'protocol': { + allowed: ['string'], + allowed_data: ['cms', 'openpgp'] + }, + 'base64': { + allowed: ['boolean'] + }, + }, + answer: { + type: [], + data: { + 'result': 'object' + } + } + }, + + delete: { + pinentry: true, + required:{ + 'key': { + allowed: ['string'] + } + }, + optional: { + 'protocol': { + allowed: ['string'], + allowed_data: ['cms', 'openpgp'] + }, + }, + answer: { + data: { + 'success': 'boolean' + } + } + }, + + version: { + required: {}, + optional: {}, + answer: { + type: [''], + data: { + 'gpgme': 'string', + 'info': 'object' + } + } + }, + + createkey: { + pinentry: true, + required: { + userid: { + allowed: ['string'] + } + }, + optional: { + algo: { + allowed: ['string'] + }, + 'subkey-algo': { + allowed: ['string'] + }, + expires: { + allowed: ['number'], + } + }, + answer: { + type: [''], + data: { 'fingerprint': 'string' } + } + }, + + verify: { + required: { + data: { + allowed: ['string'] + } + }, + optional: { + 'protocol': { + allowed: ['string'], + allowed_data: ['cms', 'openpgp'] + }, + 'signature': { + allowed: ['string'] + }, + 'base64':{ + allowed: ['boolean'] + } + }, + answer: { + type: ['plaintext'], + data:{ + data: 'string', + base64:'boolean', + info: 'object' + // info.file_name: Optional string of the plaintext file name. + // info.is_mime: Boolean if the messages claims it is MIME. + // info.signatures: Array of signatures + } + } + }, + + config_opt: { + required: { + 'component':{ + allowed: ['string'], + // allowed_data: ['gpg'] // TODO check all available + }, + 'option': { + allowed: ['string'], + // allowed_data: ['default-key'] // TODO check all available + } + }, + optional: {}, + answer: { + type: [], + data: { + option: 'object' + } + } + } + + /** + * TBD handling of secrets + * TBD key modification? + */ + +}; |