2018. 1. 7. 23:04
반응형

최근 비트코인, 이더리움, 리플 등의 가상화폐 거래소가 인기를 끌고 있다.

그리고 대부분의 사이트에서 보안을 위해 구글OTP 등의 2차인증을 이용한다.

최근 구글OTP 연동을 해야할 일이 생겨서 검색을 통해 방법을 찾았다.

처음에는 API 문서같은게 잘 없어서 당황스러웠다.


참고사이트 : http://zero-gravity.tistory.com/221

위의 소스코드를 거의 그대로 이용하여 그냥 내가 알아보기 쉽게 정리한 것에 불과하다.


generate를 통해 키값, QR코드 주소를 생성한다.

생성된 키값은 회원목록을 저장하는 데이터베이스에 저장하는 것이 좋을 것 같다.

otp 검증은 checkCode를 통해서 이루어진다.

위에서 생성한 QR코드를 통해 구글OTP앱에 등록을 하고 표시되는 번호와 키값을 매개변수로 넣어서 검증한다.


참고로 테스트를 해봤는데 이미 지난 코드를 넣어도 통과하는 것을 볼 수 있다.

대략 1~2분 전에 생성된 코드도 입력이 가능하다는 얘기다.

2분정도 지나면 안된다.


OTP 앱

안드로이드: https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=ko

iOS: https://itunes.apple.com/kr/app/google-authenticator/id388497605?mt=8


import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.Random; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import org.apache.commons.codec.binary.Base32; public class GoogleOTP { public static void main(String[] args) { GoogleOTP otp = new GoogleOTP(); HashMap<String, String> map = otp.generate("name", "host"); String otpkey = map.get("encodedKey"); String url = map.get("url"); System.out.println(otpkey); // 아래의 결과는 당연히 압도적인 확률로 false가 나온다. // 우선 위의 과정으로 생성된 키/url을 otp앱에 등록하고나서 표시되는 번호와 생성된 키를 넣어주면 true가 나올 것이다. boolean check = otp.checkCode("123123", otpkey); System.out.println(check); } public HashMap<String, String> generate(String userName, String hostName) { HashMap<String, String> map = new HashMap<String, String>(); byte[] buffer = new byte[5 + 5 * 5]; new Random().nextBytes(buffer); Base32 codec = new Base32(); byte[] secretKey = Arrays.copyOf(buffer, 10); byte[] bEncodedKey = codec.encode(secretKey); String encodedKey = new String(bEncodedKey); String url = getQRBarcodeURL(userName, hostName, encodedKey); // Google OTP 앱에 userName@hostName 으로 저장됨 // key를 입력하거나 생성된 QR코드를 바코드 스캔하여 등록 map.put("encodedKey", encodedKey); map.put("url", url); return map; } public boolean checkCode(String userCode, String otpkey) { long otpnum = Integer.parseInt(userCode); // Google OTP 앱에 표시되는 6자리 숫자 long wave = new Date().getTime() / 30000; // Google OTP의 주기는 30초 boolean result = false; try { Base32 codec = new Base32(); byte[] decodedKey = codec.decode(otpkey); int window = 3; for (int i = -window; i <= window; ++i) { long hash = verify_code(decodedKey, wave + i); if (hash == otpnum) result = true; } } catch (InvalidKeyException | NoSuchAlgorithmException e) { e.printStackTrace(); } return result; } 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; } public static String getQRBarcodeURL(String user, String host, String secret) { // QR코드 주소 생성 String format2 = "http://chart.apis.google.com/chart?cht=qr&chs=200x200&chl=otpauth://totp/%s@%s%%3Fsecret%%3D%s&chld=H|0"; return String.format(format2, user, host, secret); } }

반응형