All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sun.pdfview.decrypt.StandardDecrypter Maven / Gradle / Ivy

The newest version!
/*
 * 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 com.sun.pdfview.PDFStringUtil;

import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.security.*;
import java.util.List;
import java.util.Arrays;

/**
 * Standard simple decrypter for versions 1, 2 and 4 of the Standard
 * password-based decryption mechanisms, as described in section 3.5 of
 * the PDF Reference version 1.7.
 *
 * @author Luke Kirby
 */
public class StandardDecrypter implements PDFDecrypter {

    /**
     * Extra salt to add to AES-based decryption keys, as per PDF Reference 1.7
     */
    private static final byte[] AESV2_SALT = {'s', 'A', 'l', 'T'};

    /**
     * Describes an encryption algorithm to be used, declaring not only the
     * cipher type, but also key generation techniques
     */
    public enum EncryptionAlgorithm {
        RC4, AESV2;

        boolean isRC4() {
            return this == RC4;
        }

        boolean isAES() {
            return this == AESV2;
        }

    }

    /**
     * Padding used to bring passwords up to 32 bytes, as specified by the
     * first step of Algorithm 3.2 in the PDF Reference version 1.7.
     */
    private final static byte[] PW_PADDING = new byte[]{
            0x28, (byte) 0xBF, 0x4E, 0x5E, 0x4E, 0x75, (byte) 0x8A, 0x41,
            0x64, 0x00, 0x4E, 0x56, (byte) 0xFF, (byte) 0xFA, 0x01, 0x08,
            0x2E, 0x2E, 0x00, (byte) 0xB6, (byte) 0xD0, 0x68, 0x3E, (byte) 0x80,
            0x2F, 0x0C, (byte) 0xA9, (byte) 0xFE, 0x64, 0x53, 0x69, 0x7A
    };

    /**
     * The specification of the RC4 cipher for JCE interactions
     */
    private static final String CIPHER_RC4 = "RC4";
    /**
     * The key type for RC4 keys
     */
    private static final String KEY_RC4 = "RC4";

    /**
     * The specification of the AES cipher for JCE interactions. As per the
     * spec, cipher-block chanining (CBC) mode and PKCS5 padding are used
     */
    private static final String CIPHER_AES = "AES/CBC/PKCS5Padding";
    /**
     * The key type for AES keys
     */
    private static final String KEY_AES = "AES";

    /**
     * Whether the owner password was specified
     */
    private boolean ownerAuthorised = false;

    /**
     * The general encryption key; may be mutated to form individual
     * stream/string encryption keys
     */
    private byte[] generalKeyBytes;

    /**
     * The encryption algorithm being employed
     */
    private EncryptionAlgorithm encryptionAlgorithm;

    /**
     * Class constructor
     *
     * @param encryptionAlgorithm the algorithm used for encryption
     * @param documentId the contents of the ID entry of the document's trailer
     * dictionary; can be null, but according to the spec, shouldn't be. Is
     * expected to be an array of two byte sequences.
     * @param keyBitLength the length of the key in bits; should be a multiple
     * of 8 between 40 and 128
     * @param revision the revision of the Standard encryption security handler
     * being employed. Should be 2, 3 or 4.
     * @param oValue the value of the O entry from the Encrypt dictionary
     * @param uValue the value of the U entry from the Encrypt dictionary
     * @param pValue the value of the P entry from the Encrypt dictionary
     * @param encryptMetadata whether metadata is being encrypted, as identified
     * by the Encrypt dict (with default true if not explicitly identified)
     * @param password the password; not null
     * @throws IOException if there's a problem reading the file
     * @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
     */
    public StandardDecrypter(
            EncryptionAlgorithm encryptionAlgorithm,
            PDFObject documentId, int keyBitLength,
            int revision, byte[] oValue, byte[] uValue, int pValue,
            boolean encryptMetadata, PDFPassword password)
            throws
            IOException,
            EncryptionUnsupportedByProductException,
            EncryptionUnsupportedByPlatformException {

        this.encryptionAlgorithm = encryptionAlgorithm;

        // The spec (sensibly) demands that the documentId be present,
        // but we'll play it safe
        final byte[] firstDocIdValue;
        if (documentId == null) {
            firstDocIdValue = null;
        } else {
            firstDocIdValue = documentId.getAt(0).getStream();
        }

        testJceAvailability(keyBitLength);

        try {
            final List passwordBytePossibilities =
                    password.getPasswordBytes(false);
            for (int i = 0;
                    generalKeyBytes == null && i < passwordBytePossibilities.size();
                    ++i) {
                final byte[] passwordBytes = passwordBytePossibilities.get(i);
                generalKeyBytes = checkOwnerPassword(
                        passwordBytes, firstDocIdValue, keyBitLength,
                        revision, oValue, uValue, pValue, encryptMetadata);
                if (generalKeyBytes != null) {
                    // looks like the password was the owner password!
                    ownerAuthorised = true;
                } else {
                    // try it as the user password
                    generalKeyBytes = checkUserPassword(
                            passwordBytes, firstDocIdValue, keyBitLength,
                            revision, oValue, uValue, pValue, encryptMetadata);

                }
            }
        } catch (GeneralSecurityException e) {
            // Unexpected, as our test of JCE availability should have caught
            // problems with cipher availability.
            // It may well be a problem with document content?
            throw new PDFParseException("Unable to check passwords: " +
                    e.getMessage(), e);
        }

        if (generalKeyBytes == null) {
            throw new PDFAuthenticationFailureException(
                    "Password failed authentication for both " +
                            "owner and user password");
        }

    }

