]> source.dussan.org Git - poi.git/commitdiff
Bug 56486 - Add XOR obfuscation/decryption support to HSSF
authorAndreas Beeker <kiwiwings@apache.org>
Mon, 5 May 2014 21:41:31 +0000 (21:41 +0000)
committerAndreas Beeker <kiwiwings@apache.org>
Mon, 5 May 2014 21:41:31 +0000 (21:41 +0000)
git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1592636 13f79535-47bb-0310-9956-ffa450edef68

18 files changed:
src/java/org/apache/poi/hssf/record/FilePassRecord.java
src/java/org/apache/poi/hssf/record/RecordFactoryInputStream.java
src/java/org/apache/poi/hssf/record/crypto/Biff8Cipher.java [new file with mode: 0644]
src/java/org/apache/poi/hssf/record/crypto/Biff8DecryptingStream.java
src/java/org/apache/poi/hssf/record/crypto/Biff8EncryptionKey.java
src/java/org/apache/poi/hssf/record/crypto/Biff8RC4.java
src/java/org/apache/poi/hssf/record/crypto/Biff8RC4Key.java [new file with mode: 0644]
src/java/org/apache/poi/hssf/record/crypto/Biff8XOR.java [new file with mode: 0644]
src/java/org/apache/poi/hssf/record/crypto/Biff8XORKey.java [new file with mode: 0644]
src/java/org/apache/poi/hssf/record/crypto/RC4.java [deleted file]
src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java
src/testcases/org/apache/poi/hssf/record/TestRecordFactoryInputStream.java
src/testcases/org/apache/poi/hssf/record/crypto/AllHSSFEncryptionTests.java
src/testcases/org/apache/poi/hssf/record/crypto/TestBiff8DecryptingStream.java
src/testcases/org/apache/poi/hssf/record/crypto/TestBiff8EncryptionKey.java
src/testcases/org/apache/poi/hssf/record/crypto/TestRC4.java [deleted file]
src/testcases/org/apache/poi/hssf/record/crypto/TestXorEncryption.java [new file with mode: 0644]
src/testcases/org/apache/poi/hssf/usermodel/TestBugs.java

