blob: 43a323335850b071454bfb53c075e83f66f47076 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* $Id$ */
package org.apache.fop.pdf;
// Java
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Random;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.CipherOutputStream;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
/**
* class representing a /Filter /Standard object.
*
*/
public class PDFEncryptionJCE extends PDFObject implements PDFEncryption {
private class EncryptionFilter extends PDFFilter {
private PDFEncryptionJCE encryption;
private int number;
private int generation;
/**
* The constructor for the internal PDFEncryptionJCE filter
* @param encryption The encryption object to use
* @param number The number of the object to be encrypted
* @param generation The generation of the object to be encrypted
*/
public EncryptionFilter(PDFEncryptionJCE encryption,
int number, int generation) {
super();
this.encryption = encryption;
this.number = number;
this.generation = generation;
log.debug("new encryption filter for number "
+ number + " and generation " + generation);
}
/**
* Return a PDF string representation of the filter. In this
* case no filter name is passed.
* @return The filter name, blank in this case
*/
public String getName() {
return "";
}
/**
* Return a parameter dictionary for this filter, or null
* @return The parameter dictionary. In this case, null.
*/
public PDFObject getDecodeParms() {
return null;
}
/**
* Encode the given data with the filter
* @param data The data to be encrypted
* @return The encrypted data
*/
public byte[] encode(byte[] data) {
return encryption.encryptData(data, number, generation);
}
/**
* {@inheritDoc}
*/
public void encode(InputStream in, OutputStream out, int length)
throws IOException {
byte[] buffer = new byte[length];
in.read(buffer);
buffer = encode(buffer);
out.write(buffer);
}
/**
* {@inheritDoc}
*/
public OutputStream applyFilter(OutputStream out) throws IOException {
return new CipherOutputStream(out,
encryption.initCipher(number, generation));
}
}
private static final char [] PAD
= {0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41,
0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01, 0x08,
0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80,
0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53, 0x69, 0x7A};
/** Value of PRINT permission */
public static final int PERMISSION_PRINT = 4;
/** Value of content editting permission */
public static final int PERMISSION_EDIT_CONTENT = 8;
/** Value of content extraction permission */
public static final int PERMISSION_COPY_CONTENT = 16;
/** Value of annotation editting permission */
public static final int PERMISSION_EDIT_ANNOTATIONS = 32;
// Encryption tools
private MessageDigest digest = null;
//private Cipher cipher = null;
private Random random = new Random();
// Control attributes
private PDFEncryptionParams params;
// Output attributes
private byte[] fileID = null;
private byte[] encryptionKey = null;
private String dictionary = null;
/**
* Create a /Filter /Standard object.
*
* @param objnum the object's number
*/
public PDFEncryptionJCE(int objnum) {
/* generic creation of object */
super();
setObjectNumber(objnum);
try {
digest = MessageDigest.getInstance("MD5");
//cipher = Cipher.getInstance("RC4");
} catch (NoSuchAlgorithmException e) {
throw new UnsupportedOperationException(e.getMessage());
/*} catch (NoSuchPaddingException e) {
throw new UnsupportedOperationException(e.getMessage());*/
}
}
/**
* Local factory method.
* @param objnum PDF object number for the encryption object
* @param params PDF encryption parameters
* @return PDFEncryption the newly created PDFEncryption object
*/
public static PDFEncryption make(int objnum, PDFEncryptionParams params) {
PDFEncryptionJCE impl = new PDFEncryptionJCE(objnum);
impl.setParams(params);
impl.init();
return impl;
}
/**
* Returns the encryption parameters.
* @return the encryption parameters
*/
public PDFEncryptionParams getParams() {
return this.params;
}
/**
* Sets the encryption parameters.
* @param params The parameterss to set
*/
public void setParams(PDFEncryptionParams params) {
this.params = params;
}
// Internal procedures
private byte[] prepPassword(String password) {
byte[] obuffer = new byte[32];
byte[] pbuffer = password.getBytes();
int i = 0;
int j = 0;
while (i < obuffer.length && i < pbuffer.length) {
obuffer[i] = pbuffer[i];
i++;
}
while (i < obuffer.length) {
obuffer[i++] = (byte) PAD[j++];
}
return obuffer;
}
/**
* Returns the document file ID
* @return The file ID
*/
public byte[] getFileID() {
if (fileID == null) {
fileID = new byte[16];
random.nextBytes(fileID);
}
return fileID;
}
/**
* This method returns the indexed file ID
* @param index The index to access the file ID
* @return The file ID
*/
public String getFileID(int index) {
if (index == 1) {
return PDFText.toHex(getFileID());
}
byte[] id = new byte[16];
random.nextBytes(id);
return PDFText.toHex(id);
}
private byte[] encryptWithKey(byte[] data, byte[] key) {
try {
final Cipher c = initCipher(key);
return c.doFinal(data);
} catch (IllegalBlockSizeException e) {
throw new IllegalStateException(e.getMessage());
} catch (BadPaddingException e) {
throw new IllegalStateException(e.getMessage());
}
}
private Cipher initCipher(byte[] key) {
try {
Cipher c = Cipher.getInstance("RC4");
SecretKeySpec keyspec = new SecretKeySpec(key, "RC4");
c.init(Cipher.ENCRYPT_MODE, keyspec);
return c;
} catch (InvalidKeyException e) {
throw new IllegalStateException(e.getMessage());
} catch (NoSuchAlgorithmException e) {
throw new UnsupportedOperationException(e.getMessage());
} catch (NoSuchPaddingException e) {
throw new UnsupportedOperationException(e.getMessage());
}
}
private Cipher initCipher(int number, int generation) {
byte[] hash = calcHash(number, generation);
int size = hash.length;
hash = digest.digest(hash);
byte[] key = calcKey(hash, size);
return initCipher(key);
}
private byte[] encryptWithHash(byte[] data, byte[] hash, int size) {
hash = digest.digest(hash);
byte[] key = calcKey(hash, size);
return encryptWithKey(data, key);
}
private byte[] calcKey(byte[] hash, int size) {
byte[] key = new byte[size];
for (int i = 0; i < size; i++) {
key[i] = hash[i];
}
return key;
}
/**
* This method initializes the encryption algorithms and values
*/
public void init() {
// Generate the owner value
byte[] oValue;
if (params.getOwnerPassword().length() > 0) {
oValue = encryptWithHash(
prepPassword(params.getUserPassword()),
prepPassword(params.getOwnerPassword()), 5);
} else {
oValue = encryptWithHash(
prepPassword(params.getUserPassword()),
prepPassword(params.getUserPassword()), 5);
}
// Generate permissions value
int permissions = -4;
if (!params.isAllowPrint()) {
permissions -= PERMISSION_PRINT;
}
if (!params.isAllowCopyContent()) {
permissions -= PERMISSION_COPY_CONTENT;
}
if (!params.isAllowEditContent()) {
permissions -= PERMISSION_EDIT_CONTENT;
}
if (!params.isAllowEditAnnotations()) {
permissions -= PERMISSION_EDIT_ANNOTATIONS;
}
// Create the encrption key
digest.update(prepPassword(params.getUserPassword()));
digest.update(oValue);
digest.update((byte) (permissions >>> 0));
digest.update((byte) (permissions >>> 8));
digest.update((byte) (permissions >>> 16));
digest.update((byte) (permissions >>> 24));
digest.update(getFileID());
byte [] hash = digest.digest();
this.encryptionKey = new byte[5];
for (int i = 0; i < 5; i++) {
this.encryptionKey[i] = hash[i];
}
// Create the user value
byte[] uValue = encryptWithKey(prepPassword(""), this.encryptionKey);
// Create the dictionary
this.dictionary = getObjectID()
+ "<< /Filter /Standard\n"
+ "/V 1\n"
+ "/R 2\n"
+ "/Length 40\n"
+ "/P " + permissions + "\n"
+ "/O " + PDFText.toHex(oValue) + "\n"
+ "/U " + PDFText.toHex(uValue) + "\n"
+ ">>\n"
+ "endobj\n";
}
/**
* This method encrypts the passed data using the generated keys.
* @param data The data to be encrypted
* @param number The block number
* @param generation The block generation
* @return The encrypted data
*/
public byte[] encryptData(byte[] data, int number, int generation) {
if (this.encryptionKey == null) {
throw new IllegalStateException("PDF Encryption has not been initialized");
}
byte[] hash = calcHash(number, generation);
return encryptWithHash(data, hash, hash.length);
}
/** {@inheritDoc} */
public byte[] encrypt(byte[] data, PDFObject refObj) {
PDFObject o = refObj;
while (o != null && !o.hasObjectNumber()) {
o = o.getParent();
}
if (o == null) {
throw new IllegalStateException("No object number could be obtained for a PDF object");
}
return encryptData(data, o.getObjectNumber(), o.getGeneration());
}
private byte[] calcHash(int number, int generation) {
byte[] hash = new byte[this.encryptionKey.length + 5];
int i = 0;
while (i < this.encryptionKey.length) {
hash[i] = this.encryptionKey[i]; i++;
}
hash[i++] = (byte) (number >>> 0);
hash[i++] = (byte) (number >>> 8);
hash[i++] = (byte) (number >>> 16);
hash[i++] = (byte) (generation >>> 0);
hash[i++] = (byte) (generation >>> 8);
return hash;
}
/**
* Creates PDFFilter for the encryption object
* @param number The object number
* @param generation The objects generation
* @return The resulting filter
*/
public PDFFilter makeFilter(int number, int generation) {
return new EncryptionFilter(this, number, generation);
}
/**
* Adds a PDFFilter to the PDFStream object
* @param stream the stream to add an encryption filter to
*/
public void applyFilter(AbstractPDFStream stream) {
stream.getFilterList().addFilter(
this.makeFilter(stream.getObjectNumber(), stream.getGeneration()));
}
/**
* Represent the object in PDF
*
* @return the PDF
*/
public byte[] toPDF() {
if (this.dictionary == null) {
throw new IllegalStateException("PDF Encryption has not been initialized");
}
return encode(this.dictionary);
}
/**
* {@inheritDoc}
*/
public String getTrailerEntry() {
return "/Encrypt " + getObjectNumber() + " "
+ getGeneration() + " R\n"
+ "/ID[" + getFileID(1) + getFileID(2) + "]\n";
}
}