Index: src/java/org/apache/poi/hssf/record/RecordInputStream.java =================================================================== --- src/java/org/apache/poi/hssf/record/RecordInputStream.java (revision 801584) +++ src/java/org/apache/poi/hssf/record/RecordInputStream.java (working copy) @@ -19,12 +19,18 @@ import java.io.ByteArrayOutputStream; import java.io.InputStream; +import java.util.Set; +import java.util.Collections; +import java.util.HashSet; import org.apache.poi.hssf.dev.BiffViewer; import org.apache.poi.util.LittleEndian; import org.apache.poi.util.LittleEndianInput; import org.apache.poi.util.LittleEndianInputStream; +import org.apache.poi.util.RC4InputStream; +import javax.crypto.Cipher; + /** * Title: Record Input Stream

* Description: Wraps a stream and provides helper methods for the construction of records.

@@ -41,8 +47,26 @@ */ private static final int DATA_LEN_NEEDS_TO_BE_READ = -1; private static final byte[] EMPTY_BYTE_ARRAY = { }; - - /** + + private RC4InputStream rc4is; + + private static final Set unencryptSid = new HashSet(); + static { + /* + [MS-XLS] — v20090708 + Excel Binary File Format (.xls) Structure Specification + */ + unencryptSid.add(BOFRecord.sid); + unencryptSid.add(FilePassRecord.sid); + // UsrExcl + // FileLock + unencryptSid.add(InterfaceHdrRecord.sid); + // RRDInfo + // RRDHead + // TODO: Additionally, the lbPlyPos field of the BoundSheet8 record MUST NOT be encrypted. + } + + /** * For use in {@link BiffViewer} which may construct {@link Record}s that don't completely * read all available data. This exception should never be thrown otherwise. */ @@ -77,13 +101,8 @@ private int _currentDataOffset; public RecordInputStream(InputStream in) throws RecordFormatException { - if (in instanceof LittleEndianInput) { - // accessing directly is an optimisation - _le = (LittleEndianInput) in; - } else { - // less optimal, but should work OK just the same. Often occurs in junit tests. - _le = new LittleEndianInputStream(in); - } + rc4is = new RC4InputStream(in); + _le = new LittleEndianInputStream(rc4is); _nextSid = readNextSid(); } @@ -100,6 +119,7 @@ _currentDataOffset += LittleEndian.BYTE_SIZE; return _le.readUByte(); } + public int read(byte[] b, int off, int len) { int limit = Math.min(len, remaining()); if (limit == 0) { @@ -143,6 +163,7 @@ } return INVALID_SID_VALUE; } + rc4is.setDecrypt(false); int result = _le.readUShort(); if (result == INVALID_SID_VALUE) { throw new RecordFormatException("Found invalid sid (" + result + ")"); @@ -169,6 +190,10 @@ throw new RecordFormatException("The content of an excel record cannot exceed " + MAX_RECORD_DATA_SIZE + " bytes"); } + + if (!unencryptSid.contains((short) _currentSid)) { + rc4is.setDecrypt(true); + } } private void checkRecordPosition(int requiredByteCount) { @@ -394,4 +419,8 @@ // and before the formatting run data) return _nextSid == ContinueRecord.sid; } + + public void setRC4Header(RC4Header rc4Header) { + rc4is.setHeader(rc4Header); + } } Index: src/java/org/apache/poi/hssf/record/RecordFactoryInputStream.java =================================================================== --- src/java/org/apache/poi/hssf/record/RecordFactoryInputStream.java (revision 801584) +++ src/java/org/apache/poi/hssf/record/RecordFactoryInputStream.java (working copy) @@ -148,6 +148,10 @@ return record; } + if (record instanceof FilePassRecord) { + recStream.setRC4Header(((FilePassRecord) record).getRC4Header()); + } + if (record instanceof EOFRecord) { bofDepth--; if (bofDepth < 1) { Index: src/java/org/apache/poi/hssf/record/FilePassRecord.java =================================================================== --- src/java/org/apache/poi/hssf/record/FilePassRecord.java (revision 801584) +++ src/java/org/apache/poi/hssf/record/FilePassRecord.java (working copy) @@ -33,49 +33,86 @@ public final class FilePassRecord extends StandardRecord { - public final static short sid = 0x2F; - private int field_1_encryptedpassword; + public static final short sid = 0x2F; + private int wEncryptionType; + private static final int ENCRYPTION_XOR = 0; + private static final int ENCRYPTION_OTHER = 1; + + private static final int ENCRYPTION_OTHER_RC4 = 1; + private static final int ENCRYPTION_OTHER_CAPI_2 = 2; + private static final int ENCRYPTION_OTHER_CAPI_3 = 3; + + private RC4Header header; + public FilePassRecord() { } public FilePassRecord(RecordInputStream in) { - field_1_encryptedpassword = in.readInt(); + wEncryptionType = in.readShort(); + + switch (wEncryptionType) { + case ENCRYPTION_XOR: + throw new RecordFormatException("HSSF does not currently support XOR obfuscation"); + case ENCRYPTION_OTHER: + int encryptionInfo = in.readShort(); + + switch (encryptionInfo) { + case ENCRYPTION_OTHER_RC4: + header = new RC4Header(in); + + return; + case ENCRYPTION_OTHER_CAPI_2: + case ENCRYPTION_OTHER_CAPI_3: + throw new RecordFormatException("HSSF does not currently support CryptoAPI encryption"); + default: + throw new RecordFormatException("Unknown encryption info "+wEncryptionType); + } + default: + throw new RecordFormatException("Unknown encryption type "+wEncryptionType); + } //Whilst i have read in the password, HSSF currently has no plans to support/decrypt the remainder //of this workbook - throw new RecordFormatException("HSSF does not currently support encrypted workbooks"); +// throw new RecordFormatException("HSSF does not currently support encrypted workbooks"); } - public String toString() - { - StringBuffer buffer = new StringBuffer(); + @Override + public String toString() { + StringBuilder buffer = new StringBuilder(); buffer.append("[FILEPASS]\n"); - buffer.append(" .password = ").append(field_1_encryptedpassword) - .append("\n"); + buffer.append(" .type = ").append(wEncryptionType) + .append('\n'); buffer.append("[/FILEPASS]\n"); return buffer.toString(); } + @Override public void serialize(LittleEndianOutput out) { - out.writeInt(( short ) field_1_encryptedpassword); } + @Override protected int getDataSize() { return 4; } + @Override public short getSid() { return sid; } + @Override public Object clone() { FilePassRecord rec = new FilePassRecord(); - rec.field_1_encryptedpassword = field_1_encryptedpassword; + rec.wEncryptionType = wEncryptionType; return rec; } + + public RC4Header getRC4Header() { + return header; + } } Index: src/testcases/org/apache/poi/hssf/extractor/TestExcelExtractor.java =================================================================== --- src/testcases/org/apache/poi/hssf/extractor/TestExcelExtractor.java (revision 801584) +++ src/testcases/org/apache/poi/hssf/extractor/TestExcelExtractor.java (working copy) @@ -24,6 +24,7 @@ import junit.framework.TestCase; import org.apache.poi.hssf.HSSFTestDataSamples; +import org.apache.poi.hssf.record.RC4Header; import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.apache.poi.poifs.filesystem.DirectoryNode; import org.apache.poi.poifs.filesystem.POIFSFileSystem; @@ -289,4 +290,13 @@ assertTrue("Unable to find expected word in text\n" + text, text.indexOf("test phrase") >= 0); } } + + public void testPassword() { + RC4Header.setPassword("password"); + ExcelExtractor extractor = createExtractor("password.xls"); + String text = extractor.getText(); + RC4Header.setPassword(null); + + assertTrue(text.contains("ZIP")); + } }