1: <?php
2:
3: /**
4: * Encryption
5: * @package framework
6: * @subpackage crypt
7: */
8:
9: /**
10: * Manage request keys for modules
11: */
12: class 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:
61: class 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 >= 7.4 */
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 [$salt, self::pbkdf2($key, $salt, 32, self::$encryption_rounds, self::$hmac)];
197: }
198:
199: /**
200: * Key derivation wth pbkdf2: http://en.wikipedia.org/wiki/PBKDF2
201: * @param string $key payload
202: * @param string $salt random string from generate_salt
203: * @param integer $length result length
204: * @param integer $count iterations
205: * @param string $algo hash algorithm to use
206: * @return string
207: */
208: public static function pbkdf2($key, $salt, $length, $count, $algo) {
209: /* requires PHP >= 5.5 */
210: if (Hm_Functions::function_exists('openssl_pbkdf2')) {
211: return openssl_pbkdf2($key, $salt, $length, $count, $algo);
212: }
213:
214: /* manual version */
215: $size = strlen(hash($algo, '', true));
216: $len = ceil($length / $size);
217: $result = '';
218: for ($i = 1; $i <= $len; $i++) {
219: $tmp = hash_hmac($algo, $salt . pack('N', $i), $key, true);
220: $res = $tmp;
221: for ($j = 1; $j < $count; $j++) {
222: $tmp = hash_hmac($algo, $tmp, $key, true);
223: $res ^= $tmp;
224: }
225: $result .= $res;
226: }
227: return substr($result, 0, $length);
228: }
229:
230: /**
231: * Hash a password using PBKDF2 or PHP password_hash if availble
232: * @param string $password password to hash
233: * @param string $salt salt to use, if false generate a new one
234: * @param int $count interations for PBKDF2
235: * @param string $algo PBKDF2 algo, defaults to sha512
236: * @param string $type Can be either pbkdf2 or php
237: * @return string
238: */
239: public static function hash_password($password, $salt = false, $count = false, $algo = 'sha512', $type = 'php') {
240: if (function_exists('password_hash') && $type === 'php') {
241: return password_hash($password, PASSWORD_DEFAULT);
242: }
243: if ($salt === false) {
244: $salt = self::generate_salt();
245: }
246: if ($count === false) {
247: $count = self::$password_rounds;
248: }
249: return sprintf("%s:%s:%s:%s", $algo, $count, base64_encode($salt), base64_encode(
250: self::pbkdf2($password, $salt, 32, $count, $algo)));
251: }
252:
253: /**
254: * Check a password against it's stored hash
255: * @param string $password clear text password
256: * @param string $hash hashed password
257: * @return bool
258: */
259: public static function check_password($password, $hash) {
260: $type = 'php';
261: if (mb_substr($hash, 0, 6) === 'sha512') {
262: $type = 'pbkdf2';
263: }
264: if (function_exists('password_verify') && $type === 'php') {
265: return password_verify($password, $hash);
266: }
267: if (count(explode(':', $hash)) == 4) {
268: list($algo, $count, $salt,,) = explode(':', $hash);
269: return self::hash_compare(self::hash_password($password, base64_decode($salt), $count, $algo, $type), $hash);
270: }
271: return false;
272: }
273:
274: /**
275: * Return a unique-enough-key for session cookie ids
276: * @param int $size length of the result
277: * @return string
278: */
279: public static function unique_id($size = 128) {
280: return base64_encode(openssl_random_pseudo_bytes($size));
281: }
282:
283: /**
284: * Generate a random string
285: * @param int $size
286: * @return string
287: */
288: public static function random($size = 128) {
289: try {
290: return Hm_Functions::random_bytes($size);
291: } catch (Exception $e) {
292: Hm_Functions::cease('No reliable random byte source found');
293: }
294: }
295: }
296: