iText - 仅加密嵌入式文件

iText - Encrypt Embedded Files Only

我想按照 PDF 32000-1:2008 的第 7.6.1 节所述使用 iText 创建一个带有加密嵌入文件的未加密 pdf 文件:

Beginning with PDF 1.5, embedded files can be encrypted in an otherwise unencrypted document

然而,以下示例 (iText 7.0.1) 生成了一个带有未加密嵌入文件流的 PDF 文件(关闭压缩以更好地分析生成的 PDF 文件):

             /* cf. 7.6.3.1: Documents in which only file attachments are 
                encrypted shall use the same password as the user and owner password.*/
PdfWriter writer = new PdfWriter(fileName, new WriterProperties()
                         .setStandardEncryption("secret".getBytes(),
                         "secret".getBytes(), EncryptionConstants.ALLOW_PRINTING |
                         EncryptionConstants.ALLOW_MODIFY_ANNOTATIONS,
                         EncryptionConstants.ENCRYPTION_AES_128 |
                         EncryptionConstants.DO_NOT_ENCRYPT_METADATA |
                         EncryptionConstants.EMBEDDED_FILES_ONLY)
                         .setCompressionLevel(CompressionConstants.NO_COMPRESSION));

PdfDocument pdf = new PdfDocument(writer);  

PdfFileSpec fs = PdfFileSpec.createEmbeddedFileSpec(pdf,"attached file".getBytes(),
                         null,"attachment.txt",null,null,null,true);
pdf.addFileAttachment("attachment.txt", fs);

try (Document doc = new Document(pdf)) {
        doc.add(new Paragraph("main file"));
}

此结果似乎与说明的规范相反:

if the contents of the stream are embedded within the PDF file (see 7.11.4, "Embedded File Streams"), they shall be encrypted like any other stream in the file

上述示例生成的 pdf 文件包含加密嵌入文件流在 CF 字典中的正确条目:

<</CF<</StdCF<</AuthEvent/EFOpen/CFM/AESV2/Length 16>>>>/EFF/StdCF

Table 规范中的 20 个:

Conforming writers shall respect this value when encrypting embedded files, except for embedded file streams that have their own crypt filter specifier.

我们案例中的流没有自己的 CF 说明符,因此应该使用 AESV2 进行加密。然而,在我们的示例中,流未加密:

4 0 obj
<</Length 13/Params<</ModDate(D:20160930101501+02'00')/Size 13>>/Subtype    /application#2foctet-stream/Type/EmbeddedFile>>stream
attached file
endstream
endobj

这导致了以下问题:

  1. 这是 iText 中的错误还是我误解了 PDF 规范?
  2. 如何使用加密的嵌入文件创建未加密的 pdf 文件 使用 iText?
  3. 如果这(还)不可能,是否还有其他免费的库或命令 线工具来做到这一点?

PS:Acrobat Reader DC 和 PDF-XChange Viewer 2.5 要求输入密码才能打开附件,而(不符合要求的)readers like evince 打开附件没有任何问题。但这不是我的问题。我的问题不是关于 reader 行为和可能的道德规范,而是关于 pdf 文件本身及其是否符合规范。

