From c15f65b01eae1d40768f9224cb66ad6644c2fa20 Mon Sep 17 00:00:00 2001 From: Josh Micich Date: Fri, 7 Aug 2009 06:03:31 +0000 Subject: [PATCH] Bugzilla 47652 - Added support for reading encrypted workbooks git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@801890 13f79535-47bb-0310-9956-ffa450edef68 --- src/documentation/content/xdocs/status.xml | 1 + .../org/apache/poi/hssf/dev/BiffViewer.java | 46 ++-- .../hssf/eventusermodel/HSSFEventFactory.java | 7 +- .../poi/hssf/record/BiffHeaderInput.java | 16 ++ .../poi/hssf/record/FilePassRecord.java | 176 +++++++++----- .../apache/poi/hssf/record/RecordFactory.java | 11 +- .../hssf/record/RecordFactoryInputStream.java | 157 ++++++++++-- .../poi/hssf/record/RecordInputStream.java | 110 +++++---- .../record/crypto/Biff8DecryptingStream.java | 111 +++++++++ .../record/crypto/Biff8EncryptionKey.java | 147 +++++++++++ .../poi/hssf/record/crypto/Biff8RC4.java | 197 +++++++++++++++ .../apache/poi/hssf/record/crypto/RC4.java | 90 +++++++ .../org/apache/poi/hssf/data/password.xls | Bin 0 -> 22528 bytes .../hssf/extractor/TestExcelExtractor.java | 148 +++++------ .../poi/hssf/record/AllRecordTests.java | 6 +- .../poi/hssf/record/TestRecordFactory.java | 10 +- .../record/crypto/AllHSSFEncryptionTests.java | 38 +++ .../crypto/TestBiff8DecryptingStream.java | 230 ++++++++++++++++++ .../record/crypto/TestBiff8EncryptionKey.java | 102 ++++++++ .../poi/hssf/record/crypto/TestRC4.java | 76 ++++++ 20 files changed, 1460 insertions(+), 219 deletions(-) create mode 100644 src/java/org/apache/poi/hssf/record/BiffHeaderInput.java create mode 100644 src/java/org/apache/poi/hssf/record/crypto/Biff8DecryptingStream.java create mode 100644 src/java/org/apache/poi/hssf/record/crypto/Biff8EncryptionKey.java create mode 100644 src/java/org/apache/poi/hssf/record/crypto/Biff8RC4.java create mode 100644 src/java/org/apache/poi/hssf/record/crypto/RC4.java create mode 100755 src/testcases/org/apache/poi/hssf/data/password.xls create mode 100644 src/testcases/org/apache/poi/hssf/record/crypto/AllHSSFEncryptionTests.java create mode 100644 src/testcases/org/apache/poi/hssf/record/crypto/TestBiff8DecryptingStream.java create mode 100644 src/testcases/org/apache/poi/hssf/record/crypto/TestBiff8EncryptionKey.java create mode 100644 src/testcases/org/apache/poi/hssf/record/crypto/TestRC4.java diff --git a/src/documentation/content/xdocs/status.xml b/src/documentation/content/xdocs/status.xml index db4495fd07..eb506ceb70 100644 --- a/src/documentation/content/xdocs/status.xml +++ b/src/documentation/content/xdocs/status.xml @@ -33,6 +33,7 @@ + 47652 - Added support for reading encrypted workbooks 47604 - Implementation of an XML to XLSX Importer using Custom XML Mapping 47620 - Avoid FormulaParseException in XSSFWorkbook.setRepeatingRowsAndColumns when removing repeated rows and columns 47606 - Fixed XSSFCell to correctly parse column indexes greater than 702 (ZZ) diff --git a/src/java/org/apache/poi/hssf/dev/BiffViewer.java b/src/java/org/apache/poi/hssf/dev/BiffViewer.java index ff708b5d18..92bda82570 100644 --- a/src/java/org/apache/poi/hssf/dev/BiffViewer.java +++ b/src/java/org/apache/poi/hssf/dev/BiffViewer.java @@ -81,17 +81,23 @@ public final class BiffViewer { if (recStream.getSid() == 0) { continue; } - Record record = createRecord (recStream); - if (record.getSid() == ContinueRecord.sid) { - continue; - } - temp.add(record); + Record record; if (dumpInterpretedRecords) { - String[] headers = recListener.getRecentHeaders(); - for (int i = 0; i < headers.length; i++) { - ps.println(headers[i]); + record = createRecord (recStream); + if (record.getSid() == ContinueRecord.sid) { + continue; + } + temp.add(record); + + if (dumpInterpretedRecords) { + String[] headers = recListener.getRecentHeaders(); + for (int i = 0; i < headers.length; i++) { + ps.println(headers[i]); + } + ps.print(record.toString()); } - ps.print(record.toString()); + } else { + recStream.readRemainder(); } ps.println(); } @@ -296,8 +302,8 @@ public final class BiffViewer { out = true; } else if ("--escher".equals(arg)) { System.setProperty("poi.deserialize.escher", "true"); - } else if ("--rawhex".equals(arg)) { - rawhex = true; + } else if ("--rawhex".equals(arg)) { + rawhex = true; } else { throw new CommandParseException("Unexpected option '" + arg + "'"); } @@ -389,7 +395,7 @@ public final class BiffViewer { } else { boolean dumpInterpretedRecords = cmdArgs.shouldDumpRecordInterpretations(); boolean dumpHex = cmdArgs.shouldDumpBiffHex(); - boolean zeroAlignHexDump = dumpInterpretedRecords; + boolean zeroAlignHexDump = dumpInterpretedRecords; // TODO - fix non-zeroAlign BiffRecordListener recListener = new BiffRecordListener(dumpHex ? new OutputStreamWriter(ps) : null, zeroAlignHexDump); is = new BiffDumpingStream(is, recListener); createRecords(is, ps, recListener, dumpInterpretedRecords); @@ -558,11 +564,21 @@ public final class BiffViewer { int startDelta = globalStart % DUMP_LINE_LEN; int endDelta = globalEnd % DUMP_LINE_LEN; if (zeroAlignEachRecord) { + endDelta -= startDelta; + if (endDelta < 0) { + endDelta += DUMP_LINE_LEN; + } startDelta = 0; - endDelta = 0; } - int startLineAddr = globalStart - startDelta; - int endLineAddr = globalEnd - endDelta; + int startLineAddr; + int endLineAddr; + if (zeroAlignEachRecord) { + endLineAddr = globalEnd - endDelta - (globalStart - startDelta); + startLineAddr = 0; + } else { + startLineAddr = globalStart - startDelta; + endLineAddr = globalEnd - endDelta; + } int lineDataOffset = baseDataOffset - startDelta; int lineAddr = startLineAddr; diff --git a/src/java/org/apache/poi/hssf/eventusermodel/HSSFEventFactory.java b/src/java/org/apache/poi/hssf/eventusermodel/HSSFEventFactory.java index 2b392c0e3c..a11aee96ef 100644 --- a/src/java/org/apache/poi/hssf/eventusermodel/HSSFEventFactory.java +++ b/src/java/org/apache/poi/hssf/eventusermodel/HSSFEventFactory.java @@ -1,4 +1,3 @@ - /* ==================================================================== Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with @@ -83,7 +82,7 @@ public class HSSFEventFactory { */ public void processEvents(HSSFRequest req, InputStream in) { try { - genericProcessEvents(req, new RecordInputStream(in)); + genericProcessEvents(req, in); } catch (HSSFUserException hue) { /*If an HSSFUserException user exception is thrown, ignore it.*/ } @@ -100,7 +99,7 @@ public class HSSFEventFactory { */ public short abortableProcessEvents(HSSFRequest req, InputStream in) throws HSSFUserException { - return genericProcessEvents(req, new RecordInputStream(in)); + return genericProcessEvents(req, in); } /** @@ -111,7 +110,7 @@ public class HSSFEventFactory { * @param in a DocumentInputStream obtained from POIFS's POIFSFileSystem object * @return numeric user-specified result code. */ - protected short genericProcessEvents(HSSFRequest req, RecordInputStream in) + private short genericProcessEvents(HSSFRequest req, InputStream in) throws HSSFUserException { short userCode = 0; diff --git a/src/java/org/apache/poi/hssf/record/BiffHeaderInput.java b/src/java/org/apache/poi/hssf/record/BiffHeaderInput.java new file mode 100644 index 0000000000..a70fbe9009 --- /dev/null +++ b/src/java/org/apache/poi/hssf/record/BiffHeaderInput.java @@ -0,0 +1,16 @@ +package org.apache.poi.hssf.record; + +public interface BiffHeaderInput { + + /** + * Read an unsigned short from the stream without decrypting + */ + int readRecordSID(); + /** + * Read an unsigned short from the stream without decrypting + */ + int readDataSize(); + + int available(); + +} diff --git a/src/java/org/apache/poi/hssf/record/FilePassRecord.java b/src/java/org/apache/poi/hssf/record/FilePassRecord.java index d6d19b5608..c281bf9635 100644 --- a/src/java/org/apache/poi/hssf/record/FilePassRecord.java +++ b/src/java/org/apache/poi/hssf/record/FilePassRecord.java @@ -1,4 +1,3 @@ - /* ==================================================================== Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with @@ -15,67 +14,134 @@ See the License for the specific language governing permissions and limitations under the License. ==================================================================== */ - package org.apache.poi.hssf.record; +import org.apache.poi.util.HexDump; import org.apache.poi.util.LittleEndianOutput; /** - * Title: File Pass Record

- * Description: Indicates that the record after this record are encrypted. HSSF does not support encrypted excel workbooks - * and the presence of this record will cause processing to be aborted.

- * REFERENCE: PG 420 Microsoft Excel 97 Developer's Kit (ISBN: 1-57231-498-2)

+ * Title: File Pass Record (0x002F)

+ * + * Description: Indicates that the record after this record are encrypted. + * * @author Jason Height (jheight at chariot dot net dot au) - * @version 3.0-pre */ +public final class FilePassRecord extends StandardRecord { + public final static short sid = 0x002F; + private int _encryptionType; + private int _encryptionInfo; + private int _minorVersionNo; + private byte[] _docId; + private byte[] _saltData; + private byte[] _saltHash; + + 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; + + + public FilePassRecord(RecordInputStream in) { + _encryptionType = in.readUShort(); + + switch (_encryptionType) { + case ENCRYPTION_XOR: + throw new RecordFormatException("HSSF does not currently support XOR obfuscation"); + case ENCRYPTION_OTHER: + // handled below + break; + default: + throw new RecordFormatException("Unknown encryption type " + _encryptionType); + } + _encryptionInfo = in.readUShort(); + switch (_encryptionInfo) { + case ENCRYPTION_OTHER_RC4: + // handled below + break; + 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 " + _encryptionInfo); + } + _minorVersionNo = in.readUShort(); + if (_minorVersionNo!=1) { + throw new RecordFormatException("Unexpected VersionInfo number for RC4Header " + _minorVersionNo); + } + _docId = read(in, 16); + _saltData = read(in, 16); + _saltHash = read(in, 16); + } + + private static byte[] read(RecordInputStream in, int size) { + byte[] result = new byte[size]; + in.readFully(result); + return result; + } + + public void serialize(LittleEndianOutput out) { + out.writeShort(_encryptionType); + out.writeShort(_encryptionInfo); + out.writeShort(_minorVersionNo); + out.write(_docId); + out.write(_saltData); + out.write(_saltHash); + } + + protected int getDataSize() { + return 54; + } + + + + public byte[] getDocId() { + return _docId.clone(); + } + + public void setDocId(byte[] docId) { + _docId = docId.clone(); + } + + public byte[] getSaltData() { + return _saltData.clone(); + } + + public void setSaltData(byte[] saltData) { + _saltData = saltData.clone(); + } + + public byte[] getSaltHash() { + return _saltHash.clone(); + } + + public void setSaltHash(byte[] saltHash) { + _saltHash = saltHash.clone(); + } + + public short getSid() { + return sid; + } + + public Object clone() { + // currently immutable + return this; + } + + public String toString() { + StringBuffer buffer = new StringBuffer(); -public final class FilePassRecord - extends StandardRecord -{ - public final static short sid = 0x2F; - private int field_1_encryptedpassword; - - public FilePassRecord() - { - } - - public FilePassRecord(RecordInputStream in) - { - field_1_encryptedpassword = in.readInt(); - - //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"); - } - - public String toString() - { - StringBuffer buffer = new StringBuffer(); - - buffer.append("[FILEPASS]\n"); - buffer.append(" .password = ").append(field_1_encryptedpassword) - .append("\n"); - buffer.append("[/FILEPASS]\n"); - return buffer.toString(); - } - - public void serialize(LittleEndianOutput out) { - out.writeInt(( short ) field_1_encryptedpassword); - } - - protected int getDataSize() { - return 4; - } - - public short getSid() - { - return sid; - } - - public Object clone() { - FilePassRecord rec = new FilePassRecord(); - rec.field_1_encryptedpassword = field_1_encryptedpassword; - return rec; - } + buffer.append("[FILEPASS]\n"); + buffer.append(" .type = ").append(HexDump.shortToHex(_encryptionType)).append("\n"); + buffer.append(" .info = ").append(HexDump.shortToHex(_encryptionInfo)).append("\n"); + buffer.append(" .ver = ").append(HexDump.shortToHex(_minorVersionNo)).append("\n"); + buffer.append(" .docId= ").append(HexDump.toHex(_docId)).append("\n"); + buffer.append(" .salt = ").append(HexDump.toHex(_saltData)).append("\n"); + buffer.append(" .hash = ").append(HexDump.toHex(_saltHash)).append("\n"); + buffer.append("[/FILEPASS]\n"); + return buffer.toString(); + } } diff --git a/src/java/org/apache/poi/hssf/record/RecordFactory.java b/src/java/org/apache/poi/hssf/record/RecordFactory.java index f458606678..f933d4bcdb 100644 --- a/src/java/org/apache/poi/hssf/record/RecordFactory.java +++ b/src/java/org/apache/poi/hssf/record/RecordFactory.java @@ -17,15 +17,16 @@ package org.apache.poi.hssf.record; -import org.apache.poi.hssf.record.chart.*; -import org.apache.poi.hssf.record.pivottable.*; - +import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Modifier; import java.util.*; +import org.apache.poi.hssf.record.chart.*; +import org.apache.poi.hssf.record.pivottable.*; + /** * Title: Record Factory

* Description: Takes a stream and outputs an array of Record objects.

@@ -193,6 +194,7 @@ public final class RecordFactory { ChartFRTInfoRecord.class, ChartStartBlockRecord.class, ChartEndBlockRecord.class, +// TODO ChartFormatRecord.class, ChartStartObjectRecord.class, ChartEndObjectRecord.class, CatLabRecord.class, @@ -367,9 +369,10 @@ public final class RecordFactory { * @exception RecordFormatException on error processing the InputStream */ public static List createRecords(InputStream in) throws RecordFormatException { + List records = new ArrayList(NUM_RECORDS); - RecordFactoryInputStream recStream = new RecordFactoryInputStream(new RecordInputStream(in), true); + RecordFactoryInputStream recStream = new RecordFactoryInputStream(in, true); Record record; while ((record = recStream.nextRecord())!=null) { diff --git a/src/java/org/apache/poi/hssf/record/RecordFactoryInputStream.java b/src/java/org/apache/poi/hssf/record/RecordFactoryInputStream.java index 541dfd2dc0..3cf687e4cb 100755 --- a/src/java/org/apache/poi/hssf/record/RecordFactoryInputStream.java +++ b/src/java/org/apache/poi/hssf/record/RecordFactoryInputStream.java @@ -16,8 +16,13 @@ ==================================================================== */ package org.apache.poi.hssf.record; +import java.io.InputStream; +import java.util.ArrayList; +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; /** * A stream based way to get at complete records, with @@ -29,21 +34,110 @@ import org.apache.poi.hssf.eventusermodel.HSSFListener; * {@link HSSFListener} and have new records pushed to * them, but this does allow for a "pull" style of coding. */ -public class RecordFactoryInputStream { +public final class RecordFactoryInputStream { + + /** + * Keeps track of the sizes of the initial records up to and including {@link FilePassRecord} + * 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 final int _initialRecordsSize; + private final FilePassRecord _filePassRec; + private final Record _lastRecord; + private final boolean _hasBOFRecord; + + public StreamEncryptionInfo(RecordInputStream rs, List outputRecs) { + Record rec; + rs.nextRecord(); + int recSize = 4 + rs.remaining(); + rec = RecordFactory.createSingleRecord(rs); + outputRecs.add(rec); + FilePassRecord fpr = null; + if (rec instanceof BOFRecord) { + _hasBOFRecord = true; + if (rs.hasNextRecord()) { + rs.nextRecord(); + rec = RecordFactory.createSingleRecord(rs); + recSize += rec.getRecordSize(); + outputRecs.add(rec); + if (rec instanceof FilePassRecord) { + fpr = (FilePassRecord) rec; + outputRecs.remove(outputRecs.size()-1); + // TODO - add fpr not added to outputRecs + rec = outputRecs.get(0); + } else { + // workbook not encrypted (typical case) + if (rec instanceof EOFRecord) { + // A workbook stream is never empty, so crash instead + // of trying to keep track of nesting level + throw new IllegalStateException("Nothing between BOF and EOF"); + } + } + } + } else { + // Invalid in a normal workbook stream. + // However, some test cases work on sub-sections of + // the workbook stream that do not begin with BOF + _hasBOFRecord = false; + } + _initialRecordsSize = recSize; + _filePassRec = fpr; + _lastRecord = rec; + } + + public RecordInputStream createDecryptingStream(InputStream original) { + FilePassRecord fpr = _filePassRec; + String userPassword = Biff8EncryptionKey.getCurrentUserPassword(); + + Biff8EncryptionKey key; + if (userPassword == null) { + key = Biff8EncryptionKey.create(fpr.getDocId()); + } else { + key = Biff8EncryptionKey.create(userPassword, fpr.getDocId()); + } + if (!key.validate(fpr.getSaltData(), fpr.getSaltHash())) { + throw new RecordFormatException("Password/docId do not correspond to saltData/saltHash"); + } + return new RecordInputStream(original, key, _initialRecordsSize); + } + + public boolean hasEncryption() { + return _filePassRec != null; + } + + /** + * @return last record scanned while looking for encryption info. + * This will typically be the first or second record read. Possibly null + * if stream was empty + */ + public Record getLastRecord() { + return _lastRecord; + } + + /** + * false in some test cases + */ + public boolean hasBOFRecord() { + return _hasBOFRecord; + } + } + private final RecordInputStream _recStream; private final boolean _shouldIncludeContinueRecords; /** - * Temporarily stores a group of {@link NumberRecord}s. This is uses when the most - * recently read underlying record is a {@link MulRKRecord} + * Temporarily stores a group of {@link Record}s, for future return by {@link #nextRecord()}. + * This is used at the start of the workbook stream, and also when the most recently read + * underlying record is a {@link MulRKRecord} */ - private NumberRecord[] _multipleNumberRecords; + private Record[] _unreadRecordBuffer; /** - * used to help iterating over multiple number records + * used to help iterating over the unread records */ - private int _multipleNumberRecordIndex = -1; + private int _unreadRecordIndex = -1; /** * The most recent record that we gave to the user @@ -64,9 +158,24 @@ public class RecordFactoryInputStream { * {@link ContinueRecord}s should be skipped (this is sometimes useful in event based * processing). */ - public RecordFactoryInputStream(RecordInputStream inp, boolean shouldIncludeContinueRecords) { - _recStream = inp; + public RecordFactoryInputStream(InputStream in, boolean shouldIncludeContinueRecords) { + RecordInputStream rs = new RecordInputStream(in); + List records = new ArrayList(); + StreamEncryptionInfo sei = new StreamEncryptionInfo(rs, records); + if (sei.hasEncryption()) { + rs = sei.createDecryptingStream(in); + } else { + // typical case - non-encrypted stream + } + + if (!records.isEmpty()) { + _unreadRecordBuffer = new Record[records.size()]; + records.toArray(_unreadRecordBuffer); + _unreadRecordIndex =0; + } + _recStream = rs; _shouldIncludeContinueRecords = shouldIncludeContinueRecords; + _lastRecord = sei.getLastRecord(); /* * How to recognise end of stream? @@ -85,7 +194,7 @@ public class RecordFactoryInputStream { * record might follow any EOF record. So we also need to keep track of the bof/eof * nesting level. */ - _bofDepth=0; + _bofDepth = sei.hasBOFRecord() ? 1 : 0; _lastRecordWasEOFLevelZero = false; } @@ -95,15 +204,15 @@ public class RecordFactoryInputStream { */ public Record nextRecord() { Record r; - r = getNextMultipleNumberRecord(); + r = getNextUnreadRecord(); if (r != null) { - // found a NumberRecord (expanded from a recent MULRK record) + // found an unread record return r; } while (true) { if (!_recStream.hasNextRecord()) { // recStream is exhausted; - return null; + return null; } // step underlying RecordInputStream to the next record @@ -131,19 +240,19 @@ public class RecordFactoryInputStream { } /** - * @return the next {@link NumberRecord} from the multiple record group as expanded from + * @return the next {@link Record} from the multiple record group as expanded from * a recently read {@link MulRKRecord}. null if not present. */ - private NumberRecord getNextMultipleNumberRecord() { - if (_multipleNumberRecords != null) { - int ix = _multipleNumberRecordIndex; - if (ix < _multipleNumberRecords.length) { - NumberRecord result = _multipleNumberRecords[ix]; - _multipleNumberRecordIndex = ix + 1; + private Record getNextUnreadRecord() { + if (_unreadRecordBuffer != null) { + int ix = _unreadRecordIndex; + if (ix < _unreadRecordBuffer.length) { + Record result = _unreadRecordBuffer[ix]; + _unreadRecordIndex = ix + 1; return result; } - _multipleNumberRecordIndex = -1; - _multipleNumberRecords = null; + _unreadRecordIndex = -1; + _unreadRecordBuffer = null; } return null; } @@ -182,10 +291,10 @@ public class RecordFactoryInputStream { } if (record instanceof MulRKRecord) { - NumberRecord[] records = RecordFactory.convertRKRecords((MulRKRecord) record); + Record[] records = RecordFactory.convertRKRecords((MulRKRecord) record); - _multipleNumberRecords = records; - _multipleNumberRecordIndex = 1; + _unreadRecordBuffer = records; + _unreadRecordIndex = 1; return records[0]; } diff --git a/src/java/org/apache/poi/hssf/record/RecordInputStream.java b/src/java/org/apache/poi/hssf/record/RecordInputStream.java index d61d04af94..0997232fb2 100755 --- a/src/java/org/apache/poi/hssf/record/RecordInputStream.java +++ b/src/java/org/apache/poi/hssf/record/RecordInputStream.java @@ -21,6 +21,8 @@ import java.io.ByteArrayOutputStream; import java.io.InputStream; import org.apache.poi.hssf.dev.BiffViewer; +import org.apache.poi.hssf.record.crypto.Biff8DecryptingStream; +import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey; import org.apache.poi.util.LittleEndian; import org.apache.poi.util.LittleEndianInput; import org.apache.poi.util.LittleEndianInputStream; @@ -31,7 +33,7 @@ import org.apache.poi.util.LittleEndianInputStream; * * @author Jason Height (jheight @ apache dot org) */ -public final class RecordInputStream extends InputStream implements LittleEndianInput { +public final class RecordInputStream implements LittleEndianInput { /** Maximum size of a single record (minus the 4 byte header) without a continue*/ public final static short MAX_RECORD_DATA_SIZE = 8224; private static final int INVALID_SID_VALUE = -1; @@ -41,52 +43,86 @@ public final class RecordInputStream extends InputStream implements LittleEndian */ private static final int DATA_LEN_NEEDS_TO_BE_READ = -1; private static final byte[] EMPTY_BYTE_ARRAY = { }; - + /** * 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. */ public static final class LeftoverDataException extends RuntimeException { public LeftoverDataException(int sid, int remainingByteCount) { - super("Initialisation of record 0x" + Integer.toHexString(sid).toUpperCase() + super("Initialisation of record 0x" + Integer.toHexString(sid).toUpperCase() + " left " + remainingByteCount + " bytes remaining still to be read."); } } - /** {@link LittleEndianInput} facet of the wrapped {@link InputStream} */ - private final LittleEndianInput _le; + /** Header {@link LittleEndianInput} facet of the wrapped {@link InputStream} */ + private final BiffHeaderInput _bhi; + /** Data {@link LittleEndianInput} facet of the wrapped {@link InputStream} */ + private final LittleEndianInput _dataInput; /** the record identifier of the BIFF record currently being read */ private int _currentSid; - /** + /** * Length of the data section of the current BIFF record (always 4 less than the total record size). * When uninitialised, this field is set to {@link #DATA_LEN_NEEDS_TO_BE_READ}. */ private int _currentDataLength; - /** + /** * The BIFF record identifier for the next record is read when just as the current record * is finished. - * This field is only really valid during the time that ({@link #_currentDataLength} == - * {@link #DATA_LEN_NEEDS_TO_BE_READ}). At most other times its value is not really the - * 'sid of the next record'. Wwhile mid-record, this field coincidentally holds the sid + * This field is only really valid during the time that ({@link #_currentDataLength} == + * {@link #DATA_LEN_NEEDS_TO_BE_READ}). At most other times its value is not really the + * 'sid of the next record'. Wwhile mid-record, this field coincidentally holds the sid * of the current record. */ private int _nextSid; - /** + /** * index within the data section of the current BIFF record */ private int _currentDataOffset; + private static final class SimpleHeaderInput implements BiffHeaderInput { + + private final LittleEndianInput _lei; + + public SimpleHeaderInput(InputStream in) { + _lei = getLEI(in); + } + public int available() { + return _lei.available(); + } + public int readDataSize() { + return _lei.readUShort(); + } + public int readRecordSID() { + return _lei.readUShort(); + } + } + public RecordInputStream(InputStream in) throws RecordFormatException { - if (in instanceof LittleEndianInput) { - // accessing directly is an optimisation - _le = (LittleEndianInput) in; + this (in, null, 0); + } + + public RecordInputStream(InputStream in, Biff8EncryptionKey key, int initialOffset) throws RecordFormatException { + if (key == null) { + _dataInput = getLEI(in); + _bhi = new SimpleHeaderInput(in); } else { - // less optimal, but should work OK just the same. Often occurs in junit tests. - _le = new LittleEndianInputStream(in); + Biff8DecryptingStream bds = new Biff8DecryptingStream(in, initialOffset, key); + _bhi = bds; + _dataInput = bds; } _nextSid = readNextSid(); } - + + static LittleEndianInput getLEI(InputStream is) { + if (is instanceof LittleEndianInput) { + // accessing directly is an optimisation + return (LittleEndianInput) is; + } + // less optimal, but should work OK just the same. Often occurs in junit tests. + return new LittleEndianInputStream(is); + } + /** * @return the number of bytes available in the current BIFF record * @see #remaining() @@ -95,11 +131,6 @@ public final class RecordInputStream extends InputStream implements LittleEndian return remaining(); } - public int read() { - checkRecordPosition(LittleEndian.BYTE_SIZE); - _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) { @@ -114,9 +145,9 @@ public final class RecordInputStream extends InputStream implements LittleEndian } /** - * Note - this method is expected to be called only when completed reading the current BIFF + * Note - this method is expected to be called only when completed reading the current BIFF * record. - * @throws LeftoverDataException if this method is called before reaching the end of the + * @throws LeftoverDataException if this method is called before reaching the end of the * current record. */ public boolean hasNextRecord() throws LeftoverDataException { @@ -130,11 +161,10 @@ public final class RecordInputStream extends InputStream implements LittleEndian } /** - * * @return the sid of the next record or {@link #INVALID_SID_VALUE} if at end of stream */ private int readNextSid() { - int nAvailable = _le.available(); + int nAvailable = _bhi.available(); if (nAvailable < EOFRecord.ENCODED_SIZE) { if (nAvailable > 0) { // some scrap left over? @@ -143,7 +173,7 @@ public final class RecordInputStream extends InputStream implements LittleEndian } return INVALID_SID_VALUE; } - int result = _le.readUShort(); + int result = _bhi.readRecordSID(); if (result == INVALID_SID_VALUE) { throw new RecordFormatException("Found invalid sid (" + result + ")"); } @@ -164,7 +194,7 @@ public final class RecordInputStream extends InputStream implements LittleEndian } _currentSid = _nextSid; _currentDataOffset = 0; - _currentDataLength = _le.readUShort(); + _currentDataLength = _bhi.readDataSize(); if (_currentDataLength > MAX_RECORD_DATA_SIZE) { throw new RecordFormatException("The content of an excel record cannot exceed " + MAX_RECORD_DATA_SIZE + " bytes"); @@ -182,7 +212,7 @@ public final class RecordInputStream extends InputStream implements LittleEndian nextRecord(); return; } - throw new RecordFormatException("Not enough data (" + nAvailable + throw new RecordFormatException("Not enough data (" + nAvailable + ") to read requested (" + requiredByteCount +") bytes"); } @@ -192,7 +222,7 @@ public final class RecordInputStream extends InputStream implements LittleEndian public byte readByte() { checkRecordPosition(LittleEndian.BYTE_SIZE); _currentDataOffset += LittleEndian.BYTE_SIZE; - return _le.readByte(); + return _dataInput.readByte(); } /** @@ -201,19 +231,19 @@ public final class RecordInputStream extends InputStream implements LittleEndian public short readShort() { checkRecordPosition(LittleEndian.SHORT_SIZE); _currentDataOffset += LittleEndian.SHORT_SIZE; - return _le.readShort(); + return _dataInput.readShort(); } public int readInt() { checkRecordPosition(LittleEndian.INT_SIZE); _currentDataOffset += LittleEndian.INT_SIZE; - return _le.readInt(); + return _dataInput.readInt(); } public long readLong() { checkRecordPosition(LittleEndian.LONG_SIZE); _currentDataOffset += LittleEndian.LONG_SIZE; - return _le.readLong(); + return _dataInput.readLong(); } /** @@ -229,13 +259,11 @@ public final class RecordInputStream extends InputStream implements LittleEndian public int readUShort() { checkRecordPosition(LittleEndian.SHORT_SIZE); _currentDataOffset += LittleEndian.SHORT_SIZE; - return _le.readUShort(); + return _dataInput.readUShort(); } public double readDouble() { - checkRecordPosition(LittleEndian.DOUBLE_SIZE); - _currentDataOffset += LittleEndian.DOUBLE_SIZE; - long valueLongBits = _le.readLong(); + long valueLongBits = readLong(); double result = Double.longBitsToDouble(valueLongBits); if (Double.isNaN(result)) { throw new RuntimeException("Did not expect to read NaN"); // (Because Excel typically doesn't write NaN @@ -248,7 +276,7 @@ public final class RecordInputStream extends InputStream implements LittleEndian public void readFully(byte[] buf, int off, int len) { checkRecordPosition(len); - _le.readFully(buf, off, len); + _dataInput.readFully(buf, off, len); _currentDataOffset+=len; } @@ -315,7 +343,7 @@ public final class RecordInputStream extends InputStream implements LittleEndian availableChars--; } if (!isContinueNext()) { - throw new RecordFormatException("Expected to find a ContinueRecord in order to read remaining " + throw new RecordFormatException("Expected to find a ContinueRecord in order to read remaining " + (requestedLength-curLen) + " of " + requestedLength + " chars"); } if(remaining() != 0) { @@ -324,7 +352,7 @@ public final class RecordInputStream extends InputStream implements LittleEndian nextRecord(); // note - the compressed flag may change on the fly byte compressFlag = readByte(); - isCompressedEncoding = (compressFlag == 0); + isCompressedEncoding = (compressFlag == 0); } } @@ -390,7 +418,7 @@ public final class RecordInputStream extends InputStream implements LittleEndian // At what point are records continued? // - Often from within the char data of long strings (caller is within readStringCommon()). // - From UnicodeString construction (many different points - call via checkRecordPosition) - // - During TextObjectRecord construction (just before the text, perhaps within the text, + // - During TextObjectRecord construction (just before the text, perhaps within the text, // and before the formatting run data) return _nextSid == ContinueRecord.sid; } diff --git a/src/java/org/apache/poi/hssf/record/crypto/Biff8DecryptingStream.java b/src/java/org/apache/poi/hssf/record/crypto/Biff8DecryptingStream.java new file mode 100644 index 0000000000..a56f911e2a --- /dev/null +++ b/src/java/org/apache/poi/hssf/record/crypto/Biff8DecryptingStream.java @@ -0,0 +1,111 @@ +/* ==================================================================== + 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.crypto; + +import java.io.InputStream; + +import org.apache.poi.hssf.record.BiffHeaderInput; +import org.apache.poi.util.LittleEndianInput; +import org.apache.poi.util.LittleEndianInputStream; + +/** + * + * @author Josh Micich + */ +public final class Biff8DecryptingStream implements BiffHeaderInput, LittleEndianInput { + + private final LittleEndianInput _le; + private final Biff8RC4 _rc4; + + public Biff8DecryptingStream(InputStream in, int initialOffset, Biff8EncryptionKey key) { + _rc4 = new Biff8RC4(initialOffset, key); + + 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); + } + } + + public int available() { + return _le.available(); + } + + /** + * Reads an unsigned short value without decrypting + */ + public int readRecordSID() { + int sid = _le.readUShort(); + _rc4.skipTwoBytes(); + _rc4.startRecord(sid); + return sid; + } + + /** + * Reads an unsigned short value without decrypting + */ + public int readDataSize() { + int dataSize = _le.readUShort(); + _rc4.skipTwoBytes(); + return dataSize; + } + + public double readDouble() { + long valueLongBits = readLong(); + double result = Double.longBitsToDouble(valueLongBits); + if (Double.isNaN(result)) { + throw new RuntimeException("Did not expect to read NaN"); // (Because Excel typically doesn't write NaN + } + return result; + } + + public void readFully(byte[] buf) { + readFully(buf, 0, buf.length); + } + + public void readFully(byte[] buf, int off, int len) { + _le.readFully(buf, off, len); + _rc4.xor(buf, off, len); + } + + + public int readUByte() { + return _rc4.xorByte(_le.readUByte()); + } + public byte readByte() { + return (byte) _rc4.xorByte(_le.readUByte()); + } + + + public int readUShort() { + return _rc4.xorShort(_le.readUShort()); + } + public short readShort() { + return (short) _rc4.xorShort(_le.readUShort()); + } + + public int readInt() { + return _rc4.xorInt(_le.readInt()); + } + + public long readLong() { + return _rc4.xorLong(_le.readLong()); + } +} diff --git a/src/java/org/apache/poi/hssf/record/crypto/Biff8EncryptionKey.java b/src/java/org/apache/poi/hssf/record/crypto/Biff8EncryptionKey.java new file mode 100644 index 0000000000..8a3af7e17f --- /dev/null +++ b/src/java/org/apache/poi/hssf/record/crypto/Biff8EncryptionKey.java @@ -0,0 +1,147 @@ +package org.apache.poi.hssf.record.crypto; + +import java.io.ByteArrayOutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.util.HexDump; +import org.apache.poi.util.LittleEndianOutputStream; + +public final class Biff8EncryptionKey { + // these two constants coincidentally have the same value + private static final int KEY_DIGEST_LENGTH = 5; + private static final int PASSWORD_HASH_NUMBER_OF_BYTES_USED = 5; + + private final byte[] _keyDigest; + + /** + * Create using the default password and a specified docId + * @param docId 16 bytes + */ + public static Biff8EncryptionKey create(byte[] docId) { + return new Biff8EncryptionKey(createKeyDigest("VelvetSweatshop", docId)); + } + public static Biff8EncryptionKey create(String password, byte[] docIdData) { + return new Biff8EncryptionKey(createKeyDigest(password, docIdData)); + } + + Biff8EncryptionKey(byte[] keyDigest) { + if (keyDigest.length != KEY_DIGEST_LENGTH) { + throw new IllegalArgumentException("Expected 5 byte key digest, but got " + HexDump.toHex(keyDigest)); + } + _keyDigest = keyDigest; + } + + static byte[] createKeyDigest(String password, byte[] docIdData) { + check16Bytes(docIdData, "docId"); + int nChars = Math.min(password.length(), 16); + byte[] passwordData = new byte[nChars*2]; + for (int i=0; itrue if the keyDigest is compatible with the specified saltData and saltHash + */ + public boolean validate(byte[] saltData, byte[] saltHash) { + check16Bytes(saltData, "saltData"); + check16Bytes(saltHash, "saltHash"); + + // validation uses the RC4 for block zero + RC4 rc4 = createRC4(0); + byte[] saltDataPrime = saltData.clone(); + rc4.encrypt(saltDataPrime); + + byte[] saltHashPrime = saltHash.clone(); + rc4.encrypt(saltHashPrime); + + MessageDigest md5; + try { + md5 = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + md5.update(saltDataPrime); + byte[] finalSaltResult = md5.digest(); + + return Arrays.equals(saltHashPrime, finalSaltResult); + } + + private static void check16Bytes(byte[] data, String argName) { + if (data.length != 16) { + throw new IllegalArgumentException("Expected 16 byte " + argName + ", but got " + HexDump.toHex(data)); + } + } + + /** + * The {@link RC4} instance needs to be changed every 1024 bytes. + * @param keyBlockNo used to seed the newly created {@link RC4} + */ + RC4 createRC4(int keyBlockNo) { + MessageDigest md5; + try { + md5 = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + + md5.update(_keyDigest); + ByteArrayOutputStream baos = new ByteArrayOutputStream(4); + new LittleEndianOutputStream(baos).writeInt(keyBlockNo); + md5.update(baos.toByteArray()); + + byte[] digest = md5.digest(); + return new RC4(digest); + } + + + /** + * Stores the BIFF8 encryption/decryption password for the current thread. This has been done + * using a {@link ThreadLocal} in order to avoid further overloading the various public APIs + * (e.g. {@link HSSFWorkbook}) that need this functionality. + */ + private static final ThreadLocal _userPasswordTLS = new ThreadLocal(); + + /** + * Sets the BIFF8 encryption/decryption password for the current thread. + * + * @param password pass null to clear user password (and use default) + */ + public static void setCurrentUserPassword(String password) { + _userPasswordTLS.set(password); + } + + /** + * @return the BIFF8 encryption/decryption password for the current thread. + * null if it is currently unset. + */ + public static String getCurrentUserPassword() { + return _userPasswordTLS.get(); + } +} diff --git a/src/java/org/apache/poi/hssf/record/crypto/Biff8RC4.java b/src/java/org/apache/poi/hssf/record/crypto/Biff8RC4.java new file mode 100644 index 0000000000..edda6fff95 --- /dev/null +++ b/src/java/org/apache/poi/hssf/record/crypto/Biff8RC4.java @@ -0,0 +1,197 @@ +/* ==================================================================== + 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.crypto; + +import org.apache.poi.hssf.record.BOFRecord; +import org.apache.poi.hssf.record.FilePassRecord; +import org.apache.poi.hssf.record.InterfaceHdrRecord; + +/** + * Used for both encrypting and decrypting BIFF8 streams. The internal + * {@link RC4} instance is renewed (re-keyed) every 1024 bytes. + * + * @author Josh Micich + */ +final class Biff8RC4 { + + private static final int RC4_REKEYING_INTERVAL = 1024; + + private RC4 _rc4; + /** + * This field is used to keep track of when to change the {@link RC4} + * instance. The change occurs every 1024 bytes. Every byte passed over is + * counted. + */ + private int _streamPos; + private int _nextRC4BlockStart; + private int _currentKeyIndex; + private boolean _shouldSkipEncryptionOnCurrentRecord; + + private final Biff8EncryptionKey _key; + + public Biff8RC4(int initialOffset, Biff8EncryptionKey key) { + if (initialOffset >= RC4_REKEYING_INTERVAL) { + throw new RuntimeException("initialOffset (" + initialOffset + ")>" + + RC4_REKEYING_INTERVAL + " not supported yet"); + } + _key = key; + _streamPos = 0; + rekeyForNextBlock(); + _streamPos = initialOffset; + for (int i = initialOffset; i > 0; i--) { + _rc4.output(); + } + _shouldSkipEncryptionOnCurrentRecord = false; + } + + private void rekeyForNextBlock() { + _currentKeyIndex = _streamPos / RC4_REKEYING_INTERVAL; + _rc4 = _key.createRC4(_currentKeyIndex); + _nextRC4BlockStart = (_currentKeyIndex + 1) * RC4_REKEYING_INTERVAL; + } + + private int getNextRC4Byte() { + if (_streamPos >= _nextRC4BlockStart) { + rekeyForNextBlock(); + } + byte mask = _rc4.output(); + _streamPos++; + if (_shouldSkipEncryptionOnCurrentRecord) { + return 0; + } + return mask & 0xFF; + } + + public void startRecord(int currentSid) { + _shouldSkipEncryptionOnCurrentRecord = isNeverEncryptedRecord(currentSid); + } + + /** + * TODO: Additionally, the lbPlyPos (position_of_BOF) field of the BoundSheet8 record MUST NOT be encrypted. + * + * @return true if record type specified by sid is never encrypted + */ + private static boolean isNeverEncryptedRecord(int sid) { + switch (sid) { + case BOFRecord.sid: + // sheet BOFs for sure + // TODO - find out about chart BOFs + + case InterfaceHdrRecord.sid: + // don't know why this record doesn't seem to get encrypted + + case FilePassRecord.sid: + // this only really counts when writing because FILEPASS is read early + + // UsrExcl(0x0194) + // FileLock + // RRDInfo(0x0196) + // RRDHead(0x0138) + + return true; + } + return false; + } + + /** + * Used when BIFF header fields (sid, size) are being read. The internal + * {@link RC4} instance must step even when unencrypted bytes are read + */ + public void skipTwoBytes() { + getNextRC4Byte(); + getNextRC4Byte(); + } + + public void xor(byte[] buf, int pOffset, int pLen) { + int nLeftInBlock; + nLeftInBlock = _nextRC4BlockStart - _streamPos; + if (pLen <= nLeftInBlock) { + // simple case - this read does not cross key blocks + _rc4.encrypt(buf, pOffset, pLen); + _streamPos += pLen; + return; + } + + int offset = pOffset; + int len = pLen; + + // start by using the rest of the current block + if (len > nLeftInBlock) { + if (nLeftInBlock > 0) { + _rc4.encrypt(buf, offset, nLeftInBlock); + _streamPos += nLeftInBlock; + offset += nLeftInBlock; + len -= nLeftInBlock; + } + rekeyForNextBlock(); + } + // all full blocks following + while (len > RC4_REKEYING_INTERVAL) { + _rc4.encrypt(buf, offset, RC4_REKEYING_INTERVAL); + _streamPos += RC4_REKEYING_INTERVAL; + offset += RC4_REKEYING_INTERVAL; + len -= RC4_REKEYING_INTERVAL; + rekeyForNextBlock(); + } + // finish with incomplete block + _rc4.encrypt(buf, offset, len); + _streamPos += len; + } + + public int xorByte(int rawVal) { + int mask = getNextRC4Byte(); + return (byte) (rawVal ^ mask); + } + + public int xorShort(int rawVal) { + int b0 = getNextRC4Byte(); + int b1 = getNextRC4Byte(); + int mask = (b1 << 8) + (b0 << 0); + return rawVal ^ mask; + } + + public int xorInt(int rawVal) { + int b0 = getNextRC4Byte(); + int b1 = getNextRC4Byte(); + int b2 = getNextRC4Byte(); + int b3 = getNextRC4Byte(); + int mask = (b3 << 24) + (b2 << 16) + (b1 << 8) + (b0 << 0); + return rawVal ^ mask; + } + + public long xorLong(long rawVal) { + int b0 = getNextRC4Byte(); + int b1 = getNextRC4Byte(); + int b2 = getNextRC4Byte(); + int b3 = getNextRC4Byte(); + int b4 = getNextRC4Byte(); + int b5 = getNextRC4Byte(); + int b6 = getNextRC4Byte(); + int b7 = getNextRC4Byte(); + long mask = + (((long)b7) << 56) + + (((long)b6) << 48) + + (((long)b5) << 40) + + (((long)b4) << 32) + + (((long)b3) << 24) + + (b2 << 16) + + (b1 << 8) + + (b0 << 0); + return rawVal ^ mask; + } +} diff --git a/src/java/org/apache/poi/hssf/record/crypto/RC4.java b/src/java/org/apache/poi/hssf/record/crypto/RC4.java new file mode 100644 index 0000000000..a4abcfad8d --- /dev/null +++ b/src/java/org/apache/poi/hssf/record/crypto/RC4.java @@ -0,0 +1,90 @@ +/* ==================================================================== + 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.crypto; + +import org.apache.poi.util.HexDump; + +/** + * Simple implementation of the alleged RC4 algorithm. + * + * Inspired by wikipedia's RC4 article + * + * @author Josh Micich + */ +final class RC4 { + + private int _i, _j; + private final byte[] _s = new byte[256]; + + public RC4(byte[] key) { + int key_length = key.length; + + for (int i = 0; i < 256; i++) + _s[i] = (byte)i; + + for (int i=0, j=0; i < 256; i++) { + byte temp; + + j = (j + key[i % key_length] + _s[i]) & 255; + temp = _s[i]; + _s[i] = _s[j]; + _s[j] = temp; + } + + _i = 0; + _j = 0; + } + + public byte output() { + byte temp; + _i = (_i + 1) & 255; + _j = (_j + _s[_i]) & 255; + + temp = _s[_i]; + _s[_i] = _s[_j]; + _s[_j] = temp; + + return _s[(_s[_i] + _s[_j]) & 255]; + } + + public void encrypt(byte[] in) { + for (int i = 0; i < in.length; i++) { + in[i] = (byte) (in[i] ^ output()); + } + } + public void encrypt(byte[] in, int offset, int len) { + int end = offset+len; + for (int i = offset; i < end; i++) { + in[i] = (byte) (in[i] ^ output()); + } + + } + @Override + public String toString() { + StringBuffer sb = new StringBuffer(); + + sb.append(getClass().getName()).append(" ["); + sb.append("i=").append(_i); + sb.append(" j=").append(_j); + sb.append("]"); + sb.append("\n"); + sb.append(HexDump.dump(_s, 0, 0)); + + return sb.toString(); + } +} diff --git a/src/testcases/org/apache/poi/hssf/data/password.xls b/src/testcases/org/apache/poi/hssf/data/password.xls new file mode 100755 index 0000000000000000000000000000000000000000..a6ad86a1138db1a9dd9b5e5c6bb890a0f9d872bd GIT binary patch literal 22528 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\",B1:B5)\n" + - "2000.0\t2.0\n" + + "2000.0\t2.0\n" + "3000.0\t3.0\n" + - "4000.0\t4.0\n" + + "4000.0\t4.0\n" + "5000.0\t5.0\n" + - "Sheet2\nSheet3\n", + "Sheet2\nSheet3\n", extractor.getText() ); } - + public void testwithContinueRecords() { - + ExcelExtractor extractor = createExtractor("StringContinueRecords.xls"); - + extractor.getText(); - + // Has masses of text // Until we fixed bug #41064, this would've // failed by now assertTrue(extractor.getText().length() > 40960); } - + public void testStringConcat() { - + ExcelExtractor extractor = createExtractor("SimpleWithFormula.xls"); - + // Comes out as NaN if treated as a number // And as XYZ if treated as a string assertEquals("Sheet1\nreplaceme\nreplaceme\nreplacemereplaceme\nSheet2\nSheet3\n", extractor.getText()); - + extractor.setFormulasNotResults(true); - + assertEquals("Sheet1\nreplaceme\nreplaceme\nCONCATENATE(A1,A2)\nSheet2\nSheet3\n", extractor.getText()); } - + public void testStringFormula() { - + ExcelExtractor extractor = createExtractor("StringFormulas.xls"); - + // Comes out as NaN if treated as a number // And as XYZ if treated as a string assertEquals("Sheet1\nXYZ\nSheet2\nSheet3\n", extractor.getText()); - + extractor.setFormulasNotResults(true); - + assertEquals("Sheet1\nUPPER(\"xyz\")\nSheet2\nSheet3\n", extractor.getText()); } - - + + public void testEventExtractor() throws Exception { EventBasedExcelExtractor extractor; - + // First up, a simple file with string // based formulas in it extractor = new EventBasedExcelExtractor( @@ -134,17 +135,17 @@ public final class TestExcelExtractor extends TestCase { ) ); extractor.setIncludeSheetNames(true); - + String text = extractor.getText(); assertEquals("Sheet1\nreplaceme\nreplaceme\nreplacemereplaceme\nSheet2\nSheet3\n", text); - + extractor.setIncludeSheetNames(false); extractor.setFormulasNotResults(true); - + text = extractor.getText(); assertEquals("replaceme\nreplaceme\nCONCATENATE(A1,A2)\n", text); - + // Now, a slightly longer file with numeric formulas extractor = new EventBasedExcelExtractor( new POIFSFileSystem( @@ -157,14 +158,14 @@ public final class TestExcelExtractor extends TestCase { text = extractor.getText(); assertEquals( "1000.0\t1.0\tSUMIF(A1:A5,\">4000\",B1:B5)\n" + - "2000.0\t2.0\n" + + "2000.0\t2.0\n" + "3000.0\t3.0\n" + - "4000.0\t4.0\n" + + "4000.0\t4.0\n" + "5000.0\t5.0\n", text ); } - + public void testWithComments() { ExcelExtractor extractor = createExtractor("SimpleWithComments.xls"); extractor.setIncludeSheetNames(false); @@ -172,34 +173,34 @@ public final class TestExcelExtractor extends TestCase { // Check without comments assertEquals( "1.0\tone\n" + - "2.0\ttwo\n" + - "3.0\tthree\n", + "2.0\ttwo\n" + + "3.0\tthree\n", extractor.getText() ); - + // Now with extractor.setIncludeCellComments(true); assertEquals( "1.0\tone Comment by Yegor Kozlov: Yegor Kozlov: first cell\n" + - "2.0\ttwo Comment by Yegor Kozlov: Yegor Kozlov: second cell\n" + - "3.0\tthree Comment by Yegor Kozlov: Yegor Kozlov: third cell\n", + "2.0\ttwo Comment by Yegor Kozlov: Yegor Kozlov: second cell\n" + + "3.0\tthree Comment by Yegor Kozlov: Yegor Kozlov: third cell\n", extractor.getText() ); } - + public void testWithBlank() { ExcelExtractor extractor = createExtractor("MissingBits.xls"); String def = extractor.getText(); extractor.setIncludeBlankCells(true); String padded = extractor.getText(); - + assertTrue(def.startsWith( "Sheet1\n" + "&[TAB]\t\n" + "Hello\n" + "11.0\t23.0\n" )); - + assertTrue(padded.startsWith( "Sheet1\n" + "&[TAB]\t\n" + @@ -207,8 +208,8 @@ public final class TestExcelExtractor extends TestCase { "11.0\t\t\t23.0\n" )); } - - + + /** * Embded in a non-excel file */ @@ -219,22 +220,22 @@ public final class TestExcelExtractor extends TestCase { POIFSFileSystem fs = new POIFSFileSystem( new FileInputStream(filename) ); - + DirectoryNode objPool = (DirectoryNode) fs.getRoot().getEntry("ObjectPool"); DirectoryNode dirA = (DirectoryNode) objPool.getEntry("_1269427460"); DirectoryNode dirB = (DirectoryNode) objPool.getEntry("_1269427461"); HSSFWorkbook wbA = new HSSFWorkbook(dirA, fs, true); HSSFWorkbook wbB = new HSSFWorkbook(dirB, fs, true); - + ExcelExtractor exA = new ExcelExtractor(wbA); ExcelExtractor exB = new ExcelExtractor(wbB); - - assertEquals("Sheet1\nTest excel file\nThis is the first file\nSheet2\nSheet3\n", + + assertEquals("Sheet1\nTest excel file\nThis is the first file\nSheet2\nSheet3\n", exA.getText()); assertEquals("Sample Excel", exA.getSummaryInformation().getTitle()); - - assertEquals("Sheet1\nAnother excel file\nThis is the second file\nSheet2\nSheet3\n", + + assertEquals("Sheet1\nAnother excel file\nThis is the second file\nSheet2\nSheet3\n", exB.getText()); assertEquals("Sample Excel 2", exB.getSummaryInformation().getTitle()); } @@ -249,37 +250,37 @@ public final class TestExcelExtractor extends TestCase { POIFSFileSystem fs = new POIFSFileSystem( new FileInputStream(filename) ); - + DirectoryNode dirA = (DirectoryNode) fs.getRoot().getEntry("MBD0000A3B5"); DirectoryNode dirB = (DirectoryNode) fs.getRoot().getEntry("MBD0000A3B4"); - + HSSFWorkbook wbA = new HSSFWorkbook(dirA, fs, true); HSSFWorkbook wbB = new HSSFWorkbook(dirB, fs, true); - + ExcelExtractor exA = new ExcelExtractor(wbA); ExcelExtractor exB = new ExcelExtractor(wbB); - - assertEquals("Sheet1\nTest excel file\nThis is the first file\nSheet2\nSheet3\n", + + assertEquals("Sheet1\nTest excel file\nThis is the first file\nSheet2\nSheet3\n", exA.getText()); assertEquals("Sample Excel", exA.getSummaryInformation().getTitle()); - - assertEquals("Sheet1\nAnother excel file\nThis is the second file\nSheet2\nSheet3\n", + + assertEquals("Sheet1\nAnother excel file\nThis is the second file\nSheet2\nSheet3\n", exB.getText()); assertEquals("Sample Excel 2", exB.getSummaryInformation().getTitle()); - + // And the base file too ExcelExtractor ex = new ExcelExtractor(fs); assertEquals("Sheet1\nI have lots of embeded files in me\nSheet2\nSheet3\n", ex.getText()); assertEquals("Excel With Embeded", ex.getSummaryInformation().getTitle()); } - + /** * Test that we get text from headers and footers */ public void test45538() { String[] files = { - "45538_classic_Footer.xls", "45538_form_Footer.xls", + "45538_classic_Footer.xls", "45538_form_Footer.xls", "45538_classic_Header.xls", "45538_form_Header.xls" }; for(int i=0; i= 0); } } + + public void testPassword() { + Biff8EncryptionKey.setCurrentUserPassword("password"); + ExcelExtractor extractor = createExtractor("password.xls"); + String text = extractor.getText(); + Biff8EncryptionKey.setCurrentUserPassword(null); + + assertTrue(text.contains("ZIP")); + } } diff --git a/src/testcases/org/apache/poi/hssf/record/AllRecordTests.java b/src/testcases/org/apache/poi/hssf/record/AllRecordTests.java index afd4893137..5aec73999a 100755 --- a/src/testcases/org/apache/poi/hssf/record/AllRecordTests.java +++ b/src/testcases/org/apache/poi/hssf/record/AllRecordTests.java @@ -24,20 +24,22 @@ import org.apache.poi.hssf.record.aggregates.AllRecordAggregateTests; import org.apache.poi.hssf.record.cf.TestCellRange; import org.apache.poi.hssf.record.chart.AllChartRecordTests; import org.apache.poi.hssf.record.constant.TestConstantValueParser; +import org.apache.poi.hssf.record.crypto.AllHSSFEncryptionTests; import org.apache.poi.hssf.record.formula.AllFormulaTests; import org.apache.poi.hssf.record.pivot.AllPivotRecordTests; /** * Collects all tests for package org.apache.poi.hssf.record and sub-packages. - * + * * @author Josh Micich */ public final class AllRecordTests { - + public static Test suite() { TestSuite result = new TestSuite(AllRecordTests.class.getName()); result.addTest(AllChartRecordTests.suite()); + result.addTest(AllHSSFEncryptionTests.suite()); result.addTest(AllFormulaTests.suite()); result.addTest(AllPivotRecordTests.suite()); result.addTest(AllRecordAggregateTests.suite()); diff --git a/src/testcases/org/apache/poi/hssf/record/TestRecordFactory.java b/src/testcases/org/apache/poi/hssf/record/TestRecordFactory.java index cd01ca0419..4dface4251 100644 --- a/src/testcases/org/apache/poi/hssf/record/TestRecordFactory.java +++ b/src/testcases/org/apache/poi/hssf/record/TestRecordFactory.java @@ -155,7 +155,7 @@ public final class TestRecordFactory extends TestCase { */ public void testMixedContinue() throws Exception { /** - * Adapted from a real test sample file 39512.xls (Offset 0x4854). + * Adapted from a real test sample file 39512.xls (Offset 0x4854). * See Bug 39512 for details. */ String dump = @@ -208,6 +208,7 @@ public final class TestRecordFactory extends TestCase { public void testNonZeroPadding_bug46987() { Record[] recs = { new BOFRecord(), + new WriteAccessRecord(), // need *something* between BOF and EOF EOFRecord.instance, BOFRecord.createSheetBOF(), EOFRecord.instance, @@ -229,7 +230,7 @@ public final class TestRecordFactory extends TestCase { baos.write(0x00); } - + POIFSFileSystem fs = new POIFSFileSystem(); InputStream is; try { @@ -237,7 +238,7 @@ public final class TestRecordFactory extends TestCase { is = fs.getRoot().createDocumentInputStream("dummy"); } catch (IOException e) { throw new RuntimeException(e); - } + } List outRecs; try { @@ -248,7 +249,6 @@ public final class TestRecordFactory extends TestCase { } throw e; } - assertEquals(4, outRecs.size()); - + assertEquals(5, outRecs.size()); } } diff --git a/src/testcases/org/apache/poi/hssf/record/crypto/AllHSSFEncryptionTests.java b/src/testcases/org/apache/poi/hssf/record/crypto/AllHSSFEncryptionTests.java new file mode 100644 index 0000000000..4d56858c95 --- /dev/null +++ b/src/testcases/org/apache/poi/hssf/record/crypto/AllHSSFEncryptionTests.java @@ -0,0 +1,38 @@ +/* ==================================================================== + 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.crypto; + +import junit.framework.Test; +import junit.framework.TestSuite; + +/** + * Collects all tests for package org.apache.poi.hssf.record.crypto. + * + * @author Josh Micich + */ +public final class AllHSSFEncryptionTests { + + public static Test suite() { + TestSuite result = new TestSuite(AllHSSFEncryptionTests.class.getName()); + + result.addTestSuite(TestBiff8DecryptingStream.class); + result.addTestSuite(TestRC4.class); + result.addTestSuite(TestBiff8EncryptionKey.class); + return result; + } +} diff --git a/src/testcases/org/apache/poi/hssf/record/crypto/TestBiff8DecryptingStream.java b/src/testcases/org/apache/poi/hssf/record/crypto/TestBiff8DecryptingStream.java new file mode 100644 index 0000000000..00c860ad0e --- /dev/null +++ b/src/testcases/org/apache/poi/hssf/record/crypto/TestBiff8DecryptingStream.java @@ -0,0 +1,230 @@ +/* ==================================================================== + 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.crypto; + +import java.io.InputStream; +import java.util.Arrays; + +import junit.framework.AssertionFailedError; +import junit.framework.ComparisonFailure; +import junit.framework.TestCase; + +import org.apache.poi.util.HexDump; +import org.apache.poi.util.HexRead; + +/** + * Tests for {@link Biff8DecryptingStream} + * + * @author Josh Micich + */ +public final class TestBiff8DecryptingStream extends TestCase { + + /** + * A mock {@link InputStream} that keeps track of position and also produces + * slightly interesting data. Each successive data byte value is one greater + * than the previous. + */ + private static final class MockStream extends InputStream { + private int _val; + private int _position; + + public MockStream(int initialValue) { + _val = initialValue & 0xFF; + } + public int read() { + _position++; + return _val++ & 0xFF; + } + public int getPosition() { + return _position; + } + } + + private static final class StreamTester { + private static final boolean ONLY_LOG_ERRORS = true; + + private final MockStream _ms; + private final Biff8DecryptingStream _bds; + private boolean _errorsOccurred; + + /** + * @param expectedFirstInt expected value of the first int read from the decrypted stream + */ + public StreamTester(MockStream ms, String keyDigestHex, int expectedFirstInt) { + _ms = ms; + byte[] keyDigest = HexRead.readFromString(keyDigestHex); + _bds = new Biff8DecryptingStream(_ms, 0, new Biff8EncryptionKey(keyDigest)); + assertEquals(expectedFirstInt, _bds.readInt()); + _errorsOccurred = false; + } + + public Biff8DecryptingStream getBDS() { + return _bds; + } + + /** + * Used to 'skip over' the uninteresting middle bits of the key blocks. + * Also confirms that read position of the underlying stream is aligned. + */ + public void rollForward(int fromPosition, int toPosition) { + assertEquals(fromPosition, _ms.getPosition()); + for (int i = fromPosition; i < toPosition; i++) { + _bds.readByte(); + } + assertEquals(toPosition, _ms.getPosition()); + } + + public void confirmByte(int expVal) { + cmp(HexDump.byteToHex(expVal), HexDump.byteToHex(_bds.readUByte())); + } + + public void confirmShort(int expVal) { + cmp(HexDump.shortToHex(expVal), HexDump.shortToHex(_bds.readUShort())); + } + + public void confirmInt(int expVal) { + cmp(HexDump.intToHex(expVal), HexDump.intToHex(_bds.readInt())); + } + + public void confirmLong(long expVal) { + cmp(HexDump.longToHex(expVal), HexDump.longToHex(_bds.readLong())); + } + + private void cmp(char[] exp, char[] act) { + if (Arrays.equals(exp, act)) { + return; + } + _errorsOccurred = true; + if (ONLY_LOG_ERRORS) { + logErr(3, "Value mismatch " + new String(exp) + " - " + new String(act)); + return; + } + throw new ComparisonFailure("Value mismatch", new String(exp), new String(act)); + } + + public void confirmData(String expHexData) { + + byte[] expData = HexRead.readFromString(expHexData); + byte[] actData = new byte[expData.length]; + _bds.readFully(actData); + if (Arrays.equals(expData, actData)) { + return; + } + _errorsOccurred = true; + if (ONLY_LOG_ERRORS) { + logErr(2, "Data mismatch " + HexDump.toHex(expData) + " - " + + HexDump.toHex(actData)); + return; + } + throw new ComparisonFailure("Data mismatch", HexDump.toHex(expData), HexDump.toHex(actData)); + } + + private static void logErr(int stackFrameCount, String msg) { + StackTraceElement ste = new Exception().getStackTrace()[stackFrameCount]; + System.err.print("(" + ste.getFileName() + ":" + ste.getLineNumber() + ") "); + System.err.println(msg); + } + + public void assertNoErrors() { + assertFalse("Some values decrypted incorrectly", _errorsOccurred); + } + } + + /** + * Tests reading of 64,32,16 and 8 bit integers aligned with key changing boundaries + */ + public void testReadsAlignedWithBoundary() { + StreamTester st = createStreamTester(0x50, "BA AD F0 0D 00", 0x96C66829); + + st.rollForward(0x0004, 0x03FF); + st.confirmByte(0x3E); + st.confirmByte(0x28); + st.rollForward(0x0401, 0x07FE); + st.confirmShort(0x76CC); + st.confirmShort(0xD83E); + st.rollForward(0x0802, 0x0BFC); + st.confirmInt(0x25F280EB); + st.confirmInt(0xB549E99B); + st.rollForward(0x0C04, 0x0FF8); + st.confirmLong(0x6AA2D5F6B975D10CL); + st.confirmLong(0x34248ADF7ED4F029L); + st.assertNoErrors(); + } + + /** + * Tests reading of 64,32 and 16 bit integers across key changing boundaries + */ + public void testReadsSpanningBoundary() { + StreamTester st = createStreamTester(0x50, "BA AD F0 0D 00", 0x96C66829); + + st.rollForward(0x0004, 0x03FC); + st.confirmLong(0x885243283E2A5EEFL); + st.rollForward(0x0404, 0x07FE); + st.confirmInt(0xD83E76CC); + st.rollForward(0x0802, 0x0BFF); + st.confirmShort(0x9B25); + st.assertNoErrors(); + } + + /** + * Checks that the BIFF header fields (sid, size) get read without applying decryption, + * and that the RC4 stream stays aligned during these calls + */ + public void testReadHeaderUShort() { + StreamTester st = createStreamTester(0x50, "BA AD F0 0D 00", 0x96C66829); + + st.rollForward(0x0004, 0x03FF); + + Biff8DecryptingStream bds = st.getBDS(); + int hval = bds.readDataSize(); // unencrypted + int nextInt = bds.readInt(); + if (nextInt == 0x8F534029) { + throw new AssertionFailedError( + "Indentified bug in key alignment after call to readHeaderUShort()"); + } + assertEquals(0x16885243, nextInt); + if (hval == 0x283E) { + throw new AssertionFailedError("readHeaderUShort() incorrectly decrypted result"); + } + assertEquals(0x504F, hval); + + // confirm next key change + st.rollForward(0x0405, 0x07FC); + st.confirmInt(0x76CC1223); + st.confirmInt(0x4842D83E); + st.assertNoErrors(); + } + + /** + * Tests reading of byte sequences across and aligned with key changing boundaries + */ + public void testReadByteArrays() { + StreamTester st = createStreamTester(0x50, "BA AD F0 0D 00", 0x96C66829); + + st.rollForward(0x0004, 0x2FFC); + st.confirmData("66 A1 20 B1 04 A3 35 F5"); // 4 bytes on either side of boundary + st.rollForward(0x3004, 0x33F8); + st.confirmData("F8 97 59 36"); // last 4 bytes in block + st.confirmData("01 C2 4E 55"); // first 4 bytes in next block + st.assertNoErrors(); + } + + private static StreamTester createStreamTester(int mockStreamStartVal, String keyDigestHex, int expectedFirstInt) { + return new StreamTester(new MockStream(mockStreamStartVal), keyDigestHex, expectedFirstInt); + } +} diff --git a/src/testcases/org/apache/poi/hssf/record/crypto/TestBiff8EncryptionKey.java b/src/testcases/org/apache/poi/hssf/record/crypto/TestBiff8EncryptionKey.java new file mode 100644 index 0000000000..7c6ad42a74 --- /dev/null +++ b/src/testcases/org/apache/poi/hssf/record/crypto/TestBiff8EncryptionKey.java @@ -0,0 +1,102 @@ +/* ==================================================================== + 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.crypto; + +import java.util.Arrays; + +import org.apache.poi.util.HexDump; +import org.apache.poi.util.HexRead; + +import junit.framework.ComparisonFailure; +import junit.framework.TestCase; + +/** + * Tests for {@link Biff8EncryptionKey} + * + * @author Josh Micich + */ +public final class TestBiff8EncryptionKey extends TestCase { + + private static byte[] fromHex(String hexString) { + return HexRead.readFromString(hexString); + } + public void testCreateKeyDigest() { + byte[] docIdData = fromHex("17 F6 D1 6B 09 B1 5F 7B 4C 9D 03 B4 81 B5 B4 4A"); + byte[] keyDigest = Biff8EncryptionKey.createKeyDigest("MoneyForNothing", docIdData); + byte[] expResult = fromHex("C2 D9 56 B2 6B"); + if (!Arrays.equals(expResult, keyDigest)) { + throw new ComparisonFailure("keyDigest mismatch", HexDump.toHex(expResult), HexDump.toHex(keyDigest)); + } + } + + + public void testValidateWithDefaultPassword() { + + String docIdSuffixA = "F 35 52 38 0D 75 4A E6 85 C2 FD 78 CE 3D D1 B6"; // valid prefix is 'D' + String saltHashA = "30 38 BE 5E 93 C5 7E B4 5F 52 CD A1 C6 8F B6 2A"; + String saltDataA = "D4 04 43 EC B7 A7 6F 6A D2 68 C7 DF CF A8 80 68"; + + String docIdB = "39 D7 80 41 DA E4 74 2C 8C 84 F9 4D 39 9A 19 2D"; + String saltDataSuffixB = "3 EA 8D 52 11 11 37 D2 BD 55 4C 01 0A 47 6E EB"; // valid prefix is 'C' + String saltHashB = "96 19 F5 D0 F1 63 08 F1 3E 09 40 1E 87 F0 4E 16"; + + confirmValid(true, "D" + docIdSuffixA, saltDataA, saltHashA); + confirmValid(true, docIdB, "C" + saltDataSuffixB, saltHashB); + confirmValid(false, "E" + docIdSuffixA, saltDataA, saltHashA); + confirmValid(false, docIdB, "B" + saltDataSuffixB, saltHashB); + } + + public void testValidateWithSuppliedPassword() { + + String docId = "DF 35 52 38 0D 75 4A E6 85 C2 FD 78 CE 3D D1 B6"; + String saltData = "D4 04 43 EC B7 A7 6F 6A D2 68 C7 DF CF A8 80 68"; + String saltHashA = "8D C2 63 CC E1 1D E0 05 20 16 96 AF 48 59 94 64"; // for password '5ecret' + String saltHashB = "31 0B 0D A4 69 55 8E 27 A1 03 AD C9 AE F8 09 04"; // for password '5ecret' + + confirmValid(true, docId, saltData, saltHashA, "5ecret"); + confirmValid(false, docId, saltData, saltHashA, "Secret"); + confirmValid(true, docId, saltData, saltHashB, "Secret"); + confirmValid(false, docId, saltData, saltHashB, "secret"); + } + + + private static void confirmValid(boolean expectedResult, + String docIdHex, String saltDataHex, String saltHashHex) { + confirmValid(expectedResult, docIdHex, saltDataHex, saltHashHex, null); + } + private static void confirmValid(boolean expectedResult, + String docIdHex, String saltDataHex, String saltHashHex, String password) { + byte[] docId = fromHex(docIdHex); + byte[] saltData = fromHex(saltDataHex); + byte[] saltHash = fromHex(saltHashHex); + + + Biff8EncryptionKey key; + if (password == null) { + key = Biff8EncryptionKey.create(docId); + } else { + key = Biff8EncryptionKey.create(password, docId); + } + boolean actResult = key.validate(saltData, saltHash); + if (expectedResult) { + assertTrue("validate failed", actResult); + } else { + assertFalse("validate succeeded unexpectedly", actResult); + } + } +} diff --git a/src/testcases/org/apache/poi/hssf/record/crypto/TestRC4.java b/src/testcases/org/apache/poi/hssf/record/crypto/TestRC4.java new file mode 100644 index 0000000000..35b6da4fd7 --- /dev/null +++ b/src/testcases/org/apache/poi/hssf/record/crypto/TestRC4.java @@ -0,0 +1,76 @@ +/* ==================================================================== + 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.crypto; + +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.util.Arrays; + +import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; + +import junit.framework.ComparisonFailure; +import junit.framework.TestCase; + +import org.apache.poi.util.HexDump; +import org.apache.poi.util.HexRead; + +/** + * Tests for {@link RC4} + * + * @author Josh Micich + */ +public class TestRC4 extends TestCase { + public void testSimple() { + confirmRC4("Key", "Plaintext", "BBF316E8D940AF0AD3"); + confirmRC4("Wiki", "pedia", "1021BF0420"); + confirmRC4("Secret", "Attack at dawn", "45A01F645FC35B383552544B9BF5"); + + } + + private static void confirmRC4(String k, String origText, String expEncrHex) { + byte[] actEncr = origText.getBytes(); + new RC4(k.getBytes()).encrypt(actEncr); + byte[] expEncr = HexRead.readFromString(expEncrHex); + + if (!Arrays.equals(expEncr, actEncr)) { + throw new ComparisonFailure("Data mismatch", HexDump.toHex(expEncr), HexDump.toHex(actEncr)); + } + + + Cipher cipher; + try { + cipher = Cipher.getInstance("RC4"); + } catch (GeneralSecurityException e) { + throw new RuntimeException(e); + } + String k2 = k+k; // Sun has minimum of 5 bytes for key + SecretKeySpec skeySpec = new SecretKeySpec(k2.getBytes(), "RC4"); + + try { + cipher.init(Cipher.DECRYPT_MODE, skeySpec); + } catch (InvalidKeyException e) { + throw new RuntimeException(e); + } + byte[] origData = origText.getBytes(); + byte[] altEncr = cipher.update(origData); + if (!Arrays.equals(expEncr, altEncr)) { + throw new RuntimeException("Mismatch from jdk provider"); + } + } +} -- 2.39.5