열람 후 파기는 서버가 암호문을 삭제한 것은 증명해도, 상대가 스크린샷을 안 찍은 것은 증명하지 못한다

일회용 비밀번호를 열람 후 파기 링크로 만들어 보냅니다. 상대가 연 뒤 새로고침하면 페이지에 「파기됨」이 뜹니다. 이 장면을 「보고 나면 아무것도 안 남는다」로 옮기는 경우가 많습니다. 사라진 것은 서버에 있던 암호문입니다. 상대 화면의 복사, 시스템 캡처, 카카오톡에 남은 전체 URL은 이번 삭제를 거치지 않습니다. 그 자리에서 확인할 수 있는 것은, 같은 번호로 다시 가져갈 때 서버가 암호문을 내주는지뿐입니다.

먼저 좁은 결론을 적어둡니다. 열람 횟수에 도달하거나 만료된 뒤에는 서버의 암호문이 없고, 같은 번호로 원문을 다시 받을 수 없습니다. 이 문장은 「상대가 스크린샷을 안 찍었다」, 「단톡방에 제3자가 안 열었다」, 「채팅 백업에 전체 링크가 없다」를 뒷받침하지 않습니다. 확인 방법은 두 번 열기와 Network이지, 화면의 「파기됨」을 상대 기기 감사로 읽는 일이 아닙니다.

검색 의도를 한 문장으로 먼저 답하기

열람 후 파기가 뒷받침하는 것은 호스트가 더 이상 암호문을 들고 있지 않다는 점입니다. 상대의 화면, 클립보드, 채팅 기록이 지워졌다는 주장은 뒷받침하지 않습니다. 암호문을 한 번 성공적으로 가져온 뒤 같은 링크를 다시 열면, 열람 페이지는 파기됨 또는 만료됨을 보여 줍니다. HTTP 410 Gone의 의미는 원본 서버에서 대상 자원이 더 이상 쓸 수 없고, 그 상태가 대개 영구라는 것입니다. 이 코드가 답하는 질문은 「서버가 아직 내주느냐」이지, 「상대가 사본을 남겼느냐」가 아닙니다.

이 글은 개발자, 운영, 그리고 일회용 비밀번호·API Key·복구 코드를 동료에게 보내기 전에 「링크가 죽었다」와 「상대가 아무것도 안 남겼다」를 나누고 싶은 사람을 위한 것입니다. 열람 후 파기 사용 설명을 대신하지 않으며, 「링크를 어떻게 만드는가」도 반복하지 않습니다. 가져가야 할 것은 어느 층이 타는지, 새로고침 실패가 증명하는 범위, 스크린샷이 왜 그 밖에 있는지, 두 번 열기와 Network로 호스트 쪽을 그 자리에서 대조하는 방법입니다.

「파기됨」과 「상대가 사본을 안 남겼다」는 다릅니다

일상 말에서는 서로 다른 세 가지를 「읽고 나면 사라진다」한 문장으로 묶습니다. 첫째, 서버가 암호문을 더 이상 보관하지 않는다. 그 번호의 기록이 지워지고, 이후 요청은 파기됨 또는 만료됨만 받습니다. 둘째, 지금 연 탭이 평문을 더 이상 보여 주지 않는다. 탭을 닫거나, 「이 페이지에서 지우기」를 누르거나, 떠날 때 입력칸을 비우는 스크립트가 이 일을 합니다. 셋째, 상대 기기에 사본이 없다. 스크린샷도, 붙여넣기도, 전체 링크 전달도, 평문을 다시 채팅에 붙인 일도 없다.

열람 후 파기가 강제할 수 있는 것은 첫째뿐입니다. 둘째는 기껏해야 이 탭의 표시 영역을 비우는 데 그칩니다. 셋째에는 강제력이 없습니다. 열람 페이지가 「복사나 스크린샷은 막지 못한다」고 쓰는 것은 겸손이 아니라 경계입니다. 일반 웹페이지는 다른 휴대폰이 화면을 찍는 일을 막을 수 없고, 상대가 「평문 복사」를 누른 뒤에 시스템 클립보드나 메신저의 로컬 저장소를 지울 수도 없습니다.

