Merge branch 'develop'

This commit is contained in:
varjolintu 2017-12-01 11:11:58 +02:00
commit 15d6ba97c2
18 changed files with 955 additions and 742 deletions

32
.github/ISSUE_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,32 @@
<!--- Provide a general summary of the issue in the title above -->
## Expected Behavior
<!--- If you're describing a bug, tell us what should happen -->
<!--- If you're suggesting a change/improvement, tell us how it should work -->
## Current Behavior
<!--- If describing a bug, tell us what happens instead of the expected behavior -->
<!--- If suggesting a change/improvement, explain the difference from the current behavior -->
## Possible Solution
<!--- Not obligatory, but suggest a fix/reason for the bug, -->
<!--- or ideas how to implement the addition or change -->
## Steps to Reproduce (for bugs)
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
<!--- reproduce this bug. Include code to reproduce, if relevant -->
1.
2.
3.
4.
## Debug info
<!--- Please copy/paste the browser's JavaScript console messages and errors here -->
## General Info
KeePassXC fork - VERSION
keepassxc-browser - VERSION
Operating system: OS
Browser: BROWSER
Proxy used: YES/NO

View file

@ -1,3 +1,35 @@
0.4.2 (27-11-2017)
=========================
- Fixed HTTP authentication with multiple credentials (credits to smorks)
- Fixed error handling when decrypt fails
- Fixed database-locked response handling
- Fixed nonce increment when encrypting messages
0.4.1 (18-11-2017)
=========================
- Added support for the credentials dropdown menu with only password field visible
- Fixed jQuery overriding with custom scoped CSS
- Fixed non-necessary destroying of autocomplete on autofill
0.4.0 (13-11-2017)
=========================
- Fixed showing context menu on password fields with Firefox
- Ignore XML files on content scripts (Firefox shows them incorrectly)
- UDP features removed as KeePassXC switched them to Unix domain sockets and named pipes
0.3.9 (04-11-2017)
=========================
- Removed incorrect timeout waiting on init
0.3.8 (01-11-2017)
=========================
- Use browser.storage.local instead of localStorage
- Switched some functions to promise
0.3.7 (22-10-2017)
=========================
- Improved credentials check (does not use protocol requests for polling)
0.3.6 (20-10-2017)
=========================
- Restricted page credentials polling to active tab

258
README.md
View file

