빌드노트
AES-GCM 암호화는 되는데 exportKey만 실패한다
AES-GCM 키로 암·복호화가 되어도 extractable이 false이면 raw 내보내기는 거절된다. Node.js 22.23.2에서 두 설정을 비교하고, 실측 예외 이름이 명세와 다른 점과 usages를 따로 확인할 이유를 설명한다.
AES-GCM의 CryptoKey로 암·복호화는 되는데 exportKey('raw', key)만 거절된다면 key.extractable을 먼저 본다. 키를 연산에 사용할 수 있는지와 키 재료를 밖으로 꺼낼 수 있는지는 별도 설정이다. extractable이 false인 키도 허용된 암·복호화에는 사용할 수 있다.
실패한 호출에 넘긴 키를 확인한다
다음 코드는 기존 코드에서 exportKey()에 넘긴 바로 그 key의 알고리즘과 두 설정을 확인한다. 키 바이트를 출력할 필요는 없다.
console.log({
algorithm: key.algorithm.name,
extractable: key.extractable,
usages: key.usages
});
algorithm이 AES-GCM, extractable이 false, usages가 ['encrypt', 'decrypt']라면 암·복호화는 허용하지만 내보내기는 허용하지 않은 키다. Node.js 22.23.2의 CryptoKey 속성 설명 (새 탭에서 열림)은 extractable을 내보내기 허용 여부로, usages를 사용할 수 있는 연산 목록으로 구분한다. exportKey는 usages에 추가하는 연산명이 아니다.
W3C의 exportKey 처리 절차 (새 탭에서 열림)는 extractable이 false이면 반환한 Promise를 InvalidAccessError로 거절하도록 정한다. 다만 아래에서 직접 실행한 Node.js 22.23.2의 예외 이름은 InvalidAccessException이었다. 해당 버전의 공식 구현 소스 (새 탭에서 열림)의 exportKey()도 이 이름과 key is not extractable 메시지를 사용한다. 명세의 이름과 이 버전의 관찰값을 같다고 적을 수는 없다.
같은 연산 용도로 생성해 내보내기만 비교했다
다음 코드는 새 AES-GCM 키 두 개를 만들고, extractable의 차이가 암·복호화와 raw 내보내기에 어떻게 나타나는지 확인한다. Node.js 22가 설치된 환경에서 내용을 reader-example.mjs라는 UTF-8 파일로 저장한 뒤 그 폴더에서 node reader-example.mjs를 실행한다. 외부 패키지는 필요 없다. 기존 키나 실제 비밀값을 넣지 않는다.
import { webcrypto } from 'node:crypto';
const { subtle } = webcrypto;
const data = new TextEncoder().encode('demo');
for (const extractable of [false, true]) {
const key = await subtle.generateKey(
{ name: 'AES-GCM', length: 256 },
extractable,
['encrypt', 'decrypt']
);
const iv = webcrypto.getRandomValues(new Uint8Array(12));
const algorithm = { name: 'AES-GCM', iv };
const ciphertext = await subtle.encrypt(algorithm, key, data);
const restored = await subtle.decrypt(algorithm, key, ciphertext);
console.log(`extractable=${key.extractable} encrypt=OK decrypt=${
new TextDecoder().decode(restored) === 'demo'}`);
try {
const raw = await subtle.exportKey('raw', key);
console.log(`raw=OK bytes=${raw.byteLength}`);
} catch (error) {
console.log(`raw=${error.name}`);
}
}
각 키는 합성 문자열 demo를 한 번 암호화하며, 암호화마다 새 무작위 12바이트 IV를 만든다. 복호화에는 해당 암호화의 IV를 그대로 전달한다. Node의 AES-GCM IV 설명 (새 탭에서 열림)은 같은 키의 암호화마다 IV가 고유해야 한다고 명시한다. 출력에는 키 바이트를 포함하지 않았다.
2026년 10월 22일 Windows의 Node.js 22.23.2에서 새 프로세스로 실행한 결과다. decrypt=true는 복호화한 문자열이 원래 demo와 같았다는 뜻이다.
extractable=false encrypt=OK decrypt=true
raw=InvalidAccessException
extractable=true encrypt=OK decrypt=true
raw=OK bytes=32
두 키 모두 암·복호화에 성공했고 내보내기 결과만 달랐다. 마지막의 32바이트는 256비트 AES 키의 raw 길이다. 두 키는 독립적으로 생성한 새 키이며 같은 키를 잠갔다가 푼 비교가 아니다.
예외 이름이 InvalidAccessError로 나온다면 위 버전의 출력과는 다르지만, extractable: false로 인해 내보내기가 거절되는지를 먼저 확인한다. 반대로 extractable: true인데도 실패하면 예외를 그대로 읽고 알고리즘·키 종류·내보내기 형식의 조합을 확인한다. Node의 내보내기 지원 목록 (새 탭에서 열림)에서 AES-GCM은 raw와 JWK를 지원한다. 내보내기 허용이 모든 형식의 지원을 뜻하지는 않는다.
별도 대조에서는 생성 인자를 true, ['encrypt']로 정했다. 이 키는 암호화와 32바이트 raw 내보내기에 성공했지만, 자신이 만든 암호문을 복호화할 때 InvalidAccessError로 거절됐다. extractable을 켜도 decrypt 용도가 생기지 않는다는 관찰이다. 암호화는 되는데 복호화가 안 되는 문제라면 내보내기 설정을 바꾸기 전에 usages를 확인해야 한다.
내보내기가 필요한지는 키를 만들 때 정한다
키를 현재 환경의 연산에만 사용할 설계라면 extractable: false를 유지하고, raw 키를 요구하는 호출이 왜 필요한지 확인한다. 다른 환경으로 키를 전달하거나 별도로 보관해야 하는 설계라면, 새 키를 만드는 시점에 내보내기 허용과 그 키 재료를 다룰 경로를 함께 정한다. generateKey()의 두 번째 인자가 내보내기 허용 여부이고 세 번째 인자가 연산 용도다.
CryptoKey.extractable은 읽기 전용 속성이다. 생성 코드의 두 번째 인자를 true로 바꿔 다시 실행하면 새 키가 만들어진다. 기존 키의 설정이 바뀌거나 기존 암호문을 새 키로 복호화할 수 있게 되는 것은 아니다. 이미 만든 비추출 키를 되살리는 방법으로 이 수정을 적용하지 않는다. 키를 가져오는 코드라면 importKey()에 전달한 extractable 인자도 확인한다. Node의 키 생성·가져오기 인자 (새 탭에서 열림)
형식을 JWK로 바꾸거나 wrapKey()로 감싸는 것도 비추출 키를 내보내는 해결책은 아니다. 명세는 exportKey()와 wrapKey() (새 탭에서 열림) 모두 대상 키의 extractable을 검사하도록 정한다.
직접 확인한 범위는 Windows의 Node.js 22.23.2에서 새로 생성한 AES-GCM 키, 작은 합성 입력, raw 내보내기와 연산 용도 차이다. 다른 Node 버전과 브라우저 실행, JWK 내보내기, wrapKey() 실행, 실제 키 보관·이전 절차는 검증하지 않았다. 그중 형식 지원과 감싸기 제한은 공식 문서에 근거한 설명이다.