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 01/95] 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 zgujc01vSzGk0Nza&F1LUuTEsB&4raK->zI;Uos=RO<0vZlJZ`&bIOXcbMJ>Y1eUCN z;<>u9LH}WDZg9Ia4e*ckv;DMKN&3+`WIb*xd%rT8=!r$)|{`Dn$|-BuNtfmt|u`nVayb zl8n~?`y_Ce>r^Ccl9M0`o&@lPn;Ccq3f>vu_hwV@99o)%7XaLcfyYw?Pom5@UZ&tQ z8}lzA;Jx-W`cN6~1UJy!q3DVjx}890oB@VUnJm$qMCSyTDeVpPGZHBJ1Sg=cOE){8 z9?a0sr0CDgv4$*iV(5Q>tMZH7gv=QZ41GI_zAMll-N(@Ppy&gEu3ki!4sieZ0Pm53 zl8eylP2&gZJ9Lzq3ngv`jLY z?^^T`_%`V$_N_auN~s>BRq)Z-){yB%LOkFp=}-4;5#DbwJNI=LPGA}>v<6T; z6bW-MgPO7(w7tumKa}HNQn_X^xfX}XSVB(lfFb>CyIdu8wEn4}ZcwBT(2_IWc;!HA zX^}n|uXN!pQhUc*lgG)7pTel^Z$MvZ!pl^W1fu*JMkRZLT+2gyw#ltf^^HWK`u^6mocm_tu zbc4V_CsC6sE$Pw=KKD6Oo%3j!ZYdp{$CQwxB{5isG=s!JKn|gUOiXnw1}P-R1`{1P z{wg#SDbk!dN~W8HxeyuLs_(c+SjJ9ea+d7W5+4dWiGiNz#qoOpnwiPijUOY^p{ya^ zOi*=DM@c#Luv8{miB?NaDFND%f%*;N_$?Gvq>B1oheCrSG41MOe55%;pt501RCPN} zGHo+3?ZH|SP?P;_Dw&B|j_<&Dittjhgl(k37h}DZB{v;_3hQX_A*+s(RgI#EX%wXC zhj~d8x>CeCCf%NnlDsWIyhX%Rt0jxzT*h9^G-F1|6toUnP{csDauT?ev5h!3R^~=4 zoOXVUqeohP4TQ;2u))5gWNx%-Kq9B?&e8G`I!MGpK++P(PlT)nB=v@U*ht)gKY!^i zvlcJhWV%9syrY?n%CotNXqn0Yu>sX<9jo+E)mJJUAHBPrz>OwXP}l+sQLfOff2 zB&@``ygf}{zDb@*<@OjBV9WpIZ4`gqUC43e6oWv2h{)XdQL=FAw1b&Wows$Abj0eW zb7H&}^wYBKb{<59bx=+!Xwo<)g7yRGD1eS*9^8%`0)=~wz+O!!lbO*DK7<4{wI;az6Q3jcH6e;^ zKvOnIV&k8+22*L4je^Af!+^K)0sOTHXgOUE0?^VC%%j$z z0``C!>UN5lq*{J3m`I{kL7HASQfcZ2{?E`p#ZCy(zs7Tc2HR$!ONN(yK$ODEjJ>$3 zwjel5v~3+PPjG}TX>tbZNm^MRWu4~c>lTM$-qMPZt*P11wkAE5)Pi-EnTyn!07Z>s zbQhV^YI*RsppY@6`01e+@R-Ea)uE%uFew20(R>IN_}Of5j!2}f`)6!WTt$q=he`_h zymipy6FOKdLh49?mVn#4BDd}58RQBCI06|7XN~0h3TXJHiZ zstA}c?kXE|6vtjp8bFCWFJ|oV>_XtJ)xnMVp(x-J50*+yfkhf!(|J z&>u1{rzs<6h{(*MMB7(_K4|DL+mxEu#S?+Czlc$*{rlIJdc=F47)~bBjp!Hpx~e)$ z!IqL+SQJg)i!9o|xofaAe4Y44xGA6d;SO?Df~bTwYgYLySNF0%_iz3HC_~KR&Yj-> zXjta0Ts?Tv`8AcLtpHs`m9Di7+uPIXJi4%mRZw%g z{?0{9(ZV?Qo3b<;Ic23+^#RWOP3`KG7pE(uU2-#;R{py?sSl7>7Ty|J_49(p!v@Q| ziX+dncYos68sBxVIkWjhv}e%!4?^m$l4tt!cdyd@e7~2hF_==UpH(s94f>&e)A#%~ zIeOEs&bEEA`L;T1bmg+T?@o?AD~A4to}2G&Y+0D^*}aD_|JQZDp&^uY>FPt89t`S1~qDMi*d6TPY&Z_-+-RY0~ahGz;kXw4Vvc2b7DtSSV1^IM>TkYItkc)tZ8*MQg)b>XlwtTjh7B0b_sA8J1SigX z&Idi7MMqvYKqv!lmDVV%MVUaV_dj0UL#YKj&p?vKxT=) zqX~~3K$>7(kQ^f=NfTw{ib(QWN1~8G=XyqRGDwaw5Yi%&d?h2{T96eSCyDD1`eNOucZY!a1W! zY(!&Im&lM6w4o<5=*MR>ozawOuRs%8WI1mwA?v`$0Fe*0ruj9HJ((HavrIuuIv_qM zB97}wbQ&c=Z61nt3F;wxH0ZpBIs zm&%ls1C5kt$;lfOz+itGGC+K&j7(=9;n~FpwJ=($T|~t8z|;qAM{kDdC)ln0M>*+- zZWTajVd#TeI#C4e=88lPO>`GQaad#852c~pf+j-x0MimBQHhJkPpBeQ?Ko2YEqFQ` zl%}uSglYn+IYr~SA>H5^VEbtx=Aijnjtw~iq7)m-tUYL9ihf;}h1GeKj2sh}l_X^3 zEpG)~6v7D=uJ$vE`&M3VFX@xH4>;UKD9B_ZJ>W&C-OOuiadnxUASIFLguNCAp;&lY7HS>nPNN0Mk!X8~HmT|&A--c|{C&27I9G(B zZ0{)1iA#oEGIVfLOUl!z$v;ig(?g(0P_2i_)+dJ+&cG)nOfKWdF%0S+MhObBw~sIsH1@tUuXVH8#X$nk|c@|M9*5XGUcY*BIp?u^yA0N zbf1Vk-3O)^g*4|=5z{g7H!0EBDN&Nbuk8+l^cDlD zX`MM5d!2BZo0!lv$iAAnO4CqL24r6{Od-83rh`nR!cs@hI6xEu*_Tf@khh}ZkbOx~ znCy_~*G6PtV~A3-S|o|=OVCDnC6c6F!n!PZW-Cc#Urnu+A1;za_9Y!%B+Vj8+I`Gi zFE14XYYnn5uhsH4ktDJ&2@jLiZ4~W_?8~p$^6x}E$iBq8xAc!z_d)hm4B~X+>Sl-R zOSZa7cC-#6`x*(SYh^YB-m}R?@}zYT+1Kd8=~jr6tVa(lUCwf^$|j-x+%~D?PcaUv zK*Z?F=~jwU>4wvHysUgTV~=!&18j1Yl!=0wdV^buf!t{YZ3~MlwESOU@X`Tk(sZe``Z2L1KPC1)F^+A4SH-^F>Zd^m36^z2x40;r?V zgEY6>A9|(;amwm*oOx^cy?Y-Dcj=$}`srx&sSsL{rLcPYx>M%-NkgTgQF6S%ma1#I z@cmj&%$K|)C3bp=_4x1}65FcZ)?F9N06rDp*G-GF9nhPiiELi~K=Tm+d%JJW>k?LP{P8h|g{2^F;Yh;Iu-9rMi~+teXhoK!MI@KxX z#^SCLF$n6AHcL-$2g=PKQEmjv^aDWYq5X&wi)Yd=g_h_juaBG{Ya2rj6baWle-!D4 zBOJ$wN--Jg@Z;#vMbWQ5xsYxe8g+ahc)T!p4eecfHp zrmEbN!R>NR=8i6ma2H5mo`jUHc~~S&jz}>F=o|FZ-=%^RZ9V4q@{!z1E(X3e$wB7b(_V`EM_% zSG>(_X_o4{piFk4g^H?Ijh;iP@E2aO?x?sL{pYN5aG;fnN>Y;#!b|5`?b?SSX?iw`d6H_gbOA>&bJ5alRjlwIa*U0bZ0o_L ze$wHMVI2QAG+7af^uE>{4Ls3J(>eY-e47c4<*heIU;9LAF=&1PeR_1B08#dz%gBFq z(s>g-xv-;UOjU;>{g*2}^^fB`t($*1SKW1nr_6_rAsbe8uhGjF3l)jkLWWm$EzYBT z;4V2RrmE7+>h-xOjfAIv>TQP&%MuNA3us4A3KVI=$g};G51Nk)|3qYmX?HvMaD4HD zF5n;CNdBR_5R$i$p38Z^J7rk*tJ&G)gA#l}k>E0*J3WANgxvA-8{XW0-$6GXUypSc zYDeIZbR}m#X0X(r$ZmV;eY98g=q-A3>Y+VRd4tI~cGoPtW+43tLlg|MZOr+b9zBLv zdCc%6{8^Cj1+r7@@NEwqw0!4#lwPx~&a)4i^?TpTjFY3&4IiA-)3f7KT*#}h?P&X0 zz5Z$uy&jfS|G;4R^Qf~#Gm+MY_&$an@@WnZhjaZ8%4B~qaUue&nN3QE`?E=u&pI{I!lViGj8^0D#ox`iZUQ%(?Y`VrvpWC8PH;=JE<+})ZTn;K=~o)8rI|okgz|bYP|ixH%!SENvcWUa zc@C*tL5<*#^(6-jQTSDh=cr8-jP=UVY1a&cz9wyttekYSh(b6L`-ILoyD_58Pjn!tUGf zozjF}+u6ZyyJer#*63!eaV4OCn9^6F#^SM(<2)JDm`nJfkSIqmqIK;#>M5IqJj5pb zfJyDK;^Q2RN`j3b&Xv{cf1(;R6;g2#LTFxZR6U|<&Z0Vj7K|PcZUZ}NbwyAyVl<` zmz%o0Txq%Bu=f^K3Np%OKifu~b@51*!3 zLqD?LMaq{N=X8vH;vZ`(+i=ky)Bce@jaNkuS zP55>t4T0eZfi8@H)KW7q#s!kHC%f!bfs_Niosx7dk86b-!{kOSpSSML5_05vfRpYs z2H3$scGsNjPU2oqu)}dQB+vI!lGx!WCH@8WSaDJ+Sr4#y6TtegFX?+WlZLNSy6nVd zF?#FAR0gBD368r%`P}t#mGbywta*#Onf`};PBoB%SJON2C!78&i7%$IXJ^V)-nbY> z^;2%3WYs7#hko&wBxgvaKaX861jQAf4b`o@;n14$hI-PiCsGL%ozS0&i8BwEvpei` z3(tQ^rZSy%^^tBzh9Gp8HjW{3P)N5wX+@Nm$;_)zYN9?;`;)2F!CSih5gdmqKNsGG zI2PH&fI;tnfSsaqDMpBwqASa24%aXU(eNzYt9)^0iQkn zHY)G0$>{gK|07sNmZO}&%?S#)Wfz&1*y2f}B2r}XZYsIIr1i^o#`YxxyH`hp3zw<@ z6;*Ls-gu)dwwIDdm^VEWM?C3w25u&5PN{LGcTI62$SAUl{>FXkF_Z*zw(Lbfh&qBH zWc)u(mzVAyEq=MmNIcykFTEXoGML@q%M>}o1%d0Uet#>WSM>P(%*pomlKLYe>3Y$T z`N6hydCpKV$}?FAX8)n(LsM`yl0j)js6Ntu)r)b6puR&xT1ijPsQ}kpBr&zk$ZxLb zX);wmHjoo^(ZYhfheiG59dZ?JrqP4;(m~>sRC1!xo+oGl@DmyD?i=K)zyJ}d2jj}G zJI8JS05Q(gDX9&;rF28Dn~P>@A+uJamLEgY2}Q${@-R)Fv+XkF@*d^Vi)b7e_X!T1 zE-n~697UIwX~q+cfcQ&qkuxDm2*aPZK~B>Fo5MoQv&>r>uLVWixbTOYqlx2`O2Y-W zO4y-0e7|6amOp8&q|V<*Ld!flt4Aui(;P`%{nmr0hwmK9!8!>)9bfm*!LxkxF5!!F z2i`^vd-ZhPzM8JNaIV1964FjZL#d$f#v-Y8p7!iMOsGDHasaXMBU90AAEjv)oadAe zF{Lr7RDN6)y-+a&ccXon2-veHAfuSda1VJ6bwo4|rP}rQ(gD|HaTO0f?(F1beM+(e zhYnjkvOWZtZar8X2>$$sG)~~1aT%4IM&O_r#J8k5rSdA+7dM1;Qbwb$nCGh29d#y} zMd%1$VJP=>>nBBkn9%nv7wHZQh{VlgLOVA&$A@Gg9qqI{4zgTnDFH}Sh?JSDoid1L z@CU(}$+dyWl{w|1NE33|F`D7W7BFl$k&Y9waoG7ltQ%-FuVaF4>f1#E?JPyAes*%e zOtuO+f6J<;uU}Wb&iy0!uN}cD2}#!KvW-N6+n7~t8EW5SrU`r{&Cj?cSS32OoA#9) zDV27U28edQ;GIqyXnLWOIxs-DDHr7l-MIX_@U+_sVhR+$&iq~8xDl&{{(kFsccw=d z?y!=t71%WvB*}{wheg}GrORxd^!hli1W;YfteYuYwzB@j%!^Lw;(8GkEv?XUJ8(kY zIQj2rndP3rqJhaGHxg40)0AoYuq`A%5mG&dRK^atJI#D$&6XrL$#B8hv|mOq1i~jB zPTh=>HCom5_MF=$rm^xdqd60$)SIYqmkpFusU4K2Y^ms+^&{H^cIK) zduRIj^LAl;*~_wM;ah@C6(6%FWrQbP@&^(}sa!Jv)p z{KEK>(S^q&tb--=V$3Fy!&yJ}m0JJB>1fwJ))fRnpjjc#ALpm#qxaU;L&2%xkg$ ziS>;qABF4IqFyKcJBZ<;U*<4PPrCP+6z8EUpWyhV!kg8D2t?o6o8^sb5b^44b2w3u zOwnz9=NHJUn)D>5552oUjSzVg+x*u8!GX3hPL$z~!AcV@fPeRRqRey%cd_R)IvWxu z$d)~#r>*Tg9qHwsBowl6qxO&CM)!IMSAeA7C}Ip^NOa+`T-DL9S28Eq>_X~9Cvf|> z2Uj&vS#7s4LT2)%N4E9{of!b{Csw)EePB8|KBWsOQsC-0;}N@NlmI(LZK9utyWPw! zu&cL|lIv_O#*%um~EYk%^%S$SwvLd>bn%_=VRMOIw|Pe&1NzV%_zbb%gE9t@16?*xxtrlKZ6iO zkc6N0z3?>O>`cfM1+1*tNJTnwB_3yZKiW6K6k1+~VHWG7&PN#v}j97CZz( zG@nTL3uUWCUarbW(X0Qe!t-0wvIo|rUoBZL5Gjr7D4ck;|@CT5i$tIE-5R;G(( z<4M(_lY1Ce5)J4%gBJ_f$P)fO7OrWR-it3SGSXpsD^m$s#5Cx-$OyUYNV?y8BNA7o z5z`k(TL%g-aPxWVKsqt?ny^DrY6|^%O@|GT2xLjIWB;+fg~B}`Q>9#8f;5VlmDr-Q5}(6u4LISUoi{f z2PdR4^o1zDE*RGO%#+?E=jhnhNqtD<*u%-9R{MpZ&N1gQ)ti>U0lGMe^nRC- zYky5Wo$LqwJP@;M^fBsvD7$dTW#FA;h9V>FQuzS9ceBT?LW)M}%pj^wmYi;XFzbcjSz zdb<_Vhx5O-kST-iHtEYP`slICKOejN>y&Sr{`;-~ib?B5=IwlBRnN0}w7{3T%6Qe} zJwkG@ZOdw0kqVM(_MRvaf_eyH(0eJ?_>!nv0oQD=*X5r+f5!SN!%;kj$d|lq()X#U ztb^1_M-nwLScq=21T-T^mq|4{wnJOcz+L9(v8Vews~^R_ zL6pVgCje=yKjd#UFQ%{U737gyXVT4+(KdxkHT-Y2!j^O)`SHV`7RrywQO37lS7{3v z!nmakW_SBGd{_~qq_UG36GKXbPc!uUJQ0^MQjL&)zU8>TEnuJUr1{yahBj)1m|`m_ z&0iGB6W8z-BOSD7rE=)ku<-pvkJv#;neU|2brp-^_3VyBJg{t z?jrip_(4KSpx_xOf=qNIDb0tsUl4gg5tI~@Xw!?PBC}<@ZRT@#Vb|aKM40o`%@c@i ztZk(~{`u#ho~J}5X!?edx@!%3Yt!?B%l9&}Z7py9yZzM(Y6;T@-1=}9DjwM0)elsc zt?mGC`1keomX?$kv3Mjg?-ss*H9!8cR)fwt{-gKJaEDSu|MCRoE+MoefF61Jm(Z#Y 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' + } +}; From 6ab25e40d904007755c5d999bf66ae264236e745 Mon Sep 17 00:00:00 2001 From: Maximilian Krambach Date: Wed, 18 Apr 2018 16:38:06 +0200 Subject: [PATCH 02/95] js: encrypt improvement and decrypt method * Compatibility class gpgme_openpgpjs offers an API that should accept openpgpjs syntax, throwing errors if a parameter is unexpected/not implemented * tried to be more generic in methods * waiting for multiple answers if 'more' is in the answer * more consistency checking on sending and receiving * updated the example extension -- --- lang/js/CHECKLIST | 18 +- lang/js/CHECKLIST_build | 6 +- lang/js/README | 23 ++- lang/js/manifest.json | 10 +- lang/js/package.json | 6 +- lang/js/src/Connection.js | 210 +++++++++++++++------ lang/js/src/Helpers.js | 84 +++++++++ lang/js/src/Message.js | 109 +++++++++++ lang/js/src/gpgmejs.js | 294 ++++++++++++----------------- lang/js/src/gpgmejs_openpgpjs.js | 156 +++++++++++++++ lang/js/src/index.js | 33 ++-- lang/js/src/permittedOperations.js | 75 ++++++++ lang/js/test_index.js | 25 +++ lang/js/testapplication.js | 70 +++++-- lang/js/testapplication_index.html | 9 + lang/js/ui.html | 24 --- lang/js/ui2.html | 33 ++++ lang/js/webpack.conf.js | 22 +++ 18 files changed, 894 insertions(+), 313 deletions(-) create mode 100644 lang/js/src/Helpers.js create mode 100644 lang/js/src/Message.js create mode 100644 lang/js/src/gpgmejs_openpgpjs.js create mode 100644 lang/js/src/permittedOperations.js create mode 100644 lang/js/test_index.js create mode 100644 lang/js/testapplication_index.html delete mode 100644 lang/js/ui.html create mode 100644 lang/js/ui2.html diff --git a/lang/js/CHECKLIST b/lang/js/CHECKLIST index 79a35cb7..49b17265 100644 --- a/lang/js/CHECKLIST +++ b/lang/js/CHECKLIST @@ -3,16 +3,19 @@ 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 + [*] 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) + [x] encrypt + [ ] verify + [ ] sign + [*] Key handling (import/export, modifying, status queries) [ ] Configuration handling [ ] check for completeness - [ ] handling of differences to openpgpjs + [*] handling of differences to openpgpjs Communication with other implementations @@ -21,10 +24,5 @@ Communication with other implementations Management: [*] Define the gpgme interface [ ] check Permissions (e.g. csp) for the different envs - [ ] agree on license + [X] 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 index fa162a10..19eb2146 100644 --- a/lang/js/CHECKLIST_build +++ b/lang/js/CHECKLIST_build @@ -4,6 +4,6 @@ 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. +/dist should be built which is used by the example app. + +csp in manifest.json MUST NOT contain "unsafe-eval" in production! diff --git a/lang/js/README b/lang/js/README index 3ca07439..5dc3f50b 100644 --- a/lang/js/README +++ b/lang/js/README @@ -1,20 +1,28 @@ -This is an example app for gpgme-json. -As of now, it only encrypts a given text. +gpgmejs, as contained in this directory, is a javascript library for direct use +of gnupg in browsers, with the help of nativeMessaging. Installation ------------- +gpgmejs uses webpack, and thus depends on nodejs for building. Webpack can be +installed by running +`npm install webpack webpack-cli --save-dev`. -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. +To create a current version of the 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. +TODO: gpgme_openpgpjs aims to offer an API similar to openpgpjs, throwing errors +if some part of the API is not implemented, 'translating' objects if possible. +This will be incorporated into the build process later, for now it is a line to +uncomment in src/index.js + Demo WebExtension: -As soon as a bundled webpack is in dist/ (TODO: .gitignore or not?), +As soon as a bundled webpack is in dist/ 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. +Please note that it is just for demonstration/debug purposes! In the browsers' nativeMessaging configuration folder a file 'gpgmejs.json' is needed, with the following content: @@ -47,6 +55,3 @@ Firefox: 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 index 8bb5c58d..e5e17aa5 100644 --- a/lang/js/manifest.json +++ b/lang/js/manifest.json @@ -4,15 +4,11 @@ "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", + "content_security_policy": "default-src 'self' 'unsafe-eval' filesystem:", "browser_action": { "default_icon": "testicon.png", "default_title": "gpgme.js", - "default_popup": "ui.html" + "default_popup": "testapplication_index.html" }, - "permissions": ["nativeMessaging", "activeTab"], - - "background": { - "scripts": [ "dist/gpgmejs.bundle.js"] - } + "permissions": ["nativeMessaging", "activeTab"] } diff --git a/lang/js/package.json b/lang/js/package.json index 46b60fd2..2b7dd7ee 100644 --- a/lang/js/package.json +++ b/lang/js/package.json @@ -2,7 +2,7 @@ "name": "gpgmejs", "version": "0.0.1", "description": "javascript part of a nativeMessaging gnupg integration", - "main": "src/gpgmejs.js", + "main": "src/index.js", "private": true, "scripts": { "test": "echo \"Error: no test specified\" && exit 1" @@ -11,7 +11,7 @@ "author": "", "license": "", "devDependencies": { - "webpack": "^4.3.0", - "webpack-cli": "^2.0.13" + "webpack": "^4.5.0", + "webpack-cli": "^2.0.14" } } diff --git a/lang/js/src/Connection.js b/lang/js/src/Connection.js index e8fea542..784929e9 100644 --- a/lang/js/src/Connection.js +++ b/lang/js/src/Connection.js @@ -1,76 +1,180 @@ +import { GPGME_Message } from "./Message"; + +/* 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' -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 - }; - }; +export class Connection{ - this.disconnect = function () { - if (this.connection){ - this.connection.disconnect(); + /** + * Opens and closes a port. Thus, it is made sure that the connection can + * be used. + * THIS BEHAVIOUR MAY CHANGE! + * discussion is to keep a port alive as long as the context stays the same + * + * TODO returns nothing, but triggers exceptions if not successfull + */ + constructor(){ + this._connection = chrome.runtime.connectNative('gpgmejson'); + if (!this._connection){ + if (chrome.runtime.lastError){ + throw('NO_CONNECT_RLE'); + } else { + throw('NO_CONNECT'); + } } - }; + this._flags = {}; // TODO general config + } + + /** + * Immediately closes the open port + */ + disconnect() { + 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 + * @param {GPGME_Message} message + * @returns {Promise} the gnupg answer, or rejection with error + * information + * TODO: better/more consistent error information */ - this.post = function(operation, message){ - let timeout = 5000; + post(message){ + if (!message || !message instanceof GPGME_Message){ + return Promise.reject('ERR_NO_MSG'); + } + // let timeout = 5000; //TODO config 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) { + let answer = new Answer(message.op); + let listener = function(msg) { if (!msg){ - reject('empty answer.'); - } - if (msg.type === "error"){ + me._connection.onMessage.removeListener(listener) + reject('EMPTY_ANSWER'); + } else if (msg.type === "error"){ + me._connection.onMessage.removeListener(listener) reject(msg.msg); + } else { + answer.add(msg); + if (msg.more === true){ + me._connection.postMessage({'op': 'getmore'}); + } else { + me._connection.onMessage.removeListener(listener) + resolve(answer.message); + } } - resolve(msg); - }); + }; - me.connection.postMessage(me._msg); - setTimeout( - function(){ - me.disconnect(); - reject('Timeout'); - }, timeout); + me._connection.onMessage.addListener(listener); + me._connection.postMessage(message); + //TBD: needs to be aware if there is a pinentry pending + // setTimeout( + // function(){ + // me.disconnect(); + // reject('TIMEOUT'); + // }, timeout); }); - }; + } }; +/** + * 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 return keys + */ +class Answer{ -function connect(){ - let connection = chrome.runtime.connectNative('gpgmejson'); - if (!connection){ - let msg = chrome.runtime.lastError || 'no message'; //TBD - throw(msg); + constructor(operation){ + this.operation = operation; } - return connection; -}; + + /** + * + * @param {Object} msg The message as received with nativeMessaging + * TODO: "error" and "more" handling are not in here, but in post() + */ + add(msg){ + if (this._response === undefined){ + this._response = {}; + } + let messageKeys = Object.keys(msg); + let poa = permittedOperations[this.operation].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){ + console.log( 'unexpected answer type: ' + msg.type); + throw('UNEXPECTED_TYPE'); + + } + 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]){ + throw('UNEXPECTED_TYPE'); + } + } + //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 { + console.log('unexpected answer parameter: ' + key); + throw('UNEXPECTED_PARAM'); + } + break; + } + } + } + + /** + * Returns the assembled message. TODO: does not care yet if completed. + */ + get message(){ + return this._response; + } +} diff --git a/lang/js/src/Helpers.js b/lang/js/src/Helpers.js new file mode 100644 index 00000000..eeb7a3c4 --- /dev/null +++ b/lang/js/src/Helpers.js @@ -0,0 +1,84 @@ +/* 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+ + */ + +/** + * Tries to return an array of fingerprints, either from input fingerprints or + * from Key objects + * @param {String|Array} input Input value. + * @returns {Array} Array of fingerprints. + */ +export function toKeyIdArray(input){ + if (!input){ + return []; + // TODO: Warning or error here? Did we expect something or is "nothing" okay? + } + if (input instanceof Array){ + let result = []; + for (let i=0; i < input.length; i++){ + if (isFingerprint(input[i]) === true){ + result.push(input[i]); + } else { + //TODO error? + console.log('gpgmejs/Helpers.js Warning: '+ + input[i] + + ' is not a valid key fingerprint and will not be used'); + } + } + return result; + } else if (isFingerprint(input) === true) { + return [input]; + } + console.log('gpgmejs/Helpers.js Warning: ' + input + + ' is not a valid key fingerprint and will not be used'); + return []; +}; + +/** + * 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); +}; + +//TODO needed anywhere? +function isLongId(string){ + return hextest(string, 16); +}; + +//TODO needed anywhere? +function isShortId(string){ + return hextest(string, 8); +}; diff --git a/lang/js/src/Message.js b/lang/js/src/Message.js new file mode 100644 index 00000000..90b554a1 --- /dev/null +++ b/lang/js/src/Message.js @@ -0,0 +1,109 @@ +/* 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' + +export class GPGME_Message { + //TODO getter + + constructor(){ + } + + /** + * Defines the operation this message will have + * @param {String} operation Mus be defined in permittedOperations + * TODO: move to constructor? + */ + set operation (operation){ + if (!operation || typeof(operation) !== 'string'){ + throw('ERR_WRONG_PARAM'); + } + if (operation in permittedOperations){ + if (!this._msg){ + this._msg = {}; + } + this._msg.op = operation; + } else { + throw('ERR_NOT_IMPLEMENTED'); + } + } + + /** + * 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'){ + throw('ERR_WRONG_PARAM'); + } + if (!this._msg || !this._msg.op){ + console.log('There is no operation specified yet. '+ + 'The parameter cannot be set'); + return false; + } + let po = permittedOperations[this._msg.op]; + if (!po){ + throw('LAZY_PROGRAMMER'); + //TODO + return false; + } + if (po.required.indexOf(param) >= 0 || po.optional.indexOf(param) >= 0){ + this._msg[param] = value; + return true; + } + console.log('' + param + ' is invalid and could not be set'); + return false; + } + + /** + * 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 (!reqParams[i] in this._msg){ + 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; + } + + } +} \ No newline at end of file diff --git a/lang/js/src/gpgmejs.js b/lang/js/src/gpgmejs.js index dedbf809..8323ac3b 100644 --- a/lang/js/src/gpgmejs.js +++ b/lang/js/src/gpgmejs.js @@ -1,187 +1,131 @@ +/* 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 {toKeyIdArray} from "./Helpers" -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 +export class GpgME { + /** + * initial check if connection si successfull. Will throw ERR_NO_CONNECT or + * ERR_NO_CONNECT_RLE (if chrome.runtime.lastError is available) if the + * connection fails. + * TODO The connection to the nativeMessaging host will, for now, be closed + * after each interaction. Session management with gpg_agent is TBD. + * TODO: add configuration + */ + constructor(){ + let conn = new Connection(); + // this.keyring = new Keyring(); TBD + // TODO config, e.g. + this.configuration = { + null_expire_is_never: true + }; + conn.disconnect(); } - let pubkeys = toKeyIdArray(publicKeys); - let privkeys = toKeyIdArray(privateKeys); + /** + * @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){ - // 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) + let msg = new GPGME_Message; + msg.operation = 'encrypt'; - // TODO sign date overwriting implemented in gnupg? + // TODO temporary + msg.setParameter('armor', true); + msg.setParameter('always-trust', true); - 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}); -}; + let pubkeys = toKeyIdArray(publicKeys); + msg.setParameter('keys', pubkeys); -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 - }); -} + putData(msg, data); + if (wildcard === true){msg.setParameter('throw-keyids', true); + }; -// 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]; + if (msg.isComplete === true) { + let conn = new Connection(); + return (conn.post(msg.message)); } - return result; - } - for (let i=0; i < array.length; i++){ - if (isKeyId(array[i]) === true){ - result.push(array[i]); + else { + return Promise.reject('NO_CONNECT'); + //TODO } } - return result; -}; + + /** + * @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){ + throw('ERR_EMPTY_MSG'); + } + let msg = new GPGME_Message; + msg.operation = 'decrypt'; + putData(msg, data); + // TODO: needs proper EOL to be decrypted. + + if (msg.isComplete === true){ + let conn = new Connection(); + return conn.post(msg.message); + } + else { + return Promise.reject('NO_CONNECT'); + //TODO + } + } +} + +/** + * 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 ) { + throw('NO_MESSAGE_OBJECT'); + } + if (!data){ + //TODO Debug only! No data is legitimate + console.log('Warning. no data in message'); + 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 { + throw('ERR_WRONG_TYPE'); + } +} \ No newline at end of file diff --git a/lang/js/src/gpgmejs_openpgpjs.js b/lang/js/src/gpgmejs_openpgpjs.js new file mode 100644 index 00000000..1eec4da4 --- /dev/null +++ b/lang/js/src/gpgmejs_openpgpjs.js @@ -0,0 +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+ + */ + +/** + * This is a compatibility API to be used as openpgpjs syntax. + * Non-implemented options will throw an error if set (not null or undefined) + * TODO Some info about differences + */ + + import { GpgME } from "./gpgmejs"; +// import {Keyring} from "./Keyring" TODO + + +export class GpgME_openPGPCompatibility { + + constructor(){ + this._gpgme = new GpgME; + } + + /** + * Encrypt Message + * Supported: + * @param {String|Uint8Array} data + * @param {Key|Array} publicKeys + * @param {Boolean} wildcard + * TODO: + * @param {Key|Array} privateKeys + * @param {String} filename + * @param {module:enums.compression} compression + * @param {Boolean} armor + * @param {Boolean} detached + * unsupported: + * @param {String|Array} passwords + * @param {Object} sessionKey + * @param {Signature} signature + * @param {Boolean} returnSessionKey + * + * @returns {Promise} + * {data: ASCII armored message, + * signature: detached signature if 'detached' is true + * } + * @async + * @static + */ + encrypt({data = '', publicKeys = '', privateKeys, passwords, sessionKey, + filename, compression, armor=true, detached=false, signature=null, + returnSessionKey=null, wildcard=false, date=null}) { + if (passwords !== undefined + || sessionKey !== undefined + || signature !== null + || returnSessionKey !== null + || date !== null){ + throw('NOT_IMPLEMENTED'); + } + if ( privateKeys + || filename + || compression + || armor === false + || detached == true){ + console.log('may be implemented later'); + throw('NOT_YET_IMPLEMENTED'); + } + return this.GpgME.encrypt(data, translateKeyInput(publicKeys), wildcard); + } + + /** Decrypt Message + * supported + * TODO: @param {Message} message TODO: for now it accepts an armored string only + * Unsupported: + * @param {String|Array} passwords + * @param {Object|Array} sessionKeys + * @param {Date} date + + * TODO + * @param {Key|Array} privateKey + * @param {Key|Array} publicKeys + * @param {String} format (optional) return data format either as 'utf8' or 'binary' + * @param {Signature} signature (optional) detached signature for verification + + * @returns {Promise} decrypted and verified message in the form: + * { data:Uint8Array|String, filename:String, signatures:[{ keyid:String, valid:Boolean }] } + * @async + * @static + */ + decrypt({ message, privateKeys, passwords, sessionKeys, publicKeys, format='utf8', signature=null, date}) { + if (passwords !== undefined + || sessionKeys + || date){ + + throw('NOT_IMPLEMENTED'); + } + if ( privateKeys + || publicKeys + || format !== 'utf8' + || signature + ){ + console.log('may be implemented later'); + throw('NOT_YET_IMPLEMENTED'); + } + return this.GpgME.decrypt(message); + // TODO: translate between: + // openpgp: + // { data:Uint8Array|String, + // filename:String, + // signatures:[{ keyid:String, valid:Boolean }] } + // and gnupg: + // 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. + } +} + +/** + * + * @param {Object | String} Key Either a (presumably openpgp Key) Object with a + * primaryKeyproperty and a method getFingerprint, or a string. + * @returns {String} Unchecked string value claiming to be a fingerprint + * TODO: gpgmejs checks again, so it's okay here. + */ +function translateKeyInput(Key){ + if (!Key){ + return []; + } + if (!Array.isArray(Key)){ + Key = [Key]; + } + let resultslist = []; + for (let i=0; i < Key.length; i++){ + if (typeof(Key[i]) === 'string'){ + resultslist.push(Key); + } else if ( + Key[i].hasOwnProperty(primaryKey) && + Key[i].primaryKey.hasOwnProperty(getFingerprint)){ + resultslist.push(Key[i].primaryKey.getFingerprint()); + } + } + return resultslist; +} \ No newline at end of file diff --git a/lang/js/src/index.js b/lang/js/src/index.js index 02dc919d..f70bd2d8 100644 --- a/lang/js/src/index.js +++ b/lang/js/src/index.js @@ -1,14 +1,23 @@ -import * as gpgmejs from'./gpgmejs' -export default gpgmejs; - -/** - * Export each high level api function separately. - * Usage: +/* gpgme.js - Javascript integration for gpgme + * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik * - * import { encryptMessage } from 'gpgme.js' - * encryptMessage(keys, text) + * 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+ */ -export { - encrypt, decrypt, sign, verify, - generateKey, reformatKey - } from './gpgmejs'; + +import { GpgME as gpgmejs } from "./gpgmejs"; +// import { GpgME_openPGPCompatibility as gpgmejs } from "./gpgmejs_openpgpjs"; +export default gpgmejs; diff --git a/lang/js/src/permittedOperations.js b/lang/js/src/permittedOperations.js new file mode 100644 index 00000000..3c11b8e0 --- /dev/null +++ b/lang/js/src/permittedOperations.js @@ -0,0 +1,75 @@ +/* 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+ + */ + + /** + * Definition of the possible interactions with gpgme-json. + * operation: + required: Array + optional: Array + answer: + type: The payload property of the answer. May be + partial and in need of concatenation + params: Array Information that do not change throughout + the message + infos: Array arbitrary information that may change + } + } + */ + +export const permittedOperations = { + encrypt: { + required: ['keys', 'data'], + optional: [ + 'protocol', + 'chunksize', + 'base64', + 'mime', + 'armor', + 'always-trust', + 'no-encrypt-to', + 'no-compress', + 'throw-keyids', + 'want-address', + 'wrap' + ], + answer: { + type: ['ciphertext'], + data: ['data'], + params: ['base64'], + infos: [] + } + }, + + decrypt: { + required: ['data'], + optional: [ + 'protocol', + 'chunksize', + 'base64' + ], + answer: { + type: ['plaintext'], + data: ['data'], + params: ['base64', 'mime'], + infos: ['info'] + } + } +} diff --git a/lang/js/test_index.js b/lang/js/test_index.js new file mode 100644 index 00000000..9119d271 --- /dev/null +++ b/lang/js/test_index.js @@ -0,0 +1,25 @@ +/* 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+ + * + */ +document.addEventListener('DOMContentLoaded', function() { + chrome.tabs.create({ + url: './ui2.html' + }); +}); diff --git a/lang/js/testapplication.js b/lang/js/testapplication.js index d01aca99..97b35527 100644 --- a/lang/js/testapplication.js +++ b/lang/js/testapplication.js @@ -1,21 +1,57 @@ -/** -* 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); +/* 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+ + * + */ + +function encryptbuttonclicked(event){ + let data = document.getElementById('cleartext').value; + let keyId = document.getElementById('pubkey').value; + let communication = new Gpgmejs; + let enc = communication.encrypt(data, keyId).then( + function(answer){ + console.log(answer); + if (answer.data){ + console.log(answer.data); + document.getElementById('answer').value = answer.data; + } + }, function(errormsg){ + alert('Error: '+ errormsg); + }); +}; + +function decryptbuttonclicked(event){ + let data = document.getElementById("ciphertext").value; + let communication = new Gpgmejs; + let enc = communication.decrypt(data).then( + function(answer){ + console.log(answer); + if (answer.data){ + document.getElementById('answer').value = answer.data; + } + }, function(errormsg){ + alert('Error: '+ errormsg); }); }; document.addEventListener('DOMContentLoaded', function() { - document.getElementById("button0").addEventListener("click", - buttonclicked); - }); + document.getElementById("buttonencrypt").addEventListener("click", + encryptbuttonclicked); + document.getElementById("buttondecrypt").addEventListener("click", + decryptbuttonclicked); +}); diff --git a/lang/js/testapplication_index.html b/lang/js/testapplication_index.html new file mode 100644 index 00000000..866b1135 --- /dev/null +++ b/lang/js/testapplication_index.html @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/lang/js/ui.html b/lang/js/ui.html deleted file mode 100644 index 9c56c2e5..00000000 --- a/lang/js/ui.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - -
    -
  • - Text: - -
  • -
  • - Public key ID: - -
  • -
