Math.random으로 만든 비밀번호는 겉보기에 충분히 섞여 있어도, 왜 암호학적으로 안전한 난수가 아닐까

페이지에서 방금 대소문자·숫자·기호가 섞인 문자열이 나오면 「강해 보인다」고 느끼기 쉽습니다. 그건 문자 종류가 넓을 수 있다는 뜻일 뿐, 다음 한 글자를 예측할 수 없다는 증명은 아닙니다. 확인 방법은 「랜덤」「로컬」이라는 글자를 믿는 것이 아니라, 개발자 도구를 여는 것입니다. 생성 경로는 crypto.getRandomValues를 거쳐야 하고, 방금 만든 비밀번호는 Network의 업무 데이터로 나오면 안 됩니다.

먼저 결론입니다. Math.random()은 일반 의사난수(PRNG)이며, MDN은 보안 관련 용도에 쓰지 말라고 적습니다. 로그인 비밀번호, 일회용 토큰, 키는 Web Crypto의 crypto.getRandomValues()로 바꿔야 합니다. 겉보기에 섞여 있음, 오프라인에서도 생성됨, 주소창 자물쇠—셋은 각각 다른 경계를 증명하며, 「어느 API를 읽었는가」를 대신할 수 없습니다.

검색 의도를 한 문장으로 먼저 답하기

MDN의 Math.random 설명은 분명합니다. [0, 1) 구간에서 거의 균등한 의사난수를 반환하지만, 암호학적으로 안전한 난수를 제공하지 않으며 보안과 관련된 일에 쓰지 말라고 적혀 있습니다. 대신 Crypto.getRandomValues()를 쓰라고 안내합니다. 비밀번호가 「겉보기에 충분히 섞여 있다」는 것은 출력 문자의 형태를 설명할 뿐, 생성기 내부 상태를 복원할 수 있는지는 말하지 않습니다.

이 글은 「온라인 비밀번호 생성기」를 검토하는 개발자·운영, 그리고 동료가 넘겨 준 생성 페이지를 직접 보고 써도 되는지 판단하려는 사람을 위한 것입니다. 비밀번호 생성기 사용 설명을 대신하지 않으며, 「평문이 이 탭을 떠났는가」에 대한 Network 총론도 반복하지 않습니다. 가져가야 할 것은 일반 PRNG와 CSPRNG의 차이, 나머지 연산(modulo)이 좋은 API까지 망칠 수 있는 이유, 「생성」 클릭부터 소스를 읽는 구체적 절차입니다.

겉보기에 섞여 있다는 것은 문자가 넓을 수 있다는 뜻뿐

사람 눈으로 「섞여 있다」고 판단하는 것은 값싸고, 자주 틀립니다. aB3$kP9x와 Tr0ub4dor&3 모두 비밀번호처럼 보입니다. 전자는 충분히 큰 문자 집합에서 나왔을 수 있고, 후자는 흔한 문구에 치환만 한 것일 수 있습니다. 겉보기는 「비밀번호 같다」에 답하지, 「다음 비트를 계산할 수 없다」에는 답하지 않습니다.

대소문자·숫자·기호가 실제로 포함되어 있어도, 인덱스가 예측 가능한 수열에서 오면 공격자는 전체 공간을 무작위로 훑을 필요가 없습니다. 같은 생성기를 재현하거나, 몇 번의 출력을 관찰한 뒤 내부 상태를 복원해 아직 쓰이지 않은 후보를 열거할 수 있습니다. 로그인 비밀번호에는 「특수문자가 있다」보다 「예측 불가능하다」가 먼저입니다.

길이도 여전히 중요합니다. 같은 CSPRNG라도 6자와 16자의 무차별 대입 비용은 몇 자릿수 차이 납니다. 8자 미만이면 CSPRNG를 써도 공간 자체가 작습니다. FastPwd 무작위 모드는 6–128자, 기본값 16자이며 8자 미만이면 보안이 낮다고 알려 줍니다. 이는 길이 경고이지 「난수 알고리즘이 약해졌다」는 뜻이 아닙니다. 알고리즘은 예측 불가능성을, 길이는 탐색 공간을 담당합니다. 둘은 따로 봐야 합니다.

Math.random은 어떤 종류의 난수인가

ECMAScript는 Math.random()을 「0 이상 1 미만의 양수 Number를, 구현 정의 알고리즘으로 거의 균등하게」 반환한다고 적습니다. 규격은 예측 저항을 요구하지 않으며, 시드가 OS 엔트로피 풀에서 왔다고도 하지 않습니다. 엔진은 통계적으로 괜찮지만 상태를 되돌릴 수 있는 PRNG를 고를 수 있습니다.

