From 833ae007f8816edd9383e444bb5421653dac7b74 Mon Sep 17 00:00:00 2001 From: Andreas Beeker Date: Fri, 21 Feb 2014 23:19:57 +0000 Subject: [PATCH] Bug 56076 - Add document protection with password support to XWPF Bug 56077 - Add password hash function to HWPF git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1570750 13f79535-47bb-0310-9956-ffa450edef68 --- .../poi/poifs/crypt/CryptoFunctions.java | 159 +++++++++++++++++- .../poi/xwpf/usermodel/XWPFDocument.java | 91 ++++++++++ .../poi/xwpf/usermodel/XWPFSettings.java | 153 ++++++++++++++++- .../poi/xwpf/TestDocumentProtection.java | 41 ++++- test-data/document/bug56076.docx | Bin 0 -> 11033 bytes 5 files changed, 432 insertions(+), 12 deletions(-) create mode 100644 test-data/document/bug56076.docx diff --git a/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java b/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java index 7286823a1..0349c5c67 100644 --- a/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java +++ b/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java @@ -16,7 +16,7 @@ ==================================================================== */ package org.apache.poi.poifs.crypt; -import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; import java.security.DigestException; import java.security.GeneralSecurityException; import java.security.MessageDigest; @@ -74,6 +74,23 @@ public class CryptoFunctions { * @return the hashed password */ public static byte[] hashPassword(String password, HashAlgorithm hashAlgorithm, byte salt[], int spinCount) { + return hashPassword(password, hashAlgorithm, salt, spinCount, true); + } + + /** + * Generalized method for read and write protection hash generation. + * The difference is, read protection uses the order iterator then hash in the hash loop, whereas write protection + * uses first the last hash value and then the current iterator value + * + * @param password + * @param hashAlgorithm + * @param salt + * @param spinCount + * @param iteratorFirst if true, the iterator is hashed before the n-1 hash value, + * if false the n-1 hash value is applied first + * @return the hashed password + */ + public static byte[] hashPassword(String password, HashAlgorithm hashAlgorithm, byte salt[], int spinCount, boolean iteratorFirst) { // If no password was given, use the default if (password == null) { password = Decryptor.DEFAULT_PASSWORD; @@ -84,13 +101,16 @@ public class CryptoFunctions { hashAlg.update(salt); byte[] hash = hashAlg.digest(getUtf16LeString(password)); byte[] iterator = new byte[LittleEndianConsts.INT_SIZE]; + + byte[] first = (iteratorFirst ? iterator : hash); + byte[] second = (iteratorFirst ? hash : iterator); try { for (int i = 0; i < spinCount; i++) { LittleEndian.putInt(iterator, 0, i); hashAlg.reset(); - hashAlg.update(iterator); - hashAlg.update(hash); + hashAlg.update(first); + hashAlg.update(second); hashAlg.digest(hash, 0, hash.length); // don't create hash buffer everytime new } } catch (DigestException e) { @@ -222,11 +242,7 @@ public class CryptoFunctions { } public static byte[] getUtf16LeString(String str) { - try { - return str.getBytes("UTF-16LE"); - } catch (UnsupportedEncodingException e) { - throw new EncryptedDocumentException(e); - } + return str.getBytes(Charset.forName("UTF-16LE")); } public static MessageDigest getMessageDigest(HashAlgorithm hashAlgorithm) { @@ -265,4 +281,131 @@ public class CryptoFunctions { throw new EncryptedDocumentException("Only the BouncyCastle provider supports your encryption settings - please add it to the classpath."); } } + + + private static final int InitialCodeArray[] = { + 0xE1F0, 0x1D0F, 0xCC9C, 0x84C0, 0x110C, 0x0E10, 0xF1CE, + 0x313E, 0x1872, 0xE139, 0xD40F, 0x84F9, 0x280C, 0xA96A, + 0x4EC3 + }; + + private static final int EncryptionMatrix[][] = { + /* char 1 */ {0xAEFC, 0x4DD9, 0x9BB2, 0x2745, 0x4E8A, 0x9D14, 0x2A09}, + /* char 2 */ {0x7B61, 0xF6C2, 0xFDA5, 0xEB6B, 0xC6F7, 0x9DCF, 0x2BBF}, + /* char 3 */ {0x4563, 0x8AC6, 0x05AD, 0x0B5A, 0x16B4, 0x2D68, 0x5AD0}, + /* char 4 */ {0x0375, 0x06EA, 0x0DD4, 0x1BA8, 0x3750, 0x6EA0, 0xDD40}, + /* char 5 */ {0xD849, 0xA0B3, 0x5147, 0xA28E, 0x553D, 0xAA7A, 0x44D5}, + /* char 6 */ {0x6F45, 0xDE8A, 0xAD35, 0x4A4B, 0x9496, 0x390D, 0x721A}, + /* char 7 */ {0xEB23, 0xC667, 0x9CEF, 0x29FF, 0x53FE, 0xA7FC, 0x5FD9}, + /* char 8 */ {0x47D3, 0x8FA6, 0x0F6D, 0x1EDA, 0x3DB4, 0x7B68, 0xF6D0}, + /* char 9 */ {0xB861, 0x60E3, 0xC1C6, 0x93AD, 0x377B, 0x6EF6, 0xDDEC}, + /* char 10 */ {0x45A0, 0x8B40, 0x06A1, 0x0D42, 0x1A84, 0x3508, 0x6A10}, + /* char 11 */ {0xAA51, 0x4483, 0x8906, 0x022D, 0x045A, 0x08B4, 0x1168}, + /* char 12 */ {0x76B4, 0xED68, 0xCAF1, 0x85C3, 0x1BA7, 0x374E, 0x6E9C}, + /* char 13 */ {0x3730, 0x6E60, 0xDCC0, 0xA9A1, 0x4363, 0x86C6, 0x1DAD}, + /* char 14 */ {0x3331, 0x6662, 0xCCC4, 0x89A9, 0x0373, 0x06E6, 0x0DCC}, + /* char 15 */ {0x1021, 0x2042, 0x4084, 0x8108, 0x1231, 0x2462, 0x48C4} + }; + + /** + * This method generates the xored-hashed password for word documents < 2007. + * Its output will be used as password input for the newer word generations which + * utilize a real hashing algorithm like sha1. + * + * Although the code was taken from the "see"-link below, this looks similar + * to the method in [MS-OFFCRYPTO] 2.3.7.2 Binary Document XOR Array Initialization Method 1. + * + * @param password + * @return the hashed password + * + * @see How to set the editing restrictions in Word using Open XML SDK 2.0 + * @see Funny: How the new powerful cryptography implemented in Word 2007 turns it into a perfect tool for document password removal. + */ + public static int xorHashPasswordAsInt(String password) { + //Array to hold Key Values + byte[] generatedKey = new byte[4]; + + //Maximum length of the password is 15 chars. + final int intMaxPasswordLength = 15; + + if (!"".equals(password)) { + // Truncate the password to 15 characters + password = password.substring(0, Math.min(password.length(), intMaxPasswordLength)); + + // Construct a new NULL-terminated string consisting of single-byte characters: + // -- > Get the single-byte values by iterating through the Unicode characters of the truncated Password. + // --> For each character, if the low byte is not equal to 0, take it. Otherwise, take the high byte. + byte[] arrByteChars = new byte[password.length()]; + + for (int i = 0; i < password.length(); i++) { + int intTemp = password.charAt(i); + byte lowByte = (byte)(intTemp & 0x00FF); + byte highByte = (byte)((intTemp & 0xFF00) >> 8); + arrByteChars[i] = (lowByte != 0 ? lowByte : highByte); + } + + // Compute the high-order word of the new key: + + // --> Initialize from the initial code array (see below), depending on the passwords length. + int highOrderWord = InitialCodeArray[arrByteChars.length - 1]; + + // --> For each character in the password: + // --> For every bit in the character, starting with the least significant and progressing to (but excluding) + // the most significant, if the bit is set, XOR the keys high-order word with the corresponding word from + // the Encryption Matrix + for (int i = 0; i < arrByteChars.length; i++) { + int tmp = intMaxPasswordLength - arrByteChars.length + i; + for (int intBit = 0; intBit < 7; intBit++) { + if ((arrByteChars[i] & (0x0001 << intBit)) != 0) { + highOrderWord ^= EncryptionMatrix[tmp][intBit]; + } + } + } + + // Compute the low-order word of the new key: + + // Initialize with 0 + int lowOrderWord = 0; + + // For each character in the password, going backwards + for (int i = arrByteChars.length - 1; i >= 0; i--) { + // low-order word = (((low-order word SHR 14) AND 0x0001) OR (low-order word SHL 1) AND 0x7FFF)) XOR character + lowOrderWord = (((lowOrderWord >> 14) & 0x0001) | ((lowOrderWord << 1) & 0x7FFF)) ^ arrByteChars[i]; + } + + // Lastly,low-order word = (((low-order word SHR 14) AND 0x0001) OR (low-order word SHL 1) AND 0x7FFF)) XOR password length XOR 0xCE4B. + lowOrderWord = (((lowOrderWord >> 14) & 0x0001) | ((lowOrderWord << 1) & 0x7FFF)) ^ arrByteChars.length ^ 0xCE4B; + + // The byte order of the result shall be reversed [password "Example": 0x64CEED7E becomes 7EEDCE64], + // and that value shall be hashed as defined by the attribute values. + + LittleEndian.putShort(generatedKey, 0, (short)lowOrderWord); + LittleEndian.putShort(generatedKey, 2, (short)highOrderWord); + } + + return LittleEndian.getInt(generatedKey); + } + + /** + * This method generates the xored-hashed password for word documents < 2007. + */ + public static String xorHashPassword(String password) { + int hashedPassword = xorHashPasswordAsInt(password); + return String.format("%1$08X", hashedPassword); + } + + /** + * Convenience function which returns the reversed xored-hashed password for further + * processing in word documents 2007 and newer, which utilize a real hashing algorithm like sha1. + */ + public static String xorHashPasswordReversed(String password) { + int hashedPassword = xorHashPasswordAsInt(password); + + return String.format("%1$02X%2$02X%3$02X%4$02X" + , ( hashedPassword >>> 0 ) & 0xFF + , ( hashedPassword >>> 8 ) & 0xFF + , ( hashedPassword >>> 16 ) & 0xFF + , ( hashedPassword >>> 24 ) & 0xFF + ); + } } diff --git a/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFDocument.java b/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFDocument.java index 978e69d35..65952cbae 100644 --- a/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFDocument.java +++ b/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFDocument.java @@ -47,6 +47,7 @@ import org.apache.poi.openxml4j.opc.PackageRelationship; import org.apache.poi.openxml4j.opc.PackageRelationshipTypes; import org.apache.poi.openxml4j.opc.PackagingURIHelper; import org.apache.poi.openxml4j.opc.TargetMode; +import org.apache.poi.poifs.crypt.HashAlgorithm; import org.apache.poi.util.IOUtils; import org.apache.poi.util.IdentifierManager; import org.apache.poi.util.Internal; @@ -993,6 +994,26 @@ public class XWPFDocument extends POIXMLDocument implements Document, IBody { settings.setEnforcementEditValue(STDocProtect.READ_ONLY); } + /** + * Enforces the readOnly protection with a password.
+ *
+ * sample snippet from settings.xml + *
+     *   <w:documentProtection w:edit="readOnly" w:enforcement="1" 
+     *       w:cryptProviderType="rsaAES" w:cryptAlgorithmClass="hash"
+     *       w:cryptAlgorithmType="typeAny" w:cryptAlgorithmSid="14"
+     *       w:cryptSpinCount="100000" w:hash="..." w:salt="...."
+     *   />
+     * 
+ * + * @param password the plaintext password, if null no password will be applied + * @param hashAlgo the hash algorithm - only md2, m5, sha1, sha256, sha384 and sha512 are supported. + * if null, it will default default to sha1 + */ + public void enforceReadonlyProtection(String password, HashAlgorithm hashAlgo) { + settings.setEnforcementEditValue(STDocProtect.READ_ONLY, password, hashAlgo); + } + /** * Enforce the Filling Forms protection.
* In the documentProtection tag inside settings.xml file,
@@ -1009,6 +1030,26 @@ public class XWPFDocument extends POIXMLDocument implements Document, IBody { settings.setEnforcementEditValue(STDocProtect.FORMS); } + /** + * Enforce the Filling Forms protection.
+ *
+ * sample snippet from settings.xml + *
+     *   <w:documentProtection w:edit="forms" w:enforcement="1" 
+     *       w:cryptProviderType="rsaAES" w:cryptAlgorithmClass="hash"
+     *       w:cryptAlgorithmType="typeAny" w:cryptAlgorithmSid="14"
+     *       w:cryptSpinCount="100000" w:hash="..." w:salt="...."
+     *   />
+     * 
+ * + * @param password the plaintext password, if null no password will be applied + * @param hashAlgo the hash algorithm - only md2, m5, sha1, sha256, sha384 and sha512 are supported. + * if null, it will default default to sha1 + */ + public void enforceFillingFormsProtection(String password, HashAlgorithm hashAlgo) { + settings.setEnforcementEditValue(STDocProtect.FORMS, password, hashAlgo); + } + /** * Enforce the Comments protection.
* In the documentProtection tag inside settings.xml file,
@@ -1025,6 +1066,26 @@ public class XWPFDocument extends POIXMLDocument implements Document, IBody { settings.setEnforcementEditValue(STDocProtect.COMMENTS); } + /** + * Enforce the Comments protection.
+ *
+ * sample snippet from settings.xml + *
+     *   <w:documentProtection w:edit="comments" w:enforcement="1" 
+     *       w:cryptProviderType="rsaAES" w:cryptAlgorithmClass="hash"
+     *       w:cryptAlgorithmType="typeAny" w:cryptAlgorithmSid="14"
+     *       w:cryptSpinCount="100000" w:hash="..." w:salt="...."
+     *   />
+     * 
+ * + * @param password the plaintext password, if null no password will be applied + * @param hashAlgo the hash algorithm - only md2, m5, sha1, sha256, sha384 and sha512 are supported. + * if null, it will default default to sha1 + */ + public void enforceCommentsProtection(String password, HashAlgorithm hashAlgo) { + settings.setEnforcementEditValue(STDocProtect.COMMENTS, password, hashAlgo); + } + /** * Enforce the Tracked Changes protection.
* In the documentProtection tag inside settings.xml file,
@@ -1041,6 +1102,36 @@ public class XWPFDocument extends POIXMLDocument implements Document, IBody { settings.setEnforcementEditValue(STDocProtect.TRACKED_CHANGES); } + /** + * Enforce the Tracked Changes protection.
+ *
+ * sample snippet from settings.xml + *
+     *   <w:documentProtection w:edit="trackedChanges" w:enforcement="1" 
+     *       w:cryptProviderType="rsaAES" w:cryptAlgorithmClass="hash"
+     *       w:cryptAlgorithmType="typeAny" w:cryptAlgorithmSid="14"
+     *       w:cryptSpinCount="100000" w:hash="..." w:salt="...."
+     *   />
+     * 
+ * + * @param password the plaintext password, if null no password will be applied + * @param hashAlgo the hash algorithm - only md2, m5, sha1, sha256, sha384 and sha512 are supported. + * if null, it will default default to sha1 + */ + public void enforceTrackedChangesProtection(String password, HashAlgorithm hashAlgo) { + settings.setEnforcementEditValue(STDocProtect.TRACKED_CHANGES, password, hashAlgo); + } + + /** + * Validates the existing password + * + * @param password + * @return true, only if password was set and equals, false otherwise + */ + public boolean validateProtectionPassword(String password) { + return settings.validateProtectionPassword(password); + } + /** * Remove protection enforcement.
* In the documentProtection tag inside settings.xml file
diff --git a/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFSettings.java b/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFSettings.java index 8ceddbe35..1f521621b 100644 --- a/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFSettings.java +++ b/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFSettings.java @@ -20,19 +20,27 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; import javax.xml.namespace.QName; +import org.apache.poi.EncryptedDocumentException; import org.apache.poi.POIXMLDocumentPart; import org.apache.poi.openxml4j.opc.PackagePart; import org.apache.poi.openxml4j.opc.PackageRelationship; +import org.apache.poi.poifs.crypt.CryptoFunctions; +import org.apache.poi.poifs.crypt.HashAlgorithm; import org.apache.xmlbeans.XmlOptions; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTDocProtect; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTOnOff; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSettings; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTZoom; +import org.openxmlformats.schemas.wordprocessingml.x2006.main.STAlgClass; +import org.openxmlformats.schemas.wordprocessingml.x2006.main.STAlgType; +import org.openxmlformats.schemas.wordprocessingml.x2006.main.STCryptProv; import org.openxmlformats.schemas.wordprocessingml.x2006.main.STDocProtect; import org.openxmlformats.schemas.wordprocessingml.x2006.main.STOnOff; import org.openxmlformats.schemas.wordprocessingml.x2006.main.SettingsDocument; @@ -139,6 +147,150 @@ public class XWPFSettings extends POIXMLDocumentPart { safeGetDocumentProtection().setEdit(editValue); } + /** + * Enforces the protection with the option specified by passed editValue and password.
+ *
+ * sample snippet from settings.xml + *
+     *   <w:documentProtection w:edit="[passed editValue]" w:enforcement="1" 
+     *       w:cryptProviderType="rsaAES" w:cryptAlgorithmClass="hash"
+     *       w:cryptAlgorithmType="typeAny" w:cryptAlgorithmSid="14"
+     *       w:cryptSpinCount="100000" w:hash="..." w:salt="...."
+     *   />
+     * 
+ * + * @param editValue the protection type + * @param password the plaintext password, if null no password will be applied + * @param hashAlgo the hash algorithm - only md2, m5, sha1, sha256, sha384 and sha512 are supported. + * if null, it will default default to sha1 + */ + public void setEnforcementEditValue(org.openxmlformats.schemas.wordprocessingml.x2006.main.STDocProtect.Enum editValue, + String password, HashAlgorithm hashAlgo) { + safeGetDocumentProtection().setEnforcement(STOnOff.X_1); + safeGetDocumentProtection().setEdit(editValue); + + if (password == null) { + if (safeGetDocumentProtection().isSetCryptProviderType()) { + safeGetDocumentProtection().unsetCryptProviderType(); + } + + if (safeGetDocumentProtection().isSetCryptAlgorithmClass()) { + safeGetDocumentProtection().unsetCryptAlgorithmClass(); + } + + if (safeGetDocumentProtection().isSetCryptAlgorithmType()) { + safeGetDocumentProtection().unsetCryptAlgorithmType(); + } + + if (safeGetDocumentProtection().isSetCryptAlgorithmSid()) { + safeGetDocumentProtection().unsetCryptAlgorithmSid(); + } + + if (safeGetDocumentProtection().isSetSalt()) { + safeGetDocumentProtection().unsetSalt(); + } + + if (safeGetDocumentProtection().isSetCryptSpinCount()) { + safeGetDocumentProtection().unsetCryptSpinCount(); + } + + if (safeGetDocumentProtection().isSetHash()) { + safeGetDocumentProtection().unsetHash(); + } + } else { + final STCryptProv.Enum providerType; + final int sid; + switch (hashAlgo) { + case md2: + providerType = STCryptProv.RSA_FULL; + sid = 1; + break; + // md4 is not supported by JCE + case md5: + providerType = STCryptProv.RSA_FULL; + sid = 3; + break; + case sha1: + providerType = STCryptProv.RSA_FULL; + sid = 4; + break; + case sha256: + providerType = STCryptProv.RSA_AES; + sid = 12; + break; + case sha384: + providerType = STCryptProv.RSA_AES; + sid = 13; + break; + case sha512: + providerType = STCryptProv.RSA_AES; + sid = 14; + break; + default: + throw new EncryptedDocumentException + ("Hash algorithm '"+hashAlgo+"' is not supported for document write protection."); + } + + + SecureRandom random = new SecureRandom(); + byte salt[] = random.generateSeed(16); + + // Iterations specifies the number of times the hashing function shall be iteratively run (using each + // iteration's result as the input for the next iteration). + int spinCount = 100000; + + if (hashAlgo == null) hashAlgo = HashAlgorithm.sha1; + + String legacyHash = CryptoFunctions.xorHashPasswordReversed(password); + // Implementation Notes List: + // --> In this third stage, the reversed byte order legacy hash from the second stage shall + // be converted to Unicode hex string representation + byte hash[] = CryptoFunctions.hashPassword(legacyHash, hashAlgo, salt, spinCount, false); + + safeGetDocumentProtection().setSalt(salt); + safeGetDocumentProtection().setHash(hash); + safeGetDocumentProtection().setCryptSpinCount(BigInteger.valueOf(spinCount)); + safeGetDocumentProtection().setCryptAlgorithmType(STAlgType.TYPE_ANY); + safeGetDocumentProtection().setCryptAlgorithmClass(STAlgClass.HASH); + safeGetDocumentProtection().setCryptProviderType(providerType); + safeGetDocumentProtection().setCryptAlgorithmSid(BigInteger.valueOf(sid)); + } + } + + /** + * Validates the existing password + * + * @param password + * @return true, only if password was set and equals, false otherwise + */ + public boolean validateProtectionPassword(String password) { + BigInteger sid = safeGetDocumentProtection().getCryptAlgorithmSid(); + byte hash[] = safeGetDocumentProtection().getHash(); + byte salt[] = safeGetDocumentProtection().getSalt(); + BigInteger spinCount = safeGetDocumentProtection().getCryptSpinCount(); + + if (sid == null || hash == null || salt == null || spinCount == null) return false; + + HashAlgorithm hashAlgo; + switch (sid.intValue()) { + case 1: hashAlgo = HashAlgorithm.md2; break; + case 3: hashAlgo = HashAlgorithm.md5; break; + case 4: hashAlgo = HashAlgorithm.sha1; break; + case 12: hashAlgo = HashAlgorithm.sha256; break; + case 13: hashAlgo = HashAlgorithm.sha384; break; + case 14: hashAlgo = HashAlgorithm.sha512; break; + default: return false; + } + + String legacyHash = CryptoFunctions.xorHashPasswordReversed(password); + // Implementation Notes List: + // --> In this third stage, the reversed byte order legacy hash from the second stage shall + // be converted to Unicode hex string representation + byte hash2[] = CryptoFunctions.hashPassword(legacyHash, hashAlgo, salt, spinCount.intValue(), false); + + return Arrays.equals(hash, hash2); + } + /** * Removes protection enforcement.
* In the documentProtection tag inside settings.xml file
@@ -204,5 +356,4 @@ public class XWPFSettings extends POIXMLDocumentPart { throw new RuntimeException(e); } } - } diff --git a/src/ooxml/testcases/org/apache/poi/xwpf/TestDocumentProtection.java b/src/ooxml/testcases/org/apache/poi/xwpf/TestDocumentProtection.java index 3d814aada..67ad79422 100644 --- a/src/ooxml/testcases/org/apache/poi/xwpf/TestDocumentProtection.java +++ b/src/ooxml/testcases/org/apache/poi/xwpf/TestDocumentProtection.java @@ -16,19 +16,25 @@ ==================================================================== */ package org.apache.poi.xwpf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; -import junit.framework.TestCase; - +import org.apache.poi.poifs.crypt.CryptoFunctions; +import org.apache.poi.poifs.crypt.HashAlgorithm; import org.apache.poi.util.TempFile; import org.apache.poi.xwpf.usermodel.XWPFDocument; import org.apache.poi.xwpf.usermodel.XWPFParagraph; import org.apache.poi.xwpf.usermodel.XWPFRun; +import org.junit.Test; -public class TestDocumentProtection extends TestCase { +public class TestDocumentProtection { + @Test public void testShouldReadEnforcementProperties() throws Exception { XWPFDocument documentWithoutDocumentProtectionTag = XWPFTestDataSamples.openSampleDocument("documentProtection_no_protection.docx"); @@ -69,6 +75,7 @@ public class TestDocumentProtection extends TestCase { } + @Test public void testShouldEnforceForReadOnly() throws Exception { // XWPFDocument document = createDocumentFromSampleFile("test-data/document/documentProtection_no_protection.docx"); XWPFDocument document = XWPFTestDataSamples.openSampleDocument("documentProtection_no_protection.docx"); @@ -79,6 +86,7 @@ public class TestDocumentProtection extends TestCase { assertTrue(document.isEnforcedReadonlyProtection()); } + @Test public void testShouldEnforceForFillingForms() throws Exception { XWPFDocument document = XWPFTestDataSamples.openSampleDocument("documentProtection_no_protection.docx"); assertFalse(document.isEnforcedFillingFormsProtection()); @@ -88,6 +96,7 @@ public class TestDocumentProtection extends TestCase { assertTrue(document.isEnforcedFillingFormsProtection()); } + @Test public void testShouldEnforceForComments() throws Exception { XWPFDocument document = XWPFTestDataSamples.openSampleDocument("documentProtection_no_protection.docx"); assertFalse(document.isEnforcedCommentsProtection()); @@ -97,6 +106,7 @@ public class TestDocumentProtection extends TestCase { assertTrue(document.isEnforcedCommentsProtection()); } + @Test public void testShouldEnforceForTrackedChanges() throws Exception { XWPFDocument document = XWPFTestDataSamples.openSampleDocument("documentProtection_no_protection.docx"); assertFalse(document.isEnforcedTrackedChangesProtection()); @@ -106,6 +116,7 @@ public class TestDocumentProtection extends TestCase { assertTrue(document.isEnforcedTrackedChangesProtection()); } + @Test public void testShouldUnsetEnforcement() throws Exception { XWPFDocument document = XWPFTestDataSamples.openSampleDocument("documentProtection_readonly_no_password.docx"); assertTrue(document.isEnforcedReadonlyProtection()); @@ -115,6 +126,7 @@ public class TestDocumentProtection extends TestCase { assertFalse(document.isEnforcedReadonlyProtection()); } + @Test public void testIntegration() throws Exception { XWPFDocument doc = new XWPFDocument(); @@ -137,6 +149,7 @@ public class TestDocumentProtection extends TestCase { assertTrue(document.isEnforcedCommentsProtection()); } + @Test public void testUpdateFields() throws Exception { XWPFDocument doc = new XWPFDocument(); assertFalse(doc.isEnforcedUpdateFields()); @@ -144,4 +157,26 @@ public class TestDocumentProtection extends TestCase { assertTrue(doc.isEnforcedUpdateFields()); } + @Test + public void bug56076_read() throws Exception { + // test legacy xored-hashed password + assertEquals("64CEED7E", CryptoFunctions.xorHashPassword("Example")); + // check leading 0 + assertEquals("0005CB00", CryptoFunctions.xorHashPassword("34579")); + + // test document write protection with password + XWPFDocument document = XWPFTestDataSamples.openSampleDocument("bug56076.docx"); + boolean isValid = document.validateProtectionPassword("Example"); + assertTrue(isValid); + } + + @Test + public void bug56076_write() throws Exception { + // test document write protection with password + XWPFDocument document = new XWPFDocument(); + document.enforceCommentsProtection("Example", HashAlgorithm.sha512); + document = XWPFTestDataSamples.writeOutAndReadBack(document); + boolean isValid = document.validateProtectionPassword("Example"); + assertTrue(isValid); + } } diff --git a/test-data/document/bug56076.docx b/test-data/document/bug56076.docx new file mode 100644 index 0000000000000000000000000000000000000000..a1ba93e3a6189eb4e47ba23f0f08a696b232c208 GIT binary patch literal 11033 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 literal 0 HcmV?d00001