잠금 캡슐 종단간 암호화와 Shamir 키 분산 구현 - #33
Merged
Merged
Conversation
exijn
approved these changes
Aug 31, 2026
This was referenced Aug 31, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
✨ 작업 내용
#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_ENVELOPE—lockType = NONE캡슐. 서버가 키를 쥐고 있어 내용을 읽을 수 있습니다.CLIENT_E2E—lockType = PASSWORD/QUESTION캡슐. 서버는 열 수 없습니다.키 조각 보관
tbl_key_share테이블과KeyShare엔티티를 추가하였습니다.is_wrapped로 구분합니다. 감싸진 조각은 잠금 비밀에서 유도한 키로 암호화되어 있어 서버가 풀 수 없습니다.API 계약 변경
contentCipher,keyShares,keyThreshold를 받습니다. 잠금 캡슐에서는 평문content를 받지 않습니다.CLIENT_E2E캡슐은 평문 대신contentCipher와keyShares를 반환합니다.만료 캡슐 키 조각 삭제
CleanUpKeyShareService를 추가하여 만료된 캡슐의 조각을 매일 04:30 에 제거합니다.🔍 리뷰 시 참고사항
설계 문서를 그대로 구현하지 않았습니다
「🔐 암호화 설계」 문서는 CEK 를 N 조각으로 나눠 전부
tbl_key_share에 저장하고 열람 시 서버가 조각들을 반환하도록 되어 있습니다. 그런데 서버가 모든 조각을 가지고 있으면 언제든 합쳐서 CEK 를 복원할 수 있으므로, "서버는 캡슐 내용을 절대 볼 수 없다" 는 성질이 전혀 얻어지지 않습니다. CEK 를 평문 컬럼에 저장한 것과 보안상 동등합니다.문서 안에서도 "서버가 조각 일부를 갖고 있어도 단독으로 복원 불가" 라고 하면서 저장 구조는 전부를 갖도록 되어 있어 서로 어긋나 있었습니다. 자세한 분석은 #32 코멘트에 정리해 두었습니다.
그래서 서버가 정족수를 채우지 못하도록 구조를 바꿨습니다.
threshold보다 적은 수의 평문 조각만 보관합니다.서버가 스스로의 무능력을 강제합니다
CapsuleEncryptionPolicy에서평문 조각 수 < keyThreshold를 검증하고, 위반 시SERVER_HOLDS_KEY_QUORUM으로 거부합니다. 클라이언트를 신뢰하지 않는 이유는, 클라이언트 버그로 모든 조각이 평문으로 전송되면 캡슐이 여전히 종단간 암호화라고 표시되면서 실제로는 서버가 읽을 수 있는 상태가 되기 때문입니다. 이 경우가 가장 위험합니다.반대로 감싸진 조각이 하나도 없으면 아무도 열 수 없으므로 이 역시 거부합니다.
lockType = NONE은 종단간 암호화를 하지 않습니다이건 구현을 덜 한 것이 아니라 원리적으로 불가능합니다. 잠금 없는 캡슐의 열람 조건은 "그 좌표에 간다" 뿐이고 서버가 좌표를 저장하고 있으므로, 서버가 스스로 만족시킬 수 있는 조건만으로 열리는 캡슐은 정의상 서버도 열 수 있습니다. 서버가 자신에게서 숨긴 비밀은 서버가 다시 유도할 수 있습니다.
이 판단을 코드가 아니라
CapsuleEncryptionMode의 주석과CapsuleEncryptionPolicy.resolveMode에 명시해 두었습니다. 모르고 빠뜨린 것이 아니라 의도된 경계임을 남기기 위해서입니다.남은 한계 — 감싸진 조각에 대한 오프라인 공격
DB 에 접근한 공격자는 감싸진 조각을 확보한 뒤 잠금 비밀을 오프라인에서 무제한 대입할 수 있습니다. 질문형 캡슐의 정답은 엔트로피가 낮은 경우가 많아 현실적인 위협입니다.
완전한 해결책은 없고, 클라이언트가 조각을 감쌀 때 느린 KDF(Argon2 등)와 캡슐별 salt 를 쓰는 것이 표준적인 완화책입니다. 이 부분은 클라이언트 구현 사항이라 이번 PR 범위 밖이며, 별도 이슈로 남기는 것이 좋겠습니다. 만료 캡슐의 조각을 스케줄러로 지우는 것도 이 창을 좁히기 위한 조치입니다.
테스트
전체 326개가 통과합니다. (기존 310개 + 이번 16개)
Shamir 는 틀리면 캡슐이 영구 복구 불능이 되므로 두껍게 작성하였습니다.
CapsuleKeyRoundTripTest— 암호화·분할·감싸기·반환·복원까지 전체 흐름을 재현하고, 서버가 보관하는 것만으로는 CEK 가 복원되지 않음을 확인합니다.✅ 체크리스트
.env.example등) 변경이 필요한 경우 작성 또는 수정했나요?📎 관련 이슈(선택)