티스토리 뷰

개인정보보안이 강화되는 요즘 2차 보안 인증을 요구하는 시스템이 증가하고 있습니다. 

OTP(One Time Password)는 고정된 비밀번호가 아닌 무작위로 생성되는 일회용 비밀번호를 이용하는 본인 인증 수단입니다.​

개인정보에 접근하는 시스템 또는 금융권 등에서 많이 사용하고 있습니다. 


OTP 동작 방식에는 시간 동기화 방식, 첼린지 응답 방식, 이벤트 동기화 방식 등이 있습니다. 

대표적인 OTP 방식으로는 구글 OTP가 있습니다.  구글 OTP는 시간 동기화 방식(TOTP)과 HMAC 기반 일회용 비밀번호 알고리즘을 사용합니다. 

구글 OTP 적용

Server Side

1. Security Key 생성 

2. 바코드 생성

3. 화면 개발 : 보안 코드 입력 화면 개발 

4. 보안 코드 유효성 체크 개발 

Client Side 

5. 구글 OTP 앱 설치 및 이용 

 

구글 검색을 통해 많은 관련 소스를 볼 수 있습니다.

저는 아래 사이트를 참고해서 적용했습니다. 

 

참고 사이트  medium.com/@ihorsokolyk/two-factor-authentication-with-java-and-google-authenticator-9d7ea15ffee6

 

Two-Factor Authentication with Java and Google Authenticator

I am more than sure that each of you have at least one account with enabled Two-Factor Authentication (2FA). But if you are still…

medium.com

아래는 위의 사이트를 참고해서 샘플로 만들어 본 소스입니다. 

시큐어리티 코드를 개별로 생성하고 개별로 바코드를 생성한 후 각각을 DB 등에 넣고 관리할 수도 있습니다. 

저는 내부 시스템에서 사용하는 것으로 시스템 단위로 시큐어리티 코드를 만들고 쓰는 구조로 했습니다.  

일단 시큐어리티 키를 생성하고 바코드를 만든 후 이미지를 내부 사용자에게 이메일로 보낸 후 그 바코드로 OTP를 등록하라고 하였습니다. 

메일을 보낼 때 구글 PLAY에서 "구글 OTP"로 검색하고 해당 앱 설치와 바코드 등록에 대한 글을 함께 보내면 됩니다. 

먄약 시스템 단위가 아닌 개별로 만든다면 그에 맞게 처리하시면 될거 같습니다. 

 

샘플 코드 

build.gradle

plugins {
	id 'org.springframework.boot' version '2.4.3'
	id 'io.spring.dependency-management' version '1.0.11.RELEASE'
	id 'java'
	id 'war'
}

group = 'com.example.opt'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
    // 추가 
	implementation 'de.taimos:totp:1.0'
    // 추가 
	implementation 'commons-codec:commons-codec:1.10'
    // 추가 
	implementation 'com.google.zxing:javase:3.2.1'

    providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

test {
	useJUnitPlatform()
}

Security Key/barcode Generator 

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.SecureRandom;

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

public class TOTPTokenGenerator {
    
    private TOTPTokenGenerator() {
        //throw new RuntimeException("TOTPTokenUtil");
    }

    private static String GOOGLE_URL = "https://www.google.com/chart?chs=200x200&chld=M|0&cht=qr&chl=";
    
    // Security Key 생성 
    public static String generateSecretKey() {
        SecureRandom random = new SecureRandom();
        byte[] bytes = new byte[20];
        random.nextBytes(bytes);
        Base32 base32 = new Base32();
        return base32.encodeToString(bytes);
    }
    
    // barcode 생성 
    public static String getGoogleAuthenticatorBarCode(String secretKey, String account, String issuer) {
        try {
            return GOOGLE_URL+"otpauth://totp/"
                    + URLEncoder.encode(issuer + ":" + account, "UTF-8").replace("+", "%20")
                    + "?secret=" + URLEncoder.encode(secretKey, "UTF-8").replace("+", "%20")
                    + "&issuer=" + URLEncoder.encode(issuer, "UTF-8").replace("+", "%20");
        } catch (UnsupportedEncodingException e) {
            throw new IllegalStateException(e);
        }
    }
    
}

