package com.tom_roush.pdfbox.pdmodel.encryption; import android.util.Log; import com.tom_roush.pdfbox.cos.COSArray; import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.cos.COSString; import com.tom_roush.pdfbox.pdmodel.PDDocument; import com.tom_roush.pdfbox.util.Charsets; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.math.BigInteger; import java.nio.charset.Charset; import java.security.GeneralSecurityException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Arrays; import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; /** * The standard security handler. 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 * @author Manuel Kasper */ public final class StandardSecurityHandler extends SecurityHandler { /** Type of security handler. */ public static final String FILTER = "Standard"; /** Protection policy class for this handler. */ public static final Class<?> PROTECTION_POLICY_CLASS = StandardProtectionPolicy.class; /** Standard padding for encryption. */ private 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 }; // 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 StandardProtectionPolicy policy; /** * 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 version number. */ private int computeVersionNumber() { if(keyLength == 40) { return DEFAULT_VERSION; } else if(keyLength == 256) { return 5; } 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 * * @param version The version number. * * @return The computed revision number. */ private int computeRevisionNumber(int version) { if(version < 2 && !policy.getPermissions().hasAnyRevision3PermissionSet()) { return 2; } if (version == 5) { // note about revision 5: "Shall not be used. This value was used by a deprecated Adobe extension." return 6; } if ( version == 2 || version == 3 || policy.getPermissions().hasAnyRevision3PermissionSet()) { return 3; } return 4; } /** * Prepares everything to decrypt the document. * * Only if decryption of single objects is needed this should be called. * * @param encryption encryption dictionary * @param documentIDArray document id * @param decryptionMaterial Information used to decrypt the document. * * @throws IOException If there is an error accessing data. */ @Override public void prepareForDecryption(PDEncryption encryption, COSArray documentIDArray, DecryptionMaterial decryptionMaterial) throws IOException { if(!(decryptionMaterial instanceof StandardDecryptionMaterial)) { throw new IOException("Decryption material is not compatible with the document"); } setDecryptMetadata(encryption.isEncryptMetaData()); StandardDecryptionMaterial material = (StandardDecryptionMaterial)decryptionMaterial; String password = material.getPassword(); if(password == null) { password = ""; } int dicPermissions = encryption.getPermissions(); int dicRevision = encryption.getRevision(); int dicLength = encryption.getVersion() == 1 ? 5 : encryption.getLength() / 8; byte[] documentIDBytes = getDocumentIDBytes(documentIDArray); // we need to know whether the meta data was encrypted for password calculation boolean encryptMetadata = encryption.isEncryptMetaData(); byte[] userKey = encryption.getUserKey(); byte[] ownerKey = encryption.getOwnerKey(); byte[] ue = null, oe = null; Charset passwordCharset = Charsets.ISO_8859_1; if (dicRevision == 6 || dicRevision == 5) { passwordCharset = Charsets.UTF_8; ue = encryption.getUserEncryptionKey(); oe = encryption.getOwnerEncryptionKey(); } AccessPermission currentAccessPermission; if( isOwnerPassword(password.getBytes(passwordCharset), userKey, ownerKey, dicPermissions, documentIDBytes, dicRevision, dicLength, encryptMetadata) ) { currentAccessPermission = AccessPermission.getOwnerAccessPermission(); setCurrentAccessPermission(currentAccessPermission); byte[] computedPassword; if (dicRevision == 6 || dicRevision == 5) { computedPassword = password.getBytes(passwordCharset); } else { computedPassword = getUserPassword(password.getBytes(passwordCharset), ownerKey, dicRevision, dicLength ); } encryptionKey = computeEncryptedKey( computedPassword, ownerKey, userKey, oe, ue, dicPermissions, documentIDBytes, dicRevision, dicLength, encryptMetadata, true ); } else if( isUserPassword(password.getBytes(passwordCharset), userKey, ownerKey, dicPermissions, documentIDBytes, dicRevision, dicLength, encryptMetadata) ) { currentAccessPermission = new AccessPermission(dicPermissions); setCurrentAccessPermission(currentAccessPermission); encryptionKey = computeEncryptedKey( password.getBytes(passwordCharset), ownerKey, userKey, oe, ue, dicPermissions, documentIDBytes, dicRevision, dicLength, encryptMetadata, false ); } else { throw new InvalidPasswordException("Cannot decrypt PDF, the password is incorrect"); } if (dicRevision == 6 || dicRevision == 5) { validatePerms(encryption, dicPermissions, encryptMetadata); } if (encryption.getVersion() == 4 || encryption.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 = encryption.getStdCryptFilterDictionary(); if (stdCryptFilterDictionary != null) { COSName cryptFilterMethod = stdCryptFilterDictionary.getCryptFilterMethod(); if (cryptFilterMethod != null) { setAES("AESV2".equalsIgnoreCase(cryptFilterMethod.getName()) || "AESV3".equalsIgnoreCase(cryptFilterMethod.getName())); } } } } private byte[] getDocumentIDBytes(COSArray documentIDArray) { //some documents may not have document id, see //test\encryption\encrypted_doc_no_id.pdf byte[] documentIDBytes; if( documentIDArray != null && documentIDArray.size() >= 1 ) { COSString id = (COSString)documentIDArray.getObject( 0 ); documentIDBytes = id.getBytes(); } else { documentIDBytes = new byte[0]; } return documentIDBytes; } // Algorithm 13: validate permissions ("Perms" field). Relaxed to accomodate buggy encoders private void validatePerms(PDEncryption encryption, int dicPermissions, boolean encryptMetadata) throws IOException { 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') { Log.w("PdfBox-Android", "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) { Log.w("PdfBox-Android", "Verification of permissions failed (" + permsP + " != " + dicPermissions + ")"); } if (encryptMetadata && perms[8] != 'T' || !encryptMetadata && perms[8] != 'F') { Log.w("PdfBox-Android", "Verification of permissions failed (EncryptMetadata)"); } } catch (GeneralSecurityException e) { logIfStrongEncryptionMissing(); throw new IOException(e); } } /** * Prepare document for encryption. * * @param document The documeent to encrypt. * * @throws IOException If there is an error accessing data. */ public void prepareDocumentForEncryption(PDDocument document) throws IOException { PDEncryption encryptionDictionary = document.getEncryption(); if(encryptionDictionary == null) { encryptionDictionary = new PDEncryption(); } int version = computeVersionNumber(); int revision = computeRevisionNumber(version); 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 = ""; } // If no owner password is set, use the user password instead. if (ownerPassword.isEmpty()) { ownerPassword = userPassword; } int permissionInt = policy.getPermissions().getPermissionBytes(); encryptionDictionary.setPermissions(permissionInt); int length = keyLength/8; if (revision == 6) { prepareEncryptionDictRev6(ownerPassword, userPassword, encryptionDictionary, permissionInt); } else { prepareEncryptionDictRev2345(ownerPassword, userPassword, encryptionDictionary, permissionInt, document, revision, length); } document.setEncryptionDictionary( encryptionDictionary ); document.getDocument().setEncryptionDictionary(encryptionDictionary.getCOSDictionary()); } private void prepareEncryptionDictRev6(String ownerPassword, String userPassword, PDEncryption encryptionDictionary, int permissionInt) throws IOException { try { SecureRandom rnd = new SecureRandom(); Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); // make a random 256-bit file encryption key encryptionKey = new byte[32]; rnd.nextBytes(encryptionKey); // Algorithm 8a: Compute U byte[] userPasswordBytes = truncate127(userPassword.getBytes(Charsets.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(Charsets.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] = (byte) 0xFF; perms[5] = (byte) 0xFF; perms[6] = (byte) 0xFF; perms[7] = (byte) 0xFF; perms[8] = 'T'; // we always encrypt Metadata perms[9] = 'a'; perms[10] = 'd'; perms[11] = 'b'; for (int i = 12; i <= 15; i++) { perms[i] = (byte) rnd.nextInt(); } } catch (GeneralSecurityException e) { logIfStrongEncryptionMissing(); throw new IOException(e); } } private void prepareEncryptionDictRev2345(String ownerPassword, String userPassword, PDEncryption encryptionDictionary, int permissionInt, PDDocument document, int revision, int length) throws IOException { 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(Charsets.ISO_8859_1)); md.update(userPassword.getBytes(Charsets.ISO_8859_1)); md.update(document.getDocument().toString().getBytes(Charsets.ISO_8859_1)); byte[] id = md.digest(this.toString().getBytes(Charsets.ISO_8859_1)); COSString idString = new COSString(id); idArray = new COSArray(); idArray.add(idString); idArray.add(idString); document.getDocument().setDocumentID(idArray); } COSString id = (COSString) idArray.getObject(0); byte[] ownerBytes = computeOwnerPassword( ownerPassword.getBytes(Charsets.ISO_8859_1), userPassword.getBytes(Charsets.ISO_8859_1), revision, length); byte[] userBytes = computeUserPassword( userPassword.getBytes(Charsets.ISO_8859_1), ownerBytes, permissionInt, id.getBytes(), revision, length, true); encryptionKey = computeEncryptedKey(userPassword.getBytes(Charsets.ISO_8859_1), ownerBytes, null, null, null, permissionInt, id.getBytes(), revision, length, true, false); encryptionDictionary.setOwnerKey(ownerBytes); encryptionDictionary.setUserKey(userBytes); } /** * Check for owner password. * * @param ownerPassword The owner password. * @param user The u entry of the encryption dictionary. * @param owner 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 IOException If there is an error accessing data. */ public boolean isOwnerPassword(byte[] ownerPassword, byte[] user, byte[] owner, int permissions, byte[] id, int encRevision, int length, boolean encryptMetadata) throws IOException { if (encRevision == 6 || encRevision == 5) { byte[] truncatedOwnerPassword = 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; if (encRevision == 5) { hash = computeSHA256(truncatedOwnerPassword, oValidationSalt, user); } else { hash = computeHash2A(truncatedOwnerPassword, oValidationSalt, user); } return Arrays.equals(hash, oHash); } else { byte[] userPassword = getUserPassword( ownerPassword, owner, encRevision, length ); return isUserPassword( userPassword, user, owner, permissions, id, encRevision, length, encryptMetadata ); } } /** * Get the user password based on the owner password. * * @param ownerPassword The plaintext owner password. * @param owner 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 IOException If there is an error accessing data while generating the user password. */ public byte[] getUserPassword( byte[] ownerPassword, byte[] owner, int encRevision, int length ) throws IOException { ByteArrayOutputStream result = new ByteArrayOutputStream(); byte[] rc4Key = computeRC4key(ownerPassword, encRevision, length); if( encRevision == 2 ) { encryptDataRC4(rc4Key, owner, result); } else if( encRevision == 3 || encRevision == 4) { byte[] iterationKey = new byte[ rc4Key.length ]; byte[] otemp = new byte[ owner.length ]; System.arraycopy( owner, 0, otemp, 0, owner.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); } result.reset(); encryptDataRC4(iterationKey, otemp, result); otemp = result.toByteArray(); } } return result.toByteArray(); } /** * Compute the encryption key. * * @param password The password to compute the encrypted key. * @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 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 { if (encRevision == 6 || encRevision == 5) { return computeEncryptedKeyRev56(password, isOwnerPassword, o, u, oe, ue, encRevision); } else { return computeEncryptedKeyRev234(password, o, permissions, id, encryptMetadata, length, encRevision); } } private byte[] computeEncryptedKeyRev234(byte[] password, byte[] o, int permissions, byte[] id, boolean encryptMetadata, int length, int encRevision) { //Algorithm 2, based on MD5 //PDFReference 1.4 pg 78 byte[] padded = truncateOrPad(password); MessageDigest md = MessageDigests.getMD5(); md.update(padded); md.update(o); md.update((byte) permissions); md.update((byte) (permissions >>> 8)); md.update((byte) (permissions >>> 16)); md.update((byte) (permissions >>> 24)); 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(); if (encRevision == 3 || encRevision == 4) { for (int i = 0; i < 50; i++) { md.update(digest, 0, length); digest = md.digest(); } } byte[] result = new byte[length]; System.arraycopy(digest, 0, result, 0, length); return result; } private byte[] computeEncryptedKeyRev56(byte[] password, boolean isOwnerPassword, byte[] o, byte[] u, byte[] oe, byte[] ue, int encRevision) throws IOException { byte[] hash, fileKeyEnc; if (isOwnerPassword) { byte[] oKeySalt = new byte[8]; System.arraycopy(o, 40, oKeySalt, 0, 8); if (encRevision == 5) { hash = computeSHA256(password, oKeySalt, u); } else { hash = computeHash2A(password, oKeySalt, u); } fileKeyEnc = oe; } else { byte[] uKeySalt = new byte[8]; System.arraycopy(u, 40, uKeySalt, 0, 8); if (encRevision == 5) { hash = computeSHA256(password, uKeySalt, null); } else { 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])); return cipher.doFinal(fileKeyEnc); } catch (GeneralSecurityException e) { logIfStrongEncryptionMissing(); throw new IOException(e); } } /** * This will compute the user password hash. * * @param password The plain text password. * @param owner 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 IOException if the password could not be computed */ public byte[] computeUserPassword(byte[] password, byte[] owner, int permissions, byte[] id, int encRevision, int length, boolean encryptMetadata) throws IOException { ByteArrayOutputStream result = new ByteArrayOutputStream(); byte[] encKey = computeEncryptedKey( password, owner, null, null, null, permissions, id, encRevision, length, encryptMetadata, true ); if( encRevision == 2 ) { encryptDataRC4(encKey, ENCRYPT_PADDING, result ); } else if( encRevision == 3 || encRevision == 4 ) { MessageDigest md = MessageDigests.getMD5(); md.update( ENCRYPT_PADDING ); md.update( id ); result.write( md.digest() ); byte[] iterationKey = new byte[ encKey.length ]; for( int i=0; i<20; i++ ) { System.arraycopy( encKey, 0, iterationKey, 0, iterationKey.length ); for( int j=0; j< iterationKey.length; j++ ) { iterationKey[j] = (byte)(iterationKey[j] ^ i); } ByteArrayInputStream input = new ByteArrayInputStream(result.toByteArray()); result.reset(); encryptDataRC4(iterationKey, input, result); } 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 ); } 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 IOException if the owner password could not be computed */ public byte[] computeOwnerPassword(byte[] ownerPassword, byte[] userPassword, int encRevision, int length ) throws IOException { if( encRevision == 2 && length != 5 ) { throw new IOException("Expected length=5 actual=" + length ); } byte[] rc4Key = computeRC4key(ownerPassword, encRevision, length); byte[] paddedUser = truncateOrPad( userPassword ); ByteArrayOutputStream encrypted = new ByteArrayOutputStream(); encryptDataRC4(rc4Key, new ByteArrayInputStream(paddedUser), encrypted); 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); } ByteArrayInputStream input = new ByteArrayInputStream( encrypted.toByteArray() ); encrypted.reset(); encryptDataRC4(iterationKey, input, encrypted); } } return encrypted.toByteArray(); } // 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) { MessageDigest md = MessageDigests.getMD5(); byte[] digest = md.digest(truncateOrPad(ownerPassword)); 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.update(digest, 0, length); digest = md.digest(); } } byte[] rc4Key = new byte[length]; System.arraycopy(digest, 0, rc4Key, 0, 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 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 user The u entry of the encryption dictionary. * @param owner 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 IOException If there is an error accessing data. */ public boolean isUserPassword(byte[] password, byte[] user, byte[] owner, int permissions, byte[] id, int encRevision, int length, boolean encryptMetadata) throws IOException { if( encRevision == 2 ) { byte[] passwordBytes = computeUserPassword( password, owner, permissions, id, encRevision, length, encryptMetadata ); return Arrays.equals(user, passwordBytes); } else if( encRevision == 3 || encRevision == 4 ) { byte[] passwordBytes = computeUserPassword( password, owner, permissions, id, encRevision, length, encryptMetadata ); // compare first 16 bytes only return Arrays.equals(Arrays.copyOf(user, 16), Arrays.copyOf(passwordBytes, 16)); } else if (encRevision == 6 || encRevision == 5) { byte[] truncatedPassword = 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; if (encRevision == 5) { hash = computeSHA256(truncatedPassword, uValidationSalt, null); } else { hash = computeHash2A(truncatedPassword, uValidationSalt, null); } return Arrays.equals(hash, uHash); } else { throw new IOException( "Unknown Encryption Revision " + encRevision ); } } /** * Check if a plaintext password is the user password. * * @param password The plaintext password. * @param user The u entry of the encryption dictionary. * @param owner 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 IOException If there is an error accessing data. */ public boolean isUserPassword(String password, byte[] user, byte[] owner, int permissions, byte[] id, int encRevision, int length, boolean encryptMetadata) throws IOException { if (encRevision == 6 || encRevision == 5) { return isUserPassword(password.getBytes(Charsets.UTF_8), user, owner, permissions, id, encRevision, length, encryptMetadata); } else { return isUserPassword(password.getBytes(Charsets.ISO_8859_1), user, owner, permissions, id, encRevision, length, encryptMetadata); } } /** * Check for owner password. * * @param password The owner password. * @param user The u entry of the encryption dictionary. * @param owner 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 IOException If there is an error accessing data. */ 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(Charsets.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 { byte[] userKey; if (u == null) { userKey = new byte[0]; } else if (u.length < 48) { throw new IOException("Bad U length"); } else if (u.length > 48) { // must truncate userKey = new byte[48]; System.arraycopy(u, 0, userKey, 0, 48); } else { userKey = u; } byte[] truncatedPassword = truncate127(password); byte[] input = concat(truncatedPassword, salt, userKey); return computeHash2B(input, truncatedPassword, userKey); } // 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) { logIfStrongEncryptionMissing(); throw new IOException(e); } } private static byte[] computeSHA256(byte[] input, byte[] password, byte[] userKey) throws IOException { try { MessageDigest md = MessageDigest.getInstance("SHA-256"); md.update(input); md.update(password); return userKey == null ? md.digest() : md.digest(userKey); } catch (NoSuchAlgorithmException 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; } private static void logIfStrongEncryptionMissing() { try { if (Cipher.getMaxAllowedKeyLength("AES") != Integer.MAX_VALUE) { Log.w("PdfBox-Android", "JCE unlimited strength jurisdiction policy files are not installed"); } } catch (NoSuchAlgorithmException ex) { } } /** * {@inheritDoc} */ @Override public boolean hasProtectionPolicy() { return policy != null; } }