diff --git a/docs/plugins/keewebhttp/manifest.json b/docs/plugins/keewebhttp/manifest.json index f124815..01677f4 100644 --- a/docs/plugins/keewebhttp/manifest.json +++ b/docs/plugins/keewebhttp/manifest.json @@ -1,5 +1,5 @@ { - "version": "0.0.1", + "version": "0.0.2", "manifestVersion": "0.1.0", "name": "keewebhttp", "description": "KeeWebHttp allows to use browser extensions with KeeWeb", @@ -11,8 +11,9 @@ "licence": "MIT", "url": "https://plugins.keeweb.info/plugins/keewebhttp", "resources": { - "js": "dWXmIa4n78RRR6tiMNIqnejfj54lOsNJrn6mz1eQ3HYpCu4sVKv7E+9ABSGykyHJJVQDjTJAUF+7UcwCfDhIFfbFDttPmeylM3vtg+YAfaJFfU5e0l9/MtuUEuXuiUwjXi3jFg5yAYNq9ZCkHs2YCm4JyE8KXm1flmzIpmTiVi6TkX/ulmO74lLm2wXUD23eP7B8RqXk0wBoc7VcZ1x5uHomJIrQfS+J7vdXrMLBt/EJU4VNlyw8LLxcwP3/UbsBzBotpif3AH0hq+Dcwuh8v8H9Z+756T8EFEXz0JmCLhU0oyFcgOmUt0QwrIiFD18aHt0s80BmuqO600I/3M3+Sg==" + "js": "NB8wqvULI7yTqf8FAkJYx2TbyW/BjDo7zpm15MHHHTcYYVIRhYFAeiKx5f9j9UtXLYr1deVc7STdvVLhNsD//tN4LRsSXmOiMt2NnhjIHLhVHwlKxSZTMeK5NrajxjdS0YtBKMg5oDt9vYa15qOvoX0fhmc9E1gD6j5z9sJAxT450JEDRZidXLnUnEnGIZunVH5OCe75eavvcEQ64sZ3pUouTN9iWU66sjC3hG/pk3o5I4+8W4v1MWy2TR7IKaVOLrRiDrzT/rnZoroRg7QLLUb+L185S/OYMEo8MIa7lMu/ULhmbhk0+JT2pukEofPkOZAlF8BIf1EGXGp6gObo4w==" }, "publicKey": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0oZB2Kt7AzRFNqf8FuO3C3kepHPAIQYiDPYdQxHcsiaFCwyKVx6K1cE/3vBhb8/2rj+QIIWNfAAuu1Y+2VK90ZBeq6HciukWzQRO/HWhfdy0c7JwDAslmyGI5olj0ZQkNLhkde1MiMxjDPpRhZtdJaryVO5cFJaJESpv3dV6m0qXsaQCluWYOSNfSjP9C8o2zRVjSi3ZQZnZIV5pnk9K2MtlZIPXrN9iJiM5zZ9DTSnqApI6dC9mX4R3LvGN+GTovm9C8Crl+qb106nGRR3LcweicDnPyMtZLa/E0DBpWYxUVLDp6WeLhxoUBr+6+t3Xp9IDnPoANDQXJXD0f1vQxQIDAQAB", - "desktop": true + "desktop": true, + "versionMin": "1.5.1" } diff --git a/docs/plugins/keewebhttp/plugin.js b/docs/plugins/keewebhttp/plugin.js index 1756559..b9628f8 100644 --- a/docs/plugins/keewebhttp/plugin.js +++ b/docs/plugins/keewebhttp/plugin.js @@ -1,282 +1,311 @@ -const nodeRequire = window.require; - -const http = nodeRequire('http'); -const crypto = nodeRequire('crypto'); -const fs = nodeRequire('fs'); -const path = nodeRequire('path'); -const electron = nodeRequire('electron'); - -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 = ...; TODO: use AppModel.instance - -const Version = '1.8.4.2'; -const DebugMode = true; - -const keys = {}; - -const logger = new Logger('keewebhttp'); - let server; let uninstalled = false; -setTimeout(init, 0); +setTimeout(run, 500); -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(); - if (DebugMode) { - logger.debug('< ' + postData); - } - new RequestContext(postData) - .handle() - .then(response => { - if (DebugMode) { - logger.debug('> ' + response); - } - res.statusCode = 200; - res.setHeader('Content-Type', 'application/json'); - res.end(response); - }); - }); - } else { - res.statusCode = 200; - res.setHeader('Content-Type', 'text/plain'); - res.end('Hey dude, you should POST here!'); - } - }); - const port = 19455; - const hostname = '127.0.0.1'; - server.listen(port, hostname, () => { +function run() { + const nodeRequire = window.require; + + const http = nodeRequire('http'); + const crypto = nodeRequire('crypto'); + const fs = nodeRequire('fs'); + const path = nodeRequire('path'); + const electron = nodeRequire('electron'); + + const AppModel = require('models/app-model'); + const AutoTypeFilter = require('auto-type/auto-type-filter'); + const Logger = require('util/logger'); + const Alerts = require('comp/alerts'); + const Generator = require('util/password-generator'); + const GeneratorPresets = require('comp/generator-presets'); + + const Version = '1.8.4.2'; + const DebugMode = true; + + const keys = {}; + const logger = new Logger('keewebhttp'); + + startServer(); + + function startServer() { if (uninstalled) { - server.close(); return; } - logger.debug(`Server running at http://${hostname}:${port}/`); - }); - server.on('connection', function(conn) { - const key = conn.remoteAddress + ':' + conn.remotePort; - server.conn[key] = conn; - conn.on('close', () => { - if (server) { - delete server.conn[key]; - } - }); - }); - server.conn = {}; -} - -class RequestContext { - constructor(postData) { - this.postData = postData; - } - - handle() { - let result; - try { - this.req = JSON.parse(this.postData); - const response = this.execute() || this.resp; - if (response instanceof Promise) { - result = response.catch(e => { - return this.makeError(e); + 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(); + if (DebugMode) { + logger.debug('< ' + postData); + } + new RequestContext(postData) + .handle() + .then(response => { + if (DebugMode) { + logger.debug('> ' + response); + } + res.statusCode = 200; + res.setHeader('Content-Type', 'application/json'); + res.end(response); + }); }); } else { - result = Promise.resolve(response); + res.statusCode = 200; + res.setHeader('Content-Type', 'text/plain'); + res.end('Hey dude, you should POST here!'); } - } catch (e) { - result = Promise.resolve(this.makeError(e)); - } - return result.then(res => JSON.stringify(res)); - } - - execute() { - switch (this.req.RequestType) { - case 'test-associate': - return this.testAssociate(); - case 'associate': - return this.associate(); - case 'get-logins': - return this.getLogins({}); - case 'get-logins-count': - return this.getLogins({ onlyCount: true }); - case 'get-all-logins': - return this.getLogins({ all: true }); - case 'set-login': - return this.setLogin(); - case 'generate-password': - return this.generatePassword(); - default: - throw 'Not implemented'; - } - } - - makeError(e) { - logger.error('handleRequest error', e); - return { - Error: e ? e.toString() : '', - Success: false, - RequestType: this.req ? this.req.RequestType : '', - Version: Version - }; - } - - decrypt(value) { - if (!this.aesKey) { - throw 'No key'; - } - if (!this.req.Nonce) { - throw 'No nonce'; - } - const key = Buffer.from(this.aesKey, 'base64'); - const nonce = Buffer.from(this.req.Nonce, 'base64'); - const decipher = crypto.createDecipheriv('aes-256-cbc', key, nonce); - return Buffer.concat([decipher.update(value, 'base64'), decipher.final()]).toString(); - } - - encrypt(value) { - if (!this.aesKey) { - throw 'No key'; - } - if (!this.resp || !this.resp.Nonce) { - throw 'No nonce'; - } - const key = Buffer.from(this.aesKey, 'base64'); - const nonce = Buffer.from(this.resp.Nonce, 'base64'); - const cipher = crypto.createCipheriv('aes-256-cbc', key, nonce); - return Buffer.concat([cipher.update(value, 'utf8'), cipher.final()]).toString('base64'); - } - - getKeyById() { - return keys[this.req.Id]; - } - - saveKeyWithId() { - keys[this.req.Id] = this.req.Key; - // TODO - } - - verifyRequest() { - if (!this.req.Verifier) { - throw 'No verifier'; - } - if (!this.aesKey) { - this.aesKey = this.getKeyById(); - } - const decrypted = this.decrypt(this.req.Verifier); - if (decrypted !== this.req.Nonce) { - throw 'Bad signature'; - } - } - - createResponse() { - const resp = { - Success: true, - Nonce: '', - Verifier: '', - Version: Version, - RequestType: this.req.RequestType - }; - if (this.req.Id && keys[this.req.Id]) { - const key = Buffer.from(keys[this.req.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 = this.req.Id; - resp.Nonce = nonce.toString('base64'); - resp.Verifier = encrypted; - } - this.resp = resp; - } - - testAssociate() { - if (!this.req.Id) { - return this.makeError(''); - } - this.verifyRequest(); - this.createResponse(); - } - - associate() { - if (this.req.Id) { - throw 'Id not expected'; - } - if (!this.req.Key) { - throw 'No key'; - } - this.aesKey = this.req.Key; - this.verifyRequest(); - electron.remote.app.getMainWindow().focus(); - return new Promise((resolve, reject) => { - Alerts.yesno({ - header: 'External Connection', - body: 'Some app 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 by user'); } - }); - }).then(() => { - this.req.Id = 'KeeWeb_' + new Date().toISOString() + '_' + crypto.randomBytes(16).toString('hex'); - this.saveKeyWithId(); - this.createResponse(); - return this.resp; }); + 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}/`); + }); + server.on('connection', function (conn) { + const key = conn.remoteAddress + ':' + conn.remotePort; + server.conn[key] = conn; + conn.on('close', () => { + if (server) { + delete server.conn[key]; + } + }); + }); + server.conn = {}; } - getLogins(config) { - this.verifyRequest(); - if (!this.req.Url) { - throw 'No url'; + class RequestContext { + constructor(postData) { + this.postData = postData; } - const url = this.decrypt(this.req.Url); - logger.debug('get-logins', url); - this.createResponse(); - const filter = new AutoTypeFilter({ url }, AutoType.appModel); - const entries = filter.getEntries(); - this.resp.Count = entries.length; - if (!config.onlyCount) { - this.resp.Entries = entries.map(entry => ({ - Login: entry.user ? this.encrypt(entry.user) : '', - Name: entry.title ? this.encrypt(entry.title) : '', - Password: entry.password ? this.encrypt(entry.password.getText()) : '', + + handle() { + let result; + try { + this.req = JSON.parse(this.postData); + const response = this.execute() || this.resp; + if (response instanceof Promise) { + result = response.catch(e => { + return this.makeError(e); + }); + } else { + result = Promise.resolve(response); + } + } catch (e) { + result = Promise.resolve(this.makeError(e)); + } + return result.then(res => JSON.stringify(res)); + } + + execute() { + switch (this.req.RequestType) { + case 'test-associate': + return this.testAssociate(); + case 'associate': + return this.associate(); + case 'get-logins': + return this.getLogins({}); + case 'get-logins-count': + return this.getLogins({onlyCount: true}); + case 'get-all-logins': + return this.getLogins({all: true}); + case 'set-login': + return this.setLogin(); + case 'generate-password': + return this.generatePassword(); + default: + throw 'Not implemented'; + } + } + + makeError(e, skipLog) { + if (!skipLog) { + logger.error('handleRequest error', e); + } + return { + Error: e ? e.toString() : '', + Success: false, + RequestType: this.req ? this.req.RequestType : '', + Version: Version + }; + } + + decrypt(value) { + if (!this.aesKey) { + throw 'No key'; + } + if (!this.req.Nonce) { + throw 'No nonce'; + } + const key = Buffer.from(this.aesKey, 'base64'); + const nonce = Buffer.from(this.req.Nonce, 'base64'); + const decipher = crypto.createDecipheriv('aes-256-cbc', key, nonce); + return Buffer.concat([decipher.update(value, 'base64'), decipher.final()]).toString(); + } + + encrypt(value) { + if (!this.aesKey) { + throw 'No aes key'; + } + if (!this.resp || !this.resp.Nonce) { + throw 'No nonce'; + } + const key = Buffer.from(this.aesKey, 'base64'); + const nonce = Buffer.from(this.resp.Nonce, 'base64'); + const cipher = crypto.createCipheriv('aes-256-cbc', key, nonce); + let data; + if (value.isProtected) { + const binaryData = value.getBinary(); + data = Buffer.from(binaryData); + binaryData.fill(0); + } else { + data = Buffer.from(value, 'utf8'); + } + const encrypted = Buffer.concat([cipher.update(data), cipher.final()]).toString('base64'); + data.fill(0); + return encrypted; + } + + getKeyById() { + // TODO + return keys[this.req.Id]; + } + + saveKeyWithId() { + keys[this.req.Id] = this.req.Key; + // TODO + } + + verifyRequest() { + if (!this.req.Verifier) { + throw 'No verifier'; + } + if (!this.aesKey) { + this.aesKey = this.getKeyById(); + } + const decrypted = this.decrypt(this.req.Verifier); + if (decrypted !== this.req.Nonce) { + throw 'Bad signature'; + } + } + + createResponse() { + const resp = { + Success: true, + Nonce: '', + Verifier: '', + Version: Version, + RequestType: this.req.RequestType + }; + if (this.req.Id && keys[this.req.Id]) { + const key = Buffer.from(keys[this.req.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 = this.req.Id; + resp.Nonce = nonce.toString('base64'); + resp.Verifier = encrypted; + } + this.resp = resp; + } + + testAssociate() { + if (!this.req.Id) { + return this.makeError('', true); + } + if (!this.getKeyById(this.req.Id)) { + return this.makeError('Unknown Id', true); + } + this.verifyRequest(); + this.createResponse(); + } + + associate() { + if (this.req.Id) { + throw 'Id not expected'; + } + if (!this.req.Key) { + throw 'No request key'; + } + this.aesKey = this.req.Key; + this.verifyRequest(); + electron.remote.app.getMainWindow().focus(); + return new Promise((resolve, reject) => { + Alerts.yesno({ + header: 'External Connection', + body: 'Some app 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 by user'); + } + }); + }).then(() => { + this.req.Id = 'KeeWeb_' + new Date().toISOString() + '_' + crypto.randomBytes(16).toString('hex'); + logger.info('associate: ', this.req.Id); + this.saveKeyWithId(); + this.createResponse(); + return this.resp; + }); + } + + getLogins(config) { + this.verifyRequest(); + if (!this.req.Url && !config.all) { + throw 'No url'; + } + const url = this.req.Url ? this.decrypt(this.req.Url) : ''; + this.createResponse(); + const filter = new AutoTypeFilter({url}, AppModel.instance); + if (config.all) { + filter.ignoreWindowInfo = true; + } + const entries = filter.getEntries(); + this.resp.Count = entries.length; + logger.info(`getLogins(${url}): ${this.resp.Count}`); + if (!config.onlyCount) { + this.resp.Entries = entries.map(entry => ({ + Login: entry.user ? this.encrypt(entry.user) : '', + Name: entry.title ? this.encrypt(entry.title) : '', + Password: entry.password ? this.encrypt(entry.password) : '', + StringFields: null, + Uuid: this.encrypt(entry.id) + })); + } + } + + setLogin() { + this.verifyRequest(); + if (!this.req.Url || !this.req.Login || !this.req.Password) { + throw 'Invalid request'; + } + const url = this.decrypt(this.req.Url); + const login = this.decrypt(this.req.Login); + const password = this.decrypt(this.req.Password); + logger.info('setLogin', url, login, password); + this.createResponse(); + } + + generatePassword() { + this.verifyRequest(); + this.createResponse(); + const preset = GeneratorPresets.all.filter(p => p.default)[0] || GeneratorPresets.defaultPreset; + const password = Generator.generate(preset); + this.resp.Count = 1; + this.resp.Entries = [{ + Login: '', + Name: '', + Password: this.encrypt(password), StringFields: null, - Uuid: this.encrypt(entry.id) - })); + Uuid: '' + }]; } } - - setLogin() { - this.verifyRequest(); - if (!this.req.Url || !this.req.Login || !this.req.Password) { - throw 'Invalid request'; - } - const url = this.decrypt(this.req.Url); - const login = this.decrypt(this.req.Login); - const password = this.decrypt(this.req.Password); - logger.debug('set-login', url, login, password); - this.createResponse(); - } - - generatePassword() { - this.verifyRequest(); - this.createResponse(); - this.resp.Count = 1; - this.resp.Entries = [{ - Login: '', - Name: '', - Password: this.encrypt('I am generated password: ' + new Date()), - StringFields: null, - Uuid: '' - }]; - } } module.exports.uninstall = function() {