「온라인 암호화」나 「브라우저에서 파일 암호화」를 검색하면 결과 페이지는 거의 같은 말을 씁니다. 계산은 로컬, 파일은 올리지 않음, 키는 기기를 떠나지 않음. 읽으면 안심이 되지만 증거가 되지는 않습니다. 페이지는 그렇게 적으면서도, 스크립트는 입력칸의 암호, 파일 조각, 주소창의 키를 fetch로 보낼 수 있습니다. 확인할 대상은 문구가 아닙니다. 이번 한 번의 동작이 평문을 브라우저 밖으로 보냈는지입니다.
이 글은 암호화 제품을 고르는 법이 아니고, 어떤 도구 페이지의 기능 목록을 다시 쓰는 글도 아닙니다. 질문은 하나입니다. 브라우저에서 암호화한다고 할 때, 개발자 도구와 주소창에서 그 자리에 무엇이 보이는가. MyPassGen의 파일 암호화와 일회용 링크는 둘 다 Web Crypto API의 AES-256-GCM을 쓰고, 열면 바로 씁니다. 아래는 같은 확인법으로 경계를 나누는 글입니다. 「저장하지 않는다」는 문장을 믿으라는 말이 아닙니다.
문구는 확인할 수 없고, 트래픽은 된다
「클라이언트 사이드 암호화」는 닳아버린 말입니다. 어떤 사이트는 파일을 자기 서버에 POST한 뒤 서버에서 AES로 감싸고 다운로드 링크를 줍니다. 사용자에게는 이것도 「온라인 암호화」입니다. 어떤 사이트는 탭 안에서 crypto.subtle.encrypt를 호출하고 .lock이나 .enc를 받으며, 업무용 업로드가 없습니다. 마케팅 페이지에서는 한 문장으로 쓸 수 있습니다. Network에서는 전혀 다릅니다.
브라우저는 문구를 심사하지 않습니다. 이번 클릭이 만든 문서 요청, 스크립트, 이미지, XHR / Fetch만 나열합니다. 요청 줄에 평문이 있는지, 본문에 암호가 있는지, 분석 전송의 url에 # 뒤의 키가 있는지는 보입니다. 안 보이면 「기본으로 이 기기를 떠나지 않는다」가 아직 서 있는 것입니다. 보이면 문구는 이미 끝난 것입니다.
소스 전체를 읽을 필요는 없습니다. 필요한 것은 반복 가능한 한 바퀴입니다. 패널을 열고, 구체적인 일 하나를 하고(비밀번호 생성, 버려도 되는 암호 검사, 작은 파일 암호화, 일회용 링크 만들기), 새로 생긴 요청을 하나씩 엽니다. 먼저 「로컬」이 어느 층을 말하는지 나누고, 우물정자와 물음표를 대조한 뒤, 단계로 내려갑니다.
「로컬」이 가리키는 층
Web Crypto API는 브라우저가 주는 암호 연산 인터페이스입니다. crypto.subtle로 해시, 키 파생, 암호화, 복호화를 합니다. W3C 사용 사례에는 이런 합법 구조가 있습니다. 사용자가 브라우저에서 키를 고르고 문서를 암호화한 다음에, 암호화된 데이터를 서비스에 올리는 것. 즉 API가 보장하는 것은 「암호화 연산이 이 기기에서 일어날 수 있다」는 점입니다. 그 뒤에 암호문을 보내는 것을 막지 않고, 페이지 작성자가 먼저 올리고 나중에 암호화하는 것도 막지 않습니다.
Web Crypto는 보안 컨텍스트도 요구합니다. 실제 서비스에서는 보통 HTTPS입니다(localhost는 예외). 마케팅 포인트가 아니라, 인터페이스가 살아 있는지의 전제입니다. 일반 HTTP 페이지가 「Web Crypto를 쓴다」고 하면, 알고리즘 이야기 전에 주소창 자물쇠부터 봅니다.
이 기기의 AES-256-GCM이 의미하는 것
MyPassGen 초판은 AES-256-GCM만 씁니다. MDN AesGcmParams와 거기서 인용하는 NIST SP 800-38D에 따르면 GCM은 인증 암호화입니다. 암호문이 바뀌면 복호화가 실패합니다. 깨진 글이 나와서 본문으로 쓰게 두지 않습니다. 초기화 벡터(IV)는 96비트, 즉 12바이트를 권장하고, 같은 키로 암호화할 때마다 새 IV를 써야 합니다. IV 자체는 비밀이 아니므로 암호문과 같이 둘 수 있습니다. 인증 태그는 기본 128비트입니다. 이 숫자는 구현에서 대조할 수 있습니다. 외울 구호가 아닙니다.
파일 암호화는 암호에서 키를 먼저 만듭니다. 이 사이트의 파일 암호화는 PBKDF2, 100,000회 반복, 해시 SHA-256, 소금 16바이트로 256비트 AES-GCM 키를 얻습니다. 파일 하나 상한은 5 GB이고, 결과는 .lock 또는 .enc입니다. 암호가 같이 POST되면 반복 횟수를 아무리 올려도 소용이 없습니다. 그래서 첫 확인 대상은 여전히 트래픽이지, 반복 횟수가 아닙니다.
전혀 다른 「온라인 암호화」 세 가지
흐름을 펼치면 적어도 세 갈래입니다. 첫째, 브라우저는 파일만 고르고 바이트는 서버로 가며, 암호화는 맞은편에서 끝납니다. 둘째, 브라우저가 먼저 암호화하고 암호문만 올리며, 키는 다른 통로(예를 들어 URL의 #)로 갑니다. 셋째, 결과는 이 기기로 바로 받고, 업무 요청에 파일 본문이 없습니다. 셋 다 「웹에서 하는 암호화」라고 부를 수 있습니다. 「평문을 기본으로 올리지 않는다」를 말할 자격이 있는 것은 뒤의 둘뿐입니다. 할 일은 Network로 지금 페이지가 어느 갈래인지 가리는 것입니다.
확인할 대상을 정한 뒤에 시작을 누릅니다
비밀번호 생성, 강도 검사, 링크 정리, 파일 암호화는 「평문을 업무 데이터로 보내지 않았다」가 기대입니다. 일회용 링크는 기대가 다릅니다. 암호문 필드는 보여도 되고, 원문은 보이면 안 되며, 요청 줄에 # 뒤의 키도 보이면 안 됩니다. 두 기대를 섞으면 정상적인 암호문 POST를 「유출」로 오인합니다.
우물정자와 물음표는 다른 물건이다
주소는 스킴, 호스트, 경로, 쿼리(? 뒤), 프래그먼트(# 뒤)로 나뉩니다. RFC 3986 §3.5는 # 뒤를 fragment라고 부릅니다. 주 자원을 받은 뒤에 클라이언트가 해석하는 이차 위치입니다. MDN의 URI fragment 설명은 더 직설적입니다. 그 URI를 요청할 때 fragment는 서버로 가지 않고, 자원이 도착한 뒤 클라이언트가 처리합니다.
쿼리 문자열은 반대입니다. https 링크를 열면 ? 뒤의 이름=값은 요청 줄에 들어가고, 맞은편 접속 로그에도 남습니다. 앞글 링크를 공유할 때 지워도 되는 파라미터, 지우면 안 되는 파라미터에서 이미 적었습니다. utm_source와 id=는 쿼리에 삽니다. 키를 ?key=로 쓰면 서버, 리버스 프록시, CDN 로그가 다 볼 수 있습니다. # 뒤에 쓰면 기본 요청 줄에는 그 조각이 없습니다.
Referer도 fragment를 뗍니다. MDN Referer는 이 헤더가 출처, 경로, 쿼리를 가져갈 수 있다고 적습니다. URL fragment와 사용자 이름·비밀번호는 가져가지 않습니다. W3C Referrer Policy도 「보내기 전 제거」 단계에서 fragment를 비웁니다. 따라서 「키를 # 뒤에 두면, 암호문을 맡아 두는 기계에는 HTTP로 가지 않는다」는 프로토콜 동작입니다. 어떤 사이트의 구두 약속이 아닙니다.
일회용 링크의 형태는 s.html?id={id}#{key}입니다. 쿼리에는 암호문 번호만 있고, 키는 우물정자 뒤에만 있습니다. 만들 때 브라우저는 32바이트 난수 키로 AES-256-GCM(IV 12바이트)을 한 뒤 암호문을 POST합니다. 읽을 때 스크립트는 location.hash에서 키를 꺼내 이 기기에서 복호화합니다. 평문 상한은 32 KB입니다. 확인할 것은 이것입니다. 만들기 요청 JSON에는 암호문 필드가 있어야 하고 원문은 없어야 합니다. 읽기 페이지 문서 요청 URL에는 id=가 있어야 하고 # 뒤 조각은 없어야 합니다.
Network로 그 자리에서 확인하기
Chrome, Edge, Firefox, Safari 모두 네트워크 패널이 있습니다. 이름만 조금 다르고 단계는 같습니다. 확장 프로그램은 필요 없습니다. 파일이나 전체 URL을 제3자 「검사 사이트」에 다시 올리는 것도 하지 마세요. 노출면만 넓어집니다.
- 개발자 도구를 열고 Network(네트워크)로 갑니다. 「로그 보존 / Preserve log」를 켭니다. 이동 뒤에 목록이 비워지지 않게 합니다.
- 필터를 Fetch / XHR(또는 「XHR」)로 둡니다. 정적 자원은 나중에 봐도 됩니다. 업무 업로드는 거의 이 종류의 요청입니다.
- 확인할 동작을 한 번 합니다. 비밀번호를 만들고, 버려도 되는 시험 암호를 넣고, UTM이 붙은 링크를 붙이고, 작은 파일을 고르거나, 시험용 한 줄을 적어 일회용 링크를 만듭니다.
- 새로 생긴 요청을 하나씩 엽니다. Request URL을 봅니다. 문서와 API 주소에 방금 넣은 평문이 없어야 하고,
#와 그 뒤의 키도 없어야 합니다. - Payload / Request를 엽니다. JSON이나 폼에 원문, 암호, 검사 중인 비밀번호, 파일 바이트가 없어야 합니다. 일회용 링크는 암호문 필드가 있어도 됩니다. 방금 친 문장이 아니라, 난수 이진을 Base64로 쓴 것처럼 보여야 합니다.
- 분석·통계 요청을 한 번 더 훑습니다. 이름에
matomo,collect,g/collect가 자주 붙습니다. 올린 페이지 주소나 사용자 정의 파라미터에location.href가 우물정자까지 실려 있는지 봅니다.
파일 암호화에는 더 단단한 대조가 있습니다. 네트워크를 끊거나 비행기 모드를 켠 뒤 작은 파일을 암호화합니다. 계산이 이 기기에서만 끝난다면 .lock / .enc 다운로드는 그대로 되어야 합니다. 일회용 링크는 이 시험을 통과할 수 없습니다. 암호문을 서버에 잠시 맡겨야 하므로, 끊긴 상태에서 만들기가 실패하는 것은 예상입니다. 「요청이 하나도 없다」를 유일한 합격선으로 두면, 영지식 일회용 링크를 잘못 탈락시킵니다.
강도 검사 페이지는 정적 약한 암호 목록을 가져올 수 있습니다. 이 사이트는 leaked-top10k.txt를 씁니다. 그것은 단어 목록이지, 입력값이 아닙니다. 요청에서 단어 목록 파일은 보여도 됩니다. 쿼리나 본문에 입력칸의 검사 대상 비밀번호가 보이면 안 됩니다. 전 웹 HIBP 조회와는 다른 일입니다. 그쪽은 해시나 비밀번호 조각을 외부 API로 보냅니다.
| 지금 하는 일 | Network에 나와도 되는 것 | 나오면 안 되는 것 |
|---|---|---|
| 난수 비밀번호 생성 | 페이지 자신의 스크립트와 스타일 | 생성 결과, 고른 문자 집합이 POST됨 |
| 이 기기에서 암호 강도 검사 | 정적 약한 암호 목록 | 입력칸의 검사 대상 비밀번호 |
| 링크 정리 또는 마스킹 | 원문 업무 요청 없음 | 전체 URL, 신분증 번호, 이메일 원문 |
| 일회용 링크 | 암호문, id, 만료, 열람 횟수 |
평문; 요청 줄의 # 키 |
| 파일 암호화 | 파일 본문 업무 업로드 없음 | 파일 바이트, 암호 |
MyPassGen은 이 표대로 동작합니다. 비밀번호 길이는 난수 모드 6–128자(기본 16, 8 미만이면 약하다고 알림)입니다. 강도 검사는 로컬 목록과 대조하며 전 웹 조회가 아닙니다. 정리와 파일 암·복호화는 원문을 업무 데이터로 올리지 않는 것이 기본입니다. 일회용 링크는 암호문만 잠시 둡니다. 전부 열면 바로 쓰고, 가입 문은 없습니다. 표의 「나오면 안 되는 것」은 패널에서 직접 체크할 수 있습니다. 브랜드 약속을 먼저 받아들일 필요는 없습니다.
암호문이 나가는 것과 평문이 나가는 것은 다르다
영지식 공유와 「완전 단기계」는 한 문장으로 뭉개지기 쉽습니다. 파일 암호화는 후자입니다. 결과는 다운로드 폴더에 떨어지고, 서버에는 그 파일의 업무 사본이 없습니다. 일회용 링크는 전자입니다. 맞은편이 암호문 조각을 꺼내야 받는 사람이 자기 브라우저에서 풀 수 있습니다. 서버가 암호문은 보고 키는 못 보는 것이 설계입니다. 사고가 아닙니다.
유출 판단 기준도 갈라야 합니다. 일회용 링크를 만들 때 POST 본문에 긴 Base64가 보이는 것은 정상입니다. 같은 필드에서 방금 넣은 「시험용 API 키」가 읽히면 평문이 나간 것입니다. 읽기 페이지의 문서 요청은 s.html?id=…이어야 합니다. 규격대로라면 Network에 나열된 Request URL에 #는 붙지 않습니다. 그 요청의 Headers를 열어 요청 줄과 Referer 모두에 키가 없는지 확인합니다.
GCM의 인증 태그는 「몇 바이트만 바꾸고 풀어 주기」를 어렵게 만듭니다. 태그가 맞지 않으면 decrypt가 바로 실패합니다. 보호하는 것은 무결성과 진본성입니다. 「링크가 절대 전달되지 않는다」가 아닙니다. 전체 URL(우물정자 뒤의 키 포함)을 가진 사람은 열람 횟수가 끝나기 전에 풀 수 있습니다. Network가 보는 것은 서버가 키를 받았는지입니다. 채팅, 메일, 스크린샷은 다른 위험입니다. 다음 절에서 따로 적습니다.
원문을 올리는 「검사 사이트」에 일회용 링크 전체를 붙이지 마세요
우물정자 뒤의 키는 HTTP 서버에는 안 보입니다. 다음에 붙이는 사이트에는 그대로 보입니다. 확인은 자기 개발자 도구에서 요청을 보는 일입니다. s.html?id=…#… 한 줄을 출처 불명 스캔 페이지에 넘기지 마세요.
규격이 막지 못하는 유출
HTTP가 fragment를 보내지 않는다고 해서, 키가 이 컴퓨터를 영원히 떠나지 않는다는 뜻은 아닙니다. 페이지 스크립트는 window.location.hash를 읽은 뒤 스스로 fetch할 수 있습니다. 구현 오류이지, 프로토콜 구멍이 아닙니다. 그래서 여섯 번째 단계에서 분석 요청을 봅니다. 통계가 location.href를 페이지 주소 그대로 올리면, 우물정자 뒤가 통계 로그에 들어갑니다. 올바른 방법은 경로만 올리거나 hash를 버리는 것입니다. 「어차피 HTTP는 안 실는다」에 기대면 안 됩니다.
브라우저 기록, 동기화된 탭, 일부 충돌 보고는 전체 URL을 남깁니다. 일회용 링크를 주소창에 두면 키를 로컬 기록에 두는 것과 같습니다. 공유는 한 번만 쓰는 통로로 하고, 읽은 뒤에는 #가 붙은 주소를 티켓 서식에 쓰지 마세요. Referer는 fragment를 떼지만, 쿼리 파라미터는 제3자로 갈 수 있습니다. 키를 ?key=로 옮기면 안 됩니다.
화면, 클립보드, 단체 채팅 로그는 프로토콜 밖입니다. 동료가 전체 링크를 채팅에 캡처해도 서버는 여전히 키가 없고, 받는 사람 밖의 사람은 키가 있습니다. 브라우저 로컬 암호화가 답하는 질문은 「계산이 어디서 일어났고, 누가 기본으로 평문을 못 보느냐」입니다. 「전체 링크를 받은 사람이 전달하느냐」가 아닙니다. 파일 암호도 같습니다. .lock은 클라우드 드라이브에 올려도 되고, 암호는 다른 통로로 가야 합니다. 짧은 비밀은 일회용 링크가 맞고, 같은 메일 본문에 암호를 쓰지 마세요.
흔한 오해
「오프라인에서 안 되니 평문을 올리는 것이다.」 일회용 링크는 암호문을 잠시 둬야 합니다. 끊기면 실패하는 것은 암호문 전송 한 번에 의존한다는 뜻일 뿐입니다. 볼 것은 그 한 번의 내용이지, 요청이 있었는지가 아닙니다.
「HTTPS가 이미 암호화하니 로컬 암호화는 중복이다.」 HTTPS가 막는 것은 전송 경로의 엿듣는 사람입니다. 서버는 TLS의 끝점이므로 요청 본문과 쿼리를 볼 수 있습니다. 로컬 암호화가 막는 층은 「서버나 중간 분석이 기본으로 평문을 읽는다」입니다. TLS와 겹쳐 쓰고, 하는 일이 다릅니다.
「페이지에 Web Crypto가 적혀 있으니 평문은 안 올라간다.」 Web Crypto는 원시 연산만 줍니다. 먼저 encrypt하고 암호문을 올리는 것과, 먼저 FormData를 보내고 서버에서 암호화하는 것은, 같은 API 이름을 장식으로 붙일 수 있습니다. 패널의 payload가 바닥글 구호보다 앞입니다.
「제품 도메인으로 가는 요청이 없으니 안전하다.」 확장, 시스템 프록시, 일부 사내 게이트웨이는 페이지 Network에 다 그려지지 않습니다. 패널은 「완전 무업로드」 광고를 반증할 수 있습니다. 세상에 두 번째 통로가 없다고 증명하지는 못합니다. 일상에서 온라인 도구를 고를 때는 반증이면 충분합니다. 평문이 보이면 바로 멈춥니다.
어디서부터 하면 되나
오늘 해야 하고, 버려도 되는 일 하나를 고릅니다. 파일 암호화가 대조하기 쉽습니다. 민감한 정보가 없는 스크린샷을 고르고, 임시 암호를 넣고, Network를 연 뒤 암호화를 누릅니다. 파일 본문 POST가 없는 것을 확인한 다음 .lock 또는 .enc를 받습니다. 같은 그림을 남에게 줄 때는 암호문은 드라이브로, 암호는 다른 메시지로 보냅니다. 파일 하나는 5 GB를 넘기지 마세요.
한 줄 비밀번호나 복구 코드를 넘길 때는 일회용 링크로 바꿉니다. 시험 문장을 쓰고 링크를 만든 뒤, 만들기 요청에 암호문만 있는지 봅니다. 사생활 보호 창에서 읽기 페이지를 열고, 문서 요청 줄에 id=만 있는지 봅니다. 읽기 페이지를 색인되는 콘텐츠 페이지처럼 퍼뜨리지 마세요. 한 번 쓰는 암호문 장면입니다. 링크 정리는 앞글의 대조표대로 UTM과 클릭 ID를 지우고, 역시 이 기기에서 끝냅니다.
한 바퀴를 돌면 이미 이 글의 질문에 답할 수 있습니다. 브라우저 암호화가 진짜인지는 문구가 아니라, 이번 트래픽에 평문·암호·우물정자 뒤의 키가 있었는지입니다. 알고리즘이 AES-256-GCM이고, 계산이 Web Crypto이며, 도구가 가입 없이 열린다는 것도 나란히 확인할 수 있는 제약입니다. 가입해야 들을 수 있는 약속이 아닙니다.
자주 묻는 질문
파일 암호화는 오프라인에서 되고, 일회용 링크는 안 되는 이유는?
파일 암호화의 결과는 이 기기 다운로드입니다. 일회용 링크는 받는 사람이 나중에 암호문을 꺼내야 하므로, 만들 때 암호문을 POST합니다. 둘 다 브라우저에서 AES-256-GCM을 끝낼 수 있습니다. 나가도 되는 내용의 자격이 다릅니다. 전자에는 파일 본문이 없어야 하고, 후자는 암호문은 되고 평문과 키는 안 됩니다.
우물정자 뒤의 키를 서버가 정말 못 받나요?
일반적인 URI·HTTP 구현에서는 문서 요청과 Referer에 fragment가 실리지 않습니다. Network의 그 문서 요청에서 그 자리에서 볼 수 있습니다. 스크립트가 location.hash를 읽어 올리면 페이지 자신의 동작이므로, Fetch 목록에서도 찾습니다.
키를 쿼리 파라미터에 넣는 편이 편하지 않나요?
편하고, 서버 로그에 들어갑니다. ?id=로 암호문을 찾는 것은 됩니다. ?key=는 복호화 능력을 호스트와 경로상의 로그에 넘기는 일입니다. 받는 사람은 풀 수 있고 호스트는 못 풀어야 할 때, 키는 # 뒤에 둡니다.
이 확인에 가입이 필요한가요? 원문을 분석 스크립트가 가져가나요?
가입은 필요 없습니다. 분석이 페이지 종류나 hash를 버린 경로만 적으면 입력칸 내용은 가져가지 않습니다. 전체 href를 올리면 우물정자 뒤의 키가 통계에 들어갈 수 있습니다. 「프라이버시를 중시한다」를 읽는 것보다, Network에서 분석 요청의 쿼리를 보는 편이 직접적입니다.