콘텐츠로 이동

PDF를 암호화하고 권한 제한하기

이 레시피는 AES-256 표준 보안 핸들러로 문서를 암호화합니다. 사용자 비밀번호(열 때 필요)와 소유자 비밀번호(전체 접근 권한)를 설정하고, 권한 비트마스크로 작업을 제한합니다. 이 레시피는 해당 권한의 리더 협조형 특성을 분명히 합니다. 즉, 암호화는 기밀성만 제공하고 무결성은 제공하지 않으며, 권한 비트는 협조하는 소프트웨어에서만 준수됩니다. 이 레시피는 examples/22-protection.php를 따릅니다.

신뢰 경계(모든 권한 주장과 함께 기억하십시오). PDF 암호화는 비밀번호가 없는 당사자로부터 콘텐츠의 기밀성을 보호합니다 (ISO 32000-2 §7.6). 암호화는 결코 무결성을 보호하지 않습니다. 즉 수정을 감지하거나 방지하지 않습니다. 권한 P 항목은 부호 없는 32 비트 플래그 집합이며, 준수하는 리더가 따르도록 요청받는 값입니다. 이는 접근 제어가 아닙니다. 준수하지 않는 도구나 소유자 비밀번호를 사용한 모든 도구는 모든 “거부된” 작업을 수행할 수 있습니다. 암호화된 PDF를 “안전함”, “변조 불가”, “복사 방지됨”이라고 설명하지 마십시오.

Terminal window
composer require nextpdf/core:^3

PHP 확장 openssl을 활성화하십시오. AES-256 암호화기는 암호화와 키 파생에 이를 사용합니다.

표준 보안 핸들러는 암호화 딕셔너리 V/R 코드를 통해 선택됩니다(ISO 32000-2 §7.6). NextPDF의 Aes256Encryptor는 보안 핸들러 리비전 6(V=5/R=6)에서 AESV3 암호 필터를 구현합니다. 즉, 무작위 256 비트 파일 암호화 키, 솔트 적용 반복 해시 키 파생(알고리즘 2.B), 그리고 무작위 초기화 벡터를 사용하는 객체별 AES-256-CBC 암호화입니다. CBC는 기밀성 모드입니다(NIST SP 800-38A). 해당 IV는 예측할 수 없어야 합니다.

IV는 객체별로, 실행별로 새로 생성되므로 원시 바이트는 실행마다 다릅니다. 따라서 재현성 프로파일은 structural입니다. 두 실행을 비교하기 전에 하니스는 암호화 IV, 객체 순서, 트레일러 /ID를 정규화합니다. 이 프로파일은 암호화를 생략한 레시피의 프로파일보다 더 엄격합니다.

권한 비트마스크는 P 항목을 설정합니다. 비트 3은 인쇄를 허용하고, 비트 6은 annotation/form-fill을 허용합니다. 값은 문서화된 부호 없는 32 비트 수량입니다.

NextPDF\Core\Concerns\HasSecurity(Document에 믹스인됨):

  • setEncryption(#[SensitiveParameter] string $userPassword, #[SensitiveParameter] string $ownerPassword = '', int $permissions = -1): static — AES-256 표준 핸들러 암호화를 구성합니다. permissions = -1은 모두 허용합니다. ownerPassword가 비어 있으면 사용자 비밀번호가 소유자 비밀번호로 재사용됩니다. addPage() 전에 호출하십시오.
  • getEncryptor(): ?Aes256Encryptor — 구성된 암호화기 또는 null을 반환합니다.
  • useAesGcm(?bool $enabled = true): static — ISO/TS 32003 AES-256-GCM을 선택합니다. 호스트 OpenSSL/libsodium에 해당 암호가 없으면 예외를 던집니다.

두 비밀번호 매개변수 모두 #[SensitiveParameter]로 표시되므로, PHP는 스택 트레이스에서 이를 가립니다.

권한 비트(P 항목, 일반적으로 사용되는 하위 비트 3–6):

비트작업
34문서 인쇄
48문서 내용 수정
516텍스트 및 그래픽 복사 / 추출
632주석 추가 또는 수정 및 양식 필드 채우기
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;
$doc = Document::createStandalone();
$doc->setTitle('Confidential Memo');
// Grant printing only (bit 3 = 4). MUST run before addPage().
$doc->setEncryption(
userPassword: 'open-me',
ownerPassword: 'owner-secret',
permissions: 4,
);
$doc->addPage();
$doc->setFont('helvetica', '', 12);
$doc->cell(0, 10, 'Encrypted with AES-256; printing allowed only.', newLine: true);
$doc->save(__DIR__ . '/confidential.pdf');
echo "Wrote confidential.pdf\n";

아래 전체 예제는 examples/22-protection.php를 반영하며, 하니스에서 사용하도록 NEXTPDF_COOKBOOK_OUTPUT에 기록합니다.

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;
$userPassword = 'demo';
$ownerPassword = 'admin';
// Grant ONLY printing (bit 3 = 4); deny copy/modify/annotate.
$permissions = 4;
$doc = Document::createStandalone();
$doc->setTitle('Encrypted Document — Restricted Permissions');
$doc->setAuthor('NextPDF Example');
// setEncryption() MUST be called before addPage().
$doc->setEncryption(
userPassword: $userPassword,
ownerPassword: $ownerPassword,
permissions: $permissions,
);
$doc->addPage();
$doc->setFont('helvetica', 'B', 20);
$doc->cell(0, 14, 'Encrypted PDF Document', newLine: true);
$doc->ln(8);
$doc->setFont('helvetica', '', 11);
$doc->multiCell(0, 7, 'This document is protected with AES-256 encryption '
. '(standard security handler, revision 6). The user password is required '
. 'to open it; the owner password grants full access. The permission '
. 'bits below are honoured by conforming readers only.');
$doc->ln(5);
$permissionTable = [
['Bit 3 (4)', 'Printing', 'ALLOWED'],
['Bit 4 (8)', 'Content modification', 'DENIED'],
['Bit 5 (16)', 'Text copying / extraction', 'DENIED'],
['Bit 6 (32)', 'Annotations / form fields', 'DENIED'],
];
$doc->setFont('helvetica', 'B', 10);
$doc->cell(30, 7, 'Flag');
$doc->cell(60, 7, 'Operation');
$doc->cell(0, 7, 'Status', newLine: true);
foreach ($permissionTable as [$bit, $operation, $status]) {
$doc->setFont('courier', '', 9);
$doc->cell(30, 7, $bit);
$doc->setFont('helvetica', '', 10);
$doc->cell(60, 7, $operation);
$doc->setFont('helvetica', 'B', 10);
$doc->cell(0, 7, $status, newLine: true);
}
$out = getenv('NEXTPDF_COOKBOOK_OUTPUT');
$doc->save($out !== false ? $out : __DIR__ . '/encrypted.pdf');
echo "Wrote encrypted PDF (AES-256, printing only)\n";

예상 출력:

Wrote encrypted PDF (AES-256, printing only)

파일을 열면 비밀번호를 묻습니다. 사용자 비밀번호로는 제한된 권한 집합으로 파일을 엽니다. 소유자 비밀번호로는 전체 접근 권한으로 파일을 엽니다.

  • 호출 순서. setEncryption()addPage() 이후에 호출하면 이전 콘텐츠를 소급하여 암호화하지 않습니다. 항상 암호화를 먼저 구성하십시오. 엔진은 각 객체 본문을 기록할 때 암호화합니다.
  • 소유자 비밀번호 기본값. 소유자 비밀번호가 비어 있으면 엔진이 사용자 비밀번호를 소유자 비밀번호로 재사용하므로, 권한이 구분되는 역할이 사실상 없어집니다. 두 역할이 달라야 할 때는 서로 다른 비밀번호를 설정하십시오.
  • 권한 의미론은 권고적입니다. 비트는 준수하는 리더에서만 따릅니다. 비트는 암호학적으로 강제되지 않습니다. 즉, 준수하지 않는 도구나 소유자 비밀번호를 사용한 모든 도구는 제한된 작업을 수행할 수 있습니다. 권한은 협조하는 소프트웨어에 대한 정책 신호로 취급하고, 의도적으로 우회하려는 당사자까지 막아 내는 접근 제어로는 결코 취급하지 마십시오.
  • 무결성 보장 없음. 암호화는 기밀성을 제공하지만 무결성은 제공하지 않습니다. 비밀번호가 없는 공격자는 콘텐츠를 읽을 수 없지만, 형식 자체는 변조를 감지하지 않습니다. 무결성 보호를 위해서는 별도의 메커니즘(디지털 서명 또는 ISO/TS 32004 문서 MAC)이 필요합니다.
  • PDF/A 충돌. PDF/A는 Encrypt 트레일러 키를 금지합니다. PDF/A 문서에서 setEncryption()을 어느 순서로든 호출하면 비호환성 예외를 던집니다.
  • AES-256-GCM 옵트인. useAesGcm()은 호스트 OpenSSL 또는 libsodium이 제공하는 경우 ISO/TS 32003 GCM 대량 암호화를 선택합니다. 그렇지 않으면 InvalidConfigException을 던집니다. 같은 이유로 PDF/A와 호환되지 않습니다.
  • 공개 키 암호화는 아직 연결되지 않았습니다. setPublicKeyEncryption()은 API 표면을 고정하지만, 라이터 연결이 완료될 때까지 save()는 예외를 던집니다(알려진 결함). 코어에서 프로덕션에 사용하지 마십시오.

키 파생은 문서당 한 번 알고리즘 2.B의 반복 해시를 실행합니다. 객체별 AES-256-CBC는 객체 본문 크기에 선형적으로 비례합니다. 일반적인 문서의 경우 비용은 1500 ms / 64 MB 예산 내에 충분히 머무릅니다. 매우 큰 문서는 객체별 AES 처리량 비용이 발생합니다. AES-NI를 사용하는 GCM은 지원되는 호스트에서 더 빠릅니다.

  • 기밀성만 제공. 신뢰 경계를 다시 말하면, 암호화는 비밀번호가 없는 당사자로부터 콘텐츠를 보호하지만 파일이 변경되지 않았음을 증명하지는 않으며, 권한 비트는 리더 협조형입니다.
  • 비밀번호 강도는 사용자 책임입니다. 핸들러는 비밀번호만큼만 강력합니다. 약한 사용자 비밀번호는 파일이 입수되면 오프라인에서 무차별 대입이 가능하며, 형식은 시도를 속도 제한할 수 없습니다.
  • 소유자 비밀번호는 마스터 키입니다. 소유자 비밀번호를 가진 사람은 누구나 모든 제한을 우회합니다. 이를 루트 자격 증명처럼 취급하십시오. 문서와 함께 절대 배포하거나 로깅하지 마십시오.
  • #[SensitiveParameter]는 심층 방어입니다. 이 속성은 PHP 스택 트레이스에서 비밀번호를 가리지만, 여전히 자신의 로그, 예외 메시지, 크래시 리포트에서 비밀번호를 제외해야 합니다.

라이브러리는 프로세스 내부에서 암호화를 수행합니다. 문서나 비밀번호를 어디에도 전송하지 않습니다. 저장 대상인 암호화된 출력 외에는 엔진이 비밀번호, 키, 문서 바이트를 디스크에 기록하지 않습니다. 출력 파일이 어디에 상주하는지와 비밀번호 관리는 통합자가 책임져야 하는 배포 사안입니다. 라이브러리는 어떠한 데이터 상주 보장도 하지 않습니다. 평문 문서에 개인 데이터가 포함된 경우, 해당 데이터는 가장 약한 비밀번호와 위의 협조 리더 주의 사항만큼만 보호됩니다. 암호화는 문서에 넣는 PII를 최소화하는 것의 대체물이 아닙니다.

안전한 텔레메트리 & 로그 스크러빙

섹션 제목: “안전한 텔레메트리 & 로그 스크러빙”

암호화는 EncryptionAppliedEvent를 발생시키며, 이 이벤트는 알고리즘 이름(AES-256)과 print/copy/modify 허용 여부를 요약하는 세 개의 불리언 값만 담습니다. 비밀번호, 키, 솔트, IV는 이벤트에 절대 포함되지 않습니다(src/Event/Security/EncryptionAppliedEvent.php). OpenTelemetry 경로는 스팬 속성을 허용 목록 새니타이저(src/Telemetry/AttributeSanitizer.php)를 통해 라우팅하며, 이 새니타이저는 비밀번호와 파일 경로를 무조건 거부합니다. 허용 목록에 있는 키 중 스칼라 값을 가진 키만 남습니다. 자신의 통합 코드에서 스팬, 로그, 예외 메시지에 비밀번호나 키 자료를 추가하지 마십시오. #[SensitiveParameter] 표시는 스택 트레이스는 보호하지만, 직접 구성한 문자열까지 보호하지는 않습니다.

범위 내: 암호화된 파일은 입수했지만 비밀번호는 입수하지 못한 공격자. 이들은 콘텐츠를 읽을 수 없으며(비밀번호 강도에 따라), 파일은 평문을 누출하지 않습니다. 범위 밖: 사용자 또는 소유자 비밀번호를 가진 공격자, 권한 비트를 무시하는 비준수 리더, 약한 비밀번호의 오프라인 무차별 대입, 변조 감지(암호화는 무결성이 아니라 기밀성을 제공함), 호스트 OpenSSL 빌드의 부채널, 그리고 전적으로 통합자의 책임인 키 관리. 이러한 위협을 문서화한다고 해서 취약점이 없음을 주장하는 것은 아닙니다.

암호 프리미티브는 호스트 OpenSSL 빌드가 제공하므로, FIPS 상태는 라이브러리 설정이 아니라 호스트 속성입니다. CryptoCapabilities::detectFipsMode()는 세 가지 상태의 FipsModeDetection(src/Security/FipsModeDetection.php)를 반환합니다. 즉, FIPS_ACTIVE, FIPS_ABSENT 또는 INDETERMINATE입니다. PHP openssl 확장은 OpenSSL 3 공급자 모델에 대한 바인딩을 노출하지 않으므로 탐지는 최선 노력 방식입니다. INDETERMINATE는 “FIPS 미입증”(페일 클로즈드)으로 취급되며, 운영자가 조치할 수 있도록 텔레메트리에서 구별됩니다. NextPDF는 FIPS 140 검증을 주장하지 않습니다. FIPS 검증된 OpenSSL에서 실행하는 것은 운영자의 책임이며, 탐지 결과는 권고적입니다.

진술사양조항reference_id (참조 ID)
암호화 딕셔너리 V 코드는 암호화 알고리즘을 선택합니다.ISO 32000-2§7.6
AESV3 암호 필터 방법은 CFM 항목으로 명명됩니다.ISO 32000-2§7.6
권한 P 항목은 부호 없는 32 비트 접근 권한 수량입니다.ISO 32000-2§7.6
권한 비트 3은 인쇄를 제어합니다.ISO 32000-2§7.6
권한 비트 6은 주석 / 양식 채우기를 제어합니다.ISO 32000-2§7.6
암호화는 무단 접근으로부터 콘텐츠를 보호합니다(기밀성).ISO 32000-2§7.6
리비전 6 키 파생은 솔트 적용 반복 해싱을 사용합니다(알고리즘 2.B).ISO 32000-2§7.6
CBC는 기밀성 모드입니다(무결성 모드가 아님).NIST SP 800-38A§6.2
CBC 초기화 벡터는 예측할 수 없어야 합니다.NIST SP 800-38A부록 C

NextPDF는 인용된 조항을 구현합니다. 이는 포괄적인 ISO 32000-2 적합성, FIPS 140 검증, 또는 어떠한 법적 또는 계약상 기밀성 보장도 주장하지 않습니다. “표준 보안 핸들러 지원”은 해당 배포 환경의 보안 인증이 아닙니다. 이는 라이브러리 외부의 비밀번호 관리 및 검증자 정책에 달려 있습니다.