diff --git a/lang/js/CHECKLIST b/lang/js/CHECKLIST index 79a35cb7..49b17265 100644 --- a/lang/js/CHECKLIST +++ b/lang/js/CHECKLIST @@ -1,30 +1,28 @@ 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 [ ] 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 + [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 @@ -1,9 +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. +/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,52 +1,57 @@ -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: (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 index 8bb5c58d..e5e17aa5 100644 --- a/lang/js/manifest.json +++ b/lang/js/manifest.json @@ -1,18 +1,14 @@ { "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", + "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 @@ -1,17 +1,17 @@ { "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" }, "keywords": [], "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 + } /** - * 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 + * Immediately closes the open port */ - this.post = function(operation, message){ - let timeout = 5000; - let me = this; - if (!message || !operation){ - return Promise.reject('no message'); // TBD + disconnect() { + if (this._connection){ + this._connection.disconnect(); } + } - let keys = Object.keys(message); - for (let i=0; i < keys.length; i++){ - let property = keys[i]; - me._msg[property] = message[property]; + /** + * 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 + */ + post(message){ + if (!message || !message instanceof GPGME_Message){ + return Promise.reject('ERR_NO_MSG'); } - me._msg['op'] = operation; - // TODO fancier checks if what we want is consistent with submitted content + // let timeout = 5000; //TODO config + let me = this; 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 @@ -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}); -}; +/* 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+ + */ -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 +import {Connection} from "./Connection" +import {GPGME_Message} from './Message' +import {toKeyIdArray} from "./Helpers" + +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(); } - //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; -} + /** + * @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){ -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 msg = new GPGME_Message; + msg.operation = 'encrypt'; - let conn = new Connection(); - let privkeys = toKeyIdArray(privateKeys); - return conn.post('sign', { - 'data': data, - 'keys': privkeys, - 'armor': armor}); -}; + // TODO temporary + msg.setParameter('armor', true); + msg.setParameter('always-trust', true); -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}); -} + let pubkeys = toKeyIdArray(publicKeys); + msg.setParameter('keys', pubkeys); + putData(msg, data); + if (wildcard === true){msg.setParameter('throw-keyids', true); + }; -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 + if (msg.isComplete === true) { + let conn = new Connection(); + return (conn.post(msg.message)); + } + else { + return Promise.reject('NO_CONNECT'); + //TODO + } } - 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; -}; + /** + * @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 + */ -export function encryptKey({ privateKey, passphrase }) { - throw('not implemented here'); - return false; -}; + decrypt(data){ -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; + 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 + } } - 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. + * 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 toKeyIdArray(array){ - let result = []; - if (!array){ - return result; - } - if (!Array.isArray(array)){ - if (isKeyId(array) === true){ - return [keyId]; - } - return result; +function putData(message, data){ + if (!message || !message instanceof GPGME_Message ) { + throw('NO_MESSAGE_OBJECT'); } - for (let i=0; i < array.length; i++){ - if (isKeyId(array[i]) === true){ - result.push(array[i]); - } + 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'); } - return result; -}; +} \ 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 + * + * 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. * - * import { encryptMessage } from 'gpgme.js' - * encryptMessage(keys, text) + * 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,13 +1,35 @@ +/* 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 = { entry: './src/index.js', // mode: 'development', mode: 'production', output: { path: path.resolve(__dirname, 'dist'), filename: 'gpgmejs.bundle.js', libraryTarget: 'var', + libraryExport: 'default', library: 'Gpgmejs' } };