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