일회용 비밀번호 구현

11486 단어
2단계 인증으로 로그인 시 일회용 비밀번호(숫자 6자리)를 입력해야 할 수 있습니다.
어떻게 구현하는지 궁금해서 구현해봤습니다.
이번 글에서는 구글 OTP를 이용하여 쉽게 생성할 수 있는 TOTP(Time-Based One-Time Password)에 대해 설명하겠습니다.

암호



이 코드를 사용하여 쉽게 2단계 인증을 시도할 수 있습니다.
https://github.com/ksrnnb/otp

사양


TOTP



TOTP는 RFC6238에서 다음과 같이 정의된다.

HOTP(HMAC-Based One-Time Password)에 대해서는 후술한다.

TOTP = HOTP(K, T)
T = (Current Unix time - T0) / X

K: a key to generate HOTP
T: an integer and represents the number of time steps between the initial counter time T0 and the current Unix time.
T0: the Unix time to start counting time steps (default value is 0)
X: the time step in seconds (default value X = 30 seconds)


Golang 코드는 다음과 같습니다.

// TOTP time step
const timeStepSecond int64 = 30

func New(secret []byte) string {
    return hotp.New(secret, counter())
}

func counter() uint64 {
    return uint64(time.Now().Unix() / timeStepSecond)
}


뜨거운



HOTP는 RFC4226에서 다음과 같이 정의된다.

HMAC-SHA-1은 비밀 키 K와 카운터 C에서 계산된 다음 잘립니다.
TOTP의 경우 카운터는 T = (Current Unix time - T0) / X로 계산됩니다.

HOTP(K,C) = Truncate(HMAC-SHA-1(K,C))


여기서는 Trancate 기능에 대해 설명하겠습니다.

1 단계



먼저 간단히 HMAC-SHA-1을 계산합니다. 이 결과는 20바이트입니다.

HS = HMAC-SHA-1(K,C)


Golang gcode는 다음과 같습니다.
이진 패키지는 숫자에서 []바이트로 변환하는 데 사용됩니다.

package hotp

import (
    "crypto/hmac"
    "crypto/sha1"
    "encoding/binary"
)

func hmacSha1(secret []byte, counter uint64) []byte {
    mac := hmac.New(sha1.New, secret)

    // uint64 => 8 byte
    byteCounter := make([]byte, 8)
    binary.BigEndian.PutUint64(byteCounter, counter)

    mac.Write(byteCounter)
    return mac.Sum(nil)
}


2 단계



4바이트 문자열을 생성하고 HMAC-SHA-1 값에서 숫자로 변환합니다.
4바이트를 생성하는 방법은 RFC4226에 작성되어 있습니다.

DT(String) // String = String[0]...String[19]
   Let OffsetBits be the low-order 4 bits of String[19]
   Offset = StToNum(OffsetBits) // 0 <= OffSet <= 15
   Let P = String[OffSet]...String[OffSet+3]
   Return the Last 31 bits of P


Golang 코드는 다음과 같습니다.
하위 4비트의 오프셋은 후행 문자와 0xf의 논리적 곱으로 계산됩니다.
그러면 offset ~ offset+3에서 마지막 31비트를 얻는다.

이 예제는 사양처럼 작성되었지만 binary.BigEndian.Uint32(hs[offset:offset+4]) & 0x7fffffff 대체하는 것이 이해하기 쉬울 수 있습니다.

func dynamicTruncate(hs []byte) uint32 {
    // get low-order 4 bits of hs[tail]
    // 0xf => 0000 1111
    offset := hs[len(hs)-1] & 0xf

    // get last 31 bits for hs[offset]...hs[offset + 3]
    // 0x7F => 0111 1111
    return uint32(hs[offset]&0x7f)<<24 |
        uint32(hs[offset+1]&0xff)<<16 |
        uint32(hs[offset+2]&0xff)<<8 |
        uint32(hs[offset+3]&0xff)
}


3단계



d를 일회용 비밀번호의 자릿수라고 하고, 2단계에서 구한 값을 10^d로 나눈 나머지가 HOTP 값이다.

Golang 코드는 다음과 같습니다.

num := dynamicTruncate(hs)
codeNum := num % uint32(math.Pow10(digits))

f := fmt.Sprintf("%%0%dd", digits)
oneTimePassword := fmt.Sprintf(f, code)


예시



Google OTP 설정



여기서는 일회용 비밀번호를 사용해 보도록 하겠습니다. ( README )

먼저 Google Authenticator를 설치하고 QR 코드를 스캔합니다.

QR 코드에서 문자열 값otpauth://totp/otp_example?secret=NBSWY3DP을 가져올 수 있습니다. 이 형식은 [Google OTP 키 URI 형식]에 정의되어 있습니다.( https://github.com/google/google-authenticator/wiki/Key-Uri-Format )

여기서 NBSWY3DP는 인코딩된 값 hello입니다. 일회용 비밀번호를 사용하는 경우 각 사용자마다 고유하고 임의의 문자열을 지정해야 합니다.



서버 실행


make run 를 실행하여 서버를 실행합니다.

로그인



localhost:8080에 접속하여 아이디/비밀번호로 로그인합니다.




이름



ID
호게호게

비밀번호
호게호게


그런 다음 일회용 비밀번호가 필요하며 Google Authenticator에서 생성된 6자리 숫자를 입력해야 합니다.



이 값과 서버에서 생성된 값을 확인한 후 로그인할 수 있습니다.

자세한 세부 사항


  • 한 번 사용한 일회용 암호는 다시 사용할 수 없습니다.
  • 1단계 이전에 일회용 암호를 사용할 수도 있습니다. (네트워크 대기 시간 고려)

  • The validation system should compare OTPs not only with the receiving timestamp but also the past timestamps that are within the transmission delay.



    기타



    TOTP는 예를 들어 재동기화와 같이 더 자세히 정의되지만 간단하기 때문에 생략합니다.
    더 자세히 알고 싶다면 참고 문헌을 읽는 것이 좋습니다.

    참조


  • RFC4226
  • RFC6238
  • Google Authenticator Key Uri Format
  • 좋은 웹페이지 즐겨찾기