같은 한글 암호인데, PDF 뷰어마다 결과가 달랐다

같은 PDF에 같은 암호를 입력했다. Chrome에서는 정상적으로 열렸지만 PDFBox에서는 암호가 틀렸다는 예외가 발생했다. Acrobat에서는 한글 암호를 붙여넣을 수도 없었다.

처음에는 파일이 손상됐거나 암호가 잘못 전달됐다고 생각했다. 하지만 조사 결과, 문제는 문자열을 처리하는 방식에 있었다.

PDF를 생성할 때와 읽을 때 암호 문자열이 서로 다른 바이트로 변환되고 있었다.

핵심 원인부터 살펴보자

이번에 확인한 PDFBox의 암호 불일치 원인은 세 단계로 정리할 수 있다.

  1. PDF 생성: hummus가 한글 암호를 UTF-8 바이트로 처리해 PDF를 생성했다.
  2. PDF 열람: PDFBox 2.0.37은 해당 PDF의 암호화 리비전 3에서 ISO-8859-1로 암호를 인코딩했다.
  3. 검증 실패: 생성과 검증에 사용된 바이트가 달라 올바른 문자열을 입력해도 암호가 일치하지 않았다.

예를 들어 café는 UTF-8에서 63 61 66 C3 A9, ISO-8859-1에서 63 61 66 E9로 표현된다.

사용자가 입력한 문자열은 같지만 암호화 알고리즘에 전달되는 바이트는 다르다.

영문과 숫자로만 시험했을 때 문제가 없었던 이유도 여기에 있다. 일반 ASCII 문자는 두 인코딩에서 같은 바이트를 사용하기 때문이다.

다만 여기서 구분해야 할 점이 있다.

PDFBox에서 암호 검증에 실패한 원인은 바이트 차이로 확인했다. 그러나 Chrome, Acrobat, PDFKit, Poppler가 각각 왜 다른 결과를 보였는지는 모두 밝혀진 상태가 아니다.

이번 글에서는 소스와 파일 분석으로 확인한 사실, 실험에서 관찰한 결과, 아직 확인하지 못한 부분을 나눠 살펴본다.

1. 실제로 어떤 차이가 있었을까?

문제는 한글 암호로 보호된 PDF에서 시작됐다.

문서 서비스 안에서는 정상적으로 열렸고, 사용자가 내려받은 PDF도 Chrome에서는 문제없었다. 그런데 서버에서 같은 파일을 PDFBox로 읽으려 하자 암호 불일치 예외가 발생했다.

원인을 좁히기 위해 실제 문서 변환 경로로 PDF를 생성했다. 영문·숫자, 한글, 악센트 문자, 한글과 영문을 섞은 암호를 여러 프로그램에서 시험했다.

실험에서 관찰한 결과

확인한 환경영문·숫자 암호비ASCII 문자 포함 암호
PDFBox 2.0.37열림암호 불일치 예외
Adobe Acrobat열림한글 붙여넣기 불가, café는 입력해도 열리지 않음
macOS PDFKit API열림unlock(withPassword:)가 false 반환
Google Chrome열림열림
Poppler열림열림

영문과 숫자로 구성된 암호는 모든 시험 환경에서 통과했다. 반면 한글이나 악센트 문자가 포함되자 결과가 갈렸다.

특히 Chrome과 Poppler에서는 파일이 정상적으로 열렸다. PDFBox와 PDFKit에서는 암호 검증에 실패했고, Acrobat에서는 입력이나 검증 단계에서 문제가 나타났다.

이 결과만으로 특정 프로그램에 버그가 있다고 단정할 수는 없다. 파일 생성 과정이 일반적인 처리 방식과 다를 가능성도 있기 때문이다.

또한 이 표는 당시 시험한 파일과 환경에 관한 기록이다. Acrobat과 Chrome의 정확한 버전은 남아 있지 않으며, macOS는 미리보기 앱이 아닌 PDFKit API로 확인했다.

확실한 사실은 하나였다.

동일한 PDF와 암호 문자열을 사용해도 프로그램에 따라 결과가 달랐다.

그 이유를 찾기 위해 파일 내부를 조사했다.

2. PDF 암호화 방식에서 발견한 단서

당시 PDF 생성에는 hummus를 사용하고 있었다.

생성된 PDF의 암호화 사전을 확인하니 다음 값이 나왔다.

