인증의 여러 방식을 살펴보았지만, 그 밑바탕에는 여전히 사용자가 아는 비밀, 곧 비밀번호가 놓여 있는 경우가 많다. 아무리 세션과 토큰을 정교하게 다루어도 비밀번호 자체를 허술하게 저장하면, 저장소가 한 번 새어 나가는 순간 모든 계정이 함께 무너진다. 그래서 비밀번호를 어떻게 저장하느냐는 인증 설계에서 가장 근본적인 방어선 가운데 하나다.


이번 편은 비밀번호를 원래 형태로 저장하지 않는다는 원칙에서 출발해, 단순한 단방향 변환만으로는 왜 부족한지, 솔트와 느린 계산이 어떤 위협을 각각 막는지, 어떤 방식을 골라야 하는지, 그리고 검증과 운영에서 무엇을 챙겨야 하는지를 다룬다. 저장소가 언젠가 새어 나갈 수 있다는 전제 아래, 새어 나가더라도 비밀번호가 드러나지 않게 하는 것이 이 편의 목표다.

비밀번호를 그대로 저장하지 않는다

가장 먼저 지켜야 할 원칙은 비밀번호를 원래 형태로 저장하지 않는 것이다. 저장소에 비밀번호가 그대로 적혀 있으면, 그 저장소를 들여다볼 수 있는 누구든 모든 사용자의 비밀번호를 알게 된다. 저장소가 외부로 새어 나가는 경우는 물론이고, 내부에서 저장소에 접근할 수 있는 사람에게도 비밀번호가 노출된다.


이 원칙의 바탕에는 저장소가 언젠가 새어 나갈 수 있다는 전제가 있다. 아무리 방어를 두어도 저장소가 완전히 안전하다고 장담할 수는 없다. 그래서 저장소가 새어 나가는 상황을 가정하고, 그때에도 비밀번호가 드러나지 않도록 미리 대비하는 것이다. 방어는 최선의 상황이 아니라 최악의 상황을 기준으로 세워야 한다.


비밀번호를 그대로 두지 않으면서도 로그인 시 대조는 해야 하므로, 비밀번호를 되돌릴 수 없는 형태로 바꾸어 저장한다. 사용자가 입력한 비밀번호를 같은 방식으로 바꾸어 저장된 값과 비교하면, 원래 비밀번호를 저장소에 두지 않고도 일치 여부를 확인할 수 있다. 저장된 값에서 원래 비밀번호를 되돌릴 수 없다는 성질이 이 방식의 핵심이다.


여기서 되돌릴 수 있는 방식과 되돌릴 수 없는 방식을 구분해야 한다. 열쇠로 풀면 원래 값이 나오는 방식은 그 열쇠가 함께 새어 나가면 무너지므로 비밀번호 저장에 맞지 않다. 비밀번호에는 원래 값으로 되돌리는 것 자체가 불가능한 단방향 변환을 써야 한다. 되돌릴 필요가 없고 대조만 하면 되기에 이 방식이 적합하다.

단순한 단방향 변환의 한계

단방향으로 값을 바꾸기만 하면 안전할 것 같지만, 단순한 변환만으로는 부족하다. 같은 비밀번호는 늘 같은 값으로 바뀌므로, 저장된 값들을 보면 같은 비밀번호를 쓴 사용자들이 드러난다. 흔한 비밀번호일수록 여러 사용자에게서 같은 값이 나타나, 어느 값이 흔한 비밀번호인지 짐작할 수 있게 된다.


더 큰 문제는 미리 계산해 둔 대조표다. 흔히 쓰이는 비밀번호들을 미리 변환해 그 결과를 표로 만들어 두면, 저장된 값을 그 표에서 찾아 원래 비밀번호를 알아낼 수 있다. 단방향 변환이라 되돌릴 수는 없어도, 후보를 미리 변환해 두고 결과를 맞춰 보는 방식으로 우회되는 것이다. 되돌릴 수 없다는 성질이 안전을 보장하지 못한다.


계산이 빠른 변환일수록 이 우회는 쉬워진다. 변환이 빠르면 짧은 시간에 수많은 후보를 시도해 볼 수 있기 때문이다. 저장된 값 하나를 두고 흔한 비밀번호부터 차례로 변환해 맞춰 보는 시도가 빠른 변환에서는 위협적이다. 변환의 속도가 오히려 방어의 약점이 되는 셈이다.


그래서 단순한 단방향 변환에는 두 가지 보강이 필요하다. 하나는 같은 비밀번호가 다른 값으로 저장되게 해 미리 만든 표를 무력화하는 것이고, 다른 하나는 변환을 의도적으로 느리게 해 수많은 후보를 시도하는 일을 감당하기 어렵게 만드는 것이다. 이 둘이 솔트와 느린 계산이다.

솔트가 막는 것

