사전 Argon2id
알고리즘

Argon2id

gabury1고친 사람 github-actions[bot]

Argon2id 는 비밀번호를 저장해 둘 값으로 바꿔 줍니다. 값 하나를 만들 때 메모리를 크게 차지합니다. 그래서 공격자가 후보를 한꺼번에 많이 넣어 보지 못합니다. Argon2 라는 함수에는 변형이 셋 있습니다. Argon2id 는 그중 둘의 방식을 섞은 변형입니다. 셋 가운데 기본으로 쓰는 것도 이 변형입니다.

쉽고 빠른 이해

Argon2id 는 비밀번호와 무작위 값 하나를 받아 정해진 길이의 바이트열을 내놓습니다. 회원 가입 때 이 값을 저장해 둡니다. 로그인 때는 같은 계산을 다시 해서 둘을 비교합니다.

이게 없으면 데이터베이스가 새어 나갔을 때 공격자가 비밀번호 후보를 빠르게 넣어 봅니다. 그래픽 카드처럼 같은 계산을 수천 개씩 동시에 돌리는 장비를 쓰면 더 빠릅니다. 계산 하나마다 큰 메모리를 요구하면 그런 장비에 계산을 몇 개 못 올립니다.

어떻게 도나:

  1. 입력을 섞어 큰 메모리 배열을 처음부터 끝까지 채웁니다
  2. 정해 둔 횟수만큼 배열을 다시 훑습니다. 칸마다 새 값으로 바꿉니다. 새 값은 앞의 칸 몇 개를 섞어 만듭니다
  3. 끝에 남은 값을 결과 하나로 줄입니다

대가는 서버도 로그인 한 번마다 그 메모리를 잡는다는 것입니다. 로그인이 한꺼번에 몰리면 서버 메모리가 먼저 모자랍니다.

사람이 고르지 않은 값에는 쓰지 않습니다. 서버가 무작위로 뽑아 준 토큰은 짐작할 수 없어서 일반 해시 함수로 충분합니다.

상세

Argon2id 는 비밀번호 해싱 함수입니다. 사람이 고른 비밀번호를 받아, 되돌릴 수 없고 만드는 데 비용이 드는 값으로 바꾸는 함수를 이렇게 부릅니다. 이 값을 저장해 두면 원래 비밀번호를 서버에 남기지 않고도 로그인을 확인할 수 있습니다.

이 절은 비밀번호 해싱이 막으려는 공격에서 시작합니다. 그다음 Argon2id 가 비용을 메모리로 매기는 까닭, 받는 입력, 메모리를 채우고 훑는 과정을 차례로 다룹니다. 이어서 Argon2 의 세 변형이 무엇으로 갈리는지와 Argon2id 가 둘을 섞는 방법을 설명합니다. 마지막은 비용을 정하는 값을 고르는 법과 쓰는 곳입니다.

비밀번호 해싱이 막으려는 공격

해시 함수는 입력을 받아 짧은 고정 길이 값을 내놓는 함수입니다. 결과만 보고 입력을 거꾸로 알아낼 수 없게 만듭니다. 그래서 비밀번호 대신 그 해시값을 저장합니다.

그래도 저장된 해시값이 새어 나가면 공격자는 후보를 하나씩 넣어 봅니다. 같은 해시값이 나오는 후보가 곧 비밀번호입니다. 이렇게 맞을 때까지 대입해 보는 것을 무차별 대입 공격이라고 합니다.

일반 해시 함수는 빠르게 돌도록 만듭니다. 빠를수록 공격자가 1초에 넣어 보는 후보도 많아집니다. 사람이 고르는 비밀번호는 몇몇 후보에 몰려 있어서, 일반 해시 앞에서는 금방 맞습니다.

그래서 비밀번호용 함수는 일부러 느리게 만듭니다. 후보 하나를 넣어 보는 비용을 키우는 것입니다. 이렇게 계산을 무겁게 만드는 장치를 키 스트레칭이라고 부릅니다.

비용을 메모리로 매기는 까닭

먼저 나온 PBKDF2 는 해시 계산을 정해 둔 횟수만큼 되풀이해서 비용을 키웁니다. 되풀이만으로 키운 비용에는 빈틈이 있습니다. 계산 하나가 쓰는 메모리가 아주 작다는 점입니다.

