업데이트됨

제로 지식 암호화: Vaulted가 비밀을 안전하게 지키는 방법

작성자

제로 지식 암호화는 서버가 평문 데이터나 암호화 키를 절대 볼 수 없다는 것을 의미한다. 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 헤더에도, 서버가 볼 수 있는 어떤 곳에도 포함하지 않는다.

수신자가 링크를 열면:

  1. 브라우저가 서버에 /s/abc123을 요청한다 (프래그먼트 없음)
  2. 서버가 암호화된 암호문과 IV를 반환한다
  3. JavaScript가 window.location.hash에서 프래그먼트를 읽는다
  4. 키가 Web Crypto API로 다시 임포트된다
  5. 암호문이 로컬에서 복호화된다

서버는 암호화된 블롭의 저장과 검색을 지원할 뿐이다. 암호학적 작업에는 절대 참여하지 않는다.

선택적 패스프레이즈 보호

추가 보호가 필요한 비밀의 경우 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는 과정을 역으로 실행한다:

  1. 프래그먼트를 .으로 분리해 래핑된 키와 솔트를 가져온다
  2. 패스프레이즈와 솔트로 PBKDF2를 사용해 동일한 AES-KW 래핑 키를 파생한다
  3. crypto.subtle.unwrapKey로 암호화 키를 언래핑한다
  4. 언래핑된 AES-GCM 키로 암호문을 복호화한다

패스프레이즈가 틀리면 unwrapKey가 오류를 발생시킨다 — 부분적인 복호화나 정보 유출은 없다.

복호화 흐름

패스프레이즈 없이는 복호화가 간단하다:

  1. URL 프래그먼트에서 키를 임포트한다 (crypto.subtle.importKey)
  2. IV로 암호문을 복호화한다 (crypto.subtle.decrypt)
  3. UTF-8 바이트에서 평문을 디코딩한다

패스프레이즈가 있는 경우:

  1. 프래그먼트를 분리해 래핑된 키와 솔트를 추출한다
  2. 패스프레이즈와 솔트로 래핑 키를 파생한다 (PBKDF2)
  3. 암호화 키를 언래핑한다 (AES-KW)
  4. 암호문을 복호화한다 (AES-GCM)
  5. 평문을 디코딩한다

이 모든 것은 브라우저에서 처리된다. 서버는 암호화된 블롭과 메타데이터(조회 수, 만료 시간)를 반환할 뿐이다 — 그 이상은 없다.

위협 모델

어떤 보안 도구도 모든 것에서 보호하지는 않는다. 다음은 Vaulted의 아키텍처가 처리하도록 설계된 것과 범위 밖에 있는 것이다.

Vaulted가 보호하는 것

  • 서버 침해 — 서버는 암호화된 암호문만 저장한다. 키 없이는 (키는 절대 서버로 전송되지 않는다) 데이터를 읽을 수 없다. 전체 데이터베이스 덤프는 암호화된 블롭만 제공한다.
  • 데이터베이스 유출 — 위와 동일하다. 키 없는 암호문은 AES-256-GCM으로 계산적으로 무용지물이다.
  • 서버 측 중간자 공격 — 암호화 키는 HTTP 요청에 절대 전송되지 않는 URL 프래그먼트에 존재한다. TLS가 전송 중 암호문을 보호한다.
  • 서비스 운영자의 비밀 열람 — 제로 지식이란 정확히 그런 의미다. 우리는 강요받더라도 키를 갖고 있지 않기 때문에 비밀을 읽을 수 없다.

Vaulted가 보호하지 않는 것

  • 침해된 브라우저 또는 기기 — 수신자의 기기에 악성 코드나 키로거가 있으면 복호화 후 복호화된 평문을 캡처할 수 있다.
  • 악성 브라우저 확장 프로그램 — 페이지 접근 권한이 있는 확장 프로그램은 복호화 후 공개된 비밀을 포함해 DOM을 읽을 수 있다.
  • 링크 가로채기 — URL에는 프래그먼트에 암호화 키가 포함된다. 전체 URL(프래그먼트 포함)을 얻은 사람은 누구든 비밀을 복호화할 수 있다. 안전한 채널을 통해 링크를 공유하자.
  • 어깨 너머로 훔쳐보기 — 비밀이 화면에 표시되면 보는 사람 누구나 볼 수 있다.
  • 수신자의 행동 — 복호화 후 수신자는 스크린샷을 찍거나, 복사하거나, 평문을 공유할 수 있다. Vaulted는 이를 막을 수 없다.

패스프레이즈 기능은 링크 가로채기 위험을 완화한다: 전체 URL이 있더라도 공격자는 여전히 패스프레이즈가 필요하다. 심층 방어를 위해 다른 채널(전화 통화, 별도의 메시지)을 통해 패스프레이즈를 공유하자.

요약

Vaulted의 제로 지식 암호화가 의미하는 것:

  1. 모든 비밀에 대해 새로운 AES-256-GCM 키가 생성된다
  2. 암호화는 전적으로 브라우저에서 이루어진다
  3. 서버는 암호화된 암호문만 저장한다
  4. 복호화 키는 브라우저가 서버로 절대 보내지 않는 URL 프래그먼트에만 존재한다
  5. 선택적 패스프레이즈 래핑이 PBKDF2와 AES-KW를 통해 두 번째 계층을 추가한다

그 결과: 모든 서버, 데이터베이스, 네트워크 경로가 동시에 침해되더라도 비밀은 암호화된 상태를 유지한다. 키는 오직 공유하는 링크 속에만 존재한다.


안전하게 비밀을 공유할 준비가 됐나? Vaulted에서 비밀 만들기 — 10초면 된다.


관련 항목