Major refactoring #2:
- split code in 2 classes: HIBPUtils and HIBP - move most code to HIBP class - throttling of calling to HIBP to avoid HIBP rate limits and cloudfare ddos protection - storage cache (based on IOBrowser) - documentation
This commit is contained in:
parent
746c7eedd7
commit
b9de642c42
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "0.4.0",
|
"version": "0.6.0",
|
||||||
"manifestVersion": "0.1.0",
|
"manifestVersion": "0.1.0",
|
||||||
"name": "haveibeenpwned",
|
"name": "haveibeenpwned",
|
||||||
"description": "Check HaveIBeenPwned password database",
|
"description": "Check HaveIBeenPwned password database",
|
||||||
|
@ -9,7 +9,7 @@
|
||||||
"url": "https://github.com/leolivier"
|
"url": "https://github.com/leolivier"
|
||||||
},
|
},
|
||||||
"resources": {
|
"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=="
|
"css": "hL9Bv6NIxyP1vsHPZftppZSoM2zvP+H8ZxaEP4NwFZPoWlWZvn6l7f/x8FJly7mv7mFqpygMP4qZguPk9ZAcE5C5wj6mI9RloUg3f+500rN1mEVTpd+wH7AxR0eh/tjrYK0oq9BKpM099NknZ/t0J+aWODlk7HHbo9R2HUuvG2cpY2R137z0vkI8NPl2KQWi5vwIzKa+/EUwVIxoRApg603XzUgK8FFvAcrCsNdrXjoKEuQpZfEz8XBMjp62SuKS8KxdtyyJdsGbNrPkR8gc52ZBY0yFPC+FrSVhl6sxvu6UAMXW5xVtN5pE2Pn7+jnaVnaCafPYeqpDk5brf8G3JA=="
|
||||||
},
|
},
|
||||||
"url": "https://plugins.keeweb.info/plugins/haveibeenpwned",
|
"url": "https://plugins.keeweb.info/plugins/haveibeenpwned",
|
||||||
|
|
|
@ -5,10 +5,22 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const Logger = require('util/logger');
|
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 = {
|
const HIBPLocale = {
|
||||||
hibpCheckPwnedPwd: 'Should I check passwords against HaveIBeenPwned list?',
|
hibpCheckPwnedPwd: 'Should I check passwords against HaveIBeenPwned list?',
|
||||||
hibpCheckPwnedName: 'Should I check user name against HaveIBeenPwned list?',
|
hibpCheckPwnedName: 'Should I check user name against HaveIBeenPwned list?',
|
||||||
|
@ -23,12 +35,18 @@ const HIBPLocale = {
|
||||||
hibpApiError: 'HaveIBeenPwned API error'
|
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 = {
|
const HIBPCheckLevel = {
|
||||||
None: 'none',
|
None: 'none',
|
||||||
Alert: 'alert',
|
Alert: 'alert',
|
||||||
AskMe: 'askme'
|
AskMe: 'askme'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Required modules */
|
||||||
const DetailsView = require('views/details/details-view');
|
const DetailsView = require('views/details/details-view');
|
||||||
const ListView = require('views/list-view');
|
const ListView = require('views/list-view');
|
||||||
const AppModel = require('models/app-model');
|
const AppModel = require('models/app-model');
|
||||||
|
@ -37,59 +55,85 @@ const Kdbxweb = require('kdbxweb');
|
||||||
const _ = require('_');
|
const _ = require('_');
|
||||||
const Tip = require('util/tip');
|
const Tip = require('util/tip');
|
||||||
const Alerts = require('comp/alerts');
|
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 detailsViewFieldChanged = DetailsView.prototype.fieldChanged;
|
||||||
const detailsViewAddFieldViews = DetailsView.prototype.addFieldViews;
|
const detailsViewAddFieldViews = DetailsView.prototype.addFieldViews;
|
||||||
const listViewRender = ListView.prototype.render;
|
const listViewRender = ListView.prototype.render;
|
||||||
const appModelGetEntriesByFilter = AppModel.prototype.getEntriesByFilter;
|
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() {
|
constructor() {
|
||||||
_seen = [];
|
super();
|
||||||
// the 3 options with their default values
|
this.name = 'cache';
|
||||||
this.checkPwnedPwd = HIBPCheckLevel.Alert;
|
this.enabled = IoBrowserCache.enabled;
|
||||||
this.checkPwnedName = HIBPCheckLevel.Alert;
|
this.system = true;
|
||||||
this.checkPwnedList = false;
|
this.init(); // storage base
|
||||||
// cache variables
|
this.io = new IoBrowserCache({
|
||||||
this._pwnedNamesCache = {};
|
cacheName: 'HIBPCache',
|
||||||
this._pwnedPwdsCache = {};
|
logger: hLogger
|
||||||
// local logger
|
});
|
||||||
this.logger = new Logger('HaveIBeenPwned');
|
|
||||||
this.logger.setLevel(LogLevel);
|
|
||||||
};
|
};
|
||||||
// used for cyclic stringifier
|
|
||||||
_replacer(key, value) {
|
save(id, data, callback) {
|
||||||
if (value != null && typeof value === 'object') {
|
this.io.save(id, data, callback);
|
||||||
if (_seen.indexOf(value) >= 0) {
|
};
|
||||||
return;
|
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;
|
||||||
}
|
});
|
||||||
return value;
|
|
||||||
};
|
|
||||||
// cyclic objects enabled stringifier
|
|
||||||
stringify(obj) {
|
|
||||||
const ret = JSON.stringify(obj, hibp._replacer);
|
|
||||||
_seen = [];
|
_seen = [];
|
||||||
return ret;
|
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();
|
const err = new Error();
|
||||||
hibp.logger.debug(err.stack);
|
hLogger.debug(err.stack);
|
||||||
}
|
},
|
||||||
// show the details of an entry in debug mode
|
/**
|
||||||
showItem(model) {
|
* XML HTTP Request with Promises,
|
||||||
if (model) {
|
* modified from StorageBase
|
||||||
hibp.logger.debug('show entry ' + model.title +
|
* @param {object} config the XML HTTP Request configuration. Same as StorageBase
|
||||||
': name=' + model.user + ', pwd=' + (model.password ? model.password.getText() : 'undefined') +
|
* @returns {Promise}
|
||||||
', namePwned=' + model.namePwned + ', pwdPwned=' + model.pwdPwned
|
*/
|
||||||
);
|
xhrpromise: (config) => {
|
||||||
}
|
|
||||||
}
|
|
||||||
// XML HTTP Request with Promises, modified from StorageBase
|
|
||||||
_xhrpromise(config) {
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
if (config.responseType) {
|
if (config.responseType) {
|
||||||
|
@ -106,63 +150,95 @@ class HIBPUtils {
|
||||||
if (statuses.indexOf(xhr.status) >= 0) {
|
if (statuses.indexOf(xhr.status) >= 0) {
|
||||||
resolve(xhr.response);
|
resolve(xhr.response);
|
||||||
} else {
|
} else {
|
||||||
|
hLogger.error(HIBPLocale.hibpApiError, 'GET', xhr.status, xhr.statusText);
|
||||||
reject(xhr.statusText);
|
reject(xhr.statusText);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
xhr.addEventListener('error', () => {
|
xhr.addEventListener('error', () => {
|
||||||
const err = xhr.response && xhr.response.error || new Error('Network 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);
|
reject(xhr.statusText);
|
||||||
});
|
});
|
||||||
xhr.send(config.data);
|
xhr.send(config.data);
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
// transforms a byte array into an hex string
|
/**
|
||||||
_hex(buffer) {
|
* Applies a digest algorithm on input string
|
||||||
const hexCodes = [];
|
* @param {algo} algorithm to be applied (e.g. 'SHA-1' 'SHA-2456'
|
||||||
const view = new DataView(buffer);
|
* @returns {string} the "digested" hex string
|
||||||
for (let i = 0; i < view.byteLength; i += 4) {
|
*/
|
||||||
// Using getUint32 reduces the number of iterations needed (we process 4 bytes each time)
|
digest: (algo, str) => {
|
||||||
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) {
|
|
||||||
const buffer = Kdbxweb.ByteUtils.stringToBytes(str);
|
const buffer = Kdbxweb.ByteUtils.stringToBytes(str);
|
||||||
const subtle = window.crypto.subtle || window.crypto.webkitSubtle;
|
const subtle = window.crypto.subtle || window.crypto.webkitSubtle;
|
||||||
return subtle.digest(algo, buffer).then(hash => {
|
return subtle.digest(algo, buffer).then(buffer => {
|
||||||
return hibp._hex(hash);
|
// 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);
|
/**
|
||||||
};
|
* This is were most HaveIBeenPwned stuff lies.
|
||||||
// returns the SHA-256 hex string of the input string
|
*/
|
||||||
sha256(str) {
|
class HIBP {
|
||||||
return hibp._digest('SHA-256', str);
|
constructor() {
|
||||||
};
|
// the 3 options with their default values
|
||||||
// add css stuff + tip on fields to show an alert on pawned fields
|
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) {
|
alert(el, msg) {
|
||||||
hibp.logger.info(msg);
|
hLogger.info(msg);
|
||||||
el.focus();
|
el.focus();
|
||||||
el.addClass('input--error');
|
el.addClass('input--error');
|
||||||
el.find('.details__field-value').addClass('hibp-pwned');
|
el.find('.details__field-value').addClass('hibp-pwned');
|
||||||
Tip.createTip(el, { title: msg, placement: 'bottom' });
|
Tip.createTip(el, { title: msg, placement: 'bottom' });
|
||||||
InputFx.shake(el);
|
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) {
|
passed(el, msg) {
|
||||||
hibp.logger.info(msg);
|
hLogger.info(msg);
|
||||||
el.removeClass('input--error');
|
el.removeClass('input--error');
|
||||||
el.find('.details__field-value').removeClass('hibp-pwned');
|
el.find('.details__field-value').removeClass('hibp-pwned');
|
||||||
const tip = el._tip;
|
const tip = el._tip;
|
||||||
|
@ -171,22 +247,83 @@ class HIBPUtils {
|
||||||
tip.title = null;
|
tip.title = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// store the cache variable name cacheName in local storage
|
/**
|
||||||
storeCache(cacheName) {
|
* Store the desired cache in local storage
|
||||||
// TODO: implement this method
|
* @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
|
* Load desired cache from local storage
|
||||||
// Returns a promise resolving to an html string containing a list of breaches names if pwned or null
|
* Values older than CacheTTL are discarded
|
||||||
checkNamePwned (name) {
|
* @param {string} cacheVarName the name of the cache variable ('_pwnedNamesCache' or '_pwnedPwdsCache')
|
||||||
hibp.logger.info('check hibp name ' + name);
|
*/
|
||||||
if (hibp._pwnedNamesCache[name]) {
|
loadCache(cacheVarName) {
|
||||||
return Promise.resolve(hibp._pwnedNamesCache[name] !== '' ? hibp._pwnedNamesCache[name] : null);
|
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 {
|
} 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`;
|
const url = `https://haveibeenpwned.com/api/v2/breachedaccount/${name}?truncateResponse=true`;
|
||||||
// hibp.logger.debug('url ' + url);
|
// hLogger.debug('url ' + url);
|
||||||
return hibp._xhrpromise({
|
return HIBPUtils.xhrpromise({
|
||||||
url: url,
|
url: url,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
responseType: 'json',
|
responseType: 'json',
|
||||||
|
@ -194,34 +331,36 @@ class HIBPUtils {
|
||||||
data: null,
|
data: null,
|
||||||
statuses: [200, 404]
|
statuses: [200, 404]
|
||||||
}).then(data => {
|
}).then(data => {
|
||||||
|
let breaches = null;
|
||||||
if (data && data.length > 0) {
|
if (data && data.length > 0) {
|
||||||
hibp.logger.debug('found breaches ' + JSON.stringify(data));
|
// hLogger.debug('found breaches ' + JSON.stringify(data));
|
||||||
let breaches = '';
|
breaches = '';
|
||||||
data.forEach(breach => { breaches += '<li>' + _.escape(breach.Name) + '</li>\n'; });
|
data.forEach(breach => { breaches += '<li>' + _.escape(breach.Name) + '</li>\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
|
* Checks if the input password (hashed in sha-1) is pawned in breaches on haveibeenpwned.
|
||||||
// Returns a promise resolving to a string containing the number of pwnages if pwned or null
|
* 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) {
|
checkPwdPwned (passwordHash) {
|
||||||
passwordHash = passwordHash.toUpperCase();
|
passwordHash = passwordHash.toUpperCase();
|
||||||
hibp.logger.info('check hibp pwd (hash) ' + passwordHash);
|
hLogger.debug('checking pwd (hashed) ' + passwordHash);
|
||||||
const prefix = passwordHash.substring(0, 5);
|
const prefix = passwordHash.substring(0, 5);
|
||||||
if (hibp._pwnedPwdsCache[passwordHash]) {
|
if (this.checkCache(this._pwnedPwdsCache[passwordHash])) {
|
||||||
return (hibp._pwnedPwdsCache[passwordHash] !== ''
|
const val = this._pwnedPwdsCache[passwordHash].val ? this._pwnedPwdsCache[passwordHash].val : null;
|
||||||
? hibp._pwnedPwdsCache[passwordHash] : null);
|
hLogger.debug('found pwd in cache: ' + passwordHash + ' val:' + val);
|
||||||
|
return Promise.resolve(val);
|
||||||
} else {
|
} 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}`,
|
url: `https://api.pwnedpasswords.com/range/${prefix}`,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
responseType: 'text',
|
responseType: 'text',
|
||||||
|
@ -231,28 +370,39 @@ class HIBPUtils {
|
||||||
}).then(data => {
|
}).then(data => {
|
||||||
let nb = null;
|
let nb = null;
|
||||||
if (data) {
|
if (data) {
|
||||||
// hibp.logger.debug('found breaches ' + JSON.stringify(data));
|
// hLogger.debug('found breaches ' + JSON.stringify(data));
|
||||||
data.split('\r\n').some(line => {
|
data.split('\r\n').some(line => {
|
||||||
const h = line.split(':');
|
const h = line.split(':');
|
||||||
const suffix = h[0];
|
const suffix = h[0];
|
||||||
if (prefix + suffix === passwordHash) {
|
if (prefix + suffix === passwordHash) {
|
||||||
nb = _.escape(h[1]);
|
nb = _.escape(h[1]);
|
||||||
// hibp.logger.debug('matching breach ' + suffix);
|
// hLogger.debug('matching breach ' + suffix);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
hibp._pwnedPwdsCache[passwordHash] = nb || '';
|
this._pwnedPwdsCache[passwordHash] = { date: Date.now(), val: nb };
|
||||||
if (nb) hibp.logger.debug(`password ${passwordHash} pawned ${nb} times`);
|
if (nb) hLogger.info(`password ${passwordHash} pawned ${nb} times`);
|
||||||
hibp.storeCache('_pwnedPwdsCache');
|
this.storeCache('_pwnedPwdsCache');
|
||||||
return nb;
|
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) {
|
elligiblePwd (pwd) {
|
||||||
return (pwd && pwd.replace(/\s/, '') !== '' && !pwd.startsWith('{REF:'));
|
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) {
|
alertPwdPwned (dview, npwned, warning, args) {
|
||||||
if (npwned) { // pwned
|
if (npwned) { // pwned
|
||||||
// record pawnage in the model to be able to show it in list view
|
// record pawnage in the model to be able to show it in list view
|
||||||
|
@ -260,16 +410,31 @@ class HIBPUtils {
|
||||||
// calls original function
|
// calls original function
|
||||||
detailsViewFieldChanged.apply(dview, args);
|
detailsViewFieldChanged.apply(dview, args);
|
||||||
// sets the alert
|
// sets the alert
|
||||||
hibp.alert(dview.passEditView.$el, warning);
|
this.alert(dview.passEditView.$el, warning);
|
||||||
} else { // not pwned
|
} else { // not pwned
|
||||||
// reset css and tip
|
// 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
|
// reset pawnage in the model
|
||||||
dview.model.pwdPwned = null;
|
dview.model.pwdPwned = null;
|
||||||
// call initial function
|
// call initial function
|
||||||
detailsViewFieldChanged.apply(dview, args);
|
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) {
|
alertNamePwned (dview, breaches, warning, args) {
|
||||||
if (breaches) { // pwned
|
if (breaches) { // pwned
|
||||||
// remember breaches in the model to be able to show it in list view
|
// remember breaches in the model to be able to show it in list view
|
||||||
|
@ -277,148 +442,256 @@ class HIBPUtils {
|
||||||
// call initial function
|
// call initial function
|
||||||
detailsViewFieldChanged.apply(dview, args);
|
detailsViewFieldChanged.apply(dview, args);
|
||||||
// adds an alert
|
// adds an alert
|
||||||
hibp.alert(dview.userEditView.$el, warning);
|
this.alert(dview.userEditView.$el, warning);
|
||||||
} else { // not pwned
|
} else { // not pwned
|
||||||
// reset alert
|
// 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
|
// reset the model
|
||||||
dview.model.namePwned = null;
|
dview.model.namePwned = null;
|
||||||
// call initial function
|
// call initial function
|
||||||
detailsViewFieldChanged.apply(dview, args);
|
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) {
|
DetailsView.prototype.fieldChanged = function (e) {
|
||||||
if (e.field) {
|
if (e.field) {
|
||||||
// hibp.logger.debug('field changed ' + hibp.stringify(e));
|
// hLogger.debug('field changed ' + hibp.stringify(e));
|
||||||
// first check password
|
// first check password
|
||||||
if (e.field === '$Password' && hibp.checkPwnedPwd !== HIBPCheckLevel.None && this.passEditView.value) {
|
if (e.field === '$Password' && hibp.checkPwnedPwd !== HIBPCheckLevel.None && this.passEditView.value) {
|
||||||
const pwd = e.val ? e.val.getText() : null;
|
hibp.handlePasswordChange(this, e.val, arguments);
|
||||||
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);
|
|
||||||
}
|
|
||||||
// second, check user name
|
// second, check user name
|
||||||
} else if (e.field === '$UserName' && hibp.checkPwnedName !== HIBPCheckLevel.None) {
|
} else if (e.field === '$UserName' && hibp.checkPwnedName !== HIBPCheckLevel.None) {
|
||||||
let name = e.val;
|
hibp.handleNameChange(this, e.val, arguments);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else { // not name, not password
|
||||||
detailsViewFieldChanged.apply(this, arguments);
|
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 () {
|
DetailsView.prototype.addFieldViews = function () {
|
||||||
// call initial function
|
// call initial function
|
||||||
detailsViewAddFieldViews.apply(this, arguments);
|
detailsViewAddFieldViews.apply(this, arguments);
|
||||||
// check password
|
hibp.displayFields(this);
|
||||||
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));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces initial render function in ListView
|
||||||
|
*
|
||||||
|
*/
|
||||||
ListView.prototype.render = function () {
|
ListView.prototype.render = function () {
|
||||||
listViewRender.apply(this, arguments);
|
listViewRender.apply(this, arguments);
|
||||||
hibp.logger.debug('rendering list in hibp');
|
hLogger.debug('rendering list in hibp');
|
||||||
// this.items.forEach(hibp.showItem);
|
|
||||||
this.items.filter(item => item.namePwned || item.pwdPwned).forEach(item => {
|
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);
|
const itemEl = document.getElementById(item.id);
|
||||||
if (itemEl) { itemEl.classList.add('hibp-pwned'); }
|
if (itemEl) { itemEl.classList.add('hibp-pwned'); }
|
||||||
});
|
});
|
||||||
|
@ -426,44 +699,10 @@ ListView.prototype.render = function () {
|
||||||
|
|
||||||
AppModel.prototype.getEntriesByFilter = function (filter) {
|
AppModel.prototype.getEntriesByFilter = function (filter) {
|
||||||
const entries = appModelGetEntriesByFilter.apply(this, arguments);
|
const entries = appModelGetEntriesByFilter.apply(this, arguments);
|
||||||
if (hibp.checkPwnedList && entries && entries.length) {
|
hibp.filterEntries(this, entries);
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
return 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 () {
|
module.exports.getSettings = function () {
|
||||||
const options = [
|
const options = [
|
||||||
{ value: HIBPCheckLevel.None, label: HIBPLocale.hibpCheckLevelNone },
|
{ 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);
|
const ccfield = field.substr(0, 1).toLowerCase() + field.substring(1);
|
||||||
hibp[ccfield] = changes[field];
|
hibp[ccfield] = changes[field];
|
||||||
}
|
}
|
||||||
hibp.logger.debug(hibp.stringify(hibp));
|
HIBPUtils.dump(hibp);
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports.uninstall = function () {
|
module.exports.uninstall = function () {
|
||||||
|
|
Loading…
Reference in New Issue