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';
|
|
|
|
import { Launcher } from 'comp/launcher';
|
|
|
|
import { AppSettingsModel } from 'models/app-settings-model';
|
2021-04-06 21:27:58 +02:00
|
|
|
import { AppModel } from 'models/app-model';
|
2021-04-08 17:28:41 +02:00
|
|
|
import { Alerts } from 'comp/ui/alerts';
|
2021-04-06 20:10:42 +02:00
|
|
|
|
|
|
|
const connectedClients = {};
|
|
|
|
|
|
|
|
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');
|
|
|
|
}
|
|
|
|
const client = connectedClients[request.clientID];
|
|
|
|
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);
|
|
|
|
|
|
|
|
if (payload?.action !== request.action) {
|
|
|
|
throw new Error(`Bad action in decrypted payload`);
|
|
|
|
}
|
|
|
|
|
|
|
|
return payload;
|
|
|
|
}
|
|
|
|
|
|
|
|
function encryptResponse(request, payload) {
|
|
|
|
const client = getClient(request);
|
|
|
|
|
|
|
|
const json = JSON.stringify(payload);
|
|
|
|
const data = new TextEncoder().encode(json);
|
|
|
|
|
|
|
|
let nonce = kdbxweb.ByteUtils.base64ToBytes(request.nonce);
|
|
|
|
incrementNonce(nonce);
|
|
|
|
|
|
|
|
const encrypted = tweetnaclBox(data, nonce, client.publicKey, client.keys.secretKey);
|
|
|
|
|
|
|
|
const message = kdbxweb.ByteUtils.bytesToBase64(encrypted);
|
|
|
|
nonce = kdbxweb.ByteUtils.bytesToBase64(nonce);
|
|
|
|
|
|
|
|
return {
|
|
|
|
action: request.action,
|
|
|
|
message,
|
|
|
|
nonce
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
const ProtocolHandlers = {
|
|
|
|
'ping'({ data }) {
|
|
|
|
return { data };
|
|
|
|
},
|
|
|
|
|
|
|
|
'change-public-keys'({ publicKey, clientID: clientId }) {
|
|
|
|
const keys = tweetnaclBox.keyPair();
|
|
|
|
publicKey = kdbxweb.ByteUtils.base64ToBytes(publicKey);
|
|
|
|
|
|
|
|
connectedClients[clientId] = { publicKey, keys };
|
|
|
|
|
|
|
|
return {
|
|
|
|
action: 'change-public-keys',
|
|
|
|
version: RuntimeInfo.version,
|
2021-04-07 21:16:48 +02:00
|
|
|
appName: 'KeeWeb',
|
2021-04-06 20:10:42 +02:00
|
|
|
publicKey: kdbxweb.ByteUtils.bytesToBase64(keys.publicKey),
|
|
|
|
success: 'true'
|
|
|
|
};
|
|
|
|
},
|
|
|
|
|
|
|
|
'get-databasehash'(request) {
|
|
|
|
decryptRequest(request);
|
2021-04-07 21:08:22 +02:00
|
|
|
|
|
|
|
const firstFile = AppModel.instance.files.firstActiveKdbxFile();
|
|
|
|
if (firstFile?.defaultGroupHash) {
|
|
|
|
return encryptResponse(request, {
|
|
|
|
action: 'hash',
|
|
|
|
version: RuntimeInfo.version,
|
2021-04-07 22:04:21 +02:00
|
|
|
hash: firstFile.defaultGroupHash,
|
2021-04-07 22:07:16 +02:00
|
|
|
hashes: AppModel.instance.files
|
|
|
|
.filter((file) => file.active && !file.backend)
|
|
|
|
.map((file) => file.defaultGroupHash)
|
2021-04-07 21:08:22 +02:00
|
|
|
});
|
|
|
|
} else {
|
|
|
|
return { action: 'get-databasehash', error: 'No open files', errorCode: '1' };
|
|
|
|
}
|
2021-04-07 22:49:23 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
'lock-database'(request) {
|
|
|
|
decryptRequest(request);
|
|
|
|
|
|
|
|
Events.emit('lock-workspace');
|
|
|
|
|
2021-04-08 17:28:41 +02:00
|
|
|
if (Alerts.alertDisplayed) {
|
|
|
|
BrowserExtensionConnector.focusKeeWeb();
|
|
|
|
}
|
|
|
|
|
2021-04-07 22:49:23 +02:00
|
|
|
return encryptResponse(request, {
|
|
|
|
action: 'lock-database',
|
|
|
|
error: 'No open files',
|
|
|
|
errorCode: '1'
|
|
|
|
});
|
2021-04-06 20:10:42 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const BrowserExtensionConnector = {
|
2021-04-06 21:27:58 +02:00
|
|
|
enabled: false,
|
|
|
|
|
2021-04-06 20:10:42 +02:00
|
|
|
init() {
|
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() {
|
|
|
|
if (!Launcher) {
|
|
|
|
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-06 20:10:42 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
stop() {
|
|
|
|
if (!Launcher) {
|
|
|
|
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-06 20:10:42 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
startWebMessageListener() {
|
|
|
|
window.addEventListener('message', this.browserWindowMessage);
|
|
|
|
},
|
|
|
|
|
|
|
|
stopWebMessageListener() {
|
|
|
|
window.removeEventListener('message', this.browserWindowMessage);
|
|
|
|
},
|
|
|
|
|
|
|
|
browserWindowMessage(e) {
|
|
|
|
if (e.origin !== location.origin) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (e.source !== window) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (e?.data?.kwConnect !== 'request') {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
let response;
|
|
|
|
try {
|
|
|
|
const handler = ProtocolHandlers[e.data.action];
|
|
|
|
if (!handler) {
|
|
|
|
throw new Error(`Handler not found: ${e.data.action}`);
|
|
|
|
}
|
|
|
|
response = handler(e.data) || {};
|
|
|
|
} catch (e) {
|
|
|
|
response = { error: e.message || 'Unknown error' };
|
|
|
|
}
|
|
|
|
if (response) {
|
2021-04-06 21:27:58 +02:00
|
|
|
this.sendResponse(response);
|
2021-04-06 20:10:42 +02:00
|
|
|
}
|
2021-04-06 21:27:58 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
sendResponse(response) {
|
|
|
|
response.kwConnect = 'response';
|
|
|
|
postMessage(response, window.location.origin);
|
|
|
|
},
|
|
|
|
|
|
|
|
fileOpened() {
|
|
|
|
this.sendResponse({ action: 'database-unlocked' });
|
|
|
|
},
|
|
|
|
|
|
|
|
oneFileClosed() {
|
|
|
|
this.sendResponse({ action: 'database-locked' });
|
|
|
|
if (AppModel.instance.files.hasOpenFiles()) {
|
|
|
|
this.sendResponse({ action: 'database-unlocked' });
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
allFilesClosed() {
|
|
|
|
this.sendResponse({ action: 'database-locked' });
|
2021-04-08 17:28:41 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
focusKeeWeb() {
|
|
|
|
this.sendResponse({ action: 'attention-required' });
|
2021-04-06 20:10:42 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
export { BrowserExtensionConnector };
|