diff --git a/lang/js/src/Connection.js b/lang/js/src/Connection.js index 928ac681..8756cce1 100644 --- a/lang/js/src/Connection.js +++ b/lang/js/src/Connection.js @@ -1,283 +1,285 @@ /* gpgme.js - Javascript integration for gpgme * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik * * This file is part of GPGME. * * GPGME is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * GPGME is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this program; if not, see . * SPDX-License-Identifier: LGPL-2.1+ * * Author(s): * Maximilian Krambach */ /* global chrome */ import { permittedOperations } from './permittedOperations'; import { gpgme_error } from './Errors'; import { GPGME_Message, createMessage } from './Message'; -import { decode } from './Helpers'; +import { decode, atobArray, Utf8ArrayToStr } 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} 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|Promise} 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} 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}. + * Decodes and verifies the base64 encoded answer data. Verified against + * {@link permittedOperations}. + * @returns {Object} The readable gpnupg answer */ 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('')); + ) { + if (this.expected === 'binary'){ + _response[key] = atobArray(_decodedResponse[key]); + _response.binary = true; + } else { + _response[key] = Utf8ArrayToStr( + atobArray(_decodedResponse[key])); + _response.binary = false; + } } else { _response[key] = decode(_decodedResponse[key]); } break; } } return _response; } } diff --git a/lang/js/src/Errors.js b/lang/js/src/Errors.js index 73418028..145c3a59 100644 --- a/lang/js/src/Errors.js +++ b/lang/js/src/Errors.js @@ -1,169 +1,173 @@ /* gpgme.js - Javascript integration for gpgme * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik * * This file is part of GPGME. * * GPGME is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * GPGME is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this program; if not, see . * SPDX-License-Identifier: LGPL-2.1+ * * Author(s): * Maximilian Krambach */ /** * 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' }, + 'DECODE_FAIL': { + msg: 'Decoding failed due to unexpected data', + 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 index ba4277ab..9fa5775b 100644 --- a/lang/js/src/Helpers.js +++ b/lang/js/src/Helpers.js @@ -1,137 +1,206 @@ /* gpgme.js - Javascript integration for gpgme * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik * * This file is part of GPGME. * * GPGME is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * GPGME is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this program; if not, see . * SPDX-License-Identifier: LGPL-2.1+ * * Author(s): * Maximilian Krambach */ 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 | String | Array} input * @returns {Array} 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 +} + +/** + * Turns a base64 encoded string into an uint8 array + * @param {String} base64 encoded String + * @returns {Uint8Array} + * adapted from https://gist.github.com/borismus/1032746 + */ +export function atobArray (base64) { + if (typeof (base64) !== 'string'){ + throw gpgme_error('DECODE_FAIL'); + } + const raw = window.atob(base64); + const rawLength = raw.length; + let array = new Uint8Array(new ArrayBuffer(rawLength)); + for (let i = 0; i < rawLength; i++) { + array[i] = raw.charCodeAt(i); + } + return array; +} + +/** + * Turns a Uint8Array into an utf8-String + * @param {*} array Uint8Array + * @returns {String} + * Taken and slightly adapted from + * http://www.onicos.com/staff/iz/amuse/javascript/expert/utf.txt + * (original header: + * utf.js - UTF-8 <=> UTF-16 convertion + * + * Copyright (C) 1999 Masanao Izumo + * Version: 1.0 + * LastModified: Dec 25 1999 + * This library is free. You can redistribute it and/or modify it. + * ) + */ +export function Utf8ArrayToStr (array) { + let out, i, len, c, char2, char3; + out = ''; + len = array.length; + i = 0; + if (array instanceof Uint8Array === false){ + throw gpgme_error('DECODE_FAIL'); + } + while (i < len) { + c = array[i++]; + switch (c >> 4) { + case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: + // 0xxxxxxx + out += String.fromCharCode(c); + break; + case 12: case 13: + // 110x xxxx 10xx xxxx + char2 = array[i++]; + out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F)); + break; + case 14: + // 1110 xxxx 10xx xxxx 10xx xxxx + char2 = array[i++]; + char3 = array[i++]; + out += String.fromCharCode(((c & 0x0F) << 12) | + ((char2 & 0x3F) << 6) | + ((char3 & 0x3F) << 0)); + break; + default: + break; + } + } + return out; +} diff --git a/lang/js/src/Message.js b/lang/js/src/Message.js index b83caf6d..48813df7 100644 --- a/lang/js/src/Message.js +++ b/lang/js/src/Message.js @@ -1,239 +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 . * SPDX-License-Identifier: LGPL-2.1+ * * Author(s): * Maximilian Krambach */ 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'){ + if (value === 'binary'){ 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} * @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/gpgmejs.js b/lang/js/src/gpgmejs.js index 7692298f..513e4a56 100644 --- a/lang/js/src/gpgmejs.js +++ b/lang/js/src/gpgmejs.js @@ -1,391 +1,397 @@ /* gpgme.js - Javascript integration for gpgme * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik * * This file is part of GPGME. * * GPGME is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * GPGME is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this program; if not, see . * SPDX-License-Identifier: LGPL-2.1+ * * Author(s): * Maximilian Krambach */ 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 {String|Uint8Array} data The decrypted data + * @property {Boolean} binary indicating whether data is an Uint8Array. * @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} signatures.good All valid signatures * @property {Array} 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. + * @property {Boolean} binary Indicating whether returning payload data is an + * Uint8Array. */ /** * @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} 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 + * @param {Boolean} binary (optional) if true, treat the decoded data as + * binary, and return the data as Uint8Array * @returns {Promise} Decrypted Message and information * @async */ - decrypt (data, base64=false){ + decrypt (data, base64=false, binary){ 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); } + if (binary === true){ + msg.expected = 'binary'; + } 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; + _result.binary = result.binary ? 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} * @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.expected ='binary'; } 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} *@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} 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