항목값의미
R3표준 보안 처리 방식의 리비전
V2암호화 알고리즘 버전
Length128암호화 키 길이(비트)

이 조합은 RC4 128비트 암호화를 사용하는 구형 PDF 암호화 방식에 해당한다.

파일의 암호화 결과도 조사했다. 그 결과, 암호 문자열을 UTF-8로 변환한 바이트가 암호화 결과와 일치했다.

즉, 조사한 PDF에서는 UTF-8 바이트를 사용한 암호화가 확인됐다.

그렇다면 PDFBox는 어떻게 처리하고 있을까?

PDFBox가 사용하는 문자 인코딩

PDFBox 2.0.37의 StandardSecurityHandler 소스를 살펴봤다.

이 버전은 PDF 암호화 리비전에 따라 암호 문자열의 문자셋을 선택한다. 기본적으로 ISO-8859-1을 사용하고, 리비전 5와 6에서는 UTF-8을 사용하도록 분기한다.

PDFBox 2.0.37 StandardSecurityHandler 소스

문제가 된 PDF는 리비전 3이었다.

따라서 PDFBox는 전달받은 암호를 ISO-8859-1 바이트로 변환해 검증한다. 하지만 파일을 생성할 때는 UTF-8 바이트가 사용됐다.

여기서 불일치가 발생한다.

같은 문자열, 다른 바이트

차이를 이해하기 위해 abc와 café를 비교해 보자.

문자열UTF-8 바이트ISO-8859-1 바이트
abc61 62 6361 62 63
café63 61 66 C3 A963 61 66 E9

abc는 두 인코딩에서 결과가 같다.

하지만 café의 마지막 글자인 é는 다르다. UTF-8에서는 2바이트, ISO-8859-1에서는 1바이트로 표현된다.

한글은 차이가 더 크다.

UTF-8은 한글을 표현할 수 있지만 ISO-8859-1은 그렇지 못하다. Java의 String.getBytes(Charset)처럼 표현할 수 없는 문자를 대체 바이트로 바꾸는 API도 있다. 이 과정에서 반드시 예외가 발생하는 것은 아니다.

Java String.getBytes(Charset) 문서

따라서 암호를 생성할 때와 검증할 때 서로 다른 인코딩을 사용한다면, 사용자가 같은 문자열을 입력해도 검증에 실패할 수 있다.

이번 PDFBox 오류가 바로 그런 사례였다.

어디까지 확인됐을까?

조사 결과를 구분하면 다음과 같다.

파일과 소스로 확인한 사실

  • 문제가 된 PDF의 암호화 사전은 R=3, V=2, Length=128이었다.
  • 해당 PDF의 암호화 결과는 UTF-8로 변환한 암호 바이트와 일치했다.
  • PDFBox 2.0.37은 리비전 3의 암호를 ISO-8859-1로 인코딩한다.
  • 따라서 이 파일을 PDFBox에서 열 때 생성 시점과 검증 시점의 암호 바이트가 달라진다.

실험으로 관찰한 결과

  • PDFBox와 PDFKit API에서는 비ASCII 암호가 설정된 시험 파일을 열지 못했다.
  • Chrome과 Poppler에서는 같은 방식으로 만든 시험 파일을 열었다.
  • Acrobat에서는 한글 붙여넣기가 불가능했고 café 암호 검증에도 실패했다.

아직 확인하지 못한 부분

  • Chrome과 Poppler가 내부에서 어떤 문자 인코딩이나 호환성 처리를 사용했는지
  • Acrobat과 PDFKit의 실패가 PDFBox와 정확히 같은 내부 원인에서 발생했는지
  • 각 프로그램의 다른 버전에서도 동일한 결과가 나타나는지

이 구분이 중요하다.

PDFBox의 실패 원인을 설명할 수 있다고 해서 모든 뷰어의 동작까지 설명한 것은 아니기 때문이다.

3. 기존 PDF를 다시 열 수는 없을까?

원인을 확인한 뒤에는 이미 생성된 PDF를 처리할 방법을 검토했다.

파일을 전부 다시 암호화하기는 어려웠다. 사용자에게 배포된 PDF도 있었기 때문이다.

대신 PDFBox에 전달하는 암호를 변환하는 방법을 시험했다.

원리는 간단하다.

원래 UTF-8 바이트를 ISO-8859-1 문자열로 옮겨 담은 뒤 PDFBox에 전달한다.

