오늘은 어디로 갈까...

MySQL PASSWORD() 함수를 자바로 구현하기 본문

낙서

MySQL PASSWORD() 함수를 자바로 구현하기

剛宇 2009. 3. 6. 13:25
MySQL에는 PASSWORD(str)이란 함수가 있다. 암호(?)를 암호화(?)해주는 함수이다.
예전에 MySQL에 있는 데이터를 Oracle를 변환하는 작업을 한적이 있었는데, 이 PASSWORD()로 암호화(?)되어 있는 놈들이 골치거리였다. MySQL을 사용한다면 sql문에 PASSWORD() 함수를 사용해서 값을 비교하면 되는데, Oracle에는 그 기능(?)을 하는게 없었다. 더군다나 PASSWORD() 함수의 알고리즘도 몰랐던터라, 수 많은 번뇌와 좌절속에서 방황을 했던 그때를 생각하면 한숨만 나온다.
 현재 PASSWORD()는 OLD_PASSWORD()와 PASSWORD()로 나누어져있다. MySQL 4.1이전에 사용하던 PASSWORD() 함수가 OLD_PASSWORD()란 함수로 이름이 바꼈고 새로운 놈이 PASSWORD() 함수가 된것이다.



1. OLD_PASSWORD() 구현하기
 - 방황 끝에 찾에낸것은 sql/password.c 소스였다. 이 소스에 보면 아래처럼 구현되어 있다.
/*
    Generate binary hash from raw text string 
    Used for Pre-4.1 password handling
  SYNOPSIS
    hash_password()
    result       OUT store hash in this location
    password     IN  plain text password to build hash
    password_len IN  password length (password may be not null-terminated)
*/

void hash_password(ulong *result, const char *password, uint password_len)
{
  register ulong nr=1345345333L, add=7, nr2=0x12345671L;
  ulong tmp;
  const char *password_end= password + password_len;
  for (; password < password_end; password++)
  {
    if (*password == ' ' || *password == '\t')
      continue;                                 /* skip space in password */
    tmp= (ulong) (uchar) *password;
    nr^= (((nr & 63)+add)*tmp)+ (nr << 8);
    nr2+=(nr2 << 8) ^ nr;
    add+=tmp;
  }
  result[0]=nr & (((ulong) 1L << 31) -1L); /* Don't use sign bit (str2int) */;
  result[1]=nr2 & (((ulong) 1L << 31) -1L);
}

 - 이것을 자바로 구현해보자.
	/**
	 * <p>MySQL 의 OLD_PASSWORD() 함수.</p>
	 * 
	 * <pre>
	 * MySqlFunction.oldPassword(null)                    = null
	 * MySqlFunction.oldPassword("mypassword".getBytes()) = 162eebfb6477e5d3
	 * </pre> 
	 * 
	 * @param input
	 * @return
	 */
	public static String oldPassword(byte[] input) {
		if (input == null || input.length <= 0) {
			return null;
		}
		long nr = 1345345333L;
		long add = 7;
		long nr2 = 0x12345671L;
		
		for (int i = 0; i < input.length; i++) {
			if (input[i] == ' ' || 	input[i] == '\t') {
				continue;
			}
			nr ^= (((nr & 63) + add) * input[i]) + (nr << 8);

			nr2 += (nr2 << 8) ^ nr;

			add += input[i];
		}
		
		nr = nr & 0x7FFFFFFFL;
		nr2 = nr2 & 0x7FFFFFFFL;

		StringBuilder sb = new StringBuilder(16);

		sb.append(Long.toString((nr & 0xF0000000) >> 28, 16));
		sb.append(Long.toString((nr & 0xF000000) >> 24, 16));
		sb.append(Long.toString((nr & 0xF00000) >> 20, 16));
		sb.append(Long.toString((nr & 0xF0000) >> 16, 16));
		sb.append(Long.toString((nr & 0xF000) >> 12, 16));
		sb.append(Long.toString((nr & 0xF00) >> 8, 16));
		sb.append(Long.toString((nr & 0xF0) >> 4, 16));
		sb.append(Long.toString((nr & 0x0F), 16));

		sb.append(Long.toString((nr2 & 0xF0000000) >> 28, 16));
		sb.append(Long.toString((nr2 & 0xF000000) >> 24, 16));
		sb.append(Long.toString((nr2 & 0xF00000) >> 20, 16));
		sb.append(Long.toString((nr2 & 0xF0000) >> 16, 16));
		sb.append(Long.toString((nr2 & 0xF000) >> 12, 16));
		sb.append(Long.toString((nr2 & 0xF00) >> 8, 16));
		sb.append(Long.toString((nr2 & 0xF0) >> 4, 16));
		sb.append(Long.toString((nr2 & 0x0F), 16));

		return sb.toString();
	}
	
	/**
	 * <p>MySQL 의 OLD_PASSWORD() 함수.</p>
	 *  
	 * <pre>
	 * MySqlFunction.oldPassword(null)         = null
	 * MySqlFunction.oldPassword("mypassword") = "162eebfb6477e5d3"
	 * </pre> 
	 * 
	 * @param input
	 * @return
	 */
	public static String oldPassword(String input) {
		if (input == null) {
			return null;
		}
		return oldPassword(input.getBytes());
	}
	
	/**
	 * <p>MySQL 의 OLD_PASSWORD() 함수.</p>
	 * 
	 * <pre>
	 * MySqlFunction.oldPassword(null, *)                    = null
	 * MySqlFunction.oldPassword("mypassword", "ISO-8859-1") = "162eebfb6477e5d3"
	 * </pre> 
	 * 
	 * @param input
	 * @param charsetName
	 * @return
	 * @throws UnsupportedEncodingException
	 */
	public static String oldPassword(String input, String charsetName) throws UnsupportedEncodingException {
		if (input == null) {
			return null;
		}
		return oldPassword(input.getBytes(charsetName));
	}

 - 테스트 해보자
	@Test
	public void testOldPasswordString() {
		Assert.assertEquals(MySqlFunction.oldPassword("mypass"), "6f8c114b58f2ce9e");
		Assert.assertEquals(MySqlFunction.oldPassword("passwordtest"), "071e82b12678dc44");
	}
  잘 작동하는것을 확인할 수 있다. (그런데 문자열을 한글로 넘기면 엉뚱한 값이 계산된다. 필자의 내공으로는 고칠수 가 없다. 그러니 혹시 아시는분은 연락바란다. ^^;)

