Bug 56486 - Add XOR obfuscation/decryption support to HSSF

git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1592636 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Andreas Beeker 2014-05-05 21:41:31 +00:00
parent 337f775807
commit 00e2e55338
18 changed files with 961 additions and 514 deletions

View File

@ -30,52 +30,165 @@ import org.apache.poi.util.LittleEndianOutput;
*/
public final class FilePassRecord extends StandardRecord {
public final static short sid = 0x002F;
private int _encryptionType;
private int _encryptionInfo;
private int _minorVersionNo;
private byte[] _docId;
private byte[] _saltData;
private byte[] _saltHash;
private KeyData _keyData;
private static interface KeyData {
void read(RecordInputStream in);
void serialize(LittleEndianOutput out);
int getDataSize();
void appendToString(StringBuffer buffer);
}
public static class Rc4KeyData implements KeyData {
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 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:
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");
}
}
public static class XorKeyData implements KeyData {
/**
* 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");
}
}
private static final int ENCRYPTION_XOR = 0;
private static final int ENCRYPTION_OTHER = 1;
private static final int ENCRYPTION_OTHER_RC4 = 1;
private static final int ENCRYPTION_OTHER_CAPI_2 = 2;
private static final int ENCRYPTION_OTHER_CAPI_3 = 3;
public FilePassRecord(RecordInputStream in) {
_encryptionType = in.readUShort();
switch (_encryptionType) {
case ENCRYPTION_XOR:
throw new EncryptedDocumentException("HSSF does not currently support XOR obfuscation");
_keyData = new XorKeyData();
break;
case ENCRYPTION_OTHER:
// handled below
_keyData = new Rc4KeyData();
break;
default:
throw new RecordFormatException("Unknown encryption type " + _encryptionType);
}
_encryptionInfo = in.readUShort();
switch (_encryptionInfo) {
case ENCRYPTION_OTHER_RC4:
// handled below
break;
case ENCRYPTION_OTHER_CAPI_2:
case ENCRYPTION_OTHER_CAPI_3:
throw new 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);
}
_docId = read(in, 16);
_saltData = read(in, 16);
_saltHash = read(in, 16);
_keyData.read(in);
}
private static byte[] read(RecordInputStream in, int size) {
@ -86,48 +199,88 @@ public final class FilePassRecord extends StandardRecord {
public void serialize(LittleEndianOutput out) {
out.writeShort(_encryptionType);
out.writeShort(_encryptionInfo);
out.writeShort(_minorVersionNo);
out.write(_docId);
out.write(_saltData);
out.write(_saltHash);
assert(_keyData != null);
_keyData.serialize(out);
}
protected int getDataSize() {
return 54;
assert(_keyData != null);
return _keyData.getDataSize();
}
public byte[] getDocId() {
return _docId.clone();
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;
}
/**
* @deprecated use getRc4KeyData().getSalt()
* @return the rc4 salt
*/
public byte[] getDocId() {
return checkRc4().getSalt();
}
public void setDocId(byte[] docId) {
_docId = docId.clone();
/**
* @deprecated use getRc4KeyData().setSalt()
* @param docId the new rc4 salt
*/
public void setDocId(byte[] docId) {
checkRc4().setSalt(docId);
}
public byte[] getSaltData() {
return _saltData.clone();
/**
* @deprecated use getRc4KeyData().getEncryptedVerifier()
* @return the rc4 encrypted verifier
*/
public byte[] getSaltData() {
return checkRc4().getEncryptedVerifier();
}
/**
* @deprecated use getRc4KeyData().setEncryptedVerifier()
* @param saltData the new rc4 encrypted verifier
*/
public void setSaltData(byte[] saltData) {
_saltData = saltData.clone();
getRc4KeyData().setEncryptedVerifier(saltData);
}
/**
* @deprecated use getRc4KeyData().getEncryptedVerifierHash()
* @return the rc4 encrypted verifier hash
*/
public byte[] getSaltHash() {
return _saltHash.clone();
return getRc4KeyData().getEncryptedVerifierHash();
}
/**
* @deprecated use getRc4KeyData().setEncryptedVerifierHash()
* @param saltHash the new rc4 encrypted verifier
*/
public void setSaltHash(byte[] saltHash) {
_saltHash = saltHash.clone();
getRc4KeyData().setEncryptedVerifierHash(saltHash);
}
public short getSid() {
return sid;
}
public Object clone() {
public Object clone() {
// currently immutable
return this;
}
@ -137,11 +290,7 @@ public final class FilePassRecord extends StandardRecord {
buffer.append("[FILEPASS]\n");
buffer.append(" .type = ").append(HexDump.shortToHex(_encryptionType)).append("\n");
buffer.append(" .info = ").append(HexDump.shortToHex(_encryptionInfo)).append("\n");
buffer.append(" .ver = ").append(HexDump.shortToHex(_minorVersionNo)).append("\n");
buffer.append(" .docId= ").append(HexDump.toHex(_docId)).append("\n");
buffer.append(" .salt = ").append(HexDump.toHex(_saltData)).append("\n");
buffer.append(" .hash = ").append(HexDump.toHex(_saltHash)).append("\n");
_keyData.appendToString(buffer);
buffer.append("[/FILEPASS]\n");
return buffer.toString();
}

View File

@ -20,10 +20,17 @@ import java.io.InputStream;
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.EncryptedDocumentException;
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.util.POILogFactory;
import org.apache.poi.util.POILogger;
/**
* A stream based way to get at complete records, with
@ -48,6 +55,8 @@ public final class RecordFactoryInputStream {
private final Record _lastRecord;
private final boolean _hasBOFRecord;
private static POILogger log = POILogFactory.getLogger(StreamEncryptionInfo.class);
public StreamEncryptionInfo(RecordInputStream rs, List<Record> outputRecs) {
Record rec;
rs.nextRecord();
@ -105,18 +114,34 @@ public final class RecordFactoryInputStream {
public RecordInputStream createDecryptingStream(InputStream original) {
FilePassRecord fpr = _filePassRec;
String userPassword = Biff8EncryptionKey.getCurrentUserPassword();
if (userPassword == null) {
userPassword = Decryptor.DEFAULT_PASSWORD;
}
Biff8EncryptionKey key;
if (userPassword == null) {
key = Biff8EncryptionKey.create(fpr.getDocId());
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())) {
throw new EncryptedDocumentException(
(Decryptor.DEFAULT_PASSWORD.equals(userPassword) ? "Default" : "Supplied")
+ " password is invalid for key/verifier");
}
} else {
key = Biff8EncryptionKey.create(userPassword, fpr.getDocId());
}
if (!key.validate(fpr.getSaltData(), fpr.getSaltHash())) {
throw new EncryptedDocumentException(
(userPassword == null ? "Default" : "Supplied")
+ " password is invalid for docId/saltData/saltHash");
throw new EncryptedDocumentException("Crypto API not yet supported.");
}
return new RecordInputStream(original, key, _initialRecordsSize);
}

View File

@ -0,0 +1,30 @@
/* ====================================================================
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);
}

View File

@ -19,6 +19,7 @@ package org.apache.poi.hssf.record.crypto;
import java.io.InputStream;
import org.apache.poi.EncryptedDocumentException;
import org.apache.poi.hssf.record.BiffHeaderInput;
import org.apache.poi.util.LittleEndianInput;
import org.apache.poi.util.LittleEndianInputStream;
@ -30,10 +31,16 @@ import org.apache.poi.util.LittleEndianInputStream;
public final class Biff8DecryptingStream implements BiffHeaderInput, LittleEndianInput {
private final LittleEndianInput _le;
private final Biff8RC4 _rc4;
private final Biff8Cipher _cipher;
public Biff8DecryptingStream(InputStream in, int initialOffset, Biff8EncryptionKey key) {
_rc4 = new Biff8RC4(initialOffset, 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.");
}
if (in instanceof LittleEndianInput) {
// accessing directly is an optimisation
@ -53,8 +60,8 @@ public final class Biff8DecryptingStream implements BiffHeaderInput, LittleEndia
*/
public int readRecordSID() {
int sid = _le.readUShort();
_rc4.skipTwoBytes();
_rc4.startRecord(sid);
_cipher.skipTwoBytes();
_cipher.startRecord(sid);
return sid;
}
@ -63,7 +70,8 @@ public final class Biff8DecryptingStream implements BiffHeaderInput, LittleEndia
*/
public int readDataSize() {
int dataSize = _le.readUShort();
_rc4.skipTwoBytes();
_cipher.skipTwoBytes();
_cipher.setNextRecordSize(dataSize);
return dataSize;
}
@ -82,30 +90,30 @@ public final class Biff8DecryptingStream implements BiffHeaderInput, LittleEndia
public void readFully(byte[] buf, int off, int len) {
_le.readFully(buf, off, len);
_rc4.xor(buf, off, len);
_cipher.xor(buf, off, len);
}
public int readUByte() {
return _rc4.xorByte(_le.readUByte());
return _cipher.xorByte(_le.readUByte());
}
public byte readByte() {
return (byte) _rc4.xorByte(_le.readUByte());
return (byte) _cipher.xorByte(_le.readUByte());
}
public int readUShort() {
return _rc4.xorShort(_le.readUShort());
return _cipher.xorShort(_le.readUShort());
}
public short readShort() {
return (short) _rc4.xorShort(_le.readUShort());
return (short) _cipher.xorShort(_le.readUShort());
}
public int readInt() {
return _rc4.xorInt(_le.readInt());
return _cipher.xorInt(_le.readInt());
}
public long readLong() {
return _rc4.xorLong(_le.readLong());
return _cipher.xorLong(_le.readLong());
}
}

View File

@ -16,139 +16,34 @@
==================================================================== */
package org.apache.poi.hssf.record.crypto;
import java.io.ByteArrayOutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import javax.crypto.SecretKey;
import org.apache.poi.EncryptedDocumentException;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.util.HexDump;
import org.apache.poi.util.LittleEndianOutputStream;
import org.apache.poi.poifs.crypt.Decryptor;
public final class Biff8EncryptionKey {
// these two constants coincidentally have the same value
private static final int KEY_DIGEST_LENGTH = 5;
private static final int PASSWORD_HASH_NUMBER_OF_BYTES_USED = 5;
private final byte[] _keyDigest;
public abstract class Biff8EncryptionKey {
protected SecretKey _secretKey;
/**
* Create using the default password and a specified docId
* @param docId 16 bytes
* @param salt 16 bytes
*/
public static Biff8EncryptionKey create(byte[] docId) {
return new Biff8EncryptionKey(createKeyDigest("VelvetSweatshop", docId));
public static Biff8EncryptionKey create(byte[] salt) {
return Biff8RC4Key.create(Decryptor.DEFAULT_PASSWORD, salt);
}
public static Biff8EncryptionKey create(String password, byte[] docIdData) {
return new Biff8EncryptionKey(createKeyDigest(password, docIdData));
}
Biff8EncryptionKey(byte[] keyDigest) {
if (keyDigest.length != KEY_DIGEST_LENGTH) {
throw new IllegalArgumentException("Expected 5 byte key digest, but got " + HexDump.toHex(keyDigest));
}
_keyDigest = keyDigest;
}
static byte[] createKeyDigest(String password, byte[] docIdData) {
check16Bytes(docIdData, "docId");
int nChars = Math.min(password.length(), 16);
byte[] passwordData = new byte[nChars*2];
for (int i=0; i<nChars; i++) {
char ch = password.charAt(i);
passwordData[i*2+0] = (byte) ((ch << 0) & 0xFF);
passwordData[i*2+1] = (byte) ((ch << 8) & 0xFF);
}
byte[] kd;
MessageDigest md5;
try {
md5 = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
md5.update(passwordData);
byte[] passwordHash = md5.digest();
md5.reset();
for (int i=0; i<16; i++) {
md5.update(passwordHash, 0, PASSWORD_HASH_NUMBER_OF_BYTES_USED);
md5.update(docIdData, 0, docIdData.length);
}
kd = md5.digest();
byte[] result = new byte[KEY_DIGEST_LENGTH];
System.arraycopy(kd, 0, result, 0, KEY_DIGEST_LENGTH);
return result;
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) {
check16Bytes(saltData, "saltData");
check16Bytes(saltHash, "saltHash");
// validation uses the RC4 for block zero
RC4 rc4 = createRC4(0);
byte[] saltDataPrime = saltData.clone();
rc4.encrypt(saltDataPrime);
byte[] saltHashPrime = saltHash.clone();
rc4.encrypt(saltHashPrime);
MessageDigest md5;
try {
md5 = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
md5.update(saltDataPrime);
byte[] finalSaltResult = md5.digest();
if (false) { // set true to see a valid saltHash value
byte[] saltHashThatWouldWork = xor(saltHash, xor(saltHashPrime, finalSaltResult));
System.out.println(HexDump.toHex(saltHashThatWouldWork));
}
return Arrays.equals(saltHashPrime, finalSaltResult);
throw new EncryptedDocumentException("validate is not supported (in super-class).");
}
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));
}
}
/**
* The {@link RC4} instance needs to be changed every 1024 bytes.
* @param keyBlockNo used to seed the newly created {@link RC4}
*/
RC4 createRC4(int keyBlockNo) {
MessageDigest md5;
try {
md5 = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
md5.update(_keyDigest);
ByteArrayOutputStream baos = new ByteArrayOutputStream(4);
new LittleEndianOutputStream(baos).writeInt(keyBlockNo);
md5.update(baos.toByteArray());
byte[] digest = md5.digest();
return new RC4(digest);
}
/**
* Stores the BIFF8 encryption/decryption password for the current thread. This has been done
* using a {@link ThreadLocal} in order to avoid further overloading the various public APIs

View File

@ -17,23 +17,29 @@
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 RC4} instance is renewed (re-keyed) every 1024 bytes.
*
* @author Josh Micich
* {@link Cipher} instance is renewed (re-keyed) every 1024 bytes.
*/
final class Biff8RC4 {
final class Biff8RC4 implements Biff8Cipher {
private static final int RC4_REKEYING_INTERVAL = 1024;
private RC4 _rc4;
private Cipher _rc4;
/**
* This field is used to keep track of when to change the {@link 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.
*/
@ -41,42 +47,49 @@ final class Biff8RC4 {
private int _nextRC4BlockStart;
private int _currentKeyIndex;
private boolean _shouldSkipEncryptionOnCurrentRecord;
private final Biff8RC4Key _key;
private ByteBuffer _buffer = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN);
private final Biff8EncryptionKey _key;
public Biff8RC4(int initialOffset, Biff8EncryptionKey key) {
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;
for (int i = initialOffset; i > 0; i--) {
_rc4.output();
}
_shouldSkipEncryptionOnCurrentRecord = false;
encryptBytes(new byte[initialOffset], 0, initialOffset);
}
private void rekeyForNextBlock() {
_currentKeyIndex = _streamPos / RC4_REKEYING_INTERVAL;
_rc4 = _key.createRC4(_currentKeyIndex);
_key.initCipherForBlock(_rc4, _currentKeyIndex);
_nextRC4BlockStart = (_currentKeyIndex + 1) * RC4_REKEYING_INTERVAL;
}
private int getNextRC4Byte() {
if (_streamPos >= _nextRC4BlockStart) {
rekeyForNextBlock();
}
byte mask = _rc4.output();
_streamPos++;
if (_shouldSkipEncryptionOnCurrentRecord) {
return 0;
}
return mask & 0xFF;
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);
}
@ -110,19 +123,18 @@ final class Biff8RC4 {
/**
* Used when BIFF header fields (sid, size) are being read. The internal
* {@link RC4} instance must step even when unencrypted bytes are read
* {@link Cipher} instance must step even when unencrypted bytes are read
*/
public void skipTwoBytes() {
getNextRC4Byte();
getNextRC4Byte();
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
_rc4.encrypt(buf, pOffset, pLen);
// simple case - this read does not cross key blocks
encryptBytes(buf, pOffset, pLen);
_streamPos += pLen;
return;
}
@ -133,7 +145,7 @@ final class Biff8RC4 {
// start by using the rest of the current block
if (len > nLeftInBlock) {
if (nLeftInBlock > 0) {
_rc4.encrypt(buf, offset, nLeftInBlock);
encryptBytes(buf, offset, nLeftInBlock);
_streamPos += nLeftInBlock;
offset += nLeftInBlock;
len -= nLeftInBlock;
@ -142,56 +154,42 @@ final class Biff8RC4 {
}
// all full blocks following
while (len > RC4_REKEYING_INTERVAL) {
_rc4.encrypt(buf, offset, 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
_rc4.encrypt(buf, offset, len);
encryptBytes(buf, offset, len);
_streamPos += len;
}
public int xorByte(int rawVal) {
int mask = getNextRC4Byte();
return (byte) (rawVal ^ mask);
_buffer.put(0, (byte)rawVal);
xor(_buffer.array(), 0, 1);
return _buffer.get(0);
}
public int xorShort(int rawVal) {
int b0 = getNextRC4Byte();
int b1 = getNextRC4Byte();
int mask = (b1 << 8) + (b0 << 0);
return rawVal ^ mask;
_buffer.putShort(0, (short)rawVal);
xor(_buffer.array(), 0, 2);
return _buffer.getShort(0);
}
public int xorInt(int rawVal) {
int b0 = getNextRC4Byte();
int b1 = getNextRC4Byte();
int b2 = getNextRC4Byte();
int b3 = getNextRC4Byte();
int mask = (b3 << 24) + (b2 << 16) + (b1 << 8) + (b0 << 0);
return rawVal ^ mask;
_buffer.putInt(0, rawVal);
xor(_buffer.array(), 0, 4);
return _buffer.getInt(0);
}
public long xorLong(long rawVal) {
int b0 = getNextRC4Byte();
int b1 = getNextRC4Byte();
int b2 = getNextRC4Byte();
int b3 = getNextRC4Byte();
int b4 = getNextRC4Byte();
int b5 = getNextRC4Byte();
int b6 = getNextRC4Byte();
int b7 = getNextRC4Byte();
long mask =
(((long)b7) << 56)
+ (((long)b6) << 48)
+ (((long)b5) << 40)
+ (((long)b4) << 32)
+ (((long)b3) << 24)
+ (b2 << 16)
+ (b1 << 8)
+ (b0 << 0);
return rawVal ^ mask;
_buffer.putLong(0, rawVal);
xor(_buffer.array(), 0, 8);
return _buffer.getLong(0);
}
public void setNextRecordSize(int recordSize) {
/* no-op */
}
}

View File

@ -0,0 +1,155 @@
/* ====================================================================
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, 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));
}
}
}

View File

@ -0,0 +1,153 @@
/* ====================================================================
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);
}
}

View File

@ -0,0 +1,44 @@
/* ====================================================================
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);
}
}

View File

@ -1,90 +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 org.apache.poi.util.HexDump;
/**
* Simple implementation of the alleged RC4 algorithm.
*
* Inspired by <A HREF="http://en.wikipedia.org/wiki/RC4">wikipedia's RC4 article</A>
*
* @author Josh Micich
*/
final class RC4 {
private int _i, _j;
private final byte[] _s = new byte[256];
public RC4(byte[] key) {
int key_length = key.length;
for (int i = 0; i < 256; i++)
_s[i] = (byte)i;
for (int i=0, j=0; i < 256; i++) {
byte temp;
j = (j + key[i % key_length] + _s[i]) & 255;
temp = _s[i];
_s[i] = _s[j];
_s[j] = temp;
}
_i = 0;
_j = 0;
}
public byte output() {
byte temp;
_i = (_i + 1) & 255;
_j = (_j + _s[_i]) & 255;
temp = _s[_i];
_s[_i] = _s[_j];
_s[_j] = temp;
return _s[(_s[_i] + _s[_j]) & 255];
}
public void encrypt(byte[] in) {
for (int i = 0; i < in.length; i++) {
in[i] = (byte) (in[i] ^ output());
}
}
public void encrypt(byte[] in, int offset, int len) {
int end = offset+len;
for (int i = offset; i < end; i++) {
in[i] = (byte) (in[i] ^ output());
}
}
@Override
public String toString() {
StringBuffer sb = new StringBuffer();
sb.append(getClass().getName()).append(" [");
sb.append("i=").append(_i);
sb.append(" j=").append(_j);
sb.append("]");
sb.append("\n");
sb.append(HexDump.dump(_s, 0, 0));
return sb.toString();
}
}

View File

@ -289,6 +289,12 @@ public class CryptoFunctions {
0x313E, 0x1872, 0xE139, 0xD40F, 0x84F9, 0x280C, 0xA96A,
0x4EC3
};
private static final byte PadArray[] = {
(byte)0xBB, (byte)0xFF, (byte)0xFF, (byte)0xBA, (byte)0xFF,
(byte)0xFF, (byte)0xB9, (byte)0x80, (byte)0x00, (byte)0xBE,
(byte)0x0F, (byte)0x00, (byte)0xBF, (byte)0x0F, (byte)0x00
};
private static final int EncryptionMatrix[][] = {
/* char 1 */ {0xAEFC, 0x4DD9, 0x9BB2, 0x2745, 0x4E8A, 0x9D14, 0x2A09},
@ -309,20 +315,18 @@ public class CryptoFunctions {
};
/**
* This method generates the xored-hashed password for word documents &lt; 2007.
* This method generates the xor verifier for word documents &lt; 2007 (method 2).
* Its output will be used as password input for the newer word generations which
* utilize a real hashing algorithm like sha1.
*
* Although the code was taken from the "see"-link below, this looks similar
* to the method in [MS-OFFCRYPTO] 2.3.7.2 Binary Document XOR Array Initialization Method 1.
*
* @param password
* @param password the password
* @return the hashed password
*
* @see <a href="http://msdn.microsoft.com/en-us/library/dd905229.aspx">2.3.7.4 Binary Document Password Verifier Derivation Method 2</a>
* @see <a href="http://blogs.msdn.com/b/vsod/archive/2010/04/05/how-to-set-the-editing-restrictions-in-word-using-open-xml-sdk-2-0.aspx">How to set the editing restrictions in Word using Open XML SDK 2.0</a>
* @see <a href="http://www.aspose.com/blogs/aspose-blogs/vladimir-averkin/archive/2007/08/20/funny-how-the-new-powerful-cryptography-implemented-in-word-2007-turns-it-into-a-perfect-tool-for-document-password-removal.html">Funny: How the new powerful cryptography implemented in Word 2007 turns it into a perfect tool for document password removal.</a>
*/
public static int xorHashPasswordAsInt(String password) {
public static int createXorVerifier2(String password) {
//Array to hold Key Values
byte[] generatedKey = new byte[4];
@ -391,7 +395,7 @@ public class CryptoFunctions {
* This method generates the xored-hashed password for word documents &lt; 2007.
*/
public static String xorHashPassword(String password) {
int hashedPassword = xorHashPasswordAsInt(password);
int hashedPassword = createXorVerifier2(password);
return String.format("%1$08X", hashedPassword);
}
@ -400,7 +404,7 @@ public class CryptoFunctions {
* processing in word documents 2007 and newer, which utilize a real hashing algorithm like sha1.
*/
public static String xorHashPasswordReversed(String password) {
int hashedPassword = xorHashPasswordAsInt(password);
int hashedPassword = createXorVerifier2(password);
return String.format("%1$02X%2$02X%3$02X%4$02X"
, ( hashedPassword >>> 0 ) & 0xFF
@ -409,4 +413,71 @@ public class CryptoFunctions {
, ( hashedPassword >>> 24 ) & 0xFF
);
}
/**
* Create the verifier for xor obfuscation (method 1)
*
* @see <a href="http://msdn.microsoft.com/en-us/library/dd926947.aspx">2.3.7.1 Binary Document Password Verifier Derivation Method 1</a>
* @see <a href="http://msdn.microsoft.com/en-us/library/dd905229.aspx">2.3.7.4 Binary Document Password Verifier Derivation Method 2</a>
*
* @param password the password
* @return the verifier
*/
public static int createXorVerifier1(String password) {
// the verifier for method 1 is part of the verifier for method 2
// so we simply chop it from there
return createXorVerifier2(password) & 0xFFFF;
}
/**
* Create the xor key for xor obfuscation, which is used to create the xor array (method 1)
*
* @see <a href="http://msdn.microsoft.com/en-us/library/dd924704.aspx">2.3.7.2 Binary Document XOR Array Initialization Method 1</a>
* @see <a href="http://msdn.microsoft.com/en-us/library/dd905229.aspx">2.3.7.4 Binary Document Password Verifier Derivation Method 2</a>
*
* @param password the password
* @return the xor key
*/
public static int createXorKey1(String password) {
// the xor key for method 1 is part of the verifier for method 2
// so we simply chop it from there
return createXorVerifier2(password) >>> 16;
}
/**
* Creates an byte array for xor obfuscation (method 1)
*
* @see <a href="http://msdn.microsoft.com/en-us/library/dd924704.aspx">2.3.7.2 Binary Document XOR Array Initialization Method 1</a>
* @see <a href="http://docs.libreoffice.org/oox/html/binarycodec_8cxx_source.html">Libre Office implementation</a>
*
* @param password the password
* @return the byte array for xor obfuscation
*/
public static byte[] createXorArray1(String password) {
if (password.length() > 15) password = password.substring(0, 15);
byte passBytes[] = password.getBytes(Charset.forName("ASCII"));
// this code is based on the libre office implementation.
// The MS-OFFCRYPTO misses some infos about the various rotation sizes
byte obfuscationArray[] = new byte[16];
System.arraycopy(passBytes, 0, obfuscationArray, 0, passBytes.length);
System.arraycopy(PadArray, 0, obfuscationArray, passBytes.length, PadArray.length-passBytes.length+1);
int xorKey = createXorKey1(password);
// rotation of key values is application dependent
int nRotateSize = 2; /* Excel = 2; Word = 7 */
byte baseKeyLE[] = { (byte)(xorKey & 0xFF), (byte)((xorKey >>> 8) & 0xFF) };
for (int i=0; i<obfuscationArray.length; i++) {
obfuscationArray[i] ^= baseKeyLE[i&1];
obfuscationArray[i] = rotateLeft(obfuscationArray[i], nRotateSize);
}
return obfuscationArray;
}
private static byte rotateLeft(byte bits, int shift) {
return (byte)(((bits & 0xff) << shift) | ((bits & 0xff) >>> (8 - shift)));
}
}

View File

@ -17,22 +17,25 @@
package org.apache.poi.hssf.record;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.io.ByteArrayInputStream;
import java.util.Arrays;
import junit.framework.AssertionFailedError;
import junit.framework.TestCase;
import org.apache.poi.EncryptedDocumentException;
import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey;
import org.apache.poi.util.HexRead;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
/**
* Tests for {@link RecordFactoryInputStream}
*
* @author Josh Micich
*/
public final class TestRecordFactoryInputStream extends TestCase {
public final class TestRecordFactoryInputStream {
/**
* Hex dump of a BOF record and most of a FILEPASS record.
@ -55,10 +58,15 @@ public final class TestRecordFactoryInputStream extends TestCase {
private static final String SAMPLE_WINDOW1 = "3D 00 12 00"
+ "00 00 00 00 40 38 55 23 38 00 00 00 00 00 01 00 58 02";
@Rule
public ExpectedException expectedEx = ExpectedException.none();
/**
* Makes sure that a default password mismatch condition is represented with {@link EncryptedDocumentException}
*/
public void testDefaultPassword() {
@Test
public void defaultPasswordWrong() {
// This encodng depends on docId, password and stream position
final String SAMPLE_WINDOW1_ENCR1 = "3D 00 12 00"
+ "C4, 9B, 02, 50, 86, E0, DF, 34, FB, 57, 0E, 8C, CE, 25, 45, E3, 80, 01";
@ -69,33 +77,36 @@ public final class TestRecordFactoryInputStream extends TestCase {
+ SAMPLE_WINDOW1_ENCR1
);
RecordFactoryInputStream rfis;
try {
rfis = createRFIS(dataWrongDefault);
throw new AssertionFailedError("Expected password mismatch error");
} catch (EncryptedDocumentException e) {
// expected during successful test
if (!e.getMessage().equals("Default password is invalid for docId/saltData/saltHash")) {
throw e;
}
}
byte[] dataCorrectDefault = HexRead.readFromString(""
+ COMMON_HEX_DATA
+ "137BEF04 969A200B 306329DE 52254005" // correct saltHash for default password (and docId/saltHash)
+ SAMPLE_WINDOW1_ENCR1
);
rfis = createRFIS(dataCorrectDefault);
confirmReadInitialRecords(rfis);
Biff8EncryptionKey.setCurrentUserPassword(null);
expectedEx.expect(EncryptedDocumentException.class);
expectedEx.expectMessage("Default password is invalid for salt/verifier/verifierHash");
createRFIS(dataWrongDefault);
}
@Test
public void defaultPasswordOK() {
// This encodng depends on docId, password and stream position
final String SAMPLE_WINDOW1_ENCR1 = "3D 00 12 00"
+ "C4, 9B, 02, 50, 86, E0, DF, 34, FB, 57, 0E, 8C, CE, 25, 45, E3, 80, 01";
byte[] dataCorrectDefault = HexRead.readFromString(""
+ COMMON_HEX_DATA
+ "137BEF04 969A200B 306329DE 52254005" // correct saltHash for default password (and docId/saltHash)
+ SAMPLE_WINDOW1_ENCR1
);
Biff8EncryptionKey.setCurrentUserPassword(null);
RecordFactoryInputStream rfis = createRFIS(dataCorrectDefault);
confirmReadInitialRecords(rfis);
}
/**
* Makes sure that an incorrect user supplied password condition is represented with {@link EncryptedDocumentException}
*/
public void testSuppliedPassword() {
// This encodng depends on docId, password and stream position
@Test
public void suppliedPasswordWrong() {
// This encoding depends on docId, password and stream position
final String SAMPLE_WINDOW1_ENCR2 = "3D 00 12 00"
+ "45, B9, 90, FE, B6, C6, EC, 73, EE, 3F, 52, 45, 97, DB, E3, C1, D6, FE";
@ -108,29 +119,32 @@ public final class TestRecordFactoryInputStream extends TestCase {
Biff8EncryptionKey.setCurrentUserPassword("passw0rd");
RecordFactoryInputStream rfis;
try {
rfis = createRFIS(dataWrongDefault);
throw new AssertionFailedError("Expected password mismatch error");
} catch (EncryptedDocumentException e) {
// expected during successful test
if (!e.getMessage().equals("Supplied password is invalid for docId/saltData/saltHash")) {
throw e;
}
}
byte[] dataCorrectDefault = HexRead.readFromString(""
+ COMMON_HEX_DATA
+ "C728659A C38E35E0 568A338F C3FC9D70" // correct saltHash for supplied password (and docId/saltHash)
+ SAMPLE_WINDOW1_ENCR2
);
rfis = createRFIS(dataCorrectDefault);
Biff8EncryptionKey.setCurrentUserPassword(null);
confirmReadInitialRecords(rfis);
expectedEx.expect(EncryptedDocumentException.class);
expectedEx.expectMessage("Supplied password is invalid for salt/verifier/verifierHash");
createRFIS(dataWrongDefault);
}
@Test
public void suppliedPasswordOK() {
// This encoding depends on docId, password and stream position
final String SAMPLE_WINDOW1_ENCR2 = "3D 00 12 00"
+ "45, B9, 90, FE, B6, C6, EC, 73, EE, 3F, 52, 45, 97, DB, E3, C1, D6, FE";
Biff8EncryptionKey.setCurrentUserPassword("passw0rd");
byte[] dataCorrectDefault = HexRead.readFromString(""
+ COMMON_HEX_DATA
+ "C728659A C38E35E0 568A338F C3FC9D70" // correct saltHash for supplied password (and docId/saltHash)
+ SAMPLE_WINDOW1_ENCR2
);
RecordFactoryInputStream rfis = createRFIS(dataCorrectDefault);
Biff8EncryptionKey.setCurrentUserPassword(null);
confirmReadInitialRecords(rfis);
}
/**
* makes sure the record stream starts with {@link BOFRecord} and then {@link WindowOneRecord}
* The second record is gets decrypted so this method also checks its content.

View File

@ -17,22 +17,18 @@
package org.apache.poi.hssf.record.crypto;
import junit.framework.Test;
import junit.framework.TestSuite;
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
*/
@RunWith(Suite.class)
@Suite.SuiteClasses({
TestBiff8DecryptingStream.class,
TestBiff8EncryptionKey.class
})
public final class AllHSSFEncryptionTests {
public static Test suite() {
TestSuite result = new TestSuite(AllHSSFEncryptionTests.class.getName());
result.addTestSuite(TestBiff8DecryptingStream.class);
result.addTestSuite(TestRC4.class);
result.addTestSuite(TestBiff8EncryptionKey.class);
return result;
}
}

View File

@ -17,22 +17,25 @@
package org.apache.poi.hssf.record.crypto;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import java.io.InputStream;
import java.util.Arrays;
import junit.framework.AssertionFailedError;
import junit.framework.ComparisonFailure;
import junit.framework.TestCase;
import org.apache.poi.util.HexDump;
import org.apache.poi.util.HexRead;
import org.junit.Test;
/**
* Tests for {@link Biff8DecryptingStream}
*
* @author Josh Micich
*/
public final class TestBiff8DecryptingStream extends TestCase {
public final class TestBiff8DecryptingStream {
/**
* A mock {@link InputStream} that keeps track of position and also produces
@ -40,15 +43,14 @@ public final class TestBiff8DecryptingStream extends TestCase {
* than the previous.
*/
private static final class MockStream extends InputStream {
private int _val;
private final int _initialValue;
private int _position;
public MockStream(int initialValue) {
_val = initialValue & 0xFF;
_initialValue = initialValue;
}
public int read() {
_position++;
return _val++ & 0xFF;
return (_initialValue+_position++) & 0xFF;
}
public int getPosition() {
return _position;
@ -68,7 +70,7 @@ public final class TestBiff8DecryptingStream extends TestCase {
public StreamTester(MockStream ms, String keyDigestHex, int expectedFirstInt) {
_ms = ms;
byte[] keyDigest = HexRead.readFromString(keyDigestHex);
_bds = new Biff8DecryptingStream(_ms, 0, new Biff8EncryptionKey(keyDigest));
_bds = new Biff8DecryptingStream(_ms, 0, new Biff8RC4Key(keyDigest));
assertEquals(expectedFirstInt, _bds.readInt());
_errorsOccurred = false;
}
@ -148,7 +150,8 @@ public final class TestBiff8DecryptingStream extends TestCase {
/**
* Tests reading of 64,32,16 and 8 bit integers aligned with key changing boundaries
*/
public void testReadsAlignedWithBoundary() {
@Test
public void readsAlignedWithBoundary() {
StreamTester st = createStreamTester(0x50, "BA AD F0 0D 00", 0x96C66829);
st.rollForward(0x0004, 0x03FF);
@ -169,7 +172,8 @@ public final class TestBiff8DecryptingStream extends TestCase {
/**
* Tests reading of 64,32 and 16 bit integers <i>across</i> key changing boundaries
*/
public void testReadsSpanningBoundary() {
@Test
public void readsSpanningBoundary() {
StreamTester st = createStreamTester(0x50, "BA AD F0 0D 00", 0x96C66829);
st.rollForward(0x0004, 0x03FC);
@ -185,7 +189,8 @@ public final class TestBiff8DecryptingStream extends TestCase {
* Checks that the BIFF header fields (sid, size) get read without applying decryption,
* and that the RC4 stream stays aligned during these calls
*/
public void testReadHeaderUShort() {
@Test
public void readHeaderUShort() {
StreamTester st = createStreamTester(0x50, "BA AD F0 0D 00", 0x96C66829);
st.rollForward(0x0004, 0x03FF);
@ -213,7 +218,8 @@ public final class TestBiff8DecryptingStream extends TestCase {
/**
* Tests reading of byte sequences <i>across</i> and <i>aligned with</i> key changing boundaries
*/
public void testReadByteArrays() {
@Test
public void readByteArrays() {
StreamTester st = createStreamTester(0x50, "BA AD F0 0D 00", 0x96C66829);
st.rollForward(0x0004, 0x2FFC);
@ -223,7 +229,7 @@ public final class TestBiff8DecryptingStream extends TestCase {
st.confirmData("01 C2 4E 55"); // first 4 bytes in next block
st.assertNoErrors();
}
private static StreamTester createStreamTester(int mockStreamStartVal, String keyDigestHex, int expectedFirstInt) {
return new StreamTester(new MockStream(mockStreamStartVal), keyDigestHex, expectedFirstInt);
}

View File

@ -19,12 +19,12 @@ package org.apache.poi.hssf.record.crypto;
import java.util.Arrays;
import org.apache.poi.util.HexDump;
import org.apache.poi.util.HexRead;
import junit.framework.ComparisonFailure;
import junit.framework.TestCase;
import org.apache.poi.util.HexDump;
import org.apache.poi.util.HexRead;
/**
* Tests for {@link Biff8EncryptionKey}
*
@ -37,7 +37,7 @@ public final class TestBiff8EncryptionKey extends TestCase {
}
public void testCreateKeyDigest() {
byte[] docIdData = fromHex("17 F6 D1 6B 09 B1 5F 7B 4C 9D 03 B4 81 B5 B4 4A");
byte[] keyDigest = Biff8EncryptionKey.createKeyDigest("MoneyForNothing", docIdData);
byte[] 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));

View File

@ -1,76 +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.InvalidKeyException;
import java.util.Arrays;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import junit.framework.ComparisonFailure;
import junit.framework.TestCase;
import org.apache.poi.util.HexDump;
import org.apache.poi.util.HexRead;
/**
* Tests for {@link RC4}
*
* @author Josh Micich
*/
public class TestRC4 extends TestCase {
public void testSimple() {
confirmRC4("Key", "Plaintext", "BBF316E8D940AF0AD3");
confirmRC4("Wiki", "pedia", "1021BF0420");
confirmRC4("Secret", "Attack at dawn", "45A01F645FC35B383552544B9BF5");
}
private static void confirmRC4(String k, String origText, String expEncrHex) {
byte[] actEncr = origText.getBytes();
new RC4(k.getBytes()).encrypt(actEncr);
byte[] expEncr = HexRead.readFromString(expEncrHex);
if (!Arrays.equals(expEncr, actEncr)) {
throw new ComparisonFailure("Data mismatch", HexDump.toHex(expEncr), HexDump.toHex(actEncr));
}
Cipher cipher;
try {
cipher = Cipher.getInstance("RC4");
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
String k2 = k+k; // Sun has minimum of 5 bytes for key
SecretKeySpec skeySpec = new SecretKeySpec(k2.getBytes(), "RC4");
try {
cipher.init(Cipher.DECRYPT_MODE, skeySpec);
} catch (InvalidKeyException e) {
throw new RuntimeException(e);
}
byte[] origData = origText.getBytes();
byte[] altEncr = cipher.update(origData);
if (!Arrays.equals(expEncr, altEncr)) {
throw new RuntimeException("Mismatch from jdk provider");
}
}
}

View File

@ -0,0 +1,66 @@
/* ====================================================================
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 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.usermodel.HSSFSheet;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.poifs.crypt.CryptoFunctions;
import org.apache.poi.poifs.filesystem.NPOIFSFileSystem;
import org.apache.poi.util.HexRead;
import org.junit.Test;
public class TestXorEncryption {
private static HSSFTestDataSamples samples = new HSSFTestDataSamples();
@Test
public void testXorEncryption() throws Exception {
// Xor-Password: abc
// 2.5.343 XORObfuscation
// key = 20810
// verifier = 52250
int verifier = CryptoFunctions.createXorVerifier1("abc");
int key = CryptoFunctions.createXorKey1("abc");
assertEquals(20810, key);
assertEquals(52250, verifier);
byte xorArrAct[] = CryptoFunctions.createXorArray1("abc");
byte xorArrExp[] = HexRead.readFromString("AC-CC-A4-AB-D6-BA-C3-BA-D6-A3-2B-45-D3-79-29-BB");
assertThat(xorArrExp, equalTo(xorArrAct));
}
@SuppressWarnings("static-access")
@Test
public void testUserFile() throws Exception {
Biff8EncryptionKey.setCurrentUserPassword("abc");
NPOIFSFileSystem fs = new NPOIFSFileSystem(samples.getSampleFile("xor-encryption-abc.xls"), true);
HSSFWorkbook hwb = new HSSFWorkbook(fs.getRoot(), true);
HSSFSheet sh = hwb.getSheetAt(0);
assertEquals(1.0, sh.getRow(0).getCell(0).getNumericCellValue(), 0.0);
assertEquals(2.0, sh.getRow(1).getCell(0).getNumericCellValue(), 0.0);
assertEquals(3.0, sh.getRow(2).getCell(0).getNumericCellValue(), 0.0);
fs.close();
}
}

View File

@ -54,6 +54,7 @@ import org.apache.poi.hssf.record.aggregates.FormulaRecordAggregate;
import org.apache.poi.hssf.record.aggregates.PageSettingsBlock;
import org.apache.poi.hssf.record.aggregates.RecordAggregate;
import org.apache.poi.hssf.record.common.UnicodeString;
import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey;
import org.apache.poi.poifs.filesystem.NPOIFSFileSystem;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
import org.apache.poi.ss.formula.ptg.Area3DPtg;
@ -2113,6 +2114,8 @@ public final class TestBugs extends BaseTestBugzillaIssues {
*/
@Test
public void bug50833() throws Exception {
Biff8EncryptionKey.setCurrentUserPassword(null);
HSSFWorkbook wb = openSample("50833.xls");
HSSFSheet s = wb.getSheetAt(0);
assertEquals("Sheet1", s.getSheetName());
@ -2350,14 +2353,9 @@ public final class TestBugs extends BaseTestBugzillaIssues {
* Normally encrypted files have BOF then FILEPASS, but
* some may squeeze a WRITEPROTECT in the middle
*/
@Test
@Test(expected=EncryptedDocumentException.class)
public void bug51832() {
try {
openSample("51832.xls");
fail("Encrypted file");
} catch(EncryptedDocumentException e) {
// Good
}
openSample("51832.xls");
}
@Test
@ -2480,10 +2478,15 @@ public final class TestBugs extends BaseTestBugzillaIssues {
assertEquals(rstyle.getBorderBottom(), HSSFCellStyle.BORDER_DOUBLE);
}
@Test(expected=EncryptedDocumentException.class)
@Test
public void bug35897() throws Exception {
// password is abc
openSample("xor-encryption-abc.xls");
try {
Biff8EncryptionKey.setCurrentUserPassword("abc");
openSample("xor-encryption-abc.xls");
} finally {
Biff8EncryptionKey.setCurrentUserPassword(null);
}
}
@Test