diff --git a/.gitignore b/.gitignore index 7e21a61..e956266 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,6 @@ node_modules/ bower_components/ *.log *.user +*.pem keys data/ diff --git a/docs/plugins/keewebhttp/manifest.json b/docs/plugins/keewebhttp/manifest.json new file mode 100644 index 0000000..3c9f9d0 --- /dev/null +++ b/docs/plugins/keewebhttp/manifest.json @@ -0,0 +1,17 @@ +{ + "version": "0.0.1", + "manifestVersion": "0.1.0", + "name": "keewebhttp", + "description": "KeeWebHttp allows to use browser extensions with KeeWeb", + "author": { + "name": "KeeWeb", + "email": "antelle.net@gmail.com", + "url": "http://keeweb.net" + }, + "licence": "MIT", + "url": "https://plugins.keeweb.info/plugin/keewebhttp", + "resources": { + "js": "TBYsEeXIPNOFj+l5A11vpw6L1VotzpzmbYWQqRXO++fD2kevLkzWt0jx7xDxlZ0dRN3VzSxDUYqEHAYSgVn5ts/r6NXIOpPj7V2goR1TiknAymWhK59quEsaApcySO2oVtNo4UjZ3Kk34q2Mwlg2IiH1P5JhdjIb5cnQoN4gpSob0lbgCXnjUlZEPfZpOJ0zNtZpkE86OL1Lf2LrgzmauFXe+ColID4zNIP/6iGpPOnpe6FFqLMEgy5C4lkJ2/S0AHDAs49mzMKRhi+bcoh/PclrZ5hdqgUUIt0MNC1Wg0hxGHlHIpi+ObktARND9xcoMdZunzc/peUa91ZIC7n3Ig==" + }, + "publicKey": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0oZB2Kt7AzRFNqf8FuO3C3kepHPAIQYiDPYdQxHcsiaFCwyKVx6K1cE/3vBhb8/2rj+QIIWNfAAuu1Y+2VK90ZBeq6HciukWzQRO/HWhfdy0c7JwDAslmyGI5olj0ZQkNLhkde1MiMxjDPpRhZtdJaryVO5cFJaJESpv3dV6m0qXsaQCluWYOSNfSjP9C8o2zRVjSi3ZQZnZIV5pnk9K2MtlZIPXrN9iJiM5zZ9DTSnqApI6dC9mX4R3LvGN+GTovm9C8Crl+qb106nGRR3LcweicDnPyMtZLa/E0DBpWYxUVLDp6WeLhxoUBr+6+t3Xp9IDnPoANDQXJXD0f1vQxQIDAQAB" +} \ No newline at end of file diff --git a/docs/plugins/keewebhttp/plugin.js b/docs/plugins/keewebhttp/plugin.js new file mode 100644 index 0000000..8be792e --- /dev/null +++ b/docs/plugins/keewebhttp/plugin.js @@ -0,0 +1,250 @@ +const nodeRequire = window.require; + +const http = nodeRequire('http'); +const crypto = nodeRequire('crypto'); +const fs = nodeRequire('fs'); +const path = nodeRequire('path'); + +const AutoType = require('auto-type/index'); +const AutoTypeFilter = require('auto-type/auto-type-filter'); +const Logger = require('util/logger'); +const Alerts = require('comp/alerts'); + +const appModel = AutoType.appModel; // TODO: use AppModel.instance + +const Version = '1.8.4.2'; +const KeyPrefix = 'plugin:keewebhttp:key_'; +const SignatureError = 'Request signature missing'; + +const keys = {}; + +const logger = new Logger('keewebhttp'); + +let server; +let uninstalled = false; + +setTimeout(init, 0); + +function init() { + if (uninstalled) { + return; + } + server = http.createServer((req, res) => { + if (req.method === 'POST') { + const body = []; + req.on('data', data => body.push(data)); + req.on('end', () => { + const postData = Buffer.concat(body).toString(); + logger.debug('<', postData); + handleRequest(postData).then(result => { + logger.debug('>', JSON.stringify(result)); + res.statusCode = 200; + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify(result)); + }); + }); + } else { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/plain'); + res.end('Nice to meet you! But you should POST here.'); + } + }); + const port = 19455; + const hostname = '127.0.0.1'; + server.listen(port, hostname, () => { + if (uninstalled) { + server.close(); + return; + } + logger.debug(`Server running at http://${hostname}:${port}/`); + }); +} + +function handleRequest(req) { + try { + req = JSON.parse(req); + const response = executeRequest(req); + if (response instanceof Promise) { + return response.catch(e => { + return returnError(req, e); + }); + } else { + return Promise.resolve(response); + } + } catch (e) { + return returnError(req, e); + } +} + +function returnError(req, e) { + if (e !== SignatureError) { + logger.error('handleRequest error', e); + } + return Promise.resolve({ + Error: e ? e.toString() : '', + Success: false, + RequestType: req ? req.RequestType : '', + Version + }); +} + +function executeRequest(req) { + switch (req.RequestType) { + case 'test-associate': + return testAssociate(req); + case 'associate': + return associate(req); + case 'get-logins': + return getLogins(req, {}); + case 'get-logins-count': + return getLogins(req, { onlyCount: true }); + case 'get-all-logins': + return getLogins(req, { all: true }); + case 'set-login': + return setLogin(req); + case 'generate-password': + return generatePassword(req); + default: + throw 'Not implemented'; + } +} + +function decrypt(req, value) { + const reqKey = keys[req.Id] || req.Key; + if (!reqKey || !req.Nonce || !req.Verifier) { + throw SignatureError; + } + const key = Buffer.from(reqKey, 'base64'); + const nonce = Buffer.from(req.Nonce, 'base64'); + + const decipher = crypto.createDecipheriv('aes-256-cbc', key, nonce); + return Buffer.concat([decipher.update(value, 'base64'), decipher.final()]).toString(); +} + +function encrypt(resp, value) { + const key = Buffer.from(keys[resp.Id], 'base64'); + const nonce = Buffer.from(resp.Nonce, 'base64'); + const cipher = crypto.createCipheriv('aes-256-cbc', key, nonce); + return Buffer.concat([cipher.update(value, 'utf8'), cipher.final()]).toString('base64'); +} + +function verifyRequest(req) { + if (req.Id && !keys[req.Id]) { + keys[req.Id] = appModel.settings.get(KeyPrefix + req.Id); + } + const decrypted = decrypt(req, req.Verifier); + if (decrypted !== req.Nonce) { + throw 'Invalid signature'; + } +} + +function wrapResponse(resp, id) { + resp = Object.assign({ + Success: true, + Nonce: '', + Verifier: '', + Version: Version + }, resp); + if (id && keys[id]) { + const key = Buffer.from(keys[id], 'base64'); + const nonce = crypto.randomBytes(16); + const cipher = crypto.createCipheriv('aes-256-cbc', key, nonce); + const encrypted = Buffer.concat([cipher.update(nonce.toString('base64'), 'utf8'), cipher.final()]).toString('base64'); + resp.Id = id; + resp.Nonce = nonce.toString('base64'); + resp.Verifier = encrypted; + } + return resp; +} + +function testAssociate(req) { + verifyRequest(req); + return wrapResponse({ + RequestType: req.RequestType, + TriggerUnlock: req.TriggerUnlock + }, req.Id); +} + +function associate(req) { + verifyRequest(req); + return new Promise((resolve, reject) => { + Alerts.yesno({ + header: 'Plugin Connecting', + body: 'A plugin is trying to connect to KeeWeb. If you are setting up your plugin, please allow the connection. ' + + 'Otherwise, click No.', + success: () => { resolve(); }, + cancel: () => { reject('Rejected'); } + }); + }).then(() => { + const id = 'KeeWeb_' + new Date().toISOString() + '_' + crypto.randomBytes(16).toString('hex'); + appModel.settings.set(KeyPrefix + id, req.Key); + keys[id] = req.Key; + fs.writeFileSync(path.join(__dirname, 'keys.json'), JSON.stringify(keys)); + return wrapResponse({ + RequestType: req.RequestType + }, id); + }); +} + +function getLogins(req, config) { + verifyRequest(req); + if (!req.Url) { + throw 'Invalid request'; + } + const url = decrypt(req, req.Url); + logger.debug('get-logins', url); + const response = wrapResponse({ + RequestType: req.RequestType + }, req.Id); + const filter = new AutoTypeFilter({ url }, appModel); + const entries = filter.getEntries(); + response.Count = entries.length; + if (!config.onlyCount) { + response.Entries = entries.map(entry => ({ + Login: entry.user ? encrypt(response, entry.user) : '', + Name: entry.title ? encrypt(response, entry.title) : '', + Password: entry.password ? encrypt(response, entry.password.getText()) : '', + StringFields: null, + Uuid: encrypt(response, entry.id) + })); + } + return response; +} + +function setLogin(req) { + verifyRequest(req); + if (!req.Url || !req.Login || !req.Password) { + throw 'Invalid request'; + } + const url = decrypt(req, req.Url); + const login = decrypt(req, req.Login); + const password = decrypt(req, req.Password); + logger.debug('set-login', url, login, password); + return wrapResponse({ + RequestType: req.RequestType + }, req.Id); +} + +function generatePassword(req) { + verifyRequest(req); + const response = wrapResponse({ + RequestType: req.RequestType + }, req.Id); + response.Count = 1; + response.Entries = [{ + Login: '', + Name: '', + Password: encrypt(response, 'I am generated password: ' + new Date()), + StringFields: null, + Uuid: '' + }]; + return response; +} + +module.exports.uninstall = function() { + if (server) { + server.close(); + server = null; + } + uninstalled = true; +};