암호학 & 개발 실무 IETF RFC 6238 · 12분 독서 시간

RFC 6238 TOTP 알고리즘 원리 및 코드 구현: Python, JavaScript, Go 실전

수학적 원리부터 실제 구동 가능한 소스 코드까지: Base32 디코딩, HMAC-SHA1 해싱, 동적 절단(Dynamic Truncation) 바이트 연산의 모든 것을 완벽하게 마스터합니다.

2FA
2FA-Tool 보안 엔지니어링 팀
2026년 10월 발행 · IETF 테스트 벡터 수학적 검증 완료
💡 핵심 요약 (Executive Summary)

TOTP(Time-Based One-Time Password)는 클라이언트와 서버가 사전에 공유한 대칭 비밀키 $K$와 현재 Unix 시간만을 이용해 30초마다 독립적으로 6자리 숫자를 계산하는 비동기 암호화 알고리즘입니다. 네트워크 통신 없이도 양측의 계산 결과가 100% 일치하며, 그 계산 파이프라인은 [Base32 디코딩] → [시간 윈도우 $T$ 계산] → [HMAC-SHA1 해싱] → [동적 절단 4바이트 추출] → [Modulo $10^6$]의 정확한 5단계로 구성됩니다.

1 TOTP 암호화 모델 개요: HOTP에서 TOTP로의 발전

RFC 6238 표준은 본질적으로 카운터 기반 알고리즘인 RFC 4226(HOTP)의 확장입니다. HOTP는 버튼을 누를 때마다 1씩 증가하는 카운터 $C$를 사용하지만, TOTP는 이 $C$를 시간 기반 수식으로 대체합니다:

TOTP = HOTP(K, T), 여기서 T = floor((CurrentTime - T0) / X)

기본 설정에서 $T_0 = 0$ (1970-01-01 00:00:00 UTC)이며, 시간 스텝 간격 $X = 30$초입니다. 따라서 현재 시각이 속한 30초 구간 내에서는 스마트폰과 인증 서버가 정확히 동일한 정수 카운터 $T$를 산출하게 됩니다.

2 1단계: Base32 비밀키 바이트 디코딩

거래소나 Google 계정에서 발급하는 비밀키(예: JBSWY3DPEHPK3PXP)는 RFC 4648 표준의 Base32 인코딩 문자열입니다. 암호화 해시에 전달하기 위해서는 먼저 공백과 하이픈을 제거하고 패딩(=)을 맞춘 후 원시 바이너리 바이트 배열로 디코딩해야 합니다.

입력 키: "JBSWY3DPEHPK3PXP" (16자 Base32) → 디코딩 결과: 10바이트 원시 바이너리 (Hex: 48656c6c6f21deadbeef...)

3 2단계: 시간 스텝(Time-step Counter T) 유도 및 8바이트 빅엔디언 패킹

현재 Unix 타임스탬프를 30으로 나눈 정수 몫 $T$를 계산합니다. 중요한 점은 RFC 규격상 이 64비트 정수를 반드시 8바이트 빅엔디언(Big-Endian) 바이너리로 패킹해야 한다는 것입니다.

예: 타임스탬프 1700000000 → T = 56666666 → 8바이트 Hex: 00 00 00 00 03 60 C8 2A

4 3단계: HMAC-SHA1 해시 다이제스트 연산

디코딩된 키 $K$를 비밀키로, 8바이트 시간 바이너리를 메시지로 사용하여 HMAC-SHA1 해시를 수행합니다. 그 결과로 정확히 20바이트(160비트)의 고유한 해시 다이제스트가 생성됩니다.

5 4단계: 동적 절단(Dynamic Truncation)을 통한 4바이트 정수 추출

20바이트의 해시 결과에서 6자리 코드를 추출하는 핵심 마법이 바로 동적 절단(Dynamic Truncation)입니다:

  1. 오프셋(Offset) 결정: 해시의 마지막 20번째 바이트(인덱스 19)의 하위 4비트(& 0x0F)를 읽어 0~15 범위의 오프셋을 구합니다.
  2. 4바이트 슬라이싱: 해시의 offset 위치부터 연속된 4바이트를 빅엔디언 32비트 정수로 읽어옵니다.
  3. 부호 비트 마스킹: 부호 있는 정수 연산에서의 혼선을 방지하기 위해 최상위 비트(MSB)를 0으로 마스킹합니다 (& 0x7FFFFFFF).

6 5단계: 6자리 모듈로(Modulo) 연산 및 완성

추출된 31비트 양의 정수를 $10^6$(1,000,000)으로 나눈 나머지를 구하고, 앞자리가 비어 있는 경우 0으로 패딩하여 6자리 문자열(예: 048192)을 완성합니다.

7 실전 실행 코드: Python 3 완전 구현 예제

import base64
import hashlib
import hmac
import struct
import time

def generate_totp(secret_base32: str, timestamp: int = None, step: int = 30, digits: int = 6) -> str: """RFC 6238 표준 완벽 준수 Python TOTP 구현""" if timestamp is None: timestamp = int(time.time())

1. Base32 정규화 및 바이트 디코딩

