/* * Copyright 2008 Pirion Systems Pty Ltd, 139 Warry St, * Fortitude Valley, Queensland, Australia * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ package com.sun.pdfview.decrypt; import com.sun.pdfview.PDFObject; import com.sun.pdfview.PDFParseException; import java.io.IOException; import java.util.HashMap; import java.util.Iterator; import java.util.Map; /** * Produces a {@link PDFDecrypter} for documents given a (possibly non-existent) * Encrypt dictionary. Supports decryption of versions 1, 2 and 4 of the * password-based encryption mechanisms as described in PDF Reference version * 1.7. This means that it supports RC4 and AES encryption with keys of * 40-128 bits; esentially, password-protected documents with compatibility * up to Acrobat 8. * * @See "PDF Reference version 1.7, section 3.5: Encryption" * @author Luke Kirby */ public class PDFDecrypterFactory { /** The name of the standard Identity CryptFilter */ public static final String CF_IDENTITY = "Identity"; /** Default key length for versions where key length is optional */ private static final int DEFAULT_KEY_LENGTH = 40; /** * Create a decryptor for a given encryption dictionary. A check is * immediately performed that the supplied password decrypts content * described by the encryption specification. * * @param encryptDict the Encrypt dict as found in the document's trailer. * May be null, in which case the {@link IdentityDecrypter} will * be returned. * @param documentId the object with key "ID" in the trailer's dictionary. * Should always be present if Encrypt is. * @param password the password to use; may be <code>null</code> * @return The decryptor that should be used for all encrypted data in the * PDF * @throws IOException will typically be a {@link * com.sun.pdfview.PDFParseException}, indicating an IO problem, an error * in the structure of the document, or a failure to obtain various ciphers * from the installed JCE providers * @throws EncryptionUnsupportedByPlatformException if the encryption * is not supported by the environment in which the code is executing * @throws EncryptionUnsupportedByProductException if PDFRenderer does * not currently support the specified encryption * @throws PDFAuthenticationFailureException if the supplied password * was not able to */ public static PDFDecrypter createDecryptor (PDFObject encryptDict, PDFObject documentId, PDFPassword password) throws IOException, EncryptionUnsupportedByPlatformException, EncryptionUnsupportedByProductException, PDFAuthenticationFailureException { // none of the classes beyond us want to see a null PDFPassword password = PDFPassword.nonNullPassword(password); if (encryptDict == null) { // No encryption specified return IdentityDecrypter.getInstance(); } else { PDFObject filter = encryptDict.getDictRef("Filter"); // this means that we'll fail if, for example, public key // encryption is employed if (filter != null && "Standard".equals(filter.getStringValue())) { final PDFObject vObj = encryptDict.getDictRef("V"); int v = vObj != null ? vObj.getIntValue() : 0; if (v == 1 || v == 2) { final PDFObject lengthObj = encryptDict.getDictRef("Length"); final Integer length = lengthObj != null ? lengthObj.getIntValue() : null; return createStandardDecrypter( encryptDict, documentId, password, length, false, StandardDecrypter.EncryptionAlgorithm.RC4); } else if (v == 4) { return createCryptFilterDecrypter( encryptDict, documentId, password, v); } else { throw new EncryptionUnsupportedByPlatformException( "Unsupported encryption version: " + v); } } else if (filter == null) { throw new PDFParseException( "No Filter specified in Encrypt dictionary"); } else { throw new EncryptionUnsupportedByPlatformException( "Unsupported encryption Filter: " + filter + "; only Standard is supported."); } } } /** * Create a decrypter working from a crypt filter dictionary, as in * version 4 encryption * * @param encryptDict the Encrypt dictionary * @param documentId the document ID * @param password the provided password * @param v the version of encryption being used; must be at least 4 * @return the decrypter corresponding to the scheme expressed in * encryptDict * @throws PDFAuthenticationFailureException if the provided password * does not decrypt this document * @throws IOException if there is a problem reading the PDF, an invalid * document structure, or an inability to obtain the required ciphers * from the platform's JCE * @throws EncryptionUnsupportedByPlatformException if the encryption * is not supported by the environment in which the code is executing * @throws EncryptionUnsupportedByProductException if PDFRenderer does * not currently support the specified encryption */ private static PDFDecrypter createCryptFilterDecrypter( PDFObject encryptDict, PDFObject documentId, PDFPassword password, int v) throws PDFAuthenticationFailureException, IOException, EncryptionUnsupportedByPlatformException, EncryptionUnsupportedByProductException { assert v >= 4 : "crypt filter decrypter not supported for " + "standard encryption prior to version 4"; // encryptMetadata is true if not present. Note that we don't actually // use this to change our reading of metadata streams (that's all done // internally by the document specifying a Crypt filter of None if // appropriate), but it does affect the encryption key. boolean encryptMetadata = true; final PDFObject encryptMetadataObj = encryptDict.getDictRef("EncryptMetadata"); if (encryptMetadataObj != null && encryptMetadataObj.getType() == PDFObject.BOOLEAN) { encryptMetadata = encryptMetadataObj.getBooleanValue(); } // Assemble decrypters for each filter in the // crypt filter (CF) dictionary final Map<String, PDFDecrypter> cfDecrypters = new HashMap<String, PDFDecrypter>(); final PDFObject cfDict = encryptDict.getDictRef("CF"); if (cfDict == null) { throw new PDFParseException( "No CF value present in Encrypt dict for V4 encryption"); } final Iterator<String> cfNameIt = cfDict.getDictKeys(); while (cfNameIt.hasNext()) { final String cfName = cfNameIt.next(); final PDFObject cryptFilter = cfDict.getDictRef(cfName); final PDFObject lengthObj = cryptFilter.getDictRef("Length"); // The Errata for PDF 1.7 explains that the value of // Length in CF dictionaries is in bytes final Integer length = lengthObj != null ? lengthObj.getIntValue() * 8 : null; // CFM is the crypt filter method, describing whether RC4, // AES, or None (i.e., identity) is the encryption mechanism // used for the name crypt filter final PDFObject cfmObj = cryptFilter.getDictRef("CFM"); final String cfm = cfmObj != null ? cfmObj.getStringValue() : "None"; final PDFDecrypter cfDecrypter; if ("None".equals(cfm)) { cfDecrypter = IdentityDecrypter.getInstance(); } else if ("V2".equals(cfm)) { cfDecrypter = createStandardDecrypter( encryptDict, documentId, password, length, encryptMetadata, StandardDecrypter.EncryptionAlgorithm.RC4); } else if ("AESV2".equals(cfm)) { cfDecrypter = createStandardDecrypter( encryptDict, documentId, password, length, encryptMetadata, StandardDecrypter.EncryptionAlgorithm.AESV2); } else { throw new UnsupportedOperationException( "Unknown CryptFilter method: " + cfm); } cfDecrypters.put(cfName, cfDecrypter); } // always put Identity in last so that it will override any // Identity filter sneakily declared in the CF entry cfDecrypters.put(CF_IDENTITY, IdentityDecrypter.getInstance()); PDFObject stmFObj = encryptDict.getDictRef("StmF"); final String defaultStreamFilter = stmFObj != null ? stmFObj.getStringValue() : CF_IDENTITY; PDFObject strFObj = encryptDict.getDictRef("StrF"); final String defaultStringFilter = strFObj != null ? strFObj.getStringValue() : CF_IDENTITY; return new CryptFilterDecrypter( cfDecrypters, defaultStreamFilter, defaultStringFilter); } /** * Create a standard single-algorithm AES or RC4 decrypter. The Encrypt * dictionary is used where possible, but where different encryption * versions employ different mechanisms of specifying configuration or may * be specified via a CF entry (e.g. key length), the value is specified as * a parameter. * * @param encryptDict the Encrypt dictionary * @param documentId the document ID * @param password the password * @param keyLength the key length, in bits; may be <code>null</code> * to use a {@link #DEFAULT_KEY_LENGTH default} * @param encryptMetadata whether metadata is being encrypted * @param encryptionAlgorithm, the encryption algorithm * @return the decrypter * @throws PDFAuthenticationFailureException if the provided password * is not the one expressed by the encryption dictionary * @throws IOException if there is a problem reading the PDF content, * if the content does not comply with the PDF specification * @throws EncryptionUnsupportedByPlatformException if the encryption * is not supported by the environment in which the code is executing * @throws EncryptionUnsupportedByProductException if PDFRenderer does * not currently support the specified encryption * */ private static PDFDecrypter createStandardDecrypter( PDFObject encryptDict, PDFObject documentId, PDFPassword password, Integer keyLength, boolean encryptMetadata, StandardDecrypter.EncryptionAlgorithm encryptionAlgorithm) throws PDFAuthenticationFailureException, IOException, EncryptionUnsupportedByPlatformException, EncryptionUnsupportedByProductException { if (keyLength == null) { keyLength = DEFAULT_KEY_LENGTH; } // R describes the revision of the security handler final PDFObject rObj = encryptDict.getDictRef("R"); if (rObj == null) { throw new PDFParseException( "No R entry present in Encrypt dictionary"); } final int revision = rObj.getIntValue(); if (revision < 2 || revision > 4) { throw new EncryptionUnsupportedByPlatformException( "Unsupported Standard security handler revision; R=" + revision); } // O describes validation details for the owner key final PDFObject oObj = encryptDict.getDictRef("O"); if (oObj == null) { throw new PDFParseException( "No O entry present in Encrypt dictionary"); } final byte[] o = oObj.getStream(); if (o.length != 32) { throw new PDFParseException("Expected owner key O " + "value of 32 bytes; found " + o.length); } // U describes validation details for the user key final PDFObject uObj = encryptDict.getDictRef("U"); if (uObj == null) { throw new PDFParseException( "No U entry present in Encrypt dictionary"); } final byte[] u = uObj.getStream(); if (u.length != 32) { throw new PDFParseException( "Expected user key U value of 32 bytes; found " + o.length); } // P describes the permissions regarding document usage final PDFObject pObj = encryptDict.getDictRef("P"); if (pObj == null) { throw new PDFParseException( "Required P entry in Encrypt dictionary not found"); } return new StandardDecrypter( encryptionAlgorithm, documentId, keyLength, revision, o, u, pObj.getIntValue(), encryptMetadata, password); } }