let uninstall; let restart; let serverPort = 19455; const timeout = setTimeout(run, 500); function run() { const nodeRequire = window.require; const http = nodeRequire('http'); const crypto = nodeRequire('crypto'); const electron = nodeRequire('electron'); const kdbxweb = require('kdbxweb'); const Events = require('framework/events').Events; const AppModel = require('models/app-model').AppModel; const EntryModel = require('models/entry-model').EntryModel; const GroupModel = require('models/group-model').GroupModel; const AutoTypeFilter = require('auto-type/auto-type-filter').AutoTypeFilter; const Logger = require('util/logger').Logger; const Alerts = require('comp/ui/alerts').Alerts; const PasswordGenerator = require('util/generators/password-generator').PasswordGenerator; const GeneratorPresets = require('comp/app/generator-presets').GeneratorPresets; const Version = '1.8.4.2'; const DebugMode = localStorage.keewebhttpDebug; const FileReadTimeout = 500; const EntryTitle = 'KeePassHttp Settings'; const EntryFieldPrefix = 'AES Key: '; const EntryUuid = 'NGl6QIpbQcCfNol9Yj7LMQ=='; const CreatePasswordsGroupTitle = 'KeePassHttp Passwords'; const keys = {}; const addedKeys = {}; const logger = new Logger('keewebhttp'); let uninstalled; let server; uninstall = function () { uninstalled = true; removeEventListeners(); stopServer(); }; restart = function () { stopServer(); startServer(); }; addEventListeners(); startServer(); readAllKeys(); function addEventListeners() { AppModel.instance.files.on('add', fileOpened); } function removeEventListeners() { AppModel.instance.files.off('add', fileOpened); } function startServer() { if (uninstalled) { return; } server = http.createServer((req, res) => { const origin = req.headers.origin; const userAgent = req.headers['user-agent'] || ''; const referer = req.headers.referrer || req.headers.referer; let originIsValid; if (origin) { if ( origin.startsWith('chrome-extension://') || origin.startsWith('safari-extension://') || // Firefox sends 'null' here, see https://github.com/keeweb/keeweb-plugins/pull/26 (userAgent.includes('Firefox') && origin === 'null') ) { originIsValid = true; } } else { originIsValid = true; } const isAllowed = req.method === 'POST' && !referer && originIsValid; if (!isAllowed) { if (DebugMode) { logger.debug('Request dropped', req.method, req.url, req.headers); } req.client.destroy(); res.end(); return; } 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); }); }); } }); const port = serverPort; 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', (conn) => { const key = conn.remoteAddress + ':' + conn.remotePort; server.conn[key] = conn; conn.on('close', () => { if (server) { delete server.conn[key]; } }); }); server.conn = {}; } function stopServer() { if (server) { server.close(); for (const key of Object.keys(server.conn)) { server.conn[key].destroy(); } server = null; } } function fileOpened(file) { setTimeout(() => { readKeys(file); writeAddedKeys(file); }, FileReadTimeout); } function readAllKeys() { AppModel.instance.files.forEach((file) => readKeys(file)); } function readKeys(file) { if (uninstalled) { return; } const entry = getSettingsEntry(file); if (!entry) { return; } for (const field of Object.keys(entry.fields)) { if (field.startsWith(EntryFieldPrefix)) { const key = field.replace(EntryFieldPrefix, ''); let value = entry.fields[field]; if (value && value.isProtected) { value = value.getText(); } if (key && value && !keys[key]) { keys[key] = value; } } } } function writeAddedKeysToAllFiles() { AppModel.instance.files.forEach((file) => { writeAddedKeys(file); }); } function writeAddedKeys(file) { if (uninstalled || !Object.keys(addedKeys).length) { return; } let settingsEntry = getSettingsEntry(file); if (!settingsEntry) { settingsEntry = EntryModel.newEntry(file.groups[0], file); settingsEntry.entry.uuid = new kdbxweb.KdbxUuid(EntryUuid); settingsEntry.setField('Title', EntryTitle); } for (const key of Object.keys(addedKeys)) { const keyFieldName = EntryFieldPrefix + key; const value = addedKeys[key]; let oldValue = settingsEntry.fields[keyFieldName]; if (oldValue && oldValue.isProtected) { oldValue = oldValue.getText(); } if (oldValue !== value) { settingsEntry.setField(keyFieldName, kdbxweb.ProtectedValue.fromString(value)); } } file.reload(); Events.emit('refresh'); } function getSettingsEntry(file) { return file.getEntry(file.subId(EntryUuid)); } 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); }); } 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) { const requestType = (this.req && this.req.RequestType) || ''; if (!skipLog) { logger.error('handleRequest error', requestType, e); } return { Error: e ? e.toString() : '', Success: false, RequestType: 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() { return keys[this.req.Id]; } saveKeyWithId() { keys[this.req.Id] = this.req.Key; addedKeys[this.req.Id] = this.req.Key; writeAddedKeysToAllFiles(); } 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({ icon: 'plug', header: 'External Connection', body: 'Some app is trying to manage passwords in 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(); 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'; } if (!AppModel.instance.files.hasOpenFiles()) { if (this.req.TriggerUnlock === true || this.req.TriggerUnlock === 'true') { electron.remote.app.getMainWindow().focus(); } return this.makeError('Locked', true); } 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) => { let customFields = null; for (const field of Object.keys(entry.fields)) { if (!customFields) { customFields = []; } const fieldKey = this.encrypt(field); const fieldValue = this.encrypt(entry.fields[field]); customFields.push({ Key: fieldKey, Value: fieldValue }); } return { Login: entry.user ? this.encrypt(entry.user) : '', Name: entry.title ? this.encrypt(entry.title) : '', Password: entry.password ? this.encrypt(entry.password) : '', StringFields: customFields, Uuid: this.encrypt(entry.entry.uuid.id) }; }); } } setLogin() { this.verifyRequest(); if (!this.req.Url || !this.req.Login || !this.req.Password) { throw 'Invalid request'; } const uuid = this.req.Uuid ? this.decrypt(this.req.Uuid) : null; const url = this.decrypt(this.req.Url); const login = this.decrypt(this.req.Login); const password = this.decrypt(this.req.Password); if (uuid) { let result = 'not found'; AppModel.instance.files.forEach((file) => { const entry = file.getEntry(file.subId(uuid)); if (entry) { if (entry.user !== login) { entry.setField('UserName', login); } if (!entry.password.equals(password)) { entry.setField('Password', kdbxweb.ProtectedValue.fromString(password)); } } result = 'updated'; }); logger.info(`setLogin(${url}, ${login}, ${password.length}): ${result}`); } else { logger.info(`setLogin(${url}, ${login}, ${password.length}): inserted`); let group, file; AppModel.instance.files.forEach((f) => { f.forEachGroup((g) => { if (g.title === CreatePasswordsGroupTitle) { group = g; file = f; } }); }); if (!group) { file = AppModel.instance.files[0]; group = GroupModel.newGroup(file.groups[0], file); group.setName(CreatePasswordsGroupTitle); } const entry = EntryModel.newEntry(group, file); const domain = url.match( /^(?:\w+:\/\/)?(?:(?:www|wwws|secure)\.)?([^\/]+)\/?(?:.*)/ ); const title = (domain && domain[1]) || 'Saved Password'; entry.setField('Title', title); entry.setField('URL', url); entry.setField('UserName', login); entry.setField('Password', kdbxweb.ProtectedValue.fromString(password)); } Events.emit('refresh'); this.createResponse(); } generatePassword() { this.verifyRequest(); this.createResponse(); const preset = GeneratorPresets.all.filter((p) => p.default)[0] || GeneratorPresets.defaultPreset; const password = PasswordGenerator.generate(preset); const bits = Buffer.from(password, 'utf8').byteLength * 8; this.resp.Count = 1; this.resp.Entries = [ { Login: this.encrypt(bits.toString()), Name: '', Password: this.encrypt(password), StringFields: null, Uuid: '' } ]; } } } module.exports.getSettings = function () { return [ { name: 'ServerPort', label: 'Port to listen to (do not change this setting without a special need to do so)', type: 'text', maxlength: 5, placeholder: '19455', value: '19455' } ]; }; module.exports.setSettings = function (changes) { if (changes.ServerPort) { const port = +changes.ServerPort; if (port > 1024 && port < 65535) { serverPort = port; if (restart) { restart(); } } } }; module.exports.uninstall = function () { if (uninstall) { uninstall(); } else { clearTimeout(timeout); } };