HSSF CryptoAPI decryption support
git-svn-id: https://svn.apache.org/repos/asf/poi/branches/hssf_cryptoapi@1755461 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
parent
c4ac2e7758
commit
0bfefdfc04
@ -17,8 +17,19 @@
|
||||
|
||||
package org.apache.poi.hssf.record;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.apache.poi.EncryptedDocumentException;
|
||||
import org.apache.poi.poifs.crypt.EncryptionInfo;
|
||||
import org.apache.poi.poifs.crypt.EncryptionMode;
|
||||
import org.apache.poi.poifs.crypt.binaryrc4.BinaryRC4EncryptionHeader;
|
||||
import org.apache.poi.poifs.crypt.binaryrc4.BinaryRC4EncryptionVerifier;
|
||||
import org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIEncryptionHeader;
|
||||
import org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIEncryptionVerifier;
|
||||
import org.apache.poi.poifs.crypt.xor.XOREncryptionHeader;
|
||||
import org.apache.poi.poifs.crypt.xor.XOREncryptionVerifier;
|
||||
import org.apache.poi.util.HexDump;
|
||||
import org.apache.poi.util.LittleEndianByteArrayOutputStream;
|
||||
import org.apache.poi.util.LittleEndianOutput;
|
||||
|
||||
/**
|
||||
@ -31,228 +42,82 @@ public final class FilePassRecord extends StandardRecord implements Cloneable {
|
||||
private static final int ENCRYPTION_XOR = 0;
|
||||
private static final int ENCRYPTION_OTHER = 1;
|
||||
|
||||
private int _encryptionType;
|
||||
private KeyData _keyData;
|
||||
|
||||
private static interface KeyData extends Cloneable {
|
||||
void read(RecordInputStream in);
|
||||
void serialize(LittleEndianOutput out);
|
||||
int getDataSize();
|
||||
void appendToString(StringBuffer buffer);
|
||||
KeyData clone(); // NOSONAR
|
||||
}
|
||||
|
||||
public static final class Rc4KeyData implements KeyData, Cloneable {
|
||||
private static final int ENCRYPTION_OTHER_RC4 = 1;
|
||||
private static final int ENCRYPTION_OTHER_CAPI_2 = 2;
|
||||
private static final int ENCRYPTION_OTHER_CAPI_3 = 3;
|
||||
private static final int ENCRYPTION_OTHER_CAPI_4 = 4;
|
||||
|
||||
private byte[] _salt;
|
||||
private byte[] _encryptedVerifier;
|
||||
private byte[] _encryptedVerifierHash;
|
||||
private int _encryptionInfo;
|
||||
private int _minorVersionNo;
|
||||
|
||||
public void read(RecordInputStream in) {
|
||||
_encryptionInfo = in.readUShort();
|
||||
switch (_encryptionInfo) {
|
||||
case ENCRYPTION_OTHER_RC4:
|
||||
// handled below
|
||||
break;
|
||||
case ENCRYPTION_OTHER_CAPI_2:
|
||||
case ENCRYPTION_OTHER_CAPI_3:
|
||||
case ENCRYPTION_OTHER_CAPI_4:
|
||||
throw new EncryptedDocumentException(
|
||||
"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);
|
||||
}
|
||||
_salt = FilePassRecord.read(in, 16);
|
||||
_encryptedVerifier = FilePassRecord.read(in, 16);
|
||||
_encryptedVerifierHash = FilePassRecord.read(in, 16);
|
||||
}
|
||||
|
||||
public void serialize(LittleEndianOutput out) {
|
||||
out.writeShort(_encryptionInfo);
|
||||
out.writeShort(_minorVersionNo);
|
||||
out.write(_salt);
|
||||
out.write(_encryptedVerifier);
|
||||
out.write(_encryptedVerifierHash);
|
||||
}
|
||||
|
||||
public int getDataSize() {
|
||||
return 54;
|
||||
}
|
||||
|
||||
public byte[] getSalt() {
|
||||
return _salt.clone();
|
||||
}
|
||||
|
||||
public void setSalt(byte[] salt) {
|
||||
this._salt = salt.clone();
|
||||
}
|
||||
|
||||
public byte[] getEncryptedVerifier() {
|
||||
return _encryptedVerifier.clone();
|
||||
}
|
||||
|
||||
public void setEncryptedVerifier(byte[] encryptedVerifier) {
|
||||
this._encryptedVerifier = encryptedVerifier.clone();
|
||||
}
|
||||
|
||||
public byte[] getEncryptedVerifierHash() {
|
||||
return _encryptedVerifierHash.clone();
|
||||
}
|
||||
|
||||
public void setEncryptedVerifierHash(byte[] encryptedVerifierHash) {
|
||||
this._encryptedVerifierHash = encryptedVerifierHash.clone();
|
||||
}
|
||||
|
||||
public void appendToString(StringBuffer buffer) {
|
||||
buffer.append(" .rc4.info = ").append(HexDump.shortToHex(_encryptionInfo)).append("\n");
|
||||
buffer.append(" .rc4.ver = ").append(HexDump.shortToHex(_minorVersionNo)).append("\n");
|
||||
buffer.append(" .rc4.salt = ").append(HexDump.toHex(_salt)).append("\n");
|
||||
buffer.append(" .rc4.verifier = ").append(HexDump.toHex(_encryptedVerifier)).append("\n");
|
||||
buffer.append(" .rc4.verifierHash = ").append(HexDump.toHex(_encryptedVerifierHash)).append("\n");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Rc4KeyData clone() {
|
||||
Rc4KeyData other = new Rc4KeyData();
|
||||
other._salt = this._salt.clone();
|
||||
other._encryptedVerifier = this._encryptedVerifier.clone();
|
||||
other._encryptedVerifierHash = this._encryptedVerifierHash.clone();
|
||||
other._encryptionInfo = this._encryptionInfo;
|
||||
other._minorVersionNo = this._minorVersionNo;
|
||||
return other;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class XorKeyData implements KeyData, Cloneable {
|
||||
/**
|
||||
* key (2 bytes): An unsigned integer that specifies the obfuscation key.
|
||||
* See [MS-OFFCRYPTO], 2.3.6.2 section, the first step of initializing XOR
|
||||
* array where it describes the generation of 16-bit XorKey value.
|
||||
*/
|
||||
private int _key;
|
||||
|
||||
/**
|
||||
* verificationBytes (2 bytes): An unsigned integer that specifies
|
||||
* the password verification identifier.
|
||||
*/
|
||||
private int _verifier;
|
||||
|
||||
public void read(RecordInputStream in) {
|
||||
_key = in.readUShort();
|
||||
_verifier = in.readUShort();
|
||||
}
|
||||
|
||||
public void serialize(LittleEndianOutput out) {
|
||||
out.writeShort(_key);
|
||||
out.writeShort(_verifier);
|
||||
}
|
||||
|
||||
public int getDataSize() {
|
||||
// TODO: Check!
|
||||
return 6;
|
||||
}
|
||||
|
||||
public int getKey() {
|
||||
return _key;
|
||||
}
|
||||
|
||||
public int getVerifier() {
|
||||
return _verifier;
|
||||
}
|
||||
|
||||
public void setKey(int key) {
|
||||
this._key = key;
|
||||
}
|
||||
|
||||
public void setVerifier(int verifier) {
|
||||
this._verifier = verifier;
|
||||
}
|
||||
|
||||
public void appendToString(StringBuffer buffer) {
|
||||
buffer.append(" .xor.key = ").append(HexDump.intToHex(_key)).append("\n");
|
||||
buffer.append(" .xor.verifier = ").append(HexDump.intToHex(_verifier)).append("\n");
|
||||
}
|
||||
|
||||
@Override
|
||||
public XorKeyData clone() {
|
||||
XorKeyData other = new XorKeyData();
|
||||
other._key = this._key;
|
||||
other._verifier = this._verifier;
|
||||
return other;
|
||||
}
|
||||
}
|
||||
|
||||
private int encryptionType;
|
||||
private EncryptionInfo encryptionInfo;
|
||||
private int dataLength;
|
||||
|
||||
private FilePassRecord(FilePassRecord other) {
|
||||
_encryptionType = other._encryptionType;
|
||||
_keyData = other._keyData.clone();
|
||||
dataLength = other.dataLength;
|
||||
encryptionType = other.encryptionType;
|
||||
try {
|
||||
encryptionInfo = other.encryptionInfo.clone();
|
||||
} catch (CloneNotSupportedException e) {
|
||||
throw new EncryptedDocumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public FilePassRecord(RecordInputStream in) {
|
||||
_encryptionType = in.readUShort();
|
||||
|
||||
switch (_encryptionType) {
|
||||
case ENCRYPTION_XOR:
|
||||
_keyData = new XorKeyData();
|
||||
break;
|
||||
case ENCRYPTION_OTHER:
|
||||
_keyData = new Rc4KeyData();
|
||||
break;
|
||||
default:
|
||||
throw new RecordFormatException("Unknown encryption type " + _encryptionType);
|
||||
}
|
||||
|
||||
_keyData.read(in);
|
||||
}
|
||||
|
||||
private static byte[] read(RecordInputStream in, int size) {
|
||||
byte[] result = new byte[size];
|
||||
in.readFully(result);
|
||||
return result;
|
||||
dataLength = in.remaining();
|
||||
encryptionType = in.readUShort();
|
||||
|
||||
EncryptionMode preferredMode;
|
||||
switch (encryptionType) {
|
||||
case ENCRYPTION_XOR:
|
||||
preferredMode = EncryptionMode.xor;
|
||||
break;
|
||||
case ENCRYPTION_OTHER:
|
||||
preferredMode = EncryptionMode.cryptoAPI;
|
||||
break;
|
||||
default:
|
||||
throw new EncryptedDocumentException("invalid encryption type");
|
||||
}
|
||||
|
||||
try {
|
||||
encryptionInfo = new EncryptionInfo(in, preferredMode);
|
||||
} catch (IOException e) {
|
||||
throw new EncryptedDocumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void serialize(LittleEndianOutput out) {
|
||||
out.writeShort(_encryptionType);
|
||||
assert(_keyData != null);
|
||||
_keyData.serialize(out);
|
||||
out.writeShort(encryptionType);
|
||||
|
||||
byte data[] = new byte[1024];
|
||||
LittleEndianByteArrayOutputStream bos = new LittleEndianByteArrayOutputStream(data, 0);
|
||||
|
||||
switch (encryptionInfo.getEncryptionMode()) {
|
||||
case xor:
|
||||
((XOREncryptionHeader)encryptionInfo.getHeader()).write(bos);
|
||||
((XOREncryptionVerifier)encryptionInfo.getVerifier()).write(bos);
|
||||
break;
|
||||
case binaryRC4:
|
||||
out.writeShort(encryptionInfo.getVersionMajor());
|
||||
out.writeShort(encryptionInfo.getVersionMinor());
|
||||
((BinaryRC4EncryptionHeader)encryptionInfo.getHeader()).write(bos);
|
||||
((BinaryRC4EncryptionVerifier)encryptionInfo.getVerifier()).write(bos);
|
||||
break;
|
||||
case cryptoAPI:
|
||||
out.writeShort(encryptionInfo.getVersionMajor());
|
||||
out.writeShort(encryptionInfo.getVersionMinor());
|
||||
((CryptoAPIEncryptionHeader)encryptionInfo.getHeader()).write(bos);
|
||||
((CryptoAPIEncryptionVerifier)encryptionInfo.getVerifier()).write(bos);
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("not supported");
|
||||
}
|
||||
|
||||
out.write(data, 0, bos.getWriteIndex());
|
||||
}
|
||||
|
||||
protected int getDataSize() {
|
||||
assert(_keyData != null);
|
||||
return _keyData.getDataSize();
|
||||
return dataLength;
|
||||
}
|
||||
|
||||
public Rc4KeyData getRc4KeyData() {
|
||||
return (_keyData instanceof Rc4KeyData)
|
||||
? (Rc4KeyData) _keyData
|
||||
: null;
|
||||
}
|
||||
|
||||
public XorKeyData getXorKeyData() {
|
||||
return (_keyData instanceof XorKeyData)
|
||||
? (XorKeyData) _keyData
|
||||
: null;
|
||||
}
|
||||
|
||||
private Rc4KeyData checkRc4() {
|
||||
Rc4KeyData rc4 = getRc4KeyData();
|
||||
if (rc4 == null) {
|
||||
throw new RecordFormatException("file pass record doesn't contain a rc4 key.");
|
||||
}
|
||||
return rc4;
|
||||
public EncryptionInfo getEncryptionInfo() {
|
||||
return encryptionInfo;
|
||||
}
|
||||
|
||||
public short getSid() {
|
||||
public short getSid() {
|
||||
return sid;
|
||||
}
|
||||
|
||||
@ -265,8 +130,13 @@ public final class FilePassRecord extends StandardRecord implements Cloneable {
|
||||
StringBuffer buffer = new StringBuffer();
|
||||
|
||||
buffer.append("[FILEPASS]\n");
|
||||
buffer.append(" .type = ").append(HexDump.shortToHex(_encryptionType)).append("\n");
|
||||
_keyData.appendToString(buffer);
|
||||
buffer.append(" .type = ").append(HexDump.shortToHex(encryptionType)).append("\n");
|
||||
String prefix = " ."+encryptionInfo.getEncryptionMode();
|
||||
buffer.append(prefix+".info = ").append(HexDump.shortToHex(encryptionInfo.getVersionMajor())).append("\n");
|
||||
buffer.append(prefix+".ver = ").append(HexDump.shortToHex(encryptionInfo.getVersionMinor())).append("\n");
|
||||
buffer.append(prefix+".salt = ").append(HexDump.toHex(encryptionInfo.getVerifier().getSalt())).append("\n");
|
||||
buffer.append(prefix+".verifier = ").append(HexDump.toHex(encryptionInfo.getVerifier().getEncryptedVerifier())).append("\n");
|
||||
buffer.append(prefix+".verifierHash = ").append(HexDump.toHex(encryptionInfo.getVerifier().getEncryptedVerifierHash())).append("\n");
|
||||
buffer.append("[/FILEPASS]\n");
|
||||
return buffer.toString();
|
||||
}
|
||||
|
@ -17,18 +17,16 @@
|
||||
package org.apache.poi.hssf.record;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.poi.EncryptedDocumentException;
|
||||
import org.apache.poi.hssf.eventusermodel.HSSFEventFactory;
|
||||
import org.apache.poi.hssf.eventusermodel.HSSFListener;
|
||||
import org.apache.poi.hssf.record.FilePassRecord.Rc4KeyData;
|
||||
import org.apache.poi.hssf.record.FilePassRecord.XorKeyData;
|
||||
import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey;
|
||||
import org.apache.poi.hssf.record.crypto.Biff8RC4Key;
|
||||
import org.apache.poi.hssf.record.crypto.Biff8XORKey;
|
||||
import org.apache.poi.poifs.crypt.Decryptor;
|
||||
import org.apache.poi.poifs.crypt.EncryptionInfo;
|
||||
|
||||
/**
|
||||
* A stream based way to get at complete records, with
|
||||
@ -114,31 +112,18 @@ public final class RecordFactoryInputStream {
|
||||
userPassword = Decryptor.DEFAULT_PASSWORD;
|
||||
}
|
||||
|
||||
Biff8EncryptionKey key;
|
||||
if (fpr.getRc4KeyData() != null) {
|
||||
Rc4KeyData rc4 = fpr.getRc4KeyData();
|
||||
Biff8RC4Key rc4key = Biff8RC4Key.create(userPassword, rc4.getSalt());
|
||||
key = rc4key;
|
||||
if (!rc4key.validate(rc4.getEncryptedVerifier(), rc4.getEncryptedVerifierHash())) {
|
||||
throw new EncryptedDocumentException(
|
||||
(Decryptor.DEFAULT_PASSWORD.equals(userPassword) ? "Default" : "Supplied")
|
||||
+ " password is invalid for salt/verifier/verifierHash");
|
||||
}
|
||||
} else if (fpr.getXorKeyData() != null) {
|
||||
XorKeyData xor = fpr.getXorKeyData();
|
||||
Biff8XORKey xorKey = Biff8XORKey.create(userPassword, xor.getKey());
|
||||
key = xorKey;
|
||||
|
||||
if (!xorKey.validate(userPassword, xor.getVerifier())) {
|
||||
EncryptionInfo info = fpr.getEncryptionInfo();
|
||||
try {
|
||||
if (!info.getDecryptor().verifyPassword(userPassword)) {
|
||||
throw new EncryptedDocumentException(
|
||||
(Decryptor.DEFAULT_PASSWORD.equals(userPassword) ? "Default" : "Supplied")
|
||||
+ " password is invalid for key/verifier");
|
||||
}
|
||||
} else {
|
||||
throw new EncryptedDocumentException("Crypto API not yet supported.");
|
||||
}
|
||||
(Decryptor.DEFAULT_PASSWORD.equals(userPassword) ? "Default" : "Supplied")
|
||||
+ " password is invalid for salt/verifier/verifierHash");
|
||||
}
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new EncryptedDocumentException(e);
|
||||
}
|
||||
|
||||
return new RecordInputStream(original, key, _initialRecordsSize);
|
||||
return new RecordInputStream(original, info, _initialRecordsSize);
|
||||
}
|
||||
|
||||
public boolean hasEncryption() {
|
||||
|
@ -25,6 +25,7 @@ import java.util.Locale;
|
||||
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.poifs.crypt.EncryptionInfo;
|
||||
import org.apache.poi.util.Internal;
|
||||
import org.apache.poi.util.LittleEndianConsts;
|
||||
import org.apache.poi.util.LittleEndianInput;
|
||||
@ -33,8 +34,6 @@ import org.apache.poi.util.LittleEndianInputStream;
|
||||
/**
|
||||
* Title: Record Input Stream<P>
|
||||
* Description: Wraps a stream and provides helper methods for the construction of records.<P>
|
||||
*
|
||||
* @author Jason Height (jheight @ apache dot org)
|
||||
*/
|
||||
public final class RecordInputStream implements LittleEndianInput {
|
||||
/** Maximum size of a single record (minus the 4 byte header) without a continue*/
|
||||
@ -122,7 +121,7 @@ public final class RecordInputStream implements LittleEndianInput {
|
||||
this (in, null, 0);
|
||||
}
|
||||
|
||||
public RecordInputStream(InputStream in, Biff8EncryptionKey key, int initialOffset) throws RecordFormatException {
|
||||
public RecordInputStream(InputStream in, EncryptionInfo key, int initialOffset) throws RecordFormatException {
|
||||
if (key == null) {
|
||||
_dataInput = getLEI(in);
|
||||
_bhi = new SimpleHeaderInput(in);
|
||||
|
@ -17,103 +17,202 @@
|
||||
|
||||
package org.apache.poi.hssf.record.crypto;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.PushbackInputStream;
|
||||
|
||||
import org.apache.poi.EncryptedDocumentException;
|
||||
import org.apache.poi.hssf.record.BOFRecord;
|
||||
import org.apache.poi.hssf.record.BiffHeaderInput;
|
||||
import org.apache.poi.hssf.record.FilePassRecord;
|
||||
import org.apache.poi.hssf.record.InterfaceHdrRecord;
|
||||
import org.apache.poi.hssf.record.RecordFormatException;
|
||||
import org.apache.poi.poifs.crypt.ChunkedCipherInputStream;
|
||||
import org.apache.poi.poifs.crypt.Decryptor;
|
||||
import org.apache.poi.poifs.crypt.EncryptionInfo;
|
||||
import org.apache.poi.util.LittleEndian;
|
||||
import org.apache.poi.util.LittleEndianConsts;
|
||||
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 Biff8Cipher _cipher;
|
||||
private static final int RC4_REKEYING_INTERVAL = 1024;
|
||||
|
||||
public Biff8DecryptingStream(InputStream in, int initialOffset, Biff8EncryptionKey key) {
|
||||
if (key instanceof Biff8RC4Key) {
|
||||
_cipher = new Biff8RC4(initialOffset, (Biff8RC4Key)key);
|
||||
} else if (key instanceof Biff8XORKey) {
|
||||
_cipher = new Biff8XOR(initialOffset, (Biff8XORKey)key);
|
||||
} else {
|
||||
throw new EncryptedDocumentException("Crypto API not supported yet.");
|
||||
}
|
||||
private final EncryptionInfo info;
|
||||
private ChunkedCipherInputStream ccis;
|
||||
private final byte buffer[] = new byte[LittleEndianConsts.LONG_SIZE];
|
||||
private boolean shouldSkipEncryptionOnCurrentRecord = false;
|
||||
|
||||
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 Biff8DecryptingStream(InputStream in, int initialOffset, EncryptionInfo info) throws RecordFormatException {
|
||||
try {
|
||||
byte initialBuf[] = new byte[initialOffset];
|
||||
InputStream stream;
|
||||
if (initialOffset == 0) {
|
||||
stream = in;
|
||||
} else {
|
||||
stream = new PushbackInputStream(in, initialOffset);
|
||||
((PushbackInputStream)stream).unread(initialBuf);
|
||||
}
|
||||
|
||||
this.info = info;
|
||||
Decryptor dec = this.info.getDecryptor();
|
||||
dec.setChunkSize(RC4_REKEYING_INTERVAL);
|
||||
ccis = (ChunkedCipherInputStream)dec.getDataStream(stream, Integer.MAX_VALUE, 0);
|
||||
|
||||
if (initialOffset > 0) {
|
||||
ccis.readFully(initialBuf);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RecordFormatException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public int available() {
|
||||
return _le.available();
|
||||
@Override
|
||||
public int available() {
|
||||
return ccis.available();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an unsigned short value without decrypting
|
||||
*/
|
||||
public int readRecordSID() {
|
||||
int sid = _le.readUShort();
|
||||
_cipher.skipTwoBytes();
|
||||
_cipher.startRecord(sid);
|
||||
@Override
|
||||
public int readRecordSID() {
|
||||
readPlain(buffer, 0, LittleEndianConsts.SHORT_SIZE);
|
||||
int sid = LittleEndian.getUShort(buffer, 0);
|
||||
shouldSkipEncryptionOnCurrentRecord = isNeverEncryptedRecord(sid);
|
||||
return sid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an unsigned short value without decrypting
|
||||
*/
|
||||
public int readDataSize() {
|
||||
int dataSize = _le.readUShort();
|
||||
_cipher.skipTwoBytes();
|
||||
_cipher.setNextRecordSize(dataSize);
|
||||
@Override
|
||||
public int readDataSize() {
|
||||
readPlain(buffer, 0, LittleEndianConsts.SHORT_SIZE);
|
||||
int dataSize = LittleEndian.getUShort(buffer, 0);
|
||||
ccis.setNextRecordSize(dataSize);
|
||||
return dataSize;
|
||||
}
|
||||
|
||||
public double readDouble() {
|
||||
long valueLongBits = readLong();
|
||||
@Override
|
||||
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
|
||||
// (Because Excel typically doesn't write NaN
|
||||
throw new RuntimeException("Did not expect to read NaN");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public void readFully(byte[] buf) {
|
||||
readFully(buf, 0, buf.length);
|
||||
@Override
|
||||
public void readFully(byte[] buf) {
|
||||
readFully(buf, 0, buf.length);
|
||||
}
|
||||
|
||||
public void readFully(byte[] buf, int off, int len) {
|
||||
_le.readFully(buf, off, len);
|
||||
_cipher.xor(buf, off, len);
|
||||
@Override
|
||||
public void readFully(byte[] buf, int off, int len) {
|
||||
if (shouldSkipEncryptionOnCurrentRecord) {
|
||||
readPlain(buf, off, buf.length);
|
||||
} else {
|
||||
ccis.readFully(buf, off, len);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public int readUByte() {
|
||||
return readByte() & 0xFF;
|
||||
@Override
|
||||
public int readUByte() {
|
||||
return readByte() & 0xFF;
|
||||
}
|
||||
public byte readByte() {
|
||||
return (byte) _cipher.xorByte(_le.readUByte());
|
||||
|
||||
@Override
|
||||
public byte readByte() {
|
||||
if (shouldSkipEncryptionOnCurrentRecord) {
|
||||
readPlain(buffer, 0, LittleEndianConsts.BYTE_SIZE);
|
||||
return buffer[0];
|
||||
} else {
|
||||
return ccis.readByte();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public int readUShort() {
|
||||
return readShort() & 0xFFFF;
|
||||
@Override
|
||||
public int readUShort() {
|
||||
return readShort() & 0xFFFF;
|
||||
}
|
||||
public short readShort() {
|
||||
return (short) _cipher.xorShort(_le.readUShort());
|
||||
|
||||
@Override
|
||||
public short readShort() {
|
||||
if (shouldSkipEncryptionOnCurrentRecord) {
|
||||
readPlain(buffer, 0, LittleEndianConsts.SHORT_SIZE);
|
||||
return LittleEndian.getShort(buffer);
|
||||
} else {
|
||||
return ccis.readShort();
|
||||
}
|
||||
}
|
||||
|
||||
public int readInt() {
|
||||
return _cipher.xorInt(_le.readInt());
|
||||
@Override
|
||||
public int readInt() {
|
||||
if (shouldSkipEncryptionOnCurrentRecord) {
|
||||
readPlain(buffer, 0, LittleEndianConsts.INT_SIZE);
|
||||
return LittleEndian.getInt(buffer);
|
||||
} else {
|
||||
return ccis.readInt();
|
||||
}
|
||||
}
|
||||
|
||||
public long readLong() {
|
||||
return _cipher.xorLong(_le.readLong());
|
||||
@Override
|
||||
public long readLong() {
|
||||
if (shouldSkipEncryptionOnCurrentRecord) {
|
||||
readPlain(buffer, 0, LittleEndianConsts.LONG_SIZE);
|
||||
return LittleEndian.getLong(buffer);
|
||||
} else {
|
||||
return ccis.readLong();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the absolute position in the stream
|
||||
*/
|
||||
public long getPosition() {
|
||||
return ccis.getPos();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void readPlain(byte b[], int off, int len) {
|
||||
try {
|
||||
int readBytes = ccis.readPlain(b, off, len);
|
||||
if (readBytes < len) {
|
||||
throw new RecordFormatException("buffer underrun");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RecordFormatException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -16,34 +16,9 @@
|
||||
==================================================================== */
|
||||
package org.apache.poi.hssf.record.crypto;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import org.apache.poi.EncryptedDocumentException;
|
||||
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
|
||||
import org.apache.poi.poifs.crypt.Decryptor;
|
||||
|
||||
public abstract class Biff8EncryptionKey {
|
||||
protected SecretKey _secretKey;
|
||||
|
||||
/**
|
||||
* Create using the default password and a specified docId
|
||||
* @param salt 16 bytes
|
||||
*/
|
||||
public static Biff8EncryptionKey create(byte[] salt) {
|
||||
return Biff8RC4Key.create(Decryptor.DEFAULT_PASSWORD, salt);
|
||||
}
|
||||
|
||||
public static Biff8EncryptionKey create(String password, byte[] salt) {
|
||||
return Biff8RC4Key.create(password, salt);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return <code>true</code> if the keyDigest is compatible with the specified saltData and saltHash
|
||||
*/
|
||||
public boolean validate(byte[] saltData, byte[] saltHash) {
|
||||
throw new EncryptedDocumentException("validate is not supported (in super-class).");
|
||||
}
|
||||
|
||||
public final class Biff8EncryptionKey {
|
||||
/**
|
||||
* 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
|
||||
|
@ -1,195 +0,0 @@
|
||||
/* ====================================================================
|
||||
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.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.ShortBufferException;
|
||||
|
||||
import org.apache.poi.EncryptedDocumentException;
|
||||
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 Cipher} instance is renewed (re-keyed) every 1024 bytes.
|
||||
*/
|
||||
final class Biff8RC4 implements Biff8Cipher {
|
||||
|
||||
private static final int RC4_REKEYING_INTERVAL = 1024;
|
||||
|
||||
private Cipher _rc4;
|
||||
|
||||
/**
|
||||
* This field is used to keep track of when to change the {@link Cipher}
|
||||
* 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 Biff8RC4Key _key;
|
||||
private ByteBuffer _buffer = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
public Biff8RC4(int initialOffset, Biff8RC4Key key) {
|
||||
if (initialOffset >= RC4_REKEYING_INTERVAL) {
|
||||
throw new RuntimeException("initialOffset (" + initialOffset + ")>"
|
||||
+ RC4_REKEYING_INTERVAL + " not supported yet");
|
||||
}
|
||||
_key = key;
|
||||
_rc4 = _key.getCipher();
|
||||
_streamPos = 0;
|
||||
rekeyForNextBlock();
|
||||
_streamPos = initialOffset;
|
||||
_shouldSkipEncryptionOnCurrentRecord = false;
|
||||
|
||||
encryptBytes(new byte[initialOffset], 0, initialOffset);
|
||||
}
|
||||
|
||||
|
||||
private void rekeyForNextBlock() {
|
||||
_currentKeyIndex = _streamPos / RC4_REKEYING_INTERVAL;
|
||||
_key.initCipherForBlock(_rc4, _currentKeyIndex);
|
||||
_nextRC4BlockStart = (_currentKeyIndex + 1) * RC4_REKEYING_INTERVAL;
|
||||
}
|
||||
|
||||
private void encryptBytes(byte data[], int offset, final int bytesToRead) {
|
||||
if (bytesToRead == 0) return;
|
||||
|
||||
if (_shouldSkipEncryptionOnCurrentRecord) {
|
||||
// even when encryption is skipped, we need to update the cipher
|
||||
byte dataCpy[] = new byte[bytesToRead];
|
||||
System.arraycopy(data, offset, dataCpy, 0, bytesToRead);
|
||||
data = dataCpy;
|
||||
offset = 0;
|
||||
}
|
||||
|
||||
try {
|
||||
_rc4.update(data, offset, bytesToRead, data, offset);
|
||||
} catch (ShortBufferException e) {
|
||||
throw new EncryptedDocumentException("input buffer too small", e);
|
||||
}
|
||||
}
|
||||
|
||||
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 Cipher} instance must step even when unencrypted bytes are read
|
||||
*/
|
||||
public void skipTwoBytes() {
|
||||
xor(_buffer.array(), 0, 2);
|
||||
}
|
||||
|
||||
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
|
||||
encryptBytes(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) {
|
||||
encryptBytes(buf, offset, nLeftInBlock);
|
||||
_streamPos += nLeftInBlock;
|
||||
offset += nLeftInBlock;
|
||||
len -= nLeftInBlock;
|
||||
}
|
||||
rekeyForNextBlock();
|
||||
}
|
||||
// all full blocks following
|
||||
while (len > RC4_REKEYING_INTERVAL) {
|
||||
encryptBytes(buf, offset, RC4_REKEYING_INTERVAL);
|
||||
_streamPos += RC4_REKEYING_INTERVAL;
|
||||
offset += RC4_REKEYING_INTERVAL;
|
||||
len -= RC4_REKEYING_INTERVAL;
|
||||
rekeyForNextBlock();
|
||||
}
|
||||
// finish with incomplete block
|
||||
encryptBytes(buf, offset, len);
|
||||
_streamPos += len;
|
||||
}
|
||||
|
||||
public int xorByte(int rawVal) {
|
||||
_buffer.put(0, (byte)rawVal);
|
||||
xor(_buffer.array(), 0, 1);
|
||||
return _buffer.get(0);
|
||||
}
|
||||
|
||||
public int xorShort(int rawVal) {
|
||||
_buffer.putShort(0, (short)rawVal);
|
||||
xor(_buffer.array(), 0, 2);
|
||||
return _buffer.getShort(0);
|
||||
}
|
||||
|
||||
public int xorInt(int rawVal) {
|
||||
_buffer.putInt(0, rawVal);
|
||||
xor(_buffer.array(), 0, 4);
|
||||
return _buffer.getInt(0);
|
||||
}
|
||||
|
||||
public long xorLong(long rawVal) {
|
||||
_buffer.putLong(0, rawVal);
|
||||
xor(_buffer.array(), 0, 8);
|
||||
return _buffer.getLong(0);
|
||||
}
|
||||
|
||||
public void setNextRecordSize(int recordSize) {
|
||||
/* no-op */
|
||||
}
|
||||
}
|
@ -1,155 +0,0 @@
|
||||
/* ====================================================================
|
||||
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.MessageDigest;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.ShortBufferException;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.apache.poi.EncryptedDocumentException;
|
||||
import org.apache.poi.poifs.crypt.CipherAlgorithm;
|
||||
import org.apache.poi.poifs.crypt.CryptoFunctions;
|
||||
import org.apache.poi.poifs.crypt.HashAlgorithm;
|
||||
import org.apache.poi.util.HexDump;
|
||||
import org.apache.poi.util.LittleEndian;
|
||||
import org.apache.poi.util.LittleEndianConsts;
|
||||
import org.apache.poi.util.POILogFactory;
|
||||
import org.apache.poi.util.POILogger;
|
||||
|
||||
public class Biff8RC4Key extends Biff8EncryptionKey {
|
||||
// these two constants coincidentally have the same value
|
||||
public static final int KEY_DIGEST_LENGTH = 5;
|
||||
private static final int PASSWORD_HASH_NUMBER_OF_BYTES_USED = 5;
|
||||
|
||||
private static POILogger log = POILogFactory.getLogger(Biff8RC4Key.class);
|
||||
|
||||
Biff8RC4Key(byte[] keyDigest) {
|
||||
if (keyDigest.length != KEY_DIGEST_LENGTH) {
|
||||
throw new IllegalArgumentException("Expected 5 byte key digest, but got " + HexDump.toHex(keyDigest));
|
||||
}
|
||||
|
||||
CipherAlgorithm ca = CipherAlgorithm.rc4;
|
||||
_secretKey = new SecretKeySpec(keyDigest.clone(), ca.jceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create using the default password and a specified docId
|
||||
* @param salt 16 bytes
|
||||
*/
|
||||
public static Biff8RC4Key create(String password, byte[] salt) {
|
||||
return new Biff8RC4Key(createKeyDigest(password, salt));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return <code>true</code> if the keyDigest is compatible with the specified saltData and saltHash
|
||||
*/
|
||||
public boolean validate(byte[] verifier, byte[] verifierHash) {
|
||||
check16Bytes(verifier, "verifier");
|
||||
check16Bytes(verifierHash, "verifierHash");
|
||||
|
||||
// validation uses the RC4 for block zero
|
||||
Cipher rc4 = getCipher();
|
||||
initCipherForBlock(rc4, 0);
|
||||
|
||||
byte[] verifierPrime = verifier.clone();
|
||||
byte[] verifierHashPrime = verifierHash.clone();
|
||||
|
||||
try {
|
||||
rc4.update(verifierPrime, 0, verifierPrime.length, verifierPrime);
|
||||
rc4.update(verifierHashPrime, 0, verifierHashPrime.length, verifierHashPrime);
|
||||
} catch (ShortBufferException e) {
|
||||
throw new EncryptedDocumentException("buffer too short", e);
|
||||
}
|
||||
|
||||
MessageDigest md5 = CryptoFunctions.getMessageDigest(HashAlgorithm.md5);
|
||||
md5.update(verifierPrime);
|
||||
byte[] finalVerifierResult = md5.digest();
|
||||
|
||||
if (log.check(POILogger.DEBUG)) {
|
||||
byte[] verifierHashThatWouldWork = xor(verifierHash, xor(verifierHashPrime, finalVerifierResult));
|
||||
log.log(POILogger.DEBUG, "valid verifierHash value", HexDump.toHex(verifierHashThatWouldWork));
|
||||
}
|
||||
|
||||
return Arrays.equals(verifierHashPrime, finalVerifierResult);
|
||||
}
|
||||
|
||||
Cipher getCipher() {
|
||||
CipherAlgorithm ca = CipherAlgorithm.rc4;
|
||||
Cipher rc4 = CryptoFunctions.getCipher(_secretKey, ca, null, null, Cipher.ENCRYPT_MODE);
|
||||
return rc4;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
MessageDigest md5 = CryptoFunctions.getMessageDigest(HashAlgorithm.md5);
|
||||
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);
|
||||
}
|
||||
|
||||
byte[] result = CryptoFunctions.getBlock0(md5.digest(), KEY_DIGEST_LENGTH);
|
||||
return result;
|
||||
}
|
||||
|
||||
void initCipherForBlock(Cipher rc4, int keyBlockNo) {
|
||||
byte buf[] = new byte[LittleEndianConsts.INT_SIZE];
|
||||
LittleEndian.putInt(buf, 0, keyBlockNo);
|
||||
|
||||
MessageDigest md5 = CryptoFunctions.getMessageDigest(HashAlgorithm.md5);
|
||||
md5.update(_secretKey.getEncoded());
|
||||
md5.update(buf);
|
||||
|
||||
SecretKeySpec skeySpec = new SecretKeySpec(md5.digest(), _secretKey.getAlgorithm());
|
||||
try {
|
||||
rc4.init(Cipher.ENCRYPT_MODE, skeySpec);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new EncryptedDocumentException("Can't rekey for next block", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] xor(byte[] a, byte[] b) {
|
||||
byte[] c = new byte[a.length];
|
||||
for (int i = 0; i < c.length; i++) {
|
||||
c[i] = (byte) (a[i] ^ b[i]);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
private static void check16Bytes(byte[] data, String argName) {
|
||||
if (data.length != 16) {
|
||||
throw new IllegalArgumentException("Expected 16 byte " + argName + ", but got " + HexDump.toHex(data));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,153 +0,0 @@
|
||||
/* ====================================================================
|
||||
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.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
|
||||
import org.apache.poi.hssf.record.BOFRecord;
|
||||
import org.apache.poi.hssf.record.FilePassRecord;
|
||||
import org.apache.poi.hssf.record.InterfaceHdrRecord;
|
||||
|
||||
public class Biff8XOR implements Biff8Cipher {
|
||||
|
||||
private final Biff8XORKey _key;
|
||||
private ByteBuffer _buffer = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN);
|
||||
private boolean _shouldSkipEncryptionOnCurrentRecord;
|
||||
private final int _initialOffset;
|
||||
private int _dataLength = 0;
|
||||
private int _xorArrayIndex = 0;
|
||||
|
||||
public Biff8XOR(int initialOffset, Biff8XORKey key) {
|
||||
_key = key;
|
||||
_initialOffset = initialOffset;
|
||||
|
||||
}
|
||||
|
||||
public void startRecord(int currentSid) {
|
||||
_shouldSkipEncryptionOnCurrentRecord = isNeverEncryptedRecord(currentSid);
|
||||
}
|
||||
|
||||
public void setNextRecordSize(int recordSize) {
|
||||
/*
|
||||
* From: http://social.msdn.microsoft.com/Forums/en-US/3dadbed3-0e68-4f11-8b43-3a2328d9ebd5
|
||||
*
|
||||
* The initial value for XorArrayIndex is as follows:
|
||||
* XorArrayIndex = (FileOffset + Data.Length) % 16
|
||||
*
|
||||
* The FileOffset variable in this context is the stream offset into the Workbook stream at
|
||||
* the time we are about to write each of the bytes of the record data.
|
||||
* This (the value) is then incremented after each byte is written.
|
||||
*/
|
||||
_xorArrayIndex = (_initialOffset+_dataLength+recordSize) % 16;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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 Cipher} instance must step even when unencrypted bytes are read
|
||||
*/
|
||||
public void skipTwoBytes() {
|
||||
_dataLength += 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts a xor obfuscated byte array.
|
||||
* The data is decrypted in-place
|
||||
*
|
||||
* @see <a href="http://msdn.microsoft.com/en-us/library/dd908506.aspx">2.3.7.3 Binary Document XOR Data Transformation Method 1</a>
|
||||
*/
|
||||
public void xor(byte[] buf, int pOffset, int pLen) {
|
||||
if (_shouldSkipEncryptionOnCurrentRecord) {
|
||||
_dataLength += pLen;
|
||||
return;
|
||||
}
|
||||
|
||||
// The following is taken from the Libre Office implementation
|
||||
// It seems that the encrypt and decrypt method is mixed up
|
||||
// in the MS-OFFCRYPTO docs
|
||||
|
||||
byte xorArray[] = _key._secretKey.getEncoded();
|
||||
|
||||
for (int i=0; i<pLen; i++) {
|
||||
byte value = buf[pOffset+i];
|
||||
value = rotateLeft(value, 3);
|
||||
value ^= xorArray[_xorArrayIndex];
|
||||
buf[pOffset+i] = value;
|
||||
_xorArrayIndex = (_xorArrayIndex + 1) % 16;
|
||||
_dataLength++;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte rotateLeft(byte bits, int shift) {
|
||||
return (byte)(((bits & 0xff) << shift) | ((bits & 0xff) >>> (8 - shift)));
|
||||
}
|
||||
|
||||
public int xorByte(int rawVal) {
|
||||
_buffer.put(0, (byte)rawVal);
|
||||
xor(_buffer.array(), 0, 1);
|
||||
return _buffer.get(0);
|
||||
}
|
||||
|
||||
public int xorShort(int rawVal) {
|
||||
_buffer.putShort(0, (short)rawVal);
|
||||
xor(_buffer.array(), 0, 2);
|
||||
return _buffer.getShort(0);
|
||||
}
|
||||
|
||||
public int xorInt(int rawVal) {
|
||||
_buffer.putInt(0, rawVal);
|
||||
xor(_buffer.array(), 0, 4);
|
||||
return _buffer.getInt(0);
|
||||
}
|
||||
|
||||
public long xorLong(long rawVal) {
|
||||
_buffer.putLong(0, rawVal);
|
||||
xor(_buffer.array(), 0, 8);
|
||||
return _buffer.getLong(0);
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
/* ====================================================================
|
||||
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 javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.apache.poi.poifs.crypt.CryptoFunctions;
|
||||
|
||||
|
||||
public class Biff8XORKey extends Biff8EncryptionKey {
|
||||
final int _xorKey;
|
||||
|
||||
public Biff8XORKey(String password, int xorKey) {
|
||||
_xorKey = xorKey;
|
||||
byte xorArray[] = CryptoFunctions.createXorArray1(password);
|
||||
_secretKey = new SecretKeySpec(xorArray, "XOR");
|
||||
}
|
||||
|
||||
public static Biff8XORKey create(String password, int xorKey) {
|
||||
return new Biff8XORKey(password, xorKey);
|
||||
}
|
||||
|
||||
public boolean validate(String password, int verifier) {
|
||||
int keyComp = CryptoFunctions.createXorKey1(password);
|
||||
int verifierComp = CryptoFunctions.createXorVerifier1(password);
|
||||
|
||||
return (_xorKey == keyComp && verifierComp == verifier);
|
||||
}
|
||||
}
|
@ -21,56 +21,56 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.GeneralSecurityException;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.ShortBufferException;
|
||||
|
||||
import org.apache.poi.EncryptedDocumentException;
|
||||
import org.apache.poi.util.Internal;
|
||||
import org.apache.poi.util.LittleEndianInput;
|
||||
import org.apache.poi.util.LittleEndianInputStream;
|
||||
|
||||
@Internal
|
||||
public abstract class ChunkedCipherInputStream extends LittleEndianInputStream {
|
||||
private final int _chunkSize;
|
||||
private final int _chunkBits;
|
||||
|
||||
|
||||
private final long _size;
|
||||
private final byte[] _chunk;
|
||||
private final byte[] _chunk, _plain;
|
||||
private final Cipher _cipher;
|
||||
|
||||
private int _lastIndex;
|
||||
private long _pos;
|
||||
private boolean _chunkIsValid = false;
|
||||
|
||||
public ChunkedCipherInputStream(LittleEndianInput stream, long size, int chunkSize)
|
||||
public ChunkedCipherInputStream(InputStream stream, long size, int chunkSize)
|
||||
throws GeneralSecurityException {
|
||||
this(stream, size, chunkSize, 0);
|
||||
}
|
||||
|
||||
public ChunkedCipherInputStream(LittleEndianInput stream, long size, int chunkSize, int initialPos)
|
||||
public ChunkedCipherInputStream(InputStream stream, long size, int chunkSize, int initialPos)
|
||||
throws GeneralSecurityException {
|
||||
super((InputStream)stream);
|
||||
super(stream);
|
||||
_size = size;
|
||||
_pos = initialPos;
|
||||
this._chunkSize = chunkSize;
|
||||
if (chunkSize == -1) {
|
||||
_chunk = new byte[4096];
|
||||
} else {
|
||||
_chunk = new byte[chunkSize];
|
||||
}
|
||||
int cs = chunkSize == -1 ? 4096 : chunkSize;
|
||||
_chunk = new byte[cs];
|
||||
_plain = new byte[cs];
|
||||
_chunkBits = Integer.bitCount(_chunk.length-1);
|
||||
_lastIndex = (int)(_pos >> _chunkBits);
|
||||
_cipher = initCipherForBlock(null, _lastIndex);
|
||||
}
|
||||
|
||||
|
||||
public final Cipher initCipherForBlock(int block) throws IOException, GeneralSecurityException {
|
||||
if (_chunkSize != -1) {
|
||||
throw new GeneralSecurityException("the cipher block can only be set for streaming encryption, e.g. CryptoAPI...");
|
||||
}
|
||||
|
||||
|
||||
_chunkIsValid = false;
|
||||
return initCipherForBlock(_cipher, block);
|
||||
}
|
||||
|
||||
|
||||
protected abstract Cipher initCipherForBlock(Cipher existing, int block)
|
||||
throws GeneralSecurityException;
|
||||
|
||||
@ -88,8 +88,12 @@ public abstract class ChunkedCipherInputStream extends LittleEndianInputStream {
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
return read(b, off, len, false);
|
||||
}
|
||||
|
||||
private int read(byte[] b, int off, int len, boolean readPlain) throws IOException {
|
||||
int total = 0;
|
||||
|
||||
|
||||
if (available() <= 0) {
|
||||
return -1;
|
||||
}
|
||||
@ -110,7 +114,9 @@ public abstract class ChunkedCipherInputStream extends LittleEndianInputStream {
|
||||
return total;
|
||||
}
|
||||
count = Math.min(avail, Math.min(count, len));
|
||||
System.arraycopy(_chunk, (int)(_pos & chunkMask), b, off, count);
|
||||
|
||||
System.arraycopy(readPlain ? _plain : _chunk, (int)(_pos & chunkMask), b, off, count);
|
||||
|
||||
off += count;
|
||||
len -= count;
|
||||
_pos += count;
|
||||
@ -139,7 +145,7 @@ public abstract class ChunkedCipherInputStream extends LittleEndianInputStream {
|
||||
public int available() {
|
||||
return remainingBytes();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Helper method for forbidden available call - we know the size beforehand, so it's ok ...
|
||||
*
|
||||
@ -148,7 +154,7 @@ public abstract class ChunkedCipherInputStream extends LittleEndianInputStream {
|
||||
private int remainingBytes() {
|
||||
return (int)(_size - _pos);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean markSupported() {
|
||||
return false;
|
||||
@ -158,21 +164,21 @@ public abstract class ChunkedCipherInputStream extends LittleEndianInputStream {
|
||||
public synchronized void mark(int readlimit) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public synchronized void reset() throws IOException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
private int getChunkMask() {
|
||||
protected int getChunkMask() {
|
||||
return _chunk.length-1;
|
||||
}
|
||||
|
||||
|
||||
private void nextChunk() throws GeneralSecurityException, IOException {
|
||||
if (_chunkSize != -1) {
|
||||
int index = (int)(_pos >> _chunkBits);
|
||||
initCipherForBlock(_cipher, index);
|
||||
|
||||
|
||||
if (_lastIndex != index) {
|
||||
super.skip((index - _lastIndex) << _chunkBits);
|
||||
}
|
||||
@ -183,18 +189,81 @@ public abstract class ChunkedCipherInputStream extends LittleEndianInputStream {
|
||||
final int todo = (int)Math.min(_size, _chunk.length);
|
||||
int readBytes = 0, totalBytes = 0;
|
||||
do {
|
||||
readBytes = super.read(_chunk, totalBytes, todo-totalBytes);
|
||||
readBytes = super.read(_plain, totalBytes, todo-totalBytes);
|
||||
totalBytes += Math.max(0, readBytes);
|
||||
} while (readBytes != -1 && totalBytes < todo);
|
||||
|
||||
if (readBytes == -1 && _pos+totalBytes < _size) {
|
||||
if (readBytes == -1 && _pos+totalBytes < _size && _size < Integer.MAX_VALUE) {
|
||||
throw new EOFException("buffer underrun");
|
||||
}
|
||||
|
||||
if (_chunkSize == -1) {
|
||||
_cipher.update(_chunk, 0, totalBytes, _chunk);
|
||||
System.arraycopy(_plain, 0, _chunk, 0, totalBytes);
|
||||
|
||||
invokeCipher(totalBytes, _chunkSize > -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for overriding the cipher invocation, i.e. XOR doesn't use a cipher
|
||||
* and uses it's own implementation
|
||||
*
|
||||
* @return
|
||||
* @throws BadPaddingException
|
||||
* @throws IllegalBlockSizeException
|
||||
* @throws ShortBufferException
|
||||
*/
|
||||
protected int invokeCipher(int totalBytes, boolean doFinal) throws GeneralSecurityException {
|
||||
if (doFinal) {
|
||||
return _cipher.doFinal(_chunk, 0, totalBytes, _chunk);
|
||||
} else {
|
||||
_cipher.doFinal(_chunk, 0, totalBytes, _chunk);
|
||||
return _cipher.update(_chunk, 0, totalBytes, _chunk);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used when BIFF header fields (sid, size) are being read. The internal
|
||||
* {@link Cipher} instance must step even when unencrypted bytes are read
|
||||
*/
|
||||
public int readPlain(byte b[], int off, int len) throws IOException {
|
||||
if (len <= 0) {
|
||||
return len;
|
||||
}
|
||||
|
||||
int readBytes, total = 0;
|
||||
do {
|
||||
readBytes = read(b, off, len, true);
|
||||
total += Math.max(0, readBytes);
|
||||
} while (readBytes > -1 && total < len);
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Some ciphers (actually just XOR) are based on the record size,
|
||||
* which needs to be set before encryption
|
||||
*
|
||||
* @param recordSize the size of the next record
|
||||
*/
|
||||
public void setNextRecordSize(int recordSize) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the chunk bytes
|
||||
*/
|
||||
protected byte[] getChunk() {
|
||||
return _chunk;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the plain bytes
|
||||
*/
|
||||
protected byte[] getPlain() {
|
||||
return _plain;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the absolute position in the stream
|
||||
*/
|
||||
public long getPos() {
|
||||
return _pos;
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,10 @@ import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.security.GeneralSecurityException;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.ShortBufferException;
|
||||
|
||||
import org.apache.poi.EncryptedDocumentException;
|
||||
import org.apache.poi.poifs.filesystem.DirectoryNode;
|
||||
@ -153,19 +156,17 @@ public abstract class ChunkedCipherOutputStream extends FilterOutputStream {
|
||||
|
||||
int ciLen;
|
||||
try {
|
||||
boolean doFinal = true;
|
||||
if (_chunkSize == STREAMING) {
|
||||
if (continued) {
|
||||
ciLen = _cipher.update(_chunk, 0, posInChunk, _chunk);
|
||||
} else {
|
||||
ciLen = _cipher.doFinal(_chunk, 0, posInChunk, _chunk);
|
||||
doFinal = false;
|
||||
}
|
||||
|
||||
// reset stream (not only) in case we were interrupted by plain stream parts
|
||||
_pos = 0;
|
||||
} else {
|
||||
_cipher = initCipherForBlock(_cipher, index, lastChunk);
|
||||
ciLen = _cipher.doFinal(_chunk, 0, posInChunk, _chunk);
|
||||
}
|
||||
ciLen = invokeCipher(posInChunk, doFinal);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new IOException("can't re-/initialize cipher", e);
|
||||
}
|
||||
@ -173,6 +174,23 @@ public abstract class ChunkedCipherOutputStream extends FilterOutputStream {
|
||||
out.write(_chunk, 0, ciLen);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for overriding the cipher invocation, i.e. XOR doesn't use a cipher
|
||||
* and uses it's own implementation
|
||||
*
|
||||
* @return
|
||||
* @throws BadPaddingException
|
||||
* @throws IllegalBlockSizeException
|
||||
* @throws ShortBufferException
|
||||
*/
|
||||
protected int invokeCipher(int posInChunk, boolean doFinal) throws GeneralSecurityException {
|
||||
if (doFinal) {
|
||||
return _cipher.doFinal(_chunk, 0, posInChunk, _chunk);
|
||||
} else {
|
||||
return _cipher.update(_chunk, 0, posInChunk, _chunk);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
try {
|
||||
|
@ -29,7 +29,6 @@ import org.apache.poi.poifs.filesystem.DirectoryNode;
|
||||
import org.apache.poi.poifs.filesystem.NPOIFSFileSystem;
|
||||
import org.apache.poi.poifs.filesystem.OPOIFSFileSystem;
|
||||
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
|
||||
import org.apache.poi.util.LittleEndianInput;
|
||||
|
||||
public abstract class Decryptor implements Cloneable {
|
||||
public static final String DEFAULT_PASSWORD="VelvetSweatshop";
|
||||
@ -66,7 +65,7 @@ public abstract class Decryptor implements Cloneable {
|
||||
* @param initialPos initial/current byte position within the stream
|
||||
* @return decrypted stream
|
||||
*/
|
||||
public InputStream getDataStream(LittleEndianInput stream, int size, int initialPos)
|
||||
public InputStream getDataStream(InputStream stream, int size, int initialPos)
|
||||
throws IOException, GeneralSecurityException {
|
||||
throw new RuntimeException("this decryptor doesn't support reading from a stream");
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import static org.apache.poi.poifs.crypt.EncryptionMode.agile;
|
||||
import static org.apache.poi.poifs.crypt.EncryptionMode.binaryRC4;
|
||||
import static org.apache.poi.poifs.crypt.EncryptionMode.cryptoAPI;
|
||||
import static org.apache.poi.poifs.crypt.EncryptionMode.standard;
|
||||
import static org.apache.poi.poifs.crypt.EncryptionMode.xor;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@ -35,6 +36,7 @@ import org.apache.poi.util.LittleEndianInput;
|
||||
/**
|
||||
*/
|
||||
public class EncryptionInfo implements Cloneable {
|
||||
private final EncryptionMode encryptionMode;
|
||||
private final int versionMajor;
|
||||
private final int versionMinor;
|
||||
private final int encryptionFlags;
|
||||
@ -75,49 +77,55 @@ public class EncryptionInfo implements Cloneable {
|
||||
public EncryptionInfo(POIFSFileSystem fs) throws IOException {
|
||||
this(fs.getRoot());
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens for decryption
|
||||
*/
|
||||
public EncryptionInfo(OPOIFSFileSystem fs) throws IOException {
|
||||
this(fs.getRoot());
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens for decryption
|
||||
*/
|
||||
public EncryptionInfo(NPOIFSFileSystem fs) throws IOException {
|
||||
this(fs.getRoot());
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens for decryption
|
||||
*/
|
||||
public EncryptionInfo(DirectoryNode dir) throws IOException {
|
||||
this(dir.createDocumentInputStream("EncryptionInfo"), false);
|
||||
this(dir.createDocumentInputStream("EncryptionInfo"), null);
|
||||
}
|
||||
|
||||
public EncryptionInfo(LittleEndianInput dis, boolean isCryptoAPI) throws IOException {
|
||||
final EncryptionMode encryptionMode;
|
||||
versionMajor = dis.readUShort();
|
||||
versionMinor = dis.readUShort();
|
||||
public EncryptionInfo(LittleEndianInput dis, EncryptionMode preferredEncryptionMode) throws IOException {
|
||||
if (preferredEncryptionMode == xor) {
|
||||
versionMajor = xor.versionMajor;
|
||||
versionMinor = xor.versionMinor;
|
||||
} else {
|
||||
versionMajor = dis.readUShort();
|
||||
versionMinor = dis.readUShort();
|
||||
}
|
||||
|
||||
if ( versionMajor == binaryRC4.versionMajor
|
||||
if ( versionMajor == xor.versionMajor
|
||||
&& versionMinor == xor.versionMinor) {
|
||||
encryptionMode = xor;
|
||||
encryptionFlags = -1;
|
||||
} else if ( versionMajor == binaryRC4.versionMajor
|
||||
&& versionMinor == binaryRC4.versionMinor) {
|
||||
encryptionMode = binaryRC4;
|
||||
encryptionFlags = -1;
|
||||
} else if (!isCryptoAPI
|
||||
&& versionMajor == agile.versionMajor
|
||||
} else if (
|
||||
2 <= versionMajor && versionMajor <= 4
|
||||
&& versionMinor == 2) {
|
||||
encryptionMode = (preferredEncryptionMode == cryptoAPI) ? cryptoAPI : standard;
|
||||
encryptionFlags = dis.readInt();
|
||||
} else if (
|
||||
versionMajor == agile.versionMajor
|
||||
&& versionMinor == agile.versionMinor){
|
||||
encryptionMode = agile;
|
||||
encryptionFlags = dis.readInt();
|
||||
} else if (!isCryptoAPI
|
||||
&& 2 <= versionMajor && versionMajor <= 4
|
||||
&& versionMinor == standard.versionMinor) {
|
||||
encryptionMode = standard;
|
||||
encryptionFlags = dis.readInt();
|
||||
} else if (isCryptoAPI
|
||||
&& 2 <= versionMajor && versionMajor <= 4
|
||||
&& versionMinor == cryptoAPI.versionMinor) {
|
||||
encryptionMode = cryptoAPI;
|
||||
encryptionFlags = dis.readInt();
|
||||
} else {
|
||||
encryptionFlags = dis.readInt();
|
||||
throw new EncryptedDocumentException(
|
||||
@ -170,6 +178,7 @@ public class EncryptionInfo implements Cloneable {
|
||||
, int blockSize
|
||||
, ChainingMode chainingMode
|
||||
) {
|
||||
this.encryptionMode = encryptionMode;
|
||||
versionMajor = encryptionMode.versionMajor;
|
||||
versionMinor = encryptionMode.versionMinor;
|
||||
encryptionFlags = encryptionMode.encryptionFlags;
|
||||
@ -236,6 +245,10 @@ public class EncryptionInfo implements Cloneable {
|
||||
this.encryptor = encryptor;
|
||||
}
|
||||
|
||||
public EncryptionMode getEncryptionMode() {
|
||||
return encryptionMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EncryptionInfo clone() throws CloneNotSupportedException {
|
||||
EncryptionInfo other = (EncryptionInfo)super.clone();
|
||||
|
@ -33,7 +33,9 @@ public enum EncryptionMode {
|
||||
/* @see <a href="http://msdn.microsoft.com/en-us/library/dd906097(v=office.12).aspx">2.3.4.5 \EncryptionInfo Stream (Standard Encryption)</a> */
|
||||
standard("org.apache.poi.poifs.crypt.standard.StandardEncryptionInfoBuilder", 4, 2, 0x24),
|
||||
/* @see <a href="http://msdn.microsoft.com/en-us/library/dd925810(v=office.12).aspx">2.3.4.10 \EncryptionInfo Stream (Agile Encryption)</a> */
|
||||
agile("org.apache.poi.poifs.crypt.agile.AgileEncryptionInfoBuilder", 4, 4, 0x40)
|
||||
agile("org.apache.poi.poifs.crypt.agile.AgileEncryptionInfoBuilder", 4, 4, 0x40),
|
||||
/* @see <a href="https://msdn.microsoft.com/en-us/library/dd907599(v=office.12).aspx">XOR Obfuscation</a> */
|
||||
xor("org.apache.poi.poifs.crypt.xor.XOREncryptionInfoBuilder", 0, 0, 0)
|
||||
;
|
||||
|
||||
public final String builder;
|
||||
|
@ -29,11 +29,9 @@ import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.apache.poi.EncryptedDocumentException;
|
||||
import org.apache.poi.poifs.crypt.*;
|
||||
import org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIDecryptor;
|
||||
import org.apache.poi.poifs.filesystem.DirectoryNode;
|
||||
import org.apache.poi.poifs.filesystem.DocumentInputStream;
|
||||
import org.apache.poi.util.LittleEndian;
|
||||
import org.apache.poi.util.LittleEndianInput;
|
||||
import org.apache.poi.util.StringUtil;
|
||||
|
||||
public class BinaryRC4Decryptor extends Decryptor implements Cloneable {
|
||||
@ -53,7 +51,7 @@ public class BinaryRC4Decryptor extends Decryptor implements Cloneable {
|
||||
super(stream, size, _chunkSize);
|
||||
}
|
||||
|
||||
public BinaryRC4CipherInputStream(LittleEndianInput stream)
|
||||
public BinaryRC4CipherInputStream(InputStream stream)
|
||||
throws GeneralSecurityException {
|
||||
super(stream, Integer.MAX_VALUE, _chunkSize);
|
||||
}
|
||||
@ -140,7 +138,8 @@ public class BinaryRC4Decryptor extends Decryptor implements Cloneable {
|
||||
return new BinaryRC4CipherInputStream(dis, _length);
|
||||
}
|
||||
|
||||
public InputStream getDataStream(LittleEndianInput stream)
|
||||
@Override
|
||||
public InputStream getDataStream(InputStream stream, int size, int initialPos)
|
||||
throws IOException, GeneralSecurityException {
|
||||
return new BinaryRC4CipherInputStream(stream);
|
||||
}
|
||||
|
@ -45,7 +45,6 @@ import org.apache.poi.util.BitFieldFactory;
|
||||
import org.apache.poi.util.BoundedInputStream;
|
||||
import org.apache.poi.util.IOUtils;
|
||||
import org.apache.poi.util.LittleEndian;
|
||||
import org.apache.poi.util.LittleEndianInput;
|
||||
import org.apache.poi.util.LittleEndianInputStream;
|
||||
import org.apache.poi.util.StringUtil;
|
||||
|
||||
@ -146,7 +145,7 @@ public class CryptoAPIDecryptor extends Decryptor implements Cloneable {
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChunkedCipherInputStream getDataStream(LittleEndianInput stream, int size, int initialPos)
|
||||
public ChunkedCipherInputStream getDataStream(InputStream stream, int size, int initialPos)
|
||||
throws IOException, GeneralSecurityException {
|
||||
return new CryptoAPICipherInputStream(stream, size, initialPos);
|
||||
}
|
||||
@ -233,7 +232,7 @@ public class CryptoAPIDecryptor extends Decryptor implements Cloneable {
|
||||
return CryptoAPIDecryptor.this.initCipherForBlock(existing, block);
|
||||
}
|
||||
|
||||
public CryptoAPICipherInputStream(LittleEndianInput stream, long size, int initialPos)
|
||||
public CryptoAPICipherInputStream(InputStream stream, long size, int initialPos)
|
||||
throws GeneralSecurityException {
|
||||
super(stream, size, _chunkSize, initialPos);
|
||||
}
|
||||
|
174
src/java/org/apache/poi/poifs/crypt/xor/XORDecryptor.java
Normal file
174
src/java/org/apache/poi/poifs/crypt/xor/XORDecryptor.java
Normal file
@ -0,0 +1,174 @@
|
||||
/* ====================================================================
|
||||
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.poifs.crypt.xor;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.GeneralSecurityException;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.apache.poi.poifs.crypt.ChunkedCipherInputStream;
|
||||
import org.apache.poi.poifs.crypt.CryptoFunctions;
|
||||
import org.apache.poi.poifs.crypt.Decryptor;
|
||||
import org.apache.poi.poifs.crypt.EncryptionInfo;
|
||||
import org.apache.poi.poifs.filesystem.DirectoryNode;
|
||||
import org.apache.poi.util.LittleEndian;
|
||||
|
||||
public class XORDecryptor extends Decryptor implements Cloneable {
|
||||
private long _length = -1L;
|
||||
private int _chunkSize = 512;
|
||||
|
||||
private class XORCipherInputStream extends ChunkedCipherInputStream {
|
||||
private final int _initialOffset;
|
||||
private int _recordStart = 0;
|
||||
private int _recordEnd = 0;
|
||||
|
||||
@Override
|
||||
protected Cipher initCipherForBlock(Cipher existing, int block)
|
||||
throws GeneralSecurityException {
|
||||
return XORDecryptor.this.initCipherForBlock(existing, block);
|
||||
}
|
||||
|
||||
public XORCipherInputStream(InputStream stream, int initialPos)
|
||||
throws GeneralSecurityException {
|
||||
super(stream, Integer.MAX_VALUE, _chunkSize);
|
||||
_initialOffset = initialPos;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int invokeCipher(int totalBytes, boolean doFinal) {
|
||||
final int pos = (int)getPos();
|
||||
final byte xorArray[] = getEncryptionInfo().getDecryptor().getSecretKey().getEncoded();
|
||||
final byte chunk[] = getChunk();
|
||||
final byte plain[] = getPlain();
|
||||
final int posInChunk = pos & getChunkMask();
|
||||
|
||||
/*
|
||||
* From: http://social.msdn.microsoft.com/Forums/en-US/3dadbed3-0e68-4f11-8b43-3a2328d9ebd5
|
||||
*
|
||||
* The initial value for XorArrayIndex is as follows:
|
||||
* XorArrayIndex = (FileOffset + Data.Length) % 16
|
||||
*
|
||||
* The FileOffset variable in this context is the stream offset into the Workbook stream at
|
||||
* the time we are about to write each of the bytes of the record data.
|
||||
* This (the value) is then incremented after each byte is written.
|
||||
*/
|
||||
final int xorArrayIndex = _initialOffset+_recordEnd+(pos-_recordStart);
|
||||
|
||||
for (int i=0; pos+i < _recordEnd && i < totalBytes; i++) {
|
||||
// The following is taken from the Libre Office implementation
|
||||
// It seems that the encrypt and decrypt method is mixed up
|
||||
// in the MS-OFFCRYPTO docs
|
||||
byte value = plain[posInChunk+i];
|
||||
value = rotateLeft(value, 3);
|
||||
value ^= xorArray[(xorArrayIndex+i) & 0x0F];
|
||||
chunk[posInChunk+i] = value;
|
||||
}
|
||||
|
||||
// the other bytes will be encoded, when setNextRecordSize is called the next time
|
||||
return totalBytes;
|
||||
}
|
||||
|
||||
private byte rotateLeft(byte bits, int shift) {
|
||||
return (byte)(((bits & 0xff) << shift) | ((bits & 0xff) >>> (8 - shift)));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Decrypts a xor obfuscated byte array.
|
||||
* The data is decrypted in-place
|
||||
*
|
||||
* @see <a href="http://msdn.microsoft.com/en-us/library/dd908506.aspx">2.3.7.3 Binary Document XOR Data Transformation Method 1</a>
|
||||
*/
|
||||
@Override
|
||||
public void setNextRecordSize(int recordSize) {
|
||||
_recordStart = (int)getPos();
|
||||
_recordEnd = _recordStart+recordSize;
|
||||
int pos = (int)getPos();
|
||||
byte chunk[] = getChunk();
|
||||
int chunkMask = getChunkMask();
|
||||
int nextBytes = Math.min(recordSize, chunk.length-(pos & chunkMask));
|
||||
invokeCipher(nextBytes, true);
|
||||
}
|
||||
}
|
||||
|
||||
protected XORDecryptor() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verifyPassword(String password) {
|
||||
XOREncryptionVerifier ver = (XOREncryptionVerifier)getEncryptionInfo().getVerifier();
|
||||
int keyVer = LittleEndian.getUShort(ver.getEncryptedKey());
|
||||
int verifierVer = LittleEndian.getUShort(ver.getEncryptedVerifier());
|
||||
int keyComp = CryptoFunctions.createXorKey1(password);
|
||||
int verifierComp = CryptoFunctions.createXorVerifier1(password);
|
||||
if (keyVer == keyComp && verifierVer == verifierComp) {
|
||||
byte xorArray[] = CryptoFunctions.createXorArray1(password);
|
||||
setSecretKey(new SecretKeySpec(xorArray, "XOR"));
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cipher initCipherForBlock(Cipher cipher, int block)
|
||||
throws GeneralSecurityException {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected static Cipher initCipherForBlock(Cipher cipher, int block,
|
||||
EncryptionInfo encryptionInfo, SecretKey skey, int encryptMode)
|
||||
throws GeneralSecurityException {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChunkedCipherInputStream getDataStream(DirectoryNode dir) throws IOException, GeneralSecurityException {
|
||||
throw new RuntimeException("not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getDataStream(InputStream stream, int size, int initialPos)
|
||||
throws IOException, GeneralSecurityException {
|
||||
return new XORCipherInputStream(stream, initialPos);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public long getLength() {
|
||||
if (_length == -1L) {
|
||||
throw new IllegalStateException("Decryptor.getDataStream() was not called");
|
||||
}
|
||||
|
||||
return _length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChunkSize(int chunkSize) {
|
||||
_chunkSize = chunkSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public XORDecryptor clone() throws CloneNotSupportedException {
|
||||
return (XORDecryptor)super.clone();
|
||||
}
|
||||
}
|
@ -1,30 +1,37 @@
|
||||
/* ====================================================================
|
||||
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;
|
||||
|
||||
|
||||
public interface Biff8Cipher {
|
||||
void startRecord(int currentSid);
|
||||
void setNextRecordSize(int recordSize);
|
||||
void skipTwoBytes();
|
||||
void xor(byte[] buf, int pOffset, int pLen);
|
||||
int xorByte(int rawVal);
|
||||
int xorShort(int rawVal);
|
||||
int xorInt(int rawVal);
|
||||
long xorLong(long rawVal);
|
||||
}
|
||||
/* ====================================================================
|
||||
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.poifs.crypt.xor;
|
||||
|
||||
import org.apache.poi.poifs.crypt.EncryptionHeader;
|
||||
import org.apache.poi.poifs.crypt.standard.EncryptionRecord;
|
||||
import org.apache.poi.util.LittleEndianByteArrayOutputStream;
|
||||
|
||||
public class XOREncryptionHeader extends EncryptionHeader implements EncryptionRecord, Cloneable {
|
||||
|
||||
protected XOREncryptionHeader() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(LittleEndianByteArrayOutputStream littleendianbytearrayoutputstream) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public XOREncryptionHeader clone() throws CloneNotSupportedException {
|
||||
return (XOREncryptionHeader)super.clone();
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
/* ====================================================================
|
||||
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.poifs.crypt.xor;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.apache.poi.poifs.crypt.ChainingMode;
|
||||
import org.apache.poi.poifs.crypt.CipherAlgorithm;
|
||||
import org.apache.poi.poifs.crypt.Decryptor;
|
||||
import org.apache.poi.poifs.crypt.EncryptionInfo;
|
||||
import org.apache.poi.poifs.crypt.EncryptionInfoBuilder;
|
||||
import org.apache.poi.poifs.crypt.Encryptor;
|
||||
import org.apache.poi.poifs.crypt.HashAlgorithm;
|
||||
import org.apache.poi.util.LittleEndianInput;
|
||||
|
||||
public class XOREncryptionInfoBuilder implements EncryptionInfoBuilder {
|
||||
|
||||
public XOREncryptionInfoBuilder() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(EncryptionInfo info, LittleEndianInput dis)
|
||||
throws IOException {
|
||||
info.setHeader(new XOREncryptionHeader());
|
||||
info.setVerifier(new XOREncryptionVerifier(dis));
|
||||
Decryptor dec = new XORDecryptor();
|
||||
dec.setEncryptionInfo(info);
|
||||
info.setDecryptor(dec);
|
||||
Encryptor enc = new XOREncryptor();
|
||||
enc.setEncryptionInfo(info);
|
||||
info.setEncryptor(enc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(EncryptionInfo info,
|
||||
CipherAlgorithm cipherAlgorithm, HashAlgorithm hashAlgorithm,
|
||||
int keyBits, int blockSize, ChainingMode chainingMode) {
|
||||
info.setHeader(new XOREncryptionHeader());
|
||||
info.setVerifier(new XOREncryptionVerifier());
|
||||
Decryptor dec = new XORDecryptor();
|
||||
dec.setEncryptionInfo(info);
|
||||
info.setDecryptor(dec);
|
||||
Encryptor enc = new XOREncryptor();
|
||||
enc.setEncryptionInfo(info);
|
||||
info.setEncryptor(enc);
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
/* ====================================================================
|
||||
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.poifs.crypt.xor;
|
||||
|
||||
import org.apache.poi.poifs.crypt.EncryptionVerifier;
|
||||
import org.apache.poi.poifs.crypt.standard.EncryptionRecord;
|
||||
import org.apache.poi.util.LittleEndianByteArrayOutputStream;
|
||||
import org.apache.poi.util.LittleEndianInput;
|
||||
|
||||
public class XOREncryptionVerifier extends EncryptionVerifier implements EncryptionRecord, Cloneable {
|
||||
|
||||
protected XOREncryptionVerifier() {
|
||||
setEncryptedKey(new byte[2]);
|
||||
setEncryptedVerifier(new byte[2]);
|
||||
}
|
||||
|
||||
protected XOREncryptionVerifier(LittleEndianInput is) {
|
||||
/**
|
||||
* key (2 bytes): An unsigned integer that specifies the obfuscation key.
|
||||
* See [MS-OFFCRYPTO], 2.3.6.2 section, the first step of initializing XOR
|
||||
* array where it describes the generation of 16-bit XorKey value.
|
||||
*/
|
||||
byte key[] = new byte[2];
|
||||
is.readFully(key);
|
||||
setEncryptedKey(key);
|
||||
|
||||
/**
|
||||
* verificationBytes (2 bytes): An unsigned integer that specifies
|
||||
* the password verification identifier.
|
||||
*/
|
||||
byte verifier[] = new byte[2];
|
||||
is.readFully(verifier);
|
||||
setEncryptedVerifier(verifier);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(LittleEndianByteArrayOutputStream bos) {
|
||||
bos.write(getEncryptedKey());
|
||||
bos.write(getEncryptedVerifier());
|
||||
}
|
||||
|
||||
@Override
|
||||
public XOREncryptionVerifier clone() throws CloneNotSupportedException {
|
||||
return (XOREncryptionVerifier)super.clone();
|
||||
}
|
||||
}
|
99
src/java/org/apache/poi/poifs/crypt/xor/XOREncryptor.java
Normal file
99
src/java/org/apache/poi/poifs/crypt/xor/XOREncryptor.java
Normal file
@ -0,0 +1,99 @@
|
||||
/* ====================================================================
|
||||
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.poifs.crypt.xor;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Random;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import org.apache.poi.EncryptedDocumentException;
|
||||
import org.apache.poi.poifs.crypt.ChunkedCipherOutputStream;
|
||||
import org.apache.poi.poifs.crypt.CryptoFunctions;
|
||||
import org.apache.poi.poifs.crypt.DataSpaceMapUtils;
|
||||
import org.apache.poi.poifs.crypt.EncryptionInfo;
|
||||
import org.apache.poi.poifs.crypt.Encryptor;
|
||||
import org.apache.poi.poifs.crypt.HashAlgorithm;
|
||||
import org.apache.poi.poifs.crypt.standard.EncryptionRecord;
|
||||
import org.apache.poi.poifs.filesystem.DirectoryNode;
|
||||
import org.apache.poi.util.LittleEndianByteArrayOutputStream;
|
||||
|
||||
public class XOREncryptor extends Encryptor implements Cloneable {
|
||||
|
||||
protected XOREncryptor() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void confirmPassword(String password) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void confirmPassword(String password, byte keySpec[],
|
||||
byte keySalt[], byte verifier[], byte verifierSalt[],
|
||||
byte integritySalt[]) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream getDataStream(DirectoryNode dir)
|
||||
throws IOException, GeneralSecurityException {
|
||||
OutputStream countStream = new XORCipherOutputStream(dir);
|
||||
return countStream;
|
||||
}
|
||||
|
||||
protected int getKeySizeInBytes() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
protected void createEncryptionInfoEntry(DirectoryNode dir) throws IOException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public XOREncryptor clone() throws CloneNotSupportedException {
|
||||
return (XOREncryptor)super.clone();
|
||||
}
|
||||
|
||||
protected class XORCipherOutputStream extends ChunkedCipherOutputStream {
|
||||
|
||||
@Override
|
||||
protected Cipher initCipherForBlock(Cipher cipher, int block, boolean lastChunk)
|
||||
throws GeneralSecurityException {
|
||||
return XORDecryptor.initCipherForBlock(cipher, block, getEncryptionInfo(), getSecretKey(), Cipher.ENCRYPT_MODE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void calculateChecksum(File file, int i) {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void createEncryptionInfoEntry(DirectoryNode dir, File tmpFile)
|
||||
throws IOException, GeneralSecurityException {
|
||||
XOREncryptor.this.createEncryptionInfoEntry(dir);
|
||||
}
|
||||
|
||||
public XORCipherOutputStream(DirectoryNode dir)
|
||||
throws IOException, GeneralSecurityException {
|
||||
super(dir, 512);
|
||||
}
|
||||
}
|
||||
}
|
@ -53,7 +53,8 @@ public final class DocumentEncryptionAtom extends PositionDependentRecordAtom {
|
||||
|
||||
ByteArrayInputStream bis = new ByteArrayInputStream(source, start+8, len-8);
|
||||
LittleEndianInputStream leis = new LittleEndianInputStream(bis);
|
||||
ei = new EncryptionInfo(leis, true);
|
||||
ei = new EncryptionInfo(leis, EncryptionMode.cryptoAPI);
|
||||
leis.close();
|
||||
}
|
||||
|
||||
public DocumentEncryptionAtom() {
|
||||
@ -121,6 +122,7 @@ public final class DocumentEncryptionAtom extends PositionDependentRecordAtom {
|
||||
LittleEndian.putInt(_header, 4, bos.getWriteIndex());
|
||||
out.write(_header);
|
||||
out.write(data, 0, bos.getWriteIndex());
|
||||
bos.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -21,8 +21,8 @@ import org.apache.poi.hssf.record.aggregates.AllRecordAggregateTests;
|
||||
import org.apache.poi.hssf.record.cf.TestCellRange;
|
||||
import org.apache.poi.hssf.record.chart.AllChartRecordTests;
|
||||
import org.apache.poi.hssf.record.common.TestUnicodeString;
|
||||
import org.apache.poi.hssf.record.crypto.AllHSSFEncryptionTests;
|
||||
import org.apache.poi.hssf.record.pivot.AllPivotRecordTests;
|
||||
import org.apache.poi.poifs.crypt.AllEncryptionTests;
|
||||
import org.apache.poi.ss.formula.constant.TestConstantValueParser;
|
||||
import org.apache.poi.ss.formula.ptg.AllFormulaTests;
|
||||
import org.junit.runner.RunWith;
|
||||
@ -34,7 +34,7 @@ import org.junit.runners.Suite;
|
||||
@RunWith(Suite.class)
|
||||
@Suite.SuiteClasses({
|
||||
AllChartRecordTests.class,
|
||||
AllHSSFEncryptionTests.class,
|
||||
AllEncryptionTests.class,
|
||||
AllFormulaTests.class,
|
||||
AllPivotRecordTests.class,
|
||||
AllRecordAggregateTests.class,
|
||||
|
@ -1,102 +0,0 @@
|
||||
/* ====================================================================
|
||||
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 junit.framework.ComparisonFailure;
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import org.apache.poi.util.HexDump;
|
||||
import org.apache.poi.util.HexRead;
|
||||
|
||||
/**
|
||||
* 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 = Biff8RC4Key.createKeyDigest("MoneyForNothing", docIdData);
|
||||
byte[] expResult = fromHex("C2 D9 56 B2 6B");
|
||||
if (!Arrays.equals(expResult, keyDigest)) {
|
||||
throw new ComparisonFailure("keyDigest mismatch", HexDump.toHex(expResult), HexDump.toHex(keyDigest));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void testValidateWithDefaultPassword() {
|
||||
|
||||
String docIdSuffixA = "F 35 52 38 0D 75 4A E6 85 C2 FD 78 CE 3D D1 B6"; // valid prefix is 'D'
|
||||
String saltHashA = "30 38 BE 5E 93 C5 7E B4 5F 52 CD A1 C6 8F B6 2A";
|
||||
String saltDataA = "D4 04 43 EC B7 A7 6F 6A D2 68 C7 DF CF A8 80 68";
|
||||
|
||||
String docIdB = "39 D7 80 41 DA E4 74 2C 8C 84 F9 4D 39 9A 19 2D";
|
||||
String saltDataSuffixB = "3 EA 8D 52 11 11 37 D2 BD 55 4C 01 0A 47 6E EB"; // valid prefix is 'C'
|
||||
String saltHashB = "96 19 F5 D0 F1 63 08 F1 3E 09 40 1E 87 F0 4E 16";
|
||||
|
||||
confirmValid(true, "D" + docIdSuffixA, saltDataA, saltHashA);
|
||||
confirmValid(true, docIdB, "C" + saltDataSuffixB, saltHashB);
|
||||
confirmValid(false, "E" + docIdSuffixA, saltDataA, saltHashA);
|
||||
confirmValid(false, docIdB, "B" + saltDataSuffixB, saltHashB);
|
||||
}
|
||||
|
||||
public void testValidateWithSuppliedPassword() {
|
||||
|
||||
String docId = "DF 35 52 38 0D 75 4A E6 85 C2 FD 78 CE 3D D1 B6";
|
||||
String saltData = "D4 04 43 EC B7 A7 6F 6A D2 68 C7 DF CF A8 80 68";
|
||||
String saltHashA = "8D C2 63 CC E1 1D E0 05 20 16 96 AF 48 59 94 64"; // for password '5ecret'
|
||||
String saltHashB = "31 0B 0D A4 69 55 8E 27 A1 03 AD C9 AE F8 09 04"; // for password '5ecret'
|
||||
|
||||
confirmValid(true, docId, saltData, saltHashA, "5ecret");
|
||||
confirmValid(false, docId, saltData, saltHashA, "Secret");
|
||||
confirmValid(true, docId, saltData, saltHashB, "Secret");
|
||||
confirmValid(false, docId, saltData, saltHashB, "secret");
|
||||
}
|
||||
|
||||
|
||||
private static void confirmValid(boolean expectedResult,
|
||||
String docIdHex, String saltDataHex, String saltHashHex) {
|
||||
confirmValid(expectedResult, docIdHex, saltDataHex, saltHashHex, null);
|
||||
}
|
||||
private static void confirmValid(boolean expectedResult,
|
||||
String docIdHex, String saltDataHex, String saltHashHex, String password) {
|
||||
byte[] docId = fromHex(docIdHex);
|
||||
byte[] saltData = fromHex(saltDataHex);
|
||||
byte[] saltHash = fromHex(saltHashHex);
|
||||
|
||||
|
||||
Biff8EncryptionKey key;
|
||||
if (password == null) {
|
||||
key = Biff8EncryptionKey.create(docId);
|
||||
} else {
|
||||
key = Biff8EncryptionKey.create(password, docId);
|
||||
}
|
||||
boolean actResult = key.validate(saltData, saltHash);
|
||||
if (expectedResult) {
|
||||
assertTrue("validate failed", actResult);
|
||||
} else {
|
||||
assertFalse("validate succeeded unexpectedly", actResult);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
/* ====================================================================
|
||||
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.usermodel;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.apache.poi.hssf.HSSFITestDataProvider;
|
||||
import org.apache.poi.hssf.extractor.ExcelExtractor;
|
||||
import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Test;
|
||||
|
||||
public class TestCryptoAPI {
|
||||
final HSSFITestDataProvider ssTests = HSSFITestDataProvider.instance;
|
||||
|
||||
@AfterClass
|
||||
public static void resetPW() {
|
||||
Biff8EncryptionKey.setCurrentUserPassword(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bug59857() throws IOException {
|
||||
Biff8EncryptionKey.setCurrentUserPassword("abc");
|
||||
HSSFWorkbook wb1 = ssTests.openSampleWorkbook("xor-encryption-abc.xls");
|
||||
String textExpected = "Sheet1\n1\n2\n3\n";
|
||||
String textActual = new ExcelExtractor(wb1).getText();
|
||||
assertEquals(textExpected, textActual);
|
||||
wb1.close();
|
||||
|
||||
Biff8EncryptionKey.setCurrentUserPassword("password");
|
||||
HSSFWorkbook wb2 = ssTests.openSampleWorkbook("password.xls");
|
||||
textExpected = "A ZIP bomb is a variant of mail-bombing. After most commercial mail servers began checking mail with anti-virus software and filtering certain malicious file types, trojan horse viruses tried to send themselves compressed into archives, such as ZIP, RAR or 7-Zip. Mail server software was then configured to unpack archives and check their contents as well. That gave black hats the idea to compose a \"bomb\" consisting of an enormous text file, containing, for example, only the letter z repeated millions of times. Such a file compresses into a relatively small archive, but its unpacking (especially by early versions of mail servers) would use a high amount of processing power, RAM and swap space, which could result in denial of service. Modern mail server computers usually have sufficient intelligence to recognize such attacks as well as sufficient processing power and memory space to process malicious attachments without interruption of service, though some are still susceptible to this technique if the ZIP bomb is mass-mailed.";
|
||||
textActual = new ExcelExtractor(wb2).getText();
|
||||
assertTrue(textActual.contains(textExpected));
|
||||
wb2.close();
|
||||
|
||||
Biff8EncryptionKey.setCurrentUserPassword("freedom");
|
||||
HSSFWorkbook wb3 = ssTests.openSampleWorkbook("35897-type4.xls");
|
||||
textExpected = "Sheet1\nhello there!\n";
|
||||
textActual = new ExcelExtractor(wb3).getText();
|
||||
assertEquals(textExpected, textActual);
|
||||
wb3.close();
|
||||
}
|
||||
}
|
@ -15,20 +15,19 @@
|
||||
limitations under the License.
|
||||
==================================================================== */
|
||||
|
||||
package org.apache.poi.hssf.record.crypto;
|
||||
package org.apache.poi.poifs.crypt;
|
||||
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Suite;
|
||||
|
||||
/**
|
||||
* Collects all tests for package <tt>org.apache.poi.hssf.record.crypto</tt>.
|
||||
*
|
||||
* @author Josh Micich
|
||||
* Collects all tests for package <tt>org.apache.poi.poifs.crypt</tt>.
|
||||
*/
|
||||
@RunWith(Suite.class)
|
||||
@Suite.SuiteClasses({
|
||||
TestBiff8DecryptingStream.class,
|
||||
TestBiff8EncryptionKey.class
|
||||
TestCipherAlgorithm.class,
|
||||
TestXorEncryption.class
|
||||
})
|
||||
public final class AllHSSFEncryptionTests {
|
||||
public final class AllEncryptionTests {
|
||||
}
|
@ -15,7 +15,7 @@
|
||||
limitations under the License.
|
||||
==================================================================== */
|
||||
|
||||
package org.apache.poi.hssf.record.crypto;
|
||||
package org.apache.poi.poifs.crypt;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
@ -23,17 +23,18 @@ import static org.junit.Assert.assertFalse;
|
||||
import java.io.InputStream;
|
||||
import java.util.Arrays;
|
||||
|
||||
import junit.framework.AssertionFailedError;
|
||||
import junit.framework.ComparisonFailure;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.apache.poi.hssf.record.crypto.Biff8DecryptingStream;
|
||||
import org.apache.poi.util.HexDump;
|
||||
import org.apache.poi.util.HexRead;
|
||||
import org.junit.Test;
|
||||
|
||||
import junit.framework.AssertionFailedError;
|
||||
import junit.framework.ComparisonFailure;
|
||||
|
||||
/**
|
||||
* Tests for {@link Biff8DecryptingStream}
|
||||
*
|
||||
* @author Josh Micich
|
||||
*/
|
||||
public final class TestBiff8DecryptingStream {
|
||||
|
||||
@ -49,12 +50,10 @@ public final class TestBiff8DecryptingStream {
|
||||
public MockStream(int initialValue) {
|
||||
_initialValue = initialValue;
|
||||
}
|
||||
|
||||
public int read() {
|
||||
return (_initialValue+_position++) & 0xFF;
|
||||
}
|
||||
public int getPosition() {
|
||||
return _position;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class StreamTester {
|
||||
@ -70,7 +69,11 @@ public final class TestBiff8DecryptingStream {
|
||||
public StreamTester(MockStream ms, String keyDigestHex, int expectedFirstInt) {
|
||||
_ms = ms;
|
||||
byte[] keyDigest = HexRead.readFromString(keyDigestHex);
|
||||
_bds = new Biff8DecryptingStream(_ms, 0, new Biff8RC4Key(keyDigest));
|
||||
EncryptionInfo ei = new EncryptionInfo(EncryptionMode.binaryRC4);
|
||||
Decryptor dec = ei.getDecryptor();
|
||||
dec.setSecretKey(new SecretKeySpec(keyDigest, "RC4"));
|
||||
|
||||
_bds = new Biff8DecryptingStream(_ms, 0, ei);
|
||||
assertEquals(expectedFirstInt, _bds.readInt());
|
||||
_errorsOccurred = false;
|
||||
}
|
||||
@ -84,11 +87,11 @@ public final class TestBiff8DecryptingStream {
|
||||
* Also confirms that read position of the underlying stream is aligned.
|
||||
*/
|
||||
public void rollForward(int fromPosition, int toPosition) {
|
||||
assertEquals(fromPosition, _ms.getPosition());
|
||||
assertEquals(fromPosition, _bds.getPosition());
|
||||
for (int i = fromPosition; i < toPosition; i++) {
|
||||
_bds.readByte();
|
||||
}
|
||||
assertEquals(toPosition, _ms.getPosition());
|
||||
assertEquals(toPosition, _bds.getPosition());
|
||||
}
|
||||
|
||||
public void confirmByte(int expVal) {
|
@ -17,14 +17,14 @@
|
||||
|
||||
package org.apache.poi.poifs.crypt;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import org.apache.poi.EncryptedDocumentException;
|
||||
import org.junit.Test;
|
||||
|
||||
public class TestCipherAlgorithm {
|
||||
@Test
|
||||
public void test() {
|
||||
public void validInputs() {
|
||||
assertEquals(128, CipherAlgorithm.aes128.defaultKeySize);
|
||||
|
||||
for(CipherAlgorithm alg : CipherAlgorithm.values()) {
|
||||
@ -33,27 +33,20 @@ public class TestCipherAlgorithm {
|
||||
|
||||
assertEquals(CipherAlgorithm.aes128, CipherAlgorithm.fromEcmaId(0x660E));
|
||||
assertEquals(CipherAlgorithm.aes192, CipherAlgorithm.fromXmlId("AES", 192));
|
||||
|
||||
try {
|
||||
CipherAlgorithm.fromEcmaId(0);
|
||||
fail("Should throw exception");
|
||||
} catch (EncryptedDocumentException e) {
|
||||
// expected
|
||||
}
|
||||
|
||||
try {
|
||||
CipherAlgorithm.fromXmlId("AES", 1);
|
||||
fail("Should throw exception");
|
||||
} catch (EncryptedDocumentException e) {
|
||||
// expected
|
||||
}
|
||||
|
||||
try {
|
||||
CipherAlgorithm.fromXmlId("RC1", 0x40);
|
||||
fail("Should throw exception");
|
||||
} catch (EncryptedDocumentException e) {
|
||||
// expected
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test(expected=EncryptedDocumentException.class)
|
||||
public void invalidEcmaId() {
|
||||
CipherAlgorithm.fromEcmaId(0);
|
||||
}
|
||||
|
||||
@Test(expected=EncryptedDocumentException.class)
|
||||
public void invalidXmlId1() {
|
||||
CipherAlgorithm.fromXmlId("AES", 1);
|
||||
}
|
||||
|
||||
@Test(expected=EncryptedDocumentException.class)
|
||||
public void invalidXmlId2() {
|
||||
CipherAlgorithm.fromXmlId("RC1", 0x40);
|
||||
}
|
||||
}
|
||||
|
@ -15,13 +15,14 @@
|
||||
limitations under the License.
|
||||
==================================================================== */
|
||||
|
||||
package org.apache.poi.hssf.record.crypto;
|
||||
package org.apache.poi.poifs.crypt;
|
||||
|
||||
import static org.hamcrest.core.IsEqual.equalTo;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
import org.apache.poi.hssf.HSSFTestDataSamples;
|
||||
import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey;
|
||||
import org.apache.poi.hssf.usermodel.HSSFSheet;
|
||||
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
|
||||
import org.apache.poi.poifs.crypt.CryptoFunctions;
|
@ -0,0 +1,106 @@
|
||||
/* ====================================================================
|
||||
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.poifs.crypt.binaryrc4;
|
||||
|
||||
import static org.apache.poi.util.HexRead.readFromString;
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import org.apache.poi.poifs.crypt.Decryptor;
|
||||
import org.apache.poi.poifs.crypt.EncryptionInfo;
|
||||
import org.apache.poi.poifs.crypt.EncryptionMode;
|
||||
import org.junit.Test;
|
||||
|
||||
public class TestBinaryRC4 {
|
||||
@Test
|
||||
public void createKeyDigest() throws GeneralSecurityException {
|
||||
byte[] docIdData = readFromString("17 F6 D1 6B 09 B1 5F 7B 4C 9D 03 B4 81 B5 B4 4A");
|
||||
byte[] expResult = readFromString("C2 D9 56 B2 6B");
|
||||
|
||||
EncryptionInfo ei = new EncryptionInfo(EncryptionMode.binaryRC4);
|
||||
BinaryRC4EncryptionVerifier ver = (BinaryRC4EncryptionVerifier)ei.getVerifier();
|
||||
ver.setSalt(docIdData);
|
||||
SecretKey sk = BinaryRC4Decryptor.generateSecretKey("MoneyForNothing", ver);
|
||||
|
||||
assertArrayEquals("keyDigest mismatch", expResult, sk.getEncoded());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateWithDefaultPassword() throws GeneralSecurityException {
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateWithSuppliedPassword() throws GeneralSecurityException {
|
||||
|
||||
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) throws GeneralSecurityException {
|
||||
confirmValid(expectedResult, docIdHex, saltDataHex, saltHashHex, null);
|
||||
}
|
||||
|
||||
private static void confirmValid(boolean expectedResult, String docIdHex,
|
||||
String saltDataHex, String saltHashHex, String password) throws GeneralSecurityException {
|
||||
byte[] docId = readFromString(docIdHex);
|
||||
byte[] saltData = readFromString(saltDataHex);
|
||||
byte[] saltHash = readFromString(saltHashHex);
|
||||
|
||||
EncryptionInfo ei = new EncryptionInfo(EncryptionMode.binaryRC4);
|
||||
BinaryRC4EncryptionVerifier ver = (BinaryRC4EncryptionVerifier)ei.getVerifier();
|
||||
ver.setSalt(docId);
|
||||
ver.setEncryptedVerifier(saltData);
|
||||
ver.setEncryptedVerifierHash(saltHash);
|
||||
|
||||
String pass = password == null ? Decryptor.DEFAULT_PASSWORD : password;
|
||||
boolean actResult = ei.getDecryptor().verifyPassword(pass);
|
||||
if (expectedResult) {
|
||||
assertTrue("validate failed", actResult);
|
||||
} else {
|
||||
assertFalse("validate succeeded unexpectedly", actResult);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user