따라서 「내가 새로고침했는데 안 열린다」가 뒷받침하는 결론은 좁습니다. 이번 번호로 암호문을 달라고 했을 때 서버가 내주지 않았다는 것입니다. 「그 동료만 봤다」, 「스크린샷을 안 찍었다」, 「티켓 검색에 이 문자열이 다시 안 나온다」는 뒷받침하지 않습니다. 뒤의 문장들을 확인할 수 있는 사실로 쓰려면, 보낸 쪽의 두 번째 열기가 아니라 평문이나 전체 URL이 이 탭을 나갔는지를 봐야 합니다.

서버가 실제로 지우는 층

제로 지식 열람 후 파기는 위치와 복호화를 나눕니다. 형태는 s.html?id={id}#{key}입니다. 물음표 뒤에는 번호만 있고, 키는 해시 뒤에만 있습니다. RFC 9110은 요청의 대상 URI에 프래그먼트를 넣지 않는다고 적습니다. 브라우저가 서버에 문서를 요청할 때 #와 그 뒤를 뗍니다. 설계상 서버가 보는 것은 번호이며, 키도 평문도 아닙니다.

만들 때 이 탭은 Web Crypto로 AES-256-GCM을 씁니다. 난수 32바이트 키, 12바이트 IV, 평문을 이 기기에서 암호문으로 만든 뒤 POST /secrets합니다. 나가는 필드는 암호문, 만료, 열람 횟수입니다. 키는 이후에 조립하는 공유 링크에만 나타납니다. 한 건은 32 KB까지입니다. 만료는 1시간, 24시간, 7일, 또는 열람 후에만 파기를 고를 수 있고, 열람 횟수는 1–10이며 기본값은 1입니다. 만들기와 읽기 모두 열어 바로 사용합니다. 계정이 없고, 「누가 열었는지」를 표시할 로그인 신원도 없습니다.

읽을 때 서버는 암호문을 내주고 설정 횟수에 도달하면 그 암호문을 삭제합니다. 보낸 쪽이 다시 열면 파기됨 또는 만료됨을 봅니다. 이것이 증명하는 것은 호스트가 그 암호문 바이트를 더 이상 들고 있지 않다는 점입니다. 키가 채팅 기록에 한 번도 안 나타났다는 것, 평문이 상대 화면에 안 떴다는 것, 제3자가 같은 전체 링크에서 「열어 보기」를 안 눌렀다는 것은 증명하지 않습니다.

전체 URL을 가진 사람은 키도 가집니다. https://…/s.html?id=…#…를 단톡방에 붙이면 번호와 열쇠를 함께 넘긴 것입니다. 서버가 이미 암호문을 지웠어도, 그때 이미 열고 평문을 남긴 사람을 되돌릴 수는 없습니다. 횟수가 남아 있으면 나중에 누른 사람도 암호문을 가져갈 수 있습니다. 암호문을 지우는 일은 「호스트가 평문이나 암호문을 오래 보관한다」는 층을 줄입니다. 「문자열 전체를 본 사람」이라는 층은 건드리지 않습니다.

새로고침 실패가 증명하는 것과, 증명하지 못하는 것

두 번째 열기가 실패해도 상태는 하나가 아닙니다. 세 가지 안내를 나누세요. 파기됨: 암호문이 없다. 횟수를 다 썼거나, 번호가 처음부터 유효하지 않았을 수 있습니다. 만료됨: 만들 때 고른 TTL이 끝나 아무도 안 읽어도 서버가 지웁니다. 프래그먼트 키 없음: 번호는 남아 있을 수 있으나 이 기기는 복호화할 수 없습니다. 카카오톡, Slack, Teams, 일부 메일 클라이언트가 # 뒤를 잘라 냅니다. 수신자가 보는 「프래그먼트 키가 없습니다」는 「다른 사람이 이미 읽었다」와 같은 사건이 아닙니다.

