]> source.dussan.org Git - poi.git/commitdiff
bug#51165: Add support for OOXML Agile Encryption
authorMaxim Valyanskiy <maxcom@apache.org>
Tue, 10 May 2011 10:38:17 +0000 (10:38 +0000)
committerMaxim Valyanskiy <maxcom@apache.org>
Tue, 10 May 2011 10:38:17 +0000 (10:38 +0000)
git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1101397 13f79535-47bb-0310-9956-ffa450edef68

14 files changed:
build.xml
src/documentation/content/xdocs/encryption.xml [new file with mode: 0644]
src/java/org/apache/poi/poifs/crypt/AgileDecryptor.java [new file with mode: 0644]
src/java/org/apache/poi/poifs/crypt/Decryptor.java
src/java/org/apache/poi/poifs/crypt/EcmaDecryptor.java [new file with mode: 0644]
src/java/org/apache/poi/poifs/crypt/EncryptionHeader.java
src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java
src/java/org/apache/poi/poifs/crypt/EncryptionVerifier.java
src/testcases/org/apache/poi/poifs/crypt/AllPOIFSCryptoTests.java [new file with mode: 0644]
src/testcases/org/apache/poi/poifs/crypt/DecryptorTest.java [deleted file]
src/testcases/org/apache/poi/poifs/crypt/EncryptionInfoTest.java [deleted file]
src/testcases/org/apache/poi/poifs/crypt/TestDecryptor.java [new file with mode: 0644]
src/testcases/org/apache/poi/poifs/crypt/TestEncryptionInfo.java [new file with mode: 0644]
test-data/poifs/protected_agile.docx [new file with mode: 0644]

index 9112de255fad7c0bb933acbbe7eb5b8d0081d407..5f4c5aada400c000e15a5971d5eb15439fb50a2a 100644 (file)
--- 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}"/>
@@ -311,6 +316,10 @@ under the License.
             <param name="sourcefile" value="${main.commons-logging.url}"/>
             <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}"/>
diff --git a/src/documentation/content/xdocs/encryption.xml b/src/documentation/content/xdocs/encryption.xml
new file mode 100644 (file)
index 0000000..61a2a20
--- /dev/null
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   ====================================================================
+   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.
+   ====================================================================
+-->
+<!DOCTYPE document PUBLIC "-//APACHE//DTD Documentation V1.3//EN" "./dtd/document-v13.dtd">
+
+
+<document>
+  <header>
+    <title>Apache POI - Encryption support</title>
+    <authors>
+      <person id="maxcom" name="Maxim Valyanskiy" email="maxcom@apache.org"/>
+    </authors>
+  </header>
+
+   <body>
+    <section><title>Overview</title>
+       <p>Apache POI contains support for reading few variants of encrypted office files: </p>
+       <ul>
+               <li>XLS - RC4 Encryption</li>
+               <li>XML-based formats (XLSX, DOCX and etc) - AES Encryption</li>
+       </ul>
+
+       <p>Some "write-protected" files are encrypted with build-in password, POI can read that files too.</p>
+    </section> 
+
+    <section><title>XLS</title>
+       <p>When HSSF receive encrypted file, it tries to decode it with MSOffice build-in password. 
+       Use static method setCurrentUserPassword(String password) of org.apache.poi.hssf.record.crypto.Biff8EncryptionKey to
+       set password. It sets thread local variable. Do not forget to reset it to null after text extraction.
+       </p>
+    </section>
+
+    <section><title>XML-based formats</title>
+       <p>XML-based formats are stored in OLE-package stream "EncryptedPackage". Use org.apache.poi.poifs.crypt.Decryptor
+       to decode file:</p>
+
+       <source>
+EncryptionInfo info = new EncryptionInfo(filesystem);
+Decryptor d = new Decryptor(info);
+
+try {
+    if (!d.verifyPassword(password)) {
+        throw new RuntimeException("Unable to process: document is encrypted");
+    }
+
+    InputStream dataStream = d.getDataStream(filesystem);
+
+    // parse dataStream
+
+} catch (GeneralSecurityException ex) {
+    throw new RuntimeException("Unable to process encrypted document", ex);
+}
+       </source>
+
+       <p>If you want to read file encrypted with build-in password, use Decryptor.DEFAULT_PASSWORD.</p>
+     </section>
+  </body>
+
+  <footer>
+    <legal>
+      Copyright (c) @year@ The Apache Software Foundation. All rights reserved.
+    </legal>
+  </footer>
+</document>
+
+
+
+
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 (file)
index 0000000..17ef47b
--- /dev/null
@@ -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
index e97f41b41db94823a97961bd064b13f97802de45..fb5b11f7b32f68e76827014019c6510cae0a1e3f 100644 (file)
@@ -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 (file)
index 0000000..441a433
--- /dev/null
@@ -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());
+    }
+}
index 893f8cc9c5d32f8dcca311c6c890f1f9268bc442..21dd25e99ca7131401ce07b7f46c45de2936d4fc 100644 (file)
 ==================================================================== */
 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;
     }
index 68ead77428afc4769e45ce9dded646b8f4c09323..70dd03799ee701b7d3eb2a8a702be8197380ba5d 100644 (file)
 ==================================================================== */
 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);
+            }
         }
     }
 
index 0eccf338f01a3f7e424c1286a4cb3dc7545f859c..f4028ec78f2bd77ea748d899d1186285bb8194a7 100644 (file)
 ==================================================================== */
 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 (file)
index 0000000..d7aef10
--- /dev/null
@@ -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 (file)
index bb317d7..0000000
+++ /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 (file)
index eb84727..0000000
+++ /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 (file)
index 0000000..2738571
--- /dev/null
@@ -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 (file)
index 0000000..62607e7
--- /dev/null
@@ -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 (file)
index 0000000..a7de3eb
Binary files /dev/null and b/test-data/poifs/protected_agile.docx differ