You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pdfbox.apache.org by ja...@apache.org on 2014/03/23 03:01:05 UTC
svn commit: r1580417 - in
/pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox: cos/
pdmodel/encryption/
Author: jahewson
Date: Sun Mar 23 02:01:05 2014
New Revision: 1580417
URL: http://svn.apache.org/r1580417
Log:
PDFBOX-1594: Add support for AES256 Encryption as proposed by Manuel Kasper
Modified:
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/cos/COSName.java
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/encryption/PDCryptFilterDictionary.java
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/encryption/PDEncryption.java
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/encryption/ProtectionPolicy.java
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/encryption/PublicKeySecurityHandler.java
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/encryption/SecurityHandler.java
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/encryption/StandardSecurityHandler.java
Modified: pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/cos/COSName.java
URL: http://svn.apache.org/viewvc/pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/cos/COSName.java?rev=1580417&r1=1580416&r2=1580417&view=diff
==============================================================================
--- pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/cos/COSName.java (original)
+++ pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/cos/COSName.java Sun Mar 23 02:01:05 2014
@@ -56,6 +56,7 @@ public final class COSName extends COSBa
public static final COSName ADBE_PKCS7_SHA1 = new COSName("adbe.pkcs7.sha1");
public static final COSName ADBE_X509_RSA_SHA1 = new COSName("adbe.x509.rsa_sha1");
public static final COSName ADOBE_PPKLITE = new COSName("Adobe.PPKLite");
+ public static final COSName AESV3 = new COSName("AESV3");
public static final COSName AIS = new COSName("AIS");
public static final COSName ALT = new COSName("Alt");
public static final COSName ALTERNATE = new COSName("Alternate");
@@ -306,6 +307,7 @@ public final class COSName extends COSBa
public static final COSName OCG = new COSName("OCG");
public static final COSName OCGS = new COSName("OCGs");
public static final COSName OCPROPERTIES = new COSName("OCProperties");
+ public static final COSName OE = new COSName("OE");
public static final COSName OFF = new COSName("OFF");
public static final COSName ON = new COSName("ON");
public static final COSName OP = new COSName("OP");
@@ -335,6 +337,7 @@ public final class COSName extends COSBa
public static final COSName PATTERN = new COSName("Pattern");
public static final COSName PATTERN_TYPE = new COSName("PatternType");
public static final COSName PDF_DOC_ENCODING = new COSName("PDFDocEncoding");
+ public static final COSName PERMS = new COSName("Perms");
public static final COSName PG = new COSName("Pg");
public static final COSName PRE_RELEASE = new COSName("PreRelease");
public static final COSName PREDICTOR = new COSName("Predictor");
@@ -421,6 +424,7 @@ public final class COSName extends COSBa
public static final COSName TYPE3 = new COSName("Type3");
// U
public static final COSName U = new COSName("U");
+ public static final COSName UE = new COSName("UE");
public static final COSName UF = new COSName("UF");
public static final COSName UNCHANGED = new COSName("Unchanged");
public static final COSName UNIX = new COSName("Unix");
Modified: pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/encryption/PDCryptFilterDictionary.java
URL: http://svn.apache.org/viewvc/pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/encryption/PDCryptFilterDictionary.java?rev=1580417&r1=1580416&r2=1580417&view=diff
==============================================================================
--- pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/encryption/PDCryptFilterDictionary.java (original)
+++ pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/encryption/PDCryptFilterDictionary.java Sun Mar 23 02:01:05 2014
@@ -88,7 +88,7 @@ public class PDCryptFilterDictionary
/**
* This will set the crypt filter method.
- * Allowed values are: NONE, V2, AESV2
+ * Allowed values are: NONE, V2, AESV2, AESV3
*
* @param cfm name of the crypt filter method.
*
@@ -101,7 +101,7 @@ public class PDCryptFilterDictionary
/**
* This will return the crypt filter method.
- * Allowed values are: NONE, V2, AESV2
+ * Allowed values are: NONE, V2, AESV2, AESV3
*
* @return the name of the crypt filter method.
*
Modified: pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/encryption/PDEncryption.java
URL: http://svn.apache.org/viewvc/pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/encryption/PDEncryption.java?rev=1580417&r1=1580416&r2=1580417&view=diff
==============================================================================
--- pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/encryption/PDEncryption.java (original)
+++ pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/encryption/PDEncryption.java Sun Mar 23 02:01:05 2014
@@ -314,6 +314,70 @@ public class PDEncryption
}
/**
+ * This will set the OE entry in the standard encryption dictionary.
+ *
+ * @param oe A 32 byte array or null if there is no owner encryption key.
+ *
+ * @throws IOException If there is an error setting the data.
+ */
+ public void setOwnerEncryptionKey(byte[] oe) throws IOException
+ {
+ COSString ownerEncryptionKey = new COSString();
+ ownerEncryptionKey.append(oe);
+ dictionary.setItem( COSName.OE, ownerEncryptionKey );
+ }
+
+ /**
+ * This will get the OE entry in the standard encryption dictionary.
+ *
+ * @return A 32 byte array or null if there is no owner encryption key.
+ *
+ * @throws IOException If there is an error accessing the data.
+ */
+ public byte[] getOwnerEncryptionKey() throws IOException
+ {
+ byte[] oe = null;
+ COSString ownerEncryptionKey = (COSString)dictionary.getDictionaryObject( COSName.OE );
+ if( ownerEncryptionKey != null )
+ {
+ oe = ownerEncryptionKey.getBytes();
+ }
+ return oe;
+ }
+
+ /**
+ * This will set the UE entry in the standard encryption dictionary.
+ *
+ * @param ue A 32 byte array or null if there is no user encryption key.
+ *
+ * @throws IOException If there is an error setting the data.
+ */
+ public void setUserEncryptionKey(byte[] ue) throws IOException
+ {
+ COSString userEncryptionKey = new COSString();
+ userEncryptionKey.append(ue);
+ dictionary.setItem( COSName.UE, userEncryptionKey );
+ }
+
+ /**
+ * This will get the UE entry in the standard encryption dictionary.
+ *
+ * @return A 32 byte array or null if there is no user encryption key.
+ *
+ * @throws IOException If there is an error accessing the data.
+ */
+ public byte[] getUserEncryptionKey() throws IOException
+ {
+ byte[] ue = null;
+ COSString userEncryptionKey = (COSString)dictionary.getDictionaryObject( COSName.UE );
+ if( userEncryptionKey != null )
+ {
+ ue = userEncryptionKey.getBytes();
+ }
+ return ue;
+ }
+
+ /**
* This will set the permissions bit mask.
*
* @param permissions The new permissions bit mask
@@ -425,6 +489,34 @@ public class PDEncryption
}
return null;
}
+
+ /**
+ * Sets the crypt filter with the given name.
+ *
+ * @param cryptFilterName the name of the crypt filter
+ * @param cryptFilterDictionary the crypt filter to set
+ */
+ public void setCryptFilterDictionary(COSName cryptFilterName, PDCryptFilterDictionary cryptFilterDictionary)
+ {
+ COSDictionary cfDictionary = (COSDictionary)dictionary.getDictionaryObject( COSName.CF );
+ if (cfDictionary == null)
+ {
+ cfDictionary = new COSDictionary();
+ dictionary.setItem(COSName.CF, cfDictionary);
+ }
+
+ cfDictionary.setItem(cryptFilterName, cryptFilterDictionary.getCOSDictionary());
+ }
+
+ /**
+ * Sets the standard crypt filter.
+ *
+ * @param cryptFilterDictionary the standard crypt filter to set
+ */
+ public void setStdCryptFilterDictionary(PDCryptFilterDictionary cryptFilterDictionary)
+ {
+ setCryptFilterDictionary(COSName.STD_CF, cryptFilterDictionary);
+ }
/**
* Returns the name of the filter which is used for de/encrypting streams.
@@ -443,6 +535,16 @@ public class PDEncryption
}
/**
+ * Sets the name of the filter which is used for de/encrypting streams.
+ *
+ * @param streamFilterName the name of the filter
+ */
+ public void setStreamFilterName(COSName streamFilterName)
+ {
+ dictionary.setItem(COSName.STM_F, streamFilterName);
+ }
+
+ /**
* Returns the name of the filter which is used for de/encrypting strings.
* Default value is "Identity".
*
@@ -458,6 +560,48 @@ public class PDEncryption
return strF;
}
+ /**
+ * Sets the name of the filter which is used for de/encrypting strings.
+ *
+ * @param stringFilterName the name of the filter
+ */
+ public void setStringFilterName(COSName stringFilterName)
+ {
+ dictionary.setItem(COSName.STR_F, stringFilterName);
+ }
+
+ /**
+ * Set the Perms entry in the encryption dictionary.
+ *
+ * @param perms A 16 byte array.
+ *
+ * @throws IOException If there is an error setting the data.
+ */
+ public void setPerms(byte[] perms) throws IOException
+ {
+ COSString user = new COSString();
+ user.append( perms );
+ dictionary.setItem( COSName.PERMS, user );
+ }
+
+ /**
+ * Get the Perms entry in the encryption dictionary.
+ *
+ * @return A 16 byte array or null if there is no Perms entry.
+ *
+ * @throws IOException If there is an error accessing the data.
+ */
+ public byte[] getPerms() throws IOException
+ {
+ byte[] perms = null;
+ COSString cos_perms = (COSString)dictionary.getDictionaryObject( COSName.PERMS );
+ if( cos_perms != null )
+ {
+ perms = cos_perms.getBytes();
+ }
+ return perms;
+ }
+
}
\ No newline at end of file
Modified: pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/encryption/ProtectionPolicy.java
URL: http://svn.apache.org/viewvc/pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/encryption/ProtectionPolicy.java?rev=1580417&r1=1580416&r2=1580417&view=diff
==============================================================================
--- pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/encryption/ProtectionPolicy.java (original)
+++ pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/encryption/ProtectionPolicy.java Sun Mar 23 02:01:05 2014
@@ -41,13 +41,13 @@ public abstract class ProtectionPolicy
* The default value is 40 bits, which provides a low security level
* but is compatible with old versions of Acrobat Reader.
*
- * @param l the length in bits (must be 40 or 128)
+ * @param l the length in bits (must be 40, 128 or 256)
*/
public void setEncryptionKeyLength(int l)
{
- if(l!=40 && l!=128)
+ if(l!=40 && l!=128 && l!=256)
{
- throw new RuntimeException("Invalid key length '" + l + "' value must be 40 or 128!");
+ throw new IllegalArgumentException("Invalid key length '" + l + "' value must be 40, 128 or 256!");
}
encryptionKeyLength = l;
}
Modified: pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/encryption/PublicKeySecurityHandler.java
URL: http://svn.apache.org/viewvc/pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/encryption/PublicKeySecurityHandler.java?rev=1580417&r1=1580416&r2=1580417&view=diff
==============================================================================
--- pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/encryption/PublicKeySecurityHandler.java (original)
+++ pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/encryption/PublicKeySecurityHandler.java Sun Mar 23 02:01:05 2014
@@ -246,6 +246,10 @@ public final class PublicKeySecurityHand
*/
public void prepareDocumentForEncryption(PDDocument doc) throws IOException
{
+ if (keyLength == 256)
+ {
+ throw new IOException("256 bit key length is not supported yet for public key security");
+ }
try
{
Security.addProvider(new BouncyCastleProvider());
Modified: pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/encryption/SecurityHandler.java
URL: http://svn.apache.org/viewvc/pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/encryption/SecurityHandler.java?rev=1580417&r1=1580416&r2=1580417&view=diff
==============================================================================
--- pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/encryption/SecurityHandler.java (original)
+++ pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/encryption/SecurityHandler.java Sun Mar 23 02:01:05 2014
@@ -22,10 +22,12 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
@@ -47,6 +49,7 @@ import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.cos.COSObject;
import org.apache.pdfbox.cos.COSStream;
import org.apache.pdfbox.cos.COSString;
+import org.apache.pdfbox.io.IOUtils;
import org.apache.pdfbox.pdmodel.PDDocument;
/**
@@ -55,6 +58,7 @@ import org.apache.pdfbox.pdmodel.PDDocum
*
* @author Ben Litchfield
* @author Benoit Guillon
+ * @author Manuel Kasper
*/
public abstract class SecurityHandler
{
@@ -213,96 +217,138 @@ public abstract class SecurityHandler
public void encryptData(long objectNumber, long genNumber, InputStream data,
OutputStream output, boolean decrypt) throws IOException
{
- if (useAES && !decrypt)
+ // Determine whether we're using Algorithm 1 (for RC4 and AES-128), or 1.A (for AES-256)
+ if (useAES && encryptionKey.length == 32)
{
- throw new IllegalArgumentException("AES encryption is not yet implemented.");
- }
+ byte[] iv = new byte[16];
+
+ if (decrypt)
+ {
+ // read IV from stream
+ data.read(iv);
+ }
+ else
+ {
+ // generate random IV and write to stream
+ SecureRandom rnd = new SecureRandom();
+ rnd.nextBytes(iv);
+ output.write(iv);
+ }
- byte[] newKey = new byte[encryptionKey.length + 5];
- System.arraycopy(encryptionKey, 0, newKey, 0, 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);
-
- // step 3
- MessageDigest md = MessageDigests.getMD5();
- md.update(newKey);
- if (useAES)
- {
- md.update(AES_SALT);
- }
- byte[] digestedKey = md.digest();
+ Cipher cipher;
+ try
+ {
+ cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+ SecretKeySpec keySpec = new SecretKeySpec(encryptionKey, "AES");
+ IvParameterSpec ivSpec = new IvParameterSpec(iv);
+ cipher.init(decrypt ? Cipher.DECRYPT_MODE : Cipher.ENCRYPT_MODE, keySpec, ivSpec);
- // step 4
- int length = Math.min(newKey.length, 16);
- byte[] finalKey = new byte[length];
- System.arraycopy(digestedKey, 0, finalKey, 0, length);
+ }
+ catch (GeneralSecurityException e)
+ {
+ throw new IOException(e);
+ }
- if (useAES)
+ CipherInputStream cis = new CipherInputStream(data, cipher);
+ try
+ {
+ IOUtils.copy(cis, output);
+ }
+ finally
+ {
+ cis.close();
+ }
+ }
+ else
{
- byte[] iv = new byte[16];
+ if (useAES && !decrypt)
+ {
+ throw new IllegalArgumentException("AES encryption with key length other than 256 bits is not yet implemented.");
+ }
- data.read(iv);
+ byte[] newKey = new byte[encryptionKey.length + 5];
+ System.arraycopy(encryptionKey, 0, newKey, 0, encryptionKey.length);
+ // PDF 1.4 reference pg 73
+ // step 1
+ // we have the reference
- try
- {
- Cipher decryptCipher;
- try
- {
- decryptCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
- }
- catch (NoSuchAlgorithmException e)
- {
- // should never happen
- throw new RuntimeException(e);
- }
+ // 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);
- SecretKey aesKey = new SecretKeySpec(finalKey, "AES");
+ // step 3
+ MessageDigest md = MessageDigests.getMD5();
+ md.update(newKey);
+ if (useAES)
+ {
+ md.update(AES_SALT);
+ }
+ byte[] digestedKey = md.digest();
- IvParameterSpec ips = new IvParameterSpec(iv);
+ // step 4
+ int length = Math.min(newKey.length, 16);
+ byte[] finalKey = new byte[length];
+ System.arraycopy(digestedKey, 0, finalKey, 0, length);
- decryptCipher.init(decrypt ? Cipher.DECRYPT_MODE : Cipher.ENCRYPT_MODE, aesKey, ips);
+ if (useAES)
+ {
+ byte[] iv = new byte[16];
- CipherInputStream cipherStream = new CipherInputStream(data, decryptCipher);
+ data.read(iv);
try
{
- byte[] buffer = new byte[4096];
- for (int n = 0; -1 != (n = cipherStream.read(buffer));)
+ Cipher decryptCipher;
+ try
+ {
+ decryptCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+ }
+ catch (NoSuchAlgorithmException e)
+ {
+ // should never happen
+ throw new RuntimeException(e);
+ }
+
+ SecretKey aesKey = new SecretKeySpec(finalKey, "AES");
+ IvParameterSpec ips = new IvParameterSpec(iv);
+ decryptCipher.init(decrypt ? Cipher.DECRYPT_MODE : Cipher.ENCRYPT_MODE, aesKey, ips);
+ CipherInputStream cipherStream = new CipherInputStream(data, decryptCipher);
+
+ try
+ {
+ byte[] buffer = new byte[4096];
+ for (int n = 0; -1 != (n = cipherStream.read(buffer));)
+ {
+ output.write(buffer, 0, n);
+ }
+ }
+ finally
{
- output.write(buffer, 0, n);
+ cipherStream.close();
}
}
- finally
+ catch (InvalidKeyException e)
{
- cipherStream.close();
+ throw new IOException(e);
+ }
+ catch (InvalidAlgorithmParameterException e)
+ {
+ throw new IOException(e);
+ }
+ catch (NoSuchPaddingException e)
+ {
+ throw new IOException(e);
}
}
- catch (InvalidKeyException e)
- {
- throw new IOException(e);
- }
- catch (InvalidAlgorithmParameterException e)
- {
- throw new IOException(e);
- }
- catch (NoSuchPaddingException e)
+ else
{
- throw new IOException(e);
+ rc4.setKey(finalKey);
+ rc4.write(data, output);
}
}
- else
- {
- rc4.setKey(finalKey);
- rc4.write(data, output);
- }
output.flush();
}
Modified: pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/encryption/StandardSecurityHandler.java
URL: http://svn.apache.org/viewvc/pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/encryption/StandardSecurityHandler.java?rev=1580417&r1=1580416&r2=1580417&view=diff
==============================================================================
--- pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/encryption/StandardSecurityHandler.java (original)
+++ pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/encryption/StandardSecurityHandler.java Sun Mar 23 02:01:05 2014
@@ -20,8 +20,14 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;
+import java.security.GeneralSecurityException;
import java.security.MessageDigest;
+import java.security.SecureRandom;
import java.util.Arrays;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
import org.apache.pdfbox.cos.COSArray;
import org.apache.pdfbox.cos.COSName;
@@ -33,6 +39,7 @@ import org.apache.pdfbox.pdmodel.PDDocum
* @see StandardProtectionPolicy to see how to protect document with this security handler.
* @author Ben Litchfield
* @author Benoit Guillon
+ * @author Manuel Kasper
*/
public final class StandardSecurityHandler extends SecurityHandler
{
@@ -54,6 +61,9 @@ public final class StandardSecurityHandl
(byte)0x69, (byte)0x7A
};
+ // hashes used for Algorithm 2.B, depending on remainder from E modulo 3
+ private static final String[] HASHES_2B = new String[] {"SHA-256", "SHA-384", "SHA-512"};
+
private static final int DEFAULT_VERSION = 1;
private static final int DEFAULT_REVISION = 3;
@@ -92,6 +102,11 @@ public final class StandardSecurityHandl
{
return DEFAULT_VERSION;
}
+ else if(keyLength == 256)
+ {
+ return 5;
+ }
+
return 2;
}
@@ -108,6 +123,10 @@ public final class StandardSecurityHandl
{
return 2;
}
+ if (version == 5)
+ {
+ return 6; // note about revision 5: "Shall not be used. This value was used by a deprecated Adobe extension."
+ }
if ( version == 2 || version == 3 || policy.getPermissions().hasAnyRevision3PermissionSet())
{
return 3;
@@ -187,44 +206,97 @@ public final class StandardSecurityHandl
byte[] userKey = encryption.getUserKey();
byte[] ownerKey = encryption.getOwnerKey();
+ byte[] ue = null, oe = null;
- if( isUserPassword(password.getBytes("ISO-8859-1"), userKey, ownerKey,
- dicPermissions, documentIDBytes, dicRevision,
- dicLength, encryptMetadata) )
+ String passwordCharset = "ISO-8859-1";
+ if (dicRevision == 6)
{
- currentAccessPermission = new AccessPermission( dicPermissions );
+ passwordCharset = "UTF-8";
+ ue = encryption.getUserEncryptionKey();
+ oe = encryption.getOwnerEncryptionKey();
+ }
+
+ if( isOwnerPassword(password.getBytes(passwordCharset), userKey, ownerKey,
+ dicPermissions, documentIDBytes, dicRevision,
+ dicLength, encryptMetadata) )
+ {
+ currentAccessPermission = AccessPermission.getOwnerAccessPermission();
+
+ byte[] computedPassword;
+ if (dicRevision == 6)
+ {
+ computedPassword = password.getBytes(passwordCharset);
+ }
+ else
+ {
+ computedPassword = getUserPassword(password.getBytes(passwordCharset),
+ ownerKey, dicRevision, dicLength );
+ }
+
encryptionKey =
computeEncryptedKey(
- password.getBytes("ISO-8859-1"),
- ownerKey,
+ computedPassword,
+ ownerKey, userKey, oe, ue,
dicPermissions,
documentIDBytes,
dicRevision,
dicLength,
- encryptMetadata );
+ encryptMetadata, true );
}
- else if( isOwnerPassword(password.getBytes("ISO-8859-1"), userKey, ownerKey,
- dicPermissions, documentIDBytes, dicRevision,
- dicLength, encryptMetadata) )
+ else if( isUserPassword(password.getBytes(passwordCharset), userKey, ownerKey,
+ dicPermissions, documentIDBytes, dicRevision,
+ dicLength, encryptMetadata) )
{
- currentAccessPermission = AccessPermission.getOwnerAccessPermission();
- byte[] userPassword = getUserPassword(password.getBytes("ISO-8859-1"),
- ownerKey, dicRevision, dicLength );
+ currentAccessPermission = new AccessPermission( dicPermissions );
encryptionKey =
computeEncryptedKey(
- userPassword,
- ownerKey,
+ password.getBytes(passwordCharset),
+ ownerKey, userKey, oe, ue,
dicPermissions,
documentIDBytes,
dicRevision,
dicLength,
- encryptMetadata);
+ encryptMetadata, false );
}
else
{
throw new IOException("Cannot decrypt PDF, the password is incorrect");
}
+ if (dicRevision == 6)
+ {
+ // Algorithm 13: validate permissions ("Perms" field)
+ try
+ {
+ Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
+ cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(encryptionKey, "AES"));
+ byte[] perms = cipher.doFinal(encryption.getPerms());
+
+ if (perms[9] != 'a' || perms[10] != 'd' || perms[11] != 'b')
+ {
+ throw new IOException("Verification of permissions failed (constant)");
+ }
+
+ int permsP = perms[0] & 0xFF | perms[1] & 0xFF << 8 | perms[2] & 0xFF << 16 |
+ perms[3] & 0xFF << 24;
+
+ if (permsP != dicPermissions)
+ {
+ throw new IOException("Verification of permissions failed (" + permsP +
+ " != " + dicPermissions + ")");
+ }
+
+ if (encryptMetadata && perms[8] != 'T' || !encryptMetadata && perms[8] != 'F')
+ {
+ throw new IOException("Verification of permissions failed (EncryptMetadata)");
+ }
+ }
+ catch (GeneralSecurityException e)
+ {
+ throw new IOException(e);
+ }
+ }
+
// detect whether AES encryption is used. This assumes that the encryption algo is
// stored in the PDCryptFilterDictionary
PDCryptFilterDictionary stdCryptFilterDictionary = encryption.getStdCryptFilterDictionary();
@@ -234,7 +306,8 @@ public final class StandardSecurityHandl
COSName cryptFilterMethod = stdCryptFilterDictionary.getCryptFilterMethod();
if (cryptFilterMethod != null)
{
- setAES("AESV2".equalsIgnoreCase(cryptFilterMethod.getName()));
+ setAES("AESV2".equalsIgnoreCase(cryptFilterMethod.getName()) ||
+ "AESV3".equalsIgnoreCase(cryptFilterMethod.getName()));
}
}
}
@@ -271,6 +344,12 @@ public final class StandardSecurityHandl
{
userPassword = "";
}
+
+ // If no owner password is set, use the user password instead.
+ if (ownerPassword.isEmpty())
+ {
+ ownerPassword = userPassword;
+ }
int permissionInt = policy.getPermissions().getPermissionBytes();
@@ -278,44 +357,134 @@ public final class StandardSecurityHandl
int length = keyLength/8;
- COSArray idArray = document.getDocument().getDocumentID();
-
- //check if the document has an id yet. If it does not then
- //generate one
- if( idArray == null || idArray.size() < 2 )
+ if (revision == 6)
{
- MessageDigest md = MessageDigests.getMD5();
- BigInteger time = BigInteger.valueOf( System.currentTimeMillis() );
- md.update( time.toByteArray() );
- md.update( ownerPassword.getBytes("ISO-8859-1") );
- md.update( userPassword.getBytes("ISO-8859-1") );
- md.update( document.getDocument().toString().getBytes() );
-
- byte[] id = md.digest( this.toString().getBytes("ISO-8859-1") );
- COSString idString = new COSString();
- idString.append( id );
+ try
+ {
+ SecureRandom rnd = new SecureRandom();
+ Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
- idArray = new COSArray();
- idArray.add( idString );
- idArray.add( idString );
- document.getDocument().setDocumentID( idArray );
+ // make a random 256-bit file encryption key
+ encryptionKey = new byte[32];
+ rnd.nextBytes(encryptionKey);
+
+ // Algorithm 8a: Compute U
+ byte[] userPasswordBytes = truncate127(userPassword.getBytes("UTF-8"));
+ byte[] userValidationSalt = new byte[8];
+ byte[] userKeySalt = new byte[8];
+ rnd.nextBytes(userValidationSalt);
+ rnd.nextBytes(userKeySalt);
+ byte[] hashU = computeHash2B(concat(userPasswordBytes, userValidationSalt),
+ userPasswordBytes, null);
+ byte[] u = concat(hashU, userValidationSalt, userKeySalt);
+
+ // Algorithm 8b: Compute UE
+ byte[] hashUE = computeHash2B(concat(userPasswordBytes, userKeySalt),
+ userPasswordBytes, null);
+ cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(hashUE, "AES"),
+ new IvParameterSpec(new byte[16]));
+ byte[] ue = cipher.doFinal(encryptionKey);
+
+ // Algorithm 9a: Compute O
+ byte[] ownerPasswordBytes = truncate127(ownerPassword.getBytes("UTF-8"));
+ byte[] ownerValidationSalt = new byte[8];
+ byte[] ownerKeySalt = new byte[8];
+ rnd.nextBytes(ownerValidationSalt);
+ rnd.nextBytes(ownerKeySalt);
+ byte[] hashO = computeHash2B(concat(ownerPasswordBytes, ownerValidationSalt, u),
+ ownerPasswordBytes, u);
+ byte[] o = concat(hashO, ownerValidationSalt, ownerKeySalt);
+
+ // Algorithm 9b: Compute OE
+ byte[] hashOE = computeHash2B(concat(ownerPasswordBytes, ownerKeySalt, u),
+ ownerPasswordBytes, u);
+ cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(hashOE, "AES"),
+ new IvParameterSpec(new byte[16]));
+ byte[] oe = cipher.doFinal(encryptionKey);
+
+ // Set keys and other required constants in encryption dictionary
+ encryptionDictionary.setUserKey(u);
+ encryptionDictionary.setUserEncryptionKey(ue);
+ encryptionDictionary.setOwnerKey(o);
+ encryptionDictionary.setOwnerEncryptionKey(oe);
+
+ PDCryptFilterDictionary cryptFilterDictionary = new PDCryptFilterDictionary();
+ cryptFilterDictionary.setCryptFilterMethod(COSName.AESV3);
+ cryptFilterDictionary.setLength(keyLength);
+ encryptionDictionary.setStdCryptFilterDictionary(cryptFilterDictionary);
+ encryptionDictionary.setStreamFilterName(COSName.STD_CF);
+ encryptionDictionary.setStringFilterName(COSName.STD_CF);
+ setAES(true);
+
+ // Algorithm 10: compute "Perms" value
+ byte[] perms = new byte[16];
+ perms[0] = (byte)permissionInt;
+ perms[1] = (byte)(permissionInt >>> 8);
+ perms[2] = (byte)(permissionInt >>> 16);
+ perms[3] = (byte)(permissionInt >>> 24);
+ perms[4] = perms[5] = perms[6] = perms[7] = (byte)0xFF;
+ perms[8] = 'T'; // we always use EncryptMetadata == true
+ perms[9] = 'a';
+ perms[10] = 'd';
+ perms[11] = 'b';
+ for (int i = 12; i <= 15; i++)
+ {
+ perms[i] = (byte)rnd.nextInt();
+ }
+
+ cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(encryptionKey, "AES"),
+ new IvParameterSpec(new byte[16]));
+
+ byte[] permsEnc = cipher.doFinal(perms);
+
+ encryptionDictionary.setPerms(permsEnc);
+ }
+ catch (GeneralSecurityException e)
+ {
+ throw new IOException(e);
+ }
}
+ else
+ {
+ COSArray idArray = document.getDocument().getDocumentID();
+
+ //check if the document has an id yet. If it does not then
+ //generate one
+ if( idArray == null || idArray.size() < 2 )
+ {
+ MessageDigest md = MessageDigests.getMD5();
+ BigInteger time = BigInteger.valueOf( System.currentTimeMillis() );
+ md.update( time.toByteArray() );
+ md.update( ownerPassword.getBytes("ISO-8859-1") );
+ md.update( userPassword.getBytes("ISO-8859-1") );
+ md.update( document.getDocument().toString().getBytes() );
+
+ byte[] id = md.digest( this.toString().getBytes("ISO-8859-1") );
+ COSString idString = new COSString();
+ idString.append( id );
+
+ idArray = new COSArray();
+ idArray.add( idString );
+ idArray.add( idString );
+ document.getDocument().setDocumentID( idArray );
+ }
- COSString id = (COSString)idArray.getObject( 0 );
+ COSString id = (COSString)idArray.getObject( 0 );
- byte[] ownerBytes = computeOwnerPassword(
- ownerPassword.getBytes("ISO-8859-1"),
- userPassword.getBytes("ISO-8859-1"), revision, length);
+ byte[] ownerBytes = computeOwnerPassword(
+ ownerPassword.getBytes("ISO-8859-1"),
+ userPassword.getBytes("ISO-8859-1"), revision, length);
- byte[] userBytes = computeUserPassword(
- userPassword.getBytes("ISO-8859-1"),
- ownerBytes, permissionInt, id.getBytes(), revision, length, true);
+ byte[] userBytes = computeUserPassword(
+ userPassword.getBytes("ISO-8859-1"),
+ ownerBytes, permissionInt, id.getBytes(), revision, length, true);
- encryptionKey = computeEncryptedKey(userPassword.getBytes("ISO-8859-1"), ownerBytes,
- permissionInt, id.getBytes(), revision, length, true);
+ encryptionKey = computeEncryptedKey(userPassword.getBytes("ISO-8859-1"), ownerBytes,
+ null, null, null, permissionInt, id.getBytes(), revision, length, true, false);
- encryptionDictionary.setOwnerKey(ownerBytes);
- encryptionDictionary.setUserKey(userBytes);
+ encryptionDictionary.setOwnerKey(ownerBytes);
+ encryptionDictionary.setUserKey(userBytes);
+ }
document.setEncryptionDictionary( encryptionDictionary );
document.getDocument().setEncryptionDictionary(encryptionDictionary.getCOSDictionary());
@@ -337,14 +506,33 @@ public final class StandardSecurityHandl
*
* @throws IOException If there is an error accessing data.
*/
- public final boolean isOwnerPassword(byte[] ownerPassword, byte[] user, byte[] owner,
- int permissions, byte[] id, int encRevision, int length,
- boolean encryptMetadata)
- throws IOException
- {
- byte[] userPassword = getUserPassword( ownerPassword, owner, encRevision, length );
- return isUserPassword( userPassword, user, owner, permissions, id, encRevision, length,
- encryptMetadata );
+ public boolean isOwnerPassword(byte[] ownerPassword, byte[] user, byte[] owner,
+ int permissions, byte[] id, int encRevision, int length,
+ boolean encryptMetadata) throws IOException
+ {
+ if (encRevision == 6)
+ {
+ ownerPassword = truncate127(ownerPassword);
+
+ byte[] oHash = new byte[32];
+ byte[] oValidationSalt = new byte[8];
+ System.arraycopy(owner, 0, oHash, 0, 32);
+ System.arraycopy(owner, 32, oValidationSalt, 0, 8);
+
+ byte[] hash = computeHash2A(ownerPassword, oValidationSalt, user);
+ return Arrays.equals(hash, oHash);
+ }
+ else if (encRevision == 5)
+ {
+ // Shall not be used. This value was used by a deprecated Adobe extension.
+ throw new IOException("Unsupported Encryption Revision " + encRevision);
+ }
+ else
+ {
+ byte[] userPassword = getUserPassword( ownerPassword, owner, encRevision, length );
+ return isUserPassword( userPassword, user, owner, permissions, id, encRevision, length,
+ encryptMetadata );
+ }
}
/**
@@ -359,8 +547,8 @@ public final class StandardSecurityHandl
*
* @throws IOException If there is an error accessing data while generating the user password.
*/
- public final byte[] getUserPassword( byte[] ownerPassword, byte[] owner, int encRevision,
- long length ) throws IOException
+ public byte[] getUserPassword( byte[] ownerPassword, byte[] owner, int encRevision,
+ long length ) throws IOException
{
ByteArrayOutputStream result = new ByteArrayOutputStream();
@@ -419,63 +607,105 @@ public final class StandardSecurityHandl
* Compute the encryption key.
*
* @param password The password to compute the encrypted key.
- * @param o The o entry of the encryption dictionary.
+ * @param o The O entry of the encryption dictionary.
+ * @param u The U entry of the encryption dictionary.
+ * @param oe The OE entry of the encryption dictionary.
+ * @param ue The UE entry of the encryption dictionary.
* @param permissions The permissions for the document.
* @param id The document id.
* @param encRevision The revision of the encryption algorithm.
* @param length The length of the encryption key.
* @param encryptMetadata The encryption metadata
+ * @param isOwnerPassword whether the password given is the owner password (for revision 6)
*
* @return The encrypted key bytes.
*
* @throws IOException If there is an error with encryption.
*/
- public final byte[] computeEncryptedKey(byte[] password, byte[] o, int permissions, byte[] id,
- int encRevision, int length, boolean encryptMetadata)
- throws IOException
+ public byte[] computeEncryptedKey(byte[] password, byte[] o, byte[] u, byte[] oe, byte[] ue,
+ int permissions, byte[] id, int encRevision, int length,
+ boolean encryptMetadata, boolean isOwnerPassword)
+ throws IOException
{
byte[] result = new byte[ length ];
+
+ if (encRevision == 6)
+ {
+ //Algorithm 2.A, based on SHA-2 and AES
+
+ byte[] hash, fileKeyEnc;
+ if (isOwnerPassword)
+ {
+ byte[] oKeySalt = new byte[8];
+ System.arraycopy(o, 40, oKeySalt, 0, 8);
+ hash = computeHash2A(password, oKeySalt, u);
+ fileKeyEnc = oe;
+ }
+ else
+ {
+ byte[] uKeySalt = new byte[8];
+ System.arraycopy(u, 40, uKeySalt, 0, 8);
+ hash = computeHash2A(password, uKeySalt, null);
+ fileKeyEnc = ue;
+ }
+
+ try
+ {
+ Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
+ cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(hash, "AES"),
+ new IvParameterSpec(new byte[16]));
+ result = cipher.doFinal(fileKeyEnc);
+ }
+ catch (GeneralSecurityException e)
+ {
+ throw new IOException(e);
+ }
+ }
+ else
+ {
+ //Algorithm 2, based on MD5
- //PDFReference 1.4 pg 78
- byte[] padded = truncateOrPad( password );
+ //PDFReference 1.4 pg 78
+ byte[] padded = truncateOrPad( password );
- MessageDigest md = MessageDigests.getMD5();
- md.update( padded );
+ MessageDigest md = MessageDigests.getMD5();
+ md.update( padded );
- md.update( o );
+ md.update( o );
- md.update( (byte)permissions );
- md.update( (byte)(permissions >>> 8));
- md.update( (byte)(permissions >>> 16));
- md.update( (byte)(permissions >>> 24));
+ md.update( (byte)permissions );
+ md.update( (byte)(permissions >>> 8));
+ md.update( (byte)(permissions >>> 16));
+ md.update( (byte)(permissions >>> 24));
- md.update( id );
+ 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();
+ //(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();
- if( encRevision == 3 || encRevision == 4)
- {
- for( int i=0; i<50; i++ )
+ if( encRevision == 3 || encRevision == 4)
{
- md.reset();
- md.update( digest, 0, length );
- digest = md.digest();
+ for( int i=0; i<50; i++ )
+ {
+ md.reset();
+ md.update( digest, 0, length );
+ digest = md.digest();
+ }
}
- }
- if( encRevision == 2 && length != 5 )
- {
- throw new IOException(
- "Error: length should be 5 when revision is two actual=" + length );
+ if( encRevision == 2 && length != 5 )
+ {
+ throw new IOException(
+ "Error: length should be 5 when revision is two actual=" + length );
+ }
+ System.arraycopy( digest, 0, result, 0, length );
}
- System.arraycopy( digest, 0, result, 0, length );
return result;
}
@@ -495,14 +725,13 @@ public final class StandardSecurityHandl
*
* @throws IOException if the password could not be computed
*/
- public final byte[] computeUserPassword(byte[] password, byte[] owner, int permissions,
- byte[] id, int encRevision, int length,
- boolean encryptMetadata)
- throws IOException
+ public byte[] computeUserPassword(byte[] password, byte[] owner, int permissions,
+ byte[] id, int encRevision, int length,
+ boolean encryptMetadata) throws IOException
{
ByteArrayOutputStream result = new ByteArrayOutputStream();
- byte[] encryptionKey = computeEncryptedKey( password, owner, permissions, id, encRevision,
- length, encryptMetadata );
+ byte[] encryptionKey = computeEncryptedKey( password, owner, null, null, null, permissions,
+ id, encRevision, length, encryptMetadata, true );
if( encRevision == 2 )
{
@@ -552,9 +781,8 @@ public final class StandardSecurityHandl
*
* @throws IOException if the owner password could not be computed
*/
- public final byte[] computeOwnerPassword(byte[] ownerPassword, byte[] userPassword,
- int encRevision, int length )
- throws IOException
+ public byte[] computeOwnerPassword(byte[] ownerPassword, byte[] userPassword,
+ int encRevision, int length ) throws IOException
{
byte[] ownerPadded = truncateOrPad( ownerPassword );
@@ -581,8 +809,8 @@ public final class StandardSecurityHandl
byte[] paddedUser = truncateOrPad( userPassword );
rc4.setKey( rc4Key );
- ByteArrayOutputStream crypted = new ByteArrayOutputStream();
- rc4.write( new ByteArrayInputStream( paddedUser ), crypted );
+ ByteArrayOutputStream encrypted = new ByteArrayOutputStream();
+ rc4.write( new ByteArrayInputStream( paddedUser ), encrypted );
if( encRevision == 3 || encRevision == 4 )
{
@@ -595,13 +823,13 @@ public final class StandardSecurityHandl
iterationKey[j] = (byte)(iterationKey[j] ^ (byte)i);
}
rc4.setKey( iterationKey );
- ByteArrayInputStream input = new ByteArrayInputStream( crypted.toByteArray() );
- crypted.reset();
- rc4.write( input, crypted );
+ ByteArrayInputStream input = new ByteArrayInputStream( encrypted.toByteArray() );
+ encrypted.reset();
+ rc4.write( input, encrypted );
}
}
- return crypted.toByteArray();
+ return encrypted.toByteArray();
}
/**
@@ -637,10 +865,9 @@ public final class StandardSecurityHandl
*
* @throws IOException If there is an error accessing data.
*/
- public final boolean isUserPassword(byte[] password, byte[] user, byte[] owner, int permissions,
- byte[] id, int encRevision, int length,
- boolean encryptMetadata)
- throws IOException
+ public boolean isUserPassword(byte[] password, byte[] user, byte[] owner, int permissions,
+ byte[] id, int encRevision, int length, boolean encryptMetadata)
+ throws IOException
{
byte[] passwordBytes = computeUserPassword( password, owner, permissions, id, encRevision,
length, encryptMetadata );
@@ -653,6 +880,18 @@ public final class StandardSecurityHandl
// compare first 16 bytes only
return Arrays.equals(Arrays.copyOf(user, 16), Arrays.copyOf(passwordBytes, 16));
}
+ else if (encRevision == 6)
+ {
+ password = truncate127(password);
+
+ byte[] uHash = new byte[32];
+ byte[] uValidationSalt = new byte[8];
+ System.arraycopy(user, 0, uHash, 0, 32);
+ System.arraycopy(user, 32, uValidationSalt, 0, 8);
+
+ byte[] hash = computeHash2A(password, uValidationSalt, null);
+ return Arrays.equals(hash, uHash);
+ }
else
{
throw new IOException( "Unknown Encryption Revision " + encRevision );
@@ -675,12 +914,20 @@ public final class StandardSecurityHandl
*
* @throws IOException If there is an error accessing data.
*/
- public final boolean isUserPassword(String password, byte[] user, byte[] owner, int permissions,
- byte[] id, int encRevision, int length,
- boolean encryptMetadata) throws IOException
+ public boolean isUserPassword(String password, byte[] user, byte[] owner, int permissions,
+ byte[] id, int encRevision, int length, boolean encryptMetadata)
+ throws IOException
{
- return isUserPassword(password.getBytes("ISO-8859-1"), user, owner, permissions, id,
- encRevision, length, encryptMetadata);
+ if (encRevision == 6)
+ {
+ return isUserPassword(password.getBytes("UTF-8"), user, owner, permissions, id,
+ encRevision, length, encryptMetadata);
+ }
+ else
+ {
+ return isUserPassword(password.getBytes("ISO-8859-1"), user, owner, permissions, id,
+ encRevision, length, encryptMetadata);
+ }
}
/**
@@ -699,11 +946,138 @@ public final class StandardSecurityHandl
*
* @throws IOException If there is an error accessing data.
*/
- public final boolean isOwnerPassword(String password, byte[] user, byte[] owner, int permissions,
- byte[] id, int encRevision, int length,
- boolean encryptMetadata) throws IOException
+ public boolean isOwnerPassword(String password, byte[] user, byte[] owner, int permissions,
+ byte[] id, int encRevision, int length, boolean encryptMetadata)
+ throws IOException
{
return isOwnerPassword(password.getBytes("ISO-8859-1"), user,owner,permissions, id,
encRevision, length, encryptMetadata);
}
+
+ // Algorithm 2.A from ISO 32000-1
+ private byte[] computeHash2A(byte[] password, byte[] salt, byte[] u) throws IOException
+ {
+ password = truncate127(password);
+
+ if (u == null)
+ {
+ u = new byte[0];
+ }
+ else if (u.length < 48)
+ {
+ throw new IOException("Bad U length");
+ }
+ else if (u.length > 48)
+ {
+ // must truncate
+ byte[] uTrunc = new byte[48];
+ System.arraycopy(u, 0, uTrunc, 0, 48);
+ u = uTrunc;
+ }
+
+ byte[] input = concat(password, salt, u);
+ return computeHash2B(input, password, u);
+ }
+
+ // Algorithm 2.B from ISO 32000-2
+ private static byte[] computeHash2B(byte[] input, byte[] password, byte[] userKey)
+ throws IOException
+ {
+ try
+ {
+ MessageDigest md = MessageDigest.getInstance("SHA-256");
+ byte[] k = md.digest(input);
+
+ byte[] e = null;
+ for (int round = 0; round < 64 || ((int)e[e.length-1] & 0xFF) > round - 32; round++)
+ {
+ byte[] k1;
+ if (userKey != null && userKey.length >= 48)
+ {
+ k1 = new byte[64*(password.length + k.length + 48)];
+ }
+ else
+ {
+ k1 = new byte[64*(password.length + k.length)];
+ }
+
+ int pos = 0;
+ for (int i = 0; i < 64; i++)
+ {
+ System.arraycopy(password, 0, k1, pos, password.length);
+ pos += password.length;
+ System.arraycopy(k, 0, k1, pos, k.length);
+ pos += k.length;
+ if (userKey != null && userKey.length >= 48)
+ {
+ System.arraycopy(userKey, 0, k1, pos, 48);
+ pos += 48;
+ }
+ }
+
+ byte[] kFirst = new byte[16];
+ byte[] kSecond = new byte[16];
+ System.arraycopy(k, 0, kFirst, 0, 16);
+ System.arraycopy(k, 16, kSecond, 0, 16);
+
+ Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
+ SecretKeySpec keySpec = new SecretKeySpec(kFirst, "AES");
+ IvParameterSpec ivSpec = new IvParameterSpec(kSecond);
+ cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
+ e = cipher.doFinal(k1);
+
+ byte[] eFirst = new byte[16];
+ System.arraycopy(e, 0, eFirst, 0, 16);
+ BigInteger bi = new BigInteger(1, eFirst);
+ BigInteger remainder = bi.mod(new BigInteger("3"));
+ String nextHash = HASHES_2B[remainder.intValue()];
+
+ md = MessageDigest.getInstance(nextHash);
+ k = md.digest(e);
+ }
+
+ if (k.length > 32)
+ {
+ byte[] kTrunc = new byte[32];
+ System.arraycopy(k, 0, kTrunc, 0, 32);
+ return kTrunc;
+ }
+ else
+ {
+ return k;
+ }
+ }
+ catch (GeneralSecurityException e)
+ {
+ throw new IOException(e);
+ }
+ }
+
+ private static byte[] concat(byte[] a, byte[] b)
+ {
+ byte[] o = new byte[a.length + b.length];
+ System.arraycopy(a, 0, o, 0, a.length);
+ System.arraycopy(b, 0, o, a.length, b.length);
+ return o;
+ }
+
+ private static byte[] concat(byte[] a, byte[] b, byte[] c)
+ {
+ byte[] o = new byte[a.length + b.length + c.length];
+ System.arraycopy(a, 0, o, 0, a.length);
+ System.arraycopy(b, 0, o, a.length, b.length);
+ System.arraycopy(c, 0, o, a.length + b.length, c.length);
+ return o;
+ }
+
+ private static byte[] truncate127(byte[] in)
+ {
+ if (in.length <= 127)
+ {
+ return in;
+ }
+ byte[] trunc = new byte[127];
+ System.arraycopy(in, 0, trunc, 0, 127);
+ return trunc;
+ }
}