Salted Password Hashing – Doing it Right 번역

Salted Password 해싱 – 올바르게 하기.

원문 링크


만약 당신이 웹 개발자라면 사용자 계정 시스템을 만들어야 했던 적이 있을 것입니다. 사용자 계정 시스템에서 무엇보다도 중요한 부분은 사용자 비밀번호를 어떻게 보호할 것인가입니다. 사용자 계정 데이터베이스는 자주 해킹되기 때문에, 웹사이트가 한번이라도 뚫렸던 적이 있다면 사용자의 비밀번호를 보호하기 위한 무언가를 반드시 해야합니다. 비밀번호를 보호하는 제일 좋은 방법은 Salted password hashing을 하는 것입니다. 이 페이지는 왜 그 방법으로 되어있어야 하는지 설명할 것입니다.

아마 웹에 잘못된 정보가 많아서, 어떻게 비밀번호를 적절하게 해싱해야하는지에 대한 많은 수의 서로 충돌하는 아이디어와 잘못된 개념이 퍼져있습니다. 비밀번호 해싱은 그런 방법들중 간단한 방법이지만, 많은 사람들이 잘못 이해하고 있는 것이기도 합니다. 이 페이지에선 비밀번호 해싱을 제대로 하는 방법을 설명하는 것에 그치지 않고, 왜 그렇게 해야하는지도 설명할 수 있을것입니다.

중요한 경고!!
만약 당신이 자신만의 비밀번호 해싱 코드를 작성하려 생각중이라면, 제발 그러지마십시오! 그런건 쉽게 부술수 있습니다. 대학에서 암호학 수업을 수강했다고 해도 이 경고에 해당이 안되는 것이 아닙니다. 이건 모두에게 해당되는 경고입니다. 자기 자신만의 암호화 코드를 작성하지 마십시오! 비밀번호를 저장하는 문제는 이미 해결되었습니다. phpass를 사용하거나, 이 페이지에 있는 코드를 사용하십시오

만약 어떤한 이유로 위의 커다란 붉은 경고를 놓쳤다면, 지금바로 읽고 오십시오. 이 가이드는 당신이 자신만의 저장 시스템을 만드는걸 안내하는 것이 아닙니다. 이 가이드는 왜 비밀번호가 특정한 방법으로 저장되어야 하는 이유를 설명하는 글입니다.

밑의 링크를 이용해서 이 페이지의 다른 섹션으로 이동할 수 있습니다.

1. 비밀번호 해싱이랑 무엇인가?
2. 해시는 어떻게 뚫리는가
3. 소금치기
4. 효과적이지 않은 해싱 방법
5. 어떻게 적절한 해싱을 하는가
6. 자주 묻는 질문들

BSD 라이센스의 비밀번호 해싱 소스 코드는 이 페이지의 아래에 있습니다.
(역자 주: 밑의 링크를 클릭하면 원문 페이지로 갑니다)

PHP Source Code

Java Source Code

