운영 담당이 데이터베이스 비밀번호, API 키, 복구 코드를 동료에게 넘길 때 가장 싼 방법은 슬랙이나 카카오톡에 그대로 붙이는 일입니다. 메시지는 검색됩니다. 여러 기기에 동기화됩니다. 반년 뒤에도 남아 있습니다. 「일회용 링크」로 바꿔도, 많은 구현은 복호화 키를 암호문 번호 뒤에 ?key=로 붙입니다. 그러면 호스트, 리버스 프록시, 접속 로그가 키를 봅니다. 암호화는 반만 한 셈입니다.

이 글은 읽으면 삭제되는 폼을 어떻게 누르는지 안내하지 않습니다. 그 일은 도구 페이지가 합니다. 질문은 더 좁습니다. 비밀번호를 한 번만 보낼 때 복호화 키를 왜 주소의 # 뒤에 두는가, 서버가 그래서 무엇을 못 보게 되는지, 그 보호가 어디서 끝나는지입니다. 앞글은 Network에서 평문이 올라가지 않았는지 확인하는 방법을 적었습니다. 같은 패널로 이번에는 URL의 어느 조각에 키가 사는지 봅니다. MyPassGen의 일회용 링크는 그 경계를 지킵니다. AES-256-GCM은 브라우저에서 끝나고, 서버는 암호문만 잠시 두며, 키는 # 뒤에만 있고, 만들기와 읽기 모두 가입이 없습니다.

주소를 두 물건으로 먼저 나누세요

물음표 ? 뒤는 쿼리입니다. HTTP 요청 줄에 실리고, 상대 쪽 접속 로그에도 남습니다. 우물정자 # 뒤는 프래그먼트입니다. 규격은 이 조각을 클라이언트가 처리하도록 남겨 둡니다. 키를 잘못된 쪽에 두면, 뒤에 붙는 「제로 널리지」는 모두 무너집니다.

같은 비밀번호를 보내는 세 가지 길

버려도 되는 시험용 비밀번호 orange-lake-7 하나를 두고, 보내는 길은 적어도 세 가지입니다. 차이는 페이지 구호가 아닙니다. 평문이 먼저 암호문이 됐는지, 키가 HTTP에 들어갔는지, 반년 뒤에 채팅에서 원문을 검색할 수 있는지입니다.

방법 호스트나 메신저가 받는 것 나중에 다시 찾을 수 있는 것
비밀번호를 채팅에 그대로 붙임 완전한 평문 검색 가능한 원문
암호화 링크, 키를 ?key=로 씀 암호문과 키(둘 다 요청에 실림) 접속 로그의 키, 채팅의 전체 URL
암호화 링크, 키를 # 뒤에 둠 서버는 암호문 번호만 봄. 메신저는 주소 전체를 저장할 수 있음 서버 로그에는 키 없음. 채팅 기록에는 전체 링크가 남을 수 있음

세 번째 줄이 답하는 것은 「암호문을 맡아 두는 기계가 키를 받으면 안 된다」뿐입니다. 「메신저가 주소 전체를 저장하느냐」는 답하지 않습니다. 링크 전체는 자격 정보입니다. #가 붙은 주소를 복사한 사람은 브라우저에서 풀 수 있습니다. 키를 우물정자 뒤에 두는 일은 서버 신뢰를 풉니다. 전달된 링크는 풀지 않습니다.

이 길은 짧은 글에 맞습니다. MyPassGen은 한 건 평문을 32 KB로 제한합니다. 비밀번호, 키 조각, 짧은 설명에 맞습니다. 인증서 묶음, 내보낸 표, 그 크기를 넘는 내용은 파일 암호화로 보내세요. 이 기기에서 .lock / .enc를 만든 뒤 암호는 다른 메시지로 보냅니다. 파일 장면은 클라우드에 넣기 전에 평문을 누가 읽는지에 적었습니다.

HTTP가 실제로 실어 보내는 것

RFC 3986 §3.5는 # 뒤를 fragment identifier라고 부릅니다. 주 자원을 가져온 뒤에 클라이언트가 해석하는 이차 위치를 가리킵니다. 브라우저는 경로와 쿼리로 먼저 페이지를 가져옵니다. 문서가 도착한 뒤에야 프래그먼트가 스크롤 위치를 정하거나 페이지 스크립트에 넘어갑니다. HTTP 요청 자체는 이 조각이 필요 없습니다.