솔트는 각 사용자마다 다른 무작위 값을 비밀번호에 섞어 변환하는 것이다. 같은 비밀번호라도 사용자마다 섞이는 값이 다르므로, 저장되는 결과가 서로 달라진다. 그러면 저장된 값들만 보아서는 누가 같은 비밀번호를 쓰는지 알 수 없고, 흔한 비밀번호가 같은 값으로 드러나지도 않는다.


솔트의 가장 큰 효과는 미리 만든 대조표를 무력화하는 데 있다. 사용자마다 섞이는 값이 다르므로, 후보를 미리 변환해 둔 표가 통하지 않는다. 특정 사용자의 값을 맞추려면 그 사용자의 솔트를 알아내 후보를 하나하나 다시 변환해야 하므로, 여러 사용자를 한꺼번에 노리던 표 방식이 쓸모를 잃는다.


솔트는 비밀이 아니어도 된다. 솔트의 목적은 값을 감추는 것이 아니라 같은 비밀번호가 같은 결과로 저장되는 것을 막는 데 있기 때문이다. 그래서 솔트는 저장된 값과 함께 보관해도 무방하며, 실제로 각 사용자의 변환 결과 옆에 그 솔트를 함께 둔다. 솔트가 드러나도, 후보를 사용자마다 따로 시도해야 한다는 부담은 그대로 남는다.


다만 솔트만으로는 한 사용자를 겨냥해 후보를 시도하는 공격까지 막지는 못한다. 솔트를 알아낸 뒤 그 솔트로 흔한 비밀번호부터 변환해 맞춰 보는 시도는 여전히 가능하다. 솔트가 여러 사용자를 한꺼번에 노리는 표 방식을 막는다면, 한 사용자를 집중해 노리는 시도를 막는 것은 느린 계산의 몫이다.

느린 계산이 막는 것

느린 계산은 변환에 의도적으로 시간과 비용을 들이게 만드는 것이다. 한 번의 변환에 상당한 계산을 요구하면, 흔한 비밀번호부터 차례로 시도하는 공격이 감당하기 어려워진다. 후보 하나를 변환하는 데 드는 시간이 늘어날수록, 수많은 후보를 시도하는 전체 비용이 크게 불어나기 때문이다.


정당한 로그인에서는 이 느림이 문제가 되지 않는다. 한 사용자가 로그인할 때 변환은 한 번만 일어나므로, 그 한 번에 짧은 지연이 더해질 뿐이다. 반면 수많은 후보를 시도하는 쪽에서는 그 지연이 후보 수만큼 곱해져 감당하기 힘든 벽이 된다. 정당한 사용자에게는 가볍고 공격에는 무거운 이 비대칭이 느린 계산의 핵심이다.


느린 계산에는 시간뿐 아니라 메모리를 많이 쓰게 하는 방식도 있다. 계산에 상당한 메모리를 요구하면, 값싼 전용 장비를 여럿 동원해 병렬로 시도하는 방식이 어려워진다. 시간과 메모리를 함께 요구할수록 대규모로 후보를 시도하는 공격의 비용이 커진다. 이런 성질을 갖춘 변환이 비밀번호 저장에 적합하다.


느림의 정도는 조절할 수 있어야 한다. 장비의 성능이 좋아질수록 같은 계산이 빨라지므로, 시간이 지나면 느림의 정도를 높여 방어의 강도를 유지해야 한다. 그래서 비밀번호 변환 방식에는 계산의 무게를 조절하는 손잡이가 있으며, 이 손잡이를 주기적으로 재검토해 방어 수준을 시대에 맞게 유지한다.

무엇을 쓸 것인가

비밀번호 저장에는 솔트와 느린 계산을 갖추도록 설계된 전용 방식을 쓰는 것이 원칙이다. 이런 방식은 각 사용자의 솔트를 자동으로 다루고, 계산의 무게를 조절하는 손잡이를 제공하며, 저장 형식 안에 그 설정을 함께 담는다. 직접 변환 방식을 조합해 만들기보다 검증된 전용 방식에 기대는 편이 안전하다.


이런 전용 방식은 변환 결과 안에 솔트와 계산 설정을 함께 담아 둔다. 그래서 저장된 값 하나만 보아도 어떤 설정으로 만들어졌는지 알 수 있고, 검증할 때 그 설정을 그대로 적용해 대조할 수 있다. 설정을 따로 관리하지 않아도 되므로 운영이 단순해지고, 설정이 어긋나 검증이 실패하는 실수도 줄어든다.


직접 변환 방식을 엮어 비밀번호 저장 방식을 만드는 것은 피해야 한다. 솔트를 빠뜨리거나, 느림을 두지 않거나, 조합을 잘못해 약점을 만들기 쉽기 때문이다. 이미 검증된 전용 방식이 있는데 굳이 직접 만드는 것은 위험을 자초하는 일이다. 방어에서는 검증된 것을 그대로 쓰는 편이 대개 낫다.