410 Gone은 원본 서버에서 대상 자원을 더 이상 쓸 수 없고, 그 상태가 영구일 가능성이 크다는 뜻입니다. 열람 페이지는 410을 파기됨과 만료됨으로 나눕니다. 응답에 expired가 있으면 TTL이고, 없으면 파기로 처리합니다. 404도 파기됨으로 보여 줍니다. 수신자에게 결과는 같기 때문입니다. 다시 암호문을 가져갈 수 없습니다. 네트워크 실패는 파기가 아닙니다. 「열어 보기」를 누른 뒤에 끊겼고 만들 때 한 번만 읽도록 했다면, 이 탭이 복호화하지 못했더라도 암호문은 이미 가져가졌을 수 있습니다.

그러므로 「내 쪽에서 새로고침하니 안 열린다」가 뒷받침하는 것은 이것입니다. 지금 이 번호로는 서버가 암호문을 내주지 않는다. 뒷받침하지 않는 것은 이렇습니다. 연 사람이 지명한 그 동료다. 복사하지 않았다. 스크린샷을 안 찍었다. 미리보기나 보안 스캐너가 횟수를 먼저 쓰지 않았다. 계정이 없으면 이름을 붙인 읽음 확인도 없습니다. 횟수는 서버 쪽 카운터이지, 상대 기기의 감사가 아닙니다.

실제 비밀번호, API Key, 마스킹하지 않은 접속 문자열로 실험하지 마세요. 이번에만 쓰는 테스트 문장을 준비하세요. 대조하는 것은 상태 인터페이스, 암호문 가져가기, 두 번째 열기의 안내입니다. 살아있는 자격 증명을 다시 노출하는 일이 아닙니다.

스크린샷, 복사, 채팅 기록이 이 층 밖에 있는 이유

평문이 화면에 그려지는 순간, 질문은 「서버에 암호문이 있는가」에서 「상대 단말에서 무슨 일이 있었는가」로 바뀝니다. 시스템 스크린샷, 화면 녹화, 다른 휴대폰으로 찍는 사진은 호스트에 요청을 보내지 않습니다. 열람 페이지는 그 동작을 가로챌 수 없고, 사후에 알려 줄 수도 없습니다. 어떤 제품이 기능을 빠뜨린 것이 아닙니다. 일반 웹페이지에는 「상대가 자기 화면을 기록하지 못하게 하라」는 스위치가 주어지지 않습니다.

복사도 같은 울타리 밖에 있습니다. 열람 페이지는 「평문 복사」를 제공할 수 있고, 떠날 때 칸을 비울 수 있습니다. 비우는 것은 이 DOM 노드이지, 시스템 클립보드도, 메신저 초안함도 아닙니다. macOS와 일부 입력기는 클립보드 기록을 남깁니다. 「이 페이지에서 지워짐」은 지금 탭이 그 문장을 더 이상 보여 주지 않는다는 뜻일 뿐입니다. 그 문장이 클립보드에 한 번도 안 들어갔다는 뜻이 아닙니다.

채팅 기록은 또 다른 저장소입니다. 전체 링크가 단톡방에 한 번 나타났다면, 대화 검색, 여러 기기 동기화, 이후 내보내기에서 해시 뒤의 키가 다시 보입니다. 서버가 암호문을 지운 뒤에도 URL은 「한때 유효했다」는 증거가 되고, 횟수가 다 쓰인 줄 모르는 사람이 반복해서 눌러 「링크가 고장 났다」고 오해하기에도 충분합니다. HTTP 층은 여전히 깨끗할 수 있습니다. Referer는 규격상 프래그먼트를 실으면 안 됩니다. 다만 채팅 기록을 열 수 있는 사람에게는 그 보호가 이미 없습니다.

티켓, 메일, 브라우저 히스토리도 같은 분리입니다. 주소창에 보였던 # 프래그먼트는 대개 로컬 히스토리에 들어가고, 동기화를 켠 브라우저는 그 줄을 다른 기기로 보낼 수 있습니다. 탭을 닫는 것은 히스토리를 지우는 일이 아닙니다. 열람 후 파기가 줄이는 것은 서버가 조각을 들고 있는 시간과, 같은 암호문을 반복해서 가져갈 기회입니다. 디지털 저작권 관리가 아니고, 포렌식 보고서도 아닙니다.

상태 조회가 횟수를 쓰지 않는 이유

