SoapPasswordEncoder.java

/*
 * Copyright 2019 Global Crop Diversity Trust
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.gringlobal.spring;

import java.security.SecureRandom;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * GRIN-Global uses SHA-1 hashed and Base 64 encoded passwords
 *
 * @author Matija Obreza
 */
@Slf4j
public class SoapPasswordEncoder implements PasswordEncoder {

	private final SecureRandom secureRandom = new SecureRandom();

	@Override
	public String encode(final CharSequence rawPassword) {
		if (StringUtils.isBlank(rawPassword)) {
			return null;
		}
		log.trace("Encoding password {}", rawPassword);
		return encodeNew(hashText(rawPassword.toString()));
	}

	@Override
	public boolean matches(final CharSequence rawPassword, final String encodedPassword) {
		// Sometimes we get raw password, sometimes we get hashes
		// System.err.println("Testing '" + rawPassword + "' against '" +
		// encodedPassword + "'");
		final String password = rawPassword.toString();
		final String encodeOld = hashText(password);
		log.trace("Raw password: {}", rawPassword);
		log.trace("Stored password: {}", encodedPassword);
		log.trace("Encode old: {}", encodeOld);

		if (encodedPassword.equals(encodeOld) || rawPassword.equals(encodedPassword)) {
			return true;
		}

		return comparePasswordHash(encodedPassword, password) || comparePasswordHash(encodedPassword, encodeOld);
	}

	private boolean comparePasswordHash(final String encodedPassword, final String password) {
		final String[] hashes = encodedPassword.split(":");
		final String[] passField = hashes[0].split("\\$");

		String salt;
		String storedHash;
		String hashedPassword;

		if (passField.length == 1) {
			// original format of SHA1 hash with no salt
			// crypt = "SHA1";
			salt = "";
			storedHash = passField[0];
			hashedPassword = Base64.encodeBase64String(DigestUtils.sha1(salt + password));
		} else if (passField.length == 2) {
			// two fields means salt and hash
			// crypt = "SHA256";
			salt = passField[0];
			storedHash = passField[1];
			hashedPassword = Base64.encodeBase64String(DigestUtils.sha256(salt + password));
			// } else if (passField.length == 3) {
			// // with three fields the first is the hash type
			// crypt = passField[0];
			// salt = passField[1];
			// storedHash = passField[2];
		} else {
			// can't figure out what is stored in the hash field
			log.warn("Unsupported password crpyo {}", passField[0]);
			return false;
		}

		log.trace("salt={} storedHash={} hashed={}", salt, storedHash, hashedPassword);
		return hashedPassword.equals(storedHash);
	}

	/**
	 * Original SHA1 as Base64
	 *
	 * @param password plain text password
	 * @return SHA1 of the plain text password as Base64
	 */
	public String hashText(final String password) {
		return Base64.encodeBase64String(DigestUtils.sha1(password));
	}

	private String encodeNew(final String password) {
		return saltAndHash(password);
	}

	private String saltAndHash(final String password) {
		final String salt64 = saltText(6);
		final byte[] hash = DigestUtils.sha256(salt64 + password);
		return salt64 + "$" + Base64.encodeBase64String(hash);
	}

	private String saltText(final int length) {
		final byte[] salt = new byte[length];
		secureRandom.nextBytes(salt);
		return Base64.encodeBase64String(salt);
	}

}