본문으로 건너뛰기
HexaTransfer
블로그로 돌아가기
암호화 및 보안

AES-GCM 구현 가이드: 인증된 암호화 올바르게 하기

웹 앱에서 AES-GCM 암호화를 올바르게 구현하세요. 논스 관리, 키 처리, 인증 암호화에서 피해야 할 함정.

AES-GCM(Galois/Counter Mode)은 AES-CTR 암호화와 GHASH 인증을 결합하여 인증된 연관 데이터를 가진 AEAD를 생성합니다. 올바른 구현은 256비트 키, 키당 고유한(절대 재사용하지 않는) 96비트(12바이트) 논스, 128비트 인증 태그, 그리고 선택적으로 인증되지만 암호화되지는 않는 인증된 연관 데이터(AAD)를 사용합니다. NIST SP 800-38D가 정확한 구성을 명시합니다. 이 중 어느 것이라도, 특히 논스 재사용을 잘못 처리하면 GCM의 보안이 붕괴됩니다. (키, 논스) 쌍을 한 번만 반복해도 공격자가 인증 키를 복구하고 임의의 암호문을 위조할 수 있습니다. 이 가이드는 브라우저, Node, 서버 컨텍스트에서 AES-GCM을 올바르게 사용하는 방법을 다룹니다.

GCM이 실제로 보장하는 것

두 가지 속성:

기밀성: 키 없이는 평문을 복구할 수 없습니다. AES-GCM의 CTR 모드 암호화 레이어가 이를 제공합니다.

무결성과 진정성: 암호문, 논스, 연관 데이터에 대한 수정은 복호화를 실패하게 합니다. GHASH는 복호화 시 상수 시간으로 검증되는 128비트 태그를 생성합니다.

GCM이 보장하지 않는 것: 부인 방지(대칭이므로 키를 가진 누구나 유효한 암호문을 생성할 수 있음), 재생 보호(더 상위 레이어의 문제), 순서(스트림의 경우 어떻게든 연결해야 함).

핵심 통찰: GCM은 논스가 키당 고유할 때만 안전합니다. 대부분 고유, 보통 고유가 아니라 실제로 고유. 재사용에서 보안 증명이 무너집니다.

논스 관리: 가장 중요한 것

96비트 논스는 두 가지 방법으로 생성할 수 있습니다:

무작위: crypto.getRandomValues(new Uint8Array(12)). 단일 키 하에 무작위 96비트 논스에서 생일 경계 충돌은 약 2^48번의 암호화 후 나타납니다. NIST는 안전 마진을 제안하므로 키당 사용을 2^32로 제한하세요.

카운터: 96비트 정수를 증가시킵니다. 2^96개의 메시지까지 고유성을 보장합니다. 신뢰할 수 있는 단조 증가 상태가 필요하며, 분산 시스템에서는 어렵습니다.

파일당 새 키가 있는 파일 전송에서는 무작위 논스가 완전히 안전합니다. 단일 파일 키 하의 청크 암호화에서는 논스가 청크 인덱스를 인코딩하는 카운터를 사용하세요:

const nonce = new Uint8Array(12);
new DataView(nonce.buffer).setUint32(0, messageId);
new DataView(nonce.buffer).setBigUint64(4, BigInt(chunkIndex));

재난 케이스: 무작위 논스로 같은 공유 키 하에서 암호화하는 여러 프로세스가 초당 수백만 건의 암호화로 확장됩니다. 생일 충돌이 가능성 있어집니다. 프로세스 간에 키를 공유해야 한다면 프로세스 ID 접두사가 있는 조정된 카운터를 사용하세요.

64비트 논스를 사용하지 마세요

AES-GCM은 가변 논스 길이를 지원하지만, 96비트 논스만 NIST 800-38D에 명시된 최적화된 구성을 사용합니다. 다른 길이(보통 64 또는 128비트)는 성능을 감소시키고 복잡성을 증가시키는 GHASH 전처리 단계를 트리거합니다. Web Crypto API는 비 96비트 IV를 수락하지만 스펙은 96을 권장합니다. 그냥 96을 사용하세요.

