From eef3a509fa5744e5f09ec8084985e6070b78226b Mon Sep 17 00:00:00 2001 From: "raimund.renkert@intevation.de" Date: Tue, 10 Apr 2018 11:33:14 +0200 Subject: [PATCH] js: Initial commit for JavaScript Native Messaging API -- Note this code misses all the legal boilerplate; please add this as soon as possible and provide a DCO so we can merge it into master. I also removed the dist/ directory because that was not source code. --- lang/README | 2 +- lang/js/CHECKLIST | 30 ++++++ lang/js/CHECKLIST_build | 9 ++ lang/js/README | 52 +++++++++++ lang/js/manifest.json | 18 ++++ lang/js/package.json | 17 ++++ lang/js/src/Connection.js | 76 +++++++++++++++ lang/js/src/gpgmejs.js | 187 +++++++++++++++++++++++++++++++++++++ lang/js/src/index.js | 14 +++ lang/js/testapplication.js | 21 +++++ lang/js/testicon.png | Bin 0 -> 16192 bytes lang/js/ui.css | 10 ++ lang/js/ui.html | 24 +++++ lang/js/webpack.conf.js | 13 +++ 14 files changed, 472 insertions(+), 1 deletion(-) create mode 100644 lang/js/CHECKLIST create mode 100644 lang/js/CHECKLIST_build create mode 100644 lang/js/README create mode 100644 lang/js/manifest.json create mode 100644 lang/js/package.json create mode 100644 lang/js/src/Connection.js create mode 100644 lang/js/src/gpgmejs.js create mode 100644 lang/js/src/index.js create mode 100644 lang/js/testapplication.js create mode 100644 lang/js/testicon.png create mode 100644 lang/js/ui.css create mode 100644 lang/js/ui.html create mode 100644 lang/js/webpack.conf.js 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 0000000000000000000000000000000000000000..12c3f5df5d3e71f286f5cc84e4d520bd84461489 GIT binary patch literal 16192 zcmeHuiC0rs*Y`qsB~;N66wS( z^nZzF#g4Q1=$;fhEot)Nn55*{OXid0J3C?ifW=Enk1& z2)G(Lbm#u+g)hVA{;UbSQGQ|a&*glU(IxVpoo_L3Qr$XTe(v3y2Y))(H=b{JZ52vt zC!dVV@C$9Yy!pp3@)xTcrmsAB@Sp@--hP)>=74|6lRosXiAhV`c=|%@T(;HZ zSmc7iiBuEegpP{@~pdQhe135QADY72O4ntMphTCEKa$)1Ee_ z3o~TIcOqWgZe`4De6J<^MT-8y?(Q%12Pn_K+^X*LGmEB8(Xf58dM&q?@5M)y;ZJ#L5FS(#Dt0APv z60m7J)sQj&vVQ$8LS|8gd^O`)v#=!&KQvfgfHh>T3jdXHD%Cse(8O;+K=^z8Xg4_t|^ z7d6W5*R#HxYaN~ENOIhuMC0BE$A!u()C&)|L3<+J`{xy zL(yZgVS0`F;snB9Whz)Ty56o>D1_oq9-KM$XvyDMK$o|HqHEf+tVNgzY%@dfc4hHx(fyB6_(y%n@Ch=K zw;s=#*(4!|BUMtkxMmJ`t90Q8ewC8m0e+WEZM|fr@vUT-kCg28qM+}dl{wVymXm}q zyj{8O#MfhDFFenywKuKM65TSoQXVZqz;vPN=eX?6h7U8cdw)0Dt9n8}&lQi%We$>Q zJ1LLLQ|7t}yOy{Tw;g!P{GndB$4U5JRAU5RCpUl-!41x zr?lcSpve9qH<&0%CS6oAGdxHNh)#JoF(P6{wuJD5_jG^R5K0N3FN=M+_29P-t%!eq70-lnzVbKNFb!EHZu=yyR=jP|st+9i^0PrhuL}--{p4YSI(WI!ilmsy%33j@8CCPE6aykbPU_IIjtXVH7 z@nZmRr3Qd@)x(VaMW#pw214$-Jti6&Xcz|EypqgxV=&nD-6jG#a2j&eljCT&QMP{3w{aJeG%|i9<*wZz+f##_*ik0B=&ZMTam1d+e{r$p;9MnX zuf=oLcjgEwo=4H=RC9z9&l>u?#vB0`>_oHl_+m?+K^$pI_3AUvI+>S~P>SDmkgcy) z#;>Jx9o>XZZ_Y)pdUZ8JusNSM$4&Q?WfeVm`t-|sKSK7$SSR1@dfxhIZbr(}amQC( zU#b64GCjLZ`4;N_K`Ptfm$sD+%U>oo{Mq1@FRWX<>dX2QSTHbnYM#}vEl0kh@}JUK z{kn1F6=h@yzJCIjXV3UP4Bx4sW%H~B6O`nUH=d7Nuxz}SDu!i(Q;p{UAE{;llxpgn zrqA1n7@?Z{L1mjg0ssm;&oo&!UO%EVnISK-y z($~5PIUb0=-gw?U$Tqv1lDrv%XL*h1e?dnNt>~5&sf!Cd>_$b?mj!sKk9z;$^>9Eb zFtgw@+iZ70jl}Z?u>HH1EQp}bbFCrp%0DRPkU_Se`g7!8sDhvECX_nhxjbmZh=nQk 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 sKb%m|zyAwDweY_hgA)EIxjQ{`u literal 0 HcmV?d00001 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 @@ + + + + + + + + + + +
    +
  • + Text: + +
  • +
  • + Public key ID: + +
  • +
+
+
+ + diff --git a/lang/js/webpack.conf.js b/lang/js/webpack.conf.js new file mode 100644 index 00000000..71b71161 --- /dev/null +++ b/lang/js/webpack.conf.js @@ -0,0 +1,13 @@ +const path = require('path'); + +module.exports = { + entry: './src/index.js', + // mode: 'development', + mode: 'production', + output: { + path: path.resolve(__dirname, 'dist'), + filename: 'gpgmejs.bundle.js', + libraryTarget: 'var', + library: 'Gpgmejs' + } +};