🕵️ 친구와 암호 대화
한국어

브라우저 Web Crypto API로 암호화하기

암호화 기능을 직접 만들어 보겠다는 생각은 개발자라면 한 번쯤 해 보지만, 실제로는 가장 피해야 할 일 중 하나로 꼽힙니다. 이 글에서는 친구와 암호 대화가 자바스크립트로 암호 알고리즘을 직접 짜지 않고 브라우저에 내장된 Web Crypto API를 쓰는 이유와, 번역기 하나가 글을 암호화하고 푸는 기본 흐름을 설명합니다.

암호는 직접 만들지 않는 것이 원칙입니다

암호 알고리즘은 수식만 맞으면 되는 것이 아닙니다. 계산 시간이 입력에 따라 미세하게 달라지는 문제, 난수가 예측 가능한 문제, 메모리에 남은 흔적 같은 사소해 보이는 구현 실수가 곧바로 해독으로 이어집니다. 그래서 암호 분야에는 직접 만든 암호를 쓰지 말고 검증된 것을 쓰라는 오래된 격언이 있습니다.

자바스크립트로 알고리즘을 처음부터 구현해서 쓰는 방법도 있지만, 속도가 느리고 위와 같은 실수에서 자유롭지 못합니다. 반면 브라우저가 제공하는 Web Crypto API는 운영체제나 브라우저 제작사가 오랫동안 검증한 암호 코드를 호출하는 통로입니다. 이 서비스는 후자를 선택했고, 글을 다루는 모든 암호 연산이 crypto.subtle 을 통해 이루어집니다.

Web Crypto API가 주는 것

이 API가 제공하는 핵심은 세 가지입니다. 첫째, 암호화와 해독, 키 유도 같은 연산을 표준 이름(AES-GCM, PBKDF2, HKDF, SHA-256 등)으로 호출할 수 있습니다. 둘째, 안전한 난수를 만드는 crypto.getRandomValues 를 쓸 수 있습니다. 셋째, 키를 비추출(non-extractable) 객체로 다룰 수 있습니다.

세 번째가 특히 중요합니다. 비추출 키는 한 번 만들고 나면 자바스크립트가 원래 바이트 값을 읽어 낼 수 없는 키입니다. 암호화와 해독에는 쓸 수 있지만, 코드가 키 값을 꺼내 어딘가로 보내거나 화면에 찍을 수는 없습니다. 이 서비스의 화면은 해독 키를 가져올 때 추출 가능 여부를 거짓(false)으로 지정합니다. 그래서 키가 브라우저 내부 저장소 안의 객체로만 존재하게 됩니다.

한 가지 알아 둘 점이 있습니다. Web Crypto는 보안 컨텍스트, 즉 HTTPS 주소에서만 동작합니다. 암호화가 전송 중에 바꿔치기당하지 않도록 사이트 자체를 안전한 연결로 받는 것이 전제입니다.

번역기가 글을 암호화하는 흐름

사용자가 할 말을 적으면 브라우저 안에서 다음 순서로 일이 일어납니다.

  1. 글을 UTF-8 바이트로 바꿉니다. 압축했을 때 더 짧아지면 압축한 쪽을 쓰고, 그렇지 않으면 원본을 씁니다.
  2. 앞 2바이트 헤더(버전과 압축 여부 표시)를 만들고, 12바이트짜리 무작위 IV를 새로 뽑습니다.
  3. AES-GCM으로 암호화합니다. 이때 앞 2바이트 헤더를 AAD(추가 인증 데이터)로 함께 묶습니다.
  4. 헤더, IV, 암호문(인증 태그 포함)을 이어 붙인 바이트 열을 얻고, 이를 읽기 쉬운 비밀어로 표현합니다.

같은 글을 두 번 암호화해도 매번 새 IV를 쓰기 때문에 서로 다른 비밀어가 나옵니다. 글이 같다는 사실조차 겉으로 드러나지 않게 하려는 것입니다.

해독은 거꾸로 진행됩니다. 붙여 넣은 비밀어에서 바이트 열을 복원하고, 헤더가 형식에 맞는지 확인한 뒤, 같은 키와 IV, AAD로 해독합니다. AES-GCM에는 인증 태그가 있어서 비밀어가 한 글자라도 바뀌었거나 다른 번역기의 키로 풀려고 하면 해독이 실패합니다. 엉뚱한 글이 깨져 나오는 것이 아니라 아예 실패로 끝납니다.

코드로 보는 기본 모양

아래는 개념을 보여 드리기 위한 아주 짧은 예시입니다. 실제 서비스 코드는 압축, 헤더 검사, 오류 처리 등이 더 들어 있습니다.

const iv = crypto.getRandomValues(new Uint8Array(12));
const header = new Uint8Array([1, 0]);
const ct = await crypto.subtle.encrypt(
  { name: 'AES-GCM', iv, additionalData: header },
  key,   // AES-GCM 용 CryptoKey
  new TextEncoder().encode('안녕하세요')
);

여기서 key 는 importKey 로 가져온 AES-GCM 키이고, 가져올 때 추출 불가로 지정할 수 있습니다. 직접 구현한 부분은 암호 알고리즘이 아니라 이 호출들을 어떤 순서로 이을지, 즉 포맷과 조합입니다.

키는 어디서 오나요

