package org.apache.pdfbox.pdmodel.encryption;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.spec.AlgorithmParameterSpec;
import java.util.Arrays;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
import org.apache.pdfbox.exceptions.CryptographyException;
public abstract class BaseEncryptionImplementation {
/**
* Standard padding for encryption.
*/
public static final byte[] ENCRYPT_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 };
/**
* This will compare two byte[] for equality.
*
* @param first
* The first byte array.
* @param second
* The second byte array.
* @param count
* to what index should the arrays be equal
*
* @return true If the arrays contain the exact same data up to count bytes.
*/
private static final boolean arraysEqual(final byte[] first, final byte[] second, final int count) {
boolean equal = first.length >= count && second.length >= count;
for (int i = 0; i < count && equal; i++) {
equal = first[i] == second[i];
}
return equal;
}
protected String algorithmFor(final Key key) {
return key.getAlgorithm();
}
public byte[] computeEncryptionKey(final byte[] password, final byte[] o, final int permissions, final byte[] id, final int encRevision, final int length, final boolean encryptMetadata)
throws CryptographyException {
final byte[] result = new byte[length];
try {
// PDFReference 1.4 pg 78
// step1
final byte[] padded = truncateOrPad(password);
// step 2
final MessageDigest md = MessageDigest.getInstance("MD5");
md.update(padded);
// step 3
md.update(o);
// step 4
final byte zero = (byte) (permissions >>> 0);
final byte one = (byte) (permissions >>> 8);
final byte two = (byte) (permissions >>> 16);
final byte three = (byte) (permissions >>> 24);
md.update(zero);
md.update(one);
md.update(two);
md.update(three);
// step 5
md.update(id);
// (Security handlers of revision 4 or greater) If document metadata is not being encrypted,
// pass 4 bytes with the value 0xFFFFFFFF to the MD5 hash function.
// see 7.6.3.3 Algorithm 2 Step f of PDF 32000-1:2008
if (encRevision == 4 && !encryptMetadata) {
md.update(new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff });
}
byte[] digest = md.digest();
// step 6
if (encRevision == 3 || encRevision == 4) {
for (int i = 0; i < 50; i++) {
md.reset();
md.update(digest, 0, length);
digest = md.digest();
}
}
// step 7
if (encRevision == 2 && length != 5) {
throw new CryptographyException("Error: length should be 5 when revision is two actual=" + length);
}
System.arraycopy(digest, 0, result, 0, length);
} catch (final NoSuchAlgorithmException e) {
throw new CryptographyException(e);
}
return result;
}
public byte[] computeOwnerKey(final byte[] ownerPassword, final byte[] userPassword, final int encRevision, final int length) throws CryptographyException {
// STEP 1
final byte[] ownerPadded = truncateOrPad(ownerPassword);
// STEP 2
MessageDigest md;
try {
md = MessageDigest.getInstance("MD5");
} catch (final NoSuchAlgorithmException e) {
throw new CryptographyException(e);
}
byte[] digest = md.digest(ownerPadded);
// STEP 3
if (encRevision == 3 || encRevision == 4) {
for (int i = 0; i < 50; i++) {
md.reset();
md.update(digest, 0, length);
digest = md.digest();
}
}
if (encRevision == 2 && length != 5) {
throw new CryptographyException("Error: Expected length=5 actual=" + length);
}
// STEP 4
final byte[] rc4Key = new byte[length];
System.arraycopy(digest, 0, rc4Key, 0, length);
// STEP 5
final byte[] paddedUser = truncateOrPad(userPassword);
// STEP 6
final Key key = new SecretKeySpec(rc4Key, "RC4");
final Cipher rc4Cipher = newRC4Cipher();
try {
rc4Cipher.init(Cipher.ENCRYPT_MODE, key);
} catch (final InvalidKeyException e) {
throw new CryptographyException(e);
}
byte[] crypted;
try {
crypted = rc4Cipher.doFinal(paddedUser);
} catch (final IllegalBlockSizeException e) {
throw new CryptographyException(e);
} catch (final BadPaddingException e) {
throw new CryptographyException(e);
}
// STEP 7
if (encRevision == 3 || encRevision == 4) {
final byte[] iterationKeyBytes = new byte[rc4Key.length];
for (int i = 1; i < 20; i++) {
System.arraycopy(rc4Key, 0, iterationKeyBytes, 0, rc4Key.length);
for (int j = 0; j < iterationKeyBytes.length; j++) {
iterationKeyBytes[j] = (byte) (iterationKeyBytes[j] ^ (byte) i);
}
final Key iterationKey = new SecretKeySpec(iterationKeyBytes, "RC4");
try {
rc4Cipher.init(Cipher.ENCRYPT_MODE, iterationKey);
crypted = rc4Cipher.doFinal(crypted);
} catch (final InvalidKeyException e) {
throw new CryptographyException(e);
} catch (final IllegalBlockSizeException e) {
throw new CryptographyException(e);
} catch (final BadPaddingException e) {
throw new CryptographyException(e);
}
}
}
// STEP 8
return crypted;
}
public byte[] computeUserKey(final byte[] password, final byte[] o, final int permissions, final byte[] id, final int encRevision, final int length, final boolean encryptMetadata)
throws CryptographyException {
// STEP 1
final byte[] encryptionKey = computeEncryptionKey(password, o, permissions, id, encRevision, length, encryptMetadata);
final Cipher rc4Cipher = newRC4Cipher();
if (encRevision == 2) {
// STEP 2
final Key rc4Key = new SecretKeySpec(encryptionKey, "RC4");
try {
rc4Cipher.init(Cipher.ENCRYPT_MODE, rc4Key);
return rc4Cipher.doFinal(ENCRYPT_PADDING);
} catch (final InvalidKeyException e) {
throw new CryptographyException(e);
} catch (final IllegalBlockSizeException e) {
throw new CryptographyException(e);
} catch (final BadPaddingException e) {
throw new CryptographyException(e);
}
}
else if (encRevision == 3 || encRevision == 4) {
try {
// STEP 2
final MessageDigest md = MessageDigest.getInstance("MD5");
md.update(ENCRYPT_PADDING);
// STEP 3
md.update(id);
byte[] cipher = md.digest();
// STEP 4 and 5
final byte[] iterationKeyBytes = new byte[encryptionKey.length];
for (int i = 0; i < 20; i++) {
System.arraycopy(encryptionKey, 0, iterationKeyBytes, 0, iterationKeyBytes.length);
for (int j = 0; j < iterationKeyBytes.length; j++) {
iterationKeyBytes[j] = (byte) (iterationKeyBytes[j] ^ i);
}
final Key iterationKey = new SecretKeySpec(iterationKeyBytes, "RC4");
try {
rc4Cipher.init(Cipher.ENCRYPT_MODE, iterationKey);
cipher = rc4Cipher.doFinal(cipher);
} catch (final InvalidKeyException e) {
throw new CryptographyException(e);
} catch (final IllegalBlockSizeException e) {
throw new CryptographyException(e);
} catch (final BadPaddingException e) {
throw new CryptographyException(e);
}
}
// step 6
final byte[] finalResult = new byte[32];
System.arraycopy(cipher, 0, finalResult, 0, 16);
System.arraycopy(ENCRYPT_PADDING, 0, finalResult, 16, 16);
return finalResult;
} catch (final NoSuchAlgorithmException e) {
throw new CryptographyException(e);
}
}
throw new IllegalStateException("unsupported revision: " + encRevision);
}
/**
* This will compute the user password hash.
*
* @param password
* The plain text password.
* @param o
* The owner password hash.
* @param permissions
* The document permissions.
* @param id
* The document id.
* @param encRevision
* The revision of the encryption.
* @param length
* The length of the encryption key.
*
* @return The user password.
*
* @throws CryptographyException
* If there is an error computing the user password.
* @throws IOException
* If there is an IO error.
*/
public final byte[] computeUserPassword(final byte[] password, final byte[] o, final int permissions, final byte[] id, final int encRevision, final int length, final boolean encryptMetadata)
throws CryptographyException {
// STEP 1
final byte[] encryptionKey = computeEncryptionKey(password, o, permissions, id, encRevision, length, encryptMetadata);
final Cipher rc4Cipher = newRC4Cipher();
if (encRevision == 2) {
// STEP 2
final Key rc4Key = new SecretKeySpec(encryptionKey, "RC4");
try {
rc4Cipher.init(Cipher.ENCRYPT_MODE, rc4Key);
return rc4Cipher.doFinal(ENCRYPT_PADDING);
} catch (final InvalidKeyException e) {
throw new CryptographyException(e);
} catch (final IllegalBlockSizeException e) {
throw new CryptographyException(e);
} catch (final BadPaddingException e) {
throw new CryptographyException(e);
}
}
else if (encRevision == 3 || encRevision == 4) {
try {
// STEP 2
final MessageDigest md = MessageDigest.getInstance("MD5");
md.update(ENCRYPT_PADDING);
// STEP 3
md.update(id);
byte[] cipher = md.digest();
// STEP 4 and 5
final byte[] iterationKeyBytes = new byte[encryptionKey.length];
for (int i = 0; i < 20; i++) {
System.arraycopy(encryptionKey, 0, iterationKeyBytes, 0, iterationKeyBytes.length);
for (int j = 0; j < iterationKeyBytes.length; j++) {
iterationKeyBytes[j] = (byte) (iterationKeyBytes[j] ^ i);
}
final Key iterationKey = new SecretKeySpec(iterationKeyBytes, "RC4");
try {
rc4Cipher.init(Cipher.ENCRYPT_MODE, iterationKey);
cipher = rc4Cipher.doFinal(cipher);
} catch (final InvalidKeyException e) {
throw new CryptographyException(e);
} catch (final IllegalBlockSizeException e) {
throw new CryptographyException(e);
} catch (final BadPaddingException e) {
throw new CryptographyException(e);
}
}
// step 6
final byte[] finalResult = new byte[32];
System.arraycopy(cipher, 0, finalResult, 0, 16);
System.arraycopy(ENCRYPT_PADDING, 0, finalResult, 16, 16);
return finalResult;
} catch (final NoSuchAlgorithmException e) {
throw new CryptographyException(e);
}
}
throw new IllegalStateException("unsupported revision: " + encRevision);
}
protected Cipher createCipher(final Key key, final int mode, final AlgorithmParameterSpec parameters) throws CryptographyException {
final Cipher cipher;
try {
cipher = Cipher.getInstance(algorithmFor(key));
if (parameters == null) {
cipher.init(mode, key);
}
else {
cipher.init(mode, key, parameters);
}
} catch (final NoSuchAlgorithmException e) {
throw new CryptographyException(e);
} catch (final NoSuchPaddingException e) {
throw new CryptographyException(e);
} catch (final InvalidKeyException e) {
throw new CryptographyException(e);
} catch (final InvalidAlgorithmParameterException e) {
throw new CryptographyException(e);
}
return cipher;
}
public InputStream decrypt(final Key key, final AlgorithmParameterSpec parameters, final InputStream encrypted) throws CryptographyException, IOException {
final Cipher cipher = createCipher(key, Cipher.DECRYPT_MODE, parameters);
return new CipherInputStream(encrypted, cipher);
}
public InputStream decrypt(final Key key, final InputStream encrypted) throws CryptographyException, IOException {
return decrypt(key, null, encrypted);
}
public InputStream encrypt(final Key key, final AlgorithmParameterSpec parameters, final InputStream clear) throws CryptographyException, IOException {
final Cipher cipher = createCipher(key, Cipher.ENCRYPT_MODE, parameters);
return new CipherInputStream(clear, cipher);
}
public InputStream encrypt(final Key key, final InputStream clear) throws CryptographyException, IOException {
return encrypt(key, null, clear);
}
protected byte[] generateKeyBase(final SecurityHandler securityHandler, final long objectNumber, final long genNumber) {
final byte[] newKey = new byte[securityHandler.encryptionKey.length + 5];
System.arraycopy(securityHandler.encryptionKey, 0, newKey, 0, securityHandler.encryptionKey.length);
// PDF 1.4 reference pg 73 step 1 we have the reference
// step 2
newKey[newKey.length - 5] = (byte) (objectNumber & 0xff);
newKey[newKey.length - 4] = (byte) (objectNumber >> 8 & 0xff);
newKey[newKey.length - 3] = (byte) (objectNumber >> 16 & 0xff);
newKey[newKey.length - 2] = (byte) (genNumber & 0xff);
newKey[newKey.length - 1] = (byte) (genNumber >> 8 & 0xff);
return newKey;
}
/**
* Get the user password based on the owner password.
*
* @param ownerPassword
* The plaintext owner password.
* @param o
* The o entry of the encryption dictionary.
* @param encRevision
* The encryption revision number.
* @param length
* The key length.
*
* @return The u entry of the encryption dictionary.
*
* @throws CryptographyException
* If there is an error generating the user password.
* @throws IOException
* If there is an error accessing data while generating the user password.
*/
public final byte[] getUserPassword(final byte[] ownerPassword, final byte[] o, final int encRevision, final int length) throws CryptographyException {
// 3.3 STEP 1
final byte[] ownerPadded = truncateOrPad(ownerPassword);
// 3.3 STEP 2
MessageDigest md;
try {
md = MessageDigest.getInstance("MD5");
} catch (final NoSuchAlgorithmException e) {
throw new CryptographyException(e);
}
byte[] digest = md.digest(ownerPadded);
// 3.3 STEP 3
if (encRevision == 3 || encRevision == 4) {
for (int i = 0; i < 50; i++) {
md.reset();
md.update(digest);
digest = md.digest();
}
}
if (encRevision == 2 && length != 5) {
throw new CryptographyException("Error: Expected length=5 actual=" + length);
}
// 3.3 STEP 4
final byte[] rc4Key = new byte[length];
System.arraycopy(digest, 0, rc4Key, 0, length);
// 3.7 step 2
final Cipher rc4Cipher = newRC4Cipher();
if (encRevision == 2) {
final Key key = new SecretKeySpec(rc4Key, "RC4");
try {
rc4Cipher.init(Cipher.ENCRYPT_MODE, key);
return rc4Cipher.doFinal(o);
} catch (final InvalidKeyException e) {
throw new CryptographyException(e);
} catch (final IllegalBlockSizeException e) {
throw new CryptographyException(e);
} catch (final BadPaddingException e) {
throw new CryptographyException(e);
}
}
else if (encRevision == 3 || encRevision == 4) {
byte[] iterationKey = new byte[rc4Key.length];
for (int i = 19; i >= 0; i--) {
System.arraycopy(rc4Key, 0, iterationKey, 0, rc4Key.length);
for (int j = 0; j < iterationKey.length; j++) {
iterationKey[j] = (byte) (iterationKey[j] ^ (byte) i);
}
final Key key = new SecretKeySpec(iterationKey, "RC4");
try {
rc4Cipher.init(Cipher.ENCRYPT_MODE, key);
iterationKey = rc4Cipher.doFinal(iterationKey);
} catch (final InvalidKeyException e) {
throw new CryptographyException(e);
} catch (final IllegalBlockSizeException e) {
throw new CryptographyException(e);
} catch (final BadPaddingException e) {
throw new CryptographyException(e);
}
}
return iterationKey;
}
throw new IllegalStateException("unsupported revision: " + encRevision);
}
/**
* Check for owner password.
*
* @param ownerPassword
* The owner password.
* @param u
* The u entry of the encryption dictionary.
* @param o
* The o entry of the encryption dictionary.
* @param permissions
* The set of permissions on the document.
* @param id
* The document id.
* @param encRevision
* The encryption algorithm revision.
* @param length
* The encryption key length.
*
* @return True If the ownerPassword param is the owner password.
*
* @throws CryptographyException
* If there is an error during encryption.
* @throws IOException
* If there is an error accessing data.
*/
public final boolean isOwnerPassword(final byte[] ownerPassword, final byte[] u, final byte[] o, final int permissions, final byte[] id, final int encRevision, final int length,
final boolean encryptMetadata) throws CryptographyException {
final byte[] userPassword = getUserPassword(ownerPassword, o, encRevision, length);
return isUserPassword(userPassword, u, o, permissions, id, encRevision, length, encryptMetadata);
}
/**
* Check if a plaintext password is the user password.
*
* @param password
* The plaintext password.
* @param u
* The u entry of the encryption dictionary.
* @param o
* The o entry of the encryption dictionary.
* @param permissions
* The permissions set in the the PDF.
* @param id
* The document id used for encryption.
* @param encRevision
* The revision of the encryption algorithm.
* @param length
* The length of the encryption key.
*
* @return true If the plaintext password is the user password.
*
* @throws CryptographyException
* If there is an error during encryption.
* @throws IOException
* If there is an error accessing data.
*/
public final boolean isUserPassword(final byte[] password, final byte[] u, final byte[] o, final int permissions, final byte[] id, final int encRevision, final int length,
final boolean encryptMetadata) throws CryptographyException {
boolean matches = false;
// STEP 1
final byte[] computedValue = computeUserPassword(password, o, permissions, id, encRevision, length, encryptMetadata);
if (encRevision == 2) {
// STEP 2
matches = Arrays.equals(u, computedValue);
}
else if (encRevision == 3 || encRevision == 4) {
// STEP 2
matches = arraysEqual(u, computedValue, 16);
}
else {
throw new CryptographyException("Unknown Encryption Revision " + encRevision);
}
return matches;
}
private Cipher newRC4Cipher() throws CryptographyException {
Cipher rc4Cipher;
try {
rc4Cipher = Cipher.getInstance("RC4");
} catch (final NoSuchAlgorithmException e) {
throw new CryptographyException(e);
} catch (final NoSuchPaddingException e) {
throw new CryptographyException(e);
}
return rc4Cipher;
}
/**
* This will take the password and truncate or pad it as necessary.
*
* @param password
* The password to pad or truncate.
*
* @return The padded or truncated password.
*/
private final byte[] truncateOrPad(final byte[] password) {
final byte[] padded = new byte[ENCRYPT_PADDING.length];
final int bytesBeforePad = Math.min(password.length, padded.length);
System.arraycopy(password, 0, padded, 0, bytesBeforePad);
System.arraycopy(ENCRYPT_PADDING, 0, padded, bytesBeforePad, ENCRYPT_PADDING.length - bytesBeforePad);
return padded;
}
}