2018-03-18 17:37:36 +01:00
/ * *
* KeeWeb plugin : haveibeenpwned
* @ author Olivier LEVILLAIN
* @ license MIT
* /
2019-12-29 16:56:09 +01:00
const Logger = require ( 'util/logger' ) . Logger ;
2018-03-25 13:51:50 +02:00
// change log level here.
2018-03-24 21:20:25 +01:00
const LogLevel = Logger . Level . Info ;
2018-03-24 20:55:14 +01:00
2019-12-29 16:56:09 +01:00
const DetailsView = require ( 'views/details/details-view' ) . DetailsView ;
const InputFx = require ( 'util/ui/input-fx' ) . InputFx ;
2018-03-18 20:15:07 +01:00
const Kdbxweb = require ( 'kdbxweb' ) ;
2019-12-29 16:56:09 +01:00
const utilFn = require ( 'util/fn' ) ;
const Tip = require ( 'util/ui/tip' ) . Tip ;
2018-03-19 22:03:13 +01:00
const detailsViewFieldChanged = DetailsView . prototype . fieldChanged ;
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-24 12:30:03 +01:00
this . checkPwnedPwd = true ;
this . checkPwnedName = true ;
this . blockPwnedPwd = false ;
this . blockPwnedName = false ;
this . logger = new Logger ( 'HaveIBeenPwned' ) ;
2018-03-24 20:55:14 +01:00
this . logger . setLevel ( LogLevel ) ;
2018-03-18 20:15:07 +01:00
} ;
2019-12-29 16:56:09 +01:00
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
} ;
2019-12-29 16:56:09 +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
} ;
2019-12-29 16:56:09 +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' , ( ) => {
2018-03-24 20:55:14 +01:00
const err = xhr . response && xhr . response . error || new Error ( 'Network error' ) ;
this . logger . error ( 'HaveIBeenPwned API error' , 'GET' , xhr . status , err ) ;
err . status = xhr . status ;
return err ;
2018-03-18 17:37:36 +01:00
} ) ;
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 ) {
2019-12-29 19:21:12 +01:00
for ( const key in config . headers ) {
2019-12-29 16:56:09 +01:00
xhr . setRequestHeader ( key , config . headers [ key ] ) ;
} ;
2018-03-18 20:15:07 +01:00
} ;
2018-03-18 17:37:36 +01:00
xhr . send ( config . data ) ;
} ;
2019-12-29 16:56:09 +01:00
2018-03-18 17:37:36 +01:00
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 ( '' ) ;
} ;
2019-12-29 16:56:09 +01:00
2018-03-18 17:37:36 +01:00
digest ( algo , str ) {
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
} ) ;
} ;
2019-12-29 16:56:09 +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 ) ;
} ;
2019-12-29 16:56:09 +01:00
2018-03-18 17:37:36 +01:00
sha256 ( str ) {
2018-03-18 20:15:07 +01:00
return this . digest ( 'SHA-256' , str ) ;
} ;
2019-12-29 16:56:09 +01:00
2018-03-24 20:55:14 +01:00
alert ( el , msg ) {
// Alerts.info({ body: msg, title: 'HaveIBeenPwned' });
el . focus ( ) ;
el . addClass ( 'input--error' ) ;
el . addClass ( 'hibp-pwned' ) ;
Tip . createTip ( el , { title : msg , placement : 'bottom' } ) ;
InputFx . shake ( el ) ;
2018-03-18 20:15:07 +01:00
} ;
2019-12-29 16:56:09 +01:00
2018-03-24 20:55:14 +01:00
passed ( el , msg ) {
hibp . logger . info ( msg ) ;
el . removeClass ( 'input--error' ) ;
el . removeClass ( 'hibp-pwned' ) ;
}
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-24 12:30:03 +01:00
hibp . 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-24 20:55:14 +01:00
hibp . logger . debug ( 'url ' + url ) ;
2018-03-18 20:15:07 +01:00
hibp . xhrcall ( {
2018-03-18 17:37:36 +01:00
url : url ,
method : 'GET' ,
responseType : 'json' ,
2019-12-29 19:21:12 +01:00
headers : { 'Access-Control-Allow-Origin' : '*' } ,
2018-03-18 17:37:36 +01:00
data : null ,
statuses : [ 200 , 404 ] ,
success : ( data , xhr ) => {
if ( data && data . length > 0 ) {
2018-03-24 20:55:14 +01:00
hibp . logger . debug ( 'found breaches ' + JSON . stringify ( data ) ) ;
2018-03-18 20:15:07 +01:00
let breaches = '' ;
2019-12-29 16:56:09 +01:00
data . forEach ( breach => { breaches += '<li>' + utilFn . escape ( breach . Name ) + '</li>\n' ; } ) ;
2018-03-24 20:55:14 +01:00
hibp . alert ( this . userEditView . $el , ` 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
} else {
2018-03-24 20:55:14 +01:00
hibp . passed ( this . userEditView . $el , 'check pwned user name passed...' ) ;
2018-03-18 17:37:36 +01:00
}
}
2018-03-18 20:15:07 +01:00
} ) ;
2018-03-18 17:37:36 +01:00
} ;
DetailsView . prototype . checkPwdPwned = function ( passwordHash ) {
2018-03-24 12:30:03 +01:00
hibp . logger . info ( 'check hibp pwd (hash) ' + passwordHash ) ;
2018-03-18 20:15:07 +01:00
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' ,
2019-12-29 16:56:09 +01:00
headers : { 'Access-Control-Allow-Origin' : '*' } ,
2018-03-18 17:37:36 +01:00
data : null ,
statuses : [ 200 , 404 ] ,
success : data => {
if ( data ) {
2018-03-24 20:55:14 +01:00
hibp . logger . debug ( '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 ) {
2019-12-29 16:56:09 +01:00
const nb = utilFn . escape ( h [ 1 ] ) ;
2018-03-24 20:55:14 +01:00
hibp . alert ( this . passEditView . $el , ` 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
}
} ) ;
} else {
2018-03-24 20:55:14 +01:00
hibp . passed ( this . userEditView . $el , 'check pwned password passed...' ) ;
2018-03-18 20:15:07 +01:00
}
2018-03-18 17:37:36 +01:00
}
} ) ;
} ;
DetailsView . prototype . fieldChanged = function ( e ) {
2018-03-18 20:15:07 +01:00
detailsViewFieldChanged . apply ( this , arguments ) ;
2018-03-18 17:37:36 +01:00
if ( e . field ) {
2018-03-24 20:55:14 +01:00
hibp . logger . debug ( 'field changed ' + hibp . stringify ( e ) ) ;
2018-03-24 12:30:03 +01:00
if ( e . field === '$Password' && hibp . 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-24 12:30:03 +01:00
} else if ( e . field === '$UserName' && hibp . checkPwnedName ) {
2018-03-18 17:37:36 +01:00
this . checkNamePwned ( e . val ) ;
}
}
} ;
module . exports . getSettings = function ( ) {
2018-03-24 20:55:14 +01:00
return [ {
2018-03-18 17:37:36 +01:00
name : 'checkPwnedPwd' ,
label : 'Check passwords against HaveIBeenPwned list' ,
type : 'checkbox' ,
2018-03-24 12:30:03 +01:00
value : hibp . checkPwnedPwd
2019-12-29 17:31:58 +01:00
// disabled since API V3 of HaveIbeenPwned is not free anymore for checking accounts
// }, {
// name: 'checkPwnedName',
// label: 'Check user ids against HaveIBeenPwned list',
// type: 'checkbox',
// value: hibp.checkPwnedName
2018-03-18 17:37:36 +01:00
} , {
name : 'blockPwnedPwd' ,
label : 'Block pwned passwords if they are in HaveIBeenPwned list' ,
type : 'checkbox' ,
2018-03-24 12:30:03 +01:00
value : hibp . blockPwnedPwd
2019-12-29 17:31:58 +01:00
// }, {
// name: 'blockPwnedName',
// label: 'Block pwned names if they are in HaveIBeenPwned list',
// type: 'checkbox',
// value: hibp.blockPwnedName
2018-03-24 20:55:14 +01:00
} ] ;
2018-03-18 17:37:36 +01:00
} ;
2019-12-29 17:48:42 +01:00
// disabled since API V3 of HaveIbeenPwned is not free anymore for checking accounts
hibp . checkPwnedName = false ;
hibp . blockPwnedName = false ;
2018-03-18 17:37:36 +01:00
module . exports . setSettings = function ( changes ) {
2018-03-18 20:15:07 +01:00
for ( const field in changes ) {
const ccfield = field . substr ( 0 , 1 ) . toLowerCase ( ) + field . substring ( 1 ) ;
2018-03-24 12:30:03 +01:00
hibp [ ccfield ] = changes [ field ] ;
2018-03-18 17:37:36 +01:00
}
} ;
module . exports . uninstall = function ( ) {
2018-03-18 20:15:07 +01:00
DetailsView . prototype . fieldChanged = detailsViewFieldChanged ;
2018-03-18 17:37:36 +01:00
} ;