랜딩을 처음 열 때 암호문을 가져간다면, 카카오톡 링크 미리보기, 메일 보안 게이트웨이, 일부 사내 스캐너가 수신자보다 먼저 유일한 한 번을 쓸 수 있습니다. 수신자가 눌렀을 때는 「파기됨」만 보이고, 키는 미리보기 호스트에 넘어간 적이 없습니다. 타 버린 것은 열람 횟수이지, 프래그먼트의 비밀이 아닙니다. 이전 글은 해시가 기본적으로 HTTP에 안 들어가는 이유를 적었습니다. 이 글은 횟수만 봅니다. 미리보기 요청과 「실제로 암호문을 가져가는 일」을 나누지 않으면, 「파기됨」은 「수신자가 봤다」조차 증명하지 못합니다.

FastPwd 열람 페이지는 이 두 단계를 나눕니다. 페이지를 열면 /secrets/{id}/status만 요청해 암호문이 아직 있는지만 묻고, 가져가지는 않습니다. 수신자가 「열어 보기」를 누른 뒤에야 GET /secrets/{id}로 암호문을 가져오고, 해시 뒤의 키로 이 기기에서 복호화합니다. # 키가 없으면 페이지는 암호문을 가져가지 않아 남은 횟수를 낭비하지 않습니다. 만들기 페이지와 열람 페이지 모두 계정이 필요 없습니다.

미리보기 봇은 보통 랜딩만 가져가고 버튼을 대신 누르지 않습니다. 「미리보기 = 파기」의 확률은 낮아지지만, 모든 스캐너가 랜딩에서 멈춘다는 증명은 아닙니다. Network에서는 먼저 status가 보이고, 그다음에 암호문 가져가기가 보여야 합니다. 열기만 하고 클릭하지 않으면, 암호문을 성공적으로 가져간 응답이 나오면 안 됩니다. 횟수는 여전히 서버 쪽 카운터이며, 프래그먼트의 일부가 아닙니다.

보이는 현상 뒷받침할 수 있는 결론 뒷받침할 수 없는 결론
두 번째 열기에 파기됨 지금 이 번호로는 암호문을 가져갈 수 없다 상대가 스크린샷·복사를 안 했다
만료됨이 뜬다 TTL이 끝나 서버가 삭제했다 반드시 누군가 성공적으로 읽었다
프래그먼트 키가 없다 주소에 # 뒤의 키가 없다 암호문이 반드시 이미 가져가졌다
페이지만 열고 버튼을 안 눌렀다 status 조회는 횟수를 쓰면 안 된다 모든 미리보기 봇이 암호문을 안 가져간다
Network의 POST에 암호문만 있다 이번 만들기가 원문을 업무 필드로 보내지 않았다 전체 링크를 채팅에 붙인 뒤에도 나만 본다
이 페이지에서 평문을 지웠다 지금 탭이 그 문장을 더 이상 보여 주지 않는다 클립보드와 스크린샷에도 없다

그 자리에서 대조하기: 두 번 열기, status, Network

구호는 증거가 되지 않습니다. canary-fp-20260905-burn-vs-shot처럼 버려도 되는 카나리아를 준비하세요. 실제 비밀번호를 재사용하지 마세요. 열람 횟수는 기본값 1로 두고, 만료는 24시간이면 충분합니다.

  1. 만들기 페이지에 카나리아를 넣고 링크를 만듭니다. 개발자 도구의 Network를 열고 Preserve log를 켭니다. 카나리아와 해시 뒤의 키를 각각 검색하세요. POST /secrets 본문에는 암호문, 만료, 열람 횟수만 있어야 하며, 원문이나 키는 없어야 합니다.
  2. 같은 전체 링크로 열람 페이지를 열고, 아직 「열어 보기」는 누르지 마세요. Network에는 /secrets/{id}/status가 보여야 하고, 암호문을 성공적으로 가져간 GET /secrets/{id}는 없어야 합니다. 페이지는 확인 대기에 머물러야 합니다.
  3. 「열어 보기」를 누릅니다. 이때야 번호로 암호문을 가져오고, 이 기기에서 AES-256-GCM으로 엽니다. 성공 안내에는 서버가 이 암호문을 삭제했다고 쓰이거나—횟수를 1보다 크게 두었다면—설정 횟수에 도달해야 삭제한다고 쓰입니다.
  4. 열람 페이지를 새로고침하거나, 새 브라우저 컨텍스트에서 같은 링크를 엽니다. 기본 1회라면 파기됨이 보여야 합니다. Network를 다시 검색하면 암호문 가져가기는 410이거나 파기로 처리되는 응답이어야 합니다. 카나리아가 어떤 요청 본문에도 평문으로 다시 나오면 안 됩니다.

