Author: kiwiwings
Date: Fri Feb 21 23:19:57 2014
New Revision: 1570750

URL: http://svn.apache.org/r1570750
Log:
Bug 56076 - Add document protection with password support to XWPF
Bug 56077 - Add password hash function to HWPF

Added:
    poi/trunk/test-data/document/bug56076.docx   (with props)
Modified:
    poi/site/src/documentation/content/xdocs/status.xml
    poi/trunk/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java
    poi/trunk/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFDocument.java
    poi/trunk/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFSettings.java
    
poi/trunk/src/ooxml/testcases/org/apache/poi/xwpf/TestDocumentProtection.java

Modified: poi/site/src/documentation/content/xdocs/status.xml
URL: 
http://svn.apache.org/viewvc/poi/site/src/documentation/content/xdocs/status.xml?rev=1570750&r1=1570749&r2=1570750&view=diff
==============================================================================
--- poi/site/src/documentation/content/xdocs/status.xml (original)
+++ poi/site/src/documentation/content/xdocs/status.xml Fri Feb 21 23:19:57 2014
@@ -37,6 +37,8 @@
     </devs>
 
     <release version="3.11-beta1" date="2014-??-??">
+        <action dev="PD" type="add" fixes-bug="56077">Add password hash 
function to HWPF</action>
+        <action dev="PD" type="add" fixes-bug="56076">Add document protection 
with password support to XWPF</action>
         <action dev="PD" type="fix" fixes-bug="55026">Support OOXML 
ContentTypes which include parameters</action>
         <action dev="PD" type="fix" fixes-bug="55732">PPT can't open, fails 
with "Couldn't instantiate ... StyleTextProp9Atom"</action>
         <action dev="PD" type="fix" fixes-bug="56138">HSPF code page strings 
can be zero length</action>

Modified: poi/trunk/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java
URL: 
http://svn.apache.org/viewvc/poi/trunk/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java?rev=1570750&r1=1570749&r2=1570750&view=diff
==============================================================================
--- poi/trunk/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java 
(original)
+++ poi/trunk/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java Fri Feb 
21 23:19:57 2014
@@ -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 &lt; 
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 <a 
href="http://blogs.msdn.com/b/vsod/archive/2010/04/05/how-to-set-the-editing-restrictions-in-word-using-open-xml-sdk-2-0.aspx";>How
 to set the editing restrictions in Word using Open XML SDK 2.0</a>
+     * @see <a 
href="http://www.aspose.com/blogs/aspose-blogs/vladimir-averkin/archive/2007/08/20/funny-how-the-new-powerful-cryptography-implemented-in-word-2007-turns-it-into-a-perfect-tool-for-document-password-removal.html";>Funny:
 How the new powerful cryptography implemented in Word 2007 turns it into a 
perfect tool for document password removal.</a>
+     */
+    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 &lt; 
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
+        );
+    }
 }

Modified: 
poi/trunk/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFDocument.java
URL: 
http://svn.apache.org/viewvc/poi/trunk/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFDocument.java?rev=1570750&r1=1570749&r2=1570750&view=diff
==============================================================================
--- poi/trunk/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFDocument.java 
(original)
+++ poi/trunk/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFDocument.java 
Fri Feb 21 23:19:57 2014
@@ -47,6 +47,7 @@ import org.apache.poi.openxml4j.opc.Pack
 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;
