개발 이야기4분 읽기

난수를 만들 때 Math.random을 쓰면 안 되는 자리

비밀번호와 토큰에는 예측 불가능한 난수가 필요합니다. CSPRNG와 일반 난수의 차이, 엔트로피 계산법을 정리했습니다.

임시 비밀번호나 초대 코드를 만드는 코드는 대개 짧습니다. 문자 목록을 정하고 난수로 골라 이어 붙입니다. 이때 어떤 난수 함수를 썼는지에 따라 결과의 성질이 완전히 달라집니다.

Math.random은 예측할 수 있다

Math.random, 파이썬의 random 모듈, 자바의 Random은 모두 의사난수 생성기입니다. 내부 상태에서 다음 값을 계산하는 방식이라 통계적으로는 고르게 나오지만, 출력을 몇 개 관찰하면 내부 상태를 복원할 수 있습니다. 상태를 알면 그 뒤에 나올 값이 전부 정해집니다.

시뮬레이션이나 화면 효과에는 문제가 없습니다. 하지만 다음 값이 예측되면 안 되는 자리에는 쓸 수 없습니다.

  • 비밀번호, 임시 비밀번호
  • 세션 토큰, API 키
  • 비밀번호 재설정 링크
  • 초대 코드, 쿠폰 코드
  • 암호화에 쓰는 IV, nonce, 솔트

이 자리에는 암호학적으로 안전한 난수 생성기(CSPRNG)를 씁니다.

환경 함수
브라우저 crypto.getRandomValues()
Node.js crypto.randomBytes(), crypto.randomInt()
파이썬 secrets 모듈
자바 SecureRandom
Go crypto/rand

파이썬에서 random.choicesecrets.choice는 이름이 비슷하고 동작도 같아 보이지만, 이 목적에서는 하나만 맞습니다.

범위를 줄일 때 생기는 편향

바이트를 뽑아 문자 목록에서 고를 때 나머지 연산을 쓰는 코드를 자주 봅니다.

index = randomByte % 62

0부터 255까지를 62로 나누면 앞쪽 인덱스가 조금 더 자주 나옵니다. 256이 62의 배수가 아니기 때문입니다. 이 편향은 크지 않지만, 없앨 수 있는 것을 남길 이유도 없습니다.

  • 문자 목록 크기를 2의 거듭제곱(16, 32, 64)으로 맞춥니다
  • 또는 범위를 벗어나는 값이 나오면 버리고 다시 뽑습니다
  • 언어가 제공하는 함수를 씁니다. crypto.randomInt(max)secrets.randbelow(n)이 이 처리를 이미 하고 있습니다

엔트로피로 강도를 재는 법

비밀번호의 강도는 가능한 경우의 수로 정해지고, 보통 비트로 표기합니다. 문자 종류가 P개이고 길이가 N이면 N × log2(P) 비트입니다.

구성 8자 12자 16자 20자
소문자만 (26) 37.6 56.4 75.2 94.0
소문자+숫자 (36) 41.4 62.0 82.7 103.4
대소문자+숫자 (62) 47.6 71.5 95.3 119.1
특수문자 포함 (94) 52.4 78.7 104.9 131.1

표를 가로와 세로로 비교해 보면 방향이 보입니다. 대소문자와 숫자를 섞은 8자는 47.6비트인데, 소문자만 쓴 12자는 56.4비트입니다. 문자 종류를 늘리는 것보다 길이를 늘리는 쪽이 효율적입니다. 길이는 지수로 작용하고 문자 종류는 로그로 작용하기 때문입니다.

기준은 대략 이렇습니다.

  • 60비트 미만 — 오프라인 공격에 오래 버티지 못합니다
  • 80비트 — 일반적인 계정 비밀번호로 충분합니다
  • 128비트 이상 — API 키, 세션 토큰

UUID v4는 122비트의 무작위 비트를 갖습니다. 토큰 용도로 쓸 수 있는 크기이지만, 형식이 알려져 있어 값의 성격이 드러나므로 32바이트 무작위 값을 Base64로 인코딩하는 편이 무난합니다.

사람이 외워야 한다면

문자를 섞은 비밀번호는 강도에 비해 외우기 어렵고, 결국 어딘가에 적히거나 재사용됩니다. 단어를 이어 붙이는 방식이 대안입니다.

7,776개 단어 목록에서 무작위로 고르면 단어당 12.9비트입니다.

  • 4단어 — 51.7비트
  • 6단어 — 77.5비트

여섯 단어면 특수문자를 섞은 12자와 비슷한 수준이면서 외우기는 훨씬 쉽습니다. 조건은 단어를 사람이 고르지 않는 것입니다. 직접 고른 단어 조합은 이 계산이 성립하지 않습니다.

생성기가 갖춰야 할 것

임시 비밀번호를 만드는 기능을 직접 붙인다면 다음을 확인합니다.

  • CSPRNG를 쓰는가
  • 편향 없이 범위를 좁히는가
  • "특수문자 하나 이상" 같은 규칙을 강제하며 오히려 경우의 수를 줄이지 않는가. 자리를 고정해 두면 그만큼 엔트로피가 줄어듭니다
  • 생성한 값을 로그에 남기지 않는가

확인 순서

  1. 코드에서 난수 함수를 찾습니다. 보안 용도인데 Math.random이나 random 모듈이면 교체합니다
  2. 나머지 연산으로 범위를 줄이고 있다면 언어가 제공하는 함수로 바꿉니다
  3. 길이와 문자 종류로 엔트로피를 계산해 목적에 맞는지 봅니다
  4. 사용자가 외워야 하는 값이라면 단어 조합을 검토합니다

tools.onuel.dev비밀번호 생성기에서 길이와 문자 종류를 바꿔 가며 결과를 볼 수 있고, 난수 생성기로 범위를 지정해 값을 뽑을 수 있습니다. 토큰 용도라면 UUID v4도 같은 자리에 있습니다. 생성은 브라우저 안에서 이루어지므로 값이 서버로 전송되지 않습니다.

  • #난수
  • #엔트로피
  • #보안