빌드노트
Node scrypt에서 N을 올렸더니 매개변수 오류가 난다
Node scrypt에서 N을 올린 뒤 매개변수 오류가 나면 호출의 maxmem 예산을 따로 확인한다. 같은 N/r/p에서 32MiB 거절과 64MiB 성공을 비교하고, 유효하지 않은 N과 비동기 호출의 예외 전달을 구별한다.
Node의 scrypt에서 비용 N을 16384에서 32768로 올렸는데 ERR_CRYPTO_INVALID_SCRYPT_PARAMS가 난다면, 실패한 호출의 maxmem부터 확인한다. r=8, p=1인 이번 비교에서는 호스트 메모리가 남아 있어도 32MiB 예산으로는 거절됐고, 같은 비용을 유지한 64MiB에서는 성공했다. maxmem은 호출에 주는 메모리 예산이며, 남은 RAM에 맞춰 자동으로 늘어나지 않는다.
먼저 실제 호출로 전달되는 N, r, p, maxmem을 확인한다. 설정 파일에서 읽은 값과 라이브러리에 넘긴 값이 같은지, 숫자로 전달하는지도 본다. maxmem을 생략했다면 Node의 기본값은 32 * 1024 * 1024바이트다. 비용 정책에 맞춰 N을 정했다면 오류를 없애려고 바로 낮추기보다, 그 비용을 허용할 호출 예산인지 따로 판단해야 한다. Node scrypt 옵션 (새 탭에서 열림)
32MiB라는 계산만으로는 충분하지 않다
Node 문서는 메모리 조건을 대략 128 * N * r와 maxmem의 비교로 설명한다. r=8일 때 N=16384면 16MiB, N=32768이면 32MiB다. 여기서 MiB는 1024 * 1024바이트다. 두 번째 값이 기본 예산과 같다고 해서 반드시 통과하는 것은 아니다. 문서의 식은 근사치다. Node scryptSync의 maxmem 설명 (새 탭에서 열림)
이번 런타임이 보고한 OpenSSL 버전은 3.5.7이었다. 같은 버전의 공개 구현에서는 주된 메모리 영역 외에 입력 블록과 추가 작업 공간도 합산한 뒤 예산과 비교한다. 그래서 근사식이 정확히 32MiB가 되는 이 조합도 32MiB 예산을 넘는다. 이 계산은 구현 소스를 읽어 확인한 설명이며, 프로세스 전체의 실제 메모리 사용량을 측정한 값은 아니다. OpenSSL 3.5.7의 예산 검사 (새 탭에서 열림)
비용은 고정하고 maxmem만 바꿔 본다
아래 예제는 공개 합성 입력으로 같은 호출을 두 번 실행해 성공 키 길이와 일치 여부를 확인한다. Node가 설치된 빈 작업 폴더에 scrypt-check.cjs라는 이름으로 저장한다. 실제 비밀번호는 넣지 않는다. 동기 API는 결과를 간단히 확인하기 위해 사용했다.
const { scryptSync } = require('node:crypto');
const N = Number(process.argv[2]);
const mib = Number(process.argv[3]);
if (![16384, 32768].includes(N) || ![32, 64].includes(mib)) {
throw new Error('N은 16384/32768, maxmem은 32/64MiB로 입력하세요.');
}
const options = { N, r: 8, p: 1, maxmem: mib * 1024 * 1024 };
try {
const derive = () => scryptSync(
'public-demo-password', 'public-demo-salt-2026', 32, options
);
const first = derive();
const second = derive();
console.log('OK', first.length, first.equals(second));
} catch (error) {
console.log(error.name, error.code, error.message);
}
같은 폴더에서 다음 네 명령으로 두 비용과 두 예산을 비교한다. 두 번째 인자는 MiB 단위이고, 코드에서 바이트로 변환한다.
node scrypt-check.cjs 16384 32
node scrypt-check.cjs 16384 64
node scrypt-check.cjs 32768 32
node scrypt-check.cjs 32768 64
Windows x64, Node.js 22.23.2, OpenSSL 3.5.7에서 각 명령의 출력은 순서대로 다음과 같았다.
OK 32 true
OK 32 true
RangeError ERR_CRYPTO_INVALID_SCRYPT_PARAMS Invalid scrypt params: error:030000AC:digital envelope routines::memory limit exceeded
OK 32 true
OK 32 true는 32바이트 키를 얻었고 동일 입력으로 다시 만든 키도 같았다는 뜻이다. 핵심은 세 번째와 네 번째 줄이다. N=32768, r=8, p=1을 유지한 채 예산만 바꿨더니 거절이 성공으로 바뀌었다. 별도 확인에서도 두 호출 직전 호스트의 여유 메모리는 모두 4GiB를 넘었다. 이 결과는 해당 거절을 호스트 RAM 부족보다 호출 예산으로 설명할 근거가 된다.
새 Node 프로세스로 비교를 반복했고 비동기 scrypt에서도 같은 성공·실패 조합을 확인했다. maxmem을 생략한 N=32768 호출도 32MiB를 명시한 경우처럼 실패했다. 다만 다른 Node·OpenSSL 버전이나 암호화 제공자 구성의 예외 이름·코드·문구까지 같다고 보장하지는 않는다. 자신의 런타임에서 실제 예외와 조건 변경 전후를 함께 확인한다.
예산을 늘려도 되는 경우와 입력을 고칠 경우
서비스에서 이 비용과 호출당 64MiB 예산을 허용하기로 했다면, 기존 옵션에 maxmem: 64 * 1024 * 1024를 명시하고 같은 입력으로 다시 확인한다. 이 실험의 64MiB는 비교용 예산이다. 동시 호출 수에 따른 총메모리, 처리 지연, 운영 비밀번호 보안 정책의 적절성은 검증하지 않았다. 고정한 합성 salt도 재현용이며, 실제 salt는 고유성과 무작위성을 고려해 생성해야 한다. Node의 salt 안내 (새 탭에서 열림)
허용 예산 안에서 여전히 거절된다면 N/r/p 조건을 확인한다. N은 1보다 큰 2의 거듭제곱이어야 하고, r과 p는 양의 정수를 명시한다. 이 밖에도 조합과 구현의 범위 제한이 있으므로 메모리 근사식 하나로 유효성을 판정하지 않는다. scrypt 매개변수 조건 (새 탭에서 열림)
별도 대조에서 N=16385는 64MiB에서도 같은 RangeError / ERR_CRYPTO_INVALID_SCRYPT_PARAMS로 거절됐다. 이때 메시지는 Invalid scrypt params였다. 따라서 오류 코드 하나만으로 메모리 예산 문제라고 단정할 수 없다. N과 별칭 cost를 동시에 지정한 경우에는 Error / ERR_CRYPTO_SCRYPT_INVALID_PARAMETER가 나왔다. r/blockSize, p/parallelization도 각각 한 이름만 사용해야 한다. Node의 옵션 별칭 규칙 (새 탭에서 열림)
비동기 API로 바꾸는 것만으로 예산 문제를 해결할 수도 없다. 이번 scrypt의 예산 초과는 콜백 실행 전에 동기 예외로 던져졌다. 비동기 호출을 진단할 때도 호출 시점의 예외와 콜백의 err를 모두 확인해야 한다. Node 문서 역시 잘못된 인자는 예외를 던지고, 키 유도 실패는 콜백으로 전달한다고 구분한다. 비동기 scrypt의 오류 처리 (새 탭에서 열림)