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:
parent
337f775807
commit
00e2e55338
@ -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();
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
30
src/java/org/apache/poi/hssf/record/crypto/Biff8Cipher.java
Normal file
30
src/java/org/apache/poi/hssf/record/crypto/Biff8Cipher.java
Normal 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);
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 */
|
||||
}
|
||||
}
|
||||
|
155
src/java/org/apache/poi/hssf/record/crypto/Biff8RC4Key.java
Normal file
155
src/java/org/apache/poi/hssf/record/crypto/Biff8RC4Key.java
Normal 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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
153
src/java/org/apache/poi/hssf/record/crypto/Biff8XOR.java
Normal file
153
src/java/org/apache/poi/hssf/record/crypto/Biff8XOR.java
Normal 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);
|
||||
}
|
||||
}
|
44
src/java/org/apache/poi/hssf/record/crypto/Biff8XORKey.java
Normal file
44
src/java/org/apache/poi/hssf/record/crypto/Biff8XORKey.java
Normal 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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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 < 2007.
|
||||
* This method generates the xor verifier for word documents < 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 < 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)));
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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));
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user