From a348d9530ff8108953e346f5048af77ce12d201d Mon Sep 17 00:00:00 2001 From: Andreas Beeker Date: Mon, 5 May 2014 21:41:31 +0000 Subject: [PATCH] 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 --- .../poi/hssf/record/FilePassRecord.java | 259 ++++++++++++++---- .../hssf/record/RecordFactoryInputStream.java | 43 ++- .../poi/hssf/record/crypto/Biff8Cipher.java | 30 ++ .../record/crypto/Biff8DecryptingStream.java | 32 ++- .../record/crypto/Biff8EncryptionKey.java | 129 +-------- .../poi/hssf/record/crypto/Biff8RC4.java | 124 +++++---- .../poi/hssf/record/crypto/Biff8RC4Key.java | 155 +++++++++++ .../poi/hssf/record/crypto/Biff8XOR.java | 153 +++++++++++ .../poi/hssf/record/crypto/Biff8XORKey.java | 44 +++ .../apache/poi/hssf/record/crypto/RC4.java | 90 ------ .../poi/poifs/crypt/CryptoFunctions.java | 87 +++++- .../record/TestRecordFactoryInputStream.java | 108 ++++---- .../record/crypto/AllHSSFEncryptionTests.java | 18 +- .../crypto/TestBiff8DecryptingStream.java | 30 +- .../record/crypto/TestBiff8EncryptionKey.java | 8 +- .../poi/hssf/record/crypto/TestRC4.java | 76 ----- .../hssf/record/crypto/TestXorEncryption.java | 66 +++++ .../apache/poi/hssf/usermodel/TestBugs.java | 21 +- 18 files changed, 960 insertions(+), 513 deletions(-) create mode 100644 src/java/org/apache/poi/hssf/record/crypto/Biff8Cipher.java create mode 100644 src/java/org/apache/poi/hssf/record/crypto/Biff8RC4Key.java create mode 100644 src/java/org/apache/poi/hssf/record/crypto/Biff8XOR.java create mode 100644 src/java/org/apache/poi/hssf/record/crypto/Biff8XORKey.java delete mode 100644 src/java/org/apache/poi/hssf/record/crypto/RC4.java delete mode 100644 src/testcases/org/apache/poi/hssf/record/crypto/TestRC4.java create mode 100644 src/testcases/org/apache/poi/hssf/record/crypto/TestXorEncryption.java diff --git a/src/java/org/apache/poi/hssf/record/FilePassRecord.java b/src/java/org/apache/poi/hssf/record/FilePassRecord.java index 6961ed7df9..32ad6787e2 100644 --- a/src/java/org/apache/poi/hssf/record/FilePassRecord.java +++ b/src/java/org/apache/poi/hssf/record/FilePassRecord.java @@ -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 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) { @@ -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(); } diff --git a/src/java/org/apache/poi/hssf/record/RecordFactoryInputStream.java b/src/java/org/apache/poi/hssf/record/RecordFactoryInputStream.java index aac88b80c9..61aa24769b 100644 --- a/src/java/org/apache/poi/hssf/record/RecordFactoryInputStream.java +++ b/src/java/org/apache/poi/hssf/record/RecordFactoryInputStream.java @@ -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 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); } diff --git a/src/java/org/apache/poi/hssf/record/crypto/Biff8Cipher.java b/src/java/org/apache/poi/hssf/record/crypto/Biff8Cipher.java new file mode 100644 index 0000000000..8ac742e0df --- /dev/null +++ b/src/java/org/apache/poi/hssf/record/crypto/Biff8Cipher.java @@ -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); +} diff --git a/src/java/org/apache/poi/hssf/record/crypto/Biff8DecryptingStream.java b/src/java/org/apache/poi/hssf/record/crypto/Biff8DecryptingStream.java index a56f911e2a..f52a15d814 100644 --- a/src/java/org/apache/poi/hssf/record/crypto/Biff8DecryptingStream.java +++ b/src/java/org/apache/poi/hssf/record/crypto/Biff8DecryptingStream.java @@ -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()); } } diff --git a/src/java/org/apache/poi/hssf/record/crypto/Biff8EncryptionKey.java b/src/java/org/apache/poi/hssf/record/crypto/Biff8EncryptionKey.java index f39f0ccf1b..3a28b81af1 100644 --- a/src/java/org/apache/poi/hssf/record/crypto/Biff8EncryptionKey.java +++ b/src/java/org/apache/poi/hssf/record/crypto/Biff8EncryptionKey.java @@ -16,138 +16,33 @@ ==================================================================== */ 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; itrue 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 diff --git a/src/java/org/apache/poi/hssf/record/crypto/Biff8RC4.java b/src/java/org/apache/poi/hssf/record/crypto/Biff8RC4.java index edda6fff95..9d0275fec3 100644 --- a/src/java/org/apache/poi/hssf/record/crypto/Biff8RC4.java +++ b/src/java/org/apache/poi/hssf/record/crypto/Biff8RC4.java @@ -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 */ } } diff --git a/src/java/org/apache/poi/hssf/record/crypto/Biff8RC4Key.java b/src/java/org/apache/poi/hssf/record/crypto/Biff8RC4Key.java new file mode 100644 index 0000000000..289428043a --- /dev/null +++ b/src/java/org/apache/poi/hssf/record/crypto/Biff8RC4Key.java @@ -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 true 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; itrue if record type specified by sid 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 2.3.7.3 Binary Document XOR Data Transformation Method 1 + */ + 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>> (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); + } +} diff --git a/src/java/org/apache/poi/hssf/record/crypto/Biff8XORKey.java b/src/java/org/apache/poi/hssf/record/crypto/Biff8XORKey.java new file mode 100644 index 0000000000..7f2903dd55 --- /dev/null +++ b/src/java/org/apache/poi/hssf/record/crypto/Biff8XORKey.java @@ -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); + } +} diff --git a/src/java/org/apache/poi/hssf/record/crypto/RC4.java b/src/java/org/apache/poi/hssf/record/crypto/RC4.java deleted file mode 100644 index a4abcfad8d..0000000000 --- a/src/java/org/apache/poi/hssf/record/crypto/RC4.java +++ /dev/null @@ -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 wikipedia's RC4 article - * - * @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(); - } -} diff --git a/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java b/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java index f5f52b93f4..f9f970ade9 100644 --- a/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java +++ b/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java @@ -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 2.3.7.4 Binary Document Password Verifier Derivation Method 2 * @see How to set the editing restrictions in Word using Open XML SDK 2.0 * @see Funny: How the new powerful cryptography implemented in Word 2007 turns it into a perfect tool for document password removal. */ - 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 2.3.7.1 Binary Document Password Verifier Derivation Method 1 + * @see 2.3.7.4 Binary Document Password Verifier Derivation Method 2 + * + * @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 2.3.7.2 Binary Document XOR Array Initialization Method 1 + * @see 2.3.7.4 Binary Document Password Verifier Derivation Method 2 + * + * @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 2.3.7.2 Binary Document XOR Array Initialization Method 1 + * @see Libre Office implementation + * + * @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>> (8 - shift))); + } } diff --git a/src/testcases/org/apache/poi/hssf/record/TestRecordFactoryInputStream.java b/src/testcases/org/apache/poi/hssf/record/TestRecordFactoryInputStream.java index a97bb5fb30..8df989d158 100644 --- a/src/testcases/org/apache/poi/hssf/record/TestRecordFactoryInputStream.java +++ b/src/testcases/org/apache/poi/hssf/record/TestRecordFactoryInputStream.java @@ -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 - ); + 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. diff --git a/src/testcases/org/apache/poi/hssf/record/crypto/AllHSSFEncryptionTests.java b/src/testcases/org/apache/poi/hssf/record/crypto/AllHSSFEncryptionTests.java index 4d56858c95..c727008788 100644 --- a/src/testcases/org/apache/poi/hssf/record/crypto/AllHSSFEncryptionTests.java +++ b/src/testcases/org/apache/poi/hssf/record/crypto/AllHSSFEncryptionTests.java @@ -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 org.apache.poi.hssf.record.crypto. * * @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; - } } diff --git a/src/testcases/org/apache/poi/hssf/record/crypto/TestBiff8DecryptingStream.java b/src/testcases/org/apache/poi/hssf/record/crypto/TestBiff8DecryptingStream.java index 00c860ad0e..9d9c04417f 100644 --- a/src/testcases/org/apache/poi/hssf/record/crypto/TestBiff8DecryptingStream.java +++ b/src/testcases/org/apache/poi/hssf/record/crypto/TestBiff8DecryptingStream.java @@ -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 across 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 across and aligned with 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); } diff --git a/src/testcases/org/apache/poi/hssf/record/crypto/TestBiff8EncryptionKey.java b/src/testcases/org/apache/poi/hssf/record/crypto/TestBiff8EncryptionKey.java index 7c6ad42a74..294cb09e7c 100644 --- a/src/testcases/org/apache/poi/hssf/record/crypto/TestBiff8EncryptionKey.java +++ b/src/testcases/org/apache/poi/hssf/record/crypto/TestBiff8EncryptionKey.java @@ -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)); diff --git a/src/testcases/org/apache/poi/hssf/record/crypto/TestRC4.java b/src/testcases/org/apache/poi/hssf/record/crypto/TestRC4.java deleted file mode 100644 index 35b6da4fd7..0000000000 --- a/src/testcases/org/apache/poi/hssf/record/crypto/TestRC4.java +++ /dev/null @@ -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"); - } - } -} diff --git a/src/testcases/org/apache/poi/hssf/record/crypto/TestXorEncryption.java b/src/testcases/org/apache/poi/hssf/record/crypto/TestXorEncryption.java new file mode 100644 index 0000000000..e763a18300 --- /dev/null +++ b/src/testcases/org/apache/poi/hssf/record/crypto/TestXorEncryption.java @@ -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(); + } +} diff --git a/src/testcases/org/apache/poi/hssf/usermodel/TestBugs.java b/src/testcases/org/apache/poi/hssf/usermodel/TestBugs.java index 197531965d..3bd97827cc 100644 --- a/src/testcases/org/apache/poi/hssf/usermodel/TestBugs.java +++ b/src/testcases/org/apache/poi/hssf/usermodel/TestBugs.java @@ -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 -- 2.39.5