# Time-based OTP 인증

## 작동 방식

![](/files/-MKqqW407q9SsPhIRARP)

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

### 초기 등록

{% hint style="info" %}
초기 등록시에는 서버에서 생성한  UUID값을 모바일 기기에서 받기 위해 네트워크 연결이 필요하다.
{% endhint %}

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

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

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

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

### 반납 과정&#x20;

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

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

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

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

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

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

![](/files/-MKqq2Mj6pCoL-L7UKDa)

## 테스트

간단하게 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 값이 변경되는 걸 볼 수 있다.

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

### 시연 영상

{% embed url="<https://youtu.be/JxOJ6xShL1s>" %}

## Source Code

### TOTP.class

```java
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

```java
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;
    }
}
```

### 사용 예시

```java
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값이 반환됩니다.
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://kookmoban.gitbook.io/osam/technical-note/time-based-otp.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
