/* * This file is part of the Illarion project. * * Copyright © 2015 - Illarion e.V. * * Illarion is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Illarion 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 General Public License for more details. */ package illarion.common.util; import org.jetbrains.annotations.Contract; import javax.annotation.Nonnull; import javax.annotation.concurrent.NotThreadSafe; import java.nio.charset.Charset; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; /** * This class is able to calculate a salted MD5 that is compatible to the MD5 created by the Unix crypt command. * * @author Martin Karing <nitram@illarion.org> */ @NotThreadSafe public class Md5Crypto { /** * This is the digest used to calculate the MD5 stuff. */ @Nonnull private final MessageDigest md5Digest; /** * Create a new md5 cryptographic instance. */ public Md5Crypto() { try { md5Digest = MessageDigest.getInstance("MD5"); } catch (@Nonnull NoSuchAlgorithmException e) { throw new IllegalStateException(e); } } @Nonnull public String crypt(@Nonnull String message, @Nonnull String salt) { return crypt(message, salt, "$1$"); } @SuppressWarnings("OverlyComplexMethod") @Nonnull public String crypt(@Nonnull String message, @Nonnull String salt, @Nonnull String magic) { String cleanedSalt; cleanedSalt = salt.startsWith(magic) ? salt.substring(magic.length()) : salt; if (cleanedSalt.indexOf('$') != -1) { cleanedSalt = cleanedSalt.substring(0, cleanedSalt.indexOf('$')); } if (cleanedSalt.length() > 8) { cleanedSalt = cleanedSalt.substring(0, 8); } Charset charset = Charset.forName("ISO-8859-1"); byte[] messageBytes = message.getBytes(charset); byte[] magicBytes = magic.getBytes(charset); byte[] saltBytes = cleanedSalt.getBytes(charset); md5Digest.reset(); md5Digest.update(messageBytes); md5Digest.update(saltBytes); md5Digest.update(messageBytes); byte[] currentDigest = md5Digest.digest(); md5Digest.reset(); md5Digest.update(messageBytes); md5Digest.update(magicBytes); md5Digest.update(saltBytes); for (int messageLength = message.length(); messageLength > 0; messageLength -= 16) { md5Digest.update(currentDigest, 0, Math.min(messageLength, 16)); } Arrays.fill(currentDigest, (byte) 0); for (int i = message.length(); i != 0; i >>>= 1) { if ((i & 1) == 0) { md5Digest.update(messageBytes, 0, 1); } else { md5Digest.update(currentDigest, 0, 1); } } byte[] currentEncPassword = md5Digest.digest(); for (int i = 0; i < 1000; i++) { md5Digest.reset(); if ((i & 1) == 0) { md5Digest.update(currentEncPassword, 0, 16); } else { md5Digest.update(messageBytes); } if ((i % 3) != 0) { md5Digest.update(saltBytes); } if ((i % 7) != 0) { md5Digest.update(messageBytes); } if ((i & 1) == 0) { md5Digest.update(messageBytes); } else { md5Digest.update(currentEncPassword, 0, 16); } currentEncPassword = md5Digest.digest(); } byte[] pw = currentEncPassword; return magic + salt + '$' + to64((bytes2u(pw[0]) << 16) | (bytes2u(pw[6]) << 8) | bytes2u(pw[12]), 4) + to64((bytes2u(pw[1]) << 16) | (bytes2u(pw[7]) << 8) | bytes2u(pw[13]), 4) + to64((bytes2u(pw[2]) << 16) | (bytes2u(pw[8]) << 8) | bytes2u(pw[14]), 4) + to64((bytes2u(pw[3]) << 16) | (bytes2u(pw[9]) << 8) | bytes2u(pw[15]), 4) + to64((bytes2u(pw[4]) << 16) | (bytes2u(pw[10]) << 8) | bytes2u(pw[5]), 4) + to64(bytes2u(pw[11]), 2); } /** * This is the string of characters used to perform the ITOA 64 encoding of a value. */ @Nonnull private static final String ITOA_64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; /** * Convert a 64bit integer value to a string character. * * @param v the long value * @param size the size of the resulting text in characters * @return the generated text */ @Nonnull @Contract(pure = true) private static String to64(long v, int size) { StringBuilder result = new StringBuilder(size); while (--size >= 0) { result.append(ITOA_64.charAt((int) (v & 0x3f))); v >>>= 6; } return result.toString(); } /** * Convert a byte to a unsigned value. * * @param inp the byte value * @return the unsigned byte value */ @Contract(pure = true) private static int bytes2u(byte inp) { return inp & 0xff; } }