index 6961ed7df9b7173a00ef65e9d28d58fd6d4d8299..32ad6787e2f3673c27165ed826bd5aae833fc17c 100644 (file)
@@ -30,52 +30,165 @@ import org.apache.poi.util.LittleEndianOutput;
  */
 public final class FilePassRecord extends StandardRecord {
        public final static short sid = 0x002F;
+       
        private int _encryptionType;
-       private int _encryptionInfo;
-       private int _minorVersionNo;
-       private byte[] _docId;
-       private byte[] _saltData;
-       private byte[] _saltHash;
+       private KeyData _keyData;
 
-       private static final int ENCRYPTION_XOR = 0;
-       private static final int ENCRYPTION_OTHER = 1;
+       private static interface KeyData {
+           void read(RecordInputStream in);
+           void serialize(LittleEndianOutput out);
+           int getDataSize();
+           void appendToString(StringBuffer buffer);
+       } 
+       
+       public static class Rc4KeyData implements KeyData {
+           private static final int ENCRYPTION_OTHER_RC4 = 1;
+           private static final int ENCRYPTION_OTHER_CAPI_2 = 2;
+           private static final int ENCRYPTION_OTHER_CAPI_3 = 3;
+           
+           private byte[] _salt;
+           private byte[] _encryptedVerifier;
+           private byte[] _encryptedVerifierHash;
+           private int _encryptionInfo;
+           private int _minorVersionNo;
+           
+           public void read(RecordInputStream in) {
+               _encryptionInfo = in.readUShort();
+               switch (_encryptionInfo) {
+                   case ENCRYPTION_OTHER_RC4:
+                       // handled below
+                       break;
+                   case ENCRYPTION_OTHER_CAPI_2:
+                   case ENCRYPTION_OTHER_CAPI_3:
+                       throw new EncryptedDocumentException(
+                               "HSSF does not currently support CryptoAPI encryption");
+                   default:
+                       throw new RecordFormatException("Unknown encryption info " + _encryptionInfo);
+               }
+               _minorVersionNo = in.readUShort();
+               if (_minorVersionNo!=1) {
+                   throw new RecordFormatException("Unexpected VersionInfo number for RC4Header " + _minorVersionNo);
+               }
+               _salt = FilePassRecord.read(in, 16);
+               _encryptedVerifier = FilePassRecord.read(in, 16);
+               _encryptedVerifierHash = FilePassRecord.read(in, 16);
+           }
+           
+           public void serialize(LittleEndianOutput out) {
+            out.writeShort(_encryptionInfo);
+            out.writeShort(_minorVersionNo);
+            out.write(_salt);
+            out.write(_encryptedVerifier);
+            out.write(_encryptedVerifierHash);
+           }
+           
+           public int getDataSize() {
+               return 54;
+           }
+
+        public byte[] getSalt() {
+            return _salt.clone();
+        }
+
+        public void setSalt(byte[] salt) {
+            this._salt = salt.clone();
+        }
+
+        public byte[] getEncryptedVerifier() {
+            return _encryptedVerifier.clone();
+        }
+
+        public void setEncryptedVerifier(byte[] encryptedVerifier) {
+            this._encryptedVerifier = encryptedVerifier.clone();
+        }
+
+        public byte[] getEncryptedVerifierHash() {
+            return _encryptedVerifierHash.clone();
+        }
+
+        public void setEncryptedVerifierHash(byte[] encryptedVerifierHash) {
+            this._encryptedVerifierHash = encryptedVerifierHash.clone();
+        }
+        
+        public void appendToString(StringBuffer buffer) {
+            buffer.append("    .rc4.info = ").append(HexDump.shortToHex(_encryptionInfo)).append("\n");
+            buffer.append("    .rc4.ver  = ").append(HexDump.shortToHex(_minorVersionNo)).append("\n");
+            buffer.append("    .rc4.salt = ").append(HexDump.toHex(_salt)).append("\n");
+            buffer.append("    .rc4.verifier = ").append(HexDump.toHex(_encryptedVerifier)).append("\n");
+            buffer.append("    .rc4.verifierHash = ").append(HexDump.toHex(_encryptedVerifierHash)).append("\n");
+        }
+       }
+
+       public static class XorKeyData implements KeyData {
+           /**
+            * key (2 bytes): An unsigned integer that specifies the obfuscation key. 
+            * See [MS-OFFCRYPTO], 2.3.6.2 section, the first step of initializing XOR
+            * array where it describes the generation of 16-bit XorKey value.
+            */
+           private int _key;
+
+           /**
+            * verificationBytes (2 bytes): An unsigned integer that specifies
+            * the password verification identifier.
+            */
+           private int _verifier;
+           
+        public void read(RecordInputStream in) {
+            _key = in.readUShort();
+            _verifier = in.readUShort();
+        }
+
+        public void serialize(LittleEndianOutput out) {
+            out.writeShort(_key);
+            out.writeShort(_verifier);
+        }
 
-       private static final int ENCRYPTION_OTHER_RC4 = 1;
-       private static final int ENCRYPTION_OTHER_CAPI_2 = 2;
-       private static final int ENCRYPTION_OTHER_CAPI_3 = 3;
+        public int getDataSize() {
+            // TODO: Check!
+            return 6;
+        }
 
+        public int getKey() {
+            return _key;
+        }
+        
+        public int getVerifier() {
+            return _verifier;
+        }
+        
+        public void setKey(int key) {
+            this._key = key;
+        }
+        
+        public void setVerifier(int verifier) {
+            this._verifier = verifier;
+        }
+        
+        public void appendToString(StringBuffer buffer) {
+            buffer.append("    .xor.key = ").append(HexDump.intToHex(_key)).append("\n");
+            buffer.append("    .xor.verifier  = ").append(HexDump.intToHex(_verifier)).append("\n");
+        }
+       }
+       
+       
+       private static final int ENCRYPTION_XOR = 0;
+       private static final int ENCRYPTION_OTHER = 1;
 
        public FilePassRecord(RecordInputStream in) {
                _encryptionType = in.readUShort();
 
                switch (_encryptionType) {
                        case ENCRYPTION_XOR:
-                               throw new EncryptedDocumentException("HSSF does not currently support XOR obfuscation");
+                           _keyData = new XorKeyData();
+                           break;
                        case ENCRYPTION_OTHER:
-                               // handled below
+                           _keyData = new Rc4KeyData();
                                break;
                        default:
                                throw new RecordFormatException("Unknown encryption type " + _encryptionType);
                }
-               _encryptionInfo = in.readUShort();
-               switch (_encryptionInfo) {
-                       case ENCRYPTION_OTHER_RC4:
-                               // handled below
-                               break;
-                       case ENCRYPTION_OTHER_CAPI_2:
-                       case ENCRYPTION_OTHER_CAPI_3:
-                               throw new EncryptedDocumentException(
-                                               "HSSF does not currently support CryptoAPI encryption");
-                       default:
-                               throw new RecordFormatException("Unknown encryption info " + _encryptionInfo);
-               }
-               _minorVersionNo = in.readUShort();
-               if (_minorVersionNo!=1) {
-                       throw new RecordFormatException("Unexpected VersionInfo number for RC4Header " + _minorVersionNo);
-               }
-               _docId = read(in, 16);
-               _saltData = read(in, 16);
-               _saltHash = read(in, 16);
+
+               _keyData.read(in);
        }
 
        private static byte[] read(RecordInputStream in, int size) {
@@ -86,48 +199,88 @@ public final class FilePassRecord extends StandardRecord {
 
        public void serialize(LittleEndianOutput out) {
                out.writeShort(_encryptionType);
-               out.writeShort(_encryptionInfo);
-               out.writeShort(_minorVersionNo);
-               out.write(_docId);
-               out.write(_saltData);
-               out.write(_saltHash);
+               assert(_keyData != null);
+               _keyData.serialize(out);
        }
 
        protected int getDataSize() {
-               return 54;
+           assert(_keyData != null);
+           return _keyData.getDataSize();
        }
 
-
-
-       public byte[] getDocId() {
-               return _docId.clone();
+       public Rc4KeyData getRc4KeyData() {
+           return (_keyData instanceof Rc4KeyData)
+            ? (Rc4KeyData) _keyData
+            : null;
+       }
+       
+    public XorKeyData getXorKeyData() {
+        return (_keyData instanceof XorKeyData)
+            ? (XorKeyData) _keyData
+            : null;
+    }
+    
+    private Rc4KeyData checkRc4() {
+        Rc4KeyData rc4 = getRc4KeyData();
+        if (rc4 == null) {
+            throw new RecordFormatException("file pass record doesn't contain a rc4 key.");
+        }
+        return rc4;
+    }
+    
+    /**
+     * @deprecated use getRc4KeyData().getSalt()
+     * @return the rc4 salt
+     */
+    public byte[] getDocId() {
+               return checkRc4().getSalt();
        }
 
-       public void setDocId(byte[] docId) {
-               _docId = docId.clone();
+    /**
+     * @deprecated use getRc4KeyData().setSalt()
+     * @param docId the new rc4 salt
+     */
+    public void setDocId(byte[] docId) {
+        checkRc4().setSalt(docId);
        }
 
-       public byte[] getSaltData() {
-               return _saltData.clone();
+    /**
+     * @deprecated use getRc4KeyData().getEncryptedVerifier()
+     * @return the rc4 encrypted verifier
+     */
+    public byte[] getSaltData() {
+               return checkRc4().getEncryptedVerifier();
        }
 
+    /**
+     * @deprecated use getRc4KeyData().setEncryptedVerifier()
+     * @param saltData the new rc4 encrypted verifier
+     */
        public void setSaltData(byte[] saltData) {
-               _saltData = saltData.clone();
+           getRc4KeyData().setEncryptedVerifier(saltData);
        }
 
+    /**
+     * @deprecated use getRc4KeyData().getEncryptedVerifierHash()
+     * @return the rc4 encrypted verifier hash
+     */
        public byte[] getSaltHash() {
-               return _saltHash.clone();
+               return getRc4KeyData().getEncryptedVerifierHash();
        }
 
+    /**
+     * @deprecated use getRc4KeyData().setEncryptedVerifierHash()
+     * @param saltHash the new rc4 encrypted verifier
+     */
        public void setSaltHash(byte[] saltHash) {
-               _saltHash = saltHash.clone();
+           getRc4KeyData().setEncryptedVerifierHash(saltHash);
        }
 
        public short getSid() {
                return sid;
        }
-
-       public Object clone() {
+       
+    public Object clone() {
                // currently immutable
                return this;
        }
@@ -137,11 +290,7 @@ public final class FilePassRecord extends StandardRecord {
 
                buffer.append("[FILEPASS]\n");
                buffer.append("    .type = ").append(HexDump.shortToHex(_encryptionType)).append("\n");
-               buffer.append("    .info = ").append(HexDump.shortToHex(_encryptionInfo)).append("\n");
-               buffer.append("    .ver  = ").append(HexDump.shortToHex(_minorVersionNo)).append("\n");
-               buffer.append("    .docId= ").append(HexDump.toHex(_docId)).append("\n");
-               buffer.append("    .salt = ").append(HexDump.toHex(_saltData)).append("\n");
-               buffer.append("    .hash = ").append(HexDump.toHex(_saltHash)).append("\n");
+               _keyData.appendToString(buffer);
                buffer.append("[/FILEPASS]\n");
                return buffer.toString();
        }
index aac88b80c978a7d18e338827d6866e8428125239..61aa24769bb426a3a4ac48d73516843526cb91dc 100644 (file)
@@ -20,10 +20,17 @@ import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.List;
 
+import org.apache.poi.EncryptedDocumentException;
 import org.apache.poi.hssf.eventusermodel.HSSFEventFactory;
 import org.apache.poi.hssf.eventusermodel.HSSFListener;
+import org.apache.poi.hssf.record.FilePassRecord.Rc4KeyData;
+import org.apache.poi.hssf.record.FilePassRecord.XorKeyData;
 import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey;
-import org.apache.poi.EncryptedDocumentException;
+import org.apache.poi.hssf.record.crypto.Biff8RC4Key;
+import org.apache.poi.hssf.record.crypto.Biff8XORKey;
+import org.apache.poi.poifs.crypt.Decryptor;
+import org.apache.poi.util.POILogFactory;
+import org.apache.poi.util.POILogger;
 
 /**
  * A stream based way to get at complete records, with
@@ -48,6 +55,8 @@ public final class RecordFactoryInputStream {
                private final Record _lastRecord;
                private final boolean _hasBOFRecord;
 
+               private static POILogger log = POILogFactory.getLogger(StreamEncryptionInfo.class);
+               
                public StreamEncryptionInfo(RecordInputStream rs, List<Record> outputRecs) {
                        Record rec;
                        rs.nextRecord();
@@ -105,18 +114,34 @@ public final class RecordFactoryInputStream {
                public RecordInputStream createDecryptingStream(InputStream original) {
                        FilePassRecord fpr = _filePassRec;
                        String userPassword = Biff8EncryptionKey.getCurrentUserPassword();
+                       if (userPassword == null) {
+                           userPassword = Decryptor.DEFAULT_PASSWORD;
+                       }
 
                        Biff8EncryptionKey key;
-                       if (userPassword == null) {
-                               key = Biff8EncryptionKey.create(fpr.getDocId());
+                       if (fpr.getRc4KeyData() != null) {
+                           Rc4KeyData rc4 = fpr.getRc4KeyData();
+                           Biff8RC4Key rc4key = Biff8RC4Key.create(userPassword, rc4.getSalt());
+                           key = rc4key;
+                           if (!rc4key.validate(rc4.getEncryptedVerifier(), rc4.getEncryptedVerifierHash())) {
+                       throw new EncryptedDocumentException(
+                        (Decryptor.DEFAULT_PASSWORD.equals(userPassword) ? "Default" : "Supplied")
+                        + " password is invalid for salt/verifier/verifierHash");
+                           }
+                       } else if (fpr.getXorKeyData() != null) {
+                           XorKeyData xor = fpr.getXorKeyData();
+                           Biff8XORKey xorKey = Biff8XORKey.create(userPassword, xor.getKey());
+                           key = xorKey;
+                           
+                           if (!xorKey.validate(userPassword, xor.getVerifier())) {
+                    throw new EncryptedDocumentException(
+                               (Decryptor.DEFAULT_PASSWORD.equals(userPassword) ? "Default" : "Supplied")
+                               + " password is invalid for key/verifier");
+                           }
                        } else {
-                               key = Biff8EncryptionKey.create(userPassword, fpr.getDocId());
-                       }
-                       if (!key.validate(fpr.getSaltData(), fpr.getSaltHash())) {
-                               throw new EncryptedDocumentException(
-                                               (userPassword == null ? "Default" : "Supplied")
-                                               + " password is invalid for docId/saltData/saltHash");
+                           throw new EncryptedDocumentException("Crypto API not yet supported.");
                        }
+
                        return new RecordInputStream(original, key, _initialRecordsSize);
                }
 
diff --git a/src/java/org/apache/poi/hssf/record/crypto/Biff8Cipher.java b/src/java/org/apache/poi/hssf/record/crypto/Biff8Cipher.java
new file mode 100644 (file)
index 0000000..8ac742e
--- /dev/null
@@ -0,0 +1,30 @@
+/* ====================================================================\r
+   Licensed to the Apache Software Foundation (ASF) under one or more\r
+   contributor license agreements.  See the NOTICE file distributed with\r
+   this work for additional information regarding copyright ownership.\r
+   The ASF licenses this file to You under the Apache License, Version 2.0\r
+   (the "License"); you may not use this file except in compliance with\r
+   the License.  You may obtain a copy of the License at\r
+\r
+       http://www.apache.org/licenses/LICENSE-2.0\r
+\r
+   Unless required by applicable law or agreed to in writing, software\r
+   distributed under the License is distributed on an "AS IS" BASIS,\r
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+   See the License for the specific language governing permissions and\r
+   limitations under the License.\r
+==================================================================== */\r
+\r
+package org.apache.poi.hssf.record.crypto;\r
+\r
+\r
+public interface Biff8Cipher {\r
+    void startRecord(int currentSid);\r
+    void setNextRecordSize(int recordSize);\r
+    void skipTwoBytes();\r
+    void xor(byte[] buf, int pOffset, int pLen);\r
+    int xorByte(int rawVal);\r
+    int xorShort(int rawVal);\r
+    int xorInt(int rawVal);\r
+    long xorLong(long rawVal);\r
+}\r
index a56f911e2abf6bcb3e1506bc1b4504dd728b7ecf..f52a15d814121baca970e3a57ea5e09827b6452f 100644 (file)
@@ -19,6 +19,7 @@ package org.apache.poi.hssf.record.crypto;
 
 import java.io.InputStream;
 
+import org.apache.poi.EncryptedDocumentException;
 import org.apache.poi.hssf.record.BiffHeaderInput;
 import org.apache.poi.util.LittleEndianInput;
 import org.apache.poi.util.LittleEndianInputStream;
@@ -30,10 +31,16 @@ import org.apache.poi.util.LittleEndianInputStream;
 public final class Biff8DecryptingStream implements BiffHeaderInput, LittleEndianInput {
 
        private final LittleEndianInput _le;
-       private final Biff8RC4 _rc4;
+       private final Biff8Cipher _cipher;
 
        public Biff8DecryptingStream(InputStream in, int initialOffset, Biff8EncryptionKey key) {
-               _rc4 = new Biff8RC4(initialOffset, key);
+           if (key instanceof Biff8RC4Key) {
+               _cipher = new Biff8RC4(initialOffset, (Biff8RC4Key)key);
+           } else if (key instanceof Biff8XORKey) {
+               _cipher = new Biff8XOR(initialOffset, (Biff8XORKey)key);
+           } else {
+               throw new EncryptedDocumentException("Crypto API not supported yet.");
+           }
 
                if (in instanceof LittleEndianInput) {
                        // accessing directly is an optimisation
@@ -53,8 +60,8 @@ public final class Biff8DecryptingStream implements BiffHeaderInput, LittleEndia
         */
        public int readRecordSID() {
                int sid = _le.readUShort();
-               _rc4.skipTwoBytes();
-               _rc4.startRecord(sid);
+               _cipher.skipTwoBytes();
+               _cipher.startRecord(sid);
                return sid;
        }
 
@@ -63,7 +70,8 @@ public final class Biff8DecryptingStream implements BiffHeaderInput, LittleEndia
         */
        public int readDataSize() {
                int dataSize = _le.readUShort();
-               _rc4.skipTwoBytes();
+               _cipher.skipTwoBytes();
+               _cipher.setNextRecordSize(dataSize);
                return dataSize;
        }
 
@@ -82,30 +90,30 @@ public final class Biff8DecryptingStream implements BiffHeaderInput, LittleEndia
 
        public void readFully(byte[] buf, int off, int len) {
                _le.readFully(buf, off, len);
-               _rc4.xor(buf, off, len);
+               _cipher.xor(buf, off, len);
        }
 
 
        public int readUByte() {
-               return _rc4.xorByte(_le.readUByte());
+               return _cipher.xorByte(_le.readUByte());
        }
        public byte readByte() {
-               return (byte) _rc4.xorByte(_le.readUByte());
+               return (byte) _cipher.xorByte(_le.readUByte());
        }
 
 
        public int readUShort() {
-               return _rc4.xorShort(_le.readUShort());
+               return _cipher.xorShort(_le.readUShort());
        }
        public short readShort() {
-               return (short) _rc4.xorShort(_le.readUShort());
+               return (short) _cipher.xorShort(_le.readUShort());
        }
 
        public int readInt() {
-               return _rc4.xorInt(_le.readInt());
+               return _cipher.xorInt(_le.readInt());
        }
 
        public long readLong() {
-               return _rc4.xorLong(_le.readLong());
+               return _cipher.xorLong(_le.readLong());
        }
 }
index f39f0ccf1b253843393fa9ea1110fb1dd3bc2b64..3a28b81af1e5cabe3d9af9b8a3259ccefd61f915 100644 (file)
 ==================================================================== */
 package org.apache.poi.hssf.record.crypto;
 
-import java.io.ByteArrayOutputStream;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.util.Arrays;
+import javax.crypto.SecretKey;
 
+import org.apache.poi.EncryptedDocumentException;
 import org.apache.poi.hssf.usermodel.HSSFWorkbook;
-import org.apache.poi.util.HexDump;
-import org.apache.poi.util.LittleEndianOutputStream;
+import org.apache.poi.poifs.crypt.Decryptor;
 
-public final class Biff8EncryptionKey {
-       // these two constants coincidentally have the same value
-       private static final int KEY_DIGEST_LENGTH = 5;
-       private static final int PASSWORD_HASH_NUMBER_OF_BYTES_USED = 5;
-
-       private final byte[] _keyDigest;
+public abstract class Biff8EncryptionKey {
+       protected SecretKey _secretKey;
 
        /**
         * Create using the default password and a specified docId
-        * @param docId 16 bytes
+        * @param salt 16 bytes
         */
-       public static Biff8EncryptionKey create(byte[] docId) {
-               return new Biff8EncryptionKey(createKeyDigest("VelvetSweatshop", docId));
-       }
-       public static Biff8EncryptionKey create(String password, byte[] docIdData) {
-               return new Biff8EncryptionKey(createKeyDigest(password, docIdData));
-       }
-
-       Biff8EncryptionKey(byte[] keyDigest) {
-               if (keyDigest.length != KEY_DIGEST_LENGTH) {
-                       throw new IllegalArgumentException("Expected 5 byte key digest, but got " + HexDump.toHex(keyDigest));
-               }
-               _keyDigest = keyDigest;
+       public static Biff8EncryptionKey create(byte[] salt) {
+           return Biff8RC4Key.create(Decryptor.DEFAULT_PASSWORD, salt);
        }
-
-       static byte[] createKeyDigest(String password, byte[] docIdData) {
-               check16Bytes(docIdData, "docId");
-               int nChars = Math.min(password.length(), 16);
-               byte[] passwordData = new byte[nChars*2];
-               for (int i=0; i<nChars; i++) {
-                       char ch = password.charAt(i);
-                       passwordData[i*2+0] = (byte) ((ch << 0) & 0xFF);
-                       passwordData[i*2+1] = (byte) ((ch << 8) & 0xFF);
-               }
-
-               byte[] kd;
-               MessageDigest md5;
-               try {
-                       md5 = MessageDigest.getInstance("MD5");
-               } catch (NoSuchAlgorithmException e) {
-                       throw new RuntimeException(e);
-               }
-
-               md5.update(passwordData);
-               byte[] passwordHash = md5.digest();
-               md5.reset();
-
-               for (int i=0; i<16; i++) {
-                       md5.update(passwordHash, 0, PASSWORD_HASH_NUMBER_OF_BYTES_USED);
-                       md5.update(docIdData, 0, docIdData.length);
-               }
-               kd = md5.digest();
-               byte[] result = new byte[KEY_DIGEST_LENGTH];
-               System.arraycopy(kd, 0, result, 0, KEY_DIGEST_LENGTH);
-               return result;
+       
+       public static Biff8EncryptionKey create(String password, byte[] salt) {
+        return Biff8RC4Key.create(password, salt);
        }
 
        /**
         * @return <code>true</code> if the keyDigest is compatible with the specified saltData and saltHash
         */
        public boolean validate(byte[] saltData, byte[] saltHash) {
-               check16Bytes(saltData, "saltData");
-               check16Bytes(saltHash, "saltHash");
-
-               // validation uses the RC4 for block zero
-               RC4 rc4 = createRC4(0);
-               byte[] saltDataPrime = saltData.clone();
-               rc4.encrypt(saltDataPrime);
-
-               byte[] saltHashPrime = saltHash.clone();
-               rc4.encrypt(saltHashPrime);
-
-               MessageDigest md5;
-               try {
-                       md5 = MessageDigest.getInstance("MD5");
-               } catch (NoSuchAlgorithmException e) {
-                       throw new RuntimeException(e);
-               }
-               md5.update(saltDataPrime);
-               byte[] finalSaltResult = md5.digest();
-
-               if (false) { // set true to see a valid saltHash value
-                       byte[] saltHashThatWouldWork = xor(saltHash, xor(saltHashPrime, finalSaltResult));
-                       System.out.println(HexDump.toHex(saltHashThatWouldWork));
-               }
-
-               return Arrays.equals(saltHashPrime, finalSaltResult);
-       }
-
-       private static byte[] xor(byte[] a, byte[] b) {
-               byte[] c = new byte[a.length];
-               for (int i = 0; i < c.length; i++) {
-                       c[i] = (byte) (a[i] ^ b[i]);
-               }
-               return c;
+           throw new EncryptedDocumentException("validate is not supported (in super-class).");
        }
-       private static void check16Bytes(byte[] data, String argName) {
-               if (data.length != 16) {
-                       throw new IllegalArgumentException("Expected 16 byte " + argName + ", but got " + HexDump.toHex(data));
-               }
-       }
-
-       /**
-        * The {@link RC4} instance needs to be changed every 1024 bytes.
-        * @param keyBlockNo used to seed the newly created {@link RC4}
-        */
-       RC4 createRC4(int keyBlockNo) {
-               MessageDigest md5;
-               try {
-                       md5 = MessageDigest.getInstance("MD5");
-               } catch (NoSuchAlgorithmException e) {
-                       throw new RuntimeException(e);
-               }
-
-               md5.update(_keyDigest);
-               ByteArrayOutputStream baos = new ByteArrayOutputStream(4);
-               new LittleEndianOutputStream(baos).writeInt(keyBlockNo);
-               md5.update(baos.toByteArray());
-
-               byte[] digest = md5.digest();
-               return new RC4(digest);
-       }
-
 
        /**
         * Stores the BIFF8 encryption/decryption password for the current thread.  This has been done
index edda6fff9535de10e1268118ed4d29cd3c6d3dee..9d0275fec31fe165b7afb3276263b4676c8e0a95 100644 (file)
 
 package org.apache.poi.hssf.record.crypto;
 
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+import javax.crypto.Cipher;
+import javax.crypto.ShortBufferException;
+
+import org.apache.poi.EncryptedDocumentException;
 import org.apache.poi.hssf.record.BOFRecord;
 import org.apache.poi.hssf.record.FilePassRecord;
 import org.apache.poi.hssf.record.InterfaceHdrRecord;
 
 /**
  * Used for both encrypting and decrypting BIFF8 streams. The internal
- * {@link RC4} instance is renewed (re-keyed) every 1024 bytes.
- *
- * @author Josh Micich
+ * {@link Cipher} instance is renewed (re-keyed) every 1024 bytes.
  */
-final class Biff8RC4 {
+final class Biff8RC4 implements Biff8Cipher {
 
        private static final int RC4_REKEYING_INTERVAL = 1024;
 
-       private RC4 _rc4;
+       private Cipher _rc4;
+       
        /**
-        * This field is used to keep track of when to change the {@link RC4}
+        * This field is used to keep track of when to change the {@link Cipher}
         * instance. The change occurs every 1024 bytes. Every byte passed over is
         * counted.
         */
@@ -41,42 +47,49 @@ final class Biff8RC4 {
        private int _nextRC4BlockStart;
        private int _currentKeyIndex;
        private boolean _shouldSkipEncryptionOnCurrentRecord;
+    private final Biff8RC4Key _key;
+    private ByteBuffer _buffer = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN);
 
-       private final Biff8EncryptionKey _key;
-
-       public Biff8RC4(int initialOffset, Biff8EncryptionKey key) {
+       public Biff8RC4(int initialOffset, Biff8RC4Key key) {
                if (initialOffset >= RC4_REKEYING_INTERVAL) {
                        throw new RuntimeException("initialOffset (" + initialOffset + ")>"
                                        + RC4_REKEYING_INTERVAL + " not supported yet");
                }
                _key = key;
+        _rc4 = _key.getCipher();
                _streamPos = 0;
                rekeyForNextBlock();
                _streamPos = initialOffset;
-               for (int i = initialOffset; i > 0; i--) {
-                       _rc4.output();
-               }
                _shouldSkipEncryptionOnCurrentRecord = false;
+               
+           encryptBytes(new byte[initialOffset], 0, initialOffset);
        }
+       
 
        private void rekeyForNextBlock() {
                _currentKeyIndex = _streamPos / RC4_REKEYING_INTERVAL;
-               _rc4 = _key.createRC4(_currentKeyIndex);
+               _key.initCipherForBlock(_rc4, _currentKeyIndex);
                _nextRC4BlockStart = (_currentKeyIndex + 1) * RC4_REKEYING_INTERVAL;
        }
 
-       private int getNextRC4Byte() {
-               if (_streamPos >= _nextRC4BlockStart) {
-                       rekeyForNextBlock();
-               }
-               byte mask = _rc4.output();
-               _streamPos++;
-               if (_shouldSkipEncryptionOnCurrentRecord) {
-                       return 0;
-               }
-               return mask & 0xFF;
+       private void encryptBytes(byte data[], int offset, final int bytesToRead)  {
+           if (bytesToRead == 0) return;
+           
+           if (_shouldSkipEncryptionOnCurrentRecord) {
+            // even when encryption is skipped, we need to update the cipher
+               byte dataCpy[] = new byte[bytesToRead];
+               System.arraycopy(data, offset, dataCpy, 0, bytesToRead);
+               data = dataCpy;
+               offset = 0;
+           }
+           
+        try {
+            _rc4.update(data, offset, bytesToRead, data, offset);
+        } catch (ShortBufferException e) {
+            throw new EncryptedDocumentException("input buffer too small", e);
+        }
        }
-
+       
        public void startRecord(int currentSid) {
                _shouldSkipEncryptionOnCurrentRecord = isNeverEncryptedRecord(currentSid);
        }
@@ -110,19 +123,18 @@ final class Biff8RC4 {
 
        /**
         * Used when BIFF header fields (sid, size) are being read. The internal
-        * {@link RC4} instance must step even when unencrypted bytes are read
+        * {@link Cipher} instance must step even when unencrypted bytes are read
         */
        public void skipTwoBytes() {
-               getNextRC4Byte();
-               getNextRC4Byte();
+           xor(_buffer.array(), 0, 2);
        }
-
+       
        public void xor(byte[] buf, int pOffset, int pLen) {
                int nLeftInBlock;
                nLeftInBlock = _nextRC4BlockStart - _streamPos;
                if (pLen <= nLeftInBlock) {
-                       // simple case - this read does not cross key blocks
-                       _rc4.encrypt(buf, pOffset, pLen);
+            // simple case - this read does not cross key blocks
+                   encryptBytes(buf, pOffset, pLen);
                        _streamPos += pLen;
                        return;
                }
@@ -133,7 +145,7 @@ final class Biff8RC4 {
                // start by using the rest of the current block
                if (len > nLeftInBlock) {
                        if (nLeftInBlock > 0) {
-                               _rc4.encrypt(buf, offset, nLeftInBlock);
+                   encryptBytes(buf, offset, nLeftInBlock);
                                _streamPos += nLeftInBlock;
                                offset += nLeftInBlock;
                                len -= nLeftInBlock;
@@ -142,56 +154,42 @@ final class Biff8RC4 {
                }
                // all full blocks following
                while (len > RC4_REKEYING_INTERVAL) {
-                       _rc4.encrypt(buf, offset, RC4_REKEYING_INTERVAL);
+            encryptBytes(buf, offset, RC4_REKEYING_INTERVAL);
                        _streamPos += RC4_REKEYING_INTERVAL;
                        offset += RC4_REKEYING_INTERVAL;
                        len -= RC4_REKEYING_INTERVAL;
                        rekeyForNextBlock();
                }
                // finish with incomplete block
-               _rc4.encrypt(buf, offset, len);
+        encryptBytes(buf, offset, len);
                _streamPos += len;
        }
 
        public int xorByte(int rawVal) {
-               int mask = getNextRC4Byte();
-               return (byte) (rawVal ^ mask);
+           _buffer.put(0, (byte)rawVal);
+           xor(_buffer.array(), 0, 1);
+               return _buffer.get(0);
        }
 
        public int xorShort(int rawVal) {
-               int b0 = getNextRC4Byte();
-               int b1 = getNextRC4Byte();
-               int mask = (b1 << 8) + (b0 << 0);
-               return rawVal ^ mask;
+           _buffer.putShort(0, (short)rawVal);
+           xor(_buffer.array(), 0, 2);
+               return _buffer.getShort(0);
        }
 
        public int xorInt(int rawVal) {
-               int b0 = getNextRC4Byte();
-               int b1 = getNextRC4Byte();
-               int b2 = getNextRC4Byte();
-               int b3 = getNextRC4Byte();
-               int mask = (b3 << 24) + (b2 << 16) + (b1 << 8) + (b0 << 0);
-               return rawVal ^ mask;
+           _buffer.putInt(0, rawVal);
+           xor(_buffer.array(), 0, 4);
+               return _buffer.getInt(0);
        }
 
        public long xorLong(long rawVal) {
-               int b0 = getNextRC4Byte();
-               int b1 = getNextRC4Byte();
-               int b2 = getNextRC4Byte();
-               int b3 = getNextRC4Byte();
-               int b4 = getNextRC4Byte();
-               int b5 = getNextRC4Byte();
-               int b6 = getNextRC4Byte();
-               int b7 = getNextRC4Byte();
-               long mask =
-                         (((long)b7) << 56)
-                       + (((long)b6) << 48)
-                       + (((long)b5) << 40)
-                       + (((long)b4) << 32)
-                       + (((long)b3) << 24)
-                       + (b2 << 16)
-                       + (b1 << 8)
-                       + (b0 << 0);
-               return rawVal ^ mask;
+        _buffer.putLong(0, rawVal);
+        xor(_buffer.array(), 0, 8);
+        return _buffer.getLong(0);
+       }
+       
+       public void setNextRecordSize(int recordSize) {
+           /* no-op */
        }
 }
diff --git a/src/java/org/apache/poi/hssf/record/crypto/Biff8RC4Key.java b/src/java/org/apache/poi/hssf/record/crypto/Biff8RC4Key.java
new file mode 100644 (file)
index 0000000..2894280
--- /dev/null
@@ -0,0 +1,155 @@
+/* ====================================================================\r
+   Licensed to the Apache Software Foundation (ASF) under one or more\r
+   contributor license agreements.  See the NOTICE file distributed with\r
+   this work for additional information regarding copyright ownership.\r
+   The ASF licenses this file to You under the Apache License, Version 2.0\r
+   (the "License"); you may not use this file except in compliance with\r
+   the License.  You may obtain a copy of the License at\r
+\r
+       http://www.apache.org/licenses/LICENSE-2.0\r
+\r
+   Unless required by applicable law or agreed to in writing, software\r
+   distributed under the License is distributed on an "AS IS" BASIS,\r
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+   See the License for the specific language governing permissions and\r
+   limitations under the License.\r
+==================================================================== */\r
+\r
+package org.apache.poi.hssf.record.crypto;\r
+\r
+import java.security.GeneralSecurityException;\r
+import java.security.MessageDigest;\r
+import java.util.Arrays;\r
+\r
+import javax.crypto.Cipher;\r
+import javax.crypto.ShortBufferException;\r
+import javax.crypto.spec.SecretKeySpec;\r
+\r
+import org.apache.poi.EncryptedDocumentException;\r
+import org.apache.poi.poifs.crypt.CipherAlgorithm;\r
+import org.apache.poi.poifs.crypt.CryptoFunctions;\r
+import org.apache.poi.poifs.crypt.HashAlgorithm;\r
+import org.apache.poi.util.HexDump;\r
+import org.apache.poi.util.LittleEndian;\r
+import org.apache.poi.util.LittleEndianConsts;\r
+import org.apache.poi.util.POILogFactory;\r
+import org.apache.poi.util.POILogger;\r
+\r
+public class Biff8RC4Key extends Biff8EncryptionKey {\r
+    // these two constants coincidentally have the same value\r
+    public static final int KEY_DIGEST_LENGTH = 5;\r
+    private static final int PASSWORD_HASH_NUMBER_OF_BYTES_USED = 5;\r
+\r
+    private static POILogger log = POILogFactory.getLogger(Biff8RC4Key.class);\r
+    \r
+    Biff8RC4Key(byte[] keyDigest) {\r
+        if (keyDigest.length != KEY_DIGEST_LENGTH) {\r
+            throw new IllegalArgumentException("Expected 5 byte key digest, but got " + HexDump.toHex(keyDigest));\r
+        }\r
+\r
+        CipherAlgorithm ca = CipherAlgorithm.rc4;\r
+        _secretKey = new SecretKeySpec(keyDigest, ca.jceId);\r
+    }\r
+\r
+    /**\r
+     * Create using the default password and a specified docId\r
+     * @param salt 16 bytes\r
+     */\r
+    public static Biff8RC4Key create(String password, byte[] salt) {\r
+        return new Biff8RC4Key(createKeyDigest(password, salt));\r
+    }\r
+    \r
+    /**\r
+     * @return <code>true</code> if the keyDigest is compatible with the specified saltData and saltHash\r
+     */\r
+    public boolean validate(byte[] verifier, byte[] verifierHash) {\r
+        check16Bytes(verifier, "verifier");\r
+        check16Bytes(verifierHash, "verifierHash");\r
+\r
+        // validation uses the RC4 for block zero\r
+        Cipher rc4 = getCipher();\r
+        initCipherForBlock(rc4, 0);\r
+        \r
+        byte[] verifierPrime = verifier.clone();\r
+        byte[] verifierHashPrime = verifierHash.clone();\r
+\r
+        try {\r
+            rc4.update(verifierPrime, 0, verifierPrime.length, verifierPrime);\r
+            rc4.update(verifierHashPrime, 0, verifierHashPrime.length, verifierHashPrime);\r
+        } catch (ShortBufferException e) {\r
+            throw new EncryptedDocumentException("buffer too short", e);\r
+        }\r
+\r
+        MessageDigest md5 = CryptoFunctions.getMessageDigest(HashAlgorithm.md5);\r
+        md5.update(verifierPrime);\r
+        byte[] finalVerifierResult = md5.digest();\r
+\r
+        if (log.check(POILogger.DEBUG)) {\r
+            byte[] verifierHashThatWouldWork = xor(verifierHash, xor(verifierHashPrime, finalVerifierResult));\r
+            log.log(POILogger.DEBUG, "valid verifierHash value", HexDump.toHex(verifierHashThatWouldWork));\r
+        }\r
+\r
+        return Arrays.equals(verifierHashPrime, finalVerifierResult);\r
+    }\r
+    \r
+    Cipher getCipher() {\r
+        CipherAlgorithm ca = CipherAlgorithm.rc4;\r
+        Cipher rc4 = CryptoFunctions.getCipher(_secretKey, ca, null, null, Cipher.ENCRYPT_MODE);\r
+        return rc4;\r
+    }\r
+    \r
+    static byte[] createKeyDigest(String password, byte[] docIdData) {\r
+        check16Bytes(docIdData, "docId");\r
+        int nChars = Math.min(password.length(), 16);\r
+        byte[] passwordData = new byte[nChars*2];\r
+        for (int i=0; i<nChars; i++) {\r
+            char ch = password.charAt(i);\r
+            passwordData[i*2+0] = (byte) ((ch << 0) & 0xFF);\r
+            passwordData[i*2+1] = (byte) ((ch << 8) & 0xFF);\r
+        }\r
+\r
+        MessageDigest md5 = CryptoFunctions.getMessageDigest(HashAlgorithm.md5);\r
+        md5.update(passwordData);\r
+        byte[] passwordHash = md5.digest();\r
+        md5.reset();\r
+\r
+        for (int i=0; i<16; i++) {\r
+            md5.update(passwordHash, 0, PASSWORD_HASH_NUMBER_OF_BYTES_USED);\r
+            md5.update(docIdData, 0, docIdData.length);\r
+        }\r
+        \r
+        byte[] result = CryptoFunctions.getBlock0(md5.digest(), KEY_DIGEST_LENGTH);\r
+        return result;\r
+    }\r
+\r
+    void initCipherForBlock(Cipher rc4, int keyBlockNo) {\r
+        byte buf[] = new byte[LittleEndianConsts.INT_SIZE]; \r
+        LittleEndian.putInt(buf, 0, keyBlockNo);\r
+        \r
+        MessageDigest md5 = CryptoFunctions.getMessageDigest(HashAlgorithm.md5);\r
+        md5.update(_secretKey.getEncoded());\r
+        md5.update(buf);\r
+\r
+        SecretKeySpec skeySpec = new SecretKeySpec(md5.digest(), _secretKey.getAlgorithm());\r
+        try {\r
+            rc4.init(Cipher.ENCRYPT_MODE, skeySpec);\r
+        } catch (GeneralSecurityException e) {\r
+            throw new EncryptedDocumentException("Can't rekey for next block", e);\r
+        }\r
+    }\r
+    \r
+    private static byte[] xor(byte[] a, byte[] b) {\r
+        byte[] c = new byte[a.length];\r
+        for (int i = 0; i < c.length; i++) {\r
+            c[i] = (byte) (a[i] ^ b[i]);\r
+        }\r
+        return c;\r
+    }\r
+    private static void check16Bytes(byte[] data, String argName) {\r
+        if (data.length != 16) {\r
+            throw new IllegalArgumentException("Expected 16 byte " + argName + ", but got " + HexDump.toHex(data));\r
+        }\r
+    }\r
+    \r
+\r
+}\r
diff --git a/src/java/org/apache/poi/hssf/record/crypto/Biff8XOR.java b/src/java/org/apache/poi/hssf/record/crypto/Biff8XOR.java
new file mode 100644 (file)
index 0000000..e32f2a4
--- /dev/null
@@ -0,0 +1,153 @@
+/* ====================================================================\r
+   Licensed to the Apache Software Foundation (ASF) under one or more\r
+   contributor license agreements.  See the NOTICE file distributed with\r
+   this work for additional information regarding copyright ownership.\r
+   The ASF licenses this file to You under the Apache License, Version 2.0\r
+   (the "License"); you may not use this file except in compliance with\r
+   the License.  You may obtain a copy of the License at\r
+\r
+       http://www.apache.org/licenses/LICENSE-2.0\r
+\r
+   Unless required by applicable law or agreed to in writing, software\r
+   distributed under the License is distributed on an "AS IS" BASIS,\r
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+   See the License for the specific language governing permissions and\r
+   limitations under the License.\r
+==================================================================== */\r
+\r
+package org.apache.poi.hssf.record.crypto;\r
+\r
+import java.nio.ByteBuffer;\r
+import java.nio.ByteOrder;\r
+\r
+import javax.crypto.Cipher;\r
+\r
+import org.apache.poi.hssf.record.BOFRecord;\r
+import org.apache.poi.hssf.record.FilePassRecord;\r
+import org.apache.poi.hssf.record.InterfaceHdrRecord;\r
+\r
+public class Biff8XOR implements Biff8Cipher {\r
+\r
+    private final Biff8XORKey _key;\r
+    private ByteBuffer _buffer = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN);\r
+    private boolean _shouldSkipEncryptionOnCurrentRecord;\r
+    private final int _initialOffset;\r
+    private int _dataLength = 0;\r
+    private int _xorArrayIndex = 0;\r
+    \r
+    public Biff8XOR(int initialOffset, Biff8XORKey key) {\r
+        _key = key;\r
+        _initialOffset = initialOffset;\r
+\r
+    }\r
+    \r
+    public void startRecord(int currentSid) {\r
+        _shouldSkipEncryptionOnCurrentRecord = isNeverEncryptedRecord(currentSid);\r
+    }\r
+\r
+    public void setNextRecordSize(int recordSize) {\r
+        /*\r
+         * From: http://social.msdn.microsoft.com/Forums/en-US/3dadbed3-0e68-4f11-8b43-3a2328d9ebd5\r
+         * \r
+         * The initial value for XorArrayIndex is as follows:\r
+         * XorArrayIndex = (FileOffset + Data.Length) % 16\r
+         * \r
+         * The FileOffset variable in this context is the stream offset into the Workbook stream at\r
+         * the time we are about to write each of the bytes of the record data.\r
+         * This (the value) is then incremented after each byte is written. \r
+         */\r
+        _xorArrayIndex = (_initialOffset+_dataLength+recordSize) % 16;\r
+    }\r
+    \r
+    \r
+    /**\r
+     * TODO: Additionally, the lbPlyPos (position_of_BOF) field of the BoundSheet8 record MUST NOT be encrypted.\r
+     *\r
+     * @return <code>true</code> if record type specified by <tt>sid</tt> is never encrypted\r
+     */\r
+    private static boolean isNeverEncryptedRecord(int sid) {\r
+        switch (sid) {\r
+            case BOFRecord.sid:\r
+                // sheet BOFs for sure\r
+                // TODO - find out about chart BOFs\r
+\r
+            case InterfaceHdrRecord.sid:\r
+                // don't know why this record doesn't seem to get encrypted\r
+\r
+            case FilePassRecord.sid:\r
+                // this only really counts when writing because FILEPASS is read early\r
+\r
+            // UsrExcl(0x0194)\r
+            // FileLock\r
+            // RRDInfo(0x0196)\r
+            // RRDHead(0x0138)\r
+\r
+                return true;\r
+        }\r
+        return false;\r
+    }\r
+\r
+    /**\r
+     * Used when BIFF header fields (sid, size) are being read. The internal\r
+     * {@link Cipher} instance must step even when unencrypted bytes are read\r
+     */\r
+    public void skipTwoBytes() {\r
+        _dataLength += 2;\r
+    }\r
+\r
+    /**\r
+     * Decrypts a xor obfuscated byte array.\r
+     * The data is decrypted in-place\r
+     * \r
+     * @see <a href="http://msdn.microsoft.com/en-us/library/dd908506.aspx">2.3.7.3 Binary Document XOR Data Transformation Method 1</a>\r
+     */\r
+    public void xor(byte[] buf, int pOffset, int pLen) {\r
+        if (_shouldSkipEncryptionOnCurrentRecord) {\r
+            _dataLength += pLen;\r
+            return;\r
+        }\r
+        \r
+        // The following is taken from the Libre Office implementation\r
+        // It seems that the encrypt and decrypt method is mixed up\r
+        // in the MS-OFFCRYPTO docs\r
+\r
+        byte xorArray[] = _key._secretKey.getEncoded();\r
+        \r
+        for (int i=0; i<pLen; i++) {\r
+            byte value = buf[pOffset+i];\r
+            value = rotateLeft(value, 3);\r
+            value ^= xorArray[_xorArrayIndex];\r
+            buf[pOffset+i] = value;\r
+            _xorArrayIndex = (_xorArrayIndex + 1) % 16;\r
+            _dataLength++;\r
+        }\r
+    }\r
+    \r
+    private static byte rotateLeft(byte bits, int shift) {\r
+        return (byte)(((bits & 0xff) << shift) | ((bits & 0xff) >>> (8 - shift)));\r
+    }\r
+    \r
+    public int xorByte(int rawVal) {\r
+        _buffer.put(0, (byte)rawVal);\r
+        xor(_buffer.array(), 0, 1);\r
+        return _buffer.get(0);\r
+    }\r
+\r
+    public int xorShort(int rawVal) {\r
+        _buffer.putShort(0, (short)rawVal);\r
+        xor(_buffer.array(), 0, 2);\r
+        return _buffer.getShort(0);\r
+    }\r
+\r
+    public int xorInt(int rawVal) {\r
+        _buffer.putInt(0, rawVal);\r
+        xor(_buffer.array(), 0, 4);\r
+        return _buffer.getInt(0);\r
+    }\r
+\r
+    public long xorLong(long rawVal) {\r
+        _buffer.putLong(0, rawVal);\r
+        xor(_buffer.array(), 0, 8);\r
+        return _buffer.getLong(0);\r
+    }\r
+}\r
diff --git a/src/java/org/apache/poi/hssf/record/crypto/Biff8XORKey.java b/src/java/org/apache/poi/hssf/record/crypto/Biff8XORKey.java
new file mode 100644 (file)
index 0000000..7f2903d
--- /dev/null
@@ -0,0 +1,44 @@
+/* ====================================================================\r
+   Licensed to the Apache Software Foundation (ASF) under one or more\r
+   contributor license agreements.  See the NOTICE file distributed with\r
+   this work for additional information regarding copyright ownership.\r
+   The ASF licenses this file to You under the Apache License, Version 2.0\r
+   (the "License"); you may not use this file except in compliance with\r
+   the License.  You may obtain a copy of the License at\r
+\r
+       http://www.apache.org/licenses/LICENSE-2.0\r
+\r
+   Unless required by applicable law or agreed to in writing, software\r
+   distributed under the License is distributed on an "AS IS" BASIS,\r
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+   See the License for the specific language governing permissions and\r
+   limitations under the License.\r
+==================================================================== */\r
+\r
+package org.apache.poi.hssf.record.crypto;\r
+\r
+import javax.crypto.spec.SecretKeySpec;\r
+\r
+import org.apache.poi.poifs.crypt.CryptoFunctions;\r
+\r
+\r
+public class Biff8XORKey extends Biff8EncryptionKey {\r
+    final int _xorKey;\r
+    \r
+    public Biff8XORKey(String password, int xorKey) {\r
+        _xorKey = xorKey;\r
+        byte xorArray[] = CryptoFunctions.createXorArray1(password);\r
+        _secretKey = new SecretKeySpec(xorArray, "XOR");\r
+    }\r
+    \r
+    public static Biff8XORKey create(String password, int xorKey) {\r
+        return new Biff8XORKey(password, xorKey);\r
+    }\r
+\r
+    public boolean validate(String password, int verifier) {\r
+        int keyComp = CryptoFunctions.createXorKey1(password);\r
+        int verifierComp = CryptoFunctions.createXorVerifier1(password);\r
+\r
+        return (_xorKey == keyComp && verifierComp == verifier);\r
+    }\r
+}\r
diff --git a/src/java/org/apache/poi/hssf/record/crypto/RC4.java b/src/java/org/apache/poi/hssf/record/crypto/RC4.java
deleted file mode 100644 (file)
index a4abcfa..0000000
+++ /dev/null
@@ -1,90 +0,0 @@
-/* ====================================================================
-   Licensed to the Apache Software Foundation (ASF) under one or more
-   contributor license agreements.  See the NOTICE file distributed with
-   this work for additional information regarding copyright ownership.
-   The ASF licenses this file to You under the Apache License, Version 2.0
-   (the "License"); you may not use this file except in compliance with
-   the License.  You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-   Unless required by applicable law or agreed to in writing, software
-   distributed under the License is distributed on an "AS IS" BASIS,
-   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-   See the License for the specific language governing permissions and
-   limitations under the License.
-==================================================================== */
-
-package org.apache.poi.hssf.record.crypto;
-
-import org.apache.poi.util.HexDump;
-
-/**
- * Simple implementation of the alleged RC4 algorithm.
- *
- * Inspired by <A HREF="http://en.wikipedia.org/wiki/RC4">wikipedia's RC4 article</A>
- *
- * @author Josh Micich
- */
-final class RC4 {
-
-    private int _i, _j;
-       private final byte[] _s = new byte[256];
-
-       public RC4(byte[] key) {
-               int key_length = key.length;
-
-           for (int i = 0; i < 256; i++)
-               _s[i] = (byte)i;
-
-           for (int i=0, j=0; i < 256; i++) {
-               byte temp;
-
-               j = (j + key[i % key_length] + _s[i]) & 255;
-               temp = _s[i];
-               _s[i] = _s[j];
-               _s[j] = temp;
-           }
-
-           _i = 0;
-           _j = 0;
-       }
-
-       public byte output() {
-           byte temp;
-           _i = (_i + 1) & 255;
-           _j = (_j + _s[_i]) & 255;
-
-           temp = _s[_i];
-           _s[_i] = _s[_j];
-           _s[_j] = temp;
-
-           return _s[(_s[_i] + _s[_j]) & 255];
-       }
-
-       public void encrypt(byte[] in) {
-               for (int i = 0; i < in.length; i++) {
-                       in[i] = (byte) (in[i] ^ output());
-               }
-       }
-       public void encrypt(byte[] in, int offset, int len) {
-               int end = offset+len;
-               for (int i = offset; i < end; i++) {
-                       in[i] = (byte) (in[i] ^ output());
-               }
-
-       }
-       @Override
-       public String toString() {
-               StringBuffer sb = new StringBuffer();
-
-               sb.append(getClass().getName()).append(" [");
-               sb.append("i=").append(_i);
-               sb.append(" j=").append(_j);
-               sb.append("]");
-               sb.append("\n");
-               sb.append(HexDump.dump(_s, 0, 0));
-
-               return sb.toString();
-       }
-}
index f5f52b93f4c5892492b73adc36380d5241cce530..f9f970ade9fd18dad5b56efc11ddb01181aee5d5 100644 (file)
@@ -289,6 +289,12 @@ public class CryptoFunctions {
         0x313E, 0x1872, 0xE139, 0xD40F, 0x84F9, 0x280C, 0xA96A, \r
         0x4EC3\r
     };\r
+\r
+    private static final byte PadArray[] = {\r
+        (byte)0xBB, (byte)0xFF, (byte)0xFF, (byte)0xBA, (byte)0xFF,\r
+        (byte)0xFF, (byte)0xB9, (byte)0x80, (byte)0x00, (byte)0xBE,\r
+        (byte)0x0F, (byte)0x00, (byte)0xBF, (byte)0x0F, (byte)0x00\r
+    };\r
     \r
     private static final int EncryptionMatrix[][] = {\r
         /* char 1  */ {0xAEFC, 0x4DD9, 0x9BB2, 0x2745, 0x4E8A, 0x9D14, 0x2A09},\r
@@ -309,20 +315,18 @@ public class CryptoFunctions {
     };\r
 \r
     /**\r
-     * This method generates the xored-hashed password for word documents &lt; 2007.\r
+     * This method generates the xor verifier for word documents &lt; 2007 (method 2).\r
      * Its output will be used as password input for the newer word generations which\r
      * utilize a real hashing algorithm like sha1.\r
      * \r
-     * Although the code was taken from the "see"-link below, this looks similar\r
-     * to the method in [MS-OFFCRYPTO] 2.3.7.2 Binary Document XOR Array Initialization Method 1. \r
-     *\r
-     * @param password\r
+     * @param password the password\r
      * @return the hashed password\r
      * \r
+     * @see <a href="http://msdn.microsoft.com/en-us/library/dd905229.aspx">2.3.7.4 Binary Document Password Verifier Derivation Method 2</a>\r
      * @see <a href="http://blogs.msdn.com/b/vsod/archive/2010/04/05/how-to-set-the-editing-restrictions-in-word-using-open-xml-sdk-2-0.aspx">How to set the editing restrictions in Word using Open XML SDK 2.0</a>\r
      * @see <a href="http://www.aspose.com/blogs/aspose-blogs/vladimir-averkin/archive/2007/08/20/funny-how-the-new-powerful-cryptography-implemented-in-word-2007-turns-it-into-a-perfect-tool-for-document-password-removal.html">Funny: How the new powerful cryptography implemented in Word 2007 turns it into a perfect tool for document password removal.</a>\r
      */\r
-    public static int xorHashPasswordAsInt(String password) {\r
+    public static int createXorVerifier2(String password) {\r
         //Array to hold Key Values\r
         byte[] generatedKey = new byte[4];\r
 \r
@@ -391,7 +395,7 @@ public class CryptoFunctions {
      * This method generates the xored-hashed password for word documents &lt; 2007.\r
      */\r
     public static String xorHashPassword(String password) {\r
-        int hashedPassword = xorHashPasswordAsInt(password);\r
+        int hashedPassword = createXorVerifier2(password);\r
         return String.format("%1$08X", hashedPassword);\r
     }\r
     \r
@@ -400,7 +404,7 @@ public class CryptoFunctions {
      * processing in word documents 2007 and newer, which utilize a real hashing algorithm like sha1.\r
      */\r
     public static String xorHashPasswordReversed(String password) {\r
-        int hashedPassword = xorHashPasswordAsInt(password);\r
+        int hashedPassword = createXorVerifier2(password);\r
         \r
         return String.format("%1$02X%2$02X%3$02X%4$02X"\r
             , ( hashedPassword >>> 0 ) & 0xFF\r
@@ -409,4 +413,71 @@ public class CryptoFunctions {
             , ( hashedPassword >>> 24 ) & 0xFF\r
         );\r
     }\r
+\r
+    /**\r
+     * Create the verifier for xor obfuscation (method 1)\r
+     *\r
+     * @see <a href="http://msdn.microsoft.com/en-us/library/dd926947.aspx">2.3.7.1 Binary Document Password Verifier Derivation Method 1</a>\r
+     * @see <a href="http://msdn.microsoft.com/en-us/library/dd905229.aspx">2.3.7.4 Binary Document Password Verifier Derivation Method 2</a>\r
+     * \r
+     * @param password the password\r
+     * @return the verifier\r
+     */\r
+    public static int createXorVerifier1(String password) {\r
+        // the verifier for method 1 is part of the verifier for method 2\r
+        // so we simply chop it from there\r
+        return createXorVerifier2(password) & 0xFFFF;\r
+    }\r
\r
+    /**\r
+     * Create the xor key for xor obfuscation, which is used to create the xor array (method 1)\r
+     *\r
+     * @see <a href="http://msdn.microsoft.com/en-us/library/dd924704.aspx">2.3.7.2 Binary Document XOR Array Initialization Method 1</a>\r
+     * @see <a href="http://msdn.microsoft.com/en-us/library/dd905229.aspx">2.3.7.4 Binary Document Password Verifier Derivation Method 2</a>\r
+     * \r
+     * @param password the password\r
+     * @return the xor key\r
+     */\r
+    public static int createXorKey1(String password) {\r
+        // the xor key for method 1 is part of the verifier for method 2\r
+        // so we simply chop it from there\r
+        return createXorVerifier2(password) >>> 16;\r
+    }\r
+\r
+    /**\r
+     * Creates an byte array for xor obfuscation (method 1) \r
+     *\r
+     * @see <a href="http://msdn.microsoft.com/en-us/library/dd924704.aspx">2.3.7.2 Binary Document XOR Array Initialization Method 1</a>\r
+     * @see <a href="http://docs.libreoffice.org/oox/html/binarycodec_8cxx_source.html">Libre Office implementation</a>\r
+     *\r
+     * @param password the password\r
+     * @return the byte array for xor obfuscation\r
+     */\r
+    public static byte[] createXorArray1(String password) {\r
+        if (password.length() > 15) password = password.substring(0, 15);\r
+        byte passBytes[] = password.getBytes(Charset.forName("ASCII"));\r
+        \r
+        // this code is based on the libre office implementation.\r
+        // The MS-OFFCRYPTO misses some infos about the various rotation sizes \r
+        byte obfuscationArray[] = new byte[16];\r
+        System.arraycopy(passBytes, 0, obfuscationArray, 0, passBytes.length);\r
+        System.arraycopy(PadArray, 0, obfuscationArray, passBytes.length, PadArray.length-passBytes.length+1);\r
+        \r
+        int xorKey = createXorKey1(password);\r
+        \r
+        // rotation of key values is application dependent\r
+        int nRotateSize = 2; /* Excel = 2; Word = 7 */\r
+        \r
+        byte baseKeyLE[] = { (byte)(xorKey & 0xFF), (byte)((xorKey >>> 8) & 0xFF) };\r
+        for (int i=0; i<obfuscationArray.length; i++) {\r
+            obfuscationArray[i] ^= baseKeyLE[i&1];\r
+            obfuscationArray[i] = rotateLeft(obfuscationArray[i], nRotateSize);\r
+        }\r
+        \r
+        return obfuscationArray;\r
+    }\r
+\r
+    private static byte rotateLeft(byte bits, int shift) {\r
+        return (byte)(((bits & 0xff) << shift) | ((bits & 0xff) >>> (8 - shift)));\r
+    }\r
 }\r
index a97bb5fb30bd7288d2b069200d4cb1ddb682ad0f..8df989d158e62059b72f9783730c3d7b3c5d2672 100644 (file)
 
 package org.apache.poi.hssf.record;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
 import java.io.ByteArrayInputStream;
 import java.util.Arrays;
 
-import junit.framework.AssertionFailedError;
-import junit.framework.TestCase;
-
 import org.apache.poi.EncryptedDocumentException;
 import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey;
 import org.apache.poi.util.HexRead;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
 
 /**
  * Tests for {@link RecordFactoryInputStream}
  *
  * @author Josh Micich
  */
-public final class TestRecordFactoryInputStream extends TestCase {
+public final class TestRecordFactoryInputStream {
 
        /**
         * Hex dump of a BOF record and most of a FILEPASS record.
@@ -55,10 +58,15 @@ public final class TestRecordFactoryInputStream extends TestCase {
        private static final String SAMPLE_WINDOW1 = "3D 00 12 00"
                + "00 00 00 00 40 38 55 23 38 00 00 00 00 00 01 00 58 02";
 
+       @Rule
+       public ExpectedException expectedEx = ExpectedException.none();
+
+       
        /**
         * Makes sure that a default password mismatch condition is represented with {@link EncryptedDocumentException}
         */
-       public void testDefaultPassword() {
+       @Test
+       public void defaultPasswordWrong() {
                // This encodng depends on docId, password and stream position
                final String SAMPLE_WINDOW1_ENCR1 = "3D 00 12 00"
                        + "C4, 9B, 02, 50, 86, E0, DF, 34, FB, 57, 0E, 8C, CE, 25, 45, E3, 80, 01";
@@ -69,33 +77,36 @@ public final class TestRecordFactoryInputStream extends TestCase {
                                + SAMPLE_WINDOW1_ENCR1
                );
 
-               RecordFactoryInputStream rfis;
-               try {
-                       rfis = createRFIS(dataWrongDefault);
-                       throw new AssertionFailedError("Expected password mismatch error");
-               } catch (EncryptedDocumentException e) {
-                       // expected during successful test
-                       if (!e.getMessage().equals("Default password is invalid for docId/saltData/saltHash")) {
-                               throw e;
-                       }
-               }
-
-               byte[] dataCorrectDefault = HexRead.readFromString(""
-                               + COMMON_HEX_DATA
-                               + "137BEF04 969A200B 306329DE 52254005" // correct saltHash for default password (and docId/saltHash)
-                               + SAMPLE_WINDOW1_ENCR1
-               );
-
-               rfis = createRFIS(dataCorrectDefault);
-
-               confirmReadInitialRecords(rfis);
+        Biff8EncryptionKey.setCurrentUserPassword(null);
+           expectedEx.expect(EncryptedDocumentException.class);
+           expectedEx.expectMessage("Default password is invalid for salt/verifier/verifierHash");
+               createRFIS(dataWrongDefault);
        }
+       
+    @Test
+    public void defaultPasswordOK() {
+        // This encodng depends on docId, password and stream position
+        final String SAMPLE_WINDOW1_ENCR1 = "3D 00 12 00"
+            + "C4, 9B, 02, 50, 86, E0, DF, 34, FB, 57, 0E, 8C, CE, 25, 45, E3, 80, 01";
+
+        byte[] dataCorrectDefault = HexRead.readFromString(""
+                + COMMON_HEX_DATA
+                + "137BEF04 969A200B 306329DE 52254005" // correct saltHash for default password (and docId/saltHash)
+                + SAMPLE_WINDOW1_ENCR1
+        );
+
+        Biff8EncryptionKey.setCurrentUserPassword(null);
+        RecordFactoryInputStream rfis = createRFIS(dataCorrectDefault);
+        confirmReadInitialRecords(rfis);
+    }
+       
 
        /**
         * Makes sure that an incorrect user supplied password condition is represented with {@link EncryptedDocumentException}
         */
-       public void testSuppliedPassword() {
-               // This encodng depends on docId, password and stream position
+       @Test
+       public void suppliedPasswordWrong() {
+               // This encoding depends on docId, password and stream position
                final String SAMPLE_WINDOW1_ENCR2 = "3D 00 12 00"
                        + "45, B9, 90, FE, B6, C6, EC, 73, EE, 3F, 52, 45, 97, DB, E3, C1, D6, FE";
 
@@ -108,29 +119,32 @@ public final class TestRecordFactoryInputStream extends TestCase {
 
                Biff8EncryptionKey.setCurrentUserPassword("passw0rd");
 
-               RecordFactoryInputStream rfis;
-               try {
-                       rfis = createRFIS(dataWrongDefault);
-                       throw new AssertionFailedError("Expected password mismatch error");
-               } catch (EncryptedDocumentException e) {
-                       // expected during successful test
-                       if (!e.getMessage().equals("Supplied password is invalid for docId/saltData/saltHash")) {
-                               throw e;
-                       }
-               }
-
-               byte[] dataCorrectDefault = HexRead.readFromString(""
-                               + COMMON_HEX_DATA
-                               + "C728659A C38E35E0 568A338F C3FC9D70" // correct saltHash for supplied password (and docId/saltHash)
-                               + SAMPLE_WINDOW1_ENCR2
-               );
+        expectedEx.expect(EncryptedDocumentException.class);
+        expectedEx.expectMessage("Supplied password is invalid for salt/verifier/verifierHash");
+        createRFIS(dataWrongDefault);
+       }
 
-               rfis = createRFIS(dataCorrectDefault);
-               Biff8EncryptionKey.setCurrentUserPassword(null);
+    @Test
+    public void suppliedPasswordOK() {
+        // This encoding depends on docId, password and stream position
+        final String SAMPLE_WINDOW1_ENCR2 = "3D 00 12 00"
+            + "45, B9, 90, FE, B6, C6, EC, 73, EE, 3F, 52, 45, 97, DB, E3, C1, D6, FE";
 
-               confirmReadInitialRecords(rfis);
-       }
+        Biff8EncryptionKey.setCurrentUserPassword("passw0rd");
+
+        byte[] dataCorrectDefault = HexRead.readFromString(""
+                + COMMON_HEX_DATA
+                + "C728659A C38E35E0 568A338F C3FC9D70" // correct saltHash for supplied password (and docId/saltHash)
+                + SAMPLE_WINDOW1_ENCR2
+        );
+
+        RecordFactoryInputStream rfis = createRFIS(dataCorrectDefault);
+        Biff8EncryptionKey.setCurrentUserPassword(null);
 
+        confirmReadInitialRecords(rfis);
+    }
+       
+       
        /**
         * makes sure the record stream starts with {@link BOFRecord} and then {@link WindowOneRecord}
         * The second record is gets decrypted so this method also checks its content.
index 4d56858c95bf535b49b63bb29359285f7460b97b..c7270087883a06c4d4e7086fd051fbe2b718034c 100644 (file)
 
 package org.apache.poi.hssf.record.crypto;
 
-import junit.framework.Test;
-import junit.framework.TestSuite;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
 
 /**
  * Collects all tests for package <tt>org.apache.poi.hssf.record.crypto</tt>.
  *
  * @author Josh Micich
  */
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+    TestBiff8DecryptingStream.class,
+    TestBiff8EncryptionKey.class
+})
 public final class AllHSSFEncryptionTests {
-
-       public static Test suite() {
-               TestSuite result = new TestSuite(AllHSSFEncryptionTests.class.getName());
-
-               result.addTestSuite(TestBiff8DecryptingStream.class);
-               result.addTestSuite(TestRC4.class);
-               result.addTestSuite(TestBiff8EncryptionKey.class);
-               return result;
-       }
 }
index 00c860ad0e928f04d8c06c8c6ca7afb537995ba3..9d9c04417fd810680b303383acc553d8673d733e 100644 (file)
 
 package org.apache.poi.hssf.record.crypto;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
 import java.io.InputStream;
 import java.util.Arrays;
 
 import junit.framework.AssertionFailedError;
 import junit.framework.ComparisonFailure;
-import junit.framework.TestCase;
 
 import org.apache.poi.util.HexDump;
 import org.apache.poi.util.HexRead;
+import org.junit.Test;
 
 /**
  * Tests for {@link Biff8DecryptingStream}
  *
  * @author Josh Micich
  */
-public final class TestBiff8DecryptingStream extends TestCase {
+public final class TestBiff8DecryptingStream {
 
        /**
         * A mock {@link InputStream} that keeps track of position and also produces
@@ -40,15 +43,14 @@ public final class TestBiff8DecryptingStream extends TestCase {
         * than the previous.
         */
        private static final class MockStream extends InputStream {
-               private int _val;
+               private final int _initialValue;
                private int _position;
 
                public MockStream(int initialValue) {
-                       _val = initialValue & 0xFF;
+                       _initialValue = initialValue;
                }
                public int read() {
-                       _position++;
-                       return _val++ & 0xFF;
+                       return (_initialValue+_position++) & 0xFF;
                }
                public int getPosition() {
                        return _position;
@@ -68,7 +70,7 @@ public final class TestBiff8DecryptingStream extends TestCase {
                public StreamTester(MockStream ms, String keyDigestHex, int expectedFirstInt) {
                        _ms = ms;
                        byte[] keyDigest = HexRead.readFromString(keyDigestHex);
-                       _bds = new Biff8DecryptingStream(_ms, 0, new Biff8EncryptionKey(keyDigest));
+                       _bds = new Biff8DecryptingStream(_ms, 0, new Biff8RC4Key(keyDigest));
                        assertEquals(expectedFirstInt, _bds.readInt());
                        _errorsOccurred = false;
                }
@@ -148,7 +150,8 @@ public final class TestBiff8DecryptingStream extends TestCase {
        /**
         * Tests reading of 64,32,16 and 8 bit integers aligned with key changing boundaries
         */
-       public void testReadsAlignedWithBoundary() {
+       @Test
+       public void readsAlignedWithBoundary() {
                StreamTester st = createStreamTester(0x50, "BA AD F0 0D 00", 0x96C66829);
 
                st.rollForward(0x0004, 0x03FF);
@@ -169,7 +172,8 @@ public final class TestBiff8DecryptingStream extends TestCase {
        /**
         * Tests reading of 64,32 and 16 bit integers <i>across</i> key changing boundaries
         */
-       public void testReadsSpanningBoundary() {
+    @Test
+       public void readsSpanningBoundary() {
                StreamTester st = createStreamTester(0x50, "BA AD F0 0D 00", 0x96C66829);
 
                st.rollForward(0x0004, 0x03FC);
@@ -185,7 +189,8 @@ public final class TestBiff8DecryptingStream extends TestCase {
         * Checks that the BIFF header fields (sid, size) get read without applying decryption,
         * and that the RC4 stream stays aligned during these calls
         */
-       public void testReadHeaderUShort() {
+    @Test
+       public void readHeaderUShort() {
                StreamTester st = createStreamTester(0x50, "BA AD F0 0D 00", 0x96C66829);
 
                st.rollForward(0x0004, 0x03FF);
@@ -213,7 +218,8 @@ public final class TestBiff8DecryptingStream extends TestCase {
        /**
         * Tests reading of byte sequences <i>across</i> and <i>aligned with</i> key changing boundaries
         */
-       public void testReadByteArrays() {
+    @Test
+       public void readByteArrays() {
                StreamTester st = createStreamTester(0x50, "BA AD F0 0D 00", 0x96C66829);
 
                st.rollForward(0x0004, 0x2FFC);
@@ -223,7 +229,7 @@ public final class TestBiff8DecryptingStream extends TestCase {
                st.confirmData("01 C2 4E 55");  // first 4 bytes in next block
                st.assertNoErrors();
        }
-
+    
        private static StreamTester createStreamTester(int mockStreamStartVal, String keyDigestHex, int expectedFirstInt) {
                return new StreamTester(new MockStream(mockStreamStartVal), keyDigestHex, expectedFirstInt);
        }
index 7c6ad42a74e38b04e17162457b13bc9cc03ac67d..294cb09e7c6dd3c2d796356f17f104487aa1ab14 100644 (file)
@@ -19,12 +19,12 @@ package org.apache.poi.hssf.record.crypto;
 
 import java.util.Arrays;
 
-import org.apache.poi.util.HexDump;
-import org.apache.poi.util.HexRead;
-
 import junit.framework.ComparisonFailure;
 import junit.framework.TestCase;
 
+import org.apache.poi.util.HexDump;
+import org.apache.poi.util.HexRead;
+
 /**
  * Tests for {@link Biff8EncryptionKey}
  *
@@ -37,7 +37,7 @@ public final class TestBiff8EncryptionKey extends TestCase {
        }
        public void testCreateKeyDigest() {
                byte[] docIdData = fromHex("17 F6 D1 6B 09 B1 5F 7B 4C 9D 03 B4 81 B5 B4 4A");
-               byte[] keyDigest = Biff8EncryptionKey.createKeyDigest("MoneyForNothing", docIdData);
+               byte[] keyDigest = Biff8RC4Key.createKeyDigest("MoneyForNothing", docIdData);
                byte[] expResult = fromHex("C2 D9 56 B2 6B");
                if (!Arrays.equals(expResult, keyDigest)) {
                        throw new ComparisonFailure("keyDigest mismatch", HexDump.toHex(expResult), HexDump.toHex(keyDigest));
diff --git a/src/testcases/org/apache/poi/hssf/record/crypto/TestRC4.java b/src/testcases/org/apache/poi/hssf/record/crypto/TestRC4.java
deleted file mode 100644 (file)
index 35b6da4..0000000
+++ /dev/null
@@ -1,76 +0,0 @@
-/* ====================================================================
-   Licensed to the Apache Software Foundation (ASF) under one or more
-   contributor license agreements.  See the NOTICE file distributed with
-   this work for additional information regarding copyright ownership.
-   The ASF licenses this file to You under the Apache License, Version 2.0
-   (the "License"); you may not use this file except in compliance with
-   the License.  You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-   Unless required by applicable law or agreed to in writing, software
-   distributed under the License is distributed on an "AS IS" BASIS,
-   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-   See the License for the specific language governing permissions and
-   limitations under the License.
-==================================================================== */
-
-package org.apache.poi.hssf.record.crypto;
-
-import java.security.GeneralSecurityException;
-import java.security.InvalidKeyException;
-import java.util.Arrays;
-
-import javax.crypto.Cipher;
-import javax.crypto.spec.SecretKeySpec;
-
-import junit.framework.ComparisonFailure;
-import junit.framework.TestCase;
-
-import org.apache.poi.util.HexDump;
-import org.apache.poi.util.HexRead;
-
-/**
- * Tests for {@link RC4}
- *
- * @author Josh Micich
- */
-public class TestRC4 extends TestCase {
-       public void testSimple() {
-               confirmRC4("Key", "Plaintext", "BBF316E8D940AF0AD3");
-               confirmRC4("Wiki", "pedia", "1021BF0420");
-               confirmRC4("Secret", "Attack at dawn", "45A01F645FC35B383552544B9BF5");
-
-       }
-
-       private static void confirmRC4(String k, String origText, String expEncrHex) {
-               byte[] actEncr = origText.getBytes();
-               new RC4(k.getBytes()).encrypt(actEncr);
-               byte[] expEncr = HexRead.readFromString(expEncrHex);
-
-               if (!Arrays.equals(expEncr, actEncr)) {
-                       throw new ComparisonFailure("Data mismatch", HexDump.toHex(expEncr), HexDump.toHex(actEncr));
-               }
-
-
-               Cipher cipher;
-               try {
-                       cipher = Cipher.getInstance("RC4");
-               } catch (GeneralSecurityException e) {
-                       throw new RuntimeException(e);
-               }
-               String k2 = k+k; // Sun has minimum of 5 bytes for key
-               SecretKeySpec skeySpec = new SecretKeySpec(k2.getBytes(), "RC4");
-
-               try {
-                       cipher.init(Cipher.DECRYPT_MODE, skeySpec);
-               } catch (InvalidKeyException e) {
-                       throw new RuntimeException(e);
-               }
-               byte[] origData = origText.getBytes();
-               byte[] altEncr = cipher.update(origData);
-               if (!Arrays.equals(expEncr, altEncr)) {
-                       throw new RuntimeException("Mismatch from jdk provider");
-               }
-       }
-}
diff --git a/src/testcases/org/apache/poi/hssf/record/crypto/TestXorEncryption.java b/src/testcases/org/apache/poi/hssf/record/crypto/TestXorEncryption.java
new file mode 100644 (file)
index 0000000..e763a18
--- /dev/null
@@ -0,0 +1,66 @@
+/* ====================================================================\r
+   Licensed to the Apache Software Foundation (ASF) under one or more\r
+   contributor license agreements.  See the NOTICE file distributed with\r
+   this work for additional information regarding copyright ownership.\r
+   The ASF licenses this file to You under the Apache License, Version 2.0\r
+   (the "License"); you may not use this file except in compliance with\r
+   the License.  You may obtain a copy of the License at\r
+\r
+       http://www.apache.org/licenses/LICENSE-2.0\r
+\r
+   Unless required by applicable law or agreed to in writing, software\r
+   distributed under the License is distributed on an "AS IS" BASIS,\r
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+   See the License for the specific language governing permissions and\r
+   limitations under the License.\r
+==================================================================== */\r
+\r
+package org.apache.poi.hssf.record.crypto;\r
+\r
+import static org.hamcrest.core.IsEqual.equalTo;\r
+import static org.junit.Assert.assertEquals;\r
+import static org.junit.Assert.assertThat;\r
+\r
+import org.apache.poi.hssf.HSSFTestDataSamples;\r
+import org.apache.poi.hssf.usermodel.HSSFSheet;\r
+import org.apache.poi.hssf.usermodel.HSSFWorkbook;\r
+import org.apache.poi.poifs.crypt.CryptoFunctions;\r
+import org.apache.poi.poifs.filesystem.NPOIFSFileSystem;\r
+import org.apache.poi.util.HexRead;\r
+import org.junit.Test;\r
+\r
+public class TestXorEncryption {\r
+    \r
+    private static HSSFTestDataSamples samples = new HSSFTestDataSamples();\r
+    \r
+    @Test\r
+    public void testXorEncryption() throws Exception {\r
+        // Xor-Password: abc\r
+        // 2.5.343 XORObfuscation\r
+        // key = 20810\r
+        // verifier = 52250\r
+        int verifier = CryptoFunctions.createXorVerifier1("abc");\r
+        int key = CryptoFunctions.createXorKey1("abc");\r
+        assertEquals(20810, key);\r
+        assertEquals(52250, verifier);\r
+        \r
+        byte xorArrAct[] = CryptoFunctions.createXorArray1("abc");\r
+        byte xorArrExp[] = HexRead.readFromString("AC-CC-A4-AB-D6-BA-C3-BA-D6-A3-2B-45-D3-79-29-BB");\r
+        assertThat(xorArrExp, equalTo(xorArrAct));\r
+    }\r
+\r
+    @SuppressWarnings("static-access")\r
+    @Test\r
+    public void testUserFile() throws Exception {\r
+        Biff8EncryptionKey.setCurrentUserPassword("abc");\r
+        NPOIFSFileSystem fs = new NPOIFSFileSystem(samples.getSampleFile("xor-encryption-abc.xls"), true);\r
+        HSSFWorkbook hwb = new HSSFWorkbook(fs.getRoot(), true);\r
+        \r
+        HSSFSheet sh = hwb.getSheetAt(0);\r
+        assertEquals(1.0, sh.getRow(0).getCell(0).getNumericCellValue(), 0.0);\r
+        assertEquals(2.0, sh.getRow(1).getCell(0).getNumericCellValue(), 0.0);\r
+        assertEquals(3.0, sh.getRow(2).getCell(0).getNumericCellValue(), 0.0);\r
+\r
+        fs.close();\r
+    }\r
+}\r
index 197531965d8720f54d754817b790c4e616661506..3bd97827cc7055406219347f7f4eba4ea5653a19 100644 (file)
@@ -54,6 +54,7 @@ import org.apache.poi.hssf.record.aggregates.FormulaRecordAggregate;
 import org.apache.poi.hssf.record.aggregates.PageSettingsBlock;
 import org.apache.poi.hssf.record.aggregates.RecordAggregate;
 import org.apache.poi.hssf.record.common.UnicodeString;
+import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey;
 import org.apache.poi.poifs.filesystem.NPOIFSFileSystem;
 import org.apache.poi.poifs.filesystem.POIFSFileSystem;
 import org.apache.poi.ss.formula.ptg.Area3DPtg;
@@ -2113,6 +2114,8 @@ public final class TestBugs extends BaseTestBugzillaIssues {
      */
     @Test
     public void bug50833() throws Exception {
+       Biff8EncryptionKey.setCurrentUserPassword(null); 
+        
        HSSFWorkbook wb = openSample("50833.xls");
        HSSFSheet s = wb.getSheetAt(0);
        assertEquals("Sheet1", s.getSheetName());
@@ -2350,14 +2353,9 @@ public final class TestBugs extends BaseTestBugzillaIssues {
      * Normally encrypted files have BOF then FILEPASS, but
      *  some may squeeze a WRITEPROTECT in the middle
      */
-    @Test
+    @Test(expected=EncryptedDocumentException.class)
     public void bug51832() {
-       try {
-          openSample("51832.xls");
-          fail("Encrypted file");
-       } catch(EncryptedDocumentException e) {
-          // Good
-       }
+        openSample("51832.xls");
     }
 
     @Test
@@ -2480,10 +2478,15 @@ public final class TestBugs extends BaseTestBugzillaIssues {
         assertEquals(rstyle.getBorderBottom(), HSSFCellStyle.BORDER_DOUBLE);
     }
 
-    @Test(expected=EncryptedDocumentException.class)
+    @Test
     public void bug35897() throws Exception {
         // password is abc
-        openSample("xor-encryption-abc.xls");
+        try {
+            Biff8EncryptionKey.setCurrentUserPassword("abc");
+            openSample("xor-encryption-abc.xls");
+        } finally {
+            Biff8EncryptionKey.setCurrentUserPassword(null);
+        }
     }
 
     @Test