Web Crypto API 파일 암호화 완벽 튜토리얼
브라우저 기반 파일 암호화를 위해 Web Crypto API를 완벽히 익히세요. JavaScript 애플리케이션의 AES-GCM, RSA-OAEP, 키 관리입니다.
개인정보보호위원회(PIPC)와 KISA는 개인정보가 포함된 파일 전송 시 종단 간 암호화를 권고합니다. Web Crypto API는 외부 라이브러리 없이 브라우저에서 직접 파일을 암호화할 수 있게 해주는 네이티브 window.crypto.subtle 메서드를 제공합니다. 파일 암호화에는 PBKDF2(210,000 반복, SHA-256)로 비밀번호에서 키를 도출한 후, 96비트 IV와 128비트 인증 태그로 AES-256-GCM을 사용해 파일 바이트를 암호화합니다. 공개키 워크플로는 대칭 키를 래핑하는 데 4096비트 RSA-OAEP를 사용합니다.
SubtleCrypto가 순수 JS 라이브러리보다 나은 이유
window.crypto.subtle은 브라우저의 감사된 네이티브 암호화 백엔드(Chromium의 BoringSSL, Safari의 CommonCrypto)를 호출합니다. CryptoJS나 sjcl 같은 순수 JS 옵션에 비해 AES-GCM에서 30~80배 빠르고, JavaScript 인터프리터의 타이밍 사이드 채널을 피하며, 사용자에게 0바이트를 전달합니다. 트레이드오프는 ArrayBuffer와 CryptoKey 객체만 다루는 Promise 기반 API로, 100MB 이상의 파일에서는 Uint8Array, Blob, ReadableStream 사이의 변환이 원시 암호화 속도보다 더 중요해집니다.
PBKDF2로 비밀번호에서 키 도출
비밀번호를 AES 키로 직접 사용해서는 안 됩니다. 대신 비밀번호를 원시 재료로 가져온 후 256비트 키를 도출하세요:
async function deriveKey(password, salt) {
const enc = new TextEncoder();
const material = await crypto.subtle.importKey(
'raw', enc.encode(password), 'PBKDF2', false, ['deriveKey']
);
return crypto.subtle.deriveKey(
{ name: 'PBKDF2', salt, iterations: 210000, hash: 'SHA-256' },
material,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt']
);
}
OWASP의 2026년 가이드는 SHA-256으로 최소 600,000 반복을 권고하지만 저위험 맥락에서는 210,000도 허용됩니다. crypto.getRandomValues로 파일당 새로운 16바이트 솔트를 생성해 암호문과 함께 저장하세요.
AES-GCM으로 파일 암호화
AES-GCM은 단일 패스로 기밀성과 인증성을 제공합니다. (key, IV) 쌍을 절대 재사용해서는 안 됩니다:
async function encryptFile(file, key) {
const iv = crypto.getRandomValues(new Uint8Array(12));
const plaintext = await file.arrayBuffer();
const ciphertext = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv, tagLength: 128 },
key,
plaintext
);
return { iv, ciphertext };
}
2GB 파일에서 file.arrayBuffer()는 전체 버퍼를 할당해 모바일 Safari를 자주 충돌시킵니다. 파일을 4MB 청크로 분할하고, 카운터와 무작위 프리픽스를 연결해 도출된 고유 IV로 각 청크를 암호화하며, 버전 바이트와 솔트를 앞에 추가해 복호화 프로그램이 무엇을 처리하는지 알게 하세요.
TransformStream으로 대용량 파일 스트리밍
메모리 팽창을 피하려면 암호화를 TransformStream으로 감싸고 파일을 파이프하세요:
const chunkSize = 4 * 1024 * 1024;
const encryptor = new TransformStream({
async transform(chunk, controller) {
const iv = nextIV(counter++);
const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, chunk);
controller.enqueue(new Uint8Array([...iv, ...new Uint8Array(ct)]));
}
});
await file.stream()
.pipeThrough(sliceByChunks(chunkSize))
.pipeThrough(encryptor)
.pipeTo(uploadSink);
file.stream()은 디스크에서 지연 읽기하는 ReadableStream<Uint8Array>를 반환합니다. 피크 메모리는 10GB 업로드에서도 20MB 미만을 유지합니다.
RSA-OAEP로 대칭 키 래핑
특정 수신자와 파일을 공유할 때는 RSA 키쌍을 한 번 생성하고 공개 키를 게시하세요:
const keypair = await crypto.subtle.generateKey(
{ name: 'RSA-OAEP', modulusLength: 4096,
publicExponent: new Uint8Array([1,0,1]), hash: 'SHA-256' },
true, ['wrapKey', 'unwrapKey']
);
const wrapped = await crypto.subtle.wrapKey(
'raw', fileKey, keypair.publicKey, { name: 'RSA-OAEP' }
);
4096비트 RSA 키는 NIST SP 800-57에 따라 2030년까지 약 150비트 보안을 제공합니다. 순방향 비밀성이나 양자 내성이 필요하다면 P-384 ECDH를 RSA-OAEP와 결합하거나 WebCrypto 워킹 그룹이 ML-KEM(Kyber)을 도입할 때 마이그레이션하세요.
IndexedDB에 키 안전하게 저장
CryptoKey 객체는 기본적으로 추출 불가능하여 원시 바이트를 JavaScript에 노출하지 않고 IndexedDB에 유지할 수 있습니다:
const db = await openDB('keystore', 1);
await db.put('keys', keypair.privateKey, 'user-signing-key');
브라우저는 구조화 복제 알고리즘으로 키를 직렬화하고 실제 바이트를 암호화 백엔드에 유지합니다. 손상된 스크립트가 저장된 키로 encrypt나 decrypt를 호출할 수 있지만 재료를 읽어낼 수는 없습니다. 이는 base64 키를 localStorage에 저장하는 것보다 의미 있는 강화 조치입니다.
API가 던지는 오류 처리
SubtleCrypto는 인증된 복호화 실패(암호문 변조, 잘못된 IV, 틀린 비밀번호)에 OperationError를 던지고, 입력 버퍼 길이가 틀리면 DataError, 알고리즘이 구현되지 않으면 NotSupportedError, 키가 올바른 사용 플래그 없이 가져왔으면 InvalidAccessError를 던집니다. 복호화를 항상 try/catch로 감싸고, 중립적인 "파일을 복호화할 수 없습니다" 메시지를 표시하며, 태그 실패인지 구조 실패인지를 누출하지 마세요.
HexaTransfer는 모든 업로드에 이 정확한 AES-256-GCM + PBKDF2 파이프라인을 사용하며, 키는 Worker에서 도출되고 암호문은 서버가 평문을 절대 보지 못한 채 스토리지로 스트리밍됩니다.
https://hexatransfer.com — 무료, 계정 불필요, 최대 10GB.
엔드투엔드 암호화로 대용량 파일을 안전하게 전송
엔드투엔드 암호화로 최대 10GB의 파일을 무료로 전송하세요. 계정이 필요하지 않습니다. 업로드 전에 브라우저에서 파일이 암호화되어 다른 사람은 읽을 수 없습니다.
파일 보내기