Skip to content

잠금 캡슐 종단간 암호화와 Shamir 키 분산 구현 - #33

Merged
cfcromn merged 5 commits into
developfrom
feature/32-client-side-capsule-encryption
Aug 31, 2026
Merged

잠금 캡슐 종단간 암호화와 Shamir 키 분산 구현#33
cfcromn merged 5 commits into
developfrom
feature/32-client-side-capsule-encryption

Conversation

@cfcromn

@cfcromn cfcromn commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator

✨ 작업 내용

#32 에서 A-1(잠금 있는 캡슐만 종단간 암호화)로 결정된 내용을 구현하였습니다.

Shamir's Secret Sharing 직접 구현

  • GaloisField256 — AES 기약다항식(0x11B) 기반 GF(2^8) 연산을 log/exp 테이블로 구현하였습니다.
  • ShamirSecretSharing — 바이트 단위 분할과 Lagrange 보간 복원을 구현하였습니다. 2-of-3, 3-of-5 구성을 지원합니다.

암호화 모드 도입

  • CapsuleEncryptionMode 를 추가하고 tbl_time_capsule.encryption_mode 로 저장합니다.
  • SERVER_ENVELOPElockType = NONE 캡슐. 서버가 키를 쥐고 있어 내용을 읽을 수 있습니다.
  • CLIENT_E2ElockType = PASSWORD/QUESTION 캡슐. 서버는 열 수 없습니다.

키 조각 보관

  • tbl_key_share 테이블과 KeyShare 엔티티를 추가하였습니다.
  • 조각은 is_wrapped 로 구분합니다. 감싸진 조각은 잠금 비밀에서 유도한 키로 암호화되어 있어 서버가 풀 수 없습니다.

API 계약 변경

  • 캡슐 생성: contentCipher, keyShares, keyThreshold 를 받습니다. 잠금 캡슐에서는 평문 content 를 받지 않습니다.
  • 캡슐 열람: CLIENT_E2E 캡슐은 평문 대신 contentCipherkeyShares 를 반환합니다.

만료 캡슐 키 조각 삭제

  • CleanUpKeyShareService 를 추가하여 만료된 캡슐의 조각을 매일 04:30 에 제거합니다.

🔍 리뷰 시 참고사항

설계 문서를 그대로 구현하지 않았습니다

「🔐 암호화 설계」 문서는 CEK 를 N 조각으로 나눠 전부 tbl_key_share 에 저장하고 열람 시 서버가 조각들을 반환하도록 되어 있습니다. 그런데 서버가 모든 조각을 가지고 있으면 언제든 합쳐서 CEK 를 복원할 수 있으므로, "서버는 캡슐 내용을 절대 볼 수 없다" 는 성질이 전혀 얻어지지 않습니다. CEK 를 평문 컬럼에 저장한 것과 보안상 동등합니다.

문서 안에서도 "서버가 조각 일부를 갖고 있어도 단독으로 복원 불가" 라고 하면서 저장 구조는 전부를 갖도록 되어 있어 서로 어긋나 있었습니다. 자세한 분석은 #32 코멘트에 정리해 두었습니다.

그래서 서버가 정족수를 채우지 못하도록 구조를 바꿨습니다.

  • 서버는 threshold 보다 적은 수의 평문 조각만 보관합니다.
  • 나머지 조각은 잠금 비밀(비밀번호·정답)에서 유도한 키로 감싸서 보관합니다. 서버는 잠금 비밀을 bcrypt 해시로만 알고 있어 이 조각을 풀 수 없습니다.
  • 열람 시 서버는 위치와 잠금을 검증한 뒤 보관 중인 조각을 그대로 반환하고, 클라이언트가 감싸진 조각을 풀어 정족수를 채웁니다.

서버가 스스로의 무능력을 강제합니다

CapsuleEncryptionPolicy 에서 평문 조각 수 < keyThreshold 를 검증하고, 위반 시 SERVER_HOLDS_KEY_QUORUM 으로 거부합니다. 클라이언트를 신뢰하지 않는 이유는, 클라이언트 버그로 모든 조각이 평문으로 전송되면 캡슐이 여전히 종단간 암호화라고 표시되면서 실제로는 서버가 읽을 수 있는 상태가 되기 때문입니다. 이 경우가 가장 위험합니다.