2. PASSWORD() 구현하기
 - 새로 만들어진 PASSWORD() 함수는 구현하기가 한결 간단하다. 왜나면 SHA-1을 사용해서 구현되어있기때문에, 자바의 SHA-1을 사용하면 간단히 해결된다.
	/**
	 * <p>입력한 데이터(바이트 배열)을 SHA1 알고리즘으로 처리하여 해쉬값을 도출한다.</p>
	 * 
	 * <pre>
	 * getHash([0x68, 0x61, 0x6e]) = [0x4f, 0xf6, 0x15, 0x25, 0x34, 0x69, 0x98, 0x99, 0x32, 0x53, 0x2e, 0x92, 0x60, 0x06, 0xae, 0x5c, 0x99, 0x5e, 0x5d, 0xd6]
	 * </pre>
	 * 
	 * @param input 입력 데이터(<code>null</code>이면 안된다.)
	 * @return 해쉬값
	 */
    public static byte[] getHash(byte[] input) {
        try {
				MessageDigest md = MessageDigest.getInstance("SHA1");
				return md.digest(input);
			} catch (NoSuchAlgorithmException e) {
				// 일어날 경우가 없다고 보지만 만약을 위해 Exception 발생
				throw new RuntimeException("SHA1" + " Algorithm Not Found", e);
			}
    }
    
	/**
	 * <p>MySQL 의 PASSWORD() 함수.</p>
	 * 
	 * <pre>
	 * MySqlFunction.password(null)                    = null
	 * MySqlFunction.password("mypassword".getBytes()) = "*FABE5482D5AADF36D028AC443D117BE1180B9725"
	 * </pre> 
	 * 
	 * @param input
	 * @return
	 */
	public static String password(byte[] input)  {
		byte[] digest = null;
		
		// Stage 1
		digest = getHash(input);
		// Stage 2
		digest = getHash(digest);

		StringBuilder sb = new StringBuilder(1 + digest.length);
		sb.append("*");
		sb.append(ByteUtils.toHexString(digest).toUpperCase());
		return sb.toString();
	}
	
	/**
	 * <p>MySQL 의 PASSWORD() 함수.</p>
	 * 
	 * <pre>
	 * MySqlFunction.password(null)         = null
	 * MySqlFunction.password("mypassword") = "*FABE5482D5AADF36D028AC443D117BE1180B9725"
	 * </pre> 
	 * 
	 * @param input
	 * @return
	 * @throws NoSuchAlgorithmException
	 */
	public static String password(String input) {
		if (input == null) {
			return null;
		}
		return password(input.getBytes());
	}
	
	/**
	 * <p>MySQL 의 PASSWORD() 함수.</p>
	 * 
	 * <pre>
	 * MySqlFunction.password(null, *)                    = null
	 * MySqlFunction.password("mypassword", "ISO-8859-1") = "*FABE5482D5AADF36D028AC443D117BE1180B9725"
	 * </pre> 
	 * 
	 * @param input
	 * @param charsetName
	 * @return
	 * @throws UnsupportedEncodingException
	 */
	public static String password(String input, String charsetName) throws UnsupportedEncodingException {
		if (input == null) {
			return null;
		}
		return password(input.getBytes(charsetName));
	}
  별거없다. SHA1을 두번 사용해서 출약메시지를 만들다음 16진수 문자열로 변환하여 앞에다 "*"를 붙여준것뿐이다.
 