    public ByteBuffer decryptBuffer(
            String cryptFilterName, PDFObject streamObj, ByteBuffer streamBuf)
            throws PDFParseException {

        if (cryptFilterName != null) {
            throw new PDFParseException(
                    "This encryption version does not support Crypt filters");
        }

        if (streamObj != null) {
            checkNums(streamObj.getObjNum(), streamObj.getObjGen());
        }

        final byte[] decryptionKeyBytes;
        if (streamObj == null) {
            // lack of a stream object indicates the unsalted key should be
            // used
            decryptionKeyBytes = getUnsaltedDecryptionKey();
        } else {
            decryptionKeyBytes = getObjectSaltedDecryptionKey(
                    streamObj.getObjNum(), streamObj.getObjGen());
        }
        return decryptBuffer(streamBuf, decryptionKeyBytes);
    }

    public String decryptString(int objNum, int objGen, String inputBasicString)
            throws PDFParseException {
        final byte[] crypted = PDFStringUtil.asBytes(inputBasicString);
        final byte[] decryptionKey = getObjectSaltedDecryptionKey(objNum, objGen);
        final ByteBuffer decrypted = decryptBuffer(ByteBuffer.wrap(crypted), decryptionKey);
        return PDFStringUtil.asBasicString(decrypted.array(), decrypted.arrayOffset(), decrypted.limit());
    }

    public boolean isOwnerAuthorised() {
        return ownerAuthorised;
    }

    public boolean isEncryptionPresent() {
        return true;
    }

    /**
     * Test that the platform (i.e., the JCE) can offer us all of the ciphers at
     * the key length we need for content decryption. This shouldn't be a
     * problem on the Java 5 platform unless a particularly restrictive policy
     * file is in place. Calling this on construction should avoid problems like
     * these being exposed as PDFParseExceptions as they're used during
     * decryption and key establishment.
     *
     * @param keyBitLength the length of the content key, in bits
     * @throws EncryptionUnsupportedByPlatformException if the platform does not
     * support the required ciphers and key lengths
     * @throws PDFParseException if there's an internal error while testing
     * cipher availability
     */
    private void testJceAvailability(int keyBitLength)
            throws
            EncryptionUnsupportedByPlatformException, PDFParseException {

        // we need to supply a little buffer for AES, which will look
        // for an initialisation vector of 16 bytes
        final byte[] junkBuffer = new byte[16];
        Arrays.fill(junkBuffer, (byte) 0xAE);
        // test using the longer key length for salted content so that
        // we can check for maximum key length problems
        final byte[] junkKey =
                new byte[getSaltedContentKeyByteLength(keyBitLength / 8)];
        Arrays.fill(junkKey, (byte) 0xAE);

        try {
            createAndInitialiseContentCipher(
                    ByteBuffer.wrap(junkBuffer),
                    junkKey);
        } catch (PDFParseException e) {
            throw new PDFParseException("Internal error; " +
                    "failed to produce test cipher: " + e.getMessage());
        } catch (NoSuchAlgorithmException e) {
            throw new EncryptionUnsupportedByPlatformException(
                    "JCE does not offer required cipher", e);
        } catch (NoSuchPaddingException e) {
            throw new EncryptionUnsupportedByPlatformException(
                    "JCE does not offer required padding", e);
        } catch (InvalidKeyException e) {
            throw new EncryptionUnsupportedByPlatformException(
                    "JCE does accept key size of " +
                            (getSaltedContentKeyByteLength() * 8) +
                            " bits- could it be a policy restriction?", e);
        } catch (InvalidAlgorithmParameterException e) {
            throw new EncryptionUnsupportedByPlatformException(
                    "JCE did not accept cipher parameter", e);
        }

        try {
            createMD5Digest();
        } catch (NoSuchAlgorithmException e) {
            throw new EncryptionUnsupportedByPlatformException(
                    "No MD5 digest available from JCE", e);
        }

        if (encryptionAlgorithm != EncryptionAlgorithm.RC4) {
            // we still need RC4 for U and O value checks. Check again!
            final Cipher rc4;
            try {
                rc4 = createRC4Cipher();
            } catch (GeneralSecurityException e) {
                throw new EncryptionUnsupportedByPlatformException(
                        "JCE did not offer RC4 cipher", e);
            }
            // 40 byte key is used for base U and O ciphers
            final byte[] rc4JunkKey = new byte[5];
            Arrays.fill(junkKey, (byte) 0xAE);
            try {
                initDecryption(rc4, createRC4Key(rc4JunkKey));
            } catch (InvalidKeyException ex) {
                throw new EncryptionUnsupportedByPlatformException(
                        "JCE did not accept 40-bit RC4 key; " +
                                "policy problem?",
                        ex);
            }
        }
    }

    /**
     * Decrypt a buffer
     *
     * @param encrypted the encrypted content
     * @param decryptionKeyBytes the key to use for decryption
     * @return a freshly allocated buffer containing the decrypted content
     * @throws PDFParseException if there's a problem decrypting the content
     */
    private ByteBuffer decryptBuffer(
            ByteBuffer encrypted, byte[] decryptionKeyBytes)
            throws PDFParseException {

        final Cipher cipher;
        try {
            cipher = createAndInitialiseContentCipher(
                    encrypted, decryptionKeyBytes);
        } catch (GeneralSecurityException e) {
            // we should have caught this earlier in testCipherAvailability
            throw new PDFParseException(
                    "Unable to create cipher due to platform limitation: " +
                            e.getMessage(), e);
        }

        try {
            // the decrypted content will never be more than the encrypted
            // content. Thanks to padding, this buffer will be at most 16
            // bytes bigger than the encrypted content
            final ByteBuffer decryptedBuf =
                    ByteBuffer.allocate(encrypted.remaining());
            cipher.doFinal(encrypted, decryptedBuf);
            decryptedBuf.flip();
            return decryptedBuf;
        } catch (GeneralSecurityException e) {
            throw new PDFParseException(
                    "Could not decrypt: " + e.getMessage(), e);
        }
    }

    /**
     * Setup the cipher for decryption
     *
     * @param encrypted the encrypted content; required by AES encryption so
     * that the initialisation vector can be established
     * @param decryptionKeyBytes the bytes for the decryption key
     * @return a content decryption cypher, ready to accept input
     * @throws PDFParseException if the encrypted buffer is malformed or on an
     * internal error
     * @throws NoSuchAlgorithmException if the cipher algorithm is not supported
     * by the platform
     * @throws NoSuchPaddingException if the cipher padding is not supported by
     * the platform
     * @throws InvalidKeyException if the key is invalid according to the
     * cipher, or too long
     * @throws InvalidAlgorithmParameterException if the cipher parameters are
     * bad
     */
    private Cipher createAndInitialiseContentCipher(
            ByteBuffer encrypted,
            byte[] decryptionKeyBytes)
            throws
            PDFParseException,
            NoSuchAlgorithmException,
            NoSuchPaddingException,
            InvalidKeyException,
            InvalidAlgorithmParameterException {

        final Cipher cipher;
        if (encryptionAlgorithm.isRC4()) {
            cipher = Cipher.getInstance(CIPHER_RC4);
            cipher.init(Cipher.DECRYPT_MODE, createRC4Key(decryptionKeyBytes));
        } else if (encryptionAlgorithm.isAES()) {
            cipher = createAESCipher();
            final byte[] initialisationVector = new byte[16];
            if (encrypted.remaining() >= initialisationVector.length) {
                encrypted.get(initialisationVector);
            } else {
                throw new PDFParseException(
                        "AES encrypted stream too short - " +
                                "no room for initialisation vector");
            }

            final SecretKeySpec aesKey =
                    new SecretKeySpec(decryptionKeyBytes, KEY_AES);
            final IvParameterSpec aesIv =
                    new IvParameterSpec(initialisationVector);
            cipher.init(Cipher.DECRYPT_MODE, aesKey, aesIv);
        } else {
            throw new PDFParseException(
                    "Internal error - unhandled cipher type: " +
                            encryptionAlgorithm);
        }
        return cipher;
    }

    /**
     * Get the unsalted content decryption key, used for streams with specific
     * crypt filters, which aren't specific to particular objects
     *
     * @return the general key
     */
    private byte[] getUnsaltedDecryptionKey() {
        return generalKeyBytes;
    }

    /**
     * Get a decryption key salted with an object number and object generation,
     * for use when decrypting a string or stream within an object numbered so
     *
     * @param objNum the object number
     * @param objGen the object generation
     * @return the key to be used for decrypting data associated with the object
     *         numbered so
     * @throws PDFParseException if the MD5 digest is not available
     */
    private byte[] getObjectSaltedDecryptionKey(int objNum, int objGen)
            throws PDFParseException {

        byte[] decryptionKeyBytes;
        final MessageDigest md5;
        try {
            md5 = createMD5Digest();
        } catch (NoSuchAlgorithmException e) {
            // unexpected, as we will already have tested availability
            throw new PDFParseException("Unable to get MD5 digester", e);
        }
        md5.update(this.generalKeyBytes);
        md5.update((byte) objNum);
        md5.update((byte) (objNum >> 8));
        md5.update((byte) (objNum >> 16));
        md5.update((byte) objGen);
        md5.update((byte) (objGen >> 8));
        if (encryptionAlgorithm == EncryptionAlgorithm.AESV2) {
            md5.update(AESV2_SALT);
        }
        final byte[] hash = md5.digest();
        final int keyLen = getSaltedContentKeyByteLength();
        decryptionKeyBytes = new byte[keyLen];
        System.arraycopy(hash, 0, decryptionKeyBytes, 0, keyLen);
        return decryptionKeyBytes;
    }

    /**
     * Get the length of a salted key
     *
     * @return length in bytes
     */
    private int getSaltedContentKeyByteLength() {
        return getSaltedContentKeyByteLength(generalKeyBytes.length);
    }

    /**
     * Get the length of salted keys, in bytes. Unsalted keys will be the same
     * length as {@link #generalKeyBytes}
     *
     * @param generalKeyByteLength the length of the general key, in bytes
     * @return byte length of salted keys
     */
    private int getSaltedContentKeyByteLength(int generalKeyByteLength) {
        return Math.min(generalKeyByteLength + 5, 16);
    }

    /**
     * Check that object number and object generations are well-formed. It is
     * possible for some {@link PDFObject}s to have uninitialised object numbers
     * and generations, but such objects should not required decryption
     *
     * @param objNum the object number
     * @param objGen the object generation
     * @throws PDFParseException if the object numbering indicates that they
     * aren't true object numbers
     */
    private void checkNums(int objNum, int objGen)
            throws PDFParseException {
        if (objNum < 0) {
            throw new PDFParseException(
                    "Internal error: Object has bogus object number");
        } else if (objGen < 0) {
            throw new PDFParseException(
                    "Internal error: Object has bogus generation number");
        }
    }

    /**
     * Calculate what the U value should consist of given a particular key and
     * document configuration. Correponds to Algorithms 3.4 and 3.5 of the
     * PDF Reference version 1.7
     *
     * @param generalKey the general encryption key
     * @param firstDocIdValue the value of the first element in the document's
     * ID entry in the trailer dictionary
     * @param revision the revision of the security handler
     * @return the U value for the given configuration
     * @throws GeneralSecurityException if there's an error getting required
     * ciphers, etc. (unexpected, since a check for algorithm availability is
     * performed on construction)
     * @throws EncryptionUnsupportedByProductException if the revision is not
     * supported
     */
    private byte[] calculateUValue(
            byte[] generalKey, byte[] firstDocIdValue, int revision)
            throws
            GeneralSecurityException,
            EncryptionUnsupportedByProductException {

        if (revision == 2) {

            // Algorithm 3.4: Computing the encryption dictionary’s U (user
            // password) value (Revision 2)

            // Step 1 is provided to us as the parameter generalKey:
            //  Create an encryption key based on the user password string, as
            //  described in Algorithm 3.2

            // Step 2: Encrypt the 32-byte padding string shown in step 1 of
            // Algorithm 3.2, using an RC4 encryption function with the
            // encryption key from the preceding step.

            Cipher rc4 = createRC4Cipher();
            SecretKey key = createRC4Key(generalKey);
            initEncryption(rc4, key);
            return crypt(rc4, PW_PADDING);

        } else if (revision >= 3) {

            // Algorithm 3.5: Computing the encryption dictionary’s U (user
            // password) value (Revision 3 or greater)

            // Step 1 is provided to us as the parameter generalKey:
            //  Create an encryption key based on the user password string, as
            //  described in Algorithm 3.2

            // Step 2: Initialize the MD5 hash function and pass the 32-byte
            // padding string shown in step 1 of Algorithm 3.2 as input to this
            // function
            MessageDigest md5 = createMD5Digest();
            md5.update(PW_PADDING);

            // Step 3: Pass the first element of the file’s file identifier
            // array (the value of the ID entry in the document’s trailer
            // dictionary; see Table 3.13 on page 97) to the hash function and
            // finish the hash. (See implementation note 26 in Appendix H.)
            if (firstDocIdValue != null) {
                md5.update(firstDocIdValue);
            }
            final byte[] hash = md5.digest();

            // Step 4: Encrypt the 16-byte result of the hash, using an RC4
            // encryption function with the encryption key from step 1.
            Cipher rc4 = createRC4Cipher();
            SecretKey key = createRC4Key(generalKey);
            initEncryption(rc4, key);
            final byte[] v = crypt(rc4, hash);

            // Step 5: Do the following 19 times: Take the output from the
            // previous invocation of the RC4 function and pass it as input to
            // a new invocation of the function; use an encryption key generated
            // by taking each byte of the original encryption key (obtained in
            // step 1) and performing an XOR (exclusive or) operation between
            // that byte and the single-byte value of the iteration counter
            // (from 1 to 19).
            rc4shuffle(v, generalKey, rc4);

            // Step 6: Append 16 bytes of arbitrary padding to the output from
            // the final invocation of the RC4 function and store the 32-byte
            // result as the value of the U entry in the encryption dictionary.
            assert v.length == 16;
            final byte[] entryValue = new byte[32];
            System.arraycopy(v, 0, entryValue, 0, v.length);
            System.arraycopy(v, 0, entryValue, 16, v.length);
            return entryValue;

        } else {
            throw new EncryptionUnsupportedByProductException(
                    "Unsupported standard security handler revision " +
                            revision);
        }
    }

    /**
     * Calculate what the O value of the Encrypt dict should look like given a
     * particular configuration. Not used, but useful for reference; this
     * process is reversed to determine whether a given password is the
     * owner password. Corresponds to Algorithm 3.3 of the PDF Reference
     * version 1.7.
     *
     * @see #checkOwnerPassword
     * @param ownerPassword the owner password
     * @param userPassword the user password
     * @param keyBitLength the key length in bits (40-128)
     * @param revision the security handler revision
     * @return the O value entry
     * @throws GeneralSecurityException if ciphers are unavailable or
     *  inappropriately used
     */
    private byte[] calculuateOValue(
            byte[] ownerPassword, byte[] userPassword,
            int keyBitLength, int revision)
            throws GeneralSecurityException {

        // Steps 1-4
        final byte[] rc4KeyBytes =
                getInitialOwnerPasswordKeyBytes(
                        ownerPassword, keyBitLength, revision);
        final Cipher rc4 = createRC4Cipher();
        initEncryption(rc4, createRC4Key(rc4KeyBytes));

        // Step 5: Pad or truncate the user password string as described in step
        // 1 of Algorithm 3.2.
        // Step 6: Encrypt the result of step 5, using an RC4 encryption
        // function with the encryption key obtained in step 4.
        byte[] pwvalue = crypt(rc4, padPassword(userPassword));

        // Step 7: (Revision 3 or greater) Do the following 19 times: Take the
        // output from the previous invocation of the RC4 function and pass it
        // as input to a new invocation of the function; use an encryption key
        // generated by taking each byte of the encryption key obtained in step
        // 4 and performing an XOR (exclusive or) operation between
        if (revision >= 3) {
            rc4shuffle(pwvalue, rc4KeyBytes, rc4);
        }
        assert pwvalue.length == 32;
        return pwvalue;

    }

    /**
     * Check to see whether a given password is the owner password. Corresponds
     * to algorithm 3.6 of PDF Reference version 1.7.
     *
     * @param ownerPassword the suggested owner password (may be null or
     * empty)
     * @param firstDocIdValue the byte stream from the first element of the
     *  value of the ID entry in the trailer dictionary
     * @param keyBitLength the key length in bits
     * @param revision the security handler revision
     * @param oValue the O value from the Encrypt dictionary
     * @param uValue the U value from the Encrypt dictionary
     * @param pValue the P value from the Encrypt dictionary
     * @param encryptMetadata the EncryptMetadata entry from the Encrypt dictionary
     *  (or false if not present or revision <= 3)
     * @return the general/user key bytes if the owner password is currect,
     *  null otherwise
     * @throws GeneralSecurityException if there's a problem with
     * cipher or digest usage; unexpected
     * @throws EncryptionUnsupportedByProductException if PDFRenderer doesn't
     * support the security handler revision
     * @throws PDFParseException if the document is malformed
     */
    private byte[] checkOwnerPassword(
            byte[] ownerPassword, byte[] firstDocIdValue, int keyBitLength,
            int revision, byte[] oValue, byte[] uValue, int pValue,
            boolean encryptMetadata)
            throws
            GeneralSecurityException,
            EncryptionUnsupportedByProductException,
            PDFParseException {

        // Step 1: Compute an encryption key from the supplied password string,
        // as described in steps 1 to 4 of Algorithm 3.3.
        final byte[] rc4KeyBytes =
                getInitialOwnerPasswordKeyBytes(ownerPassword,
                        keyBitLength, revision);
        final Cipher rc4 = createRC4Cipher();
        initDecryption(rc4, createRC4Key(rc4KeyBytes));

        // Step 2:
        final byte[] possibleUserPassword;
        if (revision == 2) {
            // (Revision 2 only) Decrypt the value of the encryption
            // dictionary’s O entry, using an RC4 encryption function with the
            // encryption key computed in step 1.

            possibleUserPassword = crypt(rc4, oValue);
        } else if (revision >= 3) {
            // (Revision 3 or greater) Do the following 20 times: Decrypt the
            // value of the encryption dictionary’s O entry (first iteration) or
            // the output from the previous iteration (all subsequent
            // iterations), using an RC4 encryption function with a different
            // encryption key at each iteration. The key is generated by taking
            // the original key (obtained in step 1) and performing an XOR
            // (exclusive or) operation between each byte of the key and the
            // single-byte value of the iteration counter (from 19 to 0).

            // unshuffle the O entry; the unshuffle operation also
            // contains the final decryption with the original key
            possibleUserPassword = new byte[32];
            System.arraycopy(oValue, 0, possibleUserPassword, 0,
                    possibleUserPassword.length);
            rc4unshuffle(rc4, possibleUserPassword, rc4KeyBytes);
        } else {
            throw new EncryptionUnsupportedByProductException(
                    "Unsupported revision: " + revision);
        }

        // Step 3: The result of step 2 purports to be the user password.
        // Authenticate this user password using Algorithm 3.6. If it is
        // correct, the password supplied is the correct owner password.
        return checkUserPassword(
                possibleUserPassword, firstDocIdValue, keyBitLength,
                revision, oValue, uValue, pValue, encryptMetadata);

    }

    /**
     * Establish the key to be used for the generation and validation
     * of the user password via the O entry. Corresponds to steps 1-4 in
     * Algorithm 3.3 of the PDF Reference version 1.7.
     * @param ownerPassword the owner password
     * @param keyBitLength the length of the key in bits
     * @param revision the security handler revision
     * @return the key bytes to use for generation/validation of the O entry
     * @throws GeneralSecurityException if there's a problem wranling ciphers
     */
    private byte[] getInitialOwnerPasswordKeyBytes(
            byte[] ownerPassword, int keyBitLength, int revision)
            throws GeneralSecurityException {

        final MessageDigest md5 = createMD5Digest();

        // Step 1: Pad or truncate the owner password string as described in
        // step 1 of Algorithm 3.2. If there is no owner password, use the user
        // password instead. (See implementation note 27 in Appendix H.)
        // Step 2: Initialize the MD5 hash function and pass the result of step 1 as
        // input to this function.
        md5.update(padPassword(ownerPassword));

        // Step 3.(Revision 3 or greater) Do the following 50 times: Take the
        // output from the previous MD5 hash and pass it as input into a new MD5
        // hash
        final byte[] hash = md5.digest();
        if (revision >= 3) {
            for (int i = 0; i < 50; ++i) {
                md5.update(hash);
                digestTo(md5, hash);
            }
        }

        // Step 4: Create an RC4 encryption key using the first n bytes of
        // the output from the final MD5 hash, where n is always 5 for revision
        // 2 but, for revision 3 or greater, depends on the value of the
        // encryption dictionary’s Length entry
        final byte[] rc4KeyBytes = new byte[keyBitLength / 8];
        System.arraycopy(hash, 0, rc4KeyBytes, 0, rc4KeyBytes.length);
        return rc4KeyBytes;
    }

    /**
     * Check to see whether a provided user password is correct with respect
     * to an Encrypt dict configuration. Corresponds to algorithm 3.6 of
     * the PDF Reference version 1.7
     * @param userPassword the user password to test; may be null or empty
     * @param firstDocIdValue the byte stream from the first element of the
     *  value of the ID entry in the trailer dictionary
     * @param keyBitLength the length of the key in bits
     * @param revision the security handler revision
     * @param oValue the O value from the Encrypt dictionary
     * @param uValue the U value from the Encrypt dictionary
     * @param pValue the P value from the Encrypt dictionary
     * @param encryptMetadata the EncryptMetadata entry from the Encrypt dictionary
     *  (or false if not present or revision <= 3)
     * @return the general/user encryption key if the user password is correct,
     *  or null if incorrect
     * @throws GeneralSecurityException if there's a problem with
     * cipher or digest usage; unexpected
     * @throws EncryptionUnsupportedByProductException if PDFRenderer doesn't
     * support the security handler revision
     * @throws PDFParseException if the document is improperly constructed
     */
    private byte[] checkUserPassword(
            byte[] userPassword, byte[] firstDocIdValue, int keyBitLength,
            int revision, byte[] oValue, byte[] uValue, int pValue,
            boolean encryptMetadata)
            throws
            GeneralSecurityException,
            EncryptionUnsupportedByProductException,
            PDFParseException {

        // Algorithm 3.6: Authenticating the user password

        // Step 1: Perform all but the last step of Algorithm 3.4 (Revision 2)
        // or Algorithm 3.5 (Revision 3 or greater) using the supplied password
        // string
        //
        // I.e., figure out what the general key would be with the
        // given password
        // Algorithm 3.4/5,Step1:
        // Determine general key based on user password, as per Algorithm 3.2
        final byte[] generalKey = calculateGeneralEncryptionKey(
                userPassword, firstDocIdValue, keyBitLength,
                revision, oValue, pValue, encryptMetadata);
        // Algorithm 3.4/5,RemainingSteps:
        final byte[] calculatedUValue =
                calculateUValue(generalKey, firstDocIdValue, revision);

        // Step 2: If the result of step 1 is equal to the value of the
        // encryption dictionary’s U entry (comparing on the first 16 bytes in
        // the case of Revision 3 or greater), the password supplied is the
        // correct user password. The key obtained in step 1 (that is, in the
        // first step of Algorithm 3.4 or 3.5) can be used to decrypt the
        // document using Algorithm 3.1 on page 119.
        assert calculatedUValue.length == 32;
        if (uValue.length != calculatedUValue.length) {
            throw new PDFParseException("Improper U entry length; " +
                    "expected 32, is " + uValue.length);
        }
        // Only the first 16 bytes are significant if using revision > 2
        final int numSignificantBytes = revision == 2 ? 32 : 16;
        for (int i = 0; i < numSignificantBytes; ++i) {
            if (uValue[i] != calculatedUValue[i]) {
                return null;
            }
        }
        return generalKey;
    }


