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
This commit is contained in:
Nick Burch 2013-11-12 11:37:45 +00:00
parent 35161a686b
commit d1118ce0d5
8 changed files with 149 additions and 88 deletions

View File

@ -16,25 +16,25 @@
==================================================================== */ ==================================================================== */
package org.apache.poi.poifs.crypt; package org.apache.poi.poifs.crypt;
import java.util.Arrays;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.security.MessageDigest;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import org.apache.poi.poifs.filesystem.DirectoryNode; import java.util.Arrays;
import org.apache.poi.EncryptedDocumentException;
import javax.crypto.Cipher; import javax.crypto.Cipher;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import javax.crypto.spec.IvParameterSpec; 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.poifs.filesystem.DocumentInputStream;
import org.apache.poi.util.LittleEndian; import org.apache.poi.util.LittleEndian;
/** /**
* @author Gary King *
*/ */
public class AgileDecryptor extends Decryptor { public class AgileDecryptor extends Decryptor {
@ -60,35 +60,34 @@ public class AgileDecryptor extends Decryptor {
public boolean verifyPassword(String password) throws GeneralSecurityException { public boolean verifyPassword(String password) throws GeneralSecurityException {
EncryptionVerifier verifier = _info.getVerifier(); EncryptionVerifier verifier = _info.getVerifier();
int algorithm = verifier.getAlgorithm(); byte[] salt = verifier.getSalt();
int mode = verifier.getCipherMode();
byte[] pwHash = hashPassword(_info, password); byte[] pwHash = hashPassword(_info, password);
byte[] iv = generateIv(algorithm, verifier.getSalt(), null); byte[] iv = generateIv(salt, null);
SecretKey skey; SecretKey skey;
skey = new SecretKeySpec(generateKey(pwHash, kVerifierInputBlock), "AES"); 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()); byte[] verifierHashInput = cipher.doFinal(verifier.getVerifier());
MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); 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); System.arraycopy(verifierHashInput, 0, trimmed, 0, trimmed.length);
byte[] hashedVerifier = sha1.digest(trimmed); byte[] hashedVerifier = sha1.digest(trimmed);
skey = new SecretKeySpec(generateKey(pwHash, kHashedVerifierBlock), "AES"); skey = new SecretKeySpec(generateKey(pwHash, kHashedVerifierBlock), "AES");
iv = generateIv(algorithm, verifier.getSalt(), null); iv = generateIv(salt, null);
cipher = getCipher(algorithm, mode, skey, iv); cipher = getCipher(skey, iv);
byte[] verifierHash = cipher.doFinal(verifier.getVerifierHash()); byte[] verifierHash = cipher.doFinal(verifier.getVerifierHash());
trimmed = new byte[hashedVerifier.length]; trimmed = new byte[hashedVerifier.length];
System.arraycopy(verifierHash, 0, trimmed, 0, trimmed.length); System.arraycopy(verifierHash, 0, trimmed, 0, trimmed.length);
if (Arrays.equals(trimmed, hashedVerifier)) { if (Arrays.equals(trimmed, hashedVerifier)) {
skey = new SecretKeySpec(generateKey(pwHash, kCryptoKeyBlock), "AES"); skey = new SecretKeySpec(generateKey(pwHash, kCryptoKeyBlock), "AES");
iv = generateIv(algorithm, verifier.getSalt(), null); iv = generateIv(salt, null);
cipher = getCipher(algorithm, mode, skey, iv); cipher = getCipher(skey, iv);
byte[] inter = cipher.doFinal(verifier.getEncryptedKey()); 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); System.arraycopy(inter, 0, keyspec, 0, keyspec.length);
_secretKey = new SecretKeySpec(keyspec, "AES"); _secretKey = new SecretKeySpec(keyspec, "AES");
return true; return true;
@ -124,9 +123,7 @@ public class AgileDecryptor extends Decryptor {
throws GeneralSecurityException { throws GeneralSecurityException {
_size = size; _size = size;
_stream = stream; _stream = stream;
_cipher = getCipher(_info.getHeader().getAlgorithm(), _cipher = getCipher(_secretKey, _info.getHeader().getKeySalt());
_info.getHeader().getCipherMode(),
_secretKey, _info.getHeader().getKeySalt());
} }
public int read() throws IOException { public int read() throws IOException {
@ -183,8 +180,7 @@ public class AgileDecryptor extends Decryptor {
int index = (int)(_pos >> 12); int index = (int)(_pos >> 12);
byte[] blockKey = new byte[4]; byte[] blockKey = new byte[4];
LittleEndian.putInt(blockKey, 0, index); LittleEndian.putInt(blockKey, 0, index);
byte[] iv = generateIv(_info.getHeader().getAlgorithm(), byte[] iv = generateIv(_info.getHeader().getKeySalt(), blockKey);
_info.getHeader().getKeySalt(), blockKey);
_cipher.init(Cipher.DECRYPT_MODE, _secretKey, new IvParameterSpec(iv)); _cipher.init(Cipher.DECRYPT_MODE, _secretKey, new IvParameterSpec(iv));
if (_lastIndex != index) if (_lastIndex != index)
_stream.skip((index - _lastIndex) << 12); _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 { throws GeneralSecurityException {
String name = null; String name = null;
String chain = null; String chain = null;
if (algorithm == EncryptionHeader.ALGORITHM_AES_128 || EncryptionVerifier verifier = _info.getVerifier();
algorithm == EncryptionHeader.ALGORITHM_AES_192 ||
algorithm == EncryptionHeader.ALGORITHM_AES_256)
name = "AES";
if (mode == EncryptionHeader.MODE_CBC) 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");
}
switch (verifier.getCipherMode()) {
case EncryptionHeader.MODE_CBC:
chain = "CBC"; chain = "CBC";
else if (mode == EncryptionHeader.MODE_CFB) break;
case EncryptionHeader.MODE_CFB:
chain = "CFB"; chain = "CFB";
break;
default:
throw new EncryptedDocumentException("Unsupported chain mode");
}
Cipher cipher = Cipher.getInstance(name + "/" + chain + "/NoPadding"); Cipher cipher = Cipher.getInstance(name + "/" + chain + "/NoPadding");
IvParameterSpec iv = new IvParameterSpec(vec); IvParameterSpec iv = new IvParameterSpec(vec);
@ -217,8 +226,8 @@ public class AgileDecryptor extends Decryptor {
return cipher; return cipher;
} }
private byte[] getBlock(int algorithm, byte[] hash) { private byte[] getBlock(byte[] hash, int size) {
byte[] result = new byte[getBlockSize(algorithm)]; byte[] result = new byte[size];
Arrays.fill(result, (byte)0x36); Arrays.fill(result, (byte)0x36);
System.arraycopy(hash, 0, result, 0, Math.min(result.length, hash.length)); System.arraycopy(hash, 0, result, 0, Math.min(result.length, hash.length));
return result; return result;
@ -227,18 +236,27 @@ public class AgileDecryptor extends Decryptor {
private byte[] generateKey(byte[] hash, byte[] blockKey) throws NoSuchAlgorithmException { private byte[] generateKey(byte[] hash, byte[] blockKey) throws NoSuchAlgorithmException {
MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
sha1.update(hash); 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 { throws NoSuchAlgorithmException {
if (blockKey == null) if (blockKey == null)
return getBlock(algorithm, salt); return getBlock(salt, getBlockSizeInBytes());
MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
sha1.update(salt); 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;
} }
} }

View File

@ -19,6 +19,7 @@ package org.apache.poi.poifs.crypt;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.security.DigestException;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException; 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.poifs.filesystem.DirectoryNode;
import org.apache.poi.EncryptedDocumentException; import org.apache.poi.EncryptedDocumentException;
import org.apache.poi.util.LittleEndian; import org.apache.poi.util.LittleEndian;
import org.apache.poi.util.LittleEndianConsts;
public abstract class Decryptor { public abstract class Decryptor {
public static final String DEFAULT_PASSWORD="VelvetSweatshop"; public static final String DEFAULT_PASSWORD="VelvetSweatshop";
@ -85,15 +87,6 @@ public abstract class Decryptor {
return getDataStream(fs.getRoot()); 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, protected byte[] hashPassword(EncryptionInfo info,
String password) throws NoSuchAlgorithmException { String password) throws NoSuchAlgorithmException {
// If no password was given, use the default // If no password was given, use the default
@ -101,23 +94,30 @@ public abstract class Decryptor {
password = DEFAULT_PASSWORD; password = DEFAULT_PASSWORD;
} }
MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); byte[] pass;
byte[] bytes;
try { try {
bytes = password.getBytes("UTF-16LE"); pass = password.getBytes("UTF-16LE");
} catch (UnsupportedEncodingException e) { } catch (UnsupportedEncodingException e) {
throw new EncryptedDocumentException("UTF16 not supported"); throw new EncryptedDocumentException("UTF16 not supported");
} }
sha1.update(info.getVerifier().getSalt()); byte[] salt = info.getVerifier().getSalt();
byte[] hash = sha1.digest(bytes);
byte[] iterator = new byte[4];
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++) { for (int i = 0; i < info.getVerifier().getSpinCount(); i++) {
sha1.reset();
LittleEndian.putInt(iterator, 0, i); LittleEndian.putInt(iterator, 0, i);
sha1.reset();
sha1.update(iterator); 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; return hash;

View File

@ -33,8 +33,6 @@ import org.apache.poi.poifs.filesystem.DocumentInputStream;
import org.apache.poi.util.LittleEndian; import org.apache.poi.util.LittleEndian;
/** /**
* @author Maxim Valyanskiy
* @author Gary King
*/ */
public class EcmaDecryptor extends Decryptor { public class EcmaDecryptor extends Decryptor {
private final EncryptionInfo info; private final EncryptionInfo info;

View File

@ -55,6 +55,7 @@ public class EncryptionHeader {
private final int algorithm; private final int algorithm;
private final int hashAlgorithm; private final int hashAlgorithm;
private final int keySize; private final int keySize;
private final int blockSize;
private final int providerType; private final int providerType;
private final int cipherMode; private final int cipherMode;
private final byte[] keySalt; private final byte[] keySalt;
@ -66,6 +67,7 @@ public class EncryptionHeader {
algorithm = is.readInt(); algorithm = is.readInt();
hashAlgorithm = is.readInt(); hashAlgorithm = is.readInt();
keySize = is.readInt(); keySize = is.readInt();
blockSize = keySize;
providerType = is.readInt(); providerType = is.readInt();
is.readLong(); // skip reserved is.readLong(); // skip reserved
@ -110,20 +112,22 @@ public class EncryptionHeader {
sizeExtra = 0; sizeExtra = 0;
cspName = null; cspName = null;
int blockSize = Integer.parseInt(keyData.getNamedItem("blockSize"). blockSize = Integer.parseInt(keyData.getNamedItem("blockSize").
getNodeValue()); getNodeValue());
String cipher = keyData.getNamedItem("cipherAlgorithm").getNodeValue(); String cipher = keyData.getNamedItem("cipherAlgorithm").getNodeValue();
if ("AES".equals(cipher)) { if ("AES".equals(cipher)) {
providerType = PROVIDER_AES; providerType = PROVIDER_AES;
if (blockSize == 16) switch (keySize) {
algorithm = ALGORITHM_AES_128; case 128:
else if (blockSize == 24) algorithm = ALGORITHM_AES_128; break;
algorithm = ALGORITHM_AES_192; case 192:
else if (blockSize == 32) algorithm = ALGORITHM_AES_192; break;
algorithm = ALGORITHM_AES_256; case 256:
else algorithm = ALGORITHM_AES_256; break;
throw new EncryptedDocumentException("Unsupported key length " + blockSize); default:
throw new EncryptedDocumentException("Unsupported key length " + keySize);
}
} else { } else {
throw new EncryptedDocumentException("Unsupported cipher " + cipher); throw new EncryptedDocumentException("Unsupported cipher " + cipher);
} }
@ -138,8 +142,8 @@ public class EncryptionHeader {
throw new EncryptedDocumentException("Unsupported chaining mode " + chaining); throw new EncryptedDocumentException("Unsupported chaining mode " + chaining);
String hashAlg = keyData.getNamedItem("hashAlgorithm").getNodeValue(); String hashAlg = keyData.getNamedItem("hashAlgorithm").getNodeValue();
int hashSize = Integer.parseInt(keyData.getNamedItem("hashSize") int hashSize = Integer.parseInt(
.getNodeValue()); keyData.getNamedItem("hashSize").getNodeValue());
if ("SHA1".equals(hashAlg) && hashSize == 20) { if ("SHA1".equals(hashAlg) && hashSize == 20) {
hashAlgorithm = HASH_SHA1; hashAlgorithm = HASH_SHA1;
@ -190,6 +194,10 @@ public class EncryptionHeader {
return keySize; return keySize;
} }
public int getBlockSize() {
return blockSize;
}
public byte[] getKeySalt() { public byte[] getKeySalt() {
return keySalt; return keySalt;
} }

View File

@ -24,8 +24,6 @@ import org.apache.poi.poifs.filesystem.POIFSFileSystem;
import java.io.IOException; import java.io.IOException;
/** /**
* @author Maxim Valyanskiy
* @author Gary King
*/ */
public class EncryptionInfo { public class EncryptionInfo {
private final int versionMajor; private final int versionMajor;

View File

@ -18,19 +18,17 @@ package org.apache.poi.poifs.crypt;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import javax.xml.parsers.DocumentBuilderFactory;
import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.Base64;
import org.apache.poi.EncryptedDocumentException;
import org.apache.poi.poifs.filesystem.DocumentInputStream; import org.apache.poi.poifs.filesystem.DocumentInputStream;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node; import org.w3c.dom.Node;
import org.w3c.dom.NodeList; import org.w3c.dom.NodeList;
import org.w3c.dom.NamedNodeMap;
import javax.xml.parsers.DocumentBuilderFactory;
import org.apache.poi.EncryptedDocumentException;
/** /**
* @author Maxim Valyanskiy * Used when checking if a key is valid for a document
* @author Gary King
*/ */
public class EncryptionVerifier { public class EncryptionVerifier {
private final byte[] salt; private final byte[] salt;
@ -89,15 +87,20 @@ public class EncryptionVerifier {
String alg = keyData.getNamedItem("cipherAlgorithm").getNodeValue(); String alg = keyData.getNamedItem("cipherAlgorithm").getNodeValue();
int keyBits = Integer.parseInt(keyData.getNamedItem("keyBits")
.getNodeValue());
if ("AES".equals(alg)) { if ("AES".equals(alg)) {
if (blockSize == 16) switch (keyBits) {
algorithm = EncryptionHeader.ALGORITHM_AES_128; case 128:
else if (blockSize == 24) algorithm = EncryptionHeader.ALGORITHM_AES_128; break;
algorithm = EncryptionHeader.ALGORITHM_AES_192; case 192:
else if (blockSize == 32) algorithm = EncryptionHeader.ALGORITHM_AES_192; break;
algorithm = EncryptionHeader.ALGORITHM_AES_256; case 256:
else algorithm = EncryptionHeader.ALGORITHM_AES_256; break;
throw new EncryptedDocumentException("Unsupported block size"); default:
throw new EncryptedDocumentException("Unsupported key size");
}
} else { } else {
throw new EncryptedDocumentException("Unsupported cipher"); throw new EncryptedDocumentException("Unsupported cipher");
} }

View File

@ -20,7 +20,7 @@ public class TestXWPFBugs extends TestCase {
* A word document that's encrypted with non-standard * A word document that's encrypted with non-standard
* Encryption options, and no cspname section. See bug 53475 * Encryption options, and no cspname section. See bug 53475
*/ */
public void test53475() throws Exception { public void test53475NoCSPName() throws Exception {
try { try {
Biff8EncryptionKey.setCurrentUserPassword("solrcell"); Biff8EncryptionKey.setCurrentUserPassword("solrcell");
File file = POIDataSamples.getDocumentInstance().getFile("bug53475-password-is-solrcell.docx"); File file = POIDataSamples.getDocumentInstance().getFile("bug53475-password-is-solrcell.docx");
@ -49,4 +49,40 @@ public class TestXWPFBugs extends TestCase {
Biff8EncryptionKey.setCurrentUserPassword(null); 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);
}
}
} }

View File

@ -50,7 +50,7 @@ public class TestEncryptionInfo extends TestCase {
assertEquals(4, info.getVersionMajor()); assertEquals(4, info.getVersionMajor());
assertEquals(4, info.getVersionMinor()); 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(EncryptionHeader.HASH_SHA512, info.getHeader().getHashAlgorithm());
assertEquals(256, info.getHeader().getKeySize()); assertEquals(256, info.getHeader().getKeySize());
assertEquals(64, info.getVerifier().getVerifierHash().length); assertEquals(64, info.getVerifier().getVerifierHash().length);