@ -1,15 +1,15 @@
# keepassxc-browser
Chrome extension for [KeePassXC](https://keepassxc.org/) with Native Messaging.
Browser extension for [KeePassXC](https://keepassxc.org/) with Native Messaging.
This is a heavily forked version of [pfn](https://github.com/pfn)'s [chromeIPass](https://github.com/pfn/passifox).
Some changes merged also from [smorks'](https://github.com/smorks/keepasshttp-connector) KeePassHttp-Connector fork.
For testing purposes, please use following unofficial KeePassXC [release's](https://github.com/varjolintu/keepassxc/releases).
For testing purposes, please use only the following unofficial KeePassXC [release's](https://github.com/varjolintu/keepassxc/releases).
Get the extension for [Firefox](https://addons.mozilla.org/en-US/firefox/addon/keepassxc-browser/) or [Chrome/Chromium](https://chrome.google.com/webstore/detail/keepassxc-browser/iopaggbpplllidnfmcghoonnokmjoicf).
The extension is supported with Firefox 55 and newer. If you want to load it as a temporary plugin with Firefox 54 you can just change the minimum version from the manifest file before loading it.
The extension is supported with Firefox 55 and newer. If you want to load it as a temporary plugin with Firefox 54 or ESR you can just change the minimum version from the manifest file before loading it.
Please thee this [wiki page](https://github.com/varjolintu/keepassxc-browser/wiki/Connecting-the-database-with-current-beta-build) for instructions how to configure this KeePassXC fork in order to connect the database correctly.
Please thee this [wiki page](hhttps://github.com/varjolintu/keepassxc-browser/wiki/Connecting-the-database-with-keepassxc-browser) for instructions how to configure this KeePassXC fork in order to connect the database correctly.
## How it works
There are two methods which you can use keepassxc-browser to connect to KeePassXC:
@ -17,10 +17,11 @@ There are two methods which you can use keepassxc-browser to connect to KeePassX
1. keepassxc-browser communicates directly with KeePassXC via stdin/stdout. This method launches KeePassXC every time you start the browser and closes when you exit.
This can cause unsaved changes not to be saved. If you use this method it's important to enable `Automatically save after every change` from KeePassXC's preferences.
2. keepassxc-browser communicated with KeePassXC through [keepassxc-proxy](https://github.com/varjolintu/keepassxc-proxy) or [keepassxc-proxy-rust](https://github.com/varjolintu/keepassxc-proxy-rust). The proxy handles listening stdin/stdout
and transfers these messages through a localhost UDP port 19700 (configurable) to KeePassXC. This means KeePassXC can be used and started normally without inteference from
2. keepassxc-browser communicated with KeePassXC through [keepassxc-proxy](https://github.com/varjolintu/keepassxc-proxy). The proxy handles listening stdin/stdout
and transfers these messages through Unix domain sockets / named pipes to KeePassXC. This means KeePassXC can be used and started normally without inteference from
Native Messaging API. keepassxc-browser starts only the proxy application and there's no risk of shutting down KeePassXC or losing any unsaved changes. keepassxc-proxy
is still under development. If you want, you are free to write your own proxy that handles the traffic.
is still under development. If you want, you are free to write your own proxy that handles the traffic. You don't need to install keepassxc-proxy separately. It is
included in the latest KeePassXC fork. Use it if you want to make your own proxy or improve/extend it.
## Improvements
The following improvements and features have been made after the fork. At this point some features are only available with the KeePassXC fork:
@ -34,248 +35,11 @@ The following improvements and features have been made after the fork. At this p
- New buttons, icons and settings page graphics
- Redesigned password generator dialog
- Password generator supports diceware passphrases and extended ASCII characters
- Autocomplete works also when only password fields are visible
## Protocol
Transmitting messages between KeePassXC and keepassxc-browser is totally rewritten. This is still under development.
Now the requests are encrypted by [TweetNaCl.js](https://github.com/dchest/tweetnacl-js) box method and does the following:
1. keepassxc-browser generates a key pair (with public and secret key) and transfers the public key to KeePassXC
2. When KeePassXC receives the public key it generates its own key pair and transfers the public key to keepassxc-browser
3. All messages between the browser extension and KeePassXC are now encrypted.
4. When keepassxc-browser sends a message it is encrypted with KeePassXC's public key, a random generated nonce and keepassxc-browser's secret key.
5. When KeePassXC sends a message it is encrypted with keepassxc-browser's public key etc.
6. Databases are stored based on the current public key used with `associate`. A new key pair for data transfer is generated each time keepassxc-browser is launched.
Encrypted messages are built with these JSON parameters:
- action - `test-associate`, `associate`, `get-logins`, `get-logins-count`, `set-login`...
- message - Encrypted message, base64 encoded
- nonce - 24 bytes long random data, base64 encoded. This must be the same when responding to a request.
- clientID - 24 bytes long random data, base64 encoded. This is used to identify different browsers if multiple are used with proxy application.
### change-public-keys
Request:
```javascript
{
"action": "change-public-keys",
"publicKey": "<current public key>",
"proxyPort": "<UDP port for proxy applications>",
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q",
"clientID": "<clientID>"
}
```
Response (success):
```javascript
{
"action": "change-public-keys",
"version": "2.2.0",
"publicKey": "<host public key>",
"success": "true"
}
```
### get-databasehash
Request (unencrypted):
```javascript
{
"action": "get-databasehash"
}
```
Response message data (success, decrypted):
```javascript
{
"action": "hash",
"hash": "29234e32274a32276e25666a42",
"version": "2.2.0"
}
```
### associate
Unencrypted message:
```javascript
{
"action": "associate",
"key": "<current public key>"
}
```
Request:
```javascript
{
"action": "associate",
"message": encryptedMessage
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q",
"clientID": "<clientID>"
}
```
Response message data (success, decrypted):
```javascript
{
"hash": "29234e32274a32276e25666a42",
"version": "2.2.0",
"success": "true",
"id": "testclient",
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q"
}
```
### test-associate
Unencrypted message:
```javascript
{
"action": "test-associate",
"id": "<saved database identifier>",
"key": "<saved database public key>",
"clientID": "<clientID>"
}
```
Request:
```javascript
{
"action": "test-associate",
"message": encryptedMessage
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q"
}
```
Response message data (success, decrypted):
```javascript
{
"version": "2.2.0",
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q",
"hash": "29234e32274a32276e25666a42",
"id": "testclient",
"success": "true"
}
```
### generate-password
Request:
```javascript
{
"action": "generate-password",
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q",
"clientID": "<clientID>"
}
```
Response message data (success, decrypted):
```javascript
{
"version": "2.2.0",
"entries": [
{
"login": 144,
"password": "testclientpassword"
}
],
"success": "true",
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q"
}
```
### get-logins
Unencrypted message:
```javascript
{
"action": "get-logins",
"url": "<snip>",
"submitUrl": optional
}
```
Request:
```javascript
{
"action": "get-logins",
"message": encryptedMessage
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q",
"clientID": "<clientID>"
}
```
Response message data (success, decrypted):
```javascript
{
"count": "2",
"entries" : [
{
"login": "user1",
"name": "user1",
"password": "passwd1"
},
{
"login": "user2",
"name": "user2",
"password": "passwd2"
}],
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q",
"success": "true",
"hash": "29234e32274a32276e25666a42",
"version": "2.2.0"
}
```
### set-login
Unencrypted message:
```javascript
{
"action": "set-login",
"url": "<snip>",
"submitUrl": "<snip>",
"id": "testclient",
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q",
"login": "user1",
"password": "passwd1"
}
```
Request:
```javascript
{
"action": "set-login",
"message": encryptedMessage
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q",
"clientID": "<clientID>"
}
```
Response message data (success, decrypted):
```javascript
{
"count": null,
"entries" : null,
"error": "",
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q",
"success": "true",
"hash": "29234e32274a32276e25666a42",
"version": "2.2.0"
}
```
### lock-database
Request:
```javascript
{
"action": "lock-database",
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q",
"clientID": "<clientID>"
}
```
Response message data (success always returns an error, decrypted):
```javascript
{
"action": "lock-database",
"errorCode": 1,
"error": "Database not opened",
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q"
}
```
The details about the messaging protocol used with the browser extension and KeePassXC can be found [here](keepassxc-protocol.md).
## Licenses
@ -315,4 +79,4 @@ Feel free to support this project:
- Donate via [PayPal](https://paypal.me/varjolintu)
- Donate via Bitcoin: 1LHbD69CcmpLW5hjUXs2MGJhw3GxwqLdw3
Also consider donating to [KeePassXC](https://flattr.com/submit/auto?fid=x7yqz0&url=https%3A%2F%2Fkeepassxc.org) and passifox teams [(1)](https://github.com/smorks/passifox),[(2)](https://github.com/projectgus/passifox),[(3)](https://github.com/pfn/passifox). They are doing great job.
Also consider donating to [KeePassXC](https://flattr.com/submit/auto?fid=x7yqz0&url=https%3A%2F%2Fkeepassxc.org) and passifox teams [(1)](https://github.com/smorks/passifox),[(2)](https://github.com/pfn/passifox). They are doing great job.

View file

@ -1,4 +1,4 @@
var browserAction = {};
const browserAction = {};
const BLINK_TIMEOUT_DEFAULT = 7500;
const BLINK_TIMEOUT_REDIRECT_THRESHOLD_TIME_DEFAULT = -1;
@ -70,7 +70,7 @@ browserAction.showDefault = function(callback, tab) {
iconType: 'normal',
popup: 'popup.html'
};
keepass.isConfigured((response) => {
keepass.isConfigured().then((response) => {
if (!response || keepass.isDatabaseClosed || !keepass.isKeePassXCAvailable || page.tabs[tab.id].errorMessage) {
stackData.iconType = 'cross';
}

View file

@ -1,4 +1,4 @@
var kpxcEvent = {};
const kpxcEvent = {};
kpxcEvent.onMessage = function(request, sender, callback) {
if (request.action in kpxcEvent.messageHandlers) {
@ -72,7 +72,7 @@ kpxcEvent.invoke = function(handler, callback, senderTabId, args, secondTime) {
else {
console.log('undefined handler for tab ' + tab.id);
}
});
}).catch((e) => {console.log(e);});
};
kpxcEvent.onShowAlert = function(callback, tab, message) {
@ -100,35 +100,52 @@ kpxcEvent.showStatus = function(configured, tab, callback) {
};
kpxcEvent.onLoadSettings = function(callback, tab) {
page.settings = (typeof(localStorage.settings) === 'undefined') ? {} : JSON.parse(localStorage.settings);
browser.storage.local.get({'settings': {}}).then((item) => {
callback(item.settings);
}, (err) => {
console.log('error loading settings: ' + err);
});
};
kpxcEvent.onLoadKeyRing = function(callback, tab) {
keepass.keyRing = (typeof(localStorage.keyRing) === 'undefined') ? {} : JSON.parse(localStorage.keyRing);
if (keepass.isAssociated() && !keepass.keyRing[keepass.associated.hash]) {
keepass.associated = {
value: false,
hash: null
};
}
};
kpxcEvent.onGetSettings = function(callback, tab) {
kpxcEvent.onLoadSettings();
callback({ data: page.settings });
browser.storage.local.get({'keyRing': {}}).then(function(item) {
keepass.keyRing = item.keyRing;
if (keepass.isAssociated() && !keepass.keyRing[keepass.associated.hash]) {
keepass.associated = {
"value": false,
"hash": null
};
}
callback(item.keyRing);
}, (err) => {
console.log('error loading keyRing: ' + err);
});
};
kpxcEvent.onSaveSettings = function(callback, tab, settings) {
localStorage.settings = JSON.stringify(settings);
kpxcEvent.onLoadSettings();
browser.storage.local.set({'settings': settings}).then(function() {
kpxcEvent.onLoadSettings(callback, tab);
});
};
kpxcEvent.onGetStatus = function(callback, tab) {
keepass.testAssociation((response) => {
keepass.isConfigured((configured) => {
kpxcEvent.onGetStatus = function(callback, tab, internalPoll = false) {
// When internalPoll is true the event is triggered from content script in intervals -> don't poll KeePassXC
if (!internalPoll) {
keepass.testAssociation((response) => {
if (!response) {
kpxcEvent.showStatus(false, tab, callback);
return;
}
keepass.isConfigured().then((configured) => {
kpxcEvent.showStatus(configured, tab, callback);
});
}, tab, true);
} else {
keepass.isConfigured().then((configured) => {
kpxcEvent.showStatus(configured, tab, callback);
});
}, tab, true);
}
};
kpxcEvent.onReconnect = function(callback, tab) {
@ -137,13 +154,13 @@ kpxcEvent.onReconnect = function(callback, tab) {
// Add a small timeout after reconnecting. Just to make sure. It's not pretty, I know :(
setTimeout(() => {
keepass.generateNewKeyPair();
keepass.changePublicKeys(tab, (pkRes) => {
keepass.changePublicKeys(tab).then((pkRes) => {
keepass.getDatabaseHash((gdRes) => {
if (gdRes) {
keepass.testAssociation((response) => {
keepass.isConfigured((configured) => {
keepass.isConfigured().then((configured) => {
kpxcEvent.showStatus(configured, tab, callback);
});
}).catch((e) => {console.log(e);});
}, tab);
}
}, null);
@ -152,9 +169,9 @@ kpxcEvent.onReconnect = function(callback, tab) {
};
kpxcEvent.lockDatabase = function(callback, tab) {
keepass.lockDatabase((response) => {
keepass.lockDatabase(tab).then((response => {
kpxcEvent.showStatus(true, tab, callback);
}, tab);
}));
};
kpxcEvent.onPopStack = function(callback, tab) {
@ -175,12 +192,13 @@ kpxcEvent.onGetConnectedDatabase = function(callback, tab) {
};
kpxcEvent.onGetKeePassXCVersions = function(callback, tab) {
if (keepass.currentKeePassXC.version === 0) {
keepass.getDatabaseHash((response) => {
callback({current: keepass.currentKeePassXC.version, latest: keepass.latestKeePassXC.version});
if(keepass.currentKeePassXC.version == 0) {
keepass.getDatabaseHash((res) => {
callback({"current": keepass.currentKeePassXC.version, "latest": keepass.currentKeePassXC.version});
}, tab);
}
callback({current: keepass.currentKeePassXC.version, latest: keepass.latestKeePassXC.version});
} else {
callback({"current": keepass.currentKeePassXC.version, "latest": keepass.currentKeePassXC.version});
}
};
kpxcEvent.onCheckUpdateKeePassXC = function(callback, tab) {
@ -240,7 +258,7 @@ kpxcEvent.pageClearLogins = function(callback, tab) {
kpxcEvent.oldDatabaseHash = 'no-hash';
kpxcEvent.checkDatabaseHash = function(callback, tab) {
keepass.getDatabaseHash((response) => {
keepass.checkDatabaseHash((response) => {
callback({old: kpxcEvent.oldDatabaseHash, new: response});
kpxcEvent.oldDatabaseHash = response;
});
@ -254,7 +272,6 @@ kpxcEvent.messageHandlers = {
'check_update_keepassxc': kpxcEvent.onCheckUpdateKeePassXC,
'get_connected_database': kpxcEvent.onGetConnectedDatabase,
'get_keepassxc_versions': kpxcEvent.onGetKeePassXCVersions,
'get_settings': kpxcEvent.onGetSettings,
'get_status': kpxcEvent.onGetStatus,
'get_tab_information': kpxcEvent.onGetTabInformation,
'load_keyring': kpxcEvent.onLoadKeyRing,

View file

@ -1,11 +1,11 @@
var httpAuth = httpAuth || {};
const httpAuth = {};
httpAuth.requests = [];
httpAuth.pendingCallbacks = [];
httpAuth.requestCompleted = function(details) {
let index = httpAuth.requests.indexOf(details.requestId);
if (index > -1) {
if (index >= 0) {
httpAuth.requests.splice(index, 1);
}
};
@ -23,6 +23,7 @@ httpAuth.handleRequestCallback = function(details, callback) {
httpAuth.processPendingCallbacks = function(details, resolve, reject) {
if (httpAuth.requests.indexOf(details.requestId) >= 0 || !page.tabs[details.tabId]) {
reject({});
return;
}
httpAuth.requests.push(details.requestId);
@ -41,10 +42,7 @@ httpAuth.processPendingCallbacks = function(details, resolve, reject) {
httpAuth.loginOrShowCredentials = function(logins, details, resolve, reject) {
// at least one login found --> use first to login
if (logins.length > 0) {
kpxcEvent.onHTTPAuthPopup(null, { "id": details.tabId }, { "logins": logins, "url": details.searchUrl });
//generate popup-list for HTTP Auth usernames + descriptions
if (page.settings.autoFillAndSend) {
if (logins.length == 1 && page.settings.autoFillAndSend) {
resolve({
authCredentials: {
username: logins[0].login,
@ -52,7 +50,7 @@ httpAuth.loginOrShowCredentials = function(logins, details, resolve, reject) {
}
});
} else {
reject({});
kpxcEvent.onHTTPAuthPopup(null, { 'id': details.tabId }, { 'logins': logins, 'url': details.searchUrl, 'resolve': resolve });
}
}
// no logins found

View file

@ -1,15 +1,17 @@
keepass.convertKeyToKeyRing();
page.initSettings();
page.initOpenedTabs();
keepass.connectToNative();
keepass.generateNewKeyPair();
keepass.changePublicKeys(null, (pkRes) => {
keepass.getDatabaseHash((gdRes) => {}, null);
keepass.migrateKeyRing().then(() => {
page.initSettings().then(() => {
page.initOpenedTabs().then(() => {
keepass.connectToNative();
keepass.generateNewKeyPair();
keepass.changePublicKeys(null).then((pkRes) => {
keepass.getDatabaseHash((gdRes) => {}, null);
});
});
});
});
// Milliseconds for intervall (e.g. to update browserAction)
let _interval = 250;
const _interval = 250;
/**
@ -90,17 +92,23 @@ if (browser.webRequest.onAuthRequired) {
browser.runtime.onMessage.addListener(kpxcEvent.onMessage);
const contextMenuItems = [
{title: 'Fill &User + Pass', action: 'fill_user_pass'},
{title: 'Fill &Pass Only', action: 'fill_pass_only'},
{title: 'Show Password &Generator Icons', action: 'activate_password_generator'},
{title: '&Save credentials', action: 'remember_credentials'}
{title: 'Fill User + Pass', action: 'fill_user_pass'},
{title: 'Fill Pass Only', action: 'fill_pass_only'},
{title: 'Show Password Generator Icons', action: 'activate_password_generator'},
{title: 'Save credentials', action: 'remember_credentials'}
];
let menuContexts = ['editable'];
if (isFirefox()) {
menuContexts.push('password');
}
// Create context menu items
for (const item of contextMenuItems) {
browser.contextMenus.create({
title: item.title,
contexts: [ 'editable' ],
contexts: menuContexts,
onclick: (info, tab) => {
browser.tabs.sendMessage(tab.id, {
action: item.action

View file

@ -1,6 +1,6 @@
'use strict';
var keepass = {};
const keepass = {};
keepass.associated = {'value': false, 'hash': null};
keepass.keyPair = {publicKey: null, secretKey: null};
@ -11,7 +11,6 @@ keepass.isDatabaseClosed = false;
keepass.isKeePassXCAvailable = false;
keepass.isEncryptionKeyUnrecognized = false;
keepass.currentKeePassXC = {'version': 0, 'versionParsed': 0};
keepass.latestKeePassXC = (typeof(localStorage.latestKeePassXC) === 'undefined') ? {'version': 0, 'versionParsed': 0, 'lastChecked': null} : JSON.parse(localStorage.latestKeePassXC);
keepass.requiredKeePassXC = 220;
keepass.nativeHostName = 'com.varjolintu.keepassxc_browser';
keepass.nativePort = null;
@ -19,10 +18,10 @@ 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.keyRing = (typeof(localStorage.keyRing) === 'undefined') ? {} : JSON.parse(localStorage.keyRing);
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',
@ -77,6 +76,55 @@ const kpErrors = {
}
};
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);
};
@ -86,8 +134,7 @@ keepass.updateCredentials = function(callback, tab, entryId, username, password,
page.tabs[tab.id].errorMessage = null;
keepass.testAssociation((response) => {
if (!response)
{
if (!response) {
browserAction.showDefault(null, tab);
callback([]);
return;
@ -95,7 +142,8 @@ keepass.updateCredentials = function(callback, tab, entryId, username, password,
const kpAction = kpActions.SET_LOGIN;
const {dbid} = keepass.getCryptoKey();
const nonce = nacl.randomBytes(keepass.keySize);
const nonce = keepass.getNonce();
const incrementedNonce = keepass.incrementedNonce(nonce);
let messageData = {
action: kpAction,
@ -113,19 +161,22 @@ keepass.updateCredentials = function(callback, tab, entryId, username, password,
const request = {
action: kpAction,
message: keepass.encrypt(messageData, nonce),
nonce: keepass.b64e(nonce),
nonce: nonce,
clientID: keepass.clientID
};
console.log(request);
keepass.callbackOnId(keepass.nativePort.onMessage, kpAction, tab, (response) => {
keepass.sendNativeMessage(request).then((response) => {
if (response.message && response.nonce) {
const res = keepass.decrypt(response.message, response.nonce);
if (res) {
const message = nacl.util.encodeUTF8(res);
const parsed = JSON.parse(message);
callback(keepass.verifyResponse(parsed, response.nonce) ? 'success' : 'error');
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);
@ -134,7 +185,6 @@ keepass.updateCredentials = function(callback, tab, entryId, username, password,
browserAction.showDefault(null, tab);
}
});
keepass.nativePort.postMessage(request);
});
};
@ -142,8 +192,7 @@ keepass.retrieveCredentials = function(callback, tab, url, submiturl, forceCallb
page.debug('keepass.retrieveCredentials(callback, {1}, {2}, {3}, {4})', tab.id, url, submiturl, forceCallback);
keepass.testAssociation((response) => {
if (!response)
{
if (!response) {
browserAction.showDefault(null, tab);
if (forceCallback) {
callback([]);
@ -160,7 +209,8 @@ keepass.retrieveCredentials = function(callback, tab, url, submiturl, forceCallb
let entries = [];
const kpAction = kpActions.GET_LOGINS;
const nonce = nacl.randomBytes(keepass.keySize);
const nonce = keepass.getNonce();
const incrementedNonce = keepass.incrementedNonce(nonce);
const {dbid} = keepass.getCryptoKey();
let messageData = {
@ -176,32 +226,36 @@ keepass.retrieveCredentials = function(callback, tab, url, submiturl, forceCallb
const request = {
action: kpAction,
message: keepass.encrypt(messageData, nonce),
nonce: keepass.b64e(nonce),
nonce: nonce,
clientID: keepass.clientID
};
keepass.callbackOnId(keepass.nativePort.onMessage, kpAction, tab, (response) => {
keepass.sendNativeMessage(request).then((response) => {
if (response.message && response.nonce) {
const res = keepass.decrypt(response.message, response.nonce);
if (res) {
const message = nacl.util.encodeUTF8(res);
const parsed = JSON.parse(message);
keepass.setcurrentKeePassXCVersion(parsed.version);
if (keepass.verifyResponse(parsed, response.nonce)) {
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);
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);
@ -210,42 +264,9 @@ keepass.retrieveCredentials = function(callback, tab, url, submiturl, forceCallb
browserAction.showDefault(null, tab);
}
});
keepass.nativePort.postMessage(request);
}, tab);
};
// Redirects the callback to a listener (handleReply())
keepass.callbackOnId = function(ev, action, tab, callback, enableTimeout = false) {
let timeout;
let listener = ((port, action) => {
let handler = (msg) => {
if (msg && msg.action === action) {
ev.removeListener(handler);
if (enableTimeout) {
clearTimeout(timeout);
}
callback(msg);
}
};
return handler;
})(ev, action, tab, callback);
ev.addListener(listener);
// Handle timeouts
if (enableTimeout) {
timeout = setTimeout(() => {
const errorMessage = {
action: action,
error: kpErrors.getError(5),
errorCode: 5
};
keepass.isKeePassXCAvailable = false;
callback(errorMessage);
ev.removeListener(listener.handler);
}, keepass.messageTimeout);
}
};
keepass.generatePassword = function(callback, tab, forceCallback) {
if (!keepass.isConnected) {
callback([]);
@ -253,8 +274,7 @@ keepass.generatePassword = function(callback, tab, forceCallback) {
}
keepass.testAssociation((taresponse) => {
if (!taresponse)
{
if (!taresponse) {
browserAction.showDefault(null, tab);
if (forceCallback) {
callback([]);
@ -269,42 +289,46 @@ keepass.generatePassword = function(callback, tab, forceCallback) {
let passwords = [];
const kpAction = kpActions.GENERATE_PASSWORD;
const nonce = nacl.randomBytes(keepass.keySize);
const nonce = keepass.getNonce();
const incrementedNonce = keepass.incrementedNonce(nonce);
const request = {
action: kpAction,
nonce: keepass.b64e(nonce),
nonce: nonce,
clientID: keepass.clientID
};
keepass.callbackOnId(keepass.nativePort.onMessage, kpAction, tab, (response) => {
keepass.sendNativeMessage(request).then((response) => {
if (response.message && response.nonce) {
const res = keepass.decrypt(response.message, response.nonce);
if (res) {
const message = nacl.util.encodeUTF8(res);
const parsed = JSON.parse(message);
keepass.setcurrentKeePassXCVersion(parsed.version);
if (!res) {
keepass.handleError(tab, kpErrors.CANNOT_DECRYPT_MESSAGE);
callback([]);
return;
}
if (keepass.verifyResponse(parsed, response.nonce)) {
if (parsed.entries) {
passwords = parsed.entries;
keepass.updateLastUsed(keepass.databaseHash);
}
else {
console.log('No entries returned. Is KeePassXC up-to-date?');
}
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('GeneratePassword rejected');
console.log('No entries returned. Is KeePassXC up-to-date?');
}
callback(passwords);
}
else {
console.log('GeneratePassword rejected');
}
callback(passwords);
}
else if (response.error && response.errorCode) {
keepass.handleError(tab, response.errorCode, response.error);
}
});
keepass.nativePort.postMessage(request);
}, tab);
};
@ -323,8 +347,9 @@ keepass.associate = function(callback, tab) {
page.tabs[tab.id].errorMessage = null;
const kpAction = kpActions.ASSOCIATE;
const key = keepass.b64e(keepass.keyPair.publicKey);
const nonce = nacl.randomBytes(keepass.keySize);
const key = nacl.util.encodeBase64(keepass.keyPair.publicKey);
const nonce = keepass.getNonce();
const incrementedNonce = keepass.incrementedNonce(nonce);
const messageData = {
action: kpAction,
@ -334,36 +359,38 @@ keepass.associate = function(callback, tab) {
const request = {
action: kpAction,
message: keepass.encrypt(messageData, nonce),
nonce: keepass.b64e(nonce),
nonce: nonce,
clientID: keepass.clientID
};
keepass.callbackOnId(keepass.nativePort.onMessage, kpAction, tab, (response) => {
keepass.sendNativeMessage(request).then((response) => {
if (response.message && response.nonce) {
const res = keepass.decrypt(response.message, response.nonce);
if (res) {
const message = nacl.util.encodeUTF8(res);
const parsed = JSON.parse(message);
keepass.setcurrentKeePassXCVersion(parsed.version);
const id = parsed.id;
if (!keepass.verifyResponse(parsed, response.nonce)) {
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);
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);
}
});
keepass.nativePort.postMessage(request);
}, tab);
};
@ -397,7 +424,8 @@ keepass.testAssociation = function(callback, tab, enableTimeout = false) {
}
const kpAction = kpActions.TEST_ASSOCIATE;
const nonce = nacl.randomBytes(keepass.keySize);
const nonce = keepass.getNonce();
const incrementedNonce = keepass.incrementedNonce(nonce);
const {dbid, dbkey} = keepass.getCryptoKey();
if (dbkey === null || dbid === null) {
@ -417,34 +445,38 @@ keepass.testAssociation = function(callback, tab, enableTimeout = false) {
const request = {
action: kpAction,
message: keepass.encrypt(messageData, nonce),
nonce: keepass.b64e(nonce),
nonce: nonce,
clientID: keepass.clientID
};
keepass.callbackOnId(keepass.nativePort.onMessage, kpAction, tab, (response) => {
keepass.sendNativeMessage(request, enableTimeout).then((response) => {
if (response.message && response.nonce) {
const res = keepass.decrypt(response.message, response.nonce);
if (res) {
const message = nacl.util.encodeUTF8(res);
const parsed = JSON.parse(message);
keepass.setcurrentKeePassXCVersion(parsed.version);
keepass.isEncryptionKeyUnrecognized = false;
if (!res) {
keepass.handleError(tab, kpErrors.CANNOT_DECRYPT_MESSAGE);
callback(false);
return;
}
if (!keepass.verifyResponse(parsed, response.nonce)) {
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;
}
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;
}
}
}
@ -453,7 +485,6 @@ keepass.testAssociation = function(callback, tab, enableTimeout = false) {
}
callback(keepass.isAssociated());
});
keepass.nativePort.postMessage(request);
}, tab, enableTimeout);
};
@ -465,11 +496,12 @@ keepass.getDatabaseHash = function(callback, tab, enableTimeout = false) {
}
if (!keepass.serverPublicKey) {
keepass.changePublicKeys(tab, null);
keepass.changePublicKeys(tab);
}
const kpAction = kpActions.GET_DATABASE_HASH;
const nonce = nacl.randomBytes(keepass.keySize);
const nonce = keepass.getNonce();
const incrementedNonce = keepass.incrementedNonce(nonce);
const messageData = {
action: kpAction
@ -485,173 +517,202 @@ keepass.getDatabaseHash = function(callback, tab, enableTimeout = false) {
const request = {
action: kpAction,
message: encrypted,
nonce: keepass.b64e(nonce),
nonce: nonce,
clientID: keepass.clientID
};
keepass.callbackOnId(keepass.nativePort.onMessage, kpAction, tab, (response) => {
keepass.sendNativeMessage(request, enableTimeout).then((response) => {
if (response.message && response.nonce) {
const res = keepass.decrypt(response.message, response.nonce);
if (res) {
const message = nacl.util.encodeUTF8(res);
const parsed = JSON.parse(message);
if (!res) {
keepass.handleError(tab, kpErrors.CANNOT_DECRYPT_MESSAGE);
callback('no-hash');
return;
}
if (parsed.hash) {
const oldDatabaseHash = keepass.databaseHash;
keepass.setcurrentKeePassXCVersion(parsed.version);
keepass.databaseHash = parsed.hash || 'no-hash';
const message = nacl.util.encodeUTF8(res);
const parsed = JSON.parse(message);
if (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);
}
else if (parsed.errorCode) {
keepass.databaseHash = 'no-hash';
keepass.isDatabaseClosed = true;
keepass.handleError(tab, kpErrors.DATABASE_NOT_OPENED);
callback(keepass.databaseHash);
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;
keepass.isKeePassXCAvailable = false;
if (response.message === "") {
if (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;
}
}, enableTimeout);
keepass.nativePort.postMessage(request);
};
keepass.changePublicKeys = function(tab, callback) {
if (!keepass.isConnected) {
callback([]);
return;
}
const kpAction = kpActions.CHANGE_PUBLIC_KEYS;
const key = keepass.b64e(keepass.keyPair.publicKey);
let nonce = nacl.randomBytes(keepass.keySize);
nonce = keepass.b64e(nonce);
keepass.clientID = keepass.b64e(nacl.randomBytes(keepass.keySize));
const message = {
action: kpAction,
publicKey: key,
proxyPort: (page.settings.port ? page.settings.port : 19700),
nonce: nonce,
clientID: keepass.clientID
};
keepass.callbackOnId(keepass.nativePort.onMessage, kpAction, tab, (response) => {
keepass.setcurrentKeePassXCVersion(response.version);
if (!keepass.verifyKeyResponse(response, key, nonce)) {
if (tab && page.tabs[tab.id]) {
keepass.handleError(tab, kpErrors.KEY_CHANGE_FAILED);
callback(false);
}
}
else {
keepass.isKeePassXCAvailable = true;
console.log('Server public key: ' + keepass.b64e(keepass.serverPublicKey));
}
callback(true);
});
keepass.nativePort.postMessage(message);
};
keepass.lockDatabase = function(callback, tab, forceCallback) {
if (!keepass.isConnected) {
keepass.handleError(tab, kpErrors.TIMEOUT_OR_NOT_CONNECTED);
callback([]);
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.LOCK_DATABASE;
const nonce = nacl.randomBytes(keepass.keySize);
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 messageData = {
action: kpAction
};
const request = {
action: kpAction,
publicKey: key,
nonce: nonce,
clientID: keepass.clientID
};
const request = {
action: kpAction,
message: keepass.encrypt(messageData, nonce),
nonce: keepass.b64e(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;
}
keepass.callbackOnId(keepass.nativePort.onMessage, kpAction, tab, (response) => {
if (response.message && response.nonce) {
const res = keepass.decrypt(response.message, response.nonce);
if (res) {
const message = nacl.util.encodeUTF8(res);
const parsed = JSON.parse(message);
keepass.setcurrentKeePassXCVersion(parsed.version);
if (keepass.verifyResponse(parsed, response.nonce)) {
if (keepass.verifyResponse(parsed, incrementedNonce)) {
keepass.isDatabaseClosed = true;
// Display error message in the popup
keepass.handleError(tab, kpErrors.DATABASE_NOT_OPENED);
callback(false);
resolve(true);
}
}
}
else if (response.error && response.errorCode) {
keepass.handleError(tab, response.errorCode, response.error);
}
callback(false);
else if (response.error && response.errorCode) {
keepass.isDatabaseClosed = true;
keepass.handleError(tab, response.errorCode, response.error);
}
resolve(false);
});
});
keepass.nativePort.postMessage(request);
};
keepass.generateNewKeyPair = function() {
keepass.keyPair = nacl.box.keyPair();
//console.log(keepass.b64e(keepass.keyPair.publicKey) + ' ' + keepass.b64e(keepass.keyPair.secretKey));
//console.log(nacl.util.encodeBase64(keepass.keyPair.publicKey) + ' ' + nacl.util.encodeBase64(keepass.keyPair.secretKey));
};
keepass.isConfigured = function(callback) {
if (typeof(keepass.databaseHash) === 'undefined') {
keepass.getDatabaseHash((dbHash) => {
callback(keepass.databaseHash in keepass.keyRing);
}, null);
}
else {
callback(keepass.databaseHash in keepass.keyRing);
}
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.convertKeyToKeyRing = function() {
if (keepass.keyId in localStorage && keepass.keyBody in localStorage && !('keyRing' in localStorage)) {
keepass.getDatabaseHash((hash) => {
keepass.saveKey(hash, localStorage[keepass.keyId], localStorage[keepass.keyBody]);
if ('keyRing' in localStorage) {
delete localStorage[keepass.keyId];
delete localStorage[keepass.keyBody];
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 });
}
}
}, null);
}
if ('keyRing' in localStorage) {
delete localStorage[keepass.keyId];
delete localStorage[keepass.keyBody];
}
resolve();
});
});
};
keepass.saveKey = function(hash, id, key) {
@ -660,8 +721,8 @@ keepass.saveKey = function(hash, id, key) {
id: id,
key: key,
hash: hash,
created: new Date(),
lastUsed: new Date()
created: new Date().valueOf(),
lastUsed: new Date().valueOf()
};
}
else {
@ -669,19 +730,19 @@ keepass.saveKey = function(hash, id, key) {
keepass.keyRing[hash].key = key;
keepass.keyRing[hash].hash = hash;
}
localStorage.keyRing = JSON.stringify(keepass.keyRing);
browser.storage.local.set({'keyRing': keepass.keyRing});
};
keepass.updateLastUsed = function(hash) {
if ((hash in keepass.keyRing)) {
keepass.keyRing[hash].lastUsed = new Date();
localStorage.keyRing = JSON.stringify(keepass.keyRing);
keepass.keyRing[hash].lastUsed = new Date().valueOf();
browser.storage.local.set({'keyRing': keepass.keyRing});
}
};
keepass.deleteKey = function(hash) {
delete keepass.keyRing[hash];
localStorage.keyRing = JSON.stringify(keepass.keyRing);
browser.storage.local.set({'keyRing': keepass.keyRing});
};
keepass.setcurrentKeePassXCVersion = function(version) {
@ -695,7 +756,7 @@ keepass.setcurrentKeePassXCVersion = function(version) {
keepass.keePassXCUpdateAvailable = function() {
if (page.settings.checkUpdateKeePassXC && page.settings.checkUpdateKeePassXC > 0) {
const lastChecked = (keepass.latestKeePassXC.lastChecked) ? new Date(keepass.latestKeePassXC.lastChecked) : new Date('11/21/1986');
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();
@ -720,7 +781,7 @@ keepass.checkForNewKeePassXCVersion = function() {
}
if (version !== -1) {
localStorage.latestKeePassXC = JSON.stringify(keepass.latestKeePassXC);
browser.storage.local.set({'latestKeePassXC': keepass.latestKeePassXC});
}
};
@ -735,13 +796,14 @@ keepass.checkForNewKeePassXCVersion = function() {
catch (ex) {
console.log(ex);
}
keepass.latestKeePassXC.lastChecked = new Date();
keepass.latestKeePassXC.lastChecked = new Date().valueOf();
};
keepass.connectToNative = function() {
if (!keepass.isConnected) {
keepass.nativeConnect();
if (keepass.nativePort) {
keepass.nativePort.disconnect();
}
keepass.nativeConnect();
};
keepass.onNativeMessage = function(response) {
@ -750,7 +812,7 @@ keepass.onNativeMessage = function(response) {
// Handle database lock/unlock status
if (response.action === kpActions.DATABASE_LOCKED || response.action === kpActions.DATABASE_UNLOCKED) {
keepass.testAssociation((response) => {
keepass.isConfigured((configured) => {
keepass.isConfigured().then((configured) => {
let data = page.tabs[page.currentTabId].stack[page.tabs[page.currentTabId].stack.length - 1];
data.iconType = configured ? 'normal' : 'cross';
browserAction.show(null, {'id': page.currentTabId});
@ -769,6 +831,26 @@ function onDisconnected() {
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);
@ -784,13 +866,14 @@ keepass.verifyKeyResponse = function(response, key, nonce) {
}
let reply = false;
if (keepass.b64d(nonce).length !== nacl.secretbox.nonceLength)
if (nacl.util.decodeBase64(nonce).length !== nacl.secretbox.nonceLength) {
return false;
}
reply = (response.nonce === nonce);
if (response.publicKey) {
keepass.serverPublicKey = keepass.b64d(response.publicKey);
keepass.serverPublicKey = nacl.util.decodeBase64(response.publicKey);
reply = true;
}
@ -806,10 +889,14 @@ keepass.verifyResponse = function(response, nonce, id) {
keepass.associated.hash = keepass.databaseHash;
if (keepass.b64d(response.nonce).length !== nacl.secretbox.nonceLength)
if (nacl.util.decodeBase64(response.nonce).length !== nacl.secretbox.nonceLength) {
return false;
}
keepass.associated.value = (response.nonce === nonce);
if (keepass.associated.value === false) {
console.log("Compare failed");
}
if (id) {
keepass.associated.value = (keepass.associated.value && id === response.id);
@ -830,14 +917,6 @@ keepass.handleError = function(tab, errorCode, errorMessage = '') {
}
};
keepass.b64e = function(d) {
return nacl.util.encodeBase64(d);
};
keepass.b64d = function(d) {
return nacl.util.decodeBase64(d);
};
keepass.getCryptoKey = function() {
let dbkey = null;
let dbid = null;
@ -860,19 +939,20 @@ keepass.setCryptoKey = function(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, nonce, keepass.serverPublicKey, keepass.keyPair.secretKey);
const message = nacl.box(messageData, messageNonce, keepass.serverPublicKey, keepass.keyPair.secretKey);
if (message) {
return keepass.b64e(message);
return nacl.util.encodeBase64(message);
}
}
return '';
};
keepass.decrypt = function(input, nonce, toStr) {
const m = keepass.b64d(input);
const n = keepass.b64d(nonce);
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;
};

View file

@ -4,56 +4,59 @@ const defaultSettings = {
autoFillAndSend: true,
usePasswordGenerator: true,
autoFillSingleEntry: false,
autoRetrieveCredentials: true,
proxyPort: '19700'
autoRetrieveCredentials: true
};
var page = {};
page.tabs = {};
page.currentTabId = -1;
page.settings = (typeof(localStorage.settings) === 'undefined') ? {} : JSON.parse(localStorage.settings);
page.blockedTabs = {};
page.initSettings = function() {
kpxcEvent.onLoadSettings();
if (!('checkUpdateKeePassXC' in page.settings)) {
page.settings.checkUpdateKeePassXC = defaultSettings.checkUpdateKeePassXC;
}
if (!('autoCompleteUsernames' in page.settings)) {
page.settings.autoCompleteUsernames = defaultSettings.autoCompleteUsernames;
}
if (!('autoFillAndSend' in page.settings)) {
page.settings.autoFillAndSend = defaultSettings.autoFillAndSend;
}
if (!('usePasswordGenerator' in page.settings)) {
page.settings.usePasswordGenerator = defaultSettings.usePasswordGenerator;
}
if (!('autoFillSingleEntry' in page.settings)) {
page.settings.autoFillSingleEntry = defaultSettings.autoFillSingleEntry;
}
if (!('autoRetrieveCredentials' in page.settings)) {
page.settings.autoRetrieveCredentials = defaultSettings.autoRetrieveCredentials;
}
if (!('port' in page.settings)) {
page.settings.port = defaultSettings.proxyPort;
}
localStorage.settings = JSON.stringify(page.settings);
return new Promise((resolve, reject) => {
kpxcEvent.onLoadSettings((settings) => {
page.settings = settings;
if (!('checkUpdateKeePassXC' in page.settings)) {
page.settings.checkUpdateKeePassXC = defaultSettings.checkUpdateKeePassXC;
}
if (!('autoCompleteUsernames' in page.settings)) {
page.settings.autoCompleteUsernames = defaultSettings.autoCompleteUsernames;
}
if (!('autoFillAndSend' in page.settings)) {
page.settings.autoFillAndSend = defaultSettings.autoFillAndSend;
}
if (!('usePasswordGenerator' in page.settings)) {
page.settings.usePasswordGenerator = defaultSettings.usePasswordGenerator;
}
if (!('autoFillSingleEntry' in page.settings)) {
page.settings.autoFillSingleEntry = defaultSettings.autoFillSingleEntry;
}
if (!('autoRetrieveCredentials' in page.settings)) {
page.settings.autoRetrieveCredentials = defaultSettings.autoRetrieveCredentials;
}
browser.storage.local.set({'settings': page.settings});
resolve();
});
});
};
page.initOpenedTabs = function() {
browser.tabs.query({}).then((tabs) => {
for (const i of tabs) {
page.createTabEntry(i.id);
}
// set initial tab-ID
browser.tabs.query({ "active": true, "currentWindow": true }).then((tabs) => {
if (tabs.length === 0) {
return; // For example: only the background devtools or a popup are opened
return new Promise((resolve, reject) => {
browser.tabs.query({}).then((tabs) => {
for (const i of tabs) {
page.createTabEntry(i.id);
}
page.currentTabId = tabs[0].id;
browserAction.show(null, tabs[0]);
// set initial tab-ID
browser.tabs.query({ 'active': true, 'currentWindow': true }).then((tabs) => {
if (tabs.length === 0) {
resolve();
return; // For example: only the background devtools or a popup are opened
}
page.currentTabId = tabs[0].id;
browserAction.show(null, tabs[0]);
resolve();
});
});
});
};
@ -66,7 +69,7 @@ page.isValidProtocol = function(url) {
page.switchTab = function(callback, tab) {
browserAction.showDefault(null, tab);
browser.tabs.sendMessage(tab.id, {action: 'activated_tab'}).catch((e) => {console.log(e);});
browser.tabs.sendMessage(tab.id, {action: 'activated_tab'}).catch((e) => {});
};
page.clearCredentials = function(tabId, complete) {
@ -82,7 +85,7 @@ page.clearCredentials = function(tabId, complete) {
browser.tabs.sendMessage(tabId, {
action: 'clear_credentials'
}).catch((e) => {console.log(e);});
}).catch((e) => {});
}
};
@ -101,7 +104,7 @@ page.createTabEntry = function(tabId) {
page.removePageInformationFromNotExistingTabs = function() {
let rand = Math.floor(Math.random()*1001);
if (rand === 28) {
browser.tabs.query({}, (tabs) => {
browser.tabs.query({}).then(function(tabs) {
let $tabIds = {};
const $infoIds = Object.keys(page.tabs);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,38 +1,47 @@
.cip-ui-autocomplete li.cip-ui-menu-item {
.kpxc .ui-autocomplete li.ui-menu-item {
text-align: left !important;
font-size: 12px !important;
font-size: .9em !important;
font-weight: normal !important;
font-style: normal !important;
font-family: Verdana, Arial, sans-serif !important;
color: #222222 !important;
}
.ui-dialog-titlebar-close {
.ui-helper-hidden-accessible {
display: none !important;
}
.kpxc .ui-dialog-titlebar-close {
visibility: hidden !important;
}
.ui-dialog {
.kpxc .ui-dialog {
font-size: 12px !important;
}
.dialog-form .ui-dialog-content .ui-widget-content {
.kpxc .ui-widget-overlay {
opacity: 0.0 !important;
}
.kpxc .dialog-form .ui-dialog-content .ui-widget-content {
max-height: 80px !important;
}
.ui-dialog-titlebar {
.kpxc .ui-dialog-titlebar {
background-color: #3a8233;
color: #fff;
}
.ui-dialog .ui-dialog-buttonpane {
.kpxc .ui-dialog .ui-dialog-buttonpane {
text-align: center !important;
}
.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset {
.kpxc .ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset {
float: none !important;
}
.ui-button .ui-button-text .ui-button {
font-size: .10em !important;
.kpxc .ui-button .ui-button-text .ui-button {
font-size: .10em !important;
}
input.genpw-text {
@ -44,6 +53,7 @@ input.genpw-text {
border-collapse: separate;
width: 100%;
}
.genpw-input-group-addon {
font-size: inherit !important;
background-color: #eee;
@ -69,10 +79,12 @@ input.genpw-text {
position: absolute;
cursor: pointer;
}
.cip-genpw-icon.key {
background: url('chrome-extension://__MSG_@@extension_id__/icons/key.png') right no-repeat;
background-size: contain;
}
.cip-genpw-icon.key-moz {
background: url('moz-extension://__MSG_@@extension_id__/icons/key.png') right no-repeat;
background-size: contain;
@ -90,6 +102,7 @@ input.genpw-text {
left: 0;
z-index: 2147483645;
}
.b2c-modal-backdrop:after {
content:'';
position: fixed;
@ -101,9 +114,11 @@ input.genpw-text {
opacity: 0.8;
filter: alpha(opacity=80);
}
#b2c-cipDefine-fields {
z-index: 2147483646;
}
#b2c-cipDefine-description {
z-index: 2147483646;
color: #efefef;
@ -117,6 +132,7 @@ input.genpw-text {
background-color:rgba(255,255,255,0.3);
font-size: 15px;
}
#b2c-cipDefine-description div:first-of-type {
margin-top: 0;
padding-top: 0;
@ -126,6 +142,7 @@ input.genpw-text {
font-weight: bold;
font-size: 160%;
}
#b2c-cipDefine-description p {
margin-top: 10px;
padding-top: 10px;
@ -133,9 +150,11 @@ input.genpw-text {
border-top: 2px solid #666666;
line-height: 110%;
}
#b2c-help {
margin-bottom: 3px;
}
.b2c-fixed-field {
position: absolute;
border: 2px solid #efefef;
@ -145,27 +164,30 @@ input.genpw-text {
font-weight: bold;
background-color:rgba(239,239,239,0.4);
}
.b2c-fixed-hover-field {
border: 2px solid orange;
background-color:rgba(255,165,239,0.4);
}
.b2c-fixed-password-field {
border: 2px solid red;
color: #efefef;
background-color:rgba(255,0,0,0.4);
}
.b2c-fixed-username-field {
border: 2px solid limegreen;
color: #efefef;
background-color:rgba(50,205,50,0.4);
}
.b2c-fixed-string-field {
border: 2px solid deepskyblue;
color: #efefef;
background-color:rgba(30,144,255,0.4);
}
.b2c-input-append {
display: inline-block;
margin-bottom: 10px;
@ -197,4 +219,4 @@ input.genpw-text {
.b2c-input-append select:focus,
.b2c-input-append .b2c-b2c-uneditable-input:focus {
z-index: 2;
}
}

View file

@ -4,6 +4,9 @@ $(this.target).find('input').autocomplete();
// contains already called method names
var _called = {};
// Count of detected form fields on the page
var _detectedFields = 0;
browser.runtime.onMessage.addListener(function(req, sender, callback) {
if ('action' in req) {
if (req.action === 'fill_user_pass_with_specific_login') {
@ -46,13 +49,15 @@ browser.runtime.onMessage.addListener(function(req, sender, callback) {
}
else if (req.action === 'clear_credentials') {
cipEvents.clearCredentials();
callback();
}
else if (req.action === 'activated_tab') {
cipEvents.triggerActivatedTab();
callback();
}
else if (req.action === 'redetect_fields') {
browser.runtime.sendMessage({
action: 'get_settings',
action: 'load_settings',
}).then((response) => {
cip.settings = response.data;
cip.initCredentialFields(true);
@ -78,13 +83,19 @@ var cipAutocomplete = {};
cipAutocomplete.elements = [];
cipAutocomplete.init = function(field) {
if (field.hasClass('ui-autocomplete-input')) {
//_f(credentialInputs[i].username).autocomplete('source', autocompleteSource);
if (cip.settings.autoFillSingleEntry && cip.credentials.length === 1 && field.hasClass('ui-autocomplete-input')) {
field.autocomplete('destroy');
}
let acMenu = jQuery('#kpxc-ac-menu');
if (acMenu.length == 0) {
jQuery('<div id="kpxc-ac-menu" class="kpxc"></div>').appendTo('body');
}
field
.addClass('kpxc')
.autocomplete({
appendTo: '#kpxc-ac-menu',
minLength: 0,
source: cipAutocomplete.onSource,
select: cipAutocomplete.onSelect,
@ -101,12 +112,10 @@ cipAutocomplete.onClick = function() {
};
cipAutocomplete.onOpen = function(event, ui) {
// NOT BEAUTIFUL!
// modifies ALL ui-autocomplete menus of class .cip-ui-menu
jQuery('ul.ui-autocomplete.ui-menu').css('z-index', 2147483636);
};
cipAutocomplete.onSource = function (request, callback) {
cipAutocomplete.onSource = function(request, callback) {
const matches = jQuery.map(cipAutocomplete.elements, (tag) => {
if (tag.label.toUpperCase().indexOf(request.term.toUpperCase()) === 0) {
return tag;
@ -115,7 +124,7 @@ cipAutocomplete.onSource = function (request, callback) {
callback(matches);
};
cipAutocomplete.onSelect = function (e, ui) {
cipAutocomplete.onSelect = function(e, ui) {
e.preventDefault();
cip.setValueWithChange(jQuery(this), ui.item.value);
const fieldId = cipFields.prepareId(jQuery(this).attr('data-cip-id'));
@ -237,6 +246,7 @@ cipPassword.createDialog = function() {
$dialog.hide();
jQuery('body').append($dialog);
$dialog.dialog({
autoOpen: false,
modal: true,
@ -254,7 +264,7 @@ cipPassword.createDialog = function() {
e.preventDefault();
browser.runtime.sendMessage({
action: 'generate_password'
}).then(cipPassword.callbackGeneratedPassword);
}).then(cipPassword.callbackGeneratedPassword).catch((e) => {console.log(e);});
}
},
'Copy':
@ -304,14 +314,20 @@ cipPassword.createDialog = function() {
}
},
open: function(event, ui) {
// Dirty hacks for overlay and custom CSS
jQuery('.ui-widget-overlay').wrap('<span class="kpxc"></span>');
jQuery('.ui-widget-overlay').click(function() {
jQuery('#cip-genpw-dialog:first').dialog('close');
jQuery('span').remove('.kpxc');
});
if (jQuery('input#cip-genpw-textfield-password:first').val() === '') {
jQuery('button#cip-genpw-btn-generate:first').click();
}
}
},
create: function(event, ui) {
jQuery('.ui-dialog').wrap('<div class="kpxc"></span>');
}
});
};
@ -394,7 +410,6 @@ cipPassword.callbackPasswordCopied = function(bool) {
cipPassword.callbackGeneratedPassword = function(entries) {
if (entries && entries.length >= 1) {
console.log(entries[0]);
jQuery('#cip-genpw-btn-clipboard:first').removeClass('btn-success');
jQuery('input#cip-genpw-textfield-password:first').val(entries[0].password);
if (isNaN(entries[0].login)) {
@ -406,7 +421,7 @@ cipPassword.callbackGeneratedPassword = function(entries) {
}
else {
if (jQuery('div#cip-genpw-error:first').length === 0) {
jQuery('button#cip-genpw-btn-generate:first').after('<div style=\'block\' id=\'cip-genpw-error\'>Cannot receive generated password.<br />Is your version of KeePassXC up-to-date?<br /><br /><a href=\'https://keepassxc.org\'>Please visit the KeePassXC homepage</a></div>');
jQuery('button#cip-genpw-btn-generate:first').after('<div style=\'block\' id=\'cip-genpw-error\'>Cannot receive generated password.<br />Is KeePassXC opened?<br /></div>');
jQuery('input#cip-genpw-textfield-password:first').parent().hide();
jQuery('input#cip-genpw-checkbox-next-field:first').parent('label').hide();
jQuery('button#cip-genpw-btn-generate').hide();
@ -805,6 +820,7 @@ cipFields.getAllFields = function() {
}
});
_detectedFields = fields.length;
return fields;
};
@ -1074,7 +1090,7 @@ cipFields.useDefinedCredentialFields = function() {
}
if ($found) {
var fields = {
let fields = {
username: creds.username,
password: creds.password,
fields: creds.fields
@ -1107,31 +1123,30 @@ jQuery(function() {
cip.init = function() {
browser.runtime.sendMessage({
action: 'get_settings',
action: 'load_settings',
}).then((response) => {
cip.settings = response.data;
cip.initCredentialFields();
cip.settings = response;
cip.initCredentialFields(true);
});
};
cip.detectNewActiveFields = function() {
const hiddenFields = cipFields.getHiddenFieldCount();
// If hidden fields aren't detected, setInterval is being looped in each frame of the page
//if (hiddenFields > 0) {
const divDetect = setInterval(function() {
const divDetect = setInterval(function() {
if (document.visibilityState !== 'hidden') {
const fields = cipFields.getAllFields();
if (fields.length > 1) {
// If only password field is shown it's enough to have one field visible for initCredentialFields
if (fields.length > (_detectedFields == 1 ? 0 : 1)) {
cip.initCredentialFields(true);
clearInterval(divDetect);
}
}, 1000);
//}
}
}, 1000);
};
// Switch credentials if database is changed or closed
cip.detectDatabaseChange = function() {
const dbDetectInterval = setInterval(function() {
let dbDetectInterval = setInterval(function() {
if (document.visibilityState !== 'hidden') {
browser.runtime.sendMessage({
action: 'check_databasehash'
@ -1145,21 +1160,22 @@ cip.detectDatabaseChange = function() {
// Switch back to default popup
browser.runtime.sendMessage({
action: 'get_status'
action: 'get_status',
args: [ true ] // Set polling to true, this is an internal function call
});
} else {
if (response.new !== 'no-hash' && response.new !== response.old) {
browser.runtime.sendMessage({
action: 'get_settings',
action: 'load_settings',
}).then((response) => {
cip.settings = response.data;
cip.settings = response;
cip.initCredentialFields(true);
});
}
}
});
}).catch((e) => {console.log(e);});
}
}, 2000);
}, 1000);
};
cip.initCredentialFields = function(forceCall) {
@ -1193,7 +1209,7 @@ cip.initCredentialFields = function(forceCall) {
browser.runtime.sendMessage({
action: 'retrieve_credentials',
args: [ cip.url, cip.submitUrl ]
}).then(cip.retrieveCredentialsCallback);
}).then(cip.retrieveCredentialsCallback).catch((e) => {console.log(e);});
}
});
};
@ -1293,8 +1309,16 @@ cip.preparePageForMultipleCredentials = function(credentials) {
// initialize autocomplete for username fields
if (cip.settings.autoCompleteUsernames) {
for (const i of cipFields.combinations) {
if (_f(i.username)) {
cipAutocomplete.init(_f(i.username));
// Both username and password fields are visible
if (_detectedFields >= 2) {
if (_f(i.username)) {
cipAutocomplete.init(_f(i.username));
}
} else if (_detectedFields == 1) {
// If only password field is the visible one
if (_f(i.password)) {
cipAutocomplete.init(_f(i.password));
}
}
}
}
@ -1747,6 +1771,6 @@ cipEvents.triggerActivatedTab = function() {
browser.runtime.sendMessage({
action: 'retrieve_credentials',
args: [ cip.url, cip.submitUrl ]
}).then(cip.retrieveCredentialsCallback);
}).then(cip.retrieveCredentialsCallback).catch((e) => {console.log(e);});
}
};

View file

@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "keepassxc-browser",
"version": "0.3.6",
"version": "0.4.2",
"description": "KeePassXC integration for modern web browsers",
"author": "Sami Vänttinen",
"icons": {
@ -41,6 +41,9 @@
"matches": [
"<all_urls>"
],
"exclude_matches": [
"*://*/*.xml"
],
"js": [
"browser-polyfill.min.js",
"global.js",
@ -67,8 +70,8 @@
"fill-password": {
"description": "Insert a password",
"suggested_key": {
"default": "Ctrl+Shift+P",
"mac": "Command+Shift+P"
"default": "Alt+Shift+P",
"mac": "Alt+Shift+P"
}
}
},

View file

@ -84,24 +84,6 @@
</span>
</div>
</p>
<p>
<div class="form-group">
<label for="port">UDP port for proxy applications:</label>
<div class="control-group">
<div class="input-append">
<input type="number" id="port" placeholder="19700" value="19700" />
<button class="btn btn-sm btn-primary" id="portButton" type="button"><span class="glyphicon glyphicon-floppy-disk"></span> Save</button>
</div>
<span class="help-inline">
Change the port if you have trouble with running KeePassXC on the default port.
<br />
You have to set the same port number in KeePassXC options.
<br />
Default: 19700
</span>
</div>
</div>
</p>
<hr />
<p>
<div class="checkbox">

View file

@ -3,18 +3,21 @@ if (jQuery) {
}
$(function() {
options.initMenu();
options.initGeneralSettings();
options.initConnectedDatabases();
options.initSpecifiedCredentialFields();
options.initAbout();
browser.runtime.sendMessage({ action: 'load_settings' }).then((settings) => {
options.settings = settings;
browser.runtime.sendMessage({ action: 'load_keyring' }).then((keyRing) => {
options.keyRing = keyRing;
options.initMenu();
options.initGeneralSettings();
options.initConnectedDatabases();
options.initSpecifiedCredentialFields();
options.initAbout();
});
});
});
var options = options || {};
options.settings = typeof(localStorage.settings) === 'undefined' ? {} : JSON.parse(localStorage.settings);
options.keyRing = typeof(localStorage.keyRing) === 'undefined' ? {} : JSON.parse(localStorage.keyRing);
options.initMenu = function() {
$('.navbar:first ul.nav:first li a').click(function(e) {
e.preventDefault();
@ -32,22 +35,21 @@ options.saveSetting = function(name) {
$($id).closest('.control-group').removeClass('error').addClass('success');
setTimeout(() => { $($id).closest('.control-group').removeClass('success'); }, 2500);
localStorage.settings = JSON.stringify(options.settings);
browser.storage.local.set({'settings': options.settings});
browser.runtime.sendMessage({
action: 'load_settings'
});
};
options.saveSettings = function() {
localStorage.settings = JSON.stringify(options.settings);
browser.storage.local.set({'settings': options.settings});
browser.runtime.sendMessage({
action: 'load_settings'
});
};
options.saveKeyRing = function() {
localStorage.keyRing = JSON.stringify(options.keyRing);
browser.storage.local.set({'keyRing': options.keyRing});
browser.runtime.sendMessage({
action: 'load_keyring'
});
@ -86,28 +88,14 @@ options.initGeneralSettings = function() {
}).then(options.showKeePassXCVersions);
});
$('#port').val(options.settings['port']);
$('#blinkTimeout').val(options.settings['blinkTimeout']);
$('#blinkMinTimeout').val(options.settings['blinkMinTimeout']);
$('#allowedRedirect').val(options.settings['allowedRedirect']);
$('#portButton').click(function() {
const port = $.trim($('#port').val());
const portNumber = Number(port);
if (isNaN(port) || portNumber < 1025 || portNumber > 99999) {
$('#port').closest('.control-group').addClass('error');
alert('The port number has to be in range 1025 - 99999.\nNothing saved!');
return;
}
options.settings['port'] = String(portNumber);
options.saveSetting('port');
});
$('#blinkTimeoutButton').click(function(){
const blinkTimeout = $.trim($('#blinkTimeout').val());
const blinkTimeoutval = Number(blinkTimeout);
options.settings['blinkTimeout'] = String(blinkTimeoutval);
options.saveSetting('blinkTimeout');
});
@ -115,7 +103,7 @@ options.initGeneralSettings = function() {
$('#blinkMinTimeoutButton').click(function(){
const blinkMinTimeout = $.trim($('#blinkMinTimeout').val());
const blinkMinTimeoutval = Number(blinkMinTimeout);
options.settings['blinkMinTimeout'] = String(blinkMinTimeoutval);
options.saveSetting('blinkMinTimeout');
});
@ -123,7 +111,7 @@ options.initGeneralSettings = function() {
$('#allowedRedirectButton').click(function(){
const allowedRedirect = $.trim($('#allowedRedirect').val());
const allowedRedirectval = Number(allowedRedirect);
options.settings['allowedRedirect'] = String(allowedRedirectval);
options.saveSetting('allowedRedirect');
});

View file

@ -1,17 +1,25 @@
$(function() {
browser.runtime.getBackgroundPage().then((global) => {
browser.tabs.query({"active": true, "currentWindow": true}).then((tab) => {
browser.tabs.query({'active': true, 'currentWindow': true}).then((tabs) => {
let tab = tabs[0];
const data = global.page.tabs[tab.id].loginList;
let ul = document.getElementById('login-list');
for (let i = 0; i < data.logins.length; i++) {
const li = document.createElement('li');
const a = document.createElement('a');
a.textContent = data.logins[i].login + ' (' + data.logins[i].name + ')';
li.setAttribute('class', 'list-group-item');
a.textContent = data.logins[i].login + " (" + data.logins[i].name + ")";
li.appendChild(a);
$(a).data('url', data.url.replace(/:\/\//g, '://' + data.logins[i].login + ':' + data.logins[i].password + '@'));
$(a).click(() => {
browser.tabs.update(tab.id, {'url': $(this).data('url')});
$(a).data('creds', data.logins[i]);
$(a).click(function () {
if (data.resolve) {
const creds = $(this).data('creds');
data.resolve({
authCredentials: {
username: creds.login,
password: creds.password
}
});
}
close();
});
ul.appendChild(li);

252
keepassxc-protocol.md Normal file
View file

@ -0,0 +1,252 @@
## keepassxc-protocol
Transmitting messages between KeePassXC and keepassxc-browser is totally rewritten. This is still under development.
Now the requests are encrypted by [TweetNaCl.js](https://github.com/dchest/tweetnacl-js) box method and does the following:
1. keepassxc-browser generates a key pair (with public and secret key) and transfers the public key to KeePassXC
2. When KeePassXC receives the public key it generates its own key pair and transfers the public key to keepassxc-browser
3. All messages between the browser extension and KeePassXC are now encrypted.
4. When keepassxc-browser sends a message it is encrypted with KeePassXC's public key, a random generated nonce and keepassxc-browser's secret key.
5. When KeePassXC sends a message it is encrypted with keepassxc-browser's public key and an incremented nonce.
6. Databases are stored based on the current public key used with `associate`. A new key pair for data transfer is generated each time keepassxc-browser is launched.
Encrypted messages are built with these JSON parameters:
- action - `test-associate`, `associate`, `get-logins`, `get-logins-count`, `set-login`...
- message - Encrypted message, base64 encoded
- nonce - 24 bytes long random data, base64 encoded. This must be the same when responding to a request.
- clientID - 24 bytes long random data, base64 encoded. This is used to identify different browsers if multiple are used with proxy application.
Currently these messages are implemented:
- `change-public-keys`: Request for passing public keys from client to server and back.
- `get-databasehash`: Request for receiving the database hash (SHA256) of the current active database.
- `associate`: Request for associating a new client with KeePassXC.
- `test-associate`: Request for testing if the client has been associated with KeePassXC.
- `generate-password`: Request for generating a password. KeePassXC's settings are used.
- `get-logins`: Requests for receiving credentials for the current URL match.
- `set-login`: Request for adding or updating credentials to the database.
- `lock-database`: Request for locking the database from client.
- `database-locked`: A signal from KeePassXC, the current active database is locked.
- `database-unlocked`: A signal from KeePassXC, the current active database is unlocked.
### change-public-keys
Request:
```javascript
{
"action": "change-public-keys",
"publicKey": "<current public key>",
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q",
"clientID": "<clientID>"
}
```
Response (success):
```javascript
{
"action": "change-public-keys",
"version": "2.2.0",
"publicKey": "<host public key>",
"success": "true"
}
```
### get-databasehash
Request (unencrypted):
```javascript
{
"action": "get-databasehash"
}
```
Response message data (success, decrypted):
```javascript
{
"action": "hash",
"hash": "29234e32274a32276e25666a42",
"version": "2.2.0"
}
```
### associate
Unencrypted message:
```javascript
{
"action": "associate",
"key": "<current public key>"
}
```
Request:
```javascript
{
"action": "associate",
"message": encryptedMessage
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q",
"clientID": "<clientID>"
}
```
Response message data (success, decrypted):
```javascript
{
"hash": "29234e32274a32276e25666a42",
"version": "2.2.0",
"success": "true",
"id": "testclient",
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q"
}
```
### test-associate
Unencrypted message:
```javascript
{
"action": "test-associate",
"id": "<saved database identifier>",
"key": "<saved database public key>",
"clientID": "<clientID>"
}
```
Request:
```javascript
{
"action": "test-associate",
"message": encryptedMessage
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q"
}
```
Response message data (success, decrypted):
```javascript
{
"version": "2.2.0",
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q",
"hash": "29234e32274a32276e25666a42",
"id": "testclient",
"success": "true"
}
```
### generate-password
Request:
```javascript
{
"action": "generate-password",
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q",
"clientID": "<clientID>"
}
```
Response message data (success, decrypted):
```javascript
{
"version": "2.2.0",
"entries": [
{
"login": 144,
"password": "testclientpassword"
}
],
"success": "true",
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q"
}
```
### get-logins
Unencrypted message:
```javascript
{
"action": "get-logins",
"url": "<snip>",
"submitUrl": optional
}
```
Request:
```javascript
{
"action": "get-logins",
"message": encryptedMessage
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q",
"clientID": "<clientID>"
}
```
Response message data (success, decrypted):
```javascript
{
"count": "2",
"entries" : [
{
"login": "user1",
"name": "user1",
"password": "passwd1"
},
{
"login": "user2",
"name": "user2",
"password": "passwd2"
}],
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q",
"success": "true",
"hash": "29234e32274a32276e25666a42",
"version": "2.2.0"
}
```
### set-login
Unencrypted message:
```javascript
{
"action": "set-login",
"url": "<snip>",
"submitUrl": "<snip>",
"id": "testclient",
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q",
"login": "user1",
"password": "passwd1"
}
```
Request:
```javascript
{
"action": "set-login",
"message": encryptedMessage
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q",
"clientID": "<clientID>"
}
```
Response message data (success, decrypted):
```javascript
{
"count": null,
"entries" : null,
"error": "",
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q",
"success": "true",
"hash": "29234e32274a32276e25666a42",
"version": "2.2.0"
}
```
### lock-database
Request:
```javascript
{
"action": "lock-database",
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q",
"clientID": "<clientID>"
}
```
Response message data (success always returns an error, decrypted):
```javascript
{
"action": "lock-database",
"errorCode": 1,
"error": "Database not opened",
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q"
}
```