본문으로 건너뛰기
HexaTransfer
블로그로 돌아가기
기술 심층 분석

JavaScript 파일 암호화 단계별 튜토리얼

JavaScript로 브라우저에서 파일을 암호화하세요. AES 암호화, 키 유도, 안전한 파일 처리를 다루는 실용 튜토리얼입니다.

JavaScript는 Web Crypto API를 사용해 서버 개입 없이 브라우저에서 파일을 완전히 암호화할 수 있습니다. 표준 파이프라인: 파일을 ArrayBuffer로 읽고, PBKDF2-SHA256(60만 회 반복)으로 비밀번호에서 256비트 AES-GCM 키를 유도하고, 무작위 12바이트 IV로 암호화하고, 솔트, IV, 암호문을 다운로드 가능한 Blob으로 패키징합니다. 모든 주요 브라우저가 window.crypto.subtle을 통해 이를 기본으로 지원하며, 최대 2~3 GB 파일의 경우 서드파티 라이브러리를 사용하지 않고 최신 노트북에서 10초 이내에 완료됩니다.

중요한 알고리즘 선택

AES-CBC가 아닌 AES-GCM을 선택하세요. GCM은 128비트 태그로 변조를 감지하면서 한 번의 패스로 인증된 암호화를 제공합니다. 반면 CBC는 대부분의 튜토리얼이 잘못 구현하는 별도의 HMAC 단계가 필요합니다. 256비트 키를 사용하세요. AES-NI 하드웨어에서 128비트와의 성능 차이는 무시할 수 있습니다. 비밀번호 기반 키 유도에는 PBKDF2-SHA256을 선택하세요. WebAssembly를 통해 Argon2id를 제공할 수 없다면 — 더 강력하지만 50 KB의 다운로드 용량을 추가합니다.

피해야 할 것: ECB 모드(근본적으로 취약), 직접 구현한 패딩(10년간의 CBC 패딩 오라클 공격), MD5 또는 SHA-1(충돌 취약), 기본적으로 PKCS7을 사용한 CBC를 사용한다는 것을 이해하지 못한 채 crypto-js 패키지를 사용하는 것.

파일을 메모리로 읽기

File API는 바이트를 가져오는 세 가지 방법을 제공합니다.

const buf = await file.arrayBuffer();          // 전체 파일
const stream = file.stream();                  // 스트리밍
const text = await file.text();                // UTF-8 디코딩

500 MB 이상의 파일에서 arrayBuffer()는 모바일 Safari에서 자주 실패합니다. 대신 스트림을 사용하세요.

async function* chunks(file, size = 4 * 1024 * 1024) {
  for (let off = 0; off < file.size; off += size) {
    yield new Uint8Array(await file.slice(off, off + size).arrayBuffer());
  }
}

각 슬라이스는 디스크에서 지연 읽기되므로 최대 메모리가 제한됩니다.

사용자 비밀번호에서 키 유도

원시 비밀번호를 encrypt에 직접 전달하지 마세요. 먼저 키를 유도하세요.

async function deriveKey(password, salt) {
  const enc = new TextEncoder();
  const material = await crypto.subtle.importKey(
    'raw', enc.encode(password), { name: 'PBKDF2' }, false, ['deriveKey']
  );
  return crypto.subtle.deriveKey(
    { name: 'PBKDF2', salt, iterations: 600000, hash: 'SHA-256' },
    material,
    { name: 'AES-GCM', length: 256 },
    false,
    ['encrypt', 'decrypt']
  );
}

crypto.getRandomValues(new Uint8Array(16))로 파일별로 새 16바이트 솔트를 생성하세요. 암호문과 함께 솔트를 저장하세요. 파일 간에 솔트를 재사용하면 PBKDF2의 목적이 무력화됩니다. OWASP의 2026년 12월 비밀번호 저장 치트 시트는 현재 PBKDF2-SHA256에 60만 회 반복을 권장하며, 이는 중간급 휴대폰에서 약 500 ms의 키 유도 시간에 해당합니다.

파일 암호화

키가 준비되면 암호화는 버퍼당 subtle.encrypt 한 번 호출입니다.

async function encryptBuffer(key, plaintext) {
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const ciphertext = await crypto.subtle.encrypt(
    { name: 'AES-GCM', iv, tagLength: 128 },
    key,
    plaintext
  );
  return { iv, ciphertext };
}

GCM은 (iv, key) 쌍을 재사용하면 키스트림 충돌로 두 평문이 모두 유출되는 치명적인 실패가 발생합니다. 96비트 무작위 IV는 하나의 키로 약 2^48번의 안전한 암호화를 제공하며, 파일 암호화에는 충분합니다. 같은 키로 많은 청크를 암호화한다면 카운터와 무작위 32비트 접두사에서 IV를 유도하세요.

