git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@801890 13f79535-47bb-0310-9956-ffa450edef68tags/REL_3_5-FINAL
@@ -33,6 +33,7 @@ | |||
<changes> | |||
<release version="3.5-beta7" date="2009-??-??"> | |||
<action dev="POI-DEVELOPERS" type="add">47652 - Added support for reading encrypted workbooks</action> | |||
<action dev="POI-DEVELOPERS" type="add">47604 - Implementation of an XML to XLSX Importer using Custom XML Mapping</action> | |||
<action dev="POI-DEVELOPERS" type="fix">47620 - Avoid FormulaParseException in XSSFWorkbook.setRepeatingRowsAndColumns when removing repeated rows and columns</action> | |||
<action dev="POI-DEVELOPERS" type="fix">47606 - Fixed XSSFCell to correctly parse column indexes greater than 702 (ZZ)</action> |
@@ -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; |
@@ -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; | |||
@@ -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(); | |||
} |
@@ -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<P> | |||
* 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.<p> | |||
* REFERENCE: PG 420 Microsoft Excel 97 Developer's Kit (ISBN: 1-57231-498-2)<P> | |||
* Title: File Pass Record (0x002F) <p/> | |||
* | |||
* 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(); | |||
} | |||
} |
@@ -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<P> | |||
* Description: Takes a stream and outputs an array of Record objects.<P> | |||
@@ -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<Record> createRecords(InputStream in) throws RecordFormatException { | |||
List<Record> records = new ArrayList<Record>(NUM_RECORDS); | |||
RecordFactoryInputStream recStream = new RecordFactoryInputStream(new RecordInputStream(in), true); | |||
RecordFactoryInputStream recStream = new RecordFactoryInputStream(in, true); | |||
Record record; | |||
while ((record = recStream.nextRecord())!=null) { |
@@ -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<Record> 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 <code>null</code> | |||
* if stream was empty | |||
*/ | |||
public Record getLastRecord() { | |||
return _lastRecord; | |||
} | |||
/** | |||
* <code>false</code> 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<Record> records = new ArrayList<Record>(); | |||
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}. <code>null</code> 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]; | |||
} | |||
@@ -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; | |||
} |
@@ -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()); | |||
} | |||
} |
@@ -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; i<nChars; i++) { | |||
char ch = password.charAt(i); | |||
passwordData[i*2+0] = (byte) ((ch << 0) & 0xFF); | |||
passwordData[i*2+1] = (byte) ((ch << 8) & 0xFF); | |||
} | |||
byte[] kd; | |||
MessageDigest md5; | |||
try { | |||
md5 = MessageDigest.getInstance("MD5"); | |||
} catch (NoSuchAlgorithmException e) { | |||
throw new RuntimeException(e); | |||
} | |||
md5.update(passwordData); | |||
byte[] passwordHash = md5.digest(); | |||
md5.reset(); | |||
for (int i=0; i<16; i++) { | |||
md5.update(passwordHash, 0, PASSWORD_HASH_NUMBER_OF_BYTES_USED); | |||
md5.update(docIdData, 0, docIdData.length); | |||
} | |||
kd = md5.digest(); | |||
byte[] result = new byte[KEY_DIGEST_LENGTH]; | |||
System.arraycopy(kd, 0, result, 0, KEY_DIGEST_LENGTH); | |||
return result; | |||
} | |||
/** | |||
* @return <code>true</code> 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<String> _userPasswordTLS = new ThreadLocal<String>(); | |||
/** | |||
* Sets the BIFF8 encryption/decryption password for the current thread. | |||
* | |||
* @param password pass <code>null</code> 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. | |||
* <code>null</code> if it is currently unset. | |||
*/ | |||
public static String getCurrentUserPassword() { | |||
return _userPasswordTLS.get(); | |||
} | |||
} |
@@ -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 <code>true</code> if record type specified by <tt>sid</tt> 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; | |||
} | |||
} |
@@ -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 <A HREF="http://en.wikipedia.org/wiki/RC4">wikipedia's RC4 article</A> | |||
* | |||
* @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(); | |||
} | |||
} |
@@ -24,108 +24,109 @@ import java.io.InputStream; | |||
import junit.framework.TestCase; | |||
import org.apache.poi.hssf.HSSFTestDataSamples; | |||
import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey; | |||
import org.apache.poi.hssf.usermodel.HSSFWorkbook; | |||
import org.apache.poi.poifs.filesystem.DirectoryNode; | |||
import org.apache.poi.poifs.filesystem.POIFSFileSystem; | |||
/** | |||
* | |||
* | |||
*/ | |||
public final class TestExcelExtractor extends TestCase { | |||
private static ExcelExtractor createExtractor(String sampleFileName) { | |||
InputStream is = HSSFTestDataSamples.openSampleFileStream(sampleFileName); | |||
try { | |||
return new ExcelExtractor(new POIFSFileSystem(is)); | |||
} catch (IOException e) { | |||
throw new RuntimeException(e); | |||
} | |||
} | |||
public void testSimple() { | |||
ExcelExtractor extractor = createExtractor("Simple.xls"); | |||
assertEquals("Sheet1\nreplaceMe\nSheet2\nSheet3\n", extractor.getText()); | |||
// Now turn off sheet names | |||
extractor.setIncludeSheetNames(false); | |||
assertEquals("replaceMe\n", extractor.getText()); | |||
} | |||
public void testNumericFormula() { | |||
ExcelExtractor extractor = createExtractor("sumifformula.xls"); | |||
assertEquals( | |||
"Sheet1\n" + | |||
"1000.0\t1.0\t5.0\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() | |||
); | |||
extractor.setFormulasNotResults(true); | |||
assertEquals( | |||
"Sheet1\n" + | |||
"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" + | |||
"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<files.length; i++) { | |||
@@ -289,4 +290,13 @@ public final class TestExcelExtractor extends TestCase { | |||
assertTrue("Unable to find expected word in text\n" + text, text.indexOf("test phrase") >= 0); | |||
} | |||
} | |||
public void testPassword() { | |||
Biff8EncryptionKey.setCurrentUserPassword("password"); | |||
ExcelExtractor extractor = createExtractor("password.xls"); | |||
String text = extractor.getText(); | |||
Biff8EncryptionKey.setCurrentUserPassword(null); | |||
assertTrue(text.contains("ZIP")); | |||
} | |||
} |
@@ -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 <tt>org.apache.poi.hssf.record</tt> 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()); |
@@ -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<Record> outRecs; | |||
try { | |||
@@ -248,7 +249,6 @@ public final class TestRecordFactory extends TestCase { | |||
} | |||
throw e; | |||
} | |||
assertEquals(4, outRecs.size()); | |||
assertEquals(5, outRecs.size()); | |||
} | |||
} |
@@ -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 <tt>org.apache.poi.hssf.record.crypto</tt>. | |||
* | |||
* @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; | |||
} | |||
} |
@@ -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 <i>across</i> 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 <i>across</i> and <i>aligned with</i> 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); | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} |
@@ -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"); | |||
} | |||
} | |||
} |