TOTP 생성기, 6자리 번호는 이렇게 계산됩니다

구글 OTP나 Authy 같은 앱에 뜨는 6자리 숫자는 서버가 실시간으로 보내주는 값이 아닙니다. 내 기기가 비밀 키 하나와 현재 시각만으로 직접 계산해 낸 결과입니다. 그래서 비행기 모드에서도 번호가 나오고, 반대로 기기 시계가 어긋나면 번호가 틀립니다.
이 글은 그 6자리가 어떤 순서로 만들어지는지, QR 코드에는 무엇이 담기는지를 표준(RFC) 그대로 풀어냅니다. 어떤 인증 방식을 써야 한다는 판단이 아니라, 작동 원리를 검증 가능한 사실로 확인하려는 분을 위한 글입니다.
요약 ① Base32 비밀 키가 번호를 만드는 유일한 비밀 재료이고, 발급기관·계정은 앱 목록에 뜨는 이름표일 뿐 6자리 계산에는 들어가지 않습니다. ② 6자리는 "비밀 키(열쇠) + 현재 UTC 시각을 30초로 나눈 카운터(메시지)"를 HMAC-SHA1로 섞은 뒤, 그 결과를 잘라 1,000,000으로 나눈 나머지입니다. ③ 번호가 안 맞는 대부분의 원인은 기기 시계가 어긋났거나, Base32에 없는 문자(0·1·8·9 등)를 비밀 키에 넣은 경우입니다.
번호가 왜 안 맞는지 알 수 없을 때
새 휴대폰으로 바꾸면서 인증 앱을 옮기는 상황을 생각해 보겠습니다. QR 코드를 다시 띄울 수 없어 "설정 키를 직접 입력" 화면으로 들어가면, 앱은 비밀 키·발급 기관·계정 이름 세 칸을 물어봅니다.
이때 무엇을 어디에 넣어야 하는지, 셋 중 무엇이 실제로 번호를 바꾸는지 모르면 그냥 감으로 채우게 됩니다. 등록을 마쳤는데 "코드가 올바르지 않습니다"가 뜨면, 비밀 키가 틀린 건지 시계가 어긋난 건지 구분이 안 됩니다.
문제는 6자리가 만들어지는 과정이 앱 안에 감춰져 있다는 점입니다. 원리를 한 번 눈으로 확인해 두면, 어느 단계에서 틀렸는지 스스로 짚어낼 수 있습니다.
도구 없이 6자리를 직접 계산하는 표준 절차
TOTP는 공개된 표준입니다. 번호를 만드는 규칙이 RFC 문서에 그대로 적혀 있어서, 원한다면 손이나 명령어로도 같은 값을 재현할 수 있습니다.
세 입력값이 각각 무슨 뜻인가
먼저 앱이 물어보는 세 칸의 역할을 구분해야 합니다.
- Base32 비밀 키(Secret): 서버와 내 기기가 나눠 가진 원본 재료입니다. 번호 계산에 들어가는 유일한 비밀 값입니다.
- 발급 기관(Issuer): "Google", "GitHub"처럼 앱 목록에 표시되는 서비스 이름입니다.
- 계정(Account): 같은 서비스에 여러 계정이 있을 때 구분하는 이메일·아이디입니다.
핵심은 뒤의 두 값은 번호 계산에 전혀 관여하지 않는다는 점입니다. 이름표를 아무리 바꿔도 6자리는 그대로입니다.
6자리를 뽑아내는 다섯 단계
RFC 4226(HOTP)과 RFC 6238(TOTP)이 정한 순서입니다.
- Base32 디코딩: 비밀 키를 바이트로 되돌립니다. Base32(RFC 4648)는 A
Z와 27, 32글자만 쓰며 5비트씩 묶여 있어 8비트 단위로 다시 풉니다. - 시각을 카운터로: UTC 기준 1970년 1월 1일부터 흐른 초(유닉스 시각)를 30으로 나눈 몫이 카운터입니다. RFC 6238의 기본 간격이 30초입니다. 이 값을 8바이트로 적습니다.
- HMAC-SHA1: 비밀 키를 열쇠로, 8바이트 카운터를 메시지로 넣어 HMAC-SHA1을 계산하면 20바이트 해시가 나옵니다.
- 동적 절단(dynamic truncation): 20바이트 중 마지막 바이트의 하위 4비트로 시작 위치(0~15)를 정하고, 그 자리부터 4바이트를 꺼내 최상위 비트를 버려 31비트 정수로 만듭니다.
- 6자리로 자르기: 그 정수를 1,000,000으로 나눈 나머지가 6자리입니다. 앞이 비면 0으로 채웁니다.
같은 비밀 키와 같은 30초 구간이면 세계 어디서 계산하든 결과가 같습니다. 서버와 내 앱의 번호가 일치하는 이유가 여기 있습니다.
QR 코드에는 무엇이 담기나
QR 코드는 그림이 아니라 한 줄의 URI를 담은 것입니다. 형식은 이렇습니다.
otpauth://totp/발급기관:계정?secret=비밀키&issuer=발급기관
여기서 번호를 만드는 값은 secret 하나뿐입니다. 앞의 발급기관:계정과 issuer는 앱이 목록에 이름을 표시할 때 쓰는 라벨입니다. 그래서 QR을 스캔하는 것과 비밀 키를 직접 입력하는 것은 결과가 같습니다.
명령어로 검증하기
직접 재현해 보려면 오픈소스 명령줄 도구 oathtool(oath-toolkit)로 한 줄이면 됩니다.
oathtool --totp -b JBSWY3DPEHPK3PXP
-b는 비밀 키가 Base32라는 뜻이고, 출력값이 지금 이 순간의 6자리입니다. 30초 뒤 다시 실행하면 값이 바뀝니다. 파이썬의 hmac·hashlib 표준 라이브러리로도 같은 다섯 단계를 몇 줄로 짤 수 있습니다.
저희 도구로 직접 확인해 보겠습니다.
회원가입 없이 브라우저에서 Base32 비밀 키·QR 코드 생성과 30초 주기 6자리 토큰 계산을 바로 확인합니다

