Merge branch 'leolivier-master'

This commit is contained in:
antelle 2018-03-19 22:09:59 +01:00
commit a86aad3c64
3 changed files with 280 additions and 0 deletions

View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>KeeWeb Plugin: HaveIBeenPwned</title>
<link rel="shortcut icon" href="/favicon.png" />
<style>
body {
font-family: -apple-system, "BlinkMacSystemFont", "Helvetica Neue", "Helvetica", "Roboto", "Arial", sans-serif;;
font-size: 14px;
}
</style>
</head>
<body>
<h1>KeeWeb Plugin: HaveIBeenPwned</h1>
<a href="https://plugins.keeweb.info/plugins/haveibeenpwned">https://plugins.keeweb.info/plugins/haveibeenpwned</a>
<p>This plugin checks the <a href="https://haveibeenpwned.com">HaveIBeenPwned</a> site each time you enter either a user name or a password to look if they have been pawned in a breach. The password is safely checked (not sent over the network).</p>
<p>Freely inspired from the equivalent keepass plugin</p>
</body>
</html>

View File

@ -0,0 +1,17 @@
{
"version": "0.0.1",
"manifestVersion": "0.1.0",
"name": "haveibeenpwned",
"description": "Check HaveIBeenPwned password database",
"author": {
"name": "Olivier LEVILLAIN",
"email": "olivier.levillain@free.fr",
"url": "https://github.com/leolivier"
},
"resources": {
"js": "na+696slok4iEscUEExyD2dF6bwT1AlmtiIogdAda9xZiBGiSNM4u43Q9oaBbnT+U1Kgoumgh0lHbnb+Hy5YtSFUTCi5M3+DKgaGVIQD0DivXqojGQF+aPKRnBv5tYu4QlGCXVVr3rXf65FHghfTXTrN0vSXM/PcysZkkrgaY6qxUZsROfnLX4EeX9JKdHcqRA3JpQI3ptBiYPL6zzJuQZcD18IMuULvs07f3QpP5IiTsgOWftBEQ20RPr3gqAjd6aAhKYGiNqBaJ7wR8MCKqNiJ4yPv6fPpNdimbh/mP3LY69CFaGvi9ZyABcP7I5zenw1ZScQsYfC+Qix25LZqBw=="
},
"url": "https://plugins.keeweb.info/plugins/haveibeenpwned",
"publicKey": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp9Cay/z7whBsHcf9klDjlA4qylWT7a/igTJ2nvUq2XuQrx98PTOexzzzg5oflk8nPEaMIsaFIf90V/rvQjJ1z9DR4zuQKDb4/GZVzxoylECAwNk80LvSPc1G0+6mwXIFp48wc6Advd4iYQCMkzWDCJXEm/1E+q85ty+H6EaLleKcJI0vlW96bbA9vFCmOsM5PYZfoGnVFRBLVthyUcGneilMvsxu5J7DKggQKPs04/WQZ5oHbUG83mxkxTdYDC3glpvV4BiaAD6z+2usO+fA97bXb+rY3O2iHJgWsa7jH0ybO0Nif6txE4d2+LJOLmfoImv7kdyu/eN3A78KejCOhwIDAQAB",
"license": "MIT"
}

View File

