제로널리지 암호화 미디어 클라우드 (Ciphek)

웹 · 앱 · 시스템7분 읽기

제로널리지 미디어 클라우드 Ciphek 소개 영상

문제

미디어를 클라우드에 올린다는 것은 대개 "누군가의 서버가 내 영상과 사진을 볼 수 있는 상태로 보관한다"는 뜻입니다. 일반적인 클라우드 스토리지는 서버 사이드 암호화를 하더라도 키를 서버가 쥐고 있어서 서버 관리자, 클라우드 프로바이더, 법적 요청을 받은 운영사가 마음만 먹으면 평문에 접근할 수 있습니다. 사진과 영상처럼 사적인 미디어에서 이 전제는 그 자체로 위험입니다.

Ciphek이 풀려던 문제는 신뢰의 위치를 옮기는 것이었습니다. 서버를 믿어야만 안전한 구조가 아니라, 서버를 믿지 않아도 안전한 구조. 즉 서버는 암호문 덩어리만 저장하는 "멍청한 저장소"로 두고 열쇠는 사용자의 브라우저 밖으로 한 번도 나가지 않게 만드는 것입니다. 마스터 패스워드에서 파생된 키가 오직 브라우저 메모리에만 존재하고 서버로는 암호문과 암호화된 메타데이터만 흘러갑니다.

그런데 이 원칙을 미디어에 적용하는 순간 현실적인 벽이 줄줄이 등장합니다. 영상은 크고 스트리밍은 전체를 받기 전에 앞부분부터 재생돼야 합니다. 브라우저 표준 복호화 API는 스트리밍과 잘 맞지 않습니다. 키 하나로 모든 파일을 암호화하면 키 회전이 불가능합니다. 패스워드를 잊으면 아무도 데이터를 살릴 수 없다는 사실은 보안상 옳지만 사용자에겐 재앙입니다. 제로널리지는 선언하기는 쉽지만 미디어 클라우드에서 끝까지 지키기는 까다롭습니다.

접근

Ciphek의 핵심은 키 계층입니다. 사용자의 마스터 패스워드는 그대로 열쇠가 되지 않습니다. 브라우저의 Web Worker 안에서 Argon2id로 무겁게 늘려 32바이트 마스터키를 만들고 그 마스터키에서 HKDF로 용도별 서브키를 갈라냅니다. 인증용, 메타데이터 암호화용, 파일 키 래핑용 서브키가 각기 다른 라벨로 분리되고 이 서브키들은 바이트로 다시 꺼낼 수 없는 non-extractable 상태로만 존재합니다.

기능

Argon2id로 파생하고 HKDF로 도메인을 분리한 키 계층

마스터 패스워드는 Web Worker 안에서 Argon2id(64MiB 메모리, 3회 반복, OWASP 2024 최소치)를 거쳐 32바이트 마스터키가 됩니다. 마스터키는 HKDF-SHA-256으로 인증 서브키(HMAC 검증자), 메타데이터 서브키(AES-GCM-256), 파일 래핑 서브키(AES-KW-256) 세 갈래로 분리되고 서브키는 모두 브라우저 밖으로 직렬화할 수 없는 non-extractable 키로 다룹니다.

파일은 파일마다 다른 무작위 32바이트 FileKey로 암호화합니다. 이 FileKey를 앞서 만든 래핑 서브키로 AES-KW 감싸서 서버에 보관하므로, 파일 키가 마스터키와 분리되면서도 마스터 패스워드가 있어야만 풀립니다. 파일 본문은 4MB 청크로 잘라 XChaCha20-Poly1305로 암호화하는데, 여기서 청크마다 AAD(부가 인증 데이터)를 붙여 청크의 위치를 암호문에 묶습니다.

기능

XChaCha20-Poly1305 청크 암호화와 AAD 위치 바인딩