메모리를 조금만 쓰는 계산은 GPU(Graphics Processing Unit, 그래픽 처리 장치)에 잘 맞습니다. GPU 는 같은 계산을 수천 개씩 동시에 돌립니다.

그 계산만 하도록 만든 전용 칩도 있습니다. 이런 칩을 ASIC(Application-Specific Integrated Circuit, 주문형 반도체)이라고 부릅니다. GPU 보다 더 싸게 더 많이 돌립니다.

방어하는 서버는 일반 CPU(Central Processing Unit, 중앙 처리 장치)에서 한 번씩 계산합니다. 양쪽이 같은 계산을 해도 공격자가 같은 시간에 넣어 보는 후보 수가 훨씬 많습니다.

메모리는 이 격차를 줄입니다. 칩 위에 계산 장치는 촘촘히 늘릴 수 있어도, 계산마다 큰 메모리를 따로 붙이기는 비쌉니다. 계산 하나가 64 MiB 를 요구하면 장비 하나에 올릴 수 있는 계산 수가 메모리 크기로 묶입니다.

이렇게 메모리를 많이 쓰지 않고는 빨리 풀 수 없게 만든 함수를 메모리 하드 함수라고 합니다. Argon2id 도 이런 함수입니다.

Argon2 는 비밀번호 해싱 공모전(Password Hashing Competition)에서 뽑혔습니다. 비밀번호용 함수를 고르려고 연 공개 경연입니다. Argon2id 는 그 Argon2 의 한 변형입니다.

입력과 출력

Argon2id 가 받는 값은 아래와 같습니다. 이 가운데 계산 비용을 정하는 값은 메모리 크기 · 반복 횟수 · 병렬도 셋입니다. 이 글은 이 셋을 비용 파라미터라고 부릅니다. (선택)이 붙은 비밀 키는 넣지 않아도 됩니다.

입력 담는 것
비밀번호 사람이 고른 문자열
솔트 사용자마다 새로 뽑는 무작위 값
메모리 크기 계산 한 번이 차지할 메모리. 킬로바이트 단위
반복 횟수 그 메모리 전체를 몇 번 훑을지
병렬도 메모리를 몇 갈래로 나눠 동시에 채울지
출력 길이 몇 바이트짜리 값을 받을지
비밀 키 (선택) 데이터베이스 밖에 따로 두는 비밀 값. 이런 값을 페퍼라고 부릅니다

솔트는 숨기는 값이 아닙니다. 사람마다 다르게 뽑아서 결과 옆에 저장해 둡니다. 같은 비밀번호를 쓰는 두 사람도 솔트가 다르면 서로 다른 값을 남깁니다. 흔한 비밀번호의 결과를 미리 표로 만들어 두는 레인보우 테이블도 이 때문에 못 씁니다.

내놓는 값은 출력 길이만큼의 바이트열 하나입니다. 이 값을 태그라고 부릅니다. 같은 입력을 넣으면 언제나 같은 태그가 나옵니다.

아래는 같은 입력으로 두 번 불러 본 것입니다. 함수 이름과 숫자는 설명하려고 고른 것입니다.

Python
cost = dict(m=65536, t=3, p=4)
tag  = argon2id(pw, salt, **cost, n=32)
tag2 = argon2id(pw, salt, **cost, n=32)
len(tag)      # 32
tag == tag2   # True

m=65536 은 킬로바이트 단위라 64 MiB 입니다. t 는 반복 횟수, p 는 병렬도, n 은 출력 길이입니다. 마지막 줄은 같은 입력이 같은 태그를 다시 만든다는 것을 보입니다. 로그인 확인은 이 성질에 기댑니다.

메모리를 채우고 다시 훑는 과정

Argon2id 는 메모리를 1 KiB 짜리 칸으로 나눠 씁니다. 이 칸을 블록이라고 부릅니다. 메모리 크기가 64 MiB 면 블록이 65,536 개입니다. 이 블록들을 한 줄로 늘어놓은 것을 메모리 배열이라고 부릅니다.

첫 단계에서는 입력을 전부 해시 함수에 넣어 섞습니다. 여기서 쓰는 해시 함수는 BLAKE2b 입니다. 섞은 값에서 맨 앞 블록 두 개를 만듭니다.

그다음 블록부터는 두 블록을 섞어 새 블록을 만듭니다. 하나는 바로 앞 블록입니다. 다른 하나는 이미 채운 블록 가운데 골라 온 것입니다. 이렇게 배열 끝까지 채우는 것을 패스 한 번이라고 부릅니다.

반복 횟수가 2 이상이면 채운 배열을 처음부터 다시 훑습니다. 두 번째 패스부터는 블록을 새로 만들 때 원래 있던 값까지 함께 섞습니다.

반복 횟수만큼 패스를 돌면 배열의 마지막 블록을 해시 함수에 넣어 원하는 길이의 태그를 뽑습니다. 병렬도가 2 이상이면 배열이 여러 줄로 나뉘어 줄마다 마지막 블록이 하나씩 생깁니다(다음 소절). 그때는 이 블록들을 XOR(exclusive or, 배타적 논리합)로 겹쳐 하나로 만든 뒤 해시 함수에 넣습니다.

flowchart TD
    A["입력을 해시 함수로 섞는다"] --> B["맨 앞 블록 두 개를 만든다"]
    B --> C["바로 앞 블록과 골라 온 블록을 섞어 새 블록을 만든다"]
    C --> D{"배열 끝인가"}
    D -->|아니다| C
    D -->|끝이다| E{"반복 횟수만큼 돌았나"}
    E -->|아직| C
    E -->|다 돌았다| F["마지막 블록을 하나로 모은다"]
    F --> G["해시 함수로 태그를 뽑는다"]

그림에서 볼 것은 두 겹의 루프입니다. 안쪽 루프가 블록 하나하나를 채웁니다. 바깥 루프는 패스를 되풀이합니다. 계산량은 블록 수와 반복 횟수를 곱한 만큼입니다. 차지하는 메모리는 블록 수만큼입니다.

병렬도와 레인

병렬도가 2 면 메모리 배열을 두 줄로 나눕니다. 이 줄 하나를 레인(lane)이라고 부릅니다. 레인마다 스레드 하나가 맡아서 동시에 채웁니다.

레인끼리 완전히 떨어져 있으면 공격자가 레인 하나씩 따로 풀 수 있습니다. 그러면 한 번에 레인 하나만큼의 메모리만 있으면 됩니다. 메모리 비용이 레인 수만큼 나뉘어 줄어드는 것입니다. 그래서 새 블록을 만들 때 다른 레인의 블록도 골라 올 수 있게 합니다.

골라 오려면 상대 레인이 그 블록을 이미 채웠어야 합니다. 그래서 각 레인을 앞에서부터 네 토막으로 자릅니다. 토막 하나를 다 채울 때마다 모든 레인이 서로를 기다립니다.

이 토막을 슬라이스(slice)라고 부릅니다. 슬라이스 수는 병렬도와 상관없이 늘 넷입니다.

block-beta
columns 4
  a1["레인 1 · 슬라이스 1"] a2["슬라이스 2"] a3["슬라이스 3"] a4["슬라이스 4"]
  b1["레인 2 · 슬라이스 1"] b2["슬라이스 2"] b3["슬라이스 3"] b4["슬라이스 4"]

각 줄이 레인입니다. 각 열은 슬라이스입니다. 두 레인이 같은 열을 동시에 채웁니다. 한 열이 끝나야 다음 열로 넘어갑니다.

병렬도를 올려도 메모리 크기와 계산량은 그대로입니다. 서버에 코어가 여럿이면 같은 비용을 더 짧은 시간에 치를 수 있다는 것이 병렬도의 쓸모입니다.

골라 올 블록을 정하는 두 방식

새 블록을 만들 때 섞을 앞 블록을 어떻게 고르느냐로 Argon2 의 변형이 갈립니다. 방식은 둘입니다.

첫째는 바로 앞 블록의 값을 보고 고르는 방식입니다. 어느 블록을 골라 올지가 비밀번호에 따라 달라집니다. 이 방식을 쓰는 변형이 Argon2d 입니다. 이름의 d 는 데이터에 기댄다(data-dependent)는 뜻입니다.

둘째는 비밀번호와 상관없는 순서로 고르는 방식입니다. 몇 번째 패스의 몇 번째 블록인지만으로 위치가 정해집니다. 이 방식을 쓰는 변형이 Argon2i 입니다. i 는 데이터와 무관하다(data-independent)는 뜻입니다.

두 방식은 막는 공격이 다릅니다.

메모리를 아끼는 공격. 공격자는 블록을 일부만 저장합니다. 버린 블록은 필요할 때 다시 계산해서 메모리를 아낍니다. 메모리 대신 계산을 더 쓰는 셈이라 시간-메모리 절충 공격이라고 부릅니다.