AES-GCM에 넣을 키는 번역기 주소의 # 뒤에 있는 번역기 키에서 만들어집니다. 비밀번호가 없는 번역기는 번역기 키에서 HKDF로 곧바로 유도하고, 비밀번호가 있는 번역기는 PBKDF2로 만든 값과 서버가 확인 후에 내주는 값을 함께 섞습니다. 이 유도 과정은 비밀번호에서 키를 만드는 PBKDF2와 키 유도에서 자세히 설명합니다. 키를 주소 조각에 두는 이유는 URL의 # 뒤 내용은 왜 서버로 전송되지 않을까에서 다루었습니다.

해독 키를 잠시 기억해 두는 방법

비밀번호를 건 번역기는 열 때마다 비밀번호를 입력해야 해서 번거로울 수 있습니다. 이 서비스에는 설정한 시간 동안 비밀번호 입력을 면제해 주는 기능이 있고, 선택할 수 있는 시간은 0, 5, 10, 30, 60분입니다. 이때 브라우저는 해독 키를 비추출 CryptoKey 형태 그대로 IndexedDB라는 브라우저 저장소에 만료 시각과 함께 보관합니다. 키의 원래 바이트 값은 어디에도 적혀 있지 않고, 서버 쿠키도 쓰지 않습니다. 시간이 지나면 저장된 항목은 지워집니다.

사생활 보호 창처럼 저장소가 막힌 환경에서는 면제 없이 매번 비밀번호를 묻는 방식으로 동작합니다. 편의 기능이 있다고 해서 안전 쪽을 포기하지 않도록, 저장에 실패하면 기능을 그냥 끄는 방식을 택한 것입니다. 한편 비추출 키라고 해도 같은 기기에서 같은 브라우저 프로필을 쓰는 사람이라면 그 키로 해독을 시도할 수 있습니다. 공용 기기에서는 면제 시간을 0분으로 두는 것이 좋습니다.

직접 확인해 보는 실습

비추출 키가 정말 꺼내지지 않는지 브라우저 개발자 도구의 콘솔에서 확인해 볼 수 있습니다. HTTPS 페이지에서 다음 코드를 실행해 보세요.

const key = await crypto.subtle.generateKey(
  { name: 'AES-GCM', length: 256 },
  false,                 // extractable: false
  ['encrypt', 'decrypt']
);
await crypto.subtle.exportKey('raw', key);

마지막 줄은 오류를 일으킵니다. 키를 추출 불가로 만들었기 때문에 내보내기가 거부되는 것입니다. 첫 번째 인자 다음의 false를 true로 바꾸면 내보내기가 성공하는데, 그만큼 코드가 키를 읽을 수 있게 된다는 뜻이므로 필요한 경우가 아니면 false로 두는 편이 안전합니다.

이번에는 길이를 계산해 보겠습니다. 앞서 설명한 형식에서 헤더는 2바이트, IV는 12바이트, 인증 태그는 일반적으로 16바이트입니다. 압축이 이루어지지 않았다고 가정하면 5바이트짜리 글은 암호화 후 2+12+5+16으로 35바이트가 됩니다. 아무리 짧은 글도 최소 30바이트의 고정 부담이 생긴다는 뜻이며, 이 서비스가 30바이트에 못 미치는 데이터를 올바른 형식으로 보지 않는 것도 이 때문입니다.

흔한 오해

자주 묻는 질문

어떤 브라우저에서 쓸 수 있나요?

최신 데스크톱과 모바일 주요 브라우저는 모두 Web Crypto API를 지원합니다. 아주 오래된 브라우저에서는 일부 기능이 빠져 있을 수 있으니 업데이트하는 것이 좋습니다.

비밀번호를 그대로 AES 키로 쓰면 안 되나요?

안 됩니다. 사람이 만든 비밀번호는 길이와 무작위성이 부족해서 키로 바로 쓰기에 적합하지 않습니다. 그래서 PBKDF2 같은 키 유도 함수를 거쳐 반복 계산을 통해 키를 만듭니다. 이 서비스는 반복 횟수를 60만 번으로 두고 있어, 비밀번호를 하나씩 대입해 보는 공격의 비용을 크게 높입니다.

직접 암호화 코드를 만들어 쓰면 안 되나요?

학습 목적으로 구현해 보는 것은 좋은 공부입니다. 다만 실제 데이터를 지키는 용도라면 검증된 라이브러리나 표준 API를 쓰는 것이 원칙입니다.

Web Crypto를 써도 남는 한계

검증된 도구를 쓴다고 해서 모든 문제가 사라지는 것은 아닙니다. 웹페이지의 자바스크립트는 방문할 때마다 서버에서 새로 받아 오는 코드입니다. 서버가 악의적으로 바뀐 코드를 내려보내면 키를 가로챌 수 있다는 이론적 한계가 있고, 이 서비스는 이를 줄이려고 외부 스크립트를 쓰지 않고 CSP를 엄격하게 둡니다. 다만 이 위험을 완전히 없앨 수는 없습니다. 사용자의 기기가 악성코드에 감염되었다면 브라우저 암호 기능도 의미를 잃습니다. 이런 현실적인 한계는 이 서비스가 막아 주는 것과 막아 주지 못하는 것에서 솔직하게 정리했습니다.

결국 좋은 암호 서비스란 새 알고리즘을 만드는 곳이 아니라, 검증된 부품을 올바르게 조립하는 곳입니다. 직접 확인해 보고 싶으시면 번역기 열기에서 써 보시기 바랍니다.

잘못된 내용이나 보완할 점을 발견하셨다면 소개·문의의 이메일로 알려 주세요. 확인 후 글을 고치고 수정일을 갱신합니다.

함께 읽으면 좋은 글

다음 글: 비밀번호에서 키를 만드는 PBKDF2와 키 유도 → 번역기 열기