파일은 4MB 청크로 나뉘어 각 청크가 XChaCha20-Poly1305(24바이트 nonce)로 암호화됩니다. 각 청크의 AAD에는 파일 식별자, 청크 인덱스(4바이트 빅엔디언), 마지막 청크 여부가 41바이트 규칙으로 인코딩되어 들어갑니다. 덕분에 청크를 서로 바꿔치기하거나 순서를 뒤섞으면 복호화 단계에서 인증 태그가 어긋나 즉시 걸립니다.

재생은 제로널리지에서 가장 까다로운 부분입니다. 브라우저 표준 AES-GCM 복호화는 전체 인증 태그를 확인해야 끝나서 청크 스트리밍과 맞지 않기 때문에, MediaSource API와 mp4box.js를 조합해 직접 스트리밍 파이프라인을 만들었습니다. 청크를 받아 복호화한 뒤 SourceBuffer로 흘려보내는 구조라, 긴 영상도 전체를 내려받지 않고 앞부분부터 재생됩니다.

기능

MediaSource 기반 청크 복호화 스트리밍

청크를 순차로 받아 브라우저에서 복호화하고 mp4box.js로 디먹싱해 MediaSource에 흘려보냅니다. moov 아톰이 파일 앞이나 끝에 있는 두 경우를 모두 잡으려고 첫 청크와 마지막 청크를 병렬로 먼저 탐색하고 재생 위치보다 2청크 앞을 미리 가져오며 이미 복호화한 청크는 LRU 캐시로 재사용합니다. iPhone 사파리는 ManagedMediaSource로, 스트리밍이 안 되는 컨테이너는 다운로드 후 재생으로 대응합니다.

패스워드를 잊었을 때에 대비해 BIP39 24단어 복구 니모닉을 둡니다. 니모닉에서 파생한 키로 마스터키를 감싼 복구 blob을 서버에 저장하고 복구 시에는 니모닉 → 복구키 → 마스터키 순으로 되살립니다. 니모닉과 패스워드를 둘 다 잃으면 데이터는 수학적으로 복구 불가능한데, 이것을 숨기지 않고 온보딩에서 명시적으로 확인받는 것까지가 설계에 포함됩니다.

전체 데이터 흐름은 다음과 같습니다.

브라우저에서 파생한 키로 미디어를 암호화해 R2에 암호문으로 저장하고, 서버는 평문에 접근하지 않는 흐름브라우저 (모든 암복호화)마스터 패스워드 → Argon2id → 마스터키HKDF 서브키 · 무작위 FileKey (AES-KW 래핑)4MB 청크 XChaCha20-Poly1305 (AAD 바인딩)평문과 키는 이 경계를 넘지 않음암호문Hono API (Bun · Fly.io)권한 확인 · presigned URL 발급평문 복호화 로직 없음Cloudflare R2암호문 청크Neon PostgreSQL암호화 메타데이터복구 · 스트리밍BIP39 24단어 → 복구 blob → 마스터키MediaSource + mp4box.js 청크 재생

정리하면 브라우저가 파생한 키로 미디어를 암호화해 R2에 암호문으로 올리고 Hono API 서버는 권한 확인과 짧은 수명의 presigned URL 발급만 담당하며 평문에는 손대지 않습니다. 메타데이터마저 브라우저에서 암호화해 저장하므로 파일명이나 태그로 내용을 유추할 여지도 없앴습니다. 스택은 Next.js 15와 React 19 프런트, Hono/Bun 백엔드(Fly.io), Drizzle과 Neon PostgreSQL, Cloudflare R2, 그리고 @noble/ciphers와 hash-wasm 같은 감사 이력이 있는 암호 라이브러리로 구성했습니다.

결과

