Base32
텍스트나 16진수를 Base32나 base32hex로 인코딩하고 브라우저에서 다시 디코딩합니다. 패딩과 대소문자를 고를 수 있고, 제자리에 있지 않은 문자는 줄과 열을 알립니다.
5SKYR24FSXWZLGHMQS4OZGUU
32가지 문자로 쓰는 바이트, 그리고 빠진 숫자
Base32는 바이트를 텍스트로 씁니다. 문자 하나가 5비트를 나타내는 32가지 문자로 쓰므로, 5바이트, 곧 40비트마다 8문자가 됩니다. Hello는 5바이트이고, JBSWY3DP로 쓰입니다. ‘디코딩’을 고르고 Base32를 붙여넣으면, 페이지는 그 Base32가 나타내는 바이트를 돌려줍니다. 텍스트로, 또는 ‘바이트 표기’에서 ‘16진수’를 고르면 16진수로 돌려줍니다.
이를 정의하는 RFC 4648은 대문자 26개와 숫자 2부터 7까지를 씁니다. RFC는 0과 1이 빠진 이유를 밝힙니다. 텍스트를 다루는 사람은 0을 O로, 1을 l이나 I로 착각하기 쉬우므로, 알파벳은 글자 쪽을 남기고 이 두 숫자를 뺀 것입니다. 글자 말고도 숫자가 여섯 개 필요한데, 그 여섯 개가 2부터 7까지이므로 8과 9도 들어 있지 않습니다. 이 알파벳으로 디코딩할 때 페이지는 0을 O로, 1을 I로 읽지 않고, 0, 1, 8, 9가 있으면 그 자리에서 거부합니다. RFC는 디코더가 그렇게 읽는 것을 허용하면서도, 기본적으로는 그러지 말아야 한다고 말합니다.
대소문자는 값의 일부가 아닙니다. RFC 4648은 대소문자와 상관없이 읽혀야 하는 텍스트를 위해 Base32를 설계했으므로, JBSWY3DP와 마찬가지로 jbswy3dp도 Hello입니다. RFC는 알파벳을 대문자로 쓰며, 이 페이지도 ‘소문자’를 켜지 않는 한 대문자로 씁니다. 또한 읽기는 줄바꿈을 포함해 어디에 있는 공백이든 건너뛰므로, 그룹으로 나눈 Base32나 여러 줄에 걸친 Base32도 한 덩어리로 쓴 것처럼 읽힙니다.
패딩, 그리고 Base32가 가질 수 있는 길이
바이트 수가 5의 배수로 딱 떨어지는 일은 드뭅니다. 끝에 남은 1바이트에서 4바이트까지는 각각 2, 4, 5, 7문자가 되고, RFC 4648은 그 마지막 그룹을 = 기호로 채워 8문자로 만듭니다. = 기호는 각각 여섯 개, 네 개, 세 개, 한 개입니다. 그래서 f는 MY======, fo는 MZXQ====, foo는 MZXW6===, foob는 MZXW6YQ=이고, 5바이트인 fooba는 8문자를 정확히 채워 MZXW6YTB가 됩니다.
그러니 = 기호가 있다면 그 앞까지의 문자를 8개씩 셀 때, 남는 수는 언제나 0, 2, 4, 5, 7 가운데 하나이고 1, 3, 6은 결코 아닙니다. 어떤 수의 바이트도 그만큼을 남기지 않기 때문입니다. 페이지는 그런 텍스트를 더 짧은 무언가로 읽지 않고, 그 마지막 문자에서 거부합니다. 패딩은 길이가 말해 주지 않는 것을 아무것도 말해 주지 않으므로, 페이지는 패딩을 뺀 Base32도 읽으며 — JBUQ====처럼 JBUQ도 Hi입니다 — 패딩을 거부하는 것은 패딩이 있는데 틀렸을 때뿐입니다. 뒤에 텍스트가 더 이어지는 중간의 = 기호, 또는 끝에 있으면서 개수가 틀린 = 기호가 그런 경우이며, 후자에서는 거부 문구가 그 자리에 몇 개가 와야 하는지 알려 줍니다.
인코딩할 때는 ‘패딩’ 스위치가 = 기호를 쓸지 정합니다. 처음에는 켜져 있습니다. RFC 4648이, 그것을 쓰는 사양이 달리 정하지 않는 한 패딩을 요청하기 때문입니다. 그리고 달리 정하는 사양도 여럿 있습니다. 2FA QR 코드에 담기는 otpauth:// URI를 기술하는 Key Uri Format은 시크릿의 패딩을 생략해야 한다고 말하고, NSEC3 레코드와 IPFS는 패딩을 전혀 쓰지 않습니다. 그 옆의 ‘소문자’ 스위치는 글자를 소문자로 씁니다. 숫자와 = 기호에는 바꿀 대소문자가 없습니다. 두 스위치는 인코딩할 때만 나타납니다. 읽기는 그 모든 조합을 받아들이기 때문입니다.
다른 도구들은 이 페이지보다 적게 읽습니다. GNU coreutils는 Base32를 base32로, 두 번째 알파벳인 base32hex를 basenc --base32hex로 쓰고, Python의 base64 모듈에는 각각을 위한 함수가 있습니다. 모두 이 페이지가 처음 열릴 때 쓰는 것과 같이, 패딩을 붙인 대문자로 씁니다. 하지만 base32 -d는 패딩이 빠졌거나 글자가 소문자인 Base32를 거부하고, Python의 b32decode도 둘 다 거부하며, 소문자는 casefold=True를 받았을 때만 읽습니다:
# 쓰기: 기본 알파벳, 그다음 다른 알파벳
printf 'Hi' | base32 # JBUQ====
printf 'Hi' | basenc --base32hex # 91KG====
# 되읽기: 패딩 있음, 그다음 패딩 없음, 그다음 소문자
printf 'JBUQ====' | base32 -d # Hi
printf 'JBUQ' | base32 -d # base32: invalid input
printf 'jbuq====' | base32 -d # base32: invalid input
# 같은 일을 스크립트로
import base64
base64.b32encode(b'Hi') # b'JBUQ===='
base64.b32hexencode(b'Hi') # b'91KG===='
base64.b32decode('JBUQ') # binascii.Error: Incorrect padding
base64.b32decode('jbuq====', casefold=True) # b'Hi'두 알파벳: base32와 base32hex
RFC 4648은 두 번째 알파벳으로 base32hex를 정의합니다. 숫자 0부터 9, 이어서 글자 A부터 V까지이므로, 문자는 나타내는 값의 순서대로, 영을 나타내는 0부터 서른하나를 나타내는 V까지 늘어섭니다. 그래서 첫 번째 알파벳에는 없는 성질이 하나 있으며, RFC도 이를 짚습니다. 문자 하나하나를 비교해 정렬하면, 텍스트가 그것이 나타내는 바이트의 순서대로 정렬된다는 것입니다. 바이트 00은 base32에서 AA======, base32hex에서 00======이고, 바이트 FF는 base32에서 74======, base32hex에서 VS======입니다. 텍스트로 정렬하면 숫자가 글자보다 앞에 오므로 base32는 FF를 맨 앞에 두지만, base32hex는 바이트의 순서를 지킵니다.
DNSSEC의 NSEC3 레코드가 이것을 씁니다. 레코드의 이름은 도메인 이름의 해시로 시작하며, 그 해시는 패딩 없는 base32hex로 쓰입니다. RFC 5155는 그렇게 쓰면 해시된 이름들이 해시 값의 순서와 같은 순서로 정렬된다고 적고 있습니다. 그 RFC 자신의 예시 영역은 example을 0p9mhaveqvm6t7vbl5lop2u3t2rp3tom으로 해시합니다. base32hex를 골랐다면 페이지는 이 32문자를 해시의 20바이트로 읽고, base32를 골랐다면 0에서 거부합니다 — 그리고 이 텍스트가 base32hex에서는 끝까지 읽힌다는 알림이 전환 버튼과 함께 나옵니다.
같은 바이트도 알파벳마다 다른 텍스트가 되므로 — Hi는 base32에서 JBUQ====, base32hex에서 91KG====입니다 — 고른 알파벳은 두 방향 모두에 적용되며, 페이지가 여러분 대신 그것을 바꾸는 일은 없습니다. 디코딩하는 텍스트에 고른 알파벳에 없는 문자가 있거나 텍스트가 0이 아닌 여분 비트로 끝나는데(여분 비트는 아래 질문에서 설명합니다), 다른 알파벳은 인코더가 쓰는 그대로 전체를 읽는다면, 알림이 그렇다고 알려 주고 그 옆에 전환 버튼이 나옵니다. 알파벳은 그 버튼을 누르거나 여러분이 직접 고를 때만 바뀝니다. ABCDEFGH처럼 두 알파벳 모두 문제없이 읽는 텍스트는 고른 쪽으로 읽습니다. 어느 쪽을 뜻했는지 텍스트 안의 어떤 것도 말해 주지 않기 때문입니다.
Base32를 만나는 곳: 2FA 시크릿, onion 주소, IPFS
Key Uri Format에서 인증 앱 코드의 바탕이 되는 시크릿을 쓰는 방식이 Base32입니다. Key Uri Format은 설정할 때 스캔하는 QR 코드에 담길 수 있는 otpauth:// URI를 기술하며, 그 secret 매개변수는 Base32이고 패딩은 빼야 합니다. 그런 시크릿은 대문자든 소문자든, 공백으로 나눈 그룹이든 여러 줄로 나뉜 것이든, = 기호가 있든 없든 붙여넣을 수 있습니다 — 그룹 사이의 하이픈은 그 자리에서 거부됩니다. 또 URI 전체를 붙여넣으면 페이지가 거기서 시크릿을 읽어 내고 그렇다고 알려 줍니다. URI의 다른 매개변수가 무엇을 뜻하는지, 그리고 시크릿이 어떻게 앱이 보여 주는 코드가 되는지는 ‘TOTP / 2FA 코드 디버거’의 몫입니다. 그 가이드가 URI와 그 매개변수를 설명하고, 그 페이지가 코드를 계산합니다. URI 안의 레이블과 발급자는 Key Uri Format이 요청하는 대로 퍼센트 인코딩되어 있으며, ‘URL 인코더 / 디코더’가 이를 읽습니다.
Tor 버전 3의 onion 주소도 Base32입니다. .onion 앞의 56문자는 35바이트의 Base32이며, 그 35바이트는 서비스의 32바이트 공개 키, 2바이트 체크섬, 그리고 버전 바이트 03입니다. Tor의 사양은 예시 주소들을 소문자로 쓰며, 35바이트는 그룹을 정확히 채우므로 뺄 패딩이 없습니다. .onion을 뺀 56문자를 붙여넣고 ‘바이트 표기’에서 ‘16진수’를 고르면, 마지막 바이트가 03으로 읽힙니다.
IPFS는 콘텐츠 식별자, 곧 CID를 버전 1부터는 기본으로 Base32로 씁니다. 소문자에 패딩 없이, 글자 하나 b 뒤에 씁니다. 이 b는 Base32의 일부가 아니라 그것이 Base32임을 나타내는 것 — multibase의 접두사이며, 나머지를 디코딩하기 전에 떼어 냅니다. 그래서 IPFS 자체 문서의 예시인 bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi는 여기서는 그대로라면 어떤 바이트로도 만들어지지 않는 길이로 거부됩니다. b를 떼면 36바이트, 곧 이진 CID로 읽히며, 버전을 나타내는 01로 시작합니다.
2FA 시크릿은 텍스트가 아니라 바이트다
Key Uri Format의 예시 시크릿은 JBSWY3DPEHPK3PXP이며, 그 문서는 이 값을 10바이트로 풀어 적습니다. Hello!의 문자들, 그리고 DE AD BE EF입니다. 앞의 8문자 JBSWY3DP는 그것만으로 Hello입니다. 여기서 ‘바이트 표기’를 페이지의 기본값인 ‘텍스트’로 두고 디코딩하면 Hello!, 이어서 U+07AD — DE AD가 우연히 문자 하나, 타나 문자의 기호가 됩니다 — 그리고 U+FFFD 두 개가 나오며, 각 U+FFFD는 텍스트가 아닌 시퀀스 하나를 나타냅니다. 결과 아래의 알림은 대체된 시퀀스 수를 세고, 첫 번째 것이 9번째 바이트에서 시작하며 그 비트가 1번째 줄, 13번째 열의 문자, 곧 3에서 시작한다고 말하고, 바이트가 애초에 텍스트가 아닐 수도 있다고 말합니다.
실제 시크릿은 텍스트를 이루는 일이 드문 무작위 바이트라서, 텍스트로 디코딩하면 흩어진 문자들과 U+FFFD가 되며, 그것은 시크릿이 맞는지에 대해 아무것도 알려 주지 않습니다. ‘바이트 표기’의 ‘16진수’는 바로 그럴 때를 위한 것입니다. 그것을 고르거나 알림 옆의 버튼을 누르면, 같은 시크릿이 48 65 6C 6C 6F 21 DE AD BE EF로 온전히 보입니다. 바이트 그 자체이며, 서버가 보관하는 것이자 모든 코드가 계산되는 바탕입니다. 페이지는 스스로 16진수로 전환하지 않으므로, 화면에 있는 것은 언제나 여러분이 고른 것입니다.
반대 방향도 같은 선택이며, 인코딩할 때 합니다. 서버가 16진수로 보관하는 키는 바이트입니다. ‘인코딩’과 ‘바이트 표기’의 ‘16진수’를 고르고 그 키를 붙여넣으면 — 공백으로 구분했든 이어 붙였든, 바이트 앞에 0x가 있든, 숫자 배열이든 hexdump -C나 xxd 형식의 16진수 덤프든 — 페이지가 인증 앱이 받아들이는 Base32를 씁니다. 48656C6C6F21DEADBEEF는 위의 시크릿인 JBSWY3DPEHPK3PXP가 됩니다. 10바이트는 그룹을 정확히 채우지만, 그렇지 않은 키, 예를 들어 16바이트짜리 키는 Key Uri Format이 요청하는 대로 ‘패딩’을 끄지 않는 한 뒤에 = 기호가 붙어 나옵니다. 디코딩하다가 실수로 붙여넣은 16진수, 곧 공백 문자 말고는 16진수 숫자만 짝수 개 있고 인코딩할 때 바이트로 읽을 수 있는 형태인 텍스트에는 16진수처럼 보인다는 알림이, 그것을 인코딩하는 쪽으로 바꾸는 버튼과 함께 붙습니다. 그리고 디코딩할 때 ‘내려받기’는 바이트 그 자체를 bytes.bin이라는 이름의 파일로 저장합니다.
Base32인가 Base64인가, 그리고 왜 Crockford의 알파벳은 아닌가
Base64는 같은 일을 64가지 문자로, 3바이트마다 4문자로 합니다. 그래서 Base64의 텍스트는 바이트보다 3분의 1 깁니다. 반면 Base32의 텍스트는 5분의 3 깁니다. 그 대가로 Base64가 포기하는 것은 대소문자를 가리지 않고 읽힐 수 있다는 점입니다. Base64의 알파벳은 대문자와 소문자를 서로 다른 값으로 두고, 거기에 +와 /까지 있으므로, SGk=의 바이트는 Hi이지만 sgk=의 바이트는 그와 다른 두 바이트입니다. Base32가 알맞은 것은 대소문자를 무시하는 무언가를 거치는 텍스트, 또는 사람이 한 화면에서 읽고 다른 화면에 입력하는 텍스트입니다. 여기에 붙여넣은 Base64에 어떤 Base32도 쓰지 않는 +나 /가 있고 전체가 Base64의 모양이면, Base64처럼 보인다는 알림과, Base64를 텍스트로 읽는 ‘Base64’ 도구로 가는 링크와 함께 거부됩니다.
32가지 문자로 된 알파벳은 그 밖에도 있지만, 이 페이지가 제공하는 것은 RFC 4648의 두 알파벳이며 다른 것은 없습니다. 그중 하나가 Douglas Crockford의 알파벳으로, 숫자 열 개를 모두 남기고 I, L, O, U를 빼며, ULID 형식이 이를 씁니다. Crockford는 이 알파벳을 수를 쓰기 위해 정의했고, 바이트를 쓰는 데 쓰면 답이 두 가지입니다. 00부터 0F까지의 16바이트를 RFC 4648이 바이트를 묶는 방식대로 5비트씩 묶으면 000G40R40M30E209185GR38E1W이고, ULID의 26문자를 읽는 방식대로 128비트 수 하나로 읽으면 00041061050R3GG28A1C60T3GF입니다. 이 알파벳으로 바이트를 쓰는 페이지는 두 답 가운데 하나를, 보이지 않는 곳에서 여러분 대신 고르는 셈입니다.
자주 묻는 질문
- 디코딩한 제 시크릿에 왜 U+FFFD가 나오나요?
- 2FA 시크릿은 무작위 바이트이고, 무작위 바이트가 UTF-8 텍스트가 되는 일은 드물기 때문입니다. ‘바이트 표기’가 ‘텍스트’이면 페이지는 텍스트가 아닌 시퀀스를 하나하나 대체 문자인 U+FFFD로 쓰고, 알림이 그런 시퀀스가 몇 개였는지, 그리고 첫 번째 것이 어디서 시작하는지를 몇 번째 바이트인지로, 또 그 비트가 시작하는 문자의 줄과 열로 알려 줍니다. 바이트 자체는 그대로입니다. 알림 옆의 버튼은 바이트를 16진수로 보여 주고, ‘내려받기’는 바이트를 저장합니다. 텍스트에 정말 들어 있는 U+FFFD, 곧 EF BF BD라는 바이트는 텍스트로 읽히며 세지 않습니다.
- ‘어떤 바이트로도 만들어지지 않는 길이’란 무슨 뜻인가요?
- Base32의 문자를 = 기호를 빼고 8개씩 세면, 바이트가 무엇이든 남는 수는 0, 2, 4, 5, 7 가운데 하나입니다. 그래서 1, 3, 6이 남는 텍스트는 어떤 바이트도 나타낼 수 없고, 페이지는 그 마지막 문자에서 거부합니다. 그런 텍스트는 인코더가 그렇게 쓴 것이 아닙니다. 여러분에게 오는 길에 무언가가 빠졌거나 더해진 것입니다. Key Uri Format의 시크릿에서 두 문자가 모자란 JBSWY3DPEHPK3P는 그 마지막 문자에서 거부됩니다. 잘린 자리가 바이트로 만들어지는 길이에 걸릴 수도 있는데, 그러면 더 적은 바이트로 읽힙니다 — JBSWY3DPEHPK3PX는 9바이트로. 페이지가 이를 알아챌 수 있는 것은 마지막 문자의 여분 비트가 0이 아닐 때뿐이며, 이 X의 여분 비트가 그렇습니다. 8문자 그룹 하나가 끝나는 자리에서 잘리면 알아챌 것이 아무것도 남지 않습니다.
- 여분 비트란 무엇이고, 왜 페이지는 제 여분 비트가 0이 아니라고 하나요?
- 문자 하나에 5비트, 바이트 하나에 8비트는 나누어떨어지는 일이 드뭅니다. 그래서 바이트가 5개씩 딱 맞게 오지 않는 한, 마지막 문자는 어떤 바이트에도 들어가지 않는 비트를 1비트에서 4비트까지 지니며, 인코더는 그 비트를 0으로 씁니다. MZXW6YQ=도 MZXW6YR=도 foob입니다. Q의 마지막 세 비트는 000이고 R의 마지막 세 비트는 001이며, 인코더가 쓰는 것은 앞의 것뿐입니다. 페이지는 둘 다 읽고, 뒤의 것에 대해서는 알림이 그 R과 그것이 있는 자리, 그리고 인코더가 그 자리에 쓰는 Q를 짚어 줍니다. 여분 비트가 0이 아니라는 것은 그 텍스트가 인코더가 쓴 것이 아니라는 뜻입니다. 도중에 잘렸거나, 손으로 고쳤거나, 다른 알파벳으로 쓴 것일 수 있으며, 마지막 경우는 페이지가 여러분 대신 확인합니다.
- Base32는 암호화인가요?
- 아니요. Base32는 바이트를 쓰는 방식을 바꿀 뿐 그 밖에는 아무것도 바꾸지 않습니다. 누구나 디코딩할 수 있고, RFC 4648을 따르는 디코더라면 어느 것이든 같은 알파벳에서 같은 바이트를 돌려받습니다. Base32로 쓴 2FA 시크릿은 시크릿 그 자체이므로, 설정 키를 본 사람은 누구나 여러분의 두 번째 요소를 쥐게 됩니다.
- 제가 붙여넣은 것이 서버로 전송되나요?
- 아니요. 두 방향 모두 이 페이지 안에서, 여러분 자신의 기기에서 돌아갑니다. 붙여넣은 것은 — 시크릿이든 텍스트든 16진수든 — 아무것도 업로드되지 않으며, ‘내려받기’가 저장하는 파일은 이미 브라우저에 있는 바이트로 그 브라우저 안에서 만들어집니다.
관련 도구
- Base64
Base64는 3바이트마다 문자 4개로, Base32는 5바이트마다 문자 8개로 쓰며, Base64에서는 대소문자도 문자 값의 일부입니다. 저 페이지에서 abc는 YWJj이고, YWJJ는 abI로 읽힙니다.
- TOTP / 2FA 코드 디버거
전체 도출과 함께 2FA 코드를 생성하고 디버그합니다.
- URL 인코더 / 디코더
값이나 URL 전체를 퍼센트 인코딩, 그 반대도.
- JSON 문자열 이스케이프
텍스트를 JSON 문자열용으로 이스케이프, 복원도.