티스토리 뷰

공장 (factory)/- Programming..

[Java] Google Authenticator(Google OTP)를 이용한 개발.

공부하는 나부랭이, 무중력고기 2014.06.10 20:30

   OTP 기능을 구현하라는 미션이 떨어졌고, 힌트로는 구글OTP라는 것이 있다라는 것만 받았다.


   찾아보니 거의 다 "Google Authenticator"라는 앱을 다운받아서 구글 로그인을 할 때에 이용하는 내용이었다.


   뭔가 구글에서 제공하는 API가 있어야 구글앱을 이용해서 개발을 할 수 있을 텐데, 눈을 씻고 찾아봐도 API는 없었다.


   찾다찾다 구글앱의 공식 홈페이지에서 파일들을 다운로드 할 수 있는 곳을 찾았는데, C언어로 되어있고 내가 원하는 것은 아니었다. 아마도 SSH로 접속해서 이걸 설치하고 로그인을 할 때에 사용하는 그런 종류인 듯 싶다.(이곳 참고)


   알고리즘을 중심으로 찾아본 결과, 아마도 IETF에 있는 RFC6238이라는 문서를 기반으로 구글앱이 이와 같은 알고리즘으로 구현을 해놓은 것 같다.


   따져보니 이것과 똑같은 알고리즘으로 Key/바코드를 만들어내고 그것을 검증하는 코드를 짜면 완벽하게 구글앱을 사용하는 OTP 기능을 만들어낼 수 있을 것 같았다.


   다른 사람들이 만들어놓은 자바 코드를 찾지 못했을 경우, 최악의 경우엔 정말 그렇게 해보려고 했다.


   국내에선 파이썬 코드를 쉽게 찾을 수 있었지만 자바가 아니어서 아무 쓸모가 없었다.


   지푸라기라도 잡는 심정으로 온갖 검색어 조합을 동원하여 외국 사이트를 이 잡듯이 뒤진 결과,


   다행히 누군가 자바스크립트로 구현해놓은 코드를 발견했다.


   미칠듯이 기뻤다. 속으로 감동의 눈물을 흘릴 정도였다. 그러나 의존하는 라이브러리 파일들을 제대로 다 링크해주고, 코드도 이대로 했음에도 불구하고 어떤 연유에서인지 내가 테스트하는 프로젝트에서는 작동하지 않았다. 


   이내 포기하고 다른 코드를 찾아 헤맸는데, 정말 다행스럽게도 자바로 구현한 코드를 찾았냈다!!!


   그런데 이것도 코드에서 sercretSize, numOfScratchCodes, scratchCodeSize를 몇으로 설정해줘야 하는지 별 말도 없고 그닥 친절하지 않아서 적용에 실패했다.


   더 찾아보니 이 코드를 기반으로 복잡하게 만들어놓은 코드를 발견했는데, 이건 더 머리가 터질 것 같았다. 하루 반 동안 붙잡고 하다가 GG.


   결국 다시 간단한 자바 코드로 돌아가서 붙잡고 요리조리 해본 결과, 성공!!!!


   서론이 길었다.;;


   아무튼 총 4일 동안 끙끙대서 성공한 결과물을 아래에 설명하고자 한다. 비록 내가 처음부터 날코딩한 것은 아니지만, OTP를 구현하려 나처럼 인터넷 망망대해를 떠돌고 있을 불쌍한 중생들을 위해 올려놓는다.




   먼저 OTP란 무엇이고 어떻게 돌아가는지부터 알고 들어가야 하니 간단하게 짚고 넘어가겠다.



   1. OTP란?


   One Time Password의 약자로, 우리말로 하면 일회용 비밀번호라 할 수 있다. 일회성이라는 특징 때문에 일반 비밀번호 입력이나 공인인증서 이용보다도 더 안전한 방법으로 알려져있다. 주로 금융권이나 일반 웹사이트 2차 로그인 인증으로 많이 활용되고 있다. OTP의 종류에는 원리에 따라 S/KEY방식, 시간 동기화 방식, 챌린지 응답 방식, 이벤트 동기화 방식 등이 있다. (좀더 자세한 내용을 알고 싶다면, 위키백과 참조) 여기서는 이중 시간 동기화 방식을 사용한 Google Authenticator라는 앱으로 TOTP 인증을 구현해보도록 하겠다.




   2. TOTP((Time-based One Time Password)의 원리


   많은 사람들이 오해하는 게 있는데(나 또한 그랬다), OTP 기기와 서버가 통신하는 줄 착각한다. 실상은 그렇지 않다. OTP 기기와 서버는 모두 같은 알고리즘을 바탕으로 하기 때문에 통신이 필요치 않다. 원리는 간단하다. 서버쪽에서 해당 알고리즘으로 Key나 바코드 주소를 생성해주면 그걸 OTP 기기에 입력해준다. 그러면 기기에서는 그 Key나 바코드를 기준으로 하여 30~60초 마다 계속하여 새로운 일회용 비밀번호를 생성해낸다. 그 일회용 비밀번호를 입력하여 서버로 전송하면 서버에서 그 비밀번호가 맞는지 알고리즘으로 확인하는 방식이다. 여기서 구글앱을 이용한다는 것은 OTP 기기 대신에 스마트폰에 있는 Google Authenticator라는 앱으로 대체한다는 것이다.


   뭔가 복잡해보이지만, 이해하고나면 쉽다.




   3. Java로 구현해보기.


   자 그럼 Java로 직접해보자. 아래는 JSP를 이용해서 웹사이트에 구현하다는 가정 하에 진행하였다.


   먼저 commons-codec.jar 파일을 라이브러리에 추가해준다.



commons-codec-1.9.jar



   또 만약 자바 버전이 1.6 이하일 경우에는 아래의 파일을 다운받아서 라이브러리 추가해준다.



security.jar



   그리고 자신의 스마트폰에 Google Authenticator 앱을 다운받아 설치해놓는다.

   


   다음은 실제 코드다.


   2차 인증 로그인 리퀘스트가 들어올 시, 사용자명과 계정명을 받아서 키를 생성할 부분.


import java.io.IOException;
import java.util.Arrays;
import java.util.Random;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.codec.binary.Base32;

public class OtpServlet extends HttpServlet {

	@Override
	protected void service(HttpServletRequest req, HttpServletResponse res)
			throws ServletException, IOException {
		
		// Allocating the buffer
//		byte[] buffer = new byte[secretSize + numOfScratchCodes * scratchCodeSize];
		byte[] buffer = new byte[5 + 5 * 5];
		
		// Filling the buffer with random numbers.
		// Notice: you want to reuse the same random generator
		// while generating larger random number sequences.
		new Random().nextBytes(buffer);

		// Getting the key and converting it to Base32
		Base32 codec = new Base32();
//		byte[] secretKey = Arrays.copyOf(buffer, secretSize);
		byte[] secretKey = Arrays.copyOf(buffer, 5);
		byte[] bEncodedKey = codec.encode(secretKey);
		
		// 생성된 Key!
		String encodedKey = new String(bEncodedKey);
		
		System.out.println("encodedKey : " + encodedKey);
		
//		String url = getQRBarcodeURL(userName, hostName, secretKeyStr);
		// userName과 hostName은 변수로 받아서 넣어야 하지만, 여기선 테스트를 위해 하드코딩 해줬다.
		String url = getQRBarcodeURL("hj", "company.com", encodedKey); // 생성된 바코드 주소!
		System.out.println("URL : " + url);
		
		String view = "/WEB-INF/view/otpTest.jsp";
		
		req.setAttribute("encodedKey", encodedKey);
		req.setAttribute("url", url);
		
		req.getRequestDispatcher(view).forward(req, res);
		
	}
	
	public static String getQRBarcodeURL(String user, String host, String secret) {
		String format = "http://chart.apis.google.com/chart?cht=qr&chs=300x300&chl=otpauth://totp/%s@%s%%3Fsecret%%3D%s&chld=H|0";
		
		return String.format(format, user, host, secret);
	}
	
}




   otpTest.jsp

당신의 키는 → ${encodedKey } 입니다. 
당신의 바코드 주소는 → ${url } 입니다.

code :




   키나 바코드를 이용해 구글앱에서 항목을 추가한 뒤, 거기서 생성되는 일회용 비밀번호를 code 부분에 써주고 전송을 클릭한다.



   code 부분에 저 숫자를 넣어주면 된다.




   전송 리퀘스트를 받는 부분에서 해당 코드가 맞는지 여부를 검사한다.


import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Date;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.codec.binary.Base32;

public class OtpResultServlet extends HttpServlet {

	@Override
	protected void service(HttpServletRequest req, HttpServletResponse res)
			throws ServletException, IOException {
		
		String user_codeStr = req.getParameter("user_code");
		long user_code = Integer.parseInt(user_codeStr);
		String encodedKey = req.getParameter("encodedKey");
		long l = new Date().getTime();
		long ll =  l / 30000;
		
		boolean check_code = false;
		try {
			// 키, 코드, 시간으로 일회용 비밀번호가 맞는지 일치 여부 확인. 
			check_code = check_code(encodedKey, user_code, ll);
		} catch (InvalidKeyException e) {
			e.printStackTrace();
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		}
		
		// 일치한다면 true.
		System.out.println("check_code : " + check_code);
		
	}

	private static boolean check_code(String secret, long code, long t) throws NoSuchAlgorithmException, InvalidKeyException {
		Base32 codec = new Base32();
		byte[] decodedKey = codec.decode(secret);

		// Window is used to check codes generated in the near past.
		// You can use this value to tune how far you're willing to go.
		int window = 3;
		for (int i = -window; i <= window; ++i) {
			long hash = verify_code(decodedKey, t + i);

			if (hash == code) {
				return true;
			}
		}

		// The validation code is invalid.
		return false;
	}
	
	private static int verify_code(byte[] key, long t)
			throws NoSuchAlgorithmException, InvalidKeyException {
		byte[] data = new byte[8];
		long value = t;
		for (int i = 8; i-- > 0; value >>>= 8) {
			data[i] = (byte) value;
		}

		SecretKeySpec signKey = new SecretKeySpec(key, "HmacSHA1");
		Mac mac = Mac.getInstance("HmacSHA1");
		mac.init(signKey);
		byte[] hash = mac.doFinal(data);

		int offset = hash[20 - 1] & 0xF;

		// We're using a long because Java hasn't got unsigned int.
		long truncatedHash = 0;
		for (int i = 0; i < 4; ++i) {
			truncatedHash <<= 8;
			// We are dealing with signed bytes:
			// we just keep the first byte.
			truncatedHash |= (hash[offset + i] & 0xFF);
		}

		truncatedHash &= 0x7FFFFFFF;
		truncatedHash %= 1000000;

		return (int) truncatedHash;
	}
	
}


   시간이 흘러서 일회용 비밀번호가 계속 바뀌어도 바뀐 일회용 비밀번호를 입력하면 완벽하게 true를 뱉어냄을 확인할 수 있다.


   지금까지 구글앱을 이용한 OTP 기능을 어떻게 구현하는지 알아봤다.


   사실 구글앱을 이용하는 방법 말고도 오픈소스를 이용한다든지, 돈으로 떼우는 방법 등등..  방법은 많다.


   아무쪼록 구글앱을 이용한 OTP 기능 개발을 찾아헤매는 사람들에게 도움이 되었길 바란다.



저작자 표시 비영리 변경 금지
신고
댓글
  • BlogIcon 시라솔 고맙습니다. 많은도움이 되었습니다. 2014.07.13 23:01 신고
  • ezmo 감사합니다. 많은 도움 되었습니다. 한가지만 여쭤볼께요. Google OTP를 사용하지 않고, 저 코드.. 예제에서 보여주신 415309를 클라이언트쪽에서 생성하려면 어떻게 해야 하나요? 2014.12.19 16:53 신고
  • BlogIcon 공부하는 나부랭이, 무중력고기 RFC6238과 같은 알고리즘으로 OTP를 생성하는 프로그램을 만들어야겠죠? 2014.12.22 09:23 신고
  • 안녕하세요 궁금한게 있습니다. 제가 구글 otp 어플을 받아놓고 사용중인데요. 본문에 적혀있는점은 otp는 '서버와의 통신이 이루어 지지 않는방식이다' 라고 말씀하셨는데 그럼 휴대폰 배터리에 영향을 크게 미치지는 않는다는 말씀이신가요? 현재 otp를 10개의 사이트에서 사용중이라 10개 모두 어플리케이션 안에서 실시간으로 동기화 되고 있는걸 보면 과연 그럴까 라는 의문점이 들어서요.. 2015.02.02 01:39 신고
  • BlogIcon 공부하는 나부랭이, 무중력고기 실시간은 맞지만 동기화는 아닙니다. 서버하고 통신하지 않습니다. 그냥 자체 알고리즘으로 돌아가는 겁니다. 애플리케이션에 OTP를 여러 개 등록해놓으면 그만큼 배터리는 줄겠죠. 하지만 서버하고의 통신 때문에 배터리가 소모되지는 않습니다. 2015.02.03 13:27 신고
  • BlogIcon 안녕하세요 고맙습니다. 좋은 정보 감사합니다. 2015.02.03 17:47 신고
  • Kiyacoke 구글 OTP를 구현 해보려고하니까 안드로이드 프로젝트를 만든 후에 실행 해보려하니 뭔가 계속 막히게 되는데, 혹시 샘플이 있으실까요.?
    꼭 구현 해 보고 싶은데 파라미터가 잘못된건지... 잘 모르겠어서 그런데 혹시 샘플프로젝트가 존재하시다면 kiyacoke@naver.com으로 부탁드립니다.
    2016.01.12 17:01 신고
  • BlogIcon 공부하는 나부랭이, 무중력고기 샘플은 저게 전부입니다. 2016.01.13 17:01 신고
  • Kiyacoke 마지막으로 하나 더 여쭤보고싶은게있는데.. 바코드를 생성하는 코드에서 만들어진 바코드 url로 웹페이지에 띄워보면
    400. That’s an error.

    Your client has issued a malformed or illegal request.

    The parameter 'chs=300×300' does not match the expected format.
    Invalid width or height.
    That’s all we know.
    이 에러가 나오는데, 추가적으로 따로 해줘야 하는 것이 있는건가요?
    2016.01.14 13:28 신고
  • BlogIcon MoelCano 좋은 정보 얻고 갑니다
    감사합니다~
    2016.01.15 12:08 신고
  • BlogIcon gofly 너무 너무 감사합니다.....^^
    글을을 좀 담아가도 될까요??
    2016.04.26 10:56 신고
  • BlogIcon 공부하는 나부랭이, 무중력고기 링크하시는 건 괜찮습니다 ^^ 2016.04.26 20:16 신고
  • 하루아루 덕분에 OTP를 구현하는데 많은 도움됐습니다~
    한 가지 궁금한게 생성하신 encodedKey값은 구글 앱의 키 길이값이 맞지 않더군요
    이 부분은 RFC6238에서 TOTP키값 생성하는 부분을 따로 적요해야 하는건가요?
    secretSize나 scratchCodeSize로 적용할 수 없는 부분인지 궁금합니다.
    2016.05.12 13:36 신고
  • 개발자 정말로 큰 도움이 되었습니다. 진심으로 감사드립니다. 2016.07.17 23:12 신고
  • DW 감사합니다 많은 도움이 될것같습니다. 2016.12.01 18:37 신고
  • 비밀댓글입니다 2017.01.09 00:20
  • karen 감사합니다 저에게 정말 중요한 자료입니다 큰 도움이 되었습니다. 2017.02.20 16:41 신고
  • 카라 안녕하세요. 잘 봤습니다.

    // String url = getQRBarcodeURL(userName, hostName, secretKeyStr);
    // userName과 hostName은 변수로 받아서 넣어야 하지만, 여기선 테스트를 위해 하드코딩 해줬다.
    String url = getQRBarcodeURL("hj", "company.com", encodedKey); // 생성된 바코드 주소!
    System.out.println("URL : " + url);

    여기서 처리한 내용의 hj / company.com 이것이 구글 OTP에서 처리하는 연관관계가 어떻게 되는건지 문의드립니다.
    [생성된 바코드 주소! ] 라고 주석이 달려 있어서 그 부분도 궁금합니다.
    부디 부탁드립니다.

    sis555@nate.com
    2017.08.10 11:23 신고
  • 형필 큰 도움이 되었습니다 감사합니다!! 2017.09.11 14:59 신고
  • 곰돌이 java main 부분이 없는데 어떻게 만드나요 ㅠㅠㅠ 한번 돌려서 실행해보고
    싶은데
    2017.09.18 19:19 신고
  • 비밀댓글입니다 2017.09.18 19:22
댓글쓰기 폼