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.
This commit is contained in:
raimund.renkert@intevation.de 2018-04-10 11:33:14 +02:00 committed by Werner Koch
parent 5eb261d602
commit eef3a509fa
No known key found for this signature in database
GPG Key ID: E3FDFF218E45B72B
14 changed files with 472 additions and 1 deletions

View 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.

30
lang/js/CHECKLIST Normal file
View File

@ -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

9
lang/js/CHECKLIST_build Normal file
View File

@ -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.

52
lang/js/README Normal file
View File

@ -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.

18
lang/js/manifest.json Normal file
View File

@ -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"]
}
}

17
lang/js/package.json Normal file
View File

@ -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"
}
}

76
lang/js/src/Connection.js Normal file
View File

@ -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;
};

187
lang/js/src/gpgmejs.js Normal file
View File

@ -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;
};

14
lang/js/src/index.js Normal file
View File

@ -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';

View File

@ -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);
});

BIN
lang/js/testicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

10
lang/js/ui.css Normal file
View File

@ -0,0 +1,10 @@
ul {
list-style-type: none;
padding-left: 0px;
}
ul li span {
float: left;
width: 120px;
margin-top: 6px;
}

24
lang/js/ui.html Normal file
View File

@ -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>

13
lang/js/webpack.conf.js Normal file
View File

@ -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'
}
};