keewebhttp: refactoring

This commit is contained in:
antelle 2017-05-23 00:56:52 +02:00
parent 969a6f83e0
commit ef1312bd24
2 changed files with 202 additions and 174 deletions

View File

@ -1,5 +1,5 @@
{ {
"version": "0.0.2", "version": "0.0.1",
"manifestVersion": "0.1.0", "manifestVersion": "0.1.0",
"name": "keewebhttp", "name": "keewebhttp",
"description": "KeeWebHttp allows to use browser extensions with KeeWeb", "description": "KeeWebHttp allows to use browser extensions with KeeWeb",
@ -11,7 +11,7 @@
"licence": "MIT", "licence": "MIT",
"url": "https://plugins.keeweb.info/plugins/keewebhttp", "url": "https://plugins.keeweb.info/plugins/keewebhttp",
"resources": { "resources": {
"js": "t09aSrObT2zZUcu1VyVkVW/w/dlOS3Cw/XZJg9pMf4J7ufbdJDDdNEqguu3sZKJUt6l9cm1AbZc8IUCnQKLR9X0U9y7MkH0l2HwaG0Cs8kpqSeQtuDfFJsMGHFxidn6Hb322xFhQrVH2uIr/5LOKsboPuQ0MI7TbzbOE22WL+E2OYJ7l8Z8U3Q1B1emvXVeZUDQeGUs2And9i4Dh1OwflsUX/SogMwgG8HIPJlcgsboT4wu1w77XexF0+mc3TZKGTWo2cEawCdx9QbhNtWv9MhCFdDUioS8hgfDgkmWlz0PHQuF4fWoq1gcB3jZjq4oBzj7Uf7dc+jJf2+YITptYWw==" "js": "dWXmIa4n78RRR6tiMNIqnejfj54lOsNJrn6mz1eQ3HYpCu4sVKv7E+9ABSGykyHJJVQDjTJAUF+7UcwCfDhIFfbFDttPmeylM3vtg+YAfaJFfU5e0l9/MtuUEuXuiUwjXi3jFg5yAYNq9ZCkHs2YCm4JyE8KXm1flmzIpmTiVi6TkX/ulmO74lLm2wXUD23eP7B8RqXk0wBoc7VcZ1x5uHomJIrQfS+J7vdXrMLBt/EJU4VNlyw8LLxcwP3/UbsBzBotpif3AH0hq+Dcwuh8v8H9Z+756T8EFEXz0JmCLhU0oyFcgOmUt0QwrIiFD18aHt0s80BmuqO600I/3M3+Sg=="
}, },
"publicKey": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0oZB2Kt7AzRFNqf8FuO3C3kepHPAIQYiDPYdQxHcsiaFCwyKVx6K1cE/3vBhb8/2rj+QIIWNfAAuu1Y+2VK90ZBeq6HciukWzQRO/HWhfdy0c7JwDAslmyGI5olj0ZQkNLhkde1MiMxjDPpRhZtdJaryVO5cFJaJESpv3dV6m0qXsaQCluWYOSNfSjP9C8o2zRVjSi3ZQZnZIV5pnk9K2MtlZIPXrN9iJiM5zZ9DTSnqApI6dC9mX4R3LvGN+GTovm9C8Crl+qb106nGRR3LcweicDnPyMtZLa/E0DBpWYxUVLDp6WeLhxoUBr+6+t3Xp9IDnPoANDQXJXD0f1vQxQIDAQAB", "publicKey": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0oZB2Kt7AzRFNqf8FuO3C3kepHPAIQYiDPYdQxHcsiaFCwyKVx6K1cE/3vBhb8/2rj+QIIWNfAAuu1Y+2VK90ZBeq6HciukWzQRO/HWhfdy0c7JwDAslmyGI5olj0ZQkNLhkde1MiMxjDPpRhZtdJaryVO5cFJaJESpv3dV6m0qXsaQCluWYOSNfSjP9C8o2zRVjSi3ZQZnZIV5pnk9K2MtlZIPXrN9iJiM5zZ9DTSnqApI6dC9mX4R3LvGN+GTovm9C8Crl+qb106nGRR3LcweicDnPyMtZLa/E0DBpWYxUVLDp6WeLhxoUBr+6+t3Xp9IDnPoANDQXJXD0f1vQxQIDAQAB",
"desktop": true "desktop": true

View File

@ -14,7 +14,7 @@ const Alerts = require('comp/alerts');
// const appModel = ...; TODO: use AppModel.instance // const appModel = ...; TODO: use AppModel.instance
const Version = '1.8.4.2'; const Version = '1.8.4.2';
const SignatureError = 'Request signature missing'; const DebugMode = true;
const keys = {}; const keys = {};
@ -35,18 +35,24 @@ function init() {
req.on('data', data => body.push(data)); req.on('data', data => body.push(data));
req.on('end', () => { req.on('end', () => {
const postData = Buffer.concat(body).toString(); const postData = Buffer.concat(body).toString();
logger.debug('<', postData); if (DebugMode) {
handleRequest(postData).then(result => { logger.debug('< ' + postData);
logger.debug('>', JSON.stringify(result)); }
new RequestContext(postData)
.handle()
.then(response => {
if (DebugMode) {
logger.debug('> ' + response);
}
res.statusCode = 200; res.statusCode = 200;
res.setHeader('Content-Type', 'application/json'); res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(result)); res.end(response);
}); });
}); });
} else { } else {
res.statusCode = 200; res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain'); res.setHeader('Content-Type', 'text/plain');
res.end('Nice to meet you! But you should POST here.'); res.end('Hey dude, you should POST here!');
} }
}); });
const port = 19455; const port = 19455;
@ -70,185 +76,207 @@ function init() {
server.conn = {}; server.conn = {};
} }
function handleRequest(req) { class RequestContext {
constructor(postData) {
this.postData = postData;
}
handle() {
let result;
try { try {
req = JSON.parse(req); this.req = JSON.parse(this.postData);
const response = executeRequest(req); const response = this.execute() || this.resp;
if (response instanceof Promise) { if (response instanceof Promise) {
return response.catch(e => { result = response.catch(e => {
return returnError(req, e); return this.makeError(e);
}); });
} else { } else {
return Promise.resolve(response); result = Promise.resolve(response);
} }
} catch (e) { } catch (e) {
return returnError(req, e); result = Promise.resolve(this.makeError(e));
} }
return result.then(res => JSON.stringify(res));
} }
function returnError(req, e) { execute() {
if (e !== SignatureError) { switch (this.req.RequestType) {
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': case 'test-associate':
return testAssociate(req); return this.testAssociate();
case 'associate': case 'associate':
return associate(req); return this.associate();
case 'get-logins': case 'get-logins':
return getLogins(req, {}); return this.getLogins({});
case 'get-logins-count': case 'get-logins-count':
return getLogins(req, { onlyCount: true }); return this.getLogins({ onlyCount: true });
case 'get-all-logins': case 'get-all-logins':
return getLogins(req, { all: true }); return this.getLogins({ all: true });
case 'set-login': case 'set-login':
return setLogin(req); return this.setLogin();
case 'generate-password': case 'generate-password':
return generatePassword(req); return this.generatePassword();
default: default:
throw 'Not implemented'; throw 'Not implemented';
} }
} }
function decrypt(req, value) { makeError(e) {
const reqKey = keys[req.Id] || req.Key; logger.error('handleRequest error', e);
if (!reqKey || !req.Nonce || !req.Verifier) { return {
throw SignatureError; Error: e ? e.toString() : '',
Success: false,
RequestType: this.req ? this.req.RequestType : '',
Version: Version
};
} }
const key = Buffer.from(reqKey, 'base64');
const nonce = Buffer.from(req.Nonce, 'base64');
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); const decipher = crypto.createDecipheriv('aes-256-cbc', key, nonce);
return Buffer.concat([decipher.update(value, 'base64'), decipher.final()]).toString(); return Buffer.concat([decipher.update(value, 'base64'), decipher.final()]).toString();
} }
function encrypt(resp, value) { encrypt(value) {
const key = Buffer.from(keys[resp.Id], 'base64'); if (!this.aesKey) {
const nonce = Buffer.from(resp.Nonce, 'base64'); 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); const cipher = crypto.createCipheriv('aes-256-cbc', key, nonce);
return Buffer.concat([cipher.update(value, 'utf8'), cipher.final()]).toString('base64'); return Buffer.concat([cipher.update(value, 'utf8'), cipher.final()]).toString('base64');
} }
function verifyRequest(req) { getKeyById() {
if (req.Id && !keys[req.Id]) { return keys[this.req.Id];
// TODO: get key
} }
const decrypted = decrypt(req, req.Verifier);
if (decrypted !== req.Nonce) { saveKeyWithId() {
throw 'Invalid signature'; 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';
} }
} }
function wrapResponse(resp, id) { createResponse() {
resp = Object.assign({ const resp = {
Success: true, Success: true,
Nonce: '', Nonce: '',
Verifier: '', Verifier: '',
Version: Version Version: Version,
}, resp); RequestType: this.req.RequestType
if (id && keys[id]) { };
const key = Buffer.from(keys[id], 'base64'); if (this.req.Id && keys[this.req.Id]) {
const key = Buffer.from(keys[this.req.Id], 'base64');
const nonce = crypto.randomBytes(16); const nonce = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-cbc', key, nonce); const cipher = crypto.createCipheriv('aes-256-cbc', key, nonce);
const encrypted = Buffer.concat([cipher.update(nonce.toString('base64'), 'utf8'), cipher.final()]).toString('base64'); const encrypted = Buffer.concat([cipher.update(nonce.toString('base64'), 'utf8'), cipher.final()]).toString('base64');
resp.Id = id; resp.Id = this.req.Id;
resp.Nonce = nonce.toString('base64'); resp.Nonce = nonce.toString('base64');
resp.Verifier = encrypted; resp.Verifier = encrypted;
} }
return resp; this.resp = resp;
} }
function testAssociate(req) { testAssociate() {
verifyRequest(req); if (!this.req.Id) {
return wrapResponse({ return this.makeError('');
RequestType: req.RequestType, }
TriggerUnlock: req.TriggerUnlock this.verifyRequest();
}, req.Id); this.createResponse();
} }
function associate(req) { associate() {
verifyRequest(req); 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(); electron.remote.app.getMainWindow().focus();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
Alerts.yesno({ Alerts.yesno({
header: 'Plugin Connecting', header: 'External Connection',
body: 'A plugin is trying to connect to KeeWeb. If you are setting up your plugin, please allow the connection. ' + body: 'Some app is trying to connect to KeeWeb. If you are setting up your plugin, please allow the connection. Otherwise, click No.',
'Otherwise, click No.',
success: () => { resolve(); }, success: () => { resolve(); },
cancel: () => { reject('Rejected'); } cancel: () => { reject('Rejected by user'); }
}); });
}).then(() => { }).then(() => {
const id = 'KeeWeb_' + new Date().toISOString() + '_' + crypto.randomBytes(16).toString('hex'); this.req.Id = 'KeeWeb_' + new Date().toISOString() + '_' + crypto.randomBytes(16).toString('hex');
keys[id] = req.Key; this.saveKeyWithId();
fs.writeFileSync(path.join(__dirname, 'keys.json'), JSON.stringify(keys)); this.createResponse();
return wrapResponse({ return this.resp;
RequestType: req.RequestType
}, id);
}); });
} }
function getLogins(req, config) { getLogins(config) {
verifyRequest(req); this.verifyRequest();
if (!req.Url) { if (!this.req.Url) {
throw 'Invalid request'; throw 'No url';
} }
const url = decrypt(req, req.Url); const url = this.decrypt(this.req.Url);
logger.debug('get-logins', url); logger.debug('get-logins', url);
const response = wrapResponse({ this.createResponse();
RequestType: req.RequestType
}, req.Id);
const filter = new AutoTypeFilter({ url }, AutoType.appModel); const filter = new AutoTypeFilter({ url }, AutoType.appModel);
const entries = filter.getEntries(); const entries = filter.getEntries();
response.Count = entries.length; this.resp.Count = entries.length;
if (!config.onlyCount) { if (!config.onlyCount) {
response.Entries = entries.map(entry => ({ this.resp.Entries = entries.map(entry => ({
Login: entry.user ? encrypt(response, entry.user) : '', Login: entry.user ? this.encrypt(entry.user) : '',
Name: entry.title ? encrypt(response, entry.title) : '', Name: entry.title ? this.encrypt(entry.title) : '',
Password: entry.password ? encrypt(response, entry.password.getText()) : '', Password: entry.password ? this.encrypt(entry.password.getText()) : '',
StringFields: null, StringFields: null,
Uuid: encrypt(response, entry.id) Uuid: this.encrypt(entry.id)
})); }));
} }
return response;
} }
function setLogin(req) { setLogin() {
verifyRequest(req); this.verifyRequest();
if (!req.Url || !req.Login || !req.Password) { if (!this.req.Url || !this.req.Login || !this.req.Password) {
throw 'Invalid request'; throw 'Invalid request';
} }
const url = decrypt(req, req.Url); const url = this.decrypt(this.req.Url);
const login = decrypt(req, req.Login); const login = this.decrypt(this.req.Login);
const password = decrypt(req, req.Password); const password = this.decrypt(this.req.Password);
logger.debug('set-login', url, login, password); logger.debug('set-login', url, login, password);
return wrapResponse({ this.createResponse();
RequestType: req.RequestType
}, req.Id);
} }
function generatePassword(req) { generatePassword() {
verifyRequest(req); this.verifyRequest();
const response = wrapResponse({ this.createResponse();
RequestType: req.RequestType this.resp.Count = 1;
}, req.Id); this.resp.Entries = [{
response.Count = 1;
response.Entries = [{
Login: '', Login: '',
Name: '', Name: '',
Password: encrypt(response, 'I am generated password: ' + new Date()), Password: this.encrypt('I am generated password: ' + new Date()),
StringFields: null, StringFields: null,
Uuid: '' Uuid: ''
}]; }];
return response; }
} }
module.exports.uninstall = function() { module.exports.uninstall = function() {