이 네 단계가 증명하는 변화는 하나입니다. 당신이 통제하는 두 번 열기 사이에, 서버의 암호문이 「있음」에서 「없음」으로 바뀌었다. 이것이 실제 비밀번호를 동료에게 보냈을 때 상대가 스크린샷을 안 찍었다는 증명은 되지 않습니다. 「상대가 남길지」는 인수인계 메모에 적어야 합니다. 캡처하지 말 것, 평문을 단톡방에 다시 붙이지 말 것, 쓰고 나면 바로 교체할 것. 화면의 「파기됨」이 상대 단말을 대신 감사해 주기를 바라면 안 됩니다.

전체 링크와 암호문을 같은 경로로 보내지 마세요. 클라우드나 메일로는 .lock을 보내고, 비밀번호는 열람 후 파기로 넘기세요. 열람 후 파기의 전체 URL을 그 폴더 메모에 다시 적지 마세요. 열쇠와 상자를 한 메시지에 넣으면, 암호문을 지워도 문자열 전체를 이미 본 사람은 되돌릴 수 없습니다.

「파기됨」을 너무 넓게 읽는 세 가지

첫째, 만료를 누군가 읽은 증거로 봅니다. 만들 때 1시간을 골랐고, 동료가 다음 날 누르면 안내는 만료됨입니다. 서버는 암호문을 지웠지만, 이유는 TTL이지 횟수 소진이 아닙니다. 만료되어 읽지 못한 내용은 되돌릴 수 없고, 「이미 열렸다」와 같지도 않습니다.

둘째, 프래그먼트 없음을 이미 파기된 것으로 봅니다. 카카오톡과 일부 메일 클라이언트는 # 뒤를 잘라 냅니다. 수신자가 연 주소는 s.html?id=…이고 키가 없습니다. 열람 페이지는 키가 없으면 암호문을 가져가지 않습니다. 당신이 나중에 전체 링크를 열면 내용이 아직 있을 수 있습니다. 이때는 해시가 포함된 전체 문자열을 다시 달라고 해야지, 「누가 먼저 봤다」고 단정하면 안 됩니다.

셋째, 이 페이지에서 지움을 전역 삭제로 봅니다. 열람 페이지는 떠날 때 칸을 비울 수 있고, history.replaceState로 주소창의 프래그먼트를 뗄 수 있습니다. 영향은 지금 탭의 표시와, 이후 새로고침에 키가 남는지뿐입니다. 시스템 스크린샷, 클립보드 기록, 채팅 백업, 동기화된 브라우저 히스토리는 이 지우기 범위 밖에 있습니다.

자주 묻는 질문

상대가 새로고침한 뒤 「파기됨」을 보면, 내용을 남기지 않았다는 뜻인가요?

아닙니다. 서버에 암호문이 더 이상 없어서, 같은 번호로 원문을 다시 받을 수 없다는 뜻일 뿐입니다. 스크린샷, 복사, 화면 녹화, 채팅 백업, 전체 링크를 제3자에게 넘기는 일은 이번 삭제를 거치지 않습니다. 「파기됨」은 상대 기기가 깨끗하다는 증거가 아닙니다.

카카오톡 링크 미리보기가 내용을 먼저 파기할 수 있나요?

미리보기는 보통 랜딩 페이지만 엽니다. 열람 페이지는 먼저 암호문이 아직 있는지만 묻고 가져가지 않으며, 「열어 보기」를 눌러야 번호로 암호문을 가져갑니다. 미리보기 봇은 대개 그 버튼을 대신 누르지 않지만, 모든 스캐너가 랜딩에서 멈춘다고 단정할 수는 없습니다. 횟수는 서버 쪽 카운터입니다. 대조 방법은 이렇습니다. 열기만 하고 클릭하지 않으면, Network에 암호문을 성공적으로 가져간 요청이 나오면 안 됩니다.

