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