    /**
     * Determine what the general encryption key is, given a configuration. This
     * corresponds to Algorithm 3.2 of PDF Reference version 1.7.
     *
     * @param userPassword the desired user password; may be null or empty
     * @param firstDocIdValue the byte stream from the first element of the
     * value of the ID entry in the trailer dictionary
     * @param keyBitLength the length of the key in bits
     * @param revision the security handler revision
     * @param oValue the O value from the Encrypt dictionary
     * @param pValue the P value from the Encrypt dictionary
     * @param encryptMetadata the EncryptMetadata entry from the Encrypt
     * dictionary (or false if not present or revision <= 3)
     * @return the general encryption key
     * @throws GeneralSecurityException if an error occurs when obtaining
     *  and operating ciphers/digests
     */
    private byte[] calculateGeneralEncryptionKey(
            byte[] userPassword, byte[] firstDocIdValue, int keyBitLength,
            int revision, byte[] oValue, int pValue, boolean encryptMetadata)
            throws GeneralSecurityException {

        // Algorithm 3.2: Computing an encryption key

        // Step 1: Pad or truncate the password string to exactly 32 bytes...
        final byte[] paddedPassword = padPassword(userPassword);

        // Step 2: Initialize the MD5 hash function and pass the result of step
        // 1 as input to this function.
        MessageDigest md5 = createMD5Digest();
        md5.reset();
        md5.update(paddedPassword);

        // Step 3: Pass the value of the encryption dictionary’s O entry to the
        // MD5 hash function. (Algorithm 3.3 shows how the O value is computed.)
        md5.update(oValue);

        // Step 4: Treat the value of the P entry as an unsigned 4-byte integer
        // and pass these bytes to the MD5 hash function, low-order byte first
        md5.update((byte) (pValue & 0xFF));
        md5.update((byte) ((pValue >> 8) & 0xFF));
        md5.update((byte) ((pValue >> 16) & 0xFF));
        md5.update((byte) (pValue >> 24));

        // Step 5: Pass the first element of the file’s file identifier array
        // (the value of the ID entry in the document’s trailer dictionary; see
        // Table 3.13 on page 97) to the MD5 hash function. (See implementation
        // note 26 in Appendix H.)
        if (firstDocIdValue != null) {
            md5.update(firstDocIdValue);
        }

        // Step 6: (Revision 4 or greater) If document metadata is not being
        // encrypted, pass 4 bytes with the value 0xFFFFFFFF to the MD5 hash
        // function
        if (revision >= 4 && !encryptMetadata) {
            for (int i = 0; i < 4; ++i) {
                md5.update((byte) 0xFF);
            }
        }

        // Step 7: finish the hash
        byte[] hash = md5.digest();

        final int keyLen = revision == 2 ? 5 : (keyBitLength / 8);
        final byte[] key = new byte[keyLen];

        // Step 8: (Revision 3 or greater) Do the following 50 times: Take the
        // output from the previous MD5 hash and pass the first n bytes of the
        // output as input into a new MD5 hash, where n is the number of bytes
        // of the encryption key as defined by the value of the encryption
        // dictionary’s Length entry
        if (revision >= 3) {
            for (int i = 0; i < 50; ++i) {
                md5.update(hash, 0, key.length);
                digestTo(md5, hash);
            }
        }

        // Set the encryption key to the first n bytes of the output from the
        // final MD5 hash, where n is always 5 for revision 2 but, for revision
        // 3 or greater, depends on the value of the encryption dictionary’s
        // Length entry.
        System.arraycopy(hash, 0, key, 0, key.length);
        return key;
    }

    /**
     * Pad a password as per step 1 of Algorithm 3.2 of the PDF Reference
     * version 1.7
     * @param password the password, may be null or empty
     * @return the padded password, always 32 bytes long
     */
    private byte[] padPassword(byte[] password) {

        if (password == null) {
            password = new byte[0];
        }

        // Step 1: Pad or truncate the password string to exactly 32 bytes. If
        // the password string is more than 32 bytes long, use only its first 32
        // bytes; if it is less than 32 bytes long, pad it by appending the
        // required number of additional bytes from the beginning of the
        // following padding string:
        // < 28 BF 4E 5E 4E 75 8A 41 64 00 4E 56 FF FA 01 08
        //   2E 2E 00 B6 D0 68 3E 80 2F 0C A9 FE 64 53 69 7A >
        // That is, if the password string is n bytes long, append the first 32
        // − n bytes of the padding string to the end of the password string. If
        // the password string is empty (zero-length), meaning there is no user
        // password, substitute the entire padding string in its place.

        byte[] padded = new byte[32];
        // limit password to 32 bytes
        final int numContributingPasswordBytes =
                password.length > padded.length ?
                padded.length : password.length;
        System.arraycopy(password, 0, padded, 0, numContributingPasswordBytes);
        // Copy padding
        if (password.length < padded.length) {
            System.arraycopy(PW_PADDING, 0, padded, password.length,
                    padded.length - password.length);
        }
        return padded;
    }

    /**
     * Encrypt some bytes
     *
     * @param cipher the cipher
     * @param input the plaintext
     * @return the crypt text
     * @throws BadPaddingException if there's bad padding
     * @throws IllegalBlockSizeException if the block size is bad
     */
    private byte[] crypt(Cipher cipher, byte[] input)
            throws IllegalBlockSizeException, BadPaddingException {
        return cipher.doFinal(input);
    }

