From d7bab5ca20f159bb91827b10280f40b59389eb10 Mon Sep 17 00:00:00 2001
From: Gary King <gking@iongrid.com>
Date: Fri, 6 May 2011 19:32:25 -0700
Subject: [PATCH] add support for decryption of agile-encrypted ooxml documents

[MS-OFFCRYPTO] 2.3.4.10 defines an alternate encryption and
password verification scheme called Agile, and MS Office 2011
generates documents using this scheme.

Signed-off-by: Gary King <gking@iongrid.com>
---
 build.xml                                          |    9 +
 .../org/apache/poi/poifs/crypt/AgileDecryptor.java |  244 ++++++++++++++++++++
 src/java/org/apache/poi/poifs/crypt/Decryptor.java |  170 ++++----------
 .../org/apache/poi/poifs/crypt/EcmaDecryptor.java  |  132 +++++++++++
 .../apache/poi/poifs/crypt/EncryptionHeader.java   |   88 +++++++-
 .../org/apache/poi/poifs/crypt/EncryptionInfo.java |   31 ++-
 .../apache/poi/poifs/crypt/EncryptionVerifier.java |  114 +++++++++-
 .../poi/poifs/crypt/AllPOIFSCryptoTests.java       |   37 +++
 .../org/apache/poi/poifs/crypt/DecryptorTest.java  |   68 ------
 .../apache/poi/poifs/crypt/EncryptionInfoTest.java |   45 ----
 .../org/apache/poi/poifs/crypt/TestDecryptor.java  |   83 +++++++
 .../apache/poi/poifs/crypt/TestEncryptionInfo.java |   45 ++++
 test-data/poifs/protected_agile.docx               |  Bin 0 -> 19456 bytes
 13 files changed, 814 insertions(+), 252 deletions(-)
 create mode 100644 src/java/org/apache/poi/poifs/crypt/AgileDecryptor.java
 create mode 100644 src/java/org/apache/poi/poifs/crypt/EcmaDecryptor.java
 create mode 100644 src/testcases/org/apache/poi/poifs/crypt/AllPOIFSCryptoTests.java
 delete mode 100644 src/testcases/org/apache/poi/poifs/crypt/DecryptorTest.java
 delete mode 100644 src/testcases/org/apache/poi/poifs/crypt/EncryptionInfoTest.java
 create mode 100644 src/testcases/org/apache/poi/poifs/crypt/TestDecryptor.java
 create mode 100644 src/testcases/org/apache/poi/poifs/crypt/TestEncryptionInfo.java
 create mode 100644 test-data/poifs/protected_agile.docx

diff --git a/build.xml b/build.xml
index 9112de2..5f4c5aa 100644
--- a/build.xml
+++ b/build.xml
@@ -122,6 +122,9 @@ under the License.
     <property name="main.commons-logging.jar" location="${main.lib}/commons-logging-1.1.jar"/>
     <property name="main.commons-logging.url"
               value="${repository.m2}/maven2/commons-logging/commons-logging/1.1/commons-logging-1.1.jar"/>
+    <property name="main.commons-codec.jar" location="${main.lib}/commons-codec-1.5.jar"/>
+    <property name="main.commons-codec.url"
+              value="${repository.m2}/maven2/commons-codec/commons-codec/1.5/commons-codec-1.5.jar"/>
     <property name="main.log4j.jar" location="${main.lib}/log4j-1.2.13.jar"/>
     <property name="main.log4j.url" value="${repository.m2}/maven2/log4j/log4j/1.2.13/log4j-1.2.13.jar"/>
     <property name="main.junit.jar" location="${main.lib}/junit-3.8.1.jar"/>
@@ -166,6 +169,7 @@ under the License.
 
     <path id="main.classpath">
         <pathelement location="${main.commons-logging.jar}"/>
+        <pathelement location="${main.commons-codec.jar}"/>
         <pathelement location="${main.log4j.jar}"/>
         <pathelement location="${main.junit.jar}"/>
     </path>
@@ -295,6 +299,7 @@ under the License.
             <or>
                 <and>
                     <available file="${main.commons-logging.jar}"/>
+                    <available file="${main.commons-codec.jar}"/>
                     <available file="${main.log4j.jar}"/>
                     <available file="${main.junit.jar}"/>
                     <available file="${main.ant.jar}"/>
@@ -312,6 +317,10 @@ under the License.
             <param name="destfile" value="${main.commons-logging.jar}"/>
         </antcall>
         <antcall target="downloadfile">
+            <param name="sourcefile" value="${main.commons-codec.url}"/>
+            <param name="destfile" value="${main.commons-codec.jar}"/>
+        </antcall>
+        <antcall target="downloadfile">
             <param name="sourcefile" value="${main.log4j.url}"/>
             <param name="destfile" value="${main.log4j.jar}"/>
         </antcall>