현재 HTTP 의미는 더 단호합니다. RFC 9110 §7.1은 대상 URI가 참조의 fragment 구성 요소를 제외한다고 적습니다. fragment는 클라이언트 처리용으로 남겨 두기 때문입니다. 요청 줄에서 허용되는 origin-form은 경로와 선택적 쿼리입니다. # 생성 규칙은 없습니다. 주소창에는 s.html?id=abc#키가 보여도, 탭이 내보내는 문서 요청은 GET /ko/s.html?id=abc여야 합니다. 우물정자 뒤는 이 기기에 남습니다.

Referer도 프래그먼트를 뗍니다. MDN Referer는 이 헤더가 출처, 경로, 쿼리를 실을 수 있다고 적습니다. URL fragment와 사용자 이름·비밀번호는 실을 수 없습니다. W3C Referrer Policy도 참조로 쓰기 전에 벗기는 단계에서 fragment를 비웁니다. 따라서 「복호화 키를 # 뒤에 두면, 암호문을 맡아 두는 호스트는 HTTP로 키를 받지 않는다」는 규격 동작입니다. 어느 사이트의 구두 약속이 아닙니다.

물음표와 우물정자는 바꿔 쓸 수 없다

WHATWG URL 기준으로 http / https를 열면 ? 뒤는 요청 줄에 들어갑니다. s.html?id=abc&key=키로 쓰면 키가 접속 로그, 리버스 프록시, 일부 CDN 기록에 남습니다. 공개 링크를 공유할 때 어떤 마케팅 파라미터를 지워도 되는지는 다른 문제입니다. 지워도 되는 URL 파라미터를 보세요. 복호화 키는 쿼리로 옮기면 안 됩니다.

인코딩 함정도 있습니다. %23은 #의 퍼센트 인코딩입니다. 경로나 쿼리에 쓴 %23은 서버로 보내집니다. 디코드하면 글자 그대로의 우물정자가 되고, 프래그먼트가 되지 않습니다. 키는 주소창의 인코딩되지 않은 # 뒤에 있어야 합니다. 경로에 접어 넣은 %23 뒤가 아닙니다.

서버 로그에서 빠져야 하는 것

한 번의 읽으면 삭제는 두 단계로 나뉩니다. 만들 때 탭은 32바이트 난수 키를 만들고, AES-256-GCM으로 평문을 암호화합니다. IV는 12바이트이며 암호문과 함께 둡니다. 그다음 암호문, 만료 시간, 열람 횟수를 POST합니다. 읽을 때 스크립트는 location.hash에서 키를 꺼내고, 서버에는 암호문만 요청한 뒤 이 기기에서 복호화합니다. 링크 형태는 s.html?id={id}#{key}입니다. 쿼리에는 번호만 있고, 키는 우물정자 뒤에만 있습니다.

NIST SP 800-38D는 GCM을 인증 암호화로 규정합니다. 암호문이 바뀌면 복호화는 실패해야 합니다. 깨진 본문을 본문으로 착각하게 두면 안 됩니다. RFC 5116의 AEAD_AES_256_GCM은 32바이트 키, 12바이트 nonce, 16바이트 인증 태그를 씁니다. MDN AesGcmParams도 96비트 IV를 권하고, 같은 키로 암호화할 때마다 새 IV를 써야 한다고 적습니다. 이 숫자는 구현에서 대조할 수 있습니다.

따라서 서버 로그에는 보여야 하는 것이 있습니다. 암호문 번호 id, 만들기 JSON의 ciphertext / ttl_hours / max_reads, 나중에 암호문을 가져오는 GET입니다. 로그에 보이면 안 되는 것도 있습니다. 평문, 32바이트 키, 주소창 # 뒤 조각입니다. 만료는 1시간, 24시간, 7일 중에서 고르거나 열람 횟수만으로 태울 수 있습니다. 횟수는 1에서 10입니다. 만들기 페이지에 보이는 제약입니다. 나중에 만든 SLA가 아닙니다.

규격은 페이지 스크립트가 스스로 올리는 것을 막지 않습니다

HTTP가 프래그먼트를 보내지 않는다고 해서, 키가 이 컴퓨터를 영원히 떠나지 않는다는 뜻은 아닙니다. 페이지 스크립트는 window.location.hash를 읽은 뒤 스스로 fetch할 수 있습니다. 분석이 전체 location.href를 페이지 주소로 올리면, 우물정자 뒤가 통계 로그에 들어갑니다. Network에서 분석 요청을 보세요. 「어차피 HTTP는 안 실는다」에서 멈추면 안 됩니다.

링크 전체는 자격 정보다