태그 길이: 줄이지 마세요

GCM의 태그는 최대 128비트입니다. 일부 스펙은 96, 64, 심지어 32비트로 잘라낼 수 있습니다. 하지 마세요. 잘린 태그는 위조 공격을 더 쉽게 만들고, 절약(메시지당 4~12바이트)은 파일 전송에서 무관합니다. Web Crypto의 AES-GCMtagLength 파라미터를 통해 128비트 태그로 기본 설정됩니다(기본값 128). 그대로 두세요.

연관 데이터 (AAD)

AAD는 인증되지만 암호화되지 않는 데이터입니다. 암호문에 바인딩하려는 메타데이터에 사용하세요: 파일 이름, 콘텐츠 유형, 만료 타임스탬프, 업로더 ID.

await crypto.subtle.encrypt(
  {
    name: "AES-GCM",
    iv: nonce,
    additionalData: new TextEncoder().encode(JSON.stringify({
      filename: "report.pdf",
      contentType: "application/pdf",
      expires: 1712345678,
    })),
  },
  key,
  plaintext
);

공격자가 AAD를 수정하면 복호화가 실패합니다. 이는 감지 없이 저장된 암호문의 파일 이름을 바꾸는 스왑 공격을 방지합니다. 수신자는 복호화를 위해 정확한 AAD를 알아야 하므로 암호문과 함께 저장하세요.

키 생성과 파생

파일당 키:

const key = await crypto.subtle.generateKey(
  { name: "AES-GCM", length: 256 },
  true,
  ["encrypt", "decrypt"]
);

256비트가 2026년 기본값입니다. 128비트 AES는 여전히 안전하지만 포스트 양자 마진이 더 적습니다(Grover의 알고리즘이 유효 강도를 절반으로 줄임).

비밀번호 파생 키:

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"]
);

솔트를 암호문과 함께 저장하세요. 비밀이 아니고 비밀번호당 고유해야 합니다.

핵심 코드 경로

최소한의 암호화 함수:

async function encrypt(key, plaintext, aad = new Uint8Array()) {
  const nonce = crypto.getRandomValues(new Uint8Array(12));
  const ciphertext = new Uint8Array(
    await crypto.subtle.encrypt(
      { name: "AES-GCM", iv: nonce, additionalData: aad },
      key,
      plaintext
    )
  );
  return { nonce, ciphertext, aad };
}

적절한 오류 처리를 갖춘 복호화:

async function decrypt(key, { nonce, ciphertext, aad }) {
  try {
    return await crypto.subtle.decrypt(
      { name: "AES-GCM", iv: nonce, additionalData: aad },
      key,
      ciphertext
    );
  } catch (e) {
    // 인증 실패
    throw new Error("복호화 실패: 암호문 변조 또는 잘못된 키");
  }
}

decrypt 호출은 태그 불일치, 짧은 암호문, 또는 잘못된 키에서 OperationError를 발생시킵니다. 모든 예외를 무결성 실패로 처리하세요. 구분하려 하지 마세요.

청크 방식 대형 파일

수백 메가바이트를 초과하는 파일의 경우 메모리 압력을 피하기 위해 청킹하세요:

async function encryptChunks(key, file, chunkSize = 1024 * 1024) {
  const chunks = [];
  let chunkIndex = 0;
  for (let offset = 0; offset < file.size; offset += chunkSize) {
    const chunk = await file.slice(offset, offset + chunkSize).arrayBuffer();
    const nonce = new Uint8Array(12);
    new DataView(nonce.buffer).setBigUint64(4, BigInt(chunkIndex++));
    const ct = await crypto.subtle.encrypt(
      { name: "AES-GCM", iv: nonce }, key, chunk
    );
    chunks.push(new Uint8Array(ct));
  }
  return chunks;
}

