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