1<?php 2 3/** 4 * Encryption 5 * @package framework 6 * @subpackage crypt 7 */ 8 9/** 10 * Manage request keys for modules 11 */ 12class Hm_Request_Key { 13 14 /* site key */ 15 private static $site_hash = ''; 16 17 /** 18 * Load the request key 19 * @param object $session session interface 20 * @param object $request request object 21 * @param bool $just_logged_in true if the session was created on this request 22 * @return void 23 */ 24 public static function load($session, $request, $just_logged_in) { 25 $user = ''; 26 $key = ''; 27 if ($session->is_active()) { 28 if (!$just_logged_in) { 29 $user = $session->get('username', ''); 30 $key = $session->get('request_key', ''); 31 } 32 else { 33 $session->set('request_key', Hm_Crypt::unique_id()); 34 } 35 } 36 $site_id = ''; 37 if (defined('SITE_ID')) { 38 $site_id = SITE_ID; 39 } 40 self::$site_hash = $session->build_fingerprint($request->server, $key.$user.$site_id); 41 } 42 43 /** 44 * Return the request key 45 * @return string request key 46 */ 47 public static function generate() { 48 return self::$site_hash; 49 } 50 51 /** 52 * Validate a request key 53 * @param string $key value to check 54 * @return bool true on success 55 */ 56 public static function validate($key) { 57 return $key === self::$site_hash; 58 } 59} 60 61class Hm_Crypt_Base { 62 63 static protected $method = 'aes-256-cbc'; 64 static protected $hmac = 'sha512'; 65 static protected $password_rounds = 86000; 66 static protected $encryption_rounds = 100; 67 static protected $hmac_rounds = 101; 68 69 /** 70 * Convert ciphertext to plaintext 71 * @param string $string ciphertext to decrypt 72 * @param string $key encryption key 73 * @return string|false decrypted text 74 */ 75 public static function plaintext($string, $key) { 76 $string = base64_decode($string); 77 78 /* bail if the crypt text is invalid */ 79 if (!$string || strlen($string) <= 200) { 80 return false; 81 } 82 83 /* get the payload and salt */ 84 $crypt_string = substr($string, 192); 85 $salt = substr($string, 0, 128); 86 87 /* check the signature. Temporarily allow the same key for hmac validation, eventually remove the $encryption_rounds 88 * check and require the hmac_rounds check only! */ 89 if (!self::check_hmac($crypt_string, substr($string, 128, 64), $salt, $key, self::$hmac_rounds) && 90 !self::check_hmac($crypt_string, substr($string, 128, 64), $salt, $key, self::$encryption_rounds)) { 91 Hm_Debug::add('HMAC verification failed'); 92 return false; 93 } 94 95 /* generate remaining keys */ 96 $iv = self::pbkdf2($key, $salt, 16, self::$encryption_rounds, self::$hmac); 97 $crypt_key = self::pbkdf2($key, $salt, 32, self::$encryption_rounds, self::$hmac); 98 99 /* return the decrpted text */ 100 return openssl_decrypt($crypt_string, self::$method, $crypt_key, OPENSSL_RAW_DATA, $iv); 101 102 } 103 104 /** 105 * Check hmac signature 106 * @param string $crypt_string payload to check 107 * @param string $hmac signature to check 108 * @param string $salt from generate_salt() 109 * @param string $key supplied key for the encryption 110 * @param integer $rounds iterations 111 * @return boolean 112 */ 113 public static function check_hmac($crypt_string, $hmac, $salt, $key, $rounds) { 114 $hmac_key = self::pbkdf2($key, $salt, 32, $rounds, self::$hmac); 115 116 /* make sure the crypt text has not been tampered with */ 117 return self::hash_compare($hmac, hash_hmac(self::$hmac, $crypt_string, $hmac_key, true)); 118 } 119 120 /** 121 * Convert plaintext into ciphertext 122 * @param string $string plaintext to encrypt 123 * @param string $key encryption key 124 * @return string encrypted text 125 */ 126 public static function ciphertext($string, $key) { 127 /* generate a strong salt */ 128 $salt = self::generate_salt(); 129 130 /* build required keys */ 131 $iv = self::pbkdf2($key, $salt, 16, self::$encryption_rounds, self::$hmac); 132 $crypt_key = self::pbkdf2($key, $salt, 32, self::$encryption_rounds, self::$hmac); 133 $hmac_key = self::pbkdf2($key, $salt, 32, self::$hmac_rounds, self::$hmac); 134 135 /* encrypt the string */ 136 $crypt_string = openssl_encrypt($string, self::$method, $crypt_key, OPENSSL_RAW_DATA, $iv); 137 138 /* build a hash of the crypted text */ 139 $hmac = hash_hmac(self::$hmac, $crypt_string, $hmac_key, true); 140 141 /* return the salt, hash, and crypt text */ 142 return base64_encode($salt.$hmac.$crypt_string); 143 } 144 145 /** 146 * Generate a strong random salt (hopefully) 147 * @return string 148 */ 149 public static function generate_salt() { 150 /* generate random bytes */ 151 return self::random(128); 152 } 153 154 /** 155 * Compare password hashes 156 * 157 * @param string $a hash 158 * @param string $b hash 159 * @return bool 160 */ 161 private static function hash_equals($a, $b) { 162 $res = 0; 163 $len = strlen($a); 164 for ($i = 0; $i < $len; $i++) { 165 $res |= ord($a[$i]) ^ ord($b[$i]); 166 } 167 return $res === 0; 168 } 169 170 /** 171 * Compare password hashes with hash_equals is available, otherwise use 172 * timing attack safe comparison 173 * 174 * @param string $a hash 175 * @param string $b hash 176 * @return bool 177 */ 178 public static function hash_compare($a, $b) { 179 if (!is_string($a) || !is_string($b) || strlen($a) !== strlen($b)) { 180 return false; 181 } 182 /* requires PHP >= 5.6 */ 183 if (Hm_Functions::function_exists('hash_equals')) { 184 return hash_equals($a, $b); 185 } 186 return self::hash_equals($a, $b); 187 } 188 189 /** 190 * Key derivation wth pbkdf2: http://en.wikipedia.org/wiki/PBKDF2 191 * @param string $key payload 192 * @param string $salt random string from generate_salt 193 * @return string[] 194 */ 195 protected static function keygen($key, $salt) { 196 return array($salt, self::pbkdf2($key, $salt, 32, self::$encryption_rounds, self::$hmac)); 197 } 198 /** 199 * Key derivation wth pbkdf2: http://en.wikipedia.org/wiki/PBKDF2 200 * @param string $key payload 201 * @param string $salt random string from generate_salt 202 * @param integer $length result length 203 * @param integer $count iterations 204 * @param string $algo hash algorithm to use 205 * @return string 206 */ 207 public static function pbkdf2($key, $salt, $length, $count, $algo) { 208 /* requires PHP >= 5.5 */ 209 if (Hm_Functions::function_exists('openssl_pbkdf2')) { 210 return openssl_pbkdf2($key, $salt, $length, $count, $algo); 211 } 212 213 /* manual version */ 214 $size = strlen(hash($algo, '', true)); 215 $len = ceil($length / $size); 216 $result = ''; 217 for ($i = 1; $i <= $len; $i++) { 218 $tmp = hash_hmac($algo, $salt . pack('N', $i), $key, true); 219 $res = $tmp; 220 for ($j = 1; $j < $count; $j++) { 221 $tmp = hash_hmac($algo, $tmp, $key, true); 222 $res ^= $tmp; 223 } 224 $result .= $res; 225 } 226 return substr($result, 0, $length); 227 } 228 229 /** 230 * Hash a password using PBKDF2 or PHP password_hash if availble 231 * @param string $password password to hash 232 * @param string $salt salt to use, if false generate a new one 233 * @param int $count interations for PBKDF2 234 * @param string $algo PBKDF2 algo, defaults to sha512 235 * @param string $type Can be either pbkdf2 or php 236 * @return string 237 */ 238 public static function hash_password($password, $salt=false, $count=false, $algo='sha512', $type='php') { 239 if (function_exists('password_hash') && $type === 'php') { 240 return password_hash($password, PASSWORD_DEFAULT); 241 } 242 if ($salt === false) { 243 $salt = self::generate_salt(); 244 } 245 if ($count === false) { 246 $count = self::$password_rounds; 247 } 248 return sprintf("%s:%s:%s:%s", $algo, $count, base64_encode($salt), base64_encode( 249 self::pbkdf2($password, $salt, 32, $count, $algo))); 250 } 251 252 /** 253 * Check a password against it's stored hash 254 * @param string $password clear text password 255 * @param string $hash hashed password 256 * @return bool 257 */ 258 public static function check_password($password, $hash) { 259 $type = 'php'; 260 if (substr($hash, 0, 6) === 'sha512') { 261 $type = 'pbkdf2'; 262 } 263 if (function_exists('password_verify') && $type === 'php') { 264 return password_verify($password, $hash); 265 } 266 if (count(explode(':', $hash)) == 4) { 267 list($algo, $count, $salt,,) = explode(':', $hash); 268 return self::hash_compare(self::hash_password($password, base64_decode($salt), $count, $algo, $type), $hash); 269 } 270 return false; 271 } 272 273 /** 274 * Return a unique-enough-key for session cookie ids 275 * @param int $size length of the result 276 * @return string 277 */ 278 public static function unique_id($size=128) { 279 return base64_encode(openssl_random_pseudo_bytes($size)); 280 } 281 282 /** 283 * Generate a random string 284 * @param int $size 285 * @return string 286 */ 287 public static function random($size=128) { 288 try { 289 return Hm_Functions::random_bytes($size); 290 } 291 catch (Exception $e) { 292 Hm_Functions::cease('No reliable random byte source found'); 293 } 294 } 295} 296