Skip to content

Commit ebcdaa8

Browse files
committed
crypt_type 'drupal' for Drupal 7 authentication
1 parent 135c150 commit ebcdaa8

4 files changed

Lines changed: 338 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
77
## [Unreleased]
88
### Added
99
- Options for Courier authlib authentication: courier_md5, courier_md5raw, courier_sha1, courier_sha256
10+
- crypt_type 'drupal' for Drupal 7 authentication
1011

1112
## [3.1.0] - 2018-02-06
1213
### Added

lib/drupal.php

Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
<?php
2+
3+
/**
4+
* @file
5+
* Secure password hashing functions for user authentication.
6+
* Adopted from Drupal 7.x WD 2018-01-04
7+
*
8+
* Based on the Portable PHP password hashing framework.
9+
* @see http://www.openwall.com/phpass/
10+
*
11+
* An alternative or custom version of this password hashing API may be
12+
* used by setting the variable password_inc to the name of the PHP file
13+
* containing replacement user_hash_password(), user_check_password(), and
14+
* user_needs_new_hash() functions.
15+
*/
16+
17+
/**
18+
* The standard log2 number of iterations for password stretching. This should
19+
* increase by 1 every Drupal version in order to counteract increases in the
20+
* speed and power of computers available to crack the hashes.
21+
*/
22+
define('HASH_COUNT', 15);
23+
24+
/**
25+
* The minimum allowed log2 number of iterations for password stretching.
26+
*/
27+
define('MIN_HASH_COUNT', 7);
28+
29+
/**
30+
* The maximum allowed log2 number of iterations for password stretching.
31+
*/
32+
define('MAX_HASH_COUNT', 30);
33+
34+
/**
35+
* The expected (and maximum) number of characters in a hashed password.
36+
*/
37+
define('HASH_LENGTH', 55);
38+
39+
/**
40+
* Returns a string for mapping an int to the corresponding base 64 character.
41+
*/
42+
function _password_itoa64() {
43+
return './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
44+
}
45+
46+
/**
47+
* Encodes bytes into printable base 64 using the *nix standard from crypt().
48+
*
49+
* @param $input
50+
* The string containing bytes to encode.
51+
* @param $count
52+
* The number of characters (bytes) to encode.
53+
*
54+
* @return
55+
* Encoded string
56+
*/
57+
function _password_base64_encode($input, $count) {
58+
$output = '';
59+
$i = 0;
60+
$itoa64 = _password_itoa64();
61+
do {
62+
$value = ord($input[$i++]);
63+
$output .= $itoa64[$value & 0x3f];
64+
if ($i < $count) {
65+
$value |= ord($input[$i]) << 8;
66+
}
67+
$output .= $itoa64[($value >> 6) & 0x3f];
68+
if ($i++ >= $count) {
69+
break;
70+
}
71+
if ($i < $count) {
72+
$value |= ord($input[$i]) << 16;
73+
}
74+
$output .= $itoa64[($value >> 12) & 0x3f];
75+
if ($i++ >= $count) {
76+
break;
77+
}
78+
$output .= $itoa64[($value >> 18) & 0x3f];
79+
} while ($i < $count);
80+
81+
return $output;
82+
}
83+
/**
84+
* Returns a string of highly randomized bytes (over the full 8-bit range).
85+
*
86+
* This function is better than simply calling mt_rand() or any other built-in
87+
* PHP function because it can return a long string of bytes (compared to < 4
88+
* bytes normally from mt_rand()) and uses the best available pseudo-random
89+
* source.
90+
*
91+
* @param $count
92+
* The number of characters (bytes) to return in the string.
93+
*/
94+
95+
function _random_bytes($count) {
96+
// $random_state does not use static as it stores random bytes.
97+
static $random_state, $bytes, $has_openssl;
98+
99+
$missing_bytes = $count - strlen($bytes);
100+
101+
if ($missing_bytes > 0) {
102+
// PHP versions prior 5.3.4 experienced openssl_random_pseudo_bytes()
103+
// locking on Windows and rendered it unusable.
104+
if (!isset($has_openssl)) {
105+
$has_openssl = version_compare(PHP_VERSION, '5.3.4', '>=') && function_exists('openssl_random_pseudo_bytes');
106+
}
107+
108+
// openssl_random_pseudo_bytes() will find entropy in a system-dependent
109+
// way.
110+
if ($has_openssl) {
111+
$bytes .= openssl_random_pseudo_bytes($missing_bytes);
112+
}
113+
114+
// Else, read directly from /dev/urandom, which is available on many *nix
115+
// systems and is considered cryptographically secure.
116+
elseif ($fh = @fopen('/dev/urandom', 'rb')) {
117+
// PHP only performs buffered reads, so in reality it will always read
118+
// at least 4096 bytes. Thus, it costs nothing extra to read and store
119+
// that much so as to speed any additional invocations.
120+
$bytes .= fread($fh, max(4096, $missing_bytes));
121+
fclose($fh);
122+
}
123+
124+
// If we couldn't get enough entropy, this simple hash-based PRNG will
125+
// generate a good set of pseudo-random bytes on any system.
126+
// Note that it may be important that our $random_state is passed
127+
// through hash() prior to being rolled into $output, that the two hash()
128+
// invocations are different, and that the extra input into the first one -
129+
// the microtime() - is prepended rather than appended. This is to avoid
130+
// directly leaking $random_state via the $output stream, which could
131+
// allow for trivial prediction of further "random" numbers.
132+
if (strlen($bytes) < $count) {
133+
// Initialize on the first call. The contents of $_SERVER includes a mix of
134+
// user-specific and system information that varies a little with each page.
135+
if (!isset($random_state)) {
136+
$random_state = print_r($_SERVER, TRUE);
137+
if (function_exists('getmypid')) {
138+
// Further initialize with the somewhat random PHP process ID.
139+
$random_state .= getmypid();
140+
}
141+
$bytes = '';
142+
}
143+
144+
do {
145+
$random_state = hash('sha256', microtime() . mt_rand() . $random_state);
146+
$bytes .= hash('sha256', mt_rand() . $random_state, TRUE);
147+
}
148+
while (strlen($bytes) < $count);
149+
}
150+
}
151+
$output = substr($bytes, 0, $count);
152+
$bytes = substr($bytes, $count);
153+
return $output;
154+
}
155+
156+
/**
157+
* Generates a random base 64-encoded salt prefixed with settings for the hash.
158+
*
159+
* Proper use of salts may defeat a number of attacks, including:
160+
* - The ability to try candidate passwords against multiple hashes at once.
161+
* - The ability to use pre-hashed lists of candidate passwords.
162+
* - The ability to determine whether two users have the same (or different)
163+
* password without actually having to guess one of the passwords.
164+
*
165+
* @param $count_log2
166+
* Integer that determines the number of iterations used in the hashing
167+
* process. A larger value is more secure, but takes more time to complete.
168+
*
169+
* @return
170+
* A 12 character string containing the iteration count and a random salt.
171+
*/
172+
function _password_generate_salt($count_log2) {
173+
$output = '$S$';
174+
// Ensure that $count_log2 is within set bounds.
175+
$count_log2 = _password_enforce_log2_boundaries($count_log2);
176+
// We encode the final log2 iteration count in base 64.
177+
$itoa64 = _password_itoa64();
178+
$output .= $itoa64[$count_log2];
179+
// 6 bytes is the standard salt for a portable phpass hash.
180+
$output .= _password_base64_encode(_random_bytes(6), 6);
181+
return $output;
182+
}
183+
184+
/**
185+
* Ensures that $count_log2 is within set bounds.
186+
*
187+
* @param $count_log2
188+
* Integer that determines the number of iterations used in the hashing
189+
* process. A larger value is more secure, but takes more time to complete.
190+
*
191+
* @return
192+
* Integer within set bounds that is closest to $count_log2.
193+
*/
194+
function _password_enforce_log2_boundaries($count_log2) {
195+
if ($count_log2 < MIN_HASH_COUNT) {
196+
return MIN_HASH_COUNT;
197+
}
198+
elseif ($count_log2 > MAX_HASH_COUNT) {
199+
return MAX_HASH_COUNT;
200+
}
201+
202+
return (int) $count_log2;
203+
}
204+
205+
/**
206+
* Hash a password using a secure stretched hash.
207+
*
208+
* By using a salt and repeated hashing the password is "stretched". Its
209+
* security is increased because it becomes much more computationally costly
210+
* for an attacker to try to break the hash by brute-force computation of the
211+
* hashes of a large number of plain-text words or strings to find a match.
212+
*
213+
* @param $algo
214+
* The string name of a hashing algorithm usable by hash(), like 'sha256'.
215+
* @param $password
216+
* Plain-text password up to 512 bytes (128 to 512 UTF-8 characters) to hash.
217+
* @param $setting
218+
* An existing hash or the output of _password_generate_salt(). Must be
219+
* at least 12 characters (the settings and salt).
220+
*
221+
* @return
222+
* A string containing the hashed password (and salt) or FALSE on failure.
223+
* The return string will be truncated at DRUPAL_HASH_LENGTH characters max.
224+
*/
225+
function _password_crypt($algo, $password, $setting) {
226+
// Prevent DoS attacks by refusing to hash large passwords.
227+
if (strlen($password) > 512) {
228+
return FALSE;
229+
}
230+
// The first 12 characters of an existing hash are its setting string.
231+
$setting = substr($setting, 0, 12);
232+
233+
if ($setting[0] != '$' || $setting[2] != '$') {
234+
return FALSE;
235+
}
236+
$count_log2 = _password_get_count_log2($setting);
237+
// Hashes may be imported from elsewhere, so we allow != DRUPAL_HASH_COUNT
238+
if ($count_log2 < MIN_HASH_COUNT || $count_log2 > MAX_HASH_COUNT) {
239+
return FALSE;
240+
}
241+
$salt = substr($setting, 4, 8);
242+
// Hashes must have an 8 character salt.
243+
if (strlen($salt) != 8) {
244+
return FALSE;
245+
}
246+
247+
// Convert the base 2 logarithm into an integer.
248+
$count = 1 << $count_log2;
249+
250+
// We rely on the hash() function being available in PHP 5.2+.
251+
$hash = hash($algo, $salt . $password, TRUE);
252+
do {
253+
$hash = hash($algo, $hash . $password, TRUE);
254+
} while (--$count);
255+
256+
$len = strlen($hash);
257+
$output = $setting . _password_base64_encode($hash, $len);
258+
// _password_base64_encode() of a 16 byte MD5 will always be 22 characters.
259+
// _password_base64_encode() of a 64 byte sha512 will always be 86 characters.
260+
$expected = 12 + ceil((8 * $len) / 6);
261+
return (strlen($output) == $expected) ? substr($output, 0, HASH_LENGTH) : FALSE;
262+
}
263+
264+
/**
265+
* Parse the log2 iteration count from a stored hash or setting string.
266+
*/
267+
function _password_get_count_log2($setting) {
268+
$itoa64 = _password_itoa64();
269+
return strpos($itoa64, $setting[3]);
270+
}
271+
272+
/**
273+
* Hash a password using a secure hash.
274+
*
275+
* @param $password
276+
* A plain-text password.
277+
* @param $count_log2
278+
* Optional integer to specify the iteration count. Generally used only during
279+
* mass operations where a value less than the default is needed for speed.
280+
*
281+
* @return
282+
* A string containing the hashed password (and a salt), or FALSE on failure.
283+
*/
284+
function user_hash_password($password, $count_log2 = 0) {
285+
if (empty($count_log2)) {
286+
// Use the standard iteration count.
287+
$count_log2 = variable_get('password_count_log2', DRUPAL_HASH_COUNT);
288+
}
289+
return _password_crypt('sha512', $password, _password_generate_salt($count_log2));
290+
}
291+
292+
/**
293+
* Check whether a plain text password matches a stored hashed password.
294+
*
295+
* @param $password
296+
* A plain-text password
297+
* @param $hashpass
298+
*
299+
* @return
300+
* TRUE or FALSE.
301+
*/
302+
function user_check_password($password, $hashpass) {
303+
$stored_hash = $hashpass;
304+
$type = substr($stored_hash, 0, 3);
305+
switch ($type) {
306+
case '$S$':
307+
// A normal Drupal 7 password using sha512.
308+
$hash = _password_crypt('sha512', $password, $stored_hash);
309+
break;
310+
case '$H$':
311+
// phpBB3 uses "$H$" for the same thing as "$P$".
312+
case '$P$':
313+
// A phpass password generated using md5. This is an
314+
// imported password or from an earlier Drupal version.
315+
$hash = _password_crypt('md5', $password, $stored_hash);
316+
break;
317+
default:
318+
return FALSE;
319+
}
320+
return ($hash && $stored_hash == $hash);
321+
}

