From 075e2bfce2f6c881820c7ec2d069d5331a2b6c9b Mon Sep 17 00:00:00 2001 From: Andreas Beeker Date: Mon, 8 Aug 2016 00:10:44 +0000 Subject: [PATCH] HSSF CryptoAPI decryption support git-svn-id: https://svn.apache.org/repos/asf/poi/branches/hssf_cryptoapi@1755461 13f79535-47bb-0310-9956-ffa450edef68 --- .../poi/hssf/record/FilePassRecord.java | 288 +++++------------- .../hssf/record/RecordFactoryInputStream.java | 41 +-- .../poi/hssf/record/RecordInputStream.java | 5 +- .../record/crypto/Biff8DecryptingStream.java | 213 +++++++++---- .../record/crypto/Biff8EncryptionKey.java | 27 +- .../poi/hssf/record/crypto/Biff8RC4.java | 195 ------------ .../poi/hssf/record/crypto/Biff8RC4Key.java | 155 ---------- .../poi/hssf/record/crypto/Biff8XOR.java | 153 ---------- .../poi/hssf/record/crypto/Biff8XORKey.java | 44 --- .../poifs/crypt/ChunkedCipherInputStream.java | 123 ++++++-- .../crypt/ChunkedCipherOutputStream.java | 28 +- .../org/apache/poi/poifs/crypt/Decryptor.java | 3 +- .../poi/poifs/crypt/EncryptionInfo.java | 49 +-- .../poi/poifs/crypt/EncryptionMode.java | 4 +- .../crypt/binaryrc4/BinaryRC4Decryptor.java | 7 +- .../crypt/cryptoapi/CryptoAPIDecryptor.java | 5 +- .../poi/poifs/crypt/xor/XORDecryptor.java | 174 +++++++++++ .../crypt/xor/XOREncryptionHeader.java} | 67 ++-- .../crypt/xor/XOREncryptionInfoBuilder.java | 62 ++++ .../crypt/xor/XOREncryptionVerifier.java | 61 ++++ .../poi/poifs/crypt/xor/XOREncryptor.java | 99 ++++++ .../hslf/record/DocumentEncryptionAtom.java | 4 +- .../poi/hssf/record/AllRecordTests.java | 4 +- .../record/crypto/TestBiff8EncryptionKey.java | 102 ------- .../poi/hssf/usermodel/TestCryptoAPI.java | 62 ++++ .../crypt/AllEncryptionTests.java} | 11 +- .../crypt}/TestBiff8DecryptingStream.java | 25 +- .../poi/poifs/crypt/TestCipherAlgorithm.java | 41 ++- .../crypt}/TestXorEncryption.java | 3 +- .../poifs/crypt/binaryrc4/TestBinaryRC4.java | 106 +++++++ 30 files changed, 1054 insertions(+), 1107 deletions(-) delete mode 100644 src/java/org/apache/poi/hssf/record/crypto/Biff8RC4.java delete mode 100644 src/java/org/apache/poi/hssf/record/crypto/Biff8RC4Key.java delete mode 100644 src/java/org/apache/poi/hssf/record/crypto/Biff8XOR.java delete mode 100644 src/java/org/apache/poi/hssf/record/crypto/Biff8XORKey.java create mode 100644 src/java/org/apache/poi/poifs/crypt/xor/XORDecryptor.java rename src/java/org/apache/poi/{hssf/record/crypto/Biff8Cipher.java => poifs/crypt/xor/XOREncryptionHeader.java} (60%) create mode 100644 src/java/org/apache/poi/poifs/crypt/xor/XOREncryptionInfoBuilder.java create mode 100644 src/java/org/apache/poi/poifs/crypt/xor/XOREncryptionVerifier.java create mode 100644 src/java/org/apache/poi/poifs/crypt/xor/XOREncryptor.java delete mode 100644 src/testcases/org/apache/poi/hssf/record/crypto/TestBiff8EncryptionKey.java create mode 100644 src/testcases/org/apache/poi/hssf/usermodel/TestCryptoAPI.java rename src/testcases/org/apache/poi/{hssf/record/crypto/AllHSSFEncryptionTests.java => poifs/crypt/AllEncryptionTests.java} (83%) rename src/testcases/org/apache/poi/{hssf/record/crypto => poifs/crypt}/TestBiff8DecryptingStream.java (94%) rename src/testcases/org/apache/poi/{hssf/record/crypto => poifs/crypt}/TestXorEncryption.java (94%) create mode 100644 src/testcases/org/apache/poi/poifs/crypt/binaryrc4/TestBinaryRC4.java diff --git a/src/java/org/apache/poi/hssf/record/FilePassRecord.java b/src/java/org/apache/poi/hssf/record/FilePassRecord.java index 7c8ae948f9..9f43b94ef9 100644 --- a/src/java/org/apache/poi/hssf/record/FilePassRecord.java +++ b/src/java/org/apache/poi/hssf/record/FilePassRecord.java @@ -17,8 +17,19 @@ package org.apache.poi.hssf.record; +import java.io.IOException; + import org.apache.poi.EncryptedDocumentException; +import org.apache.poi.poifs.crypt.EncryptionInfo; +import org.apache.poi.poifs.crypt.EncryptionMode; +import org.apache.poi.poifs.crypt.binaryrc4.BinaryRC4EncryptionHeader; +import org.apache.poi.poifs.crypt.binaryrc4.BinaryRC4EncryptionVerifier; +import org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIEncryptionHeader; +import org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIEncryptionVerifier; +import org.apache.poi.poifs.crypt.xor.XOREncryptionHeader; +import org.apache.poi.poifs.crypt.xor.XOREncryptionVerifier; import org.apache.poi.util.HexDump; +import org.apache.poi.util.LittleEndianByteArrayOutputStream; import org.apache.poi.util.LittleEndianOutput; /** @@ -31,228 +42,82 @@ public final class FilePassRecord extends StandardRecord implements Cloneable { private static final int ENCRYPTION_XOR = 0; private static final int ENCRYPTION_OTHER = 1; - private int _encryptionType; - private KeyData _keyData; - - private static interface KeyData extends Cloneable { - void read(RecordInputStream in); - void serialize(LittleEndianOutput out); - int getDataSize(); - void appendToString(StringBuffer buffer); - KeyData clone(); // NOSONAR - } - - public static final class Rc4KeyData implements KeyData, Cloneable { - private static final int ENCRYPTION_OTHER_RC4 = 1; - private static final int ENCRYPTION_OTHER_CAPI_2 = 2; - private static final int ENCRYPTION_OTHER_CAPI_3 = 3; - private static final int ENCRYPTION_OTHER_CAPI_4 = 4; - - private byte[] _salt; - private byte[] _encryptedVerifier; - private byte[] _encryptedVerifierHash; - private int _encryptionInfo; - private int _minorVersionNo; - - public void read(RecordInputStream in) { - _encryptionInfo = in.readUShort(); - switch (_encryptionInfo) { - case ENCRYPTION_OTHER_RC4: - // handled below - break; - case ENCRYPTION_OTHER_CAPI_2: - case ENCRYPTION_OTHER_CAPI_3: - case ENCRYPTION_OTHER_CAPI_4: - throw new EncryptedDocumentException( - "HSSF does not currently support CryptoAPI encryption"); - default: - throw new RecordFormatException("Unknown encryption info " + _encryptionInfo); - } - _minorVersionNo = in.readUShort(); - if (_minorVersionNo!=1) { - throw new RecordFormatException("Unexpected VersionInfo number for RC4Header " + _minorVersionNo); - } - _salt = FilePassRecord.read(in, 16); - _encryptedVerifier = FilePassRecord.read(in, 16); - _encryptedVerifierHash = FilePassRecord.read(in, 16); - } - - public void serialize(LittleEndianOutput out) { - out.writeShort(_encryptionInfo); - out.writeShort(_minorVersionNo); - out.write(_salt); - out.write(_encryptedVerifier); - out.write(_encryptedVerifierHash); - } - - public int getDataSize() { - return 54; - } - - public byte[] getSalt() { - return _salt.clone(); - } - - public void setSalt(byte[] salt) { - this._salt = salt.clone(); - } - - public byte[] getEncryptedVerifier() { - return _encryptedVerifier.clone(); - } - - public void setEncryptedVerifier(byte[] encryptedVerifier) { - this._encryptedVerifier = encryptedVerifier.clone(); - } - - public byte[] getEncryptedVerifierHash() { - return _encryptedVerifierHash.clone(); - } - - public void setEncryptedVerifierHash(byte[] encryptedVerifierHash) { - this._encryptedVerifierHash = encryptedVerifierHash.clone(); - } - - public void appendToString(StringBuffer buffer) { - buffer.append(" .rc4.info = ").append(HexDump.shortToHex(_encryptionInfo)).append("\n"); - buffer.append(" .rc4.ver = ").append(HexDump.shortToHex(_minorVersionNo)).append("\n"); - buffer.append(" .rc4.salt = ").append(HexDump.toHex(_salt)).append("\n"); - buffer.append(" .rc4.verifier = ").append(HexDump.toHex(_encryptedVerifier)).append("\n"); - buffer.append(" .rc4.verifierHash = ").append(HexDump.toHex(_encryptedVerifierHash)).append("\n"); - } - - @Override - public Rc4KeyData clone() { - Rc4KeyData other = new Rc4KeyData(); - other._salt = this._salt.clone(); - other._encryptedVerifier = this._encryptedVerifier.clone(); - other._encryptedVerifierHash = this._encryptedVerifierHash.clone(); - other._encryptionInfo = this._encryptionInfo; - other._minorVersionNo = this._minorVersionNo; - return other; - } - } - - public static final class XorKeyData implements KeyData, Cloneable { - /** - * key (2 bytes): An unsigned integer that specifies the obfuscation key. - * See [MS-OFFCRYPTO], 2.3.6.2 section, the first step of initializing XOR - * array where it describes the generation of 16-bit XorKey value. - */ - private int _key; - - /** - * verificationBytes (2 bytes): An unsigned integer that specifies - * the password verification identifier. - */ - private int _verifier; - - public void read(RecordInputStream in) { - _key = in.readUShort(); - _verifier = in.readUShort(); - } - - public void serialize(LittleEndianOutput out) { - out.writeShort(_key); - out.writeShort(_verifier); - } - - public int getDataSize() { - // TODO: Check! - return 6; - } - - public int getKey() { - return _key; - } - - public int getVerifier() { - return _verifier; - } - - public void setKey(int key) { - this._key = key; - } - - public void setVerifier(int verifier) { - this._verifier = verifier; - } - - public void appendToString(StringBuffer buffer) { - buffer.append(" .xor.key = ").append(HexDump.intToHex(_key)).append("\n"); - buffer.append(" .xor.verifier = ").append(HexDump.intToHex(_verifier)).append("\n"); - } - - @Override - public XorKeyData clone() { - XorKeyData other = new XorKeyData(); - other._key = this._key; - other._verifier = this._verifier; - return other; - } - } - + private int encryptionType; + private EncryptionInfo encryptionInfo; + private int dataLength; private FilePassRecord(FilePassRecord other) { - _encryptionType = other._encryptionType; - _keyData = other._keyData.clone(); + dataLength = other.dataLength; + encryptionType = other.encryptionType; + try { + encryptionInfo = other.encryptionInfo.clone(); + } catch (CloneNotSupportedException e) { + throw new EncryptedDocumentException(e); + } } public FilePassRecord(RecordInputStream in) { - _encryptionType = in.readUShort(); - - switch (_encryptionType) { - case ENCRYPTION_XOR: - _keyData = new XorKeyData(); - break; - case ENCRYPTION_OTHER: - _keyData = new Rc4KeyData(); - break; - default: - throw new RecordFormatException("Unknown encryption type " + _encryptionType); - } - - _keyData.read(in); - } - - private static byte[] read(RecordInputStream in, int size) { - byte[] result = new byte[size]; - in.readFully(result); - return result; + dataLength = in.remaining(); + encryptionType = in.readUShort(); + + EncryptionMode preferredMode; + switch (encryptionType) { + case ENCRYPTION_XOR: + preferredMode = EncryptionMode.xor; + break; + case ENCRYPTION_OTHER: + preferredMode = EncryptionMode.cryptoAPI; + break; + default: + throw new EncryptedDocumentException("invalid encryption type"); + } + + try { + encryptionInfo = new EncryptionInfo(in, preferredMode); + } catch (IOException e) { + throw new EncryptedDocumentException(e); + } } public void serialize(LittleEndianOutput out) { - out.writeShort(_encryptionType); - assert(_keyData != null); - _keyData.serialize(out); + out.writeShort(encryptionType); + + byte data[] = new byte[1024]; + LittleEndianByteArrayOutputStream bos = new LittleEndianByteArrayOutputStream(data, 0); + + switch (encryptionInfo.getEncryptionMode()) { + case xor: + ((XOREncryptionHeader)encryptionInfo.getHeader()).write(bos); + ((XOREncryptionVerifier)encryptionInfo.getVerifier()).write(bos); + break; + case binaryRC4: + out.writeShort(encryptionInfo.getVersionMajor()); + out.writeShort(encryptionInfo.getVersionMinor()); + ((BinaryRC4EncryptionHeader)encryptionInfo.getHeader()).write(bos); + ((BinaryRC4EncryptionVerifier)encryptionInfo.getVerifier()).write(bos); + break; + case cryptoAPI: + out.writeShort(encryptionInfo.getVersionMajor()); + out.writeShort(encryptionInfo.getVersionMinor()); + ((CryptoAPIEncryptionHeader)encryptionInfo.getHeader()).write(bos); + ((CryptoAPIEncryptionVerifier)encryptionInfo.getVerifier()).write(bos); + break; + default: + throw new RuntimeException("not supported"); + } + + out.write(data, 0, bos.getWriteIndex()); } protected int getDataSize() { - assert(_keyData != null); - return _keyData.getDataSize(); + return dataLength; } - public Rc4KeyData getRc4KeyData() { - return (_keyData instanceof Rc4KeyData) - ? (Rc4KeyData) _keyData - : null; - } - - public XorKeyData getXorKeyData() { - return (_keyData instanceof XorKeyData) - ? (XorKeyData) _keyData - : null; - } - - private Rc4KeyData checkRc4() { - Rc4KeyData rc4 = getRc4KeyData(); - if (rc4 == null) { - throw new RecordFormatException("file pass record doesn't contain a rc4 key."); - } - return rc4; + public EncryptionInfo getEncryptionInfo() { + return encryptionInfo; } - public short getSid() { + public short getSid() { return sid; } @@ -265,8 +130,13 @@ public final class FilePassRecord extends StandardRecord implements Cloneable { StringBuffer buffer = new StringBuffer(); buffer.append("[FILEPASS]\n"); - buffer.append(" .type = ").append(HexDump.shortToHex(_encryptionType)).append("\n"); - _keyData.appendToString(buffer); + buffer.append(" .type = ").append(HexDump.shortToHex(encryptionType)).append("\n"); + String prefix = " ."+encryptionInfo.getEncryptionMode(); + buffer.append(prefix+".info = ").append(HexDump.shortToHex(encryptionInfo.getVersionMajor())).append("\n"); + buffer.append(prefix+".ver = ").append(HexDump.shortToHex(encryptionInfo.getVersionMinor())).append("\n"); + buffer.append(prefix+".salt = ").append(HexDump.toHex(encryptionInfo.getVerifier().getSalt())).append("\n"); + buffer.append(prefix+".verifier = ").append(HexDump.toHex(encryptionInfo.getVerifier().getEncryptedVerifier())).append("\n"); + buffer.append(prefix+".verifierHash = ").append(HexDump.toHex(encryptionInfo.getVerifier().getEncryptedVerifierHash())).append("\n"); buffer.append("[/FILEPASS]\n"); return buffer.toString(); } diff --git a/src/java/org/apache/poi/hssf/record/RecordFactoryInputStream.java b/src/java/org/apache/poi/hssf/record/RecordFactoryInputStream.java index c7480147b3..6338f9a691 100644 --- a/src/java/org/apache/poi/hssf/record/RecordFactoryInputStream.java +++ b/src/java/org/apache/poi/hssf/record/RecordFactoryInputStream.java @@ -17,18 +17,16 @@ package org.apache.poi.hssf.record; import java.io.InputStream; +import java.security.GeneralSecurityException; import java.util.ArrayList; import java.util.List; import org.apache.poi.EncryptedDocumentException; import org.apache.poi.hssf.eventusermodel.HSSFEventFactory; import org.apache.poi.hssf.eventusermodel.HSSFListener; -import org.apache.poi.hssf.record.FilePassRecord.Rc4KeyData; -import org.apache.poi.hssf.record.FilePassRecord.XorKeyData; import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey; -import org.apache.poi.hssf.record.crypto.Biff8RC4Key; -import org.apache.poi.hssf.record.crypto.Biff8XORKey; import org.apache.poi.poifs.crypt.Decryptor; +import org.apache.poi.poifs.crypt.EncryptionInfo; /** * A stream based way to get at complete records, with @@ -114,31 +112,18 @@ public final class RecordFactoryInputStream { userPassword = Decryptor.DEFAULT_PASSWORD; } - Biff8EncryptionKey key; - if (fpr.getRc4KeyData() != null) { - Rc4KeyData rc4 = fpr.getRc4KeyData(); - Biff8RC4Key rc4key = Biff8RC4Key.create(userPassword, rc4.getSalt()); - key = rc4key; - if (!rc4key.validate(rc4.getEncryptedVerifier(), rc4.getEncryptedVerifierHash())) { - throw new EncryptedDocumentException( - (Decryptor.DEFAULT_PASSWORD.equals(userPassword) ? "Default" : "Supplied") - + " password is invalid for salt/verifier/verifierHash"); - } - } else if (fpr.getXorKeyData() != null) { - XorKeyData xor = fpr.getXorKeyData(); - Biff8XORKey xorKey = Biff8XORKey.create(userPassword, xor.getKey()); - key = xorKey; - - if (!xorKey.validate(userPassword, xor.getVerifier())) { + EncryptionInfo info = fpr.getEncryptionInfo(); + try { + if (!info.getDecryptor().verifyPassword(userPassword)) { throw new EncryptedDocumentException( - (Decryptor.DEFAULT_PASSWORD.equals(userPassword) ? "Default" : "Supplied") - + " password is invalid for key/verifier"); - } - } else { - throw new EncryptedDocumentException("Crypto API not yet supported."); - } - - return new RecordInputStream(original, key, _initialRecordsSize); + (Decryptor.DEFAULT_PASSWORD.equals(userPassword) ? "Default" : "Supplied") + + " password is invalid for salt/verifier/verifierHash"); + } + } catch (GeneralSecurityException e) { + throw new EncryptedDocumentException(e); + } + + return new RecordInputStream(original, info, _initialRecordsSize); } public boolean hasEncryption() { diff --git a/src/java/org/apache/poi/hssf/record/RecordInputStream.java b/src/java/org/apache/poi/hssf/record/RecordInputStream.java index 8991eb7705..929f0f2bfe 100644 --- a/src/java/org/apache/poi/hssf/record/RecordInputStream.java +++ b/src/java/org/apache/poi/hssf/record/RecordInputStream.java @@ -25,6 +25,7 @@ import java.util.Locale; import org.apache.poi.hssf.dev.BiffViewer; import org.apache.poi.hssf.record.crypto.Biff8DecryptingStream; import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey; +import org.apache.poi.poifs.crypt.EncryptionInfo; import org.apache.poi.util.Internal; import org.apache.poi.util.LittleEndianConsts; import org.apache.poi.util.LittleEndianInput; @@ -33,8 +34,6 @@ import org.apache.poi.util.LittleEndianInputStream; /** * Title: Record Input Stream

