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