2021 年更新:
Release 7.1.16 最终在未加密的 pdf 文档中实现了嵌入文件的加密。
(API 略有变化:在下面的 iText 7 测试中,删除 createEmbeddedFileSpec 的最后一个参数,使其显示为 PdfFileSpec.createEmbeddedFileSpec(pdf,"attached file".getBytes(),null,"attachment.txt",null,null,null);


原回答

因为我没有得到任何答案,所以我用 iText 5.5.9 和 iText 7.0.1 进行了更多测试,得出的结论是 不要使用 [=14= 加密嵌入式文件流]是新版iText 7的一个bug。它只适用于 iText 5 和 ENCRYPTION_AES_256,尽管 Acrobat reader 发出警告,指出该页面存在错误,它可能无法正确显示该页面。详情见以下table:

以下是用于生成上述 table 和 iText 5.5.9 中使用的 pdf 文件的最小、完整和可验证示例的代码 ...

package pdfencryptef_itext5;

import com.itextpdf.text.Document;
import com.itextpdf.text.DocumentException;
import com.itextpdf.text.Paragraph;
import com.itextpdf.text.pdf.PdfFileSpecification;
import com.itextpdf.text.pdf.PdfWriter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.Security;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

public class PDFEncryptEF_iText5 {

    public static void main(String[] args) throws Exception {

        new PDFEncryptEF_iText5().createPDF("iText5_STD128.pdf", PdfWriter.STANDARD_ENCRYPTION_128);
        new PDFEncryptEF_iText5().createPDF("iText5_AES128.pdf", PdfWriter.ENCRYPTION_AES_128);
        new PDFEncryptEF_iText5().createPDF("iText5_AES256.pdf", PdfWriter.ENCRYPTION_AES_256);
        
        Security.addProvider(new BouncyCastleProvider());
        new PDFEncryptEF_iText5().createPDF("iText5_AES128C.pdf", -PdfWriter.ENCRYPTION_AES_128);
        new PDFEncryptEF_iText5().createPDF("iText5_AES256C.pdf", -PdfWriter.ENCRYPTION_AES_256);
        
    }
 
    public void createPDF(String fileName, int encryption  ) throws FileNotFoundException, DocumentException, IOException, CertificateException {
     
        Document document = new Document();
        Document.compress = false;

        PdfWriter writer = PdfWriter.getInstance(document, new FileOutputStream(fileName));
        
        if( encryption >= 0 ){
            writer.setEncryption("secret".getBytes(),"secret".getBytes(), 0,
                   encryption | PdfWriter.EMBEDDED_FILES_ONLY);
        } else {
            Certificate cert = getPublicCertificate("MyCert.cer" );
            writer.setEncryption( new Certificate[] {cert}, new int[] {0}, -encryption  | PdfWriter.EMBEDDED_FILES_ONLY);
        }
        writer.setPdfVersion(PdfWriter.VERSION_1_6);

        document.open();
        
        PdfFileSpecification fs = PdfFileSpecification.fileEmbedded(writer, null, "attachment.txt", "attached file".getBytes(), 0);
        writer.addFileAttachment( fs );
        
        document.add(new Paragraph("main file"));
        document.close(); 

    }
    
     public Certificate getPublicCertificate(String path) throws IOException, CertificateException {
        FileInputStream is = new FileInputStream(path);
        CertificateFactory cf = CertificateFactory.getInstance("X.509");
        X509Certificate cert = (X509Certificate) cf.generateCertificate(is);
        return cert;
    }
    
}

...和 ​​iText 7.0.1:

package pdfencryptef_itext7;

import com.itextpdf.kernel.pdf.CompressionConstants;
import com.itextpdf.kernel.pdf.EncryptionConstants;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfVersion;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.WriterProperties;
import com.itextpdf.kernel.pdf.filespec.PdfFileSpec;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.Paragraph;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.Security;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

public class PDFEncryptEF_iText7 {

    public static void main(String[] args) throws Exception {

        new PDFEncryptEF_iText7().createPDF("iText7_STD128.pdf", EncryptionConstants.STANDARD_ENCRYPTION_128);
        new PDFEncryptEF_iText7().createPDF("iText7_AES128.pdf", EncryptionConstants.ENCRYPTION_AES_128);
        new PDFEncryptEF_iText7().createPDF("iText7_AES256.pdf", EncryptionConstants.ENCRYPTION_AES_256);
        
        Security.addProvider(new BouncyCastleProvider());
        new PDFEncryptEF_iText7().createPDF("iText7_AES128C.pdf", -EncryptionConstants.ENCRYPTION_AES_128);
        new PDFEncryptEF_iText7().createPDF("iText7_AES256C.pdf", -EncryptionConstants.ENCRYPTION_AES_256);
    }
 
    public void createPDF(String fileName, int encryption  ) throws FileNotFoundException, IOException, CertificateException{
     
        PdfWriter writer;
        
        if( encryption >= 0 ){
            writer = new PdfWriter(fileName, new WriterProperties().setStandardEncryption("secret".getBytes(),"secret".getBytes(),
                    0,
                    encryption | EncryptionConstants.EMBEDDED_FILES_ONLY)
                    .setPdfVersion(PdfVersion.PDF_1_6));
        } else {
            Certificate cert = getPublicCertificate("MyCert.cer" );
            writer = new PdfWriter(fileName, new WriterProperties().setPublicKeyEncryption( new Certificate[] {cert}, 
                    new int[] {0},
                    -encryption  | EncryptionConstants.EMBEDDED_FILES_ONLY )
                    .setPdfVersion(PdfVersion.PDF_1_6));
        }
        writer.setCompressionLevel(CompressionConstants.NO_COMPRESSION);
        
        PdfDocument pdf = new PdfDocument(writer);  

        PdfFileSpec fs = PdfFileSpec.createEmbeddedFileSpec(pdf,"attached file".getBytes(),null,"attachment.txt",null,null,null,true);
        pdf.addFileAttachment("attachment.txt", fs);

        try (Document doc = new Document(pdf)) {
            doc.add(new Paragraph("main file"));
        }
    }
    
    public Certificate getPublicCertificate(String path) throws IOException, CertificateException {
        FileInputStream is = new FileInputStream(path);
        CertificateFactory cf = CertificateFactory.getInstance("X.509");
        X509Certificate cert = (X509Certificate) cf.generateCertificate(is);
        return cert;
    }
    
}

我必须承认,iText 人员对我的三个问题中的至少第一个问题没有任何反馈让我感到有点失望,但希望 iText 7 的未来版本能够正确处理 EMBEDDED_FILES_ONLY 旗帜。正如测试所示,对于 pdf 制作者和 reader 来说,要正确处理此功能似乎绝非易事。