diff --git a/lang/README b/lang/README index ee99f0f1..afd7b083 100644 --- a/lang/README +++ b/lang/README @@ -13,4 +13,4 @@ cl Common Lisp cpp C++ qt Qt-Framework API python Python 2 and 3 (module name: gpg) -javascript Native messaging client for the gpgme-json server. +js Native messaging client for the gpgme-json server. diff --git a/lang/js/CHECKLIST b/lang/js/CHECKLIST new file mode 100644 index 00000000..79a35cb7 --- /dev/null +++ b/lang/js/CHECKLIST @@ -0,0 +1,30 @@ +NativeConnection: + + [X] nativeConnection: successfully sending an encrypt request, +receiving an answer + [X] nativeConnection successfull on Chromium, chrome and firefox + [ ] nativeConnection successfull on Windows, macOS, Linux + [ ] nativeConnection with delayed, multipart (> 1MB) answer + +replicating Openpgpjs API: + + [*] Message handling (encrypt, verify, sign) + [ ] Key handling (import/export, modifying, status queries) + [ ] Configuration handling + [ ] check for completeness + [ ] handling of differences to openpgpjs + +Communication with other implementations + + [ ] option to export SECRET Key into localstore used by e.g. mailvelope + +Management: + [*] Define the gpgme interface + [ ] check Permissions (e.g. csp) for the different envs + [ ] agree on license + [ ] tests + + +Problems: + [X] gpgme-json: interactive mode vs. bytelength; filename + [X] nativeApp chokes on arrays. We will get rid of that bnativeapp anyhow diff --git a/lang/js/CHECKLIST_build b/lang/js/CHECKLIST_build new file mode 100644 index 00000000..fa162a10 --- /dev/null +++ b/lang/js/CHECKLIST_build @@ -0,0 +1,9 @@ +- Checklist for build/install: + +browsers' manifests (see README) need allowedextension added, and the path set + +manifest.json/ csp needs adaption + +/dist contains a current build which is used by example app. +We may either want to update it on every commit, or never at all, but not +inconsistently. diff --git a/lang/js/README b/lang/js/README new file mode 100644 index 00000000..3ca07439 --- /dev/null +++ b/lang/js/README @@ -0,0 +1,52 @@ +This is an example app for gpgme-json. +As of now, it only encrypts a given text. + +Installation +------------- + +gpgmejs uses webpack, the builds can be found in dist/ +(the testapplication uses that script at that location). To create a new +package, the command is npx webpack --config webpack.conf.js. +If you want a more debuggable (i.e. not minified) build, just change the mode +in webpack.conf.js. + +Demo WebExtension: +As soon as a bundled webpack is in dist/ (TODO: .gitignore or not?), +the gpgmejs folder can just be included in the extensions tab of the browser in +questions (extension debug mode needs to be active). For chrome, selecting the +folder is sufficient, for firefox, the manifest.json needs to be selected. + +In the browsers' nativeMessaging configuration folder a file 'gpgmejs.json' +is needed, with the following content: + +(The path to the native app gpgme-json may need adaption) + +Chromium: +~/.config/chromium/NativeMessagingHosts/gpgmejson.json + +{ + "name": "gpgmejson", + "description": "This is a test application for gpgmejs", + "path": "/usr/bin/gpgme-json", + "type": "stdio", + "allowed_origins": ["chrome-extension://ExtensionIdentifier/"] +} +The ExtensionIdentifier can be seen on the chrome://extensions page, and +changes on each reinstallation. Note the slashes in allowed_origins. + + +Firefox: +~/.mozilla/native-messaging-hosts/gpgmejson.json +{ + "name": "gpgmejson", + "description": "This is a test application for gpgmejs", + "path": "/usr/bin/gpgme-json", + "type": "stdio", + "allowed_extensions": ["ExtensionIdentifier@temporary-addon"] +} +The ExtensionIdentifier can be seen as Extension ID on the about:addons page if +addon-debugging is active. In firefox, the temporary addon is removed once +firefox exits, and the identifier will need to be changed more often. + +For testing purposes, it could be a good idea to change the keyID in the +ui.html, to not having to type it every time. diff --git a/lang/js/manifest.json b/lang/js/manifest.json new file mode 100644 index 00000000..8bb5c58d --- /dev/null +++ b/lang/js/manifest.json @@ -0,0 +1,18 @@ +{ + "manifest_version": 2, + + "name": "gpgme-json with native Messaging", + "description": "This should be able to encrypt a text using gpgme-json", + "version": "0.1", + "content_security_policy": "default-src 'self' 'unsafe-eval' filesystem", + "browser_action": { + "default_icon": "testicon.png", + "default_title": "gpgme.js", + "default_popup": "ui.html" + }, + "permissions": ["nativeMessaging", "activeTab"], + + "background": { + "scripts": [ "dist/gpgmejs.bundle.js"] + } +} diff --git a/lang/js/package.json b/lang/js/package.json new file mode 100644 index 00000000..46b60fd2 --- /dev/null +++ b/lang/js/package.json @@ -0,0 +1,17 @@ +{ + "name": "gpgmejs", + "version": "0.0.1", + "description": "javascript part of a nativeMessaging gnupg integration", + "main": "src/gpgmejs.js", + "private": true, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "", + "devDependencies": { + "webpack": "^4.3.0", + "webpack-cli": "^2.0.13" + } +} diff --git a/lang/js/src/Connection.js b/lang/js/src/Connection.js new file mode 100644 index 00000000..e8fea542 --- /dev/null +++ b/lang/js/src/Connection.js @@ -0,0 +1,76 @@ +/** + * 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. + */ + +export function Connection(){ + if (!this.connection){ + this.connection = connect(); + this._msg = { + 'always-trust': true, + // 'no-encrypt-to': false, + // 'no-compress': true, + // 'throw-keyids': false, + // 'wrap': false, + 'armor': true, + 'base64': false + }; + }; + + this.disconnect = function () { + if (this.connection){ + this.connection.disconnect(); + } + }; + + /** + * Sends a message and resolves with the answer. + * @param {*} operation The interaction requested from gpgme + * @param {*} message A json-capable object to pass the operation details. + * TODO: _msg should contain configurable parameters + */ + this.post = function(operation, message){ + let timeout = 5000; + let me = this; + if (!message || !operation){ + return Promise.reject('no message'); // TBD + } + + let keys = Object.keys(message); + for (let i=0; i < keys.length; i++){ + let property = keys[i]; + me._msg[property] = message[property]; + } + me._msg['op'] = operation; + // TODO fancier checks if what we want is consistent with submitted content + return new Promise(function(resolve, reject){ + me.connection.onMessage.addListener(function(msg) { + if (!msg){ + reject('empty answer.'); + } + if (msg.type === "error"){ + reject(msg.msg); + } + resolve(msg); + }); + + me.connection.postMessage(me._msg); + setTimeout( + function(){ + me.disconnect(); + reject('Timeout'); + }, timeout); + }); + }; +}; + + +function connect(){ + let connection = chrome.runtime.connectNative('gpgmejson'); + if (!connection){ + let msg = chrome.runtime.lastError || 'no message'; //TBD + throw(msg); + } + return connection; +}; diff --git a/lang/js/src/gpgmejs.js b/lang/js/src/gpgmejs.js new file mode 100644 index 00000000..dedbf809 --- /dev/null +++ b/lang/js/src/gpgmejs.js @@ -0,0 +1,187 @@ +import {Connection} from "./Connection" + +export function encrypt(data, publicKeys, privateKeys, passwords=null, + sessionKey, filename, compression, armor=true, detached=false, + signature=null, returnSessionKey=false, wildcard=false, date=new Date()){ + // gpgme_op_encrypt ( <-gpgme doc on this operation + // gpgme_ctx_t ctx, + // gpgme_key_t recp[], + // gpgme_encrypt_flags_t flags, + // gpgme_data_t plain, + // gpgme_data_t cipher) + // flags: + // GPGME_ENCRYPT_ALWAYS_TRUST + // GPGME_ENCRYPT_NO_ENCRYPT_TO + // GPGME_ENCRYPT_NO_COMPRESS + // GPGME_ENCRYPT_PREPARE + // GPGME_ENCRYPT_EXPECT_SIGN + // GPGME_ENCRYPT_SYMMETRIC + // GPGME_ENCRYPT_THROW_KEYIDS + // GPGME_ENCRYPT_WRAP + if (passwords !== null){ + throw('Password!'); // TBD + } + + let pubkeys = toKeyIdArray(publicKeys); + let privkeys = toKeyIdArray(privateKeys); + + // TODO filename: data is supposed to be empty, file is provided + // TODO config compression detached signature + // TODO signature to add to the encrypted message (?) || privateKeys: signature is desired + // gpgme_op_encrypt_sign (gpgme_ctx_t ctx, gpgme_key_t recp[], gpgme_encrypt_flags_t flags, gpgme_data_t plain, gpgme_data_t cipher) + + // TODO sign date overwriting implemented in gnupg? + + let conn = new Connection(); + if (wildcard){ + // Connection.set('throw-keyids', true); TODO Connection.set not yet existant + } + return conn.post('encrypt', { + 'data': data, + 'keys': publicKeys, + 'armor': armor}); +}; + +export function decrypt(message, privateKeys, passwords, sessionKeys, publicKeys, + format='utf8', signature=null, date=new Date()) { + if (passwords !== null){ + throw('Password!'); // TBD + } + if (format === 'binary'){ + // Connection.set('base64', true); + } + if (publicKeys || signature){ + // Connection.set('signature', signature); + // request verification, too + } + //privateKeys optionally if keyId was thrown? + // gpgme_op_decrypt (gpgme_ctx_t ctx, gpgme_data_t cipher, gpgme_data_t plain) + // response is gpgme_op_decrypt_result (gpgme_ctx_t ctx) (next available?) + return conn.post('decrypt', { + 'data': message + }); +} + +// BIG TODO. +export function generateKey({userIds=[], passphrase, numBits=2048, unlocked=false, keyExpirationTime=0, curve="", date=new Date()}){ + throw('not implemented here'); + // gpgme_op_createkey (gpgme_ctx_t ctx, const char *userid, const char *algo, unsigned long reserved, unsigned long expires, gpgme_key_t extrakey, unsigned int flags); + return false; +} + +export function sign({ data, privateKeys, armor=true, detached=false, date=new Date() }) { + //TODO detached GPGME_SIG_MODE_DETACH | GPGME_SIG_MODE_NORMAL + // gpgme_op_sign (gpgme_ctx_t ctx, gpgme_data_t plain, gpgme_data_t sig, gpgme_sig_mode_t mode) + // TODO date not supported + + let conn = new Connection(); + let privkeys = toKeyIdArray(privateKeys); + return conn.post('sign', { + 'data': data, + 'keys': privkeys, + 'armor': armor}); +}; + +export function verify({ message, publicKeys, signature=null, date=new Date() }) { + //TODO extra signature: sig, signed_text, plain: null + // inline sig: signed_text:null, plain as writable (?) + // date not supported + //gpgme_op_verify (gpgme_ctx_t ctx, gpgme_data_t sig, gpgme_data_t signed_text, gpgme_data_t plain) + let conn = new Connection(); + let privkeys = toKeyIdArray(privateKeys); + return conn.post('sign', { + 'data': data, + 'keys': privkeys, + 'armor': armor}); +} + + +export function reformatKey(privateKey, userIds=[], passphrase="", unlocked=false, keyExpirationTime=0){ + let privKey = toKeyIdArray(privateKey); + if (privKey.length !== 1){ + return false; //TODO some error handling. There is not exactly ONE key we are editing + } + let conn = new Connection(); + // TODO key management needs to be changed somewhat + return conn.post('TODO', { + 'key': privKey[0], + 'keyExpirationTime': keyExpirationTime, //TODO check if this is 0 or a positive and plausible number + 'userIds': userIds //TODO check if empty or plausible strings + }); + // unlocked will be ignored +} + +export function decryptKey({ privateKey, passphrase }) { + throw('not implemented here'); + return false; +}; + +export function encryptKey({ privateKey, passphrase }) { + throw('not implemented here'); + return false; +}; + +export function encryptSessionKey({data, algorithm, publicKeys, passwords, wildcard=false }) { + //openpgpjs: + // Encrypt a symmetric session key with public keys, passwords, or both at + // once. At least either public keys or passwords must be specified. + throw('not implemented here'); + return false; +}; + +export function decryptSessionKeys({ message, privateKeys, passwords }) { + throw('not implemented here'); + return false; +}; + +// //TODO worker handling + +// //TODO key representation +// //TODO: keyring handling + + +/** + * Helper functions and checks + */ + +/** + * Checks if the submitted value is a keyID. + * TODO: should accept all strings that are accepted as keyID by gnupg + * TODO: See if Key becomes an object later on + * @param {*} key input value. Is expected to be a string of 8,16 or 40 chars + * representing hex values. Will return false if that expectation is not met + */ +function isKeyId(key){ + if (!key || typeof(key) !== "string"){ + return false; + } + if ([8,16,40].indexOf(key.length) < 0){ + return false; + } + let regexp= /^[0-9a-fA-F]*$/i; + return regexp.test(key); +}; + +/** + * Tries to return an array of keyID values, either from a string or an array. + * Filters out those that do not meet the criteria. (TODO: silently for now) + * @param {*} array Input value. + */ +function toKeyIdArray(array){ + let result = []; + if (!array){ + return result; + } + if (!Array.isArray(array)){ + if (isKeyId(array) === true){ + return [keyId]; + } + return result; + } + for (let i=0; i < array.length; i++){ + if (isKeyId(array[i]) === true){ + result.push(array[i]); + } + } + return result; +}; diff --git a/lang/js/src/index.js b/lang/js/src/index.js new file mode 100644 index 00000000..02dc919d --- /dev/null +++ b/lang/js/src/index.js @@ -0,0 +1,14 @@ +import * as gpgmejs from'./gpgmejs' +export default gpgmejs; + +/** + * Export each high level api function separately. + * Usage: + * + * import { encryptMessage } from 'gpgme.js' + * encryptMessage(keys, text) + */ +export { + encrypt, decrypt, sign, verify, + generateKey, reformatKey + } from './gpgmejs'; diff --git a/lang/js/testapplication.js b/lang/js/testapplication.js new file mode 100644 index 00000000..d01aca99 --- /dev/null +++ b/lang/js/testapplication.js @@ -0,0 +1,21 @@ +/** +* Testing nativeMessaging. This is a temporary plugin using the gpgmejs + implemetation as contained in src/ +*/ +function buttonclicked(event){ + let data = document.getElementById("text0").value; + let keyId = document.getElementById("key").value; + let enc = Gpgmejs.encrypt(data, [keyId]).then(function(answer){ + console.log(answer); + console.log(answer.type); + console.log(answer.data); + alert(answer.data); + }, function(errormsg){ + alert('Error: '+ errormsg); + }); +}; + +document.addEventListener('DOMContentLoaded', function() { + document.getElementById("button0").addEventListener("click", + buttonclicked); + }); diff --git a/lang/js/testicon.png b/lang/js/testicon.png new file mode 100644 index 00000000..12c3f5df Binary files /dev/null and b/lang/js/testicon.png differ diff --git a/lang/js/ui.css b/lang/js/ui.css new file mode 100644 index 00000000..9c88698b --- /dev/null +++ b/lang/js/ui.css @@ -0,0 +1,10 @@ +ul { + list-style-type: none; + padding-left: 0px; +} + +ul li span { + float: left; + width: 120px; + margin-top: 6px; +} diff --git a/lang/js/ui.html b/lang/js/ui.html new file mode 100644 index 00000000..9c56c2e5 --- /dev/null +++ b/lang/js/ui.html @@ -0,0 +1,24 @@ + + +
+ + + + + + + +