PDF 암호화: 권한 암호만 걸면 그냥 열리는 이유
권한 암호가 걸린 PDF 를 qpdf 로 들여다보면 이런 모양이 나옵니다.
$ qpdf --show-encryption report.pdf
R = 4
User password = ""
modify document: not allowed
extract for accessibility: not allowed암호화된 파일이라고 나오는데 사용자 암호가 빈 문자열입니다. 이 파일은 어떤 뷰어에서도 암호를 묻지 않고 열립니다. 인쇄와 복사를 막아 두었을 뿐입니다.
암호가 하나가 아니라 둘
PDF 표준 보안 처리기(standard security handler)는 암호를 두 개 받습니다. ISO 32000 이 정의한 구분이고, 이름이 비슷해서 자주 뭉뚱그려집니다.
| 사용자 암호(user password) | 소유자 암호(owner password) | |
|---|---|---|
| 다른 이름 | 열기 암호, open password | 권한 암호, permissions password |
| 없으면 | 파일을 열 수 없습니다 | 열리되 권한이 제한됩니다 |
| 강제하는 주체 | 암호 알고리즘 | 뷰어의 자발적 준수 |
여기서 갈립니다. 사용자 암호는 복호화 키를 만드는 재료입니다. 소유자 암호는 그렇지 않습니다.
빈 사용자 암호가 만드는 키
소유자 암호만 걸면 사용자 암호는 빈 문자열이 됩니다. 그런데 파일은 여전히 암호화되어 있습니다. 무엇으로 암호화한 걸까요.
빈 문자열입니다. 표준 보안 처리기는 사용자 암호에서 파일 암호화 키를 유도하는데, 사용자 암호가 비어 있으면 그 빈 값으로 키를 만듭니다. 키를 만드는 재료가 파일 안에 다 들어 있으므로 어떤 뷰어든 열 수 있습니다.
그래서 qpdf --decrypt 한 줄이면 암호화가 사라집니다.
$ qpdf --decrypt report.pdf plain.pdf이건 취약점을 찌르는 것이 아닙니다. 명세가 그렇게 동작하도록 정해 두었습니다. 권한 제한은 뷰어가 지켜 주기로 한 약속이지 암호가 강제하는 것이 아닙니다.
저는 이 구분이 실무에서 가장 자주 무너진다고 봅니다. 「PDF 에 암호를 걸었다」는 말이 두 가지 전혀 다른 상태를 가리키는데, 말하는 쪽과 듣는 쪽이 다른 쪽을 떠올립니다.
암호화되는 것과 남는 것
사용자 암호를 제대로 걸어도 파일 전체가 하나의 덩어리로 암호화되는 것은 아닙니다.
PDF 는 객체들의 그래프입니다. 표준 암호화는 그중 문자열과 스트림만 암호화합니다. 객체 번호, 참조 관계, 페이지가 몇 장인지 같은 구조는 그대로 남습니다. 파일을 열 수 없는 사람도 문서가 몇 페이지짜리인지, 어떤 폰트를 쓰는지, 첨부 파일이 몇 개 붙어 있는지는 알 수 있습니다.
메타데이터는 따로 정해집니다. 암호화 사전의 EncryptMetadata 가 false 면 XMP 메타데이터가 평문으로 남습니다. 검색 색인이 제목과 작성자를 읽을 수 있게 하려고 일부러 그렇게 두는 경우가 있습니다. 제목에 상대방 회사 이름이 들어가는 문서라면 그 선택이 그대로 유출 경로가 됩니다.
리비전 번호가 곧 강도
암호화 사전의 R 값이 어떤 알고리즘을 쓰는지 말해 줍니다. 위 예시의 R = 4 는 2000년대 초 명세입니다.
| R | 알고리즘 | 비고 |
|---|---|---|
| 2 | RC4 40비트 | 오늘 기준으로는 없는 것과 같습니다 |
| 3 | RC4 최대 128비트 | |
| 4 | RC4 또는 AES-128 | 암호 필터로 갈립니다 |
| 5 | AES-256 (Adobe 확장) | 키 유도가 약해 폐기되었습니다 |
| 6 | AES-256 | PDF 2.0, ISO 32000-2 |
R5 는 짚어 둘 만합니다. AES-256 을 쓰는데도 폐기된 이유는 암호 알고리즘이 아니라 암호에서 키를 만드는 과정에 있었습니다. R6 은 그 유도 과정을 강화한 판이고, ISO 32000-2 가 채택한 쪽도 R6 입니다.
키 길이만 보고 안전하다고 판단하면 놓치는 지점이 여기입니다. AES-256 이라는 표기는 R5 와 R6 에 똑같이 붙습니다.
암호화와 서명이 갈리는 지점
둘은 같은 서랍에 들어가기 쉽습니다. 묻는 것이 다릅니다.
암호화는 「이 내용을 누가 읽을 수 있는가」에 답합니다. 서명은 「이 내용이 그 뒤로 바뀌지 않았는가」에 답합니다. 표준 PDF 암호화는 무결성을 보장하지 않습니다. 암호화된 PDF 의 스트림을 조작해도 그것을 탐지할 수단이 명세 안에 없습니다.
반대 방향도 마찬가지입니다. 서명된 PDF 는 누구나 읽을 수 있습니다. 서명은 내용을 감추는 장치가 아닙니다.
문서에 암호를 걸기 전에
암호화를 문서 자체에 거는 선택은 대가가 큽니다. 값을 매기기 전에 따져 볼 것이 몇 가지 있습니다.
암호를 어떻게 전달할지가 먼저입니다. 문서와 같은 경로로 보내면 아무 의미가 없고, 다른 경로로 보내려면 그 경로를 운영해야 합니다. 이건 기술 문제라기보다 절차 문제이고, 대개 여기서 실제 보안 수준이 정해집니다.
브라우저 내장 뷰어와 미리보기가 막힙니다. 웹에서 문서를 보여 주는 화면이라면 암호를 건 순간 사용자는 파일을 내려받아 별도 프로그램으로 열어야 합니다. 모바일에서는 그 경로가 더 험합니다.
검색과 색인도 함께 막힙니다. 사내 문서 검색이 내용을 읽어야 한다면 암호화된 파일은 그 대상에서 빠집니다.
저라면 대부분의 경우 문서 자체가 아니라 저장소와 전송 구간을 암호화하는 쪽을 먼저 봅니다. 파일이 놓이는 자리를 암호화하면 위의 세 가지 대가를 치르지 않고 「누가 읽을 수 있는가」의 상당 부분이 해결됩니다. 문서 암호화는 그것으로 못 막는 경로가 남아 있을 때, 그 경로를 이름 붙일 수 있을 때 얹는 것이 맞다고 봅니다.
뷰어에 달린 부분
권한 플래그를 뷰어가 얼마나 지키는지는 명세가 강제하지 않습니다. 인쇄 금지를 지키는 뷰어도 있고 무시하는 뷰어도 있으며, 같은 뷰어라도 판이 바뀌면서 동작이 달라지기도 합니다.
그래서 권한 제한은 실수를 줄이는 장치로는 쓸모가 있지만 막는 장치로는 쓸 수 없습니다. 받는 사람이 실수로 편집하는 것을 줄이고 싶다면 켤 만합니다. 받는 사람이 작정하고 편집하는 것을 막고 싶다면 다른 수단이 필요합니다.
여기서부터는 저도 단정하지 않겠습니다. 뷰어별 동작은 제품과 판마다 갈리고, 그 차이를 일반화해서 적어 두면 그 문장이 곧 틀립니다. 특정 뷰어에서 무엇이 막히는지가 중요한 상황이라면 그 뷰어로 직접 확인하는 편이 빠릅니다.
함께 읽기
- Next.js 보안 헤더와 색인 차단: 새 프로젝트마다 같은 설정을 두는 이유Vercel 로 옮기고 나서 응답 헤더를 다시 봤더니 보안 헤더가 하나도 없었습니다. 예전에는 오리진 앞의 nginx 가 붙여 주고 있었는데, 그 nginx 가 경로에서 빠지면서 전 경로에서 같이 사라진 것입니다. 앱은 그대로였고 오류도 없었습니다. 헤더는 없어져도 화면이 멀쩡해서, 재보기 전에는 모릅니다.
- 전자서명의 효력과 증거력: 유효한 것과 증명되는 것의 차이전자서명 도입을 검토하면 거의 항상 같은 질문이 먼저 나옵니다. "이거 법적으로 효력 있나요?"
- 전자서명 구현: 손글씨보다 문서 동결이 먼저인 이유전자서명 서비스를 처음 보면 가장 눈에 띄는 것은 화면에 손가락이나 마우스로 사인을 그리는 부분입니다. 계약이 그 그림으로 성립하는 것처럼 보입니다.
- API 키는 왜 다시 볼 수 없을까: 발급·검증·권한이 갈리는 자리GitHub 에서 토큰을 만들면 화면에 한 번 뜨고 다시는 볼 수 없습니다. Stripe 도 AWS 도 같습니다. 복사를 놓치면 방법이 없고, 새로 만들라는 안내만 나옵니다.
- 개인 액세스 토큰의 한계: 토큰은 권한이 아니라 신원- run: eas build --platform all --non-interactive