001/*
002 * $Id: MD5CryptPasswordEncoder.html 4177 2019-06-03 18:56:16Z ball $
003 *
004 * Copyright 2018 Allen D. Ball.  All rights reserved.
005 */
006package ball.spring;
007
008import java.security.MessageDigest;
009import java.security.NoSuchAlgorithmException;
010import java.util.HashMap;
011import java.util.Random;
012import org.apache.logging.log4j.LogManager;
013import org.apache.logging.log4j.Logger;
014import org.springframework.security.crypto.factory.PasswordEncoderFactories;
015import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
016import org.springframework.security.crypto.password.PasswordEncoder;
017import org.springframework.stereotype.Service;
018
019import static java.nio.charset.StandardCharsets.UTF_8;
020
021/**
022 * Dovecot compatible {@link PasswordEncoder} implementation.  MD5-CRYPT
023 * reference implementation available at
024 * {@link.uri https://github.com/dovecot/core/blob/master/src/auth/password-scheme-md5crypt.c target=newtab password-scheme-md5crypt.c}.
025 *
026 * @author {@link.uri mailto:ball@hcf.dev Allen D. Ball}
027 * @version $Revision: 4177 $
028 */
029@Service
030public class MD5CryptPasswordEncoder extends DelegatingPasswordEncoder {
031    private static final Logger LOGGER = LogManager.getLogger();
032
033    private static final String MD5_CRYPT = "MD5-CRYPT";
034    private static final HashMap<String,PasswordEncoder> MAP = new HashMap<>();
035
036    static {
037        MAP.put(MD5_CRYPT, MD5Crypt.INSTANCE);
038        MAP.put("CLEAR", NoCrypt.INSTANCE);
039        MAP.put("CLEARTEXT", NoCrypt.INSTANCE);
040        MAP.put("PLAIN", NoCrypt.INSTANCE);
041        MAP.put("PLAINTEXT", NoCrypt.INSTANCE);
042    }
043
044    private static final Random RANDOM = new Random();
045
046    /**
047     * Sole constructor.
048     */
049    public MD5CryptPasswordEncoder() {
050        super(MD5_CRYPT, MAP);
051
052        setDefaultPasswordEncoderForMatches(PasswordEncoderFactories.createDelegatingPasswordEncoder());
053    }
054
055    @Override
056    public String toString() { return super.toString(); }
057
058    private static class NoCrypt implements PasswordEncoder {
059        private static final String SALT =
060            "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
061        private static final String ITOA64 =
062            "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
063
064        public static final NoCrypt INSTANCE = new NoCrypt();
065
066        public NoCrypt() { }
067
068        @Override
069        public String encode(CharSequence raw) {
070            return raw.toString();
071        }
072
073        @Override
074        public boolean matches(CharSequence raw, String encoded) {
075            return raw.toString().equals(encoded);
076        }
077
078        protected String salt(int length) {
079            StringBuilder buffer = new StringBuilder();
080
081            while (buffer.length() < length) {
082                int index = (int) (RANDOM.nextFloat() * SALT.length());
083
084                buffer.append(SALT.charAt(index));
085            }
086
087            return buffer.toString();
088        }
089
090        protected String itoa64(long value, int size) {
091            StringBuilder buffer = new StringBuilder();
092
093            while (--size >= 0) {
094                buffer.append(ITOA64.charAt((int) (value & 0x3f)));
095
096                value >>>= 6;
097            }
098
099            return buffer.toString();
100        }
101
102        @Override
103        public String toString() { return super.toString(); }
104    }
105
106    private static class MD5Crypt extends NoCrypt {
107        private static final String MD5 = "md5";
108        private static final String MAGIC = "$1$";
109        private static final int SALT_LENGTH = 8;
110
111        public static final MD5Crypt INSTANCE = new MD5Crypt();
112
113        public MD5Crypt() { }
114
115        @Override
116        public String encode(CharSequence raw) {
117            return encode(raw.toString(), salt(SALT_LENGTH));
118        }
119
120        private String encode(String raw, String salt) {
121            if (salt.length() > SALT_LENGTH) {
122                salt = salt.substring(0, SALT_LENGTH);
123            }
124
125            return (MAGIC + salt + "$"
126                    + encode(raw.getBytes(UTF_8), salt.getBytes(UTF_8)));
127        }
128
129        private String encode(byte[] password, byte[] salt) {
130            byte[] bytes = null;
131
132            try {
133                MessageDigest ctx = MessageDigest.getInstance(MD5);
134                MessageDigest ctx1 = MessageDigest.getInstance(MD5);
135
136                ctx.update(password);
137                ctx.update(MAGIC.getBytes(UTF_8));
138                ctx.update(salt);
139
140                ctx1.update(password);
141                ctx1.update(salt);
142                ctx1.update(password);
143                bytes = ctx1.digest();
144
145                for (int i = password.length; i > 0;  i -= 16) {
146                    ctx.update(bytes, 0, (i > 16) ? 16 : i);
147                }
148
149                for (int i = 0; i < bytes.length; i += 1) {
150                    bytes[i] = 0;
151                }
152
153                for (int i = password.length; i != 0; i >>>= 1) {
154                    if ((i & 1) != 0) {
155                        ctx.update(bytes, 0, 1);
156                    } else {
157                        ctx.update(password, 0, 1);
158                    }
159                }
160
161                bytes = ctx.digest();
162
163                for (int i = 0; i < 1000; i += 1) {
164                    ctx1 = MessageDigest.getInstance(MD5);
165
166                    if ((i & 1) != 0) {
167                        ctx1.update(password);
168                    } else {
169                        ctx1.update(bytes, 0, 16);
170                    }
171
172                    if ((i % 3) != 0) {
173                        ctx1.update(salt);
174                    }
175
176                    if ((i % 7) != 0) {
177                        ctx1.update(password);
178                    }
179
180                    if ((i & 1) != 0) {
181                        ctx1.update(bytes, 0, 16);
182                    } else {
183                        ctx1.update(password);
184                    }
185
186                    bytes = ctx1.digest();
187                }
188            } catch (NoSuchAlgorithmException exception) {
189                throw new IllegalStateException(exception);
190            }
191
192            StringBuilder result =
193                new StringBuilder()
194                .append(combine(bytes[0], bytes[6], bytes[12], 4))
195                .append(combine(bytes[1], bytes[7], bytes[13], 4))
196                .append(combine(bytes[2], bytes[8], bytes[14], 4))
197                .append(combine(bytes[3], bytes[9], bytes[15], 4))
198                .append(combine(bytes[4], bytes[10], bytes[5], 4))
199                .append(combine((byte) 0, (byte) 0, bytes[11], 2));
200
201            return result.toString();
202        }
203
204        private String combine(byte b0, byte b1, byte b2, int size) {
205            return itoa64(((((long) b0) & 0xff) << 16)
206                          | ((((long) b1) & 0xff) << 8)
207                          | (((long) b2) & 0xff),
208                          size);
209        }
210
211        @Override
212        public boolean matches(CharSequence raw, String encoded) {
213            String salt = null;
214
215            if (encoded.startsWith(MAGIC)) {
216                salt = encoded.substring(MAGIC.length()).split("[$]")[0];
217            } else {
218                throw new IllegalArgumentException("Invalid format");
219            }
220
221            return encoded.equals(encode(raw.toString(), salt));
222        }
223    }
224}