위의 코드로 시큐어리티 키와 바코드를 url을 생성합니다. 

TOTP 코드입니다. 제가 만든게 아니고 아래 github 소스로 참고하시라고 붙여 놓습니다. 

github.com/taimos/totp/blob/master/src/main/java/de/taimos/totp/TOTP.java

import java.lang.reflect.UndeclaredThrowableException;
import java.math.BigInteger;
import java.security.GeneralSecurityException;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

/**
 *  github : https://github.com/taimos/totp/blob/master/src/main/java/de/taimos/totp/TOTP.java
 * Implementation of TOTP: Time-based One-time Password Algorithm
 *
 * @author thoeger
 */
public final class TOTP {
    
    private TOTP() {
        // private utility class constructor
    }

    /**
     * @param key - secret credential key (HEX)
     * @return the OTP
     */
    public static String getOTP(String key) {
        return TOTP.getOTP(TOTP.getStep(), key);
    }

    /**
     * @param key - secret credential key (HEX)
     * @param otp - OTP to validate
     * @return valid?
     */
    public static boolean validate(final String key, final String otp) {
        return TOTP.validate(TOTP.getStep(), key, otp);
    }

    private static boolean validate(final long step, final String key, final String otp) {
        return TOTP.getOTP(step, key).equals(otp) || TOTP.getOTP(step - 1, key).equals(otp);
    }

    private static long getStep() {
        // 30 seconds StepSize (ID TOTP)
        return System.currentTimeMillis() / 30000;
    }

    private static String getOTP(final long step, final String key) {
        String steps = Long.toHexString(step).toUpperCase();
        while (steps.length() < 16) {
            steps = "0" + steps;
        }

        // Get the HEX in a Byte[]
        final byte[] msg = TOTP.hexStr2Bytes(steps);
        final byte[] k = TOTP.hexStr2Bytes(key);

        final byte[] hash = TOTP.hmac_sha1(k, msg);

        // put selected bytes into result int
        final int offset = hash[hash.length - 1] & 0xf;
        final int binary = ((hash[offset] & 0x7f) << 24) | ((hash[offset + 1] & 0xff) << 16) | ((hash[offset + 2] & 0xff) << 8) | (hash[offset + 3] & 0xff);
        final int otp = binary % 1000000;

        String result = Integer.toString(otp);
        while (result.length() < 6) {
            result = "0" + result;
        }
        return result;
    }

    /**
     * This method converts HEX string to Byte[]
     *
     * @param hex the HEX string
     *
     * @return A byte array
     */
    private static byte[] hexStr2Bytes(final String hex) {
        // Adding one byte to get the right conversion
        // values starting with "0" can be converted
        final byte[] bArray = new BigInteger("10" + hex, 16).toByteArray();
        final byte[] ret = new byte[bArray.length - 1];

        // Copy all the REAL bytes, not the "first"
        System.arraycopy(bArray, 1, ret, 0, ret.length);
        return ret;
    }

    /**
     * This method uses the JCE to provide the crypto algorithm. HMAC computes a Hashed Message Authentication Code with the crypto hash
     * algorithm as a parameter.
     *
     * @param crypto the crypto algorithm (HmacSHA1, HmacSHA256, HmacSHA512)
     * @param keyBytes the bytes to use for the HMAC key
     * @param text the message or text to be authenticated.
     */
    private static byte[] hmac_sha1(final byte[] keyBytes, final byte[] text) {
        try {
            final Mac hmac = Mac.getInstance("HmacSHA1");
            final SecretKeySpec macKey = new SecretKeySpec(keyBytes, "RAW");
            hmac.init(macKey);
            return hmac.doFinal(text);
        } catch (final GeneralSecurityException gse) {
            throw new UndeclaredThrowableException(gse);
        }
    }

}

코드 체크 소스 

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

public class TOTPTokenValidation {
    
    private static String secretKey = "임의의 생성한 키값";

    public static boolean validate(String inputCode) {
        String code = getTOTPCode();
        return code.equals(inputCode);
    }
    
    public static String getTOTPCode() {
        Base32 base32 = new Base32();
        byte[] bytes = base32.decode(TOTPTokenValidation.secretKey);
        String hexKey = Hex.encodeHexString(bytes);
        return TOTP.getOTP(hexKey);
    }

}

