TOTP 동작 원리
한 줄 요약
TOTP = 사용자와 서버가 공유한 비밀(secret)과 현재 시간을 입력으로, 30초마다 6자리 일회용 코드를 생성하는 인증 방식. RFC 6238 표준.
Google Authenticator, Authy, 1Password, iOS의 코드 자동 채우기 — 전부 이 표준의 동일한 알고리즘을 쓴다.
왜 OTP인가
비밀번호만으로는 부족한 이유:
- 유출 시 영구적으로 위험 (재사용·재설정 전까지)
- 피싱·키로깅에 그대로 노출
OTP는 30초마다 새 코드 → 한 번 노출되어도 30초 안에 써야만 의미 → 재사용 공격 무력화.
등록 (Setup) 플로우
sequenceDiagram
participant U as User
participant S as Server
participant A as Authenticator
U->>S: TOTP 활성화 요청
S->>S: 랜덤 시크릿 K 생성 (160bit)
S->>S: K를 사용자 계정에 임시 저장
S-->>U: QR 코드 (otpauth URI)
U->>A: QR 스캔
A->>A: K 저장
S->>U: 첫 코드 입력 요청
A-->>U: 6자리 코드 표시
U->>S: 코드 입력
S->>S: 같은 K, 현재시간으로 코드 재계산
alt 일치
S-->>U: TOTP 활성화 완료, 백업코드 발급
else 불일치
S-->>U: 재시도 요청
end
핵심: 첫 코드 검증을 통과하기 전엔 K를 영구 저장하지 않는다. 스캔 실수·시계 오차 등으로 등록이 실패한 채 K가 박혀버리면 사용자가 영영 들어올 수 없게 됨.
인증 (Verify) 플로우
sequenceDiagram
autonumber
participant A as "Authenticator 앱"
participant U as "사용자"
participant S as "서버"
Note over A: T = floor(now / 30)
A->>A: HMAC-SHA1(K, T)
A->>A: Truncate → 6자리
A->>U: "847263" 표시
U->>S: 비밀번호 + "847263"
S->>S: 같은 알고리즘으로 코드 생성
Note over S: ±1 윈도우도 함께 검사
alt 일치
S->>U: 로그인 성공
else 불일치
S->>U: 인증 실패
end
알고리즘
T = floor((unix_time - T0) / X)
T0 = 0 (1970-01-01)
X = 30 (초)
HS = HMAC-SHA1(K, T) ; 20바이트 결과
offset = HS[19] & 0x0F ; 마지막 바이트의 하위 4비트
binary = (HS[offset] & 0x7F) << 24
| (HS[offset+1] & 0xFF) << 16
| (HS[offset+2] & 0xFF) << 8
| (HS[offset+3] & 0xFF)
OTP = binary mod 10^6 ; 6자리
offset이 매번 결과의 마지막 바이트로 결정되니까 같은 K라도 시간이 바뀌면 추출 위치도 바뀐다 → 통계적으로 균등.
시간 동기화 — 가장 흔한 사고
flowchart LR
A[사용자 디바이스 시계] -->|±10초| B[Authenticator 코드]
C[서버 시계] -->|±10초| D[검증]
B -.예상치 못한 큰 차이.-> E[검증 실패]
D -.->|±1 윈도우| F[관용 처리]
style E fill:#fdd
서버는 보통 T-1, T, T+1 세 개를 모두 검증해서 30~60초 정도의 시계 오차를 흡수한다. 그 이상 어긋나면 사용자에게 디바이스 시간 자동 동기화를 안내해야 함.
보안 모델
flowchart TD
X["공격자"]
C1["30초 안에 사용"]
C2["만료"]
C3["모든 코드 생성"]
C4["실시간 릴레이"]
R1["TOTP 무용"]
R2["코드 릴레이로 즉시 우회"]
X -->|"코드 1개 절취"| C1
X -->|"30초 후"| C2
X -->|"시크릿 K 절취"| C3
X -->|"중간자 공격"| C4
C3 --> R1
C4 --> R2
classDef bad fill:#fcc,stroke:#d99,color:#600
class C3,R1,C4,R2 bad
요약:
- 재사용 공격: TOTP가 막아준다 ✅
- 시크릿 유출: TOTP는 무력 → DB 컬럼 암호화 + 마스터키 분리 보관 필수
- 실시간 피싱(릴레이): TOTP만으로는 못 막음 → FIDO2/WebAuthn(원점 검증)이 진정한 해법
구현 시 체크리스트
- 시크릿 컬럼은 AES-GCM 암호화 후 저장 (그린홈 PhoneCrypto 패턴 그대로)
- 마스터 암호화 키는 DB 밖에 (env 또는 KMS)
- 코드 비교는 timing-safe (
String == String금지, 상수시간 비교 함수 사용) - 같은 코드의 재사용 차단 (한 번 검증 성공한
T값은 짧은 캐시에 기록) - 등록 시 백업 코드 8~10개 즉시 발급 (사용자가 안전한 곳에 저장)
- 레이트 리미터 (IP·계정 단위 5회/분 정도)
otpauth URI 형식 (QR로 인코딩되는 것)
otpauth://totp/<issuer>:<account>
?secret=<base32>
&issuer=<issuer>
&algorithm=SHA1
&digits=6
&period=30
예:
otpauth://totp/pyo-site:[email protected]?secret=JBSWY3DPEHPK3PXP&issuer=pyo-site&algorithm=SHA1&digits=6&period=30
base32 secret은 알파벳 A-Z + 2-7만 사용 — 사람이 받아 적기 좋게.
관련 글
- Resize 테스트 — 이 글의 다이어그램들도 그 슬라이더 시스템과 같이 미리보기에서 작동
- NotYetLinkedConcept — 미존재 위키링크 데모 (빨간 점선)
이 글은 pyo-site의 Wiki test 위키링크 + Mermaid 다이어그램 +  이미지 사이즈 조절 기능까지 통합 검증용 샘플.