| 1 | <?php
 | 
  
    | 2 | 
 | 
  
    | 3 | /*
 | 
  
    | 4 |  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 | 
  
    | 5 |  *
 | 
  
    | 6 |  * This program is free software: you can redistribute it and/or modify
 | 
  
    | 7 |  * it under the terms of the GNU General Public License as published by
 | 
  
    | 8 |  * the Free Software Foundation, either version 3 of the License, or
 | 
  
    | 9 |  * (at your option) any later version.
 | 
  
    | 10 |  *
 | 
  
    | 11 |  * This program is distributed in the hope that it will be useful,
 | 
  
    | 12 |  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
  
    | 13 |  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
  
    | 14 |  * GNU General Public License for more details.
 | 
  
    | 15 |  *
 | 
  
    | 16 |  * You should have received a copy of the GNU General Public License
 | 
  
    | 17 |  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
  
    | 18 |  */
 | 
  
    | 19 | 
 | 
  
    | 20 | /**
 | 
  
    | 21 |  * SecureTokens.php
 | 
  
    | 22 |  *
 | 
  
    | 23 |  * @category      Core
 | 
  
    | 24 |  * @package       Core_Security
 | 
  
    | 25 |  * @subpackage    WB-2.8.4 and up
 | 
  
    | 26 |  * @copyright     Manuela v.d.Decken <manuela@isteam.de>
 | 
  
    | 27 |  * @author        Manuela v.d.Decken <manuela@isteam.de>
 | 
  
    | 28 |  * @license       http://www.gnu.org/licenses/gpl.html   GPL License
 | 
  
    | 29 |  * @
 | 
  
    | 30 |  * @version       0.1.2
 | 
  
    | 31 |  * @revision      $Revision: $
 | 
  
    | 32 |  * @link          $HeadURL: $
 | 
  
    | 33 |  * @lastmodified $Date: $
 | 
  
    | 34 |  * @since         File available since 12.09.2015
 | 
  
    | 35 |  * @description
 | 
  
    | 36 |  * This class is a replacement for the former class SecureForm using the SecureTokensInterface
 | 
  
    | 37 |  *
 | 
  
    | 38 |  * Settings for this class
 | 
  
    | 39 |  * TYPE    KONSTANTE                    REGISTY-VAR                       DEFAULTWERT
 | 
  
    | 40 |  * boolean SEC_TOKEN_FINGERPRINT        ($oReg->SecTokenFingerprint)      [default=true]
 | 
  
    | 41 |  * integer SEC_TOKEN_IPV4_NETMASK       ($oReg->SecTokenIpv4Netmask)      0-255 [default=24]
 | 
  
    | 42 |  * integer SEC_TOKEN_IPV6_PREFIX_LENGTH ($oReg->SecTokenIpv6PrefixLength) 0-128 [default=64]
 | 
  
    | 43 |  * integer SEC_TOKEN_LIFE_TIME          ($oReg->SecTokenLifeTime)         1800 | 2700 | 3600[default] | 7200
 | 
  
    | 44 | */
 | 
  
    | 45 | 
 | 
  
    | 46 | class SecureTokens
 | 
  
    | 47 | {
 | 
  
    | 48 | /**
 | 
  
    | 49 |  * possible settings for TokenLifeTime in seconds
 | 
  
    | 50 |  * @description seconds for 30min / 45min / 1h / 75min / 90min / 105min / 2h
 | 
  
    | 51 |  */
 | 
  
    | 52 | /** minimum lifetime in seconds */
 | 
  
    | 53 |     const LIFETIME_MIN  = 1800; // 30min
 | 
  
    | 54 | /** maximum lifetime in seconds */
 | 
  
    | 55 |     const LIFETIME_MAX  = 7200; // 120min (2h)
 | 
  
    | 56 | /** stepwidth between min and max */
 | 
  
    | 57 |     const LIFETIME_STEP =  900; // 15min
 | 
  
    | 58 | /** lifetime in seconds to use in DEBUG mode if negative value is given (-1) */
 | 
  
    | 59 |     const DEBUG_LIFETIME = 300; // 5
 | 
  
    | 60 | /** array to hold all tokens from the session */
 | 
  
    | 61 |     private $aTokens = array(
 | 
  
    | 62 |         'default' => array('value' => 0, 'expire' => 0, 'instance' => 0)
 | 
  
    | 63 |     );
 | 
  
    | 64 | /** the salt for this instance */
 | 
  
    | 65 |     private $sSalt             = '';
 | 
  
    | 66 | /** fingerprint of the current connection */
 | 
  
    | 67 |     private $sFingerprint      = '';
 | 
  
    | 68 | /** the FTAN token which is valid for this instance */
 | 
  
    | 69 |     private $aLastCreatedFtan  = null;
 | 
  
    | 70 | /** the time when tokens expired if they created in this instance */
 | 
  
    | 71 |     private $iExpireTime       = 0;
 | 
  
    | 72 | /** remove selected tokens only and update all others */
 | 
  
    | 73 |     private $bPreserveAllOtherTokens = false;
 | 
  
    | 74 | /** id of the current instance */
 | 
  
    | 75 |     private $sCurrentInstance  = null;
 | 
  
    | 76 | /** id of the instance to remove */
 | 
  
    | 77 |     private $sInstanceToDelete = null;
 | 
  
    | 78 | /** id of the instance to update expire time */
 | 
  
    | 79 |     private $sInstanceToUpdate = null;
 | 
  
    | 80 | /* --- settings for SecureTokens ------------------------------------------------------ */
 | 
  
    | 81 | /** use fingerprinting to encode */
 | 
  
    | 82 |     private $bUseFingerprint   = true;
 | 
  
    | 83 | /** maximum lifetime of a token in seconds */
 | 
  
    | 84 |     private $iTokenLifeTime    = 1800; // between LIFETIME_MIN and LIFETIME_MAX (default = 30min)
 | 
  
    | 85 | /** bit length of the IPv4 Netmask (0-32 // 0 = off  default = 24) */
 | 
  
    | 86 |     private $iNetmaskLengthV4  = 0;
 | 
  
    | 87 | /** bit length of the IPv6 Netmask (0-128 // 0 = off  default = 64) */
 | 
  
    | 88 |     private $iNetmaskLengthV6  = 0;
 | 
  
    | 89 | 
 | 
  
    | 90 | /**
 | 
  
    | 91 |  * constructor
 | 
  
    | 92 |  * @param (void)
 | 
  
    | 93 |  */
 | 
  
    | 94 |     protected function __construct()
 | 
  
    | 95 |     {
 | 
  
    | 96 |     // load settings if available
 | 
  
    | 97 |         $this->getSettings();
 | 
  
    | 98 |     // generate salt for calculations in this instance
 | 
  
    | 99 |         $this->sSalt        = $this->generateSalt();
 | 
  
    | 100 |     // generate fingerprint for the current connection
 | 
  
    | 101 |         $this->sFingerprint = $this->buildFingerprint();
 | 
  
    | 102 |     // define the expiretime for this instance
 | 
  
    | 103 |         $this->iExpireTime  = time() + $this->iTokenLifeTime;
 | 
  
    | 104 |     // calculate the instance id for this instance
 | 
  
    | 105 |         $this->sCurrentInstance = $this->encodeHash(md5($this->iExpireTime.$this->sSalt));
 | 
  
    | 106 |     // load array of tokens from session
 | 
  
    | 107 |         $this->loadTokens();
 | 
  
    | 108 |     // at first of all remove expired tokens
 | 
  
    | 109 |         $this->removeExpiredTokens();
 | 
  
    | 110 |     }
 | 
  
    | 111 | 
 | 
  
    | 112 | /**
 | 
  
    | 113 |  * destructor
 | 
  
    | 114 |  */
 | 
  
    | 115 |     final public function __destruct()
 | 
  
    | 116 |     {
 | 
  
    | 117 |         foreach ($this->aTokens as $sKey => $aToken) {
 | 
  
    | 118 |             if ($aToken['instance'] == $this->sInstanceToUpdate) {
 | 
  
    | 119 |                 $this->aTokens[$sKey]['instance'] = $this->sCurrentInstance;
 | 
  
    | 120 |                 $this->aTokens[$sKey]['expire']   = $this->iExpireTime;
 | 
  
    | 121 |             } elseif ($aToken['instance'] == $this->sInstanceToDelete) {
 | 
  
    | 122 |                 unset($this->aTokens[$sKey]);
 | 
  
    | 123 |             }
 | 
  
    | 124 |         }
 | 
  
    | 125 |         $this->saveTokens();
 | 
  
    | 126 |     }
 | 
  
    | 127 | 
 | 
  
    | 128 | /**
 | 
  
    | 129 |  * returns all TokenLifeTime values
 | 
  
    | 130 |  * @return array
 | 
  
    | 131 |  */
 | 
  
    | 132 |     final public function getTokenLifeTime()
 | 
  
    | 133 |     {
 | 
  
    | 134 |         return array(
 | 
  
    | 135 |             'min'   => self::LIFETIME_MIN,
 | 
  
    | 136 |             'max'   => self::LIFETIME_MAX,
 | 
  
    | 137 |             'step'  => self::LIFETIME_STEP,
 | 
  
    | 138 |             'value' => $this->iTokenLifeTime
 | 
  
    | 139 |         );
 | 
  
    | 140 |     }
 | 
  
    | 141 | 
 | 
  
    | 142 | /**
 | 
  
    | 143 |  * Dummy method for backward compatibility
 | 
  
    | 144 |  * @return void
 | 
  
    | 145 |  * @deprecated from WB-2.8.3-SP5
 | 
  
    | 146 |  */
 | 
  
    | 147 |     final public function createFTAN()
 | 
  
    | 148 |     {
 | 
  
    | 149 |         trigger_error('Deprecated function call: '.__CLASS__.'::'.__METHOD__, E_USER_DEPRECATED);
 | 
  
    | 150 |     } // do nothing
 | 
  
    | 151 | 
 | 
  
    | 152 | /**
 | 
  
    | 153 |  * Dummy method for backward compatibility
 | 
  
    | 154 |  * @return void
 | 
  
    | 155 |  * @deprecated from WB-2.8.3-SP5
 | 
  
    | 156 |  */
 | 
  
    | 157 |     final public function clearIDKEY()
 | 
  
    | 158 |     {
 | 
  
    | 159 |         trigger_error('Deprecated function call: '.__CLASS__.'::'.__METHOD__, E_USER_DEPRECATED);
 | 
  
    | 160 |     } // do nothing
 | 
  
    | 161 | 
 | 
  
    | 162 | /**
 | 
  
    | 163 |  * returns the current FTAN
 | 
  
    | 164 |  * @param bool $mode: true or POST returns a complete prepared, hidden HTML-Input-Tag (default)
 | 
  
    | 165 |  *                     false or GET returns an GET argument 'key=value'
 | 
  
    | 166 |  * @return mixed:     array or string
 | 
  
    | 167 |  * @deprecated the param $mMode is set deprecated
 | 
  
    | 168 |  *              string retvals are set deprecated. From versions after 2.8.4 retval will be array only
 | 
  
    | 169 |  */
 | 
  
    | 170 |     final public function getFTAN($mMode = 'POST')
 | 
  
    | 171 |     {
 | 
  
    | 172 |         if (is_null($this->aLastCreatedFtan)) {
 | 
  
    | 173 |             $sFtan = md5($this->sSalt);
 | 
  
    | 174 |             $this->aLastCreatedFtan = $this->addToken(
 | 
  
    | 175 |                 substr($sFtan, rand(0,15), 16),
 | 
  
    | 176 |                 substr($sFtan, rand(0,15), 16)
 | 
  
    | 177 |             );
 | 
  
    | 178 |         }
 | 
  
    | 179 |         $aFtan = $this->aTokens[$this->aLastCreatedFtan];
 | 
  
    | 180 |         $aFtan['name']  = $this->aLastCreatedFtan;
 | 
  
    | 181 |         $aFtan['value'] = $this->encodeHash(md5($aFtan['value'].$this->sFingerprint));
 | 
  
    | 182 |         if (is_string($mMode)) {
 | 
  
    | 183 |             $mMode = strtoupper($mMode);
 | 
  
    | 184 |         } else {
 | 
  
    | 185 |             $mMode = $mMode === true ? 'POST' : 'GET';
 | 
  
    | 186 |         }
 | 
  
    | 187 |         switch ($mMode):
 | 
  
    | 188 |             case 'POST':
 | 
  
    | 189 |                 return '<input type="hidden" name="'.$aFtan['name'].'" value="'
 | 
  
    | 190 |                       .$aFtan['value'].'" title="">';
 | 
  
    | 191 |                 break;
 | 
  
    | 192 |             case 'GET':
 | 
  
    | 193 |                 return $aFtan['name'].'='.$aFtan['value'];
 | 
  
    | 194 |                 break;
 | 
  
    | 195 |             default:
 | 
  
    | 196 |                 return array('name' => $aFtan['name'], 'value' => $aFtan['value']);
 | 
  
    | 197 |         endswitch;
 | 
  
    | 198 |     }
 | 
  
    | 199 | 
 | 
  
    | 200 | /**
 | 
  
    | 201 |  * checks received form-transactionnumbers against session-stored one
 | 
  
    | 202 |  * @param string $mode: requestmethode POST(default) or GET
 | 
  
    | 203 |  * @param bool $bPreserve (default=false)
 | 
  
    | 204 |  * @return bool:    true if numbers matches against stored ones
 | 
  
    | 205 |  *
 | 
  
    | 206 |  * requirements: an active session must be available
 | 
  
    | 207 |  * this check will prevent from multiple sending a form. history.back() also will never work
 | 
  
    | 208 |  */
 | 
  
    | 209 |     final public function checkFTAN($mMode = 'POST', $bPreserve = false)
 | 
  
    | 210 |     {
 | 
  
    | 211 |         $bRetval = false;
 | 
  
    | 212 |         $this->bPreserveAllOtherTokens = $bPreserve ?: $this->bPreserveAllOtherTokens;
 | 
  
    | 213 |         // get the POST/GET arguments
 | 
  
    | 214 |         $aArguments = (strtoupper($mMode) == 'POST' ? $_POST : $_GET);
 | 
  
    | 215 |         // encode the value of all matching tokens
 | 
  
    | 216 |         $aMatchingTokens = array_map(
 | 
  
    | 217 |             function ($aToken) {
 | 
  
    | 218 |                 return $this->encodeHash(md5($aToken['value'].$this->sFingerprint));
 | 
  
    | 219 |             },
 | 
  
    | 220 |             // extract all matching tokens from $this->aTokens
 | 
  
    | 221 |             array_intersect_key($this->aTokens, $aArguments)
 | 
  
    | 222 |         );
 | 
  
    | 223 |         // extract all matching arguments from $aArguments
 | 
  
    | 224 |         $aMatchingArguments = array_intersect_key($aArguments, $this->aTokens);
 | 
  
    | 225 |         // get all tokens with matching values from match lists
 | 
  
    | 226 |         $aHits = array_intersect($aMatchingTokens, $aMatchingArguments);
 | 
  
    | 227 |         foreach ($aHits as $sTokenName => $sValue) {
 | 
  
    | 228 |             $bRetval = true;
 | 
  
    | 229 |             $this->removeToken($sTokenName);
 | 
  
    | 230 |         }
 | 
  
    | 231 |         return $bRetval;
 | 
  
    | 232 |     }
 | 
  
    | 233 | /**
 | 
  
    | 234 |  * store value in session and returns an accesskey to it
 | 
  
    | 235 |  * @param mixed $mValue can be numeric, string or array
 | 
  
    | 236 |  * @return string
 | 
  
    | 237 |  */
 | 
  
    | 238 |     final public function getIDKEY($mValue)
 | 
  
    | 239 |     {
 | 
  
    | 240 |         if (is_array($mValue) == true) {
 | 
  
    | 241 |             // serialize value, if it's an array
 | 
  
    | 242 |             $mValue = serialize($mValue);
 | 
  
    | 243 |         }
 | 
  
    | 244 |         // crypt value with salt into md5-hash and return a 16-digit block from random start position
 | 
  
    | 245 |         $sTokenName = $this->addToken(
 | 
  
    | 246 |             substr(md5($this->sSalt.(string)$mValue), rand(0,15), 16),
 | 
  
    | 247 |             $mValue
 | 
  
    | 248 |         );
 | 
  
    | 249 |         return $sTokenName;
 | 
  
    | 250 |     }
 | 
  
    | 251 | 
 | 
  
    | 252 | /*
 | 
  
    | 253 |  * search for key in session and returns the original value
 | 
  
    | 254 |  * @param string $sFieldname: name of the POST/GET-Field containing the key or hex-key itself
 | 
  
    | 255 |  * @param mixed $mDefault: returnvalue if key not exist (default 0)
 | 
  
    | 256 |  * @param string $sRequest: requestmethode can be POST or GET or '' (default POST)
 | 
  
    | 257 |  * @param bool $bPreserve (default=false)
 | 
  
    | 258 |  * @return mixed: the original value (string, numeric, array) or DEFAULT if request fails
 | 
  
    | 259 |  * @description: each IDKEY can be checked only once. Unused Keys stay in list until they expire
 | 
  
    | 260 |  */
 | 
  
    | 261 |     final public function checkIDKEY($sFieldname, $mDefault = 0, $sRequest = 'POST', $bPreserve = false)
 | 
  
    | 262 |     {
 | 
  
    | 263 |         $mReturnValue = $mDefault; // set returnvalue to default
 | 
  
    | 264 |         $this->bPreserveAllOtherTokens = $bPreserve ?: $this->bPreserveAllOtherTokens;
 | 
  
    | 265 |         $sRequest = strtoupper($sRequest);
 | 
  
    | 266 |         switch ($sRequest) {
 | 
  
    | 267 |             case 'POST':
 | 
  
    | 268 |             case 'GET':
 | 
  
    | 269 |                 $sTokenName = @$GLOBALS['_'.$sRequest][$sFieldname] ?: $sFieldname;
 | 
  
    | 270 |                 break;
 | 
  
    | 271 |             default:
 | 
  
    | 272 |                 $sTokenName = $sFieldname;
 | 
  
    | 273 |         }
 | 
  
    | 274 |         if (preg_match('/^[0-9a-f]{16}$/i', $sTokenName)) {
 | 
  
    | 275 |         // key must be a 16-digit hexvalue
 | 
  
    | 276 |             if (array_key_exists($sTokenName, $this->aTokens)) {
 | 
  
    | 277 |             // check if key is stored in IDKEYs-list
 | 
  
    | 278 |                 $mReturnValue = $this->aTokens[$sTokenName]['value']; // get stored value
 | 
  
    | 279 |                 $this->removeToken($sTokenName);   // remove from list to prevent multiuse
 | 
  
    | 280 |                 if (preg_match('/.*(?<!\{).*(\d:\{.*;\}).*(?!\}).*/', $mReturnValue)) {
 | 
  
    | 281 |                 // if value is a serialized array, then deserialize it
 | 
  
    | 282 |                     $mReturnValue = unserialize($mReturnValue);
 | 
  
    | 283 |                 }
 | 
  
    | 284 |             }
 | 
  
    | 285 |         }
 | 
  
    | 286 |         return $mReturnValue;
 | 
  
    | 287 |     }
 | 
  
    | 288 | 
 | 
  
    | 289 | /**
 | 
  
    | 290 |  * make a valid LifeTime value from given integer on the rules of class SecureTokens
 | 
  
    | 291 |  * @param integer  $iLifeTime
 | 
  
    | 292 |  * @return integer
 | 
  
    | 293 |  */
 | 
  
    | 294 |     final public function sanitizeLifeTime($iLifeTime)
 | 
  
    | 295 |     {
 | 
  
    | 296 |         $iLifeTime = intval($iLifeTime);
 | 
  
    | 297 |         for ($i = self::LIFETIME_MIN; $i <= self::LIFETIME_MAX; $i += self::LIFETIME_STEP) {
 | 
  
    | 298 |             $aLifeTimes[] = $i;
 | 
  
    | 299 |         }
 | 
  
    | 300 |         $iRetval = array_pop($aLifeTimes);
 | 
  
    | 301 |         foreach ($aLifeTimes as $iValue) {
 | 
  
    | 302 |             if ($iLifeTime <= $iValue) {
 | 
  
    | 303 |                 $iRetval = $iValue;
 | 
  
    | 304 |                 break;
 | 
  
    | 305 |             }
 | 
  
    | 306 |         }
 | 
  
    | 307 |         return $iRetval;
 | 
  
    | 308 |     }
 | 
  
    | 309 | /* ************************************************************************************ */
 | 
  
    | 310 | /* *** from here private methods only                                               *** */
 | 
  
    | 311 | /* ************************************************************************************ */
 | 
  
    | 312 | /**
 | 
  
    | 313 |  * load all tokens from session
 | 
  
    | 314 |  */
 | 
  
    | 315 |     private function loadTokens()
 | 
  
    | 316 |     {
 | 
  
    | 317 |         if (isset($_SESSION['TOKENS'])) {
 | 
  
    | 318 |             $this->aTokens = unserialize($_SESSION['TOKENS']);
 | 
  
    | 319 |         } else {
 | 
  
    | 320 |             $this->saveTokens();
 | 
  
    | 321 |         }
 | 
  
    | 322 |     }
 | 
  
    | 323 | 
 | 
  
    | 324 | /**
 | 
  
    | 325 |  * save all tokens into session
 | 
  
    | 326 |  */
 | 
  
    | 327 |     private function saveTokens()
 | 
  
    | 328 |     {
 | 
  
    | 329 |         $_SESSION['TOKENS'] = serialize($this->aTokens);
 | 
  
    | 330 |     }
 | 
  
    | 331 | 
 | 
  
    | 332 | /**
 | 
  
    | 333 |  * add new token to the list
 | 
  
    | 334 |  * @param string $sTokenName
 | 
  
    | 335 |  * @param string $sValue
 | 
  
    | 336 |  * @return string  name(index) of the token
 | 
  
    | 337 |  */
 | 
  
    | 338 |     private function addToken($sTokenName, $sValue)
 | 
  
    | 339 |     {
 | 
  
    | 340 |         // limit TokenName to 16 digits
 | 
  
    | 341 |         $sTokenName = substr(str_pad($sTokenName, 16, '0', STR_PAD_LEFT), -16);
 | 
  
    | 342 |         // make sure, first digit is a alpha char [a-f]
 | 
  
    | 343 |         $sTokenName[0] = dechex(10 + (hexdec($sTokenName[0]) % 5));
 | 
  
    | 344 |         // loop as long the generated TokenName already exists in list
 | 
  
    | 345 |         while (isset($this->aTokens[$sTokenName])) {
 | 
  
    | 346 |             // split TokenName into 4 words
 | 
  
    | 347 |             $aWords = str_split($sTokenName, 4);
 | 
  
    | 348 |             // get lowest word and increment it
 | 
  
    | 349 |             $iWord = hexdec($aWords[3]) + 1;
 | 
  
    | 350 |             // reformat integer into a 4 digit hex string
 | 
  
    | 351 |             $aWords[3] = sprintf('%04x', ($iWord > 0xffff ? 1 : $iWord));
 | 
  
    | 352 |             // rebuild the TokenName
 | 
  
    | 353 |             $sTokenName = implode('', $aWords);
 | 
  
    | 354 |         }
 | 
  
    | 355 |         // store Token in list
 | 
  
    | 356 |         $this->aTokens[$sTokenName] = array(
 | 
  
    | 357 |             'value'    => $sValue,
 | 
  
    | 358 |             'expire'   => $this->iExpireTime,
 | 
  
    | 359 |             'instance' => $this->sCurrentInstance
 | 
  
    | 360 |         );
 | 
  
    | 361 |         return $sTokenName;
 | 
  
    | 362 |     }
 | 
  
    | 363 | 
 | 
  
    | 364 | /**
 | 
  
    | 365 |  * remove the token, called sTokenName from list
 | 
  
    | 366 |  * @param type $sTokenName
 | 
  
    | 367 |  */
 | 
  
    | 368 |     private function removeToken($sTokenName)
 | 
  
    | 369 |     {
 | 
  
    | 370 |         if (isset($this->aTokens[$sTokenName])) {
 | 
  
    | 371 |             if ($this->bPreserveAllOtherTokens) {
 | 
  
    | 372 |                 if ($this->sInstanceToDelete) {
 | 
  
    | 373 |                     $this->sInstanceToUpdate = $this->sInstanceToDelete;
 | 
  
    | 374 |                     $this->sInstanceToDelete = null;
 | 
  
    | 375 |                 } else {
 | 
  
    | 376 |                     $this->sInstanceToUpdate = $this->aTokens[$sTokenName]['instance'];
 | 
  
    | 377 |                 }
 | 
  
    | 378 |             } else {
 | 
  
    | 379 |                 $this->sInstanceToDelete = $this->aTokens[$sTokenName]['instance'];
 | 
  
    | 380 |             }
 | 
  
    | 381 |             unset($this->aTokens[$sTokenName]);
 | 
  
    | 382 |         }
 | 
  
    | 383 |     }
 | 
  
    | 384 | 
 | 
  
    | 385 | /**
 | 
  
    | 386 |  * remove all expired tokens from list
 | 
  
    | 387 |  */
 | 
  
    | 388 |     private function removeExpiredTokens()
 | 
  
    | 389 |     {
 | 
  
    | 390 |         $iTimestamp = time();
 | 
  
    | 391 |         foreach ($this->aTokens as $sTokenName => $aToken) {
 | 
  
    | 392 |             if ($aToken['expire'] <= $iTimestamp && $aToken['expire'] != 0){
 | 
  
    | 393 |                 unset($this->aTokens[$sTokenName]);
 | 
  
    | 394 |             }
 | 
  
    | 395 |         }
 | 
  
    | 396 |     }
 | 
  
    | 397 | 
 | 
  
    | 398 | /**
 | 
  
    | 399 |  * generate a runtime depended hash
 | 
  
    | 400 |  * @return string  md5 hash
 | 
  
    | 401 |  */
 | 
  
    | 402 |     private function generateSalt()
 | 
  
    | 403 |     {
 | 
  
    | 404 |         list($fUsec, $fSec) = explode(" ", microtime());
 | 
  
    | 405 |         $sSalt = (string)rand(10000, 99999)
 | 
  
    | 406 |                . (string)((float)$fUsec + (float)$fSec)
 | 
  
    | 407 |                . (string)rand(10000, 99999);
 | 
  
    | 408 |         return md5($sSalt);
 | 
  
    | 409 |     }
 | 
  
    | 410 | 
 | 
  
    | 411 | /**
 | 
  
    | 412 |  * build a simple fingerprint
 | 
  
    | 413 |  * @return string
 | 
  
    | 414 |  */
 | 
  
    | 415 |     private function buildFingerprint()
 | 
  
    | 416 |     {
 | 
  
    | 417 |         if (!$this->bUseFingerprint) { return md5('this_is_a_dummy_only'); }
 | 
  
    | 418 |         $sClientIp = '127.0.0.1';
 | 
  
    | 419 |         if (array_key_exists('HTTP_X_FORWARDED_FOR', $_SERVER)){
 | 
  
    | 420 |             $sClientIp = array_pop(preg_split('/\s*?,\s*?/', $_SERVER['HTTP_X_FORWARDED_FOR'], null, PREG_SPLIT_NO_EMPTY));
 | 
  
    | 421 |         }else if (array_key_exists('REMOTE_ADDR', $_SERVER)) {
 | 
  
    | 422 |             $sClientIp = $_SERVER['REMOTE_ADDR'];
 | 
  
    | 423 |         }else if (array_key_exists('HTTP_CLIENT_IP', $_SERVER)) {
 | 
  
    | 424 |             $sClientIp = $_SERVER['HTTP_CLIENT_IP'];
 | 
  
    | 425 |         }
 | 
  
    | 426 |         $aTmp = array_chunk(stat(__FILE__), 11);
 | 
  
    | 427 |         unset($aTmp[0][8]);
 | 
  
    | 428 |         return md5(
 | 
  
    | 429 |             __FILE__ . PHP_VERSION . implode('', $aTmp[0])
 | 
  
    | 430 |             . (array_key_exists('HTTP_USER_AGENT', $_SERVER) ? $_SERVER['HTTP_USER_AGENT'] : 'AGENT')
 | 
  
    | 431 |             . $this->calcClientIpHash($sClientIp)
 | 
  
    | 432 |         );
 | 
  
    | 433 |     }
 | 
  
    | 434 | 
 | 
  
    | 435 | /**
 | 
  
    | 436 |  * mask IPv4 as well IPv6 addresses with netmask and make a md5 hash from
 | 
  
    | 437 |  * @param string $sClientIp IP as string from $_SERVER['REMOTE_ADDR']
 | 
  
    | 438 |  * @return md5 value of masked ip
 | 
  
    | 439 |  * @description this method does not accept the IPv6/IPv4 mixed format
 | 
  
    | 440 |  *               like "2222:3333:4444:5555:6666:7777:192.168.1.200"
 | 
  
    | 441 |  */
 | 
  
    | 442 |     private function calcClientIpHash($sRawIp)
 | 
  
    | 443 |     {
 | 
  
    | 444 |         // clean address from netmask/prefix and port
 | 
  
    | 445 |         $sPattern = '/^\[?([.:a-f0-9]*)(?:\/[0-1]*)?(?:\]?.*)$/im';
 | 
  
    | 446 |         $sRawIp = preg_replace($sPattern, '$1', $sRawIp);
 | 
  
    | 447 |         if (strpos($sRawIp, ':') === false) {
 | 
  
    | 448 | // sanitize IPv4 ---------------------------------------------------------------------- //
 | 
  
    | 449 |             $iIpV4 = ip2long($sRawIp);
 | 
  
    | 450 |             // calculate netmask
 | 
  
    | 451 |             $iMask = ($this->iNetmaskLengthV4 < 1)
 | 
  
    | 452 |                 ? 0
 | 
  
    | 453 |                 : bindec(
 | 
  
    | 454 |                     str_repeat('1', $this->iNetmaskLengthV4).
 | 
  
    | 455 |                     str_repeat('0', 32 - $this->iNetmaskLengthV4)
 | 
  
    | 456 |                 );
 | 
  
    | 457 |             // apply mask and reformat to IPv4 string notation.
 | 
  
    | 458 |             $sIp = long2ip($iIpV4 & $iMask);
 | 
  
    | 459 |         } else {
 | 
  
    | 460 | // sanitize IPv6 ---------------------------------------------------------------------- //
 | 
  
    | 461 |             // check if IP includes a IPv4 part and convert this into IPv6 format
 | 
  
    | 462 |             $sPattern = '/^([:a-f0-9]*?)\:([0-9]{1,3}(?:\.[0-9]{1,3}){3})$/is';
 | 
  
    | 463 |             if (preg_match($sPattern, $sRawIp, $aMatches)) {
 | 
  
    | 464 |                 // convert IPv4 into full size 32bit binary string
 | 
  
    | 465 |                 $sIpV4Bin = str_pad((string)decbin(ip2long($aMatches[2])), 32, '0', STR_PAD_LEFT) ;
 | 
  
    | 466 |                 // split into 2 parts of 16bit
 | 
  
    | 467 |                 $aIpV6Hex = str_split($sIpV4Bin, 16);
 | 
  
    | 468 |                 // concate the IPv6/96 part and hex of both IPv4 parts
 | 
  
    | 469 |                 $sRawIp = $aMatches[1].':'.dechex(bindec($aIpV6Hex[0])).':'.dechex(bindec($aIpV6Hex[1]));
 | 
  
    | 470 |             }
 | 
  
    | 471 |             // calculate number of missing IPv6 words
 | 
  
    | 472 |             $iWords = 8 - count(preg_split('/:/', $sRawIp, null, PREG_SPLIT_NO_EMPTY));
 | 
  
    | 473 |             // build multiple ':0000:' replacements for '::'
 | 
  
    | 474 |             $sReplacement = $iWords ? implode(':', array_fill(0, $iWords, '0000')) : '';
 | 
  
    | 475 |             // insert replacements and remove trailing/leading ':'
 | 
  
    | 476 |             $sClientIp = trim(preg_replace('/\:\:/', ':'.$sReplacement.':', $sRawIp), ':');
 | 
  
    | 477 |             // split all 8 parts from IP into an array
 | 
  
    | 478 |             $aIpV6 = array_map(
 | 
  
    | 479 |                 function($sPart) {
 | 
  
    | 480 |                     // expand all parts to 4 hex digits using leading '0'
 | 
  
    | 481 |                     return str_pad($sPart, 4, '0', STR_PAD_LEFT);
 | 
  
    | 482 |                 },
 | 
  
    | 483 |                 preg_split('/:/', $sClientIp)
 | 
  
    | 484 |             );
 | 
  
    | 485 |             // build binary netmask from iNetmaskLengthV6
 | 
  
    | 486 |             // and split all 8 parts into an array
 | 
  
    | 487 |             if ($this->iNetmaskLengthV6 < 1) {
 | 
  
    | 488 |                 $aMask = array_fill(0, 8, str_repeat('0', 16));
 | 
  
    | 489 |             } else {
 | 
  
    | 490 |                 $aMask = str_split(
 | 
  
    | 491 |                     str_repeat('1', $this->iNetmaskLengthV6).
 | 
  
    | 492 |                     str_repeat('0', 128 - $this->iNetmaskLengthV6),
 | 
  
    | 493 |                     16
 | 
  
    | 494 |                 );
 | 
  
    | 495 |             }
 | 
  
    | 496 |             // iterate all IP parts, apply its mask and reformat to IPv6 string notation.
 | 
  
    | 497 |             array_walk(
 | 
  
    | 498 |                 $aIpV6,
 | 
  
    | 499 |                 function(&$sWord, $iIndex) use ($aMask) {
 | 
  
    | 500 |                     $sWord = sprintf('%04x', hexdec($sWord) & bindec($aMask[$iIndex]));
 | 
  
    | 501 |                 }
 | 
  
    | 502 |             );
 | 
  
    | 503 |             // reformat to IPv6 string notation.
 | 
  
    | 504 |             $sIp = implode(':', $aIpV6);
 | 
  
    | 505 | // ------------------------------------------------------------------------------------ //
 | 
  
    | 506 |         }
 | 
  
    | 507 |         return md5($sIp); // return the hashed IP string
 | 
  
    | 508 |     }
 | 
  
    | 509 | 
 | 
  
    | 510 | /**
 | 
  
    | 511 |  * encode a hex string into a 64char based string
 | 
  
    | 512 |  * @param string $sMd5Hash
 | 
  
    | 513 |  * @return string
 | 
  
    | 514 |  * @description reduce the 32char length of a MD5 to 22 chars
 | 
  
    | 515 |  */
 | 
  
    | 516 |     private function encodeHash($sMd5Hash)
 | 
  
    | 517 |     {
 | 
  
    | 518 |         return rtrim(base64_encode(pack('h*',$sMd5Hash)), '+-= ');
 | 
  
    | 519 |     }
 | 
  
    | 520 | 
 | 
  
    | 521 | /**
 | 
  
    | 522 |  * read settings if available
 | 
  
    | 523 |  */
 | 
  
    | 524 |     private function getSettings()
 | 
  
    | 525 |     {
 | 
  
    | 526 |         $this->bUseFingerprint  = isset($this->oReg->SecTokenFingerprint)
 | 
  
    | 527 |                                   ? $this->oReg->SecTokenFingerprint
 | 
  
    | 528 |                                   : $this->bUseFingerprint;
 | 
  
    | 529 |         $this->iNetmaskLengthV4 = isset($this->oReg->SecTokenIpv4Netmask)
 | 
  
    | 530 |                                   ? $this->oReg->SecTokenIpv4Netmask
 | 
  
    | 531 |                                   : $this->iNetmaskLengthV4;
 | 
  
    | 532 |         $this->iNetmaskLengthV6 = isset($this->oReg->SecTokenIpv6PrefixLength)
 | 
  
    | 533 |                                   ? $this->oReg->SecTokenIpv6PrefixLength
 | 
  
    | 534 |                                   : $this->iNetmaskLengthV6;
 | 
  
    | 535 |         $this->iTokenLifeTime   = isset($this->oReg->SecTokenLifeTime)
 | 
  
    | 536 |                                   ? $this->oReg->SecTokenLifeTime
 | 
  
    | 537 |                                   : $this->iTokenLifeTime;
 | 
  
    | 538 |         $this->iNetmaskLengthV4 = ($this->iNetmaskLengthV4 < 1 || $this->iNetmaskLengthV4 > 32)
 | 
  
    | 539 |                                   ? 0 :$this->iNetmaskLengthV4;
 | 
  
    | 540 |         $this->iNetmaskLengthV6 = ($this->iNetmaskLengthV6 < 1 || $this->iNetmaskLengthV6 > 128)
 | 
  
    | 541 |                                   ? 0 :$this->iNetmaskLengthV6;
 | 
  
    | 542 |         $this->iTokenLifeTime   = $this->sanitizeLifeTime($this->iTokenLifeTime);
 | 
  
    | 543 |         if ($this->iTokenLifeTime <= self::LIFETIME_MIN && DEBUG) {
 | 
  
    | 544 |             $this->iTokenLifeTime = self::DEBUG_LIFETIME;
 | 
  
    | 545 |         }
 | 
  
    | 546 |     }
 | 
  
    | 547 | 
 | 
  
    | 548 | 
 | 
  
    | 549 | } // end of class SecureTokens
 |