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
This commit is contained in:
Josh Micich 2009-08-07 06:03:31 +00:00
parent 755b86af67
commit c7ef83811b
20 changed files with 1452 additions and 211 deletions

View File

@ -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>

View File

@ -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;
}
ps.print(record.toString());
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());
}
} 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;

View File

@ -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;

View File

@ -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();
}

View File

@ -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;
public final class FilePassRecord
extends StandardRecord
{
public final static short sid = 0x2F;
private int field_1_encryptedpassword;
private static final int ENCRYPTION_XOR = 0;
private static final int ENCRYPTION_OTHER = 1;
public FilePassRecord()
{
}
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)
{
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();
public FilePassRecord(RecordInputStream in) {
_encryptionType = in.readUShort();
buffer.append("[FILEPASS]\n");
buffer.append(" .password = ").append(field_1_encryptedpassword)
.append("\n");
buffer.append("[/FILEPASS]\n");
return buffer.toString();
}
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);
}
public void serialize(LittleEndianOutput out) {
out.writeInt(( short ) field_1_encryptedpassword);
}
private static byte[] read(RecordInputStream in, int size) {
byte[] result = new byte[size];
in.readFully(result);
return result;
}
protected int getDataSize() {
return 4;
}
public void serialize(LittleEndianOutput out) {
out.writeShort(_encryptionType);
out.writeShort(_encryptionInfo);
out.writeShort(_minorVersionNo);
out.write(_docId);
out.write(_saltData);
out.write(_saltHash);
}
public short getSid()
{
return sid;
}
protected int getDataSize() {
return 54;
}
public Object clone() {
FilePassRecord rec = new FilePassRecord();
rec.field_1_encryptedpassword = field_1_encryptedpassword;
return rec;
}
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();
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();
}
}

View File

@ -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) {

View File

@ -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];
}

View File

@ -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;
}

View File

@ -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());
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

Binary file not shown.

View File

@ -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"));
}
}

View File

@ -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());

View File

@ -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());
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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");
}
}
}