]> source.dussan.org Git - poi.git/commitdiff
Patch from Andreas Beeker from bug #53475 - further OOXML Encryption support, coverin...
authorNick Burch <nick@apache.org>
Tue, 12 Nov 2013 11:37:45 +0000 (11:37 +0000)
committerNick Burch <nick@apache.org>
Tue, 12 Nov 2013 11:37:45 +0000 (11:37 +0000)
git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1541009 13f79535-47bb-0310-9956-ffa450edef68

src/java/org/apache/poi/poifs/crypt/AgileDecryptor.java
src/java/org/apache/poi/poifs/crypt/Decryptor.java
src/java/org/apache/poi/poifs/crypt/EcmaDecryptor.java
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/ooxml/testcases/org/apache/poi/xwpf/TestXWPFBugs.java
src/testcases/org/apache/poi/poifs/crypt/TestEncryptionInfo.java

index 88e5f887f425a618f70005af9b9f6bae5cb26cb2..031aff5a24cb4fa30e6c7d249480bb6f2475b667 100644 (file)
 ==================================================================== */
 package org.apache.poi.poifs.crypt;
 
-import java.util.Arrays;
 import java.io.IOException;
 import java.io.InputStream;
-import java.security.MessageDigest;
 import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
-import org.apache.poi.poifs.filesystem.DirectoryNode;
-import org.apache.poi.EncryptedDocumentException;
+import java.util.Arrays;
 
 import javax.crypto.Cipher;
 import javax.crypto.SecretKey;
-import javax.crypto.spec.SecretKeySpec;
 import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
 