예를 들어 café를 UTF-8로 인코딩하면 다음 바이트가 나온다.

1
63 61 66 C3 A9

이 바이트를 ISO-8859-1 문자열로 해석하면 café가 된다.

PDFBox에 이 문자열을 전달하면 내부에서 다시 ISO-8859-1로 인코딩한다.

결과는 처음의 UTF-8 바이트와 같다.

1
63 61 66 C3 A9

문자열은 달라졌지만 최종 바이트는 복원된 셈이다.

실험 결과

이 방식으로 한글, 악센트 문자, 혼합 암호가 설정된 시험 파일을 열 수 있었다. 첫 페이지 렌더링도 성공했다.

따라서 조사한 과거 PDF를 PDFBox로 처리하는 데 활용할 수 있다는 사실은 확인했다.

다만 범용 해결책은 아니다.

처음부터 ISO-8859-1 기반으로 생성한 PDF에 같은 변환을 적용하면 오히려 암호 검증에 실패할 수 있다.

안전한 방향은 기존 방식으로 먼저 암호를 확인하고, 불일치가 발생했을 때 알려진 과거 생성 경로에 한해 보완하는 것이다.

또한 이 방법은 서버가 PDFBox로 문서를 처리할 때만 도움이 된다.

이미 내려받은 PDF를 Acrobat에서 직접 여는 문제까지 해결하지는 못한다.

4. AES-256으로 바꾸면 해결될까?

문자 인코딩 문제를 우회하는 대신 암호화 방식을 변경하는 방법도 있다.

기존 파일은 RC4 128비트와 리비전 3을 사용했다. 이를 AES-256과 리비전 6으로 바꾸면 비ASCII 암호 처리 결과가 달라질 수 있다.

실제로 PDFBox로 AES-256·리비전 6 시험 파일을 만들었다.

실험 결과

확인한 환경비ASCII 암호 결과
PDFBox열림
Poppler열림
macOS PDFKit API열리지 않음
Acrobat미확인
Chrome미확인

PDFBox와 Poppler에서는 정상적으로 열렸다.

하지만 같은 조사 환경의 PDFKit API에서는 여전히 실패했다.

이 결과로 무엇을 알 수 있을까?

적어도 조사한 환경에서는 암호화 방식을 변경해도 모든 프로그램에서 동일한 결과를 얻지 못했다.

그렇다고 AES-256이나 리비전 6 자체에 호환성 문제가 있다고 결론 내릴 수는 없다. PDFKit이 실패한 구체적인 원인은 추가로 분석하지 않았고, Acrobat과 Chrome에서도 시험하지 못했다.

따라서 여기서 내릴 수 있는 판단은 제한적이다.

암호화 방식을 바꾸는 것만으로 모든 대상 PDF 뷰어에서 비ASCII 암호가 작동한다고 보장할 근거는 부족했다.

알고리즘 교체와 뷰어 호환성 검증은 별도로 다뤄야 했다.

5. 새 암호는 어디까지 허용해야 할까?

이번 조사에서 선택한 방향은 새로운 문서 암호의 문자 범위를 제한하는 것이었다.

허용 범위는 U+0020부터 U+007E까지, 공백을 포함한 출력 가능한 ASCII 문자다.

문자 종류허용 여부
영문 대소문자허용
숫자허용
기호허용
일반 공백허용
한글거절
악센트 문자 é거절
이모지거절
탭과 줄바꿈거절

이 범위의 문자는 UTF-8과 ISO-8859-1에서 같은 바이트로 표현된다.

따라서 이번에 확인한 인코딩 불일치를 피할 수 있다.

물론 ASCII만 허용한다고 모든 PDF 뷰어에서 정상적으로 열린다는 보장은 없다. 암호화 방식, 뷰어 버전, 구현에 따라 다른 문제가 발생할 수도 있다.

이 정책은 모든 PDF 환경을 검증한 결과가 아니라 확인된 호환성 위험을 줄이기 위한 선택이다.

또한 문자 호환성과 암호 보안성은 구분해야 한다.

한글이나 이모지를 허용하지 않는다고 암호가 더 안전해지지는 않는다. 암호 길이와 복잡도, 암호화 알고리즘의 안전성은 별도로 검토할 문제다.

기존 한글 암호는 어떻게 할까?

새 암호를 제한하는 일과 기존 문서를 계속 열 수 있게 하는 일은 다르다.

