프로젝트 기능 중 PC 2차 비밀번호를 위해 모바일 기능으로 OTP를 도입하려고 하였다.
구글에 otp를 검색하면 대표적으로 google Authenticator를 모방한 코드가 가장 많이 나오는데
실제 google github에 공식적으로 등록되어있는 코드와 다르기도 하고 사용하기는 더 편하다
가볍게 사용하시는 분들을 위해 코드 리뷰를 하고자 한다.
우선 OTP는 One-Time password로 일회용 비밀번호이다. 사용을 많이 해보신 분들은 로그인시 2차 인증기능으로 많이 사용해 봤을 것이다. 먼저 계정 로그인을 한 뒤에 핸드폰으로 번호를 확인하고 컴퓨터에 입력하는게 그것이다.
Google Authenticator는 시간 기반 일회용 비밀번호 알고리즘(TOTP) 와 HMAC 기반 일회용 비밀번호 알고리즘 (HOTP) 두가지를 만족한다.
시간 기반 일회용 비밀번호는 매 시간마다 다른 코드를 생성한다고 이해하면 쉽고,
HMAC 기반 일회용 비밀번호 알고리즘은 Hashed Meessage Authentication Code 방식을 사용하여 키를 생성한다 라고 이해하면 된다.
즉 생성한 코드는 "시간을 기반으로 하여 HMAC방식으로 생성" 이라고 쉽게 생각하자.
통상적으로 사용자 관점에서 OTP의 흐름은 다음과 같다.
- 사용자는 PC에 1차 로그인을 한 뒤 2차비밀번호(OTP)를 입력해야 한다.
- 핸드폰으로 OTP를 생성한다.
- 눈으로 보고 PC에 손으로 입력한다.
이 때 모바일에서 OTP생성 시에는 인터넷 연결이 필요없이 로컬환경에서 가능하다는 점과,
기존에 OTP를 생성하기 위한 키를 교환해야 한다는 특징이 있다.
코드는 크게 두 가지 함수로 구분이 되는데
- OTP를 생성하는 VerifyCode
- OTP를 검증하는 CheckCode
생성하는 함수인데 왜 이름이 Verify? 라고 생각할 수 있지만 이유는 뒤쪽에 말하도록 하겠다.
1. VerifyCode
public static int VerifyCode(String key, long time) throws NoSuchAlgorithmException, InvalidKeyException {
byte[] data = new byte[8];
//1초 단위로 otp 갱신
long value = time / 1000;
for (int i = 8; i-- > 0; value >>>= 8) {
data[i] = (byte) value;
}
SecretKey signKey = new SecretKeySpec(key.getBytes(), "HmacSHA1");
Mac mac = Mac.getInstance("HmacSHA1");
mac.init(signKey);
byte[] hash = mac.doFinal(data);
int offset = hash[20 - 1] & 0xF;
long truncatedHash = 0;
for (int i = 0; i < 4; i++) {
truncatedHash <<= 8;
truncatedHash |= (hash[offset + i] & 0xFF);
}
truncatedHash &= 0x7FFFFFFF;
//생성된 truncatedHash를 100 000 ~ 999 999 숫자로 표기
truncatedHash %= 900000;
truncatedHash += 100000;
return (int) truncatedHash;
}
먼저 OTP를 생성하는 VerifyCode이다. 변수로는 OTP를 생성하기 위한 key값과 현재 시간 time이 필요하다.
OTP를 1초 단위로 갱신하기 위해 time 입력받은 뒤에 / 1000을 하였고 HMAC과정을 거쳐 truncatedHash를 생성한다.
이후 % 900000 , + 100000을 통해 100,000 ~ 999,999 숫자를 가지는 OTP를 반환하게 만들었다.
2. CheckCode
public static boolean CheckCode(String user_otp, String key) throws Exception {
long otpnum = Integer.parseInt(user_otp);
long time = new Date().getTime();
boolean result = false;
//유효 시간 설정 : 1000 * 60 = 60초
int window = 1000 * 60;
//현재 시각 기준 -60초 ~ 1초
for (int i = -window; i <= 1000; i += 1000) {
long hash = VerifyCode(key, time + i);
if (hash == otpnum)
result = true;
}
return result;
}
검증하는 코드 내부에 VerifyCode라 이름 붙인 OTP 생성 함수가 보인다.
입력 받은 코드가 유효한지 검증하는 방법이 개발자가 설정한 유효시간을 모두 확인하면서 입력 받은 OTP 코드의 유효성을 판단한다.
동일한 코드를 생성 했다면 유효한 OTP가 되는 것이고, 없다면 유효하지 않은 OTP가 되는 것이다.
입력받은 user_otp를 long 타입으로 바꾸어 otpnum 변수에 넣고
for 반복문에서 -60000 ~ 1000 까지 i 값을 1000단위로 VerifyCode를 실행하면서 반환값 hash가 otpnum과 동일한지 확인하고 있다.
이것은 VerifyCode에서 말했듯이 OTP코드를 1초단위로 갱신하기 때문이다. OTP 코드의 생성주기를 변경하고 싶다면 VerifyCode의 value와 i 의 증감값을 수정해야 한다.
또한 발급한 OTP의 유효시간을 바꾸기 위해서는 window 값을 조정하면 되고(1000 = 1초) 나의 경우에는 서버시간과 안드로이드 시간이 약간의 편차가 있어 i <= 1000으로 작성하였다.
실제 코드 사용 흐름은
- 기존 PC와 모바일 사이에서 키값을 교환한다.
- 프론트 엔드에서 로그인시 2차 비밀번호를 요구한다.
- 모바일에서 VerifyCode를 통해 OTP를 생성하고 사용자는 프론트 엔드에 타이핑 한다.
- 프론트 엔드에서 서버에 OTP코드를 전송한다.
- 서버에서는 전송받은 코드를 CheckCode를 통해 validation을 판단한다.
많은 분들이 OTP 사용 경험을 가지고 있을거라고 생각하기 때문에 개념은 어렵다고 생각하지 않는다.
하지만 내가 어려웠던 점은 OTP를 단순히 시간을 기준으로 생성하는 것이 아니라 키값과 적절한 암호화 과정을 거치고 싶었는데, 그 과정에서 google authenticator 를 찾았지만, 코드를 분석하고 키 값을 선정하는 등 외적인 요소에 시간 투자가 많이되었다.
google github에 가면 어? 할 정도로 많은 함수들이 서로 얽혀있어 이해가 어렵지만, 궁금하신 분들은 한번 보는 것을 추천한다.
Reference:
https://en.wikipedia.org/wiki/One-time_password