Time-based OTP 인증

Time-based One-time Password Algorithm

작동 방식

초기 등록시 seed값을 서버와 사용자의 기기에 각각 보관한다.

초기 등록

초기 등록시에는 서버에서 생성한 UUID값을 모바일 기기에서 받기 위해 네트워크 연결이 필요하다.

앱 → 서버 : 앱에서 사용자의 정보를 입력하고 서버로 전송한다

서버 → 앱 : 전송받은 정보를 데이터베이스에 저장하고 랜덤 값을 하나 생성하여 병사 고유 값(Seed 값)으로 저장하고 응답으로 Seed 값을 넘겨준다

앱 : 받은 Seed 값을 디바이스에 저장한다.

이때 Seed 값은 앱, 서버 두 곳에서 보유한다.

반납 과정

이때부터는 네트워크 연결 필요 없이 보유하고 있는 seed값과 현재 시간만을 이용하여 Time-based OTP값을 생성한다.

앱 : 반납 버튼을 누르면 위에 Seed값과 현재 시간 값을 기반으로 TOTP 값을 생성한다.

앱 : TOTP 값과 사용자 정보를 이용하여 QR코드를 생성한다.

앱 → 라즈 → 서버 : QR코드를 인식해 앱에서 생성한 TOTP 값과 사용자 정보를 서버로 전송한다

서버 : 서버에서 앱에서 받은 TOTP 값과, 서버에 저장되어 있는 병사 정보 중 Seed값을 불러와 현재 시간으로 TOTP 생성을 한다. 만약 앱에서 생성한 TOTP값과 서버에서 생성한 TOTP값이 동일하면 인증 완료.

위의 과정에서, 앱에서 TOTP 생성한 시간과 웹에서 TOTP 생성한 시간이 다를 수 있지만 TOTP 생성 과정에서 키가 유지되는 시간을 지정할 수 있기 때문에 문제를 해결할 수 있다.

테스트

간단하게 seed값에 따라 TOTP 값을 반환하는 API를 만들었다. 아래의 seed/ 경로 뒤에 40자리 임의의 숫자(seed 값)을 넣으면 된다.

https://osam.riyenas.dev/api/totp/generate/seed/{seed value}

예시

https://osam.riyenas.dev/api/totp/generate/seed/0123456789012345678901234567890123456789

새로고침을 할 때마다 키 값을 생성하는데 키 유지 시간은 10초로 설정하였다 따라서 10초 마다 TOTP 값이 변경되는 걸 볼 수 있다.

또한 시드 값만 서로 보유하고 있으면 네트워크 연결 없이도 인증을 진행할 수 있다.

시연 영상

Source Code

TOTP.class

package dev.riyenas.osam.domain.auth;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.lang.reflect.UndeclaredThrowableException;
import java.math.BigInteger;
import java.security.GeneralSecurityException;

public class TimeBasedOTP {

    private TimeBasedOTP() {}

    private static final int[] DIGITS_POWER = {1,10,100,1000,10000,100000,1000000,10000000,100000000};

    private static byte[] hmac_sha1(String crypto, byte[] keyBytes, byte[] text) {
        try {
            Mac hmac;
            hmac = Mac.getInstance(crypto);
            SecretKeySpec macKey =
                    new SecretKeySpec(keyBytes, "RAW");
            hmac.init(macKey);
            return hmac.doFinal(text);
        } catch (GeneralSecurityException gse) {
            throw new UndeclaredThrowableException(gse);
        }
    }

    private static byte[] hexStrToBytes(String hex) {
        byte[] bArray = new BigInteger("10" + hex, 16).toByteArray();

        byte[] ret = new byte[bArray.length - 1];

        for (int i = 0; i < ret.length ; i++)
            ret[i] = bArray[i+1];

        return ret;
    }

    public static String calcSteps(Long time, Long T0, Long X) {
        long T = (time - T0) / X;

        String steps = "0";
        steps = Long.toHexString(T).toUpperCase();

        while (steps.length() < 16) {
            steps = "0" + steps;
        }

        return steps;
    }

    public static String generateTOTP(String key, String time, String returnDigits) {
        return generateTOTP(key, time, returnDigits, CryptoType.HmacSHA1);
    }

    public static String generateTOTP256(String key, String time, String returnDigits) {
        return generateTOTP(key, time, returnDigits, CryptoType.HmacSHA256);
    }

    public static String generateTOTP512(String key, String time, String returnDigits) {
        return generateTOTP(key, time, returnDigits, CryptoType.HmacSHA512);
    }

    public static String generateTOTP(String key, String time, String returnDigits, CryptoType type) {
        int codeDigits = Integer.decode(returnDigits).intValue();
        String result = null;
        byte[] hash;

        while(time.length() < 16 ) {
            time = "0" + time;
        }

        byte[] msg = hexStrToBytes(time);
        byte[] k = hexStrToBytes(key);

        hash = hmac_sha1(type.toString(), k, msg);

        int offset = hash[hash.length - 1] & 0xf;

        int binary = ((hash[offset] & 0x7f) << 24) |
                ((hash[offset + 1] & 0xff) << 16) |
                ((hash[offset + 2] & 0xff) << 8) |
                (hash[offset + 3] & 0xff);

        int otp = binary % DIGITS_POWER[codeDigits];

        result = Integer.toString(otp);

        while (result.length() < codeDigits) {
            result = "0" + result;
        }

        return result;
    }
}

CryptoType.class

package dev.riyenas.osam.domain.auth;

public enum CryptoType {
    HmacSHA1("HmacSHA1"),
    HmacSHA256("HmacSHA256"),
    HmacSHA512("HMacSHA512");

    CryptoType(String crypto) {
        this.crypto = crypto;
    }

    private final String crypto;

    public String toString() {
        return crypto;
    }
}

사용 예시

Calendar time = Calendar.getInstance();

String steps = TimeBasedOTP.calcSteps(time.getTimeInMillis() / 1000, 0L, 10L);
//현재 시간, 0L(초깃값), 10L(10초까지 TOTP 키 유지 시간)

return TimeBasedOTP.generateTOTP(device.getUuid(), steps, "8", CryptoType.HmacSHA512);
//seed(시드값), steps(위에서 계산한 값), TOTP 출력 자리수, 암호화 방식
//이대로면 8자리 TOTP값이 반환됩니다.

Last updated

Was this helpful?