주의: 청크 방식 AES-GCM은 잘림을 감지하지 못합니다. 공격자가 후미 청크를 삭제할 수 있고 살아남은 각 청크는 정상적으로 복호화됩니다. 방어를 위해 각 청크의 AAD에 총 청크 수를 포함하거나, 이를 처리하는 libsodium의 crypto_secretstream을 사용하세요.

서버 측 복호화 (Node.js)

Node의 crypto 모듈은 브라우저에서 암호화된 데이터를 복호화할 수 있습니다:

const { createDecipheriv } = require('crypto');

function decrypt(key, nonce, ciphertextWithTag) {
  const tag = ciphertextWithTag.slice(-16);
  const ct = ciphertextWithTag.slice(0, -16);
  const decipher = createDecipheriv('aes-256-gcm', key, nonce);
  decipher.setAuthTag(tag);
  return Buffer.concat([decipher.update(ct), decipher.final()]);
}

Web Crypto는 128비트 태그를 암호문에 추가하지만, Node의 API는 태그와 암호문을 별도로 기대합니다. 그에 맞게 분할하세요.

성능 수치

AES-NI를 갖춘 일반적인 2024~2026년 하드웨어에서:

  • 네이티브(OpenSSL, AES-NI): 코어당 3~5 GB/s
  • Web Crypto(하드웨어 가속이 있는 브라우저): 1~2 GB/s
  • libsodium.js WASM AES-GCM: 400~800 MB/s
  • 순수 JS(@noble/ciphers): 50~150 MB/s

1GB 파일에서 Web Crypto 암호화는 0.5~1초 걸립니다. 순수 JS는 7~20초 걸립니다. 이 현실에 기반하여 구현을 선택하세요. 대형 파일 전송 UX를 위해 Web Crypto가 실용적인 선택입니다.

흔한 함정 요약

  • 논스 재사용: 최대 실패 모드입니다.
  • crypto.getRandomValues() 대신 Math.random() 사용.
  • AAD로 연관 메타데이터를 인증하지 않음.
  • "익숙해서" CBC 모드 사용. CBC는 GCM의 무결성에 맞추기 위해 별도 MAC이 필요하고, HMAC-CBC 구성은 올바르지만 복잡하며 GCM이 함정을 피합니다.
  • 복호화 오류를 조용히 잡아 쓰레기를 반환. 항상 크게 실패하세요.
  • 자체 GCM 구현. Web Crypto, libsodium, 또는 node:crypto를 사용하세요. GHASH 구현에는 전문가들이 올바르게 처리하는 데 수년이 걸린 사이드 채널 함정이 있습니다.

HexaTransfer는 파일당 96비트 무작위 논스, 128비트 태그, 키가 파일당이고 파일 이름이 AEAD 보호 메타데이터에 별도로 저장되므로 AAD 없이 Web Crypto의 AES-256-GCM을 사용합니다. 단순하고, 올바르고, 빠릅니다.

다른 것을 선택해야 할 때

AES-GCM은 파일 전송에 최적이지만, 특정 경우에는 대안을 고려하세요:

  • XChaCha20-Poly1305: 192비트 논스로 임의 규모에서 무작위 논스 안전성이 자명합니다. AES-NI가 있는 하드웨어에서는 약간 느리지만, AES-NI가 없는 구형 ARM에서는 더 빠릅니다. libsodium이 제공합니다.
  • AES-GCM-SIV: 오용 방지. 논스 재사용이 키를 유출하지 않고 평문이 같았는지만 드러냅니다. 논스 고유성을 보장할 수 없을 때 유용합니다.

표준 웹 스택에서 대부분의 파일 전송 워크로드에서 파일당 새 키와 무작위 96비트 논스가 있는 AES-256-GCM이 올바른 선택이며 올바르게 구현하기 가장 간단한 선택입니다.

hexatransfer.com에서 사용해 보세요 — 무료, 계정 불필요, 최대 10GB.

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

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

파일 보내기