org.apache.pdfbox.pdmodel.encryption.StandardSecurityHandler Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of pdfbox Show documentation
Show all versions of pdfbox Show documentation
The Apache PDFBox library is an open source Java tool for working with PDF documents.
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.pdfbox.pdmodel.encryption;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import org.apache.pdfbox.cos.COSArray;
import org.apache.pdfbox.cos.COSDocument;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.cos.COSString;
import org.apache.pdfbox.encryption.ARCFour;
import org.apache.pdfbox.exceptions.CryptographyException;
import org.apache.pdfbox.pdmodel.PDDocument;
/**
*
* The class implements the standard security handler as decribed
* in the PDF specifications. This security handler protects document
* with password.
*
* @see StandardProtectionPolicy to see how to protect document with this security handler.
*
* @author Ben Litchfield
* @author Benoit Guillon ([email protected])
*
*/
public class StandardSecurityHandler extends SecurityHandler
{
/**
* Type of security handler.
*/
public static final String FILTER = "Standard";
private static final int DEFAULT_VERSION = 1;
private static final int DEFAULT_REVISION = 3;
private int revision = DEFAULT_REVISION;
private StandardProtectionPolicy policy;
private ARCFour rc4 = new ARCFour();
/**
* Protection policy class for this handler.
*/
public static final Class PROTECTION_POLICY_CLASS = StandardProtectionPolicy.class;
/**
* 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
};
/**
* Constructor.
*/
public StandardSecurityHandler()
{
}
/**
* Constructor used for encryption.
*
* @param p The protection policy.
*/
public StandardSecurityHandler(StandardProtectionPolicy p)
{
policy = p;
keyLength = policy.getEncryptionKeyLength();
}
/**
* Computes the version number of the StandardSecurityHandler
* regarding the encryption key length.
* See PDF Spec 1.6 p 93 and PDF 1.7 AEL3
*
* @return The computed cersion number.
*/
private int computeVersionNumber()
{
if(keyLength == 40)
{
return DEFAULT_VERSION;
}
return 2;
}
/**
* Computes the revision version of the StandardSecurityHandler to
* use regarding the version number and the permissions bits set.
* See PDF Spec 1.6 p98
*
* @return The computed revision number.
*/
private int computeRevisionNumber()
{
if(version < 2 && !policy.getPermissions().hasAnyRevision3PermissionSet())
{
return 2;
}
if ( version == 2 || version == 3 || policy.getPermissions().hasAnyRevision3PermissionSet())
{
return 3;
}
return 4;
}
/**
* Decrypt the document.
*
* @param doc The document to be decrypted.
* @param decryptionMaterial Information used to decrypt the document.
*
* @throws IOException If there is an error accessing data.
* @throws CryptographyException If there is an error with decryption.
*/
public void decryptDocument(PDDocument doc, DecryptionMaterial decryptionMaterial)
throws CryptographyException, IOException
{
document = doc;
PDEncryptionDictionary dictionary = document.getEncryptionDictionary();
COSArray documentIDArray = document.getDocument().getDocumentID();
prepareForDecryption(dictionary, documentIDArray, decryptionMaterial);
this.proceedDecryption();
}
/**
* Prepares everything to decrypt the document.
*
* If {@link #decryptDocument(PDDocument, DecryptionMaterial)} is used, this method is
* called from there. Only if decryption of single objects is needed this should be called instead.
*
* @param encDictionary encryption dictionary, can be retrieved via {@link PDDocument#getEncryptionDictionary()}
* @param documentIDArray document id which is returned via {@link COSDocument#getDocumentID()}
* @param decryptionMaterial Information used to decrypt the document.
*
* @throws IOException If there is an error accessing data.
* @throws CryptographyException If there is an error with decryption.
*/
public void prepareForDecryption(PDEncryptionDictionary encDictionary, COSArray documentIDArray,
DecryptionMaterial decryptionMaterial)
throws CryptographyException, IOException
{
if(!(decryptionMaterial instanceof StandardDecryptionMaterial))
{
throw new CryptographyException("Provided decryption material is not compatible with the document");
}
decryptMetadata = encDictionary.isEncryptMetaData();
StandardDecryptionMaterial material = (StandardDecryptionMaterial)decryptionMaterial;
String password = material.getPassword();
if(password == null)
{
password = "";
}
int dicPermissions = encDictionary.getPermissions();
int dicRevision = encDictionary.getRevision();
int dicLength = encDictionary.getLength()/8;
//some documents may have not document id, see
//test\encryption\encrypted_doc_no_id.pdf
byte[] documentIDBytes = null;
if( documentIDArray != null && documentIDArray.size() >= 1 )
{
COSString id = (COSString)documentIDArray.getObject( 0 );
documentIDBytes = id.getBytes();
}
else
{
documentIDBytes = new byte[0];
}
// we need to know whether the meta data was encrypted for password calculation
boolean encryptMetadata = encDictionary.isEncryptMetaData();
byte[] u = encDictionary.getUserKey();
byte[] o = encDictionary.getOwnerKey();
boolean isUserPassword =
isUserPassword(
password.getBytes("ISO-8859-1"),
u,
o,
dicPermissions,
documentIDBytes,
dicRevision,
dicLength,
encryptMetadata);
boolean isOwnerPassword =
isOwnerPassword(
password.getBytes("ISO-8859-1"),
u,
o,
dicPermissions,
documentIDBytes,
dicRevision,
dicLength,
encryptMetadata);
if( isUserPassword )
{
currentAccessPermission = new AccessPermission( dicPermissions );
currentAccessPermission.setReadOnly();
encryptionKey =
computeEncryptedKey(
password.getBytes("ISO-8859-1"),
o,
dicPermissions,
documentIDBytes,
dicRevision,
dicLength,
encryptMetadata );
}
else if( isOwnerPassword )
{
currentAccessPermission = AccessPermission.getOwnerAccessPermission();
byte[] computedUserPassword = getUserPassword(password.getBytes("ISO-8859-1"),o,dicRevision,dicLength );
encryptionKey =
computeEncryptedKey(
computedUserPassword,
o,
dicPermissions,
documentIDBytes,
dicRevision,
dicLength,
encryptMetadata);
}
else
{
throw new CryptographyException(
"Error: The supplied password does not match either the owner or user password in the document." );
}
if (encDictionary.getVersion() == 4 || encDictionary.getVersion() == 5)
{
// detect whether AES encryption is used. This assumes that the encryption algo is
// stored in the PDCryptFilterDictionary
// However, crypt filters are used only when V is 4 or 5.
PDCryptFilterDictionary stdCryptFilterDictionary = encDictionary.getStdCryptFilterDictionary();
if (stdCryptFilterDictionary != null)
{
COSName cryptFilterMethod = stdCryptFilterDictionary.getCryptFilterMethod();
if (cryptFilterMethod != null)
{
setAES("AESV2".equalsIgnoreCase(cryptFilterMethod.getName()));
}
}
}
}
/**
* Prepare document for encryption.
*
* @param doc The document to encrypt.
*
* @throws IOException If there is an error accessing data.
* @throws CryptographyException If there is an error with decryption.
*/
public void prepareDocumentForEncryption(PDDocument doc) throws CryptographyException, IOException
{
document = doc;
PDEncryptionDictionary encryptionDictionary = document.getEncryptionDictionary();
if(encryptionDictionary == null)
{
encryptionDictionary = new PDEncryptionDictionary();
}
version = computeVersionNumber();
revision = computeRevisionNumber();
encryptionDictionary.setFilter(FILTER);
encryptionDictionary.setVersion(version);
if (version != 4 && version != 5)
{
// remove CF, StmF, and StrF entries that may be left from a previous encryption
encryptionDictionary.removeV45filters();
}
encryptionDictionary.setRevision(revision);
encryptionDictionary.setLength(keyLength);
String ownerPassword = policy.getOwnerPassword();
String userPassword = policy.getUserPassword();
if( ownerPassword == null )
{
ownerPassword = "";
}
if( userPassword == null )
{
userPassword = "";
}
int permissionInt = policy.getPermissions().getPermissionBytes();
encryptionDictionary.setPermissions(permissionInt);
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 )
{
idArray = new COSArray();
try
{
MessageDigest md = MessageDigest.getInstance( "MD5" );
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("ISO-8859-1") );
byte[] id = md.digest( this.toString().getBytes("ISO-8859-1") );
COSString idString = new COSString();
idString.append( id );
idArray.add( idString );
idArray.add( idString );
document.getDocument().setDocumentID( idArray );
}
catch( NoSuchAlgorithmException e )
{
throw new CryptographyException( e );
}
catch(IOException e )
{
throw new CryptographyException( e );
}
}
COSString id = (COSString)idArray.getObject( 0 );
byte[] o = computeOwnerPassword(
ownerPassword.getBytes("ISO-8859-1"),
userPassword.getBytes("ISO-8859-1"), revision, length);
byte[] u = computeUserPassword(
userPassword.getBytes("ISO-8859-1"),
o, permissionInt, id.getBytes(), revision, length, true);
encryptionKey = computeEncryptedKey(
userPassword.getBytes("ISO-8859-1"), o, permissionInt, id.getBytes(), revision, length, true);
encryptionDictionary.setOwnerKey(o);
encryptionDictionary.setUserKey(u);
document.setEncryptionDictionary( encryptionDictionary );
document.getDocument().setEncryptionDictionary(encryptionDictionary.getCOSDictionary());
}
/**
* 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.
* @param encryptMetadata The encryption metadata
*
* @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(
byte[] ownerPassword,
byte[] u,
byte[] o,
int permissions,
byte[] id,
int encRevision,
int length,
boolean encryptMetadata)
throws CryptographyException, IOException
{
byte[] userPassword = getUserPassword( ownerPassword, o, encRevision, length );
return isUserPassword( userPassword, u, o, permissions, id, encRevision, length, encryptMetadata );
}
/**
* 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(
byte[] ownerPassword,
byte[] o,
int encRevision,
int length )
throws CryptographyException, IOException
{
try
{
ByteArrayOutputStream result = new ByteArrayOutputStream();
byte[] rc4Key = computeRC4key(ownerPassword, encRevision, length);
if( encRevision == 2 && length != 5 )
{
throw new CryptographyException(
"Error: Expected length=5 actual=" + length );
}
//3.7 step 2
if( encRevision == 2 )
{
rc4.setKey( rc4Key );
rc4.write( o, result );
}
else if( encRevision == 3 || encRevision == 4)
{
byte[] iterationKey = new byte[ rc4Key.length ];
byte[] otemp = new byte[ o.length ]; //sm
System.arraycopy( o, 0, otemp, 0, o.length ); //sm
rc4.write( o, result);//sm
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);
}
rc4.setKey( iterationKey );
result.reset(); //sm
rc4.write( otemp, result ); //sm
otemp = result.toByteArray(); //sm
}
}
return result.toByteArray();
}
catch( NoSuchAlgorithmException e )
{
throw new CryptographyException( e );
}
}
/**
* Compute the encryption key.
*
* @param password The password to compute the encrypted key.
* @param o The o 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
*
* @return The encrypted key bytes.
*
* @throws CryptographyException 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 CryptographyException
{
byte[] result = new byte[ length ];
try
{
//PDFReference 1.4 pg 78
//step1
byte[] padded = truncateOrPad( password );
//step 2
MessageDigest md = MessageDigest.getInstance("MD5");
md.update( padded );
//step 3
md.update( o );
//step 4
byte zero = (byte)(permissions >>> 0);
byte one = (byte)(permissions >>> 8);
byte two = (byte)(permissions >>> 16);
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( NoSuchAlgorithmException e )
{
throw new CryptographyException( e );
}
return result;
}
/**
* 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.
* @param encryptMetadata The encryption metadata
*
* @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(
byte[] password,
byte[] o,
int permissions,
byte[] id,
int encRevision,
int length,
boolean encryptMetadata)
throws CryptographyException, IOException
{
ByteArrayOutputStream result = new ByteArrayOutputStream();
//STEP 1
byte[] encryptionKey = computeEncryptedKey( password, o, permissions, id, encRevision, length, encryptMetadata );
if( encRevision == 2 )
{
//STEP 2
rc4.setKey( encryptionKey );
rc4.write( ENCRYPT_PADDING, result );
}
else if( encRevision == 3 || encRevision == 4 )
{
try
{
//STEP 2
MessageDigest md = MessageDigest.getInstance("MD5");
//md.update( truncateOrPad( password ) );
md.update( ENCRYPT_PADDING );
//STEP 3
md.update( id );
result.write( md.digest() );
//STEP 4 and 5
byte[] iterationKey = new byte[ encryptionKey.length ];
for( int i=0; i<20; i++ )
{
System.arraycopy( encryptionKey, 0, iterationKey, 0, iterationKey.length );
for( int j=0; j< iterationKey.length; j++ )
{
iterationKey[j] = (byte)(iterationKey[j] ^ i);
}
rc4.setKey( iterationKey );
ByteArrayInputStream input = new ByteArrayInputStream( result.toByteArray() );
result.reset();
rc4.write( input, result );
}
//step 6
byte[] finalResult = new byte[32];
System.arraycopy( result.toByteArray(), 0, finalResult, 0, 16 );
System.arraycopy( ENCRYPT_PADDING, 0, finalResult, 16, 16 );
result.reset();
result.write( finalResult );
}
catch( NoSuchAlgorithmException e )
{
throw new CryptographyException( e );
}
}
return result.toByteArray();
}
/**
* Compute the owner entry in the encryption dictionary.
*
* @param ownerPassword The plaintext owner password.
* @param userPassword The plaintext user password.
* @param encRevision The revision number of the encryption algorithm.
* @param length The length of the encryption key.
*
* @return The o entry of the encryption dictionary.
*
* @throws CryptographyException If there is an error with encryption.
* @throws IOException If there is an error accessing data.
*/
public final byte[] computeOwnerPassword(
byte[] ownerPassword,
byte[] userPassword,
int encRevision,
int length )
throws CryptographyException, IOException
{
try
{
byte[] rc4Key = computeRC4key(ownerPassword, encRevision, length);
//STEP 5
byte[] paddedUser = truncateOrPad( userPassword );
//STEP 6
rc4.setKey( rc4Key );
ByteArrayOutputStream crypted = new ByteArrayOutputStream();
rc4.write( new ByteArrayInputStream( paddedUser ), crypted );
//STEP 7
if( encRevision == 3 || encRevision == 4 )
{
byte[] iterationKey = new byte[ rc4Key.length ];
for( int i=1; i<20; i++ )
{
System.arraycopy( rc4Key, 0, iterationKey, 0, rc4Key.length );
for( int j=0; j< iterationKey.length; j++ )
{
iterationKey[j] = (byte)(iterationKey[j] ^ (byte)i);
}
rc4.setKey( iterationKey );
ByteArrayInputStream input = new ByteArrayInputStream( crypted.toByteArray() );
crypted.reset();
rc4.write( input, crypted );
}
}
//STEP 8
return crypted.toByteArray();
}
catch( NoSuchAlgorithmException e )
{
throw new CryptographyException( e.getMessage() );
}
}
// steps (a) to (d) of "Algorithm 3: Computing the encryption dictionary's O (owner password) value".
private byte[] computeRC4key(byte[] ownerPassword, int encRevision, int length)
throws NoSuchAlgorithmException, CryptographyException
{
if (encRevision == 2 && length != 5)
{
throw new CryptographyException(
"Error: Expected length=5 actual=" + length);
}
//3.3 STEP a
byte[] ownerPadded = truncateOrPad(ownerPassword);
//3.3 STEP b
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(ownerPadded);
byte[] digest = md.digest();
//3.3 STEP c
if (encRevision == 3 || encRevision == 4)
{
for (int i = 0; i < 50; i++)
{
// this deviates from the spec - however, omitting the length
// parameter prevents the file to be opened in Adobe Reader
// with the owner password when the key length is 40 bit (= 5 bytes)
md.reset();
md.update(digest, 0, length);
digest = md.digest();
}
}
//3.3 STEP d
byte[] rc4Key = new byte[(int) length];
System.arraycopy(digest, 0, rc4Key, 0, (int) length);
return rc4Key;
}
/**
* 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( byte[] password )
{
byte[] padded = new byte[ ENCRYPT_PADDING.length ];
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;
}
/**
* 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 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.
* @param encryptMetadata The encryption metadata
*
* @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(
byte[] password,
byte[] u,
byte[] o,
int permissions,
byte[] id,
int encRevision,
int length,
boolean encryptMetadata)
throws CryptographyException, IOException
{
boolean matches = false;
//STEP 1
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 IOException( "Unknown Encryption Revision " + encRevision );
}
return matches;
}
/**
* 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 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.
* @param encryptMetadata The encryption metadata
*
* @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(
String password,
byte[] u,
byte[] o,
int permissions,
byte[] id,
int encRevision,
int length,
boolean encryptMetadata )
throws CryptographyException, IOException
{
return isUserPassword(password.getBytes("ISO-8859-1"),
u,o,permissions, id, encRevision, length, encryptMetadata);
}
/**
* Check for owner password.
*
* @param password 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.
* @param encryptMetadata The encryption metadata
*
* @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(
String password,
byte[] u,
byte[] o,
int permissions,
byte[] id,
int encRevision,
int length,
boolean encryptMetadata)
throws CryptographyException, IOException
{
return isOwnerPassword(password.getBytes("ISO-8859-1"),
u,o,permissions, id, encRevision, length, encryptMetadata);
}
private static final boolean arraysEqual( byte[] first, byte[] second, int count )
{
// both arrays have to have a minimum length of count
if (first.length < count || second.length < count)
{
return false;
}
for( int i=0; i