Permalink
Switch branches/tags
Nothing to show
Find file
Fetching contributors…
Cannot retrieve contributors at this time
1441 lines (1144 sloc) 49.9 KB
/**
*
* @file model\services\SecurityService.cfc
* @author Denard Springle ( denard.springle@gmail.com )
* @description I provide security and session related functions
*
*/
component displayname="SecurityService" accessors="true" {
property encryptionKey1;
property encryptionAlgorithm1;
property encryptionEncoding1;
property encryptionKey2;
property encryptionAlgorithm2;
property encryptionEncoding2;
property encryptionKey3;
property encryptionAlgorithm3;
property encryptionEncoding3;
property hmacKey;
property hmacAlgorithm;
property hmacEncoding;
property keyRingPath;
property masterKey;
/**
* @displayname init
* @description I am the constructor method for SecurityService
* @param encryptionKey1 {String} - I am the encryption key used for pass number 1
* @param encryptionAlgorithm1 {String} - I am the encryption algorithm used for pass number 1
* @param encryptionEncoding1 {String} - I am the encryption encoding used for pass number 1
* @param encryptionKey2 {String} - I am the encryption key used for pass number 2
* @param encryptionAlgorithm2 {String} - I am the encryption algorithm used for pass number 2
* @param encryptionEncoding2 {String} - I am the encryption encoding used for pass number 2
* @param encryptionKey3 {String} - I am the encryption key used for pass number 3
* @param encryptionAlgorithm3 {String} - I am the encryption algorithm used for pass number 3
* @param encryptionEncoding3 {String} - I am the encryption encoding used for pass number 3
* @param hmacKey {String} - I am the key used for hmac hashing
* @param hmacAlgorithm {String} - I am the hashing algorithm used for hmac hashing
* @param hmacEncoding {String} - I am the encoding used for hmac hashing
* @param keyRingPath {String} - I am the path to the keyring file on disk
* @param masterKey {String} - I am the master key used for encryption/decryption of the keyring
* @return this
*/
public function init(
string encryptionKey1 = '',
string encryptionAlgorithm1 = '',
string encryptionEncoding1 = '',
string encryptionKey2 = '',
string encryptionAlgorithm2 = '',
string encryptionEncoding2 = '',
string encryptionKey3 = '',
string encryptionAlgorithm3 = '',
string encryptionEncoding3 = '',
string hmacKey = '',
string hmacAlgorithm = '',
string hmacEncoding = '',
string keyRingPath = '',
string masterKey = ''
) {
variables.encryptionKey1 = arguments.encryptionKey1;
variables.encryptionAlgorithm1 = arguments.encryptionAlgorithm1;
variables.encryptionEncoding1 = arguments.encryptionEncoding1;
variables.encryptionKey2 = arguments.encryptionKey2;
variables.encryptionAlgorithm2 = arguments.encryptionAlgorithm2;
variables.encryptionEncoding2 = arguments.encryptionEncoding2;
variables.encryptionKey3 = arguments.encryptionKey3;
variables.encryptionAlgorithm3 = arguments.encryptionAlgorithm3;
variables.encryptionEncoding3 = arguments.encryptionEncoding3;
variables.hmacKey = arguments.hmacKey;
variables.hmacAlgorithm = arguments.hmacAlgorithm;
variables.hmacEncoding = arguments.hmacEncoding;
variables.keyRingPath = arguments.keyRingPath;
variables.masterKey = arguments.masterKey;
return this;
}
/* DATA ENCRYPTION
This section of the security service provides functions to
help manage encrypting values in the application.
Functions include:
encrypting a plain text input value by mode
Modes include:
db - triple-pass encryption for database storage of values
repeatable - triple-pass repeatable encryption (for storing usernames)
url - single-pass encryption for values passed on the url
form - single-pass encryption using a different key for values passed in the form
rform - double-pass repeatable encryption (for encrypting uid's in form selects)
cookie - single-pass encryption using a different key for values passed in cookies
master - double-pass encryption of the keyring using CBC and CTR
*/
/**
* @displayname dataEnc
* @description I encrypt passed in values based on scope
* @param value {String} required - I am the value to encrypt
* @param mode {String} default: db - I am the mode of encryption to use - one of db, repeatable, url, form, cookie or master
* @return string
*/
public string function dataEnc( required string value, string mode = 'db' ) {
var onePass = '';
var twoPass = '';
var lastPass = '';
// check if the passed value has length
// cannot use `arguments.value.len()` with Lucee when `value` is numeric.
// https://luceeserver.atlassian.net/browse/LDEV-332
if( len( arguments.value ) ) {
// switch on the encryption mode
switch( arguments.mode ) {
// database
case 'db':
// using database encryption, encrypt with the first set of keys and algorithm
onepass = encrypt( arguments.value, variables.encryptionKey1, variables.encryptionAlgorithm1, variables.encryptionEncoding1 );
// and again with the second set of keys and algorithm
twopass = encrypt( onepass, variables.encryptionKey2, variables.encryptionAlgorithm2, variables.encryptionEncoding2 );
// and again with the third set of keys and algorithm
lastPass = encrypt( twopass, variables.encryptionKey3, variables.encryptionAlgorithm3, variables.encryptionEncoding3 );
break;
// repeatable database
case 'repeatable':
// using repeatable database encryption, encrypt with the first set of keys and algorithm
onepass = encrypt( arguments.value, variables.encryptionKey1, listFirst( variables.encryptionAlgorithm1, '/' ), variables.encryptionEncoding1 );
// and again with the second set of keys and algorithm
twopass = encrypt( onepass, variables.encryptionKey2, listFirst( variables.encryptionAlgorithm2, '/' ), variables.encryptionEncoding2 );
// and again with the third set of keys and algorithm
lastPass = encrypt( twopass, variables.encryptionKey3, listFirst( variables.encryptionAlgorithm3, '/' ), variables.encryptionEncoding3);
break;
// master
case 'master':
// using master encryption, encrypt with the master key
onePass = encrypt( arguments.value, variables.masterKey, 'AES/CBC/PKCS5Padding', 'HEX' );
lastPass = encrypt( onePass, variables.masterKey, 'BLOWFISH/CTR/PKCS5Padding', 'HEX' );
break;
// URL
case 'url':
// using url encryption, check if using BASE64 encoding on the URL key
if( findNoCase( 'BASE64', variables.encryptionEncoding1 ) ) {
// we are, encrypt with the first set of keys and repeatable algorithm
lastPass = encrypt( arguments.value, variables.encryptionKey1, listFirst( variables.encryptionAlgorithm1, '/' ), variables.encryptionEncoding1);
// using BASE64 encoding, URL encode the value
lastPass = URLEncodedFormat( lastPass );
// otherwise
} else {
// not BASE64 encoded, encrypt with the first set of keys and algorithm
lastPass = encrypt( arguments.value, variables.encryptionKey1, variables.encryptionAlgorithm1, variables.encryptionEncoding1 );
// end checking if useing BASE64 encoding on the URL key
}
break;
// FORM
case 'form':
// using form encryption, encrypt with the second set of keys and algorithm
lastPass = encrypt( arguments.value, variables.encryptionKey2, variables.encryptionAlgorithm2, variables.encryptionEncoding2 );
break;
// REPEATABLE FORM
case 'rform':
// using rform encryption, encrypt with the second set of keys and repeatable algorithm
onePass = encrypt( arguments.value, variables.encryptionKey2, listFirst( variables.encryptionAlgorithm2, '/' ), variables.encryptionEncoding2 );
// and encrypt with the third set of keys and repeatable algorithm
lastPass = encrypt( onePass, variables.encryptionKey3, listFirst( variables.encryptionAlgorithm3, '/' ), variables.encryptionEncoding3 );
break;
// COOKIE
case 'cookie':
// using cookie encryption, encrypt with the first set of keys and algorithm
lastPass = encrypt( arguments.value, variables.encryptionKey3, variables.encryptionAlgorithm3, variables.encryptionEncoding3 );
break;
// default
default:
return lastPass;
break;
}
// end checking if the passed value has length
}
// return the encrypted value (or null if passed value has no length)
return lastPass;
}
/* DATA DECRYPTION
This section of the security service provides functions to
help manage decrypting values in the application.
Functions include:
decrypting an encrypted input value by mode
Modes include:
db - triple-pass decryption for values stored in the database
repeatable - triple-pass repeatable decryption (for decrypting usernames)
url - single-pass decryption for values passed on the url
form - single-pass decryption using a different key for values passed in the form
rform - double-pass repeatable decryption (for decrypting uid's from form selects)
cookie - single-pass decryption using a different key for values passed in cookies
master - double-pass decryption of the keyring using CBC and CTR
*/
/**
* @displayname dataDec
* @description I decrypt passed in values based on scope
* @param value {String} required - I am the value to decrypt
* @param mode {String} default: db - I am the mode of decryption to use - one of db, repeatable, url, form, cookie or master
* @return string
*/
public string function dataDec( required string value, string mode = 'db' ) {
// var scope
var onePass = '';
var twoPass = '';
var lastPass = '';
// check if the passed value has length
// cannot use `arguments.value.len()` with Lucee when `value` is numeric.
// https://luceeserver.atlassian.net/browse/LDEV-332
if( len( arguments.value ) ) {
// switch on the encryption mode
switch( arguments.mode ) {
// database
case 'db':
// using database encryption, decrypt with the third set of keys and algorithm
var onePass = decrypt( arguments.value, variables.encryptionKey3, variables.encryptionAlgorithm3, variables.encryptionEncoding3 );
// and again with the second set of keys and algorithm
var twoPass = decrypt( onepass, variables.encryptionKey2, variables.encryptionAlgorithm2, variables.encryptionEncoding2 );
// and again with the first set of keys and algorithm
var lastPass = decrypt( twopass, variables.encryptionKey1, variables.encryptionAlgorithm1, variables.encryptionEncoding1 );
break;
// repeatable database
case 'repeatable':
// using database encryption, decrypt with the third set of keys and algorithm
var onePass = decrypt( arguments.value, variables.encryptionKey3, listFirst( variables.encryptionAlgorithm3, '/' ), variables.encryptionEncoding3 );
// and again with the second set of keys and algorithm
var twoPass = decrypt( onepass, variables.encryptionKey2, listFirst( variables.encryptionAlgorithm2, '/' ), variables.encryptionEncoding2 );
// and again with the first set of keys and algorithm
var lastPass = decrypt( twopass, variables.encryptionKey1, listFirst( variables.encryptionAlgorithm1, '/' ), variables.encryptionEncoding1 );
break;
// master
case 'master':
// using master encryption, decrypt with the master key and second algorithm
onePass = decrypt( arguments.value, variables.masterKey, 'BLOWFISH/CTR/PKCS5Padding', 'HEX' );
lastPass = decrypt( onePass, variables.masterKey, 'AES/CBC/PKCS5Padding', 'HEX' );
break;
// URL
case 'url':
// using url encryption, check if useing BASE64 encoding on the URL key
if( findNoCase( 'BASE64', variables.encryptionEncoding1 ) ) {
// using BASE64 encoding, URL decode the value
arguments.value = urlDecode( arguments.value );
// replace spaces with +
arguments.value = replace( arguments.value, chr(32), '+', 'ALL' );
// decrypt with the first set of keys and repeatable algorithm
lastPass = decrypt( arguments.value, variables.encryptionKey1, listFirst( variables.encryptionAlgorithm1, '/' ), variables.encryptionEncoding1 );
// otherwise
} else {
// not BASE64 encoded, decrypt with the first set of keys and algorithm
lastPass = decrypt( arguments.value, variables.encryptionKey1, variables.encryptionAlgorithm1, variables.encryptionEncoding1 );
// end checking if useing BASE64 encoding on the URL key
}
break;
// FORM
case 'form':
// using form encryption, decrypt with the second set of keys and algorithm
lastPass = decrypt( arguments.value, variables.encryptionKey2, variables.encryptionAlgorithm2, variables.encryptionEncoding2 );
break;
// REPEATABLE FORM
case 'rform':
// using rform encryption, decrypt with the third set of keys and repeatable algorithm
onePass = decrypt( arguments.value, variables.encryptionKey3, listFirst( variables.encryptionAlgorithm3, '/' ), variables.encryptionEncoding3 );
// and decrypt with the second set of keys and repeatable algorithm
lastPass = decrypt( onePass, variables.encryptionKey2, listFirst( variables.encryptionAlgorithm2, '/' ), variables.encryptionEncoding2 );
break;
// COOKIE
case 'cookie':
// using cookie encryption, decrypt with the first set of keys and algorithm
lastPass = decrypt( arguments.value, variables.encryptionKey3, variables.encryptionAlgorithm3, variables.encryptionEncoding3 );
break;
// default
default:
return lastPass;
break;
}
// end checking if the passed value has length
}
// return the decrypted value (or null if passed value has no length)
return lastPass;
}
/* HMAC
This section of the security service provides functions to
help manage keyed-hash method authentication code (HMAC)
requirements for sessions.
Functions include:
generating a signed HMAC value from input using the HMAC
key and algorithm given to this security service upon
initialization.
*/
/**
* @displayname dataHmac
* @description I hash and return passed in values based on HMAC
* @param input {String} required - I am the string to HMAC
* @return string
*/
public string function dataHmac( required string input ) {
return hmac( arguments.input, variables.hmacKey, variables.hmacAlgorithm, variables.hmacEncoding );
}
/* HASHING
This section of the security service provides functions to
help manage obfuscation with hashing.
Functions include:
generating a hash by passing in the input, method, iterations
and flags for case and date addition
NOTE: Breaking change in this release:
outcase (lower/upper) {String} parameter has been removed
it has been replaced with useLowercase {Boolean} instead
the default remains 'true' so unless you specified 'outcase'
when calling this function, it will not affect you. If you have
I apologize but hope you agree this function is now more descriptive
while also reducing the code required to execute.
NOTE: This function has found new use with the addition of an additional
parameter: addDate {Boolean}. This defaults to false to maintain
backwards compatibility, however, seting addDate to true now appends
the current date in yyyymmdd format to the input string before being
hashed - providing increased day to day protection for obfuscated
url parameter names against would-be hackers. It is recommended that
you use addDate = true on internal, non-public or otherwise non-indexed
views within your application. Use of addDate on public/indexed pages
will break the link and is, obviously, *not* recommended.
*/
/**
* @displayname uberHash
* @description I hash and return passed in values based on method and iterations (ACF support)
* @param input {String} required - I am the string to hash
* @param method {String} default: SHA-384 - I am the encoding to use for this hash
* @param iterations {Numeric} default: 1000 - I am the number of times to has the value
* @param useLowercase {Boolean} default: true - I flag if the hash should be returned lowercase (true) or uppercase (false)
* @param addDate {Boolean} default: false - I flag if the current date should be appended to the input when hashing
* @return string
*/
public string function uberHash( required string input, string method = 'SHA-384', numeric iterations = 1000, boolean useLowercase = true, boolean addDate = false ) {
// use the native hash() function with UTF-8 encoding to encode the input string
var output = hash( arguments.input & ( ( arguments.addDate ) ? dateFormat( now(), 'yyyymmdd' ) : '' ), arguments.method, 'UTF-8', arguments.iterations );
// check if we're returning lowercase
if( arguments.useLowercase ) {
// we are, set the case of the hash to lowercase
output = lCase( output );
}
// return the hashed input value
return output;
}
/* RANDOM PASSWORD
This section of the security service provides functions to
help manage generating random passwords for users (new, reset).
Functions include:
generating a random password of a specified or random length
*/
/**
* @displayname getRandomPassword
* @description I generate a random password of random length
* @param length {Numeric} default: 0 - I am the length of the password to generate, if not specified then a random length between 12 and 18 characters is chosen
* @return string
*/
public string function getRandomPassword( numeric length = 0 ) {
// configure special chars to use
var special = '!,@,##,$,%,^,&,*';
var password = '';
var ix = 0;
var pattern = '';
var char = '';
// check if a password length was specified
if( !arguments.length ) {
// it wasn't specified, set a random length
arguments.length = randRange( 12, 18, 'SHA1PRNG' );
}
// loop through the length of the password
for( ix = 1; ix <= arguments.length; ix++ ) {
// choose a random pattern (1 to 4)
pattern = randRange( 1, 4, 'SHA1PRNG' );
// switch on the pattern of 1 to 4
switch( pattern ) {
// case 1 - lowercase alpha
case 1:
// select random lowercase alpha character
char = chr( randRange( 97, 122, 'SHA1PRNG' ) );
break;
// case 2 - uppercase alpha
case 2:
// select random uppercase alpha character
char = chr( randRange( 65, 90, 'SHA1PRNG' ) );
break;
// case 3 - numeric
case 3:
// select random numeric character
char = chr( randRange( 48, 57, 'SHA1PRNG' ) );
break;
// case 4 - special
case 4:
// select random special character from the list
char = listGetAt( special, randRange( 1, listLen( special ), 'SHA1PRNG' ) );
break;
}
// add this character to the password
password &= char;
}
// return the random password
return password;
}
/* SESSION MANAGEMENT
This section of the security service provides functions to
help manage logged on user sessions.
Functions include:
checking if a user's session exists in the cache
creating a new user session after authentication
storing, clearing, and updating a user's session object
generating and rotating a user's session id
setting and retreiving the encrypted session id used in cookies
*/
/**
* @displayname checkUserSession
* @description I retrieve the users session object from cache, and return it if it exists, else I return a blank session object
* @param sessionId {String} required - I am the session id to check
* @return any
*/
public any function checkUserSession( required string sessionId ) {
// get the session object from the cache
var sessionObj = cacheGet( uberHash( arguments.sessionId, 'MD5', 3000 ) );
// ensure it is still in the cache
if( isNull( sessionObj ) ) {
// it isn't, return an empty session object
return new model.beans.Session();
// otherwise, ensure the session shouldn't have already expired (30 mins)
} else if( dateDiff('n', sessionObj.getLastActionAt(), now() ) GTE application.timeoutMinutes ) {
// it should have expired, return an empty session object
return new model.beans.Session();
// otherwise, ensure that the hmac code matches for this session
} else if( len( sessionObj.getHmacCode() ) and dataHmac( arguments.sessionId ) neq sessionObj.getHmacCode() ) {
// it doesn't match, return an empty session object
return new model.beans.Session();
// otherwise
} else {
// session is valid, return the session object
return sessionObj;
}
}
/**
* @displayname createUserSession
* @description I generate and return a session object based on passed in values
* @param userId {Numeric} required - I am the user id of the user to generate a session for
* @param role {Numeric} required - I am the role assigned to the user
* @param firstName {String} required - I am the first name of the user
* @param lastName {String} required - I am the last name of the user
* @return any
*/
public any function createUserSession( required numeric userId, required numeric role, required string firstName, required string lastName ) {
// create a session object based on the passed in arguments
var sessionObj = new model.beans.Session(
sessionId = getSessionId(),
userId = arguments.userId,
role = arguments.role,
firstName = arguments.firstName,
lastName = arguments.lastName,
mfaCode = getMfaCode(),
isAuthenticated = false,
lastActionAt = now()
);
// save the user session to the cache
setUserSession( sessionObj );
// and return the session object
return sessionObj;
}
/**
* @displayname setUserSession
* @description I store a sessio0n object in the cache
* @param sessionObj {Any} required - I am the session object to store in the cache
* @return void
*/
public void function setUserSession( required any sessionObj ) {
// put the user's session object into the cache
cachePut( uberHash( arguments.sessionObj.getSessionId(), 'MD5', 3000 ), arguments.sessionObj, createTimeSpan( 0, 0, application.timeoutMinutes, 0), createTimeSpan( 0, 0, application.timeoutMinutes, 0 ) );
}
/**
* @displayname clearUserSession
* @description I remove a sessio0n object from the cache
* @param sessionObj {Any} required - I am the session object to clear from the cache
* @return void
*/
public void function clearUserSession( required any sessionObj ) {
// remove the user's session object from the cache
cacheRemove( uberHash( arguments.sessionObj.getSessionId(), 'MD5', 3000 ) );
}
/**
* @displayname rotateUserSession
* @description I update the session id of a session object
* @param sessionObj {Any} required - I am the session object to rotate the id for
* @return any
*/
public any function rotateUserSession( required any sessionObj ) {
// assign a new session id to the session object
arguments.sessionObj.setSessionId( getSessionId() );
// and return the session object
return arguments.sessionObj;
}
/**
* @displayname updateUserSession
* @description I update the last action at of a session object, remove the old session and save the new one
* @param sessionObj {Any} required - I am the session object to update
* @return any
*/
public any function updateUserSession( required any sessionObj ) {
// clear out the existing user's session
clearUserSession( arguments.sessionObj );
// set the last action time to now
arguments.sessionObj.setLastActionAt( now() );
// set the hmac code for the session cookie
arguments.sessionObj.setHmacCode( dataHmac( arguments.sessionObj.getSessionId() ) );
// save the session to the cache
setUserSession( arguments.sessionObj );
// and return the session object
return arguments.sessionObj;
}
/**
* @displayname getSessionId
* @description I generate a random hashed session id
* @return string
*/
public string function getSessionId() {
// generate a random session id hash
var sessionId = uberHash( createUUID() & now(), 'SHA-384', 2000 );
// and return the session id
return sessionId;
}
/**
* @displayname setSessionIdForCookie
* @description I encrypt the session id of a session object for cookie storage
* @param sessionId {Any} required - I am the session id to get for the cookie
* @return string
*/
public string function setSessionIdForCookie( required string sessionId ) {
// encrypt the session id for cookie storage
var cookieId = dataEnc( arguments.sessionId, 'cookie' );
// and return the encrypted id
return cookieId;
}
/**
* @displayname getSessionIdFromCookie
* @description I decrypt the session id of a session object from cookie storage
* @param cookieId {Any} required - I am the cookie id to get the session id from
* @return string
*/
public string function getSessionIdFromCookie( required string cookieId ) {
// get the value of the session id from the cookie
var sessionId = dataDec( arguments.cookieId, 'cookie' );
// and return the session id
return sessionId;
}
/* AUTHENTICATION
This section of the security service provides functions to
help manage authentication with the application.
Functions include:
generating a random heartbeat used to hash passwords during login
generating a random two-factor auth code to be sent to the user when using 2FA
*/
/**
* @displayname getHeartbeat
* @description I generate a random hash of random length for use in authentication (to prevent password disclosure)
* @return string
*/
public string function getHeartbeat() {
// get a random value for the heartbeat of the login form
var heartbeat = lCase( left( uberHash( now() & createUUID() & randRange( 1000, 9999, 'SHA1PRNG' ), 'SHA-384', randRange( 1000, 3000, 'SHA1PRNG' ) ), randRange( 32, 64, 'SHA1PRNG' ) ) );
// and return the heartbeat value
return heartbeat;
}
/**
* @displayname getMfaCode
* @description I generate a random hashed two-factor authentication code of a random length
* @return string
*/
public string function getMfaCode() {
// get a random auth code of a random length for multi-factor authentication
var mfaCode = left( uberHash( createUUID() & now(), 'MD5', randRange( 1000, 3000, 'SHA1PRNG' ) ), randRange( 4, 8, 'SHA1PRNG' ) );
// and return the mfa code
return mfaCode;
}
/* CSRF
This section of the security service provides functions to
help manage Cross-Site Request Forgery (CSRF) attacks.
Functions include:
generating a random token key used as the session variable
used to store the token in when using
CSRFGenerateToken( [token key] [,forceNew] )
*/
/**
* @displayname generateTokenKey
* @description I generate a random value to use as the CSRF token key
* @return string
*/
public string function generateTokenKey() {
// return randomly generated, random length valid variable (key) name
return chr( randRange( 97, 122, 'SHA1PRNG' ) ) & left( lCase( uberHash( createUUID() & randRange( 1000, 100000, 'SHA1PRNG' ), 'SHA-384', randRange( 25, 150, 'SHA1PRNG' ) ) ), randRange( 31, 63, 'SHA1PRNG' ) );
}
/* KEYRING MANAGEMENT
This section of the security service provides functions to
help manage the security (encryption) keys used by the application.
Functions include:
generating a new random keyring (for new applications only)
reading and writing the keyring file from the disk (on app start/restart)
*/
/**
* @displayname generateKeyRing
* @description I generate a new random keyring and save it to disk
* @param keyLength {Numeric} default: 128 - I am the keylength to use when generating encryption keys (128 or 256 are supported)
* @return array
*/
public array function generateKeyRing( numeric keyLength = 128 ) {
// set up a new array to hold the keyring
variables.keyRing = arrayNew(1);
// loop 3 times to generate 3 keys
for( i=1; i<=3; i++ ) {
// randomly choose between AES and BLOWFISH algorithms for this key
variables.algorithm = ( ( randRange( 0, 1, 'SHA1PRNG' ) ) ? 'AES' : 'BLOWFISH' );
// set up a struct to hold the key
variables.keyStruct = {};
// generate an encryption key based on the algorithm
variables.keyStruct['key'] = generateSecretKey( variables.algorithm, arguments.keyLength );
// set the algoritm to use CBC/PKCS5Padding
variables.keyStruct['alg'] = variables.algorithm & '/CBC/PKCS5Padding';
// set the encoding to HEX
variables.keyStruct['enc'] = 'HEX';
// add this key to the keyring
variables.keyRing[i] = variables.keyStruct;
}
// save the keyring to disk
saveKeyRingToDisk( variables.keyRing );
// and return the keyring
return variables.keyRing;
}
/**
* @displayname readKeyRingFromDisk
* @description I read the keyRing from disk
* @return array
*/
public array function readKeyRingFromDisk() {
// check if the keyring file exists
if( !fileExists( variables.keyRingPath ) ) {
// it doesn't exist, return an empty array
return arrayNew(1);
}
// and return the JSON as an array
return deserializeJSON( dataDec( charsetEncode( fileReadBinary( variables.keyRingPath ), 'utf-8' ), 'master' ) );
}
/**
* @displayname saveKeyRingToDisk
* @description I save the keyRing to disk
* @param keyRing {Array} I am the Key Ring array
* @return void
*/
private void function saveKeyRingToDisk( required array keyRing ) {
// write the keyring file to disk
fileWrite( variables.keyRingPath, charsetDecode( dataEnc( serializeJSON( arguments.keyRing ), 'master' ), 'utf-8' ) );
}
/* IP BLOCKING
This section of the security service provides functions to
help manage blocking of hackers/bots by IP address.
Functions include:
checking if an IP is on the blocked IP list
adding and removing IP's from the blocked IP list
reading and writing the blocked IP list to disk
importing blocked ip lists from other hosts via http
*/
/**
* @displayname isBlockedIp
* @description I parse the blocked ip array and determine if the passed ip is blocked
* @param ipAddress {String} required - I am the ip address to check
* @param blockReserved {Boolean} default: false - I am a flag to determine if reserved (internal) ip addresses should be blocked (10.x.x.x, 192.168.x.x, etc. )
* @return boolean
*/
public boolean function isBlockedIP( required string ipAddress, boolean blockReserved = false ) {
// get blocked IP's from the cache
var blockedIpArr = cacheGet( uberHash( 'blockedIpArr', 'MD5', 120 ) );
var blockedIp = '';
if( arguments.blockReserved ) {
// check if the ip address is a 10.x.x.x address
if( listFirst( arguments.ipAddress, '.') eq 10 ) {
// it is, return true
return true;
// otherwise, check if it's a 127.0.0.x address, but not 127.0.0.1
} else if( listFirst( arguments.ipAddress, '.') eq 127 and !argument.ipAddress eq '127.0.0.1' ) {
// it is, return true
return true;
// otherwise, check if it's a 172.16-31.x.x address
} else if( listFirst( arguments.ipAddress, '.') eq 172 and listFind( '16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31', listGetAt( arguments.ipAddress, 2, '.' ) ) ) {
// it is, return true
return true;
// otherwise, check if it's a 192.168.x.x address
} else if( listFirst( arguments.ipAddress, '.') eq 192 and listGetAt( arguments.ipAddress, 2, '.' ) eq 168 ) {
// it is, return true
return true;
}
}
// check if the cached version of the blocked IP array exists
if( isNull( blockedIpArr ) ) {
// it doesn't, read in the blocked ip file
blockedIpArr = readBlockedIPFileFromDisk();
// and save the blocked ip array to the cache
cachePut( uberHash( 'blockedIpArr', 'MD5', 120 ), blockedIpArr, createTimeSpan( 30, 0, 0, 0 ), createTimeSpan( 15, 0, 0, 0 ) );
}
// loop through the blocked ip array
for( blockedIp in blockedIpArr ) {
// and check if this ip address exists in the array
if( blockedIp.ipAddress eq arguments.ipAddress ) {
// it does, so it is blocked, return true
return true;
}
}
// ip address was not found in the array, return false
return false;
}
/**
* @displayname addBlockedIp
* @description I add an ip address to the blocked ip array
* @param ipAddress {String} required - I am the ip address to add
* @param reason {String} - I am the reason why this IP is being blocked
* @return void
*/
public void function addBlockedIP( required string ipAddress, string reason = '' ) {
// get blocked IP's from the cache
var blockedIpArr = cacheGet( uberHash( 'blockedIpArr', 'MD5', 120 ) );
var blockedIp = '';
var ipRecord = structNew();
var found = false;
// check if the cached version of the blocked IP array exists
if( isNull( blockedIPs ) ) {
// it doesn't, read in the blocked ip file
blockedIpArr = readBlockedIPFileFromDisk();
}
// loop through the blocked ip array
for( blockedIp in blockedIpArr ) {
// check if this ip address exists in the blocked ip list
if( blockedIp.ipAddress eq arguments.ipAddress ) {
// it does, set found to true
found = true;
}
}
// check if the ip address was already found in the array
if( !found ) {
// it wasn't found, create a json friendly struct
ipRecord[ 'ipAddress' ] = arguments.ipAddress;
ipRecord[ 'timestamp' ] = now();
ipRecord[ 'reason' ] = arguments.reason;
// and add it to the array
blockedIpArr.append( ipRecord );
// remove the existing cached blocked ip array
cacheRemove( uberHash( 'blockedIpArr', 'MD5', 120 ) );
// and save the blocked ip array to the cache
cachePut( uberHash( 'blockedIpArr', 'MD5', 120 ), blockedIpArr, createTimeSpan( 30, 0, 0, 0 ), createTimeSpan( 15, 0, 0, 0 ) );
// save the blocked ip's as a json file
saveBlockedIPFileToDisk( blockedIpArr );
}
}
/**
* @displayname removeBlockedIp
* @description I remove an ip address from the blocked ip array
* @param ipAddress {String} required - I am the ip address to remove
* @return void
*/
public void function removeBlockedIP( required string ipAddress ) {
// get blocked IP's from the cache
var blockedIpArr = cacheGet( uberHash( 'blockedIpArr', 'MD5', 120 ) );
var blockedIp = '';
var ix = 0;
var found = false;
// check if the cached version of the blocked IP array exists
if( isNull( blockedIPs ) ) {
// it doesn't, read in the blocked ip file
blockedIpArr = readBlockedIPFileFromDisk();
}
// loop through the blocked ip array
for( blockedIp in blockedIpArr ) {
// increase the array index
ix++;
// check if this ip address exists in the blocked ip list
if( blockedIp.ipAddress eq arguments.ipAddress ) {
// it does, remove the element from the array
blockedIpArr.deleteAt( ix );
// and set found to true
found = true;
}
}
// check if the ip address was found
if( found ) {
// remove the existing cached blocked ip array
cacheRemove( uberHash( 'blockedIpArr', 'MD5', 120 ) );
// and save the blocked ip array to the cache
cachePut( uberHash( 'blockedIpArr', 'MD5', 120 ), blockedIpArr, createTimeSpan( 30, 0, 0, 0 ), createTimeSpan( 15, 0, 0, 0 ) );
// save the blocked ip's as a json file
saveBlockedIPFileToDisk( blockedIpArr );
}
}
/**
* @displayname saveBlockedIPFileToDiskToDisk
* @description I convert the blocked ip array into json and save it to disk
* @param blockedIpArr {Array} required - I am the blocked ip array to save
* @return void
*/
public void function saveBlockedIPFileToDisk( required array blockedIpArr ) {
// convert the array to json
var blockedIpJson = serializeJSON( arguments.blockedIpArr );
// check if the blocked ip file exists
if( fileExists( expandPath( application.blockedIpDir ) & 'blocked_ips.json' ) ) {
// it does, delete the existing file
fileDelete( expandPath( application.blockedIpDir ) & 'blocked_ips.json' );
}
// write the JSON to disk
fileWrite( expandPath( application.blockedIpDir ) & 'blocked_ips.json', blockedIpJson, 'UTF-8' );
}
/**
* @displayname readBlockedIPFileFromDisk
* @description I read the blocked ip json from disk and convert it into an array
* @return array
*/
public array function readBlockedIPFileFromDisk() {
var blockedIpJson = '';
// check if the blocked ip file exists
if( fileExists( expandPath( application.blockedIpDir ) & 'blocked_ips.json' ) ) {
// it does, read in the JSON
blockedIpJson = fileRead( expandPath( application.blockedIpDir ) & 'blocked_ips.json', 'UTF-8' );
// and return an array of the JSON data
return deserializeJSON( blockedIpJson );
}
// file does not exist, return an empty array
return arrayNew(1);
}
/**
* @displayname importBlockedIPFileFromUrl
* @description I make an http call to a remote blocked_ips.json file and add them to our local blocked ip file
* @param importUrl {String} required - I am the FQDN URL to the blocked_ips.json file on the remote server (ex: https://domain.com/blocked/blocked_ips.json)
* @return void
*/
public void function importBlockedIPFileFromUrl( required string importUrl ) {
var httpService = new http();
var blockedIpArr = '';
var blockedIp = '';
// try to perform the import
try {
// set up the http attributes
httpService.setMethod( 'GET' );
httpService.setCharset( 'UTF-8' );
httpService.setUrl( arguments.importUrl );
// convert the returned JSON into an array
blockedIpArr = deserializeJSON( httpService.send().getPrefix().fileContent );
// loop through the array
for( blockedIp in blockedIpArr ) {
// and add each ip from the remote JSON file to our local array
addBlockedIP( blockedIp.ipAddress, blockedIp.reason );
}
// catch any errors (e.g. bad URL, network issues, etc.)
} catch( any e ) {
// and fail gracefully (you may wish to log here as well)
}
}
/* IP WATCHING
This section of the security service provides functions to
help manage watching of potential hackers/bots by IP address.
Functions include:
checking if an IP is on the watched IP list
adding and removing IP's from the watched IP list
increasing the total times an ip has been flagged for potential abuse
reading and writing the watched IP list to disk
importing watched ip lists from other hosts via http
*/
/**
* @displayname getWatchedIp
* @description I parse the watched ip array and determine if the passed ip is being watched
* @param ipAddress {String} required - I am the ip address to check
* @return struct
*/
public struct function getWatchedIp( required string ipAddress ) {
// get watched IP's from the cache
var watchedIpArr = cacheGet( uberHash( 'watchedIpArr', 'MD5', 57 ) );
var watchedIp = '';
var returnStruct = structNew();
// set defaults for watched return struct
returnStruct.isWatched = false;
returnStruct.totalCount = 0;
// check if the cached version of the watched IP array exists
if( isNull( watchedIpArr ) ) {
// it doesn't, read in the watched ip file
watchedIpArr = readWatchedIpFileFromDisk();
// and save the watched ip array to the cache
cachePut( uberHash( 'watchedIpArr', 'MD5', 57 ), watchedIpArr, createTimeSpan( 30, 0, 0, 0 ), createTimeSpan( 15, 0, 0, 0 ) );
}
// loop through the watched ip array
for( watchedIp in watchedIpArr ) {
// and check if this ip address exists in the array
if( watchedIp.ipAddress eq arguments.ipAddress ) {
// it does, so it is watched, set return struct details
returnStruct.isWatched = true;
returnStruct.totalCount = watchedIp.totalCount
}
}
// and return the return struct
return returnStruct;
}
/**
* @displayname addWatchedIp
* @description I add an ip address to the watched ip array
* @param ipAddress {String} required - I am the ip address to add
* @param reason {String} - I am the reason why this IP is being watched
* @return void
*/
public void function addWatchedIp( required string ipAddress, string reason = '' ) {
// get watched IP's from the cache
var watchedIpArr = cacheGet( uberHash( 'watchedIpArr', 'MD5', 57 ) );
var watchedIp = '';
var ipRecord = structNew();
var found = false;
// check if the cached version of the watched IP array exists
if( isNull( watchedIPs ) ) {
// it doesn't, read in the watched ip file
watchedIpArr = readWatchedIpFileFromDisk();
}
// loop through the watched ip array
for( watchedIp in watchedIpArr ) {
// check if this ip address exists in the watched ip list
if( watchedIp.ipAddress eq arguments.ipAddress ) {
// it does, set found to true
found = true;
}
}
// check if the ip address was already found in the array
if( !found ) {
// it wasn't found, create a json friendly struct
ipRecord[ 'ipAddress' ] = arguments.ipAddress;
ipRecord[ 'timestamp' ] = now();
ipRecord[ 'reason' ] = arguments.reason;
ipRecord[ 'totalCount' ] = 1;
// and add it to the array
watchedIpArr.append( ipRecord );
// log hack attempt
writeLog( text = 'ip: ' & arguments.ipAddress & ' reason: ' & arguments.reason & ' timestamp: ' & now() & ' count: 1', type = 'warning', file = 'abuse' );
// remove the existing cached watched ip array
cacheRemove( uberHash( 'watchedIpArr', 'MD5', 57 ) );
// and save the watched ip array to the cache
cachePut( uberHash( 'watchedIpArr', 'MD5', 57 ), watchedIpArr, createTimeSpan( 30, 0, 0, 0 ), createTimeSpan( 15, 0, 0, 0 ) );
// save the watched ip's as a json file
saveWatchedIpFileToDisk( watchedIpArr );
}
}
/**
* @displayname increaseWatchedIpCount
* @description I increase the total times an ip has been flagged for potential abuse
* @param ipAddress {String} required - I am the ip address to add
* @param reason {String} - I am the reason why this IP is being watched
* @return void
*/
public void function increaseWatchedIpCount( required string ipAddress, string reason = '' ) {
// get watched IP's from the cache
var watchedIpArr = cacheGet( uberHash( 'watchedIpArr', 'MD5', 57 ) );
var watchedIp = '';
var count = 0;
// check if the cached version of the watched IP array exists
if( isNull( watchedIPs ) ) {
// it doesn't, read in the watched ip file
watchedIpArr = readWatchedIpFileFromDisk();
}
// loop through the watched ip array
for( watchedIp in watchedIpArr ) {
// check if this ip address exists in the watched ip list
if( watchedIp.ipAddress eq arguments.ipAddress ) {
// it does, increase the counter of this record
watchedIp.totalCount++;
count = watchedIp.totalCount;
}
}
// log hack attempt
writeLog( text = 'ip: ' & arguments.ipAddress & ' reason: ' & arguments.reason & ' timestamp: ' & now() & ' count: ' & count, type = 'warning', file = 'abuse' );
// remove the existing cached watched ip array
cacheRemove( uberHash( 'watchedIpArr', 'MD5', 57 ) );
// and save the watched ip array to the cache
cachePut( uberHash( 'watchedIpArr', 'MD5', 57 ), watchedIpArr, createTimeSpan( 30, 0, 0, 0 ), createTimeSpan( 15, 0, 0, 0 ) );
// save the watched ip's as a json file
saveWatchedIpFileToDisk( watchedIpArr );
}
/**
* @displayname removeWatchedIp
* @description I remove an ip address from the watched ip array
* @param ipAddress {String} required - I am the ip address to remove
* @return void
*/
public void function removeWatchedIp( required string ipAddress ) {
// get watched IP's from the cache
var watchedIpArr = cacheGet( uberHash( 'watchedIpArr', 'MD5', 57 ) );
var watchedIp = '';
var ix = 0;
var found = false;
// check if the cached version of the watched IP array exists
if( isNull( watchedIPs ) ) {
// it doesn't, read in the watched ip file
watchedIpArr = readWatchedIpFileFromDisk();
}
// loop through the watched ip array
for( watchedIp in watchedIpArr ) {
// increase the array index
ix++;
// check if this ip address exists in the watched ip list
if( watchedIp.ipAddress eq arguments.ipAddress ) {
// it does, remove the element from the array
watchedIpArr.deleteAt( ix );
// and set found to true
found = true;
}
}
// check if the ip address was found
if( found ) {
// remove the existing cached watched ip array
cacheRemove( uberHash( 'watchedIpArr', 'MD5', 57 ) );
// and save the watched ip array to the cache
cachePut( uberHash( 'watchedIpArr', 'MD5', 57 ), watchedIpArr, createTimeSpan( 30, 0, 0, 0 ), createTimeSpan( 15, 0, 0, 0 ) );
// save the watched ip's as a json file
saveWatchedIpFileToDisk( watchedIpArr );
}
}
/**
* @displayname saveWatchedIpFileToDiskToDisk
* @description I convert the watched ip array into json and save it to disk
* @param watchedIpArr {Array} required - I am the watched ip array to save
* @return void
*/
public void function saveWatchedIpFileToDisk( required array watchedIpArr ) {
// convert the array to json
var watchedIpJson = serializeJSON( arguments.watchedIpArr );
// check if the watched ip file exists
if( fileExists( expandPath( application.blockedIpDir ) & 'watched_ips.json' ) ) {
// it does, delete the existing file
fileDelete( expandPath( application.blockedIpDir ) & 'watched_ips.json' );
}
// write the JSON to disk
fileWrite( expandPath( application.blockedIpDir ) & 'watched_ips.json', watchedIpJson, 'UTF-8' );
}
/**
* @displayname readWatchedIpFileFromDisk
* @description I read the watched ip json from disk and convert it into an array
* @return array
*/
public array function readWatchedIpFileFromDisk() {
var watchedIpJson = '';
// check if the watched ip file exists
if( fileExists( expandPath( application.blockedIpDir ) & 'watched_ips.json' ) ) {
// it does, read in the JSON
watchedIpJson = fileRead( expandPath( application.blockedIpDir ) & 'watched_ips.json', 'UTF-8' );
// and return an array of the JSON data
return deserializeJSON( watchedIpJson );
}
// file does not exist, return an empty array
return arrayNew(1);
}
/**
* @displayname importWatchedIpFileFromUrl
* @description I make an http call to a remote watched_ips.json file and add them to our local watched ip file
* @param importUrl {String} required - I am the FQDN URL to the watched_ips.json file on the remote server (ex: https://domain.com/blocked/watched_ips.json)
* @return void
*/
public void function importWatchedIpFileFromUrl( required string importUrl ) {
var httpService = new http();
var watchedIpArr = '';
var watchedIp = '';
// try to perform the import
try {
// set up the http attributes
httpService.setMethod( 'GET' );
httpService.setCharset( 'UTF-8' );
httpService.setUrl( arguments.importUrl );
// convert the returned JSON into an array
watchedIpArr = deserializeJSON( httpService.send().getPrefix().fileContent );
// loop through the array
for( watchedIp in watchedIpArr ) {
// and add each ip from the remote JSON file to our local array
addWatchedIp( watchedIp.ipAddress, watchedIp.reason );
}
// catch any errors (e.g. bad URL, network issues, etc.)
} catch( any e ) {
// and fail gracefully (you may wish to log here as well)
}
}
/* SQL INJECTION
This section of the security service provides functions to
help detect SQL injection attempts and throw errors if found.
Functions include:
checking if a query string contains SQL injection attempts
Notes:
This code is by no means exhaustive and may trigger false
positives if you are not obfuscating and encrypting your
URL parameters and values
Errors are caught by the home.main.error function and
will cause the offending IP address to be added to the
blocked IP list.
*/
/**
* @displayname checkSqlInjectionAttempt
* @description I check a passed in query string to ensure it does not contain SQL injection attempts
* @param queryString {String} required - I am the query string to parse for SQL injection attempts
* @return void
*/
public void function checkSqlInjectionAttempt( required string queryString ) {
var msg = '';
// decode the query string
arguments.queryString = urlDecode( arguments.queryString );
// encode the query string for JSON storage
msg = encodeForJavaScript( arguments.queryString );
// check for passing of hex string (0xHEXNUMBER) in query string
if( reFindNoCase( '0[X][0-9A-F]+', arguments.queryString ) ) {
// hex string found, throw an error to be caught
throw( type = 'SQLInjection.Hex', message = '#msg# contains hexadecimal characters');
}
// check for passing of chr([]), char([]) or concat([]) in query string
if( listFindNoCase( 'chr(,char(,concat(', arguments.queryString ) ) {
// value found, throw an error to be caught
throw( type = 'SQLInjection.Char', message = '#msg# contains SQL string attack characters');
}
// check for passing semi-colon followed by any common SQL command in query string
if( reFindNoCase( ';.*(select|insert|update|delete|drop|alter|create)', arguments.queryString ) ) {
// found semi-colon followed by a common SQL command, throw an error to be caught
throw( type = 'SQLInjection.Command', message = '#msg# contains SQL commands');
}
// check for passing of comments in query string
if( findNoCase( '/*', arguments.queryString ) or findNoCase( '*/', arguments.queryString ) ) {
// found a comment throw an error to be caught
throw( type = 'SQLInjection.Comment', message = '#msg# contains comment characters');
}
// check for ' and' in query string
if( findNoCase( ' and', arguments.queryString ) ) {
// found ' and', throw an error to be caught
throw( type = 'SQLInjection.And', message = '#msg# contains [ and]');
}
// check for ' or' in query string
if( findNoCase( ' or', arguments.queryString ) ) {
// found ' or', throw an error to be caught
throw( type = 'SQLInjection.Or', message = '#msg# contains [ or]');
}
// check for ' union' in query string
if( findNoCase( ' union', arguments.queryString ) ) {
// found ' union', throw an error to be caught
throw( type = 'SQLInjection.Union', message = '#msg# contains [ union]');
}
// check for apostrophe in query string
if( findNoCase( "'", arguments.queryString ) ) {
// found apostrophe, throw an error to be caught
throw( type = 'SQLInjection.Apostrophe', message = '#msg# contains apostrophe character');
}
}
/**
* @displayname generateDummyCookieValue
* @description I generate a random value for dummy cookies
* @param encoding {String} default: BASE64 - I am the encoding to use for the encryption
* @return string
*/
public string function generateDummyCookieValue( string encoding = 'BASE64' ) {
// return a random encrypted value to use for dummy cookies
return urlEncodedFormat( encrypt( lcase( hash( createUUID() & now(), 'SHA-512', 'UTF-8', 1000 ) ), generateSecretKey('AES'), 'AES/CTR/PKCS5Padding', arguments.encoding ) );
}
}