]> source.dussan.org Git - poi.git/commitdiff
- Support for Office Binary Document RC4 CryptoAPI Encryption for HSLF
authorAndreas Beeker <kiwiwings@apache.org>
Thu, 25 Dec 2014 01:56:29 +0000 (01:56 +0000)
committerAndreas Beeker <kiwiwings@apache.org>
Thu, 25 Dec 2014 01:56:29 +0000 (01:56 +0000)
- Support for Office Binary Document RC4 Encryption
- use LittleEndian class in LittleEndianInputStream
- add normalize method for HSLF, to remove edit history, which is also necessary for encryption support
- update PersistDirectoryEntry handling in PersistPtrHolder to recognize groups while serializing
- deprecated PersistPtrHolder.getSlideOffsetDataLocationsLookup() - throws now UnsupportedOperationException,
  as this wasn't used outside the scope of the class and was quite internal logic of PersistPtrHolder

git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1647867 13f79535-47bb-0310-9956-ffa450edef68

48 files changed:
src/java/org/apache/poi/POIDocument.java
src/java/org/apache/poi/poifs/crypt/ChunkedCipherInputStream.java [new file with mode: 0644]
src/java/org/apache/poi/poifs/crypt/ChunkedCipherOutputStream.java [new file with mode: 0644]
src/java/org/apache/poi/poifs/crypt/CipherProvider.java
src/java/org/apache/poi/poifs/crypt/Decryptor.java
src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java
src/java/org/apache/poi/poifs/crypt/EncryptionInfoBuilder.java
src/java/org/apache/poi/poifs/crypt/EncryptionMode.java
src/java/org/apache/poi/poifs/crypt/EncryptionVerifier.java
src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4Decryptor.java [new file with mode: 0644]
src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4EncryptionHeader.java [new file with mode: 0644]
src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4EncryptionInfoBuilder.java [new file with mode: 0644]
src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4EncryptionVerifier.java [new file with mode: 0644]
src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4Encryptor.java [new file with mode: 0644]
src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIDecryptor.java [new file with mode: 0644]
src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptionHeader.java [new file with mode: 0644]
src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptionInfoBuilder.java [new file with mode: 0644]
src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptionVerifier.java [new file with mode: 0644]
src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptor.java [new file with mode: 0644]
src/java/org/apache/poi/poifs/crypt/standard/StandardDecryptor.java
src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptionHeader.java
src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptionInfoBuilder.java
src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptionVerifier.java
src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptor.java
src/java/org/apache/poi/poifs/filesystem/DocumentInputStream.java
src/java/org/apache/poi/util/LittleEndianInputStream.java
src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileDecryptor.java
src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptionInfoBuilder.java
src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptor.java
src/ooxml/testcases/org/apache/poi/poifs/crypt/TestAgileEncryptionParameters.java
src/ooxml/testcases/org/apache/poi/poifs/crypt/TestDecryptor.java
src/ooxml/testcases/org/apache/poi/poifs/crypt/TestEncryptor.java
src/scratchpad/src/org/apache/poi/hslf/EncryptedSlideShow.java
src/scratchpad/src/org/apache/poi/hslf/HSLFSlideShow.java
src/scratchpad/src/org/apache/poi/hslf/dev/SlideIdListing.java
src/scratchpad/src/org/apache/poi/hslf/dev/UserEditAndPersistListing.java
src/scratchpad/src/org/apache/poi/hslf/exceptions/CorruptPowerPointFileException.java
src/scratchpad/src/org/apache/poi/hslf/exceptions/EncryptedPowerPointFileException.java
src/scratchpad/src/org/apache/poi/hslf/record/CurrentUserAtom.java
src/scratchpad/src/org/apache/poi/hslf/record/DocumentEncryptionAtom.java
src/scratchpad/src/org/apache/poi/hslf/record/PersistPtrHolder.java
src/scratchpad/src/org/apache/poi/hslf/record/Record.java
src/scratchpad/src/org/apache/poi/hslf/record/UserEditAtom.java
src/scratchpad/testcases/org/apache/poi/hslf/record/AllHSLFRecordTests.java
src/scratchpad/testcases/org/apache/poi/hslf/record/TestCurrentUserAtom.java
src/scratchpad/testcases/org/apache/poi/hslf/record/TestDocumentEncryption.java [new file with mode: 0644]
src/scratchpad/testcases/org/apache/poi/hslf/record/TestDocumentEncryptionAtom.java
test-data/slideshow/cryptoapi-proc2356.ppt [new file with mode: 0644]

index e61366b451eff3104c445c458516e8f2715cac80..950e5eb3e43ab9a83b56cbde6efa5079145364eb 100644 (file)
@@ -20,6 +20,7 @@ package org.apache.poi;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.OutputStream;
 import java.util.List;
 
@@ -28,6 +29,7 @@ import org.apache.poi.hpsf.MutablePropertySet;
 import org.apache.poi.hpsf.PropertySet;
 import org.apache.poi.hpsf.PropertySetFactory;
 import org.apache.poi.hpsf.SummaryInformation;
+import org.apache.poi.poifs.crypt.EncryptionInfo;
 import org.apache.poi.poifs.filesystem.DirectoryEntry;
 import org.apache.poi.poifs.filesystem.DirectoryNode;
 import org.apache.poi.poifs.filesystem.DocumentInputStream;