선택한 방식의 계산 무게는 환경에 맞게 정한다. 지나치게 무거우면 로그인이 느려지고 지나치게 가벼우면 방어가 약해지므로, 정당한 로그인이 견딜 만한 범위 안에서 되도록 무겁게 잡는다. 이 균형점은 장비 성능에 따라 달라지므로, 한 번 정하고 두는 것이 아니라 주기적으로 재검토해야 한다.

검증과 갱신을 다루기

로그인 시 비밀번호를 검증할 때는 저장된 값에 담긴 솔트와 설정을 꺼내, 입력된 비밀번호를 같은 방식으로 변환해 대조한다. 원래 비밀번호를 저장소에 두지 않고도 일치 여부를 확인하는 것이다. 이 대조는 저장된 방식이 무엇이든 그 안에 담긴 설정을 따르므로, 사용자마다 다른 설정이어도 문제없이 검증된다.


계산 무게를 높였을 때 기존 사용자의 저장 값을 어떻게 옮길지도 다루어야 한다. 저장된 값은 옛 설정으로 만들어졌으므로, 무게를 높이더라도 기존 값은 그대로다. 그래서 사용자가 다음에 로그인해 비밀번호가 확인되는 순간, 그 비밀번호를 새 설정으로 다시 변환해 저장 값을 갱신하는 방식을 쓴다. 로그인을 계기로 저장 값이 조용히 최신 설정으로 옮겨 가는 것이다.


대조 과정에서 걸리는 시간이 정보를 흘리지 않도록 주의해야 한다. 존재하는 계정과 존재하지 않는 계정에서 응답 시간이 눈에 띄게 다르면, 그 차이로 계정의 존재 여부가 새어 나갈 수 있다. 그래서 계정이 없더라도 있는 것과 비슷한 시간을 들여 응답하도록 맞추어, 시간 차이로 정보가 드러나는 것을 줄인다.


비밀번호 저장의 방어는 다른 방어와 겹쳐질 때 더 두꺼워진다. 흔하거나 이미 새어 나간 적이 있는 비밀번호를 처음부터 쓰지 못하게 막고, 로그인 시도의 빈도를 제한하며, 두 번째 인증 수단을 더하는 방어들이 함께 작동하면, 저장 값이 새어 나가더라도 실제 계정 탈취로 이어질 여지가 크게 줄어든다. 이런 겹겹의 방어는 다음 편들에서 이어서 다룬다.


저장을 넘어선 방어의 결합

비밀번호를 안전하게 저장하는 것은 방어의 한 축일 뿐이다. 저장을 아무리 잘해도, 사용자가 약하거나 이미 새어 나간 비밀번호를 쓰면 그 계정은 여전히 취약하다. 그래서 저장의 방어는 비밀번호 자체의 질을 높이는 방어와 함께 가야 한다. 이 둘이 겹칠 때 계정이 실질적으로 안전해진다.


첫 번째로 겹칠 방어는 약한 비밀번호를 처음부터 막는 것이다. 지나치게 짧거나 흔한 비밀번호는 후보를 시도하는 공격에 쉽게 뚫리므로, 가입이나 변경 시점에 이런 비밀번호를 거른다. 다만 지나치게 복잡한 규칙을 강요하기보다, 충분한 길이를 권하고 흔한 값을 막는 방향이 사용자에게도 방어에도 낫다.


두 번째는 이미 새어 나간 적이 있는 비밀번호를 막는 것이다. 다른 곳에서 새어 나가 널리 알려진 비밀번호는, 우리 서비스에서 처음 쓰더라도 위험하다. 그런 비밀번호를 후보로 시도하는 공격이 흔하기 때문이다. 그래서 새어 나간 것으로 알려진 비밀번호를 가입 시점에 걸러, 처음부터 쓰지 못하게 하는 방어가 쓰인다.


세 번째는 로그인 시도 자체를 제한하고 두 번째 인증 수단을 더하는 것이다. 짧은 시간에 수많은 로그인을 시도하는 것을 막으면 후보를 실제로 던져 보는 일이 어려워지고, 비밀번호 외에 다른 인증 수단을 더하면 비밀번호가 새어 나가도 그 하나만으로는 들어오지 못한다. 이런 방어들은 뒤 편들에서 이어서 다루며, 저장의 방어와 겹겹이 쌓일 때 가장 두꺼운 벽이 된다.

정리하면 비밀번호는 원래 형태로 저장하지 않고 되돌릴 수 없는 형태로 바꾸어 저장하되, 단순한 단방향 변환만으로는 부족하다. 솔트가 미리 만든 표를 무력화하고 느린 계산이 후보를 시도하는 공격을 감당하기 어렵게 만들며, 이 둘을 갖춘 전용 방식을 검증된 그대로 쓰는 것이 원칙이다. 다음 편에서는 비밀번호 자체를 다루지 않고 외부의 신뢰에 기대는 소셜 로그인을 들여다본다.