javaによるSaltedHashなpassword (or digest?)の検証

openldap付属?のslappasswdにより生成されたSSHAなパスワードをjavaで検証(verify)する必要が出てきた。
割りとググってみましたが、javaではperlのCrypt::SaltedHashに該当するライブラリを見つけられなかったので、Crypt::SaltedHash::validate()をjavaにre-write。

※ここでの検証とは、平文なパスワードとハッシュ化されたパスワードの整合性を確認することです。

```java
package jp.end0tknr;

import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.codec.binary.Base64;

//////// clone of Crypt::SaltedHash::validate() from perl /////////

public class VerifyPassword {

	String algorithm = "SHA-1";
	static Integer saltLen = 4;
	byte[] salt;
	MessageDigest md;
	String scheme;
	
	public static void main(String[] args) {
		////  $ /usr/local/openldap/sbin/slappasswd -h {SSHA} -s test0000
		if(VerifyPassword.validate(
				"{SSHA}aHKsIEBf5t5ItDF7Yw61SUZWS2aVTjDV",
				"test0000") ){
			System.out.println("VALID !!");  //expects VALID
		} else {
			System.out.println("IN-VALID !!");
		}
		
	}
	
	public VerifyPassword (
			String param_algorithm,
			byte[] param_salt)
			throws NoSuchAlgorithmException {
		
		if(param_algorithm != null && param_algorithm != ""){
			algorithm = param_algorithm.toUpperCase();
		}

		//SHA => SHA-1 , HMAC-SHA => HMAC-SHA-1
		Pattern regex_p = Pattern.compile("SHA$");
		Matcher regex_m = regex_p.matcher(algorithm);
		if(regex_m.find()){
			algorithm += "-1";
		}

		md = MessageDigest.getInstance(algorithm);
		salt = param_salt;
		scheme = makeScheme(algorithm);
	}

	static public boolean validate (String hashedData, String clearData){
		hashedData = hashedData.trim();
		clearData = clearData.trim();
		
		String hashedScheme =
				VerifyPassword.getPassScheme(hashedData).toUpperCase();
		String hashedAlgorithm = 
				VerifyPassword.makeAlgorithm(hashedScheme);
		String hashedHash =
				VerifyPassword.getPassHash(hashedData);
		byte[] hashedSalt =
				VerifyPassword.extractSalt(hashedHash);
		
		VerifyPassword verifyPw;
		try {
			verifyPw = new VerifyPassword(hashedAlgorithm,hashedSalt);
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
			return false;
		}

		String genHashedData = verifyPw.generate(clearData);
		String genHash = VerifyPassword.getPassHash(genHashedData);
	
		if( hashedHash.equals(genHash) ){
			return true;
		}
		return false;
	}
	
	static private String getPassScheme (String hashedData){
		Pattern regex_p = Pattern.compile("\\{([^\\}]*)");
		Matcher regex_m = regex_p.matcher(hashedData);
		if(regex_m.find()){
			return regex_m.group(1);
		}

		return "";
	}
	
	static private String makeAlgorithm (String paramAlgorithm ){
		String retAlgorithm = paramAlgorithm;

		Matcher regex_m = Pattern.compile("^S(.*)$").matcher(retAlgorithm);
		if(! regex_m.find()){
			return retAlgorithm;
		}
		retAlgorithm = regex_m.group(1);

		Matcher regex_m2 =
				Pattern.compile("([a-zA-Z]+)([0-9]+)").matcher(retAlgorithm);
		if(regex_m2.find()){
			String name = regex_m2.group(1);
			String digits = regex_m2.group(2);

			Matcher regex_m3 = Pattern.compile("^HMAC(.*)$").matcher(name);
			if(regex_m3.find()){ //HMAC-SHA-1
				name = "HMAC-"+digits;
			}			
			Matcher regex_m4 = Pattern.compile("MD$").matcher(name);
			if(! regex_m4.find()){ //MD2, MD4, MD5
				digits= "-"+digits;
			}
			retAlgorithm = name + digits;
		}
		
		return retAlgorithm;
	}
	
	static private String getPassHash(String hashedData){
		
		Matcher regex_m = Pattern.compile("}(.*)").matcher(hashedData);
		if(! regex_m.find()){
			return "";
		}
		return regex_m.group(1);
	}
	
	static private byte[] extractSalt(String hashedHash){
		byte[] binHash = Base64.decodeBase64(hashedHash);

		byte[] retSalt = new byte[saltLen];
		for (int i=0; i<saltLen; i++){
			retSalt[i] = binHash[binHash.length-saltLen+i];
		}
		
		return retSalt;
	}
	
	private String generate(String clearData) {
		MessageDigest clone;
		try {
			clone = (MessageDigest)this.md.clone();
		} catch (CloneNotSupportedException e) {
			e.printStackTrace();
			return "";
		}
		
		byte[] saltBin = saltBin();
		ByteBuffer bb = ByteBuffer.allocate(clearData.length()+saltBin.length);
		
		bb.put(clearData.getBytes());
		bb.put(saltBin);
		clone.update( bb.array() );
		byte[] tmpDigest = clone.digest();
		
		bb = ByteBuffer.allocate(tmpDigest.length+saltBin.length);
		bb.put(tmpDigest);
		bb.put(saltBin);
		
		String saltTail = "";
		try {
			saltTail = new String(Base64.encodeBase64(bb.array()),"UTF-8");
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
			return "";
		}
		return "{"+ this.scheme +"}" + saltTail;
	}
	
	private byte[] saltBin(){
		return this.salt;
	}
	
	private String makeScheme(String algorithm){
		return "S"+ algorithm.replaceFirst("-1$","");
	}
}
```