experimental keewebhttp plugin

This commit is contained in:
antelle 2017-05-22 00:24:55 +02:00
parent 5b51a999ee
commit 27fd237acd
3 changed files with 268 additions and 0 deletions

1
.gitignore vendored
View File

@ -4,5 +4,6 @@ node_modules/
bower_components/
*.log
*.user
*.pem
keys
data/

View File

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

View File

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