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