diff options
Diffstat (limited to 'lang/js/src/Key.js')
-rw-r--r-- | lang/js/src/Key.js | 688 |
1 files changed, 688 insertions, 0 deletions
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 |