비밀번호에서 키를 만드는 PBKDF2와 키 유도
사람이 외울 수 있는 비밀번호는 암호 키로 쓰기에는 너무 약하고 짧습니다. 컴퓨터가 요구하는 키는 무작위로 보이는 256비트짜리 숫자인데, 우리가 만드는 비밀번호는 "우리집강아지" 같은 규칙적인 글자이기 때문입니다. 이 간격을 메우는 기술이 키 유도(key derivation)입니다. 이 글에서는 친구와 암호 대화가 번역기에 비밀번호를 걸 때 어떤 과정을 거쳐 해독 키를 만드는지 풀어 설명합니다.
비밀번호와 암호 키는 다릅니다
비밀번호는 사람이 기억하기 위한 것이고, 암호 키는 알고리즘이 쓰기 위한 것입니다. 비밀번호를 그대로 키로 쓰면 두 가지 문제가 생깁니다. 첫째, 길이와 형식이 제각각이라 알고리즘이 요구하는 크기에 맞지 않습니다. 둘째, 더 큰 문제는 약하다는 점입니다. 공격자가 암호문을 손에 넣었다면 자주 쓰이는 비밀번호를 하나씩 대입해 보는 사전 대입 공격을 할 수 있고, 요즘 컴퓨터는 이를 초당 수십억 번 시도할 수 있습니다.
그래서 비밀번호에서 키를 만들 때는 일부러 계산을 느리게 만들어 대입 한 번의 비용을 키웁니다. 사용자는 비밀번호를 한 번 입력할 때 아주 잠깐 기다리면 되지만, 공격자는 수억 번을 시도해야 하므로 그 잠깐이 엄청난 부담으로 쌓입니다. 이런 용도로 만들어진 대표적인 방식이 PBKDF2입니다.
PBKDF2: 같은 계산을 수십만 번 되풀이합니다
PBKDF2는 비밀번호와 솔트(salt)를 섞어 해시 계산을 정해진 횟수만큼 반복하는 방식입니다. 이 서비스는 해시 함수로 SHA-256을 쓰고 반복 횟수는 600,000회로 정했습니다. 결과로는 256비트를 뽑습니다. 이 계산은 브라우저에 내장된 Web Crypto가 수행하므로 직접 구현한 코드보다 안정적입니다. 자세한 배경은 브라우저 Web Crypto API로 암호화하기에 있습니다.
반복 횟수는 높을수록 방어에 유리하지만 사용자의 기기도 그만큼 일을 합니다. 오래된 휴대폰에서는 눈에 띄는 지연이 생길 수 있으므로 균형이 필요합니다. 서버는 번역기를 만들 때 받는 반복 횟수가 100,000에서 2,000,000 사이인지만 확인하고 값 자체는 번역기와 함께 저장해 둡니다. 이렇게 하면 나중에 기본값을 올려도 예전에 만든 번역기는 자기 횟수대로 계속 열립니다.
또 한 가지, 비밀번호는 입력받은 즉시 유니코드 정규화(NFKC)를 거칩니다. 한글은 같은 글자가 서로 다른 내부 코드로 표현될 수 있는데, 기기마다 입력 방식이 달라 키가 어긋나는 일을 막기 위해서입니다.
솔트가 하는 일
솔트는 번역기마다 따로 정해지는 무작위 값입니다. 이 서비스에서는 16바이트이고, 서버가 번역기의 설정과 함께 보관하며 번역기를 여는 사람에게 알려 줍니다. 솔트는 비밀이 아닙니다. 하는 일은 같은 비밀번호라도 번역기마다 전혀 다른 결과가 나오게 하는 것입니다.
솔트가 없다면 공격자는 흔한 비밀번호의 계산 결과를 미리 표로 만들어 두고 여러 대상에 재사용할 수 있습니다. 솔트가 있으면 대상마다 계산을 다시 해야 하므로 이런 사전 계산이 소용없어집니다. 두 사람이 우연히 같은 비밀번호를 골라도 서로의 키가 같아지지 않습니다.
HKDF: 용도마다 키를 나눠 씁니다
PBKDF2로 얻은 값은 한 덩어리입니다. 이것을 서버 인증에도 쓰고 글 암호화에도 쓰면 위험하므로, HKDF라는 방식으로 용도별 키를 갈라냅니다. HKDF에는 "이 키는 무슨 용도인가"를 알리는 이름표(info)를 넣을 수 있고, 이름표가 다르면 결과도 완전히 달라집니다. 이 서비스는 세 가지 이름표를 씁니다.
nansu/v2/auth: 서버에 보내 비밀번호가 맞는지 확인받는 인증 토큰용nansu/v2/room: 글을 암호화하고 푸는 해독 키용nansu/v2/manage: 번역기 설정을 바꿀 권한을 증명하는 관리 토큰용
덕분에 서버가 인증 토큰을 받아도 거기서 해독 키를 거꾸로 계산해 낼 수 없습니다. 이름표로 분리된 값들은 서로의 원료가 되지 못하기 때문입니다.
이 서비스의 키 조립 순서
비밀번호가 없는 번역기에서는 번역기 주소의 # 뒤에 있는 번역기 키에서 HKDF로 곧바로 해독 키를 만듭니다. 비밀번호가 있는 번역기는 조금 더 복잡합니다.
- 번역기 키와 PBKDF2(비밀번호) 결과를 이어 붙여 재료(ikm)를 만듭니다.
- 이 재료로 HKDF 인증 토큰을 만들어 서버에 보냅니다. 서버는 이 토큰의 SHA-256 해시를 저장해 둔 값과 비교합니다.
- 맞으면 서버가 번역기를 만들 때 정해 둔 무작위 32바이트(server_part)를 돌려줍니다.
- 브라우저는 재료에 server_part까지 이어 붙여 HKDF로 해독 키를 만듭니다.
이 구조 때문에 비밀번호를 모르면 server_part를 받을 수 없고, 주소만 알아서는 해독 키를 만들 수 없습니다. 서버가 해시 데이터만 가진 채 침해되더라도 server_part와 솔트를 얻은 공격자가 오프라인에서 비밀번호를 대입해 볼 수 없다는 점이 이 설계의 요점입니다. 번역기 키는 서버에 없기 때문에 비밀번호 후보를 맞춰 보려면 번역기 키가 먼저 필요합니다.
한 번 입력한 비밀번호가 쓰이는 방식
번역기를 열 때 비밀번호를 입력하면 브라우저는 서버에서 솔트와 반복 횟수를 받아 와 PBKDF2 계산을 합니다. 이 계산은 사용자의 기기에서 이루어지고 비밀번호 자체는 어디로도 보내지 않습니다. 계산이 끝나면 인증 토큰만 서버로 가고, 서버가 맞다고 확인하면 server_part가 돌아옵니다. 설정 시간이 있다면 완성된 해독 키가 비추출 상태로 브라우저에 잠시 보관되어 다음 열기 때는 입력 없이 열립니다.
비밀번호를 바꾸면 해독 키도 바뀝니다. 서버는 새 솔트와 새 server_part를 정하고 키 버전 번호를 올립니다. 이미 열려 있는 다른 화면은 이 번호가 바뀐 것을 보고 다시 비밀번호를 묻게 되며, 예전에 저장된 면제 정보도 버려집니다. 한 가지 유의할 점은 비밀번호를 바꾼 뒤에는 예전 키로 만든 비밀어를 새 키로 풀 수 없다는 것입니다. 중요한 비밀어는 바꾸기 전에 미리 읽어 두세요.
비밀번호를 고를 때 알아 둘 것
서버는 오입력을 5회까지 허용하고, 한도를 넘으면 번역기를 지우지 않고 10분 동안 잠급니다. 열기 요청에는 속도 제한도 있어서 온라인으로 무작정 대입하기는 어렵습니다. 그래도 비밀번호가 쉬우면 한계가 있으니 길게 만드는 것이 좋습니다.
기억하기 쉬우면서 약하지 않은 방법으로는 서로 관련 없는 낱말 여러 개를 이어 쓰는 방식이 있습니다. 이 서비스의 번역기 화면에도 낱말 목록에서 무작위로 세 개를 뽑아 암구호를 제안해 주는 기능이 있습니다. 사람이 고른 "그럴듯한" 낱말보다 무작위로 뽑힌 낱말이 훨씬 예측하기 어렵습니다. 그리고 비밀번호는 번역기 주소를 전달한 경로와 다른 경로로 알려 주세요. 둘이 같은 대화방을 지나면 한 번의 유출로 둘 다 새게 됩니다.
키 유도가 아무리 튼튼해도 기기가 감염되었거나 비밀번호를 입력하는 모습이 노출되면 소용이 없습니다. 이런 한계는 이 서비스가 막아 주는 것과 막아 주지 못하는 것에 정리해 두었습니다. 비밀번호를 걸어 보시려면 번역기 열기에서 설정 메뉴를 이용하시면 됩니다.
잘못된 내용이나 보완할 점을 발견하셨다면 소개·문의의 이메일로 알려 주세요. 확인 후 글을 고치고 수정일을 갱신합니다.