bcrypt 72바이트 오류 — 짧은 비밀번호에서도 발생하는 이유
서로 다른 두 가지 문제가 똑같은 메시지를 띄우는데, 그중 하나만 실제 비밀번호와 관계가 있습니다.
비밀번호가 정말로 bcrypt 72바이트 한계를 넘는다면, bcrypt는 앞의 72바이트만 읽고 나머지는 버립니다. 앞 72바이트가 동일한 82바이트 비밀번호 두 개를 고정 salt로 해싱해 봤습니다. 둘 다 $2a$10$abcdefghijklmnopqrstuu.hioaszd4nGKdJlcuRzR1xqPIcN/X.S를 내놓았고, bcrypt.compareSync(p2, hash(p1))는 true를 반환했습니다. 두 번째 비밀번호로 첫 번째 비밀번호의 계정에 로그인할 수 있다는 뜻입니다.
비밀번호가 누가 봐도 짧은데 이 오류가 난다면:
password cannot be longer than 72 bytes, truncate manually if necessary (e.g. my_password[:72])
(“비밀번호는 72바이트보다 길 수 없습니다. 필요하면 직접 잘라 내십시오”라는 뜻입니다.)
이때는 메시지가 원인을 잘못 지목하고 있습니다. passlib 1.7.4와 bcrypt 5.0.0 조합에서는 14바이트짜리 비밀번호도 이 오류를 냅니다.
진짜 원인은 passlib 내부에 있는 고정 길이 255바이트 자가 진단 프로브입니다. 이 프로브는 백엔드가 초기화될 때 한 번 실행되며, 여러분의 비밀번호가 해싱 호출까지 도달하기도 전에 돌아갑니다. bcrypt 5.0.0이 그 프로브를 거부하고, 예외가 그대로 빠져나오고, 아무도 입력한 적 없는 비밀번호에 대한 불평을 읽게 되는 것입니다.
먼저 정리하고 갈 것이 하나 있습니다. 이 오류를 검색하면 가장 많이 나오는 __about__ 몽키 패치는 문제를 해결하지 못합니다. 깨끗한 프로세스에서 import passlib보다 먼저 패치를 적용해 다시 돌려 봤지만, ValueError는 그대로 다시 나왔습니다.
30초 분류: 어느 쪽에 해당하는가
| 비밀번호 상태 | 오류가 나는 시점 | 근본 원인 | 읽을 곳 |
|---|---|---|---|
| 72바이트보다 길다 | hash를 호출할 때 | 진짜로 너무 길다. bcrypt 5.0은 예외를 던지고, bcrypt 4.x는 조용히 잘라 낸다 | 2절과 3절 |
| 72바이트 미만이고 passlib을 쓴다 | 프로세스의 첫 호출에서 | passlib의 255바이트 프로브. 비밀번호와는 아무 상관이 없다 | 4절 |
| 한국어, 중국어, 일본어, 이모지가 들어 있다 | 짧아 보이지만 실제로는 아니다 | 문자 수는 바이트 수가 아니다 | 3절 |
| 의존성을 업그레이드한 뒤부터 실패한다 | 배포 이후 | bcrypt 5.0의 breaking change | 4절과 5절 |
두 번째 행에 해당한다면 바로 건너뛰세요. 이어지는 두 절에는 도움이 될 내용이 없고, 해결책도 다릅니다.
bcrypt 72바이트 한계가 비밀번호에 하는 일
bcrypt가 72에서 멈추는 이유
bcrypt는 Blowfish 위에 세워졌고, 비밀번호를 Blowfish 키로 그대로 집어넣습니다. Blowfish는 키를 18개의 서브키로 이루어진 P-array로 확장하며, 각 서브키는 32비트입니다. 18 × 4 = 72바이트의 키 재료가 되고, 확장 루프는 18개 슬롯을 모두 채우고 나면 키의 처음으로 되돌아갑니다.
그러니까 이 상한은 Blowfish 키 스케줄에서 곧바로 나오는 구조적 제약이며, 설정으로 올릴 수 있는 버퍼 크기가 아닙니다. 모든 플랫폼의 모든 표준 준수 bcrypt 구현이 같은 한계를 가지며, 그래서 Python, Node, Go, Java, PHP 어디서나 똑같이 72라는 숫자를 보게 됩니다.
서로 다른 두 비밀번호, 같은 해시
bcrypt의 비밀번호 잘림은 길이 제약에서 오는 불편이 아니라 보안에 직접 영향을 주는 동작입니다. 그래서 이 절은 직접 재현해 보고 넘어갈 값어치가 있습니다.
bcryptjs 3.0.3에서 고정 salt $2a$10$abcdefghijklmnopqrstuv를 써서, 각각 82바이트인 비밀번호 두 개를 해싱했습니다.
| 비밀번호 | 값 | 바이트 |
|---|---|---|
| p1 | "A"×72 + "XXXXXXXXXX" | 82 |
| p2 | "A"×72 + "ZZZZZZZZZZ" | 82 |
둘 다 같은 다이제스트를 내놓았습니다.
$2a$10$abcdefghijklmnopqrstuu.hioaszd4nGKdJlcuRzR1xqPIcN/X.S
서로 다른 두 비밀번호가 하나의 해시로 수렴합니다. 뒤따르는 결과는 이렇습니다.
bcrypt.compareSync(p2, hash(p1)) // true
긴 패스프레이즈의 앞 72바이트를 아는 공격자는 뒤에 무엇을 붙이든 인증에 성공합니다. 경계 너머의 모든 바이트는 사용자가 아무리 신중하게 골랐더라도 저장된 해시의 강도에 정확히 0만큼 기여합니다. 이미 가지고 있는 해시를 어떤 후보 비밀번호와 대조해 보고 싶은데 스크립트를 짜기는 번거롭다면, 브라우저에서 bcrypt 해시를 생성하고 검증하면서 같은 동작을 직접 확인할 수 있습니다.
경계는 정확히 어디에 있는가
공통 접두사를 유지한 채 그 뒤의 정확히 한 바이트만 바꿔 가면서, 한 바이트씩 잘라 경계를 좁혔습니다.
| 동일한 접두사 바이트 | 차이가 나는 바이트 위치 | 해시가 같은가? |
|---|---|---|
| 70 | 71 | false |
| 71 | 72 | false |
| 72 | 73 | true |
| 73 | 74 | true |
72번째 바이트까지는 여전히 반영됩니다. 73번째 바이트가 반영되지 않는 첫 바이트입니다. 서서히 옅어지는 구간도, 부분적으로 섞이는 구간도 없기 때문에, 직접 쓰는 라이브러리로 로컬에서 확인해 보기에도 비용이 거의 들지 않습니다.
문자 수는 바이트 수가 아닙니다
bcrypt는 UTF-8 바이트를 세고, 사용자는 문자를 입력합니다. ASCII에서는 두 숫자가 우연히 일치하는데, 바로 그래서 영어권 밖으로 서비스를 내보내는 순간 이 문제가 팀의 발목을 잡습니다.
| 문자 종류 | 예시 | 문자당 바이트 | 72바이트는 곧 |
|---|---|---|---|
| ASCII 라틴 문자 | A | 1 | 72자 |
| 중국어 한자 | 密 | 3 | 24자 |
| 일본어 가나 | あ | 3 | 24자 |
| 이모지 | 🔒 | 4 | 18자 |
| 키릴 문자 | я | 2 | 36자 |
| 독일어 움라우트 | ü | 2 | 36자 |
양쪽 극단을 모두 확인했습니다. 중국어 비밀번호에서는 24번째 문자 이후의 차이가 무시되었고(true), 이모지 비밀번호에서는 18번째 문자 이후의 차이가 무시되었습니다(true).
25자짜리 중국어 패스프레이즈는 비밀번호 입력란에서 넉넉해 보입니다. 하지만 이미 선을 넘었습니다. 이모지 20개를 고른 사용자는 두 문자만큼 한계를 초과한 상태이고, 그 사실을 끝내 통보받지 못합니다.
내 코드에서 바이트 길이 재기
문자 수를 기준으로 쓴 길이 검사는, 실제 값이 이미 너무 긴데도 통과합니다. 바이트를 재세요.
# Python
len(pw.encode("utf-8"))
// Node.js
Buffer.byteLength(pw, "utf8")
// Go
len([]byte(pw))
Buffer가 없는 브라우저에서는 new TextEncoder().encode(pw).length가 같은 숫자를 줍니다. 이 검사를 해싱 호출 앞에 두고, 라이브러리가 새벽 3시에 대신 판단하도록 놔두는 대신 제대로 된 유효성 검사 메시지를 반환하세요. 이참에 최소 길이 정책도 다시 들여다보고 있다면, 비밀번호 강도를 실제로 어떻게 측정하는지에서 길이 규칙이 무엇을 보장하고 무엇은 보장하지 못하는지 다룹니다.
짧은 비밀번호도 실패하는 이유: passlib의 255바이트 프로브
대부분의 사람을 검색 엔진으로 보내는 쪽은 이쪽입니다. 비밀번호는 열네 글자인데 라이브러리는 72바이트를 넘었다고 우깁니다.
재현하기
Python 3.14.5, bcrypt 5.0.0, passlib 1.7.4에서 세 줄이면 됩니다.
from passlib.hash import bcrypt
bcrypt.hash("short-password") # 14바이트
# ValueError: password cannot be longer than 72 bytes, truncate manually if necessary (e.g. my_password[:72])
14바이트가 들어갔는데 72바이트 이야기가 나옵니다. passlib bcrypt 오류는 실재하지만, 거기 적힌 숫자는 전혀 다른 것을 가리키고 있습니다.
전체 호출 스택
추측이 아닙니다. passlib 1.7.4를 따라가며 추적한, 실제로 실행되는 경로입니다.
- 첫 호출이 백엔드 초기화를 촉발합니다:
_calc_checksum→_stub_requires_backend()→set_backend(). _load_backend_mixin이bcrypt.__about__.__version__을 읽습니다. 이 속성이 존재하지 않으므로AttributeError가 발생합니다. passlib은 이를 삼키고(trapped) error reading bcrypt version을 출력합니다.- 초기화는
_finalize_backend_mixin(passlib/handlers/bcrypt.py:421)으로 이어지고, 여기서detect_wrap_bug(IDENT_2A)를 호출합니다. detect_wrap_bug(같은 파일:378)는 고정 길이 255바이트 프로브를 검증합니다.- bcrypt 5.0.0은 72바이트를 넘는 모든 입력에
ValueError를 던지므로, 프로브가 제 발에 걸려 터집니다. - 예외는 여러분의 호출 지점까지 전파됩니다. 애초에 입력과는 무관했던 72바이트 메시지를 보게 됩니다.
이 전체 순서는 프로세스당 한 번, 첫 hash 또는 첫 verify에서 일어납니다. 그래서 이 실패가 그토록 확실하게 재현되고, 무엇을 넣든 전혀 반응하지 않는 것입니다.
프로브의 정체
secret = (b"0123456789" * 26)[:255]
이 상수는 Openwall이 2012년에 공개한 BSD bcrypt의 wraparound 버그에서 왔습니다. 긴 키가 되돌아 감기면서 더 약한 해시로 무너지던 문제였습니다. passlib은 방금 로드한 백엔드가 그 결함을 안고 있는지 시작 시점에 검사하고, 결함이 있는 백엔드는 신뢰하기를 거부합니다.
정확하게 부릅시다. detect_wrap_bug는 passlib의 버그가 아닙니다. 10년 넘게 유효했던 테스트 벡터를 써서, 작성된 목적 그대로 동작하는 방어 코드입니다. 바뀐 것은 bcrypt 5.0.0이 이제 255바이트 입력을 해싱할 대상이 아니라 오류로 취급한다는 점이고, 그 변화가 통과하던 자가 진단을 잡을 수 없는 예외로 바꿔 놓았습니다. 두 라이브러리의 충돌은 pyca/bcrypt 이슈 #1082의 논의에서 다룹니다.
__about__ 패치가 문제를 해결하지 못하는 이유
이 오류를 검색하면 bcrypt가 __about__을 제거했고 그것을 복원하면 passlib이 고쳐진다는 이야기를 반복해서 듣게 됩니다. 두 주장 모두 틀렸고, 아래 측정이 그것을 보여 줍니다.
| 버전 | hasattr(bcrypt, "__about__") | trapped 경고 출력 | passlib 동작 |
|---|---|---|---|
| bcrypt 5.0.0 | False | 예 | 아니요 (ValueError) |
| bcrypt 4.3.0 | False | 예 | 예 |
bcrypt 4.3.0에도 __about__은 없습니다. 똑같이 (trapped) error reading bcrypt version 줄을 출력합니다. 그런데도 passlib은 아무 불평 없이 돌아갑니다. 따라서 속성이 없다는 사실은 동작과 고장을 가르는 경계선이 아닙니다. 경계선은 5.0.0의 ValueError 동작 변경입니다.
그렇다면 그 유명한 패치는 작동할 수가 없고, 실제로도 작동하지 않습니다.
import bcrypt, types
bcrypt.__about__ = types.SimpleNamespace(__version__=bcrypt.__version__) # passlib을 import하기 전에
from passlib.hash import bcrypt as pl
pl.hash("short-password")
# 여전히 ValueError: password cannot be longer than 72 bytes, ...
누구도 실패를 import 순서 탓으로 돌릴 수 없도록, 깨끗한 프로세스에서 import passlib보다 먼저 패치를 적용한 뒤 실행했습니다. 그래도 실패합니다. 이 패치가 이루는 것은 무해한 경고 하나를 잠재우는 일뿐입니다. 4단계의 255바이트 프로브는 애초에 __about__을 참조한 적도 없는 별개의 단계이고, 어느 쪽이든 터집니다.
bcrypt 5.0이 실제로 바꾼 것
bcrypt 5.0의 breaking change는 동작 한 줄이지만 폭발 반경이 넓습니다.
| 입력 | bcrypt 4.3.0 | bcrypt 5.0.0 |
|---|---|---|
| 72바이트 | 성공 | 성공 |
| 73바이트 | 성공 (조용히 잘림) | ValueError |
| 100바이트 | 성공 (조용히 잘림) | ValueError |
| 255바이트 | 성공 (조용히 잘림) | ValueError |
4.x 열의 잘림은 비유가 아닙니다. 4.3.0에서 같은 접두사로 만든 hash(73 bytes)와 hash(100 bytes)는 서로 같게 나옵니다: true.
그러니 여기서는 bcrypt 5.0이 더 올바른 라이브러리입니다. 키 재료를 조용히 버리는 쪽이 더 나쁜 결과이고, 주어진 입력을 온전히 반영할 수 없을 때 멈추는 것이 해싱 라이브러리가 할 일입니다. 그렇다고 업그레이드가 고통 없이 지나가지는 않습니다. 몇 년째 조용히 바이트를 잃고 있던 코드가 이제 예외를 던지고, 그 코드 경로가 passlib 뒤에 있다면 여러분의 입력이 개입하기도 전에 던집니다.
저 표에서 두 가지가 따라 나오는데, 각각 다른 팀에 떨어집니다. bcrypt를 직접 호출한다면 업그레이드는 눈에 보입니다. 가입이나 로그인에서 예외가 나고, 터지는 위치는 여러분이 소유한 코드이고, 스택 트레이스도 그 해싱 호출을 가리킵니다. 그 앞에 바이트 길이 검사를 하나 넣으면 오후 반나절이면 끝납니다.
passlib을 거친다면 업그레이드는 전면적으로 터지기 전까지는 보이지 않습니다. 이 실패는 긴 비밀번호를 쓰는 사용자 비율에 비례하지 않습니다. 애초에 사용자 입력에 의존하지 않기 때문입니다. 비밀번호 처리와 관련해 아무것도 바꾸지 않은 코드베이스에서, 프로세스의 첫 호출부터 모든 hash와 모든 verify가 실패합니다. 그래서 이 문제는 버그 리포트가 아니라 배포 사고의 모습으로 나타나고, 오류 문구는 사람들을 정확히 엉뚱한 곳으로 보냅니다.
고치기
코드를 고칠 수 있다면
passlib을 걷어 내고 bcrypt를 직접 호출하세요. passlib의 마지막 릴리스는 1.7.4였고 프로젝트는 오랫동안 조용했으므로, bcrypt만 필요한 프로젝트에서 이 계층이 해 주는 일은 거의 없습니다.
import bcrypt
password = "correct horse battery staple".encode("utf-8")
hashed = bcrypt.hashpw(password, bcrypt.gensalt(rounds=12))
bcrypt.checkpw(password, hashed) # True
hashpw와 checkpw는 둘 다 바이트를 받으므로, 경계에서 인코딩하고 나머지 코드는 str로 유지하세요. 백엔드 감지도, 자가 진단 프로브도, 여러분이 넘기지도 않은 비밀번호를 두고 보고하는 실패 모드도 없습니다. 결과 해시를 눈으로 확인하거나 앱이 만들어 낸 해시를 검증하고 싶다면, bcrypt 생성기가 전부 브라우저 안에서 돌아갑니다. HTTP Basic 인증에 bcrypt를 쓰는 서버도 파일 형식만 다를 뿐 같은 구조적 제약을 받는데, 이는 htpasswd 가이드에서 짚어 줍니다.
오늘 당장 코드를 고칠 수 없다면
5 미만으로 고정하세요.
bcrypt<5
bcrypt 4.3.0과 passlib 1.7.4 조합을 확인했고, 정상 동작합니다. 다만 지금 무엇을 얻은 것인지는 분명히 해 두세요. 이것은 지혈대이지 수리가 아닙니다. 긴 bcrypt 비밀번호를 만나면 바이트를 조용히 버리는 동작을 하는 버전에 머무는 것이고, 그것이 바로 5.0이 막으려고 릴리스된 문제입니다. 고정에 기한을 적어 두고 이전 계획을 세우세요.
사용자가 정말로 긴 패스프레이즈를 입력한다면
비밀번호를 먼저 SHA-256으로 한 번 해싱하고, 다이제스트를 base64로 인코딩한 다음, 그것을 bcrypt에 넘기세요.
import base64, hashlib, bcrypt
def prehash(password: str) -> bytes:
return base64.b64encode(hashlib.sha256(password.encode("utf-8")).digest())
hashed = bcrypt.hashpw(prehash(pw), bcrypt.gensalt(rounds=12))
bcrypt.checkpw(prehash(pw), hashed)
출력은 입력 길이와 상관없이 항상 44바이트로, 72보다 넉넉히 아래입니다. 그리고 잘림이 파괴했던 성질을 되살립니다. 도입부에 나온 82바이트 비밀번호 두 개를 이 함수에 통과시키면 checkpw(prehash(p2), hash(prehash(p1)))는 False가 됩니다. 충돌이 사라진 것입니다.
base64 단계는 실제로 일을 하고 있으니 빼지 마세요. 가공되지 않은 SHA-256 다이제스트는 임의의 이진 데이터라 NUL 바이트를 포함할 수 있는데, bcrypt 구현들은 NUL을 일관되지 않게 처리합니다. base64는 NUL이 없는 고정 길이 ASCII 문자열을 줍니다. 가입 시점과 로그인 시점에 같은 함수를 적용하세요. 그러지 않으면 기존 해시가 전부 검증되지 않습니다.
하지 말아야 할 것
솔깃한 선택지가 둘 있는데, 둘 다 해롭습니다.
__about__ 몽키 패치는 작동하지 않습니다. 4절에 측정 결과가 있습니다. 팀의 누군가가 그것을 붙여 넣으려 한다면, 위의 네 줄이 그 사람의 반나절을 아껴 줍니다.
pw[:72]로 직접 잘라 내는 것은 아무것도 하지 않는 것보다 나쁩니다. 시끄럽던 실패를 다시 조용한 실패로 되돌리고, 2절의 충돌을 여러분의 코드 안에 다시 만들어 냅니다. bcrypt 5.0이 없애려고 릴리스된 바로 그 동작을 손으로 재현하는 셈인데, 라이브러리 버전과 달리 직접 짠 쪽은 아무에게도 경고하지 않습니다. 긴 비밀번호가 온전히 동작해야 한다면 사전 해싱을 하세요. 그럴 필요가 없다면 바이트 길이를 검사하고 명확한 메시지와 함께 거부하세요.
이미 데이터베이스에 저장된 해시는 어떻게 하나
영향을 받는 행
72바이트를 넘는 비밀번호로 가입한 계정만 해당됩니다. 대부분의 소비자 제품에서 이는 작은 집합이고, ASCII만 쓰는 서비스라면 대개 패스프레이즈 애호가들을 뜻합니다. 사용자가 한국어, 중국어, 일본어, 이모지를 입력하는 제품이라면 3절이 적용되며, 영향 범위는 바이트를 보지 않은 감사가 시사하는 것보다 훨씬 클 수 있습니다.
이 행들을 해시만 보고 골라낼 수는 없습니다. bcrypt 다이제스트는 고정 폭이고, 입력이 얼마나 길었는지에 대한 기록을 전혀 담지 않습니다. 가입 시점에 비밀번호 길이를 로그로 남겼다면 그 로그가 유일한 목록입니다. 대부분의 팀은 남기지 않았고 사후에 복원하는 것도 불가능하므로, 목록을 전제로 계획하지 말고 모른다는 것을 전제로 계획하세요.
일괄 재계산은 불가능합니다
다시 해싱할 평문이 없고, 그것이 해시를 저장하는 이유의 전부입니다. 그래서 마이그레이션은 지연 방식이어야 합니다. 각 계정을 그 주인이 다음번에 인증에 성공하는 시점에, 평문을 잠시 메모리에 들고 있는 동안 업그레이드하는 것입니다.
def login(user, password: str) -> bool:
if not verify_legacy(password, user.password_hash):
return False
if needs_rehash(user.password_hash):
user.password_hash = hash_new_scheme(password)
save(user)
return True
먼저 기존 방식으로 검증하고, 그다음에만 다시 해싱하세요. 두 단계를 뒤집으면 비밀번호가 맞았는지 확인하기도 전에 저장된 해시를 덮어쓰게 됩니다. 각 해시 옆에 방식 식별자를 저장해 needs_rehash가 추측이 아니라 필드 비교가 되도록 하고, 끝내 로그인하지 않는 휴면 계정의 긴 꼬리를 예상해 두세요. 그런 계정은 강제로 처리하지 말고 비밀번호 재설정 시점에 처리하면 됩니다.
전면 마이그레이션이 값어치를 할 때
이미 지연 재해싱 경로를 작성하고 있다면, 그 아래의 알고리즘을 바꿀 기회로는 앞으로 가장 저렴한 순간입니다. Argon2id에는 72바이트 상한이 존재하지 않으며, Argon2id와 bcrypt의 심층 비교에서 전환이 언제 이득이고 언제 bcrypt에 남는 것이 옳은 판단인지 다룹니다. 매개변수를 대조해 볼 기준으로는 OWASP Password Storage Cheat Sheet를 보세요.
이 오류만을 이유로 마이그레이션을 시작하지는 마세요. 비밀번호가 72바이트 아래에 넉넉히 들어온다면 bcrypt는 여전히 견실한 선택이고, 6절이 이미 문제를 해결해 주었습니다.
FAQ
비밀번호가 짧은데 bcrypt는 왜 72바이트보다 길다고 하나요?
메시지가 가리키는 대상이 여러분의 비밀번호가 아니라 passlib의 내부 프로브이기 때문입니다. 첫 호출에서 passlib은 고정 길이 255바이트 테스트 문자열로 detect_wrap_bug를 실행합니다. bcrypt 5.0.0은 72바이트를 넘는 모든 입력에 ValueError를 던지므로 프로브가 실패하고, 그 오류가 여러분의 호출 지점에 드러납니다. 14바이트 비밀번호로도 발생합니다.
bcrypt는 정말로 72바이트 이후를 전부 무시하나요?
그렇습니다. bcrypt는 72바이트 이후의 모든 바이트를 완전히 무시합니다. 앞 72바이트가 같은 82바이트 비밀번호 두 개는 동일한 해시 $2a$10$abcdefghijklmnopqrstuu.hioaszd4nGKdJlcuRzR1xqPIcN/X.S를 만들어 내고, 각각이 상대의 해시에 대해 검증을 통과합니다. 경계는 정확합니다. 72번째 바이트의 차이는 해시를 바꾸지만, 73번째 바이트의 차이는 바꾸지 않습니다.
72바이트 한계는 보안 문제인가요?
네, 긴 패스프레이즈에서는 bcrypt의 72바이트 한계가 보안 문제입니다. 앞 72바이트를 아는 사람은 임의의 바이트를 덧붙여 인증할 수 있으므로, 한계를 넘은 바이트는 아무것도 더해 주지 않습니다. 72바이트 미만인 비밀번호에서는 아무것도 달라지지 않습니다. 긴 입력이 온전히 반영되어야 한다면 SHA-256 사전 해싱이 이 노출을 없애 줍니다.
72바이트는 몇 글자인가요?
72바이트가 몇 글자인지는 인코딩에 따라 다릅니다. ASCII 문자 72자, 키릴 문자나 움라우트 문자 36자, 중국어 한자 24자, 일본어 가나 24자, 이모지라면 18자입니다. bcrypt는 문자가 아니라 UTF-8 바이트를 세므로 Python에서는 len(pw.encode("utf-8")), Node에서는 Buffer.byteLength(pw, "utf8")로 재세요.
__about__을 패치하면 passlib 오류가 해결되나요?
아닙니다. __about__ 패치로는 passlib 오류가 해결되지 않습니다. 깨끗한 프로세스에서 import passlib보다 먼저 패치를 적용했지만 ValueError는 그대로 발생했습니다. bcrypt 4.3.0에도 __about__이 없지만 passlib과 문제없이 동작하며, 이는 속성 누락이 원인이 아님을 증명합니다. 이 패치는 (trapped) error reading bcrypt version 경고를 잠재울 뿐입니다.
bcrypt를 5.0 미만으로 내려야 하나요?
임시 방편이라면 bcrypt를 5.0 미만으로 내려도 됩니다. bcrypt 4.3.0과 passlib 1.7.4 조합은 동작합니다. 다만 4.x는 72바이트를 넘는 부분을 조용히 잘라 내며 그것이 바로 5.0이 막으려고 릴리스된 동작이므로, 버전 고정은 한시적인 것으로 다루고 bcrypt 직접 호출로 옮기세요.
비밀번호를 직접 72바이트로 잘라도 되나요?
안 됩니다. 비밀번호를 직접 72바이트로 자르지 마세요. pw[:72]는 앞에서 설명한 충돌을 여러분의 코드 안에 조용히, 잡아낼 라이브러리 경고 하나 없이 다시 만들어 냅니다. 긴 입력이 서로 구별되도록 SHA-256과 base64로 사전 해싱하거나, 바이트 길이를 미리 검사해 명확한 오류 메시지와 함께 거부하세요.
고치기 전에 이미 해싱된 비밀번호는 어떻게 되나요?
이미 저장된 bcrypt 해시는 계속 검증을 통과합니다. verify 경로가 hash 경로와 똑같은 방식으로 자르기 때문입니다. 약해진 것은 72바이트를 넘는 비밀번호로 가입한 계정뿐이고, 평문 없이는 재계산할 수 없습니다. 다음 로그인 성공 시점에 지연 재해싱을 하고, 휴면 계정은 비밀번호 재설정 시점에 처리하세요.