고급 암호화
이 페이지는 TCPDF-Next Pro의 내부 암호화 구현을 문서화합니다. AES-256 AESV3 핸들러, 키 도출 알고리즘, 비밀번호 정규화, 안전한 파라미터 처리를 다룹니다. 기본 암호화 사용법을 찾고 있다면 AES-256 암호화 예제를 참조하세요.
AESV3 핸들러를 사용한 AES-256
TCPDF-Next Pro는 모든 스트림 및 문자열 암호화에 AES-256-CBC를 의무화하는 ISO 32000-2 (PDF 2.0) 표준 보안 핸들러 리비전 6을 구현합니다. 핸들러는 암호화 딕셔너리에서 /V 5 및 /R 6으로 식별됩니다.
use Yeeefang\TcpdfNext\Pro\Security\Aes256Encryptor;
$encryptor = new Aes256Encryptor(
ownerPassword: 'Str0ng!OwnerP@ss',
userPassword: 'reader2026',
);RC4 또는 AES-128을 사용하지 않는 이유
TCPDF-Next Pro는 RC4(40비트 및 128비트)와 AES-128을 의도적으로 제외합니다:
| 알고리즘 | 제외 이유 |
|---|---|
| RC4-40 | 1995년 이후 깨짐; 간단히 공격 가능 |
| RC4-128 | 키스트림의 편향; PDF 2.0에서 금지 |
| AES-128 | 리비전 6에서 AES-256으로 대체; 전방 호환성 없음 |
PDF 2.0 (ISO 32000-2:2020)은 새 문서에 AESV3를 필수로 요구합니다. 더 약한 알고리즘을 지원하면 보안 상태가 저하되고 사양을 위반하게 됩니다.
알고리즘 2.B: 키 도출
파일 암호화 키는 알고리즘 2.B(ISO 32000-2, 조항 7.6.4.3.4)를 사용하여 비밀번호에서 도출됩니다. 이는 SHA-256, SHA-384, SHA-512를 연결하는 반복적 프로세스입니다:
function computeHash(password, salt, userKey = ''):
K = SHA-256(password || salt || userKey)
round = 0
lastByte = 0
while round < 64 OR lastByte > round - 32:
K1 = (password || K || userKey) repeated 64 times
E = AES-128-CBC(key = K[0..15], iv = K[16..31], data = K1)
mod3 = (sum of all bytes in E) mod 3
if mod3 == 0: K = SHA-256(E)
elif mod3 == 1: K = SHA-384(E)
else: K = SHA-512(E)
lastByte = E[len(E) - 1]
round += 1
return K[0..31] // 32바이트 파일 암호화 키이 반복적 해싱은 무차별 대입 공격을 계산적으로 비용이 많이 들게 하면서도 정당한 사용에는 충분히 빠릅니다.
암호화 딕셔너리의 키 구성 요소
| 항목 | 길이 | 목적 |
|---|---|---|
/O | 48바이트 | 소유자 비밀번호 검증 (해시 + 검증 솔트) |
/U | 48바이트 | 사용자 비밀번호 검증 (해시 + 검증 솔트) |
/OE | 32바이트 | 소유자 암호화된 파일 암호화 키 |
/UE | 32바이트 | 사용자 암호화된 파일 암호화 키 |
/Perms | 16바이트 | AES-256 암호화된 권한 플래그 |
SASLprep 비밀번호 정규화
모든 암호화 작업 전에 비밀번호는 SASLprep(RFC 4013)을 사용하여 정규화됩니다. 이는 stringprep(RFC 3454)의 프로파일입니다. 운영 체제나 입력 방법이 사용하는 유니코드 정규화 형식에 관계없이 일관된 비밀번호 처리를 보장합니다.
use Yeeefang\TcpdfNext\Pro\Security\SaslPrep;
$normalized = SaslPrep::prepare('P\u{00E4}ssw\u{00F6}rd');
// 조합/분해 형식을 정규화하고, 특정 문자를 매핑하며,
// 금지된 코드포인트를 거부합니다.SASLprep의 동작
- 매핑 -- 일반적으로 nothing으로 매핑되는 문자(예: 소프트 하이픈
U+00AD)가 제거됩니다. - 정규화 -- 문자열이 유니코드 NFC(정규 분해 후 정규 조합)로 변환됩니다.
- 금지 -- RFC 3454 테이블 C.1.2부터 C.9까지의 문자가 거부됩니다(제어 문자, 사설 사용, 서로게이트, 비문자 등).
- 양방향 검사 -- 왼쪽에서 오른쪽 및 오른쪽에서 왼쪽 문자가 모두 포함된 문자열이 RFC 3454 조항 6에 따라 검증됩니다.
이는 사용자가 U+00FC(LATIN SMALL LETTER U WITH DIAERESIS)를 입력하든 U+0075 U+0308(LATIN SMALL LETTER U + COMBINING DIAERESIS)을 입력하든 동일한 암호화 키가 생성됨을 의미합니다.
권한 인코딩
권한은 /Perms 항목에 16바이트 AES-256-ECB 암호화 블록으로 저장됩니다. 평문 레이아웃:
Bytes 0-3: 권한 플래그 (리틀 엔디안 int32)
Bytes 4-7: 0xFFFFFFFF
Byte 8: EncryptMetadata가 true이면 'T', 아니면 'F'
Bytes 9-11: 'adb'
Bytes 12-15: 랜덤 패딩권한 플래그는 ISO 32000-2 테이블 22에 정의된 것과 동일한 비트 레이아웃을 따릅니다.
암호화된 스트림 처리
PDF의 모든 콘텐츠 스트림과 문자열은 개별적으로 암호화됩니다:
random_bytes(16)을 사용하여 스트림/문자열당 고유한 16바이트 초기화 벡터(IV)가 생성됩니다.- IV가 암호문 앞에 추가됩니다.
- 암호화 전에 PKCS#7 패딩이 적용됩니다.
- 모든 스트림 디코드 파라미터에
/AESV3가 포함된/Crypt필터가 설정됩니다.
// 내부 -- 라이터에 의해 자동으로 처리됨
$iv = random_bytes(16);
$padded = pkcs7_pad($plaintext, blockSize: 16);
$encrypted = openssl_encrypt($padded, 'aes-256-cbc', $fileKey, OPENSSL_RAW_DATA, $iv);
$output = $iv . $encrypted;WARNING
TCPDF-Next Pro는 항상 스트림 데이터를 암호화합니다. /EncryptMetadata 플래그는 기본적으로 true입니다. false로 설정하면 XMP 메타데이터 스트림은 암호화되지 않은 상태로 유지되지만(검색 인덱싱에 유용), 다른 모든 스트림은 여전히 암호화됩니다.
민감한 파라미터 처리
비밀번호를 받는 모든 메서드는 PHP 8.2의 #[\SensitiveParameter] 속성으로 주석이 달려 있습니다. 이는 비밀번호가 스택 트레이스, 디버그 출력, 오류 로그에 나타나는 것을 방지합니다:
public function setOwnerPassword(
#[\SensitiveParameter] string $password,
): self {
$this->ownerPassword = SaslPrep::prepare($password);
return $this;
}예외가 발생하면 스택 트레이스에 실제 비밀번호 문자열 대신 Object(SensitiveParameterValue)가 표시됩니다.
전체 예제
use Yeeefang\TcpdfNext\Core\Document;
use Yeeefang\TcpdfNext\Pro\Security\Aes256Encryptor;
use Yeeefang\TcpdfNext\Pro\Security\Permissions;
$pdf = Document::create()
->setTitle('Confidential Report')
->addPage()
->setFont('Helvetica', size: 12)
->multiCell(0, 6, 'This document is protected with AES-256 encryption.');
$encryptor = new Aes256Encryptor(
ownerPassword: 'Str0ng!OwnerP@ss',
userPassword: 'reader2026',
permissions: new Permissions(
print: true,
printHighQuality: false,
copy: false,
modify: false,
annotate: true,
fillForms: true,
extractForAccess: true,
assemble: false,
),
);
$pdf->encrypt($encryptor)
->save(__DIR__ . '/encrypted.pdf');