    001    /*
    002     * Licensed to the Apache Software Foundation (ASF) under one or more
    003     * contributor license agreements.  See the NOTICE file distributed with
    004     * this work for additional information regarding copyright ownership.
    005     * The ASF licenses this file to You under the Apache License, Version 2.0
    006     * (the "License"); you may not use this file except in compliance with
    007     * the License.  You may obtain a copy of the License at
    008     *
    009     *      http://www.apache.org/licenses/LICENSE-2.0
    010     *
    011     * Unless required by applicable law or agreed to in writing, software
    012     * distributed under the License is distributed on an "AS IS" BASIS,
    013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014     * See the License for the specific language governing permissions and
    015     * limitations under the License.
    016     */
    017    package org.apache.commons.codec.digest;
    018
    019    import java.security.MessageDigest;
    020    import java.util.Arrays;
    021    import java.util.regex.Matcher;
    022    import java.util.regex.Pattern;
    023
    024    import org.apache.commons.codec.Charsets;
    025
    026    /**
    027     * The libc crypt() "$1$" and Apache "$apr1$" MD5-based hash algorithm.
    028     * <p>
    029     * Based on the public domain ("beer-ware") C implementation from Poul-Henning Kamp which was found at: <a
    030     * href="http://www.freebsd.org/cgi/cvsweb.cgi/src/lib/libcrypt/crypt-md5.c?rev=1.1;content-type=text%2Fplain">
    031     * crypt-md5.c @ freebsd.org</a><br/>
    032     * <p>
    033     * Source:
    034     *
    035     * <pre>
    036     * $FreeBSD: src/lib/libcrypt/crypt-md5.c,v 1.1 1999/01/21 13:50:09 brandon Exp $
    037     * </pre>
    038     * <p>
    039     * Conversion to Kotlin and from there to Java in 2012.
    040     * <p>
    041     * The C style comments are from the original C code, the ones with "//" from the port.
    042     * <p>
    043     * This class is immutable and thread-safe.
    044     *
    045     * @version $Id: Md5Crypt.java 1429868 2013-01-07 16:08:05Z ggregory $
    046     * @since 1.7
    047     */
    048    public class Md5Crypt {
    049
    050        /** The Identifier of the Apache variant. */
    051        static final String APR1_PREFIX = "$apr1$";
    052
    053        /** The number of bytes of the final hash. */
    054        private static final int BLOCKSIZE = 16;
    055
    056        /** The Identifier of this crypt() variant. */
    057        static final String MD5_PREFIX = "$1$";
    058
    059        /** The number of rounds of the big loop. */
    060        private static final int ROUNDS = 1000;
    061
    062        /**
    063         * See {@link #apr1Crypt(String, String)} for details.
    064         *
    065         * @throws RuntimeException
    066         *             when a {@link java.security.NoSuchAlgorithmException} is caught. *
    067         */
    068        public static String apr1Crypt(final byte[] keyBytes) {
    069            return apr1Crypt(keyBytes, APR1_PREFIX + B64.getRandomSalt(8));
    070        }
    071
    072        /**
    073         * See {@link #apr1Crypt(String, String)} for details.
    074         *
    075         * @throws IllegalArgumentException
    076         *             if the salt does not match the allowed pattern
    077         * @throws RuntimeException
    078         *             when a {@link java.security.NoSuchAlgorithmException} is caught.
    079         */
    080        public static String apr1Crypt(final byte[] keyBytes, String salt) {
    081            // to make the md5Crypt regex happy
    082            if (salt != null && !salt.startsWith(APR1_PREFIX)) {
    083                salt = APR1_PREFIX + salt;
    084            }
    085            return Md5Crypt.md5Crypt(keyBytes, salt, APR1_PREFIX);
    086        }
    087
    088        /**
    089         * See {@link #apr1Crypt(String, String)} for details.
    090         *
    091         * @throws RuntimeException
    092         *             when a {@link java.security.NoSuchAlgorithmException} is caught.
    093         */
    094        public static String apr1Crypt(final String keyBytes) {
    095            return apr1Crypt(keyBytes.getBytes(Charsets.UTF_8));
    096        }
    097
    098        /**
    099         * Generates an Apache htpasswd compatible "$apr1$" MD5 based hash value.
    100         * <p>
    101         * The algorithm is identical to the crypt(3) "$1$" one but produces different outputs due to the different salt
    102         * prefix.
    103         *
    104         * @param keyBytes
    105         *            plaintext string that should be hashed.
    106         * @param salt
    107         *            salt string including the prefix and optionally garbage at the end. Will be generated randomly if
    108         *            null.
    109         * @return computed hash value
    110         * @throws IllegalArgumentException
    111         *             if the salt does not match the allowed pattern
    112         * @throws RuntimeException
    113         *             when a {@link java.security.NoSuchAlgorithmException} is caught.
    114         */
    115        public static String apr1Crypt(final String keyBytes, final String salt) {
    116            return apr1Crypt(keyBytes.getBytes(Charsets.UTF_8), salt);
    117        }
    118
    119        /**
    120         * Generates a libc6 crypt() compatible "$1$" hash value.
    121         * <p>
    122         * See {@link Crypt#crypt(String, String)} for details.
    123         *
    124         * @throws RuntimeException
    125         *             when a {@link java.security.NoSuchAlgorithmException} is caught.
    126         */
    127        public static String md5Crypt(final byte[] keyBytes) {
    128            return md5Crypt(keyBytes, MD5_PREFIX + B64.getRandomSalt(8));
    129        }
    130
    131        /**
    132         * Generates a libc crypt() compatible "$1$" MD5 based hash value.
    133         * <p>
    134         * See {@link Crypt#crypt(String, String)} for details.
    135         *
    136         * @param keyBytes
    137         *            plaintext string that should be hashed.
    138         * @param salt
    139         *            salt string including the prefix and optionally garbage at the end. Will be generated randomly if
    140         *            null.
    141         * @return computed hash value
    142         * @throws IllegalArgumentException
    143         *             if the salt does not match the allowed pattern
    144         * @throws RuntimeException
    145         *             when a {@link java.security.NoSuchAlgorithmException} is caught.
    146         */
    147        public static String md5Crypt(final byte[] keyBytes, final String salt) {
    148            return md5Crypt(keyBytes, salt, MD5_PREFIX);
    149        }
    150
    151        /**
    152         * Generates a libc6 crypt() "$1$" or Apache htpasswd "$apr1$" hash value.
    153         * <p>
    154         * See {@link Crypt#crypt(String, String)} or {@link #apr1Crypt(String, String)} for details.
    155         *
    156         * @throws IllegalArgumentException
    157         *             if the salt does not match the allowed pattern
    158         * @throws RuntimeException
    159         *             when a {@link java.security.NoSuchAlgorithmException} is caught.
    160         */
    161        public static String md5Crypt(final byte[] keyBytes, final String salt, final String prefix) {
    162            final int keyLen = keyBytes.length;
    163
    164            // Extract the real salt from the given string which can be a complete hash string.
    165            String saltString;
    166            if (salt == null) {
    167                saltString = B64.getRandomSalt(8);
    168            } else {
    169                final Pattern p = Pattern.compile("^" + prefix.replace("$", "\\$") + "([\\.\\/a-zA-Z0-9]{1,8}).*");
    170                final Matcher m = p.matcher(salt);
    171                if (m == null || !m.find()) {
    172                    throw new IllegalArgumentException("Invalid salt value: " + salt);
    173                }
    174                saltString = m.group(1);
    175            }
    176            final byte[] saltBytes = saltString.getBytes(Charsets.UTF_8);
    177
    178            final MessageDigest ctx = DigestUtils.getMd5Digest();
    179
    180            /*
    181             * The password first, since that is what is most unknown
    182             */
    183            ctx.update(keyBytes);
    184
    185            /*
    186             * Then our magic string
    187             */
    188            ctx.update(prefix.getBytes(Charsets.UTF_8));
    189
    190            /*
    191             * Then the raw salt
    192             */
    193            ctx.update(saltBytes);
    194
    195            /*
    196             * Then just as many characters of the MD5(pw,salt,pw)
    197             */
    198            MessageDigest ctx1 = DigestUtils.getMd5Digest();
    199            ctx1.update(keyBytes);
    200            ctx1.update(saltBytes);
    201            ctx1.update(keyBytes);
    202            byte[] finalb = ctx1.digest();
    203            int ii = keyLen;
    204            while (ii > 0) {
    205                ctx.update(finalb, 0, ii > 16 ? 16 : ii);
    206                ii -= 16;
    207            }
    208
    209            /*
    210             * Don't leave anything around in vm they could use.
    211             */
    212            Arrays.fill(finalb, (byte) 0);
    213
    214            /*
    215             * Then something really weird...
    216             */
    217            ii = keyLen;
    218            final int j = 0;
    219            while (ii > 0) {
    220                if ((ii & 1) == 1) {
    221                    ctx.update(finalb[j]);
    222                } else {
    223                    ctx.update(keyBytes[j]);
    224                }
    225                ii >>= 1;
    226            }
    227
    228            /*
    229             * Now make the output string
    230             */
    231            final StringBuilder passwd = new StringBuilder(prefix + saltString + "$");
    232            finalb = ctx.digest();
    233
    234            /*
    235             * and now, just to make sure things don't run too fast On a 60 Mhz Pentium this takes 34 msec, so you would
    236             * need 30 seconds to build a 1000 entry dictionary...
    237             */
    238            for (int i = 0; i < ROUNDS; i++) {
    239                ctx1 = DigestUtils.getMd5Digest();
    240                if ((i & 1) != 0) {
    241                    ctx1.update(keyBytes);
    242                } else {
    243                    ctx1.update(finalb, 0, BLOCKSIZE);
    244                }
    245
    246                if (i % 3 != 0) {
    247                    ctx1.update(saltBytes);
    248                }
    249
    250                if (i % 7 != 0) {
    251                    ctx1.update(keyBytes);
    252                }
    253
    254                if ((i & 1) != 0) {
    255                    ctx1.update(finalb, 0, BLOCKSIZE);
    256                } else {
    257                    ctx1.update(keyBytes);
    258                }
    259                finalb = ctx1.digest();
    260            }
    261
    262            // The following was nearly identical to the Sha2Crypt code.
    263            // Again, the buflen is not really needed.
    264            // int buflen = MD5_PREFIX.length() - 1 + salt_string.length() + 1 + BLOCKSIZE + 1;
    265            B64.b64from24bit(finalb[0], finalb[6], finalb[12], 4, passwd);
    266            B64.b64from24bit(finalb[1], finalb[7], finalb[13], 4, passwd);
    267            B64.b64from24bit(finalb[2], finalb[8], finalb[14], 4, passwd);
    268            B64.b64from24bit(finalb[3], finalb[9], finalb[15], 4, passwd);
    269            B64.b64from24bit(finalb[4], finalb[10], finalb[5], 4, passwd);
    270            B64.b64from24bit((byte) 0, (byte) 0, finalb[11], 2, passwd);
    271
    272            /*
    273             * Don't leave anything around in vm they could use.
    274             */
    275            // Is there a better way to do this with the JVM?
    276            ctx.reset();
    277            ctx1.reset();
    278            Arrays.fill(keyBytes, (byte) 0);
    279            Arrays.fill(saltBytes, (byte) 0);
    280            Arrays.fill(finalb, (byte) 0);
    281
    282            return passwd.toString();
    283        }
    284    }