@ -0,0 +1,242 @@
/**
* KeeWeb plugin: haveibeenpwned
* @author Olivier LEVILLAIN
* @license MIT
*/
const DetailsView = require('views/details/details-view');
const Alerts = require('comp/alerts');
const Logger = require('util/logger');
const InputFx = require('util/input-fx');
const Kdbxweb = require('kdbxweb');
const _ = require('_');
const detailsViewFieldChanged = DetailsView.prototype.fieldChanged;
const settings = { checkPwnedPwd: false, checkPwnedName: false, blockPwnedPwd: false, blockPwnedName: false };
const logger = new Logger('HaveIBeenPwned');
DetailsView.prototype.checkPwnedOnSettingsChanged = function (changes) {
// if (changes['CheckPwnedPwd'] || changes['CheckPwnedName'] || changes['BlockPwnedPwd'] || changes['BlockPwnedName']) {
// info('Full HaveIBeenPwned check not yet implemented. Checks are done one by one when you change a name or a password.');
// }
};
let _seen = [];
class HIBPUtils {
constructor() {
_seen = [];
};
replacer(key, value) {
if (value != null && typeof value === 'object') {
if (_seen.indexOf(value) >= 0) {
return;
}
_seen.push(value);
}
return value;
};
stringify(obj) {
const ret = JSON.stringify(obj, this.replacer);
_seen = [];
return ret;
};
xhrcall (config) {
const xhr = new XMLHttpRequest();
if (config.responseType) {
xhr.responseType = config.responseType;
}
const statuses = config.statuses || [200];
xhr.addEventListener('load', () => {
if (statuses.indexOf(xhr.status) >= 0) {
return config.success && config.success(xhr.response, xhr);
} else {
return config.error && config.error('http status ' + xhr.status, xhr);
}
});
xhr.addEventListener('error', () => {
return config.error && config.error('network error', xhr);
});
xhr.addEventListener('timeout', () => {
return config.error && config.error('timeout', xhr);
});
xhr.open(config.method || 'GET', config.url);
if (config.headers) {
config.headers.forEach((value, key) => {
xhr.setRequestHeader(key, value);
});
};
xhr.send(config.data);
};
hex (buffer) {
const hexCodes = [];
const view = new DataView(buffer);
for (let i = 0; i < view.byteLength; i += 4) {
// Using getUint32 reduces the number of iterations needed (we process 4 bytes each time)
const value = view.getUint32(i);
// toString(16) will give the hex representation of the number without padding
const stringValue = value.toString(16);
// We use concatenation and slice for padding
const padding = '00000000';
const paddedValue = (padding + stringValue).slice(-padding.length);
hexCodes.push(paddedValue);
}
// Join all the hex strings into one
return hexCodes.join('');
};
digest(algo, str) {
// We transform the string into an arraybuffer.
const buffer = Kdbxweb.ByteUtils.stringToBytes(str);
const subtle = window.crypto.subtle || window.crypto.webkitSubtle;
const _self = this;
return subtle.digest(algo, buffer).then(hash => {
return _self.hex(hash);
});
};
sha1(str) {
return this.digest('SHA-1', str);
};
sha256(str) {
return this.digest('SHA-256', str);
};
alert (msg) {
Alerts.info({ body: msg, title: 'HaveIBeenPwned' });
};
}
const hibp = new HIBPUtils();
DetailsView.prototype.checkNamePwned = function (name) {
logger.info('check hibp name ' + name);
name = encodeURIComponent(name);
const url = `https://haveibeenpwned.com/api/v2/breachedaccount/${name}?truncateResponse=true`;
logger.info('url ' + url);
hibp.xhrcall({
url: url,
method: 'GET',
responseType: 'json',
headers: undefined,
data: null,
statuses: [200, 404],
success: (data, xhr) => {
logger.info('xhr ' + JSON.stringify(xhr));
if (data && data.length > 0) {
logger.info('found breaches ' + JSON.stringify(data));
let breaches = '';
data.forEach(breach => { breaches += '<li>' + _.escape(breach.Name) + '</li>\n'; });
hibp.alert(`WARNING! This account has been pawned in the following breaches<br/>\n<ul>\n${breaches}\n</ul>\n<p>Please check on <a href='https://haveibeenpwned.com'>https://haveibeenpwned.com</a>\n`);
this.userEditView.$el.focus();
this.userEditView.$el.addClass('input--error');
InputFx.shake(this.userEditView.$el);
} else {
logger.info('check pwnd name passed...');
this.userEditView.$el.removeClass('input--error');
}
},
error: (e, xhr) => {
const err = xhr.response && xhr.response.error || new Error('Network error');
logger.error('Pwned Password API error', 'GET', xhr.status, err);
err.status = xhr.status;
}
});
};
DetailsView.prototype.checkPwdPwned = function (passwordHash) {
logger.info('check hibp pwd (hash) ' + passwordHash);
const prefix = passwordHash.substring(0, 5);
hibp.xhrcall({
url: `https://api.pwnedpasswords.com/range/${prefix}`,
method: 'GET',
responseType: 'text',
headers: undefined,
data: null,
statuses: [200, 404],
success: data => {
if (data) {
logger.info('found breaches ' + JSON.stringify(data));
data.split('\r\n').forEach(line => {
const h = line.split(':');
const suffix = h[0];
if (prefix + suffix === passwordHash) {
const nb = _.escape(h[1]);
hibp.alert(`WARNING: This password is referenced as pawned ${nb} times on <a href='https://haveibeenpwned.com'>https://haveibeenpwned.com</a>!\n`);
this.passEditView.$el.focus();
this.passEditView.$el.addClass('input--error');
InputFx.shake(this.passEditView.$el);
}
});
} else {
logger.info('check pwnd passwd passed...');
this.passEditView.$el.removeClass('input--error');
}
},
error: (e, xhr) => {
const err = xhr.response && xhr.response.error || new Error('Network error');
logger.error('Pwned Password API error', 'GET', xhr.status, err);
err.status = xhr.status;
}
});
};
DetailsView.prototype.fieldChanged = function (e) {
// logger.info('field changed ' + hibp.stringify(e));
detailsViewFieldChanged.apply(this, arguments);
if (e.field) {
if (e.field === '$Password' && settings.checkPwnedPwd) {
if (this.passEditView.value) {
const pwd = this.passEditView.value.getText();
if (pwd.replace(/\s/, '') !== '' && !pwd.startsWith('{REF:')) {
hibp.sha1(pwd).then(hash => {
this.checkPwdPwned(hash.toUpperCase());
});
}
}
} else if (e.field === '$UserName' && settings.checkPwnedName) {
this.checkNamePwned(e.val);
}
}
};
module.exports.getSettings = function () {
return [{
name: 'checkPwnedPwd',
label: 'Check passwords against HaveIBeenPwned list',
type: 'checkbox',
value: true
}, {
name: 'checkPwnedName',
label: 'Check user ids against HaveIBeenPwned list',
type: 'checkbox',
value: true
}, {
name: 'blockPwnedPwd',
label: 'Block pwned passwords if they are in HaveIBeenPwned list',
type: 'checkbox',
value: true
}, {
name: 'blockPwnedName',
label: 'Block pwned names if they are in HaveIBeenPwned list',
type: 'checkbox',
value: true
}];
};
module.exports.setSettings = function (changes) {
// apply changed settings in plugin logic
// this method will be called:
// 1. when any of settings fields is modified by user
// 2. after plugin startup, with saved values
// only changed settings will be passed
// example: { MyText: 'value', MySel: 'selected-value', MyCheckbox: true }
// info(JSON.stringify(changes));
for (const field in changes) {
const ccfield = field.substr(0, 1).toLowerCase() + field.substring(1);
settings[ccfield] = changes[field];
}
DetailsView.prototype.checkPwnedOnSettingsChanged.apply(changes);
};
module.exports.uninstall = function () {
DetailsView.prototype.fieldChanged = detailsViewFieldChanged;
};