mirror of
https://github.com/AuthMe/AuthMeReloaded.git
synced 2024-11-27 20:57:35 +01:00
#1400 Sync AuthMe's phpBB hash implementation with phpBB3's
- phpBB3 seems to favor using BCrypt $2y$ now - Keep unsalted MD5 and phpass salted MD5 comparisons for backwards compatibility
This commit is contained in:
parent
80f9ec88b8
commit
80ab41ae5a
@ -7,146 +7,150 @@ import java.io.UnsupportedEncodingException;
|
|||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author stefano
|
* Encryption method compatible with phpBB3.
|
||||||
|
* <p>
|
||||||
|
* As tested with phpBB 3.2.1, by default new passwords are encrypted with BCrypt $2y$.
|
||||||
|
* For backwards compatibility, phpBB3 supports other hashes for comparison. This implementation
|
||||||
|
* successfully checks against phpBB's salted MD5 hashing algorithm (adaptation of phpass),
|
||||||
|
* as well as plain MD5.
|
||||||
*/
|
*/
|
||||||
public class PhpBB extends HexSaltedMethod {
|
public class PhpBB implements EncryptionMethod {
|
||||||
|
|
||||||
private static final String itoa64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
private final BCrypt2y bCrypt2y = new BCrypt2y();
|
||||||
|
|
||||||
private static String md5(String data) {
|
@Override
|
||||||
try {
|
public HashedPassword computeHash(String password, String name) {
|
||||||
byte[] bytes = data.getBytes("ISO-8859-1");
|
String salt = generateSalt();
|
||||||
MessageDigest md5er = HashUtils.getDigest(MessageDigestAlgorithm.MD5);
|
return new HashedPassword(BCryptService.hashpw(password, salt));
|
||||||
byte[] hash = md5er.digest(bytes);
|
|
||||||
return bytes2hex(hash);
|
|
||||||
} catch (UnsupportedEncodingException e) {
|
|
||||||
throw new UnsupportedOperationException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int hexToInt(char ch) {
|
|
||||||
if (ch >= '0' && ch <= '9')
|
|
||||||
return ch - '0';
|
|
||||||
ch = Character.toUpperCase(ch);
|
|
||||||
if (ch >= 'A' && ch <= 'F')
|
|
||||||
return ch - 'A' + 0xA;
|
|
||||||
throw new IllegalArgumentException("Not a hex character: " + ch);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String bytes2hex(byte[] bytes) {
|
|
||||||
StringBuilder r = new StringBuilder(32);
|
|
||||||
for (byte b : bytes) {
|
|
||||||
String x = Integer.toHexString(b & 0xff);
|
|
||||||
if (x.length() < 2)
|
|
||||||
r.append('0');
|
|
||||||
r.append(x);
|
|
||||||
}
|
|
||||||
return r.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String pack(String hex) {
|
|
||||||
StringBuilder buf = new StringBuilder();
|
|
||||||
for (int i = 0; i < hex.length(); i += 2) {
|
|
||||||
char c1 = hex.charAt(i);
|
|
||||||
char c2 = hex.charAt(i + 1);
|
|
||||||
char packed = (char) (hexToInt(c1) * 16 + hexToInt(c2));
|
|
||||||
buf.append(packed);
|
|
||||||
}
|
|
||||||
return buf.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String phpbb_hash(String password, String salt) {
|
|
||||||
String random_state = salt;
|
|
||||||
StringBuilder random = new StringBuilder();
|
|
||||||
int count = 6;
|
|
||||||
for (int i = 0; i < count; i += 16) {
|
|
||||||
random_state = md5(salt + random_state);
|
|
||||||
random.append(pack(md5(random_state)));
|
|
||||||
}
|
|
||||||
String hash = _hash_crypt_private(password, _hash_gensalt_private(random.substring(0, count), itoa64));
|
|
||||||
if (hash.length() == 34) {
|
|
||||||
return hash;
|
|
||||||
}
|
|
||||||
return md5(password);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String _hash_gensalt_private(String input, String itoa64) {
|
|
||||||
return _hash_gensalt_private(input, itoa64, 6);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String _hash_gensalt_private(String input, String itoa64,
|
|
||||||
int iteration_count_log2) {
|
|
||||||
if (iteration_count_log2 < 4 || iteration_count_log2 > 31) {
|
|
||||||
iteration_count_log2 = 8;
|
|
||||||
}
|
|
||||||
String output = "$H$";
|
|
||||||
output += itoa64.charAt(Math.min(iteration_count_log2 + 3, 30)); // PHP_VERSION >= 5 ? 5 : 3
|
|
||||||
output += _hash_encode64(input, 6);
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String _hash_encode64(String input, int count) {
|
|
||||||
StringBuilder output = new StringBuilder();
|
|
||||||
int i = 0;
|
|
||||||
do {
|
|
||||||
int value = input.charAt(i++);
|
|
||||||
output.append(itoa64.charAt(value & 0x3f));
|
|
||||||
if (i < count)
|
|
||||||
value |= input.charAt(i) << 8;
|
|
||||||
output.append(itoa64.charAt((value >> 6) & 0x3f));
|
|
||||||
if (i++ >= count)
|
|
||||||
break;
|
|
||||||
if (i < count)
|
|
||||||
value |= input.charAt(i) << 16;
|
|
||||||
output.append(itoa64.charAt((value >> 12) & 0x3f));
|
|
||||||
if (i++ >= count)
|
|
||||||
break;
|
|
||||||
output.append(itoa64.charAt((value >> 18) & 0x3f));
|
|
||||||
} while (i < count);
|
|
||||||
return output.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String _hash_crypt_private(String password, String setting) {
|
|
||||||
String output = "*";
|
|
||||||
if (!setting.substring(0, 3).equals("$H$"))
|
|
||||||
return output;
|
|
||||||
int count_log2 = itoa64.indexOf(setting.charAt(3));
|
|
||||||
if (count_log2 < 7 || count_log2 > 30)
|
|
||||||
return output;
|
|
||||||
int count = 1 << count_log2;
|
|
||||||
String salt = setting.substring(4, 12);
|
|
||||||
if (salt.length() != 8)
|
|
||||||
return output;
|
|
||||||
String m1 = md5(salt + password);
|
|
||||||
String hash = pack(m1);
|
|
||||||
do {
|
|
||||||
hash = pack(md5(hash + password));
|
|
||||||
} while (--count > 0);
|
|
||||||
output = setting.substring(0, 12);
|
|
||||||
output += _hash_encode64(hash, 16);
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean phpbb_check_hash(String password, String hash) {
|
|
||||||
if (hash.length() == 34) {
|
|
||||||
return _hash_crypt_private(password, hash).equals(hash);
|
|
||||||
}
|
|
||||||
return md5(password).equals(hash);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String computeHash(String password, String salt, String name) {
|
public String computeHash(String password, String salt, String name) {
|
||||||
return phpbb_hash(password, salt);
|
return bCrypt2y.computeHash(password, salt, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean comparePassword(String password, HashedPassword hashedPassword, String name) {
|
public boolean comparePassword(String password, HashedPassword hashedPassword, String name) {
|
||||||
return phpbb_check_hash(password, hashedPassword.getHash());
|
final String hash = hashedPassword.getHash();
|
||||||
|
if (HashUtils.isValidBcryptHash(hash)) {
|
||||||
|
return bCrypt2y.comparePassword(password, hashedPassword, name);
|
||||||
|
} else if (hash.length() == 34) {
|
||||||
|
return PhpassSaltedMd5.phpbb_check_hash(password, hash);
|
||||||
|
} else {
|
||||||
|
return PhpassSaltedMd5.md5(password).equals(hash);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getSaltLength() {
|
public String generateSalt() {
|
||||||
return 16;
|
// Salt length 22, as seen in https://github.com/phpbb/phpbb/blob/master/phpBB/phpbb/passwords/driver/bcrypt.php
|
||||||
|
return BCryptService.gensalt(10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasSeparateSalt() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Java implementation of the salted MD5 as used in phpBB (adapted from phpass).
|
||||||
|
*
|
||||||
|
* @see <a href="https://github.com/phpbb/phpbb/blob/master/phpBB/phpbb/passwords/driver/salted_md5.php">phpBB's salted_md5.php</a>
|
||||||
|
* @see <a href="http://www.openwall.com/phpass/">phpass</a>
|
||||||
|
*/
|
||||||
|
private static final class PhpassSaltedMd5 {
|
||||||
|
|
||||||
|
private static final String itoa64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||||
|
|
||||||
|
private static String md5(String data) {
|
||||||
|
try {
|
||||||
|
byte[] bytes = data.getBytes("ISO-8859-1");
|
||||||
|
MessageDigest md5er = HashUtils.getDigest(MessageDigestAlgorithm.MD5);
|
||||||
|
byte[] hash = md5er.digest(bytes);
|
||||||
|
return bytes2hex(hash);
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
throw new UnsupportedOperationException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int hexToInt(char ch) {
|
||||||
|
if (ch >= '0' && ch <= '9')
|
||||||
|
return ch - '0';
|
||||||
|
ch = Character.toUpperCase(ch);
|
||||||
|
if (ch >= 'A' && ch <= 'F')
|
||||||
|
return ch - 'A' + 0xA;
|
||||||
|
throw new IllegalArgumentException("Not a hex character: " + ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String bytes2hex(byte[] bytes) {
|
||||||
|
StringBuilder r = new StringBuilder(32);
|
||||||
|
for (byte b : bytes) {
|
||||||
|
String x = Integer.toHexString(b & 0xff);
|
||||||
|
if (x.length() < 2)
|
||||||
|
r.append('0');
|
||||||
|
r.append(x);
|
||||||
|
}
|
||||||
|
return r.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String pack(String hex) {
|
||||||
|
StringBuilder buf = new StringBuilder();
|
||||||
|
for (int i = 0; i < hex.length(); i += 2) {
|
||||||
|
char c1 = hex.charAt(i);
|
||||||
|
char c2 = hex.charAt(i + 1);
|
||||||
|
char packed = (char) (hexToInt(c1) * 16 + hexToInt(c2));
|
||||||
|
buf.append(packed);
|
||||||
|
}
|
||||||
|
return buf.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String _hash_encode64(String input, int count) {
|
||||||
|
StringBuilder output = new StringBuilder();
|
||||||
|
int i = 0;
|
||||||
|
do {
|
||||||
|
int value = input.charAt(i++);
|
||||||
|
output.append(itoa64.charAt(value & 0x3f));
|
||||||
|
if (i < count)
|
||||||
|
value |= input.charAt(i) << 8;
|
||||||
|
output.append(itoa64.charAt((value >> 6) & 0x3f));
|
||||||
|
if (i++ >= count)
|
||||||
|
break;
|
||||||
|
if (i < count)
|
||||||
|
value |= input.charAt(i) << 16;
|
||||||
|
output.append(itoa64.charAt((value >> 12) & 0x3f));
|
||||||
|
if (i++ >= count)
|
||||||
|
break;
|
||||||
|
output.append(itoa64.charAt((value >> 18) & 0x3f));
|
||||||
|
} while (i < count);
|
||||||
|
return output.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String _hash_crypt_private(String password, String setting) {
|
||||||
|
String output = "*";
|
||||||
|
if (!setting.substring(0, 3).equals("$H$"))
|
||||||
|
return output;
|
||||||
|
int count_log2 = itoa64.indexOf(setting.charAt(3));
|
||||||
|
if (count_log2 < 7 || count_log2 > 30)
|
||||||
|
return output;
|
||||||
|
int count = 1 << count_log2;
|
||||||
|
String salt = setting.substring(4, 12);
|
||||||
|
if (salt.length() != 8)
|
||||||
|
return output;
|
||||||
|
String m1 = md5(salt + password);
|
||||||
|
String hash = pack(m1);
|
||||||
|
do {
|
||||||
|
hash = pack(md5(hash + password));
|
||||||
|
} while (--count > 0);
|
||||||
|
output = setting.substring(0, 12);
|
||||||
|
output += _hash_encode64(hash, 16);
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean phpbb_check_hash(String password, String hash) {
|
||||||
|
if (hash.length() == 34) {
|
||||||
|
return _hash_crypt_private(password, hash).equals(hash);
|
||||||
|
}
|
||||||
|
return md5(password).equals(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,12 @@
|
|||||||
package fr.xephi.authme.security.crypts;
|
package fr.xephi.authme.security.crypts;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test for {@link PhpBB}.
|
* Test for {@link PhpBB}.
|
||||||
*/
|
*/
|
||||||
@ -7,10 +14,46 @@ public class PhpBBTest extends AbstractEncryptionMethodTest {
|
|||||||
|
|
||||||
public PhpBBTest() {
|
public PhpBBTest() {
|
||||||
super(new PhpBB(),
|
super(new PhpBB(),
|
||||||
"$H$7MaSGQb0xe3Fp/a.Q.Ewpw.UKfCv.t0", // password
|
"$2a$10$1rnuna3GBduBy1NQuOpnWODqBfl8CZHeULuBThNfAvkOYDRRQR1Zi", // password
|
||||||
"$H$7ESfAVjzqajC7fJFcZKZIhyds41MuW.", // PassWord1
|
"$2a$10$F6LVgXa8.t95H0Fikr6nG.aEMgIQRXlFpzMvAjbO7ag3fny9GGS3i", // PassWord1
|
||||||
"$H$7G65SXRPbR69jLg.qZTjtqsw36Ciw7.", // &^%te$t?Pw@_
|
"$2a$10$ex57hkfuMLwYsdG8ru/4teh48kHCSv0HPLPjhhHsEB3NqXiOi7RQS", // &^%te$t?Pw@_
|
||||||
"$H$7Brcg8zO9amr2SHVgz.pFxprDu40v4/"); // âË_3(íù*
|
"$2a$10$2B/HAJ3MeoxGQgqLM6GDlOBqd.2uzLPi1VznXlrXcayLixSaRIWqC"); // âË_3(íù*
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldMatchPhpassSaltedMd5Hashes() {
|
||||||
|
// given
|
||||||
|
Map<String, String> givenHashes = ImmutableMap.of(
|
||||||
|
"password", "$H$7MaSGQb0xe3Fp/a.Q.Ewpw.UKfCv.t0",
|
||||||
|
"PassWord1", "$H$7ESfAVjzqajC7fJFcZKZIhyds41MuW.",
|
||||||
|
"&^%te$t?Pw@_", "$H$7G65SXRPbR69jLg.qZTjtqsw36Ciw7.",
|
||||||
|
"âË_3(íù*", "$H$7Brcg8zO9amr2SHVgz.pFxprDu40v4/");
|
||||||
|
PhpBB phpBB = new PhpBB();
|
||||||
|
|
||||||
|
// when / then
|
||||||
|
for (Map.Entry<String, String> hashEntry : givenHashes.entrySet()) {
|
||||||
|
if (!phpBB.comparePassword(hashEntry.getKey(), new HashedPassword(hashEntry.getValue()), null)) {
|
||||||
|
fail("Hash comparison for '" + hashEntry.getKey() + "' failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldMatchUnsaltedMd5Hashes() {
|
||||||
|
// given
|
||||||
|
Map<String, String> givenHashes = ImmutableMap.of(
|
||||||
|
"password", "5f4dcc3b5aa765d61d8327deb882cf99",
|
||||||
|
"PassWord1", "f2126d405f46ed603ff5b2950f062c96",
|
||||||
|
"&^%te$t?Pw@_", "0833dcd2bc741f90c46bbac5498fd08f",
|
||||||
|
"âË_3(íù*", "e7412bf1a9d312dc2901c3101a097abe");
|
||||||
|
PhpBB phpBB = new PhpBB();
|
||||||
|
|
||||||
|
// when / then
|
||||||
|
for (Map.Entry<String, String> hashEntry : givenHashes.entrySet()) {
|
||||||
|
if (!phpBB.comparePassword(hashEntry.getKey(), new HashedPassword(hashEntry.getValue()), null)) {
|
||||||
|
fail("Hash comparison for '" + hashEntry.getKey() + "' failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user