키를 # 뒤에 두는 일은 암호문을 맡아 두는 HTTP 서비스에게만 키를 숨깁니다. 슬랙, 카카오톡, 메일 클라이언트, 브라우저 기록, 동기화된 탭은 사용자가 본 주소 전체를 저장합니다. 우물정자 뒤도 포함합니다. 전체 링크를 가진 사람은 읽기 페이지를 열고 풀 수 있습니다. 베어러 URL입니다. 링크 자체가 자격 정보입니다. 받는 사람만 아는 두 번째 암호는, 직접 나누지 않는 한 없습니다.

더 민감한 인계는 둘로 나눌 수 있습니다. 한 메시지에는 s.html?id=…만 넣고, 다른 통로—전화, 자리에서의 말, 다른 메신저—에는 # 뒤만 넣습니다. 한쪽만으로는 풀리지 않습니다. 사용 습관이지, 제품 기본값이 아닙니다. 기본은 상대가 한 번에 열 수 있도록 완전한 링크 하나를 만듭니다.

스크린샷, 복사, 평문 전달도 막지 못합니다. 일회용 링크가 줄이는 것은 반복 열람과 서버가 평문을 오래 가지는 일입니다. 상대가 본문을 찍거나 다른 방에 다시 붙이면 규격은 도울 수 없습니다. 상대가 한 번 보고 끝낼 것을 신뢰하고, 내용을 폐기한 뒤 다시 보낼 수 있을 때만 씁니다. 오래 쓰는 마스터 비밀번호, 개인 키, 복구 문구는 이 길로 보내면 안 됩니다.

Network에서 그 자리에서 확인하기

아래 단계는 브랜드 약속에 기대지 않습니다. 버려도 되는 시험 문장으로 하세요. 실제 데이터베이스 비밀번호로 연습하지 마세요.

  1. 개발자 도구 Network를 열고 「로그 유지」를 켭니다. 일회용 링크 만들기 페이지를 연 뒤 시험 문장 orange-lake-7을 넣고, 열람 횟수를 1로 두고 링크를 만듭니다.
  2. 만들기 POST를 봅니다. 본문에는 암호문 필드가 있어야 하고 orange-lake-7은 없어야 합니다. 생성된 주소가 s.html?id=…#…인지 확인합니다. 우물정자 뒤에 조각이 있고, 물음표 뒤에는 id만 있어야 합니다.
  3. 전체 링크를 새 탭에 붙입니다. 문서 요청 줄은 …/s.html?id=…여야 합니다. #와 그 뒤의 키는 보이면 안 됩니다.
  4. 이어지는 Fetch / XHR을 봅니다. 암호문 요청 경로에는 번호가 있어야 하고 키는 없어야 합니다. 분석 전송도 # 뒤나 시험 문장 원문을 가져가면 안 됩니다.
  5. 페이지는 시험 문장을 풀어야 합니다. 다른 탭에는 우물정자 앞만 붙입니다. 원문이 아니라 링크가 불완전하다고 나와야 합니다.
  6. 이미 읽은 첫 링크를 다시 엽니다. 원문이 아니라 삭제되었거나 만료되었다고 나와야 합니다. 다시 보내려면 새로 만듭니다. 서버 쪽 평문 사본은 없습니다.

MyPassGen의 일회용 링크는 이 경계 안에서 동작합니다. AES-256-GCM, 평문 한도 32 KB, 키는 # 뒤, 만들기와 읽기 모두 계정이 없습니다. 믿어야 하는 것은 여전히 Network와 「우물정자를 떼면 풀리지 않는다」이지, 페이지의 「제로 널리지」 세 글자가 아닙니다. 전체 제약은 보안 안내에 있습니다.

#가 막지 못하는 것

회사 메일과 일부 메신저는 링크 미리보기를 합니다. 백엔드가 페이지를 한 번 GET해서 제목이나 요약을 가져갑니다. HTML만 가져오고 페이지 스크립트를 실행하지 않는 미리보기는 프래그먼트를 보지 못하고, 암호문 API도 대개 호출하지 않습니다. 페이지 스크립트를 실행하는 스캐너는 받는 사람보다 먼저 한 번 읽을 수 있고, 암호문은 그때 탑니다. 규격 구멍이 아닙니다. 「읽기 페이지를 열면 스크립트가 암호문을 가져온다」의 대가입니다. 상대가 이미 사라졌다고 하면, 미리보기가 먼저 읽었는지 물어보세요. 다시 보내려면 새로 만듭니다.

브라우저 기록, 충돌 보고, 일부 동기화 계정은 전체 URL을 남깁니다. 일회용 링크를 주소창에 두면 키를 로컬 기록에 두는 것과 같습니다. 읽은 뒤에 #가 붙은 주소를 티켓 서식에 쓰지 마세요. 「영구 백업」으로 즐겨찾기 하지도 마세요. 잃은 링크는 복구할 수 없습니다. 지원 초기화는 없고, 바닥글에도 암호문을 되살려 줄 메일함은 없습니다.

