From d1118ce0d5323c97e5641f1a4f5ce6b58de29ae2 Mon Sep 17 00:00:00 2001 From: Nick Burch Date: Tue, 12 Nov 2013 11:37:45 +0000 Subject: [PATCH] Patch from Andreas Beeker from bug #53475 - further OOXML Encryption support, covering more ciphers git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1541009 13f79535-47bb-0310-9956-ffa450edef68 --- .../poi/poifs/crypt/AgileDecryptor.java | 92 +++++++++++-------- .../org/apache/poi/poifs/crypt/Decryptor.java | 38 ++++---- .../apache/poi/poifs/crypt/EcmaDecryptor.java | 2 - .../poi/poifs/crypt/EncryptionHeader.java | 30 +++--- .../poi/poifs/crypt/EncryptionInfo.java | 2 - .../poi/poifs/crypt/EncryptionVerifier.java | 33 ++++--- .../org/apache/poi/xwpf/TestXWPFBugs.java | 38 +++++++- .../poi/poifs/crypt/TestEncryptionInfo.java | 2 +- 8 files changed, 149 insertions(+), 88 deletions(-) diff --git a/src/java/org/apache/poi/poifs/crypt/AgileDecryptor.java b/src/java/org/apache/poi/poifs/crypt/AgileDecryptor.java index 88e5f887f..031aff5a2 100644 --- a/src/java/org/apache/poi/poifs/crypt/AgileDecryptor.java +++ b/src/java/org/apache/poi/poifs/crypt/AgileDecryptor.java @@ -16,25 +16,25 @@ ==================================================================== */ 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()); } -} \ No newline at end of file + + protected int getBlockSizeInBytes() { + return _info.getHeader().getBlockSize(); + } + + protected int getKeySizeInBytes() { + return _info.getHeader().getKeySize()/8; + } +} diff --git a/src/java/org/apache/poi/poifs/crypt/Decryptor.java b/src/java/org/apache/poi/poifs/crypt/Decryptor.java index 9832ccea4..39876f2f4 100644 --- a/src/java/org/apache/poi/poifs/crypt/Decryptor.java +++ b/src/java/org/apache/poi/poifs/crypt/Decryptor.java @@ -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 diff --git a/src/java/org/apache/poi/poifs/crypt/EcmaDecryptor.java b/src/java/org/apache/poi/poifs/crypt/EcmaDecryptor.java index 641ee8cb4..65e9be908 100644 --- a/src/java/org/apache/poi/poifs/crypt/EcmaDecryptor.java +++ b/src/java/org/apache/poi/poifs/crypt/EcmaDecryptor.java @@ -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; diff --git a/src/java/org/apache/poi/poifs/crypt/EncryptionHeader.java b/src/java/org/apache/poi/poifs/crypt/EncryptionHeader.java index 2f10e98ed..e04c86236 100644 --- a/src/java/org/apache/poi/poifs/crypt/EncryptionHeader.java +++ b/src/java/org/apache/poi/poifs/crypt/EncryptionHeader.java @@ -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; } diff --git a/src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java b/src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java index 1ea9a8742..b6dd0f0b2 100644 --- a/src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java +++ b/src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java @@ -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; diff --git a/src/java/org/apache/poi/poifs/crypt/EncryptionVerifier.java b/src/java/org/apache/poi/poifs/crypt/EncryptionVerifier.java index f4028ec78..e29952bc9 100644 --- a/src/java/org/apache/poi/poifs/crypt/EncryptionVerifier.java +++ b/src/java/org/apache/poi/poifs/crypt/EncryptionVerifier.java @@ -18,19 +18,17 @@ package org.apache.poi.poifs.crypt; import java.io.ByteArrayInputStream; +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"); } diff --git a/src/ooxml/testcases/org/apache/poi/xwpf/TestXWPFBugs.java b/src/ooxml/testcases/org/apache/poi/xwpf/TestXWPFBugs.java index cf20f4d1f..ade755951 100644 --- a/src/ooxml/testcases/org/apache/poi/xwpf/TestXWPFBugs.java +++ b/src/ooxml/testcases/org/apache/poi/xwpf/TestXWPFBugs.java @@ -20,7 +20,7 @@ public class TestXWPFBugs extends TestCase { * A word document that's encrypted with non-standard * Encryption options, and no cspname section. See bug 53475 */ - public void test53475() throws Exception { + public void test53475NoCSPName() throws Exception { try { Biff8EncryptionKey.setCurrentUserPassword("solrcell"); File file = POIDataSamples.getDocumentInstance().getFile("bug53475-password-is-solrcell.docx"); @@ -49,4 +49,40 @@ public class TestXWPFBugs extends TestCase { Biff8EncryptionKey.setCurrentUserPassword(null); } } + + /** + * A word document with aes-256, i.e. aes is always 128 bit (= 128 bit block size), + * but the key can be 128/192/256 bits + */ + public void test53475_aes256() throws Exception { + try { + Biff8EncryptionKey.setCurrentUserPassword("pass"); + File file = POIDataSamples.getDocumentInstance().getFile("bug53475-password-is-pass.docx"); + NPOIFSFileSystem filesystem = new NPOIFSFileSystem(file, true); + + // Check the encryption details + EncryptionInfo info = new EncryptionInfo(filesystem); + assertEquals(16, info.getHeader().getBlockSize()); + assertEquals(256, info.getHeader().getKeySize()); + assertEquals(EncryptionHeader.ALGORITHM_AES_256, info.getHeader().getAlgorithm()); + assertEquals(EncryptionHeader.HASH_SHA1, info.getHeader().getHashAlgorithm()); + + // Check it can be decoded + Decryptor d = Decryptor.getInstance(info); + assertTrue("Unable to process: document is encrypted", d.verifyPassword("pass")); + + // Check we can read the word document in that + InputStream dataStream = d.getDataStream(filesystem); + OPCPackage opc = OPCPackage.open(dataStream); + XWPFDocument doc = new XWPFDocument(opc); + XWPFWordExtractor ex = new XWPFWordExtractor(doc); + String text = ex.getText(); + assertNotNull(text); + // I know ... a stupid typo, maybe next time ... + assertEquals("The is a password protected document.", text.trim()); + ex.close(); + } finally { + Biff8EncryptionKey.setCurrentUserPassword(null); + } + } } diff --git a/src/testcases/org/apache/poi/poifs/crypt/TestEncryptionInfo.java b/src/testcases/org/apache/poi/poifs/crypt/TestEncryptionInfo.java index 098d503dc..7869183c9 100644 --- a/src/testcases/org/apache/poi/poifs/crypt/TestEncryptionInfo.java +++ b/src/testcases/org/apache/poi/poifs/crypt/TestEncryptionInfo.java @@ -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);