--- src/java/org/apache/poi/poifs/crypt/AgileDecryptor.java (revision 1544367) +++ src/java/org/apache/poi/poifs/crypt/AgileDecryptor.java (working copy) @@ -16,11 +16,14 @@ ==================================================================== */ package org.apache.poi.poifs.crypt; +import static org.apache.poi.poifs.crypt.AgileFunctions.generateIv; +import static org.apache.poi.poifs.crypt.AgileFunctions.generateKey; +import static org.apache.poi.poifs.crypt.AgileFunctions.hashPassword; + import java.io.IOException; import java.io.InputStream; import java.security.GeneralSecurityException; import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.util.Arrays; import javax.crypto.Cipher; @@ -62,39 +65,38 @@ EncryptionVerifier verifier = _info.getVerifier(); byte[] salt = verifier.getSalt(); - byte[] pwHash = hashPassword(_info, password); - byte[] iv = generateIv(salt, null); - - SecretKey skey; - skey = new SecretKeySpec(generateKey(pwHash, kVerifierInputBlock), "AES"); - Cipher cipher = getCipher(skey, iv); - byte[] verifierHashInput = cipher.doFinal(verifier.getVerifier()); + byte[] pwHash = hashPassword(password, verifier.getSalt(), verifier.getSpinCount()); + byte verfierInputEnc[] = hashInput(pwHash, kVerifierInputBlock, verifier.getVerifier(), salt.length); MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); - 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(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); + byte[] hashedVerifier = sha1.digest(verfierInputEnc); + byte trimmed[] = hashInput(pwHash, kHashedVerifierBlock, verifier.getVerifierHash(), hashedVerifier.length); + if (Arrays.equals(trimmed, hashedVerifier)) { - skey = new SecretKeySpec(generateKey(pwHash, kCryptoKeyBlock), "AES"); - iv = generateIv(salt, null); - cipher = getCipher(skey, iv); - byte[] inter = cipher.doFinal(verifier.getEncryptedKey()); - byte[] keyspec = new byte[getKeySizeInBytes()]; - System.arraycopy(inter, 0, keyspec, 0, keyspec.length); - _secretKey = new SecretKeySpec(keyspec, "AES"); + byte keyspec[] = hashInput(pwHash, kCryptoKeyBlock, verifier.getEncryptedKey(), getKeySizeInBytes()); + _secretKey = new SecretKeySpec(keyspec, verifier.getAlgorithmName()); return true; } else { return false; } } + + protected byte[] hashInput(byte pwHash[], byte blockKey[], byte inputKey[], int trimLength) + throws GeneralSecurityException { + EncryptionVerifier verifier = _info.getVerifier(); + byte[] salt = verifier.getSalt(); + + byte key[] = generateKey(pwHash, blockKey, getKeySizeInBytes()); + SecretKey skey = new SecretKeySpec(key, verifier.getAlgorithmName()); + byte[] iv = generateIv(salt, null, getBlockSizeInBytes()); + Cipher cipher = getCipher(skey, iv); + byte[] verifierHashInput = cipher.doFinal(inputKey); + + byte[] trimmed = new byte[trimLength]; + System.arraycopy(verifierHashInput, 0, trimmed, 0, trimLength); + return trimmed; + } public InputStream getDataStream(DirectoryNode dir) throws IOException, GeneralSecurityException { DocumentInputStream dis = dir.createDocumentInputStream("EncryptedPackage"); @@ -180,7 +182,7 @@ int index = (int)(_pos >> 12); byte[] blockKey = new byte[4]; LittleEndian.putInt(blockKey, 0, index); - byte[] iv = generateIv(_info.getHeader().getKeySalt(), blockKey); + byte[] iv = generateIv(_info.getHeader().getKeySalt(), blockKey, getBlockSizeInBytes()); _cipher.init(Cipher.DECRYPT_MODE, _secretKey, new IvParameterSpec(iv)); if (_lastIndex != index) _stream.skip((index - _lastIndex) << 12); @@ -193,28 +195,10 @@ } private Cipher getCipher(SecretKey key, byte[] vec) - throws GeneralSecurityException { + throws GeneralSecurityException { + EncryptionVerifier verifier = _info.getVerifier(); - String name = null; String chain = null; - - 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"); - } - - // Ensure the JCE policies files allow for this sized key - if (Cipher.getMaxAllowedKeyLength(name) < _info.getHeader().getKeySize()) { - throw new EncryptedDocumentException("Export Restrictions in place - please install JCE Unlimited Strength Jurisdiction Policy files"); - } - switch (verifier.getCipherMode()) { case EncryptionHeader.MODE_CBC: chain = "CBC"; @@ -226,38 +210,13 @@ throw new EncryptedDocumentException("Unsupported chain mode"); } + String name = verifier.getAlgorithmName(); Cipher cipher = Cipher.getInstance(name + "/" + chain + "/NoPadding"); IvParameterSpec iv = new IvParameterSpec(vec); cipher.init(Cipher.DECRYPT_MODE, key, iv); return cipher; } - 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; - } - - private byte[] generateKey(byte[] hash, byte[] blockKey) throws NoSuchAlgorithmException { - MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); - sha1.update(hash); - byte[] key = sha1.digest(blockKey); - return getBlock(key, getKeySizeInBytes()); - } - - protected byte[] generateIv(byte[] salt, byte[] blockKey) - throws NoSuchAlgorithmException { - - - if (blockKey == null) - return getBlock(salt, getBlockSizeInBytes()); - - MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); - sha1.update(salt); - return getBlock(sha1.digest(blockKey), getBlockSizeInBytes()); - } - protected int getBlockSizeInBytes() { return _info.getHeader().getBlockSize(); } --- src/java/org/apache/poi/poifs/crypt/AgileEncryptor.java (revision 0) +++ src/java/org/apache/poi/poifs/crypt/AgileEncryptor.java (working copy) @@ -0,0 +1,25 @@ +package org.apache.poi.poifs.crypt; + +import java.io.IOException; +import java.io.OutputStream; +import java.security.GeneralSecurityException; + +import org.apache.poi.poifs.filesystem.DirectoryNode; + +public class AgileEncryptor extends Encryptor { + private final EncryptionInfo _info; + + protected AgileEncryptor(EncryptionInfo info) { + _info = info; + } + + public void confirmPassword(String password) throws GeneralSecurityException { + + } + + public OutputStream getDataStream(DirectoryNode dir) + throws IOException, GeneralSecurityException { + return null; + } + +} --- src/java/org/apache/poi/poifs/crypt/AgileFunctions.java (revision 0) +++ src/java/org/apache/poi/poifs/crypt/AgileFunctions.java (working copy) @@ -0,0 +1,96 @@ +package org.apache.poi.poifs.crypt; + +import java.io.UnsupportedEncodingException; +import java.security.DigestException; +import java.security.GeneralSecurityException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; + +import org.apache.poi.EncryptedDocumentException; +import org.apache.poi.util.LittleEndian; +import org.apache.poi.util.LittleEndianConsts; + +public class AgileFunctions { + public static byte[] hashPassword(String password, byte salt[], int spinCount) throws NoSuchAlgorithmException { + // If no password was given, use the default + if (password == null) { + password = Decryptor.DEFAULT_PASSWORD; + } + + byte[] pass; + try { + pass = password.getBytes("UTF-16LE"); + } catch (UnsupportedEncodingException e) { + throw new EncryptedDocumentException("UTF16 not supported"); + } + + 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 < spinCount; i++) { + LittleEndian.putInt(iterator, 0, i); + sha1.reset(); + sha1.update(iterator); + 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; + } + + public static byte[] generateIv(byte[] salt, byte[] blockKey, int blockSize) + throws NoSuchAlgorithmException { + + if (blockKey == null) + return getBlock(salt, blockSize); + + MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); + sha1.update(salt); + return getBlock(sha1.digest(blockKey), blockSize); + } + + public static byte[] generateKey(byte[] passwordHash, byte[] blockKey, int keySize) throws NoSuchAlgorithmException { + MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); + sha1.update(passwordHash); + byte[] key = sha1.digest(blockKey); + return getBlock(key, keySize); + } + + public static Cipher getCipher(SecretKey key, String chain, byte[] vec) + throws GeneralSecurityException { + int keySizeInBytes = key.getEncoded().length; + + // Ensure the JCE policies files allow for this sized key + if (Cipher.getMaxAllowedKeyLength(key.getAlgorithm()) < keySizeInBytes) { + throw new EncryptedDocumentException("Export Restrictions in place - please install JCE Unlimited Strength Jurisdiction Policy files"); + } + + Cipher cipher = Cipher.getInstance(key.getAlgorithm() + "/" + chain + "/NoPadding"); + if (vec == null) { + cipher.init(Cipher.DECRYPT_MODE, key); + } else { + IvParameterSpec iv = new IvParameterSpec(vec); + cipher.init(Cipher.DECRYPT_MODE, key, iv); + } + return cipher; + } + + public static 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; + } + +} --- src/java/org/apache/poi/poifs/crypt/Decryptor.java (revision 1541255) +++ src/java/org/apache/poi/poifs/crypt/Decryptor.java (working copy) @@ -18,17 +18,12 @@ 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; + +import org.apache.poi.EncryptedDocumentException; +import org.apache.poi.poifs.filesystem.DirectoryNode; 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; -import org.apache.poi.util.LittleEndianConsts; public abstract class Decryptor { public static final String DEFAULT_PASSWORD="VelvetSweatshop"; @@ -86,40 +81,4 @@ public InputStream getDataStream(POIFSFileSystem fs) throws IOException, GeneralSecurityException { return getDataStream(fs.getRoot()); } - - protected byte[] hashPassword(EncryptionInfo info, - String password) throws NoSuchAlgorithmException { - // If no password was given, use the default - if (password == null) { - password = DEFAULT_PASSWORD; - } - - byte[] pass; - try { - pass = password.getBytes("UTF-16LE"); - } catch (UnsupportedEncodingException e) { - throw new EncryptedDocumentException("UTF16 not supported"); - } - - 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(); - sha1.update(iterator); - 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; - } } --- src/java/org/apache/poi/poifs/crypt/EcmaDecryptor.java (revision 1541009) +++ src/java/org/apache/poi/poifs/crypt/EcmaDecryptor.java (working copy) @@ -16,6 +16,8 @@ ==================================================================== */ package org.apache.poi.poifs.crypt; +import static org.apache.poi.poifs.crypt.AgileFunctions.hashPassword; + import java.io.IOException; import java.io.InputStream; import java.security.GeneralSecurityException; @@ -36,62 +38,59 @@ */ public class EcmaDecryptor extends Decryptor { private final EncryptionInfo info; - private byte[] passwordHash; + private SecretKey _secretKey; + // private byte[] passwordHash; private long _length = -1; 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, 0, block); - byte[] finalHash = sha1.digest(blockValue); + public boolean verifyPassword(String password) throws GeneralSecurityException { + EncryptionVerifier verifier = info.getVerifier(); + byte passwordHash[] = hashPassword(password, verifier.getSalt(), verifier.getSpinCount()); - int requiredKeyLength = info.getHeader().getKeySize()/8; + byte[] blockKey = new byte[4]; + LittleEndian.putInt(blockKey, 0, 0); - byte[] buff = new byte[64]; + byte[] finalHash = AgileFunctions.generateKey(passwordHash, blockKey, 20); + byte x1[] = fillAndXor(finalHash, (byte) 0x36); + byte x2[] = fillAndXor(finalHash, (byte) 0x5c); - Arrays.fill(buff, (byte) 0x36); + 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); - for (int i=0; i source.length) { @@ -110,13 +109,8 @@ 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; + private static Cipher getCipher(SecretKey key) throws GeneralSecurityException { + return AgileFunctions.getCipher(key, "ECB", null); } public InputStream getDataStream(DirectoryNode dir) throws IOException, GeneralSecurityException { @@ -124,11 +118,15 @@ _length = dis.readLong(); - return new CipherInputStream(dis, getCipher()); + return new CipherInputStream(dis, getCipher(_secretKey)); } public long getLength(){ if(_length == -1) throw new IllegalStateException("EcmaDecryptor.getDataStream() was not called"); return _length; } + + protected int getKeySizeInBytes() { + return info.getHeader().getKeySize()/8; + } } --- src/java/org/apache/poi/poifs/crypt/EncryptionVerifier.java (revision 1541255) +++ src/java/org/apache/poi/poifs/crypt/EncryptionVerifier.java (working copy) @@ -35,9 +35,10 @@ private final byte[] verifier; private final byte[] verifierHash; private final byte[] encryptedKey; - private final int verifierHashSize; + // private final int verifierHashSize; private final int spinCount; private final int algorithm; + private final String algorithmName; private final int cipherMode; public EncryptionVerifier(String descriptor) { @@ -82,15 +83,15 @@ .getNamedItem("encryptedVerifierHashValue") .getNodeValue().getBytes()); - int blockSize = Integer.parseInt(keyData.getNamedItem("blockSize") - .getNodeValue()); + // int blockSize = Integer.parseInt(keyData.getNamedItem("blockSize") + // .getNodeValue()); - String alg = keyData.getNamedItem("cipherAlgorithm").getNodeValue(); + algorithmName = keyData.getNamedItem("cipherAlgorithm").getNodeValue(); int keyBits = Integer.parseInt(keyData.getNamedItem("keyBits") .getNodeValue()); - if ("AES".equals(alg)) { + if ("AES".equals(algorithmName)) { switch (keyBits) { case 128: algorithm = EncryptionHeader.ALGORITHM_AES_128; break; @@ -113,8 +114,8 @@ else throw new EncryptedDocumentException("Unsupported chaining mode"); - verifierHashSize = Integer.parseInt(keyData.getNamedItem("hashSize") - .getNodeValue()); + //verifierHashSize = Integer.parseInt(keyData.getNamedItem("hashSize") + // .getNodeValue()); } public EncryptionVerifier(DocumentInputStream is, int encryptedLength) { @@ -129,13 +130,14 @@ verifier = new byte[16]; is.readFully(verifier); - verifierHashSize = is.readInt(); + int verifierHashSize = is.readInt(); verifierHash = new byte[encryptedLength]; is.readFully(verifierHash); spinCount = 50000; algorithm = EncryptionHeader.ALGORITHM_AES_128; + algorithmName = "AES"; cipherMode = EncryptionHeader.MODE_ECB; encryptedKey = null; } @@ -164,6 +166,10 @@ return algorithm; } + public String getAlgorithmName() { + return algorithmName; + } + public byte[] getEncryptedKey() { return encryptedKey; } --- src/java/org/apache/poi/poifs/crypt/Encryptor.java (revision 0) +++ src/java/org/apache/poi/poifs/crypt/Encryptor.java (working copy) @@ -0,0 +1,53 @@ +package org.apache.poi.poifs.crypt; + +import java.io.IOException; +import java.io.OutputStream; +import java.security.GeneralSecurityException; + +import org.apache.poi.EncryptedDocumentException; +import org.apache.poi.poifs.filesystem.DirectoryNode; +import org.apache.poi.poifs.filesystem.NPOIFSFileSystem; +import org.apache.poi.poifs.filesystem.POIFSFileSystem; + +public abstract class Encryptor { + /** + * Return a stream for encrypted data. + *

+ * Use {@link #getLength()} to get the size of that data that can be safely read from the stream. + * Just reading to the end of the input stream is not sufficient because there are + * normally padding bytes that must be discarded + *

+ * + * @param dir the node to read from + * @return encrypted stream + */ + public abstract OutputStream getDataStream(DirectoryNode dir) + throws IOException, GeneralSecurityException; + + public abstract void confirmPassword(String password) + throws GeneralSecurityException; + + + + public static Encryptor getInstance(EncryptionInfo info) { + int major = info.getVersionMajor(); + int minor = info.getVersionMinor(); + + if (major == 4 && minor == 4) + return new AgileEncryptor(info); +// else if (minor == 2 && (major == 3 || major == 4)) +// return new EcmaDecryptor(info); + else + throw new EncryptedDocumentException("Unsupported version"); + } + + public OutputStream getDataStream(NPOIFSFileSystem fs) throws IOException, GeneralSecurityException { + return getDataStream(fs.getRoot()); + } + + public OutputStream getDataStream(POIFSFileSystem fs) throws IOException, GeneralSecurityException { + return getDataStream(fs.getRoot()); + } + + +}