제로 지식 암호화: Vaulted가 비밀을 안전하게 지키는 방법
작성자 Maxim Novak
제로 지식 암호화는 서버가 평문 데이터나 암호화 키를 절대 볼 수 없다는 것을 의미한다. Vaulted는 어떠한 데이터도 서버에 도달하기 전에 브라우저에서 AES-256-GCM으로 모든 비밀을 암호화함으로써 이를 구현한다. 서버는 암호문만 저장하며, URL 프래그먼트에만 존재하는 복호화 키에는 절대 접근할 수 없다.
이 글에서는 정확한 암호학적 구현을 설명한다: 키가 어떻게 생성되는지, 데이터가 어떻게 암호화되는지, 키가 수신자에게 어떻게 전달되는지, 그리고 패스프레이즈를 추가하면 어떤 일이 일어나는지. 여기의 모든 세부 사항은 브라우저에서 실제로 실행되는 코드를 반영한다.
클라이언트 측 암호화가 중요한 이유에 대한 개요는 클라이언트 측 암호화에 관한 이전 글을 참고하자. 이 글은 암호학적 기본 요소를 더 깊이 다룬다.
키 생성
비밀을 생성할 때마다 Vaulted는 Web Crypto API를 사용해 새로운 AES-256-GCM 암호화 키를 생성한다:
crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, [
'encrypt',
'decrypt',
]);
몇 가지 주목할 점:
- AES-GCM (Galois/Counter Mode)은 기밀성과 무결성을 모두 제공한다 — 무단 읽기뿐만 아니라 변조도 감지한다.
- 256비트 키 길이는 AES에서 사용할 수 있는 가장 강력한 옵션이며, 전 세계 정부와 금융 기관에서 사용된다.
- Extractable:
true— 수신자를 위해 URL 프래그먼트에 배치할 수 있도록 키를 원시 바이트로 내보낼 수 있다.
Web Crypto API는 브라우저의 네이티브 암호화 엔진(예: Chromium의 BoringSSL, Firefox의 NSS)에 위임한다. 키는 메모리에서 안전하게 생성되며 디스크나 네트워크에는 절대 닿지 않는다.
암호화
키가 생성되면 Vaulted는 평문을 암호화한다:
const iv = crypto.getRandomValues(new Uint8Array(12));
crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
new TextEncoder().encode(plaintext),
);
초기화 벡터 (IV): AES-GCM은 각 암호화 작업마다 12바이트(96비트)의 무작위 IV를 필요로 한다. IV는 비밀일 필요가 없으며 — 암호문과 함께 저장된다 — 하지만 동일한 키로 절대 재사용해서는 안 된다. Vaulted는 모든 비밀에 대해 새 키를 생성하므로 IV 재사용은 설계상 불가능하다.
인코딩: 암호문과 IV 모두 base64url 문자열로 인코딩된다(표준 base64에서 +를 -로, /를 _로 교체하고 trailing =을 제거한 형식). 이를 통해 URL과 JSON 페이로드에 안전하게 포함할 수 있다.
암호화된 암호문과 IV는 저장을 위해 서버로 전송된다. 평문은 절대 브라우저 밖으로 나가지 않는다.
URL 프래그먼트를 통한 키 전달
암호화 후 키는 내보내져 URL 프래그먼트 — # 뒤의 부분 — 에 배치된다:
const rawKey = await crypto.subtle.exportKey('raw', key);
// rawKey → base64url-encoded string
생성된 암호화된 링크는 다음과 같은 형태다:
https://vaulted.fyi/s/abc123#dGhpcyBpcyBhIGtleQ
^^^^^^^^^^^^^^^^^^^^^^^^
encryption key (base64url)
# 구분자는 매우 중요하다. RFC 3986에 따르면 프래그먼트 식별자는 완전히 클라이언트에서 처리된다. 브라우저는 HTTP 요청에 프래그먼트를 절대 포함하지 않는다 — URL에도, Referer 헤더에도, 서버가 볼 수 있는 어떤 곳에도 포함하지 않는다.
수신자가 링크를 열면:
- 브라우저가 서버에
/s/abc123을 요청한다 (프래그먼트 없음) - 서버가 암호화된 암호문과 IV를 반환한다
- JavaScript가
window.location.hash에서 프래그먼트를 읽는다 - 키가 Web Crypto API로 다시 임포트된다
- 암호문이 로컬에서 복호화된다
서버는 암호화된 블롭의 저장과 검색을 지원할 뿐이다. 암호학적 작업에는 절대 참여하지 않는다.
선택적 패스프레이즈 보호
추가 보호가 필요한 비밀의 경우 Vaulted는 패스프레이즈 기반 키 래핑을 지원한다. 이는 두 번째 계층을 추가한다: 누군가 링크를 가로채더라도 패스프레이즈 없이는 비밀을 복호화할 수 없다.
키 파생과 PBKDF2
패스프레이즈를 설정하면 Vaulted는 PBKDF2를 사용해 래핑 키를 파생한다:
crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt, // 16 bytes, randomly generated
iterations: 600_000,
hash: 'SHA-256',
},
keyMaterial, // the passphrase, imported as raw key material
{ name: 'AES-KW', length: 256 },
false,
['wrapKey', 'unwrapKey'],
);
- 600,000회 반복으로 브루트 포스 공격을 계산적으로 비용이 많이 들게 만든다.
- 16바이트 무작위 솔트로 동일한 패스프레이즈가 서로 다른 비밀에서 다른 파생 키를 생성하도록 보장한다.
- SHA-256은 각 PBKDF2 반복에서 사용되는 해시 함수다.
- 파생된 키는 AES-GCM 키가 아닌 AES-KW (AES 키 래핑, RFC 3394) 키다. 키 래핑은 암호화와 구별되는 별도의 작업이다.
암호화 키 래핑
파생된 AES-KW 키로 원래의 암호화 키를 래핑한다:
crypto.subtle.wrapKey('raw', encryptionKey, wrappingKey, 'AES-KW');
URL 프래그먼트에는 이제 래핑된 키와 솔트가 점으로 구분되어 포함된다:
#wrappedKeyBase64url.saltBase64url
복호화 시 언래핑
수신자가 패스프레이즈를 입력하면 Vaulted는 과정을 역으로 실행한다:
- 프래그먼트를
.으로 분리해 래핑된 키와 솔트를 가져온다 - 패스프레이즈와 솔트로 PBKDF2를 사용해 동일한 AES-KW 래핑 키를 파생한다
crypto.subtle.unwrapKey로 암호화 키를 언래핑한다- 언래핑된 AES-GCM 키로 암호문을 복호화한다
패스프레이즈가 틀리면 unwrapKey가 오류를 발생시킨다 — 부분적인 복호화나 정보 유출은 없다.
복호화 흐름
패스프레이즈 없이는 복호화가 간단하다:
- URL 프래그먼트에서 키를 임포트한다 (
crypto.subtle.importKey) - IV로 암호문을 복호화한다 (
crypto.subtle.decrypt) - UTF-8 바이트에서 평문을 디코딩한다
패스프레이즈가 있는 경우:
- 프래그먼트를 분리해 래핑된 키와 솔트를 추출한다
- 패스프레이즈와 솔트로 래핑 키를 파생한다 (PBKDF2)
- 암호화 키를 언래핑한다 (AES-KW)
- 암호문을 복호화한다 (AES-GCM)
- 평문을 디코딩한다
이 모든 것은 브라우저에서 처리된다. 서버는 암호화된 블롭과 메타데이터(조회 수, 만료 시간)를 반환할 뿐이다 — 그 이상은 없다.
위협 모델
어떤 보안 도구도 모든 것에서 보호하지는 않는다. 다음은 Vaulted의 아키텍처가 처리하도록 설계된 것과 범위 밖에 있는 것이다.
Vaulted가 보호하는 것
- 서버 침해 — 서버는 암호화된 암호문만 저장한다. 키 없이는 (키는 절대 서버로 전송되지 않는다) 데이터를 읽을 수 없다. 전체 데이터베이스 덤프는 암호화된 블롭만 제공한다.
- 데이터베이스 유출 — 위와 동일하다. 키 없는 암호문은 AES-256-GCM으로 계산적으로 무용지물이다.
- 서버 측 중간자 공격 — 암호화 키는 HTTP 요청에 절대 전송되지 않는 URL 프래그먼트에 존재한다. TLS가 전송 중 암호문을 보호한다.
- 서비스 운영자의 비밀 열람 — 제로 지식이란 정확히 그런 의미다. 우리는 강요받더라도 키를 갖고 있지 않기 때문에 비밀을 읽을 수 없다.
Vaulted가 보호하지 않는 것
- 침해된 브라우저 또는 기기 — 수신자의 기기에 악성 코드나 키로거가 있으면 복호화 후 복호화된 평문을 캡처할 수 있다.
- 악성 브라우저 확장 프로그램 — 페이지 접근 권한이 있는 확장 프로그램은 복호화 후 공개된 비밀을 포함해 DOM을 읽을 수 있다.
- 링크 가로채기 — URL에는 프래그먼트에 암호화 키가 포함된다. 전체 URL(프래그먼트 포함)을 얻은 사람은 누구든 비밀을 복호화할 수 있다. 안전한 채널을 통해 링크를 공유하자.
- 어깨 너머로 훔쳐보기 — 비밀이 화면에 표시되면 보는 사람 누구나 볼 수 있다.
- 수신자의 행동 — 복호화 후 수신자는 스크린샷을 찍거나, 복사하거나, 평문을 공유할 수 있다. Vaulted는 이를 막을 수 없다.
패스프레이즈 기능은 링크 가로채기 위험을 완화한다: 전체 URL이 있더라도 공격자는 여전히 패스프레이즈가 필요하다. 심층 방어를 위해 다른 채널(전화 통화, 별도의 메시지)을 통해 패스프레이즈를 공유하자.
요약
Vaulted의 제로 지식 암호화가 의미하는 것:
- 모든 비밀에 대해 새로운 AES-256-GCM 키가 생성된다
- 암호화는 전적으로 브라우저에서 이루어진다
- 서버는 암호화된 암호문만 저장한다
- 복호화 키는 브라우저가 서버로 절대 보내지 않는 URL 프래그먼트에만 존재한다
- 선택적 패스프레이즈 래핑이 PBKDF2와 AES-KW를 통해 두 번째 계층을 추가한다
그 결과: 모든 서버, 데이터베이스, 네트워크 경로가 동시에 침해되더라도 비밀은 암호화된 상태를 유지한다. 키는 오직 공유하는 링크 속에만 존재한다.
안전하게 비밀을 공유할 준비가 됐나? Vaulted에서 비밀 만들기 — 10초면 된다.
관련 항목
- Vaulted의 보안 — 보안 모델 요약
- 클라이언트 측 암호화가 중요한 이유 — 브라우저에서 암호화하는 이유
- 암호화 플레이그라운드 — AES-256-GCM 암호화 단계별로 살펴보기