-
-
- - diff --git a/lang/js/ui2.html b/lang/js/ui2.html new file mode 100644 index 00000000..8d0abd97 --- /dev/null +++ b/lang/js/ui2.html @@ -0,0 +1,33 @@ + + + + + + + + + +
    +
  • + Text: + +
  • +
  • + Public key ID: + +
  • +
+
+
+
    +
  • + Encrypted armored Text: + +
  • +
+
+
+

Result data:

+ + + diff --git a/lang/js/webpack.conf.js b/lang/js/webpack.conf.js index 71b71161..7a5392ee 100644 --- a/lang/js/webpack.conf.js +++ b/lang/js/webpack.conf.js @@ -1,3 +1,24 @@ +/* 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+ + * + * This is the configuration file for building the gpgmejs-Library with webpack + */ const path = require('path'); module.exports = { @@ -8,6 +29,7 @@ module.exports = { path: path.resolve(__dirname, 'dist'), filename: 'gpgmejs.bundle.js', libraryTarget: 'var', + libraryExport: 'default', library: 'Gpgmejs' } }; From d62f66b1fb47f2075770d896f672748a4136e70b Mon Sep 17 00:00:00 2001 From: Maximilian Krambach Date: Mon, 23 Apr 2018 17:18:46 +0200 Subject: [PATCH 03/95] js: Key handling stubs, Error handling, refactoring -- * Error handling: introduced GPGMEJS_Error class that handles errors at a more centralized and consistent position * src/Connection.js: The nativeMessaging port now opens per session instead of per message. Some methods were added that reflect this change - added methods disconnect() and reconnect() - added connection status query * src/gpgmejs.js - stub for key deletion - error handling - high level API for changing connection status * src/gpgmejs_openpgpjs.js - added stubs for Key/Keyring handling according to current state of discussion. It is still subject to change * src/Helpers.js - toKeyIdArray creates an array of KeyIds, now accepting fingerprints, GPGMEJS_Key objects and openpgp Key objects. * Key objects (src/Key.js) Querying information about a key directly from gnupg. Currently a stub, only the Key.fingerprint is functional. * Keyring queries (src/Keyring.js): Listing and searching keys. Currently a stub. --- lang/js/src/Connection.js | 86 +++++++------ lang/js/src/Errors.js | 148 +++++++++++++++++++++++ lang/js/src/Helpers.js | 56 ++++++--- lang/js/src/Key.js | 201 +++++++++++++++++++++++++++++++ lang/js/src/Keyring.js | 151 +++++++++++++++++++++++ lang/js/src/Message.js | 34 +++--- lang/js/src/gpgmejs.js | 112 ++++++++++++----- lang/js/src/gpgmejs_openpgpjs.js | 105 +++++++++++----- 8 files changed, 759 insertions(+), 134 deletions(-) create mode 100644 lang/js/src/Errors.js create mode 100644 lang/js/src/Key.js create mode 100644 lang/js/src/Keyring.js diff --git a/lang/js/src/Connection.js b/lang/js/src/Connection.js index 784929e9..87ec8cf7 100644 --- a/lang/js/src/Connection.js +++ b/lang/js/src/Connection.js @@ -26,31 +26,19 @@ import { GPGME_Message } from "./Message"; * expected. */ import { permittedOperations } from './permittedOperations' +import { GPGMEJS_Error} from "./Errors" +/** + * A Connection handles the nativeMessaging interaction. + */ export class Connection{ - /** - * Opens and closes a port. Thus, it is made sure that the connection can - * be used. - * THIS BEHAVIOUR MAY CHANGE! - * discussion is to keep a port alive as long as the context stays the same - * - * TODO returns nothing, but triggers exceptions if not successfull - */ constructor(){ - this._connection = chrome.runtime.connectNative('gpgmejson'); - if (!this._connection){ - if (chrome.runtime.lastError){ - throw('NO_CONNECT_RLE'); - } else { - throw('NO_CONNECT'); - } - } - this._flags = {}; // TODO general config + this.connect(); } /** - * Immediately closes the open port + * Immediately closes the open port. */ disconnect() { if (this._connection){ @@ -58,27 +46,56 @@ export class Connection{ } } + /** + * Opens a nativeMessaging port. + * returns nothing, but triggers errors if not successfull: + * NO_CONNECT: connection not successfull, chrome.runtime.lastError may be + * available + * ALREADY_CONNECTED: There is already a connection present. + */ + connect(){ + if (this._connection){ + return new GPGMEJS_Error('ALREADY_CONNECTED'); + } + this._connection = chrome.runtime.connectNative('gpgmejson'); + if (!this._connection){ + return new GPGMEJS_Error('NO_CONNECT'); + } + } + + /** + * checks if the connection is established + * TODO: some kind of ping to see if the other side responds as expected? + * @returns {Boolean} + */ + get connected(){ + return this._connection ? true: false; + } + /** * Sends a message and resolves with the answer. * @param {GPGME_Message} message * @returns {Promise} the gnupg answer, or rejection with error - * information - * TODO: better/more consistent error information + * information. */ post(message){ if (!message || !message instanceof GPGME_Message){ - return Promise.reject('ERR_NO_MSG'); + return Promise.reject(new GPGMEJS_Error('WRONGPARAM')); + } + if (message.isComplete !== true){ + return Promise.reject(new GPGMEJS_Error('MSG_INCOMPLETE')); } // let timeout = 5000; //TODO config let me = this; return new Promise(function(resolve, reject){ - let answer = new Answer(message.op); + let answer = new Answer(message.operation); let listener = function(msg) { if (!msg){ me._connection.onMessage.removeListener(listener) - reject('EMPTY_ANSWER'); + reject(new GPGMEJS_Error('EMPTY_GPG_ANSWER')); } else if (msg.type === "error"){ me._connection.onMessage.removeListener(listener) + //TODO: GPGMEJS_Error? reject(msg.msg); } else { answer.add(msg); @@ -92,12 +109,12 @@ export class Connection{ }; me._connection.onMessage.addListener(listener); - me._connection.postMessage(message); + me._connection.postMessage(message.message); //TBD: needs to be aware if there is a pinentry pending // setTimeout( // function(){ // me.disconnect(); - // reject('TIMEOUT'); + // reject(new GPGMEJS_Error('TIMEOUT', 5000)); // }, timeout); }); } @@ -105,8 +122,8 @@ export class Connection{ /** * 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 return keys + * the nativeMessaging communication. + * @param {String} operation The operation, to look up validity of returning messages */ class Answer{ @@ -115,9 +132,8 @@ class Answer{ } /** - * + * Add the information to the answer * @param {Object} msg The message as received with nativeMessaging - * TODO: "error" and "more" handling are not in here, but in post() */ add(msg){ if (this._response === undefined){ @@ -130,9 +146,7 @@ class Answer{ switch (key) { case 'type': if ( msg.type !== 'error' && poa.type.indexOf(msg.type) < 0){ - console.log( 'unexpected answer type: ' + msg.type); - throw('UNEXPECTED_TYPE'); - + return new GPGMEJS_Error('UNEXPECTED_ANSWER'); } break; case 'more': @@ -151,7 +165,7 @@ class Answer{ this._response[key] = msg[key]; } else if (this._response[key] !== msg[key]){ - throw('UNEXPECTED_TYPE'); + return new GPGMEJS_Error('UNEXPECTED_ANSWER',msg[key]); } } //infos may be json objects etc. Not yet defined. @@ -163,8 +177,7 @@ class Answer{ this._response.push(msg[key]); } else { - console.log('unexpected answer parameter: ' + key); - throw('UNEXPECTED_PARAM'); + return new GPGMEJS_Error('UNEXPECTED_ANSWER', key); } break; } @@ -172,7 +185,8 @@ class Answer{ } /** - * Returns the assembled message. TODO: does not care yet if completed. + * @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 new file mode 100644 index 00000000..c2356f7c --- /dev/null +++ b/lang/js/src/Errors.js @@ -0,0 +1,148 @@ +/* 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+ + */ + +// This is a preliminary collection of erors and warnings to be thrown and implemented. + +// general idea: if throw , throw the NAME +// return false || 'return' property + +//TODO: Connection.NOCONNECT promise +//connection.timeout: Be aware of pinentry + +export class GPGMEJS_Error { + + constructor(code = 'GENERIC_ERROR', details){ + let config = { //TODO TEMP + debug: 'console', // |'alert' + throw: 'default' // | 'always' | 'never' + }; + let errors = { //TODO: someplace else + //Connection errors + 'ALREADY_CONNECTED':{ + msg: 'The connection was already established. The action would overwrite the context', + throw: true + }, + 'NO_CONNECT': { + msg:'Connection with the nativeMessaging host could not be established.', + throw: true + }, + 'EMPTY_GPG_ANSWER':{ + msg: 'The nativeMesaging answer was empty', + throw: true + }, + 'TIMEOUT': { + msg: 'A timeout was exceeded.', + throw: false + }, + + 'UNEXPECTED_ANSWER': { + msg: 'The answer from gnupg was not as expected', + throw: true + }, + + // Message/Data Errors + + 'NO_KEYS' : { + msg: 'There were no valid keys provided.', + throw: true + }, + 'NOT_A_FPR': { + msg: 'The String is not an accepted fingerprint', + throw: false + }, + 'MSG_INCOMPLETE': { + msg: 'The Message did not match the minimum requirements for the interaction', + throw: true + }, + 'EMPTY_MSG' : { + msg: 'The Message has no data.', + throw: true + }, + 'MSG_NODATA':{ + msg: 'The data sent is empty. This may be unintentional.', + throw: false + }, + 'MSG_OP_PENDING': { + msg: 'There is no operation specified yet. The parameter cannot be set', + throw: false + }, + 'WRONG_OP': { + msg: "The operation requested could not be found", + throw: true + }, + + //generic errors + + 'WRONGPARAM':{ + msg: 'invalid parameter was found', + throw: true + }, + 'WRONGTYPE':{ + msg: 'invalid parameter type was found', + throw: true + }, + 'NOT_IMPLEMENTED': { + msg: 'A openpgpjs parameter was submitted that is not implemented', + throw: true + }, + 'GENERIC_ERROR': { + msg: 'Unspecified error', + throw: true + }, + + // hopefully temporary errors + + 'NOT_YET_IMPLEMENTED': { + msg: 'Support of this is probable, but it is not implemented yet', + throw: false + } + } + if (!errors.hasOwnProperty(code)){ + throw('GENERIC_ERROR'); + } + let msg = code; + if (errors[code].msg !== undefined){ + msg = msg + ': ' + errors[code].msg; + } + if (details){ + msg = msg + ' ' + details; + } + if (config.debug === 'console'){ + console.log(msg); + } else if (config.debug === 'alert'){ + alert(msg); + } + switch (config.throw) { + case 'default': + if (errors[code].throw === true){ + throw(code); + } + break; + case 'always': + throw(code); + break; + + case 'never': + break; + default: + throw('GENERIC_ERROR'); + } + } +} diff --git a/lang/js/src/Helpers.js b/lang/js/src/Helpers.js index eeb7a3c4..922ca06c 100644 --- a/lang/js/src/Helpers.js +++ b/lang/js/src/Helpers.js @@ -1,3 +1,5 @@ +import { GPGMEJS_Error } from "./Errors"; + /* gpgme.js - Javascript integration for gpgme * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik * @@ -21,33 +23,46 @@ /** * Tries to return an array of fingerprints, either from input fingerprints or * from Key objects - * @param {String|Array} input Input value. + * @param {Key |Array| GPGME_Key | Array|String|Array} input + * @param {Boolean} nocheck if set, an empty result is acceptable * @returns {Array} Array of fingerprints. */ -export function toKeyIdArray(input){ + +export function toKeyIdArray(input, nocheck){ if (!input){ - return []; - // TODO: Warning or error here? Did we expect something or is "nothing" okay? + return (nocheck ===true)? [] : new GPGMEJS_Error('NO_KEYS'); } - if (input instanceof Array){ - let result = []; - for (let i=0; i < input.length; i++){ + 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 { - //TODO error? - console.log('gpgmejs/Helpers.js Warning: '+ - input[i] + - ' is not a valid key fingerprint and will not be used'); + GPGMEJS_Error } + } else if (typeof(input[i]) === 'object'){ + let fpr = ''; + if (input[i] instanceof GPGME_Key){ + 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 { + return new GPGMEJS_Error('WRONGTYPE'); } - return result; - } else if (isFingerprint(input) === true) { - return [input]; } - console.log('gpgmejs/Helpers.js Warning: ' + input + - ' is not a valid key fingerprint and will not be used'); - return []; + if (result.length === 0){ + return (nocheck===true)? [] : new GPGMEJS_Error('NO_KEYS'); + } else { + return result; + } }; /** @@ -72,13 +87,14 @@ function hextest(key, len){ export function isFingerprint(string){ return hextest(string, 40); }; - -//TODO needed anywhere? +/** + * check if the input is a valid Hex string with a length of 16 + */ function isLongId(string){ return hextest(string, 16); }; -//TODO needed anywhere? +// 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 new file mode 100644 index 00000000..d8f16c55 --- /dev/null +++ b/lang/js/src/Key.js @@ -0,0 +1,201 @@ +/* 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 {GPGMEJS_Error} from './Errors' + +export class GPGME_Key { + + constructor(fingerprint){ + if (isFingerprint(fingerprint) === true){ + this._fingerprint = fingerprint; + } else { + return new GPGMEJS_Error('WRONGPARAM', 'Key.js: invalid fingerprint'); + } + } + + 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(new GPGMEJS_Error('NOT_YET_IMPLEMENTED')); + + return new Promise(function(resolve, reject){ + if (!isFingerprint(fingerprint)){ + reject('not a fingerprint'); //TBD + } + let conn = new Connection(); + conn.post('getkey',{ // TODO not yet implemented in gpgme + 'fingerprint': this.fingerprint}) + .then(function(result){ + if (property !== undefined){ + if (result.hasOwnProperty(key)){ + resolve(result[property]); + } + else if (property == 'secret'){ + // property undefined means "not true" in case of secret + resolve(false); + } else { + reject('ERR_INVALID_PROPERTY') //TBD + } + } + + + resolve(result); + }, function(error){ + reject(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..52fa7f71 --- /dev/null +++ b/lang/js/src/Keyring.js @@ -0,0 +1,151 @@ +/* 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 {Connection} from './Connection' +import {GPGME_Key} from './Key' +import { isFingerprint, isLongId } from './Helpers'; + +export class GPGME_Keyring { + constructor(){ + this.reconnect(); + } + + /** + * (Re)-establishes the connection + * TODO TEMP: should we better use the connection of our parent, + * which we do not control? + */ + reconnect(){ + if (!this._connection || ! this._connection instanceof Connection){ + this._connection = new Connection; + } else { + this._connection.disconnect(); + this._connection.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; + msg.operation = 'listkeys'; + 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.defaultKey Only Keys marked as Default Keys + * @param {Boolean} flags.secret Only Keys containing a secret part. + * @param {Boolean} flags.valid Valid Keys only + * @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 90b554a1..6a93b6f4 100644 --- a/lang/js/src/Message.js +++ b/lang/js/src/Message.js @@ -18,21 +18,24 @@ * SPDX-License-Identifier: LGPL-2.1+ */ import { permittedOperations } from './permittedOperations' - +import { GPGMEJS_Error } from './Errors' export class GPGME_Message { //TODO getter - constructor(){ + constructor(operation){ + if (operation){ + this.operation(operation); + } } /** * Defines the operation this message will have - * @param {String} operation Mus be defined in permittedOperations + * @param {String} operation Must be defined in permittedOperations * TODO: move to constructor? */ set operation (operation){ if (!operation || typeof(operation) !== 'string'){ - throw('ERR_WRONG_PARAM'); + return new GPGMEJS_Error('WRONGPARAM'); } if (operation in permittedOperations){ if (!this._msg){ @@ -40,10 +43,14 @@ export class GPGME_Message { } this._msg.op = operation; } else { - throw('ERR_NOT_IMPLEMENTED'); + return new GPGMEJS_Error('WRONG_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 @@ -53,25 +60,20 @@ export class GPGME_Message { */ setParameter(param,value){ if (!param || typeof(param) !== 'string'){ - throw('ERR_WRONG_PARAM'); + return new GPGMEJS_Error('WRONGPARAM', 'type check failed'); } if (!this._msg || !this._msg.op){ - console.log('There is no operation specified yet. '+ - 'The parameter cannot be set'); - return false; + return new GPGMEJS_Error('MSG_OP_PENDING'); } let po = permittedOperations[this._msg.op]; if (!po){ - throw('LAZY_PROGRAMMER'); - //TODO - return false; + return new GPGMEJS_Error('WRONG_OP', param); } if (po.required.indexOf(param) >= 0 || po.optional.indexOf(param) >= 0){ this._msg[param] = value; return true; } - console.log('' + param + ' is invalid and could not be set'); - return false; + return new GPGMEJS_Error('WRONGPARAM', param); } /** @@ -85,7 +87,9 @@ export class GPGME_Message { } let reqParams = permittedOperations[this._msg.op].required; for (let i=0; i < reqParams.length; i++){ - if (!reqParams[i] in this._msg){ + + if (!this._msg.hasOwnProperty(reqParams[i])){ + console.log(reqParams[i] + 'missing'); return false; } } diff --git a/lang/js/src/gpgmejs.js b/lang/js/src/gpgmejs.js index 8323ac3b..c23a356b 100644 --- a/lang/js/src/gpgmejs.js +++ b/lang/js/src/gpgmejs.js @@ -21,26 +21,54 @@ import {Connection} from "./Connection" import {GPGME_Message} from './Message' import {toKeyIdArray} from "./Helpers" +import {GPGMEJS_Error as Error, GPGMEJS_Error} from "./Errors" export class GpgME { /** - * initial check if connection si successfull. Will throw ERR_NO_CONNECT or - * ERR_NO_CONNECT_RLE (if chrome.runtime.lastError is available) if the - * connection fails. - * TODO The connection to the nativeMessaging host will, for now, be closed - * after each interaction. Session management with gpg_agent is TBD. + * initializes GpgME by opening a nativeMessaging port * TODO: add configuration */ - constructor(){ - let conn = new Connection(); - // this.keyring = new Keyring(); TBD - // TODO config, e.g. - this.configuration = { - null_expire_is_never: true - }; - conn.disconnect(); + constructor(configuration = { + null_expire_is_never: false + }){ + this._connection = new Connection; } + /** + * refreshes the nativeApp connection + */ + reconnect(){ + if (!this._connection || ! this._connection instanceof Connection){ + this._connection = new Connection; + } else { + this._connection.disconnect(); + this._connection.connect(); + } + } + + /** + * inmediately tries to destroy the nativeMessaging connection. + * TODO: may not be included in final API, as it is redundant. + * For now, it just serves paranoia + */ + disconnect(){ + if (this._connection){ + this._connection.disconnect(); + this._connection = null; + } + } + + /** + * tests the nativeApp connection + */ + get connected(){ + if (!this._connection || ! this._connection instanceof Connection){ + return false; + } + return this._connection.connected; + } + + /** * @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 @@ -62,14 +90,7 @@ export class GpgME { if (wildcard === true){msg.setParameter('throw-keyids', true); }; - if (msg.isComplete === true) { - let conn = new Connection(); - return (conn.post(msg.message)); - } - else { - return Promise.reject('NO_CONNECT'); - //TODO - } + return (this._connection.post(msg)); } /** @@ -85,22 +106,47 @@ export class GpgME { decrypt(data){ if (data === undefined){ - throw('ERR_EMPTY_MSG'); + return Promise.reject(new GPGMEJS_Error ('EMPTY_MSG')); } let msg = new GPGME_Message; msg.operation = 'decrypt'; putData(msg, data); - // TODO: needs proper EOL to be decrypted. + return this._connection.post(msg); - if (msg.isComplete === true){ - let conn = new Connection(); - return conn.post(msg.message); - } - else { - return Promise.reject('NO_CONNECT'); - //TODO - } } + + deleteKey(key, delete_secret = false, no_confirm = false){ + return Promise.reject(new GPGMEJS_Error ('NOT_YET_IMPLEMENTED')); + let msg = new GPGME_Message; + msg.operation = 'deletekey'; + let key_arr = toKeyIdArray(key); + if (key_arr.length !== 1){ + throw('TODO'); + //should always be ONE key + } + msg.setParameter('key', key_arr[0]); + if (delete_secret === true){ + 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 + } + this._connection.post(msg).then(function(success){ + //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(new GPGMEJS_Error); + // INV_VALUE, + // GPG_ERR_NO_PUBKEY, + // GPG_ERR_AMBIGUOUS_NAME, + // GPG_ERR_CONFLICT + } + }); + } + } /** @@ -112,7 +158,7 @@ export class GpgME { */ function putData(message, data){ if (!message || !message instanceof GPGME_Message ) { - throw('NO_MESSAGE_OBJECT'); + return new GPGMEJS_Error('WRONGPARAM'); } if (!data){ //TODO Debug only! No data is legitimate @@ -126,6 +172,6 @@ function putData(message, data){ message.setParameter('base64', false); message.setParameter('data', data); } else { - throw('ERR_WRONG_TYPE'); + return new GPGMEJS_Error('WRONGPARAM'); } } \ No newline at end of file diff --git a/lang/js/src/gpgmejs_openpgpjs.js b/lang/js/src/gpgmejs_openpgpjs.js index 1eec4da4..54b9dd45 100644 --- a/lang/js/src/gpgmejs_openpgpjs.js +++ b/lang/js/src/gpgmejs_openpgpjs.js @@ -25,13 +25,18 @@ */ import { GpgME } from "./gpgmejs"; -// import {Keyring} from "./Keyring" TODO - + import {GPGME_Keyring} from "./Keyring" + import { GPGME_Key } from "./Key"; + import { isFingerprint } from "./Helpers" + import { GPGMEJS_Error } from './Errors' export class GpgME_openPGPCompatibility { constructor(){ - this._gpgme = new GpgME; + this._gpgme = new GpgME({ + null_expire_is_never: false + }); + this.Keyring = this.initKeyring(); } /** @@ -67,15 +72,14 @@ export class GpgME_openPGPCompatibility { || signature !== null || returnSessionKey !== null || date !== null){ - throw('NOT_IMPLEMENTED'); + return Promise.reject(new GPMGEJS_Error('NOT_IMPLEMENTED')); } if ( privateKeys || filename || compression || armor === false || detached == true){ - console.log('may be implemented later'); - throw('NOT_YET_IMPLEMENTED'); + return Promise.reject(new GPGMEJS_Error('NOT_YET_IMPLEMENTED')); } return this.GpgME.encrypt(data, translateKeyInput(publicKeys), wildcard); } @@ -103,16 +107,14 @@ export class GpgME_openPGPCompatibility { if (passwords !== undefined || sessionKeys || date){ - - throw('NOT_IMPLEMENTED'); + return Promise.reject(new GPGMEJS_Error('NOT_IMPLEMENTED')); } if ( privateKeys || publicKeys || format !== 'utf8' || signature ){ - console.log('may be implemented later'); - throw('NOT_YET_IMPLEMENTED'); + return Promise.reject(new GPGMEJS_Error('NOT_YET_IMPLEMENTED')); } return this.GpgME.decrypt(message); // TODO: translate between: @@ -126,31 +128,74 @@ export class GpgME_openPGPCompatibility { // mime: A Boolean indicating whether the data is a MIME object. // info: An optional object with extra information. } + initKeyring(){ + return new GPGME_Keyring_openPGPCompatibility; + } } /** - * - * @param {Object | String} Key Either a (presumably openpgp Key) Object with a - * primaryKeyproperty and a method getFingerprint, or a string. - * @returns {String} Unchecked string value claiming to be a fingerprint - * TODO: gpgmejs checks again, so it's okay here. + * Translation layer offering basic Keyring API to be used in Mailvelope. + * It may still be changed/expanded/merged with GPGME_Keyring */ -function translateKeyInput(Key){ - if (!Key){ - return []; +class GPGME_Keyring_openPGPCompatibility { + constructor(){ + this._gpgme_keyring = new GPGME_Keyring; } - if (!Array.isArray(Key)){ - Key = [Key]; + + /** + * Returns a GPGME_Key Object for each Key in the gnupg Keyring. This + * includes keys openpgpjs considers 'private' (usable for signing), with + * the difference that Key.armored will NOT contain any secret information. + * Please also note that a GPGME_Key does not offer full openpgpjs- Key + * compatibility. + * @returns {Array} with the objects offering at least: + * @property {String} armored The armored key block (does not include secret blocks) + * @property {Boolean} hasSecret Indicator if a private/secret key exists + * @property {Boolean} isDefault Indicator if private key exists and is the default key in this keyring + * @property {String} fingerprint The fingerprint identifying this key + * //TODO: Check if IsDefault is also always hasSecret + */ + getPublicKeys(){ + return this._gpgme_keyring.getKeys(null, true); } - let resultslist = []; - for (let i=0; i < Key.length; i++){ - if (typeof(Key[i]) === 'string'){ - resultslist.push(Key); - } else if ( - Key[i].hasOwnProperty(primaryKey) && - Key[i].primaryKey.hasOwnProperty(getFingerprint)){ - resultslist.push(Key[i].primaryKey.getFingerprint()); + + /** + * Returns the Default Key used for crypto operation in gnupg. + * Please note that the armored property does not contained secret key blocks, + * despite secret blocks being part of the key itself. + * @returns {Promise } + */ + getDefaultKey(){ + this._gpgme_keyring.getSubset({defaultKey: true}).then(function(result){ + if (result.length === 1){ + return Promise.resolve(result[0]); + } + else { + // TODO: Can there be "no default key"? + // TODO: Can there be several default keys? + return new GPGMEJS_Error; //TODO + } + }); + } + + /** + * Deletes a Key + * @param {Object} Object identifying key + * @param {String} key.fingerprint - fingerprint of the to be deleted key + * @param {Boolean} key.secret - indicator if private key should be deleted as well + + * @returns {Promise., Error>} TBD: Not sure what is wanted + TODO @throws {Error} error.code = ‘KEY_NOT_EXIST’ - there is no key for the given fingerprint + TODO @throws {Error} error.code = ‘NO_SECRET_KEY’ - secret indicator set, but no secret key exists + */ + deleteKey(key){ + if (typeof(key) !== "object"){ + return Promise.reject(new GPGMEJS_Error('WRONGPARAM')); } + if ( !key.fingerprint || ! isFingerprint(key.fingerprint)){ + return Promise.reject(new GPGMEJS_Error('WRONGPARAM')); + } + let key_to_delete = new GPGME_Key(key.fingerprint); + return key_to_delete.deleteKey(key.secret); } - return resultslist; -} \ No newline at end of file +} From 727340b295f25e04cb595022ba143cda48364697 Mon Sep 17 00:00:00 2001 From: Maximilian Krambach Date: Mon, 23 Apr 2018 19:15:40 +0200 Subject: [PATCH 04/95] js: don't allow message operation changes -- Once an operation is changed, their set of allowed/required parameters will change. So we shouldn't set/change the operation later. --- lang/js/src/Keyring.js | 3 +-- lang/js/src/Message.js | 42 ++++++++++++++++++++---------------------- lang/js/src/gpgmejs.js | 9 +++------ 3 files changed, 24 insertions(+), 30 deletions(-) diff --git a/lang/js/src/Keyring.js b/lang/js/src/Keyring.js index 52fa7f71..d8cb84b2 100644 --- a/lang/js/src/Keyring.js +++ b/lang/js/src/Keyring.js @@ -49,8 +49,7 @@ export class GPGME_Keyring { * */ getKeys(pattern, include_secret){ - let msg = new GPGME_Message; - msg.operation = 'listkeys'; + let msg = new GPGME_Message('listkeys'); if (pattern && typeof(pattern) === 'string'){ msg.setParameter('pattern', pattern); } diff --git a/lang/js/src/Message.js b/lang/js/src/Message.js index 6a93b6f4..f5e21e00 100644 --- a/lang/js/src/Message.js +++ b/lang/js/src/Message.js @@ -23,28 +23,7 @@ export class GPGME_Message { //TODO getter constructor(operation){ - if (operation){ - this.operation(operation); - } - } - - /** - * Defines the operation this message will have - * @param {String} operation Must be defined in permittedOperations - * TODO: move to constructor? - */ - set operation (operation){ - if (!operation || typeof(operation) !== 'string'){ - return new GPGMEJS_Error('WRONGPARAM'); - } - if (operation in permittedOperations){ - if (!this._msg){ - this._msg = {}; - } - this._msg.op = operation; - } else { - return new GPGMEJS_Error('WRONG_OP'); - } + setOperation(this, operation); } get operation(){ @@ -110,4 +89,23 @@ export class GPGME_Message { } } +} + +/** + * 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 new GPGMEJS_Error('WRONGTYPE'); + } + if (permittedOperations.hasOwnProperty(operation)){ + if (!scope._msg){ + scope._msg = {}; + } + scope._msg.op = operation; + } else { + return new GPGMEJS_Error('WRONG_OP'); + } } \ No newline at end of file diff --git a/lang/js/src/gpgmejs.js b/lang/js/src/gpgmejs.js index c23a356b..b15477f0 100644 --- a/lang/js/src/gpgmejs.js +++ b/lang/js/src/gpgmejs.js @@ -76,8 +76,7 @@ export class GpgME { */ encrypt (data, publicKeys, wildcard=false){ - let msg = new GPGME_Message; - msg.operation = 'encrypt'; + let msg = new GPGME_Message('encrypt'); // TODO temporary msg.setParameter('armor', true); @@ -108,8 +107,7 @@ export class GpgME { if (data === undefined){ return Promise.reject(new GPGMEJS_Error ('EMPTY_MSG')); } - let msg = new GPGME_Message; - msg.operation = 'decrypt'; + let msg = new GPGME_Message('decrypt'); putData(msg, data); return this._connection.post(msg); @@ -117,8 +115,7 @@ export class GpgME { deleteKey(key, delete_secret = false, no_confirm = false){ return Promise.reject(new GPGMEJS_Error ('NOT_YET_IMPLEMENTED')); - let msg = new GPGME_Message; - msg.operation = 'deletekey'; + let msg = new GPGME_Message('deletekey'); let key_arr = toKeyIdArray(key); if (key_arr.length !== 1){ throw('TODO'); From 461dd0c8b41683a91073b362d100ee5217ec53f6 Mon Sep 17 00:00:00 2001 From: Maximilian Krambach Date: Tue, 24 Apr 2018 18:44:30 +0200 Subject: [PATCH 05/95] js: change in initialization ancd connection handling -- * The Connection will now be started before an object is created, to better account for failures. * index.js: now exposes an init(), which returns a Promise of configurable with an established connection. * TODO: There is currently no way to recover from a "connection lost" * Connection.js offers Connection.isConnected, which toggles on port closing. --- lang/js/src/Connection.js | 41 ++++++++++++-------- lang/js/src/Keyring.js | 30 +++++++-------- lang/js/src/gpgmejs.js | 65 ++++++++++++++----------------- lang/js/src/gpgmejs_openpgpjs.js | 33 +++++++++++----- lang/js/src/index.js | 40 +++++++++++++++++-- lang/js/testapplication.js | 66 ++++++++++++++++---------------- 6 files changed, 159 insertions(+), 116 deletions(-) diff --git a/lang/js/src/Connection.js b/lang/js/src/Connection.js index 87ec8cf7..e6ff67be 100644 --- a/lang/js/src/Connection.js +++ b/lang/js/src/Connection.js @@ -35,6 +35,18 @@ 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; } /** @@ -48,28 +60,20 @@ export class Connection{ /** * Opens a nativeMessaging port. - * returns nothing, but triggers errors if not successfull: - * NO_CONNECT: connection not successfull, chrome.runtime.lastError may be - * available - * ALREADY_CONNECTED: There is already a connection present. + * TODO: Error handling ALREADY_CONNECTED */ connect(){ - if (this._connection){ + if (this._isConnected === true){ return new GPGMEJS_Error('ALREADY_CONNECTED'); } + this._isConnected = true; this._connection = chrome.runtime.connectNative('gpgmejson'); - if (!this._connection){ - return new GPGMEJS_Error('NO_CONNECT'); - } - } - - /** - * checks if the connection is established - * TODO: some kind of ping to see if the other side responds as expected? - * @returns {Boolean} - */ - get connected(){ - return this._connection ? true: false; + let me = this; + this._connection.onDisconnect.addListener( + function(){ + me._isConnected = false; + } + ); } /** @@ -79,6 +83,9 @@ export class Connection{ * information. */ post(message){ + if (!this.isConnected){ + return Promise.reject(new GPGMEJS_Error('NO_CONNECT')); + } if (!message || !message instanceof GPGME_Message){ return Promise.reject(new GPGMEJS_Error('WRONGPARAM')); } diff --git a/lang/js/src/Keyring.js b/lang/js/src/Keyring.js index d8cb84b2..ef8028ff 100644 --- a/lang/js/src/Keyring.js +++ b/lang/js/src/Keyring.js @@ -19,28 +19,28 @@ */ import {GPGME_Message} from './Message' -import {Connection} from './Connection' import {GPGME_Key} from './Key' import { isFingerprint, isLongId } from './Helpers'; export class GPGME_Keyring { - constructor(){ - this.reconnect(); + constructor(connection){ + this.connection = connection; } - /** - * (Re)-establishes the connection - * TODO TEMP: should we better use the connection of our parent, - * which we do not control? - */ - reconnect(){ - if (!this._connection || ! this._connection instanceof Connection){ - this._connection = new Connection; - } else { - this._connection.disconnect(); - this._connection.connect(); + 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 undefined; //TODO: connection was lost! + } + return undefined; //TODO: no connection there + } /** * @param {String} (optional) pattern A pattern to search for, in userIds or KeyIds @@ -57,7 +57,7 @@ export class GPGME_Keyring { msg.setParameter('with-secret', true); } - this._connection.post(msg).then(function(result){ + this.connection.post(msg).then(function(result){ let fpr_list = []; let resultset = []; if (!Array.isArray(result.keys)){ diff --git a/lang/js/src/gpgmejs.js b/lang/js/src/gpgmejs.js index b15477f0..4b2a03a4 100644 --- a/lang/js/src/gpgmejs.js +++ b/lang/js/src/gpgmejs.js @@ -22,59 +22,51 @@ import {Connection} from "./Connection" import {GPGME_Message} from './Message' import {toKeyIdArray} from "./Helpers" import {GPGMEJS_Error as Error, GPGMEJS_Error} from "./Errors" +import { GPGME_Keyring } from "./Keyring"; export class GpgME { /** * initializes GpgME by opening a nativeMessaging port * TODO: add configuration */ - constructor(configuration = { - null_expire_is_never: false - }){ - this._connection = new Connection; + constructor(connection){ + this.connection = connection; } - /** - * refreshes the nativeApp connection - */ - reconnect(){ - if (!this._connection || ! this._connection instanceof Connection){ - this._connection = new Connection; - } else { - this._connection.disconnect(); - this._connection.connect(); + set connection(connection){ + if (this._connection instanceof Connection){ + //TODO Warning: Connection already established + } + if (connection instanceof Connection){ + this._connection = connection; } } - /** - * inmediately tries to destroy the nativeMessaging connection. - * TODO: may not be included in final API, as it is redundant. - * For now, it just serves paranoia - */ - disconnect(){ - if (this._connection){ - this._connection.disconnect(); - this._connection = null; + 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; } } - /** - * tests the nativeApp connection - */ - get connected(){ - if (!this._connection || ! this._connection instanceof Connection){ - return false; - } - return this._connection.connected; + get 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){ + encrypt(data, publicKeys, wildcard=false){ let msg = new GPGME_Message('encrypt'); @@ -89,7 +81,7 @@ export class GpgME { if (wildcard === true){msg.setParameter('throw-keyids', true); }; - return (this._connection.post(msg)); + return (this.connection.post(msg)); } /** @@ -109,7 +101,7 @@ export class GpgME { } let msg = new GPGME_Message('decrypt'); putData(msg, data); - return this._connection.post(msg); + return this.connection.post(msg); } @@ -128,7 +120,7 @@ export class GpgME { if (no_confirm === true){ //TODO: Do we want this hidden deep in the code? msg.setParameter('delete_force', true); //TBD } - this._connection.post(msg).then(function(success){ + this.connection.post(msg).then(function(success){ //TODO: it seems that there is always errors coming back: }, function(error){ switch (error.msg){ @@ -143,7 +135,6 @@ export class GpgME { } }); } - } /** @@ -171,4 +162,4 @@ function putData(message, data){ } else { return new GPGMEJS_Error('WRONGPARAM'); } -} \ No newline at end of file +} diff --git a/lang/js/src/gpgmejs_openpgpjs.js b/lang/js/src/gpgmejs_openpgpjs.js index 54b9dd45..f1ddb5d6 100644 --- a/lang/js/src/gpgmejs_openpgpjs.js +++ b/lang/js/src/gpgmejs_openpgpjs.js @@ -30,13 +30,29 @@ import { isFingerprint } from "./Helpers" import { GPGMEJS_Error } from './Errors' + export class GpgME_openPGPCompatibility { - constructor(){ - this._gpgme = new GpgME({ - null_expire_is_never: false - }); - this.Keyring = this.initKeyring(); + constructor(connection){ + this.initGpgME(connection); + } + + get Keyring(){ + if (this._keyring){ + return this._keyring; + } + return undefined; + } + + initGpgME(connection){ + this._GpgME = new GpgME(connection); + this._Keyring = new GPGME_Keyring_openPGPCompatibility(connection); + } + + get GpgME(){ + if (this._GpGME){ + return this._GpGME; + } } /** @@ -128,9 +144,6 @@ export class GpgME_openPGPCompatibility { // mime: A Boolean indicating whether the data is a MIME object. // info: An optional object with extra information. } - initKeyring(){ - return new GPGME_Keyring_openPGPCompatibility; - } } /** @@ -138,8 +151,8 @@ export class GpgME_openPGPCompatibility { * It may still be changed/expanded/merged with GPGME_Keyring */ class GPGME_Keyring_openPGPCompatibility { - constructor(){ - this._gpgme_keyring = new GPGME_Keyring; + constructor(connection){ + this._gpgme_keyring = new GPGME_Keyring(connection); } /** diff --git a/lang/js/src/index.js b/lang/js/src/index.js index f70bd2d8..0e2beda4 100644 --- a/lang/js/src/index.js +++ b/lang/js/src/index.js @@ -18,6 +18,40 @@ * SPDX-License-Identifier: LGPL-2.1+ */ -import { GpgME as gpgmejs } from "./gpgmejs"; -// import { GpgME_openPGPCompatibility as gpgmejs } from "./gpgmejs_openpgpjs"; -export default gpgmejs; +import { GpgME } from "./gpgmejs"; +import { GpgME_openPGPCompatibility } from "./gpgmejs_openpgpjs"; +import { Connection } from "./Connection"; + +/** + * Initializes a nativeMessaging Connection and returns a GPGMEjs object + * @param {*} conf Configuration. TBD + */ +function init( config = { + api_style: 'gpgme', // | gpgme_openpgpjs + null_expire_is_never: true // Boolean + }){ + return new Promise(function(resolve, reject){ + let connection = new Connection; + // TODO: Delayed reaction is ugly. We need to listen to the port's + // event listener in isConnected, but this takes some time (<5ms) to + // disconnect if there is no successfull connection. + let delayedreaction = function(){ + if (connection.isConnected === true){ + let gpgme = null; + if (config.api_style && config.api_style === 'gpgme_openpgpjs'){ + resolve( + new GpgME_openPGPCompatibility(connection)); + } else { + resolve(new GpgME(connection)); + } + } else { + reject('NO_CONNECT'); + } + }; + setTimeout(delayedreaction, 5); + }); +}; + +export default { + init: init +} \ No newline at end of file diff --git a/lang/js/testapplication.js b/lang/js/testapplication.js index 97b35527..f47299e8 100644 --- a/lang/js/testapplication.js +++ b/lang/js/testapplication.js @@ -19,39 +19,37 @@ * */ -function encryptbuttonclicked(event){ - let data = document.getElementById('cleartext').value; - let keyId = document.getElementById('pubkey').value; - let communication = new Gpgmejs; - let enc = communication.encrypt(data, keyId).then( - function(answer){ - console.log(answer); - if (answer.data){ - console.log(answer.data); - document.getElementById('answer').value = answer.data; - } - }, function(errormsg){ - alert('Error: '+ errormsg); - }); -}; - -function decryptbuttonclicked(event){ - let data = document.getElementById("ciphertext").value; - let communication = new Gpgmejs; - let enc = communication.decrypt(data).then( - function(answer){ - console.log(answer); - if (answer.data){ - document.getElementById('answer').value = answer.data; - } - }, function(errormsg){ - alert('Error: '+ errormsg); - }); -}; - document.addEventListener('DOMContentLoaded', function() { - document.getElementById("buttonencrypt").addEventListener("click", - encryptbuttonclicked); - document.getElementById("buttondecrypt").addEventListener("click", - decryptbuttonclicked); + Gpgmejs.init().then(function(gpgmejs){ + document.getElementById("buttonencrypt").addEventListener("click", + function(){ + let data = document.getElementById('cleartext').value; + let keyId = document.getElementById('pubkey').value; + gpgmejs.encrypt(data, keyId).then( + function(answer){ + console.log(answer); + if (answer.data){ + console.log(answer.data); + document.getElementById('answer').value = answer.data; + } + }, function(errormsg){ + alert('Error: '+ errormsg); + }); + }); + + document.getElementById("buttondecrypt").addEventListener("click", + function(){ + let data = document.getElementById("ciphertext").value; + gpgmejs.decrypt(data).then( + function(answer){ + console.log(answer); + if (answer.data){ + document.getElementById('answer').value = answer.data; + } + }, function(errormsg){ + alert('Error: '+ errormsg); + }); + }); + }, + function(error){console.log(error)}); }); From e2aa8066a9b3ce694169ad9fcc26cae486a804af Mon Sep 17 00:00:00 2001 From: Maximilian Krambach Date: Tue, 24 Apr 2018 19:29:32 +0200 Subject: [PATCH 06/95] js: Key object adjustments after discussion -- * src/aKey.js changed fingerprint to setter (to avoid overwrites) * src/gpgmejs_openpgpjs.js - Added a class GPGME_Key_openpgpmode, which allows for renaming and deviation from GPGME. - renamed classes *_openPGPCompatibility to *_openpgpmode. They are not fully compatible, but only offer a subset of properties. Also, the name seems less clunky --- lang/js/src/Key.js | 8 ++-- lang/js/src/gpgmejs_openpgpjs.js | 67 +++++++++++++++++++++++++++----- lang/js/src/index.js | 4 +- 3 files changed, 64 insertions(+), 15 deletions(-) diff --git a/lang/js/src/Key.js b/lang/js/src/Key.js index d8f16c55..f59b9901 100644 --- a/lang/js/src/Key.js +++ b/lang/js/src/Key.js @@ -32,10 +32,12 @@ import {GPGMEJS_Error} from './Errors' export class GPGME_Key { constructor(fingerprint){ - if (isFingerprint(fingerprint) === true){ + this.fingerprint = fingerprint; + } + + set fingerprint(fpr){ + if (isFingerprint(fpr) === true && !this._fingerprint){ this._fingerprint = fingerprint; - } else { - return new GPGMEJS_Error('WRONGPARAM', 'Key.js: invalid fingerprint'); } } diff --git a/lang/js/src/gpgmejs_openpgpjs.js b/lang/js/src/gpgmejs_openpgpjs.js index f1ddb5d6..23076569 100644 --- a/lang/js/src/gpgmejs_openpgpjs.js +++ b/lang/js/src/gpgmejs_openpgpjs.js @@ -31,7 +31,7 @@ import { GPGMEJS_Error } from './Errors' -export class GpgME_openPGPCompatibility { + export class GpgME_openpgpmode { constructor(connection){ this.initGpgME(connection); @@ -46,7 +46,7 @@ export class GpgME_openPGPCompatibility { initGpgME(connection){ this._GpgME = new GpgME(connection); - this._Keyring = new GPGME_Keyring_openPGPCompatibility(connection); + this._Keyring = new GPGME_Keyring_openpgpmode(connection); } get GpgME(){ @@ -150,7 +150,7 @@ export class GpgME_openPGPCompatibility { * Translation layer offering basic Keyring API to be used in Mailvelope. * It may still be changed/expanded/merged with GPGME_Keyring */ -class GPGME_Keyring_openPGPCompatibility { +class GPGME_Keyring_openpgpmode { constructor(connection){ this._gpgme_keyring = new GPGME_Keyring(connection); } @@ -161,15 +161,13 @@ class GPGME_Keyring_openPGPCompatibility { * the difference that Key.armored will NOT contain any secret information. * Please also note that a GPGME_Key does not offer full openpgpjs- Key * compatibility. - * @returns {Array} with the objects offering at least: - * @property {String} armored The armored key block (does not include secret blocks) - * @property {Boolean} hasSecret Indicator if a private/secret key exists - * @property {Boolean} isDefault Indicator if private key exists and is the default key in this keyring - * @property {String} fingerprint The fingerprint identifying this key + * @returns {Array} * //TODO: Check if IsDefault is also always hasSecret + * TODO Check if async is required */ getPublicKeys(){ - return this._gpgme_keyring.getKeys(null, true); + return translateKeys( + this._gpgme_keyring.getKeys(null, true)); } /** @@ -181,7 +179,8 @@ class GPGME_Keyring_openPGPCompatibility { getDefaultKey(){ this._gpgme_keyring.getSubset({defaultKey: true}).then(function(result){ if (result.length === 1){ - return Promise.resolve(result[0]); + return Promise.resolve( + translateKeys(result)[0]); } else { // TODO: Can there be "no default key"? @@ -212,3 +211,51 @@ class GPGME_Keyring_openPGPCompatibility { return key_to_delete.deleteKey(key.secret); } } + +/** + * TODO error handling. + * Offers the Key information as the openpgpmode wants + */ +class GPGME_Key_openpgpmode { + constructor(value){ + this.init = value; + } + + set init (value){ + if (!this._GPGME_Key && value instanceof GPGME_Key){ + this._GPGME_Key = value; + } else if (!this._GPGME_Key && isFingerprint(fpr)){ + this._GPGME_Key = new GPGME_Key; + } + } + + get fingerprint(){ + return this._GPGME_Key.fingerprint; + } + + get armor(){ + return this._GPGME_Key.armored; + } + + get secret(){ + return this._GPGME_Key.hasSecret; + } + + get default(){ + return this._GPGME_Key.isDefault; + } +} + +/** + * creates GPGME_Key_openpgpmode from GPGME_Keys + */ +function translateKeys(input){ + if (!Array.isArray(input)){ + input = [input]; + } + let resultset; + for (let i=0; i< input.length; i++){ + resultset.push(new GPGME_Key_openpgpmode(input[i])); + } + return resultset; +} \ No newline at end of file diff --git a/lang/js/src/index.js b/lang/js/src/index.js index 0e2beda4..0cb2301c 100644 --- a/lang/js/src/index.js +++ b/lang/js/src/index.js @@ -19,7 +19,7 @@ */ import { GpgME } from "./gpgmejs"; -import { GpgME_openPGPCompatibility } from "./gpgmejs_openpgpjs"; +import { GpgME_openpgpmode } from "./gpgmejs_openpgpjs"; import { Connection } from "./Connection"; /** @@ -40,7 +40,7 @@ function init( config = { let gpgme = null; if (config.api_style && config.api_style === 'gpgme_openpgpjs'){ resolve( - new GpgME_openPGPCompatibility(connection)); + new GpgME_openpgpmode(connection)); } else { resolve(new GpgME(connection)); } From 30c47d80a27054aa340cbd6dc39d1b8a5dc5cf22 Mon Sep 17 00:00:00 2001 From: Maximilian Krambach Date: Tue, 24 Apr 2018 19:47:48 +0200 Subject: [PATCH 07/95] js: allow openpgp-like Message objects as Data -- * src/gpgmejs.js: If a message offers a getText, consider it as the message's content --- lang/js/src/gpgmejs.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lang/js/src/gpgmejs.js b/lang/js/src/gpgmejs.js index 4b2a03a4..03ed5cb6 100644 --- a/lang/js/src/gpgmejs.js +++ b/lang/js/src/gpgmejs.js @@ -159,6 +159,13 @@ function putData(message, 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 new GPGMEJS_Error('WRONGPARAM'); } From c72adc00965fe4fcedd9d18609211021a091b28b Mon Sep 17 00:00:00 2001 From: Maximilian Krambach Date: Wed, 25 Apr 2018 10:54:24 +0200 Subject: [PATCH 08/95] js: change in Error behaviour -- * Error objects will now return the error code if defined as error type in src/Errors.js, or do a console.log if it is a warning. Errors from the native gpgme-json will be marked as GNUPG_ERROR. --- lang/js/src/Connection.js | 58 ++++++----- lang/js/src/Errors.js | 170 +++++++++++++------------------ lang/js/src/Helpers.js | 16 +-- lang/js/src/Key.js | 2 +- lang/js/src/Message.js | 12 +-- lang/js/src/gpgmejs.js | 13 +-- lang/js/src/gpgmejs_openpgpjs.js | 14 +-- lang/js/src/index.js | 3 +- 8 files changed, 132 insertions(+), 156 deletions(-) diff --git a/lang/js/src/Connection.js b/lang/js/src/Connection.js index e6ff67be..8bc3d42a 100644 --- a/lang/js/src/Connection.js +++ b/lang/js/src/Connection.js @@ -1,5 +1,3 @@ -import { GPGME_Message } from "./Message"; - /* gpgme.js - Javascript integration for gpgme * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik * @@ -26,7 +24,8 @@ import { GPGME_Message } from "./Message"; * expected. */ import { permittedOperations } from './permittedOperations' -import { GPGMEJS_Error} from "./Errors" +import { GPGMEJS_Error } from "./Errors" +import { GPGME_Message } from "./Message"; /** * A Connection handles the nativeMessaging interaction. @@ -60,20 +59,20 @@ export class Connection{ /** * Opens a nativeMessaging port. - * TODO: Error handling ALREADY_CONNECTED */ connect(){ if (this._isConnected === true){ - return new GPGMEJS_Error('ALREADY_CONNECTED'); + GPGMEJS_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; + } + ); } - this._isConnected = true; - this._connection = chrome.runtime.connectNative('gpgmejson'); - let me = this; - this._connection.onDisconnect.addListener( - function(){ - me._isConnected = false; - } - ); } /** @@ -84,28 +83,31 @@ export class Connection{ */ post(message){ if (!this.isConnected){ - return Promise.reject(new GPGMEJS_Error('NO_CONNECT')); + return Promise.reject(GPGMEJS_Error('CONN_NO_CONNECT')); } if (!message || !message instanceof GPGME_Message){ - return Promise.reject(new GPGMEJS_Error('WRONGPARAM')); + return Promise.reject(GPGMEJS_Error('PARAM_WRONG'), message); } if (message.isComplete !== true){ - return Promise.reject(new GPGMEJS_Error('MSG_INCOMPLETE')); + return Promise.reject(GPGMEJS_Error('MSG_INCOMPLETE')); } - // let timeout = 5000; //TODO config 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(new GPGMEJS_Error('EMPTY_GPG_ANSWER')); + reject(GPGMEJS_Error('CONN_EMPTY_GPG_ANSWER')); } else if (msg.type === "error"){ me._connection.onMessage.removeListener(listener) - //TODO: GPGMEJS_Error? - reject(msg.msg); + reject( + {code: 'GNUPG_ERROR', + msg: msg.msg} ); } else { - answer.add(msg); + let answer_result = answer.add(msg); + if (answer_result !== true){ + reject(answer_result); + } if (msg.more === true){ me._connection.postMessage({'op': 'getmore'}); } else { @@ -117,11 +119,12 @@ export class Connection{ me._connection.onMessage.addListener(listener); me._connection.postMessage(message.message); + //TBD: needs to be aware if there is a pinentry pending // setTimeout( // function(){ // me.disconnect(); - // reject(new GPGMEJS_Error('TIMEOUT', 5000)); + // reject(GPGMEJS_Error('CONN_TIMEOUT')); // }, timeout); }); } @@ -141,6 +144,7 @@ class Answer{ /** * Add the information to the answer * @param {Object} msg The message as received with nativeMessaging + * returns true if successfull, GPGMEJS_Error otherwise */ add(msg){ if (this._response === undefined){ @@ -148,12 +152,15 @@ class Answer{ } let messageKeys = Object.keys(msg); let poa = permittedOperations[this.operation].answer; + if (messageKeys.length === 0){ + return GPGMEJS_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 new GPGMEJS_Error('UNEXPECTED_ANSWER'); + return GPGMEJS_Error('CONN_UNEXPECTED_ANSWER'); } break; case 'more': @@ -172,7 +179,7 @@ class Answer{ this._response[key] = msg[key]; } else if (this._response[key] !== msg[key]){ - return new GPGMEJS_Error('UNEXPECTED_ANSWER',msg[key]); + return GPGMEJS_Error('CONN_UNEXPECTED_ANSWER',msg[key]); } } //infos may be json objects etc. Not yet defined. @@ -184,11 +191,12 @@ class Answer{ this._response.push(msg[key]); } else { - return new GPGMEJS_Error('UNEXPECTED_ANSWER', key); + return GPGMEJS_Error('CONN_UNEXPECTED_ANSWER', key); } break; } } + return true; } /** diff --git a/lang/js/src/Errors.js b/lang/js/src/Errors.js index c2356f7c..c49bfe21 100644 --- a/lang/js/src/Errors.js +++ b/lang/js/src/Errors.js @@ -18,131 +18,99 @@ * SPDX-License-Identifier: LGPL-2.1+ */ -// This is a preliminary collection of erors and warnings to be thrown and implemented. - -// general idea: if throw , throw the NAME -// return false || 'return' property - -//TODO: Connection.NOCONNECT promise -//connection.timeout: Be aware of pinentry - -export class GPGMEJS_Error { - - constructor(code = 'GENERIC_ERROR', details){ - let config = { //TODO TEMP - debug: 'console', // |'alert' - throw: 'default' // | 'always' | 'never' - }; +/** + * Checks the given error code and returns some information about it's meaning + * @param {String} code The error code + * @returns {Object} An object containing string properties code and msg + * TODO: error-like objects with the code 'GNUPG_ERROR' are errors sent + * directly by gnupg as answer in Connection.post() + */ +export function GPGMEJS_Error(code = 'GENERIC_ERROR'){ + if (!typeof(code) === 'string'){ + code = 'GENERIC_ERROR'; + } let errors = { //TODO: someplace else - //Connection errors - 'ALREADY_CONNECTED':{ - msg: 'The connection was already established. The action would overwrite the context', - throw: true + // Connection + 'CONN_NO_CONNECT': { + msg:'Connection with the nativeMessaging host could not be' + + ' established.', + type: 'error' }, - 'NO_CONNECT': { - msg:'Connection with the nativeMessaging host could not be established.', - throw: true + 'CONN_EMPTY_GPG_ANSWER':{ + msg: 'The nativeMessaging answer was empty.', + type: 'error' }, - 'EMPTY_GPG_ANSWER':{ - msg: 'The nativeMesaging answer was empty', - throw: true + 'CONN_TIMEOUT': { + msg: 'A connection timeout was exceeded.', + type: 'error' }, - 'TIMEOUT': { - msg: 'A timeout was exceeded.', - throw: false + 'CONN_UNEXPECTED_ANSWER': { + msg: 'The answer from gnupg was not as expected.', + type: 'error' }, - - 'UNEXPECTED_ANSWER': { - msg: 'The answer from gnupg was not as expected', - throw: true - }, - - // Message/Data Errors - - 'NO_KEYS' : { - msg: 'There were no valid keys provided.', - throw: true - }, - 'NOT_A_FPR': { - msg: 'The String is not an accepted fingerprint', - throw: false + 'CONN_ALREADY_CONNECTED':{ + msg: 'A connection was already established.', + type: 'warn' }, + // Message/Data 'MSG_INCOMPLETE': { - msg: 'The Message did not match the minimum requirements for the interaction', - throw: true + msg: 'The Message did not match the minimum requirements for' + + ' the interaction.', + type: 'error' }, - 'EMPTY_MSG' : { - msg: 'The Message has no data.', - throw: true - }, - 'MSG_NODATA':{ - msg: 'The data sent is empty. This may be unintentional.', - throw: false + 'MSG_EMPTY' : { + msg: 'The Message is empty.', + type: 'error' }, 'MSG_OP_PENDING': { - msg: 'There is no operation specified yet. The parameter cannot be set', - throw: false + msg: 'There is no operation specified yet. The parameter cannot' + + ' be set', + type: 'warning' }, - 'WRONG_OP': { - msg: "The operation requested could not be found", - throw: true + 'MSG_WRONG_OP': { + msg: 'The operation requested could not be found', + type: 'warning' + }, + 'MSG_NO_KEYS' : { + msg: 'There were no valid keys provided.', + type: 'warn' + }, + 'MSG_NOT_A_FPR': { + msg: 'The String is not an accepted fingerprint', + type: 'warn' }, - //generic errors - - 'WRONGPARAM':{ + // generic + 'PARAM_WRONG':{ msg: 'invalid parameter was found', - throw: true - }, - 'WRONGTYPE':{ - msg: 'invalid parameter type was found', - throw: true + type: 'error' }, 'NOT_IMPLEMENTED': { msg: 'A openpgpjs parameter was submitted that is not implemented', - throw: true + type: 'error' + }, + 'NOT_YET_IMPLEMENTED': { + msg: 'Support of this is probable, but it is not implemented yet', + type: 'error' }, 'GENERIC_ERROR': { msg: 'Unspecified error', - throw: true + type: 'error' }, - - // hopefully temporary errors - - 'NOT_YET_IMPLEMENTED': { - msg: 'Support of this is probable, but it is not implemented yet', - throw: false - } } - if (!errors.hasOwnProperty(code)){ - throw('GENERIC_ERROR'); + if (code === 'TODO'){ + alert('TODO_Error!'); } - let msg = code; - if (errors[code].msg !== undefined){ - msg = msg + ': ' + errors[code].msg; + if (errors.hasOwnProperty(code)){ + code = 'GENERIC_ERROR'; } - if (details){ - msg = msg + ' ' + details; + if (error.type === 'error'){ + return {code: 'code', + msg: errors[code].msg + }; } - if (config.debug === 'console'){ - console.log(msg); - } else if (config.debug === 'alert'){ - alert(msg); + if (error.type === 'warning'){ + console.log(code + ': ' + error[code].msg); } - switch (config.throw) { - case 'default': - if (errors[code].throw === true){ - throw(code); - } - break; - case 'always': - throw(code); - break; - - case 'never': - break; - default: - throw('GENERIC_ERROR'); - } - } + return undefined; } diff --git a/lang/js/src/Helpers.js b/lang/js/src/Helpers.js index 922ca06c..d9750ba7 100644 --- a/lang/js/src/Helpers.js +++ b/lang/js/src/Helpers.js @@ -1,5 +1,3 @@ -import { GPGMEJS_Error } from "./Errors"; - /* gpgme.js - Javascript integration for gpgme * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik * @@ -19,18 +17,19 @@ import { GPGMEJS_Error } from "./Errors"; * License along with this program; if not, see . * SPDX-License-Identifier: LGPL-2.1+ */ +import { GPGMEJS_Error } from "./Errors"; /** * Tries to return an array of fingerprints, either from input fingerprints or * from Key objects * @param {Key |Array| GPGME_Key | Array|String|Array} input - * @param {Boolean} nocheck if set, an empty result is acceptable * @returns {Array} Array of fingerprints. */ export function toKeyIdArray(input, nocheck){ if (!input){ - return (nocheck ===true)? [] : new GPGMEJS_Error('NO_KEYS'); + GPGMEJS_Error('MSG_NO_KEYS'); + return []; } if (!Array.isArray(input)){ input = [input]; @@ -41,7 +40,7 @@ export function toKeyIdArray(input, nocheck){ if (isFingerprint(input[i]) === true){ result.push(input[i]); } else { - GPGMEJS_Error + GPGMEJS_Error('MSG_NOT_A_FPR'); } } else if (typeof(input[i]) === 'object'){ let fpr = ''; @@ -53,13 +52,16 @@ export function toKeyIdArray(input, nocheck){ } if (isFingerprint(fpr) === true){ result.push(fpr); + } else { + GPGMEJS_Error('MSG_NOT_A_FPR'); } } else { - return new GPGMEJS_Error('WRONGTYPE'); + return GPGMEJS_Error('PARAM_WRONG'); } } if (result.length === 0){ - return (nocheck===true)? [] : new GPGMEJS_Error('NO_KEYS'); + GPGMEJS_Error('MSG_NO_KEYS'); + return []; } else { return result; } diff --git a/lang/js/src/Key.js b/lang/js/src/Key.js index f59b9901..5ae80438 100644 --- a/lang/js/src/Key.js +++ b/lang/js/src/Key.js @@ -172,7 +172,7 @@ export class GPGME_Key { * */ function checkKey(fingerprint, property){ - return Promise.reject(new GPGMEJS_Error('NOT_YET_IMPLEMENTED')); + return Promise.reject(GPGMEJS_Error('NOT_YET_IMPLEMENTED')); return new Promise(function(resolve, reject){ if (!isFingerprint(fingerprint)){ diff --git a/lang/js/src/Message.js b/lang/js/src/Message.js index f5e21e00..1b36f11d 100644 --- a/lang/js/src/Message.js +++ b/lang/js/src/Message.js @@ -39,20 +39,20 @@ export class GPGME_Message { */ setParameter(param,value){ if (!param || typeof(param) !== 'string'){ - return new GPGMEJS_Error('WRONGPARAM', 'type check failed'); + return GPGMEJS_Error('PARAM_WRONG'); } if (!this._msg || !this._msg.op){ - return new GPGMEJS_Error('MSG_OP_PENDING'); + return GPGMEJS_Error('MSG_OP_PENDING'); } let po = permittedOperations[this._msg.op]; if (!po){ - return new GPGMEJS_Error('WRONG_OP', param); + return GPGMEJS_Error('MSG_WRONG_OP'); } if (po.required.indexOf(param) >= 0 || po.optional.indexOf(param) >= 0){ this._msg[param] = value; return true; } - return new GPGMEJS_Error('WRONGPARAM', param); + return GPGMEJS_Error('PARAM_WRONG'); } /** @@ -98,7 +98,7 @@ export class GPGME_Message { */ function setOperation (scope, operation){ if (!operation || typeof(operation) !== 'string'){ - return new GPGMEJS_Error('WRONGTYPE'); + return GPGMEJS_Error('PARAM_WRONG'); } if (permittedOperations.hasOwnProperty(operation)){ if (!scope._msg){ @@ -106,6 +106,6 @@ function setOperation (scope, operation){ } scope._msg.op = operation; } else { - return new GPGMEJS_Error('WRONG_OP'); + return GPGMEJS_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 03ed5cb6..b20ff0f2 100644 --- a/lang/js/src/gpgmejs.js +++ b/lang/js/src/gpgmejs.js @@ -95,9 +95,8 @@ export class GpgME { */ decrypt(data){ - if (data === undefined){ - return Promise.reject(new GPGMEJS_Error ('EMPTY_MSG')); + return Promise.reject(GPGMEJS_Error('MSG_EMPTY')); } let msg = new GPGME_Message('decrypt'); putData(msg, data); @@ -106,7 +105,7 @@ export class GpgME { } deleteKey(key, delete_secret = false, no_confirm = false){ - return Promise.reject(new GPGMEJS_Error ('NOT_YET_IMPLEMENTED')); + return Promise.reject(GPGMEJS_Error('NOT_YET_IMPLEMENTED')); let msg = new GPGME_Message('deletekey'); let key_arr = toKeyIdArray(key); if (key_arr.length !== 1){ @@ -127,7 +126,7 @@ export class GpgME { case 'ERR_NO_ERROR': return Promise.resolve('okay'); //TBD default: - return Promise.reject(new GPGMEJS_Error); + return Promise.reject(GPGMEJS_Error('TODO') ); // // INV_VALUE, // GPG_ERR_NO_PUBKEY, // GPG_ERR_AMBIGUOUS_NAME, @@ -146,11 +145,9 @@ export class GpgME { */ function putData(message, data){ if (!message || !message instanceof GPGME_Message ) { - return new GPGMEJS_Error('WRONGPARAM'); + return GPGMEJS_Error('PARAM_WRONG'); } if (!data){ - //TODO Debug only! No data is legitimate - console.log('Warning. no data in message'); message.setParameter('data', ''); } else if (data instanceof Uint8Array){ let decoder = new TextDecoder('utf8'); @@ -167,6 +164,6 @@ function putData(message, data){ message.setParameter ('data', decoder.decode(txt)); } } else { - return new GPGMEJS_Error('WRONGPARAM'); + return GPGMEJS_Error('PARAM_WRONG'); } } diff --git a/lang/js/src/gpgmejs_openpgpjs.js b/lang/js/src/gpgmejs_openpgpjs.js index 23076569..e32f43a3 100644 --- a/lang/js/src/gpgmejs_openpgpjs.js +++ b/lang/js/src/gpgmejs_openpgpjs.js @@ -88,14 +88,14 @@ || signature !== null || returnSessionKey !== null || date !== null){ - return Promise.reject(new GPMGEJS_Error('NOT_IMPLEMENTED')); + return Promise.reject(GPMGEJS_Error('NOT_IMPLEMENTED')); } if ( privateKeys || filename || compression || armor === false || detached == true){ - return Promise.reject(new GPGMEJS_Error('NOT_YET_IMPLEMENTED')); + return Promise.reject(GPGMEJS_Error('NOT_YET_IMPLEMENTED')); } return this.GpgME.encrypt(data, translateKeyInput(publicKeys), wildcard); } @@ -123,14 +123,14 @@ if (passwords !== undefined || sessionKeys || date){ - return Promise.reject(new GPGMEJS_Error('NOT_IMPLEMENTED')); + return Promise.reject(GPGMEJS_Error('NOT_IMPLEMENTED')); } if ( privateKeys || publicKeys || format !== 'utf8' || signature ){ - return Promise.reject(new GPGMEJS_Error('NOT_YET_IMPLEMENTED')); + return Promise.reject(GPGMEJS_Error('NOT_YET_IMPLEMENTED')); } return this.GpgME.decrypt(message); // TODO: translate between: @@ -185,7 +185,7 @@ class GPGME_Keyring_openpgpmode { else { // TODO: Can there be "no default key"? // TODO: Can there be several default keys? - return new GPGMEJS_Error; //TODO + return GPGMEJS_Error('TODO'); } }); } @@ -202,10 +202,10 @@ class GPGME_Keyring_openpgpmode { */ deleteKey(key){ if (typeof(key) !== "object"){ - return Promise.reject(new GPGMEJS_Error('WRONGPARAM')); + return Promise.reject(GPGMEJS_Error('PARAM_WRONG')); } if ( !key.fingerprint || ! isFingerprint(key.fingerprint)){ - return Promise.reject(new GPGMEJS_Error('WRONGPARAM')); + return Promise.reject(GPGMEJS_Error('PARAM_WRONG')); } let key_to_delete = new GPGME_Key(key.fingerprint); return key_to_delete.deleteKey(key.secret); diff --git a/lang/js/src/index.js b/lang/js/src/index.js index 0cb2301c..a54277c2 100644 --- a/lang/js/src/index.js +++ b/lang/js/src/index.js @@ -19,6 +19,7 @@ */ import { GpgME } from "./gpgmejs"; +import { GPGMEJS_Error } from "./Errors"; import { GpgME_openpgpmode } from "./gpgmejs_openpgpjs"; import { Connection } from "./Connection"; @@ -45,7 +46,7 @@ function init( config = { resolve(new GpgME(connection)); } } else { - reject('NO_CONNECT'); + reject(GPGMEJS_Error('CONN_NO_CONNECT')); } }; setTimeout(delayedreaction, 5); From 5befa1c9751fe54b5ae87906d7f09772ce9de6ea Mon Sep 17 00:00:00 2001 From: Maximilian Krambach Date: Wed, 25 Apr 2018 11:32:21 +0200 Subject: [PATCH 09/95] js: reactivate timeout on connection -- * A timeout of 5 seconds is activated for functions that do not require a pinentry. This definition is written to src/permittedOperations.js * testapplication.js now alerts the proper error codes and messages. * src/Errors.js fixed two typos in error handling --- lang/js/src/Connection.js | 20 ++++++++++++-------- lang/js/src/Errors.js | 4 ++-- lang/js/src/permittedOperations.js | 3 +++ lang/js/testapplication.js | 4 ++-- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/lang/js/src/Connection.js b/lang/js/src/Connection.js index 8bc3d42a..4270be58 100644 --- a/lang/js/src/Connection.js +++ b/lang/js/src/Connection.js @@ -118,14 +118,18 @@ export class Connection{ }; me._connection.onMessage.addListener(listener); - me._connection.postMessage(message.message); - - //TBD: needs to be aware if there is a pinentry pending - // setTimeout( - // function(){ - // me.disconnect(); - // reject(GPGMEJS_Error('CONN_TIMEOUT')); - // }, timeout); + let timeout = new Promise(function(resolve, reject){ + setTimeout(function(){ + reject(GPGMEJS_Error('CONN_TIMEOUT')); + }, 5000); + }); + if (permittedOperations[message.operation].pinentry){ + return me._connection.postMessage(message.message); + } else { + return Promise.race([timeout, + me._connection.postMessage(message.message) + ]); + } }); } }; diff --git a/lang/js/src/Errors.js b/lang/js/src/Errors.js index c49bfe21..04b13e10 100644 --- a/lang/js/src/Errors.js +++ b/lang/js/src/Errors.js @@ -104,12 +104,12 @@ export function GPGMEJS_Error(code = 'GENERIC_ERROR'){ if (errors.hasOwnProperty(code)){ code = 'GENERIC_ERROR'; } - if (error.type === 'error'){ + if (errors.type === 'error'){ return {code: 'code', msg: errors[code].msg }; } - if (error.type === 'warning'){ + if (errors.type === 'warning'){ console.log(code + ': ' + error[code].msg); } return undefined; diff --git a/lang/js/src/permittedOperations.js b/lang/js/src/permittedOperations.js index 3c11b8e0..892f4f2e 100644 --- a/lang/js/src/permittedOperations.js +++ b/lang/js/src/permittedOperations.js @@ -23,6 +23,8 @@ * operation: required: Array optional: Array + pinentry: Boolean If a pinentry dialog is expected, and a timeout of + 5000 ms would be too short answer: type: The payload property of the answer. May be @@ -59,6 +61,7 @@ export const permittedOperations = { }, decrypt: { + pinentry: true, required: ['data'], optional: [ 'protocol', diff --git a/lang/js/testapplication.js b/lang/js/testapplication.js index f47299e8..b2cb4c23 100644 --- a/lang/js/testapplication.js +++ b/lang/js/testapplication.js @@ -33,7 +33,7 @@ document.addEventListener('DOMContentLoaded', function() { document.getElementById('answer').value = answer.data; } }, function(errormsg){ - alert('Error: '+ errormsg); + alert( errormsg.code + ' ' + errormsg.msg); }); }); @@ -47,7 +47,7 @@ document.addEventListener('DOMContentLoaded', function() { document.getElementById('answer').value = answer.data; } }, function(errormsg){ - alert('Error: '+ errormsg); + alert( errormsg.code + ' ' + errormsg.msg); }); }); }, From 1fb310cabe578625f96fce5d84ff6f0092c08d24 Mon Sep 17 00:00:00 2001 From: Maximilian Krambach Date: Wed, 25 Apr 2018 15:59:36 +0200 Subject: [PATCH 10/95] js: Configuration and Error handling -- * gpgmejs_openpgpjs - unsuported values with no negative consequences can now reject, warn or be ignored, according to config.unconsidered_params - cleanup of unsupported/supported parameters and TODOS * A src/index.js init() now accepts a configuration object * Errors will now be derived from Error, offering more info and a stacktrace. * Fixed Connection.post() timeout triggering on wrong cases * Added comments in permittedOperations.js, which gpgme interactions are still unimplemented and should be added next --- lang/js/src/Connection.js | 39 +++--- lang/js/src/Errors.js | 210 ++++++++++++++++------------- lang/js/src/Helpers.js | 12 +- lang/js/src/Key.js | 44 +++--- lang/js/src/Keyring.js | 13 +- lang/js/src/Message.js | 14 +- lang/js/src/gpgmejs.js | 19 +-- lang/js/src/gpgmejs_openpgpjs.js | 106 +++++++++------ lang/js/src/index.js | 9 +- lang/js/src/permittedOperations.js | 52 ++++++- 10 files changed, 310 insertions(+), 208 deletions(-) diff --git a/lang/js/src/Connection.js b/lang/js/src/Connection.js index 4270be58..5b092ab0 100644 --- a/lang/js/src/Connection.js +++ b/lang/js/src/Connection.js @@ -24,7 +24,7 @@ * expected. */ import { permittedOperations } from './permittedOperations' -import { GPGMEJS_Error } from "./Errors" +import { gpgme_error } from "./Errors" import { GPGME_Message } from "./Message"; /** @@ -62,7 +62,7 @@ export class Connection{ */ connect(){ if (this._isConnected === true){ - GPGMEJS_Error('CONN_ALREADY_CONNECTED'); + gpgme_error('CONN_ALREADY_CONNECTED'); } else { this._isConnected = true; this._connection = chrome.runtime.connectNative('gpgmejson'); @@ -83,13 +83,13 @@ export class Connection{ */ post(message){ if (!this.isConnected){ - return Promise.reject(GPGMEJS_Error('CONN_NO_CONNECT')); + return Promise.reject(gpgme_error('CONN_NO_CONNECT')); } if (!message || !message instanceof GPGME_Message){ - return Promise.reject(GPGMEJS_Error('PARAM_WRONG'), message); + return Promise.reject(gpgme_error('PARAM_WRONG'), message); } if (message.isComplete !== true){ - return Promise.reject(GPGMEJS_Error('MSG_INCOMPLETE')); + return Promise.reject(gpgme_error('MSG_INCOMPLETE')); } let me = this; return new Promise(function(resolve, reject){ @@ -97,7 +97,7 @@ export class Connection{ let listener = function(msg) { if (!msg){ me._connection.onMessage.removeListener(listener) - reject(GPGMEJS_Error('CONN_EMPTY_GPG_ANSWER')); + reject(gpgme_error('CONN_EMPTY_GPG_ANSWER')); } else if (msg.type === "error"){ me._connection.onMessage.removeListener(listener) reject( @@ -118,17 +118,18 @@ export class Connection{ }; me._connection.onMessage.addListener(listener); - let timeout = new Promise(function(resolve, reject){ - setTimeout(function(){ - reject(GPGMEJS_Error('CONN_TIMEOUT')); - }, 5000); - }); if (permittedOperations[message.operation].pinentry){ return me._connection.postMessage(message.message); } else { - return Promise.race([timeout, - me._connection.postMessage(message.message) - ]); + return Promise.race([ + me._connection.postMessage(message.message), + function(resolve, reject){ + setTimeout(function(){ + reject(gpgme_error('CONN_TIMEOUT')); + }, 5000); + }]).then(function(result){ + return result; + }); } }); } @@ -148,7 +149,7 @@ class Answer{ /** * Add the information to the answer * @param {Object} msg The message as received with nativeMessaging - * returns true if successfull, GPGMEJS_Error otherwise + * returns true if successfull, gpgme_error otherwise */ add(msg){ if (this._response === undefined){ @@ -157,14 +158,14 @@ class Answer{ let messageKeys = Object.keys(msg); let poa = permittedOperations[this.operation].answer; if (messageKeys.length === 0){ - return GPGMEJS_Error('CONN_UNEXPECTED_ANSWER'); + 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 GPGMEJS_Error('CONN_UNEXPECTED_ANSWER'); + return gpgme_error('CONN_UNEXPECTED_ANSWER'); } break; case 'more': @@ -183,7 +184,7 @@ class Answer{ this._response[key] = msg[key]; } else if (this._response[key] !== msg[key]){ - return GPGMEJS_Error('CONN_UNEXPECTED_ANSWER',msg[key]); + return gpgme_error('CONN_UNEXPECTED_ANSWER',msg[key]); } } //infos may be json objects etc. Not yet defined. @@ -195,7 +196,7 @@ class Answer{ this._response.push(msg[key]); } else { - return GPGMEJS_Error('CONN_UNEXPECTED_ANSWER', key); + return gpgme_error('CONN_UNEXPECTED_ANSWER', key); } break; } diff --git a/lang/js/src/Errors.js b/lang/js/src/Errors.js index 04b13e10..2f53aa89 100644 --- a/lang/js/src/Errors.js +++ b/lang/js/src/Errors.js @@ -18,99 +18,125 @@ * SPDX-License-Identifier: LGPL-2.1+ */ -/** - * Checks the given error code and returns some information about it's meaning - * @param {String} code The error code - * @returns {Object} An object containing string properties code and msg - * TODO: error-like objects with the code 'GNUPG_ERROR' are errors sent - * directly by gnupg as answer in Connection.post() - */ -export function GPGMEJS_Error(code = 'GENERIC_ERROR'){ - if (!typeof(code) === 'string'){ - code = 'GENERIC_ERROR'; - } - let errors = { //TODO: someplace else - // 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: 'warn' - }, - // 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' - }, - 'MSG_NO_KEYS' : { - msg: 'There were no valid keys provided.', - type: 'warn' - }, - 'MSG_NOT_A_FPR': { - msg: 'The String is not an accepted fingerprint', - type: 'warn' - }, +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' + }, + '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', + 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' + } +}; - // generic - 'PARAM_WRONG':{ - msg: 'invalid parameter was found', - type: 'error' - }, - '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 (code === 'TODO'){ - alert('TODO_Error!'); + if (err_list[code].type === 'warning'){ + console.log(new GPGME_Error(code)); } - if (errors.hasOwnProperty(code)){ - code = 'GENERIC_ERROR'; - } - if (errors.type === 'error'){ - return {code: 'code', - msg: errors[code].msg - }; - } - if (errors.type === 'warning'){ - console.log(code + ': ' + error[code].msg); - } - return undefined; + 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 d9750ba7..841c0eda 100644 --- a/lang/js/src/Helpers.js +++ b/lang/js/src/Helpers.js @@ -17,7 +17,7 @@ * License along with this program; if not, see . * SPDX-License-Identifier: LGPL-2.1+ */ -import { GPGMEJS_Error } from "./Errors"; +import { gpgme_error } from "./Errors"; /** * Tries to return an array of fingerprints, either from input fingerprints or @@ -28,7 +28,7 @@ import { GPGMEJS_Error } from "./Errors"; export function toKeyIdArray(input, nocheck){ if (!input){ - GPGMEJS_Error('MSG_NO_KEYS'); + gpgme_error('MSG_NO_KEYS'); return []; } if (!Array.isArray(input)){ @@ -40,7 +40,7 @@ export function toKeyIdArray(input, nocheck){ if (isFingerprint(input[i]) === true){ result.push(input[i]); } else { - GPGMEJS_Error('MSG_NOT_A_FPR'); + gpgme_error('MSG_NOT_A_FPR'); } } else if (typeof(input[i]) === 'object'){ let fpr = ''; @@ -53,14 +53,14 @@ export function toKeyIdArray(input, nocheck){ if (isFingerprint(fpr) === true){ result.push(fpr); } else { - GPGMEJS_Error('MSG_NOT_A_FPR'); + gpgme_error('MSG_NOT_A_FPR'); } } else { - return GPGMEJS_Error('PARAM_WRONG'); + return gpgme_error('PARAM_WRONG'); } } if (result.length === 0){ - GPGMEJS_Error('MSG_NO_KEYS'); + gpgme_error('MSG_NO_KEYS'); return []; } else { return result; diff --git a/lang/js/src/Key.js b/lang/js/src/Key.js index 5ae80438..f6fa7ae3 100644 --- a/lang/js/src/Key.js +++ b/lang/js/src/Key.js @@ -27,7 +27,9 @@ */ import {isFingerprint} from './Helpers' -import {GPGMEJS_Error} from './Errors' +import {gpgme_error} from './Errors' +import { GPGME_Message } from './Message'; +import { permittedOperations } from './permittedOperations'; export class GPGME_Key { @@ -172,32 +174,30 @@ export class GPGME_Key { * */ function checkKey(fingerprint, property){ - return Promise.reject(GPGMEJS_Error('NOT_YET_IMPLEMENTED')); - + 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('not a fingerprint'); //TBD + reject('KEY_INVALID'); } - let conn = new Connection(); - conn.post('getkey',{ // TODO not yet implemented in gpgme - 'fingerprint': this.fingerprint}) - .then(function(result){ - if (property !== undefined){ - if (result.hasOwnProperty(key)){ - resolve(result[property]); - } - else if (property == 'secret'){ - // property undefined means "not true" in case of secret - resolve(false); - } else { - reject('ERR_INVALID_PROPERTY') //TBD - } + 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')); } - - - resolve(result); }, function(error){ - reject(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 ef8028ff..e1f0a50f 100644 --- a/lang/js/src/Keyring.js +++ b/lang/js/src/Keyring.js @@ -21,6 +21,7 @@ import {GPGME_Message} from './Message' import {GPGME_Key} from './Key' import { isFingerprint, isLongId } from './Helpers'; +import { gpgme_error } from './Errors'; export class GPGME_Keyring { constructor(connection){ @@ -37,9 +38,9 @@ export class GPGME_Keyring { if (this._connection.isConnected){ return this._connection; } - return undefined; //TODO: connection was lost! + return gpgme_error('CONN_DISCONNECTED'); } - return undefined; //TODO: no connection there + return gpgme_error('CONN_NO_CONNECT'); } /** @@ -81,9 +82,7 @@ export class GPGME_Keyring { * 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.defaultKey Only Keys marked as Default Keys * @param {Boolean} flags.secret Only Keys containing a secret part. - * @param {Boolean} flags.valid Valid Keys only * @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 @@ -108,16 +107,20 @@ export class GPGME_Keyring { } 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){ + */ + /** + * 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){ diff --git a/lang/js/src/Message.js b/lang/js/src/Message.js index 1b36f11d..06ac8db2 100644 --- a/lang/js/src/Message.js +++ b/lang/js/src/Message.js @@ -18,7 +18,7 @@ * SPDX-License-Identifier: LGPL-2.1+ */ import { permittedOperations } from './permittedOperations' -import { GPGMEJS_Error } from './Errors' +import { gpgme_error } from './Errors' export class GPGME_Message { //TODO getter @@ -39,20 +39,20 @@ export class GPGME_Message { */ setParameter(param,value){ if (!param || typeof(param) !== 'string'){ - return GPGMEJS_Error('PARAM_WRONG'); + return gpgme_error('PARAM_WRONG'); } if (!this._msg || !this._msg.op){ - return GPGMEJS_Error('MSG_OP_PENDING'); + return gpgme_error('MSG_OP_PENDING'); } let po = permittedOperations[this._msg.op]; if (!po){ - return GPGMEJS_Error('MSG_WRONG_OP'); + return gpgme_error('MSG_WRONG_OP'); } if (po.required.indexOf(param) >= 0 || po.optional.indexOf(param) >= 0){ this._msg[param] = value; return true; } - return GPGMEJS_Error('PARAM_WRONG'); + return gpgme_error('PARAM_WRONG'); } /** @@ -98,7 +98,7 @@ export class GPGME_Message { */ function setOperation (scope, operation){ if (!operation || typeof(operation) !== 'string'){ - return GPGMEJS_Error('PARAM_WRONG'); + return gpgme_error('PARAM_WRONG'); } if (permittedOperations.hasOwnProperty(operation)){ if (!scope._msg){ @@ -106,6 +106,6 @@ function setOperation (scope, operation){ } scope._msg.op = operation; } else { - return GPGMEJS_Error('MSG_WRONG_OP'); + 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 b20ff0f2..b504a457 100644 --- a/lang/js/src/gpgmejs.js +++ b/lang/js/src/gpgmejs.js @@ -21,7 +21,7 @@ import {Connection} from "./Connection" import {GPGME_Message} from './Message' import {toKeyIdArray} from "./Helpers" -import {GPGMEJS_Error as Error, GPGMEJS_Error} from "./Errors" +import { gpgme_error } from "./Errors" import { GPGME_Keyring } from "./Keyring"; export class GpgME { @@ -35,10 +35,12 @@ export class GpgME { set connection(connection){ if (this._connection instanceof Connection){ - //TODO Warning: Connection already established + gpgme_error('CONN_ALREADY_CONNECTED'); } if (connection instanceof Connection){ this._connection = connection; + } else { + gpgme_error('PARAM_WRONG'); } } @@ -54,11 +56,12 @@ export class GpgME { set Keyring(keyring){ if (ring && ring instanceof GPGME_Keyring){ - this.Keyring = ring; + this._Keyring = ring; } } get Keyring(){ + return this._Keyring; } /** @@ -96,7 +99,7 @@ export class GpgME { decrypt(data){ if (data === undefined){ - return Promise.reject(GPGMEJS_Error('MSG_EMPTY')); + return Promise.reject(gpgme_error('MSG_EMPTY')); } let msg = new GPGME_Message('decrypt'); putData(msg, data); @@ -105,7 +108,7 @@ export class GpgME { } deleteKey(key, delete_secret = false, no_confirm = false){ - return Promise.reject(GPGMEJS_Error('NOT_YET_IMPLEMENTED')); + return Promise.reject(gpgme_error('NOT_YET_IMPLEMENTED')); let msg = new GPGME_Message('deletekey'); let key_arr = toKeyIdArray(key); if (key_arr.length !== 1){ @@ -126,7 +129,7 @@ export class GpgME { case 'ERR_NO_ERROR': return Promise.resolve('okay'); //TBD default: - return Promise.reject(GPGMEJS_Error('TODO') ); // + return Promise.reject(gpgme_error('TODO') ); // // INV_VALUE, // GPG_ERR_NO_PUBKEY, // GPG_ERR_AMBIGUOUS_NAME, @@ -145,7 +148,7 @@ export class GpgME { */ function putData(message, data){ if (!message || !message instanceof GPGME_Message ) { - return GPGMEJS_Error('PARAM_WRONG'); + return gpgme_error('PARAM_WRONG'); } if (!data){ message.setParameter('data', ''); @@ -164,6 +167,6 @@ function putData(message, data){ message.setParameter ('data', decoder.decode(txt)); } } else { - return GPGMEJS_Error('PARAM_WRONG'); + return gpgme_error('PARAM_WRONG'); } } diff --git a/lang/js/src/gpgmejs_openpgpjs.js b/lang/js/src/gpgmejs_openpgpjs.js index e32f43a3..4e5e1ea0 100644 --- a/lang/js/src/gpgmejs_openpgpjs.js +++ b/lang/js/src/gpgmejs_openpgpjs.js @@ -28,13 +28,13 @@ import {GPGME_Keyring} from "./Keyring" import { GPGME_Key } from "./Key"; import { isFingerprint } from "./Helpers" - import { GPGMEJS_Error } from './Errors' + import { gpgme_error } from "./Errors" export class GpgME_openpgpmode { - constructor(connection){ - this.initGpgME(connection); + constructor(connection, config = {}){ + this.initGpgME(connection, config); } get Keyring(){ @@ -44,9 +44,16 @@ return undefined; } - initGpgME(connection){ - this._GpgME = new GpgME(connection); - this._Keyring = new GPGME_Keyring_openpgpmode(connection); + initGpgME(connection, config = {}){ + if (connection && typeof(config) ==='object'){ + this._config = config; + if (!this._GPGME){ + this._GpgME = new GpgME(connection, config); + } + if (!this._Keyring){ + this._Keyring = new GPGME_Keyring_openpgpmode(connection); + } + } } get GpgME(){ @@ -59,19 +66,23 @@ * Encrypt Message * Supported: * @param {String|Uint8Array} data + * //an openpgp Message also accepted here. TODO: is this wanted? * @param {Key|Array} publicKeys + * //Strings of Fingerprints * @param {Boolean} wildcard * TODO: - * @param {Key|Array} privateKeys - * @param {String} filename - * @param {module:enums.compression} compression - * @param {Boolean} armor - * @param {Boolean} detached + * @param {Key|Array} privateKeys // -> encryptsign + * @param {module:enums.compression} compression //TODO accepts integer, if 0 (no compression) it won't compress + * @param {Boolean} armor // TODO base64 switch + * @param {Boolean} detached // --> encryptsign * unsupported: * @param {String|Array} passwords * @param {Object} sessionKey * @param {Signature} signature * @param {Boolean} returnSessionKey + * @param {String} filename + * + * Can be set, but will be ignored: * * @returns {Promise} * {data: ASCII armored message, @@ -80,57 +91,66 @@ * @async * @static */ - encrypt({data = '', publicKeys = '', privateKeys, passwords, sessionKey, - filename, compression, armor=true, detached=false, signature=null, - returnSessionKey=null, wildcard=false, date=null}) { - if (passwords !== undefined - || sessionKey !== undefined + encrypt({data = '', publicKeys = '', privateKeys, passwords=null, + sessionKey = null, filename, compression, armor=true, detached=false, + signature=null, returnSessionKey=null, wildcard=false, date=null}) { + if (passwords !== null + || sessionKey !== null || signature !== null || returnSessionKey !== null - || date !== null){ + || date !== null + ){ return Promise.reject(GPMGEJS_Error('NOT_IMPLEMENTED')); } if ( privateKeys - || filename || compression || armor === false || detached == true){ - return Promise.reject(GPGMEJS_Error('NOT_YET_IMPLEMENTED')); + return Promise.reject(gpgme_error('NOT_YET_IMPLEMENTED')); + } + if (filename){ + if (this._config.unconsidered_params === 'warn'){ + GPMGEJS_Error('PARAM_IGNORED'); + } else if (this._config.unconsidered_params === 'error'){ + return Promise.reject(GPMGEJS_Error('NOT_IMPLEMENTED')); + } } return this.GpgME.encrypt(data, translateKeyInput(publicKeys), wildcard); } /** Decrypt Message - * supported - * TODO: @param {Message} message TODO: for now it accepts an armored string only + * supported openpgpjs parameters: + * @param {Message|Uint8Array|String} message Message object from openpgpjs * Unsupported: * @param {String|Array} passwords + * @param {Key|Array} privateKeys * @param {Object|Array} sessionKeys - * @param {Date} date - - * TODO - * @param {Key|Array} privateKey - * @param {Key|Array} publicKeys + * Not yet supported, but planned * @param {String} format (optional) return data format either as 'utf8' or 'binary' * @param {Signature} signature (optional) detached signature for verification - + * Ignored values: can be safely set, but have no effect + * @param {Date} date + * @param {Key|Array} publicKeys + * * @returns {Promise} decrypted and verified message in the form: * { data:Uint8Array|String, filename:String, signatures:[{ keyid:String, valid:Boolean }] } * @async * @static */ - decrypt({ message, privateKeys, passwords, sessionKeys, publicKeys, format='utf8', signature=null, date}) { - if (passwords !== undefined - || sessionKeys - || date){ - return Promise.reject(GPGMEJS_Error('NOT_IMPLEMENTED')); + decrypt({ message, privateKeys, passwords=null, sessionKeys, + publicKeys, format='utf8', signature=null, date= null}) { + if (passwords !== null || sessionKeys || privateKeys){ + return Promise.reject(gpgme_error('NOT_IMPLEMENTED')); } - if ( privateKeys - || publicKeys - || format !== 'utf8' - || signature - ){ - return Promise.reject(GPGMEJS_Error('NOT_YET_IMPLEMENTED')); + if ( format !== 'utf8' || signature){ + return Promise.reject(gpgme_error('NOT_YET_IMPLEMENTED')); + } + if (date !== null || publicKeys){ + if (this._config.unconsidered_params === 'warn'){ + GPMGEJS_Error('PARAM_IGNORED'); + } else if (this._config.unconsidered_params === 'reject'){ + return Promise.reject(GPMGEJS_Error('NOT_IMPLEMENTED')); + } } return this.GpgME.decrypt(message); // TODO: translate between: @@ -185,7 +205,7 @@ class GPGME_Keyring_openpgpmode { else { // TODO: Can there be "no default key"? // TODO: Can there be several default keys? - return GPGMEJS_Error('TODO'); + return gpgme_error('TODO'); } }); } @@ -202,10 +222,10 @@ class GPGME_Keyring_openpgpmode { */ deleteKey(key){ if (typeof(key) !== "object"){ - return Promise.reject(GPGMEJS_Error('PARAM_WRONG')); + return Promise.reject(gpgme_error('PARAM_WRONG')); } if ( !key.fingerprint || ! isFingerprint(key.fingerprint)){ - return Promise.reject(GPGMEJS_Error('PARAM_WRONG')); + return Promise.reject(gpgme_error('PARAM_WRONG')); } let key_to_delete = new GPGME_Key(key.fingerprint); return key_to_delete.deleteKey(key.secret); @@ -224,8 +244,8 @@ class GPGME_Key_openpgpmode { set init (value){ if (!this._GPGME_Key && value instanceof GPGME_Key){ this._GPGME_Key = value; - } else if (!this._GPGME_Key && isFingerprint(fpr)){ - this._GPGME_Key = new GPGME_Key; + } else if (!this._GPGME_Key && isFingerprint(value)){ + this._GPGME_Key = new GPGME_Key(value); } } diff --git a/lang/js/src/index.js b/lang/js/src/index.js index a54277c2..48904316 100644 --- a/lang/js/src/index.js +++ b/lang/js/src/index.js @@ -19,7 +19,7 @@ */ import { GpgME } from "./gpgmejs"; -import { GPGMEJS_Error } from "./Errors"; +import { gpgme_error } from "./Errors"; import { GpgME_openpgpmode } from "./gpgmejs_openpgpjs"; import { Connection } from "./Connection"; @@ -29,7 +29,8 @@ import { Connection } from "./Connection"; */ function init( config = { api_style: 'gpgme', // | gpgme_openpgpjs - null_expire_is_never: true // Boolean + null_expire_is_never: true, // Boolean + unconsidered_params: 'warn'//'warn' || 'reject' }){ return new Promise(function(resolve, reject){ let connection = new Connection; @@ -41,12 +42,12 @@ function init( config = { let gpgme = null; if (config.api_style && config.api_style === 'gpgme_openpgpjs'){ resolve( - new GpgME_openpgpmode(connection)); + new GpgME_openpgpmode(connection, config)); } else { resolve(new GpgME(connection)); } } else { - reject(GPGMEJS_Error('CONN_NO_CONNECT')); + reject(gpgme_error('CONN_NO_CONNECT')); } }; setTimeout(delayedreaction, 5); diff --git a/lang/js/src/permittedOperations.js b/lang/js/src/permittedOperations.js index 892f4f2e..79e74223 100644 --- a/lang/js/src/permittedOperations.js +++ b/lang/js/src/permittedOperations.js @@ -31,7 +31,7 @@ partial and in need of concatenation params: Array Information that do not change throughout the message - infos: Array arbitrary information that may change + infos: Array<*> arbitrary information that may result in a list } } */ @@ -72,7 +72,55 @@ export const permittedOperations = { type: ['plaintext'], data: ['data'], params: ['base64', 'mime'], - infos: ['info'] + infos: [] // pending. Info about signatures and validity + //signature: [{Key Fingerprint, valid Boolean}] + } + }, + /** + keyinfo: { // querying the Key's information. + required: ['fingerprint'], + anser: { + type: ['TBD'], + data: [], + params: ['hasSecret', 'isRevoked', 'isExpired', 'armored', + 'timestamp', 'expires', 'pubkey_algo'], + infos: ['subkeys', 'userIds'] + }*/ + + /** + listkeys:{ + optional: ['with-secret', 'pattern'], + answer: { + type: ['TBD'], //Array of fingerprints? + infos: ['TBD'] //the property with infos + }, + */ + + /** + importkey: { + required: ['keyarmored'], + answer: { + type: ['TBD'], + infos: [''], // for each key if import was a success, if it was an update + } + }, + */ + + /** + deletekey: { + required: ['fingerprint'], + answer: { + type ['TBD'], + infos: [''] //success:true? in gpgme, an error NO_ERROR is returned } } + */ + + /** + *get armored secret different treatment from keyinfo! + */ + + /** + * TBD key modification requests? + */ } From 3685913bf510a14b8cb324d980217d90489e6453 Mon Sep 17 00:00:00 2001 From: Maximilian Krambach Date: Wed, 25 Apr 2018 19:45:39 +0200 Subject: [PATCH 11/95] js: First testing and improvements -- * Introduced Mocha/chai as testsuite. After development build 'npm test' should run the unit tests. Functionality exclusive to Browsers/WebExtensions cannot be run this way, so some other testing is still needed. - package.json: Added required development packages - .babelrc indirect configuration for mocha. ES6 transpiling needs some babel configuration, but mocha has no setting for it. - test/mocha.opts Vonfiguration for mocha runs * Fixed errors: - Helpers.js toKeyIdArray; isLongId is now exported - Key.js Key constructor failed - Message.js will not throw an Error during construction, a new message is now created with createMessage, which can return an Error or a GPGME_Message object * Tests: - test/Helpers: exports from Helpers.js, GPGME_Error handling - test/Message: first init test with bad parameters --- lang/js/.babelrc | 1 + lang/js/package.json | 6 ++- lang/js/src/Connection.js | 2 +- lang/js/src/Errors.js | 11 ++-- lang/js/src/Helpers.js | 7 +-- lang/js/src/Key.js | 15 +++--- lang/js/src/Keyring.js | 7 ++- lang/js/src/Message.js | 47 ++++++++-------- lang/js/src/gpgmejs.js | 31 +++++++---- lang/js/test/Helpers.js | 110 ++++++++++++++++++++++++++++++++++++++ lang/js/test/Message.js | 42 +++++++++++++++ lang/js/test/mocha.opts | 4 ++ 12 files changed, 227 insertions(+), 56 deletions(-) create mode 100644 lang/js/.babelrc create mode 100644 lang/js/test/Helpers.js create mode 100644 lang/js/test/Message.js create mode 100644 lang/js/test/mocha.opts 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 @@ -5,13 +5,15 @@ "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 @@ -83,7 +83,7 @@ export class Connection{ */ 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); 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 @@ -55,14 +55,9 @@ const err_list = { 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.', @@ -78,7 +73,7 @@ const err_list = { }, // generic 'PARAM_WRONG':{ - msg: 'invalid parameter was found', + msg: 'Invalid parameter was found', type: 'error' }, 'PARAM_IGNORED': { @@ -111,7 +106,7 @@ export function gpgme_error(code = 'GENERIC_ERROR', info){ 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'){ 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 @@ -18,6 +18,7 @@ * 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 @@ -26,7 +27,7 @@ import { gpgme_error } from "./Errors"; * @returns {Array} Array of fingerprints. */ -export function toKeyIdArray(input, nocheck){ +export function toKeyIdArray(input){ if (!input){ gpgme_error('MSG_NO_KEYS'); return []; @@ -46,7 +47,7 @@ export function toKeyIdArray(input, nocheck){ 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(); } @@ -92,7 +93,7 @@ export function isFingerprint(string){ /** * 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); }; 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 @@ -26,9 +26,9 @@ * */ -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 { @@ -39,7 +39,7 @@ export class GPGME_Key { set fingerprint(fpr){ if (isFingerprint(fpr) === true && !this._fingerprint){ - this._fingerprint = fingerprint; + this._fingerprint = fpr; } } @@ -181,9 +181,12 @@ function checkKey(fingerprint, property){ } 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)){ 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 @@ -18,7 +18,7 @@ * 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'; @@ -50,7 +50,10 @@ export class GPGME_Keyring { * */ 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); } 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 @@ -19,13 +19,34 @@ */ 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; } @@ -41,9 +62,6 @@ export class GPGME_Message { 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'); @@ -90,22 +108,3 @@ export class GPGME_Message { } } - -/** - * 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 @@ -19,7 +19,7 @@ */ 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"; @@ -71,8 +71,10 @@ export class GpgME { */ 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); @@ -101,7 +103,10 @@ export class GpgME { 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); @@ -109,21 +114,27 @@ export class GpgME { 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': 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 From 1f7b19512cfa7e1b153b99d6a2b40bad82a5496e Mon Sep 17 00:00:00 2001 From: Maximilian Krambach Date: Thu, 26 Apr 2018 17:13:34 +0200 Subject: [PATCH 12/95] js: created TestExtension and smaller fixes -- * Extensions: - Moved testapplication to Demoextension - Created BrowserTestExtension. Includes mocha and chai. For running tests that cannot be run outside a WebExtension Both Extensions can be found zipped in build/extensions after running build_extensions.sh * Code changes: - src/Config: Place for the configuration - small fixes raised during testing in Keyring.js, Message.js, - src/gpgmejs_openpgpjs.js don't offer direct GpgME object to the outside, as it only causes confusion - index.js init() now checks the config for validity * Tests: - Reordered tests in test/. - Input values are now in a separate file which may be of use for bulk testing * moved the build directory from dist to build --- lang/js/BrowserTestExtension/browsertest.html | 23 +++ lang/js/BrowserTestExtension/manifest.json | 13 ++ lang/js/BrowserTestExtension/popup.html | 9 ++ lang/js/BrowserTestExtension/popup.js | 44 ++++++ .../js/BrowserTestExtension/runbrowsertest.js | 21 +++ lang/js/BrowserTestExtension/setup_testing.js | 22 +++ .../{ => BrowserTestExtension}/testicon.png | Bin .../BrowserTestExtension/tests/inputvalues.js | 28 ++++ lang/js/BrowserTestExtension/tests/startup.js | 51 +++++++ lang/js/CHECKLIST | 8 +- lang/js/CHECKLIST_build | 6 - .../{test_index.js => DemoExtension/entry.js} | 2 +- .../maindemo.js} | 0 .../{ui2.html => DemoExtension/mainui.html} | 4 +- lang/js/{ => DemoExtension}/manifest.json | 6 +- .../popup.html} | 0 lang/js/DemoExtension/testicon.png | Bin 0 -> 16192 bytes lang/js/{ => DemoExtension}/ui.css | 0 lang/js/build_extensions.sh | 14 ++ lang/js/src/Config.js | 31 ++++ lang/js/src/Keyring.js | 1 + lang/js/src/Message.js | 2 +- lang/js/src/gpgmejs_openpgpjs.js | 14 +- lang/js/src/index.js | 70 ++++++--- lang/js/test/Helpers.js | 139 ++++++++---------- lang/js/test/Message.js | 33 +++-- lang/js/test/inputvalues.js | 29 ++++ lang/js/webpack.conf.js | 2 +- 28 files changed, 429 insertions(+), 143 deletions(-) create mode 100644 lang/js/BrowserTestExtension/browsertest.html create mode 100644 lang/js/BrowserTestExtension/manifest.json create mode 100644 lang/js/BrowserTestExtension/popup.html create mode 100644 lang/js/BrowserTestExtension/popup.js create mode 100644 lang/js/BrowserTestExtension/runbrowsertest.js create mode 100644 lang/js/BrowserTestExtension/setup_testing.js rename lang/js/{ => BrowserTestExtension}/testicon.png (100%) create mode 100644 lang/js/BrowserTestExtension/tests/inputvalues.js create mode 100644 lang/js/BrowserTestExtension/tests/startup.js rename lang/js/{test_index.js => DemoExtension/entry.js} (96%) rename lang/js/{testapplication.js => DemoExtension/maindemo.js} (100%) rename lang/js/{ui2.html => DemoExtension/mainui.html} (89%) rename lang/js/{ => DemoExtension}/manifest.json (54%) rename lang/js/{testapplication_index.html => DemoExtension/popup.html} (100%) create mode 100644 lang/js/DemoExtension/testicon.png rename lang/js/{ => DemoExtension}/ui.css (100%) create mode 100755 lang/js/build_extensions.sh create mode 100644 lang/js/src/Config.js create mode 100644 lang/js/test/inputvalues.js diff --git a/lang/js/BrowserTestExtension/browsertest.html b/lang/js/BrowserTestExtension/browsertest.html new file mode 100644 index 00000000..d2c6396f --- /dev/null +++ b/lang/js/BrowserTestExtension/browsertest.html @@ -0,0 +1,23 @@ + + + + + + + +

Browsertest

+
+ + + + + + + + + + + + + + diff --git a/lang/js/BrowserTestExtension/manifest.json b/lang/js/BrowserTestExtension/manifest.json new file mode 100644 index 00000000..a9e605bc --- /dev/null +++ b/lang/js/BrowserTestExtension/manifest.json @@ -0,0 +1,13 @@ +{ + "manifest_version": 2, + + "name": "Browsertests for gpgmejs", + "description": "Run the browsertests.", + "version": "0.1", + "content_security_policy": "default-src 'self' filesystem:", + "browser_action": { + "default_icon": "testicon.png", + "default_popup": "popup.html" + }, + "permissions": ["nativeMessaging", "activeTab"] + } diff --git a/lang/js/BrowserTestExtension/popup.html b/lang/js/BrowserTestExtension/popup.html new file mode 100644 index 00000000..f17f262a --- /dev/null +++ b/lang/js/BrowserTestExtension/popup.html @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/lang/js/BrowserTestExtension/popup.js b/lang/js/BrowserTestExtension/popup.js new file mode 100644 index 00000000..4764df55 --- /dev/null +++ b/lang/js/BrowserTestExtension/popup.js @@ -0,0 +1,44 @@ +/* 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+ + */ +/* 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+ + */ + +document.addEventListener('DOMContentLoaded', function() { + chrome.tabs.create({ + url: './browsertest.html' + }); +}); diff --git a/lang/js/BrowserTestExtension/runbrowsertest.js b/lang/js/BrowserTestExtension/runbrowsertest.js new file mode 100644 index 00000000..39bc3fb9 --- /dev/null +++ b/lang/js/BrowserTestExtension/runbrowsertest.js @@ -0,0 +1,21 @@ +/* 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+ + */ + +mocha.run(); diff --git a/lang/js/BrowserTestExtension/setup_testing.js b/lang/js/BrowserTestExtension/setup_testing.js new file mode 100644 index 00000000..7f70d347 --- /dev/null +++ b/lang/js/BrowserTestExtension/setup_testing.js @@ -0,0 +1,22 @@ +/* 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+ + */ +mocha.setup('bdd'); +var expect = chai.expect; +chai.config.includeStack = true; \ No newline at end of file diff --git a/lang/js/testicon.png b/lang/js/BrowserTestExtension/testicon.png similarity index 100% rename from lang/js/testicon.png rename to lang/js/BrowserTestExtension/testicon.png diff --git a/lang/js/BrowserTestExtension/tests/inputvalues.js b/lang/js/BrowserTestExtension/tests/inputvalues.js new file mode 100644 index 00000000..47600c84 --- /dev/null +++ b/lang/js/BrowserTestExtension/tests/inputvalues.js @@ -0,0 +1,28 @@ +/* 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+ + */ + +var inputvalues = { + encrypt: { + good:{ + data : 'Hello World.', + keyid : 'CDC3A2B2860625CCBFC5A5A9FC6D1B604967FC40' + } + } +}; diff --git a/lang/js/BrowserTestExtension/tests/startup.js b/lang/js/BrowserTestExtension/tests/startup.js new file mode 100644 index 00000000..14d12c0a --- /dev/null +++ b/lang/js/BrowserTestExtension/tests/startup.js @@ -0,0 +1,51 @@ +/* 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+ + */ + + describe('GPGME context', function(){ + it('Starting a GpgME instance', function(done){ + Gpgmejs.init().then( + function(context){ + expect(context.connection).to.not.be.undefined; + expect(context).to.be.an('object'); + expect(context.connection).to.be.an('object'); + expect(context.Keyring).to.be.undefined; + expect(context.encrypt).to.be.a('function'); + expect(context.decrypt).to.be.a('function'); + done(); + }, function(err){ + done(err); + }); + }); + it('Starting an openpgp mode GPGME instance', function(done){ + Gpgmejs.init({api_style:"gpgme_openpgpjs"}).then( + function(context){ + console.log(context); + done(); + // expect(context).to.be.an('object'); + // expect(context.connection).to.be.undefined; + // expect(context.Keyring).to.be.an('object'); + // expect(context.encrypt).to.be.a('function'); + // expect(context.decrypt).to.be.a('function'); + // done(); + }, function(err){ + done(err); + }); + }); + }); diff --git a/lang/js/CHECKLIST b/lang/js/CHECKLIST index 49b17265..75664ae5 100644 --- a/lang/js/CHECKLIST +++ b/lang/js/CHECKLIST @@ -8,8 +8,8 @@ receiving an answer replicating Openpgpjs API: - [*] Message handling (encrypt, verify, sign) - [x] encrypt + [*] Message handling (encrypt, decrypt verify, sign) + [x] encrypt, decrypt [ ] verify [ ] sign [*] Key handling (import/export, modifying, status queries) @@ -23,6 +23,6 @@ Communication with other implementations Management: [*] Define the gpgme interface - [ ] check Permissions (e.g. csp) for the different envs + [x] check Permissions (e.g. csp) for the different envs [X] agree on license - [ ] tests + [*] tests diff --git a/lang/js/CHECKLIST_build b/lang/js/CHECKLIST_build index 19eb2146..a7c8d08d 100644 --- a/lang/js/CHECKLIST_build +++ b/lang/js/CHECKLIST_build @@ -1,9 +1,3 @@ - Checklist for build/install: browsers' manifests (see README) need allowedextension added, and the path set - -manifest.json/ csp needs adaption - -/dist should be built which is used by the example app. - -csp in manifest.json MUST NOT contain "unsafe-eval" in production! diff --git a/lang/js/test_index.js b/lang/js/DemoExtension/entry.js similarity index 96% rename from lang/js/test_index.js rename to lang/js/DemoExtension/entry.js index 9119d271..7e5e1ffe 100644 --- a/lang/js/test_index.js +++ b/lang/js/DemoExtension/entry.js @@ -20,6 +20,6 @@ */ document.addEventListener('DOMContentLoaded', function() { chrome.tabs.create({ - url: './ui2.html' + url: './uimainui.html' }); }); diff --git a/lang/js/testapplication.js b/lang/js/DemoExtension/maindemo.js similarity index 100% rename from lang/js/testapplication.js rename to lang/js/DemoExtension/maindemo.js diff --git a/lang/js/ui2.html b/lang/js/DemoExtension/mainui.html similarity index 89% rename from lang/js/ui2.html rename to lang/js/DemoExtension/mainui.html index 8d0abd97..d85e7a46 100644 --- a/lang/js/ui2.html +++ b/lang/js/DemoExtension/mainui.html @@ -3,8 +3,8 @@ - - + +