- 테스트 해보자
	@Test
	public void testPasswordString() {
		Assert.assertEquals(MySqlFunction.password("mypass"), "*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF4");
		Assert.assertEquals(MySqlFunction.password("passwordtest"), "*A76A397AE758994B641D5C456139B88F40610926");
	}

 잘 작동하는것을 확인할 수 있다. 이 password(String) 메소드는 알맞는 charset을 지정해주면 한글도 잘 처리한다.
 이 SHA1(Secure Hash Algorithm) 해쉬 알고리즘은 226비트보다 작은 길이의 입력메시지에 대해서 축약 메시지(Message Digest)라 불리는 160비트 길이의 고정된값을 출력하는 단방향성 함수이다. 보통 메시지의 무결성 검사나 인증등의 업무에서 사용된다. MD5보다는 다소 느리지만, 좀 더 안정하다고 한다.

3. 비밀번호 알아내기
 - OLD_PASSWORD(), PASSWORD() 함수는 단방향성 함수이다. 즉, 결과값을 가지고 원본값을 만들어낼 수 없다는것이다. 그렇다면 비밀번호를 알아낼 수 없을까? 불행히도 우리에겐 무차별 대입법(brute force)이른 무서운 놈이 있다. 즉, 모든 조합의 문자를 단방향함수를 통해서 결과값을 만들어낸 다음, 그 결과값을 비교하면 되는것이다. 이 방법의 최대 단점은 시간과의 싸움이라는것인데, OLD_PASSWORD() 같은 경우 4-5자리 정도는 현재의 PC로도 몇분안에 풀어버릴 수가 있다. 한번 해볼까? 무식한 무차별 대입법이므로, 소스도 무식하게 짜보자. 비밀번호로 사용되는 문자는 0-9, a-z으로 한정하겠다.
	public static void main(String[] args) {
		String target = "552151cf55e12624";
		byte[] charArray = " 1234567890abcdefghijklmopqrstuvwxyz".getBytes();
		byte[] password = new byte[6];
		int len = charArray.length;
		for (int i = 0 ; i < len; i++) {
			password[0] = charArray[i];
			for (int j = 0 ; j < len; j++) {
				password[1] = charArray[j];
				for (int k = 0 ; k < len; k++) {
					password[2] = charArray[k];
					for (int l = 0 ; l < len; l++) {
						password[3] = charArray[l];
						for (int m = 0 ; m < len; m++) {
							password[4] = charArray[m];
							for (int n = 0 ; n < len; n++) {
								password[5] = charArray[n];
								if (target.equals(oldPassword(password))) {
									System.out.println("비밀번호는 \"" + new String(password).trim() + "\" 입니다.");
									System.exit(0);
								}
							}
						}
					}
				}
			}
		}
	}
* 실행 결과
비밀번호는 "good" 입니다.

4자리는 순식간에 나오며,  5-6자리도 몇분정도 소요되지만 나온다. 자리수가 더 많아지면 시간은 엄청나가 증가한다.
이런식으로 비밀번호를 저장하는 방식일 경우 비밀번호의 허용가능문자열과 자리수를 많이 늘리면 무식한 무차별대입법이 거의 무용지물이된다.(시간이 너무 오래걸리므로..) 그러니 비밀번호를 입력받을때, 최소 6자리이상은 받자.