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.html 889935 2013-12-11 05:05:13Z 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(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(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(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(String keyBytes, 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(byte[] keyBytes, 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            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            byte[] saltBytes = saltString.getBytes(Charsets.UTF_8);
177    
178            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            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            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    }