Web Crypto API 가이드: 개발자를 위한 브라우저 네이티브 암호화
암호화 파일 전송 앱 구축을 위한 Web Crypto API 마스터. AES-GCM, RSA-OAEP, 브라우저 키 관리 완벽 가이드.
Web Crypto API(window.crypto.subtle을 통해 노출되는 W3C Web Cryptography API 권고안에 명시)는 네트워크를 통해 암호화 라이브러리를 제공하지 않고도 브라우저에서 기본적으로 암호화를 수행하는 방법입니다. 모든 주요 브라우저(Chrome 37+, Firefox 34+, Safari 10.1+, Edge 79+)에서 AES-GCM, AES-CBC, AES-CTR, AES-KW, HMAC, RSA-OAEP, RSA-PSS, RSASSA-PKCS1-v1_5, ECDH, ECDSA, HKDF, PBKDF2를 지원합니다. 파일 전송 애플리케이션에서 이것이 중요한 이유는 업로드 전에 모든 암호문 바이트를 클라이언트 사이드에서 생성할 수 있고, 브라우저가 상수 시간의 감사된 구현을 제공하기 때문입니다. 이 가이드는 암호화 파일 전송에 중요한 프리미티브와 첫 구현에서 모든 사람이 걸리는 함정을 설명합니다.
SubtleCrypto는 Promise 기반이고 비동기입니다
crypto.subtle의 모든 메서드는 Promise를 반환합니다. 이는 의도적입니다. 암호화 작업은 하드웨어나 백그라운드 스레드로 오프로드될 수 있으므로, 비동기를 강제함으로써 API가 메인 스레드를 차단하는 방식으로 잘못 사용되는 것을 방지합니다. 코드 형태:
const key = await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
true, // 추출 가능
["encrypt", "decrypt"]
);
두 번째 인자(true)는 키를 추출 가능으로 표시하며, 나중에 crypto.subtle.exportKey()를 통해 내보낼 수 있습니다. 장기 키의 경우 JavaScript에서 원시 바이트에 접근할 수 없도록 false로 설정하세요. URL 프래그먼트에 직렬화해야 하는 키(HexaTransfer 패턴)의 경우 true로 설정하세요.
세 번째 인자는 키 사용법 배열입니다. ["encrypt"]로 생성된 키는 AES-GCM이 대칭이더라도 복호화에 사용할 수 없습니다. 이 분리는 침해된 암호화 흐름이 과거 데이터를 복호화하는 데 남용되는 것을 방지합니다.
대칭 파일 암호화를 위한 AES-GCM
AES-GCM은 파일 콘텐츠의 주력입니다. 인증된 연관 데이터 AEAD(암호문 + 인증 태그 + 인증되지만 암호화되지 않는 선택적 연관 데이터)를 제공합니다. 파일 전송에서는 NIST SP 800-38D 권고에 따라 256비트 키와 96비트 논스를 사용하세요.
const iv = crypto.getRandomValues(new Uint8Array(12)); // 96비트 논스
const ciphertext = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
key,
plaintext
);
출력에는 암호문에 추가된 128비트 GCM 인증 태그가 포함됩니다. 복호화는 태그를 자동으로 검증하고 일치하지 않으면 예외를 발생시킵니다. 같은 키로 논스를 절대 재사용하지 마세요. GCM의 보안은 논스 재사용 시 완전히 붕괴됩니다(공격자가 인증 키를 복구할 수 있음). 각 파일이 새 키를 받는 파일 전송에서는 무작위 논스가 안전합니다. 장기 키의 경우 카운터를 사용하세요.
비밀번호 파생 키를 위한 PBKDF2
사용자가 파일을 보호하기 위해 비밀번호를 입력할 때 비밀번호를 AES 키로 직접 사용할 수 없습니다. 먼저 PBKDF2를 통해 실행하세요:
const passwordKey = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(password),
"PBKDF2",
false,
["deriveKey"]
);
const aesKey = await crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt: crypto.getRandomValues(new Uint8Array(16)),
iterations: 600000,
hash: "SHA-256",
},
passwordKey,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
);
OWASP의 2023 비밀번호 해싱 지침은 PBKDF2-SHA-256의 경우 600,000회 반복을 권고합니다. 310,000회 미만은 현재 모범 사례 이하입니다. 솔트는 무작위이어야 하며 암호문과 함께 저장해야 합니다(비밀이 아니지만 고유해야 합니다).
2026년의 새 코드에서는 PBKDF2 대신 Argon2id를 고려하세요. Argon2는 아직 Web Crypto API에 없지만, argon2-browser나 @noble/hashes 같은 라이브러리가 JavaScript/WASM 구현을 제공합니다. Argon2id는 GPU 공격에 훨씬 더 잘 저항합니다.
키 래핑을 위한 RSA-OAEP
수신자의 공개 키로 파일의 AES 키를 암호화하려는 시나리오에서는 RSA-OAEP를 사용하세요. 키 생성:
const keyPair = await crypto.subtle.generateKey(
{
name: "RSA-OAEP",
modulusLength: 4096,
publicExponent: new Uint8Array([1, 0, 1]), // 65537
hash: "SHA-256",
},
true,
["encrypt", "decrypt"]
);
새 키에는 modulusLength 4096을 사용하세요. 2048은 허용되지만 양자 시간표가 확정됨에 따라 폐기되기 시작할 것입니다. RSA-OAEP는 소형 페이로드만 암호화합니다(modulusLength/8 - 2*hashLength - 2 바이트 이하). 파일 콘텐츠를 직접 암호화하는 대신 256비트 AES 키를 래핑하세요.
성능에 민감한 앱에서는 P-256 또는 P-384를 사용하는 ECDH가 RSA보다 더 나은 대안입니다. 키 생성 속도가 한 자릿수 더 빠르고 키 크기가 훨씬 작습니다.
대형 파일을 위한 스트리밍
2GB 파일은 브라우저 ArrayBuffer에 편안하게 들어가지 않습니다. Chrome, Firefox, Safari 모두 File.stream()으로 파일을 읽어 ReadableStream을 반환하고 청크 단위로 처리할 수 있습니다. Web Crypto API 자체에는 아직 스트리밍 암호화/복호화 메서드가 없으므로(스펙의 공백), 두 가지 해결 방법이 있습니다:
- 청크로 분할하고(64KB 또는 1MB) 각각을 고유한 논스로 암호화합니다. 수신자는 순서대로 연결합니다. 전체 파일에 대한 진정한 AEAD를 잃지만 대부분의 경우에 작동합니다.
- 스트리밍 AEAD 모드(XChaCha20-Poly1305 또는 AES-GCM-SIV)를 지원하는 WASM 암호화 라이브러리(libsodium.js, WASM 백엔드를 사용하는 @noble/ciphers)를 사용합니다.
수백 메가바이트 미만의 전송에서는 버퍼링된 AES-GCM이 잘 작동하며 훨씬 더 간단합니다. 그 이상에서는 메모리 압력을 피하기 위해 스트리밍이 필요합니다.
키 내보내기, 가져오기, URL 프래그먼트
키가 URL 프래그먼트로 이동하는 HexaTransfer 스타일 흐름에서:
const rawKey = await crypto.subtle.exportKey("raw", aesKey);
const keyBase64 = btoa(String.fromCharCode(...new Uint8Array(rawKey)));
// https://example.com/file/abc123#key=keyBase64 같은 URL 공유
URL 프래그먼트는 HTTP 요청에서 서버로 전송되지 않습니다(브라우저가 제거). 이는 사용자가 링크를 공유해도 키가 클라이언트 사이드에 유지됨을 의미합니다. 수신자 측에서:
const keyBase64 = window.location.hash.slice(5); // "#key=" 제거
const rawKey = Uint8Array.from(atob(keyBase64), c => c.charCodeAt(0));
const key = await crypto.subtle.importKey(
"raw", rawKey, "AES-GCM", false, ["decrypt"]
);
URL 인코딩 문제를 피하려면 base64url 인코딩(+를 -로, /를 _로 교체, 패딩 제거)을 사용하세요.
흔한 함정
솔트나 IV에 Math.random() 사용. Math.random()은 암호학적으로 안전하지 않습니다. 항상 crypto.getRandomValues()를 사용하세요.
같은 키로 IV 재사용. GCM의 보안 속성은 논스 재사용 시 완전히 무너집니다. 무작위 96비트 논스는 같은 키 하에 약 2^48번의 암호화 후 충돌이 발생합니다. 파일당 자체 키가 있는 파일 전송에서는 안전하지만, 장기 키는 카운터를 사용하세요.
HTTPS 잊기. crypto.subtle은 보안 컨텍스트(HTTPS 또는 localhost)에서만 사용 가능합니다. 안전하지 않은 출처에서는 crypto.subtle이 undefined입니다.
IndexedDB에 추출 가능한 키를 보호 없이 저장. 키를 지속해야 한다면 저장 전에 래핑하세요. 출처의 모든 스크립트가 접근 가능한 localStorage에는 절대 원시 AES 키를 저장하지 마세요.
PBKDF2 없이 사용자 제공 비밀번호를 신뢰. UTF-8 바이트로 변환된 원시 비밀번호는 256비트 키가 아닙니다. 항상 파생하세요.
인증 태그 검증 안 함. crypto.subtle.decrypt()는 AES-GCM에서 자동으로 이를 수행하지만, 위에 커스텀 프로토콜을 구현하는 경우 검사를 건너뛰지 마세요.
브라우저 지원 뉘앙스
모든 주요 브라우저가 HTTPS에서 Web Crypto를 지원합니다. 몇 가지 특이 사항:
- Safari의 PBKDF2는 수년간 Chrome/Firefox보다 느렸습니다. 차이는 Safari 15에서 해소되었습니다.
- Firefox는 더 엄격한 입력 검증을 적용합니다. Chrome에서 실행되는 코드가 Firefox에서
OperationError를 발생시킬 수 있습니다. 양쪽에서 테스트하세요. - 서비스 워커에서의 Web Crypto는 작동하지만 등록 범위가 HTTPS이어야 합니다.
- Node.js는 Node 15부터 호환 API를 가진
require("crypto").webcrypto를 제공합니다. 동형 암호화 코드에 유용합니다.
라이브러리를 대신 사용해야 할 때
Web Crypto는 기본을 잘 커버하지만 ChaCha20-Poly1305, Argon2, X25519, Ed25519(다만 Ed25519는 추가되고 있음) 같은 최신 프리미티브는 부족합니다. 이를 위해 libsodium.js(WASM을 통해) 또는 @noble/ciphers / @noble/curves(순수 JavaScript, 감사됨)가 선도적인 옵션입니다. HexaTransfer는 AES-GCM과 PBKDF2에 대해 의존성 없이 파일 전송 경로를 커버하므로 Web Crypto 프리미티브를 직접 사용합니다.
완전한 암호화 전송 흐름에서 Web Crypto는 100줄 미만의 코드로 목적을 달성합니다. AES 키 생성, 파생 또는 무작위, 파일 암호화, 암호문 업로드, 프래그먼트에 키가 있는 링크 공유, 수신자가 키를 가져와 복호화. 그것이 전부입니다.
hexatransfer.com에서 사용해 보세요 — 무료, 계정 불필요, 최대 10GB.
엔드투엔드 암호화로 대용량 파일을 안전하게 전송
엔드투엔드 암호화로 최대 10GB의 파일을 무료로 전송하세요. 계정이 필요하지 않습니다. 업로드 전에 브라우저에서 파일이 암호화되어 다른 사람은 읽을 수 없습니다.
파일 보내기