@@ -994,6 +995,26 @@ public class XWPFDocument extends POIXML
     }
 
     /**
+     * Enforces the readOnly protection with a password.<br/>
+     * <br/>
+     * sample snippet from settings.xml
+     * <pre>
+     *   &lt;w:documentProtection w:edit=&quot;readOnly&quot; 
w:enforcement=&quot;1&quot; 
+     *       w:cryptProviderType=&quot;rsaAES&quot; 
w:cryptAlgorithmClass=&quot;hash&quot;
+     *       w:cryptAlgorithmType=&quot;typeAny&quot; 
w:cryptAlgorithmSid=&quot;14&quot;
+     *       w:cryptSpinCount=&quot;100000&quot; w:hash=&quot;...&quot; 
w:salt=&quot;....&quot;
+     *   /&gt;
+     * </pre>
+     * 
+     * @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.<br/>
      * In the documentProtection tag inside settings.xml file, <br/>
      * it sets the value of enforcement to "1" (w:enforcement="1") <br/>
@@ -1010,6 +1031,26 @@ public class XWPFDocument extends POIXML
     }
 
     /**
+     * Enforce the Filling Forms protection.<br/>
+     * <br/>
+     * sample snippet from settings.xml
+     * <pre>
+     *   &lt;w:documentProtection w:edit=&quot;forms&quot; 
w:enforcement=&quot;1&quot; 
+     *       w:cryptProviderType=&quot;rsaAES&quot; 
w:cryptAlgorithmClass=&quot;hash&quot;
+     *       w:cryptAlgorithmType=&quot;typeAny&quot; 
w:cryptAlgorithmSid=&quot;14&quot;
+     *       w:cryptSpinCount=&quot;100000&quot; w:hash=&quot;...&quot; 
w:salt=&quot;....&quot;
+     *   /&gt;
+     * </pre>
+     * 
+     * @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.<br/>
      * In the documentProtection tag inside settings.xml file,<br/>
      * it sets the value of enforcement to "1" (w:enforcement="1") <br/>
@@ -1026,6 +1067,26 @@ public class XWPFDocument extends POIXML
     }
 
     /**
+     * Enforce the Comments protection.<br/>
+     * <br/>
+     * sample snippet from settings.xml
+     * <pre>
+     *   &lt;w:documentProtection w:edit=&quot;comments&quot; 
w:enforcement=&quot;1&quot; 
+     *       w:cryptProviderType=&quot;rsaAES&quot; 
w:cryptAlgorithmClass=&quot;hash&quot;
+     *       w:cryptAlgorithmType=&quot;typeAny&quot; 
w:cryptAlgorithmSid=&quot;14&quot;
+     *       w:cryptSpinCount=&quot;100000&quot; w:hash=&quot;...&quot; 
w:salt=&quot;....&quot;
+     *   /&gt;
+     * </pre>
+     * 
+     * @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.<br/>
      * In the documentProtection tag inside settings.xml file, <br/>
      * it sets the value of enforcement to "1" (w:enforcement="1") <br/>
@@ -1042,6 +1103,36 @@ public class XWPFDocument extends POIXML
     }
 
     /**
+     * Enforce the Tracked Changes protection.<br/>
+     * <br/>
+     * sample snippet from settings.xml
+     * <pre>
+     *   &lt;w:documentProtection w:edit=&quot;trackedChanges&quot; 
w:enforcement=&quot;1&quot; 
+     *       w:cryptProviderType=&quot;rsaAES&quot; 
w:cryptAlgorithmClass=&quot;hash&quot;
+     *       w:cryptAlgorithmType=&quot;typeAny&quot; 
w:cryptAlgorithmSid=&quot;14&quot;
+     *       w:cryptSpinCount=&quot;100000&quot; w:hash=&quot;...&quot; 
w:salt=&quot;....&quot;
+     *   /&gt;
+     * </pre>
+     * 
+     * @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.<br/>
      * In the documentProtection tag inside settings.xml file <br/>
      * it sets the value of enforcement to "0" (w:enforcement="0") <br/>

Modified: 
poi/trunk/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFSettings.java
URL: 
http://svn.apache.org/viewvc/poi/trunk/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFSettings.java?rev=1570750&r1=1570749&r2=1570750&view=diff
==============================================================================
--- poi/trunk/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFSettings.java 
(original)
+++ poi/trunk/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFSettings.java 
Fri Feb 21 23:19:57 2014
@@ -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;
@@ -140,6 +148,150 @@ public class XWPFSettings extends POIXML
     }
 
     /**
+     * Enforces the protection with the option specified by passed editValue 
and password.<br/>
+     * <br/>
+     * sample snippet from settings.xml
+     * <pre>
+     *   &lt;w:documentProtection w:edit=&quot;[passed editValue]&quot; 
w:enforcement=&quot;1&quot; 
+     *       w:cryptProviderType=&quot;rsaAES&quot; 
w:cryptAlgorithmClass=&quot;hash&quot;
+     *       w:cryptAlgorithmType=&quot;typeAny&quot; 
w:cryptAlgorithmSid=&quot;14&quot;
+     *       w:cryptSpinCount=&quot;100000&quot; w:hash=&quot;...&quot; 
w:salt=&quot;....&quot;
+     *   /&gt;
+     * </pre>
+     * 
+     * @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.<br/>
      * In the documentProtection tag inside settings.xml file <br/>
      * it sets the value of enforcement to "0" (w:enforcement="0") <br/>
@@ -204,5 +356,4 @@ public class XWPFSettings extends POIXML
             throw new RuntimeException(e);
         }
     }
-
 }

Modified: 
poi/trunk/src/ooxml/testcases/org/apache/poi/xwpf/TestDocumentProtection.java
URL: 
http://svn.apache.org/viewvc/poi/trunk/src/ooxml/testcases/org/apache/poi/xwpf/TestDocumentProtection.java?rev=1570750&r1=1570749&r2=1570750&view=diff
==============================================================================
--- 
poi/trunk/src/ooxml/testcases/org/apache/poi/xwpf/TestDocumentProtection.java 
(original)
+++ 
poi/trunk/src/ooxml/testcases/org/apache/poi/xwpf/TestDocumentProtection.java 
Fri Feb 21 23:19:57 2014
@@ -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 exte
 
     }
 
+    @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 exte
         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 exte
         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 exte
         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 exte
         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 exte
         assertFalse(document.isEnforcedReadonlyProtection());
     }
 
+    @Test
     public void testIntegration() throws Exception {
         XWPFDocument doc = new XWPFDocument();
 
@@ -137,6 +149,7 @@ public class TestDocumentProtection exte
         assertTrue(document.isEnforcedCommentsProtection());
     }
 
+    @Test
     public void testUpdateFields() throws Exception {
         XWPFDocument doc = new XWPFDocument();
         assertFalse(doc.isEnforcedUpdateFields());
@@ -144,4 +157,26 @@ public class TestDocumentProtection exte
         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);
+    }
 }

Added: poi/trunk/test-data/document/bug56076.docx
URL: 
http://svn.apache.org/viewvc/poi/trunk/test-data/document/bug56076.docx?rev=1570750&view=auto
==============================================================================
Binary file - no diff available.

Propchange: poi/trunk/test-data/document/bug56076.docx
------------------------------------------------------------------------------
    svn:mime-type = application/octet-stream



---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to