mirror of
https://github.com/keepassxreboot/keepassxc-browser.git
synced 2026-03-11 08:54:43 +00:00
1047 lines
34 KiB
JavaScript
Executable file
1047 lines
34 KiB
JavaScript
Executable file
'use strict';
|
|
|
|
const keepass = {};
|
|
|
|
keepass.associated = {'value': false, 'hash': null};
|
|
keepass.keyPair = {publicKey: null, secretKey: null};
|
|
keepass.serverPublicKey = '';
|
|
keepass.clientID = '';
|
|
keepass.isConnected = false;
|
|
keepass.isDatabaseClosed = false;
|
|
keepass.isKeePassXCAvailable = false;
|
|
keepass.isEncryptionKeyUnrecognized = false;
|
|
keepass.currentKeePassXC = {'version': 0, 'versionParsed': 0};
|
|
keepass.requiredKeePassXC = 230;
|
|
keepass.nativeHostName = 'org.keepassxc.keepassxc_browser';
|
|
keepass.nativePort = null;
|
|
keepass.keySize = 24;
|
|
keepass.latestVersionUrl = 'https://api.github.com/repos/keepassxreboot/keepassxc/releases/latest';
|
|
keepass.cacheTimeout = 30 * 1000; // milliseconds
|
|
keepass.databaseHash = 'no-hash'; //no-hash = KeePassXC is too old and does not return a hash value
|
|
keepass.previousDatabaseHash = 'no-hash';
|
|
keepass.keyId = 'keepassxc-browser-cryptokey-name';
|
|
keepass.keyBody = 'keepassxc-browser-key';
|
|
keepass.messageTimeout = 500; // milliseconds
|
|
keepass.nonce = nacl.util.encodeBase64(nacl.randomBytes(keepass.keySize));
|
|
|
|
const kpActions = {
|
|
SET_LOGIN: 'set-login',
|
|
GET_LOGINS: 'get-logins',
|
|
GENERATE_PASSWORD: 'generate-password',
|
|
ASSOCIATE: 'associate',
|
|
TEST_ASSOCIATE: 'test-associate',
|
|
GET_DATABASE_HASH: 'get-databasehash',
|
|
CHANGE_PUBLIC_KEYS: 'change-public-keys',
|
|
LOCK_DATABASE: 'lock-database',
|
|
DATABASE_LOCKED: 'database-locked',
|
|
DATABASE_UNLOCKED: 'database-unlocked'
|
|
};
|
|
|
|
const kpErrors = {
|
|
UNKNOWN_ERROR: 0,
|
|
DATABASE_NOT_OPENED: 1,
|
|
DATABASE_HASH_NOT_RECEIVED: 2,
|
|
CLIENT_PUBLIC_KEY_NOT_RECEIVED: 3,
|
|
CANNOT_DECRYPT_MESSAGE: 4,
|
|
TIMEOUT_OR_NOT_CONNECTED: 5,
|
|
ACTION_CANCELLED_OR_DENIED: 6,
|
|
PUBLIC_KEY_NOT_FOUND: 7,
|
|
ASSOCIATION_FAILED: 8,
|
|
KEY_CHANGE_FAILED: 9,
|
|
ENCRYPTION_KEY_UNRECOGNIZED: 10,
|
|
NO_SAVED_DATABASES_FOUND: 11,
|
|
INCORRECT_ACTION: 12,
|
|
EMPTY_MESSAGE_RECEIVED: 13,
|
|
NO_URL_PROVIDED: 14,
|
|
NO_LOGINS_FOUND: 15,
|
|
|
|
errorMessages : {
|
|
0: { msg: 'Unknown error' },
|
|
1: { msg: 'Database not opened' },
|
|
2: { msg: 'Database hash not received' },
|
|
3: { msg: 'Client public key not reveiced' },
|
|
4: { msg: 'Cannot decrypt message' },
|
|
5: { msg: 'Timeout or not connected to KeePassXC' },
|
|
6: { msg: 'Action cancelled or denied' },
|
|
7: { msg: 'Cannot encrypt message or public key not found. Is native messaging or support for your browser enabled in KeePassXC?' },
|
|
8: { msg: 'KeePassXC association failed, try again.' },
|
|
9: { msg: 'Key change was not successful.' },
|
|
10: { msg: 'Encryption key is not recognized' },
|
|
11: { msg: 'No saved databases found.' },
|
|
12: { msg: 'Incorrect action.' },
|
|
13: { msg: 'Empty message received.' },
|
|
14: { msg: 'No URL provided.' },
|
|
15: { msg: 'No logins found.' }
|
|
},
|
|
|
|
getError(errorCode) {
|
|
return this.errorMessages[errorCode].msg;
|
|
}
|
|
};
|
|
|
|
browser.storage.local.get({
|
|
'latestKeePassXC': {'version': 0, 'versionParsed': 0, 'lastChecked': null},
|
|
'keyRing': {}}).then((item) => {
|
|
keepass.latestKeePassXC = item.latestKeePassXC;
|
|
keepass.keyRing = item.keyRing;
|
|
});
|
|
|
|
keepass.sendNativeMessage = function(request, enableTimeout = false) {
|
|
return new Promise((resolve, reject) => {
|
|
let timeout;
|
|
let action = request.action;
|
|
let ev = keepass.nativePort.onMessage;
|
|
|
|
let listener = ((port, action) => {
|
|
let handler = (msg) => {
|
|
if (msg && msg.action === action) {
|
|
port.removeListener(handler);
|
|
if (enableTimeout) {
|
|
clearTimeout(timeout);
|
|
}
|
|
resolve(msg);
|
|
}
|
|
};
|
|
return handler;
|
|
})(ev, action);
|
|
ev.addListener(listener);
|
|
|
|
|
|
// Handle timeouts
|
|
if (enableTimeout) {
|
|
timeout = setTimeout(() => {
|
|
const errorMessage = {
|
|
action: action,
|
|
error: kpErrors.getError(kpErrors.TIMEOUT_OR_NOT_CONNECTED),
|
|
errorCode: kpErrors.TIMEOUT_OR_NOT_CONNECTED
|
|
};
|
|
keepass.isKeePassXCAvailable = false;
|
|
ev.removeListener(listener.handler);
|
|
resolve(errorMessage);
|
|
}, keepass.messageTimeout);
|
|
}
|
|
|
|
// Send the request
|
|
if (keepass.nativePort) {
|
|
keepass.nativePort.postMessage(request);
|
|
}
|
|
});
|
|
};
|
|
|
|
keepass.addCredentials = function(callback, tab, username, password, url) {
|
|
keepass.updateCredentials(callback, tab, null, username, password, url);
|
|
};
|
|
|
|
keepass.updateCredentials = function(callback, tab, entryId, username, password, url) {
|
|
page.debug('keepass.updateCredentials(callback, {1}, {2}, {3}, [password], {4})', tab.id, entryId, username, url);
|
|
if (tab && page.tabs[tab.id]) {
|
|
page.tabs[tab.id].errorMessage = null;
|
|
}
|
|
|
|
keepass.testAssociation((response) => {
|
|
if (!response) {
|
|
browserAction.showDefault(null, tab);
|
|
callback([]);
|
|
return;
|
|
}
|
|
|
|
const kpAction = kpActions.SET_LOGIN;
|
|
const {dbid} = keepass.getCryptoKey();
|
|
const nonce = keepass.getNonce();
|
|
const incrementedNonce = keepass.incrementedNonce(nonce);
|
|
|
|
let messageData = {
|
|
action: kpAction,
|
|
id: dbid,
|
|
login: username,
|
|
password: password,
|
|
url: url,
|
|
submitUrl: url
|
|
};
|
|
|
|
if (entryId) {
|
|
messageData.uuid = entryId;
|
|
}
|
|
|
|
const request = {
|
|
action: kpAction,
|
|
message: keepass.encrypt(messageData, nonce),
|
|
nonce: nonce,
|
|
clientID: keepass.clientID
|
|
};
|
|
|
|
keepass.sendNativeMessage(request).then((response) => {
|
|
if (response.message && response.nonce) {
|
|
const res = keepass.decrypt(response.message, response.nonce);
|
|
if (!res) {
|
|
keepass.handleError(tab, kpErrors.CANNOT_DECRYPT_MESSAGE);
|
|
callback('error');
|
|
return;
|
|
}
|
|
|
|
const message = nacl.util.encodeUTF8(res);
|
|
const parsed = JSON.parse(message);
|
|
callback(keepass.verifyResponse(parsed, incrementedNonce) ? 'success' : 'error');
|
|
}
|
|
else if (response.error && response.errorCode) {
|
|
keepass.handleError(tab, response.errorCode, response.error);
|
|
}
|
|
else {
|
|
browserAction.showDefault(null, tab);
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
keepass.retrieveCredentials = function(callback, tab, url, submiturl, forceCallback, triggerUnlock = false) {
|
|
page.debug('keepass.retrieveCredentials(callback, {1}, {2}, {3}, {4})', tab.id, url, submiturl, forceCallback);
|
|
|
|
keepass.testAssociation((response) => {
|
|
if (!response) {
|
|
browserAction.showDefault(null, tab);
|
|
if (forceCallback) {
|
|
callback([]);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (tab && page.tabs[tab.id]) {
|
|
page.tabs[tab.id].errorMessage = null;
|
|
}
|
|
|
|
if (!keepass.isConnected) {
|
|
callback([]);
|
|
return;
|
|
}
|
|
|
|
let entries = [];
|
|
let keys = [];
|
|
const kpAction = kpActions.GET_LOGINS;
|
|
const nonce = keepass.getNonce();
|
|
const incrementedNonce = keepass.incrementedNonce(nonce);
|
|
const {dbid} = keepass.getCryptoKey();
|
|
|
|
for (let keyHash in keepass.keyRing) {
|
|
keys.push({
|
|
id: keepass.keyRing[keyHash].id,
|
|
key: keepass.keyRing[keyHash].key
|
|
});
|
|
}
|
|
|
|
let messageData = {
|
|
action: kpAction,
|
|
id: dbid,
|
|
url: url,
|
|
keys: keys
|
|
};
|
|
|
|
if (submiturl) {
|
|
messageData.submitUrl = submiturl;
|
|
}
|
|
|
|
const request = {
|
|
action: kpAction,
|
|
message: keepass.encrypt(messageData, nonce),
|
|
nonce: nonce,
|
|
clientID: keepass.clientID
|
|
};
|
|
|
|
keepass.sendNativeMessage(request).then((response) => {
|
|
if (response.message && response.nonce) {
|
|
const res = keepass.decrypt(response.message, response.nonce);
|
|
if (!res) {
|
|
keepass.handleError(tab, kpErrors.CANNOT_DECRYPT_MESSAGE);
|
|
callback([]);
|
|
return;
|
|
}
|
|
|
|
const message = nacl.util.encodeUTF8(res);
|
|
const parsed = JSON.parse(message);
|
|
keepass.setcurrentKeePassXCVersion(parsed.version);
|
|
|
|
if (keepass.verifyResponse(parsed, incrementedNonce)) {
|
|
entries = parsed.entries;
|
|
keepass.updateLastUsed(keepass.databaseHash);
|
|
if (entries.length === 0) {
|
|
// questionmark-icon is not triggered, so we have to trigger for the normal symbol
|
|
browserAction.showDefault(null, tab);
|
|
}
|
|
callback(entries);
|
|
}
|
|
else {
|
|
console.log('RetrieveCredentials for ' + url + ' rejected');
|
|
}
|
|
page.debug('keepass.retrieveCredentials() => entries.length = {1}', entries.length);
|
|
}
|
|
else if (response.error && response.errorCode) {
|
|
keepass.handleError(tab, response.errorCode, response.error);
|
|
callback([]);
|
|
}
|
|
else {
|
|
browserAction.showDefault(null, tab);
|
|
callback([]);
|
|
}
|
|
});
|
|
}, tab, false, triggerUnlock);
|
|
};
|
|
|
|
keepass.generatePassword = function(callback, tab, forceCallback) {
|
|
if (!keepass.isConnected) {
|
|
callback([]);
|
|
return;
|
|
}
|
|
|
|
keepass.testAssociation((taresponse) => {
|
|
if (!taresponse) {
|
|
browserAction.showDefault(null, tab);
|
|
if (forceCallback) {
|
|
callback([]);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (keepass.currentKeePassXC.versionParsed < keepass.requiredKeePassXC) {
|
|
callback([]);
|
|
return;
|
|
}
|
|
|
|
let passwords = [];
|
|
const kpAction = kpActions.GENERATE_PASSWORD;
|
|
const nonce = keepass.getNonce();
|
|
const incrementedNonce = keepass.incrementedNonce(nonce);
|
|
|
|
const request = {
|
|
action: kpAction,
|
|
nonce: nonce,
|
|
clientID: keepass.clientID
|
|
};
|
|
|
|
keepass.sendNativeMessage(request).then((response) => {
|
|
if (response.message && response.nonce) {
|
|
const res = keepass.decrypt(response.message, response.nonce);
|
|
if (!res) {
|
|
keepass.handleError(tab, kpErrors.CANNOT_DECRYPT_MESSAGE);
|
|
callback([]);
|
|
return;
|
|
}
|
|
|
|
const message = nacl.util.encodeUTF8(res);
|
|
const parsed = JSON.parse(message);
|
|
keepass.setcurrentKeePassXCVersion(parsed.version);
|
|
|
|
if (keepass.verifyResponse(parsed, incrementedNonce)) {
|
|
if (parsed.entries) {
|
|
passwords = parsed.entries;
|
|
keepass.updateLastUsed(keepass.databaseHash);
|
|
}
|
|
else {
|
|
console.log('No entries returned. Is KeePassXC up-to-date?');
|
|
}
|
|
}
|
|
else {
|
|
console.log('GeneratePassword rejected');
|
|
}
|
|
callback(passwords);
|
|
}
|
|
else if (response.error && response.errorCode) {
|
|
keepass.handleError(tab, response.errorCode, response.error);
|
|
}
|
|
});
|
|
}, tab);
|
|
};
|
|
|
|
keepass.associate = function(callback, tab) {
|
|
if (keepass.isAssociated()) {
|
|
callback([]);
|
|
return;
|
|
}
|
|
|
|
keepass.getDatabaseHash((res) => {
|
|
if (keepass.isDatabaseClosed || !keepass.isKeePassXCAvailable) {
|
|
callback([]);
|
|
return;
|
|
}
|
|
|
|
if (tab && page.tabs[tab.id]) {
|
|
page.tabs[tab.id].errorMessage = null;
|
|
}
|
|
|
|
const kpAction = kpActions.ASSOCIATE;
|
|
const key = nacl.util.encodeBase64(keepass.keyPair.publicKey);
|
|
const nonce = keepass.getNonce();
|
|
const incrementedNonce = keepass.incrementedNonce(nonce);
|
|
|
|
const messageData = {
|
|
action: kpAction,
|
|
key: key
|
|
};
|
|
|
|
const request = {
|
|
action: kpAction,
|
|
message: keepass.encrypt(messageData, nonce),
|
|
nonce: nonce,
|
|
clientID: keepass.clientID,
|
|
triggerUnlock: 'true'
|
|
};
|
|
|
|
keepass.sendNativeMessage(request).then((response) => {
|
|
if (response.message && response.nonce) {
|
|
const res = keepass.decrypt(response.message, response.nonce);
|
|
if (!res) {
|
|
keepass.handleError(tab, kpErrors.CANNOT_DECRYPT_MESSAGE);
|
|
return;
|
|
}
|
|
|
|
const message = nacl.util.encodeUTF8(res);
|
|
const parsed = JSON.parse(message);
|
|
keepass.setcurrentKeePassXCVersion(parsed.version);
|
|
const id = parsed.id;
|
|
|
|
if (!keepass.verifyResponse(parsed, incrementedNonce)) {
|
|
keepass.handleError(tab, kpErrors.ASSOCIATION_FAILED);
|
|
}
|
|
else {
|
|
keepass.setCryptoKey(id, key); // Save the current public key as id key for the database
|
|
keepass.associated.value = true;
|
|
keepass.associated.hash = parsed.hash || 0;
|
|
}
|
|
|
|
browserAction.show(callback, tab);
|
|
}
|
|
else if (response.error && response.errorCode) {
|
|
keepass.handleError(tab, response.errorCode, response.error);
|
|
}
|
|
});
|
|
}, tab);
|
|
};
|
|
|
|
keepass.testAssociation = function(callback, tab, enableTimeout = false, triggerUnlock = false) {
|
|
if (tab && page.tabs[tab.id]) {
|
|
page.tabs[tab.id].errorMessage = null;
|
|
}
|
|
|
|
keepass.getDatabaseHash((dbHash) => {
|
|
if (!dbHash) {
|
|
callback(false);
|
|
return false;
|
|
}
|
|
|
|
if (keepass.isDatabaseClosed || !keepass.isKeePassXCAvailable) {
|
|
callback(false);
|
|
return false;
|
|
}
|
|
|
|
if (keepass.isAssociated()) {
|
|
callback(true);
|
|
return true;
|
|
}
|
|
|
|
if (!keepass.serverPublicKey) {
|
|
if (tab && page.tabs[tab.id]) {
|
|
keepass.handleError(tab, kpErrors.PUBLIC_KEY_NOT_FOUND);
|
|
}
|
|
callback(false);
|
|
return false;
|
|
}
|
|
|
|
const kpAction = kpActions.TEST_ASSOCIATE;
|
|
const nonce = keepass.getNonce();
|
|
const incrementedNonce = keepass.incrementedNonce(nonce);
|
|
const {dbid, dbkey} = keepass.getCryptoKey();
|
|
|
|
if (dbkey === null || dbid === null) {
|
|
if (tab && page.tabs[tab.id]) {
|
|
keepass.handleError(tab, kpErrors.NO_SAVED_DATABASES_FOUND);
|
|
}
|
|
callback(false);
|
|
return false;
|
|
}
|
|
|
|
const messageData = {
|
|
action: kpAction,
|
|
id: dbid,
|
|
key: dbkey
|
|
};
|
|
|
|
const request = {
|
|
action: kpAction,
|
|
message: keepass.encrypt(messageData, nonce),
|
|
nonce: nonce,
|
|
clientID: keepass.clientID
|
|
};
|
|
|
|
keepass.sendNativeMessage(request, enableTimeout).then((response) => {
|
|
if (response.message && response.nonce) {
|
|
const res = keepass.decrypt(response.message, response.nonce);
|
|
if (!res) {
|
|
keepass.handleError(tab, kpErrors.CANNOT_DECRYPT_MESSAGE);
|
|
callback(false);
|
|
return;
|
|
}
|
|
|
|
const message = nacl.util.encodeUTF8(res);
|
|
const parsed = JSON.parse(message);
|
|
keepass.setcurrentKeePassXCVersion(parsed.version);
|
|
keepass.isEncryptionKeyUnrecognized = false;
|
|
|
|
if (!keepass.verifyResponse(parsed, incrementedNonce)) {
|
|
const hash = response.hash || 0;
|
|
keepass.deleteKey(hash);
|
|
keepass.isEncryptionKeyUnrecognized = true;
|
|
keepass.handleError(tab, kpErrors.ENCRYPTION_KEY_UNRECOGNIZED);
|
|
keepass.associated.value = false;
|
|
keepass.associated.hash = null;
|
|
}
|
|
else if (!keepass.isAssociated()) {
|
|
keepass.handleError(tab, kpErrors.ASSOCIATION_FAILED);
|
|
}
|
|
else {
|
|
if (tab && page.tabs[tab.id]) {
|
|
delete page.tabs[tab.id].errorMessage;
|
|
}
|
|
}
|
|
}
|
|
else if (response.error && response.errorCode) {
|
|
keepass.handleError(tab, response.errorCode, response.error);
|
|
}
|
|
callback(keepass.isAssociated());
|
|
});
|
|
}, tab, enableTimeout, triggerUnlock);
|
|
};
|
|
|
|
keepass.getDatabaseHash = function(callback, tab, enableTimeout = false, triggerUnlock = false) {
|
|
if (!keepass.isConnected) {
|
|
keepass.handleError(tab, kpErrors.TIMEOUT_OR_NOT_CONNECTED);
|
|
callback([]);
|
|
return;
|
|
}
|
|
|
|
if (!keepass.serverPublicKey) {
|
|
keepass.changePublicKeys(tab);
|
|
}
|
|
|
|
const kpAction = kpActions.GET_DATABASE_HASH;
|
|
const nonce = keepass.getNonce();
|
|
const incrementedNonce = keepass.incrementedNonce(nonce);
|
|
|
|
const messageData = {
|
|
action: kpAction
|
|
};
|
|
|
|
const encrypted = keepass.encrypt(messageData, nonce);
|
|
if (encrypted.length <= 0) {
|
|
keepass.handleError(tab, kpErrors.PUBLIC_KEY_NOT_FOUND);
|
|
callback(keepass.databaseHash);
|
|
return;
|
|
}
|
|
|
|
const request = {
|
|
action: kpAction,
|
|
message: encrypted,
|
|
nonce: nonce,
|
|
clientID: keepass.clientID
|
|
};
|
|
|
|
if (triggerUnlock === true) {
|
|
request.triggerUnlock = 'true';
|
|
}
|
|
|
|
keepass.sendNativeMessage(request, enableTimeout).then((response) => {
|
|
if (response.message && response.nonce) {
|
|
const res = keepass.decrypt(response.message, response.nonce);
|
|
if (!res) {
|
|
keepass.handleError(tab, kpErrors.CANNOT_DECRYPT_MESSAGE);
|
|
callback('no-hash');
|
|
return;
|
|
}
|
|
|
|
const message = nacl.util.encodeUTF8(res);
|
|
const parsed = JSON.parse(message);
|
|
if (keepass.verifyDatabaseResponse(parsed, incrementedNonce) && parsed.hash) {
|
|
const oldDatabaseHash = keepass.databaseHash;
|
|
keepass.setcurrentKeePassXCVersion(parsed.version);
|
|
keepass.databaseHash = parsed.hash || 'no-hash';
|
|
|
|
if (oldDatabaseHash && oldDatabaseHash != keepass.databaseHash) {
|
|
keepass.associated.value = false;
|
|
keepass.associated.hash = null;
|
|
}
|
|
|
|
keepass.isDatabaseClosed = false;
|
|
keepass.isKeePassXCAvailable = true;
|
|
callback(parsed.hash);
|
|
return;
|
|
}
|
|
else if (parsed.errorCode) {
|
|
keepass.databaseHash = 'no-hash';
|
|
keepass.isDatabaseClosed = true;
|
|
keepass.handleError(tab, kpErrors.DATABASE_NOT_OPENED);
|
|
callback(keepass.databaseHash);
|
|
return;
|
|
}
|
|
}
|
|
else {
|
|
keepass.databaseHash = 'no-hash';
|
|
keepass.isDatabaseClosed = true;
|
|
if (response.message && response.message === '') {
|
|
keepass.isKeePassXCAvailable = false;
|
|
keepass.handleError(tab, kpErrors.TIMEOUT_OR_NOT_CONNECTED);
|
|
}
|
|
else {
|
|
keepass.handleError(tab, response.errorCode, response.error);
|
|
}
|
|
callback(keepass.databaseHash);
|
|
return;
|
|
}
|
|
});
|
|
};
|
|
|
|
keepass.changePublicKeys = function(tab, enableTimeout = false) {
|
|
return new Promise((resolve, reject) => {
|
|
if (!keepass.isConnected) {
|
|
keepass.handleError(tab, kpErrors.TIMEOUT_OR_NOT_CONNECTED);
|
|
reject(false);
|
|
}
|
|
|
|
const kpAction = kpActions.CHANGE_PUBLIC_KEYS;
|
|
const key = nacl.util.encodeBase64(keepass.keyPair.publicKey);
|
|
const nonce = keepass.getNonce();
|
|
const incrementedNonce = keepass.incrementedNonce(nonce);
|
|
keepass.clientID = nacl.util.encodeBase64(nacl.randomBytes(keepass.keySize));
|
|
|
|
const request = {
|
|
action: kpAction,
|
|
publicKey: key,
|
|
nonce: nonce,
|
|
clientID: keepass.clientID
|
|
};
|
|
|
|
keepass.sendNativeMessage(request, enableTimeout).then((response) => {
|
|
keepass.setcurrentKeePassXCVersion(response.version);
|
|
|
|
if (!keepass.verifyKeyResponse(response, key, incrementedNonce)) {
|
|
if (tab && page.tabs[tab.id]) {
|
|
keepass.handleError(tab, kpErrors.KEY_CHANGE_FAILED);
|
|
reject(false);
|
|
}
|
|
}
|
|
else {
|
|
keepass.isKeePassXCAvailable = true;
|
|
console.log('Server public key: ' + nacl.util.encodeBase64(keepass.serverPublicKey));
|
|
}
|
|
resolve(true);
|
|
});
|
|
});
|
|
};
|
|
|
|
keepass.lockDatabase = function(tab) {
|
|
return new Promise((resolve, reject) => {
|
|
if (!keepass.isConnected) {
|
|
keepass.handleError(tab, kpErrors.TIMEOUT_OR_NOT_CONNECTED);
|
|
reject(false);
|
|
}
|
|
|
|
const kpAction = kpActions.LOCK_DATABASE;
|
|
const nonce = keepass.getNonce();
|
|
const incrementedNonce = keepass.incrementedNonce(nonce);
|
|
|
|
const messageData = {
|
|
action: kpAction
|
|
};
|
|
|
|
const request = {
|
|
action: kpAction,
|
|
message: keepass.encrypt(messageData, nonce),
|
|
nonce: nonce,
|
|
clientID: keepass.clientID
|
|
};
|
|
|
|
keepass.sendNativeMessage(request).then((response) => {
|
|
if (response.message && response.nonce) {
|
|
const res = keepass.decrypt(response.message, response.nonce);
|
|
if (!res) {
|
|
keepass.handleError(tab, kpErrors.CANNOT_DECRYPT_MESSAGE);
|
|
resolve(false);
|
|
return;
|
|
}
|
|
|
|
const message = nacl.util.encodeUTF8(res);
|
|
const parsed = JSON.parse(message);
|
|
keepass.setcurrentKeePassXCVersion(parsed.version);
|
|
|
|
if (keepass.verifyResponse(parsed, incrementedNonce)) {
|
|
keepass.isDatabaseClosed = true;
|
|
|
|
// Display error message in the popup
|
|
keepass.handleError(tab, kpErrors.DATABASE_NOT_OPENED);
|
|
resolve(true);
|
|
}
|
|
}
|
|
else if (response.error && response.errorCode) {
|
|
keepass.isDatabaseClosed = true;
|
|
keepass.handleError(tab, response.errorCode, response.error);
|
|
}
|
|
resolve(false);
|
|
});
|
|
});
|
|
};
|
|
|
|
keepass.generateNewKeyPair = function() {
|
|
keepass.keyPair = nacl.box.keyPair();
|
|
//console.log(nacl.util.encodeBase64(keepass.keyPair.publicKey) + ' ' + nacl.util.encodeBase64(keepass.keyPair.secretKey));
|
|
};
|
|
|
|
keepass.isConfigured = function() {
|
|
return new Promise((resolve, reject) => {
|
|
if (typeof(keepass.databaseHash) === 'undefined') {
|
|
keepass.getDatabaseHash((hash) => {
|
|
resolve(hash in keepass.keyRing);
|
|
});
|
|
} else {
|
|
resolve(keepass.databaseHash in keepass.keyRing);
|
|
}
|
|
});
|
|
};
|
|
|
|
keepass.checkDatabaseHash = function(callback, tab) {
|
|
callback(keepass.databaseHash);
|
|
};
|
|
|
|
keepass.isAssociated = function() {
|
|
return (keepass.associated.value && keepass.associated.hash && keepass.associated.hash === keepass.databaseHash);
|
|
};
|
|
|
|
keepass.migrateKeyRing = function() {
|
|
return new Promise((resolve, reject) => {
|
|
browser.storage.local.get('keyRing').then((item) => {
|
|
const keyring = item.keyRing;
|
|
// Change dates to numbers, for compatibilty with Chromium based browsers
|
|
if (keyring) {
|
|
let num = 0;
|
|
for (let keyHash in keyring) {
|
|
let key = keyring[keyHash];
|
|
['created', 'lastUsed'].forEach((fld) => {
|
|
let v = key[fld];
|
|
if (v instanceof Date && v.valueOf() >= 0) {
|
|
key[fld] = v.valueOf();
|
|
num++;
|
|
} else if (typeof v !== 'number') {
|
|
key[fld] = Date.now().valueOf();
|
|
num++;
|
|
}
|
|
});
|
|
}
|
|
if (num > 0) {
|
|
browser.storage.local.set({ keyRing: keyring });
|
|
}
|
|
}
|
|
resolve();
|
|
});
|
|
});
|
|
};
|
|
|
|
keepass.saveKey = function(hash, id, key) {
|
|
if (!(hash in keepass.keyRing)) {
|
|
keepass.keyRing[hash] = {
|
|
id: id,
|
|
key: key,
|
|
hash: hash,
|
|
created: new Date().valueOf(),
|
|
lastUsed: new Date().valueOf()
|
|
};
|
|
}
|
|
else {
|
|
keepass.keyRing[hash].id = id;
|
|
keepass.keyRing[hash].key = key;
|
|
keepass.keyRing[hash].hash = hash;
|
|
}
|
|
browser.storage.local.set({'keyRing': keepass.keyRing});
|
|
};
|
|
|
|
keepass.updateLastUsed = function(hash) {
|
|
if ((hash in keepass.keyRing)) {
|
|
keepass.keyRing[hash].lastUsed = new Date().valueOf();
|
|
browser.storage.local.set({'keyRing': keepass.keyRing});
|
|
}
|
|
};
|
|
|
|
keepass.deleteKey = function(hash) {
|
|
delete keepass.keyRing[hash];
|
|
browser.storage.local.set({'keyRing': keepass.keyRing});
|
|
};
|
|
|
|
keepass.setcurrentKeePassXCVersion = function(version) {
|
|
if (version) {
|
|
keepass.currentKeePassXC = {
|
|
version: version,
|
|
versionParsed: Number(version.replace(/\./g, ''))
|
|
};
|
|
}
|
|
};
|
|
|
|
keepass.keePassXCUpdateAvailable = function() {
|
|
if (page.settings.checkUpdateKeePassXC && page.settings.checkUpdateKeePassXC > 0) {
|
|
const lastChecked = (keepass.latestKeePassXC.lastChecked) ? new Date(keepass.latestKeePassXC.lastChecked) : new Date(1986, 11, 21);
|
|
const daysSinceLastCheck = Math.floor(((new Date()).getTime()-lastChecked.getTime())/86400000);
|
|
if (daysSinceLastCheck >= page.settings.checkUpdateKeePassXC) {
|
|
keepass.checkForNewKeePassXCVersion();
|
|
}
|
|
}
|
|
|
|
return (keepass.currentKeePassXC.versionParsed > 0 && keepass.currentKeePassXC.versionParsed < keepass.latestKeePassXC.versionParsed);
|
|
};
|
|
|
|
keepass.checkForNewKeePassXCVersion = function() {
|
|
let xhr = new XMLHttpRequest();
|
|
let version = -1;
|
|
|
|
xhr.onload = function(e) {
|
|
if (xhr.readyState === 4 && xhr.status === 200) {
|
|
const json = JSON.parse(xhr.responseText);
|
|
if (json.tag_name) {
|
|
version = json.tag_name;
|
|
keepass.latestKeePassXC.version = version;
|
|
keepass.latestKeePassXC.versionParsed = Number(version.replace(/\./g, ''));
|
|
}
|
|
}
|
|
|
|
if (version !== -1) {
|
|
browser.storage.local.set({'latestKeePassXC': keepass.latestKeePassXC});
|
|
}
|
|
};
|
|
|
|
xhr.onerror = function(e) {
|
|
console.log('checkForNewKeePassXCVersion error:' + e);
|
|
};
|
|
|
|
try {
|
|
xhr.open('GET', keepass.latestVersionUrl, true);
|
|
xhr.send();
|
|
}
|
|
catch (ex) {
|
|
console.log(ex);
|
|
}
|
|
keepass.latestKeePassXC.lastChecked = new Date().valueOf();
|
|
};
|
|
|
|
keepass.connectToNative = function() {
|
|
if (keepass.nativePort) {
|
|
keepass.nativePort.disconnect();
|
|
}
|
|
keepass.nativeConnect();
|
|
};
|
|
|
|
keepass.onNativeMessage = function(response) {
|
|
//console.log('Received message: ' + JSON.stringify(response));
|
|
|
|
// Handle database lock/unlock status
|
|
if (response.action === kpActions.DATABASE_LOCKED || response.action === kpActions.DATABASE_UNLOCKED) {
|
|
keepass.testAssociation((associationResponse) => {
|
|
keepass.isConfigured().then((configured) => {
|
|
keepass.updatePopup(configured ? 'normal' : 'cross');
|
|
});
|
|
}, null);
|
|
}
|
|
};
|
|
|
|
function onDisconnected() {
|
|
keepass.nativePort = null;
|
|
keepass.isConnected = false;
|
|
keepass.isDatabaseClosed = true;
|
|
keepass.isKeePassXCAvailable = false;
|
|
keepass.associated.value = false;
|
|
keepass.associated.hash = null;
|
|
page.clearCredentials(page.currentTabId, true);
|
|
keepass.updatePopup('cross');
|
|
console.log('Failed to connect: ' + (browser.runtime.lastError === null ? 'Unknown error' : browser.runtime.lastError.message));
|
|
}
|
|
|
|
keepass.getNonce = function() {
|
|
return nacl.util.encodeBase64(nacl.randomBytes(keepass.keySize));
|
|
};
|
|
|
|
keepass.incrementedNonce = function(nonce) {
|
|
const oldNonce = nacl.util.decodeBase64(nonce);
|
|
let newNonce = oldNonce.slice(0);
|
|
|
|
// from libsodium/utils.c
|
|
let i = 0;
|
|
let c = 1;
|
|
for (; i < newNonce.length; ++i) {
|
|
c += newNonce[i];
|
|
newNonce[i] = c;
|
|
c >>= 8;
|
|
}
|
|
|
|
return nacl.util.encodeBase64(newNonce);
|
|
};
|
|
|
|
keepass.nativeConnect = function() {
|
|
console.log('Connecting to native messaging host ' + keepass.nativeHostName);
|
|
keepass.nativePort = browser.runtime.connectNative(keepass.nativeHostName);
|
|
keepass.nativePort.onMessage.addListener(keepass.onNativeMessage);
|
|
keepass.nativePort.onDisconnect.addListener(onDisconnected);
|
|
keepass.isConnected = true;
|
|
};
|
|
|
|
keepass.verifyKeyResponse = function(response, key, nonce) {
|
|
if (!response.success || !response.publicKey) {
|
|
keepass.associated.hash = null;
|
|
return false;
|
|
}
|
|
|
|
let reply = false;
|
|
if (!keepass.checkNonceLength(response.nonce)) {
|
|
console.log('Error: Invalid nonce length');
|
|
return false;
|
|
}
|
|
|
|
reply = (response.nonce === nonce);
|
|
|
|
if (response.publicKey) {
|
|
keepass.serverPublicKey = nacl.util.decodeBase64(response.publicKey);
|
|
reply = true;
|
|
}
|
|
|
|
return reply;
|
|
};
|
|
|
|
keepass.verifyResponse = function(response, nonce, id) {
|
|
keepass.associated.value = response.success;
|
|
if (response.success !== 'true') {
|
|
keepass.associated.hash = null;
|
|
return false;
|
|
}
|
|
|
|
keepass.associated.hash = keepass.databaseHash;
|
|
|
|
if (!keepass.checkNonceLength(response.nonce)) {
|
|
return false;
|
|
}
|
|
|
|
keepass.associated.value = (response.nonce === nonce);
|
|
if (keepass.associated.value === false) {
|
|
console.log('Error: Nonce compare failed');
|
|
return false;
|
|
}
|
|
|
|
if (id) {
|
|
keepass.associated.value = (keepass.associated.value && id === response.id);
|
|
}
|
|
|
|
keepass.associated.hash = (keepass.associated.value) ? keepass.databaseHash : null;
|
|
return keepass.isAssociated();
|
|
};
|
|
|
|
keepass.verifyDatabaseResponse = function(response, nonce) {
|
|
if (response.success !== 'true') {
|
|
keepass.associated.hash = null;
|
|
return false;
|
|
}
|
|
|
|
if (!keepass.checkNonceLength(response.nonce)) {
|
|
console.log('Error: Invalid nonce length');
|
|
return false;
|
|
}
|
|
|
|
if (response.nonce !== nonce) {
|
|
console.log('Error: Nonce compare failed');
|
|
return false;
|
|
}
|
|
|
|
keepass.associated.hash = response.hash;
|
|
return response.hash !== 'no-hash' && response.success === 'true';
|
|
};
|
|
|
|
keepass.checkNonceLength = function(nonce) {
|
|
return nacl.util.decodeBase64(nonce).length === nacl.secretbox.nonceLength;
|
|
};
|
|
|
|
keepass.handleError = function(tab, errorCode, errorMessage = '') {
|
|
if (errorMessage.length === 0) {
|
|
errorMessage = kpErrors.getError(errorCode);
|
|
}
|
|
console.log('Error ' + errorCode + ': ' + errorMessage);
|
|
if (tab && page.tabs[tab.id]) {
|
|
page.tabs[tab.id].errorMessage = errorMessage;
|
|
}
|
|
};
|
|
|
|
keepass.getCryptoKey = function() {
|
|
let dbkey = null;
|
|
let dbid = null;
|
|
if (!(keepass.databaseHash in keepass.keyRing)) {
|
|
return {dbid, dbkey};
|
|
}
|
|
|
|
dbid = keepass.keyRing[keepass.databaseHash].id;
|
|
|
|
if (dbid) {
|
|
dbkey = keepass.keyRing[keepass.databaseHash].key;
|
|
}
|
|
|
|
return {dbid, dbkey};
|
|
};
|
|
|
|
keepass.setCryptoKey = function(id, key) {
|
|
keepass.saveKey(keepass.databaseHash, id, key);
|
|
};
|
|
|
|
keepass.encrypt = function(input, nonce) {
|
|
const messageData = nacl.util.decodeUTF8(JSON.stringify(input));
|
|
const messageNonce = nacl.util.decodeBase64(nonce);
|
|
|
|
if (keepass.serverPublicKey) {
|
|
const message = nacl.box(messageData, messageNonce, keepass.serverPublicKey, keepass.keyPair.secretKey);
|
|
if (message) {
|
|
return nacl.util.encodeBase64(message);
|
|
}
|
|
}
|
|
return '';
|
|
};
|
|
|
|
keepass.decrypt = function(input, nonce) {
|
|
const m = nacl.util.decodeBase64(input);
|
|
const n = nacl.util.decodeBase64(nonce);
|
|
const res = nacl.box.open(m, n, keepass.serverPublicKey, keepass.keyPair.secretKey);
|
|
return res;
|
|
};
|
|
|
|
keepass.enableAutomaticReconnect = function() {
|
|
setInterval(() => {
|
|
if (!keepass.isKeePassXCAvailable) {
|
|
keepass.connectToNative();
|
|
keepass.reconnect();
|
|
}
|
|
}, 1000);
|
|
};
|
|
|
|
keepass.reconnect = function(callback, tab) {
|
|
return new Promise((resolve, reject) => {
|
|
keepass.generateNewKeyPair();
|
|
keepass.changePublicKeys(tab).then((pkRes) => {
|
|
// Database hash should be received if the reconnection succeeded
|
|
keepass.getDatabaseHash((gdRes) => {
|
|
if (!gdRes) {
|
|
reject(false);
|
|
return;
|
|
}
|
|
|
|
keepass.testAssociation((response) => {
|
|
keepass.isConfigured().then((configured) => {
|
|
resolve(configured);
|
|
}).catch((e) => {
|
|
console.log(e);
|
|
reject(e);
|
|
});
|
|
}, tab);
|
|
}, null);
|
|
});
|
|
});
|
|
};
|
|
|
|
keepass.updatePopup = function(iconType) {
|
|
const data = page.tabs[page.currentTabId].stack[page.tabs[page.currentTabId].stack.length - 1];
|
|
data.iconType = iconType;
|
|
browserAction.show(null, {'id': page.currentTabId});
|
|
};
|