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