열람 후 파기는 누가 열었는지 증명할 수 있나요?

없습니다. 만들기와 읽기 모두 계정이 없고, 서버도 로그인 신원으로 열람자를 표시하지 않습니다. 확인할 수 있는 것은 이번 가져가기 뒤에 그 번호에 암호문이 남아 있는지뿐입니다. 「파기됨」을 누가 열었는지로 뒤집을 수 없고, 읽음 확인으로 적을 수도 없습니다.

만들기와 읽기에 가입이 필요한가요? 평문을 올리나요?

가입은 필요 없습니다. 지금 연 탭이 AES-256-GCM으로 암호화한 뒤 암호문만 올립니다. 키는 링크의 # 프래그먼트에 둡니다. 카나리아로 Network를 검색하면 본문에는 암호문만 있어야 하며, 원문이나 해시 뒤의 키는 없어야 합니다. 32 KB를 넘거나 파일 전체라면 파일 암호화에서 이 기기로 처리하세요. 열람 후 파기를 클라우드 드라이브처럼 쓰지 마세요.

같은 카나리아를 열어 바로 사용하는 도구로 한 번 대조하기

계산 경계를 글로 적어 둔 페이지에서 연습하려면 FastPwd 열람 후 파기를 여세요. 가입은 필요 없습니다. 위의 카나리아를 넣고 열람 횟수는 1로 둔 채 링크를 만든 뒤, 네 번째 절의 두 번 열기를 끝까지 하세요. 동시에 Network를 보세요. 만들기 POST의 암호문, 열람 페이지의 status, 클릭 뒤의 가져가기, 두 번째 열기의 파기 응답이 보여야 합니다. 원문이나 해시 뒤의 키가 업무 필드로 보이면 안 됩니다.

비밀번호를 반드시 동료에게 넘겨야 할 때만 이 경로를 쓰세요. 난수 비밀번호는 비밀번호 생성기에서 이 기기로 만들 수 있습니다. 길이는 6–128자, 기본값은 16자이며, 8자 미만이면 보안이 낮다고 알려 줍니다. 생성 과정은 올리지 않습니다. 계약서 전체, 신분증 스캔, 32 KB를 넘는 자료는 파일 암호화에서 같은 AES-256-GCM으로 .lock / .enc를 만들고, 한 파일은 5 GB를 넘지 않습니다. 비밀번호는 따로 열람 후 파기로 보내고, 클라우드 폴더 메모에 적지 마세요.

다음에 보내기 전에 세 가지만 기억하세요. 첫째, 파기됨이 증명하는 것은 서버의 암호문이 없어졌다는 것이지, 상대가 스크린샷을 안 찍었다는 것이 아닙니다. 둘째, 랜딩만 열면 횟수를 쓰면 안 되고, 「열어 보기」를 눌러야 암호문을 가져갑니다. 셋째, 전체 링크를 채팅에 붙이면 해시 뒤의 키는 그 메시지를 볼 수 있는 사람에게 이미 평문으로 보입니다. 반복해서 관찰할 수 있는 사실만 여러분 인수인계 메모에 적을 가치가 있습니다.

더 읽을거리

해시 뒤가 기본적으로 HTTP에 안 들어가는 이유와, 전체 링크를 붙일 때 어느 층이 사라지는지는 URL 해시 뒤에 키를 두는 이유, 그리고 그 보호가 깨질 때에 있습니다. 「로컬 암호화, 올리지 않음」은 카나리아로 Network를 검색하기 전에는 증거가 되지 않습니다. 브라우저 로컬 암호화에서 원문이 이 기기를 떠나지 않았는지 확인하는 방법. 티켓 본문의 신분증 번호를 이 기기를 떠나기 전에 가리려면 티켓과 로그를 AI에 붙이기 전에, 어떤 항목이 이 기기를 떠나는지 먼저 확인하는 방법을 보세요.