0499

개발

구글도, 개발자도 절대 알 수 없는 비밀번호? bcrypt는 어떻게 그걸 가능하게 만들까

| 서론

안녕하세요, 팡일입니다.

회원가입을 할 때 우리는 아무렇지 않게 비밀번호를 입력합니다. 그런데 문득 이런 생각이 들었습니다.

“내 비밀번호는 어디에 저장될까?”

많은 사람들은 데이터베이스 어딘가에 그대로 저장되어 있을 것이라고 생각합니다. 그래서 서비스 운영자나 개발자라면 언제든지 내 비밀번호를 볼 수 있을 것이라고 생각하기도 하죠.

하지만 대부분의 서비스는 그렇지 않습니다. 놀랍게도 서비스를 만든 개발자조차 사용자의 비밀번호를 알 수 없습니다.

더 놀라운 점은 데이터베이스가 통째로 해킹당하더라도 비밀번호를 바로 알아내는 것이 거의 불가능하도록 설계되어 있다는 것입니다.

이런 보안의 핵심에는 bcrypt라는 기술이 있습니다.

오늘은 bcrypt가 어떤 원리로 동작하는지, 왜 "절대 복호화할 수 없다"는 이야기가 나오는지, 그리고 실제 서비스에서는 어떻게 활용되는지 알아보겠습니다.

| bcrypt는 암호화가 아니다

먼저 많은 사람들이 오해하는 부분부터 짚고 넘어가겠습니다.

“bcrypt는 비밀번호를 암호화하는 기술이다.”

사실 이 표현은 엄밀히 말하면 맞지 않습니다. bcrypt는 암호화(Encryption) 가 아니라 해시(Hash) 를 만드는 알고리즘입니다. 둘은 얼핏 비슷해 보이지만 목적이 완전히 다릅니다.

암호화는 원본 데이터를 숨겼다가 나중에 다시 복원하는 기술입니다. 예를 들어 HTTPS 통신이나 메신저의 종단 간 암호화는 데이터를 암호화한 뒤, 필요한 사람이 다시 복호화해서 읽을 수 있습니다.

반면 해시는 한 방향으로만 동작합니다. 입력값을 특정한 규칙으로 계산하여 새로운 문자열을 만들어내지만, 그 결과만 가지고는 원래 데이터를 되돌릴 수 없습니다.

bash
비밀번호

   bcrypt

$2b$12$...

즉, 여기까지가 끝이고, 다시 위로 올라가는 과정은 존재하지 않습니다. 그래서 bcrypt를 "암호화"보다는 일방향 변환이라고 이해하는 것이 더 정확합니다.

| 그런데 로그인은 어떻게 가능한 걸까?

여기서 대부분의 사람들이 한 가지 의문을 갖습니다.

복호화가 안 된다면 로그인할 때는 어떻게 비밀번호를 확인할까?

생각보다 원리는 단순합니다. 회원가입을 할 때 사용자가 입력한 비밀번호를 bcrypt로 변환한 뒤 그 결과만 데이터베이스에 저장합니다. 그리고 로그인할 때 사용자가 입력한 비밀번호를 다시 bcrypt로 계산합니다. 새롭게 계산한 결과가 데이터베이스에 저장된 값과 같다면 같은 비밀번호를 입력했다고 판단하는 것입니다.

즉, 비밀번호를 복원해서 비교하는 것이 아니라 같은 계산을 한 번 더 수행하는 방식입니다. 그래서 서비스는 사용자의 비밀번호를 몰라도 로그인을 처리할 수 있습니다.

| 그런데 같은 비밀번호인데 결과가 매번 다르다?

여기서 bcrypt의 가장 신기한 특징이 등장합니다.

예를 들어 같은 사람이 "1234"라는 비밀번호를 세 번 암호화한다고 가정해 보겠습니다. 일반적인 생각이라면 세 번 모두 같은 결과가 나와야 할 것 같습니다.

하지만 실제 bcrypt는 그렇지 않습니다.

code
1234
→ $2b$12$A....

1234
→ $2b$12$M....

1234
→ $2b$12$Q....

매번 결과가 달라집니다.

처음 보면 버그처럼 느껴질 수도 있지만, 사실은 의도된 동작입니다. bcrypt는 Salt라는 랜덤한 값을 비밀번호와 함께 사용하기 때문입니다.

code
비밀번호 + 랜덤한 Salt

즉 실제로는 이와 같이 함께 계산합니다. 덕분에 같은 비밀번호를 사용하는 두 명의 사용자도 전혀 다른 해시값을 갖게 됩니다.

| Salt가 왜 그렇게 중요할까?

Salt가 등장한 이유는 오래전부터 사용되던 공격 기법 때문입니다. 해커들은 미리 수십억 개의 비밀번호를 해시값으로 계산해 놓은 거대한 데이터베이스를 가지고 있습니다.