diff --git a/src/java/org/apache/poi/poifs/crypt/AgileDecryptor.java b/src/java/org/apache/poi/poifs/crypt/AgileDecryptor.java
new file mode 100644
index 0000000..17ef47b
--- /dev/null
+++ b/src/java/org/apache/poi/poifs/crypt/AgileDecryptor.java
@@ -0,0 +1,244 @@
+/* ====================================================================
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+==================================================================== */
+package org.apache.poi.poifs.crypt;
+
+import java.util.Arrays;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.FilterInputStream;
+import java.io.ByteArrayInputStream;
+import java.security.MessageDigest;
+import java.security.GeneralSecurityException;
+import java.security.NoSuchAlgorithmException;
+import org.apache.poi.poifs.filesystem.POIFSFileSystem;
+import org.apache.poi.poifs.filesystem.DirectoryNode;
+import org.apache.poi.EncryptedDocumentException;
+
+import javax.crypto.Cipher;
+import javax.crypto.CipherInputStream;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+import javax.crypto.spec.IvParameterSpec;
+
+import org.apache.poi.poifs.filesystem.DirectoryNode;
+import org.apache.poi.poifs.filesystem.DocumentInputStream;
+import org.apache.poi.poifs.filesystem.POIFSFileSystem;
+import org.apache.poi.util.LittleEndian;
+
+/**
+ * @author Gary King
+ */
+public class AgileDecryptor extends Decryptor {
+
+    private final EncryptionInfo _info;
+    private SecretKey _secretKey;
+
+    private static final byte[] kVerifierInputBlock;
+    private static final byte[] kHashedVerifierBlock;
+    private static final byte[] kCryptoKeyBlock;
+
+    static {
+        kVerifierInputBlock =
+            new byte[] { (byte)0xfe, (byte)0xa7, (byte)0xd2, (byte)0x76,
+                         (byte)0x3b, (byte)0x4b, (byte)0x9e, (byte)0x79 };
+        kHashedVerifierBlock =
+            new byte[] { (byte)0xd7, (byte)0xaa, (byte)0x0f, (byte)0x6d,
+                         (byte)0x30, (byte)0x61, (byte)0x34, (byte)0x4e };
+        kCryptoKeyBlock =
+            new byte[] { (byte)0x14, (byte)0x6e, (byte)0x0b, (byte)0xe7,
+                         (byte)0xab, (byte)0xac, (byte)0xd0, (byte)0xd6 };
+    }
+
+    public boolean verifyPassword(String password) throws GeneralSecurityException {
+        EncryptionVerifier verifier = _info.getVerifier();
+        int algorithm = verifier.getAlgorithm();
+        int mode = verifier.getCipherMode();
+
+        byte[] pwHash = hashPassword(_info, password);
+        byte[] iv = generateIv(algorithm, verifier.getSalt(), null);
+
+        SecretKey skey;
+        skey = new SecretKeySpec(generateKey(pwHash, kVerifierInputBlock), "AES");
+        Cipher cipher = getCipher(algorithm, mode, skey, iv);
+        byte[] verifierHashInput = cipher.doFinal(verifier.getVerifier());
+
+        MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
+        byte[] trimmed = new byte[verifier.getSalt().length];
+        System.arraycopy(verifierHashInput, 0, trimmed, 0, trimmed.length);
+        byte[] hashedVerifier = sha1.digest(trimmed);
+
+        skey = new SecretKeySpec(generateKey(pwHash, kHashedVerifierBlock), "AES");
+        iv = generateIv(algorithm, verifier.getSalt(), null);
+        cipher = getCipher(algorithm, mode, skey, iv);
+        byte[] verifierHash = cipher.doFinal(verifier.getVerifierHash());
+        trimmed = new byte[hashedVerifier.length];
+        System.arraycopy(verifierHash, 0, trimmed, 0, trimmed.length);
+
+        if (Arrays.equals(trimmed, hashedVerifier)) {
+            skey = new SecretKeySpec(generateKey(pwHash, kCryptoKeyBlock), "AES");
+            iv = generateIv(algorithm, verifier.getSalt(), null);
+            cipher = getCipher(algorithm, mode, skey, iv);
+            byte[] inter = cipher.doFinal(verifier.getEncryptedKey());
+            byte[] keyspec = new byte[_info.getHeader().getKeySize() / 8];
+            System.arraycopy(inter, 0, keyspec, 0, keyspec.length);
+            _secretKey = new SecretKeySpec(keyspec, "AES");
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    public InputStream getDataStream(DirectoryNode dir) throws IOException, GeneralSecurityException {
+        DocumentInputStream dis = dir.createDocumentInputStream("EncryptedPackage");
+        long size = dis.readLong();
+        return new ChunkedCipherInputStream(dis, size);
+    }
+
+    protected AgileDecryptor(EncryptionInfo info) {
+        _info = info;
+    }
+
+    private class ChunkedCipherInputStream extends InputStream {
+        private int _lastIndex = 0;
+        private long _pos = 0;
+        private final long _size;
+        private final DocumentInputStream _stream;
+        private byte[] _chunk;
+        private Cipher _cipher;
+
+        public ChunkedCipherInputStream(DocumentInputStream stream, long size)
+            throws GeneralSecurityException {
+            _size = size;
+            _stream = stream;
+            _cipher = getCipher(_info.getHeader().getAlgorithm(),
+                                _info.getHeader().getCipherMode(),
+                                _secretKey, _info.getHeader().getKeySalt());
+        }
+
+        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;
+
+            while (len > 0) {
+                if (_chunk == null) {
+                    try {
+                        _chunk = nextChunk();
+                    } catch (GeneralSecurityException e) {
+                        throw new EncryptedDocumentException(e.getMessage());
+                    }
+                }
+                int count = (int)(4096L - (_pos & 0xfff));
+                count = Math.min(available(), 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, index);
+            byte[] iv = generateIv(_info.getHeader().getAlgorithm(),
+                                   _info.getHeader().getKeySalt(), blockKey);
+            _cipher.init(Cipher.DECRYPT_MODE, _secretKey, new IvParameterSpec(iv));
+            if (_lastIndex != index)
+                _stream.skip((index - _lastIndex) << 12);
+
+            byte[] block = new byte[Math.min(_stream.available(), 4096)];
+            _stream.readFully(block);
+            _lastIndex = index + 1;
+            return _cipher.doFinal(block);
+        }
+    }
+
+    private Cipher getCipher(int algorithm, int mode, SecretKey key, byte[] vec)
+        throws GeneralSecurityException {
+        String name = null;
+        String chain = null;
+
+        if (algorithm == EncryptionHeader.ALGORITHM_AES_128 ||
+            algorithm == EncryptionHeader.ALGORITHM_AES_192 ||
+            algorithm == EncryptionHeader.ALGORITHM_AES_256)
+            name = "AES";
+
+        if (mode == EncryptionHeader.MODE_CBC)
+            chain = "CBC";
+        else if (mode == EncryptionHeader.MODE_CFB)
+            chain = "CFB";
+
+        Cipher cipher = Cipher.getInstance(name + "/" + chain + "/NoPadding");
+        IvParameterSpec iv = new IvParameterSpec(vec);
+        cipher.init(Cipher.DECRYPT_MODE, key, iv);
+        return cipher;
+    }
+
+    private byte[] getBlock(int algorithm, byte[] hash) {
+        byte[] result = new byte[getBlockSize(algorithm)];
+        Arrays.fill(result, (byte)0x36);
+        System.arraycopy(hash, 0, result, 0, Math.min(result.length, hash.length));
+        return result;
+    }
+
+    private byte[] generateKey(byte[] hash, byte[] blockKey) throws NoSuchAlgorithmException {
+        MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
+        sha1.update(hash);
+        return getBlock(_info.getVerifier().getAlgorithm(), sha1.digest(blockKey));
+    }
+
+    protected byte[] generateIv(int algorithm, byte[] salt, byte[] blockKey)
+        throws NoSuchAlgorithmException {
+
+
+        if (blockKey == null)
+            return getBlock(algorithm, salt);
+
+        MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
+        sha1.update(salt);
+        return getBlock(algorithm, sha1.digest(blockKey));
+    }
+}
\ No newline at end of file
diff --git a/src/java/org/apache/poi/poifs/crypt/Decryptor.java b/src/java/org/apache/poi/poifs/crypt/Decryptor.java
index e97f41b..fb5b11f 100644
--- a/src/java/org/apache/poi/poifs/crypt/Decryptor.java
+++ b/src/java/org/apache/poi/poifs/crypt/Decryptor.java
@@ -19,150 +19,74 @@ package org.apache.poi.poifs.crypt;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.UnsupportedEncodingException;
-import java.security.GeneralSecurityException;
 import java.security.MessageDigest;
+import java.security.GeneralSecurityException;
 import java.security.NoSuchAlgorithmException;
-import java.util.Arrays;
-
-import javax.crypto.Cipher;
-import javax.crypto.CipherInputStream;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.SecretKeySpec;
-
-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.poifs.filesystem.DirectoryNode;
+import org.apache.poi.EncryptedDocumentException;
 import org.apache.poi.util.LittleEndian;
 
-/**
- *  @author Maxim Valyanskiy
- */
-public class Decryptor {
+public abstract class Decryptor {
     public static final String DEFAULT_PASSWORD="VelvetSweatshop";
 
-    private final EncryptionInfo info;
-    private byte[] passwordHash;
+    public abstract InputStream getDataStream(DirectoryNode dir)
+        throws IOException, GeneralSecurityException;
+
+    public abstract boolean verifyPassword(String password)
+        throws GeneralSecurityException;
+
+    public static Decryptor getInstance(EncryptionInfo info) {
+        int major = info.getVersionMajor();
+        int minor = info.getVersionMinor();
 
-    public Decryptor(EncryptionInfo info) {
-        this.info = info;
+        if (major == 4 && minor == 4)
+            return new AgileDecryptor(info);
+        else if (minor == 2 && (major == 3 || major == 4))
+            return new EcmaDecryptor(info);
+        else
+            throw new EncryptedDocumentException("Unsupported version");
     }
 
-    private void generatePasswordHash(String password) throws NoSuchAlgorithmException {
+    public InputStream getDataStream(NPOIFSFileSystem fs) throws IOException, GeneralSecurityException {
+        return getDataStream(fs.getRoot());
+    }
+
+    public InputStream getDataStream(POIFSFileSystem fs) throws IOException, GeneralSecurityException {
+        return getDataStream(fs.getRoot());
+    }
+
+    protected static int getBlockSize(int algorithm) {
+        switch (algorithm) {
+        case EncryptionHeader.ALGORITHM_AES_128: return 16;
+        case EncryptionHeader.ALGORITHM_AES_192: return 24;
+        case EncryptionHeader.ALGORITHM_AES_256: return 32;
+        }
+        throw new EncryptedDocumentException("Unknown block size");
+    }
+
+    protected byte[] hashPassword(EncryptionInfo info,
+                                  String password) throws NoSuchAlgorithmException {
         MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
-        
-        byte[] passwordBytes;
+        byte[] bytes;
         try {
-           passwordBytes = password.getBytes("UTF-16LE");
-        } catch(UnsupportedEncodingException e) {
-           throw new RuntimeException("Your JVM is broken - UTF16 not found!");
+            bytes = password.getBytes("UTF-16LE");
+        } catch (UnsupportedEncodingException e) {
+            throw new EncryptedDocumentException("UTF16 not supported");
         }
 
         sha1.update(info.getVerifier().getSalt());
-        byte[] hash = sha1.digest(passwordBytes);
-
+        byte[] hash = sha1.digest(bytes);
         byte[] iterator = new byte[4];
-        for (int i = 0; i<50000; i++) {
-            sha1.reset();
 
+        for (int i = 0; i < info.getVerifier().getSpinCount(); i++) {
+            sha1.reset();
             LittleEndian.putInt(iterator, i);
             sha1.update(iterator);
             hash = sha1.digest(hash);
         }
 
-        passwordHash = hash;
-    }
-
-    private byte[] generateKey(int block) throws NoSuchAlgorithmException {
-        MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
-
-        sha1.update(passwordHash);
-        byte[] blockValue = new byte[4];
-        LittleEndian.putInt(blockValue, block);
-        byte[] finalHash = sha1.digest(blockValue);
-
-        int requiredKeyLength = info.getHeader().getKeySize()/8;
-
-        byte[] buff = new byte[64];
-
-        Arrays.fill(buff, (byte) 0x36);
-
-        for (int i=0; i<finalHash.length; i++) {
-            buff[i] = (byte) (buff[i] ^ finalHash[i]);
-        }
-
-        sha1.reset();
-        byte[] x1 = sha1.digest(buff);
-
-        Arrays.fill(buff, (byte) 0x5c);
-        for (int i=0; i<finalHash.length; i++) {
-            buff[i] = (byte) (buff[i] ^ finalHash[i]);
-        }
-
-        sha1.reset();
-        byte[] x2 = sha1.digest(buff);
-
-        byte[] x3 = new byte[x1.length + x2.length];
-        System.arraycopy(x1, 0, x3, 0, x1.length);
-        System.arraycopy(x2, 0, x3, x1.length, x2.length);
-
-        return truncateOrPad(x3, requiredKeyLength);
-    }
-
-    public boolean verifyPassword(String password) throws GeneralSecurityException {
-        generatePasswordHash(password);
-
-        Cipher cipher = getCipher();
-
-        byte[] verifier = cipher.doFinal(info.getVerifier().getVerifier());
-
-        MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
-        byte[] calcVerifierHash = sha1.digest(verifier);
-
-        byte[] verifierHash = truncateOrPad(cipher.doFinal(info.getVerifier().getVerifierHash()), calcVerifierHash.length);
-
-        return Arrays.equals(calcVerifierHash, verifierHash);
-    }
-    
-    /**
-     * Returns a byte array of the requested length,
-     *  truncated or zero padded as needed.
-     * Behaves like Arrays.copyOf in Java 1.6
-     */
-    private 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() throws GeneralSecurityException {
-        byte[] key = generateKey(0);
-        Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
-        SecretKey skey = new SecretKeySpec(key, "AES");
-        cipher.init(Cipher.DECRYPT_MODE, skey);
-
-        return cipher;
-    }
-
-    public InputStream getDataStream(POIFSFileSystem fs) throws IOException, GeneralSecurityException {
-       return getDataStream(fs.getRoot());
-    }
-
-    public InputStream getDataStream(NPOIFSFileSystem fs) throws IOException, GeneralSecurityException {
-       return getDataStream(fs.getRoot());
-    }
-
-    @SuppressWarnings("unused")
-    public InputStream getDataStream(DirectoryNode dir) throws IOException, GeneralSecurityException {
-       DocumentInputStream dis = dir.createDocumentInputStream("EncryptedPackage");
-
-       long size = dis.readLong();
-
-       return new CipherInputStream(dis, getCipher());
+        return hash;
     }
-}
+}
\ No newline at end of file
diff --git a/src/java/org/apache/poi/poifs/crypt/EcmaDecryptor.java b/src/java/org/apache/poi/poifs/crypt/EcmaDecryptor.java
new file mode 100644
index 0000000..441a433
--- /dev/null
+++ b/src/java/org/apache/poi/poifs/crypt/EcmaDecryptor.java
@@ -0,0 +1,132 @@
+/* ====================================================================
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+==================================================================== */
+package org.apache.poi.poifs.crypt;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+import javax.crypto.Cipher;
+import javax.crypto.CipherInputStream;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.apache.poi.poifs.filesystem.DirectoryNode;
+import org.apache.poi.poifs.filesystem.DocumentInputStream;
+import org.apache.poi.poifs.filesystem.POIFSFileSystem;
+import org.apache.poi.util.LittleEndian;
+
+/**
+ *  @author Maxim Valyanskiy
+ *  @author Gary King
+ */
+public class EcmaDecryptor extends Decryptor {
+    private final EncryptionInfo info;
+    private byte[] passwordHash;
+
+    public EcmaDecryptor(EncryptionInfo info) {
+        this.info = info;
+    }
+
+    private byte[] generateKey(int block) throws NoSuchAlgorithmException {
+        MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
+
+        sha1.update(passwordHash);
+        byte[] blockValue = new byte[4];
+        LittleEndian.putInt(blockValue, block);
+        byte[] finalHash = sha1.digest(blockValue);
+
+        int requiredKeyLength = info.getHeader().getKeySize()/8;
+
+        byte[] buff = new byte[64];
+
+        Arrays.fill(buff, (byte) 0x36);
+
+        for (int i=0; i<finalHash.length; i++) {
+            buff[i] = (byte) (buff[i] ^ finalHash[i]);
+        }
+
+        sha1.reset();
+        byte[] x1 = sha1.digest(buff);
+
+        Arrays.fill(buff, (byte) 0x5c);
+        for (int i=0; i<finalHash.length; i++) {
+            buff[i] = (byte) (buff[i] ^ finalHash[i]);
+        }
+
+        sha1.reset();
+        byte[] x2 = sha1.digest(buff);
+
+        byte[] x3 = new byte[x1.length + x2.length];
+        System.arraycopy(x1, 0, x3, 0, x1.length);
+        System.arraycopy(x2, 0, x3, x1.length, x2.length);
+
+        return truncateOrPad(x3, requiredKeyLength);
+    }
+
+    public boolean verifyPassword(String password) throws GeneralSecurityException {
+        passwordHash = hashPassword(info, password);
+
+        Cipher cipher = getCipher();
+
+        byte[] verifier = cipher.doFinal(info.getVerifier().getVerifier());
+
+        MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
+        byte[] calcVerifierHash = sha1.digest(verifier);
+
+        byte[] verifierHash = truncateOrPad(cipher.doFinal(info.getVerifier().getVerifierHash()), calcVerifierHash.length);
+
+        return Arrays.equals(calcVerifierHash, verifierHash);
+    }
+
+    /**
+     * Returns a byte array of the requested length,
+     *  truncated or zero padded as needed.
+     * Behaves like Arrays.copyOf in Java 1.6
+     */
+    private 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() throws GeneralSecurityException {
+        byte[] key = generateKey(0);
+        Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
+        SecretKey skey = new SecretKeySpec(key, "AES");
+        cipher.init(Cipher.DECRYPT_MODE, skey);
+
+        return cipher;
+    }
+
+    public InputStream getDataStream(DirectoryNode dir) throws IOException, GeneralSecurityException {
+        DocumentInputStream dis = dir.createDocumentInputStream("EncryptedPackage");
+
+        long size = dis.readLong();
+
+        return new CipherInputStream(dis, getCipher());
+    }
+}
diff --git a/src/java/org/apache/poi/poifs/crypt/EncryptionHeader.java b/src/java/org/apache/poi/poifs/crypt/EncryptionHeader.java
index 893f8cc..21dd25e 100644
--- a/src/java/org/apache/poi/poifs/crypt/EncryptionHeader.java
+++ b/src/java/org/apache/poi/poifs/crypt/EncryptionHeader.java
@@ -16,12 +16,19 @@
 ==================================================================== */
 package org.apache.poi.poifs.crypt;
 
+import org.apache.commons.codec.binary.Base64;
 import org.apache.poi.poifs.filesystem.DocumentInputStream;
 
 import java.io.IOException;
+import java.io.ByteArrayInputStream;
+
+import org.w3c.dom.NamedNodeMap;
+import javax.xml.parsers.DocumentBuilderFactory;
+import org.apache.poi.EncryptedDocumentException;
 
 /**
  *  @author Maxim Valyanskiy
+ *  @author Gary King
  */
 public class EncryptionHeader {
     public static final int ALGORITHM_RC4 = 0x6801;
@@ -32,7 +39,11 @@ public class EncryptionHeader {
     public static final int HASH_SHA1 = 0x8004;
 
     public static final int PROVIDER_RC4 = 1;
-    public static final int PROVIDER_AES = 0x18; 
+    public static final int PROVIDER_AES = 0x18;
+
+    public static final int MODE_ECB = 1;
+    public static final int MODE_CBC = 2;
+    public static final int MODE_CFB = 3;
 
     private final int flags;
     private final int sizeExtra;
@@ -40,6 +51,8 @@ public class EncryptionHeader {
     private final int hashAlgorithm;
     private final int keySize;
     private final int providerType;
+    private final int cipherMode;
+    private final byte[] keySalt;
     private final String cspName;
 
     public EncryptionHeader(DocumentInputStream is) throws IOException {
@@ -63,8 +76,75 @@ public class EncryptionHeader {
 
             builder.append(c);
         }
-
         cspName = builder.toString();
+        cipherMode = MODE_ECB;
+        keySalt = null;
+    }
+
+    public EncryptionHeader(String descriptor) throws IOException {
+        NamedNodeMap keyData;
+        try {
+            ByteArrayInputStream is;
+            is = new ByteArrayInputStream(descriptor.getBytes());
+            keyData = DocumentBuilderFactory.newInstance()
+                .newDocumentBuilder().parse(is)
+                .getElementsByTagName("keyData").item(0).getAttributes();
+        } catch (Exception e) {
+            throw new EncryptedDocumentException("Unable to parse keyData");
+        }
+
+        keySize = Integer.parseInt(keyData.getNamedItem("keyBits")
+                                   .getNodeValue());
+        flags = 0;
+        sizeExtra = 0;
+        cspName = null;
+
+        int blockSize = Integer.parseInt(keyData.getNamedItem("blockSize").
+                                         getNodeValue());
+        String cipher = keyData.getNamedItem("cipherAlgorithm").getNodeValue();
+
+        if ("AES".equals(cipher)) {
+            providerType = PROVIDER_AES;
+            if (blockSize == 16)
+                algorithm = ALGORITHM_AES_128;
+            else if (blockSize == 24)
+                algorithm = ALGORITHM_AES_192;
+            else if (blockSize == 32)
+                algorithm = ALGORITHM_AES_256;
+            else
+                throw new EncryptedDocumentException("Unsupported key length");
+        } else {
+            throw new EncryptedDocumentException("Unsupported cipher");
+        }
+
+        String chaining = keyData.getNamedItem("cipherChaining").getNodeValue();
+
+        if ("ChainingModeCBC".equals(chaining))
+            cipherMode = MODE_CBC;
+        else if ("ChainingModeCFB".equals(chaining))
+            cipherMode = MODE_CFB;
+        else
+            throw new EncryptedDocumentException("Unsupported chaining mode");
+
+        String hashAlg = keyData.getNamedItem("hashAlgorithm").getNodeValue();
+        int hashSize = Integer.parseInt(keyData.getNamedItem("hashSize")
+                                        .getNodeValue());
+
+        if ("SHA1".equals(hashAlg) && hashSize == 20)
+            hashAlgorithm = HASH_SHA1;
+        else
+            throw new EncryptedDocumentException("Unsupported hash algorithm");
+
+        String salt = keyData.getNamedItem("saltValue").getNodeValue();
+        int saltLength = Integer.parseInt(keyData.getNamedItem("saltSize")
+                                          .getNodeValue());
+        keySalt = Base64.decodeBase64(salt.getBytes());
+        if (keySalt.length != saltLength)
+            throw new EncryptedDocumentException("Invalid salt length");
+    }
+
+    public int getCipherMode() {
+        return cipherMode;
     }
 
     public int getFlags() {
@@ -87,6 +167,10 @@ public class EncryptionHeader {
         return keySize;
     }
 
+    public byte[] getKeySalt() {
+        return keySalt;
+    }
+
     public int getProviderType() {
         return providerType;
     }
diff --git a/src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java b/src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java
index 68ead77..70dd037 100644
--- a/src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java
+++ b/src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java
@@ -16,15 +16,16 @@
 ==================================================================== */
 package org.apache.poi.poifs.crypt;
 
+import org.apache.poi.poifs.filesystem.DocumentEntry;
 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 java.io.IOException;
 
 /**
  *  @author Maxim Valyanskiy
+ *  @author Gary King
  */
 public class EncryptionInfo {
     private final int versionMajor;
@@ -37,24 +38,30 @@ public class EncryptionInfo {
     public EncryptionInfo(POIFSFileSystem fs) throws IOException {
        this(fs.getRoot());
     }
-    public EncryptionInfo(NPOIFSFileSystem fs) throws IOException {
-       this(fs.getRoot());
-    }
     public EncryptionInfo(DirectoryNode dir) throws IOException {
         DocumentInputStream dis = dir.createDocumentInputStream("EncryptionInfo");
-
         versionMajor = dis.readShort();
         versionMinor = dis.readShort();
-        encryptionFlags = dis.readInt();
-
-        int hSize = dis.readInt();
 
-        header = new EncryptionHeader(dis);
+        encryptionFlags = dis.readInt();
 
-        if (header.getAlgorithm()==EncryptionHeader.ALGORITHM_RC4) {
-            verifier = new EncryptionVerifier(dis, 20);
+        if (versionMajor == 4 && versionMinor == 4 && encryptionFlags == 0x40) {
+            StringBuilder builder = new StringBuilder();
+            byte[] xmlDescriptor = new byte[dis.available()];
+            dis.read(xmlDescriptor);
+            for (byte b : xmlDescriptor)
+                builder.append((char)b);
+            String descriptor = builder.toString();
+            header = new EncryptionHeader(descriptor);
+            verifier = new EncryptionVerifier(descriptor);
         } else {
-            verifier = new EncryptionVerifier(dis, 32);            
+            int hSize = dis.readInt();
+            header = new EncryptionHeader(dis);
+            if (header.getAlgorithm()==EncryptionHeader.ALGORITHM_RC4) {
+                verifier = new EncryptionVerifier(dis, 20);
+            } else {
+                verifier = new EncryptionVerifier(dis, 32);
+            }
         }
     }
 
diff --git a/src/java/org/apache/poi/poifs/crypt/EncryptionVerifier.java b/src/java/org/apache/poi/poifs/crypt/EncryptionVerifier.java
index 0eccf33..f4028ec 100644
--- a/src/java/org/apache/poi/poifs/crypt/EncryptionVerifier.java
+++ b/src/java/org/apache/poi/poifs/crypt/EncryptionVerifier.java
@@ -16,16 +16,103 @@
 ==================================================================== */
 package org.apache.poi.poifs.crypt;
 
+import java.io.ByteArrayInputStream;
+
+import org.apache.commons.codec.binary.Base64;
+
 import org.apache.poi.poifs.filesystem.DocumentInputStream;
 
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.w3c.dom.NamedNodeMap;
+import javax.xml.parsers.DocumentBuilderFactory;
+import org.apache.poi.EncryptedDocumentException;
+
 /**
  *  @author Maxim Valyanskiy
+ *  @author Gary King
  */
 public class EncryptionVerifier {
-    private final byte[] salt = new byte[16];
-    private final byte[] verifier = new byte[16];
+    private final byte[] salt;
+    private final byte[] verifier;
     private final byte[] verifierHash;
+    private final byte[] encryptedKey;
     private final int verifierHashSize;
+    private final int spinCount;
+    private final int algorithm;
+    private final int cipherMode;
+
+    public EncryptionVerifier(String descriptor) {
+        NamedNodeMap keyData = null;
+        try {
+            ByteArrayInputStream is;
+            is = new ByteArrayInputStream(descriptor.getBytes());
+            NodeList keyEncryptor = DocumentBuilderFactory.newInstance()
+                .newDocumentBuilder().parse(is)
+                .getElementsByTagName("keyEncryptor").item(0).getChildNodes();
+            for (int i = 0; i < keyEncryptor.getLength(); i++) {
+                Node node = keyEncryptor.item(i);
+                if (node.getNodeName().equals("p:encryptedKey")) {
+                    keyData = node.getAttributes();
+                    break;
+                }
+            }
+            if (keyData == null)
+                throw new EncryptedDocumentException("");
+        } catch (Exception e) {
+            throw new EncryptedDocumentException("Unable to parse keyEncryptor");
+        }
+
+        spinCount = Integer.parseInt(keyData.getNamedItem("spinCount")
+                                     .getNodeValue());
+        verifier = Base64.decodeBase64(keyData
+                                       .getNamedItem("encryptedVerifierHashInput")
+                                       .getNodeValue().getBytes());
+        salt = Base64.decodeBase64(keyData.getNamedItem("saltValue")
+                                   .getNodeValue().getBytes());
+
+        encryptedKey = Base64.decodeBase64(keyData
+                                           .getNamedItem("encryptedKeyValue")
+                                           .getNodeValue().getBytes());
+
+        int saltSize = Integer.parseInt(keyData.getNamedItem("saltSize")
+                                        .getNodeValue());
+        if (saltSize != salt.length)
+            throw new EncryptedDocumentException("Invalid salt size");
+
+        verifierHash = Base64.decodeBase64(keyData
+                                           .getNamedItem("encryptedVerifierHashValue")
+                                           .getNodeValue().getBytes());
+
+        int blockSize = Integer.parseInt(keyData.getNamedItem("blockSize")
+                                         .getNodeValue());
+
+        String alg = keyData.getNamedItem("cipherAlgorithm").getNodeValue();
+
+        if ("AES".equals(alg)) {
+            if (blockSize == 16)
+                algorithm = EncryptionHeader.ALGORITHM_AES_128;
+            else if (blockSize == 24)
+                algorithm = EncryptionHeader.ALGORITHM_AES_192;
+            else if (blockSize == 32)
+                algorithm = EncryptionHeader.ALGORITHM_AES_256;
+            else
+                throw new EncryptedDocumentException("Unsupported block size");
+        } else {
+            throw new EncryptedDocumentException("Unsupported cipher");
+        }
+
+        String chain = keyData.getNamedItem("cipherChaining").getNodeValue();
+        if ("ChainingModeCBC".equals(chain))
+            cipherMode = EncryptionHeader.MODE_CBC;
+        else if ("ChainingModeCFB".equals(chain))
+            cipherMode = EncryptionHeader.MODE_CFB;
+        else
+            throw new EncryptedDocumentException("Unsupported chaining mode");
+
+        verifierHashSize = Integer.parseInt(keyData.getNamedItem("hashSize")
+                                            .getNodeValue());
+    }
 
     public EncryptionVerifier(DocumentInputStream is, int encryptedLength) {
         int saltSize = is.readInt();
@@ -34,13 +121,20 @@ public class EncryptionVerifier {
             throw new RuntimeException("Salt size != 16 !?");
         }
 
+        salt = new byte[16];
         is.readFully(salt);
+        verifier = new byte[16];
         is.readFully(verifier);
 
         verifierHashSize = is.readInt();
 
         verifierHash = new byte[encryptedLength];
         is.readFully(verifierHash);
+
+        spinCount = 50000;
+        algorithm = EncryptionHeader.ALGORITHM_AES_128;
+        cipherMode = EncryptionHeader.MODE_ECB;
+        encryptedKey = null;
     }
 
     public byte[] getSalt() {
@@ -54,4 +148,20 @@ public class EncryptionVerifier {
     public byte[] getVerifierHash() {
         return verifierHash;
     }
+
+    public int getSpinCount() {
+        return spinCount;
+    }
+
+    public int getCipherMode() {
+        return cipherMode;
+    }
+
+    public int getAlgorithm() {
+        return algorithm;
+    }
+
+    public byte[] getEncryptedKey() {
+        return encryptedKey;
+    }
 }
diff --git a/src/testcases/org/apache/poi/poifs/crypt/AllPOIFSCryptoTests.java b/src/testcases/org/apache/poi/poifs/crypt/AllPOIFSCryptoTests.java
new file mode 100644
index 0000000..d7aef10
--- /dev/null
+++ b/src/testcases/org/apache/poi/poifs/crypt/AllPOIFSCryptoTests.java
@@ -0,0 +1,37 @@
+/* ====================================================================
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+==================================================================== */
+
+package org.apache.poi.poifs.crypt;
+
+import junit.framework.Test;
+import junit.framework.TestSuite;
+
+
+/**
+ * Tests for org.apache.poi.poifs.crypt
+ *
+ * @author Gary King
+ */
+public final class AllPOIFSCryptoTests {
+
+    public static Test suite() {
+        TestSuite result = new TestSuite(AllPOIFSCryptoTests.class.getName());
+        result.addTestSuite(TestDecryptor.class);
+        result.addTestSuite(TestEncryptionInfo.class);
+        return result;
+    }
+}
\ No newline at end of file
diff --git a/src/testcases/org/apache/poi/poifs/crypt/DecryptorTest.java b/src/testcases/org/apache/poi/poifs/crypt/DecryptorTest.java
deleted file mode 100644
index bb317d7..0000000
--- a/src/testcases/org/apache/poi/poifs/crypt/DecryptorTest.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/* ====================================================================
-   Licensed to the Apache Software Foundation (ASF) under one or more
-   contributor license agreements.  See the NOTICE file distributed with
-   this work for additional information regarding copyright ownership.
-   The ASF licenses this file to You under the Apache License, Version 2.0
-   (the "License"); you may not use this file except in compliance with
-   the License.  You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-   Unless required by applicable law or agreed to in writing, software
-   distributed under the License is distributed on an "AS IS" BASIS,
-   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-   See the License for the specific language governing permissions and
-   limitations under the License.
-==================================================================== */
-package org.apache.poi.poifs.crypt;
-
-import junit.framework.TestCase;
-import org.apache.poi.POIDataSamples;
-import org.apache.poi.poifs.filesystem.POIFSFileSystem;
-
-import java.io.IOException;
-import java.security.GeneralSecurityException;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipInputStream;
-
-/**
- *  @author Maxim Valyanskiy
- */
-public class DecryptorTest extends TestCase {
-    public void testPasswordVerification() throws IOException, GeneralSecurityException {
-        POIFSFileSystem fs = new POIFSFileSystem(POIDataSamples.getPOIFSInstance().openResourceAsStream("protect.xlsx"));
-
-        EncryptionInfo info = new EncryptionInfo(fs);
-
-        Decryptor d = new Decryptor(info);
-
-        assertTrue(d.verifyPassword(Decryptor.DEFAULT_PASSWORD));
-    }
-
-    public void testDecrypt() throws IOException, GeneralSecurityException {
-        POIFSFileSystem fs = new POIFSFileSystem(POIDataSamples.getPOIFSInstance().openResourceAsStream("protect.xlsx"));
-
-        EncryptionInfo info = new EncryptionInfo(fs);
-
-        Decryptor d = new Decryptor(info);
-
-        d.verifyPassword(Decryptor.DEFAULT_PASSWORD);
-
-        zipOk(fs, d);
-    }
-
-    private void zipOk(POIFSFileSystem fs, Decryptor d) throws IOException, GeneralSecurityException {
-        ZipInputStream zin = new ZipInputStream(d.getDataStream(fs));
-
-        while (true) {
-            ZipEntry entry = zin.getNextEntry();
-            if (entry==null) {
-                break;
-            }
-
-            while (zin.available()>0) {
-                zin.skip(zin.available());
-            }
-        }
-    }
-}
diff --git a/src/testcases/org/apache/poi/poifs/crypt/EncryptionInfoTest.java b/src/testcases/org/apache/poi/poifs/crypt/EncryptionInfoTest.java
deleted file mode 100644
index eb84727..0000000
--- a/src/testcases/org/apache/poi/poifs/crypt/EncryptionInfoTest.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/* ====================================================================
-   Licensed to the Apache Software Foundation (ASF) under one or more
-   contributor license agreements.  See the NOTICE file distributed with
-   this work for additional information regarding copyright ownership.
-   The ASF licenses this file to You under the Apache License, Version 2.0
-   (the "License"); you may not use this file except in compliance with
-   the License.  You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-   Unless required by applicable law or agreed to in writing, software
-   distributed under the License is distributed on an "AS IS" BASIS,
-   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-   See the License for the specific language governing permissions and
-   limitations under the License.
-==================================================================== */
-package org.apache.poi.poifs.crypt;
-
-import junit.framework.TestCase;
-import org.apache.poi.POIDataSamples;
-import org.apache.poi.poifs.filesystem.POIFSFileSystem;
-
-import java.io.IOException;
-
-/**
- *  @author Maxim Valyanskiy
- */
-public class EncryptionInfoTest extends TestCase {
-    public void testEncryptionInfo() throws IOException {
-        POIFSFileSystem fs = new POIFSFileSystem(POIDataSamples.getPOIFSInstance().openResourceAsStream("protect.xlsx"));
-
-        EncryptionInfo info = new EncryptionInfo(fs);
-
-        assertEquals(3, info.getVersionMajor());
-        assertEquals(2, info.getVersionMinor());
-
-        assertEquals(EncryptionHeader.ALGORITHM_AES_128, info.getHeader().getAlgorithm());
-        assertEquals(EncryptionHeader.HASH_SHA1, info.getHeader().getHashAlgorithm());
-        assertEquals(128, info.getHeader().getKeySize());
-        assertEquals(EncryptionHeader.PROVIDER_AES, info.getHeader().getProviderType());                
-        assertEquals("Microsoft Enhanced RSA and AES Cryptographic Provider", info.getHeader().getCspName());
-
-        assertEquals(32, info.getVerifier().getVerifierHash().length);
-    }
-}
diff --git a/src/testcases/org/apache/poi/poifs/crypt/TestDecryptor.java b/src/testcases/org/apache/poi/poifs/crypt/TestDecryptor.java
new file mode 100644
index 0000000..2738571
--- /dev/null
+++ b/src/testcases/org/apache/poi/poifs/crypt/TestDecryptor.java
@@ -0,0 +1,83 @@
+/* ====================================================================
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+==================================================================== */
+package org.apache.poi.poifs.crypt;
+
+import junit.framework.TestCase;
+import org.apache.poi.POIDataSamples;
+import org.apache.poi.poifs.filesystem.POIFSFileSystem;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+/**
+ *  @author Maxim Valyanskiy
+ *  @author Gary King
+ */
+public class TestDecryptor extends TestCase {
+    public void testPasswordVerification() throws IOException, GeneralSecurityException {
+        POIFSFileSystem fs = new POIFSFileSystem(POIDataSamples.getPOIFSInstance().openResourceAsStream("protect.xlsx"));
+
+        EncryptionInfo info = new EncryptionInfo(fs);
+
+        Decryptor d = Decryptor.getInstance(info);
+
+        assertTrue(d.verifyPassword(Decryptor.DEFAULT_PASSWORD));
+    }
+
+    public void testDecrypt() throws IOException, GeneralSecurityException {
+        POIFSFileSystem fs = new POIFSFileSystem(POIDataSamples.getPOIFSInstance().openResourceAsStream("protect.xlsx"));
+
+        EncryptionInfo info = new EncryptionInfo(fs);
+
+        Decryptor d = Decryptor.getInstance(info);
+
+        d.verifyPassword(Decryptor.DEFAULT_PASSWORD);
+
+        zipOk(fs, d);
+    }
+
+    public void testAgile() throws IOException, GeneralSecurityException {
+        POIFSFileSystem fs = new POIFSFileSystem(POIDataSamples.getPOIFSInstance().openResourceAsStream("protected_agile.docx"));
+
+        EncryptionInfo info = new EncryptionInfo(fs);
+
+        assertTrue(info.getVersionMajor() == 4 && info.getVersionMinor() == 4);
+
+        Decryptor d = Decryptor.getInstance(info);
+
+        assertTrue(d.verifyPassword(Decryptor.DEFAULT_PASSWORD));
+
+        zipOk(fs, d);
+    }
+
+    private void zipOk(POIFSFileSystem fs, Decryptor d) throws IOException, GeneralSecurityException {
+        ZipInputStream zin = new ZipInputStream(d.getDataStream(fs));
+
+        while (true) {
+            ZipEntry entry = zin.getNextEntry();
+            if (entry==null) {
+                break;
+            }
+
+            while (zin.available()>0) {
+                zin.skip(zin.available());
+            }
+        }
+    }
+}
diff --git a/src/testcases/org/apache/poi/poifs/crypt/TestEncryptionInfo.java b/src/testcases/org/apache/poi/poifs/crypt/TestEncryptionInfo.java
new file mode 100644
index 0000000..62607e7
--- /dev/null
+++ b/src/testcases/org/apache/poi/poifs/crypt/TestEncryptionInfo.java
@@ -0,0 +1,45 @@
+/* ====================================================================
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+==================================================================== */
+package org.apache.poi.poifs.crypt;
+
+import junit.framework.TestCase;
+import org.apache.poi.POIDataSamples;
+import org.apache.poi.poifs.filesystem.POIFSFileSystem;
+
+import java.io.IOException;
+
+/**
+ *  @author Maxim Valyanskiy
+ */
+public class TestEncryptionInfo extends TestCase {
+    public void testEncryptionInfo() throws IOException {
+        POIFSFileSystem fs = new POIFSFileSystem(POIDataSamples.getPOIFSInstance().openResourceAsStream("protect.xlsx"));
+
+        EncryptionInfo info = new EncryptionInfo(fs);
+
+        assertEquals(3, info.getVersionMajor());
+        assertEquals(2, info.getVersionMinor());
+
+        assertEquals(EncryptionHeader.ALGORITHM_AES_128, info.getHeader().getAlgorithm());
+        assertEquals(EncryptionHeader.HASH_SHA1, info.getHeader().getHashAlgorithm());
+        assertEquals(128, info.getHeader().getKeySize());
+        assertEquals(EncryptionHeader.PROVIDER_AES, info.getHeader().getProviderType());                
+        assertEquals("Microsoft Enhanced RSA and AES Cryptographic Provider", info.getHeader().getCspName());
+
+        assertEquals(32, info.getVerifier().getVerifierHash().length);
+    }
+}
diff --git a/test-data/poifs/protected_agile.docx b/test-data/poifs/protected_agile.docx
new file mode 100644
index 0000000000000000000000000000000000000000..a7de3ebe43b9f3da765370d6a6a1d353c970e5a5
GIT binary patch
literal 19456
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

literal 0
HcmV?d00001

-- 
1.7.3.4

