From 3b7bbadb5d1ef82d63aa4773f7ff90eda3af7e3a Mon Sep 17 00:00:00 2001 From: Andreas Beeker Date: Sun, 13 Mar 2016 19:31:32 +0000 Subject: [PATCH] #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 --- .../poi/poifs/crypt/CryptoFunctions.java | 231 ++++++++++-------- .../poi/hssf/usermodel/TestHSSFSheet.java | 19 ++ 2 files changed, 154 insertions(+), 96 deletions(-) diff --git a/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java b/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java index 2b6cb9ebdf..0db54d5413 100644 --- a/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java +++ b/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java @@ -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) + *

2.3.4.7 ECMA-376 Document Encryption Key Generation (Standard Encryption)
+ * 2.3.4.11 Encryption Key Generation (Agile Encryption)

* - * 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]. + *

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].

* - * 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: + *

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:

* - * - 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: + *
H_0 = H(salt + password)
* - * - H_n = H(iterator + H_n-1) + *

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:

* - * where iterator is an unsigned 32-bit value that is initially set to 0x00000000 and then incremented + *
H_n = H(iterator + H_n-1)
+ * + *

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.

* - * For POI, H_final will be calculated by {@link #generateKey(byte[],HashAlgorithm,byte[],int)} + *

For POI, H_final will be calculated by {@link #generateKey(byte[],HashAlgorithm,byte[],int)}

* * @param password * @param hashAlgorithm @@ -124,19 +127,21 @@ public class CryptoFunctions { } /** - * 2.3.4.12 Initialization Vector Generation (Agile Encryption) + *

2.3.4.12 Initialization Vector Generation (Agile Encryption)

* - * Initialization vectors are used in all cases for agile encryption. An initialization vector MUST be + *

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:

+ * **/ 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) - * - * ... continued ... + *

2.3.4.11 Encryption Key Generation (Agile Encryption)

* - * The final hash data that is used for an encryption key is then generated by using the following - * method: + *

The final hash data that is used for an encryption key is then generated by using the following + * method:

* - * - H_final = H(H_n + blockKey) + *
H_final = H(H_n + blockKey)
* - * where blockKey represents an array of bytes used to prevent two different blocks from encrypting - * to the same cipher text. + *

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 + *

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.

* * @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 clazz = (Class)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 2.3.7.1 Binary Document Password Verifier Derivation Method 1 + * @see 2.3.7.4 Binary Document Password Verifier Derivation Method 2 + * @see Part 4 - Markup Language Reference - Ecma International - 3.2.12 fileSharing + * + * @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 2.3.7.1 Binary Document Password Verifier Derivation Method 1 - * @see 2.3.7.4 Binary Document Password Verifier Derivation Method 2 - * - * @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; iPart 4 - Markup Language Reference - Ecma International (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))); } diff --git a/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFSheet.java b/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFSheet.java index 71436195a8..56fdc2ef2c 100644 --- a/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFSheet.java +++ b/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFSheet.java @@ -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(); + } } -- 2.39.5