From d71574d53115d54f2a30b602ea131e8e1671850c Mon Sep 17 00:00:00 2001 From: Andreas Beeker Date: Thu, 4 Sep 2014 22:50:28 +0000 Subject: [PATCH] Bug 51483 - XSSF locking of specific features not working Added some documentation to the crypto functions and adapted xor1verifier code to the OFFCrypto-Docs git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1622577 13f79535-47bb-0310-9956-ffa450edef68 --- .../poi/hssf/record/PasswordRecord.java | 23 +- .../aggregates/WorksheetProtectionBlock.java | 3 +- .../poi/poifs/crypt/CryptoFunctions.java | 83 +++- .../apache/poi/poifs/crypt/HashAlgorithm.java | 7 + .../apache/poi/xssf/usermodel/XSSFSheet.java | 382 ++++++++++++------ .../poi/xssf/usermodel/XSSFWorkbook.java | 88 +++- .../usermodel/helpers/XSSFPaswordHelper.java | 128 ++++++ .../apache/poi/xssf/TestSheetProtection.java | 35 +- .../poi/xssf/TestWorkbookProtection.java | 128 ++++-- .../poi/xssf/usermodel/TestXSSFSheet.java | 24 +- ...orkbookProtection-sheet_password-2013.xlsx | Bin 0 -> 9196 bytes ...bookProtection-workbook_password-2013.xlsx | Bin 0 -> 8171 bytes ...ion-workbook_password_user_range-2010.xlsx | Bin 0 -> 11207 bytes 13 files changed, 692 insertions(+), 209 deletions(-) create mode 100644 src/ooxml/java/org/apache/poi/xssf/usermodel/helpers/XSSFPaswordHelper.java create mode 100644 test-data/spreadsheet/workbookProtection-sheet_password-2013.xlsx create mode 100644 test-data/spreadsheet/workbookProtection-workbook_password-2013.xlsx create mode 100644 test-data/spreadsheet/workbookProtection-workbook_password_user_range-2010.xlsx diff --git a/src/java/org/apache/poi/hssf/record/PasswordRecord.java b/src/java/org/apache/poi/hssf/record/PasswordRecord.java index 9baff6f97d..c38a1230ca 100644 --- a/src/java/org/apache/poi/hssf/record/PasswordRecord.java +++ b/src/java/org/apache/poi/hssf/record/PasswordRecord.java @@ -17,6 +17,7 @@ package org.apache.poi.hssf.record; +import org.apache.poi.poifs.crypt.CryptoFunctions; import org.apache.poi.util.HexDump; import org.apache.poi.util.LittleEndianOutput; @@ -39,23 +40,13 @@ public final class PasswordRecord extends StandardRecord { field_1_password = in.readShort(); } - //this is the world's lamest "security". thanks to Wouter van Vugt for making me - //not have to try real hard. -ACO + /** + * Return the password hash + * + * @deprecated use {@link CryptoFunctions#createXorVerifier1(String)} + */ public static short hashPassword(String password) { - byte[] passwordCharacters = password.getBytes(); - int hash = 0; - if (passwordCharacters.length > 0) { - int charIndex = passwordCharacters.length; - while (charIndex-- > 0) { - hash = ((hash >> 14) & 0x01) | ((hash << 1) & 0x7fff); - hash ^= passwordCharacters[charIndex]; - } - // also hash with charcount - hash = ((hash >> 14) & 0x01) | ((hash << 1) & 0x7fff); - hash ^= passwordCharacters.length; - hash ^= (0x8000 | ('N' << 8) | 'K'); - } - return (short)hash; + return (short)CryptoFunctions.createXorVerifier1(password); } /** diff --git a/src/java/org/apache/poi/hssf/record/aggregates/WorksheetProtectionBlock.java b/src/java/org/apache/poi/hssf/record/aggregates/WorksheetProtectionBlock.java index f838485f74..275fbf123a 100644 --- a/src/java/org/apache/poi/hssf/record/aggregates/WorksheetProtectionBlock.java +++ b/src/java/org/apache/poi/hssf/record/aggregates/WorksheetProtectionBlock.java @@ -24,6 +24,7 @@ import org.apache.poi.hssf.record.ProtectRecord; import org.apache.poi.hssf.record.Record; import org.apache.poi.hssf.record.RecordFormatException; import org.apache.poi.hssf.record.ScenarioProtectRecord; +import org.apache.poi.poifs.crypt.CryptoFunctions; /** * Groups the sheet protection records for a worksheet. @@ -186,7 +187,7 @@ public final class WorksheetProtectionBlock extends RecordAggregate { ProtectRecord prec = getProtect(); PasswordRecord pass = getPassword(); prec.setProtect(true); - pass.setPassword(PasswordRecord.hashPassword(password)); + pass.setPassword((short)CryptoFunctions.createXorVerifier1(password)); if (_objectProtectRecord == null && shouldProtectObjects) { ObjectProtectRecord rec = createObjectProtect(); rec.setProtect(true); diff --git a/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java b/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java index f9f970ade9..ffb7498f3f 100644 --- a/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java +++ b/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java @@ -180,14 +180,20 @@ public class CryptoFunctions { } /** - * + * Initialize a new cipher object with the given cipher properties + * If the given algorithm is not implemented in the JCE, it will try to load it from the bouncy castle + * provider. * - * @param key - * @param chain - * @param vec + * @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 + * @param padding * @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, String padding) { int keySizeInBytes = key.getEncoded().length; @@ -226,10 +232,26 @@ public class CryptoFunctions { } } + /** + * Returns a new byte array with a truncated to the given size. + * If the hash has less then size bytes, it will be filled with 0x36-bytes + * + * @param hash the to be truncated/filled hash byte array + * @param size the size of the returned byte array + * @return the padded hash + */ public static byte[] getBlock36(byte[] hash, int size) { return getBlockX(hash, size, (byte)0x36); } + /** + * Returns a new byte array with a truncated to the given size. + * If the hash has less then size bytes, it will be filled with 0-bytes + * + * @param hash the to be truncated/filled hash byte array + * @param size the size of the returned byte array + * @return the padded hash + */ public static byte[] getBlock0(byte[] hash, int size) { return getBlockX(hash, size, (byte)0); } @@ -331,11 +353,11 @@ public class CryptoFunctions { byte[] generatedKey = new byte[4]; //Maximum length of the password is 15 chars. - final int intMaxPasswordLength = 15; + final int maxPasswordLength = 15; if (!"".equals(password)) { // Truncate the password to 15 characters - password = password.substring(0, Math.min(password.length(), intMaxPasswordLength)); + 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. @@ -359,7 +381,7 @@ public class CryptoFunctions { // 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; + int tmp = maxPasswordLength - arrByteChars.length + i; for (int intBit = 0; intBit < 7; intBit++) { if ((arrByteChars[i] & (0x0001 << intBit)) != 0) { highOrderWord ^= EncryptionMatrix[tmp][intBit]; @@ -369,22 +391,28 @@ public class CryptoFunctions { // Compute the low-order word of the new key: - // Initialize with 0 - int lowOrderWord = 0; + // SET Verifier TO 0x0000 + short verifier = 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]; + // 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]; } - // 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; + // 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') // 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, 0, verifier); LittleEndian.putShort(generatedKey, 2, (short)highOrderWord); } @@ -421,7 +449,7 @@ public class CryptoFunctions { * @see 2.3.7.4 Binary Document Password Verifier Derivation Method 2 * * @param password the password - * @return the verifier + * @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 @@ -480,4 +508,25 @@ public class CryptoFunctions { private static byte rotateLeft(byte bits, int shift) { return (byte)(((bits & 0xff) << shift) | ((bits & 0xff) >>> (8 - shift))); } + + private static short rotateLeftBase15Bit(short verifier) { + /* + * IF (Verifier BITWISE AND 0x4000) is 0x0000 + * SET Intermediate1 TO 0 + * ELSE + * SET Intermediate1 TO 1 + * ENDIF + */ + short intermediate1 = (short)(((verifier & 0x4000) == 0) ? 0 : 1); + /* + * SET Intermediate2 TO Verifier MULTIPLED BY 2 + * SET most significant bit of Intermediate2 TO 0 + */ + short intermediate2 = (short)((verifier<<1) & 0x7FFF); + /* + * SET Intermediate3 TO Intermediate1 BITWISE OR Intermediate2 + */ + short intermediate3 = (short)(intermediate1 | intermediate2); + return intermediate3; + } } diff --git a/src/java/org/apache/poi/poifs/crypt/HashAlgorithm.java b/src/java/org/apache/poi/poifs/crypt/HashAlgorithm.java index 51217184ba..61608822f8 100644 --- a/src/java/org/apache/poi/poifs/crypt/HashAlgorithm.java +++ b/src/java/org/apache/poi/poifs/crypt/HashAlgorithm.java @@ -64,4 +64,11 @@ public enum HashAlgorithm { } throw new EncryptedDocumentException("hash algorithm not found"); } + + public static HashAlgorithm fromString(String string) { + for (HashAlgorithm ha : values()) { + if (ha.ecmaString.equalsIgnoreCase(string) || ha.jceId.equalsIgnoreCase(string)) return ha; + } + throw new EncryptedDocumentException("hash algorithm not found"); + } } \ No newline at end of file diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFSheet.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFSheet.java index 60ecf7bb9f..9da8a6955a 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFSheet.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFSheet.java @@ -17,6 +17,9 @@ package org.apache.poi.xssf.usermodel; +import static org.apache.poi.xssf.usermodel.helpers.XSSFPaswordHelper.setPassword; +import static org.apache.poi.xssf.usermodel.helpers.XSSFPaswordHelper.validatePassword; + import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -33,7 +36,6 @@ import javax.xml.namespace.QName; import org.apache.poi.POIXMLDocumentPart; import org.apache.poi.POIXMLException; -import org.apache.poi.hssf.record.PasswordRecord; import org.apache.poi.hssf.util.PaneInformation; import org.apache.poi.openxml4j.exceptions.InvalidFormatException; import org.apache.poi.openxml4j.exceptions.PartAlreadyExistsException; @@ -41,6 +43,7 @@ import org.apache.poi.openxml4j.opc.PackagePart; import org.apache.poi.openxml4j.opc.PackageRelationship; import org.apache.poi.openxml4j.opc.PackageRelationshipCollection; import org.apache.poi.openxml4j.opc.TargetMode; +import org.apache.poi.poifs.crypt.HashAlgorithm; import org.apache.poi.ss.SpreadsheetVersion; import org.apache.poi.ss.formula.FormulaShifter; import org.apache.poi.ss.formula.SheetNameFormatter; @@ -52,7 +55,6 @@ import org.apache.poi.ss.util.CellReference; import org.apache.poi.ss.util.SSCellRange; import org.apache.poi.ss.util.SheetUtil; import org.apache.poi.util.Beta; -import org.apache.poi.util.HexDump; import org.apache.poi.util.Internal; import org.apache.poi.util.POILogFactory; import org.apache.poi.util.POILogger; @@ -1056,7 +1058,7 @@ public class XSSFSheet extends POIXMLDocumentPart implements Sheet { */ @Override public boolean getProtect() { - return worksheet.isSetSheetProtection() && sheetProtectionEnabled(); + return isSheetLocked(); } /** @@ -1068,10 +1070,9 @@ public class XSSFSheet extends POIXMLDocumentPart implements Sheet { */ @Override public void protectSheet(String password) { - - if(password != null) { - CTSheetProtection sheetProtection = worksheet.addNewSheetProtection(); - sheetProtection.xsetPassword(stringToExcelPassword(password)); + if (password != null) { + CTSheetProtection sheetProtection = safeGetProtectionField(); + setSheetPassword(password, null); // defaults to xor password sheetProtection.setSheet(true); sheetProtection.setScenarios(true); sheetProtection.setObjects(true); @@ -1081,18 +1082,27 @@ public class XSSFSheet extends POIXMLDocumentPart implements Sheet { } /** - * Converts a String to a {@link STUnsignedShortHex} value that contains the {@link PasswordRecord#hashPassword(String)} - * value in hexadecimal format - * - * @param password the password string you wish convert to an {@link STUnsignedShortHex} - * @return {@link STUnsignedShortHex} that contains Excel hashed password in Hex format + * Sets the sheet password. + * + * @param password if null, the password will be removed + * @param hashAlgo if null, the password will be set as XOR password (Excel 2010 and earlier) + * otherwise the given algorithm is used for calculating the hash password (Excel 2013) */ - private STUnsignedShortHex stringToExcelPassword(String password) { - STUnsignedShortHex hexPassword = STUnsignedShortHex.Factory.newInstance(); - hexPassword.setStringValue(String.valueOf(HexDump.shortToHex(PasswordRecord.hashPassword(password))).substring(2)); - return hexPassword; + public void setSheetPassword(String password, HashAlgorithm hashAlgo) { + if (password == null && !isSheetProtectionEnabled()) return; + setPassword(safeGetProtectionField(), password, hashAlgo, null); } + /** + * Validate the password against the stored hash, the hashing method will be determined + * by the existing password attributes + * @return true, if the hashes match (... though original password may differ ...) + */ + public boolean validateSheetPassword(String password) { + if (!isSheetProtectionEnabled()) return (password == null); + return validatePassword(safeGetProtectionField(), password, null); + } + /** * Returns the logical row ( 0-based). If you ask for a row that is not * defined you get a null. This is to say row 4 represents the fifth row on a sheet. @@ -1546,7 +1556,7 @@ public class XSSFSheet extends POIXMLDocumentPart implements Sheet { worksheet.unsetMergeCells(); } } - + /** * Removes a number of merged regions of cells (hence letting them free) * @@ -2910,304 +2920,440 @@ public class XSSFSheet extends POIXMLDocumentPart implements Sheet { * @return true when Autofilters are locked and the sheet is protected. */ public boolean isAutoFilterLocked() { - createProtectionFieldIfNotPresent(); - return sheetProtectionEnabled() && worksheet.getSheetProtection().getAutoFilter(); + if (isSheetLocked()) { + return safeGetProtectionField().getAutoFilter(); + } + return false; } /** * @return true when Deleting columns is locked and the sheet is protected. */ public boolean isDeleteColumnsLocked() { - createProtectionFieldIfNotPresent(); - return sheetProtectionEnabled() && worksheet.getSheetProtection().getDeleteColumns(); + if (isSheetLocked()) { + return safeGetProtectionField().getDeleteColumns(); + } + return false; } /** * @return true when Deleting rows is locked and the sheet is protected. */ public boolean isDeleteRowsLocked() { - createProtectionFieldIfNotPresent(); - return sheetProtectionEnabled() && worksheet.getSheetProtection().getDeleteRows(); + if (isSheetLocked()) { + return safeGetProtectionField().getDeleteRows(); + } + return false; } /** * @return true when Formatting cells is locked and the sheet is protected. */ public boolean isFormatCellsLocked() { - createProtectionFieldIfNotPresent(); - return sheetProtectionEnabled() && worksheet.getSheetProtection().getFormatCells(); + if (isSheetLocked()) { + return safeGetProtectionField().getFormatCells(); + } + return false; } /** * @return true when Formatting columns is locked and the sheet is protected. */ public boolean isFormatColumnsLocked() { - createProtectionFieldIfNotPresent(); - return sheetProtectionEnabled() && worksheet.getSheetProtection().getFormatColumns(); + if (isSheetLocked()) { + return safeGetProtectionField().getFormatColumns(); + } + return false; } /** * @return true when Formatting rows is locked and the sheet is protected. */ public boolean isFormatRowsLocked() { - createProtectionFieldIfNotPresent(); - return sheetProtectionEnabled() && worksheet.getSheetProtection().getFormatRows(); + if (isSheetLocked()) { + return safeGetProtectionField().getFormatRows(); + } + return false; } /** * @return true when Inserting columns is locked and the sheet is protected. */ public boolean isInsertColumnsLocked() { - createProtectionFieldIfNotPresent(); - return sheetProtectionEnabled() && worksheet.getSheetProtection().getInsertColumns(); + if (isSheetLocked()) { + return safeGetProtectionField().getInsertColumns(); + } + return false; } /** * @return true when Inserting hyperlinks is locked and the sheet is protected. */ public boolean isInsertHyperlinksLocked() { - createProtectionFieldIfNotPresent(); - return sheetProtectionEnabled() && worksheet.getSheetProtection().getInsertHyperlinks(); + if (isSheetLocked()) { + return safeGetProtectionField().getInsertHyperlinks(); + } + return false; } /** * @return true when Inserting rows is locked and the sheet is protected. */ public boolean isInsertRowsLocked() { - createProtectionFieldIfNotPresent(); - return sheetProtectionEnabled() && worksheet.getSheetProtection().getInsertRows(); + if (isSheetLocked()) { + return safeGetProtectionField().getInsertRows(); + } + return false; } /** * @return true when Pivot tables are locked and the sheet is protected. */ public boolean isPivotTablesLocked() { - createProtectionFieldIfNotPresent(); - return sheetProtectionEnabled() && worksheet.getSheetProtection().getPivotTables(); + if (isSheetLocked()) { + return safeGetProtectionField().getPivotTables(); + } + return false; } /** * @return true when Sorting is locked and the sheet is protected. */ public boolean isSortLocked() { - createProtectionFieldIfNotPresent(); - return sheetProtectionEnabled() && worksheet.getSheetProtection().getSort(); + if (isSheetLocked()) { + return safeGetProtectionField().getSort(); + } + return false; } /** * @return true when Objects are locked and the sheet is protected. */ public boolean isObjectsLocked() { - createProtectionFieldIfNotPresent(); - return sheetProtectionEnabled() && worksheet.getSheetProtection().getObjects(); + if (isSheetLocked()) { + return safeGetProtectionField().getObjects(); + } + return false; } /** * @return true when Scenarios are locked and the sheet is protected. */ public boolean isScenariosLocked() { - createProtectionFieldIfNotPresent(); - return sheetProtectionEnabled() && worksheet.getSheetProtection().getScenarios(); + if (isSheetLocked()) { + return safeGetProtectionField().getScenarios(); + } + return false; } /** * @return true when Selection of locked cells is locked and the sheet is protected. */ public boolean isSelectLockedCellsLocked() { - createProtectionFieldIfNotPresent(); - return sheetProtectionEnabled() && worksheet.getSheetProtection().getSelectLockedCells(); + if (isSheetLocked()) { + return safeGetProtectionField().getSelectLockedCells(); + } + return false; } /** * @return true when Selection of unlocked cells is locked and the sheet is protected. */ public boolean isSelectUnlockedCellsLocked() { - createProtectionFieldIfNotPresent(); - return sheetProtectionEnabled() && worksheet.getSheetProtection().getSelectUnlockedCells(); + if (isSheetLocked()) { + return safeGetProtectionField().getSelectUnlockedCells(); + } + return false; } /** * @return true when Sheet is Protected. */ public boolean isSheetLocked() { - createProtectionFieldIfNotPresent(); - return sheetProtectionEnabled() && worksheet.getSheetProtection().getSheet(); + if (worksheet.isSetSheetProtection()) { + return safeGetProtectionField().getSheet(); + } + return false; } /** * Enable sheet protection */ public void enableLocking() { - createProtectionFieldIfNotPresent(); - worksheet.getSheetProtection().setSheet(true); + safeGetProtectionField().setSheet(true); } /** * Disable sheet protection */ public void disableLocking() { - createProtectionFieldIfNotPresent(); - worksheet.getSheetProtection().setSheet(false); + safeGetProtectionField().setSheet(false); } /** * Enable Autofilters locking. - * This does not modify sheet protection status. - * To enforce this locking, call {@link #enableLocking()} + * @deprecated use {@link #lockAutoFilter(boolean)} */ public void lockAutoFilter() { - createProtectionFieldIfNotPresent(); - worksheet.getSheetProtection().setAutoFilter(true); + lockAutoFilter(true); } /** - * Enable Deleting columns locking. + * Enable or disable Autofilters locking. * This does not modify sheet protection status. - * To enforce this locking, call {@link #enableLocking()} + * To enforce this un-/locking, call {@link #disableLocking()} or {@link #enableLocking()} + */ + public void lockAutoFilter(boolean enabled) { + safeGetProtectionField().setAutoFilter(enabled); + } + + /** + * Enable Deleting columns locking. + * @deprecated use {@link #lockDeleteColumns(boolean)} */ public void lockDeleteColumns() { - createProtectionFieldIfNotPresent(); - worksheet.getSheetProtection().setDeleteColumns(true); + lockDeleteColumns(true); } /** - * Enable Deleting rows locking. + * Enable or disable Deleting columns locking. * This does not modify sheet protection status. - * To enforce this locking, call {@link #enableLocking()} + * To enforce this un-/locking, call {@link #disableLocking()} or {@link #enableLocking()} + */ + public void lockDeleteColumns(boolean enabled) { + safeGetProtectionField().setDeleteColumns(enabled); + } + + /** + * Enable Deleting rows locking. + * @deprecated use {@link #lockDeleteRows(boolean)} */ public void lockDeleteRows() { - createProtectionFieldIfNotPresent(); - worksheet.getSheetProtection().setDeleteRows(true); + lockDeleteRows(true); } /** - * Enable Formatting cells locking. + * Enable or disable Deleting rows locking. * This does not modify sheet protection status. - * To enforce this locking, call {@link #enableLocking()} + * To enforce this un-/locking, call {@link #disableLocking()} or {@link #enableLocking()} + */ + public void lockDeleteRows(boolean enabled) { + safeGetProtectionField().setDeleteRows(enabled); + } + + /** + * Enable Formatting cells locking. + * @deprecated use {@link #lockFormatCells(boolean)} */ public void lockFormatCells() { - createProtectionFieldIfNotPresent(); - worksheet.getSheetProtection().setDeleteColumns(true); + lockFormatCells(true); } /** - * Enable Formatting columns locking. + * Enable or disable Formatting cells locking. * This does not modify sheet protection status. - * To enforce this locking, call {@link #enableLocking()} + * To enforce this un-/locking, call {@link #disableLocking()} or {@link #enableLocking()} + */ + public void lockFormatCells(boolean enabled) { + safeGetProtectionField().setFormatCells(enabled); + } + + /** + * Enable Formatting columns locking. + * @deprecated use {@link #lockFormatColumns(boolean)} */ public void lockFormatColumns() { - createProtectionFieldIfNotPresent(); - worksheet.getSheetProtection().setFormatColumns(true); + lockFormatColumns(true); } /** - * Enable Formatting rows locking. + * Enable or disable Formatting columns locking. * This does not modify sheet protection status. - * To enforce this locking, call {@link #enableLocking()} + * To enforce this un-/locking, call {@link #disableLocking()} or {@link #enableLocking()} + */ + public void lockFormatColumns(boolean enabled) { + safeGetProtectionField().setFormatColumns(enabled); + } + + /** + * Enable Formatting rows locking. + * @deprecated use {@link #lockFormatRows(boolean)} */ public void lockFormatRows() { - createProtectionFieldIfNotPresent(); - worksheet.getSheetProtection().setFormatRows(true); + lockFormatRows(true); } /** - * Enable Inserting columns locking. + * Enable or disable Formatting rows locking. * This does not modify sheet protection status. - * To enforce this locking, call {@link #enableLocking()} + * To enforce this un-/locking, call {@link #disableLocking()} or {@link #enableLocking()} + */ + public void lockFormatRows(boolean enabled) { + safeGetProtectionField().setFormatRows(enabled); + } + + /** + * Enable Inserting columns locking. + * @deprecated use {@link #lockInsertColumns(boolean)} */ public void lockInsertColumns() { - createProtectionFieldIfNotPresent(); - worksheet.getSheetProtection().setInsertColumns(true); + lockInsertColumns(true); } /** - * Enable Inserting hyperlinks locking. + * Enable or disable Inserting columns locking. * This does not modify sheet protection status. - * To enforce this locking, call {@link #enableLocking()} + * To enforce this un-/locking, call {@link #disableLocking()} or {@link #enableLocking()} + */ + public void lockInsertColumns(boolean enabled) { + safeGetProtectionField().setInsertColumns(enabled); + } + + /** + * Enable Inserting hyperlinks locking. + * @deprecated use {@link #lockInsertHyperlinks(boolean)} */ public void lockInsertHyperlinks() { - createProtectionFieldIfNotPresent(); - worksheet.getSheetProtection().setInsertHyperlinks(true); + lockInsertHyperlinks(true); } /** - * Enable Inserting rows locking. + * Enable or disable Inserting hyperlinks locking. * This does not modify sheet protection status. - * To enforce this locking, call {@link #enableLocking()} + * To enforce this un-/locking, call {@link #disableLocking()} or {@link #enableLocking()} + */ + public void lockInsertHyperlinks(boolean enabled) { + safeGetProtectionField().setInsertHyperlinks(enabled); + } + + /** + * Enable Inserting rows locking. + * @deprecated use {@link #lockInsertRows(boolean)} */ public void lockInsertRows() { - createProtectionFieldIfNotPresent(); - worksheet.getSheetProtection().setInsertRows(true); + lockInsertRows(true); } /** - * Enable Pivot Tables locking. + * Enable or disable Inserting rows locking. * This does not modify sheet protection status. - * To enforce this locking, call {@link #enableLocking()} + * To enforce this un-/locking, call {@link #disableLocking()} or {@link #enableLocking()} + */ + public void lockInsertRows(boolean enabled) { + safeGetProtectionField().setInsertRows(enabled); + } + + /** + * Enable Pivot Tables locking. + * @deprecated use {@link #lockPivotTables(boolean)} */ public void lockPivotTables() { - createProtectionFieldIfNotPresent(); - worksheet.getSheetProtection().setPivotTables(true); + lockPivotTables(true); } /** - * Enable Sort locking. + * Enable or disable Pivot Tables locking. * This does not modify sheet protection status. - * To enforce this locking, call {@link #enableLocking()} + * To enforce this un-/locking, call {@link #disableLocking()} or {@link #enableLocking()} + */ + public void lockPivotTables(boolean enabled) { + safeGetProtectionField().setPivotTables(enabled); + } + + /** + * Enable Sort locking. + * @deprecated use {@link #lockSort(boolean)} */ public void lockSort() { - createProtectionFieldIfNotPresent(); - worksheet.getSheetProtection().setSort(true); + lockSort(true); } /** - * Enable Objects locking. + * Enable or disable Sort locking. * This does not modify sheet protection status. - * To enforce this locking, call {@link #enableLocking()} + * To enforce this un-/locking, call {@link #disableLocking()} or {@link #enableLocking()} + */ + public void lockSort(boolean enabled) { + safeGetProtectionField().setSort(enabled); + } + + /** + * Enable Objects locking. + * @deprecated use {@link #lockObjects(boolean)} */ public void lockObjects() { - createProtectionFieldIfNotPresent(); - worksheet.getSheetProtection().setObjects(true); + lockObjects(true); } /** - * Enable Scenarios locking. + * Enable or disable Objects locking. * This does not modify sheet protection status. - * To enforce this locking, call {@link #enableLocking()} + * To enforce this un-/locking, call {@link #disableLocking()} or {@link #enableLocking()} + */ + public void lockObjects(boolean enabled) { + safeGetProtectionField().setObjects(enabled); + } + + /** + * Enable Scenarios locking. + * @deprecated use {@link #lockScenarios(boolean)} */ public void lockScenarios() { - createProtectionFieldIfNotPresent(); - worksheet.getSheetProtection().setScenarios(true); + lockScenarios(true); } /** - * Enable Selection of locked cells locking. + * Enable or disable Scenarios locking. * This does not modify sheet protection status. - * To enforce this locking, call {@link #enableLocking()} + * To enforce this un-/locking, call {@link #disableLocking()} or {@link #enableLocking()} + */ + public void lockScenarios(boolean enabled) { + safeGetProtectionField().setScenarios(enabled); + } + + /** + * Enable Selection of locked cells locking. + * @deprecated use {@link #lockSelectLockedCells(boolean)} */ public void lockSelectLockedCells() { - createProtectionFieldIfNotPresent(); - worksheet.getSheetProtection().setSelectLockedCells(true); + lockSelectLockedCells(true); } /** - * Enable Selection of unlocked cells locking. + * Enable or disable Selection of locked cells locking. * This does not modify sheet protection status. - * To enforce this locking, call {@link #enableLocking()} + * To enforce this un-/locking, call {@link #disableLocking()} or {@link #enableLocking()} + */ + public void lockSelectLockedCells(boolean enabled) { + safeGetProtectionField().setSelectLockedCells(enabled); + } + + /** + * Enable Selection of unlocked cells locking. + * @deprecated use {@link #lockSelectUnlockedCells(boolean)} */ public void lockSelectUnlockedCells() { - createProtectionFieldIfNotPresent(); - worksheet.getSheetProtection().setSelectUnlockedCells(true); + lockSelectUnlockedCells(true); + } + + /** + * Enable or disable Selection of unlocked cells locking. + * This does not modify sheet protection status. + * To enforce this un-/locking, call {@link #disableLocking()} or {@link #enableLocking()} + */ + public void lockSelectUnlockedCells(boolean enabled) { + safeGetProtectionField().setSelectUnlockedCells(enabled); } - private void createProtectionFieldIfNotPresent() { - if (worksheet.getSheetProtection() == null) { - worksheet.setSheetProtection(CTSheetProtection.Factory.newInstance()); + private CTSheetProtection safeGetProtectionField() { + if (!isSheetProtectionEnabled()) { + return worksheet.addNewSheetProtection(); } + return worksheet.getSheetProtection(); } - private boolean sheetProtectionEnabled() { - return worksheet.getSheetProtection().getSheet(); + /* package */ boolean isSheetProtectionEnabled() { + return (worksheet.isSetSheetProtection()); } /* package */ boolean isCellInArrayFormulaContext(XSSFCell cell) { diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFWorkbook.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFWorkbook.java index f5dc36b336..8beb39aa98 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFWorkbook.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFWorkbook.java @@ -17,6 +17,9 @@ package org.apache.poi.xssf.usermodel; +import static org.apache.poi.xssf.usermodel.helpers.XSSFPaswordHelper.setPassword; +import static org.apache.poi.xssf.usermodel.helpers.XSSFPaswordHelper.validatePassword; + import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; @@ -47,6 +50,7 @@ import org.apache.poi.openxml4j.opc.PackageRelationship; import org.apache.poi.openxml4j.opc.PackageRelationshipTypes; import org.apache.poi.openxml4j.opc.PackagingURIHelper; import org.apache.poi.openxml4j.opc.TargetMode; +import org.apache.poi.poifs.crypt.HashAlgorithm; import org.apache.poi.ss.formula.SheetNameFormatter; import org.apache.poi.ss.formula.udf.IndexedUDFFinder; import org.apache.poi.ss.formula.udf.UDFFinder; @@ -1736,60 +1740,108 @@ public class XSSFWorkbook extends POIXMLDocument implements Workbook, Iterable 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(password, hashAlgo, salt, spinCount, false); + + cur.insertAttributeWithValue(getAttrName(prefix, "algorithmName"), hashAlgo.jceId); + cur.insertAttributeWithValue(getAttrName(prefix, "hashValue"), DatatypeConverter.printBase64Binary(hash)); + cur.insertAttributeWithValue(getAttrName(prefix, "saltValue"), DatatypeConverter.printBase64Binary(salt)); + cur.insertAttributeWithValue(getAttrName(prefix, "spinCount"), ""+spinCount); + } + cur.dispose(); + } + + /** + * Validates the password, i.e. + * calculates the hash of the given password and compares it against the stored hash + * + * @param xobj the xmlbeans object which contains the password attributes + * @param password the password, if null the method will always return false, + * even if there's no password set + * @param prefix the prefix of the password attributes, may be null + * + * @return true, if the hashes match + */ + public static boolean validatePassword(XmlObject xobj, String password, String prefix) { + // TODO: is "velvetSweatshop" the default password? + if (password == null) return false; + + XmlCursor cur = xobj.newCursor(); + String xorHashVal = cur.getAttributeText(getAttrName(prefix, "password")); + String algoName = cur.getAttributeText(getAttrName(prefix, "algorithmName")); + String hashVal = cur.getAttributeText(getAttrName(prefix, "hashValue")); + String saltVal = cur.getAttributeText(getAttrName(prefix, "saltValue")); + String spinCount = cur.getAttributeText(getAttrName(prefix, "spinCount")); + cur.dispose(); + + if (xorHashVal != null) { + int hash1 = Integer.parseInt(xorHashVal, 16); + int hash2 = CryptoFunctions.createXorVerifier1(password); + return hash1 == hash2; + } else { + if (hashVal == null || algoName == null || saltVal == null || spinCount == null) { + return false; + } + + byte hash1[] = DatatypeConverter.parseBase64Binary(hashVal); + HashAlgorithm hashAlgo = HashAlgorithm.fromString(algoName); + byte salt[] = DatatypeConverter.parseBase64Binary(saltVal); + int spinCnt = Integer.parseInt(spinCount); + byte hash2[] = CryptoFunctions.hashPassword(password, hashAlgo, salt, spinCnt, false); + return Arrays.equals(hash1, hash2); + } + } + + + private static QName getAttrName(String prefix, String name) { + if (prefix == null || "".equals(prefix)) { + return new QName(name); + } else { + return new QName(prefix+Character.toUpperCase(name.charAt(0))+name.substring(1)); + } + } +} diff --git a/src/ooxml/testcases/org/apache/poi/xssf/TestSheetProtection.java b/src/ooxml/testcases/org/apache/poi/xssf/TestSheetProtection.java index 156c0dec49..f90804fa4c 100644 --- a/src/ooxml/testcases/org/apache/poi/xssf/TestSheetProtection.java +++ b/src/ooxml/testcases/org/apache/poi/xssf/TestSheetProtection.java @@ -16,12 +16,11 @@ ==================================================================== */ package org.apache.poi.xssf; -import org.apache.poi.xssf.usermodel.XSSFSheet; +import junit.framework.TestCase; +import org.apache.poi.xssf.usermodel.XSSFSheet; import org.apache.poi.xssf.usermodel.XSSFWorkbook; -import junit.framework.TestCase; - public class TestSheetProtection extends TestCase { private XSSFSheet sheet; @@ -75,6 +74,8 @@ public class TestSheetProtection extends TestCase { assertFalse(sheet.isAutoFilterLocked()); sheet.enableLocking(); assertTrue(sheet.isAutoFilterLocked()); + sheet.lockAutoFilter(false); + assertFalse(sheet.isAutoFilterLocked()); } public void testWriteDeleteColumns() throws Exception { @@ -83,6 +84,8 @@ public class TestSheetProtection extends TestCase { assertFalse(sheet.isDeleteColumnsLocked()); sheet.enableLocking(); assertTrue(sheet.isDeleteColumnsLocked()); + sheet.lockDeleteColumns(false); + assertFalse(sheet.isDeleteColumnsLocked()); } public void testWriteDeleteRows() throws Exception { @@ -91,6 +94,8 @@ public class TestSheetProtection extends TestCase { assertFalse(sheet.isDeleteRowsLocked()); sheet.enableLocking(); assertTrue(sheet.isDeleteRowsLocked()); + sheet.lockDeleteRows(false); + assertFalse(sheet.isDeleteRowsLocked()); } public void testWriteFormatCells() throws Exception { @@ -99,6 +104,8 @@ public class TestSheetProtection extends TestCase { assertFalse(sheet.isFormatCellsLocked()); sheet.enableLocking(); assertTrue(sheet.isFormatCellsLocked()); + sheet.lockFormatCells(false); + assertFalse(sheet.isFormatCellsLocked()); } public void testWriteFormatColumns() throws Exception { @@ -107,6 +114,8 @@ public class TestSheetProtection extends TestCase { assertFalse(sheet.isFormatColumnsLocked()); sheet.enableLocking(); assertTrue(sheet.isFormatColumnsLocked()); + sheet.lockFormatColumns(false); + assertFalse(sheet.isFormatColumnsLocked()); } public void testWriteFormatRows() throws Exception { @@ -115,6 +124,8 @@ public class TestSheetProtection extends TestCase { assertFalse(sheet.isFormatRowsLocked()); sheet.enableLocking(); assertTrue(sheet.isFormatRowsLocked()); + sheet.lockFormatRows(false); + assertFalse(sheet.isFormatRowsLocked()); } public void testWriteInsertColumns() throws Exception { @@ -123,6 +134,8 @@ public class TestSheetProtection extends TestCase { assertFalse(sheet.isInsertColumnsLocked()); sheet.enableLocking(); assertTrue(sheet.isInsertColumnsLocked()); + sheet.lockInsertColumns(false); + assertFalse(sheet.isInsertColumnsLocked()); } public void testWriteInsertHyperlinks() throws Exception { @@ -131,6 +144,8 @@ public class TestSheetProtection extends TestCase { assertFalse(sheet.isInsertHyperlinksLocked()); sheet.enableLocking(); assertTrue(sheet.isInsertHyperlinksLocked()); + sheet.lockInsertHyperlinks(false); + assertFalse(sheet.isInsertHyperlinksLocked()); } public void testWriteInsertRows() throws Exception { @@ -139,6 +154,8 @@ public class TestSheetProtection extends TestCase { assertFalse(sheet.isInsertRowsLocked()); sheet.enableLocking(); assertTrue(sheet.isInsertRowsLocked()); + sheet.lockInsertRows(false); + assertFalse(sheet.isInsertRowsLocked()); } public void testWritePivotTables() throws Exception { @@ -147,6 +164,8 @@ public class TestSheetProtection extends TestCase { assertFalse(sheet.isPivotTablesLocked()); sheet.enableLocking(); assertTrue(sheet.isPivotTablesLocked()); + sheet.lockPivotTables(false); + assertFalse(sheet.isPivotTablesLocked()); } public void testWriteSort() throws Exception { @@ -155,6 +174,8 @@ public class TestSheetProtection extends TestCase { assertFalse(sheet.isSortLocked()); sheet.enableLocking(); assertTrue(sheet.isSortLocked()); + sheet.lockSort(false); + assertFalse(sheet.isSortLocked()); } public void testWriteObjects() throws Exception { @@ -163,6 +184,8 @@ public class TestSheetProtection extends TestCase { assertFalse(sheet.isObjectsLocked()); sheet.enableLocking(); assertTrue(sheet.isObjectsLocked()); + sheet.lockObjects(false); + assertFalse(sheet.isObjectsLocked()); } public void testWriteScenarios() throws Exception { @@ -171,6 +194,8 @@ public class TestSheetProtection extends TestCase { assertFalse(sheet.isScenariosLocked()); sheet.enableLocking(); assertTrue(sheet.isScenariosLocked()); + sheet.lockScenarios(false); + assertFalse(sheet.isScenariosLocked()); } public void testWriteSelectLockedCells() throws Exception { @@ -179,6 +204,8 @@ public class TestSheetProtection extends TestCase { assertFalse(sheet.isSelectLockedCellsLocked()); sheet.enableLocking(); assertTrue(sheet.isSelectLockedCellsLocked()); + sheet.lockSelectLockedCells(false); + assertFalse(sheet.isSelectLockedCellsLocked()); } public void testWriteSelectUnlockedCells() throws Exception { @@ -187,6 +214,8 @@ public class TestSheetProtection extends TestCase { assertFalse(sheet.isSelectUnlockedCellsLocked()); sheet.enableLocking(); assertTrue(sheet.isSelectUnlockedCellsLocked()); + sheet.lockSelectUnlockedCells(false); + assertFalse(sheet.isSelectUnlockedCellsLocked()); } public void testWriteSelectEnableLocking() throws Exception { diff --git a/src/ooxml/testcases/org/apache/poi/xssf/TestWorkbookProtection.java b/src/ooxml/testcases/org/apache/poi/xssf/TestWorkbookProtection.java index 642fe30748..969061932b 100644 --- a/src/ooxml/testcases/org/apache/poi/xssf/TestWorkbookProtection.java +++ b/src/ooxml/testcases/org/apache/poi/xssf/TestWorkbookProtection.java @@ -16,42 +16,94 @@ ==================================================================== */ package org.apache.poi.xssf; -import java.io.File; - -import java.io.FileInputStream; -import java.io.FileOutputStream; - -import junit.framework.TestCase; - -import org.apache.poi.util.TempFile; +import static org.apache.poi.xssf.XSSFTestDataSamples.openSampleWorkbook; +import static org.apache.poi.xssf.XSSFTestDataSamples.writeOutAndReadBack; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.apache.poi.poifs.crypt.CryptoFunctions; +import org.apache.poi.poifs.crypt.HashAlgorithm; import org.apache.poi.xssf.usermodel.XSSFWorkbook; - -public class TestWorkbookProtection extends TestCase { - - public void testShouldReadWorkbookProtection() throws Exception { - XSSFWorkbook workbook = XSSFTestDataSamples.openSampleWorkbook("workbookProtection_not_protected.xlsx"); +import org.junit.Test; + +public class TestWorkbookProtection { + + @Test + public void workbookAndRevisionPassword() throws Exception { + XSSFWorkbook workbook; + String password = "test"; + + // validate password with an actual office file (Excel 2010) + workbook = openSampleWorkbook("workbookProtection-workbook_password_user_range-2010.xlsx"); + assertTrue(workbook.validateWorkbookPassword(password)); + + // validate with another office file (Excel 2013) + workbook = openSampleWorkbook("workbookProtection-workbook_password-2013.xlsx"); + assertTrue(workbook.validateWorkbookPassword(password)); + + + workbook = openSampleWorkbook("workbookProtection_not_protected.xlsx"); + + // setting a null password shouldn't introduce the protection element + workbook.setWorkbookPassword(null, null); + assertNull(workbook.getCTWorkbook().getWorkbookProtection()); + + // compare the hashes + workbook.setWorkbookPassword(password, null); + int hashVal = CryptoFunctions.createXorVerifier1(password); + int actualVal = Integer.parseInt(workbook.getCTWorkbook().getWorkbookProtection().xgetWorkbookPassword().getStringValue(),16); + assertEquals(hashVal, actualVal); + assertTrue(workbook.validateWorkbookPassword(password)); + + // removing the password again + workbook.setWorkbookPassword(null, null); + assertFalse(workbook.getCTWorkbook().getWorkbookProtection().isSetWorkbookPassword()); + + // removing the whole protection structure + workbook.unLock(); + assertNull(workbook.getCTWorkbook().getWorkbookProtection()); + + // setting a null password shouldn't introduce the protection element + workbook.setRevisionsPassword(null, null); + assertNull(workbook.getCTWorkbook().getWorkbookProtection()); + + // compare the hashes + password = "T\u0400ST\u0100passwordWhichIsLongerThan15Chars"; + workbook.setRevisionsPassword(password, null); + hashVal = CryptoFunctions.createXorVerifier1(password); + actualVal = Integer.parseInt(workbook.getCTWorkbook().getWorkbookProtection().xgetRevisionsPassword().getStringValue(),16); + assertEquals(hashVal, actualVal); + assertTrue(workbook.validateRevisionsPassword(password)); + } + + @Test + public void shouldReadWorkbookProtection() throws Exception { + XSSFWorkbook workbook = openSampleWorkbook("workbookProtection_not_protected.xlsx"); assertFalse(workbook.isStructureLocked()); assertFalse(workbook.isWindowsLocked()); assertFalse(workbook.isRevisionLocked()); - workbook = XSSFTestDataSamples.openSampleWorkbook("workbookProtection_workbook_structure_protected.xlsx"); + workbook = openSampleWorkbook("workbookProtection_workbook_structure_protected.xlsx"); assertTrue(workbook.isStructureLocked()); assertFalse(workbook.isWindowsLocked()); assertFalse(workbook.isRevisionLocked()); - workbook = XSSFTestDataSamples.openSampleWorkbook("workbookProtection_workbook_windows_protected.xlsx"); + workbook = openSampleWorkbook("workbookProtection_workbook_windows_protected.xlsx"); assertTrue(workbook.isWindowsLocked()); assertFalse(workbook.isStructureLocked()); assertFalse(workbook.isRevisionLocked()); - workbook = XSSFTestDataSamples.openSampleWorkbook("workbookProtection_workbook_revision_protected.xlsx"); + workbook = openSampleWorkbook("workbookProtection_workbook_revision_protected.xlsx"); assertTrue(workbook.isRevisionLocked()); assertFalse(workbook.isWindowsLocked()); assertFalse(workbook.isStructureLocked()); } - public void testShouldWriteStructureLock() throws Exception { - XSSFWorkbook workbook = XSSFTestDataSamples.openSampleWorkbook("workbookProtection_not_protected.xlsx"); + @Test + public void shouldWriteStructureLock() throws Exception { + XSSFWorkbook workbook = openSampleWorkbook("workbookProtection_not_protected.xlsx"); assertFalse(workbook.isStructureLocked()); workbook.lockStructure(); @@ -63,8 +115,9 @@ public class TestWorkbookProtection extends TestCase { assertFalse(workbook.isStructureLocked()); } - public void testShouldWriteWindowsLock() throws Exception { - XSSFWorkbook workbook = XSSFTestDataSamples.openSampleWorkbook("workbookProtection_not_protected.xlsx"); + @Test + public void shouldWriteWindowsLock() throws Exception { + XSSFWorkbook workbook = openSampleWorkbook("workbookProtection_not_protected.xlsx"); assertFalse(workbook.isWindowsLocked()); workbook.lockWindows(); @@ -76,8 +129,9 @@ public class TestWorkbookProtection extends TestCase { assertFalse(workbook.isWindowsLocked()); } - public void testShouldWriteRevisionLock() throws Exception { - XSSFWorkbook workbook = XSSFTestDataSamples.openSampleWorkbook("workbookProtection_not_protected.xlsx"); + @Test + public void shouldWriteRevisionLock() throws Exception { + XSSFWorkbook workbook = openSampleWorkbook("workbookProtection_not_protected.xlsx"); assertFalse(workbook.isRevisionLocked()); workbook.lockRevision(); @@ -89,22 +143,32 @@ public class TestWorkbookProtection extends TestCase { assertFalse(workbook.isRevisionLocked()); } + @SuppressWarnings("resource") + @Test + public void testHashPassword() throws Exception { + XSSFWorkbook wb = new XSSFWorkbook(); + wb.lockRevision(); + wb.setRevisionsPassword("test", HashAlgorithm.sha1); + + wb = writeOutAndReadBack(wb); + + assertTrue(wb.isRevisionLocked()); + assertTrue(wb.validateRevisionsPassword("test")); + } + + @SuppressWarnings("resource") + @Test public void testIntegration() throws Exception { XSSFWorkbook wb = new XSSFWorkbook(); wb.createSheet("Testing purpose sheet"); assertFalse(wb.isRevisionLocked()); wb.lockRevision(); + wb.setRevisionsPassword("test", null); - File tempFile = TempFile.createTempFile("workbookProtection", ".xlsx"); - FileOutputStream out = new FileOutputStream(tempFile); - wb.write(out); - out.close(); - - FileInputStream inputStream = new FileInputStream(tempFile); - XSSFWorkbook workbook = new XSSFWorkbook(inputStream); - inputStream.close(); - - assertTrue(workbook.isRevisionLocked()); + wb = writeOutAndReadBack(wb); + + assertTrue(wb.isRevisionLocked()); + assertTrue(wb.validateRevisionsPassword("test")); } } diff --git a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFSheet.java b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFSheet.java index 6da347671c..38bc41101e 100644 --- a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFSheet.java +++ b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFSheet.java @@ -19,6 +19,8 @@ package org.apache.poi.xssf.usermodel; import static junit.framework.TestCase.assertNotNull; import static junit.framework.TestCase.assertTrue; +import static org.apache.poi.xssf.XSSFTestDataSamples.openSampleWorkbook; +import static org.apache.poi.xssf.XSSFTestDataSamples.writeOutAndReadBack; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotSame; @@ -29,7 +31,8 @@ import static org.junit.Assert.fail; import java.util.List; import org.apache.poi.hssf.HSSFTestDataSamples; -import org.apache.poi.hssf.record.PasswordRecord; +import org.apache.poi.poifs.crypt.CryptoFunctions; +import org.apache.poi.poifs.crypt.HashAlgorithm; import org.apache.poi.ss.usermodel.AutoFilter; import org.apache.poi.ss.usermodel.BaseTestSheet; import org.apache.poi.ss.usermodel.Cell; @@ -41,7 +44,6 @@ import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.ss.util.AreaReference; import org.apache.poi.ss.util.CellRangeAddress; import org.apache.poi.ss.util.CellReference; -import org.apache.poi.util.HexDump; import org.apache.poi.xssf.SXSSFITestDataProvider; import org.apache.poi.xssf.XSSFITestDataProvider; import org.apache.poi.xssf.XSSFTestDataSamples; @@ -1068,13 +1070,27 @@ public final class TestXSSFSheet extends BaseTestSheet { assertTrue("sheet protection should be on", pr.isSetSheet()); assertTrue("object protection should be on", pr.isSetObjects()); assertTrue("scenario protection should be on", pr.isSetScenarios()); - String hash = String.valueOf(HexDump.shortToHex(PasswordRecord.hashPassword(password))).substring(2); - assertEquals("well known value for top secret hash should be "+ hash, hash, pr.xgetPassword().getStringValue()); + int hashVal = CryptoFunctions.createXorVerifier1(password); + int actualVal = Integer.parseInt(pr.xgetPassword().getStringValue(),16); + assertEquals("well known value for top secret hash should match", hashVal, actualVal); sheet.protectSheet(null); assertNull("protectSheet(null) should unset CTSheetProtection", sheet.getCTWorksheet().getSheetProtection()); } + @Test + public void protectSheet_lowlevel_2013() { + String password = "test"; + XSSFWorkbook wb = new XSSFWorkbook(); + XSSFSheet xs = wb.createSheet(); + xs.setSheetPassword(password, HashAlgorithm.sha384); + wb = writeOutAndReadBack(wb); + assertTrue(wb.getSheetAt(0).validateSheetPassword(password)); + + wb = openSampleWorkbook("workbookProtection-sheet_password-2013.xlsx"); + assertTrue(wb.getSheetAt(0).validateSheetPassword("pwd")); + } + @Test public void bug49966() { diff --git a/test-data/spreadsheet/workbookProtection-sheet_password-2013.xlsx b/test-data/spreadsheet/workbookProtection-sheet_password-2013.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..691b2cc2190086a2b2eeb33b8ed041c9ff9958d8 GIT binary patch literal 9196 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 literal 0 HcmV?d00001 diff --git a/test-data/spreadsheet/workbookProtection-workbook_password-2013.xlsx b/test-data/spreadsheet/workbookProtection-workbook_password-2013.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..978210787c5f033b350fe8c52c9584416c554423 GIT binary patch literal 8171 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 literal 0 HcmV?d00001 diff --git a/test-data/spreadsheet/workbookProtection-workbook_password_user_range-2010.xlsx b/test-data/spreadsheet/workbookProtection-workbook_password_user_range-2010.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..2712913380d250eccdf4de8943ac970d2d976683 GIT binary patch literal 11207 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