골라 올 순서가 미리 정해져 있으면 어느 블록을 버려도 되는지 미리 계획할 수 있습니다. 그래서 이 공격에는 Argon2i 가 더 약합니다. Argon2d 는 다음에 읽을 블록을 계산해 보기 전에는 모르니 더 강합니다.

메모리 접근을 엿보는 공격. 같은 기계에서 도는 다른 프로그램이 이 계산이 메모리의 어디를 읽는지 알아내는 경우가 있습니다. CPU 캐시는 CPU 가 메모리를 빨리 읽으려고 최근에 읽은 값을 담아 두는 작은 저장소입니다. 이 캐시의 변화를 재면 읽은 위치를 짐작할 수 있습니다.

이렇게 계산 결과가 아닌 곁가지 신호로 비밀을 캐는 것을 부채널 공격이라고 합니다. Argon2d 는 읽는 위치가 비밀번호에 따라 달라서 이 신호에 비밀번호의 흔적이 남습니다. Argon2i 는 읽는 위치가 비밀번호와 무관해서 남지 않습니다.

변형 고르는 방식 메모리를 아끼는 공격 메모리 접근을 엿보는 공격
Argon2d 비밀번호에 따라 강하다 약하다
Argon2i 비밀번호와 무관하게 약하다 강하다
Argon2id 앞부분만 무관하게, 나머지는 비밀번호에 따라 뒷부분 덕에 강하다 Argon2d 보다 강하다

Argon2id 가 두 방식을 섞는 법

Argon2id 는 첫 패스의 앞쪽 두 슬라이스에서만 Argon2i 의 방식으로 블록을 고릅니다. 나머지는 전부 Argon2d 의 방식으로 고릅니다. 첫 패스의 절반은 비밀번호와 무관하게, 그 뒤는 비밀번호에 따라 돕니다.

flowchart TD
    subgraph P1["첫 번째 패스"]
        S1["슬라이스 1~2: 비밀번호와 무관하게 고른다"] --> S2["슬라이스 3~4: 비밀번호에 따라 고른다"]
    end
    subgraph P2["두 번째 패스부터"]
        S3["슬라이스 1~4: 비밀번호에 따라 고른다"]
    end
    S2 --> S3

앞쪽 절반이 무관한 방식이라 메모리 접근을 엿보는 공격이 덜 통합니다. 배열의 절반이 차기 전까지는 읽는 위치에 비밀번호의 흔적이 없습니다.

뒤 절반에서는 흔적이 남습니다. 그래도 공격자가 이 흔적으로 후보를 걸러 내려면 후보마다 배열 절반을 먼저 채워야 합니다. 흔적은 거기서부터 나오기 때문입니다. Argon2d 라면 첫 몇 블록만 계산해 보고도 틀린 후보를 걸러 낼 수 있습니다.

뒤쪽이 비밀번호에 따르는 방식이라 메모리를 아끼는 공격도 막힙니다. 버릴 블록을 미리 계획할 수 없게 됩니다.

Argon2id 는 두 공격을 모두 어느 정도 막습니다. 어느 쪽 공격을 만날지 모르는 서버에 맞습니다. 그래서 세 변형 가운데 하나를 고를 일이 있으면 Argon2id 를 고릅니다.

비용 파라미터 정하기

메모리 크기 · 반복 횟수 · 병렬도는 사람이 정합니다. 메모리 크기와 반복 횟수를 올리면 공격자가 후보 하나에 치르는 비용이 늘어납니다. 서버가 로그인마다 치르는 비용도 같이 늘어납니다.

병렬도는 공격자가 치르는 메모리와 계산량을 바꾸지 않습니다. 서버가 그 비용을 여러 코어로 나눠 더 짧은 시간에 치르게 해 줍니다.

공격 비용을 가장 크게 올리는 것은 메모리 크기입니다. 반복 횟수는 메모리를 더 올릴 수 없을 때 시간을 더 쓰게 하는 값입니다.

처음 값을 고를 때 기준으로 삼는 조합이 둘 있습니다. 둘 다 병렬도 4, 솔트 16바이트, 태그 32바이트입니다.

조합 메모리 크기 반복 횟수
메모리를 넉넉히 쓸 수 있을 때 2 GiB 1
메모리가 모자랄 때 64 MiB 3

