/*
* Copyright 2006-2017 ICEsoft Technologies Canada Corp.
*
* Licensed 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.
*/
package org.icepdf.core.pobjects.security;
import org.icepdf.core.pobjects.Reference;
import org.icepdf.core.util.Utils;
import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.*;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* PDF's standard security handler allows access permissions and up to two passwords
* to be specified for a document. The purpose of this class is to encapsulate
* the algorithms used by the Standard Security Handler.
* <br>
* All of the algorithms used for encryption related calculations are based
* on the suto code described in the Adobe PDF Specification 1.5.
*
* @since 1.1
*/
class StandardEncryption {
private static final Logger logger =
Logger.getLogger(StandardEncryption.class.toString());
/**
* The application shall not decrypt data but shall direct the input stream
* to the security handler for decryption (NO SUPPORT)
*/
public static final String ENCRYPTION_TYPE_NONE = "None";
/**
* The application shall ask the security handler for the encryption key and
* shall implicitly decrypt data with "Algorithm 1: Encryption of data using
* the RC4 or AES algorithms", using the RC4 algorithm.
*/
public static final String ENCRYPTION_TYPE_V2 = "V2";
public static final String ENCRYPTION_TYPE_V3 = "V3";
/**
* (PDF 1.6) The application shall ask the security handler for the
* encryption key and shall implicitly decrypt data with "Algorithm 1:
* Encryption of data using the RC4 or AES algorithms", using the AES
* algorithm in Cipher Block Chaining (CBC) mode with a 16-byte block size
* and an initialization vector that shall be randomly generated and placed
* as the first 16 bytes in the stream or string.
*/
public static final String ENCRYPTION_TYPE_AES_V2 = "AESV2";
/**
* Padding String used in PDF encryption related algorithms
* < 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 >
*/
private static final byte[] PADDING = {
(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};
private static final byte[] AES_sAIT = {
(byte) 0x73, // s
(byte) 0x41, // A
(byte) 0x6C, // I
(byte) 0x54 // T
};
// block size of aes key.
private static final int BLOCK_SIZE = 16;
// Stores data about encryption
private EncryptionDictionary encryptionDictionary;
// Standard encryption key
private byte[] encryptionKey;
// last used object reference
private Reference objectReference;
// last used RC4 encryption key
private byte[] rc4Key = null;
// user password;
private String userPassword = "";
// user password;
private String ownerPassword = "";
/**
* Create a new instance of the StandardEncryption object.
*
* @param encryptionDictionary standard encryption dictionary values
*/
public StandardEncryption(EncryptionDictionary encryptionDictionary) {
this.encryptionDictionary = encryptionDictionary;
}
/**
* General encryption algorithm 3.1 for encryption of data using an
* encryption key.
*
* @param objectReference object number of object being encrypted
* @param encryptionKey encryption key for document
* @param algorithmType V2 or AESV2 standard encryption encryption types.
* @param inputData date to encrypted/decrypt.
* @return encrypted/decrypted data.
*/
public byte[] generalEncryptionAlgorithm(Reference objectReference,
byte[] encryptionKey,
final String algorithmType,
byte[] inputData,
boolean encrypt) {
if (objectReference == null || encryptionKey == null ||
inputData == null) {
// throw security exception
return null;
}
// Algorithm 3.1, version 1-4
if (encryptionDictionary.getVersion() < 5) {
// RC4 or AES algorithm detection
boolean isRc4 = algorithmType.equals(ENCRYPTION_TYPE_V2);
// optimization, if the encryptionKey and objectReference are the
// same there is no reason to calculate a new key.
if (rc4Key == null || this.encryptionKey != encryptionKey ||
this.objectReference != objectReference) {
this.objectReference = objectReference;
// Step 1 to 3, bytes
byte[] step3Bytes = resetObjectReference(objectReference, isRc4);
// Step 4: Use the first (n+5) byes, up to a max of 16 from the MD5
// hash
int n = encryptionKey.length;
rc4Key = new byte[Math.min(n + 5, BLOCK_SIZE)];
System.arraycopy(step3Bytes, 0, rc4Key, 0, rc4Key.length);
}
// if we are encrypting we need to properly pad the byte array.
int encryptionMode = encrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE;
// Set up an RC4 cipher and try to decrypt:
byte[] finalData = null; // return data if all goes well
try {
// Use above as key for the RC4 encryption function.
if (isRc4) {
// Use above as key for the RC4 encryption function.
SecretKeySpec key = new SecretKeySpec(rc4Key, "RC4");
Cipher rc4 = Cipher.getInstance("RC4");
rc4.init(encryptionMode, key);
// finally add the stream or string data
finalData = rc4.doFinal(inputData);
} else {
SecretKeySpec key = new SecretKeySpec(rc4Key, "AES");
Cipher aes = Cipher.getInstance("AES/CBC/PKCS5Padding");
// decrypt the data.
if (encryptionMode == Cipher.DECRYPT_MODE) {
// calculate 16 byte initialization vector.
byte[] initialisationVector = new byte[BLOCK_SIZE];
// should never happen as it would mean a string that won't encrypted properly as it
// would be missing full length 16 byte public key.
if (inputData.length < BLOCK_SIZE) {
byte[] tmp = new byte[BLOCK_SIZE];
System.arraycopy(inputData, 0, tmp, 0, inputData.length);
inputData = tmp;
}
// grab the public key.
System.arraycopy(inputData, 0, initialisationVector, 0, BLOCK_SIZE);
final IvParameterSpec iVParameterSpec =
new IvParameterSpec(initialisationVector);
// trim the input, get rid of the key and expose the data to decrypt
byte[] intermData = new byte[inputData.length - BLOCK_SIZE];
System.arraycopy(inputData, BLOCK_SIZE, intermData, 0, intermData.length);
// finally add the stream or string data
aes.init(encryptionMode, key, iVParameterSpec);
finalData = aes.doFinal(intermData);
} else {
// padding is taken care of by PKCS5Padding, so we don't have to touch the data.
final IvParameterSpec iVParameterSpec = new IvParameterSpec(generateIv());
aes.init(encryptionMode, key, iVParameterSpec);
finalData = aes.doFinal(inputData);
// add randomness to the start
byte[] output = new byte[iVParameterSpec.getIV().length + finalData.length];
System.arraycopy(iVParameterSpec.getIV(), 0, output, 0, BLOCK_SIZE);
System.arraycopy(finalData, 0, output, BLOCK_SIZE, finalData.length);
finalData = output;
}
}
} catch (NoSuchAlgorithmException ex) {
logger.log(Level.FINE, "NoSuchAlgorithmException.", ex);
} catch (IllegalBlockSizeException ex) {
logger.log(Level.FINE, "IllegalBlockSizeException.", ex);
} catch (BadPaddingException ex) {
logger.log(Level.FINE, "BadPaddingException.", ex);
} catch (NoSuchPaddingException ex) {
logger.log(Level.FINE, "NoSuchPaddingException.", ex);
} catch (InvalidKeyException ex) {
logger.log(Level.FINE, "InvalidKeyException.", ex);
} catch (InvalidAlgorithmParameterException ex) {
logger.log(Level.FINE, "InvalidAlgorithmParameterException", ex);
}
return finalData;
}
// Algorithm 3.1a, version 5
else if (encryptionDictionary.getVersion() == 5) {
// Use the 32-byte file encryption key for the AES-256 symmetric
// key algorithm, along with the string or stream data to be encrypted.
// Use the AES algorithm in Cipher Block Chaining (CBC) mode, which
// requires an initialization vector. The block size parameter is
// set to 16 bytes, and the initialization vector is a 16-byte random
// number that is stored as the first 16 bytes of the encrypted
// stream or string.
try {
SecretKeySpec key = new SecretKeySpec(encryptionKey, "AES");
Cipher aes = Cipher.getInstance("AES/CBC/PKCS5Padding");
// calculate 16 byte initialization vector.
byte[] initialisationVector = new byte[BLOCK_SIZE];
System.arraycopy(inputData, 0, initialisationVector, 0, BLOCK_SIZE);
// trim the input
byte[] intermData = new byte[inputData.length - BLOCK_SIZE];
System.arraycopy(inputData, BLOCK_SIZE, intermData, 0, intermData.length);
final IvParameterSpec iVParameterSpec =
new IvParameterSpec(initialisationVector);
aes.init(Cipher.DECRYPT_MODE, key, iVParameterSpec);
// finally add the stream or string data
byte[] finalData = aes.doFinal(intermData);
return finalData;
} catch (NoSuchAlgorithmException ex) {
logger.log(Level.FINE, "NoSuchAlgorithmException.", ex);
} catch (IllegalBlockSizeException ex) {
logger.log(Level.FINE, "IllegalBlockSizeException.", ex);
} catch (BadPaddingException ex) {
logger.log(Level.FINE, "BadPaddingException.", ex);
} catch (NoSuchPaddingException ex) {
logger.log(Level.FINE, "NoSuchPaddingException.", ex);
} catch (InvalidKeyException ex) {
logger.log(Level.FINE, "InvalidKeyException.", ex);
} catch (InvalidAlgorithmParameterException ex) {
logger.log(Level.FINE, "InvalidAlgorithmParameterException", ex);
}
}
return null;
}
/**
* Generates a recure random 16 byte (128 bit) public key for string to be
* encryped using AES.
*
* @return 16 byte public key.
*/
private byte[] generateIv() {
SecureRandom random = new SecureRandom();
byte[] ivBytes = new byte[BLOCK_SIZE];
random.nextBytes(ivBytes);
return ivBytes;
}
/**
* General encryption algorithm 3.1 for encryption of data using an
* encryption key.
*
* Must be synchronized for stream decoding.
*/
public synchronized InputStream generalEncryptionInputStream(
Reference objectReference,
byte[] encryptionKey,
final String algorithmType,
InputStream input, boolean encrypt) {
if (objectReference == null || encryptionKey == null || input == null) {
// throw security exception
return null;
}
// Algorithm 3.1, version 1-4
if (encryptionDictionary.getVersion() < 5) {
// RC4 or AES algorithm detection
boolean isRc4 = algorithmType.equals(ENCRYPTION_TYPE_V2);
// optimization, if the encryptionKey and objectReference are the
// same there is no reason to calculate a new key.
if (rc4Key == null || this.encryptionKey != encryptionKey ||
this.objectReference != objectReference) {
this.objectReference = objectReference;
// Step 1 to 3, bytes
byte[] step3Bytes = resetObjectReference(objectReference, isRc4);
// Step 4: Use the first (n+5) byes, up to a max of 16 from the MD5
// hash
int n = encryptionKey.length;
rc4Key = new byte[Math.min(n + 5, BLOCK_SIZE)];
System.arraycopy(step3Bytes, 0, rc4Key, 0, rc4Key.length);
}
// if we are encrypting we need to properly pad the byte array.
int encryptionMode = encrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE;
// Set up an RC4 cipher and try to decrypt:
try {
SecretKeySpec key = new SecretKeySpec(rc4Key, "AES");
// Use above as key for the RC4 encryption function.
if (isRc4) {
Cipher rc4 = Cipher.getInstance("RC4");
rc4.init(Cipher.DECRYPT_MODE, key);
// finally add the stream or string data
CipherInputStream cin = new CipherInputStream(input, rc4);
return cin;
}
// use above a key for the AES encryption function.
else {
Cipher aes = Cipher.getInstance("AES/CBC/PKCS5Padding");
if (encryptionMode == Cipher.DECRYPT_MODE) {
// calculate 16 byte initialization vector.
byte[] initialisationVector = new byte[BLOCK_SIZE];
input.read(initialisationVector);
final IvParameterSpec iVParameterSpec = new IvParameterSpec(initialisationVector);
aes.init(encryptionMode, key, iVParameterSpec);
// finally add the stream or string data
CipherInputStream cin = new CipherInputStream(input, aes);
return cin;
} else {
final IvParameterSpec iVParameterSpec = new IvParameterSpec(generateIv());
aes.init(encryptionMode, key, iVParameterSpec);
ByteArrayOutputStream outputByteArray = new ByteArrayOutputStream();
// finally add the stream or string data
CipherOutputStream cos = new CipherOutputStream(outputByteArray, aes);
try {
byte[] data = new byte[4096];
int read;
while ((read = input.read(data)) != -1) {
cos.write(data, 0, read);
}
} finally {
cos.close();
input.close();
}
byte[] finalData = outputByteArray.toByteArray();
// add randomness to the start
byte[] output = new byte[iVParameterSpec.getIV().length + finalData.length];
System.arraycopy(iVParameterSpec.getIV(), 0, output, 0, BLOCK_SIZE);
System.arraycopy(finalData, 0, output, BLOCK_SIZE, finalData.length);
finalData = output;
return new ByteArrayInputStream(finalData);
}
}
} catch (NoSuchAlgorithmException ex) {
logger.log(Level.FINE, "NoSuchAlgorithmException.", ex);
} catch (NoSuchPaddingException ex) {
logger.log(Level.FINE, "NoSuchPaddingException.", ex);
} catch (InvalidKeyException ex) {
logger.log(Level.FINE, "InvalidKeyException.", ex);
} catch (InvalidAlgorithmParameterException ex) {
logger.log(Level.FINE, "InvalidAlgorithmParameterException", ex);
} catch (IOException ex) {
logger.log(Level.FINE, "InvalidAlgorithmParameterException", ex);
}
}
// Algorithm 3.1a, version 5
else if (encryptionDictionary.getVersion() == 5) {
// Use the 32-byte file encryption key for the AES-256 symmetric
// key algorithm, along with the string or stream data to be encrypted.
// Use the AES algorithm in Cipher Block Chaining (CBC) mode, which
// requires an initialization vector. The block size parameter is
// set to 16 bytes, and the initialization vector is a 16-byte random
// number that is stored as the first 16 bytes of the encrypted
// stream or string.
try {
// use above a key for the AES encryption function.
SecretKeySpec key = new SecretKeySpec(encryptionKey, "AES");
Cipher aes = Cipher.getInstance("AES/CBC/PKCS5Padding");
// calculate 16 byte initialization vector.
byte[] initialisationVector = new byte[BLOCK_SIZE];
input.read(initialisationVector);
final IvParameterSpec iVParameterSpec =
new IvParameterSpec(initialisationVector);
aes.init(Cipher.DECRYPT_MODE, key, iVParameterSpec);
// finally add the stream or string data
CipherInputStream cin = new CipherInputStream(input, aes);
return cin;
} catch (NoSuchAlgorithmException ex) {
logger.log(Level.FINE, "NoSuchAlgorithmException.", ex);
} catch (NoSuchPaddingException ex) {
logger.log(Level.FINE, "NoSuchPaddingException.", ex);
} catch (InvalidKeyException ex) {
logger.log(Level.FINE, "InvalidKeyException.", ex);
} catch (InvalidAlgorithmParameterException ex) {
logger.log(Level.FINE, "InvalidAlgorithmParameterException", ex);
} catch (IOException ex) {
logger.log(Level.FINE, "InvalidAlgorithmParameterException", ex);
}
}
return null;
}
/**
* Step 1-3 of the general encryption algorithm 3.1. The procedure
* is as follows:
* <ul>
* Treat the object number and generation number as binary integers, extend
* the original n-byte encryption key to n + 5 bytes by appending the
* low-order 3 bytes of the object number and the low-order 2 bytes of the
* generation number in that order, low-order byte first. (n is 5 unless
* the value of V in the encryption dictionary is greater than 1, in which
* case the n is the value of Length divided by 8.)
* <br>
* If using the AES algorithm, extend the encryption key an additional
* 4 bytes by adding the value "sAlT", which corresponds to the hexadecimal
* values 0x73, 0x41, 0x6C, 0x54. (This addition is done for backward
* compatibility and is not intended to provide additional security.)
* </ul>
*
* @param objectReference pdf object reference or the identifier of the
* inderect object in the case of a string.
* @param isRc4 if true use the RC4 stream cipher, if false use the AES
* symmetric block cipher.
* @return Byte [] manipulated as specified.
*/
public byte[] resetObjectReference(Reference objectReference, boolean isRc4) {
// Step 1: separate object and generation numbers for objectReference
int objectNumber = objectReference.getObjectNumber();
int generationNumber = objectReference.getGenerationNumber();
// Step 2:
// v > 1 n is the value of Length divided by 8.
int n = 5;
if (encryptionDictionary.getVersion() > 1) {
n = encryptionDictionary.getKeyLength() / 8;//enencryptionKey.length;
}
// extend the original n-byte encryption key to n + 5 bytes
int paddingLength = 5;
if (!isRc4) {
paddingLength += 4;
}
byte[] step2Bytes = new byte[n + paddingLength];
// make the copy
System.arraycopy(encryptionKey, 0, step2Bytes, 0, n);
// appending the low-order 3 bytes of the object number
step2Bytes[n] = (byte) (objectNumber & 0xff);
step2Bytes[n + 1] = (byte) (objectNumber >> 8 & 0xff);
step2Bytes[n + 2] = (byte) (objectNumber >> 16 & 0xff);
// appending low-order 2 bytes of the generation number low-order
step2Bytes[n + 3] = (byte) (generationNumber & 0xff);
step2Bytes[n + 4] = (byte) (generationNumber >> 8 & 0xff);
// if using AES algorithm extend by four bytes "sAIT" (0x73, 0x41, 0x6c, 0x54)
if (!isRc4) {
step2Bytes[n + 5] = AES_sAIT[0];
step2Bytes[n + 6] = AES_sAIT[1];
step2Bytes[n + 7] = AES_sAIT[2];
step2Bytes[n + 8] = AES_sAIT[3];
}
// Step 3: Initialize the MD5 hash function and pass in step2Bytes
MessageDigest md5 = null;
try {
md5 = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException builtin) {
}
// and pass in padded password from step 1
md5.update(step2Bytes);
// finally return the modified object reference
return md5.digest();
}
/**
* Encryption key algorithm 3.2 for computing an encryption key given
* a password string.
*/
public byte[] encryptionKeyAlgorithm(String password, int keyLength) {
if (encryptionDictionary.getRevisionNumber() < 5) {
// Step 1: pad the password
byte[] paddedPassword = padPassword(password);
// Step 2: initialize the MD5 hash function
MessageDigest md5 = null;
try {
md5 = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException ex) {
logger.log(Level.FINE, "NoSuchAlgorithmException.", ex);
}
// and pass in padded password from step 1
md5.update(paddedPassword);
// Step 3: Pass the value of the encryption dictionary's 0 entry
byte[] bigO = Utils.convertByteCharSequenceToByteArray(
encryptionDictionary.getBigO());
md5.update(bigO);
// Step 4: treat P as an unsigned 4-byte integer
for (int i = 0, p = encryptionDictionary.getPermissions(); i < 4; i++,
p >>= 8) {
md5.update((byte) (p & 0xFF));
}
// Step 5: Pass in the first element of the file's file identifies array
String firstFileID = encryptionDictionary.getLiteralString(encryptionDictionary.getFileID().get(0));
byte[] fileID = Utils.convertByteCharSequenceToByteArray(firstFileID);
md5.update(fileID);
// Step 6: If document metadata is not being encrypted, pass 4 bytes with
// the value of 0xFFFFFFFF to the MD5 hash, Security handlers of revision 4 or greater)
if (encryptionDictionary.getRevisionNumber() >= 4 &&
!encryptionDictionary.isEncryptMetaData()) {
for (int i = 0; i < 4; ++i) {
md5.update((byte) 0xFF);
}
}
// Step 7: Finish Hash.
paddedPassword = md5.digest();
// key length
int keySize = encryptionDictionary.getRevisionNumber() == 2 ? 5 : keyLength / 8;
if (keySize > paddedPassword.length) {
keySize = paddedPassword.length;
}
byte[] out = new byte[keySize];
// Step 8: Do the following 50 times: take the output from the previous
// MD5 hash and pass it as a input into a new MD5 hash;
// only for R >= 3
try {
if (encryptionDictionary.getRevisionNumber() >= 3) {
for (int i = 0; i < 50; i++) {
md5.update(paddedPassword, 0, keySize);
md5.digest(paddedPassword, 0, paddedPassword.length);
}
}
} catch (DigestException e) {
logger.log(Level.WARNING, "Error creating MD5 digest.", e);
}
// Step 9: Set the encryption key to the first n bytes of the output from
// the MD5 hash
// truncate out to the appropriate value
System.arraycopy(paddedPassword,
0,
out,
0,
keySize);
// assign instance
encryptionKey = out;
return out;
}
// algorithm 3.2a for Revision 5
else if (encryptionDictionary.getRevisionNumber() == 5) {
try {
byte[] passwordBytes = Utils.convertByteCharSequenceToByteArray(password);
if (passwordBytes == null) {
passwordBytes = new byte[0];
}
byte[] ownerPassword = Utils.convertByteCharSequenceToByteArray(
encryptionDictionary.getBigO());
byte[] userPassword = Utils.convertByteCharSequenceToByteArray(
encryptionDictionary.getBigU());
// To understand the algorithm below, it is necessary to treat
// the O and U strings in the Encrypt dictionary as made up of
// three sections. The first 32 bytes are a hash value .
// The next 8 bytes are called the Validation Salt.
// The final 8 bytes are called the Key Salt.
MessageDigest md = MessageDigest.getInstance("SHA-256");
// 3.) computing the SHA-256 hash of the UTF-8 password, 127
// bytes if it is longer than 127 bytes.
md.update(passwordBytes, 0, Math.min(passwordBytes.length, 127));
// concatenated with the 8 bytes of owner Validation Salt,
md.update(ownerPassword, 32, 8);
// concatenated with the 48-byte U string
md.update(userPassword, 0, 48);
// calculate the 32 bit result
byte[] hash = md.digest();
// Check if the 32-byte result matches the first 32 bytes of the
// O string, this is the owner password.
boolean isOwnerPassword = byteCompare(hash, ownerPassword, 32);
encryptionDictionary.setAuthenticatedOwnerPassword(isOwnerPassword);
if (isOwnerPassword) {
// calculate an intermediate owner key
md.update(passwordBytes, 0, Math.min(passwordBytes.length, 127));
// concatenate 8 bytes of owner key salt
md.update(ownerPassword, 32, 8);
// concatenated with the 48-byte u string
md.update(userPassword, 0, 48);
hash = md.digest();
// the 32 byte hash result is the key to decrypt the 32byte
// oe string using AES-256 in CBC mode with no padding and an
// initialization vector of zero.
// the 32byte result is the file encryption key.
byte[] oePassword = Utils.convertByteCharSequenceToByteArray(
encryptionDictionary.getBigOE());
encryptionKey = AES256CBC(hash, oePassword);
}
// 4.)test the password against the user password.
else {
// concatenate password
md.update(passwordBytes, 0, Math.min(passwordBytes.length, 127));
// concatenated with the 8 bytes of user Validation Salt
md.update(userPassword, 32, 8);
hash = md.digest();
// test first 32 bytes against the user string.
boolean isUserPassword = byteCompare(hash, userPassword, 32);
encryptionDictionary.setAuthenticatedUserPassword(isUserPassword);
if (isUserPassword) {
// calculate an intermediate owner key
md.update(passwordBytes, 0, Math.min(passwordBytes.length, 127));
// concatenate 8 bytes of owner key salt
md.update(userPassword, 40, 8);
hash = md.digest();
// the 32 byte hash result is the key to decrypt the 32byte
// ue string using AES-256 in CBC mode with no padding and an
// initialization vector of zero.
// the 32byte result is the file encryption key.
byte[] uePassword = Utils.convertByteCharSequenceToByteArray(
encryptionDictionary.getBigUE());
encryptionKey = AES256CBC(hash, uePassword);
} else {
logger.warning("User password is incorrect. ");
}
}
// 5.)Decrypt the 16-byte Perms string using AES-256 in ECB mode
// with an initialization vector of zero and the file encryption
// key as the key.
byte[] perms = Utils.convertByteCharSequenceToByteArray(
encryptionDictionary.getPerms());
byte[] decryptedPerms = AES256CBC(encryptionKey, perms);
// Verify that bytes 9-11 of the result are the characters 'a', 'd', 'b'.
if (decryptedPerms[9] != (byte) 'a' ||
decryptedPerms[10] != (byte) 'd' ||
decryptedPerms[11] != (byte) 'b') {
logger.warning("User password is incorrect.");
return null;
}
// Bytes 0-3 of the decrypted Perms entry, treated as a
// little-endian integer, are the user permissions. They should
// match the value in the P key.
int permissions = (decryptedPerms[0] & 0xff) |
((decryptedPerms[1] & 0xff) << 8) |
((decryptedPerms[2] & 0xff) << 16) |
((decryptedPerms[2] & 0xff) << 24);
int pPermissions = encryptionDictionary.getPermissions();
if (pPermissions != permissions) {
logger.warning("Perms and P do not match");
}
return encryptionKey;
} catch (NoSuchAlgorithmException e) {
logger.warning("Error computing the the 3.2a Encryption key.");
}
} else {
logger.warning("Adobe standard Encryption R = 6 is not supported.");
}
return null;
}
/**
* ToDo: xjava.security.Padding, look at class for interface to see
* if PDFPadding class could/should be built
* <br>
* Pad or truncate the password string to exactly 32 bytes. If the
* password 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 PADDING string.
* <br>
* NOTE: This is algorithm is the <b>1st</b> step of <b>algorithm 3.2</b>
* and is commonly used by other methods in this class
*
* @param password password to padded
* @return returned updated password with appropriate padding applied
*/
protected static byte[] padPassword(String password) {
// create the standard 32 byte password
byte[] paddedPassword = new byte[32];
// Passwords can be null, if so set it to an empty string
if (password == null || "".equals(password)) {
return PADDING;
}
int passwordLength = Math.min(password.length(), 32);
byte[] bytePassword =
Utils.convertByteCharSequenceToByteArray(password);
// copy passwords bytes, but truncate the password is > 32 bytes
System.arraycopy(bytePassword, 0, paddedPassword, 0, passwordLength);
// pad the password if it is < 32 bytes
System.arraycopy(PADDING,
0,
paddedPassword,
// start copy at end of string
passwordLength,
// append need bytes from PADDING
32 - passwordLength);
return paddedPassword;
}
/**
* Computing Owner password value, Algorithm 3.3.
* <br>
* AESv3 passwords are not handle by this method, instead use
* {@link #generalEncryptionAlgorithm(org.icepdf.core.pobjects.Reference, byte[], String, byte[], boolean)}
* If the result is not null then the encryptionDictionary will container
* values for isAuthenticatedOwnerPassword and isAuthenticatedUserPassword.
*
* @param ownerPassword owner pasword string. If there is no owner,
* password use the user password instead.
* @param userPassword user password.
* @param isAuthentication if true, only steps 1-4 of the algorithm will be
* completed. If false, all 8 steps of the algorithm will be
* completed
* <b>Note : </b><br>
* There may be a bug in this algorithm when all 8 steps are called.
* 1-4 are work properly, but 1-8 can not generate an O value that is
* the same as the orgional documents O. This is not a currently a
* problem as we do not author PDF documents.
*/
public byte[] calculateOwnerPassword(String ownerPassword,
String userPassword,
boolean isAuthentication) {
// Step 1: padd the owner password, use the userPassword if empty.
if ("".equals(ownerPassword) && !"".equals(userPassword)) {
ownerPassword = userPassword;
}
byte[] paddedOwnerPassword = padPassword(ownerPassword);
// Step 2: Initialize the MD5 hash function and pass in step 2.
MessageDigest md5 = null;
try {
md5 = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
logger.log(Level.FINE, "Could not fint MD5 Digest", e);
}
// and pass in padded password from step 1
paddedOwnerPassword = md5.digest(paddedOwnerPassword);
// Step 3: Do the following 50 times: take the output from the previous
// MD5 hash and pass it as input into a new MD5 hash;
// only for R = 3
if (encryptionDictionary.getRevisionNumber() >= 3) {
for (int i = 0; i < 50; i++) {
paddedOwnerPassword = md5.digest(paddedOwnerPassword);
}
}
// Step 4: Create an RC4 encryption key using the first n bytes of the
// final MD5 hash, where n is always 5 for revision 2 and the value
// of the encryption dictionary's Length entry for revision 3.
// Set up an RC4 cipher and try to encrypt:
// grap the needed n bytes.
int dataSize = 5; // default for R == 2
if (encryptionDictionary.getRevisionNumber() >= 3) {
dataSize = encryptionDictionary.getKeyLength() / 8;
}
if (dataSize > paddedOwnerPassword.length) {
dataSize = paddedOwnerPassword.length;
}
// truncate the byte array RC4 encryption key
byte[] encryptionKey = new byte[dataSize];
System.arraycopy(paddedOwnerPassword, 0, encryptionKey, 0, dataSize);
// Key is needed by algorithm 3.7, Authenticating owner password
if (isAuthentication) {
return encryptionKey;
}
// Step 5: Pad or truncate the user password string
byte[] paddedUserPassword = padPassword(userPassword);
// Step 6: Encrypt the result of step 4, using the RC4 encryption
// function with the encryption key obtained in step 4
byte[] finalData = null;
try {
// Use above as key for the RC4 encryption function.
SecretKeySpec key = new SecretKeySpec(encryptionKey, "RC4");
Cipher rc4 = Cipher.getInstance("RC4");
rc4.init(Cipher.ENCRYPT_MODE, key);
// finally add the stream or string data
finalData = rc4.update(paddedUserPassword);
// Step 7: 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 in step 4 and performing an XOR
// operation between that byte and the single-byte value of the
// iteration counter
if (encryptionDictionary.getRevisionNumber() >= 3) {
// key to be made on each interaction
byte[] indexedKey = new byte[encryptionKey.length];
// start the 19? interactions
for (int i = 1; i <= 19; i++) {
// build new key for each i xor on each byte
for (int j = 0; j < encryptionKey.length; j++) {
indexedKey[j] = (byte) (encryptionKey[j] ^ i);
}
// create new key and init rc4
key = new SecretKeySpec(indexedKey, "RC4");
//Cipher tmpRc4 = Cipher.getInstance("RC4");
rc4.init(Cipher.ENCRYPT_MODE, key);
// encrypt the old data with the new key
finalData = rc4.update(finalData);
}
}
} catch (NoSuchAlgorithmException ex) {
logger.log(Level.FINE, "NoSuchAlgorithmException.", ex);
} catch (NoSuchPaddingException ex) {
logger.log(Level.FINE, "NoSuchPaddingException.", ex);
} catch (InvalidKeyException ex) {
logger.log(Level.FINE, "InvalidKeyException.", ex);
}
// Debug Code.
// String O = encryptionDictionary.getBigO();
// System.out.print("Original O " + O.length() + " ");
// byte[] bigO = new byte[O.length()];
// for (int i=0; i < bigO.length; i++){
// //bigO[i] = (byte)O.charAt(i);
// System.out.print((int)O.charAt(i));
// }
// System.out.println();
//
// System.out.print("new O " + finalData.length + " ");
// for (int i=0; i < finalData.length; i++){
// System.out.print((int)finalData[i]);
// }
// System.out.println();
// Step 8: return the final invocation of the RC4 function as O
return finalData;
}
/**
* Computing Owner password value, Algorithm 3.4 is respected for
* Revision = 2 and Algorithm 3.5 is respected for Revisison = 3, null
* otherwise.
* <br>
* AESv3 passwords are not handle by this method, instead use
* {@link #generalEncryptionAlgorithm(org.icepdf.core.pobjects.Reference, byte[], String, byte[], boolean)}
* If the result is not null then the encryptionDictionary will container
* values for isAuthenticatedOwnerPassword and isAuthenticatedUserPassword.
*
* @param userPassword user password.
* @return byte array representing the U value for the encryption dictionary
*/
public byte[] calculateUserPassword(String userPassword) {
// Step 1: Create an encryption key based on the user password String,
// as described in Algorithm 3.2
byte[] encryptionKey = encryptionKeyAlgorithm(
userPassword,
encryptionDictionary.getKeyLength());
// Algorithm 3.4 steps, 2 - 3
if (encryptionDictionary.getRevisionNumber() == 2) {
// Step 2: Encrypt the 32-byte padding string show in step 1, using
// an RC4 encryption function with the encryption key from the
// preceding step
// 32-byte padding string
byte[] paddedUserPassword = PADDING.clone();
// encrypt the data
byte[] finalData = null;
try {
// Use above as key for the RC4 encryption function.
SecretKeySpec key = new SecretKeySpec(encryptionKey, "RC4");
Cipher rc4 = Cipher.getInstance("RC4");
rc4.init(Cipher.ENCRYPT_MODE, key);
// finally encrypt the padding string
finalData = rc4.doFinal(paddedUserPassword);
} catch (NoSuchAlgorithmException ex) {
logger.log(Level.FINE, "NoSuchAlgorithmException.", ex);
} catch (IllegalBlockSizeException ex) {
logger.log(Level.FINE, "IllegalBlockSizeException.", ex);
} catch (BadPaddingException ex) {
logger.log(Level.FINE, "BadPaddingException.", ex);
} catch (NoSuchPaddingException ex) {
logger.log(Level.FINE, "NoSuchPaddingException.", ex);
} catch (InvalidKeyException ex) {
logger.log(Level.FINE, "InvalidKeyException.", ex);
}
// Step 3: return the result of step 2 as the value of the U entry
return finalData;
}
// algorithm 3.5 steps, 2 - 6
else if (encryptionDictionary.getRevisionNumber() >= 3 &&
encryptionDictionary.getRevisionNumber() < 5) {
// 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
byte[] paddedUserPassword = PADDING.clone();
MessageDigest md5 = null;
try {
md5 = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
logger.log(Level.FINE, "MD5 digester could not be found", e);
}
// and pass in padded password 32-byte padding string
md5.update(paddedUserPassword);
// Step 3: Pass the first element of the files identify array to the
// hash function and finish the hash.
String firstFileID = encryptionDictionary.getLiteralString(encryptionDictionary.getFileID().get(0));
byte[] fileID = Utils.convertByteCharSequenceToByteArray(firstFileID);
byte[] encryptData = md5.digest(fileID);
// Step 4: Encrypt the 16 byte result of the hash, using an RC4
// encryption function with the encryption key from step 1
//System.out.println("R=3 " + encryptData.length);
// The final data should be 16 bytes long
// currently no checking for this.
try {
// Use above as key for the RC4 encryption function.
SecretKeySpec key = new SecretKeySpec(encryptionKey, "RC4");
Cipher rc4 = Cipher.getInstance("RC4");
rc4.init(Cipher.ENCRYPT_MODE, key);
// finally encrypt the padding string
encryptData = rc4.update(encryptData);
// 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 encryption key in step 4 and performing an XOR
// operation between that byte and the single-byte value of the
// iteration counter
// key to be made on each interaction
byte[] indexedKey = new byte[encryptionKey.length];
// start the 19? interactions
for (int i = 1; i <= 19; i++) {
// build new key for each i xor on each byte
for (int j = 0; j < encryptionKey.length; j++) {
indexedKey[j] = (byte) (encryptionKey[j] ^ (byte) i);
}
// create new key and init rc4
key = new SecretKeySpec(indexedKey, "RC4");
rc4.init(Cipher.ENCRYPT_MODE, key);
// encrypt the old data with the new key
encryptData = rc4.update(encryptData);
}
} catch (NoSuchAlgorithmException ex) {
logger.log(Level.FINE, "NoSuchAlgorithmException.", ex);
} catch (NoSuchPaddingException ex) {
logger.log(Level.FINE, "NoSuchPaddingException.", ex);
} catch (InvalidKeyException ex) {
logger.log(Level.FINE, "InvalidKeyException.", ex);
}
// Step 6: Append 16 bytes of arbitrary padding to the output from
// the final invocation of the RC4 function and return the 32-byte
// result as the value of the U entry.
byte[] finalData = new byte[32];
System.arraycopy(encryptData, 0, finalData, 0, BLOCK_SIZE);
System.arraycopy(PADDING, 0, finalData, BLOCK_SIZE, BLOCK_SIZE);
return finalData;
} else {
return null;
}
}
/**
* Authenticating the user password, algorithm 3.6
*
* @param userPassword user password to check for authenticity
* @return true if the userPassword matches the value the encryption
* dictionary U value, false otherwise.
*/
public boolean authenticateUserPassword(String userPassword) {
// Step 1: Perform all but the last step of Algorithm 3.4(Revision 2) or
// Algorithm 3.5 (Revision 3) using the supplied password string.
byte[] tmpUValue = calculateUserPassword(userPassword);
byte[] bigU = Utils.convertByteCharSequenceToByteArray(
encryptionDictionary.getBigU());
byte[] trunkUValue;
// compare all 32 bytes.
if (encryptionDictionary.getRevisionNumber() == 2) {
trunkUValue = new byte[32];
System.arraycopy(tmpUValue, 0, trunkUValue, 0, trunkUValue.length);
}
// truncate to first 16 bytes for R >= 3
else if (encryptionDictionary.getRevisionNumber() >= 3 &&
encryptionDictionary.getRevisionNumber() < 5) {
trunkUValue = new byte[BLOCK_SIZE];
System.arraycopy(tmpUValue, 0, trunkUValue, 0, trunkUValue.length);
} else {
return false;
}
// Step 2: If the result of step 1 is equal o the value of the
// encryption dictionary's U entry, the password supplied is the correct
// user password.
boolean found = true;
for (int i = 0; i < trunkUValue.length; i++) {
if (trunkUValue[i] != bigU[i]) {
found = false;
break;
}
}
return found;
}
/**
* Authenticating the owner password, algorithm 3.7
*/
public boolean authenticateOwnerPassword(String ownerPassword) {
// Step 1: Computer an encryption key from the supplied password string,
// as described in steps 1 to 4 of algorithm 3.3.
byte[] encryptionKey = calculateOwnerPassword(ownerPassword,
"", true);
// Step 2: start decryption of O
byte[] decryptedO = null;
try {
// get bigO value
byte[] bigO = Utils.convertByteCharSequenceToByteArray(
encryptionDictionary.getBigO());
if (encryptionDictionary.getRevisionNumber() == 2) {
// Step 2 (R == 2): decrypt the value of the encryption dictionary
// O entry, using an RC4 encryption function with the encryption
// key computed in step 1.
// Use above as key for the RC4 encryption function.
SecretKeySpec key = new SecretKeySpec(encryptionKey, "RC4");
Cipher rc4 = Cipher.getInstance("RC4");
rc4.init(Cipher.DECRYPT_MODE, key);
decryptedO = rc4.doFinal(bigO);
}
// Step 2 (R >= 3): 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 in step 4 and performing an XOR
// operation between that byte and the single-byte value of the
// iteration counter
else {//if (encryptionDictionary.getRevisionNumber() >= 3){
// key to be made on each interaction
byte[] indexedKey = new byte[encryptionKey.length];
decryptedO = bigO;
// start the 19->0? interactions
for (int i = 19; i >= 0; i--) {
// build new key for each i xor on each byte
for (int j = 0; j < indexedKey.length; j++) {
indexedKey[j] = (byte) (encryptionKey[j] ^ (byte) i);
}
// create new key and init rc4
SecretKeySpec key = new SecretKeySpec(indexedKey, "RC4");
Cipher rc4 = Cipher.getInstance("RC4");
rc4.init(Cipher.ENCRYPT_MODE, key);
// encrypt the old data with the new key
decryptedO = rc4.update(decryptedO);
}
}
// Step 3: The result of step 2 purports to be the user password.
// Authenticate this user password using Algorithm 3.6. If it is found
// to be correct, the password supplied is the correct owner password.
String tmpUserPassword = Utils.convertByteArrayToByteString(decryptedO);
//System.out.println("tmp user password " + tmpUserPassword);
boolean isValid = authenticateUserPassword(tmpUserPassword);
if (isValid) {
userPassword = tmpUserPassword;
this.ownerPassword = ownerPassword;
// setup permissions if valid
}
return isValid;
} catch (NoSuchAlgorithmException ex) {
logger.log(Level.FINE, "NoSuchAlgorithmException.", ex);
} catch (IllegalBlockSizeException ex) {
logger.log(Level.FINE, "IllegalBlockSizeException.", ex);
} catch (BadPaddingException ex) {
logger.log(Level.FINE, "BadPaddingException.", ex);
} catch (NoSuchPaddingException ex) {
logger.log(Level.FINE, "NoSuchPaddingException.", ex);
} catch (InvalidKeyException ex) {
logger.log(Level.FINE, "InvalidKeyException.", ex);
}
return false;
}
public String getUserPassword() {
return userPassword;
}
public String getOwnerPassword() {
return ownerPassword;
}
/**
* Utility to decrypt the encryptedString via the intermediateKey. AES
* encryption with cypher block chaining and no padding.
*
* @param intermediateKey key to use for decryption
* @param encryptedString byte[] to decrypt
* @return
*/
private static byte[] AES256CBC(byte[] intermediateKey, byte[] encryptedString) {
byte[] finalData = null;
try {
// AES with cipher block chaining and no padding
SecretKeySpec key = new SecretKeySpec(intermediateKey, "AES");
Cipher aes = Cipher.getInstance("AES/CBC/NoPadding");
// empty initialization vector
final IvParameterSpec iVParameterSpec =
new IvParameterSpec(new byte[BLOCK_SIZE]);
// go!
aes.init(Cipher.DECRYPT_MODE, key, iVParameterSpec);
// finally add the stream or string data
finalData = aes.doFinal(encryptedString);
} catch (NoSuchAlgorithmException ex) {
logger.log(Level.FINE, "NoSuchAlgorithmException.", ex);
} catch (IllegalBlockSizeException ex) {
logger.log(Level.FINE, "IllegalBlockSizeException.", ex);
} catch (BadPaddingException ex) {
logger.log(Level.FINE, "BadPaddingException.", ex);
} catch (NoSuchPaddingException ex) {
logger.log(Level.FINE, "NoSuchPaddingException.", ex);
} catch (InvalidKeyException ex) {
logger.log(Level.FINE, "InvalidKeyException.", ex);
} catch (InvalidAlgorithmParameterException ex) {
logger.log(Level.FINE, "InvalidAlgorithmParameterException", ex);
}
return finalData;
}
/**
* Compare two byte arrays to the specified max index. No check is made
* for an index out of bounds error.
*
* @param byteArray1 byte array to compare
* @param byteArray2 byte array to compare
* @param range number of elements to compare starting at zero.
* @return true if the
*/
private static boolean byteCompare(byte[] byteArray1, byte[] byteArray2, int range) {
for (int i = 0; i < range; i++) {
if (byteArray1[i] != byteArray2[i]) {
return false;
}
}
return true;
}
}