Chrome의 V8은 2015년 말까지 MWC1616을 썼습니다. V8 자체 설명에 따르면 내부 상태는 64비트뿐이고 품질 편향이 있으며 TestU01에서 실패합니다. V8 4.9.41(Chrome 49)부터 xorshift128+로 바뀌었습니다. 128비트 상태, 주기 2^128 - 1, TestU01 통과. Firefox와 Safari도 비슷한 계열로 전환했습니다. 자세한 내용은 V8의 Math.random 글을 참고하세요.

같은 글은 경계를 분명히 적습니다. xorshift128+는 이전보다 훨씬 낫지만, 여전히 암호학적으로 안전하지 않습니다. 해시, 서명, 암·복호화에는 일반 PRNG가 맞지 않으며, Web Cryptography API의 window.crypto.getRandomValues를 써야 합니다. 통계적으로 「충분히 섞여 있다」와 「여러 출력을 본 뒤에도 다음 출력을 cheap하게 못 맞힌다」는 다른 기준입니다. 비밀번호에 필요한 것은 후자입니다.

따라서 Math.random()만 호출해 문자표에 매핑하는 생성기는 「눈으로는 섞여 보임」과 간단한 빈도 검사는 통과할 수 있어도, 「누군가 소스나 여러 출력을 봤다」는 가정에서는 통과하지 못할 수 있습니다. 「Math.random을 썼지만 문자 집합은 넓다」를 이미 합격으로 쓰지 마세요.

getRandomValues는 어느 층을 바꾸는가

crypto.getRandomValues(typedArray)는 넘긴 정수형 배열(예: Uint8Array, Uint32Array)을 암호학적으로 충분히 강한 난수로 채웁니다. MDN은 성능을 위해 구현이 매번 하드웨어 TRNG에서 직접 비트를 뽑지 않고, OS 엔트로피(Unix의 /dev/urandom, Windows 시스템 난수 API 등)로 시드를 만든 뒤 암호학적 PRNG를 돌리는 경우가 많다고 적습니다. 알고리즘은 브라우저마다 달라질 수 있지만, 설계 목표는 공개된 출력만으로 내부 상태를 값싸게 복원할 수 없게 하는 것입니다.

Math.random과는 규격도 다릅니다. 후자는 ECMA-262, 전자는 Web Cryptography입니다. 한 번에 65536바이트를 넘기면 QuotaExceededError가 납니다. 16자 비밀번호 생성에는 훨씬 밑입니다. 또 getRandomValues는 Crypto 인터페이스 중 비보안 컨텍스트에서도 호출 가능한 소수입니다. 키 파생이나 subtle.encrypt는 HTTPS 보안 컨텍스트가 필요합니다. 생성 페이지는 원래 HTTPS여야 하며, 「HTTP에서도 getRandomValues가 된다」를 TLS를 끄는 이유로 쓰면 안 됩니다.

「이 기기에서 난수를 뽑는다」와 「서버가 대신 뽑아 HTTP로 돌려준다」도 구분해야 합니다. 후자는 서버가 CSPRNG를 써도, 비밀번호가 상대 메모리·로그·이번 HTTP를 거칩니다. 주소창 자물쇠는 브라우저와 TLS 종단 사이 전송이 암호화됐다는 뜻이지, 종단이 평문을 못 본다는 뜻이 아닙니다. 「로컬 생성」을 주장한다면, 생성 순간에 비밀번호를 POST했다가 다시 보여 주는 API가 있으면 안 됩니다.

보이는 현상 지지할 수 있는 결론 지지할 수 없는 결론
출력이 겉보기에 섞여 있음 문자 종류가 넓을 수 있음 생성 과정이 예측 불가능함
소스에 Math.random이 있음 페이지가 일반 PRNG를 씀 반드시 비밀번호 생성에 씀
소스에 getRandomValues가 있음 CSPRNG API를 도입함 내가 누른 버튼이 반드시 이를 탐
오프라인에서도 생성됨 실시간 대리 추출 API에 의존하지 않음 Math.random이 아님
Network에서 비밀번호를 못 찾음 이번 관찰에서 업무 데이터로 안 나감 난수 알고리즘이 합격

나머지 편향: CSPRNG를 써도 잘못 매핑하면 기울어짐

두 번째 흔한 실수는 「이미 getRandomValues를 호출했다」는 뒤입니다. 스크립트가 0–255 바이트를 받아 charset[byte % charset.length]처럼 쓰면, 문자 집합 길이가 256으로 나누어떨어지지 않을 때 앞쪽 문자가 조금 더 자주 나옵니다. 16자 비밀번호에서는 편향이 작을 수 있지만, 「균등」을 끝까지 지키지 못했다는 신호입니다. 문자 집합이 더 이상하거나 8비트로 큰 표에 매핑할 때는 더 눈에 띕니다.