이미 한글 암호가 설정된 문서까지 새 문자 정책을 적용하면, 사용자는 올바른 기존 암호를 알고 있어도 문서를 열지 못할 수 있다.

그래서 암호를 새로 설정하는 과정과 기존 암호를 검증하는 과정을 구분했다.

새로 설정하는 암호에는 ASCII 문자 정책을 적용한다. 반면 기존 한글 암호로 서비스 안에서 문서를 열거나 서명하는 기능은 유지한다.

암호를 변경하지 않고 다른 설정만 수정할 때도 기존 값을 보존한다.

다만 기존 한글 암호를 완료 PDF에 새로 적용하려는 경우에는 별도 검사가 필요하다. 서비스 안에서 사용할 수 있는 암호라고 해서 외부 PDF 뷰어와도 호환된다는 보장은 없기 때문이다.

이미 배포된 PDF는 새 정책으로 바뀌지 않는다. 필요한 경우 과거 파일의 생성 방식에 맞춘 별도 처리가 필요하다.

이번 변경은 모든 기존 파일의 호환성을 해결한 작업이 아니다. 새로 만드는 문서에서 같은 문제가 반복될 가능성을 줄이고, 기존 문서의 열람 경로를 보존하는 설계다.

6. PDF 호환성을 어떻게 검증해야 할까?

이번 문제를 처음부터 피할 수는 없었을까?

돌아보면 시험 조건에 중요한 빈틈이 있었다.

영문과 숫자로만 암호를 시험했다면 인코딩 차이를 발견하기 어려웠을 것이다. ASCII 범위에서는 UTF-8과 ISO-8859-1의 바이트가 같기 때문이다.

PDF 생성에 사용한 라이브러리로 다시 열어보는 시험도 충분하지 않다.

실제 사용자는 Chrome이나 Acrobat을 사용할 수 있고, 서버에서는 PDFBox나 Poppler처럼 다른 라이브러리로 같은 파일을 처리할 수 있다.

결국 PDF 암호 호환성은 한 프로그램의 성공 여부가 아니라 생성과 열람의 조합으로 검증해야 한다.

확인해야 할 항목은 다음과 같다.

  1. 암호 문자 범위: 영문·숫자뿐 아니라 한글, 악센트 문자, 혼합 문자열을 시험한다.
  2. 암호화 방식: PDF의 암호화 리비전과 알고리즘, 문자열 인코딩 방식을 확인한다.
  3. 생성 프로그램: 실제 배포 경로에서 생성된 PDF를 사용한다.
  4. 열람 프로그램: Chrome, Acrobat, PDFKit, PDFBox, Poppler 등 지원 대상에서 확인한다.
  5. 기존 파일: 새로운 방식으로 생성한 PDF뿐 아니라 과거에 배포된 파일도 검증한다.

특히 암호 불일치 오류가 발생했다면 단순히 사용자의 입력을 의심하기보다 생성 시점과 검증 시점의 바이트부터 비교해 볼 필요가 있다.

이번 사례에서는 그 차이가 원인을 설명하는 결정적인 단서였다.

마치며

같은 한글 암호를 사용했는데도 PDF 뷰어마다 결과가 달랐다.

원인을 추적하면서 확인한 사실은 명확했다.

PDF 생성 과정에서는 UTF-8 바이트가 사용됐지만 PDFBox 2.0.37은 해당 암호화 리비전에서 ISO-8859-1로 암호를 처리했다.

문자열은 같았지만 바이트는 달랐다.

이 차이로 PDFBox의 암호 검증 실패를 설명할 수 있었고, 바이트를 복원하는 방식으로 기존 시험 파일을 다시 여는 데도 성공했다.

반면 Chrome과 Poppler가 왜 정상적으로 열었는지, Acrobat과 PDFKit이 정확히 어떤 내부 경로에서 실패했는지는 끝까지 확인하지 못했다.

관찰한 현상과 확인한 원인을 구분해야 하는 이유다.

이번 조사에서 얻은 결론은 단순하다.

같은 문자열이 반드시 같은 바이트를 뜻하지는 않는다.

PDF 암호 호환성은 암호화 알고리즘만으로 결정되지 않는다. 문자열이 어떤 바이트로 변환되는지, 그리고 파일을 읽는 프로그램이 그 바이트를 어떻게 처리하는지까지 맞아야 한다.

평소에는 드러나지 않던 차이가 한글 암호 하나로 드러난 사례였다.