Ciphek은 개인용 MVP에서 멀티테넌트 SaaS로 확장되며 실제 클라우드에 배포되어 운영 중입니다. 유저마다 격리된 금고를 가지고 마스터 패스워드 없이는 서버 운영자도 클라우드 프로바이더도 법집행기관도 미디어 내용을 확인할 수 없습니다. 검색조차 서버로 평문 쿼리를 보내지 않고 복호화된 메타데이터로 브라우저에서 인덱싱하는 방식이라 제로널리지 원칙이 검색까지 이어집니다.

보안 관점의 결과도 정직하게 정리했습니다. 내부 침투 테스트는 critical과 high 등급 0건, 내부 crypto audit는 44개 항목 중 41개 PASS였고 나머지 3개는 구현 결함이 아니라 문서와 위협 모델 서술에 관한 것이라 이후 마일스톤에서 보완했습니다. 다만 제3자 독립 감사는 아직 받지 않았다는 점을 제품 문서에 그대로 밝혀 두었습니다. 과장하지 않는 것이 이 제품의 신뢰 자산이기 때문입니다.

이 프로젝트의 암호 아키텍처는 아래 글들에서 더 깊이 풀었습니다.

자주 묻는 질문

제로널리지가 정확히 무엇을 보장하나요?

서버가 미디어 내용의 평문에 접근할 수 없다는 것을 암호학적으로 보장합니다. 모든 암복호화가 브라우저에서만 일어나고 키는 마스터 패스워드에서 파생되어 브라우저 밖으로 나가지 않기 때문에, 서버 운영자나 클라우드 프로바이더가 저장된 암호문을 열 수 없습니다. 다만 이것이 계정 이메일이나 결제 기록 같은 메타데이터까지 서버가 전혀 다루지 않는다는 뜻은 아니며 이 경계는 문서에 정확히 구분해 두었습니다.

패스워드를 잊으면 정말 복구가 안 되나요?

마스터 패스워드와 24단어 복구 니모닉을 둘 다 잃으면 데이터는 복구되지 않습니다. 이것은 정책이 아니라 암호 구조의 수학적 성질입니다. 서버는 마스터키를 가진 적이 없으므로 되살려 줄 방법 자체가 존재하지 않습니다. 그래서 가입 시 복구 니모닉을 안전하게 보관했다는 확인을 명시적으로 받고 이 한계를 약관과 온보딩에서 숨기지 않고 알립니다.

암호화된 영상을 어떻게 바로 재생하나요?

파일을 4MB 청크로 나눠 암호화해 두었기 때문에, 재생할 때 전체를 받지 않고 필요한 청크만 받아 브라우저에서 복호화합니다. MediaSource API와 mp4box.js로 복호화한 조각을 재생 버퍼에 흘려보내는 방식이라 긴 영상도 앞부분부터 스트리밍됩니다. 브라우저가 스트리밍을 지원하지 않는 경우에는 전체를 내려받아 복호화한 뒤 재생하는 방식으로 대응합니다.

왜 AES-GCM이 아니라 XChaCha20-Poly1305를 쓰나요?

브라우저 표준 AES-GCM 복호화는 전체 인증 태그를 확인해야 완료되어 청크 단위 스트리밍 복호화와 잘 맞지 않고 96비트 논스는 무작위로 대량 사용할 때 충돌 위험이 있습니다. XChaCha20-Poly1305는 24바이트의 넉넉한 논스로 청크마다 무작위 논스를 안전하게 쓸 수 있고 감사 이력이 있는 순수 구현으로 브라우저에서 스트리밍 복호화에 적합합니다.

도입 결과

마스터 패스워드 없이는 서버 운영자도, 클라우드 프로바이더도, 법집행기관도 미디어 내용을 볼 수 없는 구조를 실제 서비스로 구현하고 클라우드에 배포해 운영 중입니다. 내부 pen-test는 critical/high 0건, 내부 crypto audit는 44개 항목 중 41개 PASS로 정리됐습니다.
#제로널리지#클라이언트암호화#XChaCha20#Argon2id#미디어클라우드

비슷한 업무 자동화가 필요하신가요?

무료 상담 신청