js: Initial commit for JavaScript Native Messaging API
authorraimund.renkert@intevation.de <raimund.renkert@intevation.de>
Tue, 10 Apr 2018 09:33:14 +0000 (11:33 +0200)
committerWerner Koch <wk@gnupg.org>
Tue, 10 Apr 2018 16:47:59 +0000 (18:47 +0200)
--

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.

14 files changed:
lang/README
lang/js/CHECKLIST [new file with mode: 0644]
lang/js/CHECKLIST_build [new file with mode: 0644]
lang/js/README [new file with mode: 0644]
lang/js/manifest.json [new file with mode: 0644]
lang/js/package.json [new file with mode: 0644]
lang/js/src/Connection.js [new file with mode: 0644]
lang/js/src/gpgmejs.js [new file with mode: 0644]
lang/js/src/index.js [new file with mode: 0644]
lang/js/testapplication.js [new file with mode: 0644]
lang/js/testicon.png [new file with mode: 0644]
lang/js/ui.css [new file with mode: 0644]
lang/js/ui.html [new file with mode: 0644]
lang/js/webpack.conf.js [new file with mode: 0644]

index ee99f0f..afd7b08 100644 (file)
@@ -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 (file)
index 0000000..79a35cb
--- /dev/null
@@ -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 (file)
index 0000000..fa162a1
--- /dev/null
@@ -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 (file)
index 0000000..3ca0743
--- /dev/null
@@ -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 (file)
index 0000000..8bb5c58
--- /dev/null
@@ -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 (file)
index 0000000..46b60fd
--- /dev/null
@@ -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 (file)
index 0000000..e8fea54
--- /dev/null
@@ -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 (file)
index 0000000..dedbf80
--- /dev/null
@@ -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 (file)
index 0000000..02dc919
--- /dev/null
@@ -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 (file)
index 0000000..d01aca9
--- /dev/null
@@ -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 (file)
index 0000000..12c3f5d
Binary files /dev/null and b/lang/js/testicon.png differ
diff --git a/lang/js/ui.css b/lang/js/ui.css
new file mode 100644 (file)
index 0000000..9c88698
--- /dev/null
@@ -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 (file)
index 0000000..9c56c2e
--- /dev/null
@@ -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 (file)
index 0000000..71b7116
--- /dev/null
@@ -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'
+  }
+};