거부 샘플링은 256(또는 2^32) 이하에서 문자 집합 크기로 나누어떨어지는 상한을 구하고, 그보다 크거나 같은 값은 버린 뒤 나머지로 매핑하는 방식입니다. FastPwd의 무작위 문자와 읽기 쉬운 암호는 모두 crypto.getRandomValues로 Uint32Array(1)에서 32비트 정수를 뽑고, 상한을 Math.floor(0x100000000 / max) * max로 두며 초과분은 다시 뽑습니다. getRandomValues가 없으면 Math.random으로 되돌리지 않고 생성을 거부합니다. 다른 사이트를 볼 때도 「정수 추출 → 꼬리 버림 → 문자 매핑」까지 읽으세요. 함수 이름만 찾고 멈추지 마세요.

실제 로그인 비밀번호, 운영 API Key, 마스킹 안 된 티켓으로 실험하지 마세요. 버릴 테스트 문자열만 만들고, 확인이 끝나면 폐기하세요. 검증 대상은 생성 경로와 트래픽이지, 새 비밀번호를 또 노출하는 것이 아닙니다.

그 자리에서 확인: Sources, 콘솔, Network

슬로건은 증거가 아닙니다. 길이를 16으로 두고 생성한 뒤, 그 전체 문자열을 카나리아로 쓸 준비를 하세요. 아래 세 단계는 현재 탭에서 끝낼 수 있으며 계정은 필요 없습니다.

  1. 개발자 도구 Sources(또는 「페이지 소스 보기」)를 열고 Math.random과 getRandomValues를 각각 검색합니다. 맞은 파일과 함수 이름을 메모만 하고, 아직 결론 내리지 마세요.
  2. 「생성」 버튼의 클릭 핸들러부터 아래로 읽어 결과 문자열이 완성될 때까지 따라갑니다. 이 경로에 crypto.getRandomValues가 있어야 합니다. Math.random(), Date.now(), 고정 시드만 있다면 이번 생성은 CSPRNG로 볼 수 없습니다.
  3. Network로 가서 Preserve log를 켜고 All로 둡니다. 다시 생성한 뒤 방금 받은 전체 문자열을 검색창에 붙입니다. 요청 줄, 쿼리, 본문, 분석 전송 어디에도 이 비밀번호가 나오면 안 됩니다.

콘솔은 API 존재만 증명

Console에서 typeof crypto.getRandomValues === "function"이 true면, 이 문서의 브라우저가 API를 쓸 수 있다는 뜻뿐입니다. 페이지 스크립트가 실제로 호출했다는 증명은 아닙니다. 콘솔에서 직접 getRandomValues 한 줄을 실행한 뒤, 페이지가 만든 비밀번호까지 「확인했다」고 묶으면 안 됩니다. 서로 다른 코드입니다.

오프라인 재테스트는 대리 추출만 배제

페이지를 먼저 완전히 로드한 뒤 Network에서 Offline을 켜거나 시스템 네트워크를 끊고 생성해 보세요. 결과가 나오면 이번 클릭이 실시간 API를 기다리지 않았다는 강한 신호입니다. 하지만 Math.random도 네트워크가 필요 없습니다. 오프라인을 통과했다면 Sources 경로와 카나리아 검색은 여전히 해야 합니다.

번들된 스크립트는 이름이 한 글자로 압축될 수 있습니다. getRandomValues가 안 나오면 crypto와 Uint32Array도 검색하고, 「생성」 클릭 뒤 새 업무 POST가 있는지 대조하세요. 압축은 면제 사유가 아니라 검색을 한 단계 더 돌리라는 신호입니다.

「랜덤」을 과대 해석하는 세 가지

첫째, 주소창 자물쇠를 난수 합격으로 읽는 경우입니다. HTTPS/TLS는 브라우저와 현재 TLS 종단 사이 전송을 보호합니다. 서버가 HTTPS로 비밀번호를 POST받아도 자물쇠는 켜져 있습니다. 자물쇠는 Math.random인지 getRandomValues인지 답하지 않습니다.

둘째, 소스에 getRandomValues가 한 번이라도 나왔다고 버튼이 CSPRNG를 탄다고 보는 경우입니다. 같은 번들이 파일 암호화나 열람 후 파기 데모에는 Web Crypto를 쓰고, 비밀번호 생성만 Math.random일 수 있습니다. 반대로 광고·애니메이션의 Math.random으로 비밀번호 경로를 부정할 수도 없습니다. 「생성」 클릭의 호출 스택이 기준입니다.