+import org.apache.poi.EncryptedDocumentException;
+import org.apache.poi.poifs.filesystem.DirectoryNode;
 import org.apache.poi.poifs.filesystem.DocumentInputStream;
 import org.apache.poi.util.LittleEndian;
 
 /**
- * @author Gary King
+ * 
  */
 public class AgileDecryptor extends Decryptor {
 
@@ -60,35 +60,34 @@ public class AgileDecryptor extends Decryptor {
 
     public boolean verifyPassword(String password) throws GeneralSecurityException {
         EncryptionVerifier verifier = _info.getVerifier();
-        int algorithm = verifier.getAlgorithm();
-        int mode = verifier.getCipherMode();
+        byte[] salt = verifier.getSalt();
 
         byte[] pwHash = hashPassword(_info, password);
-        byte[] iv = generateIv(algorithm, verifier.getSalt(), null);
+        byte[] iv = generateIv(salt, null);
 
         SecretKey skey;
         skey = new SecretKeySpec(generateKey(pwHash, kVerifierInputBlock), "AES");
-        Cipher cipher = getCipher(algorithm, mode, skey, iv);
+        Cipher cipher = getCipher(skey, iv);
         byte[] verifierHashInput = cipher.doFinal(verifier.getVerifier());
 
         MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
-        byte[] trimmed = new byte[verifier.getSalt().length];
+        byte[] trimmed = new byte[salt.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);
+        iv = generateIv(salt, null);
+        cipher = getCipher(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);
+            iv = generateIv(salt, null);
+            cipher = getCipher(skey, iv);
             byte[] inter = cipher.doFinal(verifier.getEncryptedKey());
-            byte[] keyspec = new byte[_info.getHeader().getKeySize() / 8];
+            byte[] keyspec = new byte[getKeySizeInBytes()];
             System.arraycopy(inter, 0, keyspec, 0, keyspec.length);
             _secretKey = new SecretKeySpec(keyspec, "AES");
             return true;
@@ -124,9 +123,7 @@ public class AgileDecryptor extends Decryptor {
             throws GeneralSecurityException {
             _size = size;
             _stream = stream;
-            _cipher = getCipher(_info.getHeader().getAlgorithm(),
-                                _info.getHeader().getCipherMode(),
-                                _secretKey, _info.getHeader().getKeySalt());
+            _cipher = getCipher(_secretKey, _info.getHeader().getKeySalt());
         }
 
         public int read() throws IOException {
@@ -183,8 +180,7 @@ public class AgileDecryptor extends Decryptor {
             int index = (int)(_pos >> 12);
             byte[] blockKey = new byte[4];
             LittleEndian.putInt(blockKey, 0, index);
-            byte[] iv = generateIv(_info.getHeader().getAlgorithm(),
-                                   _info.getHeader().getKeySalt(), blockKey);
+            byte[] iv = generateIv(_info.getHeader().getKeySalt(), blockKey);
             _cipher.init(Cipher.DECRYPT_MODE, _secretKey, new IvParameterSpec(iv));
             if (_lastIndex != index)
                 _stream.skip((index - _lastIndex) << 12);
@@ -196,20 +192,33 @@ public class AgileDecryptor extends Decryptor {
         }
     }
 
-    private Cipher getCipher(int algorithm, int mode, SecretKey key, byte[] vec)
+    private Cipher getCipher(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";
+       EncryptionVerifier verifier = _info.getVerifier();
+        
+        switch (verifier.getAlgorithm()) {
+          case EncryptionHeader.ALGORITHM_AES_128:
+          case EncryptionHeader.ALGORITHM_AES_192:
+          case EncryptionHeader.ALGORITHM_AES_256:
+             name = "AES";
+             break;
+          default:
+             throw new EncryptedDocumentException("Unsupported algorithm");
+        }
 
-        if (mode == EncryptionHeader.MODE_CBC)
-            chain = "CBC";
-        else if (mode == EncryptionHeader.MODE_CFB)
-            chain = "CFB";
+        switch (verifier.getCipherMode()) {
+          case EncryptionHeader.MODE_CBC: 
+              chain = "CBC"; 
+              break;
+          case EncryptionHeader.MODE_CFB:
+              chain = "CFB";
+              break;
+          default: 
+              throw new EncryptedDocumentException("Unsupported chain mode");
+        }
 
         Cipher cipher = Cipher.getInstance(name + "/" + chain + "/NoPadding");
         IvParameterSpec iv = new IvParameterSpec(vec);
@@ -217,8 +226,8 @@ public class AgileDecryptor extends Decryptor {
         return cipher;
     }
 
-    private byte[] getBlock(int algorithm, byte[] hash) {
-        byte[] result = new byte[getBlockSize(algorithm)];
+    private byte[] getBlock(byte[] hash, int size) {
+        byte[] result = new byte[size];
         Arrays.fill(result, (byte)0x36);
         System.arraycopy(hash, 0, result, 0, Math.min(result.length, hash.length));
         return result;
@@ -227,18 +236,27 @@ public class AgileDecryptor extends Decryptor {
     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));
+        byte[] key = sha1.digest(blockKey);
+        return getBlock(key, getKeySizeInBytes());
     }
 
-    protected byte[] generateIv(int algorithm, byte[] salt, byte[] blockKey)
+    protected byte[] generateIv(byte[] salt, byte[] blockKey)
         throws NoSuchAlgorithmException {
 
 
         if (blockKey == null)
-            return getBlock(algorithm, salt);
+            return getBlock(salt, getBlockSizeInBytes());
 
         MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
         sha1.update(salt);
-        return getBlock(algorithm, sha1.digest(blockKey));
+        return getBlock(sha1.digest(blockKey), getBlockSizeInBytes());
+    }
+    
+    protected int getBlockSizeInBytes() {
+       return _info.getHeader().getBlockSize();
+    }
+    
+    protected int getKeySizeInBytes() {
+       return _info.getHeader().getKeySize()/8;
     }
-}
\ No newline at end of file
+}
index 9832ccea4079a675ce61aa75c68f970562246967..39876f2f4c0e925f8b4046503cd901c6b626fd8a 100644 (file)
@@ -19,6 +19,7 @@ package org.apache.poi.poifs.crypt;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.UnsupportedEncodingException;
+import java.security.DigestException;
 import java.security.MessageDigest;
 import java.security.GeneralSecurityException;
 import java.security.NoSuchAlgorithmException;
@@ -27,6 +28,7 @@ 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;
+import org.apache.poi.util.LittleEndianConsts;
 
 public abstract class Decryptor {
     public static final String DEFAULT_PASSWORD="VelvetSweatshop";
@@ -85,15 +87,6 @@ public abstract class Decryptor {
         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 {
         // If no password was given, use the default
@@ -101,25 +94,32 @@ public abstract class Decryptor {
             password = DEFAULT_PASSWORD;
         }
         
-        MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
-        byte[] bytes;
+        byte[] pass;
         try {
-            bytes = password.getBytes("UTF-16LE");
+            pass = password.getBytes("UTF-16LE");
         } catch (UnsupportedEncodingException e) {
             throw new EncryptedDocumentException("UTF16 not supported");
         }
 
-        sha1.update(info.getVerifier().getSalt());
-        byte[] hash = sha1.digest(bytes);
-        byte[] iterator = new byte[4];
-
+        byte[] salt = info.getVerifier().getSalt();
+        
+        MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
+        sha1.update(salt);
+        byte[] hash = sha1.digest(pass);
+        byte[] iterator = new byte[LittleEndianConsts.INT_SIZE];
+        
+        try {
         for (int i = 0; i < info.getVerifier().getSpinCount(); i++) {
+               LittleEndian.putInt(iterator, 0, i);
             sha1.reset();
-            LittleEndian.putInt(iterator, 0, i);
             sha1.update(iterator);
-            hash = sha1.digest(hash);
+            sha1.update(hash);
+            sha1.digest(hash, 0, hash.length); // don't create hash buffer everytime new
         }
-
+        } catch (DigestException e) {
+               throw new EncryptedDocumentException("error in password hashing");
+        }
+        
         return hash;
     }
 }
\ No newline at end of file
index 641ee8cb47a8a19665cb140d9c0009ce2c8494f1..65e9be908927b8ba0af322ea0dd25b675fc58d06 100644 (file)
@@ -33,8 +33,6 @@ import org.apache.poi.poifs.filesystem.DocumentInputStream;
 import org.apache.poi.util.LittleEndian;
 
 /**
- *  @author Maxim Valyanskiy
- *  @author Gary King
  */
 public class EcmaDecryptor extends Decryptor {
     private final EncryptionInfo info;
index 2f10e98edfa48305be785ee001410aa31d299a1f..e04c8623638d45d5bf15e1507708c239e33aee4b 100644 (file)
@@ -55,6 +55,7 @@ public class EncryptionHeader {
     private final int algorithm;
     private final int hashAlgorithm;
     private final int keySize;
+    private final int blockSize;
     private final int providerType;
     private final int cipherMode;
     private final byte[] keySalt;
@@ -66,6 +67,7 @@ public class EncryptionHeader {
         algorithm = is.readInt();
         hashAlgorithm = is.readInt();
         keySize = is.readInt();
+        blockSize = keySize;
         providerType = is.readInt();
 
         is.readLong(); // skip reserved
@@ -110,20 +112,22 @@ public class EncryptionHeader {
         sizeExtra = 0;
         cspName = null;
 
-        int blockSize = Integer.parseInt(keyData.getNamedItem("blockSize").
+        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 " + blockSize);
+            switch (keySize) {
+              case 128: 
+                algorithm = ALGORITHM_AES_128; break;
+            case 192: 
+                algorithm = ALGORITHM_AES_192; break;
+            case 256: 
+                algorithm = ALGORITHM_AES_256; break;
+            default: 
+                throw new EncryptedDocumentException("Unsupported key length " + keySize);
+            }
         } else {
             throw new EncryptedDocumentException("Unsupported cipher " + cipher);
         }
@@ -138,8 +142,8 @@ public class EncryptionHeader {
             throw new EncryptedDocumentException("Unsupported chaining mode " + chaining);
 
         String hashAlg = keyData.getNamedItem("hashAlgorithm").getNodeValue();
-        int hashSize = Integer.parseInt(keyData.getNamedItem("hashSize")
-                                        .getNodeValue());
+        int hashSize = Integer.parseInt(
+                                    keyData.getNamedItem("hashSize").getNodeValue());
 
         if ("SHA1".equals(hashAlg) && hashSize == 20) {
             hashAlgorithm = HASH_SHA1;
@@ -190,6 +194,10 @@ public class EncryptionHeader {
         return keySize;
     }
 
+    public int getBlockSize() {
+       return blockSize;
+    }
+    
     public byte[] getKeySalt() {
         return keySalt;
     }
index 1ea9a874214b936c7dadd337a8cbdf8def774402..b6dd0f0b29dba11335d39588fa28039b4d2965e2 100644 (file)
@@ -24,8 +24,6 @@ import org.apache.poi.poifs.filesystem.POIFSFileSystem;
 import java.io.IOException;
 
 /**
- *  @author Maxim Valyanskiy
- *  @author Gary King
  */
 public class EncryptionInfo {
     private final int versionMajor;
index f4028ec78f2bd77ea748d899d1186285bb8194a7..e29952bc9ae1c3e0584514359e3311efcb163076 100644 (file)
@@ -18,19 +18,17 @@ package org.apache.poi.poifs.crypt;
 
 import java.io.ByteArrayInputStream;
 
-import org.apache.commons.codec.binary.Base64;
+import javax.xml.parsers.DocumentBuilderFactory;
 
+import org.apache.commons.codec.binary.Base64;
+import org.apache.poi.EncryptedDocumentException;
 import org.apache.poi.poifs.filesystem.DocumentInputStream;
-
+import org.w3c.dom.NamedNodeMap;
 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
+ * Used when checking if a key is valid for a document 
  */
 public class EncryptionVerifier {
     private final byte[] salt;
@@ -88,16 +86,21 @@ public class EncryptionVerifier {
                                          .getNodeValue());
 
         String alg = keyData.getNamedItem("cipherAlgorithm").getNodeValue();
+        
+        int keyBits = Integer.parseInt(keyData.getNamedItem("keyBits")
+                .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");
+               switch (keyBits) {
+              case 128: 
+                  algorithm = EncryptionHeader.ALGORITHM_AES_128; break;
+              case 192: 
+                  algorithm = EncryptionHeader.ALGORITHM_AES_192; break;
+              case 256: 
+                  algorithm = EncryptionHeader.ALGORITHM_AES_256; break;
+              default: 
+                  throw new EncryptedDocumentException("Unsupported key size");
+               }
         } else {
             throw new EncryptedDocumentException("Unsupported cipher");
         }
index cf20f4d1fe7e61505bbf40c75db71d7c027a6efc..ade755951ff1f11dba28ca281266de604644152b 100644 (file)
@@ -20,7 +20,7 @@ public class TestXWPFBugs extends TestCase {
      * A word document that's encrypted with non-standard\r
      *  Encryption options, and no cspname section. See bug 53475\r
      */\r
-    public void test53475() throws Exception {\r
+    public void test53475NoCSPName() throws Exception {\r
         try {\r
             Biff8EncryptionKey.setCurrentUserPassword("solrcell");\r
             File file = POIDataSamples.getDocumentInstance().getFile("bug53475-password-is-solrcell.docx");\r
@@ -49,4 +49,40 @@ public class TestXWPFBugs extends TestCase {
             Biff8EncryptionKey.setCurrentUserPassword(null);\r
         }\r
     }\r
+\r
+    /**\r
+     * A word document with aes-256, i.e. aes is always 128 bit (= 128 bit block size),\r
+     * but the key can be 128/192/256 bits\r
+     */\r
+    public void test53475_aes256() throws Exception {\r
+        try {\r
+            Biff8EncryptionKey.setCurrentUserPassword("pass");\r
+            File file = POIDataSamples.getDocumentInstance().getFile("bug53475-password-is-pass.docx");\r
+            NPOIFSFileSystem filesystem = new NPOIFSFileSystem(file, true);\r
+\r
+            // Check the encryption details\r
+            EncryptionInfo info = new EncryptionInfo(filesystem);\r
+            assertEquals(16, info.getHeader().getBlockSize());\r
+            assertEquals(256, info.getHeader().getKeySize());\r
+            assertEquals(EncryptionHeader.ALGORITHM_AES_256, info.getHeader().getAlgorithm());\r
+            assertEquals(EncryptionHeader.HASH_SHA1, info.getHeader().getHashAlgorithm());\r
+\r
+            // Check it can be decoded\r
+            Decryptor d = Decryptor.getInstance(info);         \r
+            assertTrue("Unable to process: document is encrypted", d.verifyPassword("pass"));\r
+\r
+            // Check we can read the word document in that\r
+            InputStream dataStream = d.getDataStream(filesystem);\r
+            OPCPackage opc = OPCPackage.open(dataStream);\r
+            XWPFDocument doc = new XWPFDocument(opc);\r
+            XWPFWordExtractor ex = new XWPFWordExtractor(doc);\r
+            String text = ex.getText();\r
+            assertNotNull(text);\r
+            // I know ... a stupid typo, maybe next time ...\r
+            assertEquals("The is a password protected document.", text.trim());\r
+            ex.close();\r
+        } finally {\r
+            Biff8EncryptionKey.setCurrentUserPassword(null);\r
+        }\r
+    }\r
 }\r
index 098d503dc4cc4ed9185970c5ff4249fd30907668..7869183c9f46ecaa88a97326ab439b8a9906a0a0 100644 (file)
@@ -50,7 +50,7 @@ public class TestEncryptionInfo extends TestCase {
         assertEquals(4, info.getVersionMajor());
         assertEquals(4, info.getVersionMinor());
 
-        assertEquals(EncryptionHeader.ALGORITHM_AES_128, info.getHeader().getAlgorithm());
+        assertEquals(EncryptionHeader.ALGORITHM_AES_256, info.getHeader().getAlgorithm());
         assertEquals(EncryptionHeader.HASH_SHA512, info.getHeader().getHashAlgorithm());
         assertEquals(256, info.getHeader().getKeySize());
         assertEquals(64, info.getVerifier().getVerifierHash().length);