diff --git a/lang/js/.babelrc b/lang/js/.babelrc
new file mode 100644
index 00000000..9d8d5165
--- /dev/null
+++ b/lang/js/.babelrc
@@ -0,0 +1 @@
+{ "presets": ["es2015"] }
diff --git a/lang/js/package.json b/lang/js/package.json
index 2b7dd7ee..a794188a 100644
--- a/lang/js/package.json
+++ b/lang/js/package.json
@@ -1,17 +1,19 @@
{
"name": "gpgmejs",
"version": "0.0.1",
"description": "javascript part of a nativeMessaging gnupg integration",
"main": "src/index.js",
"private": true,
"scripts": {
- "test": "echo \"Error: no test specified\" && exit 1"
+ "test": "mocha"
},
"keywords": [],
"author": "",
"license": "",
"devDependencies": {
"webpack": "^4.5.0",
- "webpack-cli": "^2.0.14"
+ "webpack-cli": "^2.0.14",
+ "chai": "^4.1.2",
+ "mocha": "^5.1.1"
}
}
diff --git a/lang/js/src/Connection.js b/lang/js/src/Connection.js
index 5b092ab0..a10f9d9a 100644
--- a/lang/js/src/Connection.js
+++ b/lang/js/src/Connection.js
@@ -1,214 +1,214 @@
/* 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+
*/
/**
* A connection port will be opened for each communication between gpgmejs and
* gnupg. It should be alive as long as there are additional messages to be
* expected.
*/
import { permittedOperations } from './permittedOperations'
import { gpgme_error } from "./Errors"
import { GPGME_Message } from "./Message";
/**
* A Connection handles the nativeMessaging interaction.
*/
export class Connection{
constructor(){
this.connect();
let me = this;
}
/**
* (Simple) Connection check.
* @returns {Boolean} true if the onDisconnect event has not been fired.
* Please note that the event listener of the port takes some time
* (5 ms seems enough) to react after the port is created. Then this will
* return undefined
*/
get isConnected(){
return this._isConnected;
}
/**
* Immediately closes the open port.
*/
disconnect() {
if (this._connection){
this._connection.disconnect();
}
}
/**
* Opens a nativeMessaging port.
*/
connect(){
if (this._isConnected === true){
gpgme_error('CONN_ALREADY_CONNECTED');
} else {
this._isConnected = true;
this._connection = chrome.runtime.connectNative('gpgmejson');
let me = this;
this._connection.onDisconnect.addListener(
function(){
me._isConnected = false;
}
);
}
}
/**
* Sends a message and resolves with the answer.
* @param {GPGME_Message} message
* @returns {Promise} the gnupg answer, or rejection with error
* information.
*/
post(message){
if (!this.isConnected){
- return Promise.reject(gpgme_error('CONN_NO_CONNECT'));
+ return Promise.reject(gpgme_error('CONN_DISCONNECTED'));
}
if (!message || !message instanceof GPGME_Message){
return Promise.reject(gpgme_error('PARAM_WRONG'), message);
}
if (message.isComplete !== true){
return Promise.reject(gpgme_error('MSG_INCOMPLETE'));
}
let me = this;
return new Promise(function(resolve, reject){
let answer = new Answer(message.operation);
let listener = function(msg) {
if (!msg){
me._connection.onMessage.removeListener(listener)
reject(gpgme_error('CONN_EMPTY_GPG_ANSWER'));
} else if (msg.type === "error"){
me._connection.onMessage.removeListener(listener)
reject(
{code: 'GNUPG_ERROR',
msg: msg.msg} );
} else {
let answer_result = answer.add(msg);
if (answer_result !== true){
reject(answer_result);
}
if (msg.more === true){
me._connection.postMessage({'op': 'getmore'});
} else {
me._connection.onMessage.removeListener(listener)
resolve(answer.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(){
reject(gpgme_error('CONN_TIMEOUT'));
}, 5000);
}]).then(function(result){
return result;
});
}
});
}
};
/**
* A class for answer objects, checking and processing the return messages of
* the nativeMessaging communication.
* @param {String} operation The operation, to look up validity of returning messages
*/
class Answer{
constructor(operation){
this.operation = operation;
}
/**
* Add the information to the answer
* @param {Object} msg The message as received with nativeMessaging
* returns true if successfull, gpgme_error otherwise
*/
add(msg){
if (this._response === undefined){
this._response = {};
}
let messageKeys = Object.keys(msg);
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 ( msg.type !== 'error' && poa.type.indexOf(msg.type) < 0){
return gpgme_error('CONN_UNEXPECTED_ANSWER');
}
break;
case 'more':
break;
default:
//data should be concatenated
if (poa.data.indexOf(key) >= 0){
if (!this._response.hasOwnProperty(key)){
this._response[key] = '';
}
this._response[key] = this._response[key].concat(msg[key]);
}
//params should not change through the message
else if (poa.params.indexOf(key) >= 0){
if (!this._response.hasOwnProperty(key)){
this._response[key] = msg[key];
}
else if (this._response[key] !== msg[key]){
return gpgme_error('CONN_UNEXPECTED_ANSWER',msg[key]);
}
}
//infos may be json objects etc. Not yet defined.
// Pushing them into arrays for now
else if (poa.infos.indexOf(key) >= 0){
if (!this._response.hasOwnProperty(key)){
this._response[key] = [];
}
this._response.push(msg[key]);
}
else {
return gpgme_error('CONN_UNEXPECTED_ANSWER', key);
}
break;
}
}
return true;
}
/**
* @returns {Object} the assembled message.
* TODO: does not care yet if completed.
*/
get message(){
return this._response;
}
}
diff --git a/lang/js/src/Errors.js b/lang/js/src/Errors.js
index 2f53aa89..d26aab18 100644
--- a/lang/js/src/Errors.js
+++ b/lang/js/src/Errors.js
@@ -1,142 +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 .
* SPDX-License-Identifier: LGPL-2.1+
*/
const err_list = {
// Connection
'CONN_NO_CONNECT': {
msg:'Connection with the nativeMessaging host could not be'
+ ' established.',
type: 'error'
},
'CONN_DISCONNECTED': {
msg:'Connection with the nativeMessaging host was lost.',
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_OP_PENDING': {
- msg: 'There is no operation specified yet. The parameter cannot'
- + ' be set',
- type: 'warning'
- },
'MSG_WRONG_OP': {
msg: 'The operation requested could not be found',
- type: 'warning'
+ 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'
},
// generic
'PARAM_WRONG':{
- msg: 'invalid parameter was found',
+ msg: 'Invalid parameter was found',
type: 'error'
},
'PARAM_IGNORED': {
msg: 'An parameter was set that has no effect in gpgmejs',
type: 'warning'
},
'NOT_IMPLEMENTED': {
msg: 'A openpgpjs parameter was submitted that is not implemented',
type: 'error'
},
'NOT_YET_IMPLEMENTED': {
msg: 'Support of this is probable, but it is not implemented yet',
type: 'error'
},
'GENERIC_ERROR': {
msg: 'Unspecified error',
type: 'error'
}
};
/**
* Checks the given error code and returns an 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'
*/
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'){
- console.log(new GPGME_Error(code));
+ console.warn(code + ': ' + err_list[code].msg);
}
return null;
} else if (code === 'GNUPG_ERROR'){
return new GPGME_Error(code, info.msg);
}
else {
return new GPGME_Error('GENERIC_ERROR');
}
}
class GPGME_Error extends Error{
constructor(code, msg=''){
if (code === 'GNUPG_ERROR' && typeof(msg) === 'string'){
super(msg);
} else if (err_list.hasOwnProperty(code)){
super(err_list[code].msg);
} else {
super(err_list['GENERIC_ERROR'].msg);
}
this.code = code || 'GENERIC_ERROR';
}
set code(value){
this._code = value;
}
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 841c0eda..9a69f851 100644
--- a/lang/js/src/Helpers.js
+++ b/lang/js/src/Helpers.js
@@ -1,102 +1,103 @@
/* 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+
*/
import { gpgme_error } from "./Errors";
+import { GPGME_Key } from "./Key";
/**
* Tries to return an array of fingerprints, either from input fingerprints or
* from Key objects
* @param {Key |Array| GPGME_Key | Array|String|Array} input
* @returns {Array} Array of fingerprints.
*/
-export function toKeyIdArray(input, nocheck){
+export function toKeyIdArray(input){
if (!input){
gpgme_error('MSG_NO_KEYS');
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 {
gpgme_error('MSG_NOT_A_FPR');
}
} else if (typeof(input[i]) === 'object'){
let fpr = '';
if (input[i] instanceof GPGME_Key){
fpr = input[i].fingerprint;
- } else if (input[i].hasOwnProperty(primaryKey) &&
+ } 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){
gpgme_error('MSG_NO_KEYS');
return [];
} else {
return result;
}
};
/**
* check if values are valid hexadecimal values of a specified length
* @param {*} key input value.
* @param {int} len the expected length of the value
*/
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 Hex string with a length of 40
*/
export function isFingerprint(string){
return hextest(string, 40);
};
/**
* check if the input is a valid Hex string with a length of 16
*/
-function isLongId(string){
+export function isLongId(string){
return hextest(string, 16);
};
// TODO still not needed anywhere
function isShortId(string){
return hextest(string, 8);
};
diff --git a/lang/js/src/Key.js b/lang/js/src/Key.js
index f6fa7ae3..0b44b245 100644
--- a/lang/js/src/Key.js
+++ b/lang/js/src/Key.js
@@ -1,203 +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+
*/
/**
* The key class allows to query the information defined in gpgme Key Objects
* (see https://www.gnupg.org/documentation/manuals/gpgme/Key-objects.html)
*
* This is a stub, as the gpgme-json side is not yet implemented
*
*/
-import {isFingerprint} from './Helpers'
-import {gpgme_error} from './Errors'
-import { GPGME_Message } from './Message';
+import { isFingerprint } from './Helpers'
+import { gpgme_error } from './Errors'
+import { createMessage } from './Message';
import { permittedOperations } from './permittedOperations';
export class GPGME_Key {
constructor(fingerprint){
this.fingerprint = fingerprint;
}
set fingerprint(fpr){
if (isFingerprint(fpr) === true && !this._fingerprint){
- this._fingerprint = fingerprint;
+ this._fingerprint = fpr;
}
}
get fingerprint(){
return this._fingerprint;
}
/**
* hasSecret returns true if a secret subkey is included in this Key
*/
get hasSecret(){
checkKey(this._fingerprint, 'secret').then( function(result){
return Promise.resolve(result);
});
}
get isRevoked(){
return checkKey(this._fingerprint, 'revoked');
}
get isExpired(){
return checkKey(this._fingerprint, 'expired');
}
get isDisabled(){
return checkKey(this._fingerprint, 'disabled');
}
get isInvalid(){
return checkKey(this._fingerprint, 'invalid');
}
get canEncrypt(){
return checkKey(this._fingerprint, 'can_encrypt');
}
get canSign(){
return checkKey(this._fingerprint, 'can_sign');
}
get canCertify(){
return checkKey(this._fingerprint, 'can_certify');
}
get canAuthenticate(){
return checkKey(this._fingerprint, 'can_authenticate');
}
get isQualified(){
return checkKey(this._fingerprint, 'is_qualified');
}
get armored(){
let me = this;
return new Promise(function(resolve, reject){
let conn = new Connection();
conn.setFlag('armor', true);
conn.post('export',{'fpr': me._fingerprint});
});
// TODO return value not yet checked. Should result in an armored block
// in correct encoding
// TODO openpgpjs also returns secKey if private = true?
}
/**
* TODO returns true if this is the default key used to sign
*/
get isDefault(){
throw('NOT_YET_IMPLEMENTED');
}
/**
* get the Key's subkeys as GPGME_Key objects
* @returns {Array}
*/
get subkeys(){
return checkKey(this._fingerprint, 'subkeys').then(function(result){
// TBD expecting a list of fingerprints
if (!Array.isArray(result)){
result = [result];
}
let resultset = [];
for (let i=0; i < result.length; i++){
let subkey = new GPGME_Key(result[i]);
if (subkey instanceof GPGME_Key){
resultset.push(subkey);
}
}
return Promise.resolve(resultset);
});
}
/**
* creation time stamp of the key
* @returns {Date|null} TBD
*/
get timestamp(){
return checkKey(this._fingerprint, 'timestamp');
//TODO GPGME: -1 if the timestamp is invalid, and 0 if it is not available.
}
/**
* The expiration timestamp of this key TBD
* @returns {Date|null} TBD
*/
get expires(){
return checkKey(this._fingerprint, 'expires');
// TODO convert to Date; check for 0
}
/**
* getter name TBD
* @returns {String|Array} The user ids associated with this key
*/
get userIds(){
return checkKey(this._fingerprint, 'uids');
}
/**
* @returns {String} The public key algorithm supported by this subkey
*/
get pubkey_algo(){
return checkKey(this._fingerprint, 'pubkey_algo');
}
};
/**
* generic function to query gnupg information on a key.
* @param {*} fingerprint The identifier of the Keyring
* @param {*} property The gpgme-json property to check
*
*/
function checkKey(fingerprint, property){
return Promise.reject(gpgme_error('NOT_YET_IMPLEMENTED'));
if (!property ||
permittedOperations[keyinfo].indexOf(property) < 0){
return Promise.reject(gpgme_error('PARAM_WRONG'));
}
return new Promise(function(resolve, reject){
if (!isFingerprint(fingerprint)){
- reject('KEY_INVALID');
+ reject(gpgme_error('KEY_INVALID'));
+ }
+ let msg = createMessage ('keyinfo');
+ if (msg instanceof Error){
+ reject(gpgme_error('PARAM_WRONG'));
}
- let msg = new GPGME_Message('keyinfo');
msg.setParameter('fingerprint', this.fingerprint);
return (this.connection.post(msg)).then(function(result){
if (result.hasOwnProperty(property)){
resolve(result[property]);
}
else if (property == 'secret'){
// TBD property undefined means "not true" in case of secret?
resolve(false);
} else {
reject(gpgme_error('CONN_UNEXPECTED_ANSWER'));
}
}, function(error){
reject({code: 'GNUPG_ERROR',
msg: error.msg});
});
});
};
\ No newline at end of file
diff --git a/lang/js/src/Keyring.js b/lang/js/src/Keyring.js
index e1f0a50f..470eeeec 100644
--- a/lang/js/src/Keyring.js
+++ b/lang/js/src/Keyring.js
@@ -1,153 +1,156 @@
/* 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+
*/
-import {GPGME_Message} from './Message'
+import {createMessage} from './Message'
import {GPGME_Key} from './Key'
import { isFingerprint, isLongId } from './Helpers';
import { gpgme_error } from './Errors';
export class GPGME_Keyring {
constructor(connection){
this.connection = connection;
}
set connection(connection){
if (!this._connection && connection instanceof Connection){
this._connection = connection;
}
}
get connection(){
if (this._connection instanceof Connection){
if (this._connection.isConnected){
return this._connection;
}
return gpgme_error('CONN_DISCONNECTED');
}
return gpgme_error('CONN_NO_CONNECT');
}
/**
* @param {String} (optional) pattern A pattern to search for, in userIds or KeyIds
* @param {Boolean} (optional) Include listing of secret keys
* @returns {Promise.>}
*
*/
getKeys(pattern, include_secret){
- let msg = new GPGME_Message('listkeys');
+ let msg = createMessage('listkeys');
+ if (msg instanceof Error){
+ return Promise.reject(msg);
+ }
if (pattern && typeof(pattern) === 'string'){
msg.setParameter('pattern', pattern);
}
if (include_secret){
msg.setParameter('with-secret', true);
}
this.connection.post(msg).then(function(result){
let fpr_list = [];
let resultset = [];
if (!Array.isArray(result.keys)){
//TODO check assumption keys = Array
fpr_list = [result.keys];
} else {
fpr_list = result.keys;
}
for (let i=0; i < fpr_list.length; i++){
let newKey = new GPGME_Key(fpr_list[i]);
if (newKey instanceof GPGME_Key){
resultset.push(newKey);
}
}
return Promise.resolve(resultset);
});
}
/**
* @param {Object} flags subset filter expecting at least one of the
* filters described below. True will filter on the condition, False will
* reverse the filter, if not present or undefined, the filter will not be
* considered. Please note that some combination may not make sense
* @param {Boolean} flags.secret Only Keys containing a secret part.
* @param {Boolean} flags.revoked revoked Keys only
* @param {Boolean} flags.expired Expired Keys only
* @param {String} (optional) pattern A pattern to search for, in userIds or KeyIds
* @returns {Promise Array}
*
*/
getSubset(flags, pattern){
if (flags === undefined) {
throw('ERR_WRONG_PARAM');
};
let secretflag = false;
if (flags.hasOwnProperty(secret) && flags.secret){
secretflag = true;
}
this.getKeys(pattern, secretflag).then(function(queryset){
let resultset = [];
for (let i=0; i < queryset.length; i++ ){
let conditions = [];
let anticonditions = [];
if (secretflag === true){
conditions.push('hasSecret');
} else if (secretflag === false){
anticonditions.push('hasSecret');
}
/**
if (flags.defaultKey === true){
conditions.push('isDefault');
} else if (flags.defaultKey === false){
anticonditions.push('isDefault');
}
*/
/**
* if (flags.valid === true){
anticonditions.push('isInvalid');
} else if (flags.valid === false){
conditions.push('isInvalid');
}
*/
if (flags.revoked === true){
conditions.push('isRevoked');
} else if (flags.revoked === false){
anticonditions.push('isRevoked');
}
if (flags.expired === true){
conditions.push('isExpired');
} else if (flags.expired === false){
anticonditions.push('isExpired');
}
let decision = undefined;
for (let con = 0; con < conditions.length; con ++){
if (queryset[i][conditions[con]] !== true){
decision = false;
}
}
for (let acon = 0; acon < anticonditions.length; acon ++){
if (queryset[i][anticonditions[acon]] === true){
decision = false;
}
}
if (decision !== false){
resultset.push(queryset[i]);
}
}
return Promise.resolve(resultset);
});
}
};
diff --git a/lang/js/src/Message.js b/lang/js/src/Message.js
index 06ac8db2..4d242277 100644
--- a/lang/js/src/Message.js
+++ b/lang/js/src/Message.js
@@ -1,111 +1,110 @@
/* 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+
*/
import { permittedOperations } from './permittedOperations'
import { gpgme_error } from './Errors'
-export class GPGME_Message {
+
+export function createMessage(operation){
+ if (typeof(operation) !== 'string'){
+ return gpgme_error('PARAM_WRONG');
+ }
+ if (permittedOperations.hasOwnProperty(operation)){
+ return new GPGME_Message(operation);
+ } else {
+ return gpgme_error('MSG_WRONG_OP');
+ }
+}
+
+/**
+ * Prepares a communication request. It checks operations and parameters in
+ * ./permittedOperations.
+ * @param {String} operation
+ */
+class GPGME_Message {
//TODO getter
constructor(operation){
- setOperation(this, operation);
+ this.operation = operation;
}
+ set operation (op){
+
+
+ }
get operation(){
return this._msg.op;
}
/**
* Sets a parameter for the message. Note that the operation has to be set
* first, to be able to check if the parameter is permittted
* @param {String} param Parameter to set
* @param {any} value Value to set //TODO: Some type checking
* @returns {Boolean} If the parameter was set successfully
*/
setParameter(param,value){
if (!param || typeof(param) !== 'string'){
return gpgme_error('PARAM_WRONG');
}
- if (!this._msg || !this._msg.op){
- return gpgme_error('MSG_OP_PENDING');
- }
let po = permittedOperations[this._msg.op];
if (!po){
return gpgme_error('MSG_WRONG_OP');
}
if (po.required.indexOf(param) >= 0 || po.optional.indexOf(param) >= 0){
this._msg[param] = value;
return true;
}
return gpgme_error('PARAM_WRONG');
}
/**
* Check if the message has the minimum requirements to be sent, according
* to the definitions in permittedOperations
* @returns {Boolean}
*/
get isComplete(){
if (!this._msg.op){
return false;
}
let reqParams = permittedOperations[this._msg.op].required;
for (let i=0; i < reqParams.length; i++){
if (!this._msg.hasOwnProperty(reqParams[i])){
console.log(reqParams[i] + 'missing');
return false;
}
}
return true;
}
/**
* 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;
}
}
}
-
-/**
- * Defines the operation this message will have
- * @param {String} operation Must be defined in permittedOperations
- * TODO: move to constructor?
- */
-function setOperation (scope, operation){
- if (!operation || typeof(operation) !== 'string'){
- return gpgme_error('PARAM_WRONG');
- }
- if (permittedOperations.hasOwnProperty(operation)){
- if (!scope._msg){
- scope._msg = {};
- }
- scope._msg.op = operation;
- } else {
- return gpgme_error('MSG_WRONG_OP');
- }
-}
\ No newline at end of file
diff --git a/lang/js/src/gpgmejs.js b/lang/js/src/gpgmejs.js
index b504a457..2ddf2964 100644
--- a/lang/js/src/gpgmejs.js
+++ b/lang/js/src/gpgmejs.js
@@ -1,172 +1,183 @@
/* 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+
*/
import {Connection} from "./Connection"
-import {GPGME_Message} from './Message'
+import {GPGME_Message, createMessage} from './Message'
import {toKeyIdArray} from "./Helpers"
import { gpgme_error } from "./Errors"
import { GPGME_Keyring } from "./Keyring";
export class GpgME {
/**
* initializes GpgME by opening a nativeMessaging port
* TODO: add configuration
*/
constructor(connection){
this.connection = connection;
}
set connection(connection){
if (this._connection instanceof Connection){
gpgme_error('CONN_ALREADY_CONNECTED');
}
if (connection instanceof Connection){
this._connection = connection;
} else {
gpgme_error('PARAM_WRONG');
}
}
get connection(){
if (this._connection instanceof Connection){
if (this._connection.isConnected){
return this._connection;
}
return undefined; //TODO: connection was lost!
}
return undefined; //TODO: no connection there
}
set Keyring(keyring){
if (ring && ring instanceof GPGME_Keyring){
this._Keyring = ring;
}
}
get Keyring(){
return this._Keyring;
}
/**
* @param {String|Uint8Array} data text/data to be encrypted as String/Uint8Array
* @param {GPGME_Key|String|Array|Array} publicKeys Keys used to encrypt the message
* @param {Boolean} wildcard (optional) If true, recipient information will not be added to the message
*/
encrypt(data, publicKeys, wildcard=false){
- let msg = new GPGME_Message('encrypt');
-
+ let msg = createMessage('encrypt');
+ if (msg instanceof Error){
+ return Promise.reject(msg)
+ }
// TODO temporary
msg.setParameter('armor', true);
msg.setParameter('always-trust', true);
let pubkeys = toKeyIdArray(publicKeys);
msg.setParameter('keys', pubkeys);
putData(msg, data);
if (wildcard === true){msg.setParameter('throw-keyids', true);
};
return (this.connection.post(msg));
}
/**
* @param {String} data TODO Format: base64? String? Message with the encrypted data
* @returns {Promise} decrypted message:
data: The decrypted data. This may be base64 encoded.
base64: Boolean indicating whether data is base64 encoded.
mime: A Boolean indicating whether the data is a MIME object.
info: An optional object with extra information.
* @async
*/
decrypt(data){
if (data === undefined){
return Promise.reject(gpgme_error('MSG_EMPTY'));
}
- let msg = new GPGME_Message('decrypt');
+ let msg = createMessage('decrypt');
+ if (msg instanceof Error){
+ return Promise.reject(msg);
+ }
putData(msg, data);
return this.connection.post(msg);
}
deleteKey(key, delete_secret = false, no_confirm = false){
return Promise.reject(gpgme_error('NOT_YET_IMPLEMENTED'));
- let msg = new GPGME_Message('deletekey');
+ let msg = createMessage('deletekey');
+ if (msg instanceof Error){
+ return Promise.reject(msg);
+ }
let key_arr = toKeyIdArray(key);
if (key_arr.length !== 1){
- throw('TODO');
- //should always be ONE key
+ return Promise.reject(
+ gpgme_error('GENERIC_ERROR'));
+ // TBD should always be ONE key?
}
msg.setParameter('key', key_arr[0]);
if (delete_secret === true){
- msg.setParameter('allow_secret', true); //TBD
+ msg.setParameter('allow_secret', true);
+ // TBD
}
if (no_confirm === true){ //TODO: Do we want this hidden deep in the code?
- msg.setParameter('delete_force', true); //TBD
+ msg.setParameter('delete_force', true);
+ // TBD
}
this.connection.post(msg).then(function(success){
- //TODO: it seems that there is always errors coming back:
+ // TODO: it seems that there is always errors coming back:
}, function(error){
switch (error.msg){
case 'ERR_NO_ERROR':
return Promise.resolve('okay'); //TBD
default:
return Promise.reject(gpgme_error('TODO') ); //
// INV_VALUE,
// GPG_ERR_NO_PUBKEY,
// GPG_ERR_AMBIGUOUS_NAME,
// GPG_ERR_CONFLICT
}
});
}
}
/**
* Sets the data of the message, converting Uint8Array to base64 and setting
* the base64 flag
* @param {GPGME_Message} message The message where this data will be set
* @param {*} data The data to enter
* @param {String} propertyname // TODO unchecked still
*/
function putData(message, data){
if (!message || !message instanceof GPGME_Message ) {
return gpgme_error('PARAM_WRONG');
}
if (!data){
message.setParameter('data', '');
} else if (data instanceof Uint8Array){
let decoder = new TextDecoder('utf8');
message.setParameter('base64', true);
message.setParameter ('data', decoder.decode(data));
} else if (typeof(data) === 'string') {
message.setParameter('base64', false);
message.setParameter('data', data);
} else if ( typeof(data) === 'object' && data.hasOwnProperty(getText)){
let txt = data.getText();
if (txt instanceof Uint8Array){
let decoder = new TextDecoder('utf8');
message.setParameter('base64', true);
message.setParameter ('data', decoder.decode(txt));
}
} else {
return gpgme_error('PARAM_WRONG');
}
}
diff --git a/lang/js/test/Helpers.js b/lang/js/test/Helpers.js
new file mode 100644
index 00000000..590f9f65
--- /dev/null
+++ b/lang/js/test/Helpers.js
@@ -0,0 +1,110 @@
+/* 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+
+ */
+
+import { expect } from "../node_modules/chai/chai";
+import { gpgme_error} from "../src/Errors";
+import { GPGME_Key } from "../src/Key";
+import { isLongId, isFingerprint, toKeyIdArray } from "../src/Helpers"
+
+const helper_params = {
+ validLongId: '0A0A0A0A0A0A0A0A',
+ validGPGME_Key: new GPGME_Key('ADDBC303B6D31026F5EB4591A27EABDF283121BB'),
+ validKeys: [new GPGME_Key('A1E3BC45BDC8E87B74F4392D53B151A1368E50F3'),
+ 'ADDBC303B6D31026F5EB4591A27EABDF283121BB',
+ new GPGME_Key('EE17AEE730F88F1DE7713C54BBE0A4FF7851650A')],
+ validFingerprint: '9A9A7A7A8A9A9A7A7A8A9A9A7A7A8A9A9A7A7A8A',
+ invalidLongId: '9A9A7A7A8A9A9A7A7A8A',
+ invalidFingerprint: [{hello:'World'}],
+ invalidKeyArray: {curiosity:'uncat'},
+ invalidKeyArray_OneBad: [
+ new GPGME_Key('12AE9F3E41B33BF77DF52B6BE8EE1992D7909B08'),
+ 'E1D18E6E994FA9FE9360Bx0E687B940FEFEB095A',
+ '3AEA7FE4F5F416ED18CEC63DD519450D9C0FAEE5'],
+ invalidErrorCode: 'Please type in all your passwords.'
+}
+
+describe('Error Object handling', function(){
+ it('check the Timeout error', function(){
+ let test0 = gpgme_error('CONN_TIMEOUT');
+ expect(test0).to.be.an.instanceof(Error);
+ expect(test0.code).to.equal('CONN_TIMEOUT');
+ });
+ it('Error Object returns generic code if code is not listed', function(){
+ let test0 = gpgme_error(helper_params.invalidErrorCode);
+ expect(test0).to.be.an.instanceof(Error);
+ expect(test0.code).to.equal('GENERIC_ERROR');
+ });
+
+ it('Warnings like PARAM_IGNORED should not return errors', function(){
+ let test0 = gpgme_error('PARAM_IGNORED');
+ expect(test0).to.be.null;
+ });
+});
+
+describe('Fingerprint checking', function(){
+ it('isFingerprint(): valid Fingerprint', function(){
+ let test0 = isFingerprint(helper_params.validFingerprint);
+ expect(test0).to.be.true;
+ });
+ it('isFingerprint(): invalid Fingerprint', function(){
+ let test0 = isFingerprint(helper_params.invalidFingerprint);
+ expect(test0).to.be.false;
+ });
+});
+describe('Converting to Fingerprint', function(){
+ it('Correct Inputs', function(){
+ it('Fingerprint string', function(){
+ let test0 = toKeyIdArray(helper_params.validFingerprint);
+ expect(test0).to.be.an('array');
+ expect(test0).to.include(helper_params.validFingerprint);
+ });
+ it('GPGME_Key', function(){
+ expect(helper_params.validGPGME_Key).to.be.an.instanceof(GPGME_Key);
+ let test0 = toKeyIdArray(helper_params.validGPGME_Key);
+ expect(test0).to.be.an('array');
+ expect(test0).to.include(helper_params.validGPGME_Key.fingerprint);
+ });
+ it('Array of valid inputs', function(){
+ let test0 = toKeyIdArray(helper_params.validKeys);
+ expect(test0).to.be.an('array');
+ expect(test0).to.have.lengthOf(helper_params.validKeys.length);
+ });
+ });
+ describe('Incorrect inputs', function(){
+ it('valid Long ID', function(){
+ let test0 = toKeyIdArray(helper_params.validLongId);
+ expect(test0).to.be.empty;
+ });
+ it('invalidFingerprint', function(){
+ let test0 = toKeyIdArray(helper_params.invalidFingerprint);
+ expect(test0).to.be.empty;
+ });
+ it('invalidKeyArray', function(){
+ let test0 = toKeyIdArray(helper_params.invalidKeyArray);
+ expect(test0).to.be.empty;
+ });
+ it('Partially invalid array', function(){
+ let test0 = toKeyIdArray(helper_params.invalidKeyArray_OneBad);
+ expect(test0).to.be.an('array');
+ expect(test0).to.have.lengthOf(
+ helper_params.invalidKeyArray_OneBad.length - 1);
+ });
+ });
+});
diff --git a/lang/js/test/Message.js b/lang/js/test/Message.js
new file mode 100644
index 00000000..454b8ca3
--- /dev/null
+++ b/lang/js/test/Message.js
@@ -0,0 +1,42 @@
+/* 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+
+ */
+
+import { expect } from "../node_modules/chai/chai";
+import { GPGME_Message, createMessage } from "../src/Message";
+
+const message_params = {
+ invalid_op_action : 'dance',
+ invalid_op_type : [234, 34, '<>'],
+}
+
+describe('Message Object', function(){
+ describe('incorrect initialization', function(){
+ it('non-allowed operation', function(){
+ let test0 = createMessage(message_params.invalid_op_action);
+ expect(test0).to.be.an.instanceof(Error);
+ expect(test0.code).to.equal('MSG_WRONG_OP');
+ });
+ it('wrong parameter type in constructor', function(){
+ let test0 = createMessage(message_params.invalid_op_type);
+ expect(test0).to.be.an.instanceof(Error);
+ expect(test0.code).to.equal('PARAM_WRONG');
+ });
+ });
+});
diff --git a/lang/js/test/mocha.opts b/lang/js/test/mocha.opts
new file mode 100644
index 00000000..65adc1c3
--- /dev/null
+++ b/lang/js/test/mocha.opts
@@ -0,0 +1,4 @@
+--require babel-register
+--reporter spec
+--ui bdd
+--colors