#59135 - Password gets truncated when using passwords longer than 15 characters for the function protectSheet()
git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1734843 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
parent
045123e198
commit
653403e956
@ -34,6 +34,7 @@ import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.RC2ParameterSpec;
|
||||
|
||||
import org.apache.poi.EncryptedDocumentException;
|
||||
import org.apache.poi.util.Internal;
|
||||
import org.apache.poi.util.LittleEndian;
|
||||
import org.apache.poi.util.LittleEndianConsts;
|
||||
import org.apache.poi.util.StringUtil;
|
||||
@ -41,34 +42,36 @@ import org.apache.poi.util.StringUtil;
|
||||
/**
|
||||
* Helper functions used for standard and agile encryption
|
||||
*/
|
||||
@Internal
|
||||
public class CryptoFunctions {
|
||||
/**
|
||||
* 2.3.4.7 ECMA-376 Document Encryption Key Generation (Standard Encryption)
|
||||
* 2.3.4.11 Encryption Key Generation (Agile Encryption)
|
||||
* <p><cite>2.3.4.7 ECMA-376 Document Encryption Key Generation (Standard Encryption)<br/>
|
||||
* 2.3.4.11 Encryption Key Generation (Agile Encryption)</cite></p>
|
||||
*
|
||||
* The encryption key for ECMA-376 document encryption [ECMA-376] using agile encryption MUST be
|
||||
* generated by using the following method, which is derived from PKCS #5: Password-Based
|
||||
* Cryptography Version 2.0 [RFC2898].
|
||||
* <p>The encryption key for ECMA-376 document encryption [ECMA-376] using agile
|
||||
* encryption MUST be generated by using the following method, which is derived from PKCS #5:
|
||||
* <a href="https://www.ietf.org/rfc/rfc2898.txt">Password-Based Cryptography Version 2.0 [RFC2898]</a>.</p>
|
||||
*
|
||||
* Let H() be a hashing algorithm as determined by the PasswordKeyEncryptor.hashAlgorithm
|
||||
* element, H_n be the hash data of the n-th iteration, and a plus sign (+) represent concatenation. The
|
||||
* password MUST be provided as an array of Unicode characters. Limitations on the length of the
|
||||
* password and the characters used by the password are implementation-dependent. The initial
|
||||
* password hash is generated as follows:
|
||||
* <p>Let H() be a hashing algorithm as determined by the PasswordKeyEncryptor.hashAlgorithm
|
||||
* element, H_n be the hash data of the n-th iteration, and a plus sign (+) represent concatenation.
|
||||
* The password MUST be provided as an array of Unicode characters. Limitations on the length of the
|
||||
* password and the characters used by the password are implementation-dependent.
|
||||
* The initial password hash is generated as follows:</p>
|
||||
*
|
||||
* - H_0 = H(salt + password)
|
||||
*
|
||||
* The salt used MUST be generated randomly. The salt MUST be stored in the
|
||||
* PasswordKeyEncryptor.saltValue element contained within the \EncryptionInfo stream (1) as
|
||||
* specified in section 2.3.4.10. The hash is then iterated by using the following approach:
|
||||
* <pre>H_0 = H(salt + password)</pre>
|
||||
*
|
||||
* - H_n = H(iterator + H_n-1)
|
||||
* <p>The salt used MUST be generated randomly. The salt MUST be stored in the
|
||||
* PasswordKeyEncryptor.saltValue element contained within the \EncryptionInfo stream as
|
||||
* specified in section 2.3.4.10. The hash is then iterated by using the following approach:</p>
|
||||
*
|
||||
* where iterator is an unsigned 32-bit value that is initially set to 0x00000000 and then incremented
|
||||
* <pre>H_n = H(iterator + H_n-1)</pre>
|
||||
*
|
||||
* <p>where iterator is an unsigned 32-bit value that is initially set to 0x00000000 and then incremented
|
||||
* monotonically on each iteration until PasswordKey.spinCount iterations have been performed.
|
||||
* The value of iterator on the last iteration MUST be one less than PasswordKey.spinCount.
|
||||
* The value of iterator on the last iteration MUST be one less than PasswordKey.spinCount.</p>
|
||||
*
|
||||
* For POI, H_final will be calculated by {@link #generateKey(byte[],HashAlgorithm,byte[],int)}
|
||||
* <p>For POI, H_final will be calculated by {@link #generateKey(byte[],HashAlgorithm,byte[],int)}</p>
|
||||
*
|
||||
* @param password
|
||||
* @param hashAlgorithm
|
||||
@ -124,19 +127,21 @@ public class CryptoFunctions {
|
||||
}
|
||||
|
||||
/**
|
||||
* 2.3.4.12 Initialization Vector Generation (Agile Encryption)
|
||||
* <p><cite>2.3.4.12 Initialization Vector Generation (Agile Encryption)</cite></p>
|
||||
*
|
||||
* Initialization vectors are used in all cases for agile encryption. An initialization vector MUST be
|
||||
* <p>Initialization vectors are used in all cases for agile encryption. An initialization vector MUST be
|
||||
* generated by using the following method, where H() is a hash function that MUST be the same as
|
||||
* specified in section 2.3.4.11 and a plus sign (+) represents concatenation:
|
||||
* 1. If a blockKey is provided, let IV be a hash of the KeySalt and the following value:
|
||||
* blockKey: IV = H(KeySalt + blockKey)
|
||||
* 2. If a blockKey is not provided, let IV be equal to the following value:
|
||||
* KeySalt:IV = KeySalt.
|
||||
* 3. If the number of bytes in the value of IV is less than the the value of the blockSize attribute
|
||||
* corresponding to the cipherAlgorithm attribute, pad the array of bytes by appending 0x36 until
|
||||
* the array is blockSize bytes. If the array of bytes is larger than blockSize bytes, truncate the
|
||||
* array to blockSize bytes.
|
||||
* specified in section 2.3.4.11 and a plus sign (+) represents concatenation:</p>
|
||||
* <ul>
|
||||
* <li>If a blockKey is provided, let IV be a hash of the KeySalt and the following value:<br/>
|
||||
* {@code blockKey: IV = H(KeySalt + blockKey)}</li>
|
||||
* <li>If a blockKey is not provided, let IV be equal to the following value:<br/>
|
||||
* {@code KeySalt:IV = KeySalt}</li>
|
||||
* <li>If the number of bytes in the value of IV is less than the the value of the blockSize attribute
|
||||
* corresponding to the cipherAlgorithm attribute, pad the array of bytes by appending 0x36 until
|
||||
* the array is blockSize bytes. If the array of bytes is larger than blockSize bytes, truncate the
|
||||
* array to blockSize bytes.</li>
|
||||
* </ul>
|
||||
**/
|
||||
public static byte[] generateIv(HashAlgorithm hashAlgorithm, byte[] salt, byte[] blockKey, int blockSize) {
|
||||
byte iv[] = salt;
|
||||
@ -149,21 +154,19 @@ public class CryptoFunctions {
|
||||
}
|
||||
|
||||
/**
|
||||
* 2.3.4.11 Encryption Key Generation (Agile Encryption)
|
||||
* <p><cite>2.3.4.11 Encryption Key Generation (Agile Encryption)</cite></p>
|
||||
*
|
||||
* ... continued ...
|
||||
* <p>The final hash data that is used for an encryption key is then generated by using the following
|
||||
* method:</p>
|
||||
*
|
||||
* The final hash data that is used for an encryption key is then generated by using the following
|
||||
* method:
|
||||
* <pre>H_final = H(H_n + blockKey)</pre>
|
||||
*
|
||||
* - H_final = H(H_n + blockKey)
|
||||
* <p>where blockKey represents an array of bytes used to prevent two different blocks from encrypting
|
||||
* to the same cipher text.</p>
|
||||
*
|
||||
* where blockKey represents an array of bytes used to prevent two different blocks from encrypting
|
||||
* to the same cipher text.
|
||||
*
|
||||
* If the size of the resulting H_final is smaller than that of PasswordKeyEncryptor.keyBits, the key
|
||||
* <p>If the size of the resulting H_final is smaller than that of PasswordKeyEncryptor.keyBits, the key
|
||||
* MUST be padded by appending bytes with a value of 0x36. If the hash value is larger in size than
|
||||
* PasswordKeyEncryptor.keyBits, the key is obtained by truncating the hash value.
|
||||
* PasswordKeyEncryptor.keyBits, the key is obtained by truncating the hash value.</p>
|
||||
*
|
||||
* @param passwordHash
|
||||
* @param hashAlgorithm
|
||||
@ -178,6 +181,21 @@ public class CryptoFunctions {
|
||||
return getBlock36(key, keySize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a new cipher object with the given cipher properties and no padding
|
||||
* If the given algorithm is not implemented in the JCE, it will try to load it from the bouncy castle
|
||||
* provider.
|
||||
*
|
||||
* @param key the secrect key
|
||||
* @param cipherAlgorithm the cipher algorithm
|
||||
* @param chain the chaining mode
|
||||
* @param vec the initialization vector (IV), can be null
|
||||
* @param cipherMode Cipher.DECRYPT_MODE or Cipher.ENCRYPT_MODE
|
||||
* @return the requested cipher
|
||||
* @throws GeneralSecurityException
|
||||
* @throws EncryptedDocumentException if the initialization failed or if an algorithm was specified,
|
||||
* which depends on a missing bouncy castle provider
|
||||
*/
|
||||
public static Cipher getCipher(SecretKey key, CipherAlgorithm cipherAlgorithm, ChainingMode chain, byte[] vec, int cipherMode) {
|
||||
return getCipher(key, cipherAlgorithm, chain, vec, cipherMode, null);
|
||||
}
|
||||
@ -192,7 +210,7 @@ public class CryptoFunctions {
|
||||
* @param chain the chaining mode
|
||||
* @param vec the initialization vector (IV), can be null
|
||||
* @param cipherMode Cipher.DECRYPT_MODE or Cipher.ENCRYPT_MODE
|
||||
* @param padding
|
||||
* @param padding the padding (null = NOPADDING, ANSIX923Padding, PKCS5Padding, PKCS7Padding, ISO10126Padding, ...)
|
||||
* @return the requested cipher
|
||||
* @throws GeneralSecurityException
|
||||
* @throws EncryptedDocumentException if the initialization failed or if an algorithm was specified,
|
||||
@ -243,7 +261,7 @@ public class CryptoFunctions {
|
||||
* @param size the size of the returned byte array
|
||||
* @return the padded hash
|
||||
*/
|
||||
public static byte[] getBlock36(byte[] hash, int size) {
|
||||
private static byte[] getBlock36(byte[] hash, int size) {
|
||||
return getBlockX(hash, size, (byte)0x36);
|
||||
}
|
||||
|
||||
@ -296,30 +314,33 @@ public class CryptoFunctions {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static void registerBouncyCastle() {
|
||||
if (Security.getProvider("BC") != null) return;
|
||||
if (Security.getProvider("BC") != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
ClassLoader cl = Thread.currentThread().getContextClassLoader();
|
||||
String bcProviderName = "org.bouncycastle.jce.provider.BouncyCastleProvider";
|
||||
Class<Provider> clazz = (Class<Provider>)cl.loadClass(bcProviderName);
|
||||
Security.addProvider(clazz.newInstance());
|
||||
} catch (Exception e) {
|
||||
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.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static final int InitialCodeArray[] = {
|
||||
private static final int INITIAL_CODE_ARRAY[] = {
|
||||
0xE1F0, 0x1D0F, 0xCC9C, 0x84C0, 0x110C, 0x0E10, 0xF1CE,
|
||||
0x313E, 0x1872, 0xE139, 0xD40F, 0x84F9, 0x280C, 0xA96A,
|
||||
0x4EC3
|
||||
};
|
||||
|
||||
private static final byte PadArray[] = {
|
||||
private static final byte PAD_ARRAY[] = {
|
||||
(byte)0xBB, (byte)0xFF, (byte)0xFF, (byte)0xBA, (byte)0xFF,
|
||||
(byte)0xFF, (byte)0xB9, (byte)0x80, (byte)0x00, (byte)0xBE,
|
||||
(byte)0x0F, (byte)0x00, (byte)0xBF, (byte)0x0F, (byte)0x00
|
||||
};
|
||||
|
||||
private static final int EncryptionMatrix[][] = {
|
||||
private static final int ENCRYPTION_MATRIX[][] = {
|
||||
/* 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},
|
||||
@ -337,6 +358,40 @@ public class CryptoFunctions {
|
||||
/* char 15 */ {0x1021, 0x2042, 0x4084, 0x8108, 0x1231, 0x2462, 0x48C4}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create the verifier for xor obfuscation (method 1)
|
||||
*
|
||||
* @see <a href="http://msdn.microsoft.com/en-us/library/dd926947.aspx">2.3.7.1 Binary Document Password Verifier Derivation Method 1</a>
|
||||
* @see <a href="http://msdn.microsoft.com/en-us/library/dd905229.aspx">2.3.7.4 Binary Document Password Verifier Derivation Method 2</a>
|
||||
* @see <a href="http://www.ecma-international.org/news/TC45_current_work/Office Open XML Part 4 - Markup Language Reference.pdf">Part 4 - Markup Language Reference - Ecma International - 3.2.12 fileSharing</a>
|
||||
*
|
||||
* @param password the password
|
||||
* @return the verifier (actually a short value)
|
||||
*/
|
||||
public static int createXorVerifier1(String password) {
|
||||
byte[] arrByteChars = toAnsiPassword(password);
|
||||
|
||||
// SET Verifier TO 0x0000
|
||||
short verifier = 0;
|
||||
|
||||
// FOR EACH PasswordByte IN PasswordArray IN REVERSE ORDER
|
||||
for (int i = arrByteChars.length-1; i >= 0; i--) {
|
||||
// SET Verifier TO Intermediate3 BITWISE XOR PasswordByte
|
||||
verifier = rotateLeftBase15Bit(verifier);
|
||||
verifier ^= arrByteChars[i];
|
||||
}
|
||||
|
||||
// as we haven't prepended the password length into the input array
|
||||
// we need to do it now separately ...
|
||||
verifier = rotateLeftBase15Bit(verifier);
|
||||
verifier ^= arrByteChars.length;
|
||||
|
||||
// RETURN Verifier BITWISE XOR 0xCE4B
|
||||
verifier ^= 0xCE4B; // (0x8000 | ('N' << 8) | 'K')
|
||||
|
||||
return verifier & 0xFFFF;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method generates the xor verifier for word documents < 2007 (method 2).
|
||||
* Its output will be used as password input for the newer word generations which
|
||||
@ -360,22 +415,12 @@ public class CryptoFunctions {
|
||||
// Truncate the password to 15 characters
|
||||
password = password.substring(0, Math.min(password.length(), maxPasswordLength));
|
||||
|
||||
// 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()];
|
||||
byte[] arrByteChars = toAnsiPassword(password);
|
||||
|
||||
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];
|
||||
int highOrderWord = INITIAL_CODE_ARRAY[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)
|
||||
@ -385,35 +430,18 @@ public class CryptoFunctions {
|
||||
int tmp = maxPasswordLength - arrByteChars.length + i;
|
||||
for (int intBit = 0; intBit < 7; intBit++) {
|
||||
if ((arrByteChars[i] & (0x0001 << intBit)) != 0) {
|
||||
highOrderWord ^= EncryptionMatrix[tmp][intBit];
|
||||
highOrderWord ^= ENCRYPTION_MATRIX[tmp][intBit];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compute the low-order word of the new key:
|
||||
|
||||
// SET Verifier TO 0x0000
|
||||
short verifier = 0;
|
||||
|
||||
// FOR EACH PasswordByte IN PasswordArray IN REVERSE ORDER
|
||||
for (int i = arrByteChars.length-1; i >= 0; i--) {
|
||||
// SET Verifier TO Intermediate3 BITWISE XOR PasswordByte
|
||||
verifier = rotateLeftBase15Bit(verifier);
|
||||
verifier ^= arrByteChars[i];
|
||||
}
|
||||
|
||||
// as we haven't prepended the password length into the input array
|
||||
// we need to do it now separately ...
|
||||
verifier = rotateLeftBase15Bit(verifier);
|
||||
verifier ^= arrByteChars.length;
|
||||
|
||||
// RETURN Verifier BITWISE XOR 0xCE4B
|
||||
verifier ^= 0xCE4B; // (0x8000 | ('N' << 8) | 'K')
|
||||
int verifier = createXorVerifier1(password);
|
||||
|
||||
// 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, verifier);
|
||||
LittleEndian.putShort(generatedKey, 0, (short)verifier);
|
||||
LittleEndian.putShort(generatedKey, 2, (short)highOrderWord);
|
||||
}
|
||||
|
||||
@ -443,21 +471,6 @@ public class CryptoFunctions {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the verifier for xor obfuscation (method 1)
|
||||
*
|
||||
* @see <a href="http://msdn.microsoft.com/en-us/library/dd926947.aspx">2.3.7.1 Binary Document Password Verifier Derivation Method 1</a>
|
||||
* @see <a href="http://msdn.microsoft.com/en-us/library/dd905229.aspx">2.3.7.4 Binary Document Password Verifier Derivation Method 2</a>
|
||||
*
|
||||
* @param password the password
|
||||
* @return the verifier (actually a short value)
|
||||
*/
|
||||
public static int createXorVerifier1(String password) {
|
||||
// the verifier for method 1 is part of the verifier for method 2
|
||||
// so we simply chop it from there
|
||||
return createXorVerifier2(password) & 0xFFFF;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the xor key for xor obfuscation, which is used to create the xor array (method 1)
|
||||
*
|
||||
@ -490,12 +503,12 @@ public class CryptoFunctions {
|
||||
// The MS-OFFCRYPTO misses some infos about the various rotation sizes
|
||||
byte obfuscationArray[] = new byte[16];
|
||||
System.arraycopy(passBytes, 0, obfuscationArray, 0, passBytes.length);
|
||||
System.arraycopy(PadArray, 0, obfuscationArray, passBytes.length, PadArray.length-passBytes.length+1);
|
||||
System.arraycopy(PAD_ARRAY, 0, obfuscationArray, passBytes.length, PAD_ARRAY.length-passBytes.length+1);
|
||||
|
||||
int xorKey = createXorKey1(password);
|
||||
|
||||
// rotation of key values is application dependent
|
||||
int nRotateSize = 2; /* Excel = 2; Word = 7 */
|
||||
// rotation of key values is application dependent - Excel = 2 / Word = 7
|
||||
int nRotateSize = 2;
|
||||
|
||||
byte baseKeyLE[] = { (byte)(xorKey & 0xFF), (byte)((xorKey >>> 8) & 0xFF) };
|
||||
for (int i=0; i<obfuscationArray.length; i++) {
|
||||
@ -505,7 +518,33 @@ public class CryptoFunctions {
|
||||
|
||||
return obfuscationArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* The provided Unicode password string is converted to a ANSI string
|
||||
*
|
||||
* @param password the password
|
||||
* @return the ansi bytes
|
||||
*
|
||||
* @see <a href="http://www.ecma-international.org/news/TC45_current_work/Office Open XML Part 4 - Markup Language Reference.pdf">Part 4 - Markup Language Reference - Ecma International</a> (3.2.29 workbookProtection)
|
||||
*/
|
||||
private static byte[] toAnsiPassword(String password) {
|
||||
// TODO: charset conversion (see ecma spec)
|
||||
|
||||
// Get the single-byte values by iterating through the Unicode characters.
|
||||
// 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 & 0xFF);
|
||||
byte highByte = (byte)((intTemp >>> 8) & 0xFF);
|
||||
arrByteChars[i] = (lowByte != 0 ? lowByte : highByte);
|
||||
}
|
||||
|
||||
return arrByteChars;
|
||||
}
|
||||
|
||||
private static byte rotateLeft(byte bits, int shift) {
|
||||
return (byte)(((bits & 0xff) << shift) | ((bits & 0xff) >>> (8 - shift)));
|
||||
}
|
||||
|
@ -1222,4 +1222,23 @@ public final class TestHSSFSheet extends BaseTestSheet {
|
||||
|
||||
wb.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bug59135() throws IOException {
|
||||
HSSFWorkbook wb1 = new HSSFWorkbook();
|
||||
wb1.createSheet().protectSheet("1111.2222.3333.1234");
|
||||
HSSFWorkbook wb2 = HSSFTestDataSamples.writeOutAndReadBack(wb1);
|
||||
wb1.close();
|
||||
|
||||
assertEquals((short)0xb86b, wb2.getSheetAt(0).getPassword());
|
||||
wb2.close();
|
||||
|
||||
HSSFWorkbook wb3 = new HSSFWorkbook();
|
||||
wb3.createSheet().protectSheet("1111.2222.3333.12345");
|
||||
HSSFWorkbook wb4 = HSSFTestDataSamples.writeOutAndReadBack(wb3);
|
||||
wb3.close();
|
||||
|
||||
assertEquals((short)0xbecc, wb4.getSheetAt(0).getPassword());
|
||||
wb4.close();
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user