lib/user_sql.php

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,13 @@ public function setPassword($uid, $password)
308308
return false;
309309
}
310310
$old_password = $row[$this -> settings['col_password']];
311-
if($this -> settings['set_crypt_type'] === 'joomla2')
311+
312+
// Added and disabled updating passwords for Drupal 7 WD 2018-01-04
313+
if($this -> settings['set_crypt_type'] === 'drupal')
314+
{
315+
return false;
316+
}
317+
elseif($this -> settings['set_crypt_type'] === 'joomla2')
312318
{
313319
if(!class_exists('\PasswordHash'))
314320
require_once('PasswordHash.php');
@@ -415,9 +421,16 @@ public function checkPassword($uid, $password)
415421

416422
Util::writeLog('OC_USER_SQL', "Encrypting and checking password",
417423
Util::DEBUG);
424+
// Added handling for Drupal 7 passwords WD 2018-01-04
425+
if($this -> settings['set_crypt_type'] === 'drupal')
426+
{
427+
if(!function_exists('user_check_password'))
428+
require_once('drupal.php');
429+
$ret = user_check_password($password, $db_pass);
430+
}
418431
// Joomla 2.5.18 switched to phPass, which doesn't play nice with the
419432
// way we check passwords
420-
if($this -> settings['set_crypt_type'] === 'joomla2')
433+
elseif($this -> settings['set_crypt_type'] === 'joomla2')
421434
{
422435
if(!class_exists('\PasswordHash'))
423436
require_once('PasswordHash.php');

templates/admin.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070
<p><label for="col_displayname"><?php p($l -> t('Real Name Column')); ?></label><input type="text" id="col_displayname" name="col_displayname" value="<?php p($_['col_displayname']); ?>" /></p>
7171

7272
<p><label for="set_crypt_type"><?php p($l -> t('Encryption Type')); ?></label>
73-
<?php $crypt_types = array('md5' => 'MD5', 'md5crypt' => 'MD5 Crypt', 'cleartext' => 'Cleartext', 'mysql_encrypt' => 'mySQL ENCRYPT()', 'system' => 'System (crypt)', 'password_hash' => 'password_hash','mysql_password' => 'mySQL PASSWORD()', 'joomla' => 'Joomla MD5 Encryption', 'joomla2' => 'Joomla > 2.5.18 phpass', 'ssha256' => 'Salted SSHA256', 'redmine' => 'Redmine', 'sha1' => 'SHA1', 'courier_md5' => 'Courier base64-encoded MD5', 'courier_md5raw' => 'Courier hexadecimal MD5', 'courier_sha1' => 'Courier base64-encoded SHA1', 'courier_sha256' => 'Courier base64-encoded SHA256'); ?>
73+
<?php $crypt_types = array('drupal' => 'Drupal 7', 'md5' => 'MD5', 'md5crypt' => 'MD5 Crypt', 'cleartext' => 'Cleartext', 'mysql_encrypt' => 'mySQL ENCRYPT()', 'system' => 'System (crypt)', 'password_hash' => 'password_hash','mysql_password' => 'mySQL PASSWORD()', 'joomla' => 'Joomla MD5 Encryption', 'joomla2' => 'Joomla > 2.5.18 phpass', 'ssha256' => 'Salted SSHA256', 'redmine' => 'Redmine', 'sha1' => 'SHA1', 'courier_md5' => 'Courier base64-encoded MD5', 'courier_md5raw' => 'Courier hexadecimal MD5', 'courier_sha1' => 'Courier base64-encoded SHA1', 'courier_sha256' => 'Courier base64-encoded SHA256'); ?>
7474
<select id="set_crypt_type" name="set_crypt_type">
7575
<?php
7676
foreach ($crypt_types as $driver => $name):

0 commit comments

Comments
 (0)