셋째, 「업로드 안 함」을 「예측 불가능」과 동일시하는 경우입니다. Network가 깨끗하다는 것은 이번 관찰에서 비밀번호가 HTTP 업무 필드로 탭을 떠나지 않았다는 뜻입니다. 확장 프로그램은 DOM을 읽을 수 있고, 복사한 문자열은 클립보드 히스토리에 남을 수 있습니다. 그건 다른 글의 경계입니다. 여기서는 난수 품질, 업로드 여부, 복사 이후 잔존—셋이 다른 일임을 먼저 나누면 됩니다.

자주 묻는 질문

생성된 비밀번호가 겉보기에 섞여 있으면 로그인 비밀번호로 써도 되나요?

겉보기만으로는 안 됩니다. 대소문자·숫자·기호가 섞여 있다는 것은 문자 종류가 넓을 수 있다는 뜻일 뿐, 생성 과정이 예측 불가능하다는 증명은 아닙니다. MDN은 Math.random을 보안 용도에 쓰지 말라고 적습니다. 길이, 여러 사이트 재사용, 생성 경로가 crypto.getRandomValues인지도 함께 보세요.

오프라인에서도 비밀번호가 생성되면 난수가 안전한가요?

이번 생성이 실시간 API에 의존하지 않았고 서버가 대신 뽑지 않았다는 것만 증명합니다. Math.random도 오프라인에서 동작합니다. 오프라인을 통과했다면 Sources에서 getRandomValues 경로를 확인하고, Network에 방금 만든 문자열이 없어야 합니다.

소스에 Math.random과 getRandomValues가 둘 다 있으면 써도 되나요?

「생성」 버튼이 어느 함수를 부르느냐에 달립니다. 애니메이션·표시 순서는 Math.random일 수 있습니다. 비밀번호·토큰·키는 getRandomValues여야 합니다. Sources에서 클릭 핸들러부터 읽으세요. 사이트 전체 검색 횟수만 보지 마세요.

FastPwd 비밀번호 생성기는 어떤 난수 API를 쓰나요? 업로드하나요?

무작위 문자와 읽기 쉬운 암호 모두 crypto.getRandomValues를 호출하고, 32비트 정수를 문자 집합에 매핑할 때 거부 샘플링으로 나머지 편향을 버립니다. API가 없으면 생성을 거부합니다. 길이 6–128자, 기본값 16자, 8자 미만이면 보안이 낮다고 알려 줍니다. 결과는 기본적으로 업로드하지 않습니다. 열어 바로 쓰며 계정은 필요 없습니다.

열어 바로 쓰는 도구로 같은 테스트 문자열을 대조하기

난수 경계가 분명한 페이지로 연습하려면 FastPwd 비밀번호 생성기를 열면 됩니다. 가입은 필요 없습니다. 무작위 모드를 기본 16자로 두고 버릴 테스트 문자열을 하나 만드세요. 다섯 번째 절대로: Sources에서 「생성」까지 getRandomValues와 거부 샘플링 루프를 읽고, Network에서 전체 문자열을 검색해 요청 줄과 body에 맞지 않는지 확인하세요.

생성 뒤 「널리 남용되는 비밀번호처럼 보이는가」를 보려면 다른 버릴 테스트 문자열을 비밀번호 강도 검사에 넣으세요. 검사할 비밀번호는 업로드하지 않습니다. 미적중은 전 웹에서 본 적 없다는 증명이 아니고, 적중은 너무 흔하다는 뜻일 뿐입니다. 그건 취약 비밀번호 목록의 경계이지 CSPRNG의 경계가 아닙니다. 막 쓸 실제 비밀번호를 온라인 검사 칸에 붙이지 마세요.

낯선 생성 페이지를 열기 전에 세 가지만 기억하세요. 겉보기에 섞여 있다고 예측 불가능하다는 증명은 아닙니다. Math.random은 통계적으로 나아졌어도 CSPRNG가 아닙니다. 소스에 getRandomValues가 있다고 충분하지 않고, 클릭한 경로를 읽고 카나리아로 업무 데이터가 밖으로 나가지 않았는지 확인해야 합니다. 반복 가능한 관찰만 여러분의 검토 체크리스트에 넣을 가치가 있습니다.

더 읽기

「로컬 암호화, 업로드 안 함」을 증거로 쓰지 말고 카나리아로 Network를 검색하는 방법은 브라우저 로컬 암호화에서 원문이 이 기기를 떠나지 않았는지 확인하는 방법을 참고하세요. 로컬 취약 비밀번호 목록이 잡는 것과 전 웹 조회가 아닌 이유는 로컬 취약 비밀번호 목록이 증명할 수 있는 것과, 전 웹 조회가 아닌 이유에 적어 두었습니다. 생성 후 비밀번호를 동료에게 한 번만 넘겨야 한다면, # 뒤가 HTTP에 기본적으로 들어가지 않는 이유는 URL 해시 뒤에 키를 두는 이유, 그리고 그 보호가 깨질 때를 보세요.