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",
|
||||
"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",
|
||||
|
|
|
@ -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 += '<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
|
||||
// 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 () {
|
||||
|
|
Loading…
Reference in New Issue