You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
security(receiver): 다중 수신자 컨텍스트·키 보관 정책을 서버 기록함 기준으로 분리 확정한다 #1950
#598의 격리 구현과 #599의 재시작 소실 수정에서 미정 정책을 분리한다. #599/PR #1652의 SESSION DataStore·평문 마스터키 저장 선택은 승인된 제품·보안 계약으로 취급하지 않는다. #607은 서버 목록을 정본으로 삼으면 로컬 영속화 자체가 불필요해질 수 있다고 명시한다.
완료 조건:
수신자 식별 기준, 기기 공유 시 전환·로그아웃·탈퇴·세션 만료의 카드/키/본인 확인 처리 기준을 PM·BE 및 필요한 시안과 함께 확정한다.
서버 정본 목록과 로컬 캐시의 책임을 결정한다. 저장이 불필요하면 #599를 서버 조회 후 재시작 복원 검증으로 전환하고 #1652의 대체/폐기 여부를 기록한다.
로컬 저장이 필요할 경우 저장 대상·수명·암호화·키 관리·백업 제외·마이그레이션 요구사항을 확정한다. 기존 평문 저장 관례만으로 새 평문 저장을 정당화하지 않는다.
blocked_by: 기록함 접근 자격·서버 계약 #1949. #598 구현은 기존 #607 선행도 유지한다. #599는 본 정책과 #607을 확인하기 전 로컬 영속화를 정답으로 확정하지 않는다. 실제 격리 구현 #598 전체 완료를 #599의 선행으로 요구하지 않는다.
기존 보안 노출을 줄이는 독립 수정(예: #1876 백업 제외)이나 내부 visibility 정리는 이 결정 대기로 막지 않는다. PR #1652의 미확정 로컬 영속화 제안은 철회·종료했다(2026-09-08). #599는 본 정책과 #607을 기다린 뒤 남는 재시작 복원 요구를 구현·검증한다.
참고
분리 원본: #598. 사용자 요청에 따른 수신자 블로킹 범위 분리(2026-09-08). 원본의 구현 책임은 유지하며 본 이슈는 선행 계약·결정 범위만 소유한다.
판정 경로는 Afternote-FE origin/develop7d87aa3 실코드다. 여기서 2번을 채우고 1번과 4번을 절반 채운다. 3번은 2번의 결론에 따라 조건 불성립으로 적는다. 1번의 남은 축이 PM 결정이라 이슈는 닫지 않고 blocked_by 도 #1949 그대로 둔다.
1. 식별 기준과 세션 수명: 수명 축은 채움, 범위 축은 대기
수신자 컨텍스트에 새 수명 정책을 만들 필요가 없다. 로그아웃·탈퇴·세션 만료가 이미 한 자리로 모여 SESSION 스코프 로컬 저장소를 일괄로 비운다.
일괄 삭제 본체는 core/data/src/main/java/com/afternote/core/data/repoimpl/auth/AuthRepositoryImpl.kt:41-47 의 clearSession() 이고, :43 이 localStoreRegistry.clearScope(StoreScope.SESSION) 이다.
로그아웃은 같은 파일 :135-150 이고, 서버 호출 뒤 :148 에서 같은 호출을 직접 한다.
탈퇴는 core/data/src/main/java/com/afternote/core/data/repoimpl/UserRepositoryImpl.kt:56 이 clearSession() 을 부른다.
세션 만료는 세 자리다. core/network/src/main/kotlin/com/afternote/core/network/interceptor/TokenAuthenticator.kt:28(재시도 한계)와 :55(회전했는데 토큰이 그대로), 그리고 재발급 확정 거절(400·401·403)이 core/network/src/main/kotlin/com/afternote/core/network/token/TokenReissuer.kt:166 에서 락 안에 정리된다.
수신자 마스터키 저장소는 이미 그 경로에 들어 있다. feature/receiver/data/src/main/kotlin/com/afternote/feature/receiver/data/di/ReceiverMasterKeyDataStoreModule.kt:22 가 afternote_receiver_auth 를 StoreScope.SESSION 으로 등록한다. 등록은 디스크 매니페스트에도 남아서, 이번 프로세스에서 한 번도 열지 않은 저장소까지 매니페스트 기록으로 열어 지운다(core/datastore/src/main/java/com/afternote/core/datastore/LocalStoreRegistryImpl.kt:97-126, 그중 :106-112).
그래서 이 축의 결론은 수신자 컨텍스트를 계정 세션 수명에 종속시키는 것이다. #1949 가 기록한 계정 기반 식별(완료 조건 대조)과 같은 방향이라 새 정책을 세울 자리가 없다.
같은 자리에서 #598 본문의 낡은 근거 하나를 정정한다. #598 은 clearAuthCode() 호출처가 0건이라 이전 사람의 마스터 키가 남는다(#588)를 격리 붕괴 근거로 들지만, origin/develop 에서는 clearAuthCode 심볼 자체가 저장소 전체 0건이다. 디스크 정리 책임은 #912 가 도입한 스코프 레지스트리로 옮겨갔다. 격리 결함 자체는 없어지지 않았고 자리가 바뀌었을 뿐이라(4번) 근거만 갈아 끼우면 된다.
남는 것은 범위 축이다. 수신자에게 어떤 발신자가 보여야 하는지는 PM 결정이고, #1949 가 BE#93 에 물어 둔 상태 그대로다.
2. 서버 정본과 로컬 캐시의 책임: 로컬 영속화 불필요
보관할 자격값 자체가 없어졌다. #1949 실측대로 마스터키가 서버 응답 계약에서 빠졌고, 목록·상세 접근 자격은 액세스 토큰(@UserId)이다. 로컬에 영속화할 후보가 남지 않는다.
재시작 복원도 서버 조회만으로 성립한다. 액세스 토큰은 디스크 DataStore 에 있어 프로세스 사망을 넘긴다(core/datastore/src/main/java/com/afternote/core/datastore/di/TokenDataStoreModule.kt:21, 이름 Token, SESSION 스코프). 재기동 뒤 계정 인증으로 목록을 다시 받으면 카드가 돌아온다.
이 결론은 PM 의 범위 결정과 무관하게 선다. 표시 범위를 어느 쪽으로 정하든 마스터키를 로컬에 영속화하는 선택지는 되살아나지 않는다. 값이 계약에서 사라졌고, 이메일 매칭으로 되살리는 길은 BE#93 이 지적한 취약점 그 자체라 #1949 가 이미 막아 뒀다.
따라서 #599 는 서버 조회 후 재시작 복원 검증으로 전환한다. PR #1652 는 미확정 로컬 영속화 제안이라 2026-09-08 에 종료했고, 대체하지 않고 폐기로 확정한다.
3. 로컬 저장이 필요할 경우: 조건 불성립
2번 결론으로 조건이 서지 않는다. 저장 대상·수명·암호화·키 관리·마이그레이션을 정할 값이 없다.
이미 깔려 있는 afternote_receiver_auth 저장분 제거는 여기서 잡지 않는다. 수신자 흐름이 통째로 X-Auth-Code 에 매여 있어 그 제거는 #607 전환 그 자체다. 백업 제외도 여기서 다시 잡지 않는다. #1876 이 소유하고 PR #2012 가 열려 있다.
서버 쪽 조건은 #1949 가 확인했다. A 의 액세스 토큰으로 B 의 receiverId 를 호출하면 findByIdAndAcceptedUserId 에서 걸려 RECEIVER_NOT_FOUND 로 끊긴다.
FE 쪽 조건은 둘이다.
로그아웃·탈퇴·401 뒤 SESSION 스코프가 전량 비는지. 1번의 다섯 자리가 전부 같은 SESSION 스코프 일괄 삭제로 모이므로 검증도 그 한 자리에 걸면 된다. 매니페스트 경로 때문에 이번 프로세스에서 열지 않은 저장소도 대상이라는 점을 함께 확인한다.
프로세스가 살아 있는 채로 컨텍스트가 바뀔 때 in-memory 잔재가 남지 않는지.
2번이 지금 실재하는 구멍이다. feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/recordsbox/SenderRegistry.kt:27 의 MutableStateFlow 에 비우는 경로가 하나도 없다. 공개 함수는 register(:34)·findById(:44)·attachIdentity(:50)·updateVerificationStatus(:64) 뿐이고, 이 클래스는 :23 에서 @Singleton 이라 ViewModel 들이 같은 인스턴스를 공유한다. 그 안에 담기는 SenderEntry 는 SenderEntry.kt:24 에서 masterKey 를 들고 있다. 디스크는 레지스트리가 지우지만 이 목록은 지우는 사람이 없다.
이 구멍의 수정은 #598 소유다. 서버 목록 전환으로 이 클래스가 사라지면 함께 없어지므로 별도 수정을 먼저 넣을 자리는 아니다.
Activity
OnlyReadOneBook commentedon Sep 13, 2026
완료 조건 대조 (2026-09-13)
판정 경로는 Afternote-FE
origin/develop7d87aa3 실코드다. 여기서 2번을 채우고 1번과 4번을 절반 채운다. 3번은 2번의 결론에 따라 조건 불성립으로 적는다. 1번의 남은 축이 PM 결정이라 이슈는 닫지 않고 blocked_by 도 #1949 그대로 둔다.1. 식별 기준과 세션 수명: 수명 축은 채움, 범위 축은 대기
수신자 컨텍스트에 새 수명 정책을 만들 필요가 없다. 로그아웃·탈퇴·세션 만료가 이미 한 자리로 모여 SESSION 스코프 로컬 저장소를 일괄로 비운다.
core/data/src/main/java/com/afternote/core/data/repoimpl/auth/AuthRepositoryImpl.kt:41-47의clearSession()이고,:43이localStoreRegistry.clearScope(StoreScope.SESSION)이다.:135-150이고, 서버 호출 뒤:148에서 같은 호출을 직접 한다.core/data/src/main/java/com/afternote/core/data/repoimpl/UserRepositoryImpl.kt:56이clearSession()을 부른다.core/network/src/main/kotlin/com/afternote/core/network/interceptor/TokenAuthenticator.kt:28(재시도 한계)와:55(회전했는데 토큰이 그대로), 그리고 재발급 확정 거절(400·401·403)이core/network/src/main/kotlin/com/afternote/core/network/token/TokenReissuer.kt:166에서 락 안에 정리된다.수신자 마스터키 저장소는 이미 그 경로에 들어 있다.
feature/receiver/data/src/main/kotlin/com/afternote/feature/receiver/data/di/ReceiverMasterKeyDataStoreModule.kt:22가afternote_receiver_auth를StoreScope.SESSION으로 등록한다. 등록은 디스크 매니페스트에도 남아서, 이번 프로세스에서 한 번도 열지 않은 저장소까지 매니페스트 기록으로 열어 지운다(core/datastore/src/main/java/com/afternote/core/datastore/LocalStoreRegistryImpl.kt:97-126, 그중:106-112).그래서 이 축의 결론은 수신자 컨텍스트를 계정 세션 수명에 종속시키는 것이다. #1949 가 기록한 계정 기반 식별(완료 조건 대조)과 같은 방향이라 새 정책을 세울 자리가 없다.
같은 자리에서 #598 본문의 낡은 근거 하나를 정정한다. #598 은
clearAuthCode()호출처가 0건이라 이전 사람의 마스터 키가 남는다(#588)를 격리 붕괴 근거로 들지만,origin/develop에서는clearAuthCode심볼 자체가 저장소 전체 0건이다. 디스크 정리 책임은 #912 가 도입한 스코프 레지스트리로 옮겨갔다. 격리 결함 자체는 없어지지 않았고 자리가 바뀌었을 뿐이라(4번) 근거만 갈아 끼우면 된다.남는 것은 범위 축이다. 수신자에게 어떤 발신자가 보여야 하는지는 PM 결정이고, #1949 가 BE#93 에 물어 둔 상태 그대로다.
2. 서버 정본과 로컬 캐시의 책임: 로컬 영속화 불필요
보관할 자격값 자체가 없어졌다. #1949 실측대로 마스터키가 서버 응답 계약에서 빠졌고, 목록·상세 접근 자격은 액세스 토큰(
@UserId)이다. 로컬에 영속화할 후보가 남지 않는다.재시작 복원도 서버 조회만으로 성립한다. 액세스 토큰은 디스크 DataStore 에 있어 프로세스 사망을 넘긴다(
core/datastore/src/main/java/com/afternote/core/datastore/di/TokenDataStoreModule.kt:21, 이름Token, SESSION 스코프). 재기동 뒤 계정 인증으로 목록을 다시 받으면 카드가 돌아온다.이 결론은 PM 의 범위 결정과 무관하게 선다. 표시 범위를 어느 쪽으로 정하든 마스터키를 로컬에 영속화하는 선택지는 되살아나지 않는다. 값이 계약에서 사라졌고, 이메일 매칭으로 되살리는 길은 BE#93 이 지적한 취약점 그 자체라 #1949 가 이미 막아 뒀다.
따라서 #599 는 서버 조회 후 재시작 복원 검증으로 전환한다. PR #1652 는 미확정 로컬 영속화 제안이라 2026-09-08 에 종료했고, 대체하지 않고 폐기로 확정한다.
3. 로컬 저장이 필요할 경우: 조건 불성립
2번 결론으로 조건이 서지 않는다. 저장 대상·수명·암호화·키 관리·마이그레이션을 정할 값이 없다.
이미 깔려 있는
afternote_receiver_auth저장분 제거는 여기서 잡지 않는다. 수신자 흐름이 통째로X-Auth-Code에 매여 있어 그 제거는 #607 전환 그 자체다. 백업 제외도 여기서 다시 잡지 않는다. #1876 이 소유하고 PR #2012 가 열려 있다.이 항목의 최종 확정은 1번의 범위 축이 닫힌 뒤로 미룬다.
4. #598·#1690 에 인계할 검증 조건
서버 쪽 조건은 #1949 가 확인했다. A 의 액세스 토큰으로 B 의
receiverId를 호출하면findByIdAndAcceptedUserId에서 걸려RECEIVER_NOT_FOUND로 끊긴다.FE 쪽 조건은 둘이다.
2번이 지금 실재하는 구멍이다.
feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/recordsbox/SenderRegistry.kt:27의MutableStateFlow에 비우는 경로가 하나도 없다. 공개 함수는register(:34)·findById(:44)·attachIdentity(:50)·updateVerificationStatus(:64) 뿐이고, 이 클래스는:23에서@Singleton이라 ViewModel 들이 같은 인스턴스를 공유한다. 그 안에 담기는SenderEntry는SenderEntry.kt:24에서masterKey를 들고 있다. 디스크는 레지스트리가 지우지만 이 목록은 지우는 사람이 없다.이 구멍의 수정은 #598 소유다. 서버 목록 전환으로 이 클래스가 사라지면 함께 없어지므로 별도 수정을 먼저 넣을 자리는 아니다.
남은 것
OnlyReadOneBook commentedon Sep 19, 2026
2026-09-19 정리로 닫는다. 이 이슈의 완료 조건은 PM의 결정에 달려 있어 결정 전에는 FE 가 무엇을 만들지 쓸 수 없다. 그런 이슈는 FE 저장소에 열어 두지 않고 결정이 나면 그때 새 이슈로 다시 연다.
기다리는 결정: 다중 수신자 키 보관·컨텍스트 정책(BE 포함).
PM 질문은 창구가 정해지는 대로 보낸다(일혁이 목록 보관).