출력 패키징

복호화기는 솔트, IV, 암호문이 필요합니다. 작은 헤더와 함께 단일 블롭으로 패키징하세요.

function pack(salt, iv, ciphertext) {
  const magic = new TextEncoder().encode('ENC1');
  return new Blob([magic, salt, iv, new Uint8Array(ciphertext)]);
}

나중에 오래된 파일을 손상시키지 않고 알고리즘을 마이그레이션할 수 있도록 헤더를 버전 관리하세요(ENC1, ENC2…). 다음과 같이 다운로드를 제공하세요.

const url = URL.createObjectURL(packed);
const a = document.createElement('a');
a.href = url; a.download = `${file.name}.enc`; a.click();
URL.revokeObjectURL(url);

파일 복호화

복호화는 과정을 역으로 진행하며 비밀번호가 틀리거나 파일이 변조된 경우 OperationError를 던집니다.

async function decryptFile(blob, password) {
  const buf = await blob.arrayBuffer();
  const view = new Uint8Array(buf);
  const magic = new TextDecoder().decode(view.slice(0, 4));
  if (magic !== 'ENC1') throw new Error('Unknown format');
  const salt = view.slice(4, 20);
  const iv = view.slice(20, 32);
  const ct = view.slice(32);
  const key = await deriveKey(password, salt);
  const pt = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ct);
  return new Blob([pt]);
}

태그 실패와 구조 실패를 구분하지 않고 단일 오류 메시지를 표시하세요. "복호화할 수 없습니다. 비밀번호가 틀리거나 파일이 손상되었습니다." 이렇게 하면 공격자에게 패딩 오라클과 유사한 정보 유출이 제거됩니다.

메모리 부족 없이 대용량 파일 암호화

500 MB 이상의 경우 전체 파일에 arrayBuffer()를 호출하지 마세요. 카운터에서 유도된 고유 IV로 청크를 독립적으로 암호화하세요.

function ivForChunk(baseIV, index) {
  const iv = new Uint8Array(baseIV);
  const view = new DataView(iv.buffer);
  view.setUint32(8, index, false);
  return iv;
}

파일을 TransformStream을 통해 파이프하고, 각 4 MB 청크를 암호화하고, File System Access API를 통해 디스크를 가리키는 WritableStream에 결과를 씁니다. 20 GB 파일도 최대 메모리가 약 10 MB에 머뭅니다. 청크 형식은 복호화기가 올바르게 재조립할 수 있도록 헤더에 청크 크기와 수를 기록해야 합니다.

프로덕션에 출시되는 흔한 실수

자바스크립트 암호화 실제 코드 리뷰에서 세 가지 패턴이 나타납니다.

첫째, 편의를 위해 localStorage에 원시 키를 저장하는 것. localStorage는 동기적이고, 오리진 범위이며, XSS로 읽을 수 있습니다. 대신 IndexedDB에서 추출 불가능한 CryptoKey를 사용하세요.

둘째, IV나 솔트에 Math.random()을 사용하는 것. Math.random()은 예측 가능합니다. 항상 crypto.getRandomValues를 사용하세요.

셋째, subtle.encrypt가 상수 시간이라고 가정하는 것. 기본 브라우저 구현에서는 그렇지만 그것을 감싸는 자바스크립트 래퍼는 거의 확실히 그렇지 않습니다. 자신의 코드를 핫 패스에서 제외하세요.

HexaTransfer는 이 정확한 파이프라인을 적용합니다. 60만 회 반복의 PBKDF2, AES-256-GCM, 스트리밍 청크. 서버는 불투명한 암호문만 저장합니다. hexatransfer.com에서 사용해보세요 — 무료, 계정 불필요, 최대 10 GB.

구현 테스트

무작위 10 MB 블롭을 왕복하고, 바이트를 변조하고, 복호화가 예외를 던지는지 확인하는 테스트 하네스를 작성하세요. 잘못된 헤더와 잘린 암호문에 대한 퍼징을 추가하세요. 흔한 버그는 충돌 대신 거부해야 하는 slice()의 경계 검사 누락입니다. iPhone SE, 중간급 Android, Chromebook에서 벤치마크하세요. 키 유도에 2초 이상 걸리는 것은 모바일 사용자에게 너무 느립니다. 프로덕션 전에 두 번째 눈으로 감사하세요. 암호화 코드는 단순해 보이지만 테스트가 놓치는 미묘한 방식으로 깨집니다.

엔드투엔드 암호화로 대용량 파일을 안전하게 전송

엔드투엔드 암호화로 최대 10GB의 파일을 무료로 전송하세요. 계정이 필요하지 않습니다. 업로드 전에 브라우저에서 파일이 암호화되어 다른 사람은 읽을 수 없습니다.

파일 보내기