보안 정책
Herkos는 개인 키와 암호화된 메시지를 다루므로 보안 신고를 진지하게 받아들이며, 문제는 늦게보다 일찍 듣는 편을 원합니다.
취약점 신고
보안 문제는 공개 이슈로 올리지 마세요.
security@herkos.email 로 이메일을 보내 비공개로 신고해 주세요.
포함하면 좋은 내용: 문제가 무엇인지, 어떻게 재현하는지, 어떤 플랫폼과 버전에서 시험했는지, 공격자가 무엇을 할 수 있는지. 개념 증명(PoC)이 있으면 큰 도움이 됩니다.
신고에 여러분 자신의 nsec, 개인 키, bunker 비밀값을 절대 넣지 마세요.
신고는 며칠 안에 확인 회신을 드리는 것을 목표로 합니다. Herkos는 작은 프로젝트이므로 공개하기 전에 수정할 수 있도록 합리적인 시간을 주시기 바랍니다. 원하지 않으시는 경우가 아니라면 보안 권고문에 기꺼이 이름을 올려 드립니다.
대상 범위
Herkos 애플리케이션 자체: 키 처리와 저장, 메시지 암호화와 복호화(NIP-59 발신자
검사와 local_packages/nostr_mail의 숨은 참조(Bcc) 격리 포함), 앱 잠금, 릴레이 및
브리지 통신, 첨부파일 처리, 빌드와 릴리스 파이프라인. Herkos Drive의 경우 그 프로토콜
패키지(packages/herkos_drive_protocol)와 그 링크를 여는 페이지(herkos.app/f/)도
포함됩니다. 아래의 별도 절을 참조하세요.
범위 밖
- Nostr 프로토콜과 그 NIP 자체. 상위 프로젝트에 신고하세요.
- 제3자가 운영하는 릴레이, 브리지, Blossom 서버. Herkos는 기본값을 제공하지만 그것들을 운영하지 않습니다. 해당 운영자에게 신고하세요.
- 상위 프로젝트 Nostr Mail Client. 단, Herkos에서 변경한 부분에 특유한 문제라면 대상입니다.
- 이미 알려진 한계로 문서화한 보강 미비(아래 참조).
알려진 한계, 솔직하게
이것들은 취약점이 아니라 설계상의 절충입니다. 누군가 어렵게 알아내기보다 미리 적어 두는 편을 택했습니다:
- 기존 주소로 보내는 메일은 종단 간 암호화되지 않습니다. Nostr 사용자 사이의 메시지는 NIP-17 기프트 랩을 사용하며 종단 간 암호화됩니다. 일반 이메일 주소로 보내면 브리지가 메시지를 변환하며 내용을 읽을 수 있습니다.
- 앱 잠금(PIN과 생체 인식)은 편의를 위한 장벽이지 저장 데이터 암호화가 아닙니다. 잠금이 풀린 기기를 집어 든 사람은 막지만, 기기나 그 저장소에 완전히 접근할 수 있는 공격자로부터는 보호하지 못합니다.
- Windows에서는 생체 인식만으로 인증하도록 강제할 수 없습니다. Windows Hello는 인증 방식을 고르게 해 주지 않으므로, 시스템 PIN이 항상 대체 수단으로 남습니다.
- 키를 잃으면 계정을 잃습니다. 복구는 설계상 없습니다.
- 메타데이터가 완전히 숨겨지지는 않습니다. 릴레이는 연결 패턴을 관찰할 수 있고, 메시지 내용을 읽지 못하더라도 모든 릴레이가 여러분의 IP 주소를 봅니다. Herkos가 (2026년 9월 30일부터) 내주지 않는 것은 릴레이가 NIP-42 인증을 요구할 때 누가 누구에게 쓰는지입니다:
- 배달할 곳 조회. 수신자의 릴레이 목록과 Blossom 목록은 여러분을 절대 식별하지
않는 별도 연결로 요청하며, 먼저 인증을 요구하지 않는 릴레이(
purplepag.es,user.kindpag.es,nos.lol,offchain.pub)에 묻습니다. - 프로필과 예약 발송기. 다른 사람에게 표시되는 이름과 사진(kind 0), 그리고 이메일을 예약하거나 취소할 때 예약 DVM의 릴레이 목록도 같은 방식으로 요청합니다. 먼저 익명으로, 릴레이가 고집하면 아래에 설명하는 임시 키로 묻고, 여러분의 키는 절대 쓰지 않습니다(2026년 10월 1일부터. 그 전에는 인증을 요구한 릴레이가 여러분의 키를 받아, 여러분이 누구의 프로필을 보고 있는지 알 수 있었습니다). 여러분 자신의 프로필만 여전히 여러분으로서 읽습니다.
- 배달. 기프트 랩은 같은 별도 연결로 수신자의 DM 릴레이에 갑니다. 그중 하나가 인증을 요구하면 Herkos는 여러분의 키가 아니라 이 용도로 만들어 메모리에만 두는 키로 응답합니다. 기기의 계정마다 각자의 키가 있습니다. 릴레이는 여전히 한 세션 동안 같은 기기(같은 IP, 같은 임시 키)가 여러 봉투를 보냈다는 것은 볼 수 있습니다.
- 큰 이메일. 32 KB를 넘는 이메일의 암호화된 사본은 수신자의 서버를 포함한 Blossom 서버에 업로드됩니다. 여러분 자신의 서버(여러분의 Blossom 목록)만 여러분의 키로 업로드 권한을 받습니다. 그 밖의 서버는 익명으로 받고, 권한을 고집하면 위와 같은 임시 키로 서명한 권한을 받습니다(2026년 10월 1일부터. 그 전에는 모든 서버가 여러분의 키를 받았습니다). 알려진 키만 받는 서버는 이를 거부하며, 사본은 다른 서버에 남습니다.
- 여러분 자신의 데이터. 사서함, 라벨, 설정, 임시보관함을 동기화할 때는 릴레이가 요구하면 계정을 전환하는 중에도 그 데이터의 주인인 계정으로 인증합니다. 여러분 자신의 릴레이에는 이미 알던 것 이상을 알려 주지 않습니다. 요청에 어차피 여러분의 키가 들어 있기 때문입니다. 여러분 자신의 릴레이 목록과 Blossom 목록은 공개되어 있으며 여러분을 식별하지 않고 요청합니다. 이메일 링크에 지정된 릴레이도 마찬가지입니다.
- 읽음 상태와 폴더는 (현재로서는) 공개 메타데이터입니다. 이메일을 읽음, 별표, 보관으로 표시하거나 옮기면 공개 키에 묶인 암호화되지 않은 NIP-32 라벨 이벤트가 게시되며, 사용자 폴더 이름도 포함됩니다. 여러분에게 보낸 이메일의 id를 아는 발신자는 여러분이 언제 그 이메일에 조치했는지 알 수 있습니다. 이 라벨의 암호화는 계획되어 있으며, 그때까지는 폴더 이름을 공개된 것으로 생각하세요.
- 푸시 알림과 예약 발송은 제3자 서비스를 이용합니다. 푸시 서버(선택한 경우에만. 기본값은 없음.
요청하면
api.nmail.li와relay.nmail.li)는 여러분의 공개 키, 푸시 토큰, 메일을 받는 시각을 알게 됩니다. 예약 DVM은 공개 키와 게시해야 할 이미 암호화된 메시지를 알게 됩니다. 둘 다 직접 켜야 하며PRIVACY.md에 문서화되어 있습니다. 푸시 알림의 문구는 푸시 서버가 정하며, Herkos는 UnifiedPush가 그것을 암호화해 전달할 때(RFC 8291)만 신뢰합니다. 암호화되지 않은 페이로드는 앱을 깨워 메일을 직접 가져오게 할 뿐입니다. - 웹 버전은 키를 브라우저에 두며, 그곳은 키를 두기에 가장 약한 곳입니다. 설치한 앱은 개인 키를 운영체제의 보안 저장소(Android 키스토어, Windows의 DPAPI, libsecret, iOS/macOS 키체인)에 맡깁니다. 브라우저에는 그런 것이 없습니다. 키는 출처(origin)의 저장소에 있고 서명할 때마다 JavaScript를 거치므로, 페이지에서 실행되는 모든 코드(모든 사이트에 접근할 수 있는 브라우저 확장 프로그램, 앱이나 의존성의 XSS 버그, 변조된 배포)와 브라우저 프로필을 복사하는 정보 탈취 악성코드의 손이 닿는 곳에 있습니다. 또한 코드를 한 번 검증하는 순간도 없습니다. 서명된 바이너리는 설치할 때 확인되지만, 웹 페이지는 방문할 때마다 새 코드를 내보냅니다. 웹 버전에는 원격 서명기(NIP-46 bunker, Amber)로 로그인해 키가 브라우저에 닿지 않게 하거나, 버려도 되는 신원으로 여기고 진짜 키는 설치한 앱에 두세요.
- 브리지를 거친 메일은 브리지만큼만 믿을 수 있습니다. Herkos는 브리지를 통해
왔다고 표시된 메시지가 실제로 여러분이 설정한 브리지(또는 기본값 중 하나)의
서명을 받았는지 확인합니다. 브리지가 전달하는 기존
From:주소에 대해서는 아무것도 확인할 수 없습니다. - 첨부파일은 운영체제가 엽니다. Herkos는 실행 파일이나 스크립트 형식은 절대 실행하지 않으며, 미리 볼 수 없는 그 밖의 첨부파일은 시스템 기본 앱에 넘기기 전에 묻습니다. 하지만 파일을 여는 앱은 우리가 통제할 수 없습니다.
Herkos Local Relay: 보호하는 것과 보호하지 못하는 것
Herkos Local Relay는 여러분 앱의 이벤트를 비공개 이벤트(초안, 비공개 목록, 여러분이 받는 봉투)까지 포함해 여러분의 기기에 보관합니다. 여기서는 무엇이 그것을 보호하는지, 그리고 한계가 어디인지 플랫폼별로 설명합니다. 릴레이의 보안 문제도 다른 문제와 같은 방법으로 신고해 주세요(위 참고).
데이터베이스 전체가 암호화되며, 저널도 포함됩니다(SQLite3 Multiple Ciphers, AEGIS-256). 키는 기기에서 만든 32바이트 무작위 값이며 시스템의 보안 저장소에 보관됩니다. Android에서는 Keystore, Windows에서는 DPAPI, Linux에서는 키링(libsecret)입니다. 릴레이가 스스로 시작해야 하므로 여러분의 비밀번호는 쓰지 않습니다. 키가 없으면 릴레이는 시작하지 않고 그 사실을 알립니다. 암호화되지 않은 데이터베이스를 열거나 만드는 일은 절대 없으며, 열 수 없는 데이터베이스를 삭제하는 일도 절대 없습니다.
| 만약… | Android | Windows | Linux |
|---|---|---|---|
| 누군가 데이터베이스 파일을 복사한다면(백업, 동기화 폴더, 디스크 이미지) | 보호됨 | 보호됨 | 보호됨 |
| 꺼진 상태의 기기나 디스크를 도난당한다면 | 보호됨: 키는 Keystore에 있음 | 보호됨. 단, 상대가 Windows 로그인 비밀번호를 알면 예외: DPAPI가 그 비밀번호로 키를 보호함 | 보호됨. 단, 상대가 키링 비밀번호(거의 항상 세션 비밀번호)를 알면 예외 |
| 같은 기기의 다른 사용자 계정 | 보호됨 | 보호됨: DPAPI는 사용자별 | 보호됨: 키링은 사용자별 |
| 여러분의 사용자로 실행되는 다른 앱이나 프로그램 | 보호됨: Android는 앱을 서로 격리하고 Keystore는 키를 릴레이에만 줌 | 보호 안 됨: DPAPI는 앱별이 아니라 사용자별로 암호화하므로, 여러분으로 실행되는 프로그램은 릴레이와 똑같이 키를 요청할 수 있음 | 보호 안 됨: 세션의 키링은 앱을 서로 분리하지 않음 |
| 루팅된 휴대전화, 또는 앱으로서 코드를 실행하는 도구 | 보호 안 됨 | — | — |
| 누군가 잠금이 풀린 기기를 손에 쥐고 있다면 | 릴레이의 PIN이나 지문만(설정한 경우) | 동일 | 동일 |
| 누군가 실행 중인 릴레이의 메모리를 읽는다면 | 보호 안 됨 | 보호 안 됨 | 보호 안 됨 |
데스크톱에서 DPAPI와 키링의 한계는 다른 모든 프로그램과 같습니다. 다른 사용자와 디스크를 가져가는 사람으로부터는 보호하지만, 이미 여러분으로 실행 중인 악성 프로그램으로부터는 보호하지 못합니다. 그런 경우가 걱정된다면, 여러분을 보호하는 것은 시스템 디스크 암호화(BitLocker, LUKS)와 신뢰하지 않는 프로그램을 실행하지 않는 것입니다. 키링이 없는 Linux(Secret Service가 없는 경우)에서는 릴레이가 시작하지 않고 이유를 설명합니다. 키를 평문으로 보관하는 일은 절대 없습니다.
기기 안에서 누가 들어올 수 있는지. 릴레이는 127.0.0.1에서만 연결을 받지만, 기기의
어떤 앱이든, 브라우저의 웹 페이지든 그 주소에 닿을 수 있습니다. 그래서:
- 앱은 페어링해야 합니다. 앱이 릴레이 화면에 나타나면 여러분이 승인합니다(PIN이나 지문을 설정했다면 그것으로). 어떤 앱이든 스스로에게 아무 이름이나 붙일 수 있으니, 방금 그 앱에서 요청한 경우에만 승인하세요.
- 웹 페이지는 항상 페어링해야 하며, 페어링은 그것을 한 곳에서만 유효합니다. 기기 앱의 페어링은 같은 키로 서명하는 웹 페이지라도 웹 페이지에는 아무것도 열어 주지 않습니다.
- 페어링한 앱은 여러분이 허용한 종류의 이벤트만 읽고 쓰며, 쓸 수 있는 종류만 삭제합니다. 권한을 철회하면 즉시 연결이 끊어집니다.
- 페어링하지 않은 앱은 여러분의 공개 이벤트(어차피 공개된 것)만 읽고 아무것도 쓰지 않습니다. 단, 페어링하지 않은 기기 앱을 들여보내는 설정을 켠 경우는 예외입니다.
- 비공개 이벤트는 그것이 속한 키를 가지고 있음을 NIP-42로 증명하는 상대에게만, 봉투는 그 수신자에게만 전달됩니다.
여기서 보호하지 못하는 것:
- 기기의 악성 앱은 릴레이를 느리게 하거나 메모리를 쓰게 할 수 있습니다. 그렇다고 비공개 내용을 읽을 수는 없습니다.
- 릴레이가 잠들어 있거나 실행 중이 아닐 때는 포트가 비어 있습니다. Android에서는 연결 없이 10분이 지나면 잠들고, 데스크톱에서는 릴레이를 시작하지 않은 동안 포트가 비어 있습니다. 다른 앱이 그 포트에서 연결을 받으며 릴레이인 척할 수 있습니다. Herkos 앱은 비공개 내용을 보내기 전에 릴레이의 키를 확인하게 될 예정이지만 아직은 그렇게 하지 않으며, 그때까지는 이것이 실제 한계입니다.
- 데스크톱에서는 컴퓨터의 다른 사용자가 페어링하지 않은 앱으로 연결할 수 있습니다. 여러분의 공개 이벤트를 읽고 페어링을 요청할 수 있습니다.
로컬 네트워크는 켠 경우에만 해당합니다(기본값은 꺼짐):
wss://만 쓰며, 릴레이 자체 인증서를 사용합니다. 기기 밖에서 암호화되지 않은ws://는 절대 쓰지 않습니다.- 여러분 목록에 있는 키로 인증(NIP-42)하기 전에는 아무것도 할 수 없습니다. 30초 안에 인증하지 않는 연결은 닫히며, 로컬 네트워크에는 자체 연결 수가 있어 기기 앱의 연결 몫을 절대 차지하지 않습니다.
- 목록에 있는 키는 여러분의 공개 이벤트와 자신의 비공개 이벤트를 읽고, 자신이 서명한 이벤트만 씁니다. 목록에서 빼면 연결이 끊어집니다.
- 인증서는 집 안의 이름(
localhost,.local, 사설 주소)에만 유효합니다. 누군가 그 키를 훔치더라도, 인증서를 설치한 기기에서 인터넷 도메인을 서명하는 데는 쓸 수 없습니다.
여기서 보호하지 못하는 것: 같은 네트워크에 있는 사람은 기기에 열린 포트가 있고 그곳으로 트래픽이 얼마나 지나가는지는 볼 수 있지만, 무엇이 지나가는지는 볼 수 없습니다. 그리고 기기에 공인 주소가 있으면(중간에 라우터가 없으면) 그 포트는 인터넷에서 보이지만, 여전히 여러분 목록에 있는 키를 요구합니다.
백업. 암호화된 백업에는 비밀번호가 있습니다(scrypt와 XChaCha20-Poly1305). 비밀번호가 없으면 우리를 포함해 아무도 열 수 없습니다. JSONL 내보내기는 암호화되지 않으며 비공개 이벤트가 평문으로 담깁니다. 앱이 이를 경고합니다.
잠금(PIN, 그리고 지문 또는 Windows Hello)은 앱을 열 때, 30초 동안 자리를 비운 뒤, 그리고 페어링을 승인할 때 요구됩니다. Mail에서와 마찬가지로 편의를 위한 장벽이며, 아무것도 암호화하지 않습니다. Android에서는 PIN을 설정하면 스크린샷을 찍을 수 없고 최근 앱 목록에 미리보기도 나타나지 않으며, Android 12부터는 릴레이를 사용하는 동안 다른 앱의 창이 릴레이 창 위에 겹칠 수 없습니다.
Herkos Drive: 보호하는 것과 보호하지 못하는 것
Herkos Drive는 여러분의 파일을 Blossom 서버에, 파일과 폴더 목록을 Nostr 릴레이에 보관하며, 둘 다
기기에서 암호화됩니다. 여기서는 무엇이 그것을 보호하는지, 그리고 한계가 어디인지 설명합니다. 자세한
내용은 packages/herkos_drive_protocol/에 있는 spec-drive.md와 위협 모델 amenazas-drive.md에 있고,
각 릴레이와 서버가 보는 것은 개인정보 처리방침에 있습니다. Drive의
보안 문제도 다른 문제와 같은 방법으로 신고해 주세요(위 참고).
릴레이와 서버가 할 수 없는 것:
- 파일이나 그 이름을 읽는 것. 파일의 각 블록은 자체 리비전의 키로 AES-256-GCM 암호화됩니다. 이름, 폴더, 크기, 날짜, 키는 그것이 들어 있는 폴더의 키로 XChaCha20-Poly1305 봉인되어 전달됩니다. 내용에서 파생되는 것은 없습니다. 같은 파일을 두 번 올려도 서로 다른 블록이 됩니다.
- 들키지 않고 무언가를 바꾸거나, 바꿔치기하거나, 옮기는 것. 모든 블록은 그 파일 항목에 봉인된 매니페스트와 대조해 확인되며, 모든 항목은 볼륨 키로 서명되고 자기 위치(볼륨, 식별자, 폴더)에 맞게 봉인됩니다. 릴레이는 한 항목을 다른 항목으로, 한 블록을 다른 블록으로 속일 수 없습니다.
- 이전 버전을 되돌리는 것. 앱은 같은 식별자에 대해 이미 가진 것보다 오래된 항목을 절대 받아들이지 않습니다.
- 볼륨 키만으로 여러분인 척 쓰는 것. 여러분의 기기가 쓰는 것에는 소유자 표시, 즉 어떤 링크로도 주어지지 않는 키로 만든 HMAC이 붙습니다. 이 표시가 없는 항목이나 삭제는 받아들여지지 않으며, 앱이 경고합니다.
보호하지 못하는 것:
- Drive의 패턴. 릴레이는 파일과 폴더 수를 세고, 트리의 모양과 각 항목이 언제 바뀌는지 볼 수 있습니다. 서버는 각 파일의 대략적인 크기를 봅니다(블록은 64 KiB로, 항목은 2의 거듭제곱으로 패딩됩니다). 모든 릴레이와 서버는 여러분의 IP 주소를 봅니다. 이것이 중요하다면 시스템 수준에서 Tor나 Orbot을 사용하세요.
- 누군가 이미 내려받은 것. 링크나 사람을 제거하면 그들이 열 수 있던 것은 즉시 새 키를 받으므로, 나중에 추가한 것은 열 수 없습니다. 하지만 그들이 이미 저장한 것을 되돌릴 수는 없습니다.
- 유출된 링크. 비밀번호가 없는 링크는 그 자체가 키입니다. 페이지가 주소창에서 지우기 전에 브라우저가 방문 기록에 남길 수 있습니다. 민감한 것에는 링크에 비밀번호를 걸어 다른 경로로 전달하거나, 그 사람의 계정과 공유하세요. 약한 비밀번호는 시간이 지나면 뚫릴 수 있습니다. 링크를 가진 사람은 오프라인으로 시도할 수 있기 때문입니다.
herkos.app/f/페이지는 링크를 여는 사람의 브라우저에서 실행됩니다. 엄격한 Content-Security-Policy와 Subresource Integrity를 적용하고 제3자 코드 없이 제공되지만, 웹 호스트를 장악한 사람은 다른 페이지를 제공할 수 있습니다. 이는 브라우저에서 복호화하는 모든 웹 페이지의 한계입니다.- 볼륨 키를 가졌던 사람(여러분, 또는 폴더 링크와 함께 받은 사람)은 그 키로 업로드된 블록을 다시 업로드되기 전까지 삭제할 수 있습니다. 볼륨 키는 바꿀 수 있습니다(설정 → 볼륨 키). 다른 기기는 저절로 따라옵니다.
- 여러분의 기기. 파일 목록은 암호화되어 캐시되며 그 키는 시스템의 보안 저장소에 있습니다. 내려받거나 연 파일은 여러분이 고른 곳에 평문으로 저장됩니다. Drive의 PIN은 Mail에서와 마찬가지로 편의를 위한 장벽입니다.
- 훗날의 양자 컴퓨터. 블록과 봉인은 대칭 암호(AES-256, XChaCha20)지만, 로케이터는 secp256k1 위의 NIP-44로 암호화됩니다. 오늘 그것을 보관해 두었다가 내일 secp256k1을 깨는 사람은 Drive 전체를 열 수 있습니다.
검증 방법. 위협 모델에 비춘 두 차례의 내부 검토(2026년 11월 2일)와 그 수정, 각 이벤트와 블록이 무엇을 드러내는지에 대한 테스트, 외부에서 들어오는 것(이벤트, 봉인, 매니페스트, 링크, 백업)을 읽는 모든 부분에 대한 퍼징. 아직 외부 감사는 없습니다. 공개 베타 전에 한 번 계획되어 있으며, 그 보고서는 전문을 공개할 예정입니다. Drive는 아직 개발 중입니다.
지원 버전
보안 수정은 가장 최근에 릴리스된 버전에 적용됩니다. 프로젝트 규모상 이전 버전은 유지 관리하지 않으니, 신고하기 전에 업데이트해 주세요.