@@ -163,14 +165,40 @@ public abstract class POIDocument {
      *  @return The value of the given property or null if it wasn't found.
      */
     protected PropertySet getPropertySet(String setName) {
+        return getPropertySet(setName, null);
+    }
+    
+    /** 
+     * For a given named property entry, either return it or null if
+     *  if it wasn't found
+     *  
+     *  @param setName The property to read
+     *  @param encryptionInfo the encryption descriptor in case of cryptoAPI encryption
+     *  @return The value of the given property or null if it wasn't found.
+     */
+    protected PropertySet getPropertySet(String setName, EncryptionInfo encryptionInfo) {
+        DirectoryNode dirNode = directory;
+        
+        if (encryptionInfo != null) {
+            try {
+                InputStream is = encryptionInfo.getDecryptor().getDataStream(directory);
+                POIFSFileSystem poifs = new POIFSFileSystem(is);
+                is.close();
+                dirNode = poifs.getRoot();
+            } catch (Exception e) {
+                logger.log(POILogger.ERROR, "Error getting encrypted property set with name " + setName, e);
+                return null;
+            }
+        }
+        
         //directory can be null when creating new documents
-        if (directory == null || !directory.hasEntry(setName)) 
+        if (dirNode == null || !dirNode.hasEntry(setName)) 
             return null;
 
         DocumentInputStream dis;
         try {
             // Find the entry, and get an input stream for it
-            dis = directory.createDocumentInputStream( directory.getEntry(setName) );
+            dis = dirNode.createDocumentInputStream( dirNode.getEntry(setName) );
         } catch(IOException ie) {
             // Oh well, doesn't exist
             logger.log(POILogger.WARN, "Error getting property set with name " + setName + "\n" + ie);
diff --git a/src/java/org/apache/poi/poifs/crypt/ChunkedCipherInputStream.java b/src/java/org/apache/poi/poifs/crypt/ChunkedCipherInputStream.java
new file mode 100644 (file)
index 0000000..7d695a1
--- /dev/null
@@ -0,0 +1,141 @@
+/* ====================================================================\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
+package org.apache.poi.poifs.crypt;\r
+\r
+import java.io.IOException;\r
+import java.io.InputStream;\r
+import java.security.GeneralSecurityException;\r
+\r
+import javax.crypto.Cipher;\r
+\r
+import org.apache.poi.EncryptedDocumentException;\r
+import org.apache.poi.util.Internal;\r
+import org.apache.poi.util.LittleEndianInput;\r
+import org.apache.poi.util.LittleEndianInputStream;\r
+\r
+@Internal\r
+public abstract class ChunkedCipherInputStream extends LittleEndianInputStream {\r
+    private final int chunkSize;\r
+    private final int chunkMask;\r
+    private final int chunkBits;\r
+    \r
+    private int _lastIndex = 0;\r
+    private long _pos = 0;\r
+    private long _size;\r
+    private byte[] _chunk;\r
+    private Cipher _cipher;\r
+\r
+    public ChunkedCipherInputStream(LittleEndianInput stream, long size, int chunkSize)\r
+        throws GeneralSecurityException {\r
+        super((InputStream)stream);\r
+        _size = size;\r
+        this.chunkSize = chunkSize;\r
+        chunkMask = chunkSize-1;\r
+        chunkBits = Integer.bitCount(chunkMask);\r
+        \r
+        _cipher = initCipherForBlock(null, 0);\r
+    }\r
+    \r
+    protected abstract Cipher initCipherForBlock(Cipher existing, int block)\r
+    throws GeneralSecurityException;\r
+\r
+    public int read() throws IOException {\r
+        byte[] b = new byte[1];\r
+        if (read(b) == 1)\r
+            return b[0];\r
+        return -1;\r
+    }\r
+\r
+    // do not implement! -> recursion\r
+    // public int read(byte[] b) throws IOException;\r
+\r
+    public int read(byte[] b, int off, int len) throws IOException {\r
+        int total = 0;\r
+        \r
+        if (available() <= 0) return -1;\r
+\r
+        while (len > 0) {\r
+            if (_chunk == null) {\r
+                try {\r
+                    _chunk = nextChunk();\r
+                } catch (GeneralSecurityException e) {\r
+                    throw new EncryptedDocumentException(e.getMessage(), e);\r
+                }\r
+            }\r
+            int count = (int)(chunkSize - (_pos & chunkMask));\r
+            int avail = available();\r
+            if (avail == 0) {\r
+                return total;\r
+            }\r
+            count = Math.min(avail, Math.min(count, len));\r
+            System.arraycopy(_chunk, (int)(_pos & chunkMask), b, off, count);\r
+            off += count;\r
+            len -= count;\r
+            _pos += count;\r
+            if ((_pos & chunkMask) == 0)\r
+                _chunk = null;\r
+            total += count;\r
+        }\r
+\r
+        return total;\r
+    }\r
+\r
+    @Override\r
+    public long skip(long n) throws IOException {\r
+        long start = _pos;\r
+        long skip = Math.min(available(), n);\r
+\r
+        if ((((_pos + skip) ^ start) & ~chunkMask) != 0)\r
+            _chunk = null;\r
+        _pos += skip;\r
+        return skip;\r
+    }\r
+\r
+    @Override\r
+    public int available() {\r
+        return (int)(_size - _pos);\r
+    }\r
+    \r
+    @Override\r
+    public boolean markSupported() {\r
+        return false;\r
+    }\r
+\r
+    @Override\r
+    public synchronized void mark(int readlimit) {\r
+        throw new UnsupportedOperationException();\r
+    }\r
+    \r
+    @Override\r
+    public synchronized void reset() throws IOException {\r
+        throw new UnsupportedOperationException();\r
+    }\r
+\r
+    private byte[] nextChunk() throws GeneralSecurityException, IOException {\r
+        int index = (int)(_pos >> chunkBits);\r
+        initCipherForBlock(_cipher, index);\r
+        \r
+        if (_lastIndex != index) {\r
+            super.skip((index - _lastIndex) << chunkBits);\r
+        }\r
+\r
+        byte[] block = new byte[Math.min(super.available(), chunkSize)];\r
+        super.read(block, 0, block.length);\r
+        _lastIndex = index + 1;\r
+        return _cipher.doFinal(block);\r
+    }\r
+}\r
diff --git a/src/java/org/apache/poi/poifs/crypt/ChunkedCipherOutputStream.java b/src/java/org/apache/poi/poifs/crypt/ChunkedCipherOutputStream.java
new file mode 100644 (file)
index 0000000..8a2bf00
--- /dev/null
@@ -0,0 +1,171 @@
+/* ====================================================================\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
+package org.apache.poi.poifs.crypt;\r
+\r
+import java.io.File;\r
+import java.io.FileInputStream;\r
+import java.io.FileOutputStream;\r
+import java.io.FilterOutputStream;\r
+import java.io.IOException;\r
+import java.io.OutputStream;\r
+import java.security.GeneralSecurityException;\r
+\r
+import javax.crypto.Cipher;\r
+\r
+import org.apache.poi.EncryptedDocumentException;\r
+import org.apache.poi.poifs.filesystem.DirectoryNode;\r
+import org.apache.poi.poifs.filesystem.POIFSWriterEvent;\r
+import org.apache.poi.poifs.filesystem.POIFSWriterListener;\r
+import org.apache.poi.util.Internal;\r
+import org.apache.poi.util.LittleEndian;\r
+import org.apache.poi.util.LittleEndianConsts;\r
+import org.apache.poi.util.TempFile;\r
+\r
+@Internal\r
+public abstract class ChunkedCipherOutputStream extends FilterOutputStream {\r
+    protected final int chunkSize;\r
+    protected final int chunkMask;\r
+    protected final int chunkBits;\r
+    \r
+    private final byte[] _chunk;\r
+    private final File fileOut;\r
+    private final DirectoryNode dir;\r
+\r
+    private long _pos = 0;\r
+    private Cipher _cipher;\r
+    \r
+    public ChunkedCipherOutputStream(DirectoryNode dir, int chunkSize) throws IOException, GeneralSecurityException {\r
+        super(null);\r
+        this.chunkSize = chunkSize;\r
+        chunkMask = chunkSize-1;\r
+        chunkBits = Integer.bitCount(chunkMask);\r
+        _chunk = new byte[chunkSize];\r
+\r
+        fileOut = TempFile.createTempFile("encrypted_package", "crypt");\r
+        fileOut.deleteOnExit();\r
+        this.out = new FileOutputStream(fileOut);\r
+        this.dir = dir;\r
+        _cipher = initCipherForBlock(null, 0, false);\r
+    }\r
+\r
+    protected abstract Cipher initCipherForBlock(Cipher existing, int block, boolean lastChunk)\r
+    throws GeneralSecurityException;    \r
+    \r
+    protected abstract void calculateChecksum(File fileOut, int oleStreamSize)\r
+    throws GeneralSecurityException, IOException;\r
+    \r
+    protected abstract void createEncryptionInfoEntry(DirectoryNode dir, File tmpFile)\r
+    throws IOException, GeneralSecurityException;\r
+\r
+    public void write(int b) throws IOException {\r
+        write(new byte[]{(byte)b});\r
+    }\r
+\r
+    public void write(byte[] b) throws IOException {\r
+        write(b, 0, b.length);\r
+    }\r
+\r
+    public void write(byte[] b, int off, int len)\r
+    throws IOException {\r
+        if (len == 0) return;\r
+        \r
+        if (len < 0 || b.length < off+len) {\r
+            throw new IOException("not enough bytes in your input buffer");\r
+        }\r
+        \r
+        while (len > 0) {\r
+            int posInChunk = (int)(_pos & chunkMask);\r
+            int nextLen = Math.min(chunkSize-posInChunk, len);\r
+            System.arraycopy(b, off, _chunk, posInChunk, nextLen);\r
+            _pos += nextLen;\r
+            off += nextLen;\r
+            len -= nextLen;\r
+            if ((_pos & chunkMask) == 0) {\r
+                try {\r
+                    writeChunk();\r
+                } catch (GeneralSecurityException e) {\r
+                    throw new IOException(e);\r
+                }\r
+            }\r
+        }\r
+    }\r
+\r
+    protected void writeChunk() throws IOException, GeneralSecurityException {\r
+        int posInChunk = (int)(_pos & chunkMask);\r
+        // normally posInChunk is 0, i.e. on the next chunk (-> index-1)\r
+        // but if called on close(), posInChunk is somewhere within the chunk data\r
+        int index = (int)(_pos >> chunkBits);\r
+        boolean lastChunk;\r
+        if (posInChunk==0) {\r
+            index--;\r
+            posInChunk = chunkSize;\r
+            lastChunk = false;\r
+        } else {\r
+            // pad the last chunk\r
+            lastChunk = true;\r
+        }\r
+\r
+        _cipher = initCipherForBlock(_cipher, index, lastChunk);\r
+\r
+        int ciLen = _cipher.doFinal(_chunk, 0, posInChunk, _chunk);\r
+        out.write(_chunk, 0, ciLen);\r
+    }\r
+    \r
+    public void close() throws IOException {\r
+        try {\r
+            writeChunk();\r
+\r
+            super.close();\r
+            \r
+            int oleStreamSize = (int)(fileOut.length()+LittleEndianConsts.LONG_SIZE);\r
+            calculateChecksum(fileOut, oleStreamSize);\r
+            dir.createDocument("EncryptedPackage", oleStreamSize, new EncryptedPackageWriter());\r
+            createEncryptionInfoEntry(dir, fileOut);\r
+        } catch (GeneralSecurityException e) {\r
+            throw new IOException(e);\r
+        }\r
+    }\r
+\r
+    private class EncryptedPackageWriter implements POIFSWriterListener {\r
+        public void processPOIFSWriterEvent(POIFSWriterEvent event) {\r
+            try {\r
+                OutputStream os = event.getStream();\r
+                byte buf[] = new byte[chunkSize];\r
+    \r
+                // StreamSize (8 bytes): An unsigned integer that specifies the number of bytes used by data \r
+                // encrypted within the EncryptedData field, not including the size of the StreamSize field. \r
+                // Note that the actual size of the \EncryptedPackage stream (1) can be larger than this \r
+                // value, depending on the block size of the chosen encryption algorithm\r
+                LittleEndian.putLong(buf, 0, _pos);\r
+                os.write(buf, 0, LittleEndian.LONG_SIZE);\r
+\r
+                FileInputStream fis = new FileInputStream(fileOut);\r
+                int readBytes;\r
+                while ((readBytes = fis.read(buf)) != -1) {\r
+                    os.write(buf, 0, readBytes);\r
+                }\r
+                fis.close();\r
+\r
+                os.close();\r
+                \r
+                fileOut.delete();\r
+            } catch (IOException e) {\r
+                throw new EncryptedDocumentException(e);\r
+            }\r
+        }\r
+    }\r
+}\r
index de343a91dca37027df973dde80a43b503d753576..5ffe1d33ae65d617a85fc2fd4938bba19ea24e1e 100644 (file)
@@ -20,8 +20,8 @@ package org.apache.poi.poifs.crypt;
 import org.apache.poi.EncryptedDocumentException;\r
 \r
 public enum CipherProvider {\r
-    rc4("RC4", 1),\r
-    aes("AES", 0x18);\r
+    rc4("RC4", 1, "Microsoft Base Cryptographic Provider v1.0"),\r
+    aes("AES", 0x18, "Microsoft Enhanced RSA and AES Cryptographic Provider");\r
 \r
     public static CipherProvider fromEcmaId(int ecmaId) {\r
         for (CipherProvider cp : CipherProvider.values()) {\r
@@ -32,8 +32,10 @@ public enum CipherProvider {
     \r
     public final String jceId;\r
     public final int ecmaId;\r
-    CipherProvider(String jceId, int ecmaId) {\r
+    public final String cipherProviderName;\r
+    CipherProvider(String jceId, int ecmaId, String cipherProviderName) {\r
         this.jceId = jceId;\r
         this.ecmaId = ecmaId;\r
+        this.cipherProviderName = cipherProviderName;\r
     }\r
 }
\ No newline at end of file
index c2d0d5953b3679712f317ebded7a4b255b7f65a9..af449290e85634651f2c0deec2e3634ebadd97a7 100644 (file)
@@ -30,12 +30,12 @@ import org.apache.poi.poifs.filesystem.POIFSFileSystem;
 public abstract class Decryptor {
     public static final String DEFAULT_PASSWORD="VelvetSweatshop";
     
-    protected final EncryptionInfo info;
+    protected final EncryptionInfoBuilder builder;
     private SecretKey secretKey;
     private byte[] verifier, integrityHmacKey, integrityHmacValue;
 
-    protected Decryptor(EncryptionInfo info) {
-        this.info = info;
+    protected Decryptor(EncryptionInfoBuilder builder) {
+        this.builder = builder;
     }
     
     /**
@@ -56,7 +56,7 @@ public abstract class Decryptor {
         throws GeneralSecurityException;
 
     /**
-     * Returns the length of the encytpted data that can be safely read with
+     * Returns the length of the encrypted data that can be safely read with
      * {@link #getDataStream(org.apache.poi.poifs.filesystem.DirectoryNode)}.
      * Just reading to the end of the input stream is not sufficient because there are
      * normally padding bytes that must be discarded
@@ -120,4 +120,12 @@ public abstract class Decryptor {
     protected void setIntegrityHmacValue(byte[] integrityHmacValue) {
         this.integrityHmacValue = integrityHmacValue;
     }
+
+    protected int getBlockSizeInBytes() {
+        return builder.getHeader().getBlockSize();
+    }
+    
+    protected int getKeySizeInBytes() {
+        return builder.getHeader().getKeySize()/8;
+    }
 }
\ No newline at end of file
index 25f9b01e15644b86f81ca4cd39a13fe8aaf905ac..0418befe2353b451851862598935fb347dec265b 100644 (file)
 package org.apache.poi.poifs.crypt;
 
 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 java.io.IOException;
 
 import org.apache.poi.EncryptedDocumentException;
 import org.apache.poi.poifs.filesystem.DirectoryNode;
-import org.apache.poi.poifs.filesystem.DocumentInputStream;
 import org.apache.poi.poifs.filesystem.NPOIFSFileSystem;
 import org.apache.poi.poifs.filesystem.POIFSFileSystem;
+import org.apache.poi.util.BitField;
+import org.apache.poi.util.BitFieldFactory;
+import org.apache.poi.util.LittleEndianInput;
 
 /**
  */
@@ -39,6 +43,31 @@ public class EncryptionInfo {
     private final Decryptor decryptor;
     private final Encryptor encryptor;
 
+    /**
+     * A flag that specifies whether CryptoAPI RC4 or ECMA-376 encryption
+     * ECMA-376 is used. It MUST be 1 unless flagExternal is 1. If flagExternal is 1, it MUST be 0.
+     */
+    public static BitField flagCryptoAPI = BitFieldFactory.getInstance(0x04);
+
+    /**
+     * A value that MUST be 0 if document properties are encrypted.
+     * The encryption of document properties is specified in section 2.3.5.4.
+     */
+    public static BitField flagDocProps = BitFieldFactory.getInstance(0x08);
+    
+    /**
+     * A value that MUST be 1 if extensible encryption is used. If this value is 1,
+     * the value of every other field in this structure MUST be 0.
+     */
+    public static BitField flagExternal = BitFieldFactory.getInstance(0x10);
+    
+    /**
+     * A value that MUST be 1 if the protected content is an ECMA-376 document
+     * ECMA-376. If the fAES bit is 1, the fCryptoAPI bit MUST also be 1.
+     */
+    public static BitField flagAES = BitFieldFactory.getInstance(0x20);
+    
+    
     public EncryptionInfo(POIFSFileSystem fs) throws IOException {
        this(fs.getRoot());
     }
@@ -48,18 +77,43 @@ public class EncryptionInfo {
     }
     
     public EncryptionInfo(DirectoryNode dir) throws IOException {
-        DocumentInputStream dis = dir.createDocumentInputStream("EncryptionInfo");
+        this(dir.createDocumentInputStream("EncryptionInfo"), false);
+    }
+
+    public EncryptionInfo(LittleEndianInput dis, boolean isCryptoAPI) throws IOException {
+        final EncryptionMode encryptionMode;
         versionMajor = dis.readShort();
         versionMinor = dis.readShort();
-        encryptionFlags = dis.readInt();
-        
-        EncryptionMode encryptionMode;
-        if (versionMajor == agile.versionMajor
-            && versionMinor == agile.versionMinor
-            && encryptionFlags == agile.encryptionFlags) {
+
+        if (!isCryptoAPI
+            && versionMajor == binaryRC4.versionMajor
+            && versionMinor == binaryRC4.versionMinor) {
+            encryptionMode = binaryRC4;
+            encryptionFlags = -1;
+        } else if (!isCryptoAPI
+            && versionMajor == agile.versionMajor
+            && versionMinor == agile.versionMinor){
             encryptionMode = agile;
-        } else {
+            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(
+                "Unknown encryption: version major: "+versionMajor+
+                " / version minor: "+versionMinor+
+                " / fCrypto: "+flagCryptoAPI.isSet(encryptionFlags)+
+                " / fExternal: "+flagExternal.isSet(encryptionFlags)+
+                " / fDocProps: "+flagDocProps.isSet(encryptionFlags)+
+                " / fAES: "+flagAES.isSet(encryptionFlags));
         }
         
         EncryptionInfoBuilder eib;
@@ -75,22 +129,35 @@ public class EncryptionInfo {
         decryptor = eib.getDecryptor();
         encryptor = eib.getEncryptor();
     }
-
-    public EncryptionInfo(POIFSFileSystem fs, EncryptionMode encryptionMode) throws IOException {
-        this(fs.getRoot(), encryptionMode);
-     }
+    
+    /**
+     * @deprecated use constructor without fs parameter
+     */
+    @Deprecated
+    public EncryptionInfo(POIFSFileSystem fs, EncryptionMode encryptionMode) {
+        this(encryptionMode);
+    }
      
-     public EncryptionInfo(NPOIFSFileSystem fs, EncryptionMode encryptionMode) throws IOException {
-        this(fs.getRoot(), encryptionMode);
-     }
+    /**
+     * @deprecated use constructor without fs parameter
+     */
+    @Deprecated
+    public EncryptionInfo(NPOIFSFileSystem fs, EncryptionMode encryptionMode) {
+        this(encryptionMode);
+    }
      
-    public EncryptionInfo(
-          DirectoryNode dir
-        , EncryptionMode encryptionMode
-    ) throws EncryptedDocumentException {
-        this(dir, encryptionMode, null, null, -1, -1, null);
+    /**
+     * @deprecated use constructor without dir parameter
+     */
+    @Deprecated
+    public EncryptionInfo(DirectoryNode dir, EncryptionMode encryptionMode) {
+        this(encryptionMode);
     }
     
+    /**
+     * @deprecated use constructor without fs parameter
+     */
+    @Deprecated
     public EncryptionInfo(
         POIFSFileSystem fs
       , EncryptionMode encryptionMode
@@ -99,10 +166,14 @@ public class EncryptionInfo {
       , int keyBits
       , int blockSize
       , ChainingMode chainingMode
-    ) throws EncryptedDocumentException {
-        this(fs.getRoot(), encryptionMode, cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode);
+    ) {
+        this(encryptionMode, cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode);
     }
     
+    /**
+     * @deprecated use constructor without fs parameter
+     */
+    @Deprecated
     public EncryptionInfo(
         NPOIFSFileSystem fs
       , EncryptionMode encryptionMode
@@ -111,10 +182,14 @@ public class EncryptionInfo {
       , int keyBits
       , int blockSize
       , ChainingMode chainingMode
-    ) throws EncryptedDocumentException {
-        this(fs.getRoot(), encryptionMode, cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode);
+    ) {
+        this(encryptionMode, cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode);
     }
         
+    /**
+     * @deprecated use constructor without dir parameter
+     */
+    @Deprecated
     public EncryptionInfo(
           DirectoryNode dir
         , EncryptionMode encryptionMode
@@ -123,7 +198,36 @@ public class EncryptionInfo {
         , int keyBits
         , int blockSize
         , ChainingMode chainingMode
-    ) throws EncryptedDocumentException {
+    ) {
+        this(encryptionMode, cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode);
+    }        
+
+    public EncryptionInfo(EncryptionMode encryptionMode) {
+        this(encryptionMode, null, null, -1, -1, null);
+    }
+    
+    /**
+     * Constructs an EncryptionInfo from scratch
+     *
+     * @param encryptionMode see {@link EncryptionMode} for values, {@link EncryptionMode#cryptoAPI} is for
+     *   internal use only, as it's record based
+     * @param cipherAlgorithm
+     * @param hashAlgorithm
+     * @param keyBits
+     * @param blockSize
+     * @param chainingMode
+     * 
+     * @throws EncryptedDocumentException if the given parameters mismatch, e.g. only certain combinations
+     *   of keyBits, blockSize are allowed for a given {@link CipherAlgorithm}
+     */
+    public EncryptionInfo(
+            EncryptionMode encryptionMode
+          , CipherAlgorithm cipherAlgorithm
+          , HashAlgorithm hashAlgorithm
+          , int keyBits
+          , int blockSize
+          , ChainingMode chainingMode
+      ) {
         versionMajor = encryptionMode.versionMajor;
         versionMinor = encryptionMode.versionMinor;
         encryptionFlags = encryptionMode.encryptionFlags;
index 0c31fc8fdca91632f5cce432f9f9467ca3737952..e36d44da9eafbaef2977421b6150341a9ccd74db 100644 (file)
@@ -18,13 +18,36 @@ package org.apache.poi.poifs.crypt;
 \r
 import java.io.IOException;\r
 \r
-import org.apache.poi.poifs.filesystem.DocumentInputStream;\r
+import org.apache.poi.util.LittleEndianInput;\r
 \r
 public interface EncryptionInfoBuilder {\r
-    void initialize(EncryptionInfo ei, DocumentInputStream dis) throws IOException;\r
+    /**\r
+     * initialize the builder from a stream\r
+     */\r
+    void initialize(EncryptionInfo ei, LittleEndianInput dis) throws IOException;\r
+\r
+    /**\r
+     * initialize the builder from scratch\r
+     */\r
     void initialize(EncryptionInfo ei, CipherAlgorithm cipherAlgorithm, HashAlgorithm hashAlgorithm, int keyBits, int blockSize, ChainingMode chainingMode);\r
+\r
+    /**\r
+     * @return the header data\r
+     */\r
     EncryptionHeader getHeader();\r
+\r
+    /**\r
+     * @return the verifier data\r
+     */\r
     EncryptionVerifier getVerifier();\r
+\r
+    /**\r
+     * @return the decryptor\r
+     */\r
     Decryptor getDecryptor();\r
+\r
+    /**\r
+     * @return the encryptor\r
+     */\r
     Encryptor getEncryptor();\r
 }\r
index 4d9114573f468021e83d695a88e09bfae560d21f..86f4b8508a1e52a7090542605bc61da49e9e60c4 100644 (file)
 \r
 package org.apache.poi.poifs.crypt;\r
 \r
+import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey;\r
+\r
+/**\r
+ * Office supports various encryption modes.\r
+ * The encryption is either based on the whole container ({@link #agile}, {@link #standard} or {@link #binaryRC4})\r
+ * or record based ({@link #cryptoAPI}). The record based encryption can't be accessed directly, but will be\r
+ * invoked by using the {@link Biff8EncryptionKey#setCurrentUserPassword(String)} before saving the document.\r
+ */\r
 public enum EncryptionMode {\r
-      standard("org.apache.poi.poifs.crypt.standard.StandardEncryptionInfoBuilder", 4, 2, 0x24)\r
-    , agile("org.apache.poi.poifs.crypt.agile.AgileEncryptionInfoBuilder", 4, 4, 0x40);\r
+    /* @see <a href="http://msdn.microsoft.com/en-us/library/dd907466(v=office.12).aspx">2.3.6 Office Binary Document RC4 Encryption</a> */\r
+    binaryRC4("org.apache.poi.poifs.crypt.binaryrc4.BinaryRC4EncryptionInfoBuilder", 1, 1, 0x0),\r
+    /* @see <a href="http://msdn.microsoft.com/en-us/library/dd905225(v=office.12).aspx">2.3.5 Office Binary Document RC4 CryptoAPI Encryption</a> */\r
+    cryptoAPI("org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIEncryptionInfoBuilder", 4, 2, 0x04),\r
+    /* @see <a href="http://msdn.microsoft.com/en-us/library/dd906097(v=office.12).aspx">2.3.4.5 \EncryptionInfo Stream (Standard Encryption)</a> */\r
+    standard("org.apache.poi.poifs.crypt.standard.StandardEncryptionInfoBuilder", 4, 2, 0x24),\r
+    /* @see <a href="http://msdn.microsoft.com/en-us/library/dd925810(v=office.12).aspx">2.3.4.10 \EncryptionInfo Stream (Agile Encryption)</a> */\r
+    agile("org.apache.poi.poifs.crypt.agile.AgileEncryptionInfoBuilder", 4, 4, 0x40)\r
+    ;\r
     \r
     public final String builder;\r
     public final int versionMajor;\r
index ecb90e08e2022681472ef114197a4e70ae9f5235..9dafc11bf705382b58b11e99b44fd8751a2c321a 100644 (file)
@@ -41,6 +41,7 @@ public abstract class EncryptionVerifier {
      * The method name is misleading - you'll get the encrypted verifier, not the plain verifier
      * @deprecated use getEncryptedVerifier()
      */
+    @Deprecated
     public byte[] getVerifier() {
         return encryptedVerifier;
     }
@@ -53,6 +54,7 @@ public abstract class EncryptionVerifier {
      * The method name is misleading - you'll get the encrypted verifier hash, not the plain verifier hash
      * @deprecated use getEnryptedVerifierHash
      */
+    @Deprecated
     public byte[] getVerifierHash() {
         return encryptedVerifierHash;
     }
@@ -76,6 +78,7 @@ public abstract class EncryptionVerifier {
     /**
      * @deprecated use getCipherAlgorithm().jceId
      */
+    @Deprecated
     public String getAlgorithmName() {
         return cipherAlgorithm.jceId;
     }
diff --git a/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4Decryptor.java b/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4Decryptor.java
new file mode 100644 (file)
index 0000000..89b2b1f
--- /dev/null
@@ -0,0 +1,131 @@
+/* ====================================================================\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.poifs.crypt.binaryrc4;\r
+\r
+import java.io.IOException;\r
+import java.io.InputStream;\r
+import java.security.GeneralSecurityException;\r
+import java.security.MessageDigest;\r
+import java.util.Arrays;\r
+import javax.crypto.Cipher;\r
+import javax.crypto.SecretKey;\r
+import javax.crypto.spec.SecretKeySpec;\r
+import org.apache.poi.EncryptedDocumentException;\r
+import org.apache.poi.poifs.crypt.*;\r
+import org.apache.poi.poifs.filesystem.DirectoryNode;\r
+import org.apache.poi.poifs.filesystem.DocumentInputStream;\r
+import org.apache.poi.util.LittleEndian;\r
+\r
+public class BinaryRC4Decryptor extends Decryptor {\r
+    private long _length = -1L;\r
+    \r
+    private class BinaryRC4CipherInputStream extends ChunkedCipherInputStream {\r
+\r
+        protected Cipher initCipherForBlock(Cipher existing, int block)\r
+                throws GeneralSecurityException {\r
+            return BinaryRC4Decryptor.initCipherForBlock(existing, block, builder, getSecretKey(), Cipher.DECRYPT_MODE);\r
+        }\r
+\r
+        public BinaryRC4CipherInputStream(DocumentInputStream stream, long size)\r
+                throws GeneralSecurityException {\r
+            super(stream, size, 512);\r
+        }\r
+    }\r
+\r
+    protected BinaryRC4Decryptor(BinaryRC4EncryptionInfoBuilder builder) {\r
+        super(builder);\r
+    }\r
+\r
+    public boolean verifyPassword(String password) {\r
+        EncryptionVerifier ver = builder.getVerifier();\r
+        SecretKey skey = generateSecretKey(password, ver);\r
+        try {\r
+            Cipher cipher = initCipherForBlock(null, 0, builder, skey, Cipher.DECRYPT_MODE);\r
+            byte encryptedVerifier[] = ver.getEncryptedVerifier();\r
+            byte verifier[] = new byte[encryptedVerifier.length];\r
+            cipher.update(encryptedVerifier, 0, encryptedVerifier.length, verifier);\r
+            setVerifier(verifier);\r
+            byte encryptedVerifierHash[] = ver.getEncryptedVerifierHash();\r
+            byte verifierHash[] = cipher.doFinal(encryptedVerifierHash);\r
+            HashAlgorithm hashAlgo = ver.getHashAlgorithm();\r
+            MessageDigest hashAlg = CryptoFunctions.getMessageDigest(hashAlgo);\r
+            byte calcVerifierHash[] = hashAlg.digest(verifier);\r
+            if (Arrays.equals(calcVerifierHash, verifierHash)) {\r
+                setSecretKey(skey);\r
+                return true;\r
+            }\r
+        } catch (GeneralSecurityException e) {\r
+            throw new EncryptedDocumentException(e);\r
+        }\r
+        return false;\r
+    }\r
+\r
+    protected static Cipher initCipherForBlock(Cipher cipher, int block,\r
+        EncryptionInfoBuilder builder, SecretKey skey, int encryptMode)\r
+    throws GeneralSecurityException {\r
+        EncryptionVerifier ver = builder.getVerifier();\r
+        HashAlgorithm hashAlgo = ver.getHashAlgorithm();\r
+        byte blockKey[] = new byte[4];\r
+        LittleEndian.putUInt(blockKey, 0, block);\r
+        byte encKey[] = CryptoFunctions.generateKey(skey.getEncoded(), hashAlgo, blockKey, 16);\r
+        SecretKey key = new SecretKeySpec(encKey, skey.getAlgorithm());\r
+        if (cipher == null) {\r
+            EncryptionHeader em = builder.getHeader();\r
+            cipher = CryptoFunctions.getCipher(key, em.getCipherAlgorithm(), null, null, encryptMode);\r
+        } else {\r
+            cipher.init(encryptMode, key);\r
+        }\r
+        return cipher;\r
+    }\r
+\r
+    protected static SecretKey generateSecretKey(String password,\r
+            EncryptionVerifier ver) {\r
+        if (password.length() > 255)\r
+            password = password.substring(0, 255);\r
+        HashAlgorithm hashAlgo = ver.getHashAlgorithm();\r
+        MessageDigest hashAlg = CryptoFunctions.getMessageDigest(hashAlgo);\r
+        byte hash[] = hashAlg.digest(CryptoFunctions.getUtf16LeString(password));\r
+        byte salt[] = ver.getSalt();\r
+        hashAlg.reset();\r
+        for (int i = 0; i < 16; i++) {\r
+            hashAlg.update(hash, 0, 5);\r
+            hashAlg.update(salt);\r
+        }\r
+\r
+        hash = new byte[5];\r
+        System.arraycopy(hashAlg.digest(), 0, hash, 0, 5);\r
+        SecretKey skey = new SecretKeySpec(hash, ver.getCipherAlgorithm().jceId);\r
+        return skey;\r
+    }\r
+\r
+    public InputStream getDataStream(DirectoryNode dir) throws IOException,\r
+            GeneralSecurityException {\r
+        DocumentInputStream dis = dir.createDocumentInputStream("EncryptedPackage");\r
+        _length = dis.readLong();\r
+        BinaryRC4CipherInputStream cipherStream = new BinaryRC4CipherInputStream(dis, _length);\r
+        return cipherStream;\r
+    }\r
+\r
+    public long getLength() {\r
+        if (_length == -1L) {\r
+            throw new IllegalStateException("Decryptor.getDataStream() was not called");\r
+        }\r
+        \r
+        return _length;\r
+    }\r
+}
\ No newline at end of file
diff --git a/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4EncryptionHeader.java b/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4EncryptionHeader.java
new file mode 100644 (file)
index 0000000..1b811a1
--- /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.poifs.crypt.binaryrc4;\r
+\r
+import org.apache.poi.poifs.crypt.CipherAlgorithm;\r
+import org.apache.poi.poifs.crypt.CipherProvider;\r
+import org.apache.poi.poifs.crypt.EncryptionHeader;\r
+import org.apache.poi.poifs.crypt.HashAlgorithm;\r
+import org.apache.poi.poifs.crypt.standard.EncryptionRecord;\r
+import org.apache.poi.util.LittleEndianByteArrayOutputStream;\r
+\r
+public class BinaryRC4EncryptionHeader extends EncryptionHeader implements\r
+        EncryptionRecord {\r
+\r
+    protected BinaryRC4EncryptionHeader() {\r
+        setCipherAlgorithm(CipherAlgorithm.rc4);\r
+        setKeySize(40);\r
+        setBlockSize(-1);\r
+        setCipherProvider(CipherProvider.rc4);\r
+        setHashAlgorithm(HashAlgorithm.md5);\r
+        setSizeExtra(0);\r
+        setFlags(0);\r
+        setCspName("");\r
+        setChainingMode(null);\r
+    }\r
+\r
+    public void write(LittleEndianByteArrayOutputStream littleendianbytearrayoutputstream) {\r
+    }\r
+}\r
diff --git a/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4EncryptionInfoBuilder.java b/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4EncryptionInfoBuilder.java
new file mode 100644 (file)
index 0000000..10bf58d
--- /dev/null
@@ -0,0 +1,77 @@
+/* ====================================================================\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.poifs.crypt.binaryrc4;\r
+\r
+import java.io.IOException;\r
+import org.apache.poi.poifs.crypt.*;\r
+import org.apache.poi.util.LittleEndianInput;\r
+\r
+public class BinaryRC4EncryptionInfoBuilder implements EncryptionInfoBuilder {\r
+\r
+    EncryptionInfo info;\r
+    BinaryRC4EncryptionHeader header;\r
+    BinaryRC4EncryptionVerifier verifier;\r
+    BinaryRC4Decryptor decryptor;\r
+    BinaryRC4Encryptor encryptor;\r
+\r
+    public BinaryRC4EncryptionInfoBuilder() {\r
+    }\r
+\r
+    public void initialize(EncryptionInfo info, LittleEndianInput dis)\r
+    throws IOException {\r
+        this.info = info;\r
+        int vMajor = info.getVersionMajor();\r
+        int vMinor = info.getVersionMinor();\r
+        assert (vMajor == 1 && vMinor == 1);\r
+\r
+        header = new BinaryRC4EncryptionHeader();\r
+        verifier = new BinaryRC4EncryptionVerifier(dis);\r
+        decryptor = new BinaryRC4Decryptor(this);\r
+        encryptor = new BinaryRC4Encryptor(this);\r
+    }\r
+\r
+    public void initialize(EncryptionInfo info,\r
+        CipherAlgorithm cipherAlgorithm, HashAlgorithm hashAlgorithm,\r
+        int keyBits, int blockSize, ChainingMode chainingMode) {\r
+        this.info = info;\r
+        header = new BinaryRC4EncryptionHeader();\r
+        verifier = new BinaryRC4EncryptionVerifier();\r
+        decryptor = new BinaryRC4Decryptor(this);\r
+        encryptor = new BinaryRC4Encryptor(this);\r
+    }\r
+\r
+    public BinaryRC4EncryptionHeader getHeader() {\r
+        return header;\r
+    }\r
+\r
+    public BinaryRC4EncryptionVerifier getVerifier() {\r
+        return verifier;\r
+    }\r
+\r
+    public BinaryRC4Decryptor getDecryptor() {\r
+        return decryptor;\r
+    }\r
+\r
+    public BinaryRC4Encryptor getEncryptor() {\r
+        return encryptor;\r
+    }\r
+\r
+    public EncryptionInfo getEncryptionInfo() {\r
+        return info;\r
+    }\r
+}\r
diff --git a/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4EncryptionVerifier.java b/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4EncryptionVerifier.java
new file mode 100644 (file)
index 0000000..86cf4ac
--- /dev/null
@@ -0,0 +1,81 @@
+/* ====================================================================\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.poifs.crypt.binaryrc4;\r
+\r
+import org.apache.poi.EncryptedDocumentException;\r
+import org.apache.poi.poifs.crypt.*;\r
+import org.apache.poi.poifs.crypt.standard.EncryptionRecord;\r
+import org.apache.poi.util.LittleEndianByteArrayOutputStream;\r
+import org.apache.poi.util.LittleEndianInput;\r
+\r
+public class BinaryRC4EncryptionVerifier extends EncryptionVerifier implements EncryptionRecord {\r
+\r
+    protected BinaryRC4EncryptionVerifier() {\r
+        setSpinCount(-1);\r
+        setCipherAlgorithm(CipherAlgorithm.rc4);\r
+        setChainingMode(null);\r
+        setEncryptedKey(null);\r
+        setHashAlgorithm(HashAlgorithm.md5);\r
+    }\r
+\r
+    protected BinaryRC4EncryptionVerifier(LittleEndianInput is) {\r
+        byte salt[] = new byte[16];\r
+        is.readFully(salt);\r
+        setSalt(salt);\r
+        byte encryptedVerifier[] = new byte[16];\r
+        is.readFully(encryptedVerifier);\r
+        setEncryptedVerifier(encryptedVerifier);\r
+        byte encryptedVerifierHash[] = new byte[16];\r
+        is.readFully(encryptedVerifierHash);\r
+        setEncryptedVerifierHash(encryptedVerifierHash);\r
+        setSpinCount(-1);\r
+        setCipherAlgorithm(CipherAlgorithm.rc4);\r
+        setChainingMode(null);\r
+        setEncryptedKey(null);\r
+        setHashAlgorithm(HashAlgorithm.md5);\r
+    }\r
+\r
+    protected void setSalt(byte salt[]) {\r
+        if (salt == null || salt.length != 16) {\r
+            throw new EncryptedDocumentException("invalid verifier salt");\r
+        }\r
+        \r
+        super.setSalt(salt);\r
+    }\r
+\r
+    protected void setEncryptedVerifier(byte encryptedVerifier[]) {\r
+        super.setEncryptedVerifier(encryptedVerifier);\r
+    }\r
+\r
+    protected void setEncryptedVerifierHash(byte encryptedVerifierHash[]) {\r
+        super.setEncryptedVerifierHash(encryptedVerifierHash);\r
+    }\r
+\r
+    public void write(LittleEndianByteArrayOutputStream bos) {\r
+        byte salt[] = getSalt();\r
+        assert (salt.length == 16);\r
+        bos.write(salt);\r
+        byte encryptedVerifier[] = getEncryptedVerifier();\r
+        assert (encryptedVerifier.length == 16);\r
+        bos.write(encryptedVerifier);\r
+        byte encryptedVerifierHash[] = getEncryptedVerifierHash();\r
+        assert (encryptedVerifierHash.length == 16);\r
+        bos.write(encryptedVerifierHash);\r
+    }\r
+\r
+}\r
diff --git a/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4Encryptor.java b/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4Encryptor.java
new file mode 100644 (file)
index 0000000..2cf2d93
--- /dev/null
@@ -0,0 +1,127 @@
+/* ====================================================================\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.poifs.crypt.binaryrc4;\r
+\r
+import java.io.File;\r
+import java.io.IOException;\r
+import java.io.OutputStream;\r
+import java.security.GeneralSecurityException;\r
+import java.security.MessageDigest;\r
+import java.security.SecureRandom;\r
+import java.util.Random;\r
+\r
+import javax.crypto.Cipher;\r
+import javax.crypto.SecretKey;\r
+\r
+import org.apache.poi.EncryptedDocumentException;\r
+import org.apache.poi.poifs.crypt.ChunkedCipherOutputStream;\r
+import org.apache.poi.poifs.crypt.CryptoFunctions;\r
+import org.apache.poi.poifs.crypt.DataSpaceMapUtils;\r
+import org.apache.poi.poifs.crypt.EncryptionInfo;\r
+import org.apache.poi.poifs.crypt.Encryptor;\r
+import org.apache.poi.poifs.crypt.standard.EncryptionRecord;\r
+import org.apache.poi.poifs.filesystem.DirectoryNode;\r
+import org.apache.poi.util.LittleEndianByteArrayOutputStream;\r
+\r
+public class BinaryRC4Encryptor extends Encryptor {\r
+\r
+    private final BinaryRC4EncryptionInfoBuilder builder;\r
+    \r
+    protected class BinaryRC4CipherOutputStream extends ChunkedCipherOutputStream {\r
+\r
+        protected Cipher initCipherForBlock(Cipher cipher, int block, boolean lastChunk)\r
+        throws GeneralSecurityException {\r
+            return BinaryRC4Decryptor.initCipherForBlock(cipher, block, builder, getSecretKey(), Cipher.ENCRYPT_MODE);\r
+        }\r
+\r
+        protected void calculateChecksum(File file, int i) {\r
+        }\r
+\r
+        protected void createEncryptionInfoEntry(DirectoryNode dir, File tmpFile)\r
+        throws IOException, GeneralSecurityException {\r
+            BinaryRC4Encryptor.this.createEncryptionInfoEntry(dir);\r
+        }\r
+\r
+        public BinaryRC4CipherOutputStream(DirectoryNode dir)\r
+        throws IOException, GeneralSecurityException {\r
+            super(dir, 512);\r
+        }\r
+    }\r
+\r
+    protected BinaryRC4Encryptor(BinaryRC4EncryptionInfoBuilder builder) {\r
+        this.builder = builder;\r
+    }\r
+\r
+    public void confirmPassword(String password) {\r
+        Random r = new SecureRandom();\r
+        byte salt[] = new byte[16];\r
+        byte verifier[] = new byte[16];\r
+        r.nextBytes(salt);\r
+        r.nextBytes(verifier);\r
+        confirmPassword(password, null, null, verifier, salt, null);\r
+    }\r
+\r
+    public void confirmPassword(String password, byte keySpec[],\r
+            byte keySalt[], byte verifier[], byte verifierSalt[],\r
+            byte integritySalt[]) {\r
+        BinaryRC4EncryptionVerifier ver = builder.getVerifier();\r
+        ver.setSalt(verifierSalt);\r
+        SecretKey skey = BinaryRC4Decryptor.generateSecretKey(password, ver);\r
+        setSecretKey(skey);\r
+        try {\r
+            Cipher cipher = BinaryRC4Decryptor.initCipherForBlock(null, 0, builder, skey, Cipher.ENCRYPT_MODE);\r
+            byte encryptedVerifier[] = new byte[16];\r
+            cipher.update(verifier, 0, 16, encryptedVerifier);\r
+            ver.setEncryptedVerifier(encryptedVerifier);\r
+            org.apache.poi.poifs.crypt.HashAlgorithm hashAlgo = ver\r
+                    .getHashAlgorithm();\r
+            MessageDigest hashAlg = CryptoFunctions.getMessageDigest(hashAlgo);\r
+            byte calcVerifierHash[] = hashAlg.digest(verifier);\r
+            byte encryptedVerifierHash[] = cipher.doFinal(calcVerifierHash);\r
+            ver.setEncryptedVerifierHash(encryptedVerifierHash);\r
+        } catch (GeneralSecurityException e) {\r
+            throw new EncryptedDocumentException("Password confirmation failed", e);\r
+        }\r
+    }\r
+\r
+    public OutputStream getDataStream(DirectoryNode dir)\r
+    throws IOException, GeneralSecurityException {\r
+        OutputStream countStream = new BinaryRC4CipherOutputStream(dir);\r
+        return countStream;\r
+    }\r
+\r
+    protected int getKeySizeInBytes() {\r
+        return builder.getHeader().getKeySize() / 8;\r
+    }\r
+\r
+    protected void createEncryptionInfoEntry(DirectoryNode dir) throws IOException {\r
+        DataSpaceMapUtils.addDefaultDataSpace(dir);\r
+        final EncryptionInfo info = builder.getEncryptionInfo();\r
+        final BinaryRC4EncryptionHeader header = builder.getHeader();\r
+        final BinaryRC4EncryptionVerifier verifier = builder.getVerifier();\r
+        EncryptionRecord er = new EncryptionRecord() {\r
+            public void write(LittleEndianByteArrayOutputStream bos) {\r
+                bos.writeShort(info.getVersionMajor());\r
+                bos.writeShort(info.getVersionMinor());\r
+                header.write(bos);\r
+                verifier.write(bos);\r
+            }\r
+        };\r
+        DataSpaceMapUtils.createEncryptionEntry(dir, "EncryptionInfo", er);\r
+    }\r
+}\r
diff --git a/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIDecryptor.java b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIDecryptor.java
new file mode 100644 (file)
index 0000000..7042adf
--- /dev/null
@@ -0,0 +1,259 @@
+/* ====================================================================\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.poifs.crypt.cryptoapi;\r
+\r
+import java.io.ByteArrayInputStream;\r
+import java.io.ByteArrayOutputStream;\r
+import java.io.IOException;\r
+import java.io.InputStream;\r
+import java.nio.charset.Charset;\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.SecretKey;\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.CryptoFunctions;\r
+import org.apache.poi.poifs.crypt.Decryptor;\r
+import org.apache.poi.poifs.crypt.EncryptionHeader;\r
+import org.apache.poi.poifs.crypt.EncryptionInfoBuilder;\r
+import org.apache.poi.poifs.crypt.EncryptionVerifier;\r
+import org.apache.poi.poifs.crypt.HashAlgorithm;\r
+import org.apache.poi.poifs.filesystem.DirectoryNode;\r
+import org.apache.poi.poifs.filesystem.DocumentInputStream;\r
+import org.apache.poi.poifs.filesystem.DocumentNode;\r
+import org.apache.poi.poifs.filesystem.POIFSFileSystem;\r
+import org.apache.poi.util.BitField;\r
+import org.apache.poi.util.BitFieldFactory;\r
+import org.apache.poi.util.BoundedInputStream;\r
+import org.apache.poi.util.IOUtils;\r
+import org.apache.poi.util.LittleEndian;\r
+import org.apache.poi.util.LittleEndianInputStream;\r
+\r
+public class CryptoAPIDecryptor extends Decryptor {\r
+\r
+    private long _length;\r
+    \r
+    private class SeekableByteArrayInputStream extends ByteArrayInputStream {\r
+        Cipher cipher;\r
+        byte oneByte[] = { 0 };\r
+        \r
+        public void seek(int pos) {\r
+            if (pos > count) {\r
+                throw new ArrayIndexOutOfBoundsException(pos);\r
+            }\r
+            \r
+            this.pos = pos;\r
+            mark = pos;\r
+        }\r
+\r
+        public void setBlock(int block) throws GeneralSecurityException {\r
+            cipher = initCipherForBlock(cipher, block);\r
+        }\r
+\r
+        public synchronized int read() {\r
+            int ch = super.read();\r
+            if (ch == -1) return -1;\r
+            oneByte[0] = (byte) ch;\r
+            try {\r
+                cipher.update(oneByte, 0, 1, oneByte);\r
+            } catch (ShortBufferException e) {\r
+                throw new EncryptedDocumentException(e);\r
+            }\r
+            return oneByte[0];\r
+        }\r
+\r
+        public synchronized int read(byte b[], int off, int len) {\r
+            int readLen = super.read(b, off, len);\r
+            if (readLen ==-1) return -1;\r
+            try {\r
+                cipher.update(b, off, readLen, b, off);\r
+            } catch (ShortBufferException e) {\r
+                throw new EncryptedDocumentException(e);\r
+            }\r
+            return readLen;\r
+        }\r
+\r
+        public SeekableByteArrayInputStream(byte buf[])\r
+        throws GeneralSecurityException {\r
+            super(buf);\r
+            cipher = initCipherForBlock(null, 0);\r
+        }\r
+    }\r
+\r
+    static class StreamDescriptorEntry {\r
+        static BitField flagStream = BitFieldFactory.getInstance(1);\r
+        \r
+        int streamOffset;\r
+        int streamSize;\r
+        int block;\r
+        int flags;\r
+        int reserved2;\r
+        String streamName;\r
+    }\r
+\r
+    protected CryptoAPIDecryptor(CryptoAPIEncryptionInfoBuilder builder) {\r
+        super(builder);\r
+        _length = -1L;\r
+    }\r
+\r
+    public boolean verifyPassword(String password) {\r
+        EncryptionVerifier ver = builder.getVerifier();\r
+        SecretKey skey = generateSecretKey(password, ver);\r
+        try {\r
+            Cipher cipher = initCipherForBlock(null, 0, builder, skey, Cipher.DECRYPT_MODE);\r
+            byte encryptedVerifier[] = ver.getEncryptedVerifier();\r
+            byte verifier[] = new byte[encryptedVerifier.length];\r
+            cipher.update(encryptedVerifier, 0, encryptedVerifier.length, verifier);\r
+            setVerifier(verifier);\r
+            byte encryptedVerifierHash[] = ver.getEncryptedVerifierHash();\r
+            byte verifierHash[] = cipher.doFinal(encryptedVerifierHash);\r
+            HashAlgorithm hashAlgo = ver.getHashAlgorithm();\r
+            MessageDigest hashAlg = CryptoFunctions.getMessageDigest(hashAlgo);\r
+            byte calcVerifierHash[] = hashAlg.digest(verifier);\r
+            if (Arrays.equals(calcVerifierHash, verifierHash)) {\r
+                setSecretKey(skey);\r
+                return true;\r
+            }\r
+        } catch (GeneralSecurityException e) {\r
+            throw new EncryptedDocumentException(e);\r
+        }\r
+        return false;\r
+    }\r
+\r
+    /**\r
+     * Initializes a cipher object for a given block index for decryption\r
+     *\r
+     * @param cipher may be null, otherwise the given instance is reset to the new block index\r
+     * @param block the block index, e.g. the persist/slide id (hslf)\r
+     * @return a new cipher object, if cipher was null, otherwise the reinitialized cipher\r
+     * @throws GeneralSecurityException\r
+     */\r
+    public Cipher initCipherForBlock(Cipher cipher, int block)\r
+    throws GeneralSecurityException {\r
+        return initCipherForBlock(cipher, block, builder, getSecretKey(), Cipher.DECRYPT_MODE);\r
+    }\r
+\r
+    protected static Cipher initCipherForBlock(Cipher cipher, int block,\r
+        EncryptionInfoBuilder builder, SecretKey skey, int encryptMode)\r
+    throws GeneralSecurityException {\r
+        EncryptionVerifier ver = builder.getVerifier();\r
+        HashAlgorithm hashAlgo = ver.getHashAlgorithm();\r
+        byte blockKey[] = new byte[4];\r
+        LittleEndian.putUInt(blockKey, 0, block);\r
+        MessageDigest hashAlg = CryptoFunctions.getMessageDigest(hashAlgo);\r
+        hashAlg.update(skey.getEncoded());\r
+        byte encKey[] = hashAlg.digest(blockKey);\r
+        EncryptionHeader header = builder.getHeader();\r
+        int keyBits = header.getKeySize();\r
+        encKey = CryptoFunctions.getBlock0(encKey, keyBits / 8);\r
+        if (keyBits == 40) {\r
+            encKey = CryptoFunctions.getBlock0(encKey, 16);\r
+        }\r
+        SecretKey key = new SecretKeySpec(encKey, skey.getAlgorithm());\r
+        if (cipher == null) {\r
+            cipher = CryptoFunctions.getCipher(key, header.getCipherAlgorithm(), null, null, encryptMode);\r
+        } else {\r
+            cipher.init(encryptMode, key);\r
+        }\r
+        return cipher;\r
+    }\r
+\r
+    protected static SecretKey generateSecretKey(String password, EncryptionVerifier ver) {\r
+        if (password.length() > 255) {\r
+            password = password.substring(0, 255);\r
+        }\r
+        HashAlgorithm hashAlgo = ver.getHashAlgorithm();\r
+        MessageDigest hashAlg = CryptoFunctions.getMessageDigest(hashAlgo);\r
+        hashAlg.update(ver.getSalt());\r
+        byte hash[] = hashAlg.digest(CryptoFunctions.getUtf16LeString(password));\r
+        SecretKey skey = new SecretKeySpec(hash, ver.getCipherAlgorithm().jceId);\r
+        return skey;\r
+    }\r
+\r
+    /**\r
+     * Decrypt the Document-/SummaryInformation and other optionally streams.\r
+     * Opposed to other crypto modes, cryptoapi is record based and can't be used\r
+     * to stream-decrypt a whole file\r
+     * \r
+     * @see <a href="http://msdn.microsoft.com/en-us/library/dd943321(v=office.12).aspx">2.3.5.4 RC4 CryptoAPI Encrypted Summary Stream</a>\r
+     */\r
+    @SuppressWarnings("unused")\r
+    public InputStream getDataStream(DirectoryNode dir)\r
+    throws IOException, GeneralSecurityException {\r
+        POIFSFileSystem fsOut = new POIFSFileSystem();\r
+        DocumentNode es = (DocumentNode) dir.getEntry("EncryptedSummary");\r
+        DocumentInputStream dis = dir.createDocumentInputStream(es);\r
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();\r
+        IOUtils.copy(dis, bos);\r
+        dis.close();\r
+        SeekableByteArrayInputStream sbis = new SeekableByteArrayInputStream(bos.toByteArray());\r
+        LittleEndianInputStream leis = new LittleEndianInputStream(sbis);\r
+        int streamDescriptorArrayOffset = (int) leis.readUInt();\r
+        int streamDescriptorArraySize = (int) leis.readUInt();\r
+        sbis.skip(streamDescriptorArrayOffset - 8);\r
+        sbis.setBlock(0);\r
+        int encryptedStreamDescriptorCount = (int) leis.readUInt();\r
+        StreamDescriptorEntry entries[] = new StreamDescriptorEntry[encryptedStreamDescriptorCount];\r
+        for (int i = 0; i < encryptedStreamDescriptorCount; i++) {\r
+            StreamDescriptorEntry entry = new StreamDescriptorEntry();\r
+            entries[i] = entry;\r
+            entry.streamOffset = (int) leis.readUInt();\r
+            entry.streamSize = (int) leis.readUInt();\r
+            entry.block = leis.readUShort();\r
+            int nameSize = leis.readUByte();\r
+            entry.flags = leis.readUByte();\r
+            boolean isStream = StreamDescriptorEntry.flagStream.isSet(entry.flags);\r
+            entry.reserved2 = leis.readInt();\r
+            byte nameBuf[] = new byte[nameSize * 2];\r
+            leis.read(nameBuf);\r
+            entry.streamName = new String(nameBuf, Charset.forName("UTF-16LE"));\r
+            leis.readShort();\r
+            assert(entry.streamName.length() == nameSize);\r
+        }\r
+\r
+        for (StreamDescriptorEntry entry : entries) {\r
+            sbis.seek(entry.streamOffset);\r
+            sbis.setBlock(entry.block);\r
+            InputStream is = new BoundedInputStream(sbis, entry.streamSize);\r
+            fsOut.createDocument(is, entry.streamName);\r
+        }\r
+\r
+        leis.close();\r
+        sbis = null;\r
+        bos.reset();\r
+        fsOut.writeFilesystem(bos);\r
+        _length = bos.size();\r
+        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());\r
+        return bis;\r
+    }\r
+\r
+    /**\r
+     * @return the length of the stream returned by {@link #getDataStream(DirectoryNode)}\r
+     */\r
+    public long getLength() {\r
+        if (_length == -1L) {\r
+            throw new IllegalStateException("Decryptor.getDataStream() was not called");\r
+        }\r
+        return _length;\r
+    }\r
+}\r
diff --git a/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptionHeader.java b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptionHeader.java
new file mode 100644 (file)
index 0000000..151b658
--- /dev/null
@@ -0,0 +1,62 @@
+/* ====================================================================\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.poifs.crypt.cryptoapi;\r
+\r
+import java.io.IOException;\r
+\r
+import org.apache.poi.EncryptedDocumentException;\r
+import org.apache.poi.poifs.crypt.ChainingMode;\r
+import org.apache.poi.poifs.crypt.CipherAlgorithm;\r
+import org.apache.poi.poifs.crypt.CipherProvider;\r
+import org.apache.poi.poifs.crypt.HashAlgorithm;\r
+import org.apache.poi.poifs.crypt.standard.StandardEncryptionHeader;\r
+import org.apache.poi.util.LittleEndianInput;\r
+\r
+public class CryptoAPIEncryptionHeader extends StandardEncryptionHeader {\r
+\r
+    public CryptoAPIEncryptionHeader(LittleEndianInput is) throws IOException {\r
+        super(is);\r
+    }\r
+\r
+    protected CryptoAPIEncryptionHeader(CipherAlgorithm cipherAlgorithm,\r
+            HashAlgorithm hashAlgorithm, int keyBits, int blockSize,\r
+            ChainingMode chainingMode) {\r
+        super(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode);\r
+    }\r
+\r
+    public void setKeySize(int keyBits) {\r
+        // Microsoft Base Cryptographic Provider is limited up to 40 bits\r
+        // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375599(v=vs.85).aspx\r
+        boolean found = false;\r
+        for (int size : getCipherAlgorithm().allowedKeySize) {\r
+            if (size == keyBits) {\r
+                found = true;\r
+                break;\r
+            }\r
+        }\r
+        if (!found) {\r
+            throw new EncryptedDocumentException("invalid keysize "+keyBits+" for cipher algorithm "+getCipherAlgorithm());\r
+        }\r
+        super.setKeySize(keyBits);\r
+        if (keyBits > 40) {\r
+            setCspName("Microsoft Enhanced Cryptographic Provider v1.0");\r
+        } else {\r
+            setCspName(CipherProvider.rc4.cipherProviderName);\r
+        }\r
+    }\r
+}\r
diff --git a/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptionInfoBuilder.java b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptionInfoBuilder.java
new file mode 100644 (file)
index 0000000..2a8a872
--- /dev/null
@@ -0,0 +1,86 @@
+/* ====================================================================\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.poifs.crypt.cryptoapi;\r
+\r
+import java.io.IOException;\r
+\r
+import org.apache.poi.poifs.crypt.*;\r
+import org.apache.poi.util.LittleEndianInput;\r
+\r
+public class CryptoAPIEncryptionInfoBuilder implements EncryptionInfoBuilder {\r
+    EncryptionInfo info;\r
+    CryptoAPIEncryptionHeader header;\r
+    CryptoAPIEncryptionVerifier verifier;\r
+    CryptoAPIDecryptor decryptor;\r
+    CryptoAPIEncryptor encryptor;\r
+\r
+    public CryptoAPIEncryptionInfoBuilder() {\r
+    }\r
+\r
+    /**\r
+     * initialize the builder from a stream\r
+     */\r
+    @SuppressWarnings("unused")\r
+    public void initialize(EncryptionInfo info, LittleEndianInput dis)\r
+    throws IOException {\r
+        this.info = info;\r
+        int hSize = dis.readInt();\r
+        header = new CryptoAPIEncryptionHeader(dis);\r
+        verifier = new CryptoAPIEncryptionVerifier(dis, header);\r
+        decryptor = new CryptoAPIDecryptor(this);\r
+        encryptor = new CryptoAPIEncryptor(this);\r
+    }\r
+\r
+    /**\r
+     * initialize the builder from scratch\r
+     */\r
+    public void initialize(EncryptionInfo info,\r
+            CipherAlgorithm cipherAlgorithm, HashAlgorithm hashAlgorithm,\r
+            int keyBits, int blockSize, ChainingMode chainingMode) {\r
+        this.info = info;\r
+        if (cipherAlgorithm == null) cipherAlgorithm = CipherAlgorithm.rc4;\r
+        if (hashAlgorithm == null) hashAlgorithm = HashAlgorithm.sha1;\r
+        if (keyBits == -1) keyBits = 0x28; \r
+        assert(cipherAlgorithm == CipherAlgorithm.rc4 && hashAlgorithm == HashAlgorithm.sha1);\r
+        \r
+        header = new CryptoAPIEncryptionHeader(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode);\r
+        verifier = new CryptoAPIEncryptionVerifier(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode);\r
+        decryptor = new CryptoAPIDecryptor(this);\r
+        encryptor = new CryptoAPIEncryptor(this);\r
+    }\r
+\r
+    public CryptoAPIEncryptionHeader getHeader() {\r
+        return header;\r
+    }\r
+\r
+    public CryptoAPIEncryptionVerifier getVerifier() {\r
+        return verifier;\r
+    }\r
+\r
+    public CryptoAPIDecryptor getDecryptor() {\r
+        return decryptor;\r
+    }\r
+\r
+    public CryptoAPIEncryptor getEncryptor() {\r
+        return encryptor;\r
+    }\r
+\r
+    public EncryptionInfo getEncryptionInfo() {\r
+        return info;\r
+    }\r
+}\r
diff --git a/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptionVerifier.java b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptionVerifier.java
new file mode 100644 (file)
index 0000000..160d1f9
--- /dev/null
@@ -0,0 +1,50 @@
+/* ====================================================================\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.poifs.crypt.cryptoapi;\r
+\r
+import org.apache.poi.poifs.crypt.ChainingMode;\r
+import org.apache.poi.poifs.crypt.CipherAlgorithm;\r
+import org.apache.poi.poifs.crypt.HashAlgorithm;\r
+import org.apache.poi.poifs.crypt.standard.StandardEncryptionVerifier;\r
+import org.apache.poi.util.LittleEndianInput;\r
+\r
+public class CryptoAPIEncryptionVerifier extends StandardEncryptionVerifier {\r
+\r
+    protected CryptoAPIEncryptionVerifier(LittleEndianInput is,\r
+            CryptoAPIEncryptionHeader header) {\r
+        super(is, header);\r
+    }\r
+\r
+    protected CryptoAPIEncryptionVerifier(CipherAlgorithm cipherAlgorithm,\r
+            HashAlgorithm hashAlgorithm, int keyBits, int blockSize,\r
+            ChainingMode chainingMode) {\r
+        super(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode);\r
+    }\r
+\r
+    protected void setSalt(byte salt[]) {\r
+        super.setSalt(salt);\r
+    }\r
+\r
+    protected void setEncryptedVerifier(byte encryptedVerifier[]) {\r
+        super.setEncryptedVerifier(encryptedVerifier);\r
+    }\r
+\r
+    protected void setEncryptedVerifierHash(byte encryptedVerifierHash[]) {\r
+        super.setEncryptedVerifierHash(encryptedVerifierHash);\r
+    }\r
+}\r
diff --git a/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptor.java b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptor.java
new file mode 100644 (file)
index 0000000..d237f50
--- /dev/null
@@ -0,0 +1,255 @@
+/* ====================================================================\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.poifs.crypt.cryptoapi;\r
+\r
+import java.io.ByteArrayInputStream;\r
+import java.io.ByteArrayOutputStream;\r
+import java.io.IOException;\r
+import java.io.OutputStream;\r
+import java.nio.charset.Charset;\r
+import java.security.GeneralSecurityException;\r
+import java.security.MessageDigest;\r
+import java.security.SecureRandom;\r
+import java.util.ArrayList;\r
+import java.util.List;\r
+import java.util.Random;\r
+\r
+import javax.crypto.Cipher;\r
+import javax.crypto.SecretKey;\r
+\r
+import org.apache.poi.EncryptedDocumentException;\r
+import org.apache.poi.hpsf.DocumentSummaryInformation;\r
+import org.apache.poi.hpsf.PropertySetFactory;\r
+import org.apache.poi.hpsf.SummaryInformation;\r
+import org.apache.poi.hpsf.WritingNotSupportedException;\r
+import org.apache.poi.poifs.crypt.CryptoFunctions;\r
+import org.apache.poi.poifs.crypt.DataSpaceMapUtils;\r
+import org.apache.poi.poifs.crypt.EncryptionInfo;\r
+import org.apache.poi.poifs.crypt.Encryptor;\r
+import org.apache.poi.poifs.crypt.HashAlgorithm;\r
+import org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIDecryptor.StreamDescriptorEntry;\r
+import org.apache.poi.poifs.crypt.standard.EncryptionRecord;\r
+import org.apache.poi.poifs.filesystem.DirectoryNode;\r
+import org.apache.poi.poifs.filesystem.DocumentInputStream;\r
+import org.apache.poi.util.IOUtils;\r
+import org.apache.poi.util.LittleEndian;\r
+import org.apache.poi.util.LittleEndianByteArrayOutputStream;\r
+\r
+public class CryptoAPIEncryptor extends Encryptor {\r
+    private final CryptoAPIEncryptionInfoBuilder builder;\r
+\r
+    protected CryptoAPIEncryptor(CryptoAPIEncryptionInfoBuilder builder) {\r
+        this.builder = builder;\r
+    }\r
+\r
+    public void confirmPassword(String password) {\r
+        Random r = new SecureRandom();\r
+        byte salt[] = new byte[16];\r
+        byte verifier[] = new byte[16];\r
+        r.nextBytes(salt);\r
+        r.nextBytes(verifier);\r
+        confirmPassword(password, null, null, verifier, salt, null);\r
+    }\r
+\r
+    public void confirmPassword(String password, byte keySpec[],\r
+            byte keySalt[], byte verifier[], byte verifierSalt[],\r
+            byte integritySalt[]) {\r
+        assert(verifier != null && verifierSalt != null);\r
+        CryptoAPIEncryptionVerifier ver = builder.getVerifier();\r
+        ver.setSalt(verifierSalt);\r
+        SecretKey skey = CryptoAPIDecryptor.generateSecretKey(password, ver);\r
+        setSecretKey(skey);\r
+        try {\r
+            Cipher cipher = initCipherForBlock(null, 0);\r
+            byte encryptedVerifier[] = new byte[verifier.length];\r
+            cipher.update(verifier, 0, verifier.length, encryptedVerifier);\r
+            ver.setEncryptedVerifier(encryptedVerifier);\r
+            HashAlgorithm hashAlgo = ver.getHashAlgorithm();\r
+            MessageDigest hashAlg = CryptoFunctions.getMessageDigest(hashAlgo);\r
+            byte calcVerifierHash[] = hashAlg.digest(verifier);\r
+            byte encryptedVerifierHash[] = cipher.doFinal(calcVerifierHash);\r
+            ver.setEncryptedVerifierHash(encryptedVerifierHash);\r
+        } catch (GeneralSecurityException e) {\r
+            throw new EncryptedDocumentException("Password confirmation failed", e);\r
+        }\r
+    }\r
+\r
+    /**\r
+     * Initializes a cipher object for a given block index for encryption\r
+     *\r
+     * @param cipher may be null, otherwise the given instance is reset to the new block index\r
+     * @param block the block index, e.g. the persist/slide id (hslf)\r
+     * @return a new cipher object, if cipher was null, otherwise the reinitialized cipher\r
+     * @throws GeneralSecurityException\r
+     */\r
+    public Cipher initCipherForBlock(Cipher cipher, int block)\r
+    throws GeneralSecurityException {\r
+        return CryptoAPIDecryptor.initCipherForBlock(cipher, block, builder, getSecretKey(), Cipher.ENCRYPT_MODE);\r
+    }    \r
+    \r
+    /**\r
+     * Encrypt the Document-/SummaryInformation and other optionally streams.\r
+     * Opposed to other crypto modes, cryptoapi is record based and can't be used\r
+     * to stream-encrypt a whole file\r
+     * \r
+     * @see <a href="http://msdn.microsoft.com/en-us/library/dd943321(v=office.12).aspx">2.3.5.4 RC4 CryptoAPI Encrypted Summary Stream</a>\r
+     */\r
+    public OutputStream getDataStream(DirectoryNode dir)\r
+    throws IOException, GeneralSecurityException {\r
+        CipherByteArrayOutputStream bos = new CipherByteArrayOutputStream();\r
+        byte buf[] = new byte[8];\r
+        \r
+        bos.write(buf, 0, 8); // skip header\r
+        String entryNames[] = {\r
+            SummaryInformation.DEFAULT_STREAM_NAME,\r
+            DocumentSummaryInformation.DEFAULT_STREAM_NAME\r
+        };\r
+        \r
+        List<StreamDescriptorEntry> descList = new ArrayList<StreamDescriptorEntry>();\r
+\r
+        int block = 0;\r
+        for (String entryName : entryNames) {\r
+            if (!dir.hasEntry(entryName)) continue;\r
+            StreamDescriptorEntry descEntry = new StreamDescriptorEntry();\r
+            descEntry.block = block;\r
+            descEntry.streamOffset = bos.size();\r
+            descEntry.streamName = entryName;\r
+            descEntry.flags = StreamDescriptorEntry.flagStream.setValue(0, 1);\r
+            descEntry.reserved2 = 0;\r
+            \r
+            bos.setBlock(block);\r
+            DocumentInputStream dis = dir.createDocumentInputStream(entryName);\r
+            IOUtils.copy(dis, bos);\r
+            dis.close();\r
+            \r
+            descEntry.streamSize = bos.size() - descEntry.streamOffset;\r
+            descList.add(descEntry);\r
+            \r
+            dir.getEntry(entryName).delete();\r
+            \r
+            block++;\r
+        }\r
+        \r
+        int streamDescriptorArrayOffset = bos.size();\r
+        \r
+        bos.setBlock(0);\r
+        LittleEndian.putUInt(buf, 0, descList.size());\r
+        bos.write(buf, 0, 4);\r
+        \r
+        for (StreamDescriptorEntry sde : descList) {\r
+            LittleEndian.putUInt(buf, 0, sde.streamOffset);\r
+            bos.write(buf, 0, 4);\r
+            LittleEndian.putUInt(buf, 0, sde.streamSize);\r
+            bos.write(buf, 0, 4);\r
+            LittleEndian.putUShort(buf, 0, sde.block);\r
+            bos.write(buf, 0, 2);\r
+            LittleEndian.putUByte(buf, 0, (short)sde.streamName.length());\r
+            bos.write(buf, 0, 1);\r
+            LittleEndian.putUByte(buf, 0, (short)sde.flags);\r
+            bos.write(buf, 0, 1);\r
+            LittleEndian.putUInt(buf, 0, sde.reserved2);\r
+            bos.write(buf, 0, 4);\r
+            byte nameBytes[] = sde.streamName.getBytes(Charset.forName("UTF-16LE"));\r
+            bos.write(nameBytes, 0, nameBytes.length);\r
+            LittleEndian.putShort(buf, 0, (short)0); // null-termination\r
+            bos.write(buf, 0, 2);\r
+        }\r
+        \r
+        int savedSize = bos.size();\r
+        int streamDescriptorArraySize = savedSize - streamDescriptorArrayOffset;\r
+        LittleEndian.putUInt(buf, 0, streamDescriptorArrayOffset);\r
+        LittleEndian.putUInt(buf, 4, streamDescriptorArraySize);\r
+\r
+        bos.reset();\r
+        bos.setBlock(0);\r
+        bos.write(buf, 0, 8);\r
+        bos.setSize(savedSize);\r
+        \r
+        dir.createDocument("EncryptedSummary", new ByteArrayInputStream(bos.getBuf(), 0, savedSize));\r
+        DocumentSummaryInformation dsi = PropertySetFactory.newDocumentSummaryInformation();\r
+        \r
+        try {\r
+            dsi.write(dir, DocumentSummaryInformation.DEFAULT_STREAM_NAME);\r
+        } catch (WritingNotSupportedException e) {\r
+            throw new IOException(e);\r
+        }\r
+        \r
+        return bos;\r
+    }\r
+\r
+    protected int getKeySizeInBytes() {\r
+        return builder.getHeader().getKeySize() / 8;\r
+    }\r
+\r
+    protected void createEncryptionInfoEntry(DirectoryNode dir) throws IOException {\r
+        DataSpaceMapUtils.addDefaultDataSpace(dir);\r
+        final EncryptionInfo info = builder.getEncryptionInfo();\r
+        final CryptoAPIEncryptionHeader header = builder.getHeader();\r
+        final CryptoAPIEncryptionVerifier verifier = builder.getVerifier();\r
+        EncryptionRecord er = new EncryptionRecord() {\r
+            public void write(LittleEndianByteArrayOutputStream bos) {\r
+                bos.writeShort(info.getVersionMajor());\r
+                bos.writeShort(info.getVersionMinor());\r
+                header.write(bos);\r
+                verifier.write(bos);\r
+            }\r
+        };\r
+        DataSpaceMapUtils.createEncryptionEntry(dir, "EncryptionInfo", er);\r
+    }\r
+\r
+    private class CipherByteArrayOutputStream extends ByteArrayOutputStream {\r
+        Cipher cipher;\r
+        byte oneByte[] = { 0 };\r
+\r
+        public CipherByteArrayOutputStream() throws GeneralSecurityException {\r
+            setBlock(0);\r
+        }\r
+        \r
+        public byte[] getBuf() {\r
+            return buf;\r
+        }\r
+        \r
+        public void setSize(int count) {\r
+            this.count = count;\r
+        }\r
+        \r
+        public void setBlock(int block) throws GeneralSecurityException {\r
+            cipher = initCipherForBlock(cipher, block);\r
+        }\r
+        \r
+        public void write(int b) {\r
+            try {\r
+                oneByte[0] = (byte)b;\r
+                cipher.update(oneByte, 0, 1, oneByte, 0);\r
+                super.write(oneByte);\r
+            } catch (Exception e) {\r
+                throw new EncryptedDocumentException(e);\r
+            }\r
+        }\r
+\r
+        public void write(byte[] b, int off, int len) {\r
+            try {\r
+                cipher.update(b, off, len, b, off);\r
+                super.write(b, off, len);\r
+            } catch (Exception e) {\r
+                throw new EncryptedDocumentException(e);\r
+            }\r
+        }\r
+\r
+    }\r
+}\r
index 86e31fb7a261f1526a412d97b2816f7f6f75b4e1..2b2c75b52033e760616be65a743c8635c286bd39 100644 (file)
@@ -34,7 +34,7 @@ import org.apache.poi.poifs.crypt.ChainingMode;
 import org.apache.poi.poifs.crypt.CryptoFunctions;
 import org.apache.poi.poifs.crypt.Decryptor;
 import org.apache.poi.poifs.crypt.EncryptionHeader;
-import org.apache.poi.poifs.crypt.EncryptionInfo;
+import org.apache.poi.poifs.crypt.EncryptionInfoBuilder;
 import org.apache.poi.poifs.crypt.EncryptionVerifier;
 import org.apache.poi.poifs.crypt.HashAlgorithm;
 import org.apache.poi.poifs.filesystem.DirectoryNode;
@@ -47,12 +47,12 @@ import org.apache.poi.util.LittleEndian;
 public class StandardDecryptor extends Decryptor {
     private long _length = -1;
 
-    protected StandardDecryptor(EncryptionInfo info) {
-        super(info);
+    protected StandardDecryptor(EncryptionInfoBuilder builder) {
+        super(builder);
     }
 
     public boolean verifyPassword(String password) {
-        EncryptionVerifier ver = info.getVerifier();
+        EncryptionVerifier ver = builder.getVerifier();
         SecretKey skey = generateSecretKey(password, ver, getKeySizeInBytes());
         Cipher cipher = getCipher(skey);
 
@@ -64,7 +64,11 @@ public class StandardDecryptor extends Decryptor {
             byte[] calcVerifierHash = sha1.digest(verifier);
             byte encryptedVerifierHash[] = ver.getEncryptedVerifierHash();
             byte decryptedVerifierHash[] = cipher.doFinal(encryptedVerifierHash);
-            byte[] verifierHash = truncateOrPad(decryptedVerifierHash, calcVerifierHash.length);
+
+            // see 2.3.4.9 Password Verification (Standard Encryption)
+            // ... The number of bytes used by the encrypted Verifier hash MUST be 32 ...
+            // TODO: check and trim/pad the hashes to 32
+            byte[] verifierHash = Arrays.copyOf(decryptedVerifierHash, calcVerifierHash.length);
     
             if (Arrays.equals(calcVerifierHash, verifierHash)) {
                 setSecretKey(skey);
@@ -93,7 +97,7 @@ public class StandardDecryptor extends Decryptor {
         System.arraycopy(x1, 0, x3, 0, x1.length);
         System.arraycopy(x2, 0, x3, x1.length, x2.length);
         
-        byte[] key = truncateOrPad(x3, keySize);
+        byte[] key = Arrays.copyOf(x3, keySize);
 
         SecretKey skey = new SecretKeySpec(key, ver.getCipherAlgorithm().jceId);
         return skey;
@@ -111,24 +115,8 @@ public class StandardDecryptor extends Decryptor {
         return sha1.digest(buff);
     }
 
-    /**
-     * Returns a byte array of the requested length,
-     *  truncated or zero padded as needed.
-     * Behaves like Arrays.copyOf in Java 1.6
-     */
-    protected static byte[] truncateOrPad(byte[] source, int length) {
-       byte[] result = new byte[length];
-       System.arraycopy(source, 0, result, 0, Math.min(length, source.length));
-       if(length > source.length) {
-          for(int i=source.length; i<length; i++) {
-             result[i] = 0;
-          }
-       }
-       return result;
-    }
-
     private Cipher getCipher(SecretKey key) {
-        EncryptionHeader em = info.getHeader();
+        EncryptionHeader em = builder.getHeader();
         ChainingMode cm = em.getChainingMode();
         assert(cm == ChainingMode.ecb);
         return CryptoFunctions.getCipher(key, em.getCipherAlgorithm(), cm, null, Cipher.DECRYPT_MODE);
@@ -142,7 +130,7 @@ public class StandardDecryptor extends Decryptor {
         // limit wrong calculated ole entries - (bug #57080)
         // standard encryption always uses aes encoding, so blockSize is always 16 
         // http://stackoverflow.com/questions/3283787/size-of-data-after-aes-encryption
-        int blockSize = info.getHeader().getCipherAlgorithm().blockSize;
+        int blockSize = builder.getHeader().getCipherAlgorithm().blockSize;
         long cipherLen = (_length/blockSize + 1) * blockSize;
         Cipher cipher = getCipher(getSecretKey());
         
@@ -150,12 +138,11 @@ public class StandardDecryptor extends Decryptor {
         return new BoundedInputStream(new CipherInputStream(boundedDis, cipher), _length);
     }
 
+    /**
+     * @return the length of the stream returned by {@link #getDataStream(DirectoryNode)}
+     */
     public long getLength(){
         if(_length == -1) throw new IllegalStateException("Decryptor.getDataStream() was not called");
         return _length;
     }
-
-    protected int getKeySizeInBytes() {
-        return info.getHeader().getKeySize()/8;
-    }
 }
index 213cc0beb16c7dcfbef869c04332e0312fa77c03..7c65003f61e130ca09f107c70ad5845cab1add57 100644 (file)
 package org.apache.poi.poifs.crypt.standard;\r
 \r
 import static org.apache.poi.poifs.crypt.CryptoFunctions.getUtf16LeString;\r
+import static org.apache.poi.poifs.crypt.EncryptionInfo.flagAES;\r
+import static org.apache.poi.poifs.crypt.EncryptionInfo.flagCryptoAPI;\r
 \r
 import java.io.IOException;\r
+import java.io.InputStream;\r
 \r
 import org.apache.poi.poifs.crypt.ChainingMode;\r
 import org.apache.poi.poifs.crypt.CipherAlgorithm;\r
 import org.apache.poi.poifs.crypt.CipherProvider;\r
 import org.apache.poi.poifs.crypt.EncryptionHeader;\r
 import org.apache.poi.poifs.crypt.HashAlgorithm;\r
-import org.apache.poi.poifs.filesystem.DocumentInputStream;\r
-import org.apache.poi.util.BitField;\r
 import org.apache.poi.util.LittleEndianByteArrayOutputStream;\r
 import org.apache.poi.util.LittleEndianConsts;\r
+import org.apache.poi.util.LittleEndianInput;\r
 import org.apache.poi.util.LittleEndianOutput;\r
 \r
 public class StandardEncryptionHeader extends EncryptionHeader implements EncryptionRecord {\r
-    // A flag that specifies whether CryptoAPI RC4 or ECMA-376 encryption \r
-    // [ECMA-376] is used. It MUST be 1 unless fExternal is 1. If fExternal is 1, it MUST be 0.\r
-    private static BitField flagsCryptoAPI = new BitField(0x04);\r
 \r
-    // A value that MUST be 0 if document properties are encrypted. The \r
-    // encryption of document properties is specified in section 2.3.5.4 [MS-OFFCRYPTO].\r
-    @SuppressWarnings("unused")\r
-    private static BitField flagsDocProps = new BitField(0x08);\r
-    \r
-    // A value that MUST be 1 if extensible encryption is used,. If this value is 1, \r
-    // the value of every other field in this structure MUST be 0.\r
-    @SuppressWarnings("unused")\r
-    private static BitField flagsExternal = new BitField(0x10);\r
-    \r
-    // A value that MUST be 1 if the protected content is an ECMA-376 document \r
-    // [ECMA-376]. If the fAES bit is 1, the fCryptoAPI bit MUST also be 1.\r
-    private static BitField flagsAES = new BitField(0x20);\r
-    \r
-    protected StandardEncryptionHeader(DocumentInputStream is) throws IOException {\r
+    protected StandardEncryptionHeader(LittleEndianInput is) throws IOException {\r
         setFlags(is.readInt());\r
         setSizeExtra(is.readInt());\r
         setCipherAlgorithm(CipherAlgorithm.fromEcmaId(is.readInt()));\r
         setHashAlgorithm(HashAlgorithm.fromEcmaId(is.readInt()));\r
-        setKeySize(is.readInt());\r
+        int keySize = is.readInt();\r
+        if (keySize == 0) {\r
+            // for the sake of inheritance of the cryptoAPI classes\r
+            // see 2.3.5.1 RC4 CryptoAPI Encryption Header\r
+            // If set to 0x00000000, it MUST be interpreted as 0x00000028 bits.\r
+            keySize = 0x28;\r
+        }\r
+        setKeySize(keySize);\r
         setBlockSize(getKeySize());\r
         setCipherProvider(CipherProvider.fromEcmaId(is.readInt()));\r
 \r
@@ -63,9 +55,9 @@ public class StandardEncryptionHeader extends EncryptionHeader implements Encryp
 \r
         // CSPName may not always be specified\r
         // In some cases, the salt value of the EncryptionVerifier is the next chunk of data\r
-        is.mark(LittleEndianConsts.INT_SIZE+1);\r
+        ((InputStream)is).mark(LittleEndianConsts.INT_SIZE+1);\r
         int checkForSalt = is.readInt();\r
-        is.reset();\r
+        ((InputStream)is).reset();\r
         \r
         if (checkForSalt == 16) {\r
             setCspName("");\r
@@ -89,12 +81,15 @@ public class StandardEncryptionHeader extends EncryptionHeader implements Encryp
         setKeySize(keyBits);\r
         setBlockSize(blockSize);\r
         setCipherProvider(cipherAlgorithm.provider);\r
-        setFlags(flagsCryptoAPI.setBoolean(0, true)\r
-                | flagsAES.setBoolean(0, cipherAlgorithm.provider == CipherProvider.aes));\r
+        setFlags(flagCryptoAPI.setBoolean(0, true)\r
+                | flagAES.setBoolean(0, cipherAlgorithm.provider == CipherProvider.aes));\r
         // see http://msdn.microsoft.com/en-us/library/windows/desktop/bb931357(v=vs.85).aspx for a full list\r
         // setCspName("Microsoft Enhanced RSA and AES Cryptographic Provider");\r
     }\r
     \r
+    /**\r
+     * serializes the header \r
+     */\r
     public void write(LittleEndianByteArrayOutputStream bos) {\r
         int startIdx = bos.getWriteIndex();\r
         LittleEndianOutput sizeOutput = bos.createDelayedOutput(LittleEndianConsts.INT_SIZE);\r
@@ -106,10 +101,10 @@ public class StandardEncryptionHeader extends EncryptionHeader implements Encryp
         bos.writeInt(getCipherProvider().ecmaId);\r
         bos.writeInt(0); // reserved1\r
         bos.writeInt(0); // reserved2\r
-        if (getCspName() != null) {\r
-            bos.write(getUtf16LeString(getCspName()));\r
-            bos.writeShort(0);\r
-        }\r
+        String cspName = getCspName();\r
+        if (cspName == null) cspName = getCipherProvider().cipherProviderName;\r
+        bos.write(getUtf16LeString(cspName));\r
+        bos.writeShort(0);\r
         int headerSize = bos.getWriteIndex()-startIdx-LittleEndianConsts.INT_SIZE;\r
         sizeOutput.writeInt(headerSize);        \r
     }\r
index 0480ec45947bb4c5aeb821a5dcbd29572a76a39c..0a9b569cd4ee6e462009cd23d573e20493455444 100644 (file)
@@ -24,7 +24,7 @@ import org.apache.poi.poifs.crypt.CipherAlgorithm;
 import org.apache.poi.poifs.crypt.EncryptionInfo;\r
 import org.apache.poi.poifs.crypt.EncryptionInfoBuilder;\r
 import org.apache.poi.poifs.crypt.HashAlgorithm;\r
-import org.apache.poi.poifs.filesystem.DocumentInputStream;\r
+import org.apache.poi.util.LittleEndianInput;\r
 \r
 public class StandardEncryptionInfoBuilder implements EncryptionInfoBuilder {\r
     \r
@@ -34,7 +34,10 @@ public class StandardEncryptionInfoBuilder implements EncryptionInfoBuilder {
     StandardDecryptor decryptor;\r
     StandardEncryptor encryptor;\r
 \r
-    public void initialize(EncryptionInfo info, DocumentInputStream dis) throws IOException {\r
+    /**\r
+     * initialize the builder from a stream\r
+     */\r
+    public void initialize(EncryptionInfo info, LittleEndianInput dis) throws IOException {\r
         this.info = info;\r
         \r
         @SuppressWarnings("unused")\r
@@ -43,10 +46,13 @@ public class StandardEncryptionInfoBuilder implements EncryptionInfoBuilder {
         verifier = new StandardEncryptionVerifier(dis, header);\r
 \r
         if (info.getVersionMinor() == 2 && (info.getVersionMajor() == 3 || info.getVersionMajor() == 4)) {\r
-            decryptor = new StandardDecryptor(info);\r
+            decryptor = new StandardDecryptor(this);\r
         }\r
     }\r
     \r
+    /**\r
+     * initialize the builder from scratch\r
+     */\r
     public void initialize(EncryptionInfo info, CipherAlgorithm cipherAlgorithm, HashAlgorithm hashAlgorithm, int keyBits, int blockSize, ChainingMode chainingMode) {\r
         this.info = info;\r
 \r
@@ -80,7 +86,7 @@ public class StandardEncryptionInfoBuilder implements EncryptionInfoBuilder {
         }\r
         header = new StandardEncryptionHeader(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode);\r
         verifier = new StandardEncryptionVerifier(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode);\r
-        decryptor = new StandardDecryptor(info);\r
+        decryptor = new StandardDecryptor(this);\r
         encryptor = new StandardEncryptor(this);\r
     }\r
 \r
index db9361793daa28356e54249248bd5d3277a4f601..021d82f9eb7fe3325e47ee4b00222c6cb0bf06ed 100644 (file)
@@ -21,8 +21,8 @@ import org.apache.poi.poifs.crypt.ChainingMode;
 import org.apache.poi.poifs.crypt.CipherAlgorithm;
 import org.apache.poi.poifs.crypt.EncryptionVerifier;
 import org.apache.poi.poifs.crypt.HashAlgorithm;
-import org.apache.poi.poifs.filesystem.DocumentInputStream;
 import org.apache.poi.util.LittleEndianByteArrayOutputStream;
+import org.apache.poi.util.LittleEndianInput;
 
 /**
  * Used when checking if a key is valid for a document 
@@ -31,7 +31,7 @@ public class StandardEncryptionVerifier extends EncryptionVerifier implements En
     private static final int SPIN_COUNT = 50000;
     private final int verifierHashSize;
     
-    protected StandardEncryptionVerifier(DocumentInputStream is, StandardEncryptionHeader header) {
+    protected StandardEncryptionVerifier(LittleEndianInput is, StandardEncryptionHeader header) {
         int saltSize = is.readInt();
 
         if (saltSize!=16) {
@@ -53,10 +53,10 @@ public class StandardEncryptionVerifier extends EncryptionVerifier implements En
         setEncryptedVerifierHash(encryptedVerifierHash);
 
         setSpinCount(SPIN_COUNT);
-        setCipherAlgorithm(CipherAlgorithm.aes128);
-        setChainingMode(ChainingMode.ecb);
+        setCipherAlgorithm(header.getCipherAlgorithm());
+        setChainingMode(header.getChainingMode());
         setEncryptedKey(null);
-        setHashAlgorithm(HashAlgorithm.sha1); 
+        setHashAlgorithm(header.getHashAlgorithmEx()); 
     }
     
     protected StandardEncryptionVerifier(CipherAlgorithm cipherAlgorithm, HashAlgorithm hashAlgorithm, int keyBits, int blockSize, ChainingMode chainingMode) {
@@ -97,12 +97,18 @@ public class StandardEncryptionVerifier extends EncryptionVerifier implements En
         assert(encryptedVerifier.length == 16);
         bos.write(encryptedVerifier);
 
-        // The number of bytes used by the encrypted Verifier hash MUST be 32.
         // The number of bytes used by the decrypted Verifier hash is given by
         // the VerifierHashSize field, which MUST be 20
-        byte encryptedVerifierHash[] = getEncryptedVerifierHash(); 
-        assert(encryptedVerifierHash.length == 32);
         bos.writeInt(20);
+
+        // EncryptedVerifierHash: An array of bytes that contains the encrypted form of the hash of
+        // the randomly generated Verifier value. The length of the array MUST be the size of the
+        // encryption block size multiplied by the number of blocks needed to encrypt the hash of the
+        // Verifier. If the encryption algorithm is RC4, the length MUST be 20 bytes. If the encryption
+        // algorithm is AES, the length MUST be 32 bytes. After decrypting the EncryptedVerifierHash
+        // field, only the first VerifierHashSize bytes MUST be used.
+        byte encryptedVerifierHash[] = getEncryptedVerifierHash(); 
+        assert(encryptedVerifierHash.length == getCipherAlgorithm().encryptedVerifierHashLength);
         bos.write(encryptedVerifierHash);
     }
 
index 8a6ba176ffa4b019e833a439814328e1c3c07363..7049c715dec9d2cbf4472b31242bcc4f91cb998a 100644 (file)
@@ -19,7 +19,6 @@ package org.apache.poi.poifs.crypt.standard;
 \r
 import static org.apache.poi.poifs.crypt.DataSpaceMapUtils.createEncryptionEntry;\r
 import static org.apache.poi.poifs.crypt.standard.StandardDecryptor.generateSecretKey;\r
-import static org.apache.poi.poifs.crypt.standard.StandardDecryptor.truncateOrPad;\r
 \r
 import java.io.File;\r
 import java.io.FileInputStream;\r
@@ -30,6 +29,7 @@ import java.io.OutputStream;
 import java.security.GeneralSecurityException;\r
 import java.security.MessageDigest;\r
 import java.security.SecureRandom;\r
+import java.util.Arrays;\r
 import java.util.Random;\r
 \r
 import javax.crypto.Cipher;\r
@@ -96,7 +96,7 @@ public class StandardEncryptor extends Encryptor {
             // algorithm is AES, the length MUST be 32 bytes. After decrypting the EncryptedVerifierHash\r
             // field, only the first VerifierHashSize bytes MUST be used.\r
             int encVerHashSize = ver.getCipherAlgorithm().encryptedVerifierHashLength; \r
-            byte encryptedVerifierHash[] = cipher.doFinal(truncateOrPad(calcVerifierHash, encVerHashSize));\r
+            byte encryptedVerifierHash[] = cipher.doFinal(Arrays.copyOf(calcVerifierHash, encVerHashSize));\r
     \r
             ver.setEncryptedVerifier(encryptedVerifier);\r
             ver.setEncryptedVerifierHash(encryptedVerifierHash);\r
index d39582f7ad196c445ca8923d26e2e37bfce14a61..6c6b6100c0e02ba3089bd610575fc2a06f995fc3 100644 (file)
@@ -166,4 +166,9 @@ public class DocumentInputStream extends InputStream implements LittleEndianInpu
        public int readUByte() {
           return delegate.readUByte();
        }
+       
+    public long readUInt() {
+        int i = readInt();
+        return i & 0xFFFFFFFFL;
+    }
 }
index 1a37964a8f7a747a416a44c838e00b09273751c6..406570f82f50fdff07c9885279a7f002b9c7d163 100644 (file)
@@ -21,6 +21,8 @@ import java.io.FilterInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 
+import org.apache.poi.util.LittleEndian.BufferUnderrunException;
+
 /**
  * Wraps an {@link InputStream} providing {@link LittleEndianInput}<p/>
  *
@@ -33,6 +35,7 @@ public class LittleEndianInputStream extends FilterInputStream implements Little
        public LittleEndianInputStream(InputStream is) {
                super(is);
        }
+       
        public int available() {
                try {
                        return super.available();
@@ -40,86 +43,75 @@ public class LittleEndianInputStream extends FilterInputStream implements Little
                        throw new RuntimeException(e);
                }
        }
+       
        public byte readByte() {
                return (byte)readUByte();
        }
+       
        public int readUByte() {
-               int ch;
+               byte buf[] = new byte[1];
                try {
-                       ch = in.read();
+                       checkEOF(read(buf), 1);
                } catch (IOException e) {
                        throw new RuntimeException(e);
                }
-               checkEOF(ch);
-               return ch;
+               return LittleEndian.getUByte(buf);
        }
+       
        public double readDouble() {
                return Double.longBitsToDouble(readLong());
        }
+       
        public int readInt() {
-               int ch1;
-               int ch2;
-               int ch3;
-               int ch4;
+           byte buf[] = new byte[LittleEndianConsts.INT_SIZE];
                try {
-                       ch1 = in.read();
-                       ch2 = in.read();
-                       ch3 = in.read();
-                       ch4 = in.read();
+                   checkEOF(read(buf), buf.length);
                } catch (IOException e) {
                        throw new RuntimeException(e);
                }
-               checkEOF(ch1 | ch2 | ch3 | ch4);
-               return (ch4 << 24) + (ch3 << 16) + (ch2 << 8) + (ch1 << 0);
+               return LittleEndian.getInt(buf);
        }
+       
+    /**
+     * get an unsigned int value from an InputStream
+     * 
+     * @return the unsigned int (32-bit) value
+     * @exception IOException
+     *                will be propagated back to the caller
+     * @exception BufferUnderrunException
+     *                if the stream cannot provide enough bytes
+     */
+    public long readUInt() {
+       long retNum = readInt();
+       return retNum & 0x00FFFFFFFFl;
+    }
+       
        public long readLong() {
-               int b0;
-               int b1;
-               int b2;
-               int b3;
-               int b4;
-               int b5;
-               int b6;
-               int b7;
+               byte buf[] = new byte[LittleEndianConsts.LONG_SIZE];
                try {
-                       b0 = in.read();
-                       b1 = in.read();
-                       b2 = in.read();
-                       b3 = in.read();
-                       b4 = in.read();
-                       b5 = in.read();
-                       b6 = in.read();
-                       b7 = in.read();
+                   checkEOF(read(buf), LittleEndianConsts.LONG_SIZE);
                } catch (IOException e) {
                        throw new RuntimeException(e);
                }
-               checkEOF(b0 | b1 | b2 | b3 | b4 | b5 | b6 | b7);
-               return (((long)b7 << 56) +
-                               ((long)b6 << 48) +
-                               ((long)b5 << 40) +
-                               ((long)b4 << 32) +
-                               ((long)b3 << 24) +
-                               (b2 << 16) +
-                               (b1 <<  8) +
-                               (b0 <<  0));
+               return LittleEndian.getLong(buf);
        }
+       
        public short readShort() {
                return (short)readUShort();
        }
+       
        public int readUShort() {
-               int ch1;
-               int ch2;
+               byte buf[] = new byte[LittleEndianConsts.SHORT_SIZE];
                try {
-                       ch1 = in.read();
-                       ch2 = in.read();
+                   checkEOF(read(buf), LittleEndianConsts.SHORT_SIZE);
                } catch (IOException e) {
                        throw new RuntimeException(e);
                }
-               checkEOF(ch1 | ch2);
-               return (ch2 << 8) + (ch1 << 0);
+               return LittleEndian.getUShort(buf);
        }
-       private static void checkEOF(int value) {
-               if (value <0) {
+       
+       private static void checkEOF(int actualBytes, int expectedBytes) {
+               if (expectedBytes != 0 && (actualBytes == -1 || actualBytes != expectedBytes)) {
                        throw new RuntimeException("Unexpected end-of-file");
                }
        }
@@ -129,16 +121,10 @@ public class LittleEndianInputStream extends FilterInputStream implements Little
        }
 
        public void readFully(byte[] buf, int off, int len) {
-               int max = off+len;
-               for(int i=off; i<max; i++) {
-                       int ch;
-                       try {
-                               ch = in.read();
-                       } catch (IOException e) {
-                               throw new RuntimeException(e);
-                       }
-                       checkEOF(ch);
-                       buf[i] = (byte) ch;
-               }
+           try {
+               checkEOF(read(buf, off, len), len);
+           } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
        }
 }
index ccd930853b281e684c158f55694d160dd14c8749..05499685f507dffa9dc71f6d57f62ab81bbf4445 100644 (file)
@@ -40,10 +40,12 @@ import javax.crypto.spec.RC2ParameterSpec;
 import javax.crypto.spec.SecretKeySpec;
 
 import org.apache.poi.EncryptedDocumentException;
+import org.apache.poi.poifs.crypt.ChunkedCipherInputStream;
 import org.apache.poi.poifs.crypt.CipherAlgorithm;
 import org.apache.poi.poifs.crypt.CryptoFunctions;
 import org.apache.poi.poifs.crypt.Decryptor;
 import org.apache.poi.poifs.crypt.EncryptionHeader;
+import org.apache.poi.poifs.crypt.EncryptionInfoBuilder;
 import org.apache.poi.poifs.crypt.EncryptionVerifier;
 import org.apache.poi.poifs.crypt.HashAlgorithm;
 import org.apache.poi.poifs.crypt.agile.AgileEncryptionVerifier.AgileCertificateEntry;
@@ -55,9 +57,6 @@ import org.apache.poi.util.LittleEndian;
  * Decryptor implementation for Agile Encryption
  */
 public class AgileDecryptor extends Decryptor {
-    private final AgileEncryptionInfoBuilder builder;
-    
-
     private long _length = -1;
 
     protected static final byte[] kVerifierInputBlock;
@@ -85,16 +84,15 @@ public class AgileDecryptor extends Decryptor {
     }
 
     protected AgileDecryptor(AgileEncryptionInfoBuilder builder) {
-        super(builder.getInfo());
-        this.builder = builder; 
+        super(builder);
     }
     
     /**
      * set decryption password
      */
     public boolean verifyPassword(String password) throws GeneralSecurityException {
-        AgileEncryptionVerifier ver = builder.getVerifier();
-        AgileEncryptionHeader header = builder.getHeader(); 
+        AgileEncryptionVerifier ver = (AgileEncryptionVerifier)builder.getVerifier();
+        AgileEncryptionHeader header = (AgileEncryptionHeader)builder.getHeader(); 
         HashAlgorithm hashAlgo = header.getHashAlgorithmEx();
         CipherAlgorithm cipherAlgo = header.getCipherAlgorithm();
         int blockSize = header.getBlockSize();
@@ -206,8 +204,8 @@ public class AgileDecryptor extends Decryptor {
      * @throws GeneralSecurityException
      */
     public boolean verifyPassword(KeyPair keyPair, X509Certificate x509) throws GeneralSecurityException {
-        AgileEncryptionVerifier ver = builder.getVerifier();
-        AgileEncryptionHeader header = builder.getHeader();
+        AgileEncryptionVerifier ver = (AgileEncryptionVerifier)builder.getVerifier();
+        AgileEncryptionHeader header = (AgileEncryptionHeader)builder.getHeader();
         HashAlgorithm hashAlgo = header.getHashAlgorithmEx();
         CipherAlgorithm cipherAlgo = header.getCipherAlgorithm();
         int blockSize = header.getBlockSize();
@@ -257,10 +255,11 @@ public class AgileDecryptor extends Decryptor {
         return fillSize;
     }
 
-    protected static byte[] hashInput(AgileEncryptionInfoBuilder builder, byte pwHash[], byte blockKey[], byte inputKey[], int cipherMode) {
+    protected static byte[] hashInput(EncryptionInfoBuilder builder, byte pwHash[], byte blockKey[], byte inputKey[], int cipherMode) {
         EncryptionVerifier ver = builder.getVerifier();
-        int keySize = builder.getDecryptor().getKeySizeInBytes();
-        int blockSize = builder.getDecryptor().getBlockSizeInBytes();
+        AgileDecryptor dec = (AgileDecryptor)builder.getDecryptor();
+        int keySize = dec.getKeySizeInBytes();
+        int blockSize = dec.getBlockSizeInBytes();
         HashAlgorithm hashAlgo = ver.getHashAlgorithm();
         byte[] salt = ver.getSalt();
 
@@ -283,7 +282,7 @@ public class AgileDecryptor extends Decryptor {
         DocumentInputStream dis = dir.createDocumentInputStream("EncryptedPackage");
         _length = dis.readLong();
         
-        ChunkedCipherInputStream cipherStream = new ChunkedCipherInputStream(dis, _length);
+        ChunkedCipherInputStream cipherStream = new AgileCipherInputStream(dis, _length);
         return cipherStream;
     }
 
@@ -292,6 +291,31 @@ public class AgileDecryptor extends Decryptor {
         return _length;
     }
 
+
+    protected static Cipher initCipherForBlock(Cipher existing, int block, boolean lastChunk, EncryptionInfoBuilder builder, SecretKey skey, int encryptionMode)
+    throws GeneralSecurityException {
+        EncryptionHeader header = builder.getHeader();
+        if (existing == null || lastChunk) {
+            String padding = (lastChunk ? "PKCS5Padding" : "NoPadding");
+            existing = getCipher(skey, header.getCipherAlgorithm(), header.getChainingMode(), header.getKeySalt(), encryptionMode, padding);
+        }
+
+        byte[] blockKey = new byte[4];
+        LittleEndian.putInt(blockKey, 0, block);
+        byte[] iv = generateIv(header.getHashAlgorithmEx(), header.getKeySalt(), blockKey, header.getBlockSize());
+
+        AlgorithmParameterSpec aps;
+        if (header.getCipherAlgorithm() == CipherAlgorithm.rc2) {
+            aps = new RC2ParameterSpec(skey.getEncoded().length*8, iv);
+        } else {
+            aps = new IvParameterSpec(iv);
+        }
+            
+        existing.init(encryptionMode, skey, aps);
+        
+        return existing;
+    }
+
     /**
      * 2.3.4.15 Data Encryption (Agile Encryption)
      * 
@@ -307,107 +331,18 @@ public class AgileDecryptor extends Decryptor {
      * that the StreamSize field of the EncryptedPackage field specifies the number of bytes of
      * unencrypted data as specified in section 2.3.4.4.
      */
-    private class ChunkedCipherInputStream extends InputStream {
-        private int _lastIndex = 0;
-        private long _pos = 0;
-        private final long _size;
-        private final InputStream _stream;
-        private byte[] _chunk;
-        private Cipher _cipher;
-
-        public ChunkedCipherInputStream(DocumentInputStream stream, long size)
-            throws GeneralSecurityException {
-            EncryptionHeader header = info.getHeader();
-            _size = size;
-            _stream = stream;
-            _cipher = getCipher(getSecretKey(), header.getCipherAlgorithm(), header.getChainingMode(), header.getKeySalt(), Cipher.DECRYPT_MODE);
+    private class AgileCipherInputStream extends ChunkedCipherInputStream {
+        public AgileCipherInputStream(DocumentInputStream stream, long size)
+        throws GeneralSecurityException {
+            super(stream, size, 4096);
         }
 
-        public int read() throws IOException {
-            byte[] b = new byte[1];
-            if (read(b) == 1)
-                return b[0];
-            return -1;
-        }
-
-        public int read(byte[] b) throws IOException {
-            return read(b, 0, b.length);
-        }
-
-        public int read(byte[] b, int off, int len) throws IOException {
-            int total = 0;
-            
-            if (available() <= 0) return -1;
-
-            while (len > 0) {
-                if (_chunk == null) {
-                    try {
-                        _chunk = nextChunk();
-                    } catch (GeneralSecurityException e) {
-                        throw new EncryptedDocumentException(e.getMessage());
-                    }
-                }
-                int count = (int)(4096L - (_pos & 0xfff));
-                int avail = available();
-                if (avail == 0) {
-                    return total;
-                }
-                count = Math.min(avail, Math.min(count, len));
-                System.arraycopy(_chunk, (int)(_pos & 0xfff), b, off, count);
-                off += count;
-                len -= count;
-                _pos += count;
-                if ((_pos & 0xfff) == 0)
-                    _chunk = null;
-                total += count;
-            }
-
-            return total;
-        }
-
-        public long skip(long n) throws IOException {
-            long start = _pos;
-            long skip = Math.min(available(), n);
-
-            if ((((_pos + skip) ^ start) & ~0xfff) != 0)
-                _chunk = null;
-            _pos += skip;
-            return skip;
-        }
-
-        public int available() throws IOException { return (int)(_size - _pos); }
-        public void close() throws IOException { _stream.close(); }
-        public boolean markSupported() { return false; }
-
-        private byte[] nextChunk() throws GeneralSecurityException, IOException {
-            int index = (int)(_pos >> 12);
-            byte[] blockKey = new byte[4];
-            LittleEndian.putInt(blockKey, 0, index);
-            EncryptionHeader header = info.getHeader();
-            byte[] iv = generateIv(header.getHashAlgorithmEx(), header.getKeySalt(), blockKey, getBlockSizeInBytes());
-            AlgorithmParameterSpec aps;
-            if (header.getCipherAlgorithm() == CipherAlgorithm.rc2) {
-                aps = new RC2ParameterSpec(getSecretKey().getEncoded().length*8, iv);
-            } else {
-                aps = new IvParameterSpec(iv);
-            }
-            
-            _cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), aps);
-            if (_lastIndex != index)
-                _stream.skip((index - _lastIndex) << 12);
-
-            byte[] block = new byte[Math.min(_stream.available(), 4096)];
-            _stream.read(block);
-            _lastIndex = index + 1;
-            return _cipher.doFinal(block);
+        // TODO: calculate integrity hmac while reading the stream
+        // for a post-validation of the data
+        
+        protected Cipher initCipherForBlock(Cipher cipher, int block)
+        throws GeneralSecurityException {
+            return AgileDecryptor.initCipherForBlock(cipher, block, false, builder, getSecretKey(), Cipher.DECRYPT_MODE);
         }
     }
-
-    protected int getBlockSizeInBytes() {
-       return info.getHeader().getBlockSize();
-    }
-    
-    protected int getKeySizeInBytes() {
-       return info.getHeader().getKeySize()/8;
-    }
 }
index 10ca07674f29dc4464242ffbb6200fd1f4c598d5..b778c1032a2eedd4fba2e4187eac95edcebb90f8 100644 (file)
@@ -26,7 +26,7 @@ import org.apache.poi.poifs.crypt.EncryptionInfo;
 import org.apache.poi.poifs.crypt.EncryptionInfoBuilder;\r
 import org.apache.poi.poifs.crypt.EncryptionMode;\r
 import org.apache.poi.poifs.crypt.HashAlgorithm;\r
-import org.apache.poi.poifs.filesystem.DocumentInputStream;\r
+import org.apache.poi.util.LittleEndianInput;\r
 import org.apache.xmlbeans.XmlException;\r
 \r
 import com.microsoft.schemas.office.x2006.encryption.EncryptionDocument;\r
@@ -39,10 +39,10 @@ public class AgileEncryptionInfoBuilder implements EncryptionInfoBuilder {
     AgileDecryptor decryptor;\r
     AgileEncryptor encryptor;\r
 \r
-    public void initialize(EncryptionInfo info, DocumentInputStream dis) throws IOException {\r
+    public void initialize(EncryptionInfo info, LittleEndianInput dis) throws IOException {\r
         this.info = info;\r
         \r
-        EncryptionDocument ed = parseDescriptor(dis);\r
+        EncryptionDocument ed = parseDescriptor((InputStream)dis);\r
         header = new AgileEncryptionHeader(ed);\r
         verifier = new AgileEncryptionVerifier(ed);\r
         if (info.getVersionMajor() == EncryptionMode.agile.versionMajor\r
index ee3b71036f31f0728de176e02dbec96dca951da0..51ced4c2ccfc6d4d94ab73840123b4f1c1995cab 100644 (file)
 ==================================================================== */\r
 package org.apache.poi.poifs.crypt.agile;\r
 \r
-import static org.apache.poi.poifs.crypt.CryptoFunctions.generateIv;\r
 import static org.apache.poi.poifs.crypt.CryptoFunctions.getBlock0;\r
 import static org.apache.poi.poifs.crypt.CryptoFunctions.getCipher;\r
 import static org.apache.poi.poifs.crypt.CryptoFunctions.getMessageDigest;\r
 import static org.apache.poi.poifs.crypt.CryptoFunctions.hashPassword;\r
+import static org.apache.poi.poifs.crypt.DataSpaceMapUtils.createEncryptionEntry;\r
 import static org.apache.poi.poifs.crypt.agile.AgileDecryptor.getNextBlockSize;\r
 import static org.apache.poi.poifs.crypt.agile.AgileDecryptor.hashInput;\r
 import static org.apache.poi.poifs.crypt.agile.AgileDecryptor.kCryptoKeyBlock;\r
@@ -32,16 +32,12 @@ import static org.apache.poi.poifs.crypt.agile.AgileDecryptor.kVerifierInputBloc
 import java.io.ByteArrayOutputStream;\r
 import java.io.File;\r
 import java.io.FileInputStream;\r
-import java.io.FileOutputStream;\r
-import java.io.FilterOutputStream;\r
 import java.io.IOException;\r
-import java.io.InputStream;\r
 import java.io.OutputStream;\r
 import java.security.GeneralSecurityException;\r
 import java.security.MessageDigest;\r
 import java.security.SecureRandom;\r
 import java.security.cert.CertificateEncodingException;\r
-import java.security.spec.AlgorithmParameterSpec;\r
 import java.util.HashMap;\r
 import java.util.Map;\r
 import java.util.Random;\r
@@ -49,28 +45,20 @@ import java.util.Random;
 import javax.crypto.Cipher;\r
 import javax.crypto.Mac;\r
 import javax.crypto.SecretKey;\r
-import javax.crypto.spec.IvParameterSpec;\r
-import javax.crypto.spec.RC2ParameterSpec;\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.ChunkedCipherOutputStream;\r
 import org.apache.poi.poifs.crypt.CryptoFunctions;\r
 import org.apache.poi.poifs.crypt.DataSpaceMapUtils;\r
-import org.apache.poi.poifs.crypt.EncryptionHeader;\r
 import org.apache.poi.poifs.crypt.EncryptionInfo;\r
 import org.apache.poi.poifs.crypt.Encryptor;\r
 import org.apache.poi.poifs.crypt.HashAlgorithm;\r
 import org.apache.poi.poifs.crypt.agile.AgileEncryptionVerifier.AgileCertificateEntry;\r
+import org.apache.poi.poifs.crypt.standard.EncryptionRecord;\r
 import org.apache.poi.poifs.filesystem.DirectoryNode;\r
-import org.apache.poi.poifs.filesystem.POIFSWriterEvent;\r
-import org.apache.poi.poifs.filesystem.POIFSWriterListener;\r
-import org.apache.poi.util.IOUtils;\r
 import org.apache.poi.util.LittleEndian;\r
 import org.apache.poi.util.LittleEndianByteArrayOutputStream;\r
-import org.apache.poi.util.LittleEndianConsts;\r
-import org.apache.poi.util.LittleEndianOutputStream;\r
-import org.apache.poi.util.TempFile;\r
 import org.apache.xmlbeans.XmlOptions;\r
 \r
 import com.microsoft.schemas.office.x2006.encryption.CTDataIntegrity;\r
@@ -87,9 +75,7 @@ import com.microsoft.schemas.office.x2006.keyEncryptor.password.CTPasswordKeyEnc
 \r
 public class AgileEncryptor extends Encryptor {\r
     private final AgileEncryptionInfoBuilder builder;\r
-    @SuppressWarnings("unused")\r
     private byte integritySalt[];\r
-    private Mac integrityMD;\r
        private byte pwHash[];\r
     \r
        protected AgileEncryptor(AgileEncryptionInfoBuilder builder) {\r
@@ -214,10 +200,6 @@ public class AgileEncryptor extends Encryptor {
             byte encryptedHmacKey[] = cipher.doFinal(filledSalt);\r
             header.setEncryptedHmacKey(encryptedHmacKey);\r
 \r
-            this.integrityMD = CryptoFunctions.getMac(hashAlgo);\r
-            this.integrityMD.init(new SecretKeySpec(integritySalt, hashAlgo.jceHmacId));\r
-\r
-        \r
             cipher = Cipher.getInstance("RSA");\r
             for (AgileCertificateEntry ace : ver.getCertificates()) {\r
                 cipher.init(Cipher.ENCRYPT_MODE, ace.x509.getPublicKey());\r
@@ -234,182 +216,59 @@ public class AgileEncryptor extends Encryptor {
     public OutputStream getDataStream(DirectoryNode dir)\r
             throws IOException, GeneralSecurityException {\r
         // TODO: initialize headers\r
-        OutputStream countStream = new ChunkedCipherOutputStream(dir);\r
+        AgileCipherOutputStream countStream = new AgileCipherOutputStream(dir);\r
        return countStream;\r
     }\r
 \r
     /**\r
-     * 2.3.4.15 Data Encryption (Agile Encryption)\r
+     * Generate an HMAC, as specified in [RFC2104], of the encrypted form of the data (message), \r
+     * which the DataIntegrity element will verify by using the Salt generated in step 2 as the key. \r
+     * Note that the entire EncryptedPackage stream (1), including the StreamSize field, MUST be \r
+     * used as the message.\r
      * \r
-     * The EncryptedPackage stream (1) MUST be encrypted in 4096-byte segments to facilitate nearly\r
-     * random access while allowing CBC modes to be used in the encryption process.\r
-     * The initialization vector for the encryption process MUST be obtained by using the zero-based\r
-     * segment number as a blockKey and the binary form of the KeyData.saltValue as specified in\r
-     * section 2.3.4.12. The block number MUST be represented as a 32-bit unsigned integer.\r
-     * Data blocks MUST then be encrypted by using the initialization vector and the intermediate key\r
-     * obtained by decrypting the encryptedKeyValue from a KeyEncryptor contained within the\r
-     * KeyEncryptors sequence as specified in section 2.3.4.10. The final data block MUST be padded to\r
-     * the next integral multiple of the KeyData.blockSize value. Any padding bytes can be used. Note\r
-     * that the StreamSize field of the EncryptedPackage field specifies the number of bytes of\r
-     * unencrypted data as specified in section 2.3.4.4.\r
-     */\r
-    private class ChunkedCipherOutputStream extends FilterOutputStream implements POIFSWriterListener {\r
-        private long _pos = 0;\r
-        private final byte[] _chunk = new byte[4096];\r
-        private Cipher _cipher;\r
-        private final File fileOut;\r
-        protected final DirectoryNode dir;\r
-\r
-        public ChunkedCipherOutputStream(DirectoryNode dir) throws IOException {\r
-            super(null);\r
-            fileOut = TempFile.createTempFile("encrypted_package", "crypt");\r
-            this.out = new FileOutputStream(fileOut);\r
-            this.dir = dir;\r
-            EncryptionHeader header = builder.getHeader();\r
-            _cipher = getCipher(getSecretKey(), header.getCipherAlgorithm(), header.getChainingMode(), null, Cipher.ENCRYPT_MODE);\r
-        }\r
-\r
-        public void write(int b) throws IOException {\r
-            write(new byte[]{(byte)b});\r
-        }\r
-\r
-        public void write(byte[] b) throws IOException {\r
-            write(b, 0, b.length);\r
-        }\r
-\r
-        public void write(byte[] b, int off, int len)\r
-        throws IOException {\r
-            if (len == 0) return;\r
-            \r
-            if (len < 0 || b.length < off+len) {\r
-                throw new IOException("not enough bytes in your input buffer");\r
-            }\r
-            \r
-            while (len > 0) {\r
-                int posInChunk = (int)(_pos & 0xfff);\r
-                int nextLen = Math.min(4096-posInChunk, len);\r
-                System.arraycopy(b, off, _chunk, posInChunk, nextLen);\r
-                _pos += nextLen;\r
-                off += nextLen;\r
-                len -= nextLen;\r
-                if ((_pos & 0xfff) == 0) {\r
-                    writeChunk();\r
-                }\r
-            }\r
-        }\r
-\r
-        private void writeChunk() throws IOException {\r
-            EncryptionHeader header = builder.getHeader();\r
-            int blockSize = header.getBlockSize();\r
-\r
-            int posInChunk = (int)(_pos & 0xfff);\r
-            // normally posInChunk is 0, i.e. on the next chunk (-> index-1)\r
-            // but if called on close(), posInChunk is somewhere within the chunk data\r
-            int index = (int)(_pos >> 12);\r
-            if (posInChunk==0) {\r
-                index--;\r
-                posInChunk = 4096;\r
-            } else {\r
-                // pad the last chunk\r
-                _cipher = getCipher(getSecretKey(), header.getCipherAlgorithm(), header.getChainingMode(), null, Cipher.ENCRYPT_MODE, "PKCS5Padding");\r
-            }\r
+     * Encrypt the HMAC as in step 3 by using a blockKey byte array consisting of the following bytes:\r
+     * 0xa0, 0x67, 0x7f, 0x02, 0xb2, 0x2c, 0x84, and 0x33.\r
+     **/\r
+    protected void updateIntegrityHMAC(File tmpFile, int oleStreamSize) throws GeneralSecurityException, IOException {\r
+        // as the integrity hmac needs to contain the StreamSize,\r
+        // it's not possible to calculate it on-the-fly while buffering\r
+        // TODO: add stream size parameter to getDataStream()\r
+        AgileEncryptionVerifier ver = builder.getVerifier();\r
+        HashAlgorithm hashAlgo = ver.getHashAlgorithm();\r
+        Mac integrityMD = CryptoFunctions.getMac(hashAlgo);\r
+        integrityMD.init(new SecretKeySpec(integritySalt, hashAlgo.jceHmacId));\r
 \r
-            byte[] blockKey = new byte[4];\r
-            LittleEndian.putInt(blockKey, 0, index);\r
-            byte[] iv = generateIv(header.getHashAlgorithmEx(), header.getKeySalt(), blockKey, blockSize);\r
-            try {\r
-                AlgorithmParameterSpec aps;\r
-                if (header.getCipherAlgorithm() == CipherAlgorithm.rc2) {\r
-                    aps = new RC2ParameterSpec(getSecretKey().getEncoded().length*8, iv);\r
-                } else {\r
-                    aps = new IvParameterSpec(iv);\r
-                }\r
-                \r
-                _cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(), aps);\r
-                int ciLen = _cipher.doFinal(_chunk, 0, posInChunk, _chunk);\r
-                out.write(_chunk, 0, ciLen);\r
-            } catch (GeneralSecurityException e) {\r
-                throw (IOException)new IOException().initCause(e);\r
-            }\r
-        }\r
+        byte buf[] = new byte[1024];\r
+        LittleEndian.putLong(buf, 0, oleStreamSize);\r
+        integrityMD.update(buf, 0, LittleEndian.LONG_SIZE);\r
         \r
-        public void close() throws IOException {\r
-            writeChunk();\r
-            super.close();\r
-            writeToPOIFS();\r
-        }\r
-\r
-        void writeToPOIFS() throws IOException {\r
-            DataSpaceMapUtils.addDefaultDataSpace(dir);\r
-            \r
-            /**\r
-             * Generate an HMAC, as specified in [RFC2104], of the encrypted form of the data (message), \r
-             * which the DataIntegrity element will verify by using the Salt generated in step 2 as the key. \r
-             * Note that the entire EncryptedPackage stream (1), including the StreamSize field, MUST be \r
-             * used as the message.\r
-             * \r
-             * Encrypt the HMAC as in step 3 by using a blockKey byte array consisting of the following bytes:\r
-             * 0xa0, 0x67, 0x7f, 0x02, 0xb2, 0x2c, 0x84, and 0x33.\r
-             **/\r
-            byte buf[] = new byte[4096];\r
-            LittleEndian.putLong(buf, 0, _pos);\r
-            integrityMD.update(buf, 0, LittleEndianConsts.LONG_SIZE);\r
-            \r
-            InputStream fis = new FileInputStream(fileOut);\r
-            for (int readBytes; (readBytes = fis.read(buf)) != -1; integrityMD.update(buf, 0, readBytes));\r
-            fis.close();\r
-            \r
-            AgileEncryptionHeader header = builder.getHeader(); \r
-            int blockSize = header.getBlockSize();\r
-            \r
-            byte hmacValue[] = integrityMD.doFinal();\r
-            byte iv[] = CryptoFunctions.generateIv(header.getHashAlgorithmEx(), header.getKeySalt(), kIntegrityValueBlock, header.getBlockSize());\r
-            Cipher cipher = CryptoFunctions.getCipher(getSecretKey(), header.getCipherAlgorithm(), header.getChainingMode(), iv, Cipher.ENCRYPT_MODE);\r
-            try {\r
-                byte hmacValueFilled[] = getBlock0(hmacValue, getNextBlockSize(hmacValue.length, blockSize));\r
-                byte encryptedHmacValue[] = cipher.doFinal(hmacValueFilled);\r
-                header.setEncryptedHmacValue(encryptedHmacValue);\r
-            } catch (GeneralSecurityException e) {\r
-                throw new EncryptedDocumentException(e);\r
-            }\r
-\r
-            createEncryptionInfoEntry(dir);\r
-            \r
-            int oleStreamSize = (int)(fileOut.length()+LittleEndianConsts.LONG_SIZE);\r
-            dir.createDocument("EncryptedPackage", oleStreamSize, this);\r
-            // TODO: any properties???\r
-        }\r
-    \r
-        public void processPOIFSWriterEvent(POIFSWriterEvent event) {\r
-            try {\r
-                LittleEndianOutputStream leos = new LittleEndianOutputStream(event.getStream());\r
-\r
-                // StreamSize (8 bytes): An unsigned integer that specifies the number of bytes used by data \r
-                // encrypted within the EncryptedData field, not including the size of the StreamSize field. \r
-                // Note that the actual size of the \EncryptedPackage stream (1) can be larger than this \r
-                // value, depending on the block size of the chosen encryption algorithm\r
-                leos.writeLong(_pos);\r
-\r
-                FileInputStream fis = new FileInputStream(fileOut);\r
-                IOUtils.copy(fis, leos);\r
-                fis.close();\r
-                fileOut.delete();\r
-\r
-                leos.close();\r
-            } catch (IOException e) {\r
-                throw new EncryptedDocumentException(e);\r
-            }\r
+        FileInputStream fis = new FileInputStream(tmpFile);\r
+        int readBytes;\r
+        while ((readBytes = fis.read(buf)) != -1) {\r
+            integrityMD.update(buf, 0, readBytes);\r
         }\r
-    }\r
-\r
-    protected void createEncryptionInfoEntry(DirectoryNode dir) throws IOException {\r
-        final CTKeyEncryptor.Uri.Enum passwordUri = \r
-            CTKeyEncryptor.Uri.HTTP_SCHEMAS_MICROSOFT_COM_OFFICE_2006_KEY_ENCRYPTOR_PASSWORD;\r
-        final CTKeyEncryptor.Uri.Enum certificateUri = \r
-                CTKeyEncryptor.Uri.HTTP_SCHEMAS_MICROSOFT_COM_OFFICE_2006_KEY_ENCRYPTOR_CERTIFICATE;\r
+        fis.close();\r
+        \r
+        byte hmacValue[] = integrityMD.doFinal();\r
         \r
-        AgileEncryptionVerifier ver = builder.getVerifier();\r
         AgileEncryptionHeader header = builder.getHeader();\r
+        int blockSize = header.getBlockSize();\r
+        byte iv[] = CryptoFunctions.generateIv(header.getHashAlgorithmEx(), header.getKeySalt(), kIntegrityValueBlock, blockSize);\r
+        Cipher cipher = CryptoFunctions.getCipher(getSecretKey(), header.getCipherAlgorithm(), header.getChainingMode(), iv, Cipher.ENCRYPT_MODE);\r
+        byte hmacValueFilled[] = getBlock0(hmacValue, getNextBlockSize(hmacValue.length, blockSize));\r
+        byte encryptedHmacValue[] = cipher.doFinal(hmacValueFilled);\r
+        \r
+        header.setEncryptedHmacValue(encryptedHmacValue);\r
+    }\r
+    \r
+    private final CTKeyEncryptor.Uri.Enum passwordUri = \r
+        CTKeyEncryptor.Uri.HTTP_SCHEMAS_MICROSOFT_COM_OFFICE_2006_KEY_ENCRYPTOR_PASSWORD;\r
+    private final CTKeyEncryptor.Uri.Enum certificateUri = \r
+        CTKeyEncryptor.Uri.HTTP_SCHEMAS_MICROSOFT_COM_OFFICE_2006_KEY_ENCRYPTOR_CERTIFICATE;\r
+    \r
+    protected EncryptionDocument createEncryptionDocument() {\r
+        AgileEncryptionVerifier ver = builder.getVerifier();\r
+        AgileEncryptionHeader header = builder.getHeader(); \r
         \r
         EncryptionDocument ed = EncryptionDocument.Factory.newInstance();\r
         CTEncryption edRoot = ed.addNewEncryption();\r
@@ -485,6 +344,10 @@ public class AgileEncryptor extends Encryptor {
             certData.setCertVerifier(ace.certVerifier);\r
         }\r
         \r
+        return ed;\r
+    }\r
+    \r
+    protected void marshallEncryptionDocument(EncryptionDocument ed, LittleEndianByteArrayOutputStream os) {\r
         XmlOptions xo = new XmlOptions();\r
         xo.setCharacterEncoding("UTF-8");\r
         Map<String,String> nsMap = new HashMap<String,String>();\r
@@ -494,33 +357,82 @@ public class AgileEncryptor extends Encryptor {
         xo.setSaveSuggestedPrefixes(nsMap);\r
         xo.setSaveNamespacesFirst();\r
         xo.setSaveAggressiveNamespaces();\r
-        // setting standalone doesn't work with xmlbeans-2.3\r
+\r
+        // setting standalone doesn't work with xmlbeans-2.3 & 2.6\r
+        // ed.documentProperties().setStandalone(true);\r
         xo.setSaveNoXmlDecl();\r
-        \r
         ByteArrayOutputStream bos = new ByteArrayOutputStream();\r
-        bos.write("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n".getBytes("UTF-8"));\r
-        ed.save(bos, xo);\r
+        try {\r
+            bos.write("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n".getBytes("UTF-8"));\r
+            ed.save(bos, xo);\r
+            os.write(bos.toByteArray());\r
+        } catch (IOException e) {\r
+            throw new EncryptedDocumentException("error marshalling encryption info document", e);\r
+        }\r
+    }\r
 \r
-        final byte buf[] = new byte[5000];        \r
-        LittleEndianByteArrayOutputStream leos = new LittleEndianByteArrayOutputStream(buf, 0);\r
-        EncryptionInfo info = builder.getInfo();\r
+    protected void createEncryptionInfoEntry(DirectoryNode dir, File tmpFile)\r
+    throws IOException, GeneralSecurityException {\r
+        DataSpaceMapUtils.addDefaultDataSpace(dir);\r
 \r
-        // EncryptionVersionInfo (4 bytes): A Version structure (section 2.1.4), where \r
-        // Version.vMajor MUST be 0x0004 and Version.vMinor MUST be 0x0004\r
-        leos.writeShort(info.getVersionMajor());\r
-        leos.writeShort(info.getVersionMinor());\r
-        // Reserved (4 bytes): A value that MUST be 0x00000040\r
-        leos.writeInt(info.getEncryptionFlags());\r
-        leos.write(bos.toByteArray());\r
-        \r
-        dir.createDocument("EncryptionInfo", leos.getWriteIndex(), new POIFSWriterListener() {\r
-            public void processPOIFSWriterEvent(POIFSWriterEvent event) {\r
-                try {\r
-                    event.getStream().write(buf, 0, event.getLimit());\r
-                } catch (IOException e) {\r
-                    throw new EncryptedDocumentException(e);\r
-                }\r
+        final EncryptionInfo info = builder.getInfo();\r
+\r
+        EncryptionRecord er = new EncryptionRecord(){\r
+            public void write(LittleEndianByteArrayOutputStream bos) {\r
+                // EncryptionVersionInfo (4 bytes): A Version structure (section 2.1.4), where \r
+                // Version.vMajor MUST be 0x0004 and Version.vMinor MUST be 0x0004\r
+                bos.writeShort(info.getVersionMajor());\r
+                bos.writeShort(info.getVersionMinor());\r
+                // Reserved (4 bytes): A value that MUST be 0x00000040\r
+                bos.writeInt(info.getEncryptionFlags());\r
+\r
+                EncryptionDocument ed = createEncryptionDocument();\r
+                marshallEncryptionDocument(ed, bos);\r
             }\r
-        });\r
+        };\r
+        \r
+        createEncryptionEntry(dir, "EncryptionInfo", er);\r
     }\r
+    \r
+    \r
+    /**\r
+     * 2.3.4.15 Data Encryption (Agile Encryption)\r
+     * \r
+     * The EncryptedPackage stream (1) MUST be encrypted in 4096-byte segments to facilitate nearly\r
+     * random access while allowing CBC modes to be used in the encryption process.\r
+     * The initialization vector for the encryption process MUST be obtained by using the zero-based\r
+     * segment number as a blockKey and the binary form of the KeyData.saltValue as specified in\r
+     * section 2.3.4.12. The block number MUST be represented as a 32-bit unsigned integer.\r
+     * Data blocks MUST then be encrypted by using the initialization vector and the intermediate key\r
+     * obtained by decrypting the encryptedKeyValue from a KeyEncryptor contained within the\r
+     * KeyEncryptors sequence as specified in section 2.3.4.10. The final data block MUST be padded to\r
+     * the next integral multiple of the KeyData.blockSize value. Any padding bytes can be used. Note\r
+     * that the StreamSize field of the EncryptedPackage field specifies the number of bytes of\r
+     * unencrypted data as specified in section 2.3.4.4.\r
+     */\r
+    private class AgileCipherOutputStream extends ChunkedCipherOutputStream {\r
+        public AgileCipherOutputStream(DirectoryNode dir) throws IOException, GeneralSecurityException {\r
+            super(dir, 4096);\r
+        }\r
+        \r
+        @Override\r
+        protected Cipher initCipherForBlock(Cipher existing, int block, boolean lastChunk)\r
+        throws GeneralSecurityException {\r
+            return AgileDecryptor.initCipherForBlock(existing, block, lastChunk, builder, getSecretKey(), Cipher.ENCRYPT_MODE);\r
+        }\r
+\r
+        @Override\r
+        protected void calculateChecksum(File fileOut, int oleStreamSize)\r
+        throws GeneralSecurityException, IOException {\r
+            // integrityHMAC needs to be updated before the encryption document is created\r
+            updateIntegrityHMAC(fileOut, oleStreamSize); \r
+        }\r
+        \r
+        @Override\r
+        protected void createEncryptionInfoEntry(DirectoryNode dir, File tmpFile)\r
+        throws IOException, GeneralSecurityException {\r
+            AgileEncryptor.this.createEncryptionInfoEntry(dir, tmpFile);\r
+        }\r
+    }\r
+\r
 }\r
index 131a5c2e49636e475df74be505106b8bba09fad1..0d441d68da012d2472cb2b388ea81eedd8ebb84b 100644 (file)
@@ -53,7 +53,7 @@ public class TestAgileEncryptionParameters {
     @Parameter(value = 2)\r
     public ChainingMode cm;\r
 \r
-    @Parameters\r
+    @Parameters(name="{0} {1} {2}")\r
     public static Collection<Object[]> data() {\r
         CipherAlgorithm caList[] = { CipherAlgorithm.aes128, CipherAlgorithm.aes192, CipherAlgorithm.aes256, CipherAlgorithm.rc2, CipherAlgorithm.des, CipherAlgorithm.des3 };\r
         HashAlgorithm haList[] = { HashAlgorithm.sha1, HashAlgorithm.sha256, HashAlgorithm.sha384, HashAlgorithm.sha512, HashAlgorithm.md5 };\r
@@ -86,7 +86,7 @@ public class TestAgileEncryptionParameters {
         ByteArrayOutputStream bos = new ByteArrayOutputStream();\r
 \r
         POIFSFileSystem fsEnc = new POIFSFileSystem();\r
-        EncryptionInfo infoEnc = new EncryptionInfo(fsEnc, EncryptionMode.agile, ca, ha, -1, -1, cm);\r
+        EncryptionInfo infoEnc = new EncryptionInfo(EncryptionMode.agile, ca, ha, -1, -1, cm);\r
         Encryptor enc = infoEnc.getEncryptor();\r
         enc.confirmPassword("foobaa");\r
         OutputStream os = enc.getDataStream(fsEnc);\r
index fa04dde5e9cf376abda435cd2ff94647ad696d13..ed7df2ed813c9e39a4ba5f5ad11c5e962d2c1e5e 100644 (file)
@@ -29,6 +29,7 @@ import java.util.zip.ZipEntry;
 import java.util.zip.ZipInputStream;\r
 \r
 import org.apache.poi.POIDataSamples;\r
+import org.apache.poi.poifs.filesystem.DirectoryNode;\r
 import org.apache.poi.poifs.filesystem.NPOIFSFileSystem;\r
 import org.apache.poi.poifs.filesystem.POIFSFileSystem;\r
 import org.apache.poi.util.IOUtils;\r
@@ -60,7 +61,7 @@ public class TestDecryptor {
 \r
         d.verifyPassword(Decryptor.DEFAULT_PASSWORD);\r
 \r
-        zipOk(fs, d);\r
+        zipOk(fs.getRoot(), d);\r
     }\r
 \r
     @Test\r
@@ -75,21 +76,22 @@ public class TestDecryptor {
 \r
         assertTrue(d.verifyPassword(Decryptor.DEFAULT_PASSWORD));\r
 \r
-        zipOk(fs, d);\r
+        zipOk(fs.getRoot(), d);\r
     }\r
 \r
-    private void zipOk(POIFSFileSystem fs, Decryptor d) throws IOException, GeneralSecurityException {\r
-        ZipInputStream zin = new ZipInputStream(d.getDataStream(fs));\r
+    private void zipOk(DirectoryNode root, Decryptor d) throws IOException, GeneralSecurityException {\r
+        ZipInputStream zin = new ZipInputStream(d.getDataStream(root));\r
 \r
         while (true) {\r
             ZipEntry entry = zin.getNextEntry();\r
-            if (entry==null) {\r
-                break;\r
-            }\r
-\r
-            while (zin.available()>0) {\r
-                zin.skip(zin.available());\r
-            }\r
+            if (entry==null) break;\r
+            // crc32 is checked within zip-stream\r
+            if (entry.isDirectory()) continue;\r
+            zin.skip(entry.getSize());\r
+            byte buf[] = new byte[10];\r
+            int readBytes = zin.read(buf);\r
+            // zin.available() doesn't work for entries\r
+            assertEquals("size failed for "+entry.getName(), -1, readBytes);\r
         }\r
         \r
         zin.close();\r
index 3584b8f9bddb6f3b93b1f543e80dbf711c3185ed..37deba5e8203158f37ad620445f155243c444e86 100644 (file)
@@ -16,9 +16,8 @@
 ==================================================================== */\r
 package org.apache.poi.poifs.crypt;\r
 \r
-import static org.hamcrest.core.IsEqual.equalTo;\r
+import static org.junit.Assert.assertArrayEquals;\r
 import static org.junit.Assert.assertEquals;\r
-import static org.junit.Assert.assertThat;\r
 import static org.junit.Assert.assertTrue;\r
 \r
 import java.io.ByteArrayInputStream;\r
@@ -49,7 +48,44 @@ import org.junit.Test;
 \r
 public class TestEncryptor {\r
     @Test\r
-    public void testAgileEncryption() throws Exception {\r
+    public void binaryRC4Encryption() throws Exception {\r
+        // please contribute a real sample file, which is binary rc4 encrypted\r
+        // ... at least the output can be opened in Excel Viewer \r
+        String password = "pass";\r
+\r
+        InputStream is = POIDataSamples.getSpreadSheetInstance().openResourceAsStream("SimpleMultiCell.xlsx");\r
+        ByteArrayOutputStream payloadExpected = new ByteArrayOutputStream();\r
+        IOUtils.copy(is, payloadExpected);\r
+        is.close();\r
+        \r
+        POIFSFileSystem fs = new POIFSFileSystem();\r
+        EncryptionInfo ei = new EncryptionInfo(EncryptionMode.binaryRC4);\r
+        Encryptor enc = ei.getEncryptor();\r
+        enc.confirmPassword(password);\r
+        \r
+        OutputStream os = enc.getDataStream(fs.getRoot());\r
+        payloadExpected.writeTo(os);\r
+        os.close();\r
+        \r
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();\r
+        fs.writeFilesystem(bos);\r
+        \r
+        fs = new POIFSFileSystem(new ByteArrayInputStream(bos.toByteArray()));\r
+        ei = new EncryptionInfo(fs);\r
+        Decryptor dec = ei.getDecryptor();\r
+        boolean b = dec.verifyPassword(password);\r
+        assertTrue(b);\r
+        \r
+        ByteArrayOutputStream payloadActual = new ByteArrayOutputStream();\r
+        is = dec.getDataStream(fs.getRoot());\r
+        IOUtils.copy(is,payloadActual);\r
+        is.close();\r
+        \r
+        assertArrayEquals(payloadExpected.toByteArray(), payloadActual.toByteArray());\r
+    }\r
+    \r
+    @Test\r
+    public void agileEncryption() throws Exception {\r
         int maxKeyLen = Cipher.getMaxAllowedKeyLength("AES");\r
         Assume.assumeTrue("Please install JCE Unlimited Strength Jurisdiction Policy files for AES 256", maxKeyLen == 2147483647);\r
 \r
@@ -92,7 +128,7 @@ public class TestEncryptor {
         \r
         POIFSFileSystem fs = new POIFSFileSystem();\r
         EncryptionInfo infoActual = new EncryptionInfo(\r
-              fs, EncryptionMode.agile\r
+              EncryptionMode.agile\r
             , infoExpected.getVerifier().getCipherAlgorithm()\r
             , infoExpected.getVerifier().getHashAlgorithm()\r
             , infoExpected.getHeader().getKeySize()\r
@@ -134,14 +170,14 @@ public class TestEncryptor {
         \r
         AgileEncryptionHeader aehExpected = (AgileEncryptionHeader)infoExpected.getHeader();\r
         AgileEncryptionHeader aehActual = (AgileEncryptionHeader)infoActual.getHeader();\r
-        assertThat(aehExpected.getEncryptedHmacKey(), equalTo(aehActual.getEncryptedHmacKey()));\r
+        assertArrayEquals(aehExpected.getEncryptedHmacKey(), aehActual.getEncryptedHmacKey());\r
         assertEquals(decPackLenExpected, decPackLenActual);\r
-        assertThat(payloadExpected, equalTo(payloadActual));\r
-        assertThat(encPackExpected, equalTo(encPackActual));\r
+        assertArrayEquals(payloadExpected, payloadActual);\r
+        assertArrayEquals(encPackExpected, encPackActual);\r
     }\r
     \r
     @Test\r
-    public void testStandardEncryption() throws Exception {\r
+    public void standardEncryption() throws Exception {\r
         File file = POIDataSamples.getDocumentInstance().getFile("bug53475-password-is-solrcell.docx");\r
         String pass = "solrcell";\r
         \r
@@ -170,7 +206,7 @@ public class TestEncryptor {
         \r
         POIFSFileSystem fs = new POIFSFileSystem();\r
         EncryptionInfo infoActual = new EncryptionInfo(\r
-              fs, EncryptionMode.standard\r
+              EncryptionMode.standard\r
             , infoExpected.getVerifier().getCipherAlgorithm()\r
             , infoExpected.getVerifier().getHashAlgorithm()\r
             , infoExpected.getHeader().getKeySize()\r
@@ -181,15 +217,15 @@ public class TestEncryptor {
         Encryptor e = Encryptor.getInstance(infoActual);\r
         e.confirmPassword(pass, keySpec, keySalt, verifierExpected, verifierSaltExpected, null);\r
         \r
-        assertThat(infoExpected.getVerifier().getEncryptedVerifier(), equalTo(infoActual.getVerifier().getEncryptedVerifier()));\r
-        assertThat(infoExpected.getVerifier().getEncryptedVerifierHash(), equalTo(infoActual.getVerifier().getEncryptedVerifierHash()));\r
+        assertArrayEquals(infoExpected.getVerifier().getEncryptedVerifier(), infoActual.getVerifier().getEncryptedVerifier());\r
+        assertArrayEquals(infoExpected.getVerifier().getEncryptedVerifierHash(), infoActual.getVerifier().getEncryptedVerifierHash());\r
 \r
         // now we use a newly generated salt/verifier and check\r
         // if the file content is still the same \r
 \r
         fs = new POIFSFileSystem();\r
         infoActual = new EncryptionInfo(\r
-              fs, EncryptionMode.standard\r
+              EncryptionMode.standard\r
             , infoExpected.getVerifier().getCipherAlgorithm()\r
             , infoExpected.getVerifier().getHashAlgorithm()\r
             , infoExpected.getHeader().getKeySize()\r
@@ -227,12 +263,12 @@ public class TestEncryptor {
         nfs.close();\r
         byte payloadActual[] = bos.toByteArray();        \r
         \r
-        assertThat(payloadExpected, equalTo(payloadActual));\r
+        assertArrayEquals(payloadExpected, payloadActual);\r
     }\r
     \r
     @Test\r
     @Ignore\r
-    public void testInPlaceRewrite() throws Exception {\r
+    public void inPlaceRewrite() throws Exception {\r
         File f = TempFile.createTempFile("protected_agile", ".docx");\r
         // File f = new File("protected_agile.docx");\r
         FileOutputStream fos = new FileOutputStream(f);\r
@@ -264,10 +300,10 @@ public class TestEncryptor {
     \r
     \r
     private void listEntry(DocumentNode de, String ext, String path) throws IOException {\r
-        path += "\\" + de.getName().replace('\u0006', '_');\r
+        path += "\\" + de.getName().replaceAll("[\\p{Cntrl}]", "_");\r
         System.out.println(ext+": "+path+" ("+de.getSize()+" bytes)");\r
         \r
-        String name = de.getName().replace('\u0006', '_');\r
+        String name = de.getName().replaceAll("[\\p{Cntrl}]", "_");\r
         \r
         InputStream is = ((DirectoryNode)de.getParent()).createDocumentInputStream(de);\r
         FileOutputStream fos = new FileOutputStream("solr."+name+"."+ext);\r
index d8839241ccb94c4a804d61bc36117a4c7fede6cd..07a01aa244635108e021a619fc0c97744e4cf258 100644 (file)
 
 package org.apache.poi.hslf;
 
-import java.io.FileNotFoundException;
+import java.io.OutputStream;
+import java.security.GeneralSecurityException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.TreeMap;
+
+import javax.crypto.Cipher;
+import javax.crypto.CipherOutputStream;
 
 import org.apache.poi.hslf.exceptions.CorruptPowerPointFileException;
-import org.apache.poi.hslf.record.CurrentUserAtom;
+import org.apache.poi.hslf.exceptions.EncryptedPowerPointFileException;
 import org.apache.poi.hslf.record.DocumentEncryptionAtom;
 import org.apache.poi.hslf.record.PersistPtrHolder;
+import org.apache.poi.hslf.record.PositionDependentRecord;
 import org.apache.poi.hslf.record.Record;
 import org.apache.poi.hslf.record.UserEditAtom;
+import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey;
+import org.apache.poi.poifs.crypt.Decryptor;
+import org.apache.poi.poifs.crypt.EncryptionInfo;
+import org.apache.poi.poifs.crypt.Encryptor;
+import org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIDecryptor;
+import org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIEncryptor;
+import org.apache.poi.util.BitField;
+import org.apache.poi.util.Internal;
+import org.apache.poi.util.LittleEndian;
 
 /**
- * This class provides helper functions for determining if a
- *  PowerPoint document is Encrypted.
- * In future, it may also provide Encryption and Decryption
- *  functions, but first we'd need to figure out how
- *  PowerPoint encryption is really done!
- *
- * @author Nick Burch
+ * This class provides helper functions for encrypted PowerPoint documents.
  */
+@Internal
+public class EncryptedSlideShow {
+    DocumentEncryptionAtom dea;
+    CryptoAPIEncryptor enc = null;
+    CryptoAPIDecryptor dec = null;
+    Cipher cipher = null;
+    CipherOutputStream cyos = null;
+
+    private static final BitField fieldRecInst = new BitField(0xFFF0);
+    
+    protected EncryptedSlideShow(DocumentEncryptionAtom dea) {
+        this.dea = dea;
+    }
+
+    protected EncryptedSlideShow(byte[] docstream, NavigableMap<Integer,Record> recordMap) {
+        // check for DocumentEncryptionAtom, which would be at the last offset
+        // need to ignore already set UserEdit and PersistAtoms
+        UserEditAtom userEditAtomWithEncryption = null;
+        for (Map.Entry<Integer, Record> me : recordMap.descendingMap().entrySet()) {
+            Record r = me.getValue();
+            if (!(r instanceof UserEditAtom)) continue;
+            UserEditAtom uea = (UserEditAtom)r;
+            if (uea.getEncryptSessionPersistIdRef() != -1) {
+                userEditAtomWithEncryption = uea;
+                break;
+            }
+        }
+
+        if (userEditAtomWithEncryption == null) {
+            dea = null;
+            return;
+        }
+
+        Record r = recordMap.get(userEditAtomWithEncryption.getPersistPointersOffset());
+        assert(r instanceof PersistPtrHolder);
+        PersistPtrHolder ptr = (PersistPtrHolder)r;
+        
+        Integer encOffset = ptr.getSlideLocationsLookup().get(userEditAtomWithEncryption.getEncryptSessionPersistIdRef());
+        assert(encOffset != null);
+        
+        r = recordMap.get(encOffset);
+        if (r == null) {
+            r = Record.buildRecordAtOffset(docstream, encOffset);
+            recordMap.put(encOffset, r);
+        }
+        assert(r instanceof DocumentEncryptionAtom);
+        this.dea = (DocumentEncryptionAtom)r;
+        
+        CryptoAPIDecryptor dec = (CryptoAPIDecryptor)dea.getEncryptionInfo().getDecryptor();
+        String pass = Biff8EncryptionKey.getCurrentUserPassword();
+        if(!dec.verifyPassword(pass != null ? pass : Decryptor.DEFAULT_PASSWORD)) {
+            throw new EncryptedPowerPointFileException("PowerPoint file is encrypted. The correct password needs to be set via Biff8EncryptionKey.setCurrentUserPassword()");
+        }
+     }
+
+    public DocumentEncryptionAtom getDocumentEncryptionAtom() {
+        return dea;
+    }
+    
+    protected void setPersistId(int persistId) {
+        if (enc != null && dec != null) {
+            throw new EncryptedPowerPointFileException("Use instance either for en- or decryption");
+        }
+        
+        try {
+            if (enc != null) cipher = enc.initCipherForBlock(cipher, persistId);
+            if (dec != null) cipher = dec.initCipherForBlock(cipher, persistId);
+        } catch (GeneralSecurityException e) {
+            throw new EncryptedPowerPointFileException(e);
+        }
+    }
+    
+    protected void decryptInit() {
+        if (dec != null) return;
+        EncryptionInfo ei = dea.getEncryptionInfo();
+        dec = (CryptoAPIDecryptor)ei.getDecryptor();
+    }
+    
+    protected void encryptInit() {
+        if (enc != null) return;
+        EncryptionInfo ei = dea.getEncryptionInfo();
+        enc = (CryptoAPIEncryptor)ei.getEncryptor();
+    }
+    
+
+    
+    protected OutputStream encryptRecord(OutputStream plainStream, int persistId, Record record) {
+        boolean isPlain = (dea == null 
+            || record instanceof UserEditAtom
+            || record instanceof PersistPtrHolder
+            || record instanceof DocumentEncryptionAtom
+        );
+        if (isPlain) return plainStream;
+
+        encryptInit();
+        setPersistId(persistId);
+        
+        if (cyos == null) {
+            cyos = new CipherOutputStream(plainStream, cipher);
+        }
+        return cyos;
+    }
+
+    protected void decryptRecord(byte[] docstream, int persistId, int offset) {
+        if (dea == null) return;
+
+        decryptInit();
+        setPersistId(persistId);
+        
+        try {
+            // decrypt header and read length to be decrypted
+            cipher.update(docstream, offset, 8, docstream, offset);
+            // decrypt the rest of the record
+            int rlen = (int)LittleEndian.getUInt(docstream, offset+4);
+            cipher.update(docstream, offset+8, rlen, docstream, offset+8);
+        } catch (GeneralSecurityException e) {
+            throw new CorruptPowerPointFileException(e);
+        }       
+    }        
+    
+    protected void decryptPicture(byte[] pictstream, int offset) {
+        if (dea == null) return;
+        
+        decryptInit();
+        setPersistId(0);
+        
+        try {
+            // decrypt header and read length to be decrypted
+            cipher.doFinal(pictstream, offset, 8, pictstream, offset);
+            int recInst = fieldRecInst.getValue(LittleEndian.getUShort(pictstream, offset));
+            int recType = LittleEndian.getUShort(pictstream, offset+2);
+            int rlen = (int)LittleEndian.getUInt(pictstream, offset+4);
+            offset += 8;
+            int endOffset = offset + rlen; 
+
+            if (recType == 0xF007) {
+                // TOOD: get a real example file ... to actual test the FBSE entry
+                // not sure where the foDelay block is
+                
+                // File BLIP Store Entry (FBSE)
+                cipher.doFinal(pictstream, offset, 1, pictstream, offset); // btWin32
+                offset++;
+                cipher.doFinal(pictstream, offset, 1, pictstream, offset); // btMacOS
+                offset++;
+                cipher.doFinal(pictstream, offset, 16, pictstream, offset); // rgbUid
+                offset += 16;
+                cipher.doFinal(pictstream, offset, 2, pictstream, offset); // tag
+                offset += 2;
+                cipher.doFinal(pictstream, offset, 4, pictstream, offset); // size
+                offset += 4;
+                cipher.doFinal(pictstream, offset, 4, pictstream, offset); // cRef
+                offset += 4;
+                cipher.doFinal(pictstream, offset, 4, pictstream, offset); // foDelay
+                offset += 4;
+                cipher.doFinal(pictstream, offset+0, 1, pictstream, offset+0); // unused1
+                cipher.doFinal(pictstream, offset+1, 1, pictstream, offset+1); // cbName
+                cipher.doFinal(pictstream, offset+2, 1, pictstream, offset+2); // unused2
+                cipher.doFinal(pictstream, offset+3, 1, pictstream, offset+3); // unused3
+                int cbName = LittleEndian.getUShort(pictstream, offset+1);
+                offset += 4;
+                if (cbName > 0) {
+                    cipher.doFinal(pictstream, offset, cbName, pictstream, offset); // nameData
+                    offset += cbName;
+                }
+                if (offset == endOffset) {
+                    return; // no embedded blip
+                }
+                // fall through, read embedded blip now
+
+                // update header data
+                cipher.doFinal(pictstream, offset, 8, pictstream, offset);
+                recInst = fieldRecInst.getValue(LittleEndian.getUShort(pictstream, offset));
+                recType = LittleEndian.getUShort(pictstream, offset+2);
+                rlen = (int)LittleEndian.getUInt(pictstream, offset+4);
+                offset += 8;
+            }
+
+            int rgbUidCnt = (recInst == 0x217 || recInst == 0x3D5 || recInst == 0x46B || recInst == 0x543 ||
+                recInst == 0x6E1 || recInst == 0x6E3 || recInst == 0x6E5 || recInst == 0x7A9) ? 2 : 1;
+            
+            for (int i=0; i<rgbUidCnt; i++) {
+                cipher.doFinal(pictstream, offset, 16, pictstream, offset); // rgbUid 1/2
+                offset += 16;
+            }
+            
+            if (recType == 0xF01A || recType == 0XF01B || recType == 0XF01C) {
+                cipher.doFinal(pictstream, offset, 34, pictstream, offset); // metafileHeader
+                offset += 34;
+            } else {
+                cipher.doFinal(pictstream, offset, 1, pictstream, offset); // tag
+                offset += 1;
+            }
+            
+            int blipLen = endOffset - offset;
+            cipher.doFinal(pictstream, offset, blipLen, pictstream, offset);
+        } catch (GeneralSecurityException e) {
+            throw new CorruptPowerPointFileException(e);
+        }       
+    }
+
+    protected void encryptPicture(byte[] pictstream, int offset) {
+        if (dea == null) return;
+        
+        encryptInit();
+        setPersistId(0);
+
+        try {
+            int recInst = fieldRecInst.getValue(LittleEndian.getUShort(pictstream, offset));
+            int recType = LittleEndian.getUShort(pictstream, offset+2);
+            int rlen = (int)LittleEndian.getUInt(pictstream, offset+4);
+            cipher.doFinal(pictstream, offset, 8, pictstream, offset);
+            offset += 8;
+            int endOffset = offset + rlen; 
+
+            if (recType == 0xF007) {
+                // TOOD: get a real example file ... to actual test the FBSE entry
+                // not sure where the foDelay block is
+                
+                // File BLIP Store Entry (FBSE)
+                cipher.doFinal(pictstream, offset, 1, pictstream, offset); // btWin32
+                offset++;
+                cipher.doFinal(pictstream, offset, 1, pictstream, offset); // btMacOS
+                offset++;
+                cipher.doFinal(pictstream, offset, 16, pictstream, offset); // rgbUid
+                offset += 16;
+                cipher.doFinal(pictstream, offset, 2, pictstream, offset); // tag
+                offset += 2;
+                cipher.doFinal(pictstream, offset, 4, pictstream, offset); // size
+                offset += 4;
+                cipher.doFinal(pictstream, offset, 4, pictstream, offset); // cRef
+                offset += 4;
+                cipher.doFinal(pictstream, offset, 4, pictstream, offset); // foDelay
+                offset += 4;
+                int cbName = LittleEndian.getUShort(pictstream, offset+1);
+                cipher.doFinal(pictstream, offset+0, 1, pictstream, offset+0); // unused1
+                cipher.doFinal(pictstream, offset+1, 1, pictstream, offset+1); // cbName
+                cipher.doFinal(pictstream, offset+2, 1, pictstream, offset+2); // unused2
+                cipher.doFinal(pictstream, offset+3, 1, pictstream, offset+3); // unused3
+                offset += 4;
+                if (cbName > 0) {
+                    cipher.doFinal(pictstream, offset, cbName, pictstream, offset); // nameData
+                    offset += cbName;
+                }
+                if (offset == endOffset) {
+                    return; // no embedded blip
+                }
+                // fall through, read embedded blip now
+
+                // update header data
+                recInst = fieldRecInst.getValue(LittleEndian.getUShort(pictstream, offset));
+                recType = LittleEndian.getUShort(pictstream, offset+2);
+                rlen = (int)LittleEndian.getUInt(pictstream, offset+4);
+                cipher.doFinal(pictstream, offset, 8, pictstream, offset);
+                offset += 8;
+            }
+            
+            int rgbUidCnt = (recInst == 0x217 || recInst == 0x3D5 || recInst == 0x46B || recInst == 0x543 ||
+                recInst == 0x6E1 || recInst == 0x6E3 || recInst == 0x6E5 || recInst == 0x7A9) ? 2 : 1;
+                
+            for (int i=0; i<rgbUidCnt; i++) {
+                cipher.doFinal(pictstream, offset, 16, pictstream, offset); // rgbUid 1/2
+                offset += 16;
+            }
+            
+            if (recType == 0xF01A || recType == 0XF01B || recType == 0XF01C) {
+                cipher.doFinal(pictstream, offset, 34, pictstream, offset); // metafileHeader
+                offset += 34;
+            } else {
+                cipher.doFinal(pictstream, offset, 1, pictstream, offset); // tag
+                offset += 1;
+            }
+            
+            int blipLen = endOffset - offset;
+            cipher.doFinal(pictstream, offset, blipLen, pictstream, offset);
+        } catch (GeneralSecurityException e) {
+            throw new CorruptPowerPointFileException(e);
+        }       
+    }
+
+    protected Record[] updateEncryptionRecord(Record records[]) {
+        String password = Biff8EncryptionKey.getCurrentUserPassword();
+        if (password == null) {
+            if (dea == null) {
+                // no password given, no encryption record exits -> done
+                return records;
+            } else {
+                // need to remove password data
+                dea = null;
+                return removeEncryptionRecord(records);
+            }
+        } else {
+            // create password record
+            if (dea == null) {
+                dea = new DocumentEncryptionAtom();
+            }
+            EncryptionInfo ei = dea.getEncryptionInfo();
+            byte salt[] = ei.getVerifier().getSalt();
+            Encryptor enc = ei.getEncryptor();
+            if (salt == null) {
+                enc.confirmPassword(password);
+            } else {
+                byte verifier[] = ei.getDecryptor().getVerifier();
+                enc.confirmPassword(password, null, null, verifier, salt, null);
+            }
+
+            // move EncryptionRecord to last slide position
+            records = normalizeRecords(records);
+            return addEncryptionRecord(records, dea);
+        }
+    }
+
+    /**
+     * remove duplicated UserEditAtoms and merge PersistPtrHolder.
+     * Before this method is called, make sure that the offsets are correct,
+     * i.e. call {@link HSLFSlideShow#updateAndWriteDependantRecords(OutputStream, Map)}
+     */
+    protected static Record[] normalizeRecords(Record records[]) {
+        // http://msdn.microsoft.com/en-us/library/office/gg615594(v=office.14).aspx
+        // repeated slideIds can be overwritten, i.e. ignored
+        
+        UserEditAtom uea = null;
+        PersistPtrHolder pph = null;
+        TreeMap<Integer,Integer> slideLocations = new TreeMap<Integer,Integer>();
+        TreeMap<Integer,Record> recordMap = new TreeMap<Integer,Record>();
+        List<Integer> obsoleteOffsets = new ArrayList<Integer>();
+        int duplicatedCount = 0;
+        for (Record r : records) {
+            assert(r instanceof PositionDependentRecord);
+            PositionDependentRecord pdr = (PositionDependentRecord)r;
+            if (pdr instanceof UserEditAtom) {
+                uea = (UserEditAtom)pdr;
+                continue;
+            }
+            
+            if (pdr instanceof PersistPtrHolder) {
+                if (pph != null) {
+                    duplicatedCount++;
+                }
+                pph = (PersistPtrHolder)pdr;
+                for (Map.Entry<Integer,Integer> me : pph.getSlideLocationsLookup().entrySet()) {
+                    Integer oldOffset = slideLocations.put(me.getKey(), me.getValue());
+                    if (oldOffset != null) obsoleteOffsets.add(oldOffset);
+                }
+                continue;
+            }
+            
+            recordMap.put(pdr.getLastOnDiskOffset(), r);
+        }
+        recordMap.put(pph.getLastOnDiskOffset(), pph);
+        recordMap.put(uea.getLastOnDiskOffset(), uea);
+
+        assert(uea != null && pph != null && uea.getPersistPointersOffset() == pph.getLastOnDiskOffset());
+        
+        if (duplicatedCount == 0 && obsoleteOffsets.isEmpty()) {
+            return records;
+        }
+
+        uea.setLastUserEditAtomOffset(0);
+        pph.clear();
+        for (Map.Entry<Integer,Integer> me : slideLocations.entrySet()) {
+            pph.addSlideLookup(me.getKey(), me.getValue());
+        }
+        
+        for (Integer oldOffset : obsoleteOffsets) {
+            recordMap.remove(oldOffset);
+        }
+        
+        return recordMap.values().toArray(new Record[recordMap.size()]);
+    }
+     
+    
+    protected static Record[] removeEncryptionRecord(Record records[]) {
+        int deaSlideId = -1;
+        int deaOffset = -1;
+        PersistPtrHolder ptr = null;
+        UserEditAtom uea = null;
+        List<Record> recordList = new ArrayList<Record>();
+        for (Record r : records) {
+            if (r instanceof DocumentEncryptionAtom) {
+                deaOffset = ((DocumentEncryptionAtom)r).getLastOnDiskOffset();
+                continue;
+            } else if (r instanceof UserEditAtom) {
+                uea = (UserEditAtom)r;
+                deaSlideId = uea.getEncryptSessionPersistIdRef();
+                uea.setEncryptSessionPersistIdRef(-1);
+            } else if (r instanceof PersistPtrHolder) {
+                ptr = (PersistPtrHolder)r;
+            }
+            recordList.add(r);
+        }
+        
+        assert(ptr != null);
+        if (deaSlideId == -1 && deaOffset == -1) return records;
+        
+        TreeMap<Integer,Integer> tm = new TreeMap<Integer,Integer>(ptr.getSlideLocationsLookup());
+        ptr.clear();
+        int maxSlideId = -1;
+        for (Map.Entry<Integer,Integer> me : tm.entrySet()) {
+            if (me.getKey() == deaSlideId || me.getValue() == deaOffset) continue;
+            ptr.addSlideLookup(me.getKey(), me.getValue());
+            maxSlideId = Math.max(me.getKey(), maxSlideId);
+        }
+        
+        uea.setMaxPersistWritten(maxSlideId);
+
+        records = recordList.toArray(new Record[recordList.size()]);
+        
+        return records;
+    }
+
+
+    protected static Record[] addEncryptionRecord(Record records[], DocumentEncryptionAtom dea) {
+        assert(dea != null);
+        int ueaIdx = -1, ptrIdx = -1, deaIdx = -1, idx = -1;
+        for (Record r : records) {
+            idx++;
+            if (r instanceof UserEditAtom) ueaIdx = idx;
+            else if (r instanceof PersistPtrHolder) ptrIdx = idx;
+            else if (r instanceof DocumentEncryptionAtom) deaIdx = idx;
+        }
+        assert(ueaIdx != -1 && ptrIdx != -1 && ptrIdx < ueaIdx);
+        if (deaIdx != -1) {
+            DocumentEncryptionAtom deaOld = (DocumentEncryptionAtom)records[deaIdx];
+            dea.setLastOnDiskOffset(deaOld.getLastOnDiskOffset());
+            records[deaIdx] = dea;
+            return records;
+        } else {
+            PersistPtrHolder ptr = (PersistPtrHolder)records[ptrIdx];
+            UserEditAtom uea = ((UserEditAtom)records[ueaIdx]);
+            dea.setLastOnDiskOffset(ptr.getLastOnDiskOffset()-1);
+            int nextSlideId = uea.getMaxPersistWritten()+1;
+            ptr.addSlideLookup(nextSlideId, ptr.getLastOnDiskOffset()-1);
+            uea.setEncryptSessionPersistIdRef(nextSlideId);
+            uea.setMaxPersistWritten(nextSlideId);
+            
+            Record newRecords[] = new Record[records.length+1];
+            if (ptrIdx > 0) System.arraycopy(records, 0, newRecords, 0, ptrIdx);
+            if (ptrIdx < records.length-1) System.arraycopy(records, ptrIdx, newRecords, ptrIdx+1, records.length-ptrIdx);
+            newRecords[ptrIdx] = dea;
+            return newRecords;
+        }
+    }
 
-public final class EncryptedSlideShow
-{
-   /**
-    * Check to see if a HSLFSlideShow represents an encrypted
-    *  PowerPoint document, or not
-    * @param hss The HSLFSlideShow to check
-    * @return true if encrypted, otherwise false
-    */
-   public static boolean checkIfEncrypted(HSLFSlideShow hss) {
-      // Easy way to check - contains a stream
-      //  "EncryptedSummary"
-      try {
-         hss.getPOIFSDirectory().getEntry("EncryptedSummary");
-         return true;
-      } catch(FileNotFoundException fnfe) {
-         // Doesn't have encrypted properties
-      }
-
-      // If they encrypted the document but not the properties,
-      //  it's harder.
-      // We need to see what the last record pointed to by the
-      //  first PersistPrtHolder is - if it's a
-      //  DocumentEncryptionAtom, then the file's Encrypted
-      DocumentEncryptionAtom dea = fetchDocumentEncryptionAtom(hss);
-      if(dea != null) {
-         return true;
-      }
-      return false;
-   }
-
-       /**
-        * Return the DocumentEncryptionAtom for a HSLFSlideShow, or
-        *  null if there isn't one.
-        * @return a DocumentEncryptionAtom, or null if there isn't one
-        */
-       public static DocumentEncryptionAtom fetchDocumentEncryptionAtom(HSLFSlideShow hss) {
-               // Will be the last Record pointed to by the
-               //  first PersistPrtHolder, if there is one
-
-               CurrentUserAtom cua = hss.getCurrentUserAtom();
-               if(cua.getCurrentEditOffset() != 0) {
-                       // Check it's not past the end of the file
-                       if(cua.getCurrentEditOffset() > hss.getUnderlyingBytes().length) {
-                               throw new CorruptPowerPointFileException("The CurrentUserAtom claims that the offset of last edit details are past the end of the file");
-                       }
-
-                       // Grab the details of the UserEditAtom there
-                       // If the record's messed up, we could AIOOB
-                       Record r = null;
-                       try {
-                               r = Record.buildRecordAtOffset(
-                                               hss.getUnderlyingBytes(),
-                                               (int)cua.getCurrentEditOffset()
-                               );
-                       } catch (ArrayIndexOutOfBoundsException e) {
-                               return null;
-                       }
-                       if(r == null) { return null; }
-                       if(! (r instanceof UserEditAtom)) { return null; }
-                       UserEditAtom uea = (UserEditAtom)r;
-
-                       // Now get the PersistPtrHolder
-                       Record r2 = Record.buildRecordAtOffset(
-                                       hss.getUnderlyingBytes(),
-                                       uea.getPersistPointersOffset()
-                       );
-                       if(! (r2 instanceof PersistPtrHolder)) { return null; }
-                       PersistPtrHolder pph = (PersistPtrHolder)r2;
-
-                       // Now get the last record
-                       int[] slideIds = pph.getKnownSlideIDs();
-                       int maxSlideId = -1;
-                       for(int i=0; i<slideIds.length; i++) {
-                               if(slideIds[i] > maxSlideId) { maxSlideId = slideIds[i]; }
-                       }
-                       if(maxSlideId == -1) { return null; }
-
-                       int offset = (
-                                       (Integer)pph.getSlideLocationsLookup().get(
-                                                       Integer.valueOf(maxSlideId)
-                                       ) ).intValue();
-                       Record r3 = Record.buildRecordAtOffset(
-                                       hss.getUnderlyingBytes(),
-                                       offset
-                       );
-
-                       // If we have a DocumentEncryptionAtom, it'll be this one
-                       if(r3 instanceof DocumentEncryptionAtom) {
-                               return (DocumentEncryptionAtom)r3;
-                       }
-               }
-
-               return null;
-       }
 }
index 8669caf2cd146a34d329b36960890330cf1e3838..3c63317c62fc4e7966f50cc34a5038383353f18d 100644 (file)
@@ -23,6 +23,7 @@ import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.security.GeneralSecurityException;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Hashtable;
@@ -32,10 +33,11 @@ import java.util.NavigableMap;
 import java.util.TreeMap;
 
 import org.apache.poi.POIDocument;
+import org.apache.poi.hpsf.PropertySet;
 import org.apache.poi.hslf.exceptions.CorruptPowerPointFileException;
-import org.apache.poi.hslf.exceptions.EncryptedPowerPointFileException;
 import org.apache.poi.hslf.exceptions.HSLFException;
 import org.apache.poi.hslf.record.CurrentUserAtom;
+import org.apache.poi.hslf.record.DocumentEncryptionAtom;
 import org.apache.poi.hslf.record.ExOleObjStg;
 import org.apache.poi.hslf.record.PersistPtrHolder;
 import org.apache.poi.hslf.record.PersistRecord;
@@ -45,6 +47,7 @@ import org.apache.poi.hslf.record.RecordTypes;
 import org.apache.poi.hslf.record.UserEditAtom;
 import org.apache.poi.hslf.usermodel.ObjectData;
 import org.apache.poi.hslf.usermodel.PictureData;
+import org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIEncryptor;
 import org.apache.poi.poifs.filesystem.DirectoryNode;
 import org.apache.poi.poifs.filesystem.DocumentEntry;
 import org.apache.poi.poifs.filesystem.DocumentInputStream;
@@ -182,13 +185,6 @@ public final class HSLFSlideShow extends POIDocument {
                //  PowerPoint stream
                readPowerPointStream();
 
-               // Check to see if we have an encrypted document,
-               //  bailing out if we do
-               boolean encrypted = EncryptedSlideShow.checkIfEncrypted(this);
-               if(encrypted) {
-                       throw new EncryptedPowerPointFileException("Encrypted PowerPoint files are not supported");
-               }
-
                // Now, build records based on the PowerPoint stream
                buildRecords();
 
@@ -278,6 +274,7 @@ public final class HSLFSlideShow extends POIDocument {
            NavigableMap<Integer,Record> records = new TreeMap<Integer,Record>(); // offset -> record
         Map<Integer,Integer> persistIds = new HashMap<Integer,Integer>(); // offset -> persistId
         initRecordOffsets(docstream, usrOffset, records, persistIds);
+        EncryptedSlideShow decryptData = new EncryptedSlideShow(docstream, records);
         
         for (Map.Entry<Integer,Record> entry : records.entrySet()) {
             Integer offset = entry.getKey();
@@ -286,6 +283,7 @@ public final class HSLFSlideShow extends POIDocument {
             if (record == null) {
                 // all plain records have been already added,
                 // only new records need to be decrypted (tbd #35897)
+                decryptData.decryptRecord(docstream, persistId, offset);
                 record = Record.buildRecordAtOffset(docstream, offset);
                 entry.setValue(record);
             }
@@ -335,6 +333,16 @@ public final class HSLFSlideShow extends POIDocument {
         }       
     }
 
+    public DocumentEncryptionAtom getDocumentEncryptionAtom() {
+        for (Record r : _records) {
+            if (r instanceof DocumentEncryptionAtom) {
+                return (DocumentEncryptionAtom)r;
+            }
+        }
+        return null;
+    }
+    
+    
        /**
         * Find the "Current User" stream, and load it
         */
@@ -353,6 +361,7 @@ public final class HSLFSlideShow extends POIDocument {
        private void readOtherStreams() {
                // Currently, there aren't any
        }
+       
        /**
         * Find and read in pictures contained in this presentation.
         * This is lazily called as and when we want to touch pictures.
@@ -363,6 +372,8 @@ public final class HSLFSlideShow extends POIDocument {
 
         // if the presentation doesn't contain pictures - will use a null set instead
         if (!directory.hasEntry("Pictures")) return;
+
+        EncryptedSlideShow decryptData = new EncryptedSlideShow(getDocumentEncryptionAtom());
         
                DocumentEntry entry = (DocumentEntry)directory.getEntry("Pictures");
                byte[] pictstream = new byte[entry.getSize()];
@@ -375,6 +386,8 @@ public final class HSLFSlideShow extends POIDocument {
                // An empty picture record (length 0) will take up 8 bytes
         while (pos <= (pictstream.length-8)) {
             int offset = pos;
+
+            decryptData.decryptPicture(pictstream, offset);
             
             // Image signature
             int signature = LittleEndian.getUShort(pictstream, pos);
@@ -422,7 +435,21 @@ public final class HSLFSlideShow extends POIDocument {
             pos += imgsize;
         }
        }
-
+    
+    /**
+     * remove duplicated UserEditAtoms and merge PersistPtrHolder, i.e.
+     * remove document edit history
+     */
+    public void normalizeRecords() {
+        try {
+            updateAndWriteDependantRecords(null, null);
+        } catch (IOException e) {
+            throw new CorruptPowerPointFileException(e);
+        }
+        _records = EncryptedSlideShow.normalizeRecords(_records);
+    }
+   
+    
        /**
      * This is a helper functions, which is needed for adding new position dependent records
      * or finally write the slideshow to a file.
@@ -444,55 +471,67 @@ public final class HSLFSlideShow extends POIDocument {
         //   records are going to end up, in the new scheme
         // (Annoyingly, some powerpoint files have PersistPtrHolders
         //  that reference slides after the PersistPtrHolder)
-        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        UserEditAtom usr = null;
+        PersistPtrHolder ptr = null;
+        CountingOS cos = new CountingOS();
         for (Record record : _records) {
-            if(record instanceof PositionDependentRecord) {
-                PositionDependentRecord pdr = (PositionDependentRecord)record;
-                int oldPos = pdr.getLastOnDiskOffset();
-                int newPos = baos.size();
-                pdr.setLastOnDiskOffset(newPos);
-                if (oldPos != UNSET_OFFSET) {
-                    // new records don't need a mapping, as they aren't in a relation yet
-                    oldToNewPositions.put(Integer.valueOf(oldPos),Integer.valueOf(newPos));
-                }
+            // all top level records are position dependent
+            assert(record instanceof PositionDependentRecord);
+            PositionDependentRecord pdr = (PositionDependentRecord)record;
+            int oldPos = pdr.getLastOnDiskOffset();
+            int newPos = cos.size();
+            pdr.setLastOnDiskOffset(newPos);
+            if (oldPos != UNSET_OFFSET) {
+                // new records don't need a mapping, as they aren't in a relation yet
+                oldToNewPositions.put(oldPos,newPos);
+            }
+
+            // Grab interesting records as they come past
+            // this will only save the very last record of each type
+            RecordTypes.Type saveme = null;
+            int recordType = (int)record.getRecordType();
+            if (recordType == RecordTypes.PersistPtrIncrementalBlock.typeID) {
+                saveme = RecordTypes.PersistPtrIncrementalBlock;
+                ptr = (PersistPtrHolder)pdr;
+            } else if (recordType == RecordTypes.UserEditAtom.typeID) {
+                saveme = RecordTypes.UserEditAtom;
+                usr = (UserEditAtom)pdr;
+            }
+            if (interestingRecords != null && saveme != null) {
+                interestingRecords.put(saveme,pdr);
             }
             
             // Dummy write out, so the position winds on properly
-            record.writeOut(baos);
+            record.writeOut(cos);
         }
-        baos = null;
         
-        // For now, we're only handling PositionDependentRecord's that
-        // happen at the top level.
-        // In future, we'll need the handle them everywhere, but that's
-        // a bit trickier
-           UserEditAtom usr = null;
-        for (Record record : _records) {
-            if (record instanceof PositionDependentRecord) {
-                // We've already figured out their new location, and
-                // told them that
-                // Tell them of the positions of the other records though
-                PositionDependentRecord pdr = (PositionDependentRecord)record;
-                pdr.updateOtherRecordReferences(oldToNewPositions);
-    
-                // Grab interesting records as they come past
-                // this will only save the very last record of each type
-                RecordTypes.Type saveme = null;
-                int recordType = (int)record.getRecordType();
-                if (recordType == RecordTypes.PersistPtrIncrementalBlock.typeID) {
-                    saveme = RecordTypes.PersistPtrIncrementalBlock;
-                } else if (recordType == RecordTypes.UserEditAtom.typeID) {
-                    saveme = RecordTypes.UserEditAtom;
-                    usr = (UserEditAtom)pdr;
-                }
-                if (interestingRecords != null && saveme != null) {
-                    interestingRecords.put(saveme,pdr);
-                }
-            }
+        assert(usr != null && ptr != null);
+        
+        Map<Integer,Integer> persistIds = new HashMap<Integer,Integer>();
+        for (Map.Entry<Integer,Integer> entry : ptr.getSlideLocationsLookup().entrySet()) {
+            persistIds.put(oldToNewPositions.get(entry.getValue()), entry.getKey());
+        }
+        
+        EncryptedSlideShow encData = new EncryptedSlideShow(getDocumentEncryptionAtom());
+           
+           for (Record record : _records) {
+            assert(record instanceof PositionDependentRecord);
+            // We've already figured out their new location, and
+            // told them that
+            // Tell them of the positions of the other records though
+            PositionDependentRecord pdr = (PositionDependentRecord)record;
+            Integer persistId = persistIds.get(pdr.getLastOnDiskOffset());
+            if (persistId == null) persistId = 0;
+            
+            // For now, we're only handling PositionDependentRecord's that
+            // happen at the top level.
+            // In future, we'll need the handle them everywhere, but that's
+            // a bit trickier
+            pdr.updateOtherRecordReferences(oldToNewPositions);
             
             // Whatever happens, write out that record tree
             if (os != null) {
-                record.writeOut(os);
+                record.writeOut(encData.encryptRecord(os, persistId, record));
             }
         }
 
@@ -504,7 +543,7 @@ public final class HSLFSlideShow extends POIDocument {
         }
         currentUser.setCurrentEditOffset(usr.getLastOnDiskOffset());
        }
-       
+
     /**
      * Writes out the slideshow file the is represented by an instance
      *  of this class.
@@ -529,6 +568,16 @@ public final class HSLFSlideShow extends POIDocument {
      *           the passed in OutputStream
      */
     public void write(OutputStream out, boolean preserveNodes) throws IOException {
+        // read properties and pictures, with old encryption settings where appropriate 
+        if(_pictures == null) {
+           readPictures();
+        }
+        getDocumentSummaryInformation();
+
+        // set new encryption settings
+        EncryptedSlideShow encryptedSS = new EncryptedSlideShow(getDocumentEncryptionAtom());
+        _records = encryptedSS.updateEncryptionRecord(_records);
+
         // Get a new Filesystem to write into
         POIFSFileSystem outFS = new POIFSFileSystem();
 
@@ -537,8 +586,8 @@ public final class HSLFSlideShow extends POIDocument {
 
         // Write out the Property Streams
         writeProperties(outFS, writtenEntries);
-
-        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        
+        BufAccessBAOS baos = new BufAccessBAOS();
 
         // For position dependent records, hold where they were and now are
         // As we go along, update, and hand over, to any Position Dependent
@@ -546,27 +595,28 @@ public final class HSLFSlideShow extends POIDocument {
         updateAndWriteDependantRecords(baos, null);
 
         // Update our cached copy of the bytes that make up the PPT stream
-        _docstream = baos.toByteArray();
+        _docstream = new byte[baos.size()];
+        System.arraycopy(baos.getBuf(), 0, _docstream, 0, baos.size());
 
         // Write the PPT stream into the POIFS layer
-        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+        ByteArrayInputStream bais = new ByteArrayInputStream(_docstream);
         outFS.createDocument(bais,"PowerPoint Document");
         writtenEntries.add("PowerPoint Document");
+        
+        currentUser.setEncrypted(encryptedSS.getDocumentEncryptionAtom() != null);
         currentUser.writeToFS(outFS);
         writtenEntries.add("Current User");
 
 
-        // Write any pictures, into another stream
-        if(_pictures == null) {
-           readPictures();
-        }
         if (_pictures.size() > 0) {
-            ByteArrayOutputStream pict = new ByteArrayOutputStream();
+            BufAccessBAOS pict = new BufAccessBAOS();
             for (PictureData p : _pictures) {
+                int offset = pict.size();
                 p.write(pict);
+                encryptedSS.encryptPicture(pict.getBuf(), offset);
             }
             outFS.createDocument(
-                new ByteArrayInputStream(pict.toByteArray()), "Pictures"
+                new ByteArrayInputStream(pict.getBuf(), 0, pict.size()), "Pictures"
             );
             writtenEntries.add("Pictures");
         }
@@ -580,8 +630,44 @@ public final class HSLFSlideShow extends POIDocument {
         outFS.writeFilesystem(out);
     }
 
+    /** 
+     * For a given named property entry, either return it or null if
+     *  if it wasn't found
+     *  
+     *  @param setName The property to read
+     *  @return The value of the given property or null if it wasn't found.
+     */
+    protected PropertySet getPropertySet(String setName) {
+        DocumentEncryptionAtom dea = getDocumentEncryptionAtom();
+        return (dea == null)
+            ? super.getPropertySet(setName)
+            : super.getPropertySet(setName, dea.getEncryptionInfo());
+    }
 
-       /* ******************* adding methods follow ********************* */
+    /**
+     * Writes out the standard Documment Information Properties (HPSF)
+     * @param outFS the POIFSFileSystem to write the properties into
+     * @param writtenEntries a list of POIFS entries to add the property names too
+     * 
+     * @throws IOException if an error when writing to the 
+     *      {@link POIFSFileSystem} occurs
+     */
+    protected void writeProperties(POIFSFileSystem outFS, List<String> writtenEntries) throws IOException {
+        super.writeProperties(outFS, writtenEntries);
+        DocumentEncryptionAtom dea = getDocumentEncryptionAtom();
+        if (dea != null) {
+            CryptoAPIEncryptor enc = (CryptoAPIEncryptor)dea.getEncryptionInfo().getEncryptor();
+            try {
+                enc.getDataStream(outFS.getRoot()); // ignore OutputStream
+            } catch (IOException e) {
+                throw e;
+            } catch (GeneralSecurityException e) {
+                throw new IOException(e);
+            }
+        }
+    }
+    
+    /* ******************* adding methods follow ********************* */
 
        /**
         * Adds a new root level record, at the end, but before the last
@@ -688,4 +774,30 @@ public final class HSLFSlideShow extends POIDocument {
         }
         return _objects;
     }
+    
+    
+    private static class BufAccessBAOS extends ByteArrayOutputStream {
+        public byte[] getBuf() {
+            return buf;
+        }
+    }
+    
+    private static class CountingOS extends OutputStream {
+        int count = 0;
+        public void write(int b) throws IOException {
+            count++;
+        }
+
+        public void write(byte[] b) throws IOException {
+            count += b.length;
+        }
+
+        public void write(byte[] b, int off, int len) throws IOException {
+            count += len;
+        }
+        
+        public int size() {
+            return count;
+        }
+    }
 }
index 48d8352eaf19de25ecf6ad5fd8f0404579871d87..010f424c33ce50eda3daac851c6b60743dee287f 100644 (file)
 
 package org.apache.poi.hslf.dev;
 
-import org.apache.poi.hslf.*;
-import org.apache.poi.hslf.record.*;
+import java.io.ByteArrayOutputStream;
+import java.util.Map;
+
+import org.apache.poi.hslf.HSLFSlideShow;
+import org.apache.poi.hslf.record.Document;
+import org.apache.poi.hslf.record.Notes;
+import org.apache.poi.hslf.record.NotesAtom;
+import org.apache.poi.hslf.record.PersistPtrHolder;
+import org.apache.poi.hslf.record.PositionDependentRecord;
+import org.apache.poi.hslf.record.Record;
+import org.apache.poi.hslf.record.Slide;
+import org.apache.poi.hslf.record.SlideAtom;
+import org.apache.poi.hslf.record.SlideListWithText;
+import org.apache.poi.hslf.record.SlidePersistAtom;
 import org.apache.poi.hslf.usermodel.SlideShow;
-
 import org.apache.poi.util.LittleEndian;
 
-import java.io.*;
-import java.util.Hashtable;
-
 /**
  * Gets all the different things that have Slide IDs (of sorts)
  *  in them, and displays them, so you can try to guess what they
@@ -122,10 +130,10 @@ public final class SlideIdListing {
 
                                // Check the sheet offsets
                                int[] sheetIDs = pph.getKnownSlideIDs();
-                               Hashtable sheetOffsets = pph.getSlideLocationsLookup();
+                               Map<Integer,Integer> sheetOffsets = pph.getSlideLocationsLookup();
                                for(int j=0; j<sheetIDs.length; j++) {
-                                       Integer id = Integer.valueOf(sheetIDs[j]);
-                                       Integer offset = (Integer)sheetOffsets.get(id);
+                                       Integer id = sheetIDs[j];
+                                       Integer offset = sheetOffsets.get(id);
 
                                        System.out.println("  Knows about sheet " + id);
                                        System.out.println("    That sheet lives at " + offset);
index 300d315b7ffcf2a0cfed826bce27bf9b8e38eeac..749cfa94844a63de0e3a49b4e9070fe9030c973d 100644 (file)
 package org.apache.poi.hslf.dev;
 
 import java.io.ByteArrayOutputStream;
-import java.util.Hashtable;
+import java.util.Map;
 
 import org.apache.poi.hslf.HSLFSlideShow;
-import org.apache.poi.hslf.record.*;
+import org.apache.poi.hslf.record.CurrentUserAtom;
+import org.apache.poi.hslf.record.PersistPtrHolder;
+import org.apache.poi.hslf.record.PositionDependentRecord;
+import org.apache.poi.hslf.record.Record;
+import org.apache.poi.hslf.record.UserEditAtom;
 import org.apache.poi.util.LittleEndian;
 
 /**
@@ -61,10 +65,10 @@ public final class UserEditAndPersistListing {
 
                                // Check the sheet offsets
                                int[] sheetIDs = pph.getKnownSlideIDs();
-                               Hashtable sheetOffsets = pph.getSlideLocationsLookup();
+                               Map<Integer,Integer> sheetOffsets = pph.getSlideLocationsLookup();
                                for(int j=0; j<sheetIDs.length; j++) {
-                                       Integer id = Integer.valueOf(sheetIDs[j]);
-                                       Integer offset = (Integer)sheetOffsets.get(id);
+                                       Integer id = sheetIDs[j];
+                                       Integer offset = sheetOffsets.get(id);
 
                                        System.out.println("  Knows about sheet " + id);
                                        System.out.println("    That sheet lives at " + offset);
index 3e0bf5a9afc0afe9e8ff07ac3139c69a71ac07b3..3c61039483e7e929c84f5086986e9bae2d12c6ea 100644 (file)
@@ -29,4 +29,12 @@ public final class CorruptPowerPointFileException extends IllegalStateException
        public CorruptPowerPointFileException(String s) {
                super(s);
        }
+       
+       public CorruptPowerPointFileException(String s, Throwable t) {
+           super(s,t);
+       }
+
+    public CorruptPowerPointFileException(Throwable t) {
+        super(t);
+    }
 }
index 862450a1cc3e7e5f0891a525e1872cad1ebd105e..de9d5347774d625138eefbb7816ee1606c2d86db 100644 (file)
@@ -28,4 +28,12 @@ public final class EncryptedPowerPointFileException extends EncryptedDocumentExc
        public EncryptedPowerPointFileException(String s) {
                super(s);
        }
+       
+       public EncryptedPowerPointFileException(String s, Throwable t) {
+        super(s, t);
+       }
+
+    public EncryptedPowerPointFileException(Throwable t) {
+        super(t);
+    }
 }
index 0335cece396427f55b365c78f9a2bae3f8234ee2..0ed4357eec53d98a75b4836d68e9e81314b9d405 100644 (file)
 
 package org.apache.poi.hslf.record;
 
-import java.io.*;
-import org.apache.poi.poifs.filesystem.*;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.apache.poi.hslf.exceptions.CorruptPowerPointFileException;
+import org.apache.poi.hslf.exceptions.OldPowerPointFormatException;
+import org.apache.poi.poifs.filesystem.DirectoryNode;
+import org.apache.poi.poifs.filesystem.DocumentEntry;
+import org.apache.poi.poifs.filesystem.POIFSFileSystem;
 import org.apache.poi.util.LittleEndian;
 import org.apache.poi.util.POILogFactory;
 import org.apache.poi.util.POILogger;
 import org.apache.poi.util.StringUtil;
-import org.apache.poi.hslf.exceptions.CorruptPowerPointFileException;
-import org.apache.poi.hslf.exceptions.EncryptedPowerPointFileException;
-import org.apache.poi.hslf.exceptions.OldPowerPointFormatException;
 
 
 /**
@@ -47,7 +53,7 @@ public class CurrentUserAtom
        public static final byte[] atomHeader = new byte[] { 0, 0, -10, 15 };
        /** The PowerPoint magic number for a non-encrypted file */
        public static final byte[] headerToken = new byte[] { 95, -64, -111, -29 };
-       /** The PowerPoint magic number for an encrytpted file */ 
+       /** The PowerPoint magic number for an encrypted file */ 
        public static final byte[] encHeaderToken = new byte[] { -33, -60, -47, -13 };
        /** The Powerpoint 97 version, major and minor numbers */
        public static final byte[] ppt97FileVer = new byte[] { 8, 00, -13, 03, 03, 00 };
@@ -66,6 +72,9 @@ public class CurrentUserAtom
 
        /** Only correct after reading in or writing out */
        private byte[] _contents;
+       
+       /** Flag for encryption state of the whole file */
+       private boolean isEncrypted;
 
 
        /* ********************* getter/setter follows *********************** */
@@ -84,6 +93,9 @@ public class CurrentUserAtom
        public String getLastEditUsername() { return lastEditUser; }
        public void setLastEditUsername(String u) { lastEditUser = u; }
 
+       public boolean isEncrypted() { return isEncrypted; }
+       public void setEncrypted(boolean isEncrypted) { this.isEncrypted = isEncrypted; }
+       
 
        /* ********************* real code follows *************************** */
 
@@ -100,6 +112,7 @@ public class CurrentUserAtom
                releaseVersion = 8;
                currentEditOffset = 0;
                lastEditUser = "Apache POI";
+               isEncrypted = false;
        }
 
        /** 
@@ -157,14 +170,10 @@ public class CurrentUserAtom
         */
        private void init() {
                // First up is the size, in 4 bytes, which is fixed
-               // Then is the header - check for encrypted
-               if(_contents[12] == encHeaderToken[0] && 
-                       _contents[13] == encHeaderToken[1] &&
-                       _contents[14] == encHeaderToken[2] &&
-                       _contents[15] == encHeaderToken[3]) {
-                       throw new EncryptedPowerPointFileException("The CurrentUserAtom specifies that the document is encrypted");
-               }
+               // Then is the header
                
+           isEncrypted = (LittleEndian.getInt(encHeaderToken) == LittleEndian.getInt(_contents,12)); 
+           
                // Grab the edit offset
                currentEditOffset = LittleEndian.getUInt(_contents,16);
 
@@ -229,7 +238,7 @@ public class CurrentUserAtom
                LittleEndian.putInt(_contents,8,20);
 
                // Now the ppt un-encrypted header token (4 bytes)
-               System.arraycopy(headerToken,0,_contents,12,4);
+               System.arraycopy((isEncrypted ? encHeaderToken : headerToken),0,_contents,12,4);
 
                // Now the current edit offset
                LittleEndian.putInt(_contents,16,(int)currentEditOffset);
index c473feacea2b564e3a5397979ea9d18dd74df684..fa2f6d560c13662fd9a8e671adcafbf8cfe2745d 100644 (file)
 
 package org.apache.poi.hslf.record;
 
-import org.apache.poi.util.StringUtil;
-
+import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.OutputStream;
+import java.util.Hashtable;
+
+import org.apache.poi.poifs.crypt.CipherAlgorithm;
+import org.apache.poi.poifs.crypt.EncryptionInfo;
+import org.apache.poi.poifs.crypt.EncryptionMode;
+import org.apache.poi.poifs.crypt.HashAlgorithm;
+import org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIEncryptionHeader;
+import org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIEncryptionVerifier;
+import org.apache.poi.util.LittleEndian;
+import org.apache.poi.util.LittleEndianByteArrayOutputStream;
+import org.apache.poi.util.LittleEndianInputStream;
 
 /**
  * A Document Encryption Atom (type 12052). Holds information
@@ -28,56 +38,64 @@ import java.io.OutputStream;
  *
  * @author Nick Burch
  */
-public final class DocumentEncryptionAtom extends RecordAtom {
+public final class DocumentEncryptionAtom extends PositionDependentRecordAtom {
+    private static long _type = 12052l;
        private byte[] _header;
-       private static long _type = 12052l;
-
-       private byte[] data;
-       private String encryptionProviderName;
+       private EncryptionInfo ei;
 
        /**
         * For the Document Encryption Atom
         */
-       protected DocumentEncryptionAtom(byte[] source, int start, int len) {
+       protected DocumentEncryptionAtom(byte[] source, int start, int len) throws IOException {
                // Get the header
                _header = new byte[8];
                System.arraycopy(source,start,_header,0,8);
 
-               // Grab everything else, for now
-               data = new byte[len-8];
-               System.arraycopy(source, start+8, data, 0, len-8);
-
-               // Grab the provider, from byte 8+44 onwards
-               // It's a null terminated Little Endian String
-               int endPos = -1;
-               int pos = start + 8+44;
-               while(pos < (start+len) && endPos < 0) {
-                       if(source[pos] == 0 && source[pos+1] == 0) {
-                               // Hit the end
-                               endPos = pos;
-                       }
-                       pos += 2;
-               }
-               pos = start + 8+44;
-               int stringLen = (endPos-pos) / 2;
-               encryptionProviderName = StringUtil.getFromUnicodeLE(source, pos, stringLen);
+               ByteArrayInputStream bis = new ByteArrayInputStream(source, start+8, len-8);
+               LittleEndianInputStream leis = new LittleEndianInputStream(bis);
+               ei = new EncryptionInfo(leis, true);
        }
 
+       public DocumentEncryptionAtom() {
+           _header = new byte[8];
+           LittleEndian.putShort(_header, 0, (short)0x000F);
+           LittleEndian.putShort(_header, 2, (short)_type);
+           // record length not yet known ...
+           
+           ei = new EncryptionInfo(EncryptionMode.cryptoAPI);
+       }
+       
+       /**
+        * Initializes the encryption settings
+        *
+        * @param keyBits see {@link CipherAlgorithm#rc4} for allowed values, use -1 for default size
+        */
+       public void initializeEncryptionInfo(int keyBits) {
+           ei = new EncryptionInfo(EncryptionMode.cryptoAPI, CipherAlgorithm.rc4, HashAlgorithm.sha1, keyBits, -1, null);
+       }
+       
        /**
         * Return the length of the encryption key, in bits
         */
        public int getKeyLength() {
-               return data[28];
+               return ei.getHeader().getKeySize();
        }
 
        /**
         * Return the name of the encryption provider used
         */
        public String getEncryptionProviderName() {
-               return encryptionProviderName;
+               return ei.getHeader().getCspName();
        }
 
-
+       /**
+        * @return the {@link EncryptionInfo} object for details about encryption settings
+        */
+       public EncryptionInfo getEncryptionInfo() {
+           return ei;
+       }
+       
+       
        /**
         * We are of type 12052
         */
@@ -88,10 +106,24 @@ public final class DocumentEncryptionAtom extends RecordAtom {
         *  to disk
         */
        public void writeOut(OutputStream out) throws IOException {
-               // Header
-               out.write(_header);
 
                // Data
-               out.write(data);
+               byte data[] = new byte[1024];
+               LittleEndianByteArrayOutputStream bos = new LittleEndianByteArrayOutputStream(data, 0);
+               bos.writeShort(ei.getVersionMajor());
+               bos.writeShort(ei.getVersionMinor());
+               bos.writeInt(ei.getEncryptionFlags());
+               
+               ((CryptoAPIEncryptionHeader)ei.getHeader()).write(bos);
+               ((CryptoAPIEncryptionVerifier)ei.getVerifier()).write(bos);
+               
+        // Header
+               LittleEndian.putInt(_header, 4, bos.getWriteIndex());
+        out.write(_header);
+               out.write(data, 0, bos.getWriteIndex());
        }
+
+    public void updateOtherRecordReferences(Hashtable<Integer,Integer> oldToNewReferencesLookup) {
+        
+    }
 }
index de341acd1fb89716cafad2c7b016e9085ce545fc..3095b5e4223e6a18cf16d6be04c54578104a27c3 100644 (file)
 
 package org.apache.poi.hslf.record;
 
-import org.apache.poi.util.LittleEndian;
-import org.apache.poi.util.POILogger;
-
+import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.OutputStream;
-import java.util.Enumeration;
 import java.util.Hashtable;
+import java.util.Map;
+import java.util.TreeMap;
+
+import org.apache.poi.hslf.exceptions.CorruptPowerPointFileException;
+import org.apache.poi.util.BitField;
+import org.apache.poi.util.LittleEndian;
+import org.apache.poi.util.POILogger;
 
 /**
  * General holder for PersistPtrFullBlock and PersistPtrIncrementalBlock
@@ -49,12 +53,14 @@ public final class PersistPtrHolder extends PositionDependentRecordAtom
         *  that knows about a given slide to find the right location
         */
        private Hashtable<Integer,Integer> _slideLocations;
-       /**
-        * Holds the lookup from slide id to where their offset is
-        *  held inside _ptrData. Used when writing out, and updating
-        *  the positions of the slides
-        */
-       private Hashtable<Integer,Integer> _slideOffsetDataLocation;
+
+       private static final BitField persistIdFld = new BitField(0X000FFFFF);
+       private static final BitField cntPersistFld  = new BitField(0XFFF00000);
+       
+    /**
+     * Return the value we were given at creation, be it 6001 or 6002
+     */
+    public long getRecordType() { return _type; }
 
        /**
         * Get the list of slides that this PersistPtrHolder knows about.
@@ -63,10 +69,9 @@ public final class PersistPtrHolder extends PositionDependentRecordAtom
         */
        public int[] getKnownSlideIDs() {
                int[] ids = new int[_slideLocations.size()];
-               Enumeration<Integer> e = _slideLocations.keys();
-               for(int i=0; i<ids.length; i++) {
-                       Integer id = e.nextElement();
-                       ids[i] = id.intValue();
+               int i = 0;
+               for (Integer slideId : _slideLocations.keySet()) {
+                   ids[i++] = slideId;
                }
                return ids;
        }
@@ -78,46 +83,16 @@ public final class PersistPtrHolder extends PositionDependentRecordAtom
        public Hashtable<Integer,Integer> getSlideLocationsLookup() {
                return _slideLocations;
        }
+       
        /**
         * Get the lookup from slide numbers to their offsets inside
         *  _ptrData, used when adding or moving slides.
+        * 
+        * @deprecated since POI 3.11, not supported anymore
         */
+       @Deprecated
        public Hashtable<Integer,Integer> getSlideOffsetDataLocationsLookup() {
-               return _slideOffsetDataLocation;
-       }
-
-       /**
-        * Adds a new slide, notes or similar, to be looked up by this.
-        * For now, won't look for the most optimal on disk representation.
-        */
-       public void addSlideLookup(int slideID, int posOnDisk) {
-               // PtrData grows by 8 bytes:
-               //  4 bytes for the new info block
-               //  4 bytes for the slide offset
-               byte[] newPtrData = new byte[_ptrData.length + 8];
-               System.arraycopy(_ptrData,0,newPtrData,0,_ptrData.length);
-
-               // Add to the slide location lookup hash
-               _slideLocations.put(Integer.valueOf(slideID), Integer.valueOf(posOnDisk));
-               // Add to the ptrData offset lookup hash
-               _slideOffsetDataLocation.put(Integer.valueOf(slideID),
-                               Integer.valueOf(_ptrData.length + 4));
-
-               // Build the info block
-               // First 20 bits = offset number = slide ID
-               // Remaining 12 bits = offset count = 1
-               int infoBlock = slideID;
-               infoBlock += (1 << 20);
-
-               // Write out the data for this
-               LittleEndian.putInt(newPtrData,newPtrData.length-8,infoBlock);
-               LittleEndian.putInt(newPtrData,newPtrData.length-4,posOnDisk);
-
-               // Save the new ptr data
-               _ptrData = newPtrData;
-
-               // Update the atom header
-               LittleEndian.putInt(_header,4,newPtrData.length);
+               throw new UnsupportedOperationException("PersistPtrHolder.getSlideOffsetDataLocationsLookup() is not supported since 3.12-Beta1");
        }
 
        /**
@@ -141,30 +116,27 @@ public final class PersistPtrHolder extends PositionDependentRecordAtom
                //   count * 32 bit offsets
                // Repeat as many times as you have data
                _slideLocations = new Hashtable<Integer,Integer>();
-               _slideOffsetDataLocation = new Hashtable<Integer,Integer>();
                _ptrData = new byte[len-8];
                System.arraycopy(source,start+8,_ptrData,0,_ptrData.length);
 
                int pos = 0;
                while(pos < _ptrData.length) {
-                       // Grab the info field
-                       long info = LittleEndian.getUInt(_ptrData,pos);
+                   // Grab the info field
+                       int info = LittleEndian.getInt(_ptrData,pos);
 
                        // First 20 bits = offset number
                        // Remaining 12 bits = offset count
-                       int offset_count = (int)(info >> 20);
-                       int offset_no = (int)(info - (offset_count << 20));
-//System.out.println("Info is " + info + ", count is " + offset_count + ", number is " + offset_no);
-
+            int offset_no = persistIdFld.getValue(info);
+                       int offset_count = cntPersistFld.getValue(info);
+                       
                        // Wind on by the 4 byte info header
                        pos += 4;
 
                        // Grab the offsets for each of the sheets
                        for(int i=0; i<offset_count; i++) {
                                int sheet_no = offset_no + i;
-                               long sheet_offset = LittleEndian.getUInt(_ptrData,pos);
-                               _slideLocations.put(Integer.valueOf(sheet_no), Integer.valueOf((int)sheet_offset));
-                               _slideOffsetDataLocation.put(Integer.valueOf(sheet_no), Integer.valueOf(pos));
+                               int sheet_offset = (int)LittleEndian.getUInt(_ptrData,pos);
+                               _slideLocations.put(sheet_no, sheet_offset);
 
                                // Wind on by 4 bytes per sheet found
                                pos += 4;
@@ -172,48 +144,108 @@ public final class PersistPtrHolder extends PositionDependentRecordAtom
                }
        }
 
-       /**
-        * Return the value we were given at creation, be it 6001 or 6002
-        */
-       public long getRecordType() { return _type; }
+    /**
+     *  remove all slide references
+     *  
+     *  Convenience method provided, for easier reviewing of invocations
+     */
+    public void clear() {
+        _slideLocations.clear();
+    }
+    
+    /**
+     * Adds a new slide, notes or similar, to be looked up by this.
+     */
+    public void addSlideLookup(int slideID, int posOnDisk) {
+        if (_slideLocations.containsKey(slideID)) {
+            throw new CorruptPowerPointFileException("A record with persistId "+slideID+" already exists.");
+        }
+
+        _slideLocations.put(slideID, posOnDisk);
+    }
 
        /**
         * At write-out time, update the references to the sheets to their
         *  new positions
         */
        public void updateOtherRecordReferences(Hashtable<Integer,Integer> oldToNewReferencesLookup) {
-               int[] slideIDs = getKnownSlideIDs();
-
                // Loop over all the slides we know about
                // Find where they used to live, and where they now live
-               // Then, update the right bit of _ptrData with their new location
-               for(int i=0; i<slideIDs.length; i++) {
-                       Integer id = Integer.valueOf(slideIDs[i]);
-                       Integer oldPos = (Integer)_slideLocations.get(id);
-                       Integer newPos = (Integer)oldToNewReferencesLookup.get(oldPos);
-
-                       if(newPos == null) {
-                               logger.log(POILogger.WARN, "Couldn't find the new location of the \"slide\" with id " + id + " that used to be at " + oldPos);
-                               logger.log(POILogger.WARN, "Not updating the position of it, you probably won't be able to find it any more (if you ever could!)");
-                               newPos = oldPos;
-                       }
-
-                       // Write out the new location
-                       Integer dataOffset = (Integer)_slideOffsetDataLocation.get(id);
-                       LittleEndian.putInt(_ptrData,dataOffset.intValue(),newPos.intValue());
-
-                       // Update our hashtable
-                       _slideLocations.remove(id);
-                       _slideLocations.put(id,newPos);
-               }
+           for (Map.Entry<Integer,Integer> me : _slideLocations.entrySet()) {
+               Integer oldPos = me.getValue();
+               Integer newPos = oldToNewReferencesLookup.get(oldPos);
+
+            if (newPos == null) {
+                Integer id = me.getKey();
+                logger.log(POILogger.WARN, "Couldn't find the new location of the \"slide\" with id " + id + " that used to be at " + oldPos);
+                logger.log(POILogger.WARN, "Not updating the position of it, you probably won't be able to find it any more (if you ever could!)");
+            } else {
+                me.setValue(newPos);
+            }
+           }
        }
 
+       private void normalizePersistDirectory() {
+        TreeMap<Integer,Integer> orderedSlideLocations = new TreeMap<Integer,Integer>(_slideLocations);
+        
+        @SuppressWarnings("resource")
+        BufAccessBAOS bos = new BufAccessBAOS();
+        byte intbuf[] = new byte[4];
+        int lastPersistEntry = -1;
+        int lastSlideId = -1;
+        for (Map.Entry<Integer,Integer> me : orderedSlideLocations.entrySet()) {
+            int nextSlideId = me.getKey();
+            int offset = me.getValue();
+            try {
+                // Building the info block
+                // First 20 bits = offset number = slide ID (persistIdFld, i.e. first slide ID of a continuous group)
+                // Remaining 12 bits = offset count = 1 (cntPersistFld, i.e. continuous entries in a group)
+                
+                if (lastSlideId+1 == nextSlideId) {
+                    // use existing PersistDirectoryEntry, need to increase entry count
+                    assert(lastPersistEntry != -1);
+                    int infoBlock = LittleEndian.getInt(bos.getBuf(), lastPersistEntry);
+                    int entryCnt = cntPersistFld.getValue(infoBlock);
+                    infoBlock = cntPersistFld.setValue(infoBlock, entryCnt+1);
+                    LittleEndian.putInt(bos.getBuf(), lastPersistEntry, infoBlock);
+                } else {
+                    // start new PersistDirectoryEntry
+                    lastPersistEntry = bos.size();
+                    int infoBlock = persistIdFld.setValue(0, nextSlideId);
+                    infoBlock = cntPersistFld.setValue(infoBlock, 1);
+                    LittleEndian.putInt(intbuf, 0, infoBlock);
+                    bos.write(intbuf);
+                }
+                // Add to the ptrData offset lookup hash
+                LittleEndian.putInt(intbuf, 0, offset);
+                bos.write(intbuf);
+                lastSlideId = nextSlideId;
+            } catch (IOException e) {
+                // ByteArrayOutputStream is very unlikely throwing a IO exception (maybe because of OOM ...)
+                throw new RuntimeException(e);
+            }
+        }
+        
+        // Save the new ptr data
+        _ptrData = bos.toByteArray();
+
+        // Update the atom header
+        LittleEndian.putInt(_header,4,bos.size());
+       }
+       
        /**
         * Write the contents of the record back, so it can be written
         *  to disk
         */
        public void writeOut(OutputStream out) throws IOException {
+           normalizePersistDirectory();
                out.write(_header);
                out.write(_ptrData);
        }
+       
+    private static class BufAccessBAOS extends ByteArrayOutputStream {
+        public byte[] getBuf() {
+            return buf;
+        }
+    }
 }
index eb6944ac69a5dbc124c22fc1cffeff269d7eb745..78db7ee273e9b64e38fc191200fbcf000ee27f1c 100644 (file)
@@ -74,7 +74,7 @@ public abstract class Record
         */
        public static void writeLittleEndian(int i,OutputStream o) throws IOException {
                byte[] bi = new byte[4];
-               LittleEndian.putInt(bi,i);
+               LittleEndian.putInt(bi,0,i);
                o.write(bi);
        }
        /**
@@ -82,7 +82,7 @@ public abstract class Record
         */
        public static void writeLittleEndian(short s,OutputStream o) throws IOException {
                byte[] bs = new byte[2];
-               LittleEndian.putShort(bs,s);
+               LittleEndian.putShort(bs,0,s);
                o.write(bs);
        }
 
index 0a2c8a03ba65e4c1567695641d8f9091fee67ad8..d0c8b563d79e06b897769656f6e29cf8c10afcb5 100644 (file)
@@ -18,6 +18,8 @@
 package org.apache.poi.hslf.record;
 
 import org.apache.poi.util.LittleEndian;
+import org.apache.poi.util.LittleEndianConsts;
+
 import java.io.IOException;
 import java.io.OutputStream;
 import java.util.Hashtable;
@@ -42,7 +44,7 @@ public final class UserEditAtom extends PositionDependentRecordAtom
 
        private byte[] _header;
        private static long _type = 4085l;
-       private byte[] reserved;
+       private short unused;
 
        private int lastViewedSlideID;
        private int pptVersion;
@@ -51,6 +53,7 @@ public final class UserEditAtom extends PositionDependentRecordAtom
        private int docPersistRef;
        private int maxPersistWritten;
        private short lastViewType;
+       private int encryptSessionPersistIdRef = -1;
 
        // Somewhat user facing getters
        public int getLastViewedSlideID() { return lastViewedSlideID; }
@@ -61,12 +64,17 @@ public final class UserEditAtom extends PositionDependentRecordAtom
        public int getPersistPointersOffset()  { return persistPointersOffset; }
        public int getDocPersistRef()          { return docPersistRef; }
        public int getMaxPersistWritten()      { return maxPersistWritten; }
+       public int getEncryptSessionPersistIdRef() { return encryptSessionPersistIdRef; }
 
        // More scary internal setters
        public void setLastUserEditAtomOffset(int offset) { lastUserEditAtomOffset = offset; }
        public void setPersistPointersOffset(int offset)  { persistPointersOffset = offset; }
        public void setLastViewType(short type)           { lastViewType=type; }
-    public void setMaxPersistWritten(int max)           { maxPersistWritten=max; }
+    public void setMaxPersistWritten(int max)         { maxPersistWritten=max; }
+    public void setEncryptSessionPersistIdRef(int id) {
+        encryptSessionPersistIdRef=id;
+        LittleEndian.putInt(_header,4,(id == -1 ? 28 : 32));
+    }
 
        /* *************** record code follows ********************** */
 
@@ -77,39 +85,56 @@ public final class UserEditAtom extends PositionDependentRecordAtom
                // Sanity Checking
                if(len < 34) { len = 34; }
 
+               int offset = start;
                // Get the header
                _header = new byte[8];
-               System.arraycopy(source,start,_header,0,8);
+               System.arraycopy(source,offset,_header,0,8);
+               offset += 8;
 
                // Get the last viewed slide ID
-               lastViewedSlideID = LittleEndian.getInt(source,start+0+8);
+               lastViewedSlideID = LittleEndian.getInt(source,offset);
+               offset += LittleEndianConsts.INT_SIZE;
 
                // Get the PPT version
-               pptVersion = LittleEndian.getInt(source,start+4+8);
+               pptVersion = LittleEndian.getInt(source,offset);
+               offset += LittleEndianConsts.INT_SIZE;
 
                // Get the offset to the previous incremental save's UserEditAtom
                // This will be the byte offset on disk where the previous one
                //  starts, or 0 if this is the first one
-               lastUserEditAtomOffset = LittleEndian.getInt(source,start+8+8);
+               lastUserEditAtomOffset = LittleEndian.getInt(source,offset);
+               offset += LittleEndianConsts.INT_SIZE;
 
                // Get the offset to the persist pointers
                // This will be the byte offset on disk where the preceding
                //  PersistPtrFullBlock or PersistPtrIncrementalBlock starts
-               persistPointersOffset = LittleEndian.getInt(source,start+12+8);
+               persistPointersOffset = LittleEndian.getInt(source,offset);
+               offset += LittleEndianConsts.INT_SIZE;
 
                // Get the persist reference for the document persist object
                // Normally seems to be 1
-               docPersistRef = LittleEndian.getInt(source,start+16+8);
+               docPersistRef = LittleEndian.getInt(source,offset);
+               offset += LittleEndianConsts.INT_SIZE;
 
                // Maximum number of persist objects written
-               maxPersistWritten = LittleEndian.getInt(source,start+20+8);
+               maxPersistWritten = LittleEndian.getInt(source,offset);
+               offset += LittleEndianConsts.INT_SIZE;
 
                // Last view type
-               lastViewType = LittleEndian.getShort(source,start+24+8);
+               lastViewType = LittleEndian.getShort(source,offset);
+               offset += LittleEndianConsts.SHORT_SIZE;
+               
+               // unused
+               unused = LittleEndian.getShort(source,offset);
+               offset += LittleEndianConsts.SHORT_SIZE;
 
                // There might be a few more bytes, which are a reserved field
-               reserved = new byte[len-26-8];
-               System.arraycopy(source,start+26+8,reserved,0,reserved.length);
+               if (offset-start<len) {
+                   encryptSessionPersistIdRef = LittleEndian.getInt(source,offset);
+                   offset += LittleEndianConsts.INT_SIZE;
+               }
+               
+               assert(offset-start == len);
        }
 
        /**
@@ -155,8 +180,10 @@ public final class UserEditAtom extends PositionDependentRecordAtom
                writeLittleEndian(docPersistRef,out);
                writeLittleEndian(maxPersistWritten,out);
                writeLittleEndian(lastViewType,out);
-
-               // Reserved fields
-               out.write(reserved);
+               writeLittleEndian(unused,out);
+               if (encryptSessionPersistIdRef != -1) {
+                   // optional field
+                   writeLittleEndian(encryptSessionPersistIdRef,out);
+               }
        }
 }
index 6c6e10360c6b688f18c948ffce21e183b4bb554a..81bcefcb6eb2106e8f7cf45a97e59d320498fe7b 100644 (file)
 
 package org.apache.poi.hslf.record;
 
-import junit.framework.Test;
-import junit.framework.TestSuite;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
 
 /**
  * Collects all tests from the package <tt>org.apache.poi.hslf.record</tt>.
  * 
  * @author Josh Micich
  */
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+    TestAnimationInfoAtom.class,
+    TestCString.class,
+    TestColorSchemeAtom.class,
+    TestComment2000.class,
+    TestComment2000Atom.class,
+    TestCurrentUserAtom.class,
+    TestDocument.class,
+    TestDocumentAtom.class,
+    TestDocumentEncryptionAtom.class,
+    TestExControl.class,
+    TestExHyperlink.class,
+    TestExHyperlinkAtom.class,
+    TestExMediaAtom.class,
+    TestExObjList.class,
+    TestExObjListAtom.class,
+    TestExOleObjAtom.class,
+    TestExOleObjStg.class,
+    TestExVideoContainer.class,
+    TestFontCollection.class,
+    TestHeadersFootersAtom.class,
+    TestHeadersFootersContainer.class,
+    TestInteractiveInfo.class,
+    TestInteractiveInfoAtom.class,
+    TestNotesAtom.class,
+    TestRecordContainer.class,
+    TestRecordTypes.class,
+    TestSlideAtom.class,
+    TestSlidePersistAtom.class,
+    TestSound.class,
+    TestStyleTextPropAtom.class,
+    TestTextBytesAtom.class,
+    TestTextCharsAtom.class,
+    TestTextHeaderAtom.class,
+    TestTextRulerAtom.class,
+    TestTextSpecInfoAtom.class,
+    TestTxInteractiveInfoAtom.class,
+    TestTxMasterStyleAtom.class,
+    TestUserEditAtom.class
+})
 public class AllHSLFRecordTests {
-       
-       public static Test suite() {
-               TestSuite result = new TestSuite(AllHSLFRecordTests.class.getName());
-               result.addTestSuite(TestAnimationInfoAtom.class);
-               result.addTestSuite(TestCString.class);
-               result.addTestSuite(TestColorSchemeAtom.class);
-               result.addTestSuite(TestComment2000.class);
-               result.addTestSuite(TestComment2000Atom.class);
-               result.addTestSuite(TestCurrentUserAtom.class);
-               result.addTestSuite(TestDocument.class);
-               result.addTestSuite(TestDocumentAtom.class);
-               result.addTestSuite(TestDocumentEncryptionAtom.class);
-               result.addTestSuite(TestExControl.class);
-               result.addTestSuite(TestExHyperlink.class);
-               result.addTestSuite(TestExHyperlinkAtom.class);
-               result.addTestSuite(TestExMediaAtom.class);
-               result.addTestSuite(TestExObjList.class);
-               result.addTestSuite(TestExObjListAtom.class);
-               result.addTestSuite(TestExOleObjAtom.class);
-               result.addTestSuite(TestExOleObjStg.class);
-               result.addTestSuite(TestExVideoContainer.class);
-               result.addTestSuite(TestFontCollection.class);
-               result.addTestSuite(TestHeadersFootersAtom.class);
-               result.addTestSuite(TestHeadersFootersContainer.class);
-               result.addTestSuite(TestInteractiveInfo.class);
-               result.addTestSuite(TestInteractiveInfoAtom.class);
-               result.addTestSuite(TestNotesAtom.class);
-               result.addTestSuite(TestRecordContainer.class);
-               result.addTestSuite(TestRecordTypes.class);
-               result.addTestSuite(TestSlideAtom.class);
-               result.addTestSuite(TestSlidePersistAtom.class);
-               result.addTestSuite(TestSound.class);
-               result.addTestSuite(TestStyleTextPropAtom.class);
-               result.addTestSuite(TestTextBytesAtom.class);
-               result.addTestSuite(TestTextCharsAtom.class);
-               result.addTestSuite(TestTextHeaderAtom.class);
-               result.addTestSuite(TestTextRulerAtom.class);
-               result.addTestSuite(TestTextSpecInfoAtom.class);
-               result.addTestSuite(TestTxInteractiveInfoAtom.class);
-               result.addTestSuite(TestTxMasterStyleAtom.class);
-               result.addTestSuite(TestUserEditAtom.class);
-               return result;
-       }
 }
index ed1f19a0303431aa195ec38d5ce94d2edfe6ae64..05478e3374b14d00ec88864851cfdb0f9c491921 100644 (file)
 
 package org.apache.poi.hslf.record;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
-import junit.framework.TestCase;
 import java.io.ByteArrayOutputStream;
 import java.io.InputStream;
 
+import org.apache.poi.POIDataSamples;
+import org.apache.poi.hslf.HSLFSlideShow;
 import org.apache.poi.hslf.exceptions.EncryptedPowerPointFileException;
 import org.apache.poi.poifs.filesystem.DocumentEntry;
 import org.apache.poi.poifs.filesystem.POIFSFileSystem;
-import org.apache.poi.POIDataSamples;
+import org.junit.Test;
 
 /**
  * Tests that CurrentUserAtom works properly.
  *
  * @author Nick Burch (nick at torchbox dot com)
  */
-public final class TestCurrentUserAtom extends TestCase {
+public final class TestCurrentUserAtom {
     private static POIDataSamples _slTests = POIDataSamples.getSlideShowInstance();
        /** Not encrypted */
-       private String normalFile;
+       private static final String normalFile = "basic_test_ppt_file.ppt";
        /** Encrypted */
-       private String encFile;
-
-       protected void setUp() throws Exception {
-               super.setUp();
+       private static final String encFile = "Password_Protected-hello.ppt";
 
-               normalFile = "basic_test_ppt_file.ppt";
-               encFile = "Password_Protected-hello.ppt";
-       }
-
-       public void testReadNormal() throws Exception {
+       @Test
+       public void readNormal() throws Exception {
                POIFSFileSystem fs = new POIFSFileSystem(
                                _slTests.openResourceAsStream(normalFile)
                );
@@ -66,20 +63,20 @@ public final class TestCurrentUserAtom extends TestCase {
                assertEquals(0x2942, cu2.getCurrentEditOffset());
        }
 
-       public void testReadEnc() throws Exception {
+       @Test(expected = EncryptedPowerPointFileException.class)
+       public void readEnc() throws Exception {
                POIFSFileSystem fs = new POIFSFileSystem(
                                _slTests.openResourceAsStream(encFile)
                );
 
-               try {
-                       new CurrentUserAtom(fs);
-                       fail();
-               } catch(EncryptedPowerPointFileException e) {
-                       // Good
-               }
+               new CurrentUserAtom(fs);
+               assertTrue(true); // not yet failed
+               
+               new HSLFSlideShow(fs);
        }
 
-       public void testWriteNormal() throws Exception {
+       @Test
+       public void writeNormal() throws Exception {
                // Get raw contents from a known file
                POIFSFileSystem fs = new POIFSFileSystem(
                                _slTests.openResourceAsStream(normalFile)
diff --git a/src/scratchpad/testcases/org/apache/poi/hslf/record/TestDocumentEncryption.java b/src/scratchpad/testcases/org/apache/poi/hslf/record/TestDocumentEncryption.java
new file mode 100644 (file)
index 0000000..067a94c
--- /dev/null
@@ -0,0 +1,182 @@
+/* ====================================================================\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.hslf.record;\r
+\r
+\r
+import static org.junit.Assert.assertArrayEquals;\r
+import static org.junit.Assert.assertEquals;\r
+import static org.junit.Assert.assertTrue;\r
+import static org.junit.Assert.fail;\r
+\r
+import java.io.ByteArrayInputStream;\r
+import java.io.ByteArrayOutputStream;\r
+import java.security.MessageDigest;\r
+\r
+import org.apache.commons.codec.binary.Base64;\r
+import org.apache.poi.POIDataSamples;\r
+import org.apache.poi.hpsf.DocumentSummaryInformation;\r
+import org.apache.poi.hpsf.PropertySet;\r
+import org.apache.poi.hpsf.PropertySetFactory;\r
+import org.apache.poi.hpsf.SummaryInformation;\r
+import org.apache.poi.hslf.HSLFSlideShow;\r
+import org.apache.poi.hslf.exceptions.EncryptedPowerPointFileException;\r
+import org.apache.poi.hslf.model.Slide;\r
+import org.apache.poi.hslf.usermodel.PictureData;\r
+import org.apache.poi.hslf.usermodel.SlideShow;\r
+import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey;\r
+import org.apache.poi.poifs.crypt.CryptoFunctions;\r
+import org.apache.poi.poifs.crypt.EncryptionInfo;\r
+import org.apache.poi.poifs.crypt.HashAlgorithm;\r
+import org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIEncryptionHeader;\r
+import org.apache.poi.poifs.filesystem.NPOIFSFileSystem;\r
+import org.apache.poi.poifs.filesystem.POIFSFileSystem;\r
+import org.junit.Before;\r
+import org.junit.Test;\r
+\r
+/**\r
+ * Tests that DocumentEncryption works properly.\r
+ */\r
+public class TestDocumentEncryption {\r
+    POIDataSamples slTests = POIDataSamples.getSlideShowInstance();\r
+\r
+    @Before\r
+    public void resetPassword() {\r
+        Biff8EncryptionKey.setCurrentUserPassword(null);\r
+    }\r
+    \r
+    @Test\r
+    public void cryptoAPIDecryptionOther() throws Exception {\r
+        Biff8EncryptionKey.setCurrentUserPassword("hello");\r
+        String encPpts[] = {\r
+            "Password_Protected-56-hello.ppt",\r
+            "Password_Protected-hello.ppt",\r
+            "Password_Protected-np-hello.ppt",\r
+        };\r
+        \r
+        for (String pptFile : encPpts) {\r
+            try {\r
+                NPOIFSFileSystem fs = new NPOIFSFileSystem(slTests.getFile(pptFile), true);\r
+                HSLFSlideShow hss = new HSLFSlideShow(fs);\r
+                new SlideShow(hss);\r
+                fs.close();\r
+            } catch (EncryptedPowerPointFileException e) {\r
+                fail(pptFile+" can't be decrypted");\r
+            }\r
+        }\r
+    }\r
+\r
+    @Test\r
+    public void cryptoAPIChangeKeySize() throws Exception {\r
+        String pptFile = "cryptoapi-proc2356.ppt";\r
+        Biff8EncryptionKey.setCurrentUserPassword("crypto");\r
+        NPOIFSFileSystem fs = new NPOIFSFileSystem(slTests.getFile(pptFile), true);\r
+        HSLFSlideShow hss = new HSLFSlideShow(fs);\r
+        // need to cache data (i.e. read all data) before changing the key size\r
+        PictureData picsExpected[] = hss.getPictures();\r
+        hss.getDocumentSummaryInformation();\r
+        EncryptionInfo ei = hss.getDocumentEncryptionAtom().getEncryptionInfo();\r
+        ((CryptoAPIEncryptionHeader)ei.getHeader()).setKeySize(0x78);\r
+        \r
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();\r
+        hss.write(bos);\r
+        fs.close();\r
+        \r
+        fs = new NPOIFSFileSystem(new ByteArrayInputStream(bos.toByteArray()));\r
+        hss = new HSLFSlideShow(fs);\r
+        PictureData picsActual[] = hss.getPictures();\r
+        fs.close();\r
+        \r
+        assertEquals(picsExpected.length, picsActual.length);\r
+        for (int i=0; i<picsExpected.length; i++) {\r
+            assertArrayEquals(picsExpected[i].getRawData(), picsActual[i].getRawData());\r
+        }\r
+    }\r
+\r
+    @Test\r
+    public void cryptoAPIEncryption() throws Exception {\r
+        /* documents with multiple edits need to be normalized for encryption */\r
+        String pptFile = "57272_corrupted_usereditatom.ppt";\r
+        NPOIFSFileSystem fs = new NPOIFSFileSystem(slTests.getFile(pptFile), true);\r
+        HSLFSlideShow hss = new HSLFSlideShow(fs);\r
+        hss.normalizeRecords();\r
+        \r
+        // normalized ppt\r
+        ByteArrayOutputStream expected = new ByteArrayOutputStream();\r
+        hss.write(expected);\r
+        \r
+        // encrypted\r
+        Biff8EncryptionKey.setCurrentUserPassword("hello");\r
+        ByteArrayOutputStream encrypted = new ByteArrayOutputStream();\r
+        hss.write(encrypted);\r
+        fs.close();\r
+\r
+        // decrypted\r
+        ByteArrayInputStream bis = new ByteArrayInputStream(encrypted.toByteArray());\r
+        fs = new NPOIFSFileSystem(bis);\r
+        hss = new HSLFSlideShow(fs);\r
+        Biff8EncryptionKey.setCurrentUserPassword(null);\r
+        ByteArrayOutputStream actual = new ByteArrayOutputStream();\r
+        hss.write(actual);\r
+        fs.close();\r
+        \r
+        assertArrayEquals(expected.toByteArray(), actual.toByteArray());\r
+    }    \r
+    \r
+    @Test\r
+    public void cryptoAPIDecryption() throws Exception {\r
+        // taken from a msdn blog:\r
+        // http://blogs.msdn.com/b/openspecification/archive/2009/05/08/dominic-salemno.aspx\r
+        Biff8EncryptionKey.setCurrentUserPassword("crypto");\r
+        NPOIFSFileSystem fs = new NPOIFSFileSystem(slTests.getFile("cryptoapi-proc2356.ppt"));\r
+        HSLFSlideShow hss = new HSLFSlideShow(fs);\r
+        SlideShow ss = new SlideShow(hss);\r
+        \r
+        Slide slide = ss.getSlides()[0];\r
+        assertEquals("Dominic Salemno", slide.getTextRuns()[0].getText());\r
+\r
+        String picCmp[][] = {\r
+            {"0","nKsDTKqxTCR8LFkVVWlP9GSTvZ0="},\r
+            {"95163","SuNOR+9V1UVYZIoeD65l3VTaLoc="},\r
+            {"100864","Ql3IGrr4bNq07ZTp5iPg7b+pva8="},\r
+            {"714114","8pdst9NjBGSfWezSZE8+aVhIRe0="},\r
+            {"723752","go6xqW7lvkCtlOO5tYLiMfb4oxw="},\r
+            {"770128","gZUM8YqRNL5kGNfyyYvEEernvCc="},\r
+            {"957958","CNU2iiqUFAnk3TDXsXV1ihH9eRM="},                \r
+        };\r
+        \r
+        MessageDigest md = CryptoFunctions.getMessageDigest(HashAlgorithm.sha1);\r
+        PictureData pd[] = hss.getPictures();\r
+        int i = 0;\r
+        for (PictureData p : pd) {\r
+            byte hash[] = md.digest(p.getData());\r
+            assertEquals(Integer.parseInt(picCmp[i][0]), p.getOffset());\r
+            assertEquals(picCmp[i][1], Base64.encodeBase64String(hash));\r
+            i++;\r
+        }\r
+        \r
+        DocumentEncryptionAtom dea = hss.getDocumentEncryptionAtom();\r
+        \r
+        POIFSFileSystem fs2 = new POIFSFileSystem(dea.getEncryptionInfo().getDecryptor().getDataStream(fs));\r
+        PropertySet ps = PropertySetFactory.create(fs2.getRoot(), SummaryInformation.DEFAULT_STREAM_NAME);\r
+        assertTrue(ps.isSummaryInformation());\r
+        assertEquals("RC4 CryptoAPI Encryption", ps.getProperties()[1].getValue());\r
+        ps = PropertySetFactory.create(fs2.getRoot(), DocumentSummaryInformation.DEFAULT_STREAM_NAME);\r
+        assertTrue(ps.isDocumentSummaryInformation());\r
+        assertEquals("On-screen Show (4:3)", ps.getProperties()[1].getValue());\r
+    }\r
+}\r
index c93b77876f0d9c8db12d69fbf25e9e1b23d91278..c0cb35837258ba2a791685da12ec1db2b16aebb4 100644 (file)
 package org.apache.poi.hslf.record;
 
 
-import junit.framework.TestCase;
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+
+import org.junit.Test;
 
 /**
  * Tests that DocumentEncryptionAtom works properly.
  *
  * @author Nick Burch (nick at torchbox dot com)
  */
-public final class TestDocumentEncryptionAtom extends TestCase {
+public final class TestDocumentEncryptionAtom {
        // From a real file
        private byte[] data_a = new byte[] {
                0x0F, 00, 0x14, 0x2F, 0xBE-256, 00, 00, 00,
@@ -84,7 +88,8 @@ public final class TestDocumentEncryptionAtom extends TestCase {
                        3, -104, 22, 6, 102, -61, -98, 62, 40, 61, 21
        };
 
-    public void testRecordType() {
+       @Test
+    public void recordType() throws IOException {
                DocumentEncryptionAtom dea1 = new DocumentEncryptionAtom(data_a, 0, data_a.length);
                assertEquals(12052l, dea1.getRecordType());
 
@@ -95,7 +100,8 @@ public final class TestDocumentEncryptionAtom extends TestCase {
                assertEquals(198, data_b.length);
        }
 
-    public void testEncryptionTypeName() {
+       @Test
+    public void encryptionTypeName() throws IOException {
                DocumentEncryptionAtom dea1 = new DocumentEncryptionAtom(data_a, 0, data_a.length);
                assertEquals("Microsoft Base Cryptographic Provider v1.0", dea1.getEncryptionProviderName());
 
diff --git a/test-data/slideshow/cryptoapi-proc2356.ppt b/test-data/slideshow/cryptoapi-proc2356.ppt
new file mode 100644 (file)
index 0000000..bccc45d
Binary files /dev/null and b/test-data/slideshow/cryptoapi-proc2356.ppt differ