긴 설명문을 보낼 때는 링크와 본문이 다른 일입니다. utm_source와 클릭 ID는 파라미터 표대로 지웁니다. 티켓의 휴대폰, 주민등록번호는 넘기기 전에 반드시 가려야 할 항목대로 가립니다. 일회용 링크가 답하는 것은 「호스트가 키와 평문을 못 본다」입니다. 「이 논의에 완전한 계좌번호가 들어가면 안 된다」가 아닙니다.

흔한 오해

「키가 주소에 있으니 서버가 반드시 본다.」 어느 조각이냐에 달렸습니다. ? 뒤면 맞습니다. # 뒤면 RFC 9110상 문서 요청과 Referer는 빼야 합니다. Network의 요청 줄을 보세요. 직감으로 싸우지 마세요.

「# 뒤에 두면 채팅 기록이 안전하다.」 메신저는 붙인 문자열 전체를 저장합니다. 서버 로그에 키가 없다고 해서, 방 안의 사람이나 나중에 그 휴대폰을 여는 사람에게 키가 없는 것은 아닙니다.

「읽으면 삭제가 스크린샷을 막는다.」 막지 못합니다. 반복 열람과 서버 쪽 평문만 줄입니다. 복사와 촬영은 줄이지 않습니다. 폐기하고 다시 만들 수 있는 비밀번호만 보내세요.

「받는 사람도 가입해야 볼 수 있다.」 이 사이트는 만들기와 읽기 모두 계정이 없습니다. 읽기 페이지는 받는 사람에게 공개입니다. 「가입 없이 열린다」를 「링크가 있는 사람은 먼저 로그인해야 한다」로 읽으면 안 됩니다.

어디서부터 하면 되나

오늘 보내려던 그 비밀번호부터 처리하세요. 버려도 되는 시험 문장으로 위 여섯 단계를 먼저 돕니다. 만들기 요청에 원문이 없고, 읽기 페이지 요청 줄에 # 뒤 키가 없으며, 우물정자를 떼면 풀리지 않고, 두 번째 열면 삭제 상태여야 합니다. 그다음에 실제 비밀을 보내세요.

실제 비밀번호는 이 기기의 비밀번호 생성으로 만듭니다. 무작위 모드는 6–128자, 기본 16자입니다. 8자 미만은 약하다고 보세요. 전체 링크를 보냅니다. 더 민감하면 우물정자 앞뒤를 두 통로로 나눕니다. 같은 비밀번호를 채팅에 「백업」으로 다시 붙이지 마세요. 한 번 보내면 이미 이 글의 질문에 답할 수 있습니다. 복호화 키를 # 뒤에 두는 이유는, 암호문을 맡아 두는 HTTP 서비스가 키를 보지 못하게 하기 위해서입니다. 채팅 기록과 스크린샷은 막지 못합니다.

자주 묻는 질문

키를 # 뒤에 두면 서버가 정말 못 보나요?

RFC 9110에서 문서 요청의 대상 URI는 fragment를 빼니다. MDN에서도 Referer는 이 조각을 빼니다. Network를 여세요. 읽기 페이지 요청 줄에 우물정자 뒤 키가 보이면 안 됩니다. 페이지 스크립트가 location.href를 스스로 올리면 구현 유출입니다. Fetch 목록에서 찾으세요.

받는 사람도 계정이 있어야 링크를 여나요?

아닙니다. 만들기와 읽기 모두 계정이 없습니다. 받는 사람은 읽기 페이지만 열고 복호화합니다. 정한 횟수만큼 읽거나 TTL이 끝나면, 다시 열어도 원문이 아니라 삭제 또는 만료로 나옵니다.

우물정자 앞만 복사했는데 열 수 있나요?

복호화할 수 없습니다. # 뒤 키가 없으면 읽기 페이지는 링크가 불완전하다고 해야 합니다. 보낸 사람에게 전체 주소를 받으세요. 잃거나 이미 탄 링크는 복구할 수 없습니다. 다시 보내려면 새로 만듭니다.

메신저가 기록을 남기지 않게 해 주나요?

아닙니다. 메신저는 대개 붙인 URL 전체를 저장합니다. 키도 포함합니다. 우물정자가 하는 일은 암호문을 맡아 두는 HTTP 서비스에게 키를 안 보이게 하는 것뿐입니다. 오래 남는 방에 자격 정보를 두고 싶지 않다면, 그 방에 전체 링크를 붙이지 마세요.