2021-04-06 20:10:42 +02:00
|
|
|
import kdbxweb from 'kdbxweb';
|
|
|
|
import { box as tweetnaclBox } from 'tweetnacl';
|
2021-04-06 21:27:58 +02:00
|
|
|
import { Events } from 'framework/events';
|
2021-04-06 20:10:42 +02:00
|
|
|
import { RuntimeInfo } from 'const/runtime-info';
|
2021-04-10 23:35:42 +02:00
|
|
|
import { KnownAppVersions } from 'const/known-app-versions';
|
2021-04-06 20:10:42 +02:00
|
|
|
import { Launcher } from 'comp/launcher';
|
|
|
|
import { AppSettingsModel } from 'models/app-settings-model';
|
2021-04-08 17:28:41 +02:00
|
|
|
import { Alerts } from 'comp/ui/alerts';
|
2021-04-08 17:59:39 +02:00
|
|
|
import { PasswordGenerator } from 'util/generators/password-generator';
|
|
|
|
import { GeneratorPresets } from 'comp/app/generator-presets';
|
2021-04-11 10:17:02 +02:00
|
|
|
import { Logger } from 'util/logger';
|
2021-04-11 18:59:33 +02:00
|
|
|
import { Locale } from 'util/locale';
|
2021-04-11 10:17:02 +02:00
|
|
|
|
|
|
|
const logger = new Logger('browser-extension-connector');
|
2021-04-11 17:59:05 +02:00
|
|
|
if (!localStorage.debugBrowserExtension) {
|
|
|
|
logger.level = Logger.Level.Info;
|
|
|
|
}
|
2021-04-06 20:10:42 +02:00
|
|
|
|
2021-04-10 21:16:48 +02:00
|
|
|
let appModel;
|
2021-04-11 12:48:58 +02:00
|
|
|
const connectedClients = new Map();
|
2021-04-11 18:59:33 +02:00
|
|
|
const pendingBrowserMessages = [];
|
|
|
|
let processingBrowserMessage = false;
|
2021-04-11 17:22:58 +02:00
|
|
|
const MaxIncomingDataLength = 10_000;
|
|
|
|
const KeeWebAssociationId = 'KeeWeb';
|
|
|
|
const KeeWebHash = '398d9c782ec76ae9e9877c2321cbda2b31fc6d18ccf0fed5ca4bd746bab4d64a'; // sha256('KeeWeb')
|
2021-04-06 20:10:42 +02:00
|
|
|
|
|
|
|
function incrementNonce(nonce) {
|
|
|
|
// from libsodium/utils.c, like it is in KeePassXC
|
|
|
|
let i = 0;
|
|
|
|
let c = 1;
|
|
|
|
for (; i < nonce.length; ++i) {
|
|
|
|
c += nonce[i];
|
|
|
|
nonce[i] = c;
|
|
|
|
c >>= 8;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function getClient(request) {
|
|
|
|
if (!request.clientID) {
|
|
|
|
throw new Error('Empty clientID');
|
|
|
|
}
|
2021-04-11 12:48:58 +02:00
|
|
|
const client = connectedClients.get(request.clientID);
|
2021-04-06 20:10:42 +02:00
|
|
|
if (!client) {
|
|
|
|
throw new Error(`Client not connected: ${request.clientID}`);
|
|
|
|
}
|
|
|
|
return client;
|
|
|
|
}
|
|
|
|
|
|
|
|
function decryptRequest(request) {
|
|
|
|
const client = getClient(request);
|
|
|
|
|
|
|
|
if (!request.nonce) {
|
|
|
|
throw new Error('Empty nonce');
|
|
|
|
}
|
|
|
|
if (!request.message) {
|
|
|
|
throw new Error('Empty message');
|
|
|
|
}
|
|
|
|
|
|
|
|
const nonce = kdbxweb.ByteUtils.base64ToBytes(request.nonce);
|
|
|
|
const message = kdbxweb.ByteUtils.base64ToBytes(request.message);
|
|
|
|
|
|
|
|
const data = tweetnaclBox.open(message, nonce, client.publicKey, client.keys.secretKey);
|
|
|
|
|
|
|
|
const json = new TextDecoder().decode(data);
|
|
|
|
const payload = JSON.parse(json);
|
|
|
|
|
2021-04-11 17:59:05 +02:00
|
|
|
logger.debug('Extension -> KeeWeb -> (decrypted)', payload);
|
|
|
|
|
2021-04-08 17:36:42 +02:00
|
|
|
if (!payload) {
|
|
|
|
throw new Error('Empty request payload');
|
|
|
|
}
|
|
|
|
if (payload.action !== request.action) {
|
2021-04-06 20:10:42 +02:00
|
|
|
throw new Error(`Bad action in decrypted payload`);
|
|
|
|
}
|
|
|
|
|
|
|
|
return payload;
|
|
|
|
}
|
|
|
|
|
|
|
|
function encryptResponse(request, payload) {
|
2021-04-11 17:59:05 +02:00
|
|
|
logger.debug('KeeWeb -> Extension (decrypted)', payload);
|
|
|
|
|
2021-04-08 17:36:42 +02:00
|
|
|
const nonceBytes = kdbxweb.ByteUtils.base64ToBytes(request.nonce);
|
|
|
|
incrementNonce(nonceBytes);
|
|
|
|
const nonce = kdbxweb.ByteUtils.bytesToBase64(nonceBytes);
|
|
|
|
|
2021-04-06 20:10:42 +02:00
|
|
|
const client = getClient(request);
|
|
|
|
|
2021-04-08 17:36:42 +02:00
|
|
|
payload.nonce = nonce;
|
|
|
|
|
2021-04-06 20:10:42 +02:00
|
|
|
const json = JSON.stringify(payload);
|
|
|
|
const data = new TextEncoder().encode(json);
|
|
|
|
|
2021-04-08 17:36:42 +02:00
|
|
|
const encrypted = tweetnaclBox(data, nonceBytes, client.publicKey, client.keys.secretKey);
|
2021-04-06 20:10:42 +02:00
|
|
|
|
|
|
|
const message = kdbxweb.ByteUtils.bytesToBase64(encrypted);
|
|
|
|
|
|
|
|
return {
|
|
|
|
action: request.action,
|
|
|
|
message,
|
|
|
|
nonce
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-04-11 18:59:33 +02:00
|
|
|
function makeError(def) {
|
|
|
|
const e = new Error(def.message);
|
|
|
|
e.code = def.code;
|
|
|
|
return e;
|
|
|
|
}
|
|
|
|
|
2021-04-11 17:22:58 +02:00
|
|
|
function ensureAtLeastOneFileIsOpen() {
|
|
|
|
if (!appModel.files.hasOpenFiles()) {
|
2021-04-11 18:59:33 +02:00
|
|
|
throw makeError(Errors.noOpenFiles);
|
2021-04-11 17:22:58 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-11 18:59:33 +02:00
|
|
|
function checkContentRequestPermissions(request) {
|
|
|
|
ensureAtLeastOneFileIsOpen();
|
|
|
|
|
|
|
|
const client = getClient(request);
|
|
|
|
if (client.authorized) {
|
|
|
|
return;
|
2021-04-11 17:22:58 +02:00
|
|
|
}
|
2021-04-11 18:59:33 +02:00
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
if (Alerts.alertDisplayed) {
|
|
|
|
return reject(new Error(Locale.extensionErrorAlertDisplayed));
|
|
|
|
}
|
|
|
|
|
|
|
|
BrowserExtensionConnector.focusKeeWeb();
|
|
|
|
|
|
|
|
// TODO: make a proper dialog here instead of a simple question
|
|
|
|
|
|
|
|
if (Launcher) {
|
|
|
|
Alerts.yesno({
|
|
|
|
header: 'Extension connection',
|
|
|
|
body: 'Allow this extension to connect?',
|
|
|
|
success: () => {
|
|
|
|
resolve();
|
|
|
|
},
|
|
|
|
cancel: () => reject(makeError(Errors.userRejected))
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
// it's 'confirm' here because other browser extensions can't interact with browser alerts
|
|
|
|
// while they can easily press a button on our alert
|
|
|
|
// eslint-disable-next-line no-alert
|
|
|
|
const allowed = confirm('Allow this extension to connect?');
|
|
|
|
if (allowed) {
|
|
|
|
resolve();
|
|
|
|
} else {
|
|
|
|
reject(makeError(Errors.userRejected));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.then(() => {
|
|
|
|
client.authorized = true;
|
|
|
|
Launcher.hideApp();
|
|
|
|
})
|
|
|
|
.catch((e) => {
|
|
|
|
Launcher.hideApp();
|
|
|
|
throw e;
|
|
|
|
});
|
2021-04-11 17:22:58 +02:00
|
|
|
}
|
|
|
|
|
2021-04-10 23:35:42 +02:00
|
|
|
function getVersion(request) {
|
2021-04-10 23:36:21 +02:00
|
|
|
const extensionName = getClient(request).extensionName;
|
2021-04-10 23:35:42 +02:00
|
|
|
return extensionName ? RuntimeInfo.version : KnownAppVersions.KeePassXC;
|
|
|
|
}
|
|
|
|
|
|
|
|
function isKeeWebConnect(request) {
|
2021-04-10 23:36:21 +02:00
|
|
|
return getClient(request).extensionName === 'keeweb-connect';
|
2021-04-10 23:35:42 +02:00
|
|
|
}
|
|
|
|
|
2021-04-11 18:59:33 +02:00
|
|
|
const Errors = {
|
|
|
|
noOpenFiles: {
|
|
|
|
message: Locale.extensionErrorNoOpenFiles,
|
2021-04-11 19:14:34 +02:00
|
|
|
code: '1'
|
2021-04-11 18:59:33 +02:00
|
|
|
},
|
|
|
|
userRejected: {
|
|
|
|
message: Locale.extensionErrorUserRejected,
|
2021-04-11 19:14:34 +02:00
|
|
|
code: '6'
|
2021-04-11 18:59:33 +02:00
|
|
|
}
|
2021-04-11 15:04:04 +02:00
|
|
|
};
|
2021-04-11 14:53:56 +02:00
|
|
|
|
2021-04-06 20:10:42 +02:00
|
|
|
const ProtocolHandlers = {
|
|
|
|
'ping'({ data }) {
|
|
|
|
return { data };
|
|
|
|
},
|
|
|
|
|
2021-04-10 23:35:42 +02:00
|
|
|
'change-public-keys'(request) {
|
2021-04-11 12:48:58 +02:00
|
|
|
let { publicKey, extensionName, version, clientID: clientId } = request;
|
2021-04-10 23:35:42 +02:00
|
|
|
|
2021-04-11 12:48:58 +02:00
|
|
|
if (connectedClients.has(clientId)) {
|
2021-04-11 10:17:02 +02:00
|
|
|
throw new Error('Changing keys is not allowed');
|
|
|
|
}
|
|
|
|
|
2021-04-11 12:48:58 +02:00
|
|
|
if (!Launcher) {
|
|
|
|
// on web there can be only one connected client
|
|
|
|
connectedClients.clear();
|
|
|
|
}
|
|
|
|
|
2021-04-06 20:10:42 +02:00
|
|
|
const keys = tweetnaclBox.keyPair();
|
|
|
|
publicKey = kdbxweb.ByteUtils.base64ToBytes(publicKey);
|
|
|
|
|
2021-04-11 12:48:58 +02:00
|
|
|
connectedClients.set(clientId, { publicKey, extensionName, version, keys });
|
2021-04-06 20:10:42 +02:00
|
|
|
|
2021-04-11 12:48:58 +02:00
|
|
|
logger.info('New client key created', clientId, extensionName, version);
|
2021-04-11 10:17:02 +02:00
|
|
|
|
2021-04-11 14:53:56 +02:00
|
|
|
const nonceBytes = kdbxweb.ByteUtils.base64ToBytes(request.nonce);
|
|
|
|
incrementNonce(nonceBytes);
|
|
|
|
const nonce = kdbxweb.ByteUtils.bytesToBase64(nonceBytes);
|
|
|
|
|
2021-04-06 20:10:42 +02:00
|
|
|
return {
|
|
|
|
action: 'change-public-keys',
|
2021-04-10 23:35:42 +02:00
|
|
|
version: getVersion(request),
|
2021-04-06 20:10:42 +02:00
|
|
|
publicKey: kdbxweb.ByteUtils.bytesToBase64(keys.publicKey),
|
2021-04-11 14:53:56 +02:00
|
|
|
nonce,
|
2021-04-10 23:37:24 +02:00
|
|
|
success: 'true',
|
|
|
|
...(isKeeWebConnect(request) ? { appName: 'KeeWeb' } : undefined)
|
2021-04-06 20:10:42 +02:00
|
|
|
};
|
|
|
|
},
|
|
|
|
|
|
|
|
'get-databasehash'(request) {
|
|
|
|
decryptRequest(request);
|
2021-04-11 17:22:58 +02:00
|
|
|
ensureAtLeastOneFileIsOpen();
|
2021-04-07 21:08:22 +02:00
|
|
|
|
2021-04-11 17:22:58 +02:00
|
|
|
return encryptResponse(request, {
|
|
|
|
hash: KeeWebHash,
|
|
|
|
success: 'true',
|
|
|
|
version: getVersion(request)
|
|
|
|
});
|
2021-04-07 22:49:23 +02:00
|
|
|
},
|
|
|
|
|
2021-04-08 17:59:39 +02:00
|
|
|
'generate-password'(request) {
|
2021-04-08 18:10:08 +02:00
|
|
|
const password = PasswordGenerator.generate(GeneratorPresets.browserExtensionPreset);
|
2021-04-08 17:59:39 +02:00
|
|
|
|
|
|
|
return encryptResponse(request, {
|
2021-04-10 23:35:42 +02:00
|
|
|
version: getVersion(request),
|
2021-04-08 17:59:39 +02:00
|
|
|
success: 'true',
|
2021-04-11 14:53:56 +02:00
|
|
|
entries: [{ password }]
|
2021-04-08 17:59:39 +02:00
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2021-04-07 22:49:23 +02:00
|
|
|
'lock-database'(request) {
|
|
|
|
decryptRequest(request);
|
2021-04-11 17:22:58 +02:00
|
|
|
ensureAtLeastOneFileIsOpen();
|
2021-04-07 22:49:23 +02:00
|
|
|
|
2021-04-11 17:22:58 +02:00
|
|
|
Events.emit('lock-workspace');
|
2021-04-08 17:28:41 +02:00
|
|
|
|
2021-04-11 17:22:58 +02:00
|
|
|
if (Alerts.alertDisplayed) {
|
|
|
|
BrowserExtensionConnector.focusKeeWeb();
|
2021-04-11 14:53:56 +02:00
|
|
|
}
|
2021-04-11 17:22:58 +02:00
|
|
|
|
|
|
|
return encryptResponse(request, {
|
|
|
|
success: 'true',
|
|
|
|
version: getVersion(request)
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
'associate'(request) {
|
|
|
|
decryptRequest(request);
|
|
|
|
ensureAtLeastOneFileIsOpen();
|
|
|
|
|
|
|
|
return encryptResponse(request, {
|
|
|
|
success: 'true',
|
|
|
|
version: getVersion(request),
|
|
|
|
hash: KeeWebHash,
|
|
|
|
id: KeeWebAssociationId
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
'test-associate'(request) {
|
|
|
|
const payload = decryptRequest(request);
|
|
|
|
ensureAtLeastOneFileIsOpen();
|
2021-04-12 23:05:57 +02:00
|
|
|
|
|
|
|
if (payload.id !== KeeWebAssociationId) {
|
|
|
|
throw makeError(Errors.noOpenFiles);
|
|
|
|
}
|
2021-04-11 17:22:58 +02:00
|
|
|
|
|
|
|
return encryptResponse(request, {
|
|
|
|
success: 'true',
|
|
|
|
version: getVersion(request),
|
|
|
|
hash: KeeWebHash,
|
|
|
|
id: payload.id
|
|
|
|
});
|
2021-04-11 18:59:33 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
async 'get-logins'(request) {
|
|
|
|
decryptRequest(request);
|
|
|
|
await checkContentRequestPermissions(request);
|
|
|
|
|
|
|
|
throw new Error('Not implemented');
|
|
|
|
},
|
|
|
|
|
|
|
|
async 'get-totp'(request) {
|
|
|
|
decryptRequest(request);
|
|
|
|
await checkContentRequestPermissions(request);
|
|
|
|
|
|
|
|
throw new Error('Not implemented');
|
|
|
|
},
|
|
|
|
|
|
|
|
async 'set-login'(request) {
|
|
|
|
decryptRequest(request);
|
|
|
|
await checkContentRequestPermissions(request);
|
|
|
|
|
|
|
|
throw new Error('Not implemented');
|
|
|
|
},
|
|
|
|
|
|
|
|
async 'get-database-groups'(request) {
|
|
|
|
decryptRequest(request);
|
|
|
|
await checkContentRequestPermissions(request);
|
|
|
|
|
2021-04-12 23:05:57 +02:00
|
|
|
const makeGroups = (group) => {
|
|
|
|
const res = {
|
|
|
|
name: group.title,
|
|
|
|
uuid: kdbxweb.ByteUtils.bytesToHex(group.group.uuid.bytes),
|
|
|
|
children: []
|
|
|
|
};
|
|
|
|
for (const subGroup of group.items) {
|
|
|
|
if (subGroup.matches()) {
|
|
|
|
res.children.push(makeGroups(subGroup));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return res;
|
|
|
|
};
|
|
|
|
|
|
|
|
const groups = [];
|
|
|
|
for (const file of appModel.files.filter((f) => f.active)) {
|
|
|
|
for (const group of file.groups) {
|
|
|
|
groups.push(makeGroups(group));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return encryptResponse(request, {
|
|
|
|
success: 'true',
|
|
|
|
version: getVersion(request),
|
|
|
|
groups: { groups }
|
|
|
|
});
|
2021-04-11 18:59:33 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
async 'create-new-group'(request) {
|
|
|
|
decryptRequest(request);
|
|
|
|
await checkContentRequestPermissions(request);
|
|
|
|
|
|
|
|
throw new Error('Not implemented');
|
2021-04-06 20:10:42 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const BrowserExtensionConnector = {
|
2021-04-06 21:27:58 +02:00
|
|
|
enabled: false,
|
|
|
|
|
2021-04-10 21:16:48 +02:00
|
|
|
init(model) {
|
|
|
|
appModel = model;
|
|
|
|
|
2021-04-06 21:27:58 +02:00
|
|
|
this.browserWindowMessage = this.browserWindowMessage.bind(this);
|
|
|
|
this.fileOpened = this.fileOpened.bind(this);
|
|
|
|
this.oneFileClosed = this.oneFileClosed.bind(this);
|
|
|
|
this.allFilesClosed = this.allFilesClosed.bind(this);
|
|
|
|
|
2021-04-06 20:10:42 +02:00
|
|
|
AppSettingsModel.on('change:browserExtension', (model, enabled) => {
|
2021-04-06 21:27:58 +02:00
|
|
|
this.enabled = enabled;
|
2021-04-06 20:10:42 +02:00
|
|
|
if (enabled) {
|
|
|
|
this.start();
|
|
|
|
} else {
|
|
|
|
this.stop();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
if (AppSettingsModel.browserExtension) {
|
2021-04-06 21:27:58 +02:00
|
|
|
this.enabled = true;
|
2021-04-06 20:10:42 +02:00
|
|
|
this.start();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
start() {
|
2021-04-10 21:16:48 +02:00
|
|
|
if (Launcher) {
|
|
|
|
this.startDesktopAppListener();
|
|
|
|
} else {
|
2021-04-06 20:10:42 +02:00
|
|
|
this.startWebMessageListener();
|
|
|
|
}
|
2021-04-06 21:27:58 +02:00
|
|
|
Events.on('file-opened', this.fileOpened);
|
|
|
|
Events.on('one-file-closed', this.oneFileClosed);
|
|
|
|
Events.on('all-files-closed', this.allFilesClosed);
|
2021-04-11 10:17:02 +02:00
|
|
|
|
|
|
|
logger.info('Started');
|
2021-04-06 20:10:42 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
stop() {
|
2021-04-10 21:16:48 +02:00
|
|
|
if (Launcher) {
|
|
|
|
this.stopDesktopAppListener();
|
|
|
|
} else {
|
2021-04-06 20:10:42 +02:00
|
|
|
this.stopWebMessageListener();
|
|
|
|
}
|
2021-04-06 21:27:58 +02:00
|
|
|
Events.off('file-opened', this.fileOpened);
|
|
|
|
Events.off('one-file-closed', this.oneFileClosed);
|
|
|
|
Events.off('all-files-closed', this.allFilesClosed);
|
2021-04-11 10:17:02 +02:00
|
|
|
|
|
|
|
logger.info('Stopped');
|
2021-04-06 20:10:42 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
startWebMessageListener() {
|
|
|
|
window.addEventListener('message', this.browserWindowMessage);
|
|
|
|
},
|
|
|
|
|
|
|
|
stopWebMessageListener() {
|
|
|
|
window.removeEventListener('message', this.browserWindowMessage);
|
|
|
|
},
|
|
|
|
|
2021-04-10 21:16:48 +02:00
|
|
|
startDesktopAppListener() {
|
|
|
|
Launcher.closeOldBrowserExtensionSocket(() => {
|
|
|
|
const sockName = Launcher.getBrowserExtensionSocketName();
|
|
|
|
const { createServer } = Launcher.req('net');
|
|
|
|
this.connectedSockets = [];
|
|
|
|
this.connectedSocketState = new WeakMap();
|
|
|
|
this.server = createServer((socket) => {
|
2021-04-11 10:17:02 +02:00
|
|
|
logger.info('New connection');
|
2021-04-10 21:16:48 +02:00
|
|
|
this.connectedSockets.push(socket);
|
2021-04-10 21:32:29 +02:00
|
|
|
this.connectedSocketState.set(socket, {});
|
|
|
|
this.checkSocketIdentity(socket);
|
|
|
|
socket.on('data', (data) => this.onSocketData(socket, data));
|
|
|
|
socket.on('close', () => this.onSocketClose(socket));
|
2021-04-10 21:16:48 +02:00
|
|
|
});
|
|
|
|
this.server.listen(sockName);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
stopDesktopAppListener() {
|
|
|
|
for (const socket of this.connectedSockets) {
|
|
|
|
socket.destroy();
|
|
|
|
}
|
|
|
|
if (this.server) {
|
|
|
|
this.server.close();
|
|
|
|
}
|
|
|
|
this.connectedSockets = [];
|
|
|
|
this.connectedSocketState = new WeakMap();
|
|
|
|
},
|
|
|
|
|
2021-04-10 21:32:29 +02:00
|
|
|
checkSocketIdentity(socket) {
|
|
|
|
const state = this.connectedSocketState.get(socket);
|
|
|
|
if (!state) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: check the process
|
|
|
|
|
|
|
|
state.active = true;
|
|
|
|
this.processPendingSocketData(socket);
|
|
|
|
},
|
|
|
|
|
|
|
|
onSocketClose(socket) {
|
2021-04-11 12:48:58 +02:00
|
|
|
const state = this.connectedSocketState.get(socket);
|
|
|
|
if (state?.clientId) {
|
|
|
|
connectedClients.delete(state.clientId);
|
|
|
|
}
|
2021-04-10 21:32:29 +02:00
|
|
|
this.connectedSocketState.delete(socket);
|
2021-04-11 12:48:58 +02:00
|
|
|
|
|
|
|
this.connectedSockets = this.connectedSockets.filter((s) => s !== socket);
|
|
|
|
|
|
|
|
logger.info('Connection closed', state?.clientId);
|
2021-04-10 21:32:29 +02:00
|
|
|
},
|
|
|
|
|
2021-04-10 21:16:48 +02:00
|
|
|
onSocketData(socket, data) {
|
|
|
|
if (data.byteLength > MaxIncomingDataLength) {
|
2021-04-11 10:17:02 +02:00
|
|
|
logger.warn('Too many bytes rejected', data.byteLength);
|
2021-04-10 21:16:48 +02:00
|
|
|
socket.destroy();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const state = this.connectedSocketState.get(socket);
|
|
|
|
if (!state) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (state.pendingData) {
|
|
|
|
state.pendingData = Buffer.concat([state.pendingData, data]);
|
|
|
|
} else {
|
|
|
|
state.pendingData = data;
|
|
|
|
}
|
2021-04-10 21:32:29 +02:00
|
|
|
if (state.active) {
|
|
|
|
this.processPendingSocketData(socket);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2021-04-11 18:59:33 +02:00
|
|
|
async processPendingSocketData(socket) {
|
2021-04-10 21:32:29 +02:00
|
|
|
const state = this.connectedSocketState.get(socket);
|
2021-04-11 18:59:33 +02:00
|
|
|
if (!state?.pendingData || state.processingData) {
|
2021-04-10 21:16:48 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-04-11 18:59:33 +02:00
|
|
|
if (state.pendingData.length < 4) {
|
|
|
|
return;
|
|
|
|
}
|
2021-04-10 21:32:29 +02:00
|
|
|
|
2021-04-11 18:59:33 +02:00
|
|
|
const lengthBuffer = kdbxweb.ByteUtils.arrayToBuffer(state.pendingData.slice(0, 4));
|
|
|
|
const length = new Uint32Array(lengthBuffer)[0];
|
2021-04-10 21:16:48 +02:00
|
|
|
|
2021-04-11 18:59:33 +02:00
|
|
|
if (length > MaxIncomingDataLength) {
|
|
|
|
logger.warn('Large message rejected', length);
|
|
|
|
socket.destroy();
|
|
|
|
return;
|
|
|
|
}
|
2021-04-10 21:16:48 +02:00
|
|
|
|
2021-04-11 18:59:33 +02:00
|
|
|
if (state.pendingData.byteLength < length + 4) {
|
|
|
|
return;
|
|
|
|
}
|
2021-04-10 21:16:48 +02:00
|
|
|
|
2021-04-11 18:59:33 +02:00
|
|
|
const messageBytes = state.pendingData.slice(4, length + 4);
|
|
|
|
if (state.pendingData.byteLength > length + 4) {
|
|
|
|
state.pendingData = state.pendingData.slice(length + 4);
|
|
|
|
} else {
|
|
|
|
state.pendingData = null;
|
|
|
|
}
|
2021-04-10 21:16:48 +02:00
|
|
|
|
2021-04-11 18:59:33 +02:00
|
|
|
const str = messageBytes.toString();
|
|
|
|
let request;
|
|
|
|
try {
|
|
|
|
request = JSON.parse(str);
|
|
|
|
} catch {
|
|
|
|
logger.warn('Failed to parse message', str);
|
|
|
|
socket.destroy();
|
|
|
|
return;
|
|
|
|
}
|
2021-04-10 21:16:48 +02:00
|
|
|
|
2021-04-11 18:59:33 +02:00
|
|
|
logger.debug('Extension -> KeeWeb', request);
|
2021-04-11 17:59:05 +02:00
|
|
|
|
2021-04-11 18:59:33 +02:00
|
|
|
const clientId = request?.clientID;
|
|
|
|
if (!clientId) {
|
|
|
|
logger.warn('Empty client ID in request', request);
|
|
|
|
socket.destroy();
|
|
|
|
return;
|
|
|
|
}
|
2021-04-11 12:48:58 +02:00
|
|
|
|
2021-04-11 18:59:33 +02:00
|
|
|
if (!state.clientId) {
|
|
|
|
state.clientId = clientId;
|
|
|
|
} else if (state.clientId !== clientId) {
|
|
|
|
logger.warn(`Changing client ID is not allowed: ${state.clientId} => ${clientId}`);
|
|
|
|
socket.destroy();
|
|
|
|
return;
|
|
|
|
}
|
2021-04-11 12:48:58 +02:00
|
|
|
|
2021-04-11 18:59:33 +02:00
|
|
|
state.processingData = true;
|
|
|
|
|
|
|
|
let response;
|
|
|
|
try {
|
|
|
|
const handler = ProtocolHandlers[request.action];
|
|
|
|
if (!handler) {
|
|
|
|
throw new Error(`Handler not found: ${request.action}`);
|
2021-04-10 21:16:48 +02:00
|
|
|
}
|
2021-04-11 18:59:33 +02:00
|
|
|
response = await handler(request);
|
|
|
|
} catch (e) {
|
|
|
|
response = this.errorToResponse(e, request);
|
|
|
|
}
|
|
|
|
|
|
|
|
state.processingData = false;
|
|
|
|
|
|
|
|
if (response) {
|
|
|
|
this.sendSocketResponse(socket, response);
|
2021-04-10 21:16:48 +02:00
|
|
|
}
|
2021-04-11 18:59:33 +02:00
|
|
|
|
|
|
|
this.processPendingSocketData(socket);
|
2021-04-10 21:16:48 +02:00
|
|
|
},
|
|
|
|
|
2021-04-06 20:10:42 +02:00
|
|
|
browserWindowMessage(e) {
|
|
|
|
if (e.origin !== location.origin) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (e.source !== window) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (e?.data?.kwConnect !== 'request') {
|
|
|
|
return;
|
|
|
|
}
|
2021-04-11 17:59:05 +02:00
|
|
|
logger.debug('Extension -> KeeWeb', e.data);
|
2021-04-11 18:59:33 +02:00
|
|
|
pendingBrowserMessages.push(e.data);
|
|
|
|
this.processBrowserMessages();
|
|
|
|
},
|
|
|
|
|
|
|
|
async processBrowserMessages() {
|
|
|
|
if (!pendingBrowserMessages.length || processingBrowserMessage) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
processingBrowserMessage = true;
|
|
|
|
|
|
|
|
const request = pendingBrowserMessages.shift();
|
|
|
|
|
2021-04-06 20:10:42 +02:00
|
|
|
let response;
|
|
|
|
try {
|
2021-04-11 18:59:33 +02:00
|
|
|
const handler = ProtocolHandlers[request.action];
|
2021-04-06 20:10:42 +02:00
|
|
|
if (!handler) {
|
2021-04-11 18:59:33 +02:00
|
|
|
throw new Error(`Handler not found: ${request.action}`);
|
2021-04-06 20:10:42 +02:00
|
|
|
}
|
2021-04-11 18:59:33 +02:00
|
|
|
response = await handler(request);
|
2021-04-06 20:10:42 +02:00
|
|
|
} catch (e) {
|
2021-04-11 18:59:33 +02:00
|
|
|
response = this.errorToResponse(e, request);
|
2021-04-06 20:10:42 +02:00
|
|
|
}
|
2021-04-11 18:59:33 +02:00
|
|
|
|
|
|
|
processingBrowserMessage = false;
|
|
|
|
|
2021-04-06 20:10:42 +02:00
|
|
|
if (response) {
|
2021-04-10 21:16:48 +02:00
|
|
|
this.sendWebResponse(response);
|
2021-04-06 20:10:42 +02:00
|
|
|
}
|
2021-04-11 18:59:33 +02:00
|
|
|
|
|
|
|
this.processBrowserMessages();
|
2021-04-06 21:27:58 +02:00
|
|
|
},
|
|
|
|
|
2021-04-11 15:04:04 +02:00
|
|
|
errorToResponse(e, request) {
|
|
|
|
return {
|
|
|
|
action: request.action,
|
|
|
|
error: e.message || 'Unknown error',
|
2021-04-11 19:14:34 +02:00
|
|
|
errorCode: e.code || 0
|
2021-04-11 15:04:04 +02:00
|
|
|
};
|
|
|
|
},
|
|
|
|
|
2021-04-10 21:16:48 +02:00
|
|
|
sendWebResponse(response) {
|
2021-04-11 17:59:05 +02:00
|
|
|
logger.debug('KeeWeb -> Extension', response);
|
2021-04-06 21:27:58 +02:00
|
|
|
response.kwConnect = 'response';
|
|
|
|
postMessage(response, window.location.origin);
|
|
|
|
},
|
|
|
|
|
2021-04-10 21:16:48 +02:00
|
|
|
sendSocketResponse(socket, response) {
|
2021-04-11 17:59:05 +02:00
|
|
|
logger.debug('KeeWeb -> Extension', response);
|
2021-04-10 21:16:48 +02:00
|
|
|
const responseData = Buffer.from(JSON.stringify(response));
|
2021-04-11 10:17:02 +02:00
|
|
|
const lengthBuf = kdbxweb.ByteUtils.arrayToBuffer(
|
|
|
|
new Uint32Array([responseData.byteLength])
|
|
|
|
);
|
|
|
|
const lengthBytes = Buffer.from(lengthBuf);
|
2021-04-10 21:16:48 +02:00
|
|
|
const data = Buffer.concat([lengthBytes, responseData]);
|
|
|
|
socket.write(data);
|
|
|
|
},
|
|
|
|
|
|
|
|
sendSocketEvent(data) {
|
|
|
|
for (const socket of this.connectedSockets) {
|
|
|
|
const state = this.connectedSocketState.get(socket);
|
|
|
|
if (state?.active) {
|
|
|
|
this.sendSocketResponse(socket, data);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
sendEvent(data) {
|
|
|
|
if (Launcher) {
|
|
|
|
this.sendSocketEvent(data);
|
|
|
|
} else {
|
|
|
|
this.sendWebResponse(data);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2021-04-06 21:27:58 +02:00
|
|
|
fileOpened() {
|
2021-04-10 21:16:48 +02:00
|
|
|
this.sendEvent({ action: 'database-unlocked' });
|
2021-04-06 21:27:58 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
oneFileClosed() {
|
2021-04-11 17:22:58 +02:00
|
|
|
if (!appModel.files.hasOpenFiles()) {
|
|
|
|
this.sendEvent({ action: 'database-locked' });
|
2021-04-06 21:27:58 +02:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
allFilesClosed() {
|
2021-04-10 21:16:48 +02:00
|
|
|
this.sendEvent({ action: 'database-locked' });
|
2021-04-08 17:28:41 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
focusKeeWeb() {
|
2021-04-11 10:17:02 +02:00
|
|
|
logger.debug('Focus KeeWeb');
|
2021-04-10 21:16:48 +02:00
|
|
|
if (Launcher) {
|
|
|
|
Launcher.showMainWindow();
|
|
|
|
} else {
|
|
|
|
this.sendEvent({ action: 'attention-required' });
|
|
|
|
}
|
2021-04-06 20:10:42 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
export { BrowserExtensionConnector };
|