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

org.verapdf.tools.EncryptionToolsRevision4 Maven / Gradle / Ivy

There is a newer version: 1.26.1
Show newest version
/**
 * This file is part of veraPDF Parser, a module of the veraPDF project.
 * Copyright (c) 2015, veraPDF Consortium 
 * All rights reserved.
 *
 * veraPDF Parser is free software: you can redistribute it and/or modify
 * it under the terms of either:
 *
 * The GNU General public license GPLv3+.
 * You should have received a copy of the GNU General Public License
 * along with veraPDF Parser as the LICENSE.GPL file in the root of the source
 * tree.  If not, see http://www.gnu.org/licenses/ or
 * https://www.gnu.org/licenses/gpl-3.0.en.html.
 *
 * The Mozilla Public License MPLv2+.
 * You should have received a copy of the Mozilla Public License along with
 * veraPDF Parser as the LICENSE.MPL file in the root of the source tree.
 * If a copy of the MPL was not distributed with this file, you can obtain one at
 * http://mozilla.org/MPL/2.0/.
 */
package org.verapdf.tools;

import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;

/**
 * Contains methods for encryption and decryption of PDF files.
 *
 * @author Sergey Shemyakov
 */
public class EncryptionToolsRevision4 {

    private static final int PADDED_PASSWORD_LENGTH = 32;
    private static final int AMOUNT_OF_REPEATS_MD5 = 50;
    private static final int AMOUNT_OF_REPEATS_RC4 = 19;
    private static final int U_LENGTH = 16;
    private static final byte[] DEFAULT_PADDING_STRING = 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
    };
    private static final byte[] FF_STRING = new byte[]{-1, -1, -1, -1};

    private EncryptionToolsRevision4() {
    }

    /**
     * Method computes encryption key for given data as specified in 7.6.3.3.
     * Algorithm 2 of PDF32000_2008.
     *
     * @param password            is password string.
     * @param o                   is O value of encryption dict for standard
     *                            security handler.
     * @param p                   is P value of encryption dict for standard
     *                            security handler.
     * @param id                  is the value of the ID entry in the document’s
     *                            trailer dict.
     * @param revision            is R value of encryption dict for standard
     *                            security handler.
     * @param metadataIsEncrypted is true if metadata in file is encrypted.
     * @param length              is value of Length in encryption dict.
     * @return encryption key.
     */
    public static byte[] computeEncryptionKey(String password, byte[] o, int p,
                                              byte[] id, int revision,      // id is first ID in trailer
                                              boolean metadataIsEncrypted,
                                              int length) throws NoSuchAlgorithmException {
        MessageDigest md5 = MessageDigest.getInstance("MD5");
        md5.update(getPaddedPassword(password));
        md5.update(o);
        md5.update(intToBytesLowOrderFirst(p));
        md5.update(id);
        if (revision >= 4 && !metadataIsEncrypted) {
            md5.update(FF_STRING);
        }
        byte[] res = md5.digest();
        if (revision >= 3) {
            for (int i = 0; i < AMOUNT_OF_REPEATS_MD5; ++i) {
                md5.reset();
                md5.update(Arrays.copyOf(res, length / 8));
                res = md5.digest();
            }
        }
        return Arrays.copyOf(res, length / 8);
    }

    /**
     * Method computes O value in standard encryption dict for standard security
     * handler as specified in 7.6.3.4. Algorithm 3 of PDF32000_2008.
     *
     * @param ownerPassword is owner password.
     * @param revision      is R value of encryption dict for standard security
     *                      handler.
     * @param length        is value of Length in encryption dict.
     * @param userPassword  is user password.
     * @return value of O for encryption dict for standard security handler.
     */
    public static byte[] computeOValue(String ownerPassword, int revision, int length,
                                       String userPassword) throws NoSuchAlgorithmException {
        if (ownerPassword == null) {
            ownerPassword = userPassword;
        }
        byte[] password = getPaddedPassword(ownerPassword);
        MessageDigest md5 = MessageDigest.getInstance("MD5");
        md5.update(password);
        password = md5.digest();
        if (revision >= 3) {
            for (int i = 0; i < AMOUNT_OF_REPEATS_MD5; ++i) {
                md5.reset();
                md5.update(password);
                password = md5.digest();
            }
        }
        int actualLength = revision >= 3 ? length / 8 : 5;
        RC4Encryption rc4 = new RC4Encryption(Arrays.copyOf(password, actualLength));
        byte[] rc4Result = rc4.process(getPaddedPassword(ownerPassword));
        if (revision >= 3) {
            for (int i = 1; i <= AMOUNT_OF_REPEATS_RC4; i++) {
                rc4 = new RC4Encryption(modifyEncryptionKeyWithCounter(
                        Arrays.copyOf(password, actualLength), i));
                rc4Result = rc4.process(rc4Result);
            }
        }
        return rc4Result;
    }

    /**
     * Method computes U value in standard encryption dict for standard security
     * handler as specified in 7.6.3.4. Algorithm 4 and 5 of PDF32000_2008.
     *
     * @param password            is user password.
     * @param o                   is O value of encryption dict for standard
     *                            security handler.
     * @param p                   is P value of encryption dict for standard
     *                            security handler.
     * @param id                  is the value of the ID entry in the document’s
     *                            trailer dict.
     * @param revision            is R value of encryption dict for standard
     *                            security handler.
     * @param metadataIsEncrypted is true if metadata in file is encrypted.
     * @param length              is value of Length in encryption dict.
     * @return value of U for encryption dict for standard security handler.
     */
    public static byte[] computeUValue(String password, byte[] o, int p,
                                       byte[] id, int revision,
                                       boolean metadataIsEncrypted,
                                       int length) throws NoSuchAlgorithmException {
        if (revision == 2) {
            return computeUValueRevision2(password, o, p, id,
                    metadataIsEncrypted, length);
        }
        byte[] key = computeEncryptionKey(password, o, p, id, revision,
                metadataIsEncrypted, length);
        RC4Encryption rc4 = new RC4Encryption(key);
        MessageDigest md5 = MessageDigest.getInstance("MD5");
        md5.update(DEFAULT_PADDING_STRING);
        md5.update(id);
        byte[] md5Result = md5.digest();
        byte[] rc4Result = rc4.process(md5Result);
        for (int i = 1; i <= AMOUNT_OF_REPEATS_RC4; i++) {
            rc4 = new RC4Encryption(modifyEncryptionKeyWithCounter(
                    key, i));
            rc4Result = rc4.process(rc4Result);
        }
        return rc4Result;
    }

    /**
     * Authenticates user password and returns encryption key if password is
     * correct.
     *
     * @param password            is string to be checked.
     * @param o                   is O value of encryption dict for standard
     *                            security handler.
     * @param p                   is P value of encryption dict for standard
     *                            security handler.
     * @param id                  is the value of the ID entry in the document’s
     *                            trailer dict.
     * @param revision            is R value of encryption dict for standard
     *                            security handler.
     * @param metadataIsEncrypted is true if metadata in file is encrypted.
     * @param length              is value of Length in encryption dict.
     * @param u                   is U value of encryption dict for standard
     *                            security handler.
     * @return null if password is incorrect and encryption key if it is
     * correct.
     */
    public static byte[] authenticateUserPassword(String password, byte[] o, int p,
                                                  byte[] id, int revision,
                                                  boolean metadataIsEncrypted,
                                                  int length, byte[] u) throws NoSuchAlgorithmException {
        byte[] uValue = computeUValue(password, o, p, id, revision,
                metadataIsEncrypted, length);
        if (revision >= 3) {
            u = Arrays.copyOf(u, U_LENGTH);
        }
        if (Arrays.equals(u, uValue)) {
            return computeEncryptionKey(password, o, p, id, revision,
                    metadataIsEncrypted, length);
        } else {
            return null;
        }
    }

    private static byte[] computeUValueRevision2(String password, byte[] o, int p,
                                                 byte[] id,
                                                 boolean metadataIsEncrypted,
                                                 int length) throws NoSuchAlgorithmException {
        byte[] key = computeEncryptionKey(password, o, p, id, 2,
                metadataIsEncrypted, length);
        RC4Encryption rc4 = new RC4Encryption(key);
        return rc4.process(DEFAULT_PADDING_STRING);
    }

    private static byte[] getPaddedPassword(String password) {
        if (password == null) {
            password = "";
        }
        byte[] psw;
        try {
            psw = password.getBytes("ISO-8859-1");
        } catch (UnsupportedEncodingException e) {
            psw = password.getBytes();
        }
        byte[] res = new byte[PADDED_PASSWORD_LENGTH];
        if (psw.length > PADDED_PASSWORD_LENGTH) {
            System.arraycopy(psw, 0, res, 0, PADDED_PASSWORD_LENGTH);
        } else {
            System.arraycopy(psw, 0, res, 0, psw.length);
            for (int i = 0; i < PADDED_PASSWORD_LENGTH - psw.length; ++i) {
                res[i + psw.length] = DEFAULT_PADDING_STRING[i];
            }
        }
        return res;
    }

    /**
     * Represents given integer as byte array where low-order bytes go first.
     *
     * @param p is integer.
     * @return array of 4 bytes that represent this number as unsigned 32-bit
     * integer, low-order bytes go first.
     */
    public static byte[] intToBytesLowOrderFirst(long p) {
        byte[] res = new byte[4];
        for (int i = 0; i < 4; ++i) {
            byte b = (byte) (p & 0xFF);
            p >>= 8;
            res[i] = b;
        }
        return res;
    }

    private static byte[] modifyEncryptionKeyWithCounter(byte[] key, int counter) {
        byte[] res = new byte[key.length];
        for (int i = 0; i < key.length; ++i) {
            res[i] = (byte) (key[i] ^ counter);
        }
        return res;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy