| /* |
| * 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; |
| |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.io.UnsupportedEncodingException; |
| import java.security.InvalidAlgorithmParameterException; |
| import java.security.InvalidKeyException; |
| import java.security.MessageDigest; |
| import java.security.NoSuchAlgorithmException; |
| import java.security.SecureRandom; |
| import java.util.Arrays; |
| |
| import javax.crypto.BadPaddingException; |
| import javax.crypto.Cipher; |
| import javax.crypto.CipherOutputStream; |
| import javax.crypto.IllegalBlockSizeException; |
| import javax.crypto.NoSuchPaddingException; |
| import javax.crypto.spec.IvParameterSpec; |
| import javax.crypto.spec.SecretKeySpec; |
| |
| /** |
| * An implementation of the Standard Security Handler. |
| */ |
| public final class PDFEncryptionJCE extends PDFObject implements PDFEncryption { |
| |
| private final MessageDigest digest; |
| |
| private SecureRandom random; |
| |
| private byte[] encryptionKey; |
| |
| private String encryptionDictionary; |
| |
| private boolean useAlgorithm31a; |
| |
| private boolean encryptMetadata = true; |
| |
| private Version pdfVersion = Version.V1_4; |
| |
| private static byte[] ivZero = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; |
| |
| private class EncryptionInitializer { |
| |
| private final PDFEncryptionParams encryptionParams; |
| |
| private int encryptionLength; |
| |
| private int version; |
| |
| private int revision; |
| |
| EncryptionInitializer(PDFEncryptionParams params) { |
| this.encryptionParams = new PDFEncryptionParams(params); |
| } |
| |
| void init() { |
| encryptionLength = encryptionParams.getEncryptionLengthInBits(); |
| determineEncryptionAlgorithm(); |
| int permissions = Permission.computePermissions(encryptionParams); |
| EncryptionSettings encryptionSettings = new EncryptionSettings( |
| encryptionLength, permissions, |
| encryptionParams.getUserPassword(), encryptionParams.getOwnerPassword(), |
| encryptionParams.encryptMetadata()); |
| InitializationEngine initializationEngine = createEngine(encryptionSettings); |
| initializationEngine.run(); |
| encryptionDictionary = createEncryptionDictionary(permissions, initializationEngine); |
| encryptMetadata = encryptionParams.encryptMetadata(); |
| } |
| |
| private InitializationEngine createEngine(EncryptionSettings encryptionSettings) { |
| if (revision == 5) { |
| return new Rev5Engine(encryptionSettings); |
| } else if (revision == 2) { |
| return new Rev2Engine(encryptionSettings); |
| } else { |
| return new Rev3Engine(encryptionSettings); |
| } |
| } |
| |
| private void determineEncryptionAlgorithm() { |
| if (isVersion5Revision5Algorithm()) { |
| version = 5; |
| revision = 5; |
| pdfVersion = Version.V1_7; |
| } else if (isVersion1Revision2Algorithm()) { |
| version = 1; |
| revision = 2; |
| } else { |
| version = 2; |
| revision = 3; |
| } |
| } |
| |
| private boolean isVersion1Revision2Algorithm() { |
| return encryptionLength == 40 |
| && encryptionParams.isAllowFillInForms() |
| && encryptionParams.isAllowAccessContent() |
| && encryptionParams.isAllowAssembleDocument() |
| && encryptionParams.isAllowPrintHq(); |
| } |
| |
| private boolean isVersion5Revision5Algorithm() { |
| return encryptionLength == 256; |
| } |
| |
| private String createEncryptionDictionary(final int permissions, InitializationEngine engine) { |
| String encryptionDict = "<<\n" |
| + "/Filter /Standard\n" |
| + "/V " + version + "\n" |
| + "/R " + revision + "\n" |
| + "/Length " + encryptionLength + "\n" |
| + "/P " + permissions + "\n" |
| + engine.getEncryptionDictionaryPart() |
| + ">>"; |
| return encryptionDict; |
| } |
| |
| } |
| |
| private static enum Permission { |
| |
| PRINT(3), |
| EDIT_CONTENT(4), |
| COPY_CONTENT(5), |
| EDIT_ANNOTATIONS(6), |
| FILL_IN_FORMS(9), |
| ACCESS_CONTENT(10), |
| ASSEMBLE_DOCUMENT(11), |
| PRINT_HQ(12); |
| |
| private final int mask; |
| |
| /** |
| * Creates a new permission. |
| * |
| * @param bit bit position for this permission, 1-based to match the PDF Reference |
| */ |
| private Permission(int bit) { |
| mask = 1 << (bit - 1); |
| } |
| |
| private int removeFrom(int permissions) { |
| return permissions - mask; |
| } |
| |
| static int computePermissions(PDFEncryptionParams encryptionParams) { |
| int permissions = -4; |
| |
| if (!encryptionParams.isAllowPrint()) { |
| permissions = PRINT.removeFrom(permissions); |
| } |
| if (!encryptionParams.isAllowCopyContent()) { |
| permissions = COPY_CONTENT.removeFrom(permissions); |
| } |
| if (!encryptionParams.isAllowEditContent()) { |
| permissions = EDIT_CONTENT.removeFrom(permissions); |
| } |
| if (!encryptionParams.isAllowEditAnnotations()) { |
| permissions = EDIT_ANNOTATIONS.removeFrom(permissions); |
| } |
| if (!encryptionParams.isAllowFillInForms()) { |
| permissions = FILL_IN_FORMS.removeFrom(permissions); |
| } |
| if (!encryptionParams.isAllowAccessContent()) { |
| permissions = ACCESS_CONTENT.removeFrom(permissions); |
| } |
| if (!encryptionParams.isAllowAssembleDocument()) { |
| permissions = ASSEMBLE_DOCUMENT.removeFrom(permissions); |
| } |
| if (!encryptionParams.isAllowPrintHq()) { |
| permissions = PRINT_HQ.removeFrom(permissions); |
| } |
| return permissions; |
| } |
| } |
| |
| private static final class EncryptionSettings { |
| |
| final int encryptionLength; |
| |
| final int permissions; |
| |
| final String userPassword; |
| |
| final String ownerPassword; |
| |
| final boolean encryptMetadata; |
| |
| EncryptionSettings(int encryptionLength, int permissions, |
| String userPassword, String ownerPassword, boolean encryptMetadata) { |
| this.encryptionLength = encryptionLength; |
| this.permissions = permissions; |
| this.userPassword = userPassword; |
| this.ownerPassword = ownerPassword; |
| this.encryptMetadata = encryptMetadata; |
| } |
| |
| } |
| |
| private abstract class InitializationEngine { |
| |
| protected final int encryptionLengthInBytes; |
| |
| protected final int permissions; |
| |
| private final String userPassword; |
| |
| private final String ownerPassword; |
| |
| protected byte[] oValue; |
| |
| protected byte[] uValue; |
| |
| protected byte[] preparedUserPassword; |
| |
| protected byte[] preparedOwnerPassword; |
| |
| InitializationEngine(EncryptionSettings encryptionSettings) { |
| this.encryptionLengthInBytes = encryptionSettings.encryptionLength / 8; |
| this.permissions = encryptionSettings.permissions; |
| this.userPassword = encryptionSettings.userPassword; |
| this.ownerPassword = encryptionSettings.ownerPassword; |
| } |
| |
| void run() { |
| preparedUserPassword = preparePassword(userPassword); |
| if (ownerPassword == null || ownerPassword.length() == 0) { |
| preparedOwnerPassword = preparedUserPassword; |
| } else { |
| preparedOwnerPassword = preparePassword(ownerPassword); |
| } |
| } |
| |
| protected String getEncryptionDictionaryPart() { |
| String encryptionDictionaryPart = "/O " + PDFText.toHex(oValue) + "\n" |
| + "/U " + PDFText.toHex(uValue) + "\n"; |
| return encryptionDictionaryPart; |
| } |
| |
| protected abstract void computeOValue(); |
| |
| protected abstract void computeUValue(); |
| |
| protected abstract void createEncryptionKey(); |
| |
| protected abstract byte[] preparePassword(String password); |
| } |
| |
| private abstract class RevBefore5Engine extends InitializationEngine { |
| |
| /** Padding for passwords. */ |
| protected final byte[] padding = new byte[] {(byte) 0x28, (byte) 0xBF, (byte) 0x4E, (byte) 0x5E, |
| (byte) 0x4E, (byte) 0x75, (byte) 0x8A, (byte) 0x41, (byte) 0x64, (byte) 0x00, (byte) 0x4E, |
| (byte) 0x56, (byte) 0xFF, (byte) 0xFA, (byte) 0x01, (byte) 0x08, (byte) 0x2E, (byte) 0x2E, |
| (byte) 0x00, (byte) 0xB6, (byte) 0xD0, (byte) 0x68, (byte) 0x3E, (byte) 0x80, (byte) 0x2F, |
| (byte) 0x0C, (byte) 0xA9, (byte) 0xFE, (byte) 0x64, (byte) 0x53, (byte) 0x69, (byte) 0x7A}; |
| |
| RevBefore5Engine(EncryptionSettings encryptionSettings) { |
| super(encryptionSettings); |
| } |
| |
| /** |
| * Applies Algorithm 3.3 Page 79 of the PDF 1.4 Reference. |
| * |
| */ |
| protected void computeOValue() { |
| // Step 1 |
| byte[] md5Input = preparedOwnerPassword; |
| // Step 2 |
| digest.reset(); |
| byte[] hash = digest.digest(md5Input); |
| // Step 3 |
| hash = computeOValueStep3(hash); |
| // Step 4 |
| byte[] key = new byte[encryptionLengthInBytes]; |
| System.arraycopy(hash, 0, key, 0, encryptionLengthInBytes); |
| // Steps 5, 6 |
| byte[] encryptionResult = encryptWithKey(key, preparedUserPassword); |
| // Step 7 |
| oValue = computeOValueStep7(key, encryptionResult); |
| } |
| |
| /** |
| * Applies Algorithm 3.2 Page 78 of the PDF 1.4 Reference. |
| */ |
| protected void createEncryptionKey() { |
| // Steps 1, 2 |
| digest.reset(); |
| digest.update(preparedUserPassword); |
| // Step 3 |
| digest.update(oValue); |
| // Step 4 |
| digest.update((byte) (permissions >>> 0)); |
| digest.update((byte) (permissions >>> 8)); |
| digest.update((byte) (permissions >>> 16)); |
| digest.update((byte) (permissions >>> 24)); |
| // Step 5 |
| digest.update(getDocumentSafely().getFileIDGenerator().getOriginalFileID()); |
| byte[] hash = digest.digest(); |
| // Step 6 |
| hash = createEncryptionKeyStep6(hash); |
| // Step 7 |
| encryptionKey = new byte[encryptionLengthInBytes]; |
| System.arraycopy(hash, 0, encryptionKey, 0, encryptionLengthInBytes); |
| } |
| |
| /** |
| * Adds padding to the password as directed in page 78 of the PDF 1.4 Reference. |
| * |
| * @param password the password |
| * @return the password with additional padding if necessary |
| */ |
| protected byte[] preparePassword(String password) { |
| int finalLength = 32; |
| byte[] preparedPassword = new byte[finalLength]; |
| try { |
| byte[] passwordBytes = password.getBytes("UTF-8"); |
| if (passwordBytes.length >= finalLength) { |
| System.arraycopy(passwordBytes, 0, preparedPassword, 0, finalLength); |
| } else { |
| System.arraycopy(passwordBytes, 0, preparedPassword, 0, passwordBytes.length); |
| System.arraycopy(padding, 0, preparedPassword, passwordBytes.length, finalLength |
| - passwordBytes.length); |
| } |
| return preparedPassword; |
| } catch (UnsupportedEncodingException e) { |
| throw new UnsupportedOperationException(e); |
| } |
| } |
| |
| void run() { |
| super.run(); |
| computeOValue(); |
| createEncryptionKey(); |
| computeUValue(); |
| } |
| |
| protected abstract byte[] computeOValueStep3(byte[] hash); |
| |
| protected abstract byte[] computeOValueStep7(byte[] key, byte[] encryptionResult); |
| |
| protected abstract byte[] createEncryptionKeyStep6(byte[] hash); |
| |
| } |
| |
| private class Rev2Engine extends RevBefore5Engine { |
| |
| Rev2Engine(EncryptionSettings encryptionSettings) { |
| super(encryptionSettings); |
| } |
| |
| @Override |
| protected byte[] computeOValueStep3(byte[] hash) { |
| return hash; |
| } |
| |
| @Override |
| protected byte[] computeOValueStep7(byte[] key, byte[] encryptionResult) { |
| return encryptionResult; |
| } |
| |
| @Override |
| protected byte[] createEncryptionKeyStep6(byte[] hash) { |
| return hash; |
| } |
| |
| @Override |
| protected void computeUValue() { |
| uValue = encryptWithKey(encryptionKey, padding); |
| } |
| |
| } |
| |
| private class Rev3Engine extends RevBefore5Engine { |
| |
| Rev3Engine(EncryptionSettings encryptionSettings) { |
| super(encryptionSettings); |
| } |
| |
| @Override |
| protected byte[] computeOValueStep3(byte[] hash) { |
| for (int i = 0; i < 50; i++) { |
| hash = digest.digest(hash); |
| } |
| return hash; |
| } |
| |
| @Override |
| protected byte[] computeOValueStep7(byte[] key, byte[] encryptionResult) { |
| return xorKeyAndEncrypt19Times(key, encryptionResult); |
| } |
| |
| @Override |
| protected byte[] createEncryptionKeyStep6(byte[] hash) { |
| for (int i = 0; i < 50; i++) { |
| digest.update(hash, 0, encryptionLengthInBytes); |
| hash = digest.digest(); |
| } |
| return hash; |
| } |
| |
| @Override |
| protected void computeUValue() { |
| // Step 1 is encryptionKey |
| // Step 2 |
| digest.reset(); |
| digest.update(padding); |
| // Step 3 |
| digest.update(getDocumentSafely().getFileIDGenerator().getOriginalFileID()); |
| // Step 4 |
| byte[] encryptionResult = encryptWithKey(encryptionKey, digest.digest()); |
| // Step 5 |
| encryptionResult = xorKeyAndEncrypt19Times(encryptionKey, encryptionResult); |
| // Step 6 |
| uValue = new byte[32]; |
| System.arraycopy(encryptionResult, 0, uValue, 0, 16); |
| // Add the arbitrary padding |
| Arrays.fill(uValue, 16, 32, (byte) 0); |
| } |
| |
| private byte[] xorKeyAndEncrypt19Times(byte[] key, byte[] input) { |
| byte[] result = input; |
| byte[] encryptionKey = new byte[key.length]; |
| for (int i = 1; i <= 19; i++) { |
| for (int j = 0; j < key.length; j++) { |
| encryptionKey[j] = (byte) (key[j] ^ i); |
| } |
| result = encryptWithKey(encryptionKey, result); |
| } |
| return result; |
| } |
| |
| } |
| |
| private class Rev5Engine extends InitializationEngine { |
| |
| // private SecureRandom random = new SecureRandom(); |
| private byte[] userValidationSalt = new byte[8]; |
| private byte[] userKeySalt = new byte[8]; |
| private byte[] ownerValidationSalt = new byte[8]; |
| private byte[] ownerKeySalt = new byte[8]; |
| private byte[] ueValue; |
| private byte[] oeValue; |
| private final boolean encryptMetadata; |
| |
| Rev5Engine(EncryptionSettings encryptionSettings) { |
| super(encryptionSettings); |
| this.encryptMetadata = encryptionSettings.encryptMetadata; |
| } |
| |
| void run() { |
| super.run(); |
| random = new SecureRandom(); |
| createEncryptionKey(); |
| computeUValue(); |
| computeOValue(); |
| computeUEValue(); |
| computeOEValue(); |
| } |
| |
| protected String getEncryptionDictionaryPart() { |
| String encryptionDictionaryPart = super.getEncryptionDictionaryPart(); |
| encryptionDictionaryPart += "/OE " + PDFText.toHex(oeValue) + "\n" |
| + "/UE " + PDFText.toHex(ueValue) + "\n" |
| + "/Perms " + PDFText.toHex(computePermsValue(permissions)) + "\n" |
| + "/EncryptMetadata " + encryptMetadata + "\n" |
| // note: I think Length below should be 256 but Acrobat 9 uses 32... |
| + "/CF <</StdCF <</AuthEvent /DocOpen /CFM /AESV3 /Length 32>>>>\n" |
| + "/StmF /StdCF /StrF /StdCF\n"; |
| return encryptionDictionaryPart; |
| } |
| |
| /** |
| * Algorithm 3.8-1 (page 20, Adobe Supplement to the ISO 32000, BaseVersion: 1.7, ExtensionLevel: 3) |
| */ |
| @Override |
| protected void computeUValue() { |
| byte[] userBytes = new byte[16]; |
| random.nextBytes(userBytes); |
| System.arraycopy(userBytes, 0, userValidationSalt, 0, 8); |
| System.arraycopy(userBytes, 8, userKeySalt, 0, 8); |
| digest.reset(); |
| byte[] prepared = preparedUserPassword; |
| byte[] concatenated = new byte[prepared.length + 8]; |
| System.arraycopy(prepared, 0, concatenated, 0, prepared.length); |
| System.arraycopy(userValidationSalt, 0, concatenated, prepared.length, 8); |
| digest.update(concatenated); |
| byte[] sha256 = digest.digest(); |
| uValue = new byte[48]; |
| System.arraycopy(sha256, 0, uValue, 0, 32); |
| System.arraycopy(userValidationSalt, 0, uValue, 32, 8); |
| System.arraycopy(userKeySalt, 0, uValue, 40, 8); |
| } |
| |
| /** |
| * Algorithm 3.9-1 (page 20, Adobe Supplement to the ISO 32000, BaseVersion: 1.7, ExtensionLevel: 3) |
| */ |
| @Override |
| protected void computeOValue() { |
| byte[] ownerBytes = new byte[16]; |
| random.nextBytes(ownerBytes); |
| System.arraycopy(ownerBytes, 0, ownerValidationSalt, 0, 8); |
| System.arraycopy(ownerBytes, 8, ownerKeySalt, 0, 8); |
| digest.reset(); |
| byte[] prepared = preparedOwnerPassword; |
| byte[] concatenated = new byte[prepared.length + 56]; |
| System.arraycopy(prepared, 0, concatenated, 0, prepared.length); |
| System.arraycopy(ownerValidationSalt, 0, concatenated, prepared.length, 8); |
| System.arraycopy(uValue, 0, concatenated, prepared.length + 8, 48); |
| digest.update(concatenated); |
| byte[] sha256 = digest.digest(); |
| oValue = new byte[48]; |
| System.arraycopy(sha256, 0, oValue, 0, 32); |
| System.arraycopy(ownerValidationSalt, 0, oValue, 32, 8); |
| System.arraycopy(ownerKeySalt, 0, oValue, 40, 8); |
| } |
| |
| /** |
| * See Adobe Supplement to the ISO 32000, BaseVersion: 1.7, ExtensionLevel: 3, page 20, paragraph 5. |
| */ |
| protected void createEncryptionKey() { |
| encryptionKey = new byte[encryptionLengthInBytes]; |
| random.nextBytes(encryptionKey); |
| } |
| |
| /** |
| * Algorithm 3.2a-1 (page 19, Adobe Supplement to the ISO 32000, BaseVersion: 1.7, ExtensionLevel: 3) |
| */ |
| protected byte[] preparePassword(String password) { |
| byte[] passwordBytes; |
| byte[] preparedPassword; |
| try { |
| // the password needs to be normalized first but we are bypassing that step for now |
| passwordBytes = password.getBytes("UTF-8"); |
| if (passwordBytes.length > 127) { |
| preparedPassword = new byte[127]; |
| System.arraycopy(passwordBytes, 0, preparedPassword, 0, 127); |
| } else { |
| preparedPassword = new byte[passwordBytes.length]; |
| System.arraycopy(passwordBytes, 0, preparedPassword, 0, passwordBytes.length); |
| } |
| return preparedPassword; |
| } catch (UnsupportedEncodingException e) { |
| throw new UnsupportedOperationException(e.getMessage()); |
| } |
| } |
| |
| /** |
| * Algorithm 3.8-2 (page 20, Adobe Supplement to the ISO 32000, BaseVersion: 1.7, ExtensionLevel: 3) |
| */ |
| private void computeUEValue() { |
| digest.reset(); |
| byte[] prepared = preparedUserPassword; |
| byte[] concatenated = new byte[prepared.length + 8]; |
| System.arraycopy(prepared, 0, concatenated, 0, prepared.length); |
| System.arraycopy(userKeySalt, 0, concatenated, prepared.length, 8); |
| digest.update(concatenated); |
| byte[] ueEncryptionKey = digest.digest(); |
| ueValue = encryptWithKey(ueEncryptionKey, encryptionKey, true, ivZero); |
| } |
| |
| /** |
| * Algorithm 3.9-2 (page 20, Adobe Supplement to the ISO 32000, BaseVersion: 1.7, ExtensionLevel: 3) |
| */ |
| private void computeOEValue() { |
| digest.reset(); |
| byte[] prepared = preparedOwnerPassword; |
| byte[] concatenated = new byte[prepared.length + 56]; |
| System.arraycopy(prepared, 0, concatenated, 0, prepared.length); |
| System.arraycopy(ownerKeySalt, 0, concatenated, prepared.length, 8); |
| System.arraycopy(uValue, 0, concatenated, prepared.length + 8, 48); |
| digest.update(concatenated); |
| byte[] oeEncryptionKey = digest.digest(); |
| oeValue = encryptWithKey(oeEncryptionKey, encryptionKey, true, ivZero); |
| } |
| |
| /** |
| * Algorithm 3.10 (page 20, Adobe Supplement to the ISO 32000, BaseVersion: 1.7, ExtensionLevel: 3) |
| */ |
| public byte[] computePermsValue(int permissions) { |
| byte[] perms = new byte[16]; |
| long extendedPermissions = 0xffffffff00000000L | permissions; |
| for (int k = 0; k < 8; k++) { |
| perms[k] = (byte) (extendedPermissions & 0xff); |
| extendedPermissions >>= 8; |
| } |
| if (encryptMetadata) { |
| perms[8] = 'T'; |
| } else { |
| perms[8] = 'F'; |
| } |
| perms[9] = 'a'; |
| perms[10] = 'd'; |
| perms[11] = 'b'; |
| byte[] randomBytes = new byte[4]; |
| random.nextBytes(randomBytes); |
| System.arraycopy(randomBytes, 0, perms, 12, 4); |
| byte[] encryptedPerms = encryptWithKey(encryptionKey, perms, true, ivZero); |
| return encryptedPerms; |
| } |
| } |
| |
| private class EncryptionFilter extends PDFFilter { |
| |
| private PDFObjectNumber streamNumber; |
| |
| private int streamGeneration; |
| |
| EncryptionFilter(PDFObjectNumber streamNumber, int streamGeneration) { |
| this.streamNumber = streamNumber; |
| this.streamGeneration = streamGeneration; |
| } |
| |
| /** |
| * Returns a PDF string representation of this filter. |
| * |
| * @return the empty string |
| */ |
| public String getName() { |
| return ""; |
| } |
| |
| /** |
| * Returns a parameter dictionary for this filter. |
| * |
| * @return null, this filter has no parameters |
| */ |
| public PDFObject getDecodeParms() { |
| return null; |
| } |
| |
| /** {@inheritDoc} */ |
| public OutputStream applyFilter(OutputStream out) throws IOException { |
| if (useAlgorithm31a) { |
| byte[] iv = new byte[16]; |
| random.nextBytes(iv); |
| Cipher cipher = initCipher(encryptionKey, false, iv); |
| out.write(iv); |
| out.flush(); |
| return new CipherOutputStream(out, cipher); |
| } else { |
| byte[] key = createEncryptionKey(streamNumber.getNumber(), streamGeneration); |
| Cipher cipher = initCipher(key); |
| return new CipherOutputStream(out, cipher); |
| } |
| } |
| |
| } |
| |
| private PDFEncryptionJCE(PDFObjectNumber objectNumber, PDFEncryptionParams params, PDFDocument pdf) { |
| setObjectNumber(objectNumber); |
| try { |
| if (params.getEncryptionLengthInBits() == 256) { |
| digest = MessageDigest.getInstance("SHA-256"); |
| } else { |
| digest = MessageDigest.getInstance("MD5"); |
| } |
| } catch (NoSuchAlgorithmException e) { |
| throw new UnsupportedOperationException(e.getMessage()); |
| } |
| setDocument(pdf); |
| EncryptionInitializer encryptionInitializer = new EncryptionInitializer(params); |
| encryptionInitializer.init(); |
| useAlgorithm31a = encryptionInitializer.isVersion5Revision5Algorithm(); |
| } |
| |
| /** |
| * Creates and returns an encryption object. |
| * |
| * @param objectNumber the object number for the encryption dictionary |
| * @param params the encryption parameters |
| * @param pdf the PDF document to be encrypted |
| * @return the newly created encryption object |
| */ |
| public static PDFEncryption make( |
| PDFObjectNumber objectNumber, PDFEncryptionParams params, PDFDocument pdf) { |
| return new PDFEncryptionJCE(objectNumber, params, pdf); |
| } |
| |
| /** {@inheritDoc} */ |
| public byte[] encrypt(byte[] data, PDFObject refObj) { |
| PDFObject o = refObj; |
| while (o != null && !o.hasObjectNumber()) { |
| o = o.getParent(); |
| } |
| if (o == null && !useAlgorithm31a) { |
| throw new IllegalStateException("No object number could be obtained for a PDF object"); |
| } |
| if (useAlgorithm31a) { |
| byte[] iv = new byte[16]; |
| random.nextBytes(iv); |
| byte[] encryptedData = encryptWithKey(encryptionKey, data, false, iv); |
| byte[] storedData = new byte[encryptedData.length + 16]; |
| System.arraycopy(iv, 0, storedData, 0, 16); |
| System.arraycopy(encryptedData, 0, storedData, 16, encryptedData.length); |
| return storedData; |
| } else { |
| byte[] key = createEncryptionKey(o.getObjectNumber().getNumber(), o.getGeneration()); |
| return encryptWithKey(key, data); |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| public void applyFilter(AbstractPDFStream stream) { |
| if (!encryptMetadata && stream instanceof PDFMetadata) { |
| return; |
| } |
| stream.getFilterList().addFilter( |
| new EncryptionFilter(stream.getObjectNumber(), stream.getGeneration())); |
| } |
| |
| /** |
| * Prepares the encryption dictionary for output to a PDF file. |
| * |
| * @return the encryption dictionary as a byte array |
| */ |
| public byte[] toPDF() { |
| assert encryptionDictionary != null; |
| return encode(this.encryptionDictionary); |
| } |
| |
| /** {@inheritDoc} */ |
| public String getTrailerEntry() { |
| return "/Encrypt " + getObjectNumber() + " " + getGeneration() + " R\n"; |
| } |
| |
| private static byte[] encryptWithKey(byte[] key, byte[] data) { |
| 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 static byte[] encryptWithKey(byte[] key, byte[] data, boolean noPadding, byte[] iv) { |
| try { |
| final Cipher c = initCipher(key, noPadding, iv); |
| return c.doFinal(data); |
| } catch (IllegalBlockSizeException e) { |
| throw new IllegalStateException(e.getMessage()); |
| } catch (BadPaddingException e) { |
| throw new IllegalStateException(e.getMessage()); |
| } |
| } |
| |
| private static Cipher initCipher(byte[] key) { |
| try { |
| SecretKeySpec keyspec = new SecretKeySpec(key, "RC4"); |
| Cipher cipher = Cipher.getInstance("RC4"); |
| cipher.init(Cipher.ENCRYPT_MODE, keyspec); |
| return cipher; |
| } catch (InvalidKeyException e) { |
| throw new IllegalStateException(e); |
| } catch (NoSuchAlgorithmException e) { |
| throw new UnsupportedOperationException(e); |
| } catch (NoSuchPaddingException e) { |
| throw new UnsupportedOperationException(e); |
| } |
| } |
| |
| private static Cipher initCipher(byte[] key, boolean noPadding, byte[] iv) { |
| try { |
| SecretKeySpec skeySpec = new SecretKeySpec(key, "AES"); |
| IvParameterSpec ivspec = new IvParameterSpec(iv); |
| Cipher cipher = noPadding ? Cipher.getInstance("AES/CBC/NoPadding") : Cipher |
| .getInstance("AES/CBC/PKCS5Padding"); |
| cipher.init(Cipher.ENCRYPT_MODE, skeySpec, ivspec); |
| return cipher; |
| } catch (InvalidKeyException e) { |
| throw new IllegalStateException(e); |
| } catch (NoSuchAlgorithmException e) { |
| throw new UnsupportedOperationException(e); |
| } catch (NoSuchPaddingException e) { |
| throw new UnsupportedOperationException(e); |
| } catch (InvalidAlgorithmParameterException e) { |
| throw new UnsupportedOperationException(e); |
| } |
| } |
| |
| /** |
| * Applies Algorithm 3.1 from the PDF 1.4 Reference. |
| * |
| * @param objectNumber the object number |
| * @param generationNumber the generation number |
| * @return the key to use for encryption |
| */ |
| private byte[] createEncryptionKey(int objectNumber, int generationNumber) { |
| // Step 1 passed in |
| // Step 2 |
| byte[] md5Input = prepareMD5Input(objectNumber, generationNumber); |
| // Step 3 |
| digest.reset(); |
| byte[] hash = digest.digest(md5Input); |
| // Step 4 |
| int keyLength = Math.min(16, md5Input.length); |
| byte[] key = new byte[keyLength]; |
| System.arraycopy(hash, 0, key, 0, keyLength); |
| return key; |
| } |
| |
| private byte[] prepareMD5Input(int objectNumber, int generationNumber) { |
| byte[] md5Input = new byte[encryptionKey.length + 5]; |
| System.arraycopy(encryptionKey, 0, md5Input, 0, encryptionKey.length); |
| int i = encryptionKey.length; |
| md5Input[i++] = (byte) (objectNumber >>> 0); |
| md5Input[i++] = (byte) (objectNumber >>> 8); |
| md5Input[i++] = (byte) (objectNumber >>> 16); |
| md5Input[i++] = (byte) (generationNumber >>> 0); |
| md5Input[i++] = (byte) (generationNumber >>> 8); |
| return md5Input; |
| } |
| |
| /** {@inheritDoc} */ |
| public Version getPDFVersion() { |
| return pdfVersion; |
| } |
| |
| } |