로그인 할때 otp 코드를 받으면 TOTPTokenValidation.validate() 함수로 체크를 하면 됩니다. 

 

샘플 Code 

import java.util.Scanner;

public class TestTOTPTokenGenerator {
    
    public static void main(String[] args) {
        generateSecurityKey();
        testToken();
    }
    
    private static void testToken() {
        Scanner scanner = new Scanner(System.in);
        String code = scanner.nextLine();
        if (TOTPTokenValidation.validate(code)) {
            System.out.println("Logged in successfully");
        } else {
            System.out.println("Invalid 2FA Code");
        }
    }

    private static void generateSecurityKey() {
        String secretKey = TOTPTokenGenerator.generateSecretKey();
        System.out.println(secretKey);
        String email = "Manger-Auth";
        String company = "FTK";
        String barcodeUrl = TOTPTokenGenerator.getGoogleAuthenticatorBarCode(secretKey, email, company); 
        System.out.println(barcodeUrl);
    }
}

개발 후기 

아침에 출근해서 OTP 한번 적용해 볼까라는 생각을 하고 검색을 해보니 참고할 사이트가 엄청 많이 나왔습니다. 

기본적인 개념을 이해하고 샘플을 만들어 보니 노력 대비 쉽게 구현이 되었습니다. 

개인정보보안 관련 이슈로 OTP 도입이 자주 언급이 되는데 간단하게 테스트하고 적용해 볼 만합니다.

위의 코드는 샘플이고 참고할 소스는 웹상에 많으니 보시고 적합한걸 찾아서 적용해 보세요. 

 

git hub : github.com/kkaok/examples/tree/master/googleOtp

참고  

ko.wikipedia.org/wiki/%EC%9D%BC%ED%9A%8C%EC%9A%A9_%EB%B9%84%EB%B0%80%EB%B2%88%ED%98%B8#%EC%8B%9C%EA%B0%84_%EB%8F%99%EA%B8%B0%ED%99%94_%EB%B0%A9%EC%8B%9D

 

일회용 비밀번호

위키백과, 우리 모두의 백과사전. 둘러보기로 가기 검색하러 가기 일회용 비밀번호(영어: One-Time Password; OTP, 문화어: 일회용 통행암호)는 일회성으로 사용하는 비밀번호이다. OTP는 주로 높은 수

ko.wikipedia.org

medium.com/@ihorsokolyk/two-factor-authentication-with-java-and-google-authenticator-9d7ea15ffee6

 

Two-Factor Authentication with Java and Google Authenticator

I am more than sure that each of you have at least one account with enabled Two-Factor Authentication (2FA). But if you are still…

medium.com

github.com/IhorSokolyk/google-2fa/blob/31199539eacc9c51422a28459862921c11202788/src/main/java/com/example/googleauth/Utils.java

 

IhorSokolyk/google-2fa

This example shows how to use 2fa with Google Authenticator in Java applications - IhorSokolyk/google-2fa

github.com

github.com/taimos/totp/blob/master/src/main/java/de/taimos/totp/TOTP.java

 

taimos/totp

TOTP lib for RFC 6238. Contribute to taimos/totp development by creating an account on GitHub.

github.com

 

댓글
  • 프로필사진 감사합니다. 자세한 포스팅 정말 감사합니다. TOTP 적용 프로젝트를 맡았는데 정말 큰 도움이 되었습니다.

    추가로, 포스팅 내용과 어느 정도 연관이 있는 부분에서 혹시나 저와 같은 것으로 헤메는 분 있을까봐 댓글 남깁니다.

    포스팅에 제시된 코드로 작성된 QR코드 주소의 경우 그대로 주소 창에 써서 접근하면 잘 나오지만 img 태그의 src에다가 넣으면 엑박으로 나오더군요.
    그런 경우에는 서버 주소만 https://www.google.com/~에서 https://chart.googleapis.com/~ 로 바꾸면 해결이 됩니다.

    다시 한 번 정말 감사드립니다.
    2021.07.23 18:30
  • 프로필사진 까오기 까오기 도움이 되었다니 다행이네요.
    추가 정보 감사합니다. ^^
    2021.07.30 17:37 신고
댓글쓰기 폼