diff --git a/src/java/org/apache/poi/hssf/record/RecordFactoryInputStream.java b/src/java/org/apache/poi/hssf/record/RecordFactoryInputStream.java index 3cf687e4c..7ebe5177b 100755 --- a/src/java/org/apache/poi/hssf/record/RecordFactoryInputStream.java +++ b/src/java/org/apache/poi/hssf/record/RecordFactoryInputStream.java @@ -23,6 +23,7 @@ import java.util.List; import org.apache.poi.hssf.eventusermodel.HSSFEventFactory; import org.apache.poi.hssf.eventusermodel.HSSFListener; import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey; +import org.apache.poi.EncryptedDocumentException; /** * A stream based way to get at complete records, with @@ -41,7 +42,7 @@ public final class RecordFactoryInputStream { * Needed for protected files because each byte is encrypted with respect to its absolute * position from the start of the stream. */ - public static final class StreamEncryptionInfo { + private static final class StreamEncryptionInfo { private final int _initialRecordsSize; private final FilePassRecord _filePassRec; private final Record _lastRecord; @@ -97,7 +98,9 @@ public final class RecordFactoryInputStream { key = Biff8EncryptionKey.create(userPassword, fpr.getDocId()); } if (!key.validate(fpr.getSaltData(), fpr.getSaltHash())) { - throw new RecordFormatException("Password/docId do not correspond to saltData/saltHash"); + throw new EncryptedDocumentException( + (userPassword == null ? "Default" : "Supplied") + + " password is invalid for docId/saltData/saltHash"); } return new RecordInputStream(original, key, _initialRecordsSize); } diff --git a/src/java/org/apache/poi/hssf/record/crypto/Biff8EncryptionKey.java b/src/java/org/apache/poi/hssf/record/crypto/Biff8EncryptionKey.java index 8a3af7e17..fd46caf2b 100644 --- a/src/java/org/apache/poi/hssf/record/crypto/Biff8EncryptionKey.java +++ b/src/java/org/apache/poi/hssf/record/crypto/Biff8EncryptionKey.java @@ -90,9 +90,21 @@ public final class Biff8EncryptionKey { md5.update(saltDataPrime); byte[] finalSaltResult = md5.digest(); + if (false) { // set true to see a valid saltHash value + byte[] saltHashThatWouldWork = xor(saltHash, xor(saltHashPrime, finalSaltResult)); + System.out.println(HexDump.toHex(saltHashThatWouldWork)); + } + return Arrays.equals(saltHashPrime, finalSaltResult); } + private static byte[] xor(byte[] a, byte[] b) { + byte[] c = new byte[a.length]; + for (int i = 0; i < c.length; i++) { + c[i] = (byte) (a[i] ^ b[i]); + } + return c; + } private static void check16Bytes(byte[] data, String argName) { if (data.length != 16) { throw new IllegalArgumentException("Expected 16 byte " + argName + ", but got " + HexDump.toHex(data)); diff --git a/src/testcases/org/apache/poi/hssf/record/AllRecordTests.java b/src/testcases/org/apache/poi/hssf/record/AllRecordTests.java index 5aec73999..920cfe714 100755 --- a/src/testcases/org/apache/poi/hssf/record/AllRecordTests.java +++ b/src/testcases/org/apache/poi/hssf/record/AllRecordTests.java @@ -47,9 +47,11 @@ public final class AllRecordTests { result.addTestSuite(TestBOFRecord.class); result.addTestSuite(TestBoolErrRecord.class); result.addTestSuite(TestBoundSheetRecord.class); + result.addTestSuite(TestCellRange.class); result.addTestSuite(TestCFHeaderRecord.class); result.addTestSuite(TestCFRuleRecord.class); result.addTestSuite(TestCommonObjectDataSubRecord.class); + result.addTestSuite(TestConstantValueParser.class); result.addTestSuite(TestDrawingGroupRecord.class); result.addTestSuite(TestEmbeddedObjectRefSubRecord.class); result.addTestSuite(TestEndSubRecord.class); @@ -67,8 +69,9 @@ public final class AllRecordTests { result.addTestSuite(TestObjRecord.class); result.addTestSuite(TestPaletteRecord.class); result.addTestSuite(TestPaneRecord.class); - result.addTestSuite(TestRecordInputStream.class); result.addTestSuite(TestRecordFactory.class); + result.addTestSuite(TestRecordFactoryInputStream.class); + result.addTestSuite(TestRecordInputStream.class); result.addTestSuite(TestSCLRecord.class); result.addTestSuite(TestSSTDeserializer.class); result.addTestSuite(TestSSTRecord.class); @@ -84,8 +87,6 @@ public final class AllRecordTests { result.addTestSuite(TestUnicodeNameRecord.class); result.addTestSuite(TestUnicodeString.class); result.addTestSuite(TestWriteAccessRecord.class); - result.addTestSuite(TestCellRange.class); - result.addTestSuite(TestConstantValueParser.class); return result; } } diff --git a/src/testcases/org/apache/poi/hssf/record/TestRecordFactoryInputStream.java b/src/testcases/org/apache/poi/hssf/record/TestRecordFactoryInputStream.java new file mode 100644 index 000000000..a97bb5fb3 --- /dev/null +++ b/src/testcases/org/apache/poi/hssf/record/TestRecordFactoryInputStream.java @@ -0,0 +1,147 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.record; + +import java.io.ByteArrayInputStream; +import java.util.Arrays; + +import junit.framework.AssertionFailedError; +import junit.framework.TestCase; + +import org.apache.poi.EncryptedDocumentException; +import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey; +import org.apache.poi.util.HexRead; + +/** + * Tests for {@link RecordFactoryInputStream} + * + * @author Josh Micich + */ +public final class TestRecordFactoryInputStream extends TestCase { + + /** + * Hex dump of a BOF record and most of a FILEPASS record. + * A 16 byte saltHash should be added to complete the second record + */ + private static final String COMMON_HEX_DATA = "" + // BOF + + "09 08 10 00" + + "00 06 05 00 D3 10 CC 07 01 00 00 00 00 06 00 00" + // FILEPASS + + "2F 00 36 00" + + "01 00 01 00 01 00" + + "BAADF00D BAADF00D BAADF00D BAADF00D" // docId + + "DEADBEEF DEADBEEF DEADBEEF DEADBEEF" // saltData + ; + + /** + * Hex dump of a sample WINDOW1 record + */ + private static final String SAMPLE_WINDOW1 = "3D 00 12 00" + + "00 00 00 00 40 38 55 23 38 00 00 00 00 00 01 00 58 02"; + + /** + * Makes sure that a default password mismatch condition is represented with {@link EncryptedDocumentException} + */ + public void testDefaultPassword() { + // This encodng depends on docId, password and stream position + final String SAMPLE_WINDOW1_ENCR1 = "3D 00 12 00" + + "C4, 9B, 02, 50, 86, E0, DF, 34, FB, 57, 0E, 8C, CE, 25, 45, E3, 80, 01"; + + byte[] dataWrongDefault = HexRead.readFromString("" + + COMMON_HEX_DATA + + "00000000 00000000 00000000 00000001" + + SAMPLE_WINDOW1_ENCR1 + ); + + RecordFactoryInputStream rfis; + try { + rfis = createRFIS(dataWrongDefault); + throw new AssertionFailedError("Expected password mismatch error"); + } catch (EncryptedDocumentException e) { + // expected during successful test + if (!e.getMessage().equals("Default password is invalid for docId/saltData/saltHash")) { + throw e; + } + } + + byte[] dataCorrectDefault = HexRead.readFromString("" + + COMMON_HEX_DATA + + "137BEF04 969A200B 306329DE 52254005" // correct saltHash for default password (and docId/saltHash) + + SAMPLE_WINDOW1_ENCR1 + ); + + rfis = createRFIS(dataCorrectDefault); + + confirmReadInitialRecords(rfis); + } + + /** + * Makes sure that an incorrect user supplied password condition is represented with {@link EncryptedDocumentException} + */ + public void testSuppliedPassword() { + // This encodng depends on docId, password and stream position + final String SAMPLE_WINDOW1_ENCR2 = "3D 00 12 00" + + "45, B9, 90, FE, B6, C6, EC, 73, EE, 3F, 52, 45, 97, DB, E3, C1, D6, FE"; + + byte[] dataWrongDefault = HexRead.readFromString("" + + COMMON_HEX_DATA + + "00000000 00000000 00000000 00000000" + + SAMPLE_WINDOW1_ENCR2 + ); + + + Biff8EncryptionKey.setCurrentUserPassword("passw0rd"); + + RecordFactoryInputStream rfis; + try { + rfis = createRFIS(dataWrongDefault); + throw new AssertionFailedError("Expected password mismatch error"); + } catch (EncryptedDocumentException e) { + // expected during successful test + if (!e.getMessage().equals("Supplied password is invalid for docId/saltData/saltHash")) { + throw e; + } + } + + byte[] dataCorrectDefault = HexRead.readFromString("" + + COMMON_HEX_DATA + + "C728659A C38E35E0 568A338F C3FC9D70" // correct saltHash for supplied password (and docId/saltHash) + + SAMPLE_WINDOW1_ENCR2 + ); + + rfis = createRFIS(dataCorrectDefault); + Biff8EncryptionKey.setCurrentUserPassword(null); + + confirmReadInitialRecords(rfis); + } + + /** + * makes sure the record stream starts with {@link BOFRecord} and then {@link WindowOneRecord} + * The second record is gets decrypted so this method also checks its content. + */ + private void confirmReadInitialRecords(RecordFactoryInputStream rfis) { + assertEquals(BOFRecord.class, rfis.nextRecord().getClass()); + WindowOneRecord rec1 = (WindowOneRecord) rfis.nextRecord(); + assertTrue(Arrays.equals(HexRead.readFromString(SAMPLE_WINDOW1),rec1.serialize())); + } + + private static RecordFactoryInputStream createRFIS(byte[] data) { + return new RecordFactoryInputStream(new ByteArrayInputStream(data), true); + } +}