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:
parent
755b86af67
commit
c7ef83811b
@ -33,6 +33,7 @@
|
|||||||
|
|
||||||
<changes>
|
<changes>
|
||||||
<release version="3.5-beta7" date="2009-??-??">
|
<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="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">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>
|
<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) {
|
if (recStream.getSid() == 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Record record = createRecord (recStream);
|
Record record;
|
||||||
if (record.getSid() == ContinueRecord.sid) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
temp.add(record);
|
|
||||||
if (dumpInterpretedRecords) {
|
if (dumpInterpretedRecords) {
|
||||||
String[] headers = recListener.getRecentHeaders();
|
record = createRecord (recStream);
|
||||||
for (int i = 0; i < headers.length; i++) {
|
if (record.getSid() == ContinueRecord.sid) {
|
||||||
ps.println(headers[i]);
|
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();
|
ps.println();
|
||||||
}
|
}
|
||||||
@ -296,8 +302,8 @@ public final class BiffViewer {
|
|||||||
out = true;
|
out = true;
|
||||||
} else if ("--escher".equals(arg)) {
|
} else if ("--escher".equals(arg)) {
|
||||||
System.setProperty("poi.deserialize.escher", "true");
|
System.setProperty("poi.deserialize.escher", "true");
|
||||||
} else if ("--rawhex".equals(arg)) {
|
} else if ("--rawhex".equals(arg)) {
|
||||||
rawhex = true;
|
rawhex = true;
|
||||||
} else {
|
} else {
|
||||||
throw new CommandParseException("Unexpected option '" + arg + "'");
|
throw new CommandParseException("Unexpected option '" + arg + "'");
|
||||||
}
|
}
|
||||||
@ -389,7 +395,7 @@ public final class BiffViewer {
|
|||||||
} else {
|
} else {
|
||||||
boolean dumpInterpretedRecords = cmdArgs.shouldDumpRecordInterpretations();
|
boolean dumpInterpretedRecords = cmdArgs.shouldDumpRecordInterpretations();
|
||||||
boolean dumpHex = cmdArgs.shouldDumpBiffHex();
|
boolean dumpHex = cmdArgs.shouldDumpBiffHex();
|
||||||
boolean zeroAlignHexDump = dumpInterpretedRecords;
|
boolean zeroAlignHexDump = dumpInterpretedRecords; // TODO - fix non-zeroAlign
|
||||||
BiffRecordListener recListener = new BiffRecordListener(dumpHex ? new OutputStreamWriter(ps) : null, zeroAlignHexDump);
|
BiffRecordListener recListener = new BiffRecordListener(dumpHex ? new OutputStreamWriter(ps) : null, zeroAlignHexDump);
|
||||||
is = new BiffDumpingStream(is, recListener);
|
is = new BiffDumpingStream(is, recListener);
|
||||||
createRecords(is, ps, recListener, dumpInterpretedRecords);
|
createRecords(is, ps, recListener, dumpInterpretedRecords);
|
||||||
@ -558,11 +564,21 @@ public final class BiffViewer {
|
|||||||
int startDelta = globalStart % DUMP_LINE_LEN;
|
int startDelta = globalStart % DUMP_LINE_LEN;
|
||||||
int endDelta = globalEnd % DUMP_LINE_LEN;
|
int endDelta = globalEnd % DUMP_LINE_LEN;
|
||||||
if (zeroAlignEachRecord) {
|
if (zeroAlignEachRecord) {
|
||||||
|
endDelta -= startDelta;
|
||||||
|
if (endDelta < 0) {
|
||||||
|
endDelta += DUMP_LINE_LEN;
|
||||||
|
}
|
||||||
startDelta = 0;
|
startDelta = 0;
|
||||||
endDelta = 0;
|
|
||||||
}
|
}
|
||||||
int startLineAddr = globalStart - startDelta;
|
int startLineAddr;
|
||||||
int endLineAddr = globalEnd - endDelta;
|
int endLineAddr;
|
||||||
|
if (zeroAlignEachRecord) {
|
||||||
|
endLineAddr = globalEnd - endDelta - (globalStart - startDelta);
|
||||||
|
startLineAddr = 0;
|
||||||
|
} else {
|
||||||
|
startLineAddr = globalStart - startDelta;
|
||||||
|
endLineAddr = globalEnd - endDelta;
|
||||||
|
}
|
||||||
|
|
||||||
int lineDataOffset = baseDataOffset - startDelta;
|
int lineDataOffset = baseDataOffset - startDelta;
|
||||||
int lineAddr = startLineAddr;
|
int lineAddr = startLineAddr;
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
/* ====================================================================
|
/* ====================================================================
|
||||||
Licensed to the Apache Software Foundation (ASF) under one or more
|
Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
contributor license agreements. See the NOTICE file distributed with
|
contributor license agreements. See the NOTICE file distributed with
|
||||||
@ -83,7 +82,7 @@ public class HSSFEventFactory {
|
|||||||
*/
|
*/
|
||||||
public void processEvents(HSSFRequest req, InputStream in) {
|
public void processEvents(HSSFRequest req, InputStream in) {
|
||||||
try {
|
try {
|
||||||
genericProcessEvents(req, new RecordInputStream(in));
|
genericProcessEvents(req, in);
|
||||||
} catch (HSSFUserException hue) {
|
} catch (HSSFUserException hue) {
|
||||||
/*If an HSSFUserException user exception is thrown, ignore it.*/
|
/*If an HSSFUserException user exception is thrown, ignore it.*/
|
||||||
}
|
}
|
||||||
@ -100,7 +99,7 @@ public class HSSFEventFactory {
|
|||||||
*/
|
*/
|
||||||
public short abortableProcessEvents(HSSFRequest req, InputStream in)
|
public short abortableProcessEvents(HSSFRequest req, InputStream in)
|
||||||
throws HSSFUserException {
|
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
|
* @param in a DocumentInputStream obtained from POIFS's POIFSFileSystem object
|
||||||
* @return numeric user-specified result code.
|
* @return numeric user-specified result code.
|
||||||
*/
|
*/
|
||||||
protected short genericProcessEvents(HSSFRequest req, RecordInputStream in)
|
private short genericProcessEvents(HSSFRequest req, InputStream in)
|
||||||
throws HSSFUserException {
|
throws HSSFUserException {
|
||||||
short userCode = 0;
|
short userCode = 0;
|
||||||
|
|
||||||
|
16
src/java/org/apache/poi/hssf/record/BiffHeaderInput.java
Normal file
16
src/java/org/apache/poi/hssf/record/BiffHeaderInput.java
Normal 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();
|
||||||
|
|
||||||
|
}
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
/* ====================================================================
|
/* ====================================================================
|
||||||
Licensed to the Apache Software Foundation (ASF) under one or more
|
Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
contributor license agreements. See the NOTICE file distributed with
|
contributor license agreements. See the NOTICE file distributed with
|
||||||
@ -15,67 +14,134 @@
|
|||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
==================================================================== */
|
==================================================================== */
|
||||||
|
|
||||||
|
|
||||||
package org.apache.poi.hssf.record;
|
package org.apache.poi.hssf.record;
|
||||||
|
|
||||||
|
import org.apache.poi.util.HexDump;
|
||||||
import org.apache.poi.util.LittleEndianOutput;
|
import org.apache.poi.util.LittleEndianOutput;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Title: File Pass Record<P>
|
* Title: File Pass Record (0x002F) <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>
|
* Description: Indicates that the record after this record are encrypted.
|
||||||
* REFERENCE: PG 420 Microsoft Excel 97 Developer's Kit (ISBN: 1-57231-498-2)<P>
|
*
|
||||||
* @author Jason Height (jheight at chariot dot net dot au)
|
* @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
|
private static final int ENCRYPTION_XOR = 0;
|
||||||
extends StandardRecord
|
private static final int ENCRYPTION_OTHER = 1;
|
||||||
{
|
|
||||||
public final static short sid = 0x2F;
|
|
||||||
private int field_1_encryptedpassword;
|
|
||||||
|
|
||||||
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()
|
public FilePassRecord(RecordInputStream in) {
|
||||||
{
|
_encryptionType = in.readUShort();
|
||||||
StringBuffer buffer = new StringBuffer();
|
|
||||||
|
|
||||||
buffer.append("[FILEPASS]\n");
|
switch (_encryptionType) {
|
||||||
buffer.append(" .password = ").append(field_1_encryptedpassword)
|
case ENCRYPTION_XOR:
|
||||||
.append("\n");
|
throw new RecordFormatException("HSSF does not currently support XOR obfuscation");
|
||||||
buffer.append("[/FILEPASS]\n");
|
case ENCRYPTION_OTHER:
|
||||||
return buffer.toString();
|
// 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) {
|
private static byte[] read(RecordInputStream in, int size) {
|
||||||
out.writeInt(( short ) field_1_encryptedpassword);
|
byte[] result = new byte[size];
|
||||||
}
|
in.readFully(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
protected int getDataSize() {
|
public void serialize(LittleEndianOutput out) {
|
||||||
return 4;
|
out.writeShort(_encryptionType);
|
||||||
}
|
out.writeShort(_encryptionInfo);
|
||||||
|
out.writeShort(_minorVersionNo);
|
||||||
|
out.write(_docId);
|
||||||
|
out.write(_saltData);
|
||||||
|
out.write(_saltHash);
|
||||||
|
}
|
||||||
|
|
||||||
public short getSid()
|
protected int getDataSize() {
|
||||||
{
|
return 54;
|
||||||
return sid;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public Object clone() {
|
|
||||||
FilePassRecord rec = new FilePassRecord();
|
|
||||||
rec.field_1_encryptedpassword = field_1_encryptedpassword;
|
public byte[] getDocId() {
|
||||||
return rec;
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,15 +17,16 @@
|
|||||||
|
|
||||||
package org.apache.poi.hssf.record;
|
package org.apache.poi.hssf.record;
|
||||||
|
|
||||||
import org.apache.poi.hssf.record.chart.*;
|
import java.io.IOException;
|
||||||
import org.apache.poi.hssf.record.pivottable.*;
|
|
||||||
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.lang.reflect.Constructor;
|
import java.lang.reflect.Constructor;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.lang.reflect.Modifier;
|
import java.lang.reflect.Modifier;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
|
import org.apache.poi.hssf.record.chart.*;
|
||||||
|
import org.apache.poi.hssf.record.pivottable.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Title: Record Factory<P>
|
* Title: Record Factory<P>
|
||||||
* Description: Takes a stream and outputs an array of Record objects.<P>
|
* Description: Takes a stream and outputs an array of Record objects.<P>
|
||||||
@ -193,6 +194,7 @@ public final class RecordFactory {
|
|||||||
ChartFRTInfoRecord.class,
|
ChartFRTInfoRecord.class,
|
||||||
ChartStartBlockRecord.class,
|
ChartStartBlockRecord.class,
|
||||||
ChartEndBlockRecord.class,
|
ChartEndBlockRecord.class,
|
||||||
|
// TODO ChartFormatRecord.class,
|
||||||
ChartStartObjectRecord.class,
|
ChartStartObjectRecord.class,
|
||||||
ChartEndObjectRecord.class,
|
ChartEndObjectRecord.class,
|
||||||
CatLabRecord.class,
|
CatLabRecord.class,
|
||||||
@ -367,9 +369,10 @@ public final class RecordFactory {
|
|||||||
* @exception RecordFormatException on error processing the InputStream
|
* @exception RecordFormatException on error processing the InputStream
|
||||||
*/
|
*/
|
||||||
public static List<Record> createRecords(InputStream in) throws RecordFormatException {
|
public static List<Record> createRecords(InputStream in) throws RecordFormatException {
|
||||||
|
|
||||||
List<Record> records = new ArrayList<Record>(NUM_RECORDS);
|
List<Record> records = new ArrayList<Record>(NUM_RECORDS);
|
||||||
|
|
||||||
RecordFactoryInputStream recStream = new RecordFactoryInputStream(new RecordInputStream(in), true);
|
RecordFactoryInputStream recStream = new RecordFactoryInputStream(in, true);
|
||||||
|
|
||||||
Record record;
|
Record record;
|
||||||
while ((record = recStream.nextRecord())!=null) {
|
while ((record = recStream.nextRecord())!=null) {
|
||||||
|
@ -16,8 +16,13 @@
|
|||||||
==================================================================== */
|
==================================================================== */
|
||||||
package org.apache.poi.hssf.record;
|
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.HSSFEventFactory;
|
||||||
import org.apache.poi.hssf.eventusermodel.HSSFListener;
|
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
|
* 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
|
* {@link HSSFListener} and have new records pushed to
|
||||||
* them, but this does allow for a "pull" style of coding.
|
* 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 RecordInputStream _recStream;
|
||||||
private final boolean _shouldIncludeContinueRecords;
|
private final boolean _shouldIncludeContinueRecords;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Temporarily stores a group of {@link NumberRecord}s. This is uses when the most
|
* Temporarily stores a group of {@link Record}s, for future return by {@link #nextRecord()}.
|
||||||
* recently read underlying record is a {@link MulRKRecord}
|
* 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
|
* 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
|
* {@link ContinueRecord}s should be skipped (this is sometimes useful in event based
|
||||||
* processing).
|
* processing).
|
||||||
*/
|
*/
|
||||||
public RecordFactoryInputStream(RecordInputStream inp, boolean shouldIncludeContinueRecords) {
|
public RecordFactoryInputStream(InputStream in, boolean shouldIncludeContinueRecords) {
|
||||||
_recStream = inp;
|
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;
|
_shouldIncludeContinueRecords = shouldIncludeContinueRecords;
|
||||||
|
_lastRecord = sei.getLastRecord();
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* How to recognise end of stream?
|
* 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
|
* record might follow any EOF record. So we also need to keep track of the bof/eof
|
||||||
* nesting level.
|
* nesting level.
|
||||||
*/
|
*/
|
||||||
_bofDepth=0;
|
_bofDepth = sei.hasBOFRecord() ? 1 : 0;
|
||||||
_lastRecordWasEOFLevelZero = false;
|
_lastRecordWasEOFLevelZero = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,15 +204,15 @@ public class RecordFactoryInputStream {
|
|||||||
*/
|
*/
|
||||||
public Record nextRecord() {
|
public Record nextRecord() {
|
||||||
Record r;
|
Record r;
|
||||||
r = getNextMultipleNumberRecord();
|
r = getNextUnreadRecord();
|
||||||
if (r != null) {
|
if (r != null) {
|
||||||
// found a NumberRecord (expanded from a recent MULRK record)
|
// found an unread record
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
while (true) {
|
while (true) {
|
||||||
if (!_recStream.hasNextRecord()) {
|
if (!_recStream.hasNextRecord()) {
|
||||||
// recStream is exhausted;
|
// recStream is exhausted;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// step underlying RecordInputStream to the next record
|
// 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.
|
* a recently read {@link MulRKRecord}. <code>null</code> if not present.
|
||||||
*/
|
*/
|
||||||
private NumberRecord getNextMultipleNumberRecord() {
|
private Record getNextUnreadRecord() {
|
||||||
if (_multipleNumberRecords != null) {
|
if (_unreadRecordBuffer != null) {
|
||||||
int ix = _multipleNumberRecordIndex;
|
int ix = _unreadRecordIndex;
|
||||||
if (ix < _multipleNumberRecords.length) {
|
if (ix < _unreadRecordBuffer.length) {
|
||||||
NumberRecord result = _multipleNumberRecords[ix];
|
Record result = _unreadRecordBuffer[ix];
|
||||||
_multipleNumberRecordIndex = ix + 1;
|
_unreadRecordIndex = ix + 1;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
_multipleNumberRecordIndex = -1;
|
_unreadRecordIndex = -1;
|
||||||
_multipleNumberRecords = null;
|
_unreadRecordBuffer = null;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -182,10 +291,10 @@ public class RecordFactoryInputStream {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (record instanceof MulRKRecord) {
|
if (record instanceof MulRKRecord) {
|
||||||
NumberRecord[] records = RecordFactory.convertRKRecords((MulRKRecord) record);
|
Record[] records = RecordFactory.convertRKRecords((MulRKRecord) record);
|
||||||
|
|
||||||
_multipleNumberRecords = records;
|
_unreadRecordBuffer = records;
|
||||||
_multipleNumberRecordIndex = 1;
|
_unreadRecordIndex = 1;
|
||||||
return records[0];
|
return records[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,6 +21,8 @@ import java.io.ByteArrayOutputStream;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
|
||||||
import org.apache.poi.hssf.dev.BiffViewer;
|
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.LittleEndian;
|
||||||
import org.apache.poi.util.LittleEndianInput;
|
import org.apache.poi.util.LittleEndianInput;
|
||||||
import org.apache.poi.util.LittleEndianInputStream;
|
import org.apache.poi.util.LittleEndianInputStream;
|
||||||
@ -31,7 +33,7 @@ import org.apache.poi.util.LittleEndianInputStream;
|
|||||||
*
|
*
|
||||||
* @author Jason Height (jheight @ apache dot org)
|
* @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*/
|
/** Maximum size of a single record (minus the 4 byte header) without a continue*/
|
||||||
public final static short MAX_RECORD_DATA_SIZE = 8224;
|
public final static short MAX_RECORD_DATA_SIZE = 8224;
|
||||||
private static final int INVALID_SID_VALUE = -1;
|
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 int DATA_LEN_NEEDS_TO_BE_READ = -1;
|
||||||
private static final byte[] EMPTY_BYTE_ARRAY = { };
|
private static final byte[] EMPTY_BYTE_ARRAY = { };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For use in {@link BiffViewer} which may construct {@link Record}s that don't completely
|
* 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.
|
* read all available data. This exception should never be thrown otherwise.
|
||||||
*/
|
*/
|
||||||
public static final class LeftoverDataException extends RuntimeException {
|
public static final class LeftoverDataException extends RuntimeException {
|
||||||
public LeftoverDataException(int sid, int remainingByteCount) {
|
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.");
|
+ " left " + remainingByteCount + " bytes remaining still to be read.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@link LittleEndianInput} facet of the wrapped {@link InputStream} */
|
/** Header {@link LittleEndianInput} facet of the wrapped {@link InputStream} */
|
||||||
private final LittleEndianInput _le;
|
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 */
|
/** the record identifier of the BIFF record currently being read */
|
||||||
private int _currentSid;
|
private int _currentSid;
|
||||||
/**
|
/**
|
||||||
* Length of the data section of the current BIFF record (always 4 less than the total record size).
|
* 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}.
|
* When uninitialised, this field is set to {@link #DATA_LEN_NEEDS_TO_BE_READ}.
|
||||||
*/
|
*/
|
||||||
private int _currentDataLength;
|
private int _currentDataLength;
|
||||||
/**
|
/**
|
||||||
* The BIFF record identifier for the next record is read when just as the current record
|
* The BIFF record identifier for the next record is read when just as the current record
|
||||||
* is finished.
|
* is finished.
|
||||||
* This field is only really valid during the time that ({@link #_currentDataLength} ==
|
* 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
|
* {@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
|
* 'sid of the next record'. Wwhile mid-record, this field coincidentally holds the sid
|
||||||
* of the current record.
|
* of the current record.
|
||||||
*/
|
*/
|
||||||
private int _nextSid;
|
private int _nextSid;
|
||||||
/**
|
/**
|
||||||
* index within the data section of the current BIFF record
|
* index within the data section of the current BIFF record
|
||||||
*/
|
*/
|
||||||
private int _currentDataOffset;
|
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 {
|
public RecordInputStream(InputStream in) throws RecordFormatException {
|
||||||
if (in instanceof LittleEndianInput) {
|
this (in, null, 0);
|
||||||
// accessing directly is an optimisation
|
}
|
||||||
_le = (LittleEndianInput) in;
|
|
||||||
|
public RecordInputStream(InputStream in, Biff8EncryptionKey key, int initialOffset) throws RecordFormatException {
|
||||||
|
if (key == null) {
|
||||||
|
_dataInput = getLEI(in);
|
||||||
|
_bhi = new SimpleHeaderInput(in);
|
||||||
} else {
|
} else {
|
||||||
// less optimal, but should work OK just the same. Often occurs in junit tests.
|
Biff8DecryptingStream bds = new Biff8DecryptingStream(in, initialOffset, key);
|
||||||
_le = new LittleEndianInputStream(in);
|
_bhi = bds;
|
||||||
|
_dataInput = bds;
|
||||||
}
|
}
|
||||||
_nextSid = readNextSid();
|
_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
|
* @return the number of bytes available in the current BIFF record
|
||||||
* @see #remaining()
|
* @see #remaining()
|
||||||
@ -95,11 +131,6 @@ public final class RecordInputStream extends InputStream implements LittleEndian
|
|||||||
return remaining();
|
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) {
|
public int read(byte[] b, int off, int len) {
|
||||||
int limit = Math.min(len, remaining());
|
int limit = Math.min(len, remaining());
|
||||||
if (limit == 0) {
|
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.
|
* 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.
|
* current record.
|
||||||
*/
|
*/
|
||||||
public boolean hasNextRecord() throws LeftoverDataException {
|
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
|
* @return the sid of the next record or {@link #INVALID_SID_VALUE} if at end of stream
|
||||||
*/
|
*/
|
||||||
private int readNextSid() {
|
private int readNextSid() {
|
||||||
int nAvailable = _le.available();
|
int nAvailable = _bhi.available();
|
||||||
if (nAvailable < EOFRecord.ENCODED_SIZE) {
|
if (nAvailable < EOFRecord.ENCODED_SIZE) {
|
||||||
if (nAvailable > 0) {
|
if (nAvailable > 0) {
|
||||||
// some scrap left over?
|
// some scrap left over?
|
||||||
@ -143,7 +173,7 @@ public final class RecordInputStream extends InputStream implements LittleEndian
|
|||||||
}
|
}
|
||||||
return INVALID_SID_VALUE;
|
return INVALID_SID_VALUE;
|
||||||
}
|
}
|
||||||
int result = _le.readUShort();
|
int result = _bhi.readRecordSID();
|
||||||
if (result == INVALID_SID_VALUE) {
|
if (result == INVALID_SID_VALUE) {
|
||||||
throw new RecordFormatException("Found invalid sid (" + result + ")");
|
throw new RecordFormatException("Found invalid sid (" + result + ")");
|
||||||
}
|
}
|
||||||
@ -164,7 +194,7 @@ public final class RecordInputStream extends InputStream implements LittleEndian
|
|||||||
}
|
}
|
||||||
_currentSid = _nextSid;
|
_currentSid = _nextSid;
|
||||||
_currentDataOffset = 0;
|
_currentDataOffset = 0;
|
||||||
_currentDataLength = _le.readUShort();
|
_currentDataLength = _bhi.readDataSize();
|
||||||
if (_currentDataLength > MAX_RECORD_DATA_SIZE) {
|
if (_currentDataLength > MAX_RECORD_DATA_SIZE) {
|
||||||
throw new RecordFormatException("The content of an excel record cannot exceed "
|
throw new RecordFormatException("The content of an excel record cannot exceed "
|
||||||
+ MAX_RECORD_DATA_SIZE + " bytes");
|
+ MAX_RECORD_DATA_SIZE + " bytes");
|
||||||
@ -182,7 +212,7 @@ public final class RecordInputStream extends InputStream implements LittleEndian
|
|||||||
nextRecord();
|
nextRecord();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throw new RecordFormatException("Not enough data (" + nAvailable
|
throw new RecordFormatException("Not enough data (" + nAvailable
|
||||||
+ ") to read requested (" + requiredByteCount +") bytes");
|
+ ") to read requested (" + requiredByteCount +") bytes");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,7 +222,7 @@ public final class RecordInputStream extends InputStream implements LittleEndian
|
|||||||
public byte readByte() {
|
public byte readByte() {
|
||||||
checkRecordPosition(LittleEndian.BYTE_SIZE);
|
checkRecordPosition(LittleEndian.BYTE_SIZE);
|
||||||
_currentDataOffset += 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() {
|
public short readShort() {
|
||||||
checkRecordPosition(LittleEndian.SHORT_SIZE);
|
checkRecordPosition(LittleEndian.SHORT_SIZE);
|
||||||
_currentDataOffset += LittleEndian.SHORT_SIZE;
|
_currentDataOffset += LittleEndian.SHORT_SIZE;
|
||||||
return _le.readShort();
|
return _dataInput.readShort();
|
||||||
}
|
}
|
||||||
|
|
||||||
public int readInt() {
|
public int readInt() {
|
||||||
checkRecordPosition(LittleEndian.INT_SIZE);
|
checkRecordPosition(LittleEndian.INT_SIZE);
|
||||||
_currentDataOffset += LittleEndian.INT_SIZE;
|
_currentDataOffset += LittleEndian.INT_SIZE;
|
||||||
return _le.readInt();
|
return _dataInput.readInt();
|
||||||
}
|
}
|
||||||
|
|
||||||
public long readLong() {
|
public long readLong() {
|
||||||
checkRecordPosition(LittleEndian.LONG_SIZE);
|
checkRecordPosition(LittleEndian.LONG_SIZE);
|
||||||
_currentDataOffset += 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() {
|
public int readUShort() {
|
||||||
checkRecordPosition(LittleEndian.SHORT_SIZE);
|
checkRecordPosition(LittleEndian.SHORT_SIZE);
|
||||||
_currentDataOffset += LittleEndian.SHORT_SIZE;
|
_currentDataOffset += LittleEndian.SHORT_SIZE;
|
||||||
return _le.readUShort();
|
return _dataInput.readUShort();
|
||||||
}
|
}
|
||||||
|
|
||||||
public double readDouble() {
|
public double readDouble() {
|
||||||
checkRecordPosition(LittleEndian.DOUBLE_SIZE);
|
long valueLongBits = readLong();
|
||||||
_currentDataOffset += LittleEndian.DOUBLE_SIZE;
|
|
||||||
long valueLongBits = _le.readLong();
|
|
||||||
double result = Double.longBitsToDouble(valueLongBits);
|
double result = Double.longBitsToDouble(valueLongBits);
|
||||||
if (Double.isNaN(result)) {
|
if (Double.isNaN(result)) {
|
||||||
throw new RuntimeException("Did not expect to read NaN"); // (Because Excel typically doesn't write NaN
|
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) {
|
public void readFully(byte[] buf, int off, int len) {
|
||||||
checkRecordPosition(len);
|
checkRecordPosition(len);
|
||||||
_le.readFully(buf, off, len);
|
_dataInput.readFully(buf, off, len);
|
||||||
_currentDataOffset+=len;
|
_currentDataOffset+=len;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -315,7 +343,7 @@ public final class RecordInputStream extends InputStream implements LittleEndian
|
|||||||
availableChars--;
|
availableChars--;
|
||||||
}
|
}
|
||||||
if (!isContinueNext()) {
|
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");
|
+ (requestedLength-curLen) + " of " + requestedLength + " chars");
|
||||||
}
|
}
|
||||||
if(remaining() != 0) {
|
if(remaining() != 0) {
|
||||||
@ -324,7 +352,7 @@ public final class RecordInputStream extends InputStream implements LittleEndian
|
|||||||
nextRecord();
|
nextRecord();
|
||||||
// note - the compressed flag may change on the fly
|
// note - the compressed flag may change on the fly
|
||||||
byte compressFlag = readByte();
|
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?
|
// At what point are records continued?
|
||||||
// - Often from within the char data of long strings (caller is within readStringCommon()).
|
// - Often from within the char data of long strings (caller is within readStringCommon()).
|
||||||
// - From UnicodeString construction (many different points - call via checkRecordPosition)
|
// - 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)
|
// and before the formatting run data)
|
||||||
return _nextSid == ContinueRecord.sid;
|
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();
|
||||||
|
}
|
||||||
|
}
|
197
src/java/org/apache/poi/hssf/record/crypto/Biff8RC4.java
Normal file
197
src/java/org/apache/poi/hssf/record/crypto/Biff8RC4.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
90
src/java/org/apache/poi/hssf/record/crypto/RC4.java
Normal file
90
src/java/org/apache/poi/hssf/record/crypto/RC4.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
BIN
src/testcases/org/apache/poi/hssf/data/password.xls
Executable file
BIN
src/testcases/org/apache/poi/hssf/data/password.xls
Executable file
Binary file not shown.
@ -24,108 +24,109 @@ import java.io.InputStream;
|
|||||||
import junit.framework.TestCase;
|
import junit.framework.TestCase;
|
||||||
|
|
||||||
import org.apache.poi.hssf.HSSFTestDataSamples;
|
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.hssf.usermodel.HSSFWorkbook;
|
||||||
import org.apache.poi.poifs.filesystem.DirectoryNode;
|
import org.apache.poi.poifs.filesystem.DirectoryNode;
|
||||||
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
|
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public final class TestExcelExtractor extends TestCase {
|
public final class TestExcelExtractor extends TestCase {
|
||||||
|
|
||||||
private static ExcelExtractor createExtractor(String sampleFileName) {
|
private static ExcelExtractor createExtractor(String sampleFileName) {
|
||||||
|
|
||||||
InputStream is = HSSFTestDataSamples.openSampleFileStream(sampleFileName);
|
InputStream is = HSSFTestDataSamples.openSampleFileStream(sampleFileName);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return new ExcelExtractor(new POIFSFileSystem(is));
|
return new ExcelExtractor(new POIFSFileSystem(is));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void testSimple() {
|
public void testSimple() {
|
||||||
|
|
||||||
ExcelExtractor extractor = createExtractor("Simple.xls");
|
ExcelExtractor extractor = createExtractor("Simple.xls");
|
||||||
|
|
||||||
assertEquals("Sheet1\nreplaceMe\nSheet2\nSheet3\n", extractor.getText());
|
assertEquals("Sheet1\nreplaceMe\nSheet2\nSheet3\n", extractor.getText());
|
||||||
|
|
||||||
// Now turn off sheet names
|
// Now turn off sheet names
|
||||||
extractor.setIncludeSheetNames(false);
|
extractor.setIncludeSheetNames(false);
|
||||||
assertEquals("replaceMe\n", extractor.getText());
|
assertEquals("replaceMe\n", extractor.getText());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testNumericFormula() {
|
public void testNumericFormula() {
|
||||||
|
|
||||||
ExcelExtractor extractor = createExtractor("sumifformula.xls");
|
ExcelExtractor extractor = createExtractor("sumifformula.xls");
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"Sheet1\n" +
|
"Sheet1\n" +
|
||||||
"1000.0\t1.0\t5.0\n" +
|
"1000.0\t1.0\t5.0\n" +
|
||||||
"2000.0\t2.0\n" +
|
"2000.0\t2.0\n" +
|
||||||
"3000.0\t3.0\n" +
|
"3000.0\t3.0\n" +
|
||||||
"4000.0\t4.0\n" +
|
"4000.0\t4.0\n" +
|
||||||
"5000.0\t5.0\n" +
|
"5000.0\t5.0\n" +
|
||||||
"Sheet2\nSheet3\n",
|
"Sheet2\nSheet3\n",
|
||||||
extractor.getText()
|
extractor.getText()
|
||||||
);
|
);
|
||||||
|
|
||||||
extractor.setFormulasNotResults(true);
|
extractor.setFormulasNotResults(true);
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"Sheet1\n" +
|
"Sheet1\n" +
|
||||||
"1000.0\t1.0\tSUMIF(A1:A5,\">4000\",B1:B5)\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" +
|
"3000.0\t3.0\n" +
|
||||||
"4000.0\t4.0\n" +
|
"4000.0\t4.0\n" +
|
||||||
"5000.0\t5.0\n" +
|
"5000.0\t5.0\n" +
|
||||||
"Sheet2\nSheet3\n",
|
"Sheet2\nSheet3\n",
|
||||||
extractor.getText()
|
extractor.getText()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testwithContinueRecords() {
|
public void testwithContinueRecords() {
|
||||||
|
|
||||||
ExcelExtractor extractor = createExtractor("StringContinueRecords.xls");
|
ExcelExtractor extractor = createExtractor("StringContinueRecords.xls");
|
||||||
|
|
||||||
extractor.getText();
|
extractor.getText();
|
||||||
|
|
||||||
// Has masses of text
|
// Has masses of text
|
||||||
// Until we fixed bug #41064, this would've
|
// Until we fixed bug #41064, this would've
|
||||||
// failed by now
|
// failed by now
|
||||||
assertTrue(extractor.getText().length() > 40960);
|
assertTrue(extractor.getText().length() > 40960);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testStringConcat() {
|
public void testStringConcat() {
|
||||||
|
|
||||||
ExcelExtractor extractor = createExtractor("SimpleWithFormula.xls");
|
ExcelExtractor extractor = createExtractor("SimpleWithFormula.xls");
|
||||||
|
|
||||||
// Comes out as NaN if treated as a number
|
// Comes out as NaN if treated as a number
|
||||||
// And as XYZ if treated as a string
|
// And as XYZ if treated as a string
|
||||||
assertEquals("Sheet1\nreplaceme\nreplaceme\nreplacemereplaceme\nSheet2\nSheet3\n", extractor.getText());
|
assertEquals("Sheet1\nreplaceme\nreplaceme\nreplacemereplaceme\nSheet2\nSheet3\n", extractor.getText());
|
||||||
|
|
||||||
extractor.setFormulasNotResults(true);
|
extractor.setFormulasNotResults(true);
|
||||||
|
|
||||||
assertEquals("Sheet1\nreplaceme\nreplaceme\nCONCATENATE(A1,A2)\nSheet2\nSheet3\n", extractor.getText());
|
assertEquals("Sheet1\nreplaceme\nreplaceme\nCONCATENATE(A1,A2)\nSheet2\nSheet3\n", extractor.getText());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testStringFormula() {
|
public void testStringFormula() {
|
||||||
|
|
||||||
ExcelExtractor extractor = createExtractor("StringFormulas.xls");
|
ExcelExtractor extractor = createExtractor("StringFormulas.xls");
|
||||||
|
|
||||||
// Comes out as NaN if treated as a number
|
// Comes out as NaN if treated as a number
|
||||||
// And as XYZ if treated as a string
|
// And as XYZ if treated as a string
|
||||||
assertEquals("Sheet1\nXYZ\nSheet2\nSheet3\n", extractor.getText());
|
assertEquals("Sheet1\nXYZ\nSheet2\nSheet3\n", extractor.getText());
|
||||||
|
|
||||||
extractor.setFormulasNotResults(true);
|
extractor.setFormulasNotResults(true);
|
||||||
|
|
||||||
assertEquals("Sheet1\nUPPER(\"xyz\")\nSheet2\nSheet3\n", extractor.getText());
|
assertEquals("Sheet1\nUPPER(\"xyz\")\nSheet2\nSheet3\n", extractor.getText());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void testEventExtractor() throws Exception {
|
public void testEventExtractor() throws Exception {
|
||||||
EventBasedExcelExtractor extractor;
|
EventBasedExcelExtractor extractor;
|
||||||
|
|
||||||
// First up, a simple file with string
|
// First up, a simple file with string
|
||||||
// based formulas in it
|
// based formulas in it
|
||||||
extractor = new EventBasedExcelExtractor(
|
extractor = new EventBasedExcelExtractor(
|
||||||
@ -134,17 +135,17 @@ public final class TestExcelExtractor extends TestCase {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
extractor.setIncludeSheetNames(true);
|
extractor.setIncludeSheetNames(true);
|
||||||
|
|
||||||
String text = extractor.getText();
|
String text = extractor.getText();
|
||||||
assertEquals("Sheet1\nreplaceme\nreplaceme\nreplacemereplaceme\nSheet2\nSheet3\n", text);
|
assertEquals("Sheet1\nreplaceme\nreplaceme\nreplacemereplaceme\nSheet2\nSheet3\n", text);
|
||||||
|
|
||||||
extractor.setIncludeSheetNames(false);
|
extractor.setIncludeSheetNames(false);
|
||||||
extractor.setFormulasNotResults(true);
|
extractor.setFormulasNotResults(true);
|
||||||
|
|
||||||
text = extractor.getText();
|
text = extractor.getText();
|
||||||
assertEquals("replaceme\nreplaceme\nCONCATENATE(A1,A2)\n", text);
|
assertEquals("replaceme\nreplaceme\nCONCATENATE(A1,A2)\n", text);
|
||||||
|
|
||||||
|
|
||||||
// Now, a slightly longer file with numeric formulas
|
// Now, a slightly longer file with numeric formulas
|
||||||
extractor = new EventBasedExcelExtractor(
|
extractor = new EventBasedExcelExtractor(
|
||||||
new POIFSFileSystem(
|
new POIFSFileSystem(
|
||||||
@ -157,14 +158,14 @@ public final class TestExcelExtractor extends TestCase {
|
|||||||
text = extractor.getText();
|
text = extractor.getText();
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"1000.0\t1.0\tSUMIF(A1:A5,\">4000\",B1:B5)\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" +
|
"3000.0\t3.0\n" +
|
||||||
"4000.0\t4.0\n" +
|
"4000.0\t4.0\n" +
|
||||||
"5000.0\t5.0\n",
|
"5000.0\t5.0\n",
|
||||||
text
|
text
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testWithComments() {
|
public void testWithComments() {
|
||||||
ExcelExtractor extractor = createExtractor("SimpleWithComments.xls");
|
ExcelExtractor extractor = createExtractor("SimpleWithComments.xls");
|
||||||
extractor.setIncludeSheetNames(false);
|
extractor.setIncludeSheetNames(false);
|
||||||
@ -172,34 +173,34 @@ public final class TestExcelExtractor extends TestCase {
|
|||||||
// Check without comments
|
// Check without comments
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"1.0\tone\n" +
|
"1.0\tone\n" +
|
||||||
"2.0\ttwo\n" +
|
"2.0\ttwo\n" +
|
||||||
"3.0\tthree\n",
|
"3.0\tthree\n",
|
||||||
extractor.getText()
|
extractor.getText()
|
||||||
);
|
);
|
||||||
|
|
||||||
// Now with
|
// Now with
|
||||||
extractor.setIncludeCellComments(true);
|
extractor.setIncludeCellComments(true);
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"1.0\tone Comment by Yegor Kozlov: Yegor Kozlov: first cell\n" +
|
"1.0\tone Comment by Yegor Kozlov: Yegor Kozlov: first cell\n" +
|
||||||
"2.0\ttwo Comment by Yegor Kozlov: Yegor Kozlov: second 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",
|
"3.0\tthree Comment by Yegor Kozlov: Yegor Kozlov: third cell\n",
|
||||||
extractor.getText()
|
extractor.getText()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testWithBlank() {
|
public void testWithBlank() {
|
||||||
ExcelExtractor extractor = createExtractor("MissingBits.xls");
|
ExcelExtractor extractor = createExtractor("MissingBits.xls");
|
||||||
String def = extractor.getText();
|
String def = extractor.getText();
|
||||||
extractor.setIncludeBlankCells(true);
|
extractor.setIncludeBlankCells(true);
|
||||||
String padded = extractor.getText();
|
String padded = extractor.getText();
|
||||||
|
|
||||||
assertTrue(def.startsWith(
|
assertTrue(def.startsWith(
|
||||||
"Sheet1\n" +
|
"Sheet1\n" +
|
||||||
"&[TAB]\t\n" +
|
"&[TAB]\t\n" +
|
||||||
"Hello\n" +
|
"Hello\n" +
|
||||||
"11.0\t23.0\n"
|
"11.0\t23.0\n"
|
||||||
));
|
));
|
||||||
|
|
||||||
assertTrue(padded.startsWith(
|
assertTrue(padded.startsWith(
|
||||||
"Sheet1\n" +
|
"Sheet1\n" +
|
||||||
"&[TAB]\t\n" +
|
"&[TAB]\t\n" +
|
||||||
@ -207,8 +208,8 @@ public final class TestExcelExtractor extends TestCase {
|
|||||||
"11.0\t\t\t23.0\n"
|
"11.0\t\t\t23.0\n"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Embded in a non-excel file
|
* Embded in a non-excel file
|
||||||
*/
|
*/
|
||||||
@ -219,22 +220,22 @@ public final class TestExcelExtractor extends TestCase {
|
|||||||
POIFSFileSystem fs = new POIFSFileSystem(
|
POIFSFileSystem fs = new POIFSFileSystem(
|
||||||
new FileInputStream(filename)
|
new FileInputStream(filename)
|
||||||
);
|
);
|
||||||
|
|
||||||
DirectoryNode objPool = (DirectoryNode) fs.getRoot().getEntry("ObjectPool");
|
DirectoryNode objPool = (DirectoryNode) fs.getRoot().getEntry("ObjectPool");
|
||||||
DirectoryNode dirA = (DirectoryNode) objPool.getEntry("_1269427460");
|
DirectoryNode dirA = (DirectoryNode) objPool.getEntry("_1269427460");
|
||||||
DirectoryNode dirB = (DirectoryNode) objPool.getEntry("_1269427461");
|
DirectoryNode dirB = (DirectoryNode) objPool.getEntry("_1269427461");
|
||||||
|
|
||||||
HSSFWorkbook wbA = new HSSFWorkbook(dirA, fs, true);
|
HSSFWorkbook wbA = new HSSFWorkbook(dirA, fs, true);
|
||||||
HSSFWorkbook wbB = new HSSFWorkbook(dirB, fs, true);
|
HSSFWorkbook wbB = new HSSFWorkbook(dirB, fs, true);
|
||||||
|
|
||||||
ExcelExtractor exA = new ExcelExtractor(wbA);
|
ExcelExtractor exA = new ExcelExtractor(wbA);
|
||||||
ExcelExtractor exB = new ExcelExtractor(wbB);
|
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());
|
exA.getText());
|
||||||
assertEquals("Sample Excel", exA.getSummaryInformation().getTitle());
|
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());
|
exB.getText());
|
||||||
assertEquals("Sample Excel 2", exB.getSummaryInformation().getTitle());
|
assertEquals("Sample Excel 2", exB.getSummaryInformation().getTitle());
|
||||||
}
|
}
|
||||||
@ -249,37 +250,37 @@ public final class TestExcelExtractor extends TestCase {
|
|||||||
POIFSFileSystem fs = new POIFSFileSystem(
|
POIFSFileSystem fs = new POIFSFileSystem(
|
||||||
new FileInputStream(filename)
|
new FileInputStream(filename)
|
||||||
);
|
);
|
||||||
|
|
||||||
DirectoryNode dirA = (DirectoryNode) fs.getRoot().getEntry("MBD0000A3B5");
|
DirectoryNode dirA = (DirectoryNode) fs.getRoot().getEntry("MBD0000A3B5");
|
||||||
DirectoryNode dirB = (DirectoryNode) fs.getRoot().getEntry("MBD0000A3B4");
|
DirectoryNode dirB = (DirectoryNode) fs.getRoot().getEntry("MBD0000A3B4");
|
||||||
|
|
||||||
HSSFWorkbook wbA = new HSSFWorkbook(dirA, fs, true);
|
HSSFWorkbook wbA = new HSSFWorkbook(dirA, fs, true);
|
||||||
HSSFWorkbook wbB = new HSSFWorkbook(dirB, fs, true);
|
HSSFWorkbook wbB = new HSSFWorkbook(dirB, fs, true);
|
||||||
|
|
||||||
ExcelExtractor exA = new ExcelExtractor(wbA);
|
ExcelExtractor exA = new ExcelExtractor(wbA);
|
||||||
ExcelExtractor exB = new ExcelExtractor(wbB);
|
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());
|
exA.getText());
|
||||||
assertEquals("Sample Excel", exA.getSummaryInformation().getTitle());
|
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());
|
exB.getText());
|
||||||
assertEquals("Sample Excel 2", exB.getSummaryInformation().getTitle());
|
assertEquals("Sample Excel 2", exB.getSummaryInformation().getTitle());
|
||||||
|
|
||||||
// And the base file too
|
// And the base file too
|
||||||
ExcelExtractor ex = new ExcelExtractor(fs);
|
ExcelExtractor ex = new ExcelExtractor(fs);
|
||||||
assertEquals("Sheet1\nI have lots of embeded files in me\nSheet2\nSheet3\n",
|
assertEquals("Sheet1\nI have lots of embeded files in me\nSheet2\nSheet3\n",
|
||||||
ex.getText());
|
ex.getText());
|
||||||
assertEquals("Excel With Embeded", ex.getSummaryInformation().getTitle());
|
assertEquals("Excel With Embeded", ex.getSummaryInformation().getTitle());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test that we get text from headers and footers
|
* Test that we get text from headers and footers
|
||||||
*/
|
*/
|
||||||
public void test45538() {
|
public void test45538() {
|
||||||
String[] files = {
|
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"
|
"45538_classic_Header.xls", "45538_form_Header.xls"
|
||||||
};
|
};
|
||||||
for(int i=0; i<files.length; i++) {
|
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);
|
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.cf.TestCellRange;
|
||||||
import org.apache.poi.hssf.record.chart.AllChartRecordTests;
|
import org.apache.poi.hssf.record.chart.AllChartRecordTests;
|
||||||
import org.apache.poi.hssf.record.constant.TestConstantValueParser;
|
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.formula.AllFormulaTests;
|
||||||
import org.apache.poi.hssf.record.pivot.AllPivotRecordTests;
|
import org.apache.poi.hssf.record.pivot.AllPivotRecordTests;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collects all tests for package <tt>org.apache.poi.hssf.record</tt> and sub-packages.
|
* Collects all tests for package <tt>org.apache.poi.hssf.record</tt> and sub-packages.
|
||||||
*
|
*
|
||||||
* @author Josh Micich
|
* @author Josh Micich
|
||||||
*/
|
*/
|
||||||
public final class AllRecordTests {
|
public final class AllRecordTests {
|
||||||
|
|
||||||
public static Test suite() {
|
public static Test suite() {
|
||||||
TestSuite result = new TestSuite(AllRecordTests.class.getName());
|
TestSuite result = new TestSuite(AllRecordTests.class.getName());
|
||||||
|
|
||||||
result.addTest(AllChartRecordTests.suite());
|
result.addTest(AllChartRecordTests.suite());
|
||||||
|
result.addTest(AllHSSFEncryptionTests.suite());
|
||||||
result.addTest(AllFormulaTests.suite());
|
result.addTest(AllFormulaTests.suite());
|
||||||
result.addTest(AllPivotRecordTests.suite());
|
result.addTest(AllPivotRecordTests.suite());
|
||||||
result.addTest(AllRecordAggregateTests.suite());
|
result.addTest(AllRecordAggregateTests.suite());
|
||||||
|
@ -155,7 +155,7 @@ public final class TestRecordFactory extends TestCase {
|
|||||||
*/
|
*/
|
||||||
public void testMixedContinue() throws Exception {
|
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.
|
* See Bug 39512 for details.
|
||||||
*/
|
*/
|
||||||
String dump =
|
String dump =
|
||||||
@ -208,6 +208,7 @@ public final class TestRecordFactory extends TestCase {
|
|||||||
public void testNonZeroPadding_bug46987() {
|
public void testNonZeroPadding_bug46987() {
|
||||||
Record[] recs = {
|
Record[] recs = {
|
||||||
new BOFRecord(),
|
new BOFRecord(),
|
||||||
|
new WriteAccessRecord(), // need *something* between BOF and EOF
|
||||||
EOFRecord.instance,
|
EOFRecord.instance,
|
||||||
BOFRecord.createSheetBOF(),
|
BOFRecord.createSheetBOF(),
|
||||||
EOFRecord.instance,
|
EOFRecord.instance,
|
||||||
@ -229,7 +230,7 @@ public final class TestRecordFactory extends TestCase {
|
|||||||
baos.write(0x00);
|
baos.write(0x00);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
POIFSFileSystem fs = new POIFSFileSystem();
|
POIFSFileSystem fs = new POIFSFileSystem();
|
||||||
InputStream is;
|
InputStream is;
|
||||||
try {
|
try {
|
||||||
@ -237,7 +238,7 @@ public final class TestRecordFactory extends TestCase {
|
|||||||
is = fs.getRoot().createDocumentInputStream("dummy");
|
is = fs.getRoot().createDocumentInputStream("dummy");
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Record> outRecs;
|
List<Record> outRecs;
|
||||||
try {
|
try {
|
||||||
@ -248,7 +249,6 @@ public final class TestRecordFactory extends TestCase {
|
|||||||
}
|
}
|
||||||
throw e;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
76
src/testcases/org/apache/poi/hssf/record/crypto/TestRC4.java
Normal file
76
src/testcases/org/apache/poi/hssf/record/crypto/TestRC4.java
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user