/*
* Copyright (C) 2010-2016 JPEXS, All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3.0 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library.
*/
package com.jpexs.helpers;
import com.jpexs.helpers.utf8.Utf8Helper;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Random;
/**
* MD5 crypt - based on passlib.hash.md5_crypt
*
* @author JPEXS
*/
public class MD5Crypt {
private static final String SALT_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
private static final String HASH64_CHARS = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
public static final String MAGIC = "$1$";
public static final String MAGIC_APACHE = "$apr1$";
public static boolean checkPassword(String password, String hash) {
String magic;
if (hash.startsWith(MAGIC)) {
magic = MAGIC;
} else if (hash.startsWith(MAGIC_APACHE)) {
magic = MAGIC_APACHE;
} else {
return false;
}
String checksum = hash.substring(magic.length());
String salt = "";
if (checksum.contains("$")) {
salt = checksum.substring(0, checksum.indexOf('$'));
}
return hash.equals(crypt(password, salt, magic));
}
public static String generateSalt(int length) {
Random rnd = new Random();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
sb.append(SALT_CHARS.charAt(rnd.nextInt(SALT_CHARS.length())));
}
return sb.toString();
}
public static String crypt(String password, int saltLength, String magic) {
return crypt(password, generateSalt(saltLength), magic);
}
public static String crypt(String password, int saltLength) {
return crypt(password, generateSalt(saltLength), MAGIC);
}
public static String cryptApache(String password, int saltLength) {
return crypt(password, generateSalt(saltLength), MAGIC_APACHE);
}
public static String crypt(String password, String salt) {
return crypt(password, salt, MAGIC);
}
public static String cryptApache(String password, String salt) {
return crypt(password, salt, MAGIC_APACHE);
}
private static String crypt(String password, String salt, String magic) {
if (salt.startsWith(magic)) {
salt = salt.substring(magic.length());
}
if (salt.length() > 8) {
salt = salt.substring(0, 8);
}
byte[] passwordBytes = password.getBytes(Utf8Helper.charset);
byte[] saltBytes = salt.getBytes(Utf8Helper.charset);
byte[] constBytes = magic.getBytes(Utf8Helper.charset);
MessageDigest b;
try {
b = MessageDigest.getInstance("MD5"); //Start MD5 digest B
} catch (NoSuchAlgorithmException ex) {
return null;
}
b.update(passwordBytes); //Add the password to digest B
b.update(saltBytes);//Add the salt to digest B
b.update(passwordBytes); //Add the password to digest B
byte[] digest_b = b.digest(); //Finish MD5 digest B
MessageDigest a;
try {
a = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException ex) {
return null;
}
a.update(passwordBytes); //Add the password to digest A
a.update(constBytes); //Add the constant string $1$ to digest A
a.update(saltBytes); //Add the salt to digest A
//For each block of 16 bytes in the password string, add digest B to digest A
for (int i = passwordBytes.length; i > 0; i -= 16) {
if (i >= 16) {
a.update(digest_b);
} else { //For the remaining N bytes of the password string, add the first N bytes of digest B to digest A
a.update(digest_b, 0, i);
}
}
/*
For each bit in the binary representation of the length of the password string; starting with the lowest value bit, up to and including the largest-valued bit that is set to 1:
If the current bit is set 0 (!! not 1), add the first character of the password to digest A.
Otherwise, add a NULL character to digest A.
(If the password is the empty string, step 14 is omitted entirely).
*/
for (int i = passwordBytes.length; i > 0; i = i >>> 1) {
if ((i & 1) == 0) {
a.update(passwordBytes, 0, 1);
} else {
a.update((byte) 0);
}
}
byte[] digest_a = a.digest(); //Finish MD5 digest A
byte[] round_result = null;
//For 1000 rounds (round values 0..999 inclusive)
for (int round = 0; round < 1000; round++) {
MessageDigest c;
try {
c = MessageDigest.getInstance("MD5"); //Start MD5 digest C
} catch (NoSuchAlgorithmException ex) {
return null;
}
//If the round is odd, add the password to digest C
if (round % 2 == 1) {
c.update(passwordBytes);
}
//If the round is even, add the previous round’s result to digest C (for round 0, add digest A instead).
if (round % 2 == 0) {
if (round == 0) {
c.update(digest_a);
} else {
c.update(round_result);
}
}
//If the round is not a multiple of 3, add the salt to digest C.
if (round % 3 != 0) {
c.update(saltBytes);
}
//If the round is not a multiple of 7, add the password to digest C.
if (round % 7 != 0) {
c.update(passwordBytes);
}
//If the round is even, add the password to digest C.
if (round % 2 == 0) {
c.update(passwordBytes);
}
//If the round is odd, add the previous round’s result to digest C (for round 0, add digest A instead).
if (round % 2 == 1) {
if (round == 0) {
c.update(digest_a);
} else {
c.update(round_result);
}
}
//Use the final value of MD5 digest C as the result for this round.
round_result = c.digest();
}
//Transpose the 16 bytes of the final round’s result in the following order:
//12,6,0,13,7,1,14,8,2,15,9,3,5,10,4,11
byte[] transposed = new byte[]{
round_result[12],
round_result[6],
round_result[0],
round_result[13],
round_result[7],
round_result[1],
round_result[14],
round_result[8],
round_result[2],
round_result[15],
round_result[9],
round_result[3],
round_result[5],
round_result[10],
round_result[4],
round_result[11]};
//Encode the resulting 16 byte string into a 22 character hash64-encoded string
//(the 2 msb bits encoded by the last hash64 character are used as 0 padding).
StringBuilder result = new StringBuilder();
int dstCharRem = 22;
for (int srcBytePos = 0; srcBytePos < transposed.length; srcBytePos += 3, dstCharRem -= 4) {
long v = (transposed[srcBytePos] & 0xff);
if (srcBytePos + 1 < transposed.length) {
v |= ((transposed[srcBytePos + 1] & 0xff) << 8);
}
if (srcBytePos + 2 < transposed.length) {
v |= ((transposed[srcBytePos + 2] & 0xff) << 16);
}
for (int j = 0; j < (dstCharRem >= 4 ? 4 : dstCharRem); j++) {
result.append(HASH64_CHARS.charAt((int) (v & 0x3f)));
v >>>= 6;
}
}
return magic + salt + "$" + result.toString();
}
}