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