ASP.NET (C#) Source Code

Ruby (on Rails) Source Code


비밀번호 해싱이랑 무엇인가?

hash("hello") = 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
hash("hbllo") = 58756879c05c68dfac9866712fad6a93f8146f337a69afe7dd238f3364946366
hash("waltz") = c0e81794384491161f1777c232bc6bd9ec38f616560b120fda8e90f383853542

해시 알고리즘은 일방향 함수입니다. 해시 함수는 어떤 크기의 데이터라도 복원 불가능한 고정된 길이의 “지문”으로 만듭니다. 또한 해시 함수는 입력 데이터가 매우 적은 비트만 바뀌어도 결과 해시 값은 완전히 달라지게 됩니다(위의 예제를 보십시오). 우리는 비밀번호가 노출되어도 보호하길 원하고, 동시에 사용자가 입력한 비밀번호가 올바른지 확인 할 수 있기를 바라기 때문에, 해시 함수는 비밀번호를 보호하는데 매우 좋습니다.

해시 기반 시스템에서의 일반적인 계정 등록과 인증 절차는 다음과 같습니다.

  1. 사용자가 계정을 생성한다.
  2. 사용자의 비밀번호는 해시되어 데이터베이스에 저장된다. 어떤 지점에서도 플레인 텍스트(암호화되지 않은 텍스트) 비밀번호는 하드디스크에 쓰여지지 않는다.
  3. 사용자가 로그인을 시도할때, 사용자가 입력한 비밀번호의 해시 값을 진짜 비밀번호의 해시값(데이터베이스에서 가져온 해시 값)과 비교한다.
  4. 해시 값끼리 서로 일지하면, 사용자는 접근 권한을 얻는다. 그렇지 않다면 유효하지 않은 로그인 정보라고 사용자에게 알려준다.
  5. 3단계와 4단계를 모든 사용자가 계정에 로그인 할때마다 반복한다.

4단계에서 사용자에게 절대로 username이나 password가 잘못됐다고 얘기하지 마십시오. 언제나 “username이나 password가 잘못되었습니다.”라는 일반적인 메시지를 출력하십시오. 이는 공격자가 비밀번호를 모르는 상태로 유효한 username을 열거하게 되는 것을 막습니다.
(역자주: 사용자가 입력한 username과 password중 어느것이 잘못 입력되었는지 자세하게 알려주지 말고, “둘 중에 하나가 잘못 입력 되었습니다”라는 애매한 메시지를 출력하라는 의미입니다. 만약 특정 값이 잘못 되었다고만 알려준다면 본문에서 언급한것처럼 임의의 비밀번호를 입력하고 username만 바꿔가며 서버에 인증을 요청하면서 데이터베이스에 어떤 username이 존재하는지 알아 낼 수 있게 될 것입니다.)

여러분은 여러분이 데이터 구조 시간에 배운 해시 함수와, 비밀번호를 보호하기 위해 쓰이는 해시 함수가 같지 않다는 것을 알아야 합니다. 데이터 구조를 구현할때 쓰인 해시 함수는, 않전하지 않고, 빠르게 작동하도록 디자인된 것입니다. 오직 암호화 해시 함수 구현만이 비밀번호 해싱에 이용 될 수 있습니다. SHA256, SHA512, RipeMD 또는 WHIRLPOOL와 같은 해시 함수가 암호화 해시 함수입니다.

암호화 해시 함수에 비밀번호를 넣고 실행시키기만 하면 사용자의 비밀번호가 안전하게 보호될 것이라고 생각하기 쉽습니다. 하지만 그건 사실과 많이 다릅니다. 플레인 해시에서 매우 빠르게 비밀번호를 복원하는 방법은 많습니다. 그리고 그런 “공격들”을 덜 효과적으로 만드는 구현하기 쉬운 테크닉들이 몇가지 있습니다. 그러한 테크닉들에 대한 필요성 좀 더 느끼게 하기 위해, 이 간단한 웹사이트를 둘러봅시다. 첫번째 페이지에서 크랙할 해시 리스트를 제출할 수 있습니다. 그리고 1초 안에 결과를 얻을 수 있습니다. 간단한 비밀번호 해싱은 우리의 보안 기준을 만족하지 못한다는 것이 명확합니다.

다음 섹션은 플레인 비밀번호 해시값들을 공격하는 방법에 대해서 얘기할 것입니다.

해시는 어떻게 뚫리는가

사전 공격Dictionary attacks과 브루트 포스 공격Brute-force attacks

Dictionary Attack

Trying apple : failed
Trying blueberry : failed
Trying justinbeiber : failed
...
Trying letmein : failed
Trying s3cr3t : success!success!
Brute Force Attack

Trying aaaa : failed
Trying aaab : failed
Trying aaac : failed
...
Trying acdb : failed
Trying acdc : success!

해시값을 크랙하는 제일 쉬운 방법은 비밀번호를 유추하고, 유추한 비밀번호 각각을 해싱한 다음, 해싱한 유추 값들을 크랙할 해시 값이랑 같은지 체크를 하는 것입니다. 만약 해시값이 서로 같다면, 유추값은 비밀번호라는 말이 됩니다. 두가지 일반적인 비밀번호 유추 방법은 사전 공격Dictionary attacks브루트 포스 공격Brute-force attacks입니다.

사전 공격은 단어, 문장, 일반적인 비밀번호, 그외 비밀번호로 쓰일만한 문자열을 담은 파일을 이용합니다. 파일에 담긴 각각의 단어들을 해싱하여 비밀번호 해시와 비교합니다. 만약 서로 일치한다면, 그 단어는 비밀번호입니다. 이 사전파일들은 수많은 텍스트의 본문에서 추출한 단어들뿐만 아니라 진짜 비밀번호 데이터베이스에서 추출한 단어들로 만들어집니다. 그 사전파일들을 좀 더 효율적으로 만들기 위해 “leet speak”와 같은 단어를 교체하는 작업(“hello”가 “h3110″로 됨)들도 보통 적용됩니다.

브루트포스 공격은 주어진 길이안에서 가능한 모든 문자의 조합을 시도합니다. 그 공격들을 계산하는 것은 매우 비싸며(역자주: 공격하기 위한 계산을 하는데 비용이나 시간이 많이 들어간다는 얘기), 계산 시간에 비해 크랙된 해시를 찾아내는 효율이 최저입니다. 하지만 언제나 반드시 비밀번호를 찾아내게끔 되어있습다. 비밀번호는 충분히 길어야 가치가 있으며, 그렇게 함으로써 모든 가능한 조합의 문자열을 시도하는것이 너무나 오래 걸리게끔 해야합니다.

사전 공격이나 브루트 포스 공격을 막는 방법은 없습니다. 효율적이지 않게 만들수는 있지만, 그 공격을 막을 방법은 없습니다. 만약 비밀번호 해싱 시스템이 안전하다면, 그걸 크랙하는 유일한 방법은 각각의 해시에 대해 사전 공격을 하거나 브루트 포스 공격을 하는 것 뿐입다.

룩업 테이블Lookup Tables

Searching: 5f4dcc3b5aa765d61d8327deb882cf99: FOUND: password5
Searching: 6cbe615c106f422d23669b610b564800: not in database
Searching: 630bf032efe4507f2c57b280995925a9: FOUND: letMEin12
Searching: 386f43fab5d096a7a66d67c8f213e5ec: FOUND: mcd0nalds
Searching: d5ec75d5fe70d428685510fae36492d9: FOUND: p@ssw0rd!

룩업 테이블은 같은 타입의 많은 해시들을 빨리 크랙하는 극도로 효율적인 방법입니다. 비밀번호 사전에 있는 비밀번호들의 해시를 사전에 계산해두고 그에 대응하는 비밀번호를 룩업 테이블 데이터 구조에 저장해두는 것이 룩업 테이블의 기본적인 개념입니다.(역자주: 룩업 테이블 데이터 구조는 RAM에 저장됩니다.) 잘 구현된 룩업 테이블은 그 안에 10억개의 해시를 담고 있다고 해도, 초당 수백개의 해시를 처리할 수 있습니다.

만약 룩업 테이블이 얼마나 빠른지 더 잘 알고 싶다면, 다음의 sha256 해시값들을 CrackStations의 free hash cracker로 크랙해보세요.

c11083b4b0a7743af748c85d343dfee9fbb8b2576c05f3a7f0d632b0926aadfc
08eac03b80adc33dc7d8fbe44b7c7b05d3a2c511166bdb43fcb710b03ba919e7
e4ba5cbd251c98e6cd1c23f126a3b81d8d8328abc95387229850952b3ef9f904

리버스 룩업 테이블

Searching for hash(apple) in users' hash list... : Matches [alice3, 0bob0, charles8]
Searching for hash(blueberry) in users' hash list... : Matches [usr10101, timmy, john91]
Searching for hash(letmein) in users' hash list... : Matches [wilson10, dragonslayerX, joe1984]
Searching for hash(s3cr3t) in users' hash list... : Matches [bruce19, knuth1337, john87]
Searching for hash(z@29hjja) in users' hash list... : No users used this password

이 공격은 사전에 계산된 룩업 테이블 없이 사전 공격 방법이나 브루트 포스 공격 방법을 공격자가 많은 해시에 한번에 적용할 수 합니다.

먼저, 공격자는 획득한 사용자 계정 데이터베이스에서 해시들을 가져다 어떤 사용자가 그 해시에 대응되는지에 대한 룩업 테이블을 만듭니다. 그후 공격자는 유추 비밀번호를 해시하여 룩업 테이블에서 어떤 사용자가 해당 유추 비밀번호를 사용하는지 목록을 획득합니다. 이 공격은 특히나 효과적인데, 사람들이 보통 같은 비밀번호를 사용하는 경우가 많기 때문입니다.

레인보우 테이블

레인보우 테이블은 시간-메모리 교환 테크닉입니다. 룩업 테이블과 비슷하지만, 레인보우 테이블은 해시를 크랙하는 속도를 희생하여 룩업 테이블을 더 작게 만듭니다. 룩업 테이블이 더 작기 때문에, 레인보우 테이블은 같은 공간에 더 많은 해시를 저장 해 둘 수 있고, 해시들이 더 효과적이 되게 만들 수 있습니다. 레인보우 테이블은 존재하는 모든 8자리까지의 md5 해시를 크랙할 수 있습니다.

(역자주: 룩업 테이블은 시작시에는 HDD에 저장된 사전 파일만을 가지고 있습니다. 그리고 크랙킹을 시작할때 초기화 과정중 하나로 사전들을 해싱하여 램에 올려두게 됩니다. 레인보우 테이블은 그러한 해싱 결과 까지 전부 파일로 만들어두고, 램에는 오직 해시값만 올리는 기법입니다. 따라서 시간-메모리 교환이라고 본문에서 설명하게 되는 것입니다.)

다음으로, 우리는 솔팅Salting이라 불리는 테크닉을 볼것입니다. 솔팅은 룩업 테이블과 레인보우 테이블을 사용하여 해시를 크랙하는 것을 불가능하게 만들어줍니다.

소금치기Adding Salt

hash("hello") = 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
hash("hello" + "QxLUF1bgIAdeQX") = 9e209040c863f84a31e719795b2577523954739fe5ed3b58a75cff2127075ed1
hash("hello" + "bv5PehSMfV11Cd") = d1d3ec2e6f20fd420d50e2642992841d8338a314b8ea157c9e18477aaef226ab
hash("hello" + "YYLmfY6IehjZMQ") = a49670c3c18b9e079b9cfaf51634f563dc8ae3070db2c4a8544305df1b60f007

룩업 테이블과 레인보우 테이블은 각각의 비밀번호가 완전히 동일한 방법으로 해시되기 때문에 작동합니다. 만약 두명의 사용자가 똑같은 비밀번호를 가진다면, 그 둘은 똑같은 비밀번호 해시값을 가질것입니다. 각각의 해시를 임의로 하여 이러한 공격을 방지할 수 있습니다. 그래서 같은 비밀번호가 두번 해시되어도, 해시값은 같지 않게 됩니다.

임의로 해시를 하기위해, 해시를 하기 전에 솔트salt라 부르는 임의의 문자열을 비밀번호의 앞이나 뒤에 덧붙입니다. 위의 예에서 보인것처럼, 이 방법은 같은 비밀번호의 해시가 매번 완전히 다르게 만들어줍니다. 만약 비밀번호가 같은지 확인하려면 솔트가 필요합니다. 따라서 솔트는 보통 사용자 계정 데이터베이스에 해시와 같이 저장되거나, 해시 문자열 그 자체의 일부를 사용합니다.

솔트는 비밀일 필요가 없습니다. 임의로 해시를 하는 것만으로도, 룩업 테이블, 리버스 룩업 테이블, 레인보우 테이블은 효과가 없어집니다. 공격자는 솔트가 어떤것이 될지 미리 알 수 없으며, 그렇기에 공격자는 룩업 테이블이나 레인보우 테이블을 사전에 계산해둘 수 없습니다. 만약 각각의 사용자의 비밀번호가 다른 솔트로 해시된다면, 리버스 룩업 테이블 공격 또한 작동하지 않습니다.

다음 섹션에서는 일반적으로 제대로 구현되지 않은 솔트를 볼 것입니다.

잘못된 방법: 짧은 솔트 & 솔트의 재사용

대부분의 솔트 구현 에러는 같은 솔트를 여러 해시에 재사용 하거나, 너무 짧은 솔트를 쓰는 것입니다.

솔트 재사용

일반적인 실수는 같은 솔트를 각각의 해시에 사용하는 것입니다. 솔트가 프로그램에 하드코딩 되어있거나, 임의로 한번만 생생됩니다. 만약 두 사용자가 같은 비밀번호를 쓴다면, 여전히 그 둘은 같은 해시를 가지기 때문에 이러한 방법은 비효율적입니다. 공격자는 여전히 리버스 룩업 테이블 공격을 사용하여 사전 공격을 모든 해시에 동시에 할 수 있습니다. 공격자는 유추 비밀번호를 해시하기 전에 각각의 유추 비밀번호에 솔트를 적용하기만 하면 됩니다. 만약 솔트가 대중적인 제품에 하드코딩 되어있다면, 룩업 테이블과 레인보우 테이블을 그 솔트만을 위해서 만들어질 수 있습니다. 따라서 해당 제품에 의해 생성된 해시를 크랙하는 것이 쉬워집니다.

사용자가 계정을 만들거나 비밀번호를 바꿀 때마다 새로운 임의의 솔트가 생성되어야 합니다.

짧은 솔트

만약 솔트가 너무 짧다면, 공격자는 모든 가능한 솔트에 대해서 룩업 테이블을 만들 수 있습니다. 예를 들어 솔트가 오직 3글자의 ASCII 캐릭터라면, 가능한 솔트는 오직 95x95x95 = 857,375개 뿐입니다. 많아 보이지만, 각각의 룩업 테이블이 오직 1MB의 일반적인 비밀번호를 담고 있다면, 모든 경우를 계산해도 837GB밖에 되지 않습니다. 1000GB 하드 드라이브를 $100 미만으로 살수 있는 요즘에는 크게 고려할만한 크기가 아닙니다.

같은 이유로, username 또한 솔트로 이용되면 안됩니다. Username은 하나의 서비스에서는 유일할 수 있지만, 예측 가능하며 종종 다른 서비스에서도 재사용됩니다. 공격자는 일반적인 username에 대한 룩업 테이블을 만들어 username으로 솔트된 해시들을 크랙할 수 있습니다.

모든 가능한 솔트에 대한 룩업 테이블을 만드는 것을 불가능하게 만드려면, 솔트는 반드시 길어야 합니다. 일반적으로 좋은 규칙은, 해시 함수의 출력과 같은 크기의 솔트를 사용하는 것입니다. 예를 들어 SHA256의 출력은 256bits(32bytes)이기에, 솔트도 32개의 임의의 바이트어야 합니다.

잘못된 방법: 더블 해싱 & 이상한 해시 함수

이 섹션은 비밀번호 해싱에 대한 일반적인 오해를 다룹니다. 해시 알고리즘의 이상한 조합말이죠. 도취하기 쉽고, 다른 해시 함수들을 조합하여 시도하여, 그 결과가 좀 더 안전할 것이라 희망하는 것이죠. 하지만 실제로는, 그렇게 하는것은 매우 작은 이득밖에 없습니다. 오직 상호 운영성 문제만을 만들 뿐이며, 때론 해시를 안전하지 못하게 만들기도 합니다. 자기 자신만의 암호를 발명하려고 시도하지 말고, 전문가에 의해 디자인된 스탠다드를 사용하십시오. 여러개의 해시 함수를 사용하는 것이 해시를 계산하는 것을 느리게 만들고, 그로인해 크랙킹이 느려진다고 누군가는 주장할 것입니다. 하지만 잠시후에 보실 것처럼, 크랙킹 처리를 느리게 만드는 더 나은 방법이 있습니다.

여기 내가 본 인터넷 포럼에서 제안되었던 좋지 못한 이상한 해시함수들의 예가 있습니다.

  • md5(sha1(password))
  • md5(md5(salt) + md5(password))
  • sha1(sha1(password))
  • sha1(str_rot13(password + salt))
  • md5(sha1(md5(md5(password) + sha1(password)) + md5(password)))

위의 어떤 것들도 사용하지 마십시오.

Note: 이 섹션은 논란이 많다고 입증 되었습니다. 이상한 해시 함수들이 좋은 것이라는 몇개의 이메일을 받았습니다. 공격자가 어떤 해시 함수가 사용되는지 모른다면, 공격자가 미리 계산된 레인보우 테이블을 이상한 해시 함수를 위해 준비 할 수 없고, 계산하는데 더 오래걸리기에, 이상한 해시 함수가 더 낫다는 것이었습니다.

공격자는 알고리즘을 모를때 해시를 공격할 수 있습니다. 하지만 Kerckhoffs’s principle에 주의하자면, 공격자는 대개 소스코드에도 접근하며(특히 그게 무료거나 오픈소스 소프트웨어일 경우), 목표 시스템에 대한 적은 수의 비밀번호-해시 쌍을 가지게 됩니다. 그러면 그 알고리즘을 리버스 엔지니어링 하는것은 그다지 어려운 일이 아니게 됩니다. 이상한 해시 함수를 계산하는데 더 오래 걸리게 하지만, 오직 작은 변하지 않는 요소에 의존합니다. 극도로 병렬처리 하기 힘든 반복 알고리즘을 사용하는게 더 낫습니다(밑에서 다룰 것입니다). 그리고 적절하게 솔팅된 해시는 레인보우 테이블 문제를 해결합니다.

만약 “이상한” 해시 함수를 HMAC처럼 스탠다드화 하고 싶다면, 괜찮습니다. 하지만 이상한 해시 함수를 만드는 이유가 해시 계산을 느리게 하는 것이라면, 밑의 섹션의 키 스트레칭을 먼저 읽어보세요.

중요하지 않은 이득과 완전히 안전하지 않은 해시 함수를 뜻하지 않게 구현하게 되는 리스크와, 이상한 해시 함수가 만들 상호 운영성 문제를 비교해 보십시오. 스탠다드와 잘 테스트된 알고리즘을 사용하는 것이 분명한 최고입니다.

해시 충돌

해시 함수는 임의의 크기의 테이터를 고정된 길이의 문자열오 바꾸기 때문에, 같은 문자열로 해시되는 입력들이 있습니다. 암호화 해시 함수는 그러한 충돌을 찾아내는 것이 엄청나게 어렵게 디자인 되었습니다. 수시로 암호학자들은 해시 함수에 대한 충돌을 쉽게 찾아내는 “공격”을 찾아냈습니다. 최근의 예는 충돌이 실제로 찾아진 MD5 해시 함수입니다.

충돌 공격은 사용자의 비밀번호가 아닌 같은 해시를 가지는 다른 문자열일 수 있다는 신호입니다. 하지만 가장 얀학 MD5같은 함수라고 해도 충돌을 찾아내는 것은 전용의 많은 컴퓨팅 파워를 필요로 합니다. 때문에 실제로 “우연히” 그러한 충돌이 발생할 일은 적습니다. MD5와 솔트로 해시된 비밀번호는 모든 실용적인 목적에 SHA256과 솔트로 해시된 것과 똑같이 안전합니다. 그럼에도 불구하고 가능하다면 SHA256, SHA512, RipeMD, WHIRLPOOL과 같은 더 안전한 해시 함수를 이용하는 것이 더 좋은 생각입니다.

올바른 방법: 어떻게 적절하게 해싱을 하는가

이 섹션은 비밀번호가 정확히 어떻게 해시 되어야 하는지를 설명합니다. 첫번째 하위 섹션은 기본을 다룹니다. 모든것이 절대적으로 필요한 것들이죠. 뒤따라오는 하위 섹션들은 기본이 어떻게 강화되어 해시를 크랙에 더 강하게 만드는지 설명합니다.

기본: 솔트를 이용하여 해싱하기

경고: 이 섹션을 그냥 읽지만 마십시오. 당신은 절대적으로 반드니 다음 섹션인 “비밀번호 크랙을 더 어렵게 하기: 느린 해시 함수들”에서 구현해야 합니다

우린 악의적인 해커가 어떻게 룩업 테이블과 레인보우 테이블을 사용하여 매우 빠르게 플레인 해시를 크랙하는지를 보았습니다. 우린 솔트를 이용하여 해시를 임의로 하는것이 문제에 대한 해결책임을 배웠습니다. 하지만 어떻게 솔트를 생성하고, 그걸 비밀번호에 적용할까요?

솔트는 암호학적으로 안전한 의사 랜덤 숫자 생성기Cryptographically Secure Pseudo-Random Number Generator (CSPRNG)를 사용하여 생성 되어야 합니다. CSPRNG는 “C” 언어의 rand()와 같은 다른 의사 랜덤 숫자 생성기와는 매우 달리, 이름에서 알 수 있든, CSPRNG은 암호학적으로 안전하게 디자인 되었습니다. 즉 완벽하게 예측 불가능한 고수준의 랜덤성을 제공한다는 뜻이죠. 우리는 솔트가 예측 가능하길 원하지 않습니다. 그래서 반드시 CSPRNG을 써야합니다. 밑의 표는 대중적인 프로그래밍 플랫폼에서 제공하는 CSPRNG를 나열하고 있습니다.

Platform CSPRNG
PHP mcrypt_create_iv, openssl_random_pseudo_bytes
Java java.security.SecureRandom
Dot NET (C#, VB) System.Security.Cryptography.RNGCryptoServiceProvider
Ruby SecureRandom
Python os.urandom
Perl Math::Random::Secure
C/C++ (Windows API) CryptGenRandom
Any language on GNU/Linux or Unix Read from /dev/random or /dev/urandom

솔트는 사용자마다 비밀번호마다 유일해야 합니다. 사용자가 계정을 만들거나 비밀번호를 바꿀때마다, 비밀번호는 새로운 임의의 솔트로 해시되어야 합니다.절대로 솔트를 재사용하지 마십시오. 또한 솔트는 길어야 하며, 그럼으로써 가능한 많은 솔트가 있게합니다. 일반적으로 좋은 규칙처럼, 솔트의 길이는 최소한 해시함수의 출력과 같아야 합니다. 솔트는 사용자 계정 테이블에 해시와 같이 저장되어야 합니다.

비밀번호를 저장 할 때
1. CSPRNG을 이용하여 긴 임의의 솔트를 생성하기
2. 비밀번호 앞에 솔트를 덧붙여서 SHA256과 같은 스탠다드 암호화 해시 함수로 해싱하기
3. 솔트와 해시 둘 다 사용자의 데이터베이스 레코드에 저장하기.

비밀번호를 확인 할 때
1. 사용자의 솔트와 해시를 데이터베이스에서 가져오기
2. 주어진 비밀번호 앞에 솔트를 덧붙여서 동일한 해시 함수를 이용하여 해싱하기
3. 주어진 비밀번호의 해시와 데이터베이스에서 가져온 해시를 비교하기. 만약 일치한다면 비밀번호가 맞은 것입니다. 아니라면 비밀번호가 틀린것입니다.

이 페이지의 밑에,  PHP, C#, Java, 그리고 Ruby 를 이용하여 솔티드 패스워드 해싱을 구현한 것이 있습니다.

웹 애플리케이션에선, 언제나 서버에서 해시하십시오
만약 웹 애플리케이션을 작성중이라면, 어디에서 해시를 해야할지 고민일 것입니다. 사용자 브라우저에서 자바스크립트로 해시를 하나, 아니면 “깨끗한” 서버로 전송해 거기서 해시를 해야하나?

만약 사용자의 비밀번호를 자바스크립트로 해싱하더라고 여전히 해시를(역자주: 브라우저에서 한번 자바스크립트로 해싱한 결과 해시값) 서버에서 해싱해야 합니다. 사용자의 비밀번호를 사용자의 브라우저에서 해싱하여 서버에서 해시를 해싱하지 않는 웹사이트를 가정해봅시다. 사용자를 인증하기 위해 이 웹사이트는 브라우저에서 보내진 해시를 데이터베이스에 있는 해시와 정확히 일치하는지 확인할 것입니다. 이는 서버에서 해싱하는것보다 더 안전해 보입니다. 사용자의 비밀번호가 절대로 서버로 전송되지 않기 때문이죠. 하지만 아닙니다.

문제는 클라이언트측의 해시값이 논리적으로 사용자의 비밀번호가 될 수 있다는 것 입니다. 모든 사용자는 인증받기 위해 자신의 비밀번호 해시를 서버에 얘기해야 합니다. 만약 배드가이가 사용자의 해시를 획득하면 그들은 그 해시로 서버에 인증받기 위해 이용할 수 있습니다. 사용자의 비밀번호를 전혀 알지도 못하는채 말이죠! 그래서 만약 배드가이가 이 가상의 웹사이트의 데이터베이스를 훔쳐낸다면, 그들은 즉시 모든 사용자의 계정에 비밀번호를 유추하는 과정없이 접근이 가능합니다.

이는 브라우저에서 해시하지 말라고 얘기하는것은 아닙니다. 하지만 브라우저에서 해시를 해야겠다면, 서버에서도 해시하십시오. 브라우저에서 해싱하는것은 확실히 좋은 생각입니다. 하지만 구현할때 다음을 고려하십시오.

  • 클라이언트 측 해싱은 HTTPS(SSL/TLS)를 대체할 수 없습니다. 만약 브라우저와 서버 사이의 연결이 안전하지 않다면, Man-in-the-middle은 자바스크립트 코드를 수정하고, 다운로드 되게 하여 해싱 기능을 제거해서 사용자의 비밀번호를 획득 할 수 있습니다.
  • 일부 웹 브라우저는 자바스크립트를 지원하지 않습니다. 그리고 일부 유저들은 자바스크립트를 브라우저에서 비활성화 해둡니다. 그러므로 최대의 호환성을 위해 앱은 어떤 브라우저가 자바스크립트를 지원하는지 감지해야 하고, 자바스크립트가 되지 않는다면 클라이언트측 해싱을 서버에서 에뮬레이션해야 합니다.
  • 클라이언트 측 해시도 똑같이 솔트처리 되어야 합니다. 눈에 보이는 해결책은 클라이언트 측 스크립트가 서버에 사용자의 솔트를 요청하도록 만드는 것입니다. 그렇게 하지 마십시오. 이렇게 하는것은 배드가이들이 비밀번호를 알지 않은 채로 username이 유효한지 확인할 수 있게 만듭니다. 서버에서도 해싱과 솔팅(제대로 된 솔트)을 하게되므로, username(혹은 email)을 특정 사이트에 관련된 문자열(도메인 네임등)과 연결하고 클라이언트 측 솔트처럼 쓰는것도 괜찮습니다.

비밀번호 크랙킹을 어럽게 하기: 느린 해시 함수

솔트는 공격자가 룩업 테이블이나 레인보우 테이블과 같이 많은 양의 해시 모음을 크랙하는 데 전문화된 공격은 사용 할 수 없게 보장해주지만, 사전 공격이나 브루트포스 공격과 같이 각각의 해시에 대해 개별적으로 행해지는 공격은 방지하지 못합니다. 하이엔드 그래픽 카드(GPUs)와 커스텀 하드웨어는 초당 몇십억개의 해시를 계산할 수 있습니다. 그래서 그런 공격은 여전히 효과적입니다. 그런 공격을 덜 효과적으로 만들기 위해서, 우린 키 스트레칭으로 알려진 테크닉을 사용 할 수 있습니다.

이 아이디어는 해시 함수를 매우 느리게 만들어서, 빠른 GPU나 커스텀 하드웨어를 이용하더라도, 사전 공격이나 브루트 포스 공격이 쓸모 없어질만큼 느려집니다. 목표는 공격을 지연시키기에 충분할 만큼 해시 함수를 느리게 만들면서, 사용자가 눈치 챌 수 없을 정도의 충분히 빠른 지연을 만드는 것입니다.

키 스트레칭은 특수한 타입의 CPU 집약적 해시 함수를 이용해서 구현됩니다. 하드웨어에서 병렬화 될수 있거나 일반 해시처럼 빨리 실행 될 수 있는, 비밀번호를 해시하는 자신만의 간단한 반복적 해싱 함수를 발명하려 하지 마십시오. PBKDF2bcrypt와 같은 스탠다드 알고리즘을 사용하십시오. 여기에서 PBKDF2의 PHP 구현을 찾을 수 있습니다.

이 알고리즘들은 보안 요소나 반복 카운트를 인자로 받습니다. 이 값은 해시 함수가 얼마나 느려질지를 결정합니다. 데스크탑 소프트웨어나 스마트폰 앱을 위한 이 파라미터의 값을 선택하는 최선의 방법은, 짧은 디바이스에서 벤치마크를 돌려서 해시가 0.5초 정도 걸리게 하는 값을 찾아내게 하는 것입니다. 이 방법으로 프로그램은 사용자 경험에 영향을 끼치지 않으면서도 안정하게 될 수 있습니다.

만얀 키 스트레칭 해시를 웹 애플리케이션에서 이용한다면, 많은 양의 인증 리퀘스트를 처리할 추가적인 컴퓨테이셔널 리소스를 필요로 하게 될 것임에 주의 하십시오. 그리고 키 스트레칭은 웹 사이트에 대한 서비스 거부 공격(DoS)을 더 쉽게 반들어 줄것입니다. 키 스트레칭 사용을 여전히 권하지만, 적은 반복 횟수로 사용하십시오. 반복 횟수를 컴퓨테이셔널 리소스와 예상되는 최대 인증 리퀘스트율에 기반하여 계산해야 합니다. 서비스 거부 위협은 사용자가 매 로그인마다 CAPTCHA를 풀게 하여 제거할 수 있습니다. 언제나 반복횟수가 미래에 증가되거나 감소 될 수 있게 시스템을 디자인 하십시오.

만약 계산이 부담이 될 것을 염려하고 있다면, 그래도 여전히 키 스트레칭을 웹 애플리케이션에서도 쓰고 싶어한다면, 사용자의 브라우저에서 자바스크립트로 키 스트레칭 알고리즘을 실행하는 것을 고려하십시오. The Stanford JavaScript Crypto Library는 PBKDF2를 포함합니다. 반복 횟수는 모바일 디바이스같이 더 느린 시스템에서도 사용 가능할 정도로 낮아야 하며, 시스템은 사용자의 브라우저가 자바스크립트를 지원하지 않을때 서버측으로 계산을 되넘겨야 합니다. 클라이언트 측의 키 스트레칭은 서버측의 해싱 필요를 없애는것이 아닙니다. 일반적인 비밀번호를 해싱하는 것과 같은 방법으로, 클라이언트에서 생성된 해시값을 해싱해야 합니다.

크랙 불가능한 해시: Keyed 해시와 비밀번호 해싱 하드웨어

공격자는 유추한 비밀번호가 맞는지 틀리는지를 확인할때 해시를 사용 할 수 있기 때문에, 해시에 대해 사전 공격이나 브루트 포스 공격을 할 수 있습니다. 다음 단계는 비밀 키를 해시에 넣는 것입니다. 그럼으로써 키를 아는 사람만이 비밀번호의 유효성 검사를 할 때 해시를 사용 할 수 있게 할 수 있습니다. 이는 두가지 방법으로 달성 할 수 있습니다. 해시를 AES를 통해 암호화 할 수 있게 하거나, 비밀 키를 HMAC와 같은 keyed 해시 알고리즘을 이용해 해시에 포함하게 할 수 있습니다.

이는 말처럼 쉽지만은 않습니다. 키는 공격 당했을때조차 공격자에게서 숨겨져야 합니다. 만약 공격자가 시스템에 대한 전체 접근 권한을 획득하게 된다면, 공격자는 키가 어디에 저장되어있든간에 훔칠 수 있게 될 것입니다. 물리적으로 분리되어 비밀번호 유효성 검사만을 위한 서버나, YubiHSM와 같이 서버에 추가되는 특별한 하드웨어처럼,키는 반드시 외부 시스템에 저장되어야 합니다.

전 이 접근 방법을 큰 스케일(10만명 이상의 사용자)의 서비스에 적극 권장합니다. 전 100만명 이상의 사용자 계정을 호스팅하는 어떤 서비스라도 이게 필요하다고 생각합니다.

만약 복수의 전용 서버나 특별한 하드웨어를 갖출 수 없다고 해도, 스탠다드 서버에서 keyed 해시의 장점을 조금이나마 취할 수 있습니다. 상당수 데이터베이스는 대부분의 경우, 공격자에게 로컬 파일 시스템 접근 권한을 주지 않는 SQL Injection Attacks에 의해 뚫린다 (SQL 서버에서 로컬 파일 시스템 접근 권한을 비활성화 할 수 있다면 비활성화 하십시오). 만약 웹을 통해 접근 할 수 없는 파일에 생성한 랜덤 키를 저장하고, 솔티드 해시를 포함하게 한다면, 해시들은 데이터베이스가 간단한 SQL 인젝션 공격에 뚫리게 되더라도 취약하게 되지 않을 것입니다. 소스코드에 키를 하드코딩하지 말고, 애플리케이션이 설치될때 임의로 생성되게 하십시오. 이 방법은 비밀번호 해싱만을 위한 별개의 서버를 두는 것만큼 안전하지 않습니다. SQL 인젝션 취약점이 있는 웹 애플리케이션이라면, 공격자가 비밀 키 파일을 읽을 수 있는 Local File Inclusion과 같은, 또 다른 타입의 취약점도 있을 것이기 때문입니다. 하지만 아무것도 하지 않는 것보다는 낫습니다.

부디 주의 할 점은, keyed 해시가 솔트의 필요성을 없애지 않는다는 것입니다. 교활한 공격자는 언젠가 키를 얻어낼 방법을 찾을 것입니다. 때문에 해시가 솔트와 키 스트레칭으로 계속 보호 되는 것은 중요합니다.

다른 보안 방법.

비밀번호 해싱은 보안 침해 사건에서 비밀번호를 보호합니다. 비밀번호 해싱은 애플리케이션 전체를 더 안전하게 만들어주지 않습니다. 패스워드 해시(와 사용자의 다른 데이터)를 훔치는 것을 방지하기 위해 더 많은 것이 먼저 되어있어야 합니다.

경험있는 개발자라 하여도 안전한 애플리케이션을 작성하기 위해 보안에 대한 교육을 받아야 합니다. 웹 애플리케이션의 취약성을 배우는 매우 좋은 장소는 The Open Web Application Security Project (OWASP)입니다. 좋은 소개는 OWASP Top Ten Vulnerability List입니다. 목록에 있는 모든 취약점에 대해 이해하기 전에는, 민감한 데이터를 다루는 웹 애플리케이션을 작성할 시도를 하지 마십시오. 고용자는 모든 개발자들이 안전한 애플리케이션 개발에 대해 적절히 훈련되어야 할 책임을 지고 있습니다.

애플리케이션에 대해 써드파티의 “침투 테스트”를 받아보는 것은 좋은 생각입니다. 최고의 프로그래머라도 실수를 합니다. 따라서 보안 전문가가 잠재적 취약성에 대한 코드를 리뷰하는 것이 언제나 의미있습니다. 신뢰할 수 있는 단체나 고용된 직원이 정기적으로 코드를 리뷰하게 하십시오. 이 보안 리뷰 프로세스는 애플리케이션 수명의 이른 시기에 시작해서 개발 내내 지속되어야 합니다.

만약 누군가가 웹사이트를 뚫었다면 그걸 감지하고 모니터하는 것 또한 중요합니다. 전 최소한 한명의 사람을 풀타임으로 고용해 보안 뚫림에 대해 탐지하고 대응하게 할 것은 권장합니다. 만약 뚫린것이 탐지 되지 않는다면, 공격자는 웹사이트의 방문자를 말웨어로 감염시킬 수 있습니다. 따라서 뚫림을 감지하고 즉시 대응하는것은 극도로 중요합니다.

자주 묻는 질문들

어떤 해시 알고리즘을 써야 하나요?

DO use:
– 아래의 PHP source code, Java source code, C# source code 또는 Ruby source code. (역자주: 링크 클릭시 원본 페이지로 갑니다)
– OpenWall의 Portable PHP password hashing framework
– 제대로 테스트된 현대적인 해시 알고리즘. SHA256, SHA512, RopeMD, WHIRLPOOL, SHA3등.
– 잘 디자인된 키 스트레칭 알고리즘. PBKDF2, bcryptscrypt
– crypt ($2y$, $5$, $6$)의 안전한 버전

DO NOT use:
– MD5나 SHA1같은 구식 함수.
– 안전하지 않은 crypt 버전($1$, $2$, $2x$, $3$).
– 스스로 디자인한 모든 함수. 반드시 공개된 부문에 있으며, 숙련된 암호학자에 의해 잘 테스트된 기술만 사용하세요.

MD5와 SHA1에 대해 해시를 크랙하기 쉽게 만드는 암호학적 공격이 없다고 해도, 그 함수들은 오래됐고 비밀번호를 저장하기에 부적합하다고 (조금은 부정확하게) 널리 생각됩니다. 때문에 난 그 함수들을 사용하는걸 추천하지 않습니다. PBKDF2만이 유일한 예외입니다. 이 함수는 근본적 해시 함수로서 SHA1을 주기적으로 구현합니다.

사용자가 비밀번호를 잊었을때 어떻게 리셋하게 허용 할 수 있나요?

개인적 의견으로, 현재 널리 사용되는 모든 비밀번호 리셋 메카니즘들은 안전하지 않습니다. 만약 암호화 서비스가 가져야 할것처럼 높은 수준의 보안성이 요구된다면, 사용자가 스스로의 비밀번호를 리셋 할 수 없게 하십시오.

대부분의 웹사이트는 비밀번호를 잊은 사용자를 인증하기 위해 이메일 루프를 사용합니다. 이를 하기 위하, 계정과 강하게 묶인 일회용 랜덤 토큰을 만듭니다. 그 토큰을 비밀번호 리셋 링크에 포함하여 사용자의 이메일 주소로 보냅니다. 사용자가 올바른 토큰이 포함된 비밀번호 리셋 링크를 클릭했을때, 새 비밀번호를 입력하게합니다. 토큰이 사용자 계정과 강하게 묶이게 하는것을 확실하게 하여 공격자가 토큰을 공격자의 메일 주소로 보내어 다른 사용자의 비밀번호를 리셋하지 못하게 하십시오.

무엇보다도 첫째로, 토큰은 반드시 15분 뒤나, 사용되고 난 뒤에 무효화되어야합니다. 또한 존재하는 비밀번호 토큰을 사용자가 로그인 한 후(사용자가 비밀번호를 기억함)나, 다른 리셋 토큰을 요청한 후에 무효화 하는것도 좋은 생각입니다. 만약 토큰이 무효화되지 않는다면, 사용자의 계정에 부수고 들어가는데 영원히 이용될 수 있습니다. 이메일(SMTP)는 플레인-텍스트 프로토콜입니다. 그리고 인터넷에 이메일 트래픽을 수집하는 악의적인 라우터가 있을 수 있습니다. 그러면 사용자의 이메일 계정(리셋 링크를 포함한)은 비밀번호가 변경된 후에도 긴 기간동안 손상되어있을 수 있습니다. 가능한 빨리 토큰을 무효화 하는게 사용자들이 그러한 공격에 노출되는 것을 줄일 것입니다.

공격자는 또한 토큰을 변경할 수 있기에, 사용자 계정 정보나 시간 제한 정보를 토큰에 담지 마십시오. 토큰은 데이터베이스 테이블에서 레코드를 확인할때만 사용될, 예측 불가능한 바이너리 블롭blob이어야 합니다.

절대로 사용자가 새 비밀번호를 이메일을 통해서 보내게 하지 마십시오. 사용자가 비밀번호를 리셋할때 새 임의 솔트를 사용해야 한다는 것을 기억하십시오. 사용자의 예전 비밀번호를 해시할때 사용했던 솔트를 재사용하지 마십시오

사용자 계정 데이터베이스가 해킹되거나 유출 되었을때 어떻게 해야 하나요?

첫번째로 해야 할 것은 시스템이 어떻게 손상되었는지를 확인하여 공격자가 침입했던 취약점을 패치하는 것입니다. 보안 취약에 대처하는 경험이 없다면 써드파트 보안 회사를 고용하는 것을 강하게 추천합니다.

취약점을 은폐하고 아무도 눈치채지 못하길 희망하는 것은 유혹적일 수 있습니다. 하지만 취약점을 은폐하는 것은 당신을 더 나쁘게 보이게 할 뿐입니다. 그렇게 함으로서 사용자의 비밀번호와 그외 개인 정보가 새어나갈 수 있다는 것을 알리지 않음으로서 사용자를 더 큰 위험에 처하게 하는 것이기 때문입니다. 무슨일이 일어났는지 완전히 이해하지 못했어도, 가능한한 빨리 사용자들에게 알려야합니다. 가능하다면 이메일로 모든 사용자에게 알리고, 웹사이트의 프론트페이지에 자세한 정보를 제공하는 페이지 링크를 담은 공지를 올리십시오.

사용자들에게 비밀번호가 어떻게 보호되고 있는지 설명하고(솔트와 함께 해시된 것이기를 바랍니다), 솔티드 해시로 보호되고 있다고 해도, 악의를 가진 해커는 여전히 해시에 대해 사전 공격과 브루트 포스 공격을 행할 수 있습니다. 악의적 해커는 찾아낸 모든 비밀번호를 이용해서 다른 웹사이트의 사용자 계정에 로그인하려 시도할 것입니다. 사용자가 비밀번호를 두 웹사이트에 똑같이 사용하길 바라면서. 사용자들에게 이러한 위험성을 알리고, 다른 비슷한 비밀번호를 쓰는 다른 웹사이트의 비밀번호를 바꾸는 것을 권유하십시오. 당신의 서비스에 로그인 하려할때 비밀번호를 반드시 바꾸게끔 하십시오. 대부분의 사용자들은 이러한 강제 비밀번호 변경을 빨리 넘어가기 위해 비밀번호를 현재 비밀번호와 똑같이 “바꾸려”할 것입니다. 이를 방지하기 위해 현재 비밀번호의 해시를 이용하십시오.

솔트를 이용한 느린 해시라고 하여도, 공격자는 여전히 몇몇의 약한 비밀번호를 재빨리 크랙할 수 있을 가능성이 있습니다. 공격자가 이러한 비밀번호를 이용할 가능성을 줄이기 위해, 현재 비밀번호에 덧붙여서, 사용자가 비밀번호를 바꾸기 전까지 이메일 인증 루프를 하게 하십시오. 이 전의 질문인 “사용자가 비밀번호를 잊었을때 어떻게 리셋하게 허용 할 수 있나요?”에서 이메일 루프 인증에 대한 팁을 보십시오.

그리고 사용자들에게 어떤 종류의 개인정보가 웹사이트에 저장되어 있는지 얘기하십시오. 만약 데이터베이스가 신용카드 번호를 포함한다면, 사용자들에게 최근 카드 이용 내역과 미래의 카드 이용내역을 자세히 보게하여 신용카드를 취소해야 한다고 안내하십시오.

비밀번호 정책이 어떠해야 하나요? 강한 비밀번호를 강제해야 하나요?

만약 서비스가 강한 보안을 요구하지 않는다면, 사용자를 (역자주:비밀번호에 대해)제한하지 마십시오. 사용자가 비밀번호를 입력할때마다 비밀번호가 얼마나 강한지 알려주고, 사용자가 비밀번호가 얼마나 것을 권장합니다. 만약 특별한 보안이 필요하다면, 최소 12글자이며 최소 2개의 알파벳과 최소 2개의 숫자와 최소 2개의 심볼(역자주: !@#$%^&*와 같은 것들)을 포함하게 강제하십시오.

사용자에게 여섯달에 한번 이상 비밀번호를 바꾸게 강제하기 마십시오. 이러한 행동은 “사용자 피로”를 만들며, 좋은 비밀번호를 덜 선택하게끔 합니다. 대신 사용자가 스스로 유출되었다고 느낄때 비밀번호를 변경하게하고, 절대로 다른 사람에게 비밀번호를 말하지 않게 사용자들을 훈련하십시오. 만약 이러한 것들이 비즈니스 세팅이라면, 직원들이 업무시간에 비밀번호를 외우고 연습할 수 있게끔 권장하십시오.

만약 공격자가 내 데이터베이스 접근 권한을 획득했을경우, 내 해시를 공격자들만의 해시로 바꾸어서 로그인 할 수 없나요?

네 할 수 없습니다. 하지만 만야 누군가가 데이터베이스 접근 권한을 획득했을경우, 아마도 이미 서버의 모든것에 대한 접근 권한을 가졌을 것입니다. 따라서 원하는 것을 얻기 위해 당신의 계정에 로그인하지 않을 것입니다. 비밀번호 해싱의 목적은 (웹사이트 운영이라는 문맥에서 볼때) 웹사이트가 뚫리는 것을 보호하기 위함이 아니라, 웹사이트가 뚫렸을때 비밀번호를 보호하는 것입니다.

서로 다른 권한을 가진 두 사용자를 이용하여, SQL 인젝션으로 데이터 베이스에 접근하여 해시들을 교체하는 것을 방지할 수 있습니다. 한 명은 ‘create account’ 코드를 위한 계정이며, 다른 한명은 ‘login’코드를 위한 계정입니다. ‘create account’ 코드 계정은 사용자 테이블을 읽고 쓸 수 있어야 합니다, 하지만 ‘login’ 코드는 오직 읽기만 가능해야 합니다.

왜 HMAC와 같은 특별한 알고리즘을 사용해야만 하나요? 왜 비밀번호를 그냥 비밀 키에 덧붙이면 안되나요?

MD5, SHA1, SHA2 와 같은 해시 함수들은 Merkle–Damgård construction를 이용합니다. 이는 길이 확잔length extension 공격이라 불리는 취약점을 만듭니다. 즉 해시 H(X)가 주어졌을때, 공격자는 모든 문자열 Y에 대해 X를 알 필요 없이, H(pad(X)+Y)의 값을 찾아낼 수 있습니다. pad(X)는 해시 함수가 사용하는 패딩 함수입니다.

즉 해시 H(key + message)가 주어졌을때, 공격자는 H(pad(key + message) + extension)을 key를 알 필요 없이 계산할 수 있다는 얘기입니다. 만약 해시가 메시지 인증 코드로 사용되었다면, 키를 사용하여 공격자가 메시지를 변조하고 다른 유효한 해시로 교체하는 것을 방지하려는 시스템은 실패할 것입니다. 공격자가 ‘메시지 + 확장’의 유효한 해시값을 가질 수 있기 때문입니다.

공격자가 어떻게 이 공격을 이용하여 비밀번호 해시 크랙을 빨리 할지는 확실하지 않습니다. 하지만 이 공격 때문에 플레인 해시 함수를 keyed 해싱에 이용하는 것은 좋지 않은 실천으로 간주됩니다. 똑똑한 암호학자가 언젠가 기발한 방법으로 이 공격들을 사용하여 크래킹을 빠르게 할 수 있을 것입니다. 때문에 HMAC를 사용해야합니다.

솔트가 비밀번호의 앞이나 뒤 어디에 와야하나요?

상관 없습니다. 하지만 상호운용성을 위해 어느 한쪽을 선택하여 유지하십시오. 비밀번호 앞에 솔트를 위치하는게 좀 더 일반적으로 보입니다.

왜 이 페이지에 있는 해싱 코드는 해시를 “length-constant” 시간으로 비교하나요?

“length-constant” 시간으로 해시를 비교하는 것은 타이밍 공격을 하는 공격자가 온라인 시스템에서 해시를 추출하는 것을 불가능하게 하여, 오프라인으로 크랙하게 합니다.

일련의 바이트(문자열)이 같은지 비교하는 일반적인 방법은 다음과 같습니다. 먼저 첫번째 바이트를 비교합니다, 그리고 두번째, 그리고 세번째, 계속 비교합니다. 두 문자열에서 같지 않은 바이트를 찾아내는 순간 두 문자열이 다르다는걸 알게 되고, 네가티브 반응을 즉시 반환합니다. 만약 두 스트링이 다르다는걸 발견하지 못하고 마지막까지 간다면, 두 문자열이 같다는걸 알아채고 포지티브 결과를 반환합니다. 이는 곧 두 문자열을 비교하는 것은 두 문자열이 얼마나 일치하느냐에 따라 다른 시간이 걸린다는 얘기를 뜻합니다.

예를 들어, “xyzabc”와 “abcxyz”의 일반적인 비교는 첫번째 캐릭터가 다르다는걸 즉시 알아채고, 문자열의 남은 부분을 체크하지 않습니다. 또 다른 예인 “aaaaaaaaaaB”와 “aaaaaaaaaaZ”의 비교는, 비교 알고리즘이 문자열이 서로 다르다는 것을 결정하기 전까지 “a” 덩어리들을 전부 스캔합니다.

공격자가 인증 시도 횟수를 초당 한번의 비율로 제한하는 온라인 시스템에 침입하려 한다고 가정해봅시다. 또한 공격자는 비밀번호 해시의 모든 파라메터(솔트, 해시 타입, 그 외)를 알고 있다고 가정해봅니다. (명백하게) 비밀번호와 그 해시를 제외하고 말이죠. 만약 공격자가 온라인 시스템이 공격자가 준 해시와 진짜 비밀번호의 해시를 비교하는데 걸리는 시간을 정확히 잴 수 있는 수단을 가지게 된다면, 공격자는 해시의 부분을 추출하는데 타이밍 공격을 시도할 수 있고, 오프라인 공격으로 크랙할 수 있게됩니다. 시스템의 비율 제한 제한을 우회해서 말이죠.

먼저, 공격자는 모든 가능한 바이트로 시작하는 해시인, 256개 문자열을 찾아냅니다. 각각의 문자열을 온라인 시스템으로 전송해, 시스템이 반응하는 시간을 측정합니다. 제일 긴 시간을 두고 반응 한 것이 진짜 해시와 첫번째 바이트가 일치 하는 것입니다. 그리고 비슷한 방식으로 두번째, 세번째 그 이후의 바이트에 대해 공격할 수 있습니다. 해시의 충분한 길이를 알아냈다면, 공격자는 시스템의 제한 없이 공격자 자신의 하드웨어로 크랙할 수 있습니다.

타이밍 공격을 네트워크를 통해서 실행하는 것이 불가능해 보일 수 있습니다. 하지만, 이미 공격이 된 적이 있습니다, 그리고 실용적이라는 것을 보였습니다. 그게 바로 이 페이지에 있는 코드들이 문자열의 길이와는 상관 없이 똑같은 시간이 걸리는 이유입니다.

SlowEquals 코드가 어떻게 작동하나요?

이전 질문은 왜 SlowEquals가 필요한지 설명했습니다. 이번 질문은 코드가 실제로 어떻게 작동하는지 설명합니다.

1. private static boolean slowEquals(byte[] a, byte[] b)
2. {
3. int diff = a.length ^ b.length;
4. for(int i = 0; i < a.length && i < b.length; i++)
5. diff |= a[i] ^ b[i];
6. return diff == 0;
7. }

이 코드는 “==” 연산자 대신, XOR “^”를 이용하여 정수의 같음을 비교합니다. 이유는 밑에 설명 되어있습니다. XOR을 사용하여 두 정수를 지교하는 것의 결과는 둘이 서로 같을때에만 0이 됩니다(will be zero if and only if they are exactly the same). 왜냐하면 0 XOR 0 = 0, 1 XOR 1 = 0, 0 XOR 1 = 1, 1 XOR 0 = 1. 이기 때문입니다. 만약 이를 정수의 모든 비트에 적용한다면, 결과는 오직 모든 정수가 서로 일치할때만 0일 것입니다.

따라서, 첫번째 라인에서 a.lengthb.length가 서로 같다면 diff 변수는 0값을 가질 것입니다. 만약 그렇지 않다면 변수는 0이 아닌 다른 값을 가질것입니다. 다음으로 바이트들을 XOR을 이요하여 비교할 것입니다. 그리고 OR 연산자를 사용하여 결과를 diff에 저장합니다. 이렇게 하면 바이트가 다를 경우 diff를 0이 아닌 값으로 설정하게 됩니다. OR연산은 절대 비트를 언셋하지 않기에, diff가 루프의 마지막까지 0이 되는 방법은 루프의 시작 전부터 0이며(a.length == b.length), 두 배열의 모든 바이트가 일지하는 경우입니다(XOR의 결과가 전부 0이어야 함).

XOR을 “==” 연산자 대신 정수 비교에 사용하는 이유는, 보통 “==” 는 브랜치로 translated/compiled/interpreted 되기 때문입니다. 예를 들어, C 코드의 diff &amp;= a == b는 다음과 같이 x86 어셈블리어로 컴파일 됩니다.

MOV EAX, [A]
CMP [B], EAX
JZ equal
JMP done
equal:
AND [VALID], 1
done:
AND [VALID], 0

브랜칭은, 정수의 같음과 CPU의 내부 브랜치 예측 상태에 따라 다른 실행 시간이 걸리게 됩니다.

C 코드 diff |= a ^ b는 정수의 같음과 전혀 상관 없이 실행 시간이 걸리게, 다음과 비슷하게 컴파일 되어야 합니다.

MOV EAX, [A]
XOR EAX, [B]
OR [DIFF], EAX

왜 해싱에 신경쓰나요?

단싱의 사용자들은 웹사이트에 비밀번호를 입력합니다. 사용자들은 당신이 제공하는 보안을 믿습니다. 만약 데이터베이스가 해킹을 당하고, 사용자의 비밀번호들이 보호되지 않았다면, 악의를 가진 해커는 그 비밀번호들로, 다른 웹사이트나 서비스에 있는 사용자의 계정을 탈취하는데 사용할 수 있습니다(대부분의 사람들은 같은 비밀번호를 모든곳에 사용합니다). 이는 당신이 리스크를 지는 보안성이 아니라, 사용자의 리스크입니다. 당신은 사용자의 보안에 책임이 있습니다.

Advertisements

2 thoughts on “Salted Password Hashing – Doing it Right 번역

  1. 이렇게 좋은글을 잘 번역해주셔서 감사합니다. 덕분에 많이 알고갑니다.

    한 번 읽어서는 모두 이해가 어렵네요. 몇 번 더 읽어야 겠습니다. 감사합니다.

답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Google+ photo

Google+의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

%s에 연결하는 중