Search code examples
pdfencryptionitextitext7

iText - Encrypt Embedded Files Only


I want to create an unencrypted pdf file with an encrypted embedded file with iText as described in Section 7.6.1 of PDF 32000-1:2008:

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

The following example (iText 7.0.1), however, produces a PDF file with an unencrypted embedded file stream (compression switched off to better analyze the resulting PDF file):

             /* 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"));
}

This result appears to be in contrast to the spec stating:

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

The pdf file produced by the above example contains the correct entry for the encrypted embedded file stream in the CF dictionary:

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

Table 20 of the spec states:

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

The stream in our case has no own CF specifier and hence should be encrytped using AESV2. In our example, however, the stream is not encrypted:

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

This leads to the following questions:

  1. Is this a bug in iText or did I misinterpret the PDF spec?
  2. How can I create unencrypted pdf files with encrypted embedded files with iText?
  3. If this is not (yet) possible, is there any other free library or command line tool to do this?

PS: Acrobat Reader DC and PDF-XChange Viewer 2.5 ask for a password to open the attachment, whereas (non-conforming) readers like evince open the attachment without any questions. But this is not my question here. My question is not about reader behavior and possibly ethics, but about the pdf file itself and its compliance with the spec.


Solution

  • Update 2021:
    Release 7.1.16 finally implemented encryption of embedded files in otherwise not encrypted pdf documents.
    (the API changed slightly: in the test for iText 7 below, remove the last parameter of createEmbeddedFileSpec so that it reads PdfFileSpec.createEmbeddedFileSpec(pdf,"attached file".getBytes(),null,"attachment.txt",null,null,null);)


    Original answer

    As I didn't get any answers I made some more tests with iText 5.5.9 and iText 7.0.1 and came to the conclusion that not to encrypt embedded file streams with EMBEDDED_FILES_ONLY is a bug in the new version of iText 7. It only worked with iText 5 and ENCRYPTION_AES_256, although Acrobat reader gave a warning that an error existed on this page and it might not display the page correctly. For details see the following table:

    Summary of test results

    Following is the code of the minimal, complete, and verifiable examples to produce the pdf files used in the above table with iText 5.5.9 ...

    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;
        }
        
    }
    

    ... and 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;
        }
        
    }
    

    I must admit that I'm a bit disappointed that there was no feedback from the iText people to at least the first of my three questions but, hopefully, future versions of iText 7 will correctly process the EMBEDDED_FILES_ONLY flag. As the tests showed, it seems to be far from trivial for both the pdf producer as well as the reader to correctly handle this feature.