자주 헷갈리는 지점
"발급 기관이나 계정을 바꾸면 번호도 바뀌겠지." 아닙니다. 앞서 봤듯 6자리는 비밀 키와 시각만으로 정해집니다. 세 입력값의 역할을 다시 정리하면 이렇습니다.
| 입력값 | 역할 | 6자리에 영향 |
|---|---|---|
| Base32 비밀 키 | 계산의 유일한 비밀 재료(열쇠) | 예 |
| 발급 기관(Issuer) | 앱 목록에 뜨는 서비스 이름표 | 아니오 |
| 계정(Account) | 어느 계정인지 구분하는 라벨 | 아니오 |
"번호가 계속 틀려요." 가장 흔한 원인은 시각 어긋남입니다. 카운터는 기기의 UTC 시각을 30으로 나눠 만드는데, 폰 시계가 서버와 수십 초 이상 벌어지면 서로 다른 카운터로 다른 번호가 나옵니다. 검증 서버가 앞뒤 구간을 조금 허용하기도 하지만, 차이가 크면 결국 안 맞습니다.
"비밀 키를 넣었더니 오류가 떠요." Base32에 없는 문자를 넣은 경우입니다. Base32는 AZ와 27만 쓰기 때문에 0·1·8·9나 특수문자가 섞이면 디코딩 단계에서 막힙니다. 대소문자 차이나 공백은 대개 자동으로 정리되지만, 없는 글자는 되살릴 수 없습니다.
| 구분 | 문자 |
|---|---|
| Base32에서 쓰는 문자 | A |
| 쓰지 않는 문자(넣으면 오류) | 0, 1, 8, 9 및 특수문자 |
"왜 30초마다 바뀌나요." 카운터가 "시각 ÷ 30의 몫"이라서 그렇습니다. 30초 구간이 넘어가는 순간 몫이 1 늘고, 카운터가 바뀌니 HMAC 결과도, 6자리도 완전히 달라집니다. 화면의 게이지바는 지금 구간이 끝나기까지 남은 초를 보여줍니다.
정리
- 6자리는 서버가 보내는 게 아니라 계산되는 값입니다. 재료는 비밀 키와 현재 UTC 시각 두 가지뿐입니다.
- 표준 절차는 다섯 단계입니다. Base32 디코딩 → 시각을 30초 카운터로 → HMAC-SHA1 → 동적 절단 → 1,000,000으로 나눈 나머지.
- 번호를 바꾸는 건 비밀 키와 시각뿐입니다. 발급 기관·계정은 이름표라 계산에 들어가지 않습니다.
- QR 코드는
otpauth://URI 한 줄이고, 그 안에서 실제로 쓰이는 값은secret입니다. - 안 맞는 원인은 대개 둘입니다. 기기 시계 어긋남, 또는 Base32에 없는 문자.
아래 도구는 이 다섯 단계를 브라우저에서 실시간으로 돌려, 비밀 키를 넣는 순간 QR 코드와 6자리, 남은 시간을 함께 보여줍니다.
회원가입 없이 브라우저에서 Base32 비밀 키·QR 코드 생성과 30초 주기 6자리 토큰 계산을 바로 확인합니다
자주 묻는 질문
앱에 등록하지 않고 6자리만 확인할 수 있나요?
네. 올바른 Base32 비밀 키만 있으면 브라우저가 그 자리에서 HMAC-SHA1 계산을 수행해 30초 주기의 6자리를 보여줍니다. 서버로 키를 보내지 않고 화면 안에서 계산합니다.
다른 기기·서버와 번호가 똑같이 맞는 이유는 뭔가요?
TOTP가 고정된 비밀 키와 전 세계 공통인 협정 세계시(UTC)만 재료로 쓰기 때문입니다. 같은 키와 같은 30초 구간이면 계산 결과가 같습니다. 그래서 기기 간 시각 동기화가 어긋나면 번호가 어긋납니다.
비밀 키에 소문자나 공백이 있어도 되나요?
대개 괜찮습니다. 표준 처리 과정에서 소문자는 대문자로 바꾸고 공백은 제거한 뒤 디코딩하기 때문입니다. 다만 0·1·8·9처럼 Base32에 아예 없는 글자가 들어가면 그 단계에서 오류가 납니다.
30초가 지나기 직전에 뜬 번호를 입력해도 되나요?
구간이 넘어가면 카운터가 바뀌어 번호도 바뀝니다. 다만 많은 검증 서버가 바로 앞 구간의 번호도 잠시 허용하도록 설계돼, 입력 도중 넘어가도 통과되는 경우가 있습니다. 허용 폭은 서버마다 다릅니다.