* Description: Wraps a stream and provides helper methods for the construction of records.

- * - * @author Jason Height (jheight @ apache dot org) */ public final class RecordInputStream implements LittleEndianInput { /** Maximum size of a single record (minus the 4 byte header) without a continue*/ @@ -122,7 +121,7 @@ public final class RecordInputStream implements LittleEndianInput { this (in, null, 0); } - public RecordInputStream(InputStream in, Biff8EncryptionKey key, int initialOffset) throws RecordFormatException { + public RecordInputStream(InputStream in, EncryptionInfo key, int initialOffset) throws RecordFormatException { if (key == null) { _dataInput = getLEI(in); _bhi = new SimpleHeaderInput(in); 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 564174b609..ef574beea1 100644 --- a/src/java/org/apache/poi/hssf/record/crypto/Biff8DecryptingStream.java +++ b/src/java/org/apache/poi/hssf/record/crypto/Biff8DecryptingStream.java @@ -17,103 +17,202 @@ package org.apache.poi.hssf.record.crypto; +import java.io.IOException; import java.io.InputStream; +import java.io.PushbackInputStream; -import org.apache.poi.EncryptedDocumentException; +import org.apache.poi.hssf.record.BOFRecord; import org.apache.poi.hssf.record.BiffHeaderInput; +import org.apache.poi.hssf.record.FilePassRecord; +import org.apache.poi.hssf.record.InterfaceHdrRecord; +import org.apache.poi.hssf.record.RecordFormatException; +import org.apache.poi.poifs.crypt.ChunkedCipherInputStream; +import org.apache.poi.poifs.crypt.Decryptor; +import org.apache.poi.poifs.crypt.EncryptionInfo; +import org.apache.poi.util.LittleEndian; +import org.apache.poi.util.LittleEndianConsts; import org.apache.poi.util.LittleEndianInput; -import org.apache.poi.util.LittleEndianInputStream; -/** - * - * @author Josh Micich - */ public final class Biff8DecryptingStream implements BiffHeaderInput, LittleEndianInput { - private final LittleEndianInput _le; - private final Biff8Cipher _cipher; - - public Biff8DecryptingStream(InputStream in, int initialOffset, Biff8EncryptionKey key) { - if (key instanceof Biff8RC4Key) { - _cipher = new Biff8RC4(initialOffset, (Biff8RC4Key)key); - } else if (key instanceof Biff8XORKey) { - _cipher = new Biff8XOR(initialOffset, (Biff8XORKey)key); - } else { - throw new EncryptedDocumentException("Crypto API not supported yet."); - } - - if (in instanceof LittleEndianInput) { - // accessing directly is an optimisation - _le = (LittleEndianInput) in; - } else { - // less optimal, but should work OK just the same. Often occurs in junit tests. - _le = new LittleEndianInputStream(in); - } + private static final int RC4_REKEYING_INTERVAL = 1024; + + private final EncryptionInfo info; + private ChunkedCipherInputStream ccis; + private final byte buffer[] = new byte[LittleEndianConsts.LONG_SIZE]; + private boolean shouldSkipEncryptionOnCurrentRecord = false; + + public Biff8DecryptingStream(InputStream in, int initialOffset, EncryptionInfo info) throws RecordFormatException { + try { + byte initialBuf[] = new byte[initialOffset]; + InputStream stream; + if (initialOffset == 0) { + stream = in; + } else { + stream = new PushbackInputStream(in, initialOffset); + ((PushbackInputStream)stream).unread(initialBuf); + } + + this.info = info; + Decryptor dec = this.info.getDecryptor(); + dec.setChunkSize(RC4_REKEYING_INTERVAL); + ccis = (ChunkedCipherInputStream)dec.getDataStream(stream, Integer.MAX_VALUE, 0); + + if (initialOffset > 0) { + ccis.readFully(initialBuf); + } + } catch (Exception e) { + throw new RecordFormatException(e); + } } - public int available() { - return _le.available(); + @Override + public int available() { + return ccis.available(); } /** * Reads an unsigned short value without decrypting */ - public int readRecordSID() { - int sid = _le.readUShort(); - _cipher.skipTwoBytes(); - _cipher.startRecord(sid); + @Override + public int readRecordSID() { + readPlain(buffer, 0, LittleEndianConsts.SHORT_SIZE); + int sid = LittleEndian.getUShort(buffer, 0); + shouldSkipEncryptionOnCurrentRecord = isNeverEncryptedRecord(sid); return sid; } /** * Reads an unsigned short value without decrypting */ - public int readDataSize() { - int dataSize = _le.readUShort(); - _cipher.skipTwoBytes(); - _cipher.setNextRecordSize(dataSize); + @Override + public int readDataSize() { + readPlain(buffer, 0, LittleEndianConsts.SHORT_SIZE); + int dataSize = LittleEndian.getUShort(buffer, 0); + ccis.setNextRecordSize(dataSize); return dataSize; } - public double readDouble() { - long valueLongBits = readLong(); + @Override + public double readDouble() { + long valueLongBits = readLong(); double result = Double.longBitsToDouble(valueLongBits); if (Double.isNaN(result)) { - throw new RuntimeException("Did not expect to read NaN"); // (Because Excel typically doesn't write NaN + // (Because Excel typically doesn't write NaN + throw new RuntimeException("Did not expect to read NaN"); } return result; } - public void readFully(byte[] buf) { - readFully(buf, 0, buf.length); + @Override + public void readFully(byte[] buf) { + readFully(buf, 0, buf.length); } - public void readFully(byte[] buf, int off, int len) { - _le.readFully(buf, off, len); - _cipher.xor(buf, off, len); + @Override + public void readFully(byte[] buf, int off, int len) { + if (shouldSkipEncryptionOnCurrentRecord) { + readPlain(buf, off, buf.length); + } else { + ccis.readFully(buf, off, len); + } } - - public int readUByte() { - return readByte() & 0xFF; + @Override + public int readUByte() { + return readByte() & 0xFF; } - public byte readByte() { - return (byte) _cipher.xorByte(_le.readUByte()); + + @Override + public byte readByte() { + if (shouldSkipEncryptionOnCurrentRecord) { + readPlain(buffer, 0, LittleEndianConsts.BYTE_SIZE); + return buffer[0]; + } else { + return ccis.readByte(); + } } - - public int readUShort() { - return readShort() & 0xFFFF; + @Override + public int readUShort() { + return readShort() & 0xFFFF; + } + + @Override + public short readShort() { + if (shouldSkipEncryptionOnCurrentRecord) { + readPlain(buffer, 0, LittleEndianConsts.SHORT_SIZE); + return LittleEndian.getShort(buffer); + } else { + return ccis.readShort(); + } } - public short readShort() { - return (short) _cipher.xorShort(_le.readUShort()); + + @Override + public int readInt() { + if (shouldSkipEncryptionOnCurrentRecord) { + readPlain(buffer, 0, LittleEndianConsts.INT_SIZE); + return LittleEndian.getInt(buffer); + } else { + return ccis.readInt(); + } } - public int readInt() { - return _cipher.xorInt(_le.readInt()); + @Override + public long readLong() { + if (shouldSkipEncryptionOnCurrentRecord) { + readPlain(buffer, 0, LittleEndianConsts.LONG_SIZE); + return LittleEndian.getLong(buffer); + } else { + return ccis.readLong(); + } } - public long readLong() { - return _cipher.xorLong(_le.readLong()); + /** + * @return the absolute position in the stream + */ + public long getPosition() { + return ccis.getPos(); } + + /** + * TODO: Additionally, the lbPlyPos (position_of_BOF) field of the BoundSheet8 record MUST NOT be encrypted. + * + * @return true 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; + + default: + return false; + } + } + + private void readPlain(byte b[], int off, int len) { + try { + int readBytes = ccis.readPlain(b, off, len); + if (readBytes < len) { + throw new RecordFormatException("buffer underrun"); + } + } catch (IOException e) { + throw new RecordFormatException(e); + } + } + } 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 3a28b81af1..382ae2f130 100644 --- a/src/java/org/apache/poi/hssf/record/crypto/Biff8EncryptionKey.java +++ b/src/java/org/apache/poi/hssf/record/crypto/Biff8EncryptionKey.java @@ -16,34 +16,9 @@ ==================================================================== */ package org.apache.poi.hssf.record.crypto; -import javax.crypto.SecretKey; - -import org.apache.poi.EncryptedDocumentException; import org.apache.poi.hssf.usermodel.HSSFWorkbook; -import org.apache.poi.poifs.crypt.Decryptor; - -public abstract class Biff8EncryptionKey { - protected SecretKey _secretKey; - - /** - * Create using the default password and a specified docId - * @param salt 16 bytes - */ - public static Biff8EncryptionKey create(byte[] salt) { - return Biff8RC4Key.create(Decryptor.DEFAULT_PASSWORD, salt); - } - - public static Biff8EncryptionKey create(String password, byte[] salt) { - return Biff8RC4Key.create(password, salt); - } - - /** - * @return true if the keyDigest is compatible with the specified saltData and saltHash - */ - public boolean validate(byte[] saltData, byte[] saltHash) { - throw new EncryptedDocumentException("validate is not supported (in super-class)."); - } +public final class Biff8EncryptionKey { /** * Stores the BIFF8 encryption/decryption password for the current thread. This has been done * using a {@link ThreadLocal} in order to avoid further overloading the various public APIs diff --git a/src/java/org/apache/poi/hssf/record/crypto/Biff8RC4.java b/src/java/org/apache/poi/hssf/record/crypto/Biff8RC4.java deleted file mode 100644 index 9d0275fec3..0000000000 --- a/src/java/org/apache/poi/hssf/record/crypto/Biff8RC4.java +++ /dev/null @@ -1,195 +0,0 @@ -/* ==================================================================== - Licensed to the Apache Software Foundation (ASF) under one or more - contributor license agreements. See the NOTICE file distributed with - this work for additional information regarding copyright ownership. - The ASF licenses this file to You under the Apache License, Version 2.0 - (the "License"); you may not use this file except in compliance with - the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -==================================================================== */ - -package org.apache.poi.hssf.record.crypto; - -import java.nio.ByteBuffer; -import java.nio.ByteOrder; - -import javax.crypto.Cipher; -import javax.crypto.ShortBufferException; - -import org.apache.poi.EncryptedDocumentException; -import org.apache.poi.hssf.record.BOFRecord; -import org.apache.poi.hssf.record.FilePassRecord; -import org.apache.poi.hssf.record.InterfaceHdrRecord; - -/** - * Used for both encrypting and decrypting BIFF8 streams. The internal - * {@link Cipher} instance is renewed (re-keyed) every 1024 bytes. - */ -final class Biff8RC4 implements Biff8Cipher { - - private static final int RC4_REKEYING_INTERVAL = 1024; - - private Cipher _rc4; - - /** - * This field is used to keep track of when to change the {@link Cipher} - * instance. The change occurs every 1024 bytes. Every byte passed over is - * counted. - */ - private int _streamPos; - private int _nextRC4BlockStart; - private int _currentKeyIndex; - private boolean _shouldSkipEncryptionOnCurrentRecord; - private final Biff8RC4Key _key; - private ByteBuffer _buffer = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN); - - public Biff8RC4(int initialOffset, Biff8RC4Key key) { - if (initialOffset >= RC4_REKEYING_INTERVAL) { - throw new RuntimeException("initialOffset (" + initialOffset + ")>" - + RC4_REKEYING_INTERVAL + " not supported yet"); - } - _key = key; - _rc4 = _key.getCipher(); - _streamPos = 0; - rekeyForNextBlock(); - _streamPos = initialOffset; - _shouldSkipEncryptionOnCurrentRecord = false; - - encryptBytes(new byte[initialOffset], 0, initialOffset); - } - - - private void rekeyForNextBlock() { - _currentKeyIndex = _streamPos / RC4_REKEYING_INTERVAL; - _key.initCipherForBlock(_rc4, _currentKeyIndex); - _nextRC4BlockStart = (_currentKeyIndex + 1) * RC4_REKEYING_INTERVAL; - } - - private void encryptBytes(byte data[], int offset, final int bytesToRead) { - if (bytesToRead == 0) return; - - if (_shouldSkipEncryptionOnCurrentRecord) { - // even when encryption is skipped, we need to update the cipher - byte dataCpy[] = new byte[bytesToRead]; - System.arraycopy(data, offset, dataCpy, 0, bytesToRead); - data = dataCpy; - offset = 0; - } - - try { - _rc4.update(data, offset, bytesToRead, data, offset); - } catch (ShortBufferException e) { - throw new EncryptedDocumentException("input buffer too small", e); - } - } - - public void startRecord(int currentSid) { - _shouldSkipEncryptionOnCurrentRecord = isNeverEncryptedRecord(currentSid); - } - - /** - * TODO: Additionally, the lbPlyPos (position_of_BOF) field of the BoundSheet8 record MUST NOT be encrypted. - * - * @return true 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() { - xor(_buffer.array(), 0, 2); - } - - public void xor(byte[] buf, int pOffset, int pLen) { - int nLeftInBlock; - nLeftInBlock = _nextRC4BlockStart - _streamPos; - if (pLen <= nLeftInBlock) { - // simple case - this read does not cross key blocks - encryptBytes(buf, pOffset, pLen); - _streamPos += pLen; - return; - } - - int offset = pOffset; - int len = pLen; - - // start by using the rest of the current block - if (len > nLeftInBlock) { - if (nLeftInBlock > 0) { - encryptBytes(buf, offset, nLeftInBlock); - _streamPos += nLeftInBlock; - offset += nLeftInBlock; - len -= nLeftInBlock; - } - rekeyForNextBlock(); - } - // all full blocks following - while (len > RC4_REKEYING_INTERVAL) { - encryptBytes(buf, offset, RC4_REKEYING_INTERVAL); - _streamPos += RC4_REKEYING_INTERVAL; - offset += RC4_REKEYING_INTERVAL; - len -= RC4_REKEYING_INTERVAL; - rekeyForNextBlock(); - } - // finish with incomplete block - encryptBytes(buf, offset, len); - _streamPos += len; - } - - public int xorByte(int rawVal) { - _buffer.put(0, (byte)rawVal); - xor(_buffer.array(), 0, 1); - return _buffer.get(0); - } - - public int xorShort(int rawVal) { - _buffer.putShort(0, (short)rawVal); - xor(_buffer.array(), 0, 2); - return _buffer.getShort(0); - } - - public int xorInt(int rawVal) { - _buffer.putInt(0, rawVal); - xor(_buffer.array(), 0, 4); - return _buffer.getInt(0); - } - - public long xorLong(long rawVal) { - _buffer.putLong(0, rawVal); - xor(_buffer.array(), 0, 8); - return _buffer.getLong(0); - } - - public void setNextRecordSize(int recordSize) { - /* no-op */ - } -} diff --git a/src/java/org/apache/poi/hssf/record/crypto/Biff8RC4Key.java b/src/java/org/apache/poi/hssf/record/crypto/Biff8RC4Key.java deleted file mode 100644 index e75f297b03..0000000000 --- a/src/java/org/apache/poi/hssf/record/crypto/Biff8RC4Key.java +++ /dev/null @@ -1,155 +0,0 @@ -/* ==================================================================== - Licensed to the Apache Software Foundation (ASF) under one or more - contributor license agreements. See the NOTICE file distributed with - this work for additional information regarding copyright ownership. - The ASF licenses this file to You under the Apache License, Version 2.0 - (the "License"); you may not use this file except in compliance with - the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -==================================================================== */ - -package org.apache.poi.hssf.record.crypto; - -import java.security.GeneralSecurityException; -import java.security.MessageDigest; -import java.util.Arrays; - -import javax.crypto.Cipher; -import javax.crypto.ShortBufferException; -import javax.crypto.spec.SecretKeySpec; - -import org.apache.poi.EncryptedDocumentException; -import org.apache.poi.poifs.crypt.CipherAlgorithm; -import org.apache.poi.poifs.crypt.CryptoFunctions; -import org.apache.poi.poifs.crypt.HashAlgorithm; -import org.apache.poi.util.HexDump; -import org.apache.poi.util.LittleEndian; -import org.apache.poi.util.LittleEndianConsts; -import org.apache.poi.util.POILogFactory; -import org.apache.poi.util.POILogger; - -public class Biff8RC4Key extends Biff8EncryptionKey { - // these two constants coincidentally have the same value - public static final int KEY_DIGEST_LENGTH = 5; - private static final int PASSWORD_HASH_NUMBER_OF_BYTES_USED = 5; - - private static POILogger log = POILogFactory.getLogger(Biff8RC4Key.class); - - Biff8RC4Key(byte[] keyDigest) { - if (keyDigest.length != KEY_DIGEST_LENGTH) { - throw new IllegalArgumentException("Expected 5 byte key digest, but got " + HexDump.toHex(keyDigest)); - } - - CipherAlgorithm ca = CipherAlgorithm.rc4; - _secretKey = new SecretKeySpec(keyDigest.clone(), ca.jceId); - } - - /** - * Create using the default password and a specified docId - * @param salt 16 bytes - */ - public static Biff8RC4Key create(String password, byte[] salt) { - return new Biff8RC4Key(createKeyDigest(password, salt)); - } - - /** - * @return 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 deleted file mode 100644 index 7f2903dd55..0000000000 --- a/src/java/org/apache/poi/hssf/record/crypto/Biff8XORKey.java +++ /dev/null @@ -1,44 +0,0 @@ -/* ==================================================================== - Licensed to the Apache Software Foundation (ASF) under one or more - contributor license agreements. See the NOTICE file distributed with - this work for additional information regarding copyright ownership. - The ASF licenses this file to You under the Apache License, Version 2.0 - (the "License"); you may not use this file except in compliance with - the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -==================================================================== */ - -package org.apache.poi.hssf.record.crypto; - -import javax.crypto.spec.SecretKeySpec; - -import org.apache.poi.poifs.crypt.CryptoFunctions; - - -public class Biff8XORKey extends Biff8EncryptionKey { - final int _xorKey; - - public Biff8XORKey(String password, int xorKey) { - _xorKey = xorKey; - byte xorArray[] = CryptoFunctions.createXorArray1(password); - _secretKey = new SecretKeySpec(xorArray, "XOR"); - } - - public static Biff8XORKey create(String password, int xorKey) { - return new Biff8XORKey(password, xorKey); - } - - public boolean validate(String password, int verifier) { - int keyComp = CryptoFunctions.createXorKey1(password); - int verifierComp = CryptoFunctions.createXorVerifier1(password); - - return (_xorKey == keyComp && verifierComp == verifier); - } -} diff --git a/src/java/org/apache/poi/poifs/crypt/ChunkedCipherInputStream.java b/src/java/org/apache/poi/poifs/crypt/ChunkedCipherInputStream.java index 255494d6ab..7b5632dea7 100644 --- a/src/java/org/apache/poi/poifs/crypt/ChunkedCipherInputStream.java +++ b/src/java/org/apache/poi/poifs/crypt/ChunkedCipherInputStream.java @@ -21,56 +21,56 @@ import java.io.IOException; import java.io.InputStream; import java.security.GeneralSecurityException; +import javax.crypto.BadPaddingException; import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.ShortBufferException; import org.apache.poi.EncryptedDocumentException; import org.apache.poi.util.Internal; -import org.apache.poi.util.LittleEndianInput; import org.apache.poi.util.LittleEndianInputStream; @Internal public abstract class ChunkedCipherInputStream extends LittleEndianInputStream { private final int _chunkSize; private final int _chunkBits; - + private final long _size; - private final byte[] _chunk; + private final byte[] _chunk, _plain; private final Cipher _cipher; private int _lastIndex; private long _pos; private boolean _chunkIsValid = false; - public ChunkedCipherInputStream(LittleEndianInput stream, long size, int chunkSize) + public ChunkedCipherInputStream(InputStream stream, long size, int chunkSize) throws GeneralSecurityException { this(stream, size, chunkSize, 0); } - public ChunkedCipherInputStream(LittleEndianInput stream, long size, int chunkSize, int initialPos) + public ChunkedCipherInputStream(InputStream stream, long size, int chunkSize, int initialPos) throws GeneralSecurityException { - super((InputStream)stream); + super(stream); _size = size; _pos = initialPos; this._chunkSize = chunkSize; - if (chunkSize == -1) { - _chunk = new byte[4096]; - } else { - _chunk = new byte[chunkSize]; - } + int cs = chunkSize == -1 ? 4096 : chunkSize; + _chunk = new byte[cs]; + _plain = new byte[cs]; _chunkBits = Integer.bitCount(_chunk.length-1); _lastIndex = (int)(_pos >> _chunkBits); _cipher = initCipherForBlock(null, _lastIndex); } - + public final Cipher initCipherForBlock(int block) throws IOException, GeneralSecurityException { if (_chunkSize != -1) { throw new GeneralSecurityException("the cipher block can only be set for streaming encryption, e.g. CryptoAPI..."); } - + _chunkIsValid = false; return initCipherForBlock(_cipher, block); } - + protected abstract Cipher initCipherForBlock(Cipher existing, int block) throws GeneralSecurityException; @@ -88,8 +88,12 @@ public abstract class ChunkedCipherInputStream extends LittleEndianInputStream { @Override public int read(byte[] b, int off, int len) throws IOException { + return read(b, off, len, false); + } + + private int read(byte[] b, int off, int len, boolean readPlain) throws IOException { int total = 0; - + if (available() <= 0) { return -1; } @@ -110,7 +114,9 @@ public abstract class ChunkedCipherInputStream extends LittleEndianInputStream { return total; } count = Math.min(avail, Math.min(count, len)); - System.arraycopy(_chunk, (int)(_pos & chunkMask), b, off, count); + + System.arraycopy(readPlain ? _plain : _chunk, (int)(_pos & chunkMask), b, off, count); + off += count; len -= count; _pos += count; @@ -139,7 +145,7 @@ public abstract class ChunkedCipherInputStream extends LittleEndianInputStream { public int available() { return remainingBytes(); } - + /** * Helper method for forbidden available call - we know the size beforehand, so it's ok ... * @@ -148,7 +154,7 @@ public abstract class ChunkedCipherInputStream extends LittleEndianInputStream { private int remainingBytes() { return (int)(_size - _pos); } - + @Override public boolean markSupported() { return false; @@ -158,21 +164,21 @@ public abstract class ChunkedCipherInputStream extends LittleEndianInputStream { public synchronized void mark(int readlimit) { throw new UnsupportedOperationException(); } - + @Override public synchronized void reset() throws IOException { throw new UnsupportedOperationException(); } - private int getChunkMask() { + protected int getChunkMask() { return _chunk.length-1; } - + private void nextChunk() throws GeneralSecurityException, IOException { if (_chunkSize != -1) { int index = (int)(_pos >> _chunkBits); initCipherForBlock(_cipher, index); - + if (_lastIndex != index) { super.skip((index - _lastIndex) << _chunkBits); } @@ -183,18 +189,81 @@ public abstract class ChunkedCipherInputStream extends LittleEndianInputStream { final int todo = (int)Math.min(_size, _chunk.length); int readBytes = 0, totalBytes = 0; do { - readBytes = super.read(_chunk, totalBytes, todo-totalBytes); + readBytes = super.read(_plain, totalBytes, todo-totalBytes); totalBytes += Math.max(0, readBytes); } while (readBytes != -1 && totalBytes < todo); - if (readBytes == -1 && _pos+totalBytes < _size) { + if (readBytes == -1 && _pos+totalBytes < _size && _size < Integer.MAX_VALUE) { throw new EOFException("buffer underrun"); } - if (_chunkSize == -1) { - _cipher.update(_chunk, 0, totalBytes, _chunk); + System.arraycopy(_plain, 0, _chunk, 0, totalBytes); + + invokeCipher(totalBytes, _chunkSize > -1); + } + + /** + * Helper function for overriding the cipher invocation, i.e. XOR doesn't use a cipher + * and uses it's own implementation + * + * @return + * @throws BadPaddingException + * @throws IllegalBlockSizeException + * @throws ShortBufferException + */ + protected int invokeCipher(int totalBytes, boolean doFinal) throws GeneralSecurityException { + if (doFinal) { + return _cipher.doFinal(_chunk, 0, totalBytes, _chunk); } else { - _cipher.doFinal(_chunk, 0, totalBytes, _chunk); + return _cipher.update(_chunk, 0, totalBytes, _chunk); + } + } + + /** + * Used when BIFF header fields (sid, size) are being read. The internal + * {@link Cipher} instance must step even when unencrypted bytes are read + */ + public int readPlain(byte b[], int off, int len) throws IOException { + if (len <= 0) { + return len; } + + int readBytes, total = 0; + do { + readBytes = read(b, off, len, true); + total += Math.max(0, readBytes); + } while (readBytes > -1 && total < len); + + return total; + } + + /** + * Some ciphers (actually just XOR) are based on the record size, + * which needs to be set before encryption + * + * @param recordSize the size of the next record + */ + public void setNextRecordSize(int recordSize) { + } + + /** + * @return the chunk bytes + */ + protected byte[] getChunk() { + return _chunk; + } + + /** + * @return the plain bytes + */ + protected byte[] getPlain() { + return _plain; + } + + /** + * @return the absolute position in the stream + */ + public long getPos() { + return _pos; } } diff --git a/src/java/org/apache/poi/poifs/crypt/ChunkedCipherOutputStream.java b/src/java/org/apache/poi/poifs/crypt/ChunkedCipherOutputStream.java index 573cbdeb60..08ca4c3bee 100644 --- a/src/java/org/apache/poi/poifs/crypt/ChunkedCipherOutputStream.java +++ b/src/java/org/apache/poi/poifs/crypt/ChunkedCipherOutputStream.java @@ -26,7 +26,10 @@ import java.io.IOException; import java.io.OutputStream; import java.security.GeneralSecurityException; +import javax.crypto.BadPaddingException; import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.ShortBufferException; import org.apache.poi.EncryptedDocumentException; import org.apache.poi.poifs.filesystem.DirectoryNode; @@ -153,19 +156,17 @@ public abstract class ChunkedCipherOutputStream extends FilterOutputStream { int ciLen; try { + boolean doFinal = true; if (_chunkSize == STREAMING) { if (continued) { - ciLen = _cipher.update(_chunk, 0, posInChunk, _chunk); - } else { - ciLen = _cipher.doFinal(_chunk, 0, posInChunk, _chunk); + doFinal = false; } - // reset stream (not only) in case we were interrupted by plain stream parts _pos = 0; } else { _cipher = initCipherForBlock(_cipher, index, lastChunk); - ciLen = _cipher.doFinal(_chunk, 0, posInChunk, _chunk); } + ciLen = invokeCipher(posInChunk, doFinal); } catch (GeneralSecurityException e) { throw new IOException("can't re-/initialize cipher", e); } @@ -173,6 +174,23 @@ public abstract class ChunkedCipherOutputStream extends FilterOutputStream { out.write(_chunk, 0, ciLen); } + /** + * Helper function for overriding the cipher invocation, i.e. XOR doesn't use a cipher + * and uses it's own implementation + * + * @return + * @throws BadPaddingException + * @throws IllegalBlockSizeException + * @throws ShortBufferException + */ + protected int invokeCipher(int posInChunk, boolean doFinal) throws GeneralSecurityException { + if (doFinal) { + return _cipher.doFinal(_chunk, 0, posInChunk, _chunk); + } else { + return _cipher.update(_chunk, 0, posInChunk, _chunk); + } + } + @Override public void close() throws IOException { try { diff --git a/src/java/org/apache/poi/poifs/crypt/Decryptor.java b/src/java/org/apache/poi/poifs/crypt/Decryptor.java index 41621853ea..2030029551 100644 --- a/src/java/org/apache/poi/poifs/crypt/Decryptor.java +++ b/src/java/org/apache/poi/poifs/crypt/Decryptor.java @@ -29,7 +29,6 @@ import org.apache.poi.poifs.filesystem.DirectoryNode; import org.apache.poi.poifs.filesystem.NPOIFSFileSystem; import org.apache.poi.poifs.filesystem.OPOIFSFileSystem; import org.apache.poi.poifs.filesystem.POIFSFileSystem; -import org.apache.poi.util.LittleEndianInput; public abstract class Decryptor implements Cloneable { public static final String DEFAULT_PASSWORD="VelvetSweatshop"; @@ -66,7 +65,7 @@ public abstract class Decryptor implements Cloneable { * @param initialPos initial/current byte position within the stream * @return decrypted stream */ - public InputStream getDataStream(LittleEndianInput stream, int size, int initialPos) + public InputStream getDataStream(InputStream stream, int size, int initialPos) throws IOException, GeneralSecurityException { throw new RuntimeException("this decryptor doesn't support reading from a stream"); } diff --git a/src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java b/src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java index 34b83cb8c3..20115f1b48 100644 --- a/src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java +++ b/src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java @@ -20,6 +20,7 @@ import static org.apache.poi.poifs.crypt.EncryptionMode.agile; import static org.apache.poi.poifs.crypt.EncryptionMode.binaryRC4; import static org.apache.poi.poifs.crypt.EncryptionMode.cryptoAPI; import static org.apache.poi.poifs.crypt.EncryptionMode.standard; +import static org.apache.poi.poifs.crypt.EncryptionMode.xor; import java.io.IOException; @@ -35,6 +36,7 @@ import org.apache.poi.util.LittleEndianInput; /** */ public class EncryptionInfo implements Cloneable { + private final EncryptionMode encryptionMode; private final int versionMajor; private final int versionMinor; private final int encryptionFlags; @@ -75,49 +77,55 @@ public class EncryptionInfo implements Cloneable { public EncryptionInfo(POIFSFileSystem fs) throws IOException { this(fs.getRoot()); } + /** * Opens for decryption */ public EncryptionInfo(OPOIFSFileSystem fs) throws IOException { this(fs.getRoot()); } + /** * Opens for decryption */ public EncryptionInfo(NPOIFSFileSystem fs) throws IOException { this(fs.getRoot()); } + /** * Opens for decryption */ public EncryptionInfo(DirectoryNode dir) throws IOException { - this(dir.createDocumentInputStream("EncryptionInfo"), false); + this(dir.createDocumentInputStream("EncryptionInfo"), null); } - public EncryptionInfo(LittleEndianInput dis, boolean isCryptoAPI) throws IOException { - final EncryptionMode encryptionMode; - versionMajor = dis.readUShort(); - versionMinor = dis.readUShort(); + public EncryptionInfo(LittleEndianInput dis, EncryptionMode preferredEncryptionMode) throws IOException { + if (preferredEncryptionMode == xor) { + versionMajor = xor.versionMajor; + versionMinor = xor.versionMinor; + } else { + versionMajor = dis.readUShort(); + versionMinor = dis.readUShort(); + } - if ( versionMajor == binaryRC4.versionMajor + if ( versionMajor == xor.versionMajor + && versionMinor == xor.versionMinor) { + encryptionMode = xor; + encryptionFlags = -1; + } else if ( versionMajor == binaryRC4.versionMajor && versionMinor == binaryRC4.versionMinor) { encryptionMode = binaryRC4; encryptionFlags = -1; - } else if (!isCryptoAPI - && versionMajor == agile.versionMajor + } else if ( + 2 <= versionMajor && versionMajor <= 4 + && versionMinor == 2) { + encryptionMode = (preferredEncryptionMode == cryptoAPI) ? cryptoAPI : standard; + encryptionFlags = dis.readInt(); + } else if ( + versionMajor == agile.versionMajor && versionMinor == agile.versionMinor){ encryptionMode = agile; encryptionFlags = dis.readInt(); - } else if (!isCryptoAPI - && 2 <= versionMajor && versionMajor <= 4 - && versionMinor == standard.versionMinor) { - encryptionMode = standard; - encryptionFlags = dis.readInt(); - } else if (isCryptoAPI - && 2 <= versionMajor && versionMajor <= 4 - && versionMinor == cryptoAPI.versionMinor) { - encryptionMode = cryptoAPI; - encryptionFlags = dis.readInt(); } else { encryptionFlags = dis.readInt(); throw new EncryptedDocumentException( @@ -170,6 +178,7 @@ public class EncryptionInfo implements Cloneable { , int blockSize , ChainingMode chainingMode ) { + this.encryptionMode = encryptionMode; versionMajor = encryptionMode.versionMajor; versionMinor = encryptionMode.versionMinor; encryptionFlags = encryptionMode.encryptionFlags; @@ -236,6 +245,10 @@ public class EncryptionInfo implements Cloneable { this.encryptor = encryptor; } + public EncryptionMode getEncryptionMode() { + return encryptionMode; + } + @Override public EncryptionInfo clone() throws CloneNotSupportedException { EncryptionInfo other = (EncryptionInfo)super.clone(); diff --git a/src/java/org/apache/poi/poifs/crypt/EncryptionMode.java b/src/java/org/apache/poi/poifs/crypt/EncryptionMode.java index 86f4b8508a..50064b5a6b 100644 --- a/src/java/org/apache/poi/poifs/crypt/EncryptionMode.java +++ b/src/java/org/apache/poi/poifs/crypt/EncryptionMode.java @@ -33,7 +33,9 @@ public enum EncryptionMode { /* @see 2.3.4.5 \EncryptionInfo Stream (Standard Encryption) */ standard("org.apache.poi.poifs.crypt.standard.StandardEncryptionInfoBuilder", 4, 2, 0x24), /* @see 2.3.4.10 \EncryptionInfo Stream (Agile Encryption) */ - agile("org.apache.poi.poifs.crypt.agile.AgileEncryptionInfoBuilder", 4, 4, 0x40) + agile("org.apache.poi.poifs.crypt.agile.AgileEncryptionInfoBuilder", 4, 4, 0x40), + /* @see XOR Obfuscation */ + xor("org.apache.poi.poifs.crypt.xor.XOREncryptionInfoBuilder", 0, 0, 0) ; public final String builder; diff --git a/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4Decryptor.java b/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4Decryptor.java index 1fdf3a9820..b6d8eda008 100644 --- a/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4Decryptor.java +++ b/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4Decryptor.java @@ -29,11 +29,9 @@ import javax.crypto.spec.SecretKeySpec; import org.apache.poi.EncryptedDocumentException; import org.apache.poi.poifs.crypt.*; -import org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIDecryptor; import org.apache.poi.poifs.filesystem.DirectoryNode; import org.apache.poi.poifs.filesystem.DocumentInputStream; import org.apache.poi.util.LittleEndian; -import org.apache.poi.util.LittleEndianInput; import org.apache.poi.util.StringUtil; public class BinaryRC4Decryptor extends Decryptor implements Cloneable { @@ -53,7 +51,7 @@ public class BinaryRC4Decryptor extends Decryptor implements Cloneable { super(stream, size, _chunkSize); } - public BinaryRC4CipherInputStream(LittleEndianInput stream) + public BinaryRC4CipherInputStream(InputStream stream) throws GeneralSecurityException { super(stream, Integer.MAX_VALUE, _chunkSize); } @@ -140,7 +138,8 @@ public class BinaryRC4Decryptor extends Decryptor implements Cloneable { return new BinaryRC4CipherInputStream(dis, _length); } - public InputStream getDataStream(LittleEndianInput stream) + @Override + public InputStream getDataStream(InputStream stream, int size, int initialPos) throws IOException, GeneralSecurityException { return new BinaryRC4CipherInputStream(stream); } diff --git a/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIDecryptor.java b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIDecryptor.java index 07b7910749..451708c6ef 100644 --- a/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIDecryptor.java +++ b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIDecryptor.java @@ -45,7 +45,6 @@ import org.apache.poi.util.BitFieldFactory; import org.apache.poi.util.BoundedInputStream; import org.apache.poi.util.IOUtils; import org.apache.poi.util.LittleEndian; -import org.apache.poi.util.LittleEndianInput; import org.apache.poi.util.LittleEndianInputStream; import org.apache.poi.util.StringUtil; @@ -146,7 +145,7 @@ public class CryptoAPIDecryptor extends Decryptor implements Cloneable { } @Override - public ChunkedCipherInputStream getDataStream(LittleEndianInput stream, int size, int initialPos) + public ChunkedCipherInputStream getDataStream(InputStream stream, int size, int initialPos) throws IOException, GeneralSecurityException { return new CryptoAPICipherInputStream(stream, size, initialPos); } @@ -233,7 +232,7 @@ public class CryptoAPIDecryptor extends Decryptor implements Cloneable { return CryptoAPIDecryptor.this.initCipherForBlock(existing, block); } - public CryptoAPICipherInputStream(LittleEndianInput stream, long size, int initialPos) + public CryptoAPICipherInputStream(InputStream stream, long size, int initialPos) throws GeneralSecurityException { super(stream, size, _chunkSize, initialPos); } diff --git a/src/java/org/apache/poi/poifs/crypt/xor/XORDecryptor.java b/src/java/org/apache/poi/poifs/crypt/xor/XORDecryptor.java new file mode 100644 index 0000000000..cb50b47826 --- /dev/null +++ b/src/java/org/apache/poi/poifs/crypt/xor/XORDecryptor.java @@ -0,0 +1,174 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.poifs.crypt.xor; + +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +import org.apache.poi.poifs.crypt.ChunkedCipherInputStream; +import org.apache.poi.poifs.crypt.CryptoFunctions; +import org.apache.poi.poifs.crypt.Decryptor; +import org.apache.poi.poifs.crypt.EncryptionInfo; +import org.apache.poi.poifs.filesystem.DirectoryNode; +import org.apache.poi.util.LittleEndian; + +public class XORDecryptor extends Decryptor implements Cloneable { + private long _length = -1L; + private int _chunkSize = 512; + + private class XORCipherInputStream extends ChunkedCipherInputStream { + private final int _initialOffset; + private int _recordStart = 0; + private int _recordEnd = 0; + + @Override + protected Cipher initCipherForBlock(Cipher existing, int block) + throws GeneralSecurityException { + return XORDecryptor.this.initCipherForBlock(existing, block); + } + + public XORCipherInputStream(InputStream stream, int initialPos) + throws GeneralSecurityException { + super(stream, Integer.MAX_VALUE, _chunkSize); + _initialOffset = initialPos; + } + + @Override + protected int invokeCipher(int totalBytes, boolean doFinal) { + final int pos = (int)getPos(); + final byte xorArray[] = getEncryptionInfo().getDecryptor().getSecretKey().getEncoded(); + final byte chunk[] = getChunk(); + final byte plain[] = getPlain(); + final int posInChunk = pos & getChunkMask(); + + /* + * From: http://social.msdn.microsoft.com/Forums/en-US/3dadbed3-0e68-4f11-8b43-3a2328d9ebd5 + * + * The initial value for XorArrayIndex is as follows: + * XorArrayIndex = (FileOffset + Data.Length) % 16 + * + * The FileOffset variable in this context is the stream offset into the Workbook stream at + * the time we are about to write each of the bytes of the record data. + * This (the value) is then incremented after each byte is written. + */ + final int xorArrayIndex = _initialOffset+_recordEnd+(pos-_recordStart); + + for (int i=0; pos+i < _recordEnd && i < totalBytes; i++) { + // The following is taken from the Libre Office implementation + // It seems that the encrypt and decrypt method is mixed up + // in the MS-OFFCRYPTO docs + byte value = plain[posInChunk+i]; + value = rotateLeft(value, 3); + value ^= xorArray[(xorArrayIndex+i) & 0x0F]; + chunk[posInChunk+i] = value; + } + + // the other bytes will be encoded, when setNextRecordSize is called the next time + return totalBytes; + } + + private byte rotateLeft(byte bits, int shift) { + return (byte)(((bits & 0xff) << shift) | ((bits & 0xff) >>> (8 - shift))); + } + + + /** + * Decrypts a xor obfuscated byte array. + * The data is decrypted in-place + * + * @see 2.3.7.3 Binary Document XOR Data Transformation Method 1 + */ + @Override + public void setNextRecordSize(int recordSize) { + _recordStart = (int)getPos(); + _recordEnd = _recordStart+recordSize; + int pos = (int)getPos(); + byte chunk[] = getChunk(); + int chunkMask = getChunkMask(); + int nextBytes = Math.min(recordSize, chunk.length-(pos & chunkMask)); + invokeCipher(nextBytes, true); + } + } + + protected XORDecryptor() { + } + + @Override + public boolean verifyPassword(String password) { + XOREncryptionVerifier ver = (XOREncryptionVerifier)getEncryptionInfo().getVerifier(); + int keyVer = LittleEndian.getUShort(ver.getEncryptedKey()); + int verifierVer = LittleEndian.getUShort(ver.getEncryptedVerifier()); + int keyComp = CryptoFunctions.createXorKey1(password); + int verifierComp = CryptoFunctions.createXorVerifier1(password); + if (keyVer == keyComp && verifierVer == verifierComp) { + byte xorArray[] = CryptoFunctions.createXorArray1(password); + setSecretKey(new SecretKeySpec(xorArray, "XOR")); + return true; + } else { + return false; + } + } + + @Override + public Cipher initCipherForBlock(Cipher cipher, int block) + throws GeneralSecurityException { + return null; + } + + protected static Cipher initCipherForBlock(Cipher cipher, int block, + EncryptionInfo encryptionInfo, SecretKey skey, int encryptMode) + throws GeneralSecurityException { + return null; + } + + @Override + public ChunkedCipherInputStream getDataStream(DirectoryNode dir) throws IOException, GeneralSecurityException { + throw new RuntimeException("not supported"); + } + + @Override + public InputStream getDataStream(InputStream stream, int size, int initialPos) + throws IOException, GeneralSecurityException { + return new XORCipherInputStream(stream, initialPos); + } + + + @Override + public long getLength() { + if (_length == -1L) { + throw new IllegalStateException("Decryptor.getDataStream() was not called"); + } + + return _length; + } + + @Override + public void setChunkSize(int chunkSize) { + _chunkSize = chunkSize; + } + + @Override + public XORDecryptor clone() throws CloneNotSupportedException { + return (XORDecryptor)super.clone(); + } +} diff --git a/src/java/org/apache/poi/hssf/record/crypto/Biff8Cipher.java b/src/java/org/apache/poi/poifs/crypt/xor/XOREncryptionHeader.java similarity index 60% rename from src/java/org/apache/poi/hssf/record/crypto/Biff8Cipher.java rename to src/java/org/apache/poi/poifs/crypt/xor/XOREncryptionHeader.java index 8ac742e0df..cc5068f6bb 100644 --- a/src/java/org/apache/poi/hssf/record/crypto/Biff8Cipher.java +++ b/src/java/org/apache/poi/poifs/crypt/xor/XOREncryptionHeader.java @@ -1,30 +1,37 @@ -/* ==================================================================== - Licensed to the Apache Software Foundation (ASF) under one or more - contributor license agreements. See the NOTICE file distributed with - this work for additional information regarding copyright ownership. - The ASF licenses this file to You under the Apache License, Version 2.0 - (the "License"); you may not use this file except in compliance with - the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -==================================================================== */ - -package org.apache.poi.hssf.record.crypto; - - -public interface Biff8Cipher { - void startRecord(int currentSid); - void setNextRecordSize(int recordSize); - void skipTwoBytes(); - void xor(byte[] buf, int pOffset, int pLen); - int xorByte(int rawVal); - int xorShort(int rawVal); - int xorInt(int rawVal); - long xorLong(long rawVal); -} +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.poifs.crypt.xor; + +import org.apache.poi.poifs.crypt.EncryptionHeader; +import org.apache.poi.poifs.crypt.standard.EncryptionRecord; +import org.apache.poi.util.LittleEndianByteArrayOutputStream; + +public class XOREncryptionHeader extends EncryptionHeader implements EncryptionRecord, Cloneable { + + protected XOREncryptionHeader() { + } + + @Override + public void write(LittleEndianByteArrayOutputStream littleendianbytearrayoutputstream) { + } + + @Override + public XOREncryptionHeader clone() throws CloneNotSupportedException { + return (XOREncryptionHeader)super.clone(); + } +} diff --git a/src/java/org/apache/poi/poifs/crypt/xor/XOREncryptionInfoBuilder.java b/src/java/org/apache/poi/poifs/crypt/xor/XOREncryptionInfoBuilder.java new file mode 100644 index 0000000000..9fcaf8b35b --- /dev/null +++ b/src/java/org/apache/poi/poifs/crypt/xor/XOREncryptionInfoBuilder.java @@ -0,0 +1,62 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.poifs.crypt.xor; + +import java.io.IOException; + +import org.apache.poi.poifs.crypt.ChainingMode; +import org.apache.poi.poifs.crypt.CipherAlgorithm; +import org.apache.poi.poifs.crypt.Decryptor; +import org.apache.poi.poifs.crypt.EncryptionInfo; +import org.apache.poi.poifs.crypt.EncryptionInfoBuilder; +import org.apache.poi.poifs.crypt.Encryptor; +import org.apache.poi.poifs.crypt.HashAlgorithm; +import org.apache.poi.util.LittleEndianInput; + +public class XOREncryptionInfoBuilder implements EncryptionInfoBuilder { + + public XOREncryptionInfoBuilder() { + } + + @Override + public void initialize(EncryptionInfo info, LittleEndianInput dis) + throws IOException { + info.setHeader(new XOREncryptionHeader()); + info.setVerifier(new XOREncryptionVerifier(dis)); + Decryptor dec = new XORDecryptor(); + dec.setEncryptionInfo(info); + info.setDecryptor(dec); + Encryptor enc = new XOREncryptor(); + enc.setEncryptionInfo(info); + info.setEncryptor(enc); + } + + @Override + public void initialize(EncryptionInfo info, + CipherAlgorithm cipherAlgorithm, HashAlgorithm hashAlgorithm, + int keyBits, int blockSize, ChainingMode chainingMode) { + info.setHeader(new XOREncryptionHeader()); + info.setVerifier(new XOREncryptionVerifier()); + Decryptor dec = new XORDecryptor(); + dec.setEncryptionInfo(info); + info.setDecryptor(dec); + Encryptor enc = new XOREncryptor(); + enc.setEncryptionInfo(info); + info.setEncryptor(enc); + } +} diff --git a/src/java/org/apache/poi/poifs/crypt/xor/XOREncryptionVerifier.java b/src/java/org/apache/poi/poifs/crypt/xor/XOREncryptionVerifier.java new file mode 100644 index 0000000000..1dcfb941cb --- /dev/null +++ b/src/java/org/apache/poi/poifs/crypt/xor/XOREncryptionVerifier.java @@ -0,0 +1,61 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.poifs.crypt.xor; + +import org.apache.poi.poifs.crypt.EncryptionVerifier; +import org.apache.poi.poifs.crypt.standard.EncryptionRecord; +import org.apache.poi.util.LittleEndianByteArrayOutputStream; +import org.apache.poi.util.LittleEndianInput; + +public class XOREncryptionVerifier extends EncryptionVerifier implements EncryptionRecord, Cloneable { + + protected XOREncryptionVerifier() { + setEncryptedKey(new byte[2]); + setEncryptedVerifier(new byte[2]); + } + + protected XOREncryptionVerifier(LittleEndianInput is) { + /** + * key (2 bytes): An unsigned integer that specifies the obfuscation key. + * See [MS-OFFCRYPTO], 2.3.6.2 section, the first step of initializing XOR + * array where it describes the generation of 16-bit XorKey value. + */ + byte key[] = new byte[2]; + is.readFully(key); + setEncryptedKey(key); + + /** + * verificationBytes (2 bytes): An unsigned integer that specifies + * the password verification identifier. + */ + byte verifier[] = new byte[2]; + is.readFully(verifier); + setEncryptedVerifier(verifier); + } + + @Override + public void write(LittleEndianByteArrayOutputStream bos) { + bos.write(getEncryptedKey()); + bos.write(getEncryptedVerifier()); + } + + @Override + public XOREncryptionVerifier clone() throws CloneNotSupportedException { + return (XOREncryptionVerifier)super.clone(); + } +} diff --git a/src/java/org/apache/poi/poifs/crypt/xor/XOREncryptor.java b/src/java/org/apache/poi/poifs/crypt/xor/XOREncryptor.java new file mode 100644 index 0000000000..054b5e0e42 --- /dev/null +++ b/src/java/org/apache/poi/poifs/crypt/xor/XOREncryptor.java @@ -0,0 +1,99 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.poifs.crypt.xor; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.security.GeneralSecurityException; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.util.Random; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; + +import org.apache.poi.EncryptedDocumentException; +import org.apache.poi.poifs.crypt.ChunkedCipherOutputStream; +import org.apache.poi.poifs.crypt.CryptoFunctions; +import org.apache.poi.poifs.crypt.DataSpaceMapUtils; +import org.apache.poi.poifs.crypt.EncryptionInfo; +import org.apache.poi.poifs.crypt.Encryptor; +import org.apache.poi.poifs.crypt.HashAlgorithm; +import org.apache.poi.poifs.crypt.standard.EncryptionRecord; +import org.apache.poi.poifs.filesystem.DirectoryNode; +import org.apache.poi.util.LittleEndianByteArrayOutputStream; + +public class XOREncryptor extends Encryptor implements Cloneable { + + protected XOREncryptor() { + } + + @Override + public void confirmPassword(String password) { + } + + @Override + public void confirmPassword(String password, byte keySpec[], + byte keySalt[], byte verifier[], byte verifierSalt[], + byte integritySalt[]) { + } + + @Override + public OutputStream getDataStream(DirectoryNode dir) + throws IOException, GeneralSecurityException { + OutputStream countStream = new XORCipherOutputStream(dir); + return countStream; + } + + protected int getKeySizeInBytes() { + return -1; + } + + protected void createEncryptionInfoEntry(DirectoryNode dir) throws IOException { + } + + @Override + public XOREncryptor clone() throws CloneNotSupportedException { + return (XOREncryptor)super.clone(); + } + + protected class XORCipherOutputStream extends ChunkedCipherOutputStream { + + @Override + protected Cipher initCipherForBlock(Cipher cipher, int block, boolean lastChunk) + throws GeneralSecurityException { + return XORDecryptor.initCipherForBlock(cipher, block, getEncryptionInfo(), getSecretKey(), Cipher.ENCRYPT_MODE); + } + + @Override + protected void calculateChecksum(File file, int i) { + } + + @Override + protected void createEncryptionInfoEntry(DirectoryNode dir, File tmpFile) + throws IOException, GeneralSecurityException { + XOREncryptor.this.createEncryptionInfoEntry(dir); + } + + public XORCipherOutputStream(DirectoryNode dir) + throws IOException, GeneralSecurityException { + super(dir, 512); + } + } +} diff --git a/src/scratchpad/src/org/apache/poi/hslf/record/DocumentEncryptionAtom.java b/src/scratchpad/src/org/apache/poi/hslf/record/DocumentEncryptionAtom.java index c21f89dd13..57f0f31ed7 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/record/DocumentEncryptionAtom.java +++ b/src/scratchpad/src/org/apache/poi/hslf/record/DocumentEncryptionAtom.java @@ -53,7 +53,8 @@ public final class DocumentEncryptionAtom extends PositionDependentRecordAtom { ByteArrayInputStream bis = new ByteArrayInputStream(source, start+8, len-8); LittleEndianInputStream leis = new LittleEndianInputStream(bis); - ei = new EncryptionInfo(leis, true); + ei = new EncryptionInfo(leis, EncryptionMode.cryptoAPI); + leis.close(); } public DocumentEncryptionAtom() { @@ -121,6 +122,7 @@ public final class DocumentEncryptionAtom extends PositionDependentRecordAtom { LittleEndian.putInt(_header, 4, bos.getWriteIndex()); out.write(_header); out.write(data, 0, bos.getWriteIndex()); + bos.close(); } @Override diff --git a/src/testcases/org/apache/poi/hssf/record/AllRecordTests.java b/src/testcases/org/apache/poi/hssf/record/AllRecordTests.java index b7598fd124..e32816756c 100644 --- a/src/testcases/org/apache/poi/hssf/record/AllRecordTests.java +++ b/src/testcases/org/apache/poi/hssf/record/AllRecordTests.java @@ -21,8 +21,8 @@ import org.apache.poi.hssf.record.aggregates.AllRecordAggregateTests; import org.apache.poi.hssf.record.cf.TestCellRange; import org.apache.poi.hssf.record.chart.AllChartRecordTests; import org.apache.poi.hssf.record.common.TestUnicodeString; -import org.apache.poi.hssf.record.crypto.AllHSSFEncryptionTests; import org.apache.poi.hssf.record.pivot.AllPivotRecordTests; +import org.apache.poi.poifs.crypt.AllEncryptionTests; import org.apache.poi.ss.formula.constant.TestConstantValueParser; import org.apache.poi.ss.formula.ptg.AllFormulaTests; import org.junit.runner.RunWith; @@ -34,7 +34,7 @@ import org.junit.runners.Suite; @RunWith(Suite.class) @Suite.SuiteClasses({ AllChartRecordTests.class, - AllHSSFEncryptionTests.class, + AllEncryptionTests.class, AllFormulaTests.class, AllPivotRecordTests.class, AllRecordAggregateTests.class, diff --git a/src/testcases/org/apache/poi/hssf/record/crypto/TestBiff8EncryptionKey.java b/src/testcases/org/apache/poi/hssf/record/crypto/TestBiff8EncryptionKey.java deleted file mode 100644 index 294cb09e7c..0000000000 --- a/src/testcases/org/apache/poi/hssf/record/crypto/TestBiff8EncryptionKey.java +++ /dev/null @@ -1,102 +0,0 @@ -/* ==================================================================== - Licensed to the Apache Software Foundation (ASF) under one or more - contributor license agreements. See the NOTICE file distributed with - this work for additional information regarding copyright ownership. - The ASF licenses this file to You under the Apache License, Version 2.0 - (the "License"); you may not use this file except in compliance with - the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -==================================================================== */ - -package org.apache.poi.hssf.record.crypto; - -import java.util.Arrays; - -import junit.framework.ComparisonFailure; -import junit.framework.TestCase; - -import org.apache.poi.util.HexDump; -import org.apache.poi.util.HexRead; - -/** - * Tests for {@link Biff8EncryptionKey} - * - * @author Josh Micich - */ -public final class TestBiff8EncryptionKey extends TestCase { - - private static byte[] fromHex(String hexString) { - return HexRead.readFromString(hexString); - } - public void testCreateKeyDigest() { - byte[] docIdData = fromHex("17 F6 D1 6B 09 B1 5F 7B 4C 9D 03 B4 81 B5 B4 4A"); - byte[] keyDigest = Biff8RC4Key.createKeyDigest("MoneyForNothing", docIdData); - byte[] expResult = fromHex("C2 D9 56 B2 6B"); - if (!Arrays.equals(expResult, keyDigest)) { - throw new ComparisonFailure("keyDigest mismatch", HexDump.toHex(expResult), HexDump.toHex(keyDigest)); - } - } - - - public void testValidateWithDefaultPassword() { - - String docIdSuffixA = "F 35 52 38 0D 75 4A E6 85 C2 FD 78 CE 3D D1 B6"; // valid prefix is 'D' - String saltHashA = "30 38 BE 5E 93 C5 7E B4 5F 52 CD A1 C6 8F B6 2A"; - String saltDataA = "D4 04 43 EC B7 A7 6F 6A D2 68 C7 DF CF A8 80 68"; - - String docIdB = "39 D7 80 41 DA E4 74 2C 8C 84 F9 4D 39 9A 19 2D"; - String saltDataSuffixB = "3 EA 8D 52 11 11 37 D2 BD 55 4C 01 0A 47 6E EB"; // valid prefix is 'C' - String saltHashB = "96 19 F5 D0 F1 63 08 F1 3E 09 40 1E 87 F0 4E 16"; - - confirmValid(true, "D" + docIdSuffixA, saltDataA, saltHashA); - confirmValid(true, docIdB, "C" + saltDataSuffixB, saltHashB); - confirmValid(false, "E" + docIdSuffixA, saltDataA, saltHashA); - confirmValid(false, docIdB, "B" + saltDataSuffixB, saltHashB); - } - - public void testValidateWithSuppliedPassword() { - - String docId = "DF 35 52 38 0D 75 4A E6 85 C2 FD 78 CE 3D D1 B6"; - String saltData = "D4 04 43 EC B7 A7 6F 6A D2 68 C7 DF CF A8 80 68"; - String saltHashA = "8D C2 63 CC E1 1D E0 05 20 16 96 AF 48 59 94 64"; // for password '5ecret' - String saltHashB = "31 0B 0D A4 69 55 8E 27 A1 03 AD C9 AE F8 09 04"; // for password '5ecret' - - confirmValid(true, docId, saltData, saltHashA, "5ecret"); - confirmValid(false, docId, saltData, saltHashA, "Secret"); - confirmValid(true, docId, saltData, saltHashB, "Secret"); - confirmValid(false, docId, saltData, saltHashB, "secret"); - } - - - private static void confirmValid(boolean expectedResult, - String docIdHex, String saltDataHex, String saltHashHex) { - confirmValid(expectedResult, docIdHex, saltDataHex, saltHashHex, null); - } - private static void confirmValid(boolean expectedResult, - String docIdHex, String saltDataHex, String saltHashHex, String password) { - byte[] docId = fromHex(docIdHex); - byte[] saltData = fromHex(saltDataHex); - byte[] saltHash = fromHex(saltHashHex); - - - Biff8EncryptionKey key; - if (password == null) { - key = Biff8EncryptionKey.create(docId); - } else { - key = Biff8EncryptionKey.create(password, docId); - } - boolean actResult = key.validate(saltData, saltHash); - if (expectedResult) { - assertTrue("validate failed", actResult); - } else { - assertFalse("validate succeeded unexpectedly", actResult); - } - } -} diff --git a/src/testcases/org/apache/poi/hssf/usermodel/TestCryptoAPI.java b/src/testcases/org/apache/poi/hssf/usermodel/TestCryptoAPI.java new file mode 100644 index 0000000000..e7618073b3 --- /dev/null +++ b/src/testcases/org/apache/poi/hssf/usermodel/TestCryptoAPI.java @@ -0,0 +1,62 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.usermodel; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; + +import org.apache.poi.hssf.HSSFITestDataProvider; +import org.apache.poi.hssf.extractor.ExcelExtractor; +import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey; +import org.junit.AfterClass; +import org.junit.Test; + +public class TestCryptoAPI { + final HSSFITestDataProvider ssTests = HSSFITestDataProvider.instance; + + @AfterClass + public static void resetPW() { + Biff8EncryptionKey.setCurrentUserPassword(null); + } + + @Test + public void bug59857() throws IOException { + Biff8EncryptionKey.setCurrentUserPassword("abc"); + HSSFWorkbook wb1 = ssTests.openSampleWorkbook("xor-encryption-abc.xls"); + String textExpected = "Sheet1\n1\n2\n3\n"; + String textActual = new ExcelExtractor(wb1).getText(); + assertEquals(textExpected, textActual); + wb1.close(); + + Biff8EncryptionKey.setCurrentUserPassword("password"); + HSSFWorkbook wb2 = ssTests.openSampleWorkbook("password.xls"); + textExpected = "A ZIP bomb is a variant of mail-bombing. After most commercial mail servers began checking mail with anti-virus software and filtering certain malicious file types, trojan horse viruses tried to send themselves compressed into archives, such as ZIP, RAR or 7-Zip. Mail server software was then configured to unpack archives and check their contents as well. That gave black hats the idea to compose a \"bomb\" consisting of an enormous text file, containing, for example, only the letter z repeated millions of times. Such a file compresses into a relatively small archive, but its unpacking (especially by early versions of mail servers) would use a high amount of processing power, RAM and swap space, which could result in denial of service. Modern mail server computers usually have sufficient intelligence to recognize such attacks as well as sufficient processing power and memory space to process malicious attachments without interruption of service, though some are still susceptible to this technique if the ZIP bomb is mass-mailed."; + textActual = new ExcelExtractor(wb2).getText(); + assertTrue(textActual.contains(textExpected)); + wb2.close(); + + Biff8EncryptionKey.setCurrentUserPassword("freedom"); + HSSFWorkbook wb3 = ssTests.openSampleWorkbook("35897-type4.xls"); + textExpected = "Sheet1\nhello there!\n"; + textActual = new ExcelExtractor(wb3).getText(); + assertEquals(textExpected, textActual); + wb3.close(); + } +} diff --git a/src/testcases/org/apache/poi/hssf/record/crypto/AllHSSFEncryptionTests.java b/src/testcases/org/apache/poi/poifs/crypt/AllEncryptionTests.java similarity index 83% rename from src/testcases/org/apache/poi/hssf/record/crypto/AllHSSFEncryptionTests.java rename to src/testcases/org/apache/poi/poifs/crypt/AllEncryptionTests.java index c727008788..8bd67db91b 100644 --- a/src/testcases/org/apache/poi/hssf/record/crypto/AllHSSFEncryptionTests.java +++ b/src/testcases/org/apache/poi/poifs/crypt/AllEncryptionTests.java @@ -15,20 +15,19 @@ limitations under the License. ==================================================================== */ -package org.apache.poi.hssf.record.crypto; +package org.apache.poi.poifs.crypt; import org.junit.runner.RunWith; import org.junit.runners.Suite; /** - * Collects all tests for package org.apache.poi.hssf.record.crypto. - * - * @author Josh Micich + * Collects all tests for package org.apache.poi.poifs.crypt. */ @RunWith(Suite.class) @Suite.SuiteClasses({ TestBiff8DecryptingStream.class, - TestBiff8EncryptionKey.class + TestCipherAlgorithm.class, + TestXorEncryption.class }) -public final class AllHSSFEncryptionTests { +public final class AllEncryptionTests { } diff --git a/src/testcases/org/apache/poi/hssf/record/crypto/TestBiff8DecryptingStream.java b/src/testcases/org/apache/poi/poifs/crypt/TestBiff8DecryptingStream.java similarity index 94% rename from src/testcases/org/apache/poi/hssf/record/crypto/TestBiff8DecryptingStream.java rename to src/testcases/org/apache/poi/poifs/crypt/TestBiff8DecryptingStream.java index 26eb16b2f7..43ee429417 100644 --- a/src/testcases/org/apache/poi/hssf/record/crypto/TestBiff8DecryptingStream.java +++ b/src/testcases/org/apache/poi/poifs/crypt/TestBiff8DecryptingStream.java @@ -15,7 +15,7 @@ limitations under the License. ==================================================================== */ -package org.apache.poi.hssf.record.crypto; +package org.apache.poi.poifs.crypt; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -23,17 +23,18 @@ import static org.junit.Assert.assertFalse; import java.io.InputStream; import java.util.Arrays; -import junit.framework.AssertionFailedError; -import junit.framework.ComparisonFailure; +import javax.crypto.spec.SecretKeySpec; +import org.apache.poi.hssf.record.crypto.Biff8DecryptingStream; import org.apache.poi.util.HexDump; import org.apache.poi.util.HexRead; import org.junit.Test; +import junit.framework.AssertionFailedError; +import junit.framework.ComparisonFailure; + /** * Tests for {@link Biff8DecryptingStream} - * - * @author Josh Micich */ public final class TestBiff8DecryptingStream { @@ -49,12 +50,10 @@ public final class TestBiff8DecryptingStream { public MockStream(int initialValue) { _initialValue = initialValue; } + public int read() { return (_initialValue+_position++) & 0xFF; } - public int getPosition() { - return _position; - } } private static final class StreamTester { @@ -70,7 +69,11 @@ public final class TestBiff8DecryptingStream { public StreamTester(MockStream ms, String keyDigestHex, int expectedFirstInt) { _ms = ms; byte[] keyDigest = HexRead.readFromString(keyDigestHex); - _bds = new Biff8DecryptingStream(_ms, 0, new Biff8RC4Key(keyDigest)); + EncryptionInfo ei = new EncryptionInfo(EncryptionMode.binaryRC4); + Decryptor dec = ei.getDecryptor(); + dec.setSecretKey(new SecretKeySpec(keyDigest, "RC4")); + + _bds = new Biff8DecryptingStream(_ms, 0, ei); assertEquals(expectedFirstInt, _bds.readInt()); _errorsOccurred = false; } @@ -84,11 +87,11 @@ public final class TestBiff8DecryptingStream { * Also confirms that read position of the underlying stream is aligned. */ public void rollForward(int fromPosition, int toPosition) { - assertEquals(fromPosition, _ms.getPosition()); + assertEquals(fromPosition, _bds.getPosition()); for (int i = fromPosition; i < toPosition; i++) { _bds.readByte(); } - assertEquals(toPosition, _ms.getPosition()); + assertEquals(toPosition, _bds.getPosition()); } public void confirmByte(int expVal) { diff --git a/src/testcases/org/apache/poi/poifs/crypt/TestCipherAlgorithm.java b/src/testcases/org/apache/poi/poifs/crypt/TestCipherAlgorithm.java index 1e0fc14d8d..68d6ab2901 100644 --- a/src/testcases/org/apache/poi/poifs/crypt/TestCipherAlgorithm.java +++ b/src/testcases/org/apache/poi/poifs/crypt/TestCipherAlgorithm.java @@ -17,14 +17,14 @@ package org.apache.poi.poifs.crypt; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; import org.apache.poi.EncryptedDocumentException; import org.junit.Test; public class TestCipherAlgorithm { @Test - public void test() { + public void validInputs() { assertEquals(128, CipherAlgorithm.aes128.defaultKeySize); for(CipherAlgorithm alg : CipherAlgorithm.values()) { @@ -33,27 +33,20 @@ public class TestCipherAlgorithm { assertEquals(CipherAlgorithm.aes128, CipherAlgorithm.fromEcmaId(0x660E)); assertEquals(CipherAlgorithm.aes192, CipherAlgorithm.fromXmlId("AES", 192)); - - try { - CipherAlgorithm.fromEcmaId(0); - fail("Should throw exception"); - } catch (EncryptedDocumentException e) { - // expected - } - - try { - CipherAlgorithm.fromXmlId("AES", 1); - fail("Should throw exception"); - } catch (EncryptedDocumentException e) { - // expected - } - - try { - CipherAlgorithm.fromXmlId("RC1", 0x40); - fail("Should throw exception"); - } catch (EncryptedDocumentException e) { - // expected - } } - + + @Test(expected=EncryptedDocumentException.class) + public void invalidEcmaId() { + CipherAlgorithm.fromEcmaId(0); + } + + @Test(expected=EncryptedDocumentException.class) + public void invalidXmlId1() { + CipherAlgorithm.fromXmlId("AES", 1); + } + + @Test(expected=EncryptedDocumentException.class) + public void invalidXmlId2() { + CipherAlgorithm.fromXmlId("RC1", 0x40); + } } diff --git a/src/testcases/org/apache/poi/hssf/record/crypto/TestXorEncryption.java b/src/testcases/org/apache/poi/poifs/crypt/TestXorEncryption.java similarity index 94% rename from src/testcases/org/apache/poi/hssf/record/crypto/TestXorEncryption.java rename to src/testcases/org/apache/poi/poifs/crypt/TestXorEncryption.java index e79f2fcc6e..cae6426f61 100644 --- a/src/testcases/org/apache/poi/hssf/record/crypto/TestXorEncryption.java +++ b/src/testcases/org/apache/poi/poifs/crypt/TestXorEncryption.java @@ -15,13 +15,14 @@ limitations under the License. ==================================================================== */ -package org.apache.poi.hssf.record.crypto; +package org.apache.poi.poifs.crypt; import static org.hamcrest.core.IsEqual.equalTo; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import org.apache.poi.hssf.HSSFTestDataSamples; +import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey; import org.apache.poi.hssf.usermodel.HSSFSheet; import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.apache.poi.poifs.crypt.CryptoFunctions; diff --git a/src/testcases/org/apache/poi/poifs/crypt/binaryrc4/TestBinaryRC4.java b/src/testcases/org/apache/poi/poifs/crypt/binaryrc4/TestBinaryRC4.java new file mode 100644 index 0000000000..b1155c3f57 --- /dev/null +++ b/src/testcases/org/apache/poi/poifs/crypt/binaryrc4/TestBinaryRC4.java @@ -0,0 +1,106 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.poifs.crypt.binaryrc4; + +import static org.apache.poi.util.HexRead.readFromString; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.security.GeneralSecurityException; + +import javax.crypto.SecretKey; + +import org.apache.poi.poifs.crypt.Decryptor; +import org.apache.poi.poifs.crypt.EncryptionInfo; +import org.apache.poi.poifs.crypt.EncryptionMode; +import org.junit.Test; + +public class TestBinaryRC4 { + @Test + public void createKeyDigest() throws GeneralSecurityException { + byte[] docIdData = readFromString("17 F6 D1 6B 09 B1 5F 7B 4C 9D 03 B4 81 B5 B4 4A"); + byte[] expResult = readFromString("C2 D9 56 B2 6B"); + + EncryptionInfo ei = new EncryptionInfo(EncryptionMode.binaryRC4); + BinaryRC4EncryptionVerifier ver = (BinaryRC4EncryptionVerifier)ei.getVerifier(); + ver.setSalt(docIdData); + SecretKey sk = BinaryRC4Decryptor.generateSecretKey("MoneyForNothing", ver); + + assertArrayEquals("keyDigest mismatch", expResult, sk.getEncoded()); + } + + @Test + public void testValidateWithDefaultPassword() throws GeneralSecurityException { + + String docIdSuffixA = "F 35 52 38 0D 75 4A E6 85 C2 FD 78 CE 3D D1 B6"; // valid prefix is 'D' + String saltHashA = "30 38 BE 5E 93 C5 7E B4 5F 52 CD A1 C6 8F B6 2A"; + String saltDataA = "D4 04 43 EC B7 A7 6F 6A D2 68 C7 DF CF A8 80 68"; + + String docIdB = "39 D7 80 41 DA E4 74 2C 8C 84 F9 4D 39 9A 19 2D"; + String saltDataSuffixB = "3 EA 8D 52 11 11 37 D2 BD 55 4C 01 0A 47 6E EB"; // valid prefix is 'C' + String saltHashB = "96 19 F5 D0 F1 63 08 F1 3E 09 40 1E 87 F0 4E 16"; + + confirmValid(true, "D" + docIdSuffixA, saltDataA, saltHashA); + confirmValid(true, docIdB, "C" + saltDataSuffixB, saltHashB); + confirmValid(false, "E" + docIdSuffixA, saltDataA, saltHashA); + confirmValid(false, docIdB, "B" + saltDataSuffixB, saltHashB); + } + + @Test + public void testValidateWithSuppliedPassword() throws GeneralSecurityException { + + String docId = "DF 35 52 38 0D 75 4A E6 85 C2 FD 78 CE 3D D1 B6"; + String saltData = "D4 04 43 EC B7 A7 6F 6A D2 68 C7 DF CF A8 80 68"; + String saltHashA = "8D C2 63 CC E1 1D E0 05 20 16 96 AF 48 59 94 64"; // for password '5ecret' + String saltHashB = "31 0B 0D A4 69 55 8E 27 A1 03 AD C9 AE F8 09 04"; // for password '5ecret' + + confirmValid(true, docId, saltData, saltHashA, "5ecret"); + confirmValid(false, docId, saltData, saltHashA, "Secret"); + confirmValid(true, docId, saltData, saltHashB, "Secret"); + confirmValid(false, docId, saltData, saltHashB, "secret"); + } + + + private static void confirmValid(boolean expectedResult, + String docIdHex, String saltDataHex, String saltHashHex) throws GeneralSecurityException { + confirmValid(expectedResult, docIdHex, saltDataHex, saltHashHex, null); + } + + private static void confirmValid(boolean expectedResult, String docIdHex, + String saltDataHex, String saltHashHex, String password) throws GeneralSecurityException { + byte[] docId = readFromString(docIdHex); + byte[] saltData = readFromString(saltDataHex); + byte[] saltHash = readFromString(saltHashHex); + + EncryptionInfo ei = new EncryptionInfo(EncryptionMode.binaryRC4); + BinaryRC4EncryptionVerifier ver = (BinaryRC4EncryptionVerifier)ei.getVerifier(); + ver.setSalt(docId); + ver.setEncryptedVerifier(saltData); + ver.setEncryptedVerifierHash(saltHash); + + String pass = password == null ? Decryptor.DEFAULT_PASSWORD : password; + boolean actResult = ei.getDecryptor().verifyPassword(pass); + if (expectedResult) { + assertTrue("validate failed", actResult); + } else { + assertFalse("validate succeeded unexpectedly", actResult); + } + } + +} -- 2.39.5