*/
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 final int ENCRYPTION_XOR = 0;
- private static final int ENCRYPTION_OTHER = 1;
+ 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);
+ }
- 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 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;
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) {
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;
}
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();
}
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
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();
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);
}
--- /dev/null
+/* ====================================================================\r
+ Licensed to the Apache Software Foundation (ASF) under one or more\r
+ contributor license agreements. See the NOTICE file distributed with\r
+ this work for additional information regarding copyright ownership.\r
+ The ASF licenses this file to You under the Apache License, Version 2.0\r
+ (the "License"); you may not use this file except in compliance with\r
+ the License. You may obtain a copy of the License at\r
+\r
+ http://www.apache.org/licenses/LICENSE-2.0\r
+\r
+ Unless required by applicable law or agreed to in writing, software\r
+ distributed under the License is distributed on an "AS IS" BASIS,\r
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ See the License for the specific language governing permissions and\r
+ limitations under the License.\r
+==================================================================== */\r
+\r
+package org.apache.poi.hssf.record.crypto;\r
+\r
+\r
+public interface Biff8Cipher {\r
+ void startRecord(int currentSid);\r
+ void setNextRecordSize(int recordSize);\r
+ void skipTwoBytes();\r
+ void xor(byte[] buf, int pOffset, int pLen);\r
+ int xorByte(int rawVal);\r
+ int xorShort(int rawVal);\r
+ int xorInt(int rawVal);\r
+ long xorLong(long rawVal);\r
+}\r
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;
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
*/
public int readRecordSID() {
int sid = _le.readUShort();
- _rc4.skipTwoBytes();
- _rc4.startRecord(sid);
+ _cipher.skipTwoBytes();
+ _cipher.startRecord(sid);
return sid;
}
*/
public int readDataSize() {
int dataSize = _le.readUShort();
- _rc4.skipTwoBytes();
+ _cipher.skipTwoBytes();
+ _cipher.setNextRecordSize(dataSize);
return dataSize;
}
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());
}
}
==================================================================== */
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(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;
+ public static Biff8EncryptionKey create(byte[] salt) {
+ return Biff8RC4Key.create(Decryptor.DEFAULT_PASSWORD, salt);
}
-
- 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);
- }
-
- 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;
+ throw new EncryptedDocumentException("validate is not supported (in super-class).");
}
- 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
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.
*/
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);
}
/**
* 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;
}
// 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;
}
// 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 */
}
}
--- /dev/null
+/* ====================================================================\r
+ Licensed to the Apache Software Foundation (ASF) under one or more\r
+ contributor license agreements. See the NOTICE file distributed with\r
+ this work for additional information regarding copyright ownership.\r
+ The ASF licenses this file to You under the Apache License, Version 2.0\r
+ (the "License"); you may not use this file except in compliance with\r
+ the License. You may obtain a copy of the License at\r
+\r
+ http://www.apache.org/licenses/LICENSE-2.0\r
+\r
+ Unless required by applicable law or agreed to in writing, software\r
+ distributed under the License is distributed on an "AS IS" BASIS,\r
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ See the License for the specific language governing permissions and\r
+ limitations under the License.\r
+==================================================================== */\r
+\r
+package org.apache.poi.hssf.record.crypto;\r
+\r
+import java.security.GeneralSecurityException;\r
+import java.security.MessageDigest;\r
+import java.util.Arrays;\r
+\r
+import javax.crypto.Cipher;\r
+import javax.crypto.ShortBufferException;\r
+import javax.crypto.spec.SecretKeySpec;\r
+\r
+import org.apache.poi.EncryptedDocumentException;\r
+import org.apache.poi.poifs.crypt.CipherAlgorithm;\r
+import org.apache.poi.poifs.crypt.CryptoFunctions;\r
+import org.apache.poi.poifs.crypt.HashAlgorithm;\r
+import org.apache.poi.util.HexDump;\r
+import org.apache.poi.util.LittleEndian;\r
+import org.apache.poi.util.LittleEndianConsts;\r
+import org.apache.poi.util.POILogFactory;\r
+import org.apache.poi.util.POILogger;\r
+\r
+public class Biff8RC4Key extends Biff8EncryptionKey {\r
+ // these two constants coincidentally have the same value\r
+ public static final int KEY_DIGEST_LENGTH = 5;\r
+ private static final int PASSWORD_HASH_NUMBER_OF_BYTES_USED = 5;\r
+\r
+ private static POILogger log = POILogFactory.getLogger(Biff8RC4Key.class);\r
+ \r
+ Biff8RC4Key(byte[] keyDigest) {\r
+ if (keyDigest.length != KEY_DIGEST_LENGTH) {\r
+ throw new IllegalArgumentException("Expected 5 byte key digest, but got " + HexDump.toHex(keyDigest));\r
+ }\r
+\r
+ CipherAlgorithm ca = CipherAlgorithm.rc4;\r
+ _secretKey = new SecretKeySpec(keyDigest, ca.jceId);\r
+ }\r
+\r
+ /**\r
+ * Create using the default password and a specified docId\r
+ * @param salt 16 bytes\r
+ */\r
+ public static Biff8RC4Key create(String password, byte[] salt) {\r
+ return new Biff8RC4Key(createKeyDigest(password, salt));\r
+ }\r
+ \r
+ /**\r
+ * @return <code>true</code> if the keyDigest is compatible with the specified saltData and saltHash\r
+ */\r
+ public boolean validate(byte[] verifier, byte[] verifierHash) {\r
+ check16Bytes(verifier, "verifier");\r
+ check16Bytes(verifierHash, "verifierHash");\r
+\r
+ // validation uses the RC4 for block zero\r
+ Cipher rc4 = getCipher();\r
+ initCipherForBlock(rc4, 0);\r
+ \r
+ byte[] verifierPrime = verifier.clone();\r
+ byte[] verifierHashPrime = verifierHash.clone();\r
+\r
+ try {\r
+ rc4.update(verifierPrime, 0, verifierPrime.length, verifierPrime);\r
+ rc4.update(verifierHashPrime, 0, verifierHashPrime.length, verifierHashPrime);\r
+ } catch (ShortBufferException e) {\r
+ throw new EncryptedDocumentException("buffer too short", e);\r
+ }\r
+\r
+ MessageDigest md5 = CryptoFunctions.getMessageDigest(HashAlgorithm.md5);\r
+ md5.update(verifierPrime);\r
+ byte[] finalVerifierResult = md5.digest();\r
+\r
+ if (log.check(POILogger.DEBUG)) {\r
+ byte[] verifierHashThatWouldWork = xor(verifierHash, xor(verifierHashPrime, finalVerifierResult));\r
+ log.log(POILogger.DEBUG, "valid verifierHash value", HexDump.toHex(verifierHashThatWouldWork));\r
+ }\r
+\r
+ return Arrays.equals(verifierHashPrime, finalVerifierResult);\r
+ }\r
+ \r
+ Cipher getCipher() {\r
+ CipherAlgorithm ca = CipherAlgorithm.rc4;\r
+ Cipher rc4 = CryptoFunctions.getCipher(_secretKey, ca, null, null, Cipher.ENCRYPT_MODE);\r
+ return rc4;\r
+ }\r
+ \r
+ static byte[] createKeyDigest(String password, byte[] docIdData) {\r
+ check16Bytes(docIdData, "docId");\r
+ int nChars = Math.min(password.length(), 16);\r
+ byte[] passwordData = new byte[nChars*2];\r
+ for (int i=0; i<nChars; i++) {\r
+ char ch = password.charAt(i);\r
+ passwordData[i*2+0] = (byte) ((ch << 0) & 0xFF);\r
+ passwordData[i*2+1] = (byte) ((ch << 8) & 0xFF);\r
+ }\r
+\r
+ MessageDigest md5 = CryptoFunctions.getMessageDigest(HashAlgorithm.md5);\r
+ md5.update(passwordData);\r
+ byte[] passwordHash = md5.digest();\r
+ md5.reset();\r
+\r
+ for (int i=0; i<16; i++) {\r
+ md5.update(passwordHash, 0, PASSWORD_HASH_NUMBER_OF_BYTES_USED);\r
+ md5.update(docIdData, 0, docIdData.length);\r
+ }\r
+ \r
+ byte[] result = CryptoFunctions.getBlock0(md5.digest(), KEY_DIGEST_LENGTH);\r
+ return result;\r
+ }\r
+\r
+ void initCipherForBlock(Cipher rc4, int keyBlockNo) {\r
+ byte buf[] = new byte[LittleEndianConsts.INT_SIZE]; \r
+ LittleEndian.putInt(buf, 0, keyBlockNo);\r
+ \r
+ MessageDigest md5 = CryptoFunctions.getMessageDigest(HashAlgorithm.md5);\r
+ md5.update(_secretKey.getEncoded());\r
+ md5.update(buf);\r
+\r
+ SecretKeySpec skeySpec = new SecretKeySpec(md5.digest(), _secretKey.getAlgorithm());\r
+ try {\r
+ rc4.init(Cipher.ENCRYPT_MODE, skeySpec);\r
+ } catch (GeneralSecurityException e) {\r
+ throw new EncryptedDocumentException("Can't rekey for next block", e);\r
+ }\r
+ }\r
+ \r
+ private static byte[] xor(byte[] a, byte[] b) {\r
+ byte[] c = new byte[a.length];\r
+ for (int i = 0; i < c.length; i++) {\r
+ c[i] = (byte) (a[i] ^ b[i]);\r
+ }\r
+ return c;\r
+ }\r
+ private static void check16Bytes(byte[] data, String argName) {\r
+ if (data.length != 16) {\r
+ throw new IllegalArgumentException("Expected 16 byte " + argName + ", but got " + HexDump.toHex(data));\r
+ }\r
+ }\r
+ \r
+\r
+}\r
--- /dev/null
+/* ====================================================================\r
+ Licensed to the Apache Software Foundation (ASF) under one or more\r
+ contributor license agreements. See the NOTICE file distributed with\r
+ this work for additional information regarding copyright ownership.\r
+ The ASF licenses this file to You under the Apache License, Version 2.0\r
+ (the "License"); you may not use this file except in compliance with\r
+ the License. You may obtain a copy of the License at\r
+\r
+ http://www.apache.org/licenses/LICENSE-2.0\r
+\r
+ Unless required by applicable law or agreed to in writing, software\r
+ distributed under the License is distributed on an "AS IS" BASIS,\r
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ See the License for the specific language governing permissions and\r
+ limitations under the License.\r
+==================================================================== */\r
+\r
+package org.apache.poi.hssf.record.crypto;\r
+\r
+import java.nio.ByteBuffer;\r
+import java.nio.ByteOrder;\r
+\r
+import javax.crypto.Cipher;\r
+\r
+import org.apache.poi.hssf.record.BOFRecord;\r
+import org.apache.poi.hssf.record.FilePassRecord;\r
+import org.apache.poi.hssf.record.InterfaceHdrRecord;\r
+\r
+public class Biff8XOR implements Biff8Cipher {\r
+\r
+ private final Biff8XORKey _key;\r
+ private ByteBuffer _buffer = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN);\r
+ private boolean _shouldSkipEncryptionOnCurrentRecord;\r
+ private final int _initialOffset;\r
+ private int _dataLength = 0;\r
+ private int _xorArrayIndex = 0;\r
+ \r
+ public Biff8XOR(int initialOffset, Biff8XORKey key) {\r
+ _key = key;\r
+ _initialOffset = initialOffset;\r
+\r
+ }\r
+ \r
+ public void startRecord(int currentSid) {\r
+ _shouldSkipEncryptionOnCurrentRecord = isNeverEncryptedRecord(currentSid);\r
+ }\r
+\r
+ public void setNextRecordSize(int recordSize) {\r
+ /*\r
+ * From: http://social.msdn.microsoft.com/Forums/en-US/3dadbed3-0e68-4f11-8b43-3a2328d9ebd5\r
+ * \r
+ * The initial value for XorArrayIndex is as follows:\r
+ * XorArrayIndex = (FileOffset + Data.Length) % 16\r
+ * \r
+ * The FileOffset variable in this context is the stream offset into the Workbook stream at\r
+ * the time we are about to write each of the bytes of the record data.\r
+ * This (the value) is then incremented after each byte is written. \r
+ */\r
+ _xorArrayIndex = (_initialOffset+_dataLength+recordSize) % 16;\r
+ }\r
+ \r
+ \r
+ /**\r
+ * TODO: Additionally, the lbPlyPos (position_of_BOF) field of the BoundSheet8 record MUST NOT be encrypted.\r
+ *\r
+ * @return <code>true</code> if record type specified by <tt>sid</tt> is never encrypted\r
+ */\r
+ private static boolean isNeverEncryptedRecord(int sid) {\r
+ switch (sid) {\r
+ case BOFRecord.sid:\r
+ // sheet BOFs for sure\r
+ // TODO - find out about chart BOFs\r
+\r
+ case InterfaceHdrRecord.sid:\r
+ // don't know why this record doesn't seem to get encrypted\r
+\r
+ case FilePassRecord.sid:\r
+ // this only really counts when writing because FILEPASS is read early\r
+\r
+ // UsrExcl(0x0194)\r
+ // FileLock\r
+ // RRDInfo(0x0196)\r
+ // RRDHead(0x0138)\r
+\r
+ return true;\r
+ }\r
+ return false;\r
+ }\r
+\r
+ /**\r
+ * Used when BIFF header fields (sid, size) are being read. The internal\r
+ * {@link Cipher} instance must step even when unencrypted bytes are read\r
+ */\r
+ public void skipTwoBytes() {\r
+ _dataLength += 2;\r
+ }\r
+\r
+ /**\r
+ * Decrypts a xor obfuscated byte array.\r
+ * The data is decrypted in-place\r
+ * \r
+ * @see <a href="http://msdn.microsoft.com/en-us/library/dd908506.aspx">2.3.7.3 Binary Document XOR Data Transformation Method 1</a>\r
+ */\r
+ public void xor(byte[] buf, int pOffset, int pLen) {\r
+ if (_shouldSkipEncryptionOnCurrentRecord) {\r
+ _dataLength += pLen;\r
+ return;\r
+ }\r
+ \r
+ // The following is taken from the Libre Office implementation\r
+ // It seems that the encrypt and decrypt method is mixed up\r
+ // in the MS-OFFCRYPTO docs\r
+\r
+ byte xorArray[] = _key._secretKey.getEncoded();\r
+ \r
+ for (int i=0; i<pLen; i++) {\r
+ byte value = buf[pOffset+i];\r
+ value = rotateLeft(value, 3);\r
+ value ^= xorArray[_xorArrayIndex];\r
+ buf[pOffset+i] = value;\r
+ _xorArrayIndex = (_xorArrayIndex + 1) % 16;\r
+ _dataLength++;\r
+ }\r
+ }\r
+ \r
+ private static byte rotateLeft(byte bits, int shift) {\r
+ return (byte)(((bits & 0xff) << shift) | ((bits & 0xff) >>> (8 - shift)));\r
+ }\r
+ \r
+ public int xorByte(int rawVal) {\r
+ _buffer.put(0, (byte)rawVal);\r
+ xor(_buffer.array(), 0, 1);\r
+ return _buffer.get(0);\r
+ }\r
+\r
+ public int xorShort(int rawVal) {\r
+ _buffer.putShort(0, (short)rawVal);\r
+ xor(_buffer.array(), 0, 2);\r
+ return _buffer.getShort(0);\r
+ }\r
+\r
+ public int xorInt(int rawVal) {\r
+ _buffer.putInt(0, rawVal);\r
+ xor(_buffer.array(), 0, 4);\r
+ return _buffer.getInt(0);\r
+ }\r
+\r
+ public long xorLong(long rawVal) {\r
+ _buffer.putLong(0, rawVal);\r
+ xor(_buffer.array(), 0, 8);\r
+ return _buffer.getLong(0);\r
+ }\r
+}\r
--- /dev/null
+/* ====================================================================\r
+ Licensed to the Apache Software Foundation (ASF) under one or more\r
+ contributor license agreements. See the NOTICE file distributed with\r
+ this work for additional information regarding copyright ownership.\r
+ The ASF licenses this file to You under the Apache License, Version 2.0\r
+ (the "License"); you may not use this file except in compliance with\r
+ the License. You may obtain a copy of the License at\r
+\r
+ http://www.apache.org/licenses/LICENSE-2.0\r
+\r
+ Unless required by applicable law or agreed to in writing, software\r
+ distributed under the License is distributed on an "AS IS" BASIS,\r
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ See the License for the specific language governing permissions and\r
+ limitations under the License.\r
+==================================================================== */\r
+\r
+package org.apache.poi.hssf.record.crypto;\r
+\r
+import javax.crypto.spec.SecretKeySpec;\r
+\r
+import org.apache.poi.poifs.crypt.CryptoFunctions;\r
+\r
+\r
+public class Biff8XORKey extends Biff8EncryptionKey {\r
+ final int _xorKey;\r
+ \r
+ public Biff8XORKey(String password, int xorKey) {\r
+ _xorKey = xorKey;\r
+ byte xorArray[] = CryptoFunctions.createXorArray1(password);\r
+ _secretKey = new SecretKeySpec(xorArray, "XOR");\r
+ }\r
+ \r
+ public static Biff8XORKey create(String password, int xorKey) {\r
+ return new Biff8XORKey(password, xorKey);\r
+ }\r
+\r
+ public boolean validate(String password, int verifier) {\r
+ int keyComp = CryptoFunctions.createXorKey1(password);\r
+ int verifierComp = CryptoFunctions.createXorVerifier1(password);\r
+\r
+ return (_xorKey == keyComp && verifierComp == verifier);\r
+ }\r
+}\r
+++ /dev/null
-/* ====================================================================
- 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();
- }
-}
0x313E, 0x1872, 0xE139, 0xD40F, 0x84F9, 0x280C, 0xA96A, \r
0x4EC3\r
};\r
+\r
+ private static final byte PadArray[] = {\r
+ (byte)0xBB, (byte)0xFF, (byte)0xFF, (byte)0xBA, (byte)0xFF,\r
+ (byte)0xFF, (byte)0xB9, (byte)0x80, (byte)0x00, (byte)0xBE,\r
+ (byte)0x0F, (byte)0x00, (byte)0xBF, (byte)0x0F, (byte)0x00\r
+ };\r
\r
private static final int EncryptionMatrix[][] = {\r
/* char 1 */ {0xAEFC, 0x4DD9, 0x9BB2, 0x2745, 0x4E8A, 0x9D14, 0x2A09},\r
};\r
\r
/**\r
- * This method generates the xored-hashed password for word documents < 2007.\r
+ * This method generates the xor verifier for word documents < 2007 (method 2).\r
* Its output will be used as password input for the newer word generations which\r
* utilize a real hashing algorithm like sha1.\r
* \r
- * Although the code was taken from the "see"-link below, this looks similar\r
- * to the method in [MS-OFFCRYPTO] 2.3.7.2 Binary Document XOR Array Initialization Method 1. \r
- *\r
- * @param password\r
+ * @param password the password\r
* @return the hashed password\r
* \r
+ * @see <a href="http://msdn.microsoft.com/en-us/library/dd905229.aspx">2.3.7.4 Binary Document Password Verifier Derivation Method 2</a>\r
* @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>\r
* @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>\r
*/\r
- public static int xorHashPasswordAsInt(String password) {\r
+ public static int createXorVerifier2(String password) {\r
//Array to hold Key Values\r
byte[] generatedKey = new byte[4];\r
\r
* This method generates the xored-hashed password for word documents < 2007.\r
*/\r
public static String xorHashPassword(String password) {\r
- int hashedPassword = xorHashPasswordAsInt(password);\r
+ int hashedPassword = createXorVerifier2(password);\r
return String.format("%1$08X", hashedPassword);\r
}\r
\r
* processing in word documents 2007 and newer, which utilize a real hashing algorithm like sha1.\r
*/\r
public static String xorHashPasswordReversed(String password) {\r
- int hashedPassword = xorHashPasswordAsInt(password);\r
+ int hashedPassword = createXorVerifier2(password);\r
\r
return String.format("%1$02X%2$02X%3$02X%4$02X"\r
, ( hashedPassword >>> 0 ) & 0xFF\r
, ( hashedPassword >>> 24 ) & 0xFF\r
);\r
}\r
+\r
+ /**\r
+ * Create the verifier for xor obfuscation (method 1)\r
+ *\r
+ * @see <a href="http://msdn.microsoft.com/en-us/library/dd926947.aspx">2.3.7.1 Binary Document Password Verifier Derivation Method 1</a>\r
+ * @see <a href="http://msdn.microsoft.com/en-us/library/dd905229.aspx">2.3.7.4 Binary Document Password Verifier Derivation Method 2</a>\r
+ * \r
+ * @param password the password\r
+ * @return the verifier\r
+ */\r
+ public static int createXorVerifier1(String password) {\r
+ // the verifier for method 1 is part of the verifier for method 2\r
+ // so we simply chop it from there\r
+ return createXorVerifier2(password) & 0xFFFF;\r
+ }\r
+ \r
+ /**\r
+ * Create the xor key for xor obfuscation, which is used to create the xor array (method 1)\r
+ *\r
+ * @see <a href="http://msdn.microsoft.com/en-us/library/dd924704.aspx">2.3.7.2 Binary Document XOR Array Initialization Method 1</a>\r
+ * @see <a href="http://msdn.microsoft.com/en-us/library/dd905229.aspx">2.3.7.4 Binary Document Password Verifier Derivation Method 2</a>\r
+ * \r
+ * @param password the password\r
+ * @return the xor key\r
+ */\r
+ public static int createXorKey1(String password) {\r
+ // the xor key for method 1 is part of the verifier for method 2\r
+ // so we simply chop it from there\r
+ return createXorVerifier2(password) >>> 16;\r
+ }\r
+\r
+ /**\r
+ * Creates an byte array for xor obfuscation (method 1) \r
+ *\r
+ * @see <a href="http://msdn.microsoft.com/en-us/library/dd924704.aspx">2.3.7.2 Binary Document XOR Array Initialization Method 1</a>\r
+ * @see <a href="http://docs.libreoffice.org/oox/html/binarycodec_8cxx_source.html">Libre Office implementation</a>\r
+ *\r
+ * @param password the password\r
+ * @return the byte array for xor obfuscation\r
+ */\r
+ public static byte[] createXorArray1(String password) {\r
+ if (password.length() > 15) password = password.substring(0, 15);\r
+ byte passBytes[] = password.getBytes(Charset.forName("ASCII"));\r
+ \r
+ // this code is based on the libre office implementation.\r
+ // The MS-OFFCRYPTO misses some infos about the various rotation sizes \r
+ byte obfuscationArray[] = new byte[16];\r
+ System.arraycopy(passBytes, 0, obfuscationArray, 0, passBytes.length);\r
+ System.arraycopy(PadArray, 0, obfuscationArray, passBytes.length, PadArray.length-passBytes.length+1);\r
+ \r
+ int xorKey = createXorKey1(password);\r
+ \r
+ // rotation of key values is application dependent\r
+ int nRotateSize = 2; /* Excel = 2; Word = 7 */\r
+ \r
+ byte baseKeyLE[] = { (byte)(xorKey & 0xFF), (byte)((xorKey >>> 8) & 0xFF) };\r
+ for (int i=0; i<obfuscationArray.length; i++) {\r
+ obfuscationArray[i] ^= baseKeyLE[i&1];\r
+ obfuscationArray[i] = rotateLeft(obfuscationArray[i], nRotateSize);\r
+ }\r
+ \r
+ return obfuscationArray;\r
+ }\r
+\r
+ private static byte rotateLeft(byte bits, int shift) {\r
+ return (byte)(((bits & 0xff) << shift) | ((bits & 0xff) >>> (8 - shift)));\r
+ }\r
}\r
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.
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";
+ 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";
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
- );
+ expectedEx.expect(EncryptedDocumentException.class);
+ expectedEx.expectMessage("Supplied password is invalid for salt/verifier/verifierHash");
+ createRFIS(dataWrongDefault);
+ }
- rfis = createRFIS(dataCorrectDefault);
- Biff8EncryptionKey.setCurrentUserPassword(null);
+ @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";
- confirmReadInitialRecords(rfis);
- }
+ 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.
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;
- }
}
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
* 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;
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;
}
/**
* 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);
/**
* 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);
* 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);
/**
* 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);
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);
}
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}
*
}
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));
+++ /dev/null
-/* ====================================================================
- 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");
- }
- }
-}
--- /dev/null
+/* ====================================================================\r
+ Licensed to the Apache Software Foundation (ASF) under one or more\r
+ contributor license agreements. See the NOTICE file distributed with\r
+ this work for additional information regarding copyright ownership.\r
+ The ASF licenses this file to You under the Apache License, Version 2.0\r
+ (the "License"); you may not use this file except in compliance with\r
+ the License. You may obtain a copy of the License at\r
+\r
+ http://www.apache.org/licenses/LICENSE-2.0\r
+\r
+ Unless required by applicable law or agreed to in writing, software\r
+ distributed under the License is distributed on an "AS IS" BASIS,\r
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ See the License for the specific language governing permissions and\r
+ limitations under the License.\r
+==================================================================== */\r
+\r
+package org.apache.poi.hssf.record.crypto;\r
+\r
+import static org.hamcrest.core.IsEqual.equalTo;\r
+import static org.junit.Assert.assertEquals;\r
+import static org.junit.Assert.assertThat;\r
+\r
+import org.apache.poi.hssf.HSSFTestDataSamples;\r
+import org.apache.poi.hssf.usermodel.HSSFSheet;\r
+import org.apache.poi.hssf.usermodel.HSSFWorkbook;\r
+import org.apache.poi.poifs.crypt.CryptoFunctions;\r
+import org.apache.poi.poifs.filesystem.NPOIFSFileSystem;\r
+import org.apache.poi.util.HexRead;\r
+import org.junit.Test;\r
+\r
+public class TestXorEncryption {\r
+ \r
+ private static HSSFTestDataSamples samples = new HSSFTestDataSamples();\r
+ \r
+ @Test\r
+ public void testXorEncryption() throws Exception {\r
+ // Xor-Password: abc\r
+ // 2.5.343 XORObfuscation\r
+ // key = 20810\r
+ // verifier = 52250\r
+ int verifier = CryptoFunctions.createXorVerifier1("abc");\r
+ int key = CryptoFunctions.createXorKey1("abc");\r
+ assertEquals(20810, key);\r
+ assertEquals(52250, verifier);\r
+ \r
+ byte xorArrAct[] = CryptoFunctions.createXorArray1("abc");\r
+ byte xorArrExp[] = HexRead.readFromString("AC-CC-A4-AB-D6-BA-C3-BA-D6-A3-2B-45-D3-79-29-BB");\r
+ assertThat(xorArrExp, equalTo(xorArrAct));\r
+ }\r
+\r
+ @SuppressWarnings("static-access")\r
+ @Test\r
+ public void testUserFile() throws Exception {\r
+ Biff8EncryptionKey.setCurrentUserPassword("abc");\r
+ NPOIFSFileSystem fs = new NPOIFSFileSystem(samples.getSampleFile("xor-encryption-abc.xls"), true);\r
+ HSSFWorkbook hwb = new HSSFWorkbook(fs.getRoot(), true);\r
+ \r
+ HSSFSheet sh = hwb.getSheetAt(0);\r
+ assertEquals(1.0, sh.getRow(0).getCell(0).getNumericCellValue(), 0.0);\r
+ assertEquals(2.0, sh.getRow(1).getCell(0).getNumericCellValue(), 0.0);\r
+ assertEquals(3.0, sh.getRow(2).getCell(0).getNumericCellValue(), 0.0);\r
+\r
+ fs.close();\r
+ }\r
+}\r
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;
*/
@Test
public void bug50833() throws Exception {
+ Biff8EncryptionKey.setCurrentUserPassword(null);
+
HSSFWorkbook wb = openSample("50833.xls");
HSSFSheet s = wb.getSheetAt(0);
assertEquals("Sheet1", s.getSheetName());
* 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
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