Bug 56077 - Add password hash function to HWPF git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1570750 13f79535-47bb-0310-9956-ffa450edef68tags/REL_3_11_BETA1
==================================================================== */ | ==================================================================== */ | ||||
package org.apache.poi.poifs.crypt; | package org.apache.poi.poifs.crypt; | ||||
import java.io.UnsupportedEncodingException; | |||||
import java.nio.charset.Charset; | |||||
import java.security.DigestException; | import java.security.DigestException; | ||||
import java.security.GeneralSecurityException; | import java.security.GeneralSecurityException; | ||||
import java.security.MessageDigest; | import java.security.MessageDigest; | ||||
* @return the hashed password | * @return the hashed password | ||||
*/ | */ | ||||
public static byte[] hashPassword(String password, HashAlgorithm hashAlgorithm, byte salt[], int spinCount) { | 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 no password was given, use the default | ||||
if (password == null) { | if (password == null) { | ||||
password = Decryptor.DEFAULT_PASSWORD; | password = Decryptor.DEFAULT_PASSWORD; | ||||
hashAlg.update(salt); | hashAlg.update(salt); | ||||
byte[] hash = hashAlg.digest(getUtf16LeString(password)); | byte[] hash = hashAlg.digest(getUtf16LeString(password)); | ||||
byte[] iterator = new byte[LittleEndianConsts.INT_SIZE]; | byte[] iterator = new byte[LittleEndianConsts.INT_SIZE]; | ||||
byte[] first = (iteratorFirst ? iterator : hash); | |||||
byte[] second = (iteratorFirst ? hash : iterator); | |||||
try { | try { | ||||
for (int i = 0; i < spinCount; i++) { | for (int i = 0; i < spinCount; i++) { | ||||
LittleEndian.putInt(iterator, 0, i); | LittleEndian.putInt(iterator, 0, i); | ||||
hashAlg.reset(); | 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 | hashAlg.digest(hash, 0, hash.length); // don't create hash buffer everytime new | ||||
} | } | ||||
} catch (DigestException e) { | } catch (DigestException e) { | ||||
} | } | ||||
public static byte[] getUtf16LeString(String str) { | 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) { | public static MessageDigest getMessageDigest(HashAlgorithm hashAlgorithm) { | ||||
throw new EncryptedDocumentException("Only the BouncyCastle provider supports your encryption settings - please add it to the classpath."); | 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 <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 < 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 | |||||
); | |||||
} | |||||
} | } |
import org.apache.poi.openxml4j.opc.PackageRelationshipTypes; | import org.apache.poi.openxml4j.opc.PackageRelationshipTypes; | ||||
import org.apache.poi.openxml4j.opc.PackagingURIHelper; | import org.apache.poi.openxml4j.opc.PackagingURIHelper; | ||||
import org.apache.poi.openxml4j.opc.TargetMode; | 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.IOUtils; | ||||
import org.apache.poi.util.IdentifierManager; | import org.apache.poi.util.IdentifierManager; | ||||
import org.apache.poi.util.Internal; | import org.apache.poi.util.Internal; | ||||
settings.setEnforcementEditValue(STDocProtect.READ_ONLY); | settings.setEnforcementEditValue(STDocProtect.READ_ONLY); | ||||
} | } | ||||
/** | |||||
* Enforces the readOnly protection with a password.<br/> | |||||
* <br/> | |||||
* sample snippet from settings.xml | |||||
* <pre> | |||||
* <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="...." | |||||
* /> | |||||
* </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/> | * Enforce the Filling Forms protection.<br/> | ||||
* In the documentProtection tag inside settings.xml file, <br/> | * In the documentProtection tag inside settings.xml file, <br/> | ||||
settings.setEnforcementEditValue(STDocProtect.FORMS); | settings.setEnforcementEditValue(STDocProtect.FORMS); | ||||
} | } | ||||
/** | |||||
* Enforce the Filling Forms protection.<br/> | |||||
* <br/> | |||||
* sample snippet from settings.xml | |||||
* <pre> | |||||
* <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="...." | |||||
* /> | |||||
* </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/> | * Enforce the Comments protection.<br/> | ||||
* In the documentProtection tag inside settings.xml file,<br/> | * In the documentProtection tag inside settings.xml file,<br/> | ||||
settings.setEnforcementEditValue(STDocProtect.COMMENTS); | settings.setEnforcementEditValue(STDocProtect.COMMENTS); | ||||
} | } | ||||
/** | |||||
* Enforce the Comments protection.<br/> | |||||
* <br/> | |||||
* sample snippet from settings.xml | |||||
* <pre> | |||||
* <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="...." | |||||
* /> | |||||
* </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/> | * Enforce the Tracked Changes protection.<br/> | ||||
* In the documentProtection tag inside settings.xml file, <br/> | * In the documentProtection tag inside settings.xml file, <br/> | ||||
settings.setEnforcementEditValue(STDocProtect.TRACKED_CHANGES); | settings.setEnforcementEditValue(STDocProtect.TRACKED_CHANGES); | ||||
} | } | ||||
/** | |||||
* Enforce the Tracked Changes protection.<br/> | |||||
* <br/> | |||||
* sample snippet from settings.xml | |||||
* <pre> | |||||
* <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="...." | |||||
* /> | |||||
* </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/> | * Remove protection enforcement.<br/> | ||||
* In the documentProtection tag inside settings.xml file <br/> | * In the documentProtection tag inside settings.xml file <br/> |
import java.io.InputStream; | import java.io.InputStream; | ||||
import java.io.OutputStream; | import java.io.OutputStream; | ||||
import java.math.BigInteger; | import java.math.BigInteger; | ||||
import java.security.SecureRandom; | |||||
import java.util.Arrays; | |||||
import java.util.HashMap; | import java.util.HashMap; | ||||
import java.util.Map; | import java.util.Map; | ||||
import javax.xml.namespace.QName; | import javax.xml.namespace.QName; | ||||
import org.apache.poi.EncryptedDocumentException; | |||||
import org.apache.poi.POIXMLDocumentPart; | import org.apache.poi.POIXMLDocumentPart; | ||||
import org.apache.poi.openxml4j.opc.PackagePart; | import org.apache.poi.openxml4j.opc.PackagePart; | ||||
import org.apache.poi.openxml4j.opc.PackageRelationship; | 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.apache.xmlbeans.XmlOptions; | ||||
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTDocProtect; | import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTDocProtect; | ||||
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTOnOff; | import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTOnOff; | ||||
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSettings; | import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSettings; | ||||
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTZoom; | 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.STDocProtect; | ||||
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STOnOff; | import org.openxmlformats.schemas.wordprocessingml.x2006.main.STOnOff; | ||||
import org.openxmlformats.schemas.wordprocessingml.x2006.main.SettingsDocument; | import org.openxmlformats.schemas.wordprocessingml.x2006.main.SettingsDocument; | ||||
safeGetDocumentProtection().setEdit(editValue); | safeGetDocumentProtection().setEdit(editValue); | ||||
} | } | ||||
/** | |||||
* Enforces the protection with the option specified by passed editValue and password.<br/> | |||||
* <br/> | |||||
* sample snippet from settings.xml | |||||
* <pre> | |||||
* <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="...." | |||||
* /> | |||||
* </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/> | * Removes protection enforcement.<br/> | ||||
* In the documentProtection tag inside settings.xml file <br/> | * In the documentProtection tag inside settings.xml file <br/> | ||||
throw new RuntimeException(e); | throw new RuntimeException(e); | ||||
} | } | ||||
} | } | ||||
} | } |
==================================================================== */ | ==================================================================== */ | ||||
package org.apache.poi.xwpf; | 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.File; | ||||
import java.io.FileInputStream; | import java.io.FileInputStream; | ||||
import java.io.FileOutputStream; | 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.util.TempFile; | ||||
import org.apache.poi.xwpf.usermodel.XWPFDocument; | import org.apache.poi.xwpf.usermodel.XWPFDocument; | ||||
import org.apache.poi.xwpf.usermodel.XWPFParagraph; | import org.apache.poi.xwpf.usermodel.XWPFParagraph; | ||||
import org.apache.poi.xwpf.usermodel.XWPFRun; | 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 { | public void testShouldReadEnforcementProperties() throws Exception { | ||||
XWPFDocument documentWithoutDocumentProtectionTag = XWPFTestDataSamples.openSampleDocument("documentProtection_no_protection.docx"); | XWPFDocument documentWithoutDocumentProtectionTag = XWPFTestDataSamples.openSampleDocument("documentProtection_no_protection.docx"); | ||||
} | } | ||||
@Test | |||||
public void testShouldEnforceForReadOnly() throws Exception { | public void testShouldEnforceForReadOnly() throws Exception { | ||||
// XWPFDocument document = createDocumentFromSampleFile("test-data/document/documentProtection_no_protection.docx"); | // XWPFDocument document = createDocumentFromSampleFile("test-data/document/documentProtection_no_protection.docx"); | ||||
XWPFDocument document = XWPFTestDataSamples.openSampleDocument("documentProtection_no_protection.docx"); | XWPFDocument document = XWPFTestDataSamples.openSampleDocument("documentProtection_no_protection.docx"); | ||||
assertTrue(document.isEnforcedReadonlyProtection()); | assertTrue(document.isEnforcedReadonlyProtection()); | ||||
} | } | ||||
@Test | |||||
public void testShouldEnforceForFillingForms() throws Exception { | public void testShouldEnforceForFillingForms() throws Exception { | ||||
XWPFDocument document = XWPFTestDataSamples.openSampleDocument("documentProtection_no_protection.docx"); | XWPFDocument document = XWPFTestDataSamples.openSampleDocument("documentProtection_no_protection.docx"); | ||||
assertFalse(document.isEnforcedFillingFormsProtection()); | assertFalse(document.isEnforcedFillingFormsProtection()); | ||||
assertTrue(document.isEnforcedFillingFormsProtection()); | assertTrue(document.isEnforcedFillingFormsProtection()); | ||||
} | } | ||||
@Test | |||||
public void testShouldEnforceForComments() throws Exception { | public void testShouldEnforceForComments() throws Exception { | ||||
XWPFDocument document = XWPFTestDataSamples.openSampleDocument("documentProtection_no_protection.docx"); | XWPFDocument document = XWPFTestDataSamples.openSampleDocument("documentProtection_no_protection.docx"); | ||||
assertFalse(document.isEnforcedCommentsProtection()); | assertFalse(document.isEnforcedCommentsProtection()); | ||||
assertTrue(document.isEnforcedCommentsProtection()); | assertTrue(document.isEnforcedCommentsProtection()); | ||||
} | } | ||||
@Test | |||||
public void testShouldEnforceForTrackedChanges() throws Exception { | public void testShouldEnforceForTrackedChanges() throws Exception { | ||||
XWPFDocument document = XWPFTestDataSamples.openSampleDocument("documentProtection_no_protection.docx"); | XWPFDocument document = XWPFTestDataSamples.openSampleDocument("documentProtection_no_protection.docx"); | ||||
assertFalse(document.isEnforcedTrackedChangesProtection()); | assertFalse(document.isEnforcedTrackedChangesProtection()); | ||||
assertTrue(document.isEnforcedTrackedChangesProtection()); | assertTrue(document.isEnforcedTrackedChangesProtection()); | ||||
} | } | ||||
@Test | |||||
public void testShouldUnsetEnforcement() throws Exception { | public void testShouldUnsetEnforcement() throws Exception { | ||||
XWPFDocument document = XWPFTestDataSamples.openSampleDocument("documentProtection_readonly_no_password.docx"); | XWPFDocument document = XWPFTestDataSamples.openSampleDocument("documentProtection_readonly_no_password.docx"); | ||||
assertTrue(document.isEnforcedReadonlyProtection()); | assertTrue(document.isEnforcedReadonlyProtection()); | ||||
assertFalse(document.isEnforcedReadonlyProtection()); | assertFalse(document.isEnforcedReadonlyProtection()); | ||||
} | } | ||||
@Test | |||||
public void testIntegration() throws Exception { | public void testIntegration() throws Exception { | ||||
XWPFDocument doc = new XWPFDocument(); | XWPFDocument doc = new XWPFDocument(); | ||||
assertTrue(document.isEnforcedCommentsProtection()); | assertTrue(document.isEnforcedCommentsProtection()); | ||||
} | } | ||||
@Test | |||||
public void testUpdateFields() throws Exception { | public void testUpdateFields() throws Exception { | ||||
XWPFDocument doc = new XWPFDocument(); | XWPFDocument doc = new XWPFDocument(); | ||||
assertFalse(doc.isEnforcedUpdateFields()); | assertFalse(doc.isEnforcedUpdateFields()); | ||||
assertTrue(doc.isEnforcedUpdateFields()); | 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); | |||||
} | |||||
} | } |