DEV Community

Allen D. Ball
Allen D. Ball

Posted on Edited on Originally published at blog.hcf.dev

Spring PasswordEncoder Implementation

Spring Security provides multiple PasswordEncoder implementations with BCRYPT as the recommended implementation. However, the use-case of sharing an authentication database with an external application, Dovecot, is examined in this article. Dovecot uses an MD5-CRYPT algorithm.

Complete javadoc is provided.

Reference

The actual encryption algorithm is captured in the Dovecot source file password-scheme-md5crypt.c.

Implementation

The implementation extends DelegatingPasswordEncoder to provide decryption services for the other Spring Security supported password types. Two inner classes, each subclasses of PasswordEncoder, provide MD5-CRYPT and PLAIN implementations.

@Service
public class MD5CryptPasswordEncoder extends DelegatingPasswordEncoder {
    ...

    private static final String MD5_CRYPT = "MD5-CRYPT";
    private static final HashMap<String,PasswordEncoder> MAP = new HashMap<>();

    static {
        MAP.put(MD5_CRYPT, MD5Crypt.INSTANCE);
        MAP.put("CLEAR", NoCrypt.INSTANCE);
        MAP.put("CLEARTEXT", NoCrypt.INSTANCE);
        MAP.put("PLAIN", NoCrypt.INSTANCE);
        MAP.put("PLAINTEXT", NoCrypt.INSTANCE);
    }

    ...

    public MD5CryptPasswordEncoder() {
        super(MD5_CRYPT, MAP);

        setDefaultPasswordEncoderForMatches(PasswordEncoderFactories.createDelegatingPasswordEncoder());
    }

    private static class NoCrypt implements PasswordEncoder {
        ...
        public static final NoCrypt INSTANCE = new NoCrypt();
        ...
    }

    private static class MD5Crypt extends NoCrypt {
        ...
        public static final MD5Crypt INSTANCE = new MD5Crypt();
        ...
    }
}
Enter fullscreen mode Exit fullscreen mode

The MD5Crypt inner class implementation is straightforward:

    private static class MD5Crypt extends NoCrypt {
        private static final String MD5 = "md5";
        private static final String MAGIC = "$1$";
        private static final int SALT_LENGTH = 8;

        public static final MD5Crypt INSTANCE = new MD5Crypt();

        public MD5Crypt() { }

        @Override
        public String encode(CharSequence raw) {
            return encode(raw.toString(), salt(SALT_LENGTH));
        }

        private String encode(String raw, String salt) {
            if (salt.length() > SALT_LENGTH) {
                salt = salt.substring(0, SALT_LENGTH);
            }

            return (MAGIC + salt + "$" + encode(raw.getBytes(UTF_8), salt.getBytes(UTF_8)));
        }

        private String encode(byte[] password, byte[] salt) {
            /*
             * See source and password-scheme-md5crypt.c.
             */
        }

        @Override
        public boolean matches(CharSequence raw, String encoded) {
            String salt = null;

            if (encoded.startsWith(MAGIC)) {
                salt = encoded.substring(MAGIC.length()).split("[$]")[0];
            } else {
                throw new IllegalArgumentException("Invalid format");
            }

            return encoded.equals(encode(raw.toString(), salt));
        }
    }
Enter fullscreen mode Exit fullscreen mode

The NoCrypt implementation provides the methods for calculating salt and itoa64 conversion.

Spring Boot Application Integration

The PasswordEncoder may be integrated with the following @Configuration:

package some.application;

import ball.spring.MD5CryptPasswordEncoder;
import ...

@Configuration
public class PasswordEncoderConfiguration {
    ...

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new MD5CryptPasswordEncoder();
    }
}
Enter fullscreen mode Exit fullscreen mode

and must be integrated with a UserDetailsService in a WebSecurityConfigurer:

package some.application;

import ...

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfigurerImpl extends WebSecurityConfigurerAdapter {
    ...

    @Autowired private UserDetailsService userDetailsService;
    @Autowired private PasswordEncoder passwordEncoder;

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
            .passwordEncoder(passwordEncoder);
    }

    ...
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)