반대로 감싸진 조각이 하나도 없으면 아무도 열 수 없으므로 이 역시 거부합니다.

lockType = NONE 은 종단간 암호화를 하지 않습니다

이건 구현을 덜 한 것이 아니라 원리적으로 불가능합니다. 잠금 없는 캡슐의 열람 조건은 "그 좌표에 간다" 뿐이고 서버가 좌표를 저장하고 있으므로, 서버가 스스로 만족시킬 수 있는 조건만으로 열리는 캡슐은 정의상 서버도 열 수 있습니다. 서버가 자신에게서 숨긴 비밀은 서버가 다시 유도할 수 있습니다.

이 판단을 코드가 아니라 CapsuleEncryptionMode 의 주석과 CapsuleEncryptionPolicy.resolveMode 에 명시해 두었습니다. 모르고 빠뜨린 것이 아니라 의도된 경계임을 남기기 위해서입니다.

남은 한계 — 감싸진 조각에 대한 오프라인 공격

DB 에 접근한 공격자는 감싸진 조각을 확보한 뒤 잠금 비밀을 오프라인에서 무제한 대입할 수 있습니다. 질문형 캡슐의 정답은 엔트로피가 낮은 경우가 많아 현실적인 위협입니다.

완전한 해결책은 없고, 클라이언트가 조각을 감쌀 때 느린 KDF(Argon2 등)와 캡슐별 salt 를 쓰는 것이 표준적인 완화책입니다. 이 부분은 클라이언트 구현 사항이라 이번 PR 범위 밖이며, 별도 이슈로 남기는 것이 좋겠습니다. 만료 캡슐의 조각을 스케줄러로 지우는 것도 이 창을 좁히기 위한 조치입니다.

테스트

전체 326개가 통과합니다. (기존 310개 + 이번 16개)

Shamir 는 틀리면 캡슐이 영구 복구 불능이 되므로 두껍게 작성하였습니다.

  • 체 공리 전수 검증 — 덧셈·곱셈·나눗셈의 역원, 결합법칙, 분배법칙
  • 3-of-5 의 10개 부분집합 전부 복원 확인
  • 임계값 미만 조각으로는 복원되지 않음
  • 정보이론적 안전성 — 1바이트 2-of-2 에서 조각 하나가 가능한 256개 값 전부와 모순 없음을 확인하였습니다. "복원이 어렵다" 가 아니라 "정보가 0" 이라는 성질입니다.
  • CapsuleKeyRoundTripTest — 암호화·분할·감싸기·반환·복원까지 전체 흐름을 재현하고, 서버가 보관하는 것만으로는 CEK 가 복원되지 않음을 확인합니다.

✅ 체크리스트

  • 문서(README, .env.example 등) 변경이 필요한 경우 작성 또는 수정했나요?
  • 작업한 코드가 정상적으로 동작하는 것을 직접 확인했나요?
  • 필요한 경우 테스트 코드를 작성하거나 수정했나요?
  • Merge 대상 브랜치를 올바르게 설정했나요?
  • PR에 관련 없는 작업이 포함되지 않았나요?
  • 적절한 라벨과 리뷰어를 설정했나요?

📎 관련 이슈(선택)

「🔐 암호화 설계」 문서와 CLAUDE.md 를 이 구현에 맞게 개정하는 작업이 남아 있습니다. 별도로 진행하겠습니다.

@cfcromn cfcromn added the ✨ Feature 신규 기능 label Aug 30, 2026
@cfcromn
cfcromn requested a review from exijn August 31, 2026 05:08
@cfcromn cfcromn self-assigned this Aug 31, 2026
@cfcromn
cfcromn merged commit 7650c14 into develop Aug 31, 2026
2 checks passed
@cfcromn
cfcromn deleted the feature/32-client-side-capsule-encryption branch August 31, 2026 05:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feature 신규 기능

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants