예측 가능한 시드로 보안 난수 초기화

추측 가능한 값만으로 보안 난수 생성기를 초기화하는 위험

설명

보안 난수 생성기를 시스템 시간이나 고정 값만으로 초기화하면 공격자가 시드를 추측하여 토큰의 탐색 범위를 줄일 수 있습니다. SecureRandom의 실제 동작은 공급자에 따라 다르며, 모든 구현이 같은 시드로 동일한 출력을 내는 것은 아닙니다.

Java 문서에 따르면 PRNG 방식의 SecureRandom에서 첫 nextBytes() 전에 setSeed()를 호출하면 자동 시딩이 수행되지 않을 수 있으므로 호출자가 충분한 엔트로피를 제공해야 합니다. 반면 이미 확보한 시드에 대한 setSeed()는 기존 값을 대체하지 않고 보충합니다. 시간 값을 추가했다는 이유만으로 이미 충분한 엔트로피가 사라진다고 단정하면 안 됩니다.

잠재적 영향

  • 인증/세션 탈취: 예측 가능한 토큰이나 세션 ID를 추측해 다른 사용자의 세션을 가로챌 수 있습니다.
  • 권한 오용/가짜 요청: CSRF 토큰, API 키 유사 토큰을 예측해 권한 있는 요청을 위조할 수 있습니다.
  • 일회용 코드(OTP) 예측: 비밀번호 재설정 코드나 2차 인증 코드가 추측 가능해집니다.
  • 무차별 대입 범위 축소: 시간 기반 seed로 탐색 공간이 크게 줄어 공격 성공 가능성이 높아집니다.

해결 방법

  • 일반적인 토큰 생성에는 new SecureRandom()을 사용하고 공급자의 엔트로피 기반 초기화에 맡기세요. 첫 사용 전에 시간이나 상수만으로 시딩하지 마세요.
  • 명시적 시드가 꼭 필요하다면 충분히 예측 불가능한 값을 사용하세요. generateSeed(n)은 다른 난수 생성기를 초기화할 시드 바이트를 얻는 API이며, 보통은 별도 시딩 없이 사용할 수 있습니다.
  • 보안 토큰·세션 식별자에는 java.util.Random을 사용하지 마세요. 충분한 난수 길이, 만료 및 용도에 맞는 재사용 방지 정책을 적용하세요. Base64는 표현 형식일 뿐 엔트로피를 추가하지 않습니다.
  • 암호 키 생성에는 KeyGenerator 같은 전용 API를 사용하세요. 비밀번호에서 키를 유도해야 한다면 적절한 비밀번호 기반 키 유도 함수를 사용하세요.

예시

변경 전

java
import java.security.SecureRandom;
import java.util.Base64;

public class InsecureTokenService {
    // 취약: 시간 기반 seed로 초기화
    public String issueToken() {
        SecureRandom rng = new SecureRandom();
        rng.setSeed(System.currentTimeMillis()); // 예측 가능
        byte[] buf = new byte[16]; // 128비트
        rng.nextBytes(buf);
        return Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
    }
}

변경 후

java
import java.security.SecureRandom;
import java.util.Base64;

public class SecureTokenService {
    private static final SecureRandom RNG = new SecureRandom(); // 공급자의 엔트로피로 자동 초기화

    public String issueToken() {
        byte[] buf = new byte[32]; // 256비트로 예측 난이도 향상
        RNG.nextBytes(buf);
        return Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
    }

    // (선택) seed를 꼭 주어야 하는 특수 케이스라면 안전한 랜덤 seed 사용
    public String issueTokenWithExplicitSeed() {
        SecureRandom sr = new SecureRandom();
        byte[] strongSeed = sr.generateSeed(32); // 예측 불가능한 seed
        SecureRandom seeded = new SecureRandom(strongSeed);
        byte[] buf = new byte[32];
        seeded.nextBytes(buf);
        return Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
    }
}

변경 전에는 첫 난수 생성 전에 추측 가능한 시간으로 시딩합니다. 변경 후의 기본 경로는 공급자의 자동 초기화에 맡깁니다. 선택적 두 번째 메서드는 충분한 난수 시드를 별도로 얻는 경우를 보여 주며, 일반적인 토큰 생성에 필수인 절차는 아닙니다.

참조