diff --git a/lang/js/src/Connection.js b/lang/js/src/Connection.js
index 3fd1810d..c4921d53 100644
--- a/lang/js/src/Connection.js
+++ b/lang/js/src/Connection.js
@@ -1,288 +1,290 @@
/* 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, 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;
}
}
/**
* 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 _response = {
+ format: 'ascii'
+ };
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'
) {
if (this.expected === 'uint8'){
_response[key] = atobArray(_decodedResponse[key]);
_response.format = 'uint8';
} else if (this.expected === 'base64'){
_response[key] = _decodedResponse[key];
_response.format = 'base64';
} else {
_response[key] = Utf8ArrayToStr(
atobArray(_decodedResponse[key]));
_response.format = 'string';
}
} else {
_response[key] = decode(_decodedResponse[key]);
}
break;
}
}
return _response;
}
}
diff --git a/lang/js/src/gpgmejs.js b/lang/js/src/gpgmejs.js
index 5bdffeb5..b86b5f18 100644
--- a/lang/js/src/gpgmejs.js
+++ b/lang/js/src/gpgmejs.js
@@ -1,420 +1,424 @@
/* 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|Uint8Array} data The decrypted data
* @property {String} format Indicating how the data was converted after being
* received from gpgme.
* 'string': Data was decoded into an utf-8 string,
* 'base64': Data was not processed and is a base64 string
* 'uint8': data was turned into a 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 returning payload data is
- * base64 encoded
+ * @property {String} format Indicating how the data was converted after being
+ * received from gpgme.
+ * 'ascii': Data was ascii-encoded and no further processed
+ * 'string': Data was decoded into an utf-8 string,
+ * 'base64': Data was not processed and is a base64 string
+ * 'uint8': Data was turned into a 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 {Object} options
* @param {String|Object} options.data text/data to be encrypted as String.
* Also accepts Objects with a getText method
* @param {inputKeys} options.publicKeys
* Keys used to encrypt the message
* @param {inputKeys} opions.secretKeys (optional) Keys used to sign the
* message. If Keys are present, the operation requested is assumed
* to be 'encrypt and sign'
* @param {Boolean} options.base64 (optional) The data will be interpreted
* as base64 encoded data.
* @param {Boolean} options.armor (optional) Request the output as armored
* block.
* @param {Boolean} options.wildcard (optional) If true, recipient
* information will not be added to the message.
* @param {Boolean} always_trust (optional, default true) This assumes that
* used keys are fully trusted. If set to false, encryption to a key not
* fully trusted in gnupg will fail
* @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, always_trust = true, additional = {} }){
if (!data || !publicKeys){
return Promise.reject(gpgme_error('MSG_INCOMPLETE'));
}
let msg = createMessage('encrypt');
if (msg instanceof Error){
return Promise.reject(msg);
}
msg.setParameter('armor', armor);
if (base64 === true) {
msg.setParameter('base64', true);
}
if (always_trust === true) {
msg.setParameter('always-trust', true);
}
let pubkeys = toKeyIdArray(publicKeys);
if (!pubkeys.length) {
return Promise.reject(gpgme_error('MSG_NO_KEYS'));
}
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 {Object} options
* @param {String|Object} options.data text/data to be decrypted. Accepts
* Strings and Objects with a getText method
* @param {Boolean} options.base64 (optional) false if the data is an
* armored block, true if it is base64 encoded binary data
* @param {String} options.expect (optional) can be set to 'uint8' or
* 'base64'. Does no extra decoding on the data, and returns the decoded
* data as either Uint8Array or unprocessed(base64 encoded) string.
* @returns {Promise} Decrypted Message and information
* @async
*/
decrypt ({ data, base64, expect }){
if (!data){
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 (expect === 'base64' || expect === 'uint8'){
msg.expected = expect;
}
putData(msg, data);
return new Promise(function (resolve, reject){
msg.post().then(function (result){
let returnValue = { data: result.data };
returnValue.format = result.format ? result.format : null;
if (result.hasOwnProperty('dec_info')){
returnValue.is_mime = result.dec_info.is_mime ? true: false;
if (result.dec_info.file_name) {
returnValue.file_name = result.dec_info.file_name;
}
}
if (!returnValue.file_name) {
returnValue.file_name = null;
}
if (result.hasOwnProperty('info')
&& result.info.hasOwnProperty('signatures')
&& Array.isArray(result.info.signatures)
) {
returnValue.signatures = collectSignatures(
result.info.signatures);
}
if (returnValue.signatures instanceof Error){
reject(returnValue.signatures);
} else {
resolve(returnValue);
}
}, function (error){
reject(error);
});
});
}
/**
* Sign a Message
* @param {Object} options Signing options
* @param {String|Object} options.data text/data to be signed. Accepts
* Strings and Objects with a getText method.
* @param {inputKeys} options.keys The key/keys to use for signing
* @param {String} options.mode The signing mode. Currently supported:
* 'clearsign':The Message is embedded into the signature;
* 'detached': The signature is stored separately
* @param {Boolean} options.base64 input is considered base64
* @returns {Promise}
* @async
*/
sign ({ data, keys, mode = 'clearsign', base64 }){
if (!data){
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) {
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 {Object} options
* @param {String|Object} options.data text/data to be verified. Accepts
* Strings and Objects with a getText method
* @param {String} options.signature A detached signature. If not present,
* opaque mode is assumed
* @param {Boolean} options.base64 Indicating that data and signature are
* base64 encoded
* @returns {Promise}
*@async
*/
verify ({ data, signature, base64 }){
if (!data){
return Promise.reject(gpgme_error('PARAM_WRONG'));
}
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 returnValue = {
signatures: collectSignatures(message.info.signatures)
};
if (returnValue.signatures instanceof Error){
reject(returnValue.signatures);
} else {
returnValue.is_mime = message.info.is_mime? true: false;
if (message.info.filename){
returnValue.file_name = message.info.filename;
}
returnValue.data = message.data;
resolve(returnValue);
}
}
}, 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