diff --git a/docs/plugins/haveibeenpwned/manifest.json b/docs/plugins/haveibeenpwned/manifest.json
index b8f89f0..d73ea95 100644
--- a/docs/plugins/haveibeenpwned/manifest.json
+++ b/docs/plugins/haveibeenpwned/manifest.json
@@ -1,5 +1,5 @@
{
- "version": "0.4.0",
+ "version": "0.6.0",
"manifestVersion": "0.1.0",
"name": "haveibeenpwned",
"description": "Check HaveIBeenPwned password database",
@@ -9,7 +9,7 @@
"url": "https://github.com/leolivier"
},
"resources": {
- "js": "i9hmfSZ7Lj+bO/+aBMjdv7UH1AwlJo2exE6rEVvxqkaZ6JHP2ie/YhiY49KwdKdNZQc88iDUuZgA9Vr7XYVGP0Z/xH4q/659tB+veeX4Rht5FvOLbUX8bzNb2DzCVEFg8TnZogPAw6wPPWkccMcavaX8S9UgFKrp6N4gYWYRqIOh8gIrYf3IFVC4WSSG0XO2fBoTUgNG1iRKGjJ5mb40CLjiW8a3IpM+T3P7XT5RfgzHcWj2KugQV+gc59f97qxmjM+mZldrN0M4SKOAaSxvMJI453rrKgZdk6NPjW08d28ZE7V4ukJXa8wWNgNEbxtwCIT8P0eDeSUFtlQhyWrEZw==",
+ "js": "KLD9KlvagbiBTivQ5UQcTYscXlTTeEESOD3ZP7r8IKuTEVg254+FXcqwbvI8hQN/H1n1k50k+dPtzmOH9Ln2wAEyTtXSaGkQOkpOXFtCSvisnML0wzgff+KeAmt6wmIR+weyh4S5JE0Bh/FeekWAFTp3QDRp+Mp29bWYAclr6z9feEpA25DUJKk++qyivES7pTz4JVyvnTXopueyic+d935FuKBLNfMtI4VmP7X56x2xa6XRrosGEs+mv1IxwS/0rgHInM9zXwpGozJ5ywDAJ+PLN3Gfx1l/KND+rcXUG7hb7sccbz6hny7BH2MYn5vsG6Uz6Agg1EkVsixOA8lM5w==",
"css": "hL9Bv6NIxyP1vsHPZftppZSoM2zvP+H8ZxaEP4NwFZPoWlWZvn6l7f/x8FJly7mv7mFqpygMP4qZguPk9ZAcE5C5wj6mI9RloUg3f+500rN1mEVTpd+wH7AxR0eh/tjrYK0oq9BKpM099NknZ/t0J+aWODlk7HHbo9R2HUuvG2cpY2R137z0vkI8NPl2KQWi5vwIzKa+/EUwVIxoRApg603XzUgK8FFvAcrCsNdrXjoKEuQpZfEz8XBMjp62SuKS8KxdtyyJdsGbNrPkR8gc52ZBY0yFPC+FrSVhl6sxvu6UAMXW5xVtN5pE2Pn7+jnaVnaCafPYeqpDk5brf8G3JA=="
},
"url": "https://plugins.keeweb.info/plugins/haveibeenpwned",
diff --git a/docs/plugins/haveibeenpwned/plugin.js b/docs/plugins/haveibeenpwned/plugin.js
index 861eef1..ac77fbe 100644
--- a/docs/plugins/haveibeenpwned/plugin.js
+++ b/docs/plugins/haveibeenpwned/plugin.js
@@ -5,10 +5,22 @@
*/
const Logger = require('util/logger');
-// change log level here. Should be changed to Info when issue #893 fixed on keeweb
-const LogLevel = Logger.Level.Debug;
+/**
+ * local logger
+ * @type {Logger}
+ */
+const hLogger = new Logger('HaveIBeenPwned');
+/** change log level here. Should be changed to Info when issue #893 fixed on keeweb */
+hLogger.setLevel(Logger.Level.Debug);
-// Strings that should be localized
+/**
+ * Cache time to live
+ * Set to 14 days (in milliseconds)
+ * @type {integer}
+ */
+const CacheTTL = 1000 * 3600 * 24 * 14;
+
+/** Strings that should be localized */
const HIBPLocale = {
hibpCheckPwnedPwd: 'Should I check passwords against HaveIBeenPwned list?',
hibpCheckPwnedName: 'Should I check user name against HaveIBeenPwned list?',
@@ -23,12 +35,18 @@ const HIBPLocale = {
hibpApiError: 'HaveIBeenPwned API error'
};
+/** What chcking level to use
+ * None: no checking
+ * Alert: Draw an alert near the pawned item
+ * AskMe: Interactively ask me to revert to the previous value if pawned
+ */
const HIBPCheckLevel = {
None: 'none',
Alert: 'alert',
AskMe: 'askme'
};
+/** Required modules */
const DetailsView = require('views/details/details-view');
const ListView = require('views/list-view');
const AppModel = require('models/app-model');
@@ -37,59 +55,85 @@ const Kdbxweb = require('kdbxweb');
const _ = require('_');
const Tip = require('util/tip');
const Alerts = require('comp/alerts');
+const StorageBase = require('storage/storage-base');
+const IoBrowserCache = require('storage/io-browser-cache');
+/** Keeps track of 4 replaced methods */
const detailsViewFieldChanged = DetailsView.prototype.fieldChanged;
const detailsViewAddFieldViews = DetailsView.prototype.addFieldViews;
const listViewRender = ListView.prototype.render;
const appModelGetEntriesByFilter = AppModel.prototype.getEntriesByFilter;
-let _seen = [];
-class HIBPUtils {
+/**
+ * Storage cache based on IoBrowserCache.
+ * Inspired from storage-cache.js with a specific config
+ * TODO: test this cache in Desktop app
+ */
+class StorageCache extends StorageBase {
constructor() {
- _seen = [];
- // the 3 options with their default values
- this.checkPwnedPwd = HIBPCheckLevel.Alert;
- this.checkPwnedName = HIBPCheckLevel.Alert;
- this.checkPwnedList = false;
- // cache variables
- this._pwnedNamesCache = {};
- this._pwnedPwdsCache = {};
- // local logger
- this.logger = new Logger('HaveIBeenPwned');
- this.logger.setLevel(LogLevel);
+ super();
+ this.name = 'cache';
+ this.enabled = IoBrowserCache.enabled;
+ this.system = true;
+ this.init(); // storage base
+ this.io = new IoBrowserCache({
+ cacheName: 'HIBPCache',
+ logger: hLogger
+ });
};
- // used for cyclic stringifier
- _replacer(key, value) {
- if (value != null && typeof value === 'object') {
- if (_seen.indexOf(value) >= 0) {
- return;
+
+ save(id, data, callback) {
+ this.io.save(id, data, callback);
+ };
+ load(id, callback) {
+ this.io.load(id, callback);
+ };
+ remove(id, callback) {
+ this.io.remove(id, callback);
+ }
+};
+
+let _seen = [];
+const HIBPUtils = {
+ /**
+ * cyclic objects enabled stringifier
+ * @param {object} obj the object to be stringified
+ * @returns {string} the stringified object
+ */
+ stringify: (obj) => {
+ const ret = JSON.stringify(obj, (key, value) => {
+ if (value != null && typeof value === 'object') {
+ if (_seen.indexOf(value) >= 0) {
+ return;
+ }
+ _seen.push(value);
}
- _seen.push(value);
- }
- return value;
- };
- // cyclic objects enabled stringifier
- stringify(obj) {
- const ret = JSON.stringify(obj, hibp._replacer);
+ return value;
+ });
_seen = [];
return ret;
- };
- // prints a stack trace in debug mode
- stackTrace() {
+ },
+ /**
+ * writes the stingified object on the console if in debug mode
+ * @param {object} obj the object to be dumped
+ */
+ dump: (obj) => {
+ hLogger.debug(HIBPUtils.stringify(obj));
+ },
+ /**
+ * Prints a stack trace in debug mode
+ */
+ stackTrace: () => {
const err = new Error();
- hibp.logger.debug(err.stack);
- }
- // show the details of an entry in debug mode
- showItem(model) {
- if (model) {
- hibp.logger.debug('show entry ' + model.title +
- ': name=' + model.user + ', pwd=' + (model.password ? model.password.getText() : 'undefined') +
- ', namePwned=' + model.namePwned + ', pwdPwned=' + model.pwdPwned
- );
- }
- }
- // XML HTTP Request with Promises, modified from StorageBase
- _xhrpromise(config) {
+ hLogger.debug(err.stack);
+ },
+ /**
+ * XML HTTP Request with Promises,
+ * modified from StorageBase
+ * @param {object} config the XML HTTP Request configuration. Same as StorageBase
+ * @returns {Promise}
+ */
+ xhrpromise: (config) => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
if (config.responseType) {
@@ -106,63 +150,95 @@ class HIBPUtils {
if (statuses.indexOf(xhr.status) >= 0) {
resolve(xhr.response);
} else {
+ hLogger.error(HIBPLocale.hibpApiError, 'GET', xhr.status, xhr.statusText);
reject(xhr.statusText);
}
});
xhr.addEventListener('error', () => {
const err = xhr.response && xhr.response.error || new Error('Network error');
- hibp.logger.error(HIBPLocale.hibpApiError, 'GET', xhr.status, err);
+ hLogger.error(HIBPLocale.hibpApiError, 'GET', xhr.status, err);
reject(xhr.statusText);
});
xhr.send(config.data);
});
- }
- // transforms a byte array into an hex string
- _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('');
- };
- // applies a digest algorithm and returns the corresponding hex string
- _digest(algo, str) {
+ },
+ /**
+ * Applies a digest algorithm on input string
+ * @param {algo} algorithm to be applied (e.g. 'SHA-1' 'SHA-2456'
+ * @returns {string} the "digested" hex string
+ */
+ digest: (algo, str) => {
const buffer = Kdbxweb.ByteUtils.stringToBytes(str);
const subtle = window.crypto.subtle || window.crypto.webkitSubtle;
- return subtle.digest(algo, buffer).then(hash => {
- return hibp._hex(hash);
+ return subtle.digest(algo, buffer).then(buffer => {
+ // transforms the buffer into an hex string
+ 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('');
});
- };
- // returns the SHA-1 hex string of the input string
- sha1(str) {
- return hibp._digest('SHA-1', str);
- };
- // returns the SHA-256 hex string of the input string
- sha256(str) {
- return hibp._digest('SHA-256', str);
- };
- // add css stuff + tip on fields to show an alert on pawned fields
+ }
+};
+
+/**
+ * This is were most HaveIBeenPwned stuff lies.
+ */
+class HIBP {
+ constructor() {
+ // the 3 options with their default values
+ this.checkPwnedPwd = HIBPCheckLevel.Alert;
+ this.checkPwnedName = HIBPCheckLevel.Alert;
+ this.checkPwnedList = false;
+ // cache variables
+ this._pwnedNamesCache = {};
+ this._pwnedPwdsCache = {};
+ // cache manager
+ this.cache = new StorageCache();
+ this.loadCache('_pwnedNamesCache');
+ this.loadCache('_pwnedPwdsCache');
+ }
+ /**
+ * Show the details of an entry in debug mode
+ * @param {Entry} entry the entry to be shown
+ */
+ showItem(entry) {
+ if (entry) {
+ hLogger.debug('show entry ' + entry.title +
+ ': name=' + entry.user + ', pwd=' + (entry.password ? entry.password.getText() : 'undefined') +
+ ', namePwned=' + entry.namePwned + ', pwdPwned=' + entry.pwdPwned
+ );
+ }
+ }
+ /**
+ * Add css stuff + tip on fields to show an alert on pawned fields
+ * @param {Element} el the HTML element of the field
+ * @param {string} msg the message to print in the tip
+ */
alert(el, msg) {
- hibp.logger.info(msg);
+ hLogger.info(msg);
el.focus();
el.addClass('input--error');
el.find('.details__field-value').addClass('hibp-pwned');
Tip.createTip(el, { title: msg, placement: 'bottom' });
InputFx.shake(el);
- // hibp.stackTrace();
};
- // reset css stuff and tip on fields to remove alerts on pawned fields
+ /**
+ * Reset css stuff and tip on fields to remove alerts on pawned fields
+ * @param {Element} el the HTML element of the field
+ * @param {string} msg the message to print in the console
+ */
passed(el, msg) {
- hibp.logger.info(msg);
+ hLogger.info(msg);
el.removeClass('input--error');
el.find('.details__field-value').removeClass('hibp-pwned');
const tip = el._tip;
@@ -171,22 +247,83 @@ class HIBPUtils {
tip.title = null;
}
}
- // store the cache variable name cacheName in local storage
- storeCache(cacheName) {
- // TODO: implement this method
+ /**
+ * Store the desired cache in local storage
+ * @param {string} cacheVarName the name of the cache variable ('_pwnedNamesCache' or '_pwnedPwdsCache')
+ */
+ storeCache(cacheVarName) {
+ this.cache.save(cacheVarName, this[cacheVarName], (err) => {
+ if (err) {
+ hLogger.error('can\'t store cache ' + cacheVarName + ': ' + err);
+ }
+ });
}
- // checks if the input name is pawned in breaches on haveibeenpwned.
- // Uses a cache to avoid calling hibp again and again with the same values
- // Returns a promise resolving to an html string containing a list of breaches names if pwned or null
- checkNamePwned (name) {
- hibp.logger.info('check hibp name ' + name);
- if (hibp._pwnedNamesCache[name]) {
- return Promise.resolve(hibp._pwnedNamesCache[name] !== '' ? hibp._pwnedNamesCache[name] : null);
+ /**
+ * Load desired cache from local storage
+ * Values older than CacheTTL are discarded
+ * @param {string} cacheVarName the name of the cache variable ('_pwnedNamesCache' or '_pwnedPwdsCache')
+ */
+ loadCache(cacheVarName) {
+ this.cache.load(cacheVarName, (err, res) => {
+ if (err) {
+ hLogger.info('can\'t load cache ' + cacheVarName + ': ' + err);
+ } else {
+ // check each element to remove too old ones
+ for (const key in res) {
+ const value = res[key];
+ const diff = Date.now() - value.date; // cache age in millisec
+ if (diff < CacheTTL) {
+ this[cacheVarName][key] = value;
+ } else {
+ hLogger.info('cache too old for ' + cacheVarName + '.' + key);
+ this[cacheVarName][key] = undefined;
+ }
+ // HIBPUtils.dump(this);
+ }
+ }
+ });
+ }
+ /**
+ * true if cached element exist and is not too old
+ * @param {string} cachedElem the cache element to check
+ * @returns {boolean}
+ */
+ checkCache(cachedElem) {
+ return (cachedElem && (Date.now() - cachedElem.date) < CacheTTL);
+ }
+ /**
+ * Computes and returns the SHA1 hash of a string
+ * @param {string} str the input string
+ * @returns {string} the SHA-1 hex string of the input string
+ */
+ sha1(str) {
+ return HIBPUtils.digest('SHA-1', str);
+ };
+ /**
+ * Computes and returns the SHA256 hash of a string
+ * @param {string} str the input string
+ * @returns {string} the SHA-256 hex string of the input string
+ */
+ sha256(str) {
+ return HIBPUtils.digest('SHA-256', str);
+ };
+ /**
+ * Checks if the input name is pawned in breaches on haveibeenpwned.
+ * Uses a cache to avoid calling hibp again and again with the same values
+ * @param {string} name the name to check
+ * @returns {Promise} a promise resolving to an html string containing a list of breaches names if pwned, or null
+ */
+ checkNamePwned (uname) {
+ hLogger.debug('checking user name ' + uname);
+ const name = encodeURIComponent(uname);
+ if (this.checkCache(this._pwnedNamesCache[name])) {
+ hLogger.debug(' user name found in cache: ' + name);
+ return Promise.resolve(this._pwnedNamesCache[name].val);
} else {
- name = encodeURIComponent(name);
+ hLogger.debug('USER NAME NOT FOUND in cache: ' + name); // + ' cache=' + this.stringify(this._pwnedNamesCache));
const url = `https://haveibeenpwned.com/api/v2/breachedaccount/${name}?truncateResponse=true`;
- // hibp.logger.debug('url ' + url);
- return hibp._xhrpromise({
+ // hLogger.debug('url ' + url);
+ return HIBPUtils.xhrpromise({
url: url,
method: 'GET',
responseType: 'json',
@@ -194,34 +331,36 @@ class HIBPUtils {
data: null,
statuses: [200, 404]
}).then(data => {
+ let breaches = null;
if (data && data.length > 0) {
- hibp.logger.debug('found breaches ' + JSON.stringify(data));
- let breaches = '';
+ // hLogger.debug('found breaches ' + JSON.stringify(data));
+ breaches = '';
data.forEach(breach => { breaches += '
' + _.escape(breach.Name) + '\n'; });
- hibp._pwnedNamesCache[name] = breaches || '';
- if (breaches) hibp.logger.debug(`name ${name} pwned in ${breaches}`);
- hibp.storeCache('_pwnedNamesCache');
- return breaches;
- } else {
- hibp._pwnedNamesCache[name] = '';
- hibp.storeCache('_pwnedNamesCache');
- return null;
}
+ this._pwnedNamesCache[name] = { date: Date.now(), val: breaches };
+ if (breaches) hLogger.info(`name ${name} pwned in ${breaches}`);
+ this.storeCache('_pwnedNamesCache');
+ return breaches;
});
}
};
- // checks if the input password (hashed in sha-1) is pawned in breaches on haveibeenpwned.
- // Uses a cache to avoid calling hibp again and again with the same values
- // Returns a promise resolving to a string containing the number of pwnages if pwned or null
+ /**
+ * Checks if the input password (hashed in sha-1) is pawned in breaches on haveibeenpwned.
+ * Uses a cache to avoid calling hibp again and again with the same values
+ * @param { string } pwd the sha1 hashed password to check
+ * @returns { Promise } a promise resolving to the number of pwnages if pwned or null
+ */
checkPwdPwned (passwordHash) {
passwordHash = passwordHash.toUpperCase();
- hibp.logger.info('check hibp pwd (hash) ' + passwordHash);
+ hLogger.debug('checking pwd (hashed) ' + passwordHash);
const prefix = passwordHash.substring(0, 5);
- if (hibp._pwnedPwdsCache[passwordHash]) {
- return (hibp._pwnedPwdsCache[passwordHash] !== ''
- ? hibp._pwnedPwdsCache[passwordHash] : null);
+ if (this.checkCache(this._pwnedPwdsCache[passwordHash])) {
+ const val = this._pwnedPwdsCache[passwordHash].val ? this._pwnedPwdsCache[passwordHash].val : null;
+ hLogger.debug('found pwd in cache: ' + passwordHash + ' val:' + val);
+ return Promise.resolve(val);
} else {
- return hibp._xhrpromise({
+ hLogger.debug('PWD NOT FOUND in cache: ' + passwordHash); // + ' cache=' + this.stringify(this._pwnedPwdsCache));
+ return HIBPUtils.xhrpromise({
url: `https://api.pwnedpasswords.com/range/${prefix}`,
method: 'GET',
responseType: 'text',
@@ -231,28 +370,39 @@ class HIBPUtils {
}).then(data => {
let nb = null;
if (data) {
- // hibp.logger.debug('found breaches ' + JSON.stringify(data));
+ // hLogger.debug('found breaches ' + JSON.stringify(data));
data.split('\r\n').some(line => {
const h = line.split(':');
const suffix = h[0];
if (prefix + suffix === passwordHash) {
nb = _.escape(h[1]);
- // hibp.logger.debug('matching breach ' + suffix);
+ // hLogger.debug('matching breach ' + suffix);
return true;
}
});
}
- hibp._pwnedPwdsCache[passwordHash] = nb || '';
- if (nb) hibp.logger.debug(`password ${passwordHash} pawned ${nb} times`);
- hibp.storeCache('_pwnedPwdsCache');
+ this._pwnedPwdsCache[passwordHash] = { date: Date.now(), val: nb };
+ if (nb) hLogger.info(`password ${passwordHash} pawned ${nb} times`);
+ this.storeCache('_pwnedPwdsCache');
return nb;
});
}
};
- // returns true if the pwd can be checked
+ /**
+ * filter passwords needing to be checked
+ * @param {string} pwd the password to check
+ * @returns {boolean} true if the pwd can be checked
+ */
elligiblePwd (pwd) {
return (pwd && pwd.replace(/\s/, '') !== '' && !pwd.startsWith('{REF:'));
}
+ /**
+ * Change the password field to display an alert or reset it depending on npwned value
+ * @param {View} dview the details view
+ * @param {integer} npwned the number of times the password has been pawned (or null or 0 if none)
+ * @param {string} warning the warning to display
+ * @param {...} args the arguments to be passed to the original 'fieldChanged' function
+ */
alertPwdPwned (dview, npwned, warning, args) {
if (npwned) { // pwned
// record pawnage in the model to be able to show it in list view
@@ -260,16 +410,31 @@ class HIBPUtils {
// calls original function
detailsViewFieldChanged.apply(dview, args);
// sets the alert
- hibp.alert(dview.passEditView.$el, warning);
+ this.alert(dview.passEditView.$el, warning);
} else { // not pwned
// reset css and tip
- hibp.passed(dview.passEditView.$el, 'check pwned password passed...');
+ this.passed(dview.passEditView.$el, 'check pwned password passed...');
// reset pawnage in the model
dview.model.pwdPwned = null;
// call initial function
detailsViewFieldChanged.apply(dview, args);
}
};
+ /**
+ * filter names needing to be checked
+ * @param {string} name the name to check
+ * @returns {boolean} true if the name can be checked
+ */
+ elligibleName(name) {
+ return (name && name.replace(/\s/, '') !== '');
+ }
+ /**
+ * Change the name field to display an alert or reset it depending on breaches value
+ * @param {View} dview the details view
+ * @param {string} breaches the breaches in which the name has been pawned (or null or '' if none)
+ * @param {string} warning the warning to display
+ * @param {...} args the arguments to be passed to the original 'fieldChanged' function
+ */
alertNamePwned (dview, breaches, warning, args) {
if (breaches) { // pwned
// remember breaches in the model to be able to show it in list view
@@ -277,148 +442,256 @@ class HIBPUtils {
// call initial function
detailsViewFieldChanged.apply(dview, args);
// adds an alert
- hibp.alert(dview.userEditView.$el, warning);
+ this.alert(dview.userEditView.$el, warning);
} else { // not pwned
// reset alert
- hibp.passed(dview.userEditView.$el, 'check pwned user name passed...');
+ this.passed(dview.userEditView.$el, 'check pwned user name passed...');
// reset the model
dview.model.namePwned = null;
// call initial function
detailsViewFieldChanged.apply(dview, args);
}
};
+ /**
+ * Looks up the password on HaveIBeenPwned and handle the results
+ * If the password is pawned, depending on the check level, puts some icon warning, or asks to revert to the previous one
+ * @param {DetailedView} dview the detailed view containing the password
+ * @param {string} pwd the pwd to check
+ */
+ handlePasswordChange(dview, pwd, args) {
+ pwd = pwd ? pwd.getText() : null;
+ if (hibp.elligiblePwd(pwd)) {
+ // hLogger.debug('pwd:>>>' + pwd + '<<<');
+ this.sha1(pwd)
+ .then(hpwd => {
+ return this.checkPwdPwned(hpwd);
+ })
+ .then(npwned => {
+ const warning = HIBPLocale.hibpPwdWarning.replace('{}', npwned);
+ if (npwned) { // pawned
+ if (this.checkPwnedPwd === HIBPCheckLevel.AskMe) {
+ // ask before taking the field change into account
+ Alerts.yesno({
+ header: HIBPLocale.hibpChangePwd,
+ body: warning,
+ icon: 'exclamation-triangle',
+ success: () => { // keep password, just set an alert
+ this.alertPwdPwned(dview, npwned, warning, args);
+ },
+ cancel: () => { // reset password by not registering change
+ hLogger.info('keeping old passwd');
+ }
+ });
+ } else { // check level = alert, keep pwd, set an alert
+ this.alertPwdPwned(dview, npwned, warning, args);
+ }
+ } else { // not pawned
+ this.alertPwdPwned(dview, null, null, args);
+ }
+ }).catch(error => {
+ hLogger.error('check pwned password error: ' + error.message);
+ });
+ } else {
+ this.alertPwdPwned(dview, null, null, args);
+ }
+ }
+ /**
+ * Looks up the user name on HaveIBeenPwned and handle the results
+ * If the name is pawned, depending on the check level, puts some icon warning, or asks to revert to the previous one
+ * @param {DetailedView} dview the detailed view containing the user name
+ * @param {string} name the user name to check
+ */
+ handleNameChange(dview, name, args) {
+ if (this.elligibleName(name)) {
+ this.checkNamePwned(name)
+ .then(breaches => {
+ if (breaches) { // pawned
+ name = _.escape(name); // breaches already escaped
+ const warning = HIBPLocale.hibpNameWarning.replace('{name}', name).replace('{breaches}', breaches);
+ if (this.checkPwnedName === HIBPCheckLevel.AskMe) {
+ // ask before taking the field change into account
+ Alerts.yesno({
+ header: HIBPLocale.hibpChangeName,
+ body: warning,
+ icon: 'exclamation-triangle',
+ success: () => { // keep name, but set an alert
+ this.alertNamePwned(dview, breaches, warning, args);
+ },
+ cancel: () => { // reset name by not registering change
+ hLogger.info('reverting to previous user name');
+ }
+ });
+ } else { // check level = alert, keep new name but sets an alert
+ this.alertNamePwned(dview, breaches, warning, args);
+ }
+ } else { // not pawned
+ this.alertNamePwned(dview, null, null, args);
+ }
+ }).catch(error => {
+ hLogger.error('check pwned name error: ' + error.message);
+ });
+ } else {
+ hibp.alertNamePwned(this, null, null, args);
+ }
+ }
+ displayFields(dview) {
+ // check password
+ const pwd = dview.model.password ? dview.model.password.getText() : null;
+ // hLogger.debug('addfv pwd:>>>' + pwd + '<<<');
+ if (this.checkPwnedPwd !== HIBPCheckLevel.None && this.elligiblePwd(pwd)) {
+ this.sha1(pwd)
+ .then(hpwd => {
+ return this.checkPwdPwned(hpwd);
+ })
+ .then(nb => {
+ // hLogger.debug(pwd + ' pppwand: ' + nb);
+ dview.model.pwdPwned = nb;
+ if (nb) { // pawned
+ const warning = HIBPLocale.hibpPwdWarning.replace('{}', nb);
+ this.alert(dview.passEditView.$el, warning);
+ } else { // not pawned
+ this.passed(dview.passEditView.$el, 'check pwned password passed...');
+ }
+ }).catch(error => {
+ hLogger.error('check pwned pwd error: ' + error);
+ });
+ }
+ // check user name
+ let name = dview.userEditView.value;
+ // hLogger.debug('addfv name:>>>' + name + '<<<');
+ if (this.elligibleName(name) && this.checkPwnedName !== HIBPCheckLevel.None) {
+ this.checkNamePwned(name)
+ .then(breaches => {
+ dview.model.namePwned = breaches;
+ if (breaches) { // pawned
+ name = _.escape(name); // breaches already escaped
+ const warning = HIBPLocale.hibpNameWarning.replace('{name}', name).replace('{breaches}', breaches);
+ this.alert(dview.userEditView.$el, warning);
+ } else { // not pawned
+ this.passed(dview.userEditView.$el, 'check pwned user name passed...');
+ }
+ }).catch(error => {
+ hLogger.error('check pwned name error: ' + HIBPUtils.stringify(error));
+ });
+ }
+ };
+ filterEntries(app, entries) {
+ // hLogger.debug('getEntriesByFilter: entries = ' + JSON.stringify(entries));
+ if (this.checkPwnedList && entries && entries.length) {
+ // get all different names and pwds to reduce the number of calls to the HIBP API
+ const names = [];
+ const pwds = [];
+ entries.forEach(item => {
+ // hLogger.debug('getEntriesByFilter: item = ' + item.title);
+ // get different user names and pwds to optimize queries
+ const name = item.user;
+ if (this.elligibleName(name)) {
+ const fname = names.find(elem => elem.name === name);
+ // hLogger.debug("fname=" + JSON.stringify(fname));
+ if (fname) fname.items.push(item);
+ else names.push({ name: name, items: [item] });
+ }
+ let pwd = item.password;
+ if (pwd) {
+ pwd = pwd.getText();
+ if (this.elligiblePwd(pwd)) {
+ const fpwd = pwds.find(elem => elem.pwd === pwd);
+ if (fpwd) fpwd.items.push(item);
+ else pwds.push({ pwd: pwd, items: [item] });
+ }
+ }
+ });
+
+ // asynchronously look for pawned names and pwds
+ // do somme throttling on names as HIBP does not allow more than one call every 1500 millisecs
+ let throttle = 2000; // millisecs betwwen 2 calls
+ names.forEach((elem, index) => {
+ // hLogger.debug('getEntriesByFilter: check item ' + item.title);
+ setTimeout(() => {
+ this.checkNamePwned(elem.name)
+ .then(breaches => {
+ let refresh = false;
+ elem.items.forEach(item => {
+ const itemPwned = item.namePwned;
+ item.namePwned = breaches;
+ refresh = refresh || (!breaches !== !itemPwned); // XOR
+ });
+ refresh && app.refresh();
+ })
+ .catch(err => {
+ hLogger.error('error in checking name ' + elem.name + 'in get entries by filter: ' + err);
+ });
+ }, index * throttle);
+ });
+ // no need of throttling for passwords on HIBP, use a low throttle value
+ throttle = 100; // millisecs
+ pwds.forEach((elem, index) => {
+ setTimeout(() => {
+ this.sha1(elem.pwd)
+ .then(hpwd => {
+ return this.checkPwdPwned(hpwd);
+ })
+ .then(nb => {
+ let refresh = false;
+ elem.items.forEach(item => {
+ const itemPwned = item.pwdPwned;
+ item.pwdPwned = nb;
+ refresh = refresh || (!nb !== !itemPwned); // XOR
+ });
+ refresh && app.refresh();
+ })
+ .catch(err => {
+ hLogger.error('error in checking pwd ' + elem.pwd + ' in get entries by filter: ' + err);
+ });
+ }, index * throttle);
+ });
+ }
+ }
};
-const hibp = new HIBPUtils();
+/** the HIBP singleton
+ * @type {HIBP}
+ */
+const hibp = new HIBP();
-// Replaces the fiedChanged function of DetailsView to add checks on user names and passwords
+/**
+ * Replaces the fiedChanged function of DetailsView to add checks on user names and passwords
+ * @param {Event} e the event that triggered the change
+ */
DetailsView.prototype.fieldChanged = function (e) {
if (e.field) {
- // hibp.logger.debug('field changed ' + hibp.stringify(e));
+ // hLogger.debug('field changed ' + hibp.stringify(e));
// first check password
if (e.field === '$Password' && hibp.checkPwnedPwd !== HIBPCheckLevel.None && this.passEditView.value) {
- const pwd = e.val ? e.val.getText() : null;
- if (hibp.elligiblePwd(pwd)) {
- hibp.logger.debug('pwd:>>>' + pwd + '<<<');
- hibp.sha1(pwd)
- .then(hibp.checkPwdPwned)
- .then(npwned => {
- const warning = HIBPLocale.hibpPwdWarning.replace('{}', npwned);
- if (npwned) { // pawned
- if (hibp.checkPwnedPwd === HIBPCheckLevel.AskMe) {
- // ask before taking the field change into account
- Alerts.yesno({
- header: HIBPLocale.hibpChangePwd,
- body: warning,
- icon: 'exclamation-triangle',
- success: () => { // keep password, just set an alert
- hibp.alertPwdPwned(this, npwned, warning, arguments);
- },
- cancel: () => { // reset password by not registering change
- hibp.logger.info('keeping old passwd');
- }
- });
- } else { // check level = alert, keep pwd, set an alert
- hibp.alertPwdPwned(this, npwned, warning, arguments);
- }
- } else { // not pawned
- hibp.alertPwdPwned(this, null, null, arguments);
- }
- }).catch(error => {
- hibp.logger.info('check pwned password error: ' + error.message);
- });
- } else {
- hibp.alertPwdPwned(this, null, null, arguments);
- }
+ hibp.handlePasswordChange(this, e.val, arguments);
// second, check user name
} else if (e.field === '$UserName' && hibp.checkPwnedName !== HIBPCheckLevel.None) {
- let name = e.val;
- if (name && name.replace(/\s/, '') !== '') {
- hibp.checkNamePwned(name)
- .then(breaches => {
- if (breaches) { // pawned
- name = _.escape(name); // breaches already escaped
- const warning = HIBPLocale.hibpNameWarning.replace('{name}', name).replace('{breaches}', breaches);
- if (hibp.checkPwnedName === HIBPCheckLevel.AskMe) {
- // ask before taking the field change into account
- Alerts.yesno({
- header: HIBPLocale.hibpChangeName,
- body: warning,
- icon: 'exclamation-triangle',
- success: () => { // keep name, but set an alert
- hibp.alertNamePwned(this, breaches, warning, arguments);
- },
- cancel: () => { // reset name by not registering change
- hibp.logger.info('keeping old user name');
- }
- });
- } else { // check level = alert, keep new name but sets an alert
- hibp.alertNamePwned(this, breaches, warning, arguments);
- }
- } else { // not pawned
- hibp.alertNamePwned(this, null, null, arguments);
- }
- }).catch(error => {
- hibp.logger.info('check pwned name error: ' + error.message);
- });
- }
- } else {
- hibp.alertNamePwned(this, null, null, arguments);
+ hibp.handleNameChange(this, e.val, arguments);
}
- } else {
+ } else { // not name, not password
detailsViewFieldChanged.apply(this, arguments);
}
};
-// replaces initial addFieldViews in DetailsView
-// Allows showing pwned fields when displaying entry details
+/**
+ * Replaces initial addFieldViews function in DetailsView
+ * Allows showing pawned fields when displaying entry details
+ */
DetailsView.prototype.addFieldViews = function () {
// call initial function
detailsViewAddFieldViews.apply(this, arguments);
- // check password
- const pwd = this.model.password ? this.model.password.getText() : null;
- // hibp.logger.debug('addfv pwd:>>>' + pwd + '<<<');
- if (hibp.checkPwnedPwd !== HIBPCheckLevel.None && hibp.elligiblePwd(pwd)) {
- hibp.sha1(pwd)
- .then(hibp.checkPwdPwned)
- .then(npwned => {
- this.model.pwdPwned = npwned;
- if (npwned) { // pawned
- const warning = HIBPLocale.hibpPwdWarning.replace('{}', npwned);
- hibp.alert(this.passEditView.$el, warning);
- } else { // not pawned
- hibp.passed(this.passEditView.$el, 'check pwned password passed...');
- }
- }).catch(error => {
- hibp.logger.info('check pwned pwd error: ' + error);
- });
- }
- // check user name
- let name = this.userEditView.value;
- // hibp.logger.debug('addfv name:>>>' + name + '<<<');
- if (name && name.replace(/\s/, '') !== '' && hibp.checkPwnedName !== HIBPCheckLevel.None) {
- hibp.checkNamePwned(name)
- .then(breaches => {
- this.model.namePwned = breaches;
- if (breaches) { // pawned
- name = _.escape(name); // breaches already escaped
- const warning = HIBPLocale.hibpNameWarning.replace('{name}', name).replace('{breaches}', breaches);
- hibp.alert(this.userEditView.$el, warning);
- } else { // not pawned
- hibp.passed(this.userEditView.$el, 'check pwned user name passed...');
- }
- }).catch(error => {
- hibp.logger.info('check pwned name error: ' + hibp.stringify(error));
- });
- }
+ hibp.displayFields(this);
};
+/**
+ * Replaces initial render function in ListView
+ *
+ */
ListView.prototype.render = function () {
listViewRender.apply(this, arguments);
- hibp.logger.debug('rendering list in hibp');
- // this.items.forEach(hibp.showItem);
+ hLogger.debug('rendering list in hibp');
this.items.filter(item => item.namePwned || item.pwdPwned).forEach(item => {
- hibp.logger.debug('list pwned ' + item.title);
+ hLogger.debug('list pwned item "' + item.title + '"');
const itemEl = document.getElementById(item.id);
if (itemEl) { itemEl.classList.add('hibp-pwned'); }
});
@@ -426,44 +699,10 @@ ListView.prototype.render = function () {
AppModel.prototype.getEntriesByFilter = function (filter) {
const entries = appModelGetEntriesByFilter.apply(this, arguments);
- if (hibp.checkPwnedList && entries && entries.length) {
- // asynchronously look for pawned names and pwds
- setTimeout(() => {
- entries.forEach(item => {
- hibp.logger.debug('getEntriesByFilter: check item ' + item.title);
- hibp.checkNamePwned(item.user)
- .then(breaches => {
- const itemPwned = item.namePwned;
- item.namePwned = breaches;
- if (!breaches !== !itemPwned) { // XOR
- this.refresh();
- }
- });
- const pwd = item.password ? item.password.getText() : null;
- if (hibp.elligiblePwd(pwd)) {
- hibp.sha1(pwd)
- .then(hibp.checkPwdPwned)
- .then(nb => {
- const itemPwned = item.pwdPwned;
- item.pwdPwned = nb;
- if (!nb !== !itemPwned) { // XOR
- this.refresh();
- }
- });
- }
- });
- }, 20);
- }
+ hibp.filterEntries(this, entries);
return entries;
};
-// for debug purpose
-// const dvrender = DetailsView.prototype.render;
-// DetailsView.prototype.render = function () {
-// dvrender.apply(this, arguments);
-// hibp.showItem(this.model);
-// },
-
module.exports.getSettings = function () {
const options = [
{ value: HIBPCheckLevel.None, label: HIBPLocale.hibpCheckLevelNone },
@@ -498,7 +737,7 @@ module.exports.setSettings = function (changes) {
const ccfield = field.substr(0, 1).toLowerCase() + field.substring(1);
hibp[ccfield] = changes[field];
}
- hibp.logger.debug(hibp.stringify(hibp));
+ HIBPUtils.dump(hibp);
};
module.exports.uninstall = function () {