    /**
     * Initialise a cipher for encryption
     *
     * @param cipher the cipher
     * @param key the encryption key
     * @throws InvalidKeyException if the key is invalid for the cipher
     */
    private void initEncryption(Cipher cipher, SecretKey key)
            throws InvalidKeyException {
        cipher.init(Cipher.ENCRYPT_MODE, key);
    }

    /**
     * Shuffle some input using a series of RC4 encryptions with slight
     * mutations of an given key per iteration. Shuffling happens in place.
     * Refer to the documentation of the algorithm steps where this is called.
     *
     * @param shuffle the bytes to be shuffled
     * @param key the original key
     * @param rc4 the cipher to use
     * @throws GeneralSecurityException if there's a problem with cipher
     *  operation
     */
    private void rc4shuffle(byte[] shuffle, byte[] key, Cipher rc4)
            throws GeneralSecurityException {

        final byte[] shuffleKey = new byte[key.length];
        for (int i = 1; i <= 19; ++i) {
            for (int j = 0; j < shuffleKey.length; ++j) {
                shuffleKey[j] = (byte) (key[j] ^ i);
            }
            initEncryption(rc4, createRC4Key(shuffleKey));
            cryptInPlace(rc4, shuffle);
        }
    }

    /**
     * Reverse the {@link #rc4shuffle} operation, and the operation
     * that invariable preceeds it, thereby obtaining an original message
     * @param rc4 the RC4 cipher to use
     * @param shuffle the bytes in which shuffling will take place; unshuffling
     *  happens in place
     * @param key the encryption key
     * @throws GeneralSecurityException if there's a problem with cipher
     *  operation
     */
    private void rc4unshuffle(Cipher rc4, byte[] shuffle, byte[] key)
            throws GeneralSecurityException {

        // there's an extra unshuffle at the end with the original key -
        // this is why we end with i == 0, where the shuffle key will be the key
        final byte[] shuffleKeyBytes = new byte[key.length];
        for (int i = 19; i >= 0; --i) {
            for (int j = 0; j < shuffleKeyBytes.length; ++j) {
                shuffleKeyBytes[j] = (byte) (key[j] ^ i);
            }
            initDecryption(rc4, createRC4Key(shuffleKeyBytes));
            cryptInPlace(rc4, shuffle);
        }
    }

    /**
     * Encrypt/decrypt something in place
     * @param rc4 the cipher to use; must be a stream cipher producing
     *  identical output length to input (e.g., RC4)
     * @param buffer the buffer to read input from and write output to
     * @throws IllegalBlockSizeException if an inappropriate cipher is used
     * @throws ShortBufferException if an inappropriate cipher is used
     * @throws BadPaddingException if an inappropriate cipher is used
     */
    private void cryptInPlace(Cipher rc4, byte[] buffer)
            throws IllegalBlockSizeException, ShortBufferException, BadPaddingException {
        rc4.doFinal(buffer, 0, buffer.length, buffer);
    }

    /**
     * Setup a cipher for decryption
     * @param cipher the cipher
     * @param aKey the cipher key
     * @throws InvalidKeyException if the key is of an unacceptable size or
     *  doesn't belong to the cipher
     */
    private void initDecryption(Cipher cipher, Key aKey)
            throws InvalidKeyException {
        cipher.init(Cipher.DECRYPT_MODE, aKey);
    }

    /**
     * Create a new RC4 cipher. Should always be available for supported
     * platforms.
     * @return the cipher
     * @throws NoSuchAlgorithmException if the RC4 cipher is unavailable
     * @throws NoSuchPaddingException should not happen, as no padding
     *  is specified
     */
    private Cipher createRC4Cipher()
            throws NoSuchAlgorithmException, NoSuchPaddingException {
        return Cipher.getInstance(CIPHER_RC4);
    }

    /**
     * Create a new AES cipher. Should always be available for supported
     * platforms.
     * @return the new cipher
     * @throws NoSuchAlgorithmException if the AES cipher is unavailable
     * @throws NoSuchPaddingException if the required padding is unavailable
     */
    private Cipher createAESCipher()
            throws NoSuchAlgorithmException, NoSuchPaddingException {
        return Cipher.getInstance(CIPHER_AES);
    }

    /**
     * Create an MD5 digest. Should always be available for supported
     * platforms.
     * @return the MD5 digest
     * @throws NoSuchAlgorithmException if the digest is not available
     */
    private MessageDigest createMD5Digest()
            throws NoSuchAlgorithmException {
        return MessageDigest.getInstance("MD5");
    }

    /**
     * Create an RC4 key
     *
     * @param keyBytes the bytes for the key
     * @return the key
     */
    private SecretKeySpec createRC4Key(byte[] keyBytes) {
        return new SecretKeySpec(keyBytes, KEY_RC4);
    }

    /**
     * Hash into an existing byte array
     * @param md5 the MD5 digest
     * @param hash the hash destination
     * @throws GeneralSecurityException if there's a problem hashing; e.g.,
     *  if the buffer is too small
     */
    private void digestTo(MessageDigest md5, byte[] hash)
                throws GeneralSecurityException {
        md5.digest(hash, 0, hash.length);
    }


}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy