Merge branch 'develop'

This commit is contained in:
varjolintu 2017-11-27 18:16:45 +02:00
commit 1fd82cde38
8 changed files with 216 additions and 142 deletions

View file

@ -1,3 +1,10 @@
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

View file

@ -35,6 +35,7 @@ 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

View file

@ -5,7 +5,7 @@ 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

@ -21,6 +21,7 @@ keepass.databaseHash = 'no-hash'; //no-hash = KeePassXC is too old and does not
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',
@ -133,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;
@ -142,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,
@ -160,18 +161,22 @@ keepass.updateCredentials = function(callback, tab, entryId, username, password,
const request = {
action: kpAction,
message: keepass.encrypt(messageData, nonce),
nonce: nacl.util.encodeBase64(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) {
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);
@ -187,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([]);
@ -205,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 = {
@ -221,32 +226,36 @@ keepass.retrieveCredentials = function(callback, tab, url, submiturl, forceCallb
const request = {
action: kpAction,
message: keepass.encrypt(messageData, nonce),
nonce: nacl.util.encodeBase64(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) {
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);
@ -265,8 +274,7 @@ keepass.generatePassword = function(callback, tab, forceCallback) {
}
keepass.testAssociation((taresponse) => {
if (!taresponse)
{
if (!taresponse) {
browserAction.showDefault(null, tab);
if (forceCallback) {
callback([]);
@ -281,36 +289,41 @@ 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: nacl.util.encodeBase64(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) {
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);
@ -335,7 +348,8 @@ keepass.associate = function(callback, tab) {
const kpAction = kpActions.ASSOCIATE;
const key = nacl.util.encodeBase64(keepass.keyPair.publicKey);
const nonce = nacl.randomBytes(keepass.keySize);
const nonce = keepass.getNonce();
const incrementedNonce = keepass.incrementedNonce(nonce);
const messageData = {
action: kpAction,
@ -345,30 +359,33 @@ keepass.associate = function(callback, tab) {
const request = {
action: kpAction,
message: keepass.encrypt(messageData, nonce),
nonce: nacl.util.encodeBase64(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) {
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);
@ -407,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) {
@ -427,34 +445,38 @@ keepass.testAssociation = function(callback, tab, enableTimeout = false) {
const request = {
action: kpAction,
message: keepass.encrypt(messageData, nonce),
nonce: nacl.util.encodeBase64(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) {
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;
}
}
}
@ -478,7 +500,8 @@ keepass.getDatabaseHash = function(callback, tab, enableTimeout = false) {
}
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
@ -494,37 +517,42 @@ keepass.getDatabaseHash = function(callback, tab, enableTimeout = false) {
const request = {
action: kpAction,
message: encrypted,
nonce: nacl.util.encodeBase64(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) {
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 {
@ -538,6 +566,7 @@ keepass.getDatabaseHash = function(callback, tab, enableTimeout = false) {
keepass.handleError(tab, response.errorCode, response.error);
}
callback(keepass.databaseHash);
return;
}
});
};
@ -551,8 +580,8 @@ keepass.changePublicKeys = function(tab, enableTimeout = false) {
const kpAction = kpActions.CHANGE_PUBLIC_KEYS;
const key = nacl.util.encodeBase64(keepass.keyPair.publicKey);
let nonce = nacl.randomBytes(keepass.keySize);
nonce = nacl.util.encodeBase64(nonce);
const nonce = keepass.getNonce();
const incrementedNonce = keepass.incrementedNonce(nonce);
keepass.clientID = nacl.util.encodeBase64(nacl.randomBytes(keepass.keySize));
const request = {
@ -565,7 +594,7 @@ keepass.changePublicKeys = function(tab, enableTimeout = false) {
keepass.sendNativeMessage(request, enableTimeout).then((response) => {
keepass.setcurrentKeePassXCVersion(response.version);
if (!keepass.verifyKeyResponse(response, key, nonce)) {
if (!keepass.verifyKeyResponse(response, key, incrementedNonce)) {
if (tab && page.tabs[tab.id]) {
keepass.handleError(tab, kpErrors.KEY_CHANGE_FAILED);
reject(false);
@ -588,7 +617,8 @@ keepass.lockDatabase = function(tab) {
}
const kpAction = kpActions.LOCK_DATABASE;
const nonce = nacl.randomBytes(keepass.keySize);
const nonce = keepass.getNonce();
const incrementedNonce = keepass.incrementedNonce(nonce);
const messageData = {
action: kpAction
@ -597,23 +627,29 @@ keepass.lockDatabase = function(tab) {
const request = {
action: kpAction,
message: keepass.encrypt(messageData, nonce),
nonce: nacl.util.encodeBase64(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) {
const message = nacl.util.encodeUTF8(res);
const parsed = JSON.parse(message);
keepass.setcurrentKeePassXCVersion(parsed.version);
if (!res) {
keepass.handleError(tab, kpErrors.CANNOT_DECRYPT_MESSAGE);
resolve(false);
return;
}
if (keepass.verifyResponse(parsed, response.nonce)) {
keepass.isDatabaseClosed = true;
keepass.handleError(tab, kpErrors.DATABASE_NOT_OPENED);
resolve(false);
}
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) {
@ -795,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);
@ -838,6 +894,9 @@ keepass.verifyResponse = function(response, nonce, id) {
}
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);
@ -880,9 +939,10 @@ 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 nacl.util.encodeBase64(message);
}
@ -890,7 +950,7 @@ keepass.encrypt = function(input, nonce) {
return '';
};
keepass.decrypt = function(input, nonce, toStr) {
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);

View file

@ -48,7 +48,7 @@ page.initOpenedTabs = function() {
}
// set initial tab-ID
browser.tabs.query({ "active": true, "currentWindow": true }).then((tabs) => {
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

View file

@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "keepassxc-browser",
"version": "0.4.1",
"version": "0.4.2",
"description": "KeePassXC integration for modern web browsers",
"author": "Sami Vänttinen",
"icons": {

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);

View file

@ -7,7 +7,7 @@ Now the requests are encrypted by [TweetNaCl.js](https://github.com/dchest/tweet
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.
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: