Commit | Line | Data |
---|---|---|
14d4f286 S |
1 | <?php |
2 | /** | |
3 | * The APR1 password hashing implementation | |
4 | * | |
5 | * Use this class to generate and validate APR1 password hashes. APR1 hashes | |
6 | * are used primarrily by Apache for .htaccess password storage. | |
7 | * | |
8 | * PHP version 5.3 | |
9 | * | |
10 | * @see http://httpd.apache.org/docs/2.2/misc/password_encryptions.html | |
11 | * @category PHPCryptLib | |
12 | * @package Password | |
13 | * @subpackage Implementation | |
14 | * @author Anthony Ferrara <ircmaxell@ircmaxell.com> | |
15 | * @copyright 2011 The Authors | |
16 | * @license http://www.opensource.org/licenses/mit-license.html MIT License | |
17 | * @version Build @@version@@ | |
18 | */ | |
19 | ||
20 | namespace CryptLib\Password\Implementation; | |
21 | ||
22 | use CryptLib\Random\Factory as RandomFactory; | |
23 | ||
24 | /** | |
25 | * The APR1 password hashing implementation | |
26 | * | |
27 | * Use this class to generate and validate APR1 password hashes. APR1 hashes | |
28 | * are used primarrily by Apache for .htaccess password storage. | |
29 | * | |
30 | * @see http://httpd.apache.org/docs/2.2/misc/password_encryptions.html | |
31 | * @category PHPCryptLib | |
32 | * @package Password | |
33 | * @subpackage Implementation | |
34 | * @author Anthony Ferrara <ircmaxell@ircmaxell.com> | |
35 | */ | |
36 | class APR1 implements \CryptLib\Password\Password { | |
37 | ||
38 | /** | |
39 | * @var Generator The random generator to use for seeds | |
40 | */ | |
41 | protected $generator = null; | |
42 | ||
43 | /** | |
44 | * @var Hash The hash function to use (MD5) | |
45 | */ | |
46 | protected $hash = null; | |
47 | ||
48 | /** | |
49 | * @var int The number of iterations to perform (1000 for APR1) | |
50 | */ | |
51 | protected $iterations = 1000; | |
52 | ||
53 | /** | |
54 | * Determine if the hash was made with this method | |
55 | * | |
56 | * @param string $hash The hashed data to check | |
57 | * | |
58 | * @return boolean Was the hash created by this method | |
59 | */ | |
60 | public static function detect($hash) { | |
61 | return strncmp($hash, '$apr1$', 6) === 0; | |
62 | } | |
63 | ||
64 | /** | |
65 | * Return the prefix used by this hashing method | |
66 | * | |
67 | * @return string The prefix used | |
68 | */ | |
69 | public static function getPrefix() { | |
70 | return '$apr1$'; | |
71 | } | |
72 | ||
73 | /** | |
74 | * Load an instance of the class based upon the supplied hash | |
75 | * | |
76 | * @param string $hash The hash to load from | |
77 | * | |
78 | * @return Password the created instance | |
79 | * @throws InvalidArgumentException if the hash wasn't created here | |
80 | */ | |
81 | public static function loadFromHash($hash) { | |
82 | if (!static::detect($hash)) { | |
83 | throw new \InvalidArgumentException('Hash Not Created Here'); | |
84 | } | |
85 | return new static; | |
86 | } | |
87 | ||
88 | /** | |
89 | * Build a new instance | |
90 | * | |
91 | * @param Generator $generator The random generator to use for seeds | |
92 | * | |
93 | * @return void | |
94 | */ | |
95 | public function __construct( | |
96 | \CryptLib\Random\Generator $generator = null | |
97 | ) { | |
98 | if (is_null($generator)) { | |
99 | $random = new RandomFactory(); | |
100 | $generator = $random->getMediumStrengthGenerator(); | |
101 | } | |
102 | $this->generator = $generator; | |
103 | } | |
104 | ||
105 | /** | |
106 | * Create a password hash for a given plain text password | |
107 | * | |
108 | * @param string $password The password to hash | |
109 | * | |
110 | * @return string The formatted password hash | |
111 | */ | |
112 | public function create($password) { | |
113 | $salt = $this->to64($this->generator->generateInt(0, PHP_INT_MAX), 8); | |
114 | return $this->hash($password, $salt, $this->iterations); | |
115 | } | |
116 | ||
117 | /** | |
118 | * Verify a password hash against a given plain text password | |
119 | * | |
120 | * @param string $password The password to hash | |
121 | * @param string $hash The supplied ahsh to validate | |
122 | * | |
123 | * @return boolean Does the password validate against the hash | |
124 | */ | |
125 | public function verify($password, $hash) { | |
126 | $bits = explode('$', $hash); | |
127 | if (!isset($bits[3]) || $bits[1] != 'apr1') { | |
128 | return false; | |
129 | } | |
130 | $test = $this->hash($password, $bits[2], $this->iterations); | |
131 | return $test == $hash; | |
132 | } | |
133 | ||
134 | /** | |
135 | * Perform the hashing of the password | |
136 | * | |
137 | * @param string $password The plain text password to hash | |
138 | * @param string $salt The 8 byte salt to use | |
139 | * @param int $iterations The number of iterations to use | |
140 | * | |
141 | * @return string The hashed password | |
142 | */ | |
143 | protected function hash($password, $salt, $iterations) { | |
144 | $len = strlen($password); | |
145 | $text = $password . '$apr1$' . $salt; | |
146 | $bin = md5($password.$salt.$password, true); | |
147 | for ($i = $len; $i > 0; $i -= 16) { | |
148 | $text .= substr($bin, 0, min(16, $i)); | |
149 | } | |
150 | for ($i = $len; $i > 0; $i >>= 1) { | |
151 | $text .= ($i & 1) ? chr(0) : $password[0]; | |
152 | } | |
153 | $bin = $this->iterate($text, $iterations, $salt, $password); | |
154 | return $this->convertToHash($bin, $salt); | |
155 | } | |
156 | ||
157 | protected function iterate($text, $iterations, $salt, $password) { | |
158 | $bin = md5($text, true); | |
159 | for ($i = 0; $i < $iterations; $i++) { | |
160 | $new = ($i & 1) ? $password : $bin; | |
161 | if ($i % 3) { | |
162 | $new .= $salt; | |
163 | } | |
164 | if ($i % 7) { | |
165 | $new .= $password; | |
166 | } | |
167 | $new .= ($i & 1) ? $bin : $password; | |
168 | $bin = md5($new, true); | |
169 | } | |
170 | return $bin; | |
171 | } | |
172 | ||
173 | protected function convertToHash($bin, $salt) { | |
174 | $tmp = '$apr1$'.$salt.'$'; | |
175 | $tmp .= $this->to64( | |
176 | (ord($bin[0])<<16) | (ord($bin[6])<<8) | ord($bin[12]), | |
177 | 4 | |
178 | ); | |
179 | $tmp .= $this->to64( | |
180 | (ord($bin[1])<<16) | (ord($bin[7])<<8) | ord($bin[13]), | |
181 | 4 | |
182 | ); | |
183 | $tmp .= $this->to64( | |
184 | (ord($bin[2])<<16) | (ord($bin[8])<<8) | ord($bin[14]), | |
185 | 4 | |
186 | ); | |
187 | $tmp .= $this->to64( | |
188 | (ord($bin[3])<<16) | (ord($bin[9])<<8) | ord($bin[15]), | |
189 | 4 | |
190 | ); | |
191 | $tmp .= $this->to64( | |
192 | (ord($bin[4])<<16) | (ord($bin[10])<<8) | ord($bin[5]), | |
193 | 4 | |
194 | ); | |
195 | $tmp .= $this->to64( | |
196 | ord($bin[11]), | |
197 | 2 | |
198 | ); | |
199 | return $tmp; | |
200 | } | |
201 | ||
202 | /** | |
203 | * Convert the input number to a base64 number of the specified size | |
204 | * | |
205 | * @param int $num The number to convert | |
206 | * @param int $size The size of the result string | |
207 | * | |
208 | * @return string The converted representation | |
209 | */ | |
210 | protected function to64($num, $size) { | |
211 | static $seed = ''; | |
212 | if (empty($seed)) { | |
213 | $seed = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'. | |
214 | 'abcdefghijklmnopqrstuvwxyz'; | |
215 | } | |
216 | $result = ''; | |
217 | while (--$size >= 0) { | |
218 | $result .= $seed[$num & 0x3f]; | |
219 | $num >>= 6; | |
220 | } | |
221 | return $result; | |
222 | } | |
223 | ||
224 | } |