예를 들어 데이터베이스에서 어떤 해시값을 발견했을 때, 이미 계산된 목록에서 같은 값을 찾으면 원래 비밀번호를 바로 알아낼 수 있습니다.

이것을 레인보우 테이블(Rainbow Table) 공격이라고 합니다.

하지만 bcrypt는 매번 새로운 Salt를 생성하기 때문에 같은 비밀번호라도 결과가 항상 달라집니다. 즉, 미리 만들어 놓은 해시 목록이 아무런 의미가 없어집니다. 이 한 가지 기능만으로도 공격 난이도는 엄청나게 높아집니다.

| "절대 못 푼다"는 말은 정말일까?

인터넷에서는 bcrypt를 설명하면서 종종 "절대 복호화할 수 없다"는 표현을 사용합니다.

엄밀히 말하면 절반은 맞고 절반은 틀립니다. 맞는 이유는 bcrypt를 거꾸로 계산하는 공식 자체가 존재하지 않기 때문입니다. 하지만 그렇다고 해서 비밀번호를 절대 알아낼 수 없는 것은 아닙니다.

code
123456
password
qwerty
admin123
...

해커는 원래 비밀번호를 복원하는 대신, 이처럼 가능한 모든 비밀번호를 하나씩 넣어보면서 비교할 수 있습니다.

이것이 브루트포스(Brute Force) 공격입니다. 그래서 bcrypt의 목표는 "해독 불가능"이 아니라 "해독 비용을 현실적으로 감당할 수 없게 만드는 것"입니다.

| bcrypt가 일부러 느린 이유

사실 bcrypt는 컴퓨터 입장에서 상당히 비효율적인 알고리즘입니다. 일반적인 해시 함수는 1초에 수억 번 이상 계산할 수도 있습니다.

반면 bcrypt는 의도적으로 계산을 느리게 만들어 놓았습니다. 회원가입이나 로그인에서는 비밀번호를 하루에 몇 번만 입력하므로 100ms 정도의 지연은 거의 느껴지지 않습니다.

하지만 해커는 수십억 개의 비밀번호를 시도해야 합니다. 한 번 계산하는 데 100ms가 걸린다면 공격 시간은 기하급수적으로 늘어나게 됩니다.

즉, 정상 사용자에게는 거의 영향을 주지 않으면서 공격자에게만 엄청난 비용을 강요하는 것입니다.

| Cost라는 숫자가 보안을 결정한다

bcrypt에는 Cost라는 설정이 있습니다. 이 숫자는 "얼마나 느리게 계산할 것인가"를 의미합니다.

예를 들어 Cost가 10이라면 약 1,024번의 내부 연산을 수행하고, 11이면 약 2,048번, 12면 약 4,096번을 수행합니다.

흥미로운 점은 Cost가 단 1만 증가해도 계산량은 두 배가 된다는 것입니다. 그래서 서버 성능이 좋아질수록 Cost 값을 조금씩 높여 보안을 강화하는 경우가 많습니다.

| 실제 서비스에서는 어떻게 사용할까?

실제로 개발자가 bcrypt를 사용하는 코드는 생각보다 단순합니다.

회원가입에서는 비밀번호를 bcrypt.hash()로 변환한 뒤 데이터베이스에 저장합니다. 로그인에서는 사용자가 입력한 비밀번호를 bcrypt.compare()로 검증합니다. 이 과정에서 원본 비밀번호는 단 한 번도 저장되지 않습니다.

데이터베이스에도 없고, 서버에도 없으며, 개발자도 알 수 없습니다. 비밀번호를 알고 있는 사람은 오직 사용자뿐입니다.

| 마무리

보안을 이야기할 때 우리는 흔히 "강력한 암호"를 떠올립니다. 하지만 bcrypt의 철학은 조금 다릅니다.

애초에 아무도 원본 비밀번호를 알 수 없도록 만드는 것.

그것이 bcrypt의 핵심입니다. 그래서 데이터베이스가 유출되더라도, 내부 개발자가 데이터를 확인하더라도, 원래 비밀번호는 쉽게 드러나지 않습니다.

물론 bcrypt도 만능은 아닙니다. "123456"이나 "password"처럼 너무 쉬운 비밀번호는 브루트포스 공격에 노출될 가능성이 있습니다. 결국 좋은 보안은 강력한 비밀번호bcrypt 같은 안전한 저장 방식이 함께 갖춰질 때 비로소 완성됩니다.

평소에는 이름만 들어봤던 bcrypt지만, 그 안에는 복호화가 불가능한 해시, Salt를 이용한 무작위성, 의도적으로 느리게 만든 연산 등 현대 웹 서비스의 비밀번호 보안을 지탱하는 다양한 아이디어가 담겨 있습니다.

다음에 회원가입 화면에서 비밀번호를 입력하게 된다면, 그 짧은 입력 뒤에서 어떤 일들이 일어나고 있는지 한 번 떠올려보는 것도 재미있을 것입니다.