Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- Freemaker
- JCE
- ACAP
- Java
- date
- String
- RSA
- 암호학
- Executors
- mac
- 한글조사처리
- xlet
- Executor
- Postman
- ORM
- Callable
- Log4J
- DAMO
- 한글조사
- Runnable
- IPTV
- 자바 암호화
- sha1
- PKCS
- AES
- Instrumentation
- 이클립스 플러그인 개발
- PKCS#8
- StringUtils
- 자바
Archives
- Today
- Total
오늘은 어디로 갈까...
MySQL PASSWORD() 함수를 자바로 구현하기 본문
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 소스였다. 이 소스에 보면 아래처럼 구현되어 있다.
- 이것을 자바로 구현해보자.
- 테스트 해보자
2. PASSWORD() 구현하기
- 새로 만들어진 PASSWORD() 함수는 구현하기가 한결 간단하다. 왜나면 SHA-1을 사용해서 구현되어있기때문에, 자바의 SHA-1을 사용하면 간단히 해결된다.
- 테스트 해보자
잘 작동하는것을 확인할 수 있다. 이 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으로 한정하겠다.
비밀번호는 "good" 입니다.
4자리는 순식간에 나오며, 5-6자리도 몇분정도 소요되지만 나온다. 자리수가 더 많아지면 시간은 엄청나가 증가한다.
이런식으로 비밀번호를 저장하는 방식일 경우 비밀번호의 허용가능문자열과 자리수를 많이 늘리면 무식한 무차별대입법이 거의 무용지물이된다.(시간이 너무 오래걸리므로..) 그러니 비밀번호를 입력받을때, 최소 6자리이상은 받자.
예전에 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자리이상은 받자.