안전한 난수 생성: 강력한 암호화의 기초
안전한 난수 생성은 암호화에 필수적입니다. crypto.getRandomValues의 작동 원리와 약한 무작위성이 보안 모델을 깨는 이유.
안전한 난수 생성은 다른 모든 암호화 조각이 그 위에 놓이는 기반입니다. JavaScript에서 crypto.getRandomValues(buffer)는 운영 체제의 CSPRNG(/dev/urandom on Linux/macOS, BCryptGenRandom on Windows, SecRandomCopyBytes on iOS/macOS)에서 암호학적으로 안전한 무작위 바이트로 TypedArray를 채웁니다. 보안과 관련된 어떤 것에도 Math.random()을 사용하지 마세요. 이것은 속도를 위해 설계된 Mulberry32 또는 xorshift 스타일 PRNG로, 예측 불가능성이 아니며, 몇 가지 샘플을 관찰한 후 출력을 예측할 수 있습니다. 약한 RNG는 AES 키, TLS 핸드셰이크, GCM의 논스 고유성, 토큰 예측 불가능성, 그리고 예측 불가능한 비트에 의존하는 다른 모든 보안 프리미티브를 깨뜨립니다.
무작위와 암호학적 무작위의 차이
PRNG(의사 난수 생성기)는 시드에서 결정론적 스트림을 생성합니다. 시드와 알고리즘이 주어지면 모든 출력을 재현할 수 있습니다. 게임, 시뮬레이션, 몬테카를로 방법에는 적합합니다. 암호화에는 재앙입니다.
CSPRNG(암호학적으로 안전한 PRNG)는 진정한 엔트로피 소스(열 잡음, 인터럽트 타이밍, Intel의 RDSEED 같은 하드웨어 RNG 명령어)로 시드되고, 출력이 계산적으로 진정한 무작위와 구별 불가능하며, 과거 출력을 관찰해도 미래 출력을 예측하는 데 도움이 되지 않는 설계입니다.
JavaScript는 둘 다 제공합니다. Math.random()은 PRNG입니다. crypto.getRandomValues()는 운영 체제 CSPRNG를 감싸는 래퍼입니다. 한 줄 코드 차이, 엄청난 보안 차이.
정규 올바른 사용법
// 256비트 AES 키 분량의 무작위 바이트 생성
const keyBytes = crypto.getRandomValues(new Uint8Array(32));
// 96비트 GCM 논스 생성
const nonce = crypto.getRandomValues(new Uint8Array(12));
// 128비트 솔트 생성
const salt = crypto.getRandomValues(new Uint8Array(16));
// URL 안전 무작위 토큰 생성
const tokenBytes = crypto.getRandomValues(new Uint8Array(32));
const token = btoa(String.fromCharCode(...tokenBytes))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
crypto.getRandomValues()는 동기식이며, 버퍼를 제자리에서 채우고, 버퍼를 반환합니다. 단일 호출당 최대 요청 크기는 65,536바이트입니다(블로킹을 방지하기 위해 스펙에서 부과한 할당량). 더 큰 무작위 자료는 반복 호출하세요.
Node.js 동등물
const { randomBytes, randomFillSync, webcrypto } = require('crypto');
const keyBytes = randomBytes(32); // Buffer 반환
// 또는 Web Crypto 호환
const nonce = webcrypto.getRandomValues(new Uint8Array(12));
Node의 randomBytes는 Web Crypto와 같은 기본 CSPRNG에서 가져옵니다. 코드 스타일에 맞는 API를 사용하세요. 두 환경에서 실행되는 동형 코드의 경우 webcrypto.getRandomValues가 브라우저와 정확히 일치합니다.
Math.random이 실패하는 이유
V8(Chrome/Node), SpiderMonkey(Firefox), JavaScriptCore(Safari) 모두 Math.random()을 암호화 보장 없는 빠른 PRNG로 구현합니다. V8은 xorshift128+ 변형을 사용합니다. 연구자들은 약 5개의 출력을 관찰한 후 내부 상태를 복구하고 모든 미래 출력을 예측할 수 있음을 보여주었습니다. 2015년 Mike Pound와 동료들은 실제 버그 바운티에서 V8의 Math.random 상태를 역산했습니다.
Math.random()을 세션 토큰, 비밀번호 재설정 링크, 암호화 논스, 또는 공유 ID를 생성하는 데 사용하면, 그 중 몇 가지를 관찰하는 공격자가 나머지를 예측할 수 있습니다. 이것은 이론적인 것이 아닙니다. 감사에서 흔한 버그 클래스입니다.
흔한 오용
Math.random()으로 라이브러리 PRNG 시드:
// 나쁨
const seed = Math.floor(Math.random() * 2**32);
다운스트림은 시드보다 더 무작위일 수 없습니다. 대신 crypto.getRandomValues(new Uint32Array(1))[0]를 사용하세요.
엔트로피로 Date.now() 사용: 시간은 좁은 창 내에서 예측 가능합니다. 작은 무작위 요소와 결합해도 타임스탬프는 공격자에게 충분한 비트를 유출합니다.
소스를 XOR하여 자체 개발: 하지 마세요. 운영 체제 CSPRNG는 이미 모든 유용한 엔트로피 소스를 혼합합니다. 자체적인 혼합을 추가하면 일반적으로 엔트로피를 증가시키는 대신 감소시킵니다.
범위 생성 시 모듈러 편향: randomBytes[0] % 10은 256이 10의 배수가 아니기 때문에 0-9에서 균일하게 분포되지 않습니다. 범위 내 균일한 무작위 정수의 경우 기각 샘플링을 사용하세요:
function randomInt(max) {
const range = new Uint32Array(1);
const threshold = 2**32 - (2**32 % max);
do {
crypto.getRandomValues(range);
} while (range[0] >= threshold);
return range[0] % max;
}
엔트로피 소스와 부팅 시간 문제
Linux에서 /dev/urandom은 초기 부팅 후 항상 안전합니다. 하드웨어 RNG가 없는 시스템의 부팅 첫 몇 초 동안 커널 풀이 부족하게 시드될 수 있습니다. 이는 2008년 Debian OpenSSL 버그에서 악용되었습니다. 패치가 엔트로피 혼합을 제거하고 시드로 프로세스 ID만 남겨 이 창에서 생성된 키가 2^15개의 가능한 값만 가졌습니다. 초 단위로 열거됩니다.
최신 시스템은 CSPRNG를 다음에서 시드합니다: x86-64의 RDSEED(사용 가능한 경우), ARMv8.5-A RNG 명령어, 다양한 주변 장치의 열 잡음, 인터럽트 타이밍, 대화형이면 키보드/마우스. Intel Ice Lake나 AMD Zen 3+ 같은 하드웨어를 사용하는 서버에서 CSPRNG는 부팅 마이크로초 내에 시드됩니다.
Docker 컨테이너의 경우: 호스트의 /dev/urandom이 기본적으로 전달됩니다. 조치가 필요 없습니다. 서버리스(AWS Lambda, Cloudflare Workers)의 경우 런타임이 호출당 엔트로피 시딩을 처리합니다.
세션 토큰과 공유 ID
파일 전송 서비스의 경우 다음에 대한 무작위 식별자를 생성합니다:
- URL의 파일 ID (공격자가 유효한 ID를 추측할 수 없어야 함)
- 비밀번호 보호 링크를 위한 공유 토큰
- CSRF 토큰
- 암호화 키 (파일당 AES 키)
- GCM 논스
최소 길이: 충돌과 예측 불가능성을 위해 128비트(16바이트), 키는 256비트(32바이트). base64url 인코딩을 통한 URL 안전 인코딩은 길이를 약 33% 증가시키고, 16진수는 100% 증가시킵니다.
32바이트 base64url 인코딩 토큰은 43자이고 2^256에서 본질적으로 충돌이 없습니다.
약한 RNG 테스트
RNG가 깨졌거나 약하다는 징후:
- 다른 요청에 의해 생성된 동일한 토큰(거대해야 할 공간에서 충돌)
- 출력이 시각적 테스트를 통과하지만
dieharder또는PractRand통계 배터리를 실패 - 프로세스 재시작 후 시드 재사용 — 각 배포가 같은 초기 상태를 사용
- 생성된 키가 패턴에 빠짐 (예: 첫 4바이트는 다르지만 마지막 28개는 동일)
프로덕션에서는 무언가가 치명적으로 잘못되지 않는 한 이것들을 보지 못할 것입니다. 실패 모드는 일반적으로 조용합니다. 공격이 2^256 검색 공간이어야 할 것에서 실용적이 될 뿐입니다.
감사: 코드베이스의 Math.random()에 대한 모든 호출을 검토해야 합니다. 소스 트리에서 Math.random을 grep하는 것은 주간 위생 검사입니다. 보안 관련 콜사이트를 crypto.getRandomValues로 변환하는 것은 몇 분이 걸리고 실제 취약점을 방지합니다.
무작위 문자열과 UUID
사람이 읽는 식별자의 경우 crypto.randomUUID()는 표준 형식으로 v4 UUID(122비트 무작위성)를 반환합니다:
const id = crypto.randomUUID();
// "f47ac10b-58cc-4372-a567-0e02b2c3d479"
Chrome 92+, Firefox 95+, Safari 15.4+, Node 14.17+에서 지원됩니다. 데이터베이스 기본 키, API 요청 ID, 비보안 중요 고유 식별자에 적합합니다. 사용자 정의 형식이나 더 높은 엔트로피가 필요한 것에는 명시적 getRandomValues를 사용하세요.
서버에서: 사용자 정의 RNG 풀 피하기
일부 서버 프레임워크는 시스템 CSPRNG와 애플리케이션 수준 엔트로피를 "혼합"한다고 주장하는 자체 무작위 풀을 제공합니다. 이를 의심하세요. 사용자 정의 혼합은 커널 출력을 거의 개선하지 않으며 버그가 있으면 조용히 엔트로피를 줄일 수 있습니다.
Node나 주요 런타임에서는 내장 crypto.randomBytes가 올바르고 빠릅니다. 제3자 믹서로 교체하지 마세요.
결론
파일 전송 앱의 모든 암호화 조각은 예측 불가능한 무작위 바이트에 의존합니다. AES 키, GCM 논스, PBKDF2 솔트, 공유 토큰, 반CSRF 토큰, 세션 ID — 모두 같은 프리미티브가 필요합니다. 브라우저에서는 crypto.getRandomValues(), Node에서는 crypto.randomBytes() 또는 webcrypto.getRandomValues. 그것들을 사용하세요. 절대 Math.random() 사용 금지. 절대 타임스탬프 사용 금지. 절대 자체 믹서 사용 금지.
HexaTransfer는 모든 파일당 AES 키, 논스, URL 식별자를 클라이언트에서 crypto.getRandomValues()로 파생합니다. 서버 측 공유 ID는 crypto.randomBytes에서 나옵니다. 하나의 API, 일관된 동작, 우연히 예측 가능한 비트를 시스템에 유출할 방법 없음.
프리미티브는 정확히 그래야 하기 때문에 지루합니다. 지루하고, 올바르며, 어디서나 사용 가능 — 암호화 기반이 되어야 할 정확한 것입니다.
hexatransfer.com에서 사용해 보세요 — 무료, 계정 불필요, 최대 10GB.
엔드투엔드 암호화로 대용량 파일을 안전하게 전송
엔드투엔드 암호화로 최대 10GB의 파일을 무료로 전송하세요. 계정이 필요하지 않습니다. 업로드 전에 브라우저에서 파일이 암호화되어 다른 사람은 읽을 수 없습니다.
파일 보내기