clean_secret = secret_base32.replace(” ”, "").upper() pad = (8 - len(clean_secret) % 8) % 8 key_bytes = base64.b32decode(clean_secret + (”=” * pad))

2. 8바이트 빅엔디언 타임스탬프 카운터 패킹

t = timestamp // step time_bytes = struct.pack(“>Q”, t)

3. HMAC-SHA1 연산

hmac_digest = hmac.new(key_bytes, time_bytes, hashlib.sha1).digest()

4. 동적 절단 (Dynamic Truncation)

offset = hmac_digest[-1] & 0x0F code_int = struct.unpack(“>I”, hmac_digest[offset : offset + 4])[0] & 0x7FFFFFFF

5. 모듈로 연산 및 6자리 포맷팅

otp = code_int % (10 ** digits) return str(otp).zfill(digits)

테스트 실행

if name == “main”: test_key = “JBSWY3DPEHPK3PXP” print(f”현재 TOTP 인증코드: {generate_totp(test_key)}”)

8 실전 실행 코드: JavaScript (Web Crypto API 로컬 브라우저)

async function generateTotpJS(base32Secret, timeSec = Math.floor(Date.now() / 1000)) {
// 1. Base32 디코더
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
let bits = "", keyBytes = [];
const clean = base32Secret.replace(/\s/g, '').toUpperCase();
for (let c of clean) {
let val = alphabet.indexOf(c);
if (val !== -1) bits += val.toString(2).padStart(5, '0');
}
for (let i = 0; i + 8 <= bits.length; i += 8) {
keyBytes.push(parseInt(bits.substr(i, 8), 2));
}

// 2. 시간 카운터 8바이트 버퍼 const t = Math.floor(timeSec / 30); const timeBuf = new ArrayBuffer(8); new DataView(timeBuf).setBigUint64(0, BigInt(t), false);

// 3. 브라우저 내장 Web Crypto API로 HMAC-SHA1 계산 const cryptoKey = await window.crypto.subtle.importKey( “raw”, new Uint8Array(keyBytes), { name: “HMAC”, hash: “SHA-1” }, false, [“sign”] ); const signature = await window.crypto.subtle.sign(“HMAC”, cryptoKey, timeBuf); const hash = new Uint8Array(signature);

// 4. 동적 절단 const offset = hash[hash.length - 1] & 0x0F; const view = new DataView(signature, offset, 4); const binary = view.getUint32(0, false) & 0x7FFFFFFF;

// 5. 6자리 반환 return String(binary % 1000000).padStart(6, ‘0’); }

9 실전 실행 코드: Go 언어(Golang) 고성능 백엔드 구현

package main

import ( “crypto/hmac” “crypto/sha1” “encoding/base32” “encoding/binary” “fmt” “strings” “time” )

func GenerateTOTP(secret string, t time.Time) (string, error) { key, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(strings.ToUpper(strings.ReplaceAll(secret, ” ”, ""))) if err != nil { return "", err } counter := uint64(t.Unix() / 30) buf := make([]byte, 8) binary.BigEndian.PutUint64(buf, counter)

mac := hmac.New(sha1.New, key) mac.Write(buf) hash := mac.Sum(nil)

offset := hash[len(hash)-1] & 0x0F code := binary.BigEndian.Uint32(hash[offset:offset+4]) & 0x7FFFFFFF return fmt.Sprintf(“%06d”, code%1000000), nil }

10 RFC 6238 공식 부록 B 테스트 벡터 검증

구현의 정확성을 검증하기 위해 RFC 6238 명세서 Appendix B에 명시된 테스트 시크릿 키 12345678901234567890(ASCII, Base32: GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ)와 타임스탬프를 대조 검증합니다:

Unix 타임스탬프 UTC 시각 공식 기대 TOTP 코드
59 1970-01-01 00:00:59 287082
1111111109 2005-03-18 01:58:29 081804
1234567890 2009-02-13 23:31:30 005924
2000000000 2033-05-18 03:33:20 279037

❓ 자주 묻는 질문 (FAQ)

SHA-1에 충돌 취약점이 존재하는데 왜 TOTP는 여전히 HMAC-SHA1을 사용하나요? ▼

일반 SHA-1 해시 함수에 대한 충돌 공격은 HMAC(RFC 2104) 구조에 적용되지 않습니다. HMAC은 내부 및 외부 키를 이용한 이중 해싱 샌드위치 구조로 설계되어 충돌 공격을 원천 무력화합니다. 현재까지도 HMAC-SHA1은 TOTP에 있어 수학적으로 완전히 안전합니다.

시간대나 서머타임이 TOTP 코드 생성에 영향을 미치나요? ▼

전혀 영향을 주지 않습니다. Unix 타임스탬프는 1970년 1월 1일 00:00:00 UTC 기준 경과 초 단위 정수이므로 시간대나 서머타임과 완벽히 독립적입니다. 전 세계 어느 기기에서든 동일한 절대 시각에는 완전히 동일한 Unix 타임스탬프가 생성됩니다.

2FA
2FA-Tool 보안 엔지니어링 팀

응용 암호학, 클라이언트 사이드 보안 아키텍처 및 IETF RFC 표준을 전문으로 연구합니다. 모든 구현체는 수학적 테스트 벡터 검증을 거칩니다.