2 GiB 조합은 계산이 한 번에 하나씩 도는 쓰임에 맞습니다. 디스크 암호화 키를 만들 때처럼 동시 요청이 거의 없는 경우입니다. 로그인이 몰리는 웹 서버라면 64 MiB 조합에서 출발합니다.

메모리 크기는 동시 로그인 수와 곱해서 따져야 합니다. 64 MiB 짜리 계산이 동시에 100 개 돌면 6,400 MiB, 곧 6 GiB 남짓이 듭니다. 로그인이 몰리는 순간 서버 메모리가 바닥나면, 그것이 곧 서비스 거부 공격의 통로가 됩니다. 그래서 최종 값은 서비스가 도는 장비에서 재어 정합니다. 로그인 요청 수에도 속도 제한을 겁니다.

정한 비용 파라미터는 태그와 함께 저장합니다. 나중에 값을 올려도, 예전 태그를 만든 조건을 알아야 그 태그를 다시 비교할 수 있습니다. 흔히 아래처럼 한 줄 문자열에 모두 담습니다.

$argon2id$v=19$m=65536,t=3,p=4$<솔트>$<태그>

$ 로 나뉜 칸이 차례로 변형 이름, 버전 번호, 세 비용 파라미터, 솔트, 태그입니다. 솔트와 태그는 Base64 로 적습니다. 로그인 때는 이 문자열에서 조건을 꺼내 같은 계산을 한 뒤 태그를 비교합니다.

쓰는 곳과 안 쓰는 곳

가르는 기준은 사람이 고른 비밀을 받느냐입니다. 사람이 고른 값은 후보가 몰려 있어서 비용을 키울 까닭이 있습니다. 아래 표는 쓰임마다 그 기준을 적용해 본 것입니다.

쓰임 Argon2id 로 괜찮나
새로 비밀번호 저장 방식을 고르는 경우 됩니다. 이 함수가 만들어진 목적입니다
비밀번호로 파일이나 디스크를 잠글 키 만들기 됩니다. 원하는 길이의 값을 뽑습니다
이미 bcrypt 나 PBKDF2 로 저장해 온 비밀번호 옮겨 갈 수 있습니다. 사용자가 다음에 로그인할 때 새 방식으로 다시 저장합니다
무작위로 뽑은 API 키(Application Programming Interface 키)나 토큰을 저장 필요 없습니다. 짐작할 수 없는 값이라 일반 해시 함수로 충분합니다
파일이 깨졌는지 확인 아닙니다. 그 일은 일반 해시 함수가 합니다

기준에 안 걸리는 쓰임에 이 함수를 넣으면, 늘어난 메모리와 시간을 서버 혼자 치릅니다. 막아 주는 것은 없습니다.

관련 항목

Argon2id 가 속하는 상위 분류

비밀번호 해싱 · 키 유도 함수 · 키 스트레칭 · 메모리 하드 함수 · 해시 함수

Argon2id 와 한 뿌리인 변형

Argon2 · Argon2d · Argon2i · 비밀번호 해싱 공모전 · RFC 9106

Argon2id 를 대신할 수 있는 다른 함수

bcrypt · scrypt · PBKDF2 · yescrypt · Balloon

Argon2id 가 입력으로 받는 값

솔트 · 페퍼 · 메모리 크기 · 반복 횟수 · 병렬도 · 비밀번호

Argon2id 안에서 도는 계산

BLAKE2b · XOR · 압축 함수 · 해시 · 스레드

Argon2id 가 비용을 키워 막으려는 공격

무차별 대입 공격 · 사전 공격 · 레인보우 테이블 · 크리덴셜 스터핑 · 시간-메모리 절충 공격

Argon2id 의 변형을 가르는 공격

부채널 공격 · 캐시 타이밍 공격 · 타이밍 공격 · CPU 캐시

Argon2id 의 계산 비용을 좌우하는 장비

GPU · ASIC · FPGA · CPU · 메모리

Argon2id 태그를 적어 두는 형식

PHC 문자열 형식 · Base64 · 데이터베이스

Argon2id 로 로그인을 확인하는 절차와 그 방어

인증 · 다중 인증 · 속도 제한 · 서비스 거부 공격 · API 키 · 토큰

Argon2id 가 쓰이는 보안 분야

보안 엔지니어링 · 암호학 · 암호화 · 비밀번호 관리자 · 디스크 암호화

다른 이름: argon2id · Argon2 id