--- pam-mysql-0.7~RC1/pam_mysql.c	2006-01-09 21:35:59.000000000 +1100
+++ ../pam-mysql-0.7~RC1/pam_mysql.c	2011-02-26 23:19:45.616642002 +1100
@@ -597,8 +597,73 @@
 
 	return md;
 }
-#endif
-/* }}} */
+
+/**
+ * The standard log2 number of iterations for password stretching. This should
+ * increase by 1 every Drupal version in order to counteract increases in the
+ * speed and power of computers available to crack the hashes.
+ */
+#define DRUPAL_HASH_COUNT 14
+
+/**
+ * The minimum allowed log2 number of iterations for password stretching.
+ */
+#define DRUPAL_MIN_HASH_COUNT 7
+
+/**
+ * The maximum allowed log2 number of iterations for password stretching.
+ */
+#define DRUPAL_MAX_HASH_COUNT 30
+
+/**
+ * The expected (and maximum) number of characters in a hashed password.
+ */
+#define DRUPAL_HASH_LENGTH 55
+
+/**
+ * Returns a string for mapping an int to the corresponding base 64 character.
+ */
+static char * _password_itoa64(void)
+{
+  return "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
+}
+
+/**
+ * Parse the log2 iteration count from a stored hash or setting string.
+ */
+static int _password_get_count_log2(char *setting)
+{
+  char *itoa64 = _password_itoa64();
+  return strpos(itoa64, setting[3]);
+}
+
+static char * _password_base64_encode(char *input, int count, char *output) {
+	int i = 0, off = 0;
+	char *itoa64 = _password_itoa64();
+
+	do {
+		int value = ord(input[i++]);
+		output[off++] = itoa64[value & 0x3f];
+		if (i < count) {
+			value |= ord(input[i]) << 8;
+		}
+		output[off++] = itoa64[(value >> 6) & 0x3f];
+		if (i++ >= count) {
+			break;
+		}
+		if (i < count) {
+			value |= ord(input[i]) << 16;
+		}
+		output[off++] = itoa64[(value >> 12) & 0x3f];
+		if (i++ >= count) {
+			break;
+		}
+		output[off++] = itoa64[(value >> 18) & 0x3f];
+	} while (i < count);
+	output[off] = 0;
+
+	return output;
+}
 
 /* {{{ pam_mysql_sha1_data */
 #if defined(HAVE_OPENSSL)
@@ -624,6 +689,127 @@
 
 	return md;
 }
+
+static char *pam_mysql_sha512_data(const unsigned char *d, unsigned int sz, char *md)
+{
+	size_t i, j;
+	unsigned char buf[64];
+
+	if (md == NULL) {
+		if ((md = xcalloc(128 + 1, sizeof(char))) == NULL) {
+			return NULL;
+		}
+	}
+
+	SHA512(d, (unsigned long)sz, buf);
+
+	for (i = 0, j = 0; i < 64; i++, j += 2) {
+		md[j + 0] = "0123456789abcdef"[(int)(buf[i] >> 4)];
+		md[j + 1] = "0123456789abcdef"[(int)(buf[i] & 0x0f)];
+	}
+	md[j] = '\0';
+
+	return md;
+}
+#endif
+/* }}} */
+
+static char *hash(int use_md5, char *string1, char *string2)
+{
+	int len = strlen(string1) + strlen(string2) + 1;
+	char *combined = xcalloc(len, sizeof(char)), *output = NULL;
+
+	if (!combined)
+		return NULL;
+
+	sprintf(combined, "%s%s", string1, string2);
+
+	if (use_md5) {
+		pam_mysql_md5_data(combined, (unsigned long)len - 1, output);
+	} else { // sha512
+		pam_mysql_sha512_data(combined, (unsigned long)len - 1, output);
+	}
+
+	xfree(combined);
+	return output;
+}
+
+// The first 12 characters of an existing hash are its setting string.
+static char * _password_crypt(int use_md5, char *password, char *setting) {
+	char salt[8], *old, *new;
+	int expected, count, count_log2 = _password_get_count_log2(setting);
+	int len;
+
+	// Hashes may be imported from elsewhere, so we allow != DRUPAL_HASH_COUNT
+	if (count_log2 < DRUPAL_MIN_HASH_COUNT || count_log2 > DRUPAL_MAX_HASH_COUNT)
+		return NULL;
+
+	strncpy(salt, &setting[3], 8);
+	if (strlen(salt) != 8)
+		return NULL;
+
+	// Convert the base 2 logarithm into an integer.
+	count = 1 << count_log2;
+
+	old = hash(use_md5, salt , password);
+	do {
+		new = hash(use_md5, old , password);
+		xfree(old);
+		old = new;
+	} while (--count);
+
+	len = strlen(old);
+	new = xcalloc(12 + len, sizeof(char));
+	memcpy(new, setting, 12);
+	_password_base64_encode(old, len, &new[12]);
+	// _password_base64_encode() of a 16 byte MD5 will always be 22 characters.
+	// _password_base64_encode() of a 64 byte sha512 will always be 86 characters.
+	expected = 12 + ((8 * len + 5) / 6);
+	if (strlen(new) != expected) {
+		xfree(new);
+		return NULL;
+	}
+
+	new[DRUPAL_HASH_LENGTH] = 0;
+	return new;
+}
+
+static char *pam_mysql_drupal7_data(const unsigned char *pwd, unsigned int sz, char *md, char *db_pwd)
+{
+	char *stored_hash = db_pwd, *pwd_ptr = (char *) pwd, *hashed;
+	int match = 0, offset = 0;
+
+	// Algorithm taken from user_check_password in includes/password.c in D7.0.
+	if (db_pwd[0] == 'U' && db_pwd[1] == '$') {
+		// This may be an updated password from user_update_7000(). Such hashes
+		// have 'U' added as the first character and need an extra md5().
+		stored_hash = &db_pwd[1];
+		offset++;
+		pwd_ptr = pam_mysql_md5_data(pwd, (unsigned long)sz, md);
+	} else
+		stored_hash = &db_pwd[0];
+
+	if (stored_hash[0] == '$' && stored_hash[2] == '$') {
+		switch (stored_hash[1]) {
+			case 'S':
+				match = 1;
+				hashed = _password_crypt(0, pwd_ptr, stored_hash);
+				break;
+			case 'H':
+      				// phpBB3 uses "$H$" for the same thing as "$P$".
+			case 'P':
+				match = 1;
+				hashed = _password_crypt(1, pwd_ptr, stored_hash);
+				break;
+		}
+	}
+
+	if (!match) {
+		md[0] = db_pwd[0] + 1;
+		return NULL;
+	}
+	memcpy(md, hashed, strlen(hashed));
+}
 #endif
 /* }}} */
 
@@ -2696,6 +2882,21 @@
 #endif
 				} break;
 
+				case 5: {
+#ifdef HAVE_PAM_MYSQL_MD5_DRUPAL7_DATA
+					char buf[65];
+					pam_mysql_drupal7_data((unsigned char*)passwd, strlen(passwd),
+							buf, row[0]);
+					vresult = strcmp(row[0], buf);
+					{
+						char *p = buf - 1;
+						while (*(++p)) *p = '\0';
+					}
+#else
+					syslog(LOG_AUTHPRIV | LOG_ERR, PAM_MYSQL_LOG_PREFIX "non-crypt()ish MD5 hash is not supported in this build.");
+#endif
+				} break;
+
 				default: {
 				}
 			}
