diff --git a/.classpath b/.classpath index facc548d70..d3cabd61b7 100644 --- a/.classpath +++ b/.classpath @@ -24,6 +24,7 @@ + diff --git a/build.xml b/build.xml index f461bd4168..f7fed23227 100644 --- a/build.xml +++ b/build.xml @@ -116,6 +116,7 @@ under the License. + @@ -167,6 +168,11 @@ under the License. + + + + + @@ -210,6 +216,7 @@ under the License. + @@ -436,9 +443,13 @@ under the License. - - - + + + + + + + @@ -485,6 +496,40 @@ under the License. /> + + + + + + + + + + + + + + + + + + @@ -571,7 +616,7 @@ under the License. - + + + + + + + + + + + + - + @@ -951,7 +1007,7 @@ under the License. description="Generates the API documentation"> + maxmemory="384M" additionalparam="-notimestamp"> diff --git a/src/java/org/apache/poi/EncryptedDocumentException.java b/src/java/org/apache/poi/EncryptedDocumentException.java index 4922d1c81d..12196c80cd 100644 --- a/src/java/org/apache/poi/EncryptedDocumentException.java +++ b/src/java/org/apache/poi/EncryptedDocumentException.java @@ -16,9 +16,18 @@ ==================================================================== */ package org.apache.poi; +@SuppressWarnings("serial") public class EncryptedDocumentException extends IllegalStateException { public EncryptedDocumentException(String s) { super(s); } + + public EncryptedDocumentException(String message, Throwable cause) { + super(message, cause); + } + + public EncryptedDocumentException(Throwable cause) { + super(cause); + } } diff --git a/src/java/org/apache/poi/poifs/crypt/AgileDecryptor.java b/src/java/org/apache/poi/poifs/crypt/AgileDecryptor.java deleted file mode 100644 index 401049b0dd..0000000000 --- a/src/java/org/apache/poi/poifs/crypt/AgileDecryptor.java +++ /dev/null @@ -1,268 +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 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; -import javax.crypto.SecretKey; -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; - -/** - * - */ -public class AgileDecryptor extends Decryptor { - - private final EncryptionInfo _info; - private SecretKey _secretKey; - private long _length = -1; - - 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(); - 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()); - - 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); - - 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"); - return true; - } else { - return false; - } - } - - public InputStream getDataStream(DirectoryNode dir) throws IOException, GeneralSecurityException { - DocumentInputStream dis = dir.createDocumentInputStream("EncryptedPackage"); - _length = dis.readLong(); - return new ChunkedCipherInputStream(dis, _length); - } - - public long getLength(){ - if(_length == -1) throw new IllegalStateException("EcmaDecryptor.getDataStream() was not called"); - return _length; - } - - 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(_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, 0, index); - byte[] iv = generateIv(_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(SecretKey key, byte[] vec) - throws GeneralSecurityException { - - 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"; - 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); - 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(); - } - - protected int getKeySizeInBytes() { - return _info.getHeader().getKeySize()/8; - } -} diff --git a/src/java/org/apache/poi/poifs/crypt/ChainingMode.java b/src/java/org/apache/poi/poifs/crypt/ChainingMode.java new file mode 100644 index 0000000000..7fccccfb25 --- /dev/null +++ b/src/java/org/apache/poi/poifs/crypt/ChainingMode.java @@ -0,0 +1,33 @@ +/* ==================================================================== + 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; + +public enum ChainingMode { + // ecb - only for standard encryption + ecb("ECB", 1), + cbc("CBC", 2), + /* Cipher feedback chaining (CFB), with an 8-bit window */ + cfb("CFB8", 3); + + public final String jceId; + public final int ecmaId; + ChainingMode(String jceId, int ecmaId) { + this.jceId = jceId; + this.ecmaId = ecmaId; + } +} \ No newline at end of file diff --git a/src/java/org/apache/poi/poifs/crypt/CipherAlgorithm.java b/src/java/org/apache/poi/poifs/crypt/CipherAlgorithm.java new file mode 100644 index 0000000000..be507a6660 --- /dev/null +++ b/src/java/org/apache/poi/poifs/crypt/CipherAlgorithm.java @@ -0,0 +1,79 @@ +/* ==================================================================== + 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 org.apache.poi.EncryptedDocumentException; + +public enum CipherAlgorithm { + // key size for rc4: 0x00000028 - 0x00000080 (inclusive) with 8-bit increments + // no block size, because its a streaming cipher + rc4(CipherProvider.rc4, "RC4", 0x6801, 0x40, new int[]{0x28,0x30,0x38,0x40,0x48,0x50,0x58,0x60,0x68,0x70,0x78,0x80}, -1, 20, "RC4", false), + // aes has always a block size of 128 - only its keysize may vary + aes128(CipherProvider.aes, "AES", 0x660E, 128, new int[]{128}, 16, 32, "AES", false), + aes192(CipherProvider.aes, "AES", 0x660F, 192, new int[]{192}, 16, 32, "AES", false), + aes256(CipherProvider.aes, "AES", 0x6610, 256, new int[]{256}, 16, 32, "AES", false), + rc2(null, "RC2", -1, 0x80, new int[]{0x28,0x30,0x38,0x40,0x48,0x50,0x58,0x60,0x68,0x70,0x78,0x80}, 8, 20, "RC2", false), + des(null, "DES", -1, 64, new int[]{64}, 8/*for 56-bit*/, 32, "DES", false), + // desx is not supported. Not sure, if it can be simulated by des3 somehow + des3(null, "DESede", -1, 192, new int[]{192}, 8, 32, "3DES", false), + // need bouncycastle provider for this one ... + // see http://stackoverflow.com/questions/4436397/3des-des-encryption-using-the-jce-generating-an-acceptable-key + des3_112(null, "DESede", -1, 128, new int[]{128}, 8, 32, "3DES_112", true), + ; + + public final CipherProvider provider; + public final String jceId; + public final int ecmaId; + public final int defaultKeySize; + public final int allowedKeySize[]; + public final int blockSize; + public final int encryptedVerifierHashLength; + public final String xmlId; + public final boolean needsBouncyCastle; + + CipherAlgorithm(CipherProvider provider, String jceId, int ecmaId, int defaultKeySize, int allowedKeySize[], int blockSize, int encryptedVerifierHashLength, String xmlId, boolean needsBouncyCastle) { + this.provider = provider; + this.jceId = jceId; + this.ecmaId = ecmaId; + this.defaultKeySize = defaultKeySize; + this.allowedKeySize = allowedKeySize; + this.blockSize = blockSize; + this.encryptedVerifierHashLength = encryptedVerifierHashLength; + this.xmlId = xmlId; + this.needsBouncyCastle = needsBouncyCastle; + } + + public static CipherAlgorithm fromEcmaId(int ecmaId) { + for (CipherAlgorithm ca : CipherAlgorithm.values()) { + if (ca.ecmaId == ecmaId) return ca; + } + throw new EncryptedDocumentException("cipher algorithm not found"); + } + + public static CipherAlgorithm fromXmlId(String xmlId, int keySize) { + for (CipherAlgorithm ca : CipherAlgorithm.values()) { + if (!ca.xmlId.equals(xmlId)) continue; + for (int ks : ca.allowedKeySize) { + if (ks == keySize) return ca; + } + } + throw new EncryptedDocumentException("cipher algorithm not found"); + } + + +} \ No newline at end of file diff --git a/src/java/org/apache/poi/poifs/crypt/CipherProvider.java b/src/java/org/apache/poi/poifs/crypt/CipherProvider.java new file mode 100644 index 0000000000..de343a91dc --- /dev/null +++ b/src/java/org/apache/poi/poifs/crypt/CipherProvider.java @@ -0,0 +1,39 @@ +/* ==================================================================== + 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 org.apache.poi.EncryptedDocumentException; + +public enum CipherProvider { + rc4("RC4", 1), + aes("AES", 0x18); + + public static CipherProvider fromEcmaId(int ecmaId) { + for (CipherProvider cp : CipherProvider.values()) { + if (cp.ecmaId == ecmaId) return cp; + } + throw new EncryptedDocumentException("cipher provider not found"); + } + + public final String jceId; + public final int ecmaId; + CipherProvider(String jceId, int ecmaId) { + this.jceId = jceId; + this.ecmaId = ecmaId; + } +} \ No newline at end of file diff --git a/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java b/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java new file mode 100644 index 0000000000..75bf1e85ea --- /dev/null +++ b/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java @@ -0,0 +1,258 @@ +/* ==================================================================== + 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.nio.charset.Charset; +import java.security.DigestException; +import java.security.GeneralSecurityException; +import java.security.MessageDigest; +import java.security.Provider; +import java.security.Security; +import java.util.Arrays; + +import javax.crypto.Cipher; +import javax.crypto.Mac; +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; + +/** + * Helper functions used for standard and agile encryption + */ +public class CryptoFunctions { + /** + * 2.3.4.7 ECMA-376 Document Encryption Key Generation (Standard Encryption) + * 2.3.4.11 Encryption Key Generation (Agile Encryption) + * + * The encryption key for ECMA-376 document encryption [ECMA-376] using agile encryption MUST be + * generated by using the following method, which is derived from PKCS #5: Password-Based + * Cryptography Version 2.0 [RFC2898]. + * + * Let H() be a hashing algorithm as determined by the PasswordKeyEncryptor.hashAlgorithm + * element, H_n be the hash data of the n-th iteration, and a plus sign (+) represent concatenation. The + * password MUST be provided as an array of Unicode characters. Limitations on the length of the + * password and the characters used by the password are implementation-dependent. The initial + * password hash is generated as follows: + * + * - H_0 = H(salt + password) + * + * The salt used MUST be generated randomly. The salt MUST be stored in the + * PasswordKeyEncryptor.saltValue element contained within the \EncryptionInfo stream (1) as + * specified in section 2.3.4.10. The hash is then iterated by using the following approach: + * + * - H_n = H(iterator + H_n-1) + * + * where iterator is an unsigned 32-bit value that is initially set to 0x00000000 and then incremented + * monotonically on each iteration until PasswordKey.spinCount iterations have been performed. + * The value of iterator on the last iteration MUST be one less than PasswordKey.spinCount. + * + * For POI, H_final will be calculated by {@link generateKey()} + * + * @param password + * @param hashAlgorithm + * @param salt + * @param spinCount + * @return + */ + public static byte[] hashPassword(String password, HashAlgorithm hashAlgorithm, byte salt[], int spinCount) { + // If no password was given, use the default + if (password == null) { + password = Decryptor.DEFAULT_PASSWORD; + } + + MessageDigest hashAlg = getMessageDigest(hashAlgorithm); + + hashAlg.update(salt); + byte[] hash = hashAlg.digest(getUtf16LeString(password)); + byte[] iterator = new byte[LittleEndianConsts.INT_SIZE]; + + try { + for (int i = 0; i < spinCount; i++) { + LittleEndian.putInt(iterator, 0, i); + hashAlg.reset(); + hashAlg.update(iterator); + hashAlg.update(hash); + hashAlg.digest(hash, 0, hash.length); // don't create hash buffer everytime new + } + } catch (DigestException e) { + throw new EncryptedDocumentException("error in password hashing"); + } + + return hash; + } + + /** + * 2.3.4.12 Initialization Vector Generation (Agile Encryption) + * + * Initialization vectors are used in all cases for agile encryption. An initialization vector MUST be + * generated by using the following method, where H() is a hash function that MUST be the same as + * specified in section 2.3.4.11 and a plus sign (+) represents concatenation: + * 1. If a blockKey is provided, let IV be a hash of the KeySalt and the following value: + * blockKey: IV = H(KeySalt + blockKey) + * 2. If a blockKey is not provided, let IV be equal to the following value: + * KeySalt:IV = KeySalt. + * 3. If the number of bytes in the value of IV is less than the the value of the blockSize attribute + * corresponding to the cipherAlgorithm attribute, pad the array of bytes by appending 0x36 until + * the array is blockSize bytes. If the array of bytes is larger than blockSize bytes, truncate the + * array to blockSize bytes. + **/ + public static byte[] generateIv(HashAlgorithm hashAlgorithm, byte[] salt, byte[] blockKey, int blockSize) { + byte iv[] = salt; + if (blockKey != null) { + MessageDigest hashAlgo = getMessageDigest(hashAlgorithm); + hashAlgo.update(salt); + iv = hashAlgo.digest(blockKey); + } + return getBlock36(iv, blockSize); + } + + /** + * 2.3.4.11 Encryption Key Generation (Agile Encryption) + * + * ... continued ... + * + * The final hash data that is used for an encryption key is then generated by using the following + * method: + * + * - H_final = H(H_n + blockKey) + * + * where blockKey represents an array of bytes used to prevent two different blocks from encrypting + * to the same cipher text. + * + * If the size of the resulting H_final is smaller than that of PasswordKeyEncryptor.keyBits, the key + * MUST be padded by appending bytes with a value of 0x36. If the hash value is larger in size than + * PasswordKeyEncryptor.keyBits, the key is obtained by truncating the hash value. + * + * @param passwordHash + * @param hashAlgorithm + * @param blockKey + * @param keySize + * @return + */ + public static byte[] generateKey(byte[] passwordHash, HashAlgorithm hashAlgorithm, byte[] blockKey, int keySize) { + MessageDigest hashAlgo = getMessageDigest(hashAlgorithm); + hashAlgo.update(passwordHash); + byte[] key = hashAlgo.digest(blockKey); + return getBlock36(key, keySize); + } + + public static Cipher getCipher(SecretKey key, CipherAlgorithm cipherAlgorithm, ChainingMode chain, byte[] vec, int cipherMode) { + return getCipher(key, cipherAlgorithm, chain, vec, cipherMode, null); + } + + /** + * + * + * @param key + * @param chain + * @param vec + * @param cipherMode Cipher.DECRYPT_MODE or Cipher.ENCRYPT_MODE + * @return + * @throws GeneralSecurityException + */ + public static Cipher getCipher(SecretKey key, CipherAlgorithm cipherAlgorithm, ChainingMode chain, byte[] vec, int cipherMode, String padding) { + int keySizeInBytes = key.getEncoded().length; + if (padding == null) padding = "NoPadding"; + + try { + // Ensure the JCE policies files allow for this sized key + if (Cipher.getMaxAllowedKeyLength(key.getAlgorithm()) < keySizeInBytes*8) { + throw new EncryptedDocumentException("Export Restrictions in place - please install JCE Unlimited Strength Jurisdiction Policy files"); + } + + Cipher cipher; + if (cipherAlgorithm.needsBouncyCastle) { + registerBouncyCastle(); + cipher = Cipher.getInstance(key.getAlgorithm() + "/" + chain.jceId + "/" + padding, "BC"); + } else { + cipher = Cipher.getInstance(key.getAlgorithm() + "/" + chain.jceId + "/" + padding); + } + + if (vec == null) { + cipher.init(cipherMode, key); + } else { + IvParameterSpec iv = new IvParameterSpec(vec); + cipher.init(cipherMode, key, iv); + } + return cipher; + } catch (GeneralSecurityException e) { + throw new EncryptedDocumentException(e); + } + } + + public static byte[] getBlock36(byte[] hash, int size) { + return getBlockX(hash, size, (byte)0x36); + } + + public static byte[] getBlock0(byte[] hash, int size) { + return getBlockX(hash, size, (byte)0); + } + + private static byte[] getBlockX(byte[] hash, int size, byte fill) { + if (hash.length == size) return hash; + + byte[] result = new byte[size]; + Arrays.fill(result, fill); + System.arraycopy(hash, 0, result, 0, Math.min(result.length, hash.length)); + return result; + } + + public static byte[] getUtf16LeString(String str) { + Charset cs = Charset.forName("UTF-16LE"); + return str.getBytes(cs); + } + + public static MessageDigest getMessageDigest(HashAlgorithm hashAlgorithm) { + try { + if (hashAlgorithm.needsBouncyCastle) { + registerBouncyCastle(); + return MessageDigest.getInstance(hashAlgorithm.jceId, "BC"); + } else { + return MessageDigest.getInstance(hashAlgorithm.jceId); + } + } catch (GeneralSecurityException e) { + throw new EncryptedDocumentException("hash algo not supported", e); + } + } + + public static Mac getMac(HashAlgorithm hashAlgorithm) { + try { + if (hashAlgorithm.needsBouncyCastle) { + registerBouncyCastle(); + return Mac.getInstance(hashAlgorithm.jceHmacId, "BC"); + } else { + return Mac.getInstance(hashAlgorithm.jceHmacId); + } + } catch (GeneralSecurityException e) { + throw new EncryptedDocumentException("hmac algo not supported", e); + } + } + + @SuppressWarnings("unchecked") + private static void registerBouncyCastle() { + if (Security.getProvider("BC") != null) return; + try { + Class clazz = (Class)Class.forName("org.bouncycastle.jce.provider.BouncyCastleProvider"); + Security.addProvider(clazz.newInstance()); + } catch (Exception e) { + throw new EncryptedDocumentException("Only the BouncyCastle provider supports your encryption settings - please add it to the classpath."); + } + } +} diff --git a/src/java/org/apache/poi/poifs/crypt/DataSpaceMapUtils.java b/src/java/org/apache/poi/poifs/crypt/DataSpaceMapUtils.java new file mode 100644 index 0000000000..963151ff97 --- /dev/null +++ b/src/java/org/apache/poi/poifs/crypt/DataSpaceMapUtils.java @@ -0,0 +1,365 @@ +/* ==================================================================== + 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.nio.charset.Charset; + +import org.apache.poi.EncryptedDocumentException; +import org.apache.poi.poifs.crypt.standard.EncryptionRecord; +import org.apache.poi.poifs.filesystem.DirectoryEntry; +import org.apache.poi.poifs.filesystem.DocumentEntry; +import org.apache.poi.poifs.filesystem.POIFSWriterEvent; +import org.apache.poi.poifs.filesystem.POIFSWriterListener; +import org.apache.poi.util.LittleEndianByteArrayOutputStream; +import org.apache.poi.util.LittleEndianConsts; +import org.apache.poi.util.LittleEndianInput; +import org.apache.poi.util.LittleEndianOutput; + +public class DataSpaceMapUtils { + public static void addDefaultDataSpace(DirectoryEntry dir) throws IOException { + DataSpaceMapEntry dsme = new DataSpaceMapEntry( + new int[]{ 0 } + , new String[]{ "EncryptedPackage" } + , "StrongEncryptionDataSpace" + ); + DataSpaceMap dsm = new DataSpaceMap(new DataSpaceMapEntry[]{dsme}); + createEncryptionEntry(dir, "\u0006DataSpaces/DataSpaceMap", dsm); + + DataSpaceDefinition dsd = new DataSpaceDefinition(new String[]{ "StrongEncryptionTransform" }); + createEncryptionEntry(dir, "\u0006DataSpaces/DataSpaceInfo/StrongEncryptionDataSpace", dsd); + + TransformInfoHeader tih = new TransformInfoHeader( + 1 + , "{FF9A3F03-56EF-4613-BDD5-5A41C1D07246}" + , "Microsoft.Container.EncryptionTransform" + , 1, 0, 1, 0, 1, 0 + ); + IRMDSTransformInfo irm = new IRMDSTransformInfo(tih, 0, null); + createEncryptionEntry(dir, "\u0006DataSpaces/TransformInfo/StrongEncryptionTransform/\u0006Primary", irm); + + DataSpaceVersionInfo dsvi = new DataSpaceVersionInfo("Microsoft.Container.DataSpaces", 1, 0, 1, 0, 1, 0); + createEncryptionEntry(dir, "\u0006DataSpaces/Version", dsvi); + } + + public static DocumentEntry createEncryptionEntry(DirectoryEntry dir, String path, EncryptionRecord out) throws IOException { + String parts[] = path.split("/"); + for (int i=0; i 0) { + for (int i=0; i<(4-scratchedBytes); i++) { + is.readByte(); + } + } + Charset cs = Charset.forName("UTF-8"); + return new String(data, 0, data.length, cs); + } + + public static void writeUtf8LPP4(LittleEndianOutput os, String str) { + if (str == null || "".equals(str)) { + os.writeInt(str == null ? 0 : 4); + os.writeInt(0); + } else { + Charset cs = Charset.forName("UTF-8"); + byte buf[] = str.getBytes(cs); + os.writeInt(buf.length); + os.write(buf); + int scratchBytes = buf.length%4; + if (scratchBytes > 0) { + for (int i=0; i<(4-scratchBytes); i++) { + os.writeByte(0); + } + } + } + } + +} diff --git a/src/java/org/apache/poi/poifs/crypt/Decryptor.java b/src/java/org/apache/poi/poifs/crypt/Decryptor.java index 39876f2f4c..c2d0d5953b 100644 --- a/src/java/org/apache/poi/poifs/crypt/Decryptor.java +++ b/src/java/org/apache/poi/poifs/crypt/Decryptor.java @@ -18,21 +18,26 @@ 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; + +import javax.crypto.SecretKey; + +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"; + + protected final EncryptionInfo info; + private SecretKey secretKey; + private byte[] verifier, integrityHmacKey, integrityHmacValue; + protected Decryptor(EncryptionInfo info) { + this.info = info; + } + /** * Return a stream with decrypted data. *

@@ -68,15 +73,11 @@ public abstract class Decryptor { public abstract long getLength(); public static Decryptor getInstance(EncryptionInfo info) { - int major = info.getVersionMajor(); - int minor = info.getVersionMinor(); - - if (major == 4 && minor == 4) - return new AgileDecryptor(info); - else if (minor == 2 && (major == 3 || major == 4)) - return new EcmaDecryptor(info); - else + Decryptor d = info.getDecryptor(); + if (d == null) { throw new EncryptedDocumentException("Unsupported version"); + } + return d; } public InputStream getDataStream(NPOIFSFileSystem fs) throws IOException, GeneralSecurityException { @@ -86,40 +87,37 @@ public abstract class Decryptor { public InputStream getDataStream(POIFSFileSystem fs) throws IOException, GeneralSecurityException { return getDataStream(fs.getRoot()); } + + // for tests + public byte[] getVerifier() { + return verifier; + } - 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"); - } + public SecretKey getSecretKey() { + return secretKey; + } + + public byte[] getIntegrityHmacKey() { + return integrityHmacKey; + } - 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; + public byte[] getIntegrityHmacValue() { + return integrityHmacValue; + } + + protected void setSecretKey(SecretKey secretKey) { + this.secretKey = secretKey; + } + + protected void setVerifier(byte[] verifier) { + this.verifier = verifier; + } + + protected void setIntegrityHmacKey(byte[] integrityHmacKey) { + this.integrityHmacKey = integrityHmacKey; + } + + protected void setIntegrityHmacValue(byte[] integrityHmacValue) { + this.integrityHmacValue = integrityHmacValue; } } \ 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 deleted file mode 100644 index 65e9be9089..0000000000 --- a/src/java/org/apache/poi/poifs/crypt/EcmaDecryptor.java +++ /dev/null @@ -1,134 +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 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; -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.util.LittleEndian; - -/** - */ -public class EcmaDecryptor extends Decryptor { - private final EncryptionInfo info; - 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); - - int requiredKeyLength = info.getHeader().getKeySize()/8; - - byte[] buff = new byte[64]; - - Arrays.fill(buff, (byte) 0x36); - - for (int i=0; i source.length) { - for(int i=source.length; i + + + + + + +

Implementation of the ECMA-376 Document Encryption

+

The implementation is split into three packages:

+
    +
  • This package contains common functions for both current implemented cipher modes.
  • +
  • the {@link org.apache.poi.poifs.crypt.standard standard} package is part of the base poi jar and contains classes for the standard encryption ...
  • +
  • the {@link org.apache.poi.poifs.crypt.agile agile} package is part of the poi ooxml jar and the provides agile encryption support.
  • +
+ +

Related Documentation

+ +Some implementations informations can be found under: + + + +@see org.apache.poi.poifs.crypt.standard +@see org.apache.poi.poifs.crypt.agile + + diff --git a/src/java/org/apache/poi/poifs/crypt/standard/EncryptionRecord.java b/src/java/org/apache/poi/poifs/crypt/standard/EncryptionRecord.java new file mode 100644 index 0000000000..bf65fbe796 --- /dev/null +++ b/src/java/org/apache/poi/poifs/crypt/standard/EncryptionRecord.java @@ -0,0 +1,23 @@ +/* ==================================================================== + 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.standard; + +import org.apache.poi.util.LittleEndianByteArrayOutputStream; + +public interface EncryptionRecord { + void write(LittleEndianByteArrayOutputStream os); +} diff --git a/src/java/org/apache/poi/poifs/crypt/standard/StandardDecryptor.java b/src/java/org/apache/poi/poifs/crypt/standard/StandardDecryptor.java new file mode 100644 index 0000000000..18729a1ff0 --- /dev/null +++ b/src/java/org/apache/poi/poifs/crypt/standard/StandardDecryptor.java @@ -0,0 +1,158 @@ +/* ==================================================================== + 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.standard; + +import static org.apache.poi.poifs.crypt.CryptoFunctions.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; +import javax.crypto.CipherInputStream; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +import org.apache.poi.EncryptedDocumentException; +import org.apache.poi.poifs.crypt.ChainingMode; +import org.apache.poi.poifs.crypt.CryptoFunctions; +import org.apache.poi.poifs.crypt.Decryptor; +import org.apache.poi.poifs.crypt.EncryptionHeader; +import org.apache.poi.poifs.crypt.EncryptionInfo; +import org.apache.poi.poifs.crypt.EncryptionVerifier; +import org.apache.poi.poifs.crypt.HashAlgorithm; +import org.apache.poi.poifs.filesystem.DirectoryNode; +import org.apache.poi.poifs.filesystem.DocumentInputStream; +import org.apache.poi.util.BoundedInputStream; +import org.apache.poi.util.LittleEndian; + +/** + */ +public class StandardDecryptor extends Decryptor { + private long _length = -1; + + protected StandardDecryptor(EncryptionInfo info) { + super(info); + } + + public boolean verifyPassword(String password) { + EncryptionVerifier ver = info.getVerifier(); + SecretKey skey = generateSecretKey(password, ver, getKeySizeInBytes()); + Cipher cipher = getCipher(skey); + + try { + byte encryptedVerifier[] = ver.getEncryptedVerifier(); + byte verifier[] = cipher.doFinal(encryptedVerifier); + setVerifier(verifier); + MessageDigest sha1 = MessageDigest.getInstance(ver.getHashAlgorithm().jceId); + byte[] calcVerifierHash = sha1.digest(verifier); + byte encryptedVerifierHash[] = ver.getEncryptedVerifierHash(); + byte decryptedVerifierHash[] = cipher.doFinal(encryptedVerifierHash); + byte[] verifierHash = truncateOrPad(decryptedVerifierHash, calcVerifierHash.length); + + if (Arrays.equals(calcVerifierHash, verifierHash)) { + setSecretKey(skey); + return true; + } else { + return false; + } + } catch (GeneralSecurityException e) { + throw new EncryptedDocumentException(e); + } + } + + protected static SecretKey generateSecretKey(String password, EncryptionVerifier ver, int keySize) { + HashAlgorithm hashAlgo = ver.getHashAlgorithm(); + + byte pwHash[] = hashPassword(password, hashAlgo, ver.getSalt(), ver.getSpinCount()); + + byte[] blockKey = new byte[4]; + LittleEndian.putInt(blockKey, 0, 0); + + byte[] finalHash = CryptoFunctions.generateKey(pwHash, hashAlgo, blockKey, hashAlgo.hashSize); + byte x1[] = fillAndXor(finalHash, (byte) 0x36); + byte x2[] = fillAndXor(finalHash, (byte) 0x5c); + + 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); + + byte[] key = truncateOrPad(x3, keySize); + + SecretKey skey = new SecretKeySpec(key, ver.getCipherAlgorithm().jceId); + return skey; + } + + protected static byte[] fillAndXor(byte hash[], byte fillByte) { + byte[] buff = new byte[64]; + Arrays.fill(buff, fillByte); + + for (int i=0; i source.length) { + for(int i=source.length; iPOIXMLException with no diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileDecryptor.java b/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileDecryptor.java new file mode 100644 index 0000000000..7e45786db9 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileDecryptor.java @@ -0,0 +1,404 @@ +/* ==================================================================== + 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.agile; + +import static org.apache.poi.poifs.crypt.CryptoFunctions.generateIv; +import static org.apache.poi.poifs.crypt.CryptoFunctions.generateKey; +import static org.apache.poi.poifs.crypt.CryptoFunctions.getBlock0; +import static org.apache.poi.poifs.crypt.CryptoFunctions.getCipher; +import static org.apache.poi.poifs.crypt.CryptoFunctions.getMessageDigest; +import static org.apache.poi.poifs.crypt.CryptoFunctions.hashPassword; + +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.MessageDigest; +import java.security.cert.X509Certificate; +import java.util.Arrays; + +import javax.crypto.Cipher; +import javax.crypto.Mac; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +import org.apache.poi.EncryptedDocumentException; +import org.apache.poi.poifs.crypt.CipherAlgorithm; +import org.apache.poi.poifs.crypt.CryptoFunctions; +import org.apache.poi.poifs.crypt.Decryptor; +import org.apache.poi.poifs.crypt.EncryptionHeader; +import org.apache.poi.poifs.crypt.EncryptionVerifier; +import org.apache.poi.poifs.crypt.HashAlgorithm; +import org.apache.poi.poifs.crypt.agile.AgileEncryptionVerifier.AgileCertificateEntry; +import org.apache.poi.poifs.filesystem.DirectoryNode; +import org.apache.poi.poifs.filesystem.DocumentInputStream; +import org.apache.poi.util.LittleEndian; + +/** + * Decryptor implementation for Agile Encryption + */ +public class AgileDecryptor extends Decryptor { + private final AgileEncryptionInfoBuilder builder; + + + private long _length = -1; + + protected static final byte[] kVerifierInputBlock; + protected static final byte[] kHashedVerifierBlock; + protected static final byte[] kCryptoKeyBlock; + protected static final byte[] kIntegrityKeyBlock; + protected static final byte[] kIntegrityValueBlock; + + 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 }; + kIntegrityKeyBlock = + new byte[] { (byte)0x5f, (byte)0xb2, (byte)0xad, (byte)0x01, + (byte)0x0c, (byte)0xb9, (byte)0xe1, (byte)0xf6 }; + kIntegrityValueBlock = + new byte[] { (byte)0xa0, (byte)0x67, (byte)0x7f, (byte)0x02, + (byte)0xb2, (byte)0x2c, (byte)0x84, (byte)0x33 }; + } + + protected AgileDecryptor(AgileEncryptionInfoBuilder builder) { + super(builder.getInfo()); + this.builder = builder; + } + + /** + * set decryption password + */ + public boolean verifyPassword(String password) throws GeneralSecurityException { + AgileEncryptionVerifier ver = builder.getVerifier(); + AgileEncryptionHeader header = builder.getHeader(); + HashAlgorithm hashAlgo = header.getHashAlgorithmEx(); + CipherAlgorithm cipherAlgo = header.getCipherAlgorithm(); + int blockSize = header.getBlockSize(); + int keySize = header.getKeySize()/8; + + byte[] pwHash = hashPassword(password, ver.getHashAlgorithm(), ver.getSalt(), ver.getSpinCount()); + + /** + * encryptedVerifierHashInput: This attribute MUST be generated by using the following steps: + * 1. Generate a random array of bytes with the number of bytes used specified by the saltSize + * attribute. + * 2. Generate an encryption key as specified in section 2.3.4.11 by using the user-supplied password, + * the binary byte array used to create the saltValue attribute, and a blockKey byte array + * consisting of the following bytes: 0xfe, 0xa7, 0xd2, 0x76, 0x3b, 0x4b, 0x9e, and 0x79. + * 3. Encrypt the random array of bytes generated in step 1 by using the binary form of the saltValue + * attribute as an initialization vector as specified in section 2.3.4.12. If the array of bytes is not an + * integral multiple of blockSize bytes, pad the array with 0x00 to the next integral multiple of + * blockSize bytes. + * 4. Use base64 to encode the result of step 3. + */ + byte verfierInputEnc[] = hashInput(builder, pwHash, kVerifierInputBlock, ver.getEncryptedVerifier(), Cipher.DECRYPT_MODE); + setVerifier(verfierInputEnc); + MessageDigest hashMD = getMessageDigest(hashAlgo); + byte[] verifierHash = hashMD.digest(verfierInputEnc); + + /** + * encryptedVerifierHashValue: This attribute MUST be generated by using the following steps: + * 1. Obtain the hash value of the random array of bytes generated in step 1 of the steps for + * encryptedVerifierHashInput. + * 2. Generate an encryption key as specified in section 2.3.4.11 by using the user-supplied password, + * the binary byte array used to create the saltValue attribute, and a blockKey byte array + * consisting of the following bytes: 0xd7, 0xaa, 0x0f, 0x6d, 0x30, 0x61, 0x34, and 0x4e. + * 3. Encrypt the hash value obtained in step 1 by using the binary form of the saltValue attribute as + * an initialization vector as specified in section 2.3.4.12. If hashSize is not an integral multiple of + * blockSize bytes, pad the hash value with 0x00 to an integral multiple of blockSize bytes. + * 4. Use base64 to encode the result of step 3. + */ + byte verifierHashDec[] = hashInput(builder, pwHash, kHashedVerifierBlock, ver.getEncryptedVerifierHash(), Cipher.DECRYPT_MODE); + verifierHashDec = getBlock0(verifierHashDec, hashAlgo.hashSize); + + /** + * encryptedKeyValue: This attribute MUST be generated by using the following steps: + * 1. Generate a random array of bytes that is the same size as specified by the + * Encryptor.KeyData.keyBits attribute of the parent element. + * 2. Generate an encryption key as specified in section 2.3.4.11, using the user-supplied password, + * the binary byte array used to create the saltValue attribute, and a blockKey byte array + * consisting of the following bytes: 0x14, 0x6e, 0x0b, 0xe7, 0xab, 0xac, 0xd0, and 0xd6. + * 3. Encrypt the random array of bytes generated in step 1 by using the binary form of the saltValue + * attribute as an initialization vector as specified in section 2.3.4.12. If the array of bytes is not an + * integral multiple of blockSize bytes, pad the array with 0x00 to an integral multiple of + * blockSize bytes. + * 4. Use base64 to encode the result of step 3. + */ + byte keyspec[] = hashInput(builder, pwHash, kCryptoKeyBlock, ver.getEncryptedKey(), Cipher.DECRYPT_MODE); + keyspec = getBlock0(keyspec, keySize); + SecretKeySpec secretKey = new SecretKeySpec(keyspec, ver.getCipherAlgorithm().jceId); + + /** + * 1. Obtain the intermediate key by decrypting the encryptedKeyValue from a KeyEncryptor + * contained within the KeyEncryptors sequence. Use this key for encryption operations in the + * remaining steps of this section. + * 2. Generate a random array of bytes, known as Salt, of the same length as the value of the + * KeyData.hashSize attribute. + * 3. Encrypt the random array of bytes generated in step 2 by using the binary form of the + * KeyData.saltValue attribute and a blockKey byte array consisting of the following bytes: 0x5f, + * 0xb2, 0xad, 0x01, 0x0c, 0xb9, 0xe1, and 0xf6 used to form an initialization vector as specified in + * section 2.3.4.12. If the array of bytes is not an integral multiple of blockSize bytes, pad the + * array with 0x00 to the next integral multiple of blockSize bytes. + * 4. Assign the encryptedHmacKey attribute to the base64-encoded form of the result of step 3. + */ + byte vec[] = CryptoFunctions.generateIv(hashAlgo, header.getKeySalt(), kIntegrityKeyBlock, blockSize); + Cipher cipher = getCipher(secretKey, cipherAlgo, ver.getChainingMode(), vec, Cipher.DECRYPT_MODE); + byte hmacKey[] = cipher.doFinal(header.getEncryptedHmacKey()); + hmacKey = getBlock0(hmacKey, hashAlgo.hashSize); + + /** + * 5. Generate an HMAC, as specified in [RFC2104], of the encrypted form of the data (message), + * which the DataIntegrity element will verify by using the Salt generated in step 2 as the key. + * Note that the entire EncryptedPackage stream (1), including the StreamSize field, MUST be + * used as the message. + * 6. Encrypt the HMAC as in step 3 by using a blockKey byte array consisting of the following bytes: + * 0xa0, 0x67, 0x7f, 0x02, 0xb2, 0x2c, 0x84, and 0x33. + * 7. Assign the encryptedHmacValue attribute to the base64-encoded form of the result of step 6. + */ + vec = CryptoFunctions.generateIv(hashAlgo, header.getKeySalt(), kIntegrityValueBlock, blockSize); + cipher = getCipher(secretKey, cipherAlgo, ver.getChainingMode(), vec, Cipher.DECRYPT_MODE); + byte hmacValue[] = cipher.doFinal(header.getEncryptedHmacValue()); + hmacValue = getBlock0(hmacValue, hashAlgo.hashSize); + + if (Arrays.equals(verifierHashDec, verifierHash)) { + setSecretKey(secretKey); + setIntegrityHmacKey(hmacKey); + setIntegrityHmacValue(hmacValue); + return true; + } else { + return false; + } + } + + /** + * instead of a password, it's also possible to decrypt via certificate. + * Warning: this code is experimental and hasn't been validated + * + * {@linkplain http://social.msdn.microsoft.com/Forums/en-US/cc9092bb-0c82-4b5b-ae21-abf643bdb37c/agile-encryption-with-certificates} + * + * @param keyPair + * @param x509 + * @return + * @throws GeneralSecurityException + */ + public boolean verifyPassword(KeyPair keyPair, X509Certificate x509) throws GeneralSecurityException { + AgileEncryptionVerifier ver = builder.getVerifier(); + AgileEncryptionHeader header = builder.getHeader(); + HashAlgorithm hashAlgo = header.getHashAlgorithmEx(); + CipherAlgorithm cipherAlgo = header.getCipherAlgorithm(); + int blockSize = header.getBlockSize(); + + AgileCertificateEntry ace = null; + for (AgileCertificateEntry aceEntry : ver.getCertificates()) { + if (x509.equals(aceEntry.x509)) { + ace = aceEntry; + break; + } + } + if (ace == null) return false; + + Cipher cipher = Cipher.getInstance("RSA"); + cipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate()); + byte keyspec[] = cipher.doFinal(ace.encryptedKey); + SecretKeySpec secretKey = new SecretKeySpec(keyspec, ver.getCipherAlgorithm().jceId); + + Mac x509Hmac = CryptoFunctions.getMac(hashAlgo); + x509Hmac.init(secretKey); + byte certVerifier[] = x509Hmac.doFinal(ace.x509.getEncoded()); + + byte vec[] = CryptoFunctions.generateIv(hashAlgo, header.getKeySalt(), kIntegrityKeyBlock, blockSize); + cipher = getCipher(secretKey, cipherAlgo, ver.getChainingMode(), vec, Cipher.DECRYPT_MODE); + byte hmacKey[] = cipher.doFinal(header.getEncryptedHmacKey()); + hmacKey = getBlock0(hmacKey, hashAlgo.hashSize); + + vec = CryptoFunctions.generateIv(hashAlgo, header.getKeySalt(), kIntegrityValueBlock, blockSize); + cipher = getCipher(secretKey, cipherAlgo, ver.getChainingMode(), vec, Cipher.DECRYPT_MODE); + byte hmacValue[] = cipher.doFinal(header.getEncryptedHmacValue()); + hmacValue = getBlock0(hmacValue, hashAlgo.hashSize); + + + if (Arrays.equals(ace.certVerifier, certVerifier)) { + setSecretKey(secretKey); + setIntegrityHmacKey(hmacKey); + setIntegrityHmacValue(hmacValue); + return true; + } else { + return false; + } + } + + protected static int getNextBlockSize(int inputLen, int blockSize) { + int fillSize; + for (fillSize=blockSize; fillSize 0) { + if (_chunk == null) { + try { + _chunk = nextChunk(); + } catch (GeneralSecurityException e) { + throw new EncryptedDocumentException(e.getMessage()); + } + } + int count = (int)(4096L - (_pos & 0xfff)); + int avail = available(); + if (avail == 0) { + return total; + } + count = Math.min(avail, 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, 0, index); + EncryptionHeader header = info.getHeader(); + byte[] iv = generateIv(header.getHashAlgorithmEx(), header.getKeySalt(), blockKey, getBlockSizeInBytes()); + _cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), new IvParameterSpec(iv)); + if (_lastIndex != index) + _stream.skip((index - _lastIndex) << 12); + + byte[] block = new byte[Math.min(_stream.available(), 4096)]; + _stream.read(block); + _lastIndex = index + 1; + return _cipher.doFinal(block); + } + } + + protected int getBlockSizeInBytes() { + return info.getHeader().getBlockSize(); + } + + protected int getKeySizeInBytes() { + return info.getHeader().getKeySize()/8; + } +} diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptionHeader.java b/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptionHeader.java new file mode 100644 index 0000000000..965207ea47 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptionHeader.java @@ -0,0 +1,130 @@ +/* ==================================================================== + 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.agile; + +import java.io.IOException; + +import org.apache.poi.EncryptedDocumentException; +import org.apache.poi.poifs.crypt.ChainingMode; +import org.apache.poi.poifs.crypt.CipherAlgorithm; +import org.apache.poi.poifs.crypt.EncryptionHeader; +import org.apache.poi.poifs.crypt.HashAlgorithm; +import org.apache.xmlbeans.XmlException; + +import com.microsoft.schemas.office.x2006.encryption.CTDataIntegrity; +import com.microsoft.schemas.office.x2006.encryption.CTKeyData; +import com.microsoft.schemas.office.x2006.encryption.EncryptionDocument; +import com.microsoft.schemas.office.x2006.encryption.STCipherChaining; + +public class AgileEncryptionHeader extends EncryptionHeader { + private byte encryptedHmacKey[], encryptedHmacValue[]; + + public AgileEncryptionHeader(String descriptor) throws IOException { + EncryptionDocument ed; + try { + ed = EncryptionDocument.Factory.parse(descriptor); + } catch (XmlException e) { + throw new EncryptedDocumentException("Unable to parse encryption descriptor", e); + } + + CTKeyData keyData; + try { + keyData = ed.getEncryption().getKeyData(); + if (keyData == null) { + throw new NullPointerException("keyData not set"); + } + } catch (Exception e) { + throw new EncryptedDocumentException("Unable to parse keyData"); + } + + setKeySize((int)keyData.getKeyBits()); + setFlags(0); + setSizeExtra(0); + setCspName(null); + setBlockSize(keyData.getBlockSize()); + + int keyBits = (int)keyData.getKeyBits(); + + CipherAlgorithm ca = CipherAlgorithm.fromXmlId(keyData.getCipherAlgorithm().toString(), keyBits); + setCipherAlgorithm(ca); + setCipherProvider(ca.provider); + + switch (keyData.getCipherChaining().intValue()) { + case STCipherChaining.INT_CHAINING_MODE_CBC: + setChainingMode(ChainingMode.cbc); + break; + case STCipherChaining.INT_CHAINING_MODE_CFB: + setChainingMode(ChainingMode.cfb); + break; + default: + throw new EncryptedDocumentException("Unsupported chaining mode - "+keyData.getCipherChaining().toString()); + } + + int hashSize = keyData.getHashSize(); + + HashAlgorithm ha = HashAlgorithm.fromEcmaId(keyData.getHashAlgorithm().toString()); + setHashAlgorithm(ha); + + if (getHashAlgorithmEx().hashSize != hashSize) { + throw new EncryptedDocumentException("Unsupported hash algorithm: " + + keyData.getHashAlgorithm() + " @ " + hashSize + " bytes"); + } + + int saltLength = keyData.getSaltSize(); + setKeySalt(keyData.getSaltValue()); + if (getKeySalt().length != saltLength) { + throw new EncryptedDocumentException("Invalid salt length"); + } + + CTDataIntegrity di = ed.getEncryption().getDataIntegrity(); + setEncryptedHmacKey(di.getEncryptedHmacKey()); + setEncryptedHmacValue(di.getEncryptedHmacValue()); + } + + + public AgileEncryptionHeader(CipherAlgorithm algorithm, HashAlgorithm hashAlgorithm, int keyBits, int blockSize, ChainingMode chainingMode) { + setCipherAlgorithm(algorithm); + setHashAlgorithm(hashAlgorithm); + setKeySize(keyBits); + setBlockSize(blockSize); + setChainingMode(chainingMode); + } + + // make method visible for this package + protected void setKeySalt(byte salt[]) { + if (salt == null || salt.length != getBlockSize()) { + throw new EncryptedDocumentException("invalid verifier salt"); + } + super.setKeySalt(salt); + } + + public byte[] getEncryptedHmacKey() { + return encryptedHmacKey; + } + + protected void setEncryptedHmacKey(byte[] encryptedHmacKey) { + this.encryptedHmacKey = encryptedHmacKey; + } + + public byte[] getEncryptedHmacValue() { + return encryptedHmacValue; + } + + protected void setEncryptedHmacValue(byte[] encryptedHmacValue) { + this.encryptedHmacValue = encryptedHmacValue; + } +} diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptionInfoBuilder.java b/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptionInfoBuilder.java new file mode 100644 index 0000000000..12a74620bc --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptionInfoBuilder.java @@ -0,0 +1,111 @@ +/* ==================================================================== + 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.agile; + +import java.io.IOException; + +import org.apache.poi.EncryptedDocumentException; +import org.apache.poi.poifs.crypt.ChainingMode; +import org.apache.poi.poifs.crypt.CipherAlgorithm; +import org.apache.poi.poifs.crypt.EncryptionInfo; +import org.apache.poi.poifs.crypt.EncryptionInfoBuilder; +import org.apache.poi.poifs.crypt.HashAlgorithm; +import org.apache.poi.poifs.filesystem.DocumentInputStream; + +public class AgileEncryptionInfoBuilder implements EncryptionInfoBuilder { + + EncryptionInfo info; + AgileEncryptionHeader header; + AgileEncryptionVerifier verifier; + AgileDecryptor decryptor; + AgileEncryptor encryptor; + + public void initialize(EncryptionInfo info, DocumentInputStream dis) throws IOException { + this.info = info; + + 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 AgileEncryptionHeader(descriptor); + verifier = new AgileEncryptionVerifier(descriptor); + if (info.getVersionMajor() == 4 && info.getVersionMinor() == 4) { + decryptor = new AgileDecryptor(this); + } + } + + public void initialize(EncryptionInfo info, CipherAlgorithm cipherAlgorithm, HashAlgorithm hashAlgorithm, int keyBits, int blockSize, ChainingMode chainingMode) { + this.info = info; + + if (cipherAlgorithm == null) { + cipherAlgorithm = CipherAlgorithm.aes128; + } + if (cipherAlgorithm == CipherAlgorithm.rc4) { + throw new EncryptedDocumentException("RC4 must not be used with agile encryption."); + } + if (hashAlgorithm == null) { + hashAlgorithm = HashAlgorithm.sha1; + } + if (chainingMode == null) { + chainingMode = ChainingMode.cbc; + } + if (!(chainingMode == ChainingMode.cbc || chainingMode == ChainingMode.cfb)) { + throw new EncryptedDocumentException("Agile encryption only supports CBC/CFB chaining."); + } + if (keyBits == -1) { + keyBits = cipherAlgorithm.defaultKeySize; + } + if (blockSize == -1) { + blockSize = cipherAlgorithm.blockSize; + } + boolean found = false; + for (int ks : cipherAlgorithm.allowedKeySize) { + found |= (ks == keyBits); + } + if (!found) { + throw new EncryptedDocumentException("KeySize "+keyBits+" not allowed for Cipher "+cipherAlgorithm.toString()); + } + header = new AgileEncryptionHeader(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode); + verifier = new AgileEncryptionVerifier(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode); + decryptor = new AgileDecryptor(this); + encryptor = new AgileEncryptor(this); + } + + public AgileEncryptionHeader getHeader() { + return header; + } + + public AgileEncryptionVerifier getVerifier() { + return verifier; + } + + public AgileDecryptor getDecryptor() { + return decryptor; + } + + public AgileEncryptor getEncryptor() { + return encryptor; + } + + protected EncryptionInfo getInfo() { + return info; + } + + +} diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptionVerifier.java b/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptionVerifier.java new file mode 100644 index 0000000000..b3d2494c20 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptionVerifier.java @@ -0,0 +1,164 @@ +/* ==================================================================== + 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.agile; + +import java.io.ByteArrayInputStream; +import java.security.GeneralSecurityException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.apache.poi.EncryptedDocumentException; +import org.apache.poi.poifs.crypt.ChainingMode; +import org.apache.poi.poifs.crypt.CipherAlgorithm; +import org.apache.poi.poifs.crypt.EncryptionVerifier; +import org.apache.poi.poifs.crypt.HashAlgorithm; +import org.apache.xmlbeans.XmlException; + +import com.microsoft.schemas.office.x2006.encryption.CTKeyEncryptor; +import com.microsoft.schemas.office.x2006.encryption.EncryptionDocument; +import com.microsoft.schemas.office.x2006.encryption.STCipherChaining; +import com.microsoft.schemas.office.x2006.keyEncryptor.certificate.CTCertificateKeyEncryptor; +import com.microsoft.schemas.office.x2006.keyEncryptor.password.CTPasswordKeyEncryptor; + +/** + * Used when checking if a key is valid for a document + */ +public class AgileEncryptionVerifier extends EncryptionVerifier { + + public static class AgileCertificateEntry { + X509Certificate x509; + byte encryptedKey[]; + byte certVerifier[]; + } + + private List certList = new ArrayList(); + + + public AgileEncryptionVerifier(String descriptor) { + EncryptionDocument ed; + try { + ed = EncryptionDocument.Factory.parse(descriptor); + } catch (XmlException e) { + throw new EncryptedDocumentException("Unable to parse encryption descriptor", e); + } + + Iterator encList = ed.getEncryption().getKeyEncryptors().getKeyEncryptorList().iterator(); + CTPasswordKeyEncryptor keyData; + try { + keyData = encList.next().getEncryptedPasswordKey(); + if (keyData == null) { + throw new NullPointerException("encryptedKey not set"); + } + } catch (Exception e) { + throw new EncryptedDocumentException("Unable to parse keyData", e); + } + + int keyBits = (int)keyData.getKeyBits(); + + CipherAlgorithm ca = CipherAlgorithm.fromXmlId(keyData.getCipherAlgorithm().toString(), keyBits); + setCipherAlgorithm(ca); + + int hashSize = keyData.getHashSize(); + + HashAlgorithm ha = HashAlgorithm.fromEcmaId(keyData.getHashAlgorithm().toString()); + setHashAlgorithm(ha); + + if (getHashAlgorithm().hashSize != hashSize) { + throw new EncryptedDocumentException("Unsupported hash algorithm: " + + keyData.getHashAlgorithm() + " @ " + hashSize + " bytes"); + } + + setSpinCount(keyData.getSpinCount()); + setEncryptedVerifier(keyData.getEncryptedVerifierHashInput()); + setSalt(keyData.getSaltValue()); + setEncryptedKey(keyData.getEncryptedKeyValue()); + setEncryptedVerifierHash(keyData.getEncryptedVerifierHashValue()); + + int saltSize = keyData.getSaltSize(); + if (saltSize != getSalt().length) + throw new EncryptedDocumentException("Invalid salt size"); + + switch (keyData.getCipherChaining().intValue()) { + case STCipherChaining.INT_CHAINING_MODE_CBC: + setChainingMode(ChainingMode.cbc); + break; + case STCipherChaining.INT_CHAINING_MODE_CFB: + setChainingMode(ChainingMode.cfb); + break; + default: + throw new EncryptedDocumentException("Unsupported chaining mode - "+keyData.getCipherChaining().toString()); + } + + if (!encList.hasNext()) return; + + try { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + while (encList.hasNext()) { + CTCertificateKeyEncryptor certKey = encList.next().getEncryptedCertificateKey(); + AgileCertificateEntry ace = new AgileCertificateEntry(); + ace.certVerifier = certKey.getCertVerifier(); + ace.encryptedKey = certKey.getEncryptedKeyValue(); + ace.x509 = (X509Certificate)cf.generateCertificate(new ByteArrayInputStream(certKey.getX509Certificate())); + certList.add(ace); + } + } catch (GeneralSecurityException e) { + throw new EncryptedDocumentException("can't parse X509 certificate", e); + } + } + + public AgileEncryptionVerifier(CipherAlgorithm cipherAlgorithm, HashAlgorithm hashAlgorithm, int keyBits, int blockSize, ChainingMode chainingMode) { + setCipherAlgorithm(cipherAlgorithm); + setHashAlgorithm(hashAlgorithm); + setChainingMode(chainingMode); + setSpinCount(100000); // TODO: use parameter + } + + protected void setSalt(byte salt[]) { + if (salt == null || salt.length != getCipherAlgorithm().blockSize) { + throw new EncryptedDocumentException("invalid verifier salt"); + } + super.setSalt(salt); + } + + // make method visible for this package + protected void setEncryptedVerifier(byte encryptedVerifier[]) { + super.setEncryptedVerifier(encryptedVerifier); + } + + // make method visible for this package + protected void setEncryptedVerifierHash(byte encryptedVerifierHash[]) { + super.setEncryptedVerifierHash(encryptedVerifierHash); + } + + // make method visible for this package + protected void setEncryptedKey(byte[] encryptedKey) { + super.setEncryptedKey(encryptedKey); + } + + public void addCertificate(X509Certificate x509) { + AgileCertificateEntry ace = new AgileCertificateEntry(); + ace.x509 = x509; + certList.add(ace); + } + + public List getCertificates() { + return certList; + } +} diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptor.java b/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptor.java new file mode 100644 index 0000000000..558d07ec88 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptor.java @@ -0,0 +1,511 @@ +/* ==================================================================== + 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.agile; + +import static org.apache.poi.poifs.crypt.CryptoFunctions.generateIv; +import static org.apache.poi.poifs.crypt.CryptoFunctions.getBlock0; +import static org.apache.poi.poifs.crypt.CryptoFunctions.getCipher; +import static org.apache.poi.poifs.crypt.CryptoFunctions.getMessageDigest; +import static org.apache.poi.poifs.crypt.CryptoFunctions.hashPassword; +import static org.apache.poi.poifs.crypt.agile.AgileDecryptor.getNextBlockSize; +import static org.apache.poi.poifs.crypt.agile.AgileDecryptor.hashInput; +import static org.apache.poi.poifs.crypt.agile.AgileDecryptor.kCryptoKeyBlock; +import static org.apache.poi.poifs.crypt.agile.AgileDecryptor.kHashedVerifierBlock; +import static org.apache.poi.poifs.crypt.agile.AgileDecryptor.kIntegrityKeyBlock; +import static org.apache.poi.poifs.crypt.agile.AgileDecryptor.kIntegrityValueBlock; +import static org.apache.poi.poifs.crypt.agile.AgileDecryptor.kVerifierInputBlock; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.GeneralSecurityException; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.security.cert.CertificateEncodingException; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; + +import javax.crypto.Cipher; +import javax.crypto.Mac; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +import org.apache.poi.EncryptedDocumentException; +import org.apache.poi.poifs.crypt.CryptoFunctions; +import org.apache.poi.poifs.crypt.DataSpaceMapUtils; +import org.apache.poi.poifs.crypt.EncryptionHeader; +import org.apache.poi.poifs.crypt.EncryptionInfo; +import org.apache.poi.poifs.crypt.Encryptor; +import org.apache.poi.poifs.crypt.HashAlgorithm; +import org.apache.poi.poifs.crypt.agile.AgileEncryptionVerifier.AgileCertificateEntry; +import org.apache.poi.poifs.filesystem.DirectoryNode; +import org.apache.poi.poifs.filesystem.POIFSWriterEvent; +import org.apache.poi.poifs.filesystem.POIFSWriterListener; +import org.apache.poi.util.IOUtils; +import org.apache.poi.util.LittleEndian; +import org.apache.poi.util.LittleEndianByteArrayOutputStream; +import org.apache.poi.util.LittleEndianConsts; +import org.apache.poi.util.LittleEndianOutputStream; +import org.apache.poi.util.TempFile; +import org.apache.xmlbeans.XmlOptions; + +import com.microsoft.schemas.office.x2006.encryption.CTDataIntegrity; +import com.microsoft.schemas.office.x2006.encryption.CTEncryption; +import com.microsoft.schemas.office.x2006.encryption.CTKeyData; +import com.microsoft.schemas.office.x2006.encryption.CTKeyEncryptor; +import com.microsoft.schemas.office.x2006.encryption.CTKeyEncryptors; +import com.microsoft.schemas.office.x2006.encryption.EncryptionDocument; +import com.microsoft.schemas.office.x2006.encryption.STCipherAlgorithm; +import com.microsoft.schemas.office.x2006.encryption.STCipherChaining; +import com.microsoft.schemas.office.x2006.encryption.STHashAlgorithm; +import com.microsoft.schemas.office.x2006.keyEncryptor.certificate.CTCertificateKeyEncryptor; +import com.microsoft.schemas.office.x2006.keyEncryptor.password.CTPasswordKeyEncryptor; + +public class AgileEncryptor extends Encryptor { + private final AgileEncryptionInfoBuilder builder; + @SuppressWarnings("unused") + private byte integritySalt[]; + private Mac integrityMD; + private byte pwHash[]; + + protected AgileEncryptor(AgileEncryptionInfoBuilder builder) { + this.builder = builder; + } + + public void confirmPassword(String password) { + // see [MS-OFFCRYPTO] - 2.3.3 EncryptionVerifier + Random r = new SecureRandom(); + int blockSize = builder.getHeader().getBlockSize(); + int keySize = builder.getHeader().getKeySize()/8; + int hashSize = builder.getHeader().getHashAlgorithmEx().hashSize; + + byte[] verifierSalt = new byte[blockSize] + , verifier = new byte[blockSize] + , keySalt = new byte[blockSize] + , keySpec = new byte[keySize] + , integritySalt = new byte[hashSize]; + r.nextBytes(verifierSalt); // blocksize + r.nextBytes(verifier); // blocksize + r.nextBytes(keySalt); // blocksize + r.nextBytes(keySpec); // keysize + r.nextBytes(integritySalt); // hashsize + + confirmPassword(password, keySpec, keySalt, verifierSalt, verifier, integritySalt); + } + + public void confirmPassword(String password, byte keySpec[], byte keySalt[], byte verifier[], byte verifierSalt[], byte integritySalt[]) { + AgileEncryptionVerifier ver = builder.getVerifier(); + ver.setSalt(verifierSalt); + AgileEncryptionHeader header = builder.getHeader(); + header.setKeySalt(keySalt); + HashAlgorithm hashAlgo = ver.getHashAlgorithm(); + + int blockSize = header.getBlockSize(); + + pwHash = hashPassword(password, hashAlgo, verifierSalt, ver.getSpinCount()); + + /** + * encryptedVerifierHashInput: This attribute MUST be generated by using the following steps: + * 1. Generate a random array of bytes with the number of bytes used specified by the saltSize + * attribute. + * 2. Generate an encryption key as specified in section 2.3.4.11 by using the user-supplied password, + * the binary byte array used to create the saltValue attribute, and a blockKey byte array + * consisting of the following bytes: 0xfe, 0xa7, 0xd2, 0x76, 0x3b, 0x4b, 0x9e, and 0x79. + * 3. Encrypt the random array of bytes generated in step 1 by using the binary form of the saltValue + * attribute as an initialization vector as specified in section 2.3.4.12. If the array of bytes is not an + * integral multiple of blockSize bytes, pad the array with 0x00 to the next integral multiple of + * blockSize bytes. + * 4. Use base64 to encode the result of step 3. + */ + byte encryptedVerifier[] = hashInput(builder, pwHash, kVerifierInputBlock, verifier, Cipher.ENCRYPT_MODE); + ver.setEncryptedVerifier(encryptedVerifier); + + + /** + * encryptedVerifierHashValue: This attribute MUST be generated by using the following steps: + * 1. Obtain the hash value of the random array of bytes generated in step 1 of the steps for + * encryptedVerifierHashInput. + * 2. Generate an encryption key as specified in section 2.3.4.11 by using the user-supplied password, + * the binary byte array used to create the saltValue attribute, and a blockKey byte array + * consisting of the following bytes: 0xd7, 0xaa, 0x0f, 0x6d, 0x30, 0x61, 0x34, and 0x4e. + * 3. Encrypt the hash value obtained in step 1 by using the binary form of the saltValue attribute as + * an initialization vector as specified in section 2.3.4.12. If hashSize is not an integral multiple of + * blockSize bytes, pad the hash value with 0x00 to an integral multiple of blockSize bytes. + * 4. Use base64 to encode the result of step 3. + */ + MessageDigest hashMD = getMessageDigest(hashAlgo); + byte[] hashedVerifier = hashMD.digest(verifier); + byte encryptedVerifierHash[] = hashInput(builder, pwHash, kHashedVerifierBlock, hashedVerifier, Cipher.ENCRYPT_MODE); + ver.setEncryptedVerifierHash(encryptedVerifierHash); + + /** + * encryptedKeyValue: This attribute MUST be generated by using the following steps: + * 1. Generate a random array of bytes that is the same size as specified by the + * Encryptor.KeyData.keyBits attribute of the parent element. + * 2. Generate an encryption key as specified in section 2.3.4.11, using the user-supplied password, + * the binary byte array used to create the saltValue attribute, and a blockKey byte array + * consisting of the following bytes: 0x14, 0x6e, 0x0b, 0xe7, 0xab, 0xac, 0xd0, and 0xd6. + * 3. Encrypt the random array of bytes generated in step 1 by using the binary form of the saltValue + * attribute as an initialization vector as specified in section 2.3.4.12. If the array of bytes is not an + * integral multiple of blockSize bytes, pad the array with 0x00 to an integral multiple of + * blockSize bytes. + * 4. Use base64 to encode the result of step 3. + */ + byte encryptedKey[] = hashInput(builder, pwHash, kCryptoKeyBlock, keySpec, Cipher.ENCRYPT_MODE); + ver.setEncryptedKey(encryptedKey); + + SecretKey secretKey = new SecretKeySpec(keySpec, ver.getCipherAlgorithm().jceId); + setSecretKey(secretKey); + + /* + * 2.3.4.14 DataIntegrity Generation (Agile Encryption) + * + * The DataIntegrity element contained within an Encryption element MUST be generated by using + * the following steps: + * 1. Obtain the intermediate key by decrypting the encryptedKeyValue from a KeyEncryptor + * contained within the KeyEncryptors sequence. Use this key for encryption operations in the + * remaining steps of this section. + * 2. Generate a random array of bytes, known as Salt, of the same length as the value of the + * KeyData.hashSize attribute. + * 3. Encrypt the random array of bytes generated in step 2 by using the binary form of the + * KeyData.saltValue attribute and a blockKey byte array consisting of the following bytes: + * 0x5f, 0xb2, 0xad, 0x01, 0x0c, 0xb9, 0xe1, and 0xf6 used to form an initialization vector as + * specified in section 2.3.4.12. If the array of bytes is not an integral multiple of blockSize + * bytes, pad the array with 0x00 to the next integral multiple of blockSize bytes. + * 4. Assign the encryptedHmacKey attribute to the base64-encoded form of the result of step 3. + * 5. Generate an HMAC, as specified in [RFC2104], of the encrypted form of the data (message), + * which the DataIntegrity element will verify by using the Salt generated in step 2 as the key. + * Note that the entire EncryptedPackage stream (1), including the StreamSize field, MUST be + * used as the message. + * 6. Encrypt the HMAC as in step 3 by using a blockKey byte array consisting of the following bytes: + * 0xa0, 0x67, 0x7f, 0x02, 0xb2, 0x2c, 0x84, and 0x33. + * 7. Assign the encryptedHmacValue attribute to the base64-encoded form of the result of step 6. + */ + this.integritySalt = integritySalt; + + try { + byte vec[] = CryptoFunctions.generateIv(hashAlgo, header.getKeySalt(), kIntegrityKeyBlock, header.getBlockSize()); + Cipher cipher = getCipher(secretKey, ver.getCipherAlgorithm(), ver.getChainingMode(), vec, Cipher.ENCRYPT_MODE); + byte filledSalt[] = getBlock0(integritySalt, getNextBlockSize(integritySalt.length, blockSize)); + byte encryptedHmacKey[] = cipher.doFinal(filledSalt); + header.setEncryptedHmacKey(encryptedHmacKey); + + this.integrityMD = CryptoFunctions.getMac(hashAlgo); + this.integrityMD.init(new SecretKeySpec(integritySalt, hashAlgo.jceHmacId)); + + + cipher = Cipher.getInstance("RSA"); + for (AgileCertificateEntry ace : ver.getCertificates()) { + cipher.init(Cipher.ENCRYPT_MODE, ace.x509.getPublicKey()); + ace.encryptedKey = cipher.doFinal(getSecretKey().getEncoded()); + Mac x509Hmac = CryptoFunctions.getMac(hashAlgo); + x509Hmac.init(getSecretKey()); + ace.certVerifier = x509Hmac.doFinal(ace.x509.getEncoded()); + } + } catch (GeneralSecurityException e) { + throw new EncryptedDocumentException(e); + } + } + + public OutputStream getDataStream(DirectoryNode dir) + throws IOException, GeneralSecurityException { + // TODO: initialize headers + OutputStream countStream = new ChunkedCipherOutputStream(dir); + return countStream; + } + + /** + * 2.3.4.15 Data Encryption (Agile Encryption) + * + * The EncryptedPackage stream (1) MUST be encrypted in 4096-byte segments to facilitate nearly + * random access while allowing CBC modes to be used in the encryption process. + * The initialization vector for the encryption process MUST be obtained by using the zero-based + * segment number as a blockKey and the binary form of the KeyData.saltValue as specified in + * section 2.3.4.12. The block number MUST be represented as a 32-bit unsigned integer. + * Data blocks MUST then be encrypted by using the initialization vector and the intermediate key + * obtained by decrypting the encryptedKeyValue from a KeyEncryptor contained within the + * KeyEncryptors sequence as specified in section 2.3.4.10. The final data block MUST be padded to + * the next integral multiple of the KeyData.blockSize value. Any padding bytes can be used. Note + * that the StreamSize field of the EncryptedPackage field specifies the number of bytes of + * unencrypted data as specified in section 2.3.4.4. + */ + private class ChunkedCipherOutputStream extends FilterOutputStream implements POIFSWriterListener { + private long _pos = 0; + private final byte[] _chunk = new byte[4096]; + private Cipher _cipher; + private final File fileOut; + protected final DirectoryNode dir; + + public ChunkedCipherOutputStream(DirectoryNode dir) throws IOException { + super(null); + fileOut = TempFile.createTempFile("encrypted_package", "crypt"); + this.out = new FileOutputStream(fileOut); + this.dir = dir; + EncryptionHeader header = builder.getHeader(); + _cipher = getCipher(getSecretKey(), header.getCipherAlgorithm(), header.getChainingMode(), null, Cipher.ENCRYPT_MODE); + } + + public void write(int b) throws IOException { + write(new byte[]{(byte)b}); + } + + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + + public void write(byte[] b, int off, int len) + throws IOException { + if (len == 0) return; + + if (len < 0 || b.length < off+len) { + throw new IOException("not enough bytes in your input buffer"); + } + + while (len > 0) { + int posInChunk = (int)(_pos & 0xfff); + int nextLen = Math.min(4096-posInChunk, len); + System.arraycopy(b, off, _chunk, posInChunk, nextLen); + _pos += nextLen; + off += nextLen; + len -= nextLen; + if ((_pos & 0xfff) == 0) { + writeChunk(); + } + } + } + + private void writeChunk() throws IOException { + EncryptionHeader header = builder.getHeader(); + int blockSize = header.getBlockSize(); + + int posInChunk = (int)(_pos & 0xfff); + // normally posInChunk is 0, i.e. on the next chunk (-> index-1) + // but if called on close(), posInChunk is somewhere within the chunk data + int index = (int)(_pos >> 12); + if (posInChunk==0) { + index--; + posInChunk = 4096; + } else { + // pad the last chunk + _cipher = getCipher(getSecretKey(), header.getCipherAlgorithm(), header.getChainingMode(), null, Cipher.ENCRYPT_MODE, "PKCS5Padding"); + } + + byte[] blockKey = new byte[4]; + LittleEndian.putInt(blockKey, 0, index); + byte[] iv = generateIv(header.getHashAlgorithmEx(), header.getKeySalt(), blockKey, blockSize); + try { + _cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(), new IvParameterSpec(iv)); + int ciLen = _cipher.doFinal(_chunk, 0, posInChunk, _chunk); + out.write(_chunk, 0, ciLen); + } catch (GeneralSecurityException e) { + throw new IOException(e); + } + } + + public void close() throws IOException { + writeChunk(); + super.close(); + writeToPOIFS(); + } + + void writeToPOIFS() throws IOException { + DataSpaceMapUtils.addDefaultDataSpace(dir); + + /** + * Generate an HMAC, as specified in [RFC2104], of the encrypted form of the data (message), + * which the DataIntegrity element will verify by using the Salt generated in step 2 as the key. + * Note that the entire EncryptedPackage stream (1), including the StreamSize field, MUST be + * used as the message. + * + * Encrypt the HMAC as in step 3 by using a blockKey byte array consisting of the following bytes: + * 0xa0, 0x67, 0x7f, 0x02, 0xb2, 0x2c, 0x84, and 0x33. + **/ + byte buf[] = new byte[4096]; + LittleEndian.putLong(buf, 0, _pos); + integrityMD.update(buf, 0, LittleEndianConsts.LONG_SIZE); + + InputStream fis = new FileInputStream(fileOut); + for (int readBytes; (readBytes = fis.read(buf)) != -1; integrityMD.update(buf, 0, readBytes)); + fis.close(); + + AgileEncryptionHeader header = builder.getHeader(); + int blockSize = header.getBlockSize(); + + byte hmacValue[] = integrityMD.doFinal(); + byte iv[] = CryptoFunctions.generateIv(header.getHashAlgorithmEx(), header.getKeySalt(), kIntegrityValueBlock, header.getBlockSize()); + Cipher cipher = CryptoFunctions.getCipher(getSecretKey(), header.getCipherAlgorithm(), header.getChainingMode(), iv, Cipher.ENCRYPT_MODE); + try { + byte hmacValueFilled[] = getBlock0(hmacValue, getNextBlockSize(hmacValue.length, blockSize)); + byte encryptedHmacValue[] = cipher.doFinal(hmacValueFilled); + header.setEncryptedHmacValue(encryptedHmacValue); + } catch (GeneralSecurityException e) { + throw new EncryptedDocumentException(e); + } + + createEncryptionInfoEntry(dir); + + int oleStreamSize = (int)(fileOut.length()+LittleEndianConsts.LONG_SIZE); + dir.createDocument("EncryptedPackage", oleStreamSize, this); + // TODO: any properties??? + } + + public void processPOIFSWriterEvent(POIFSWriterEvent event) { + try { + LittleEndianOutputStream leos = new LittleEndianOutputStream(event.getStream()); + + // StreamSize (8 bytes): An unsigned integer that specifies the number of bytes used by data + // encrypted within the EncryptedData field, not including the size of the StreamSize field. + // Note that the actual size of the \EncryptedPackage stream (1) can be larger than this + // value, depending on the block size of the chosen encryption algorithm + leos.writeLong(_pos); + + FileInputStream fis = new FileInputStream(fileOut); + IOUtils.copy(fis, leos); + fis.close(); + fileOut.delete(); + + leos.close(); + } catch (IOException e) { + throw new EncryptedDocumentException(e); + } + } + } + + protected void createEncryptionInfoEntry(DirectoryNode dir) throws IOException { + AgileEncryptionVerifier ver = builder.getVerifier(); + AgileEncryptionHeader header = builder.getHeader(); + + EncryptionDocument ed = EncryptionDocument.Factory.newInstance(); + CTEncryption edRoot = ed.addNewEncryption(); + + CTKeyData keyData = edRoot.addNewKeyData(); + CTKeyEncryptors keyEncList = edRoot.addNewKeyEncryptors(); + CTKeyEncryptor keyEnc = keyEncList.addNewKeyEncryptor(); + keyEnc.setUri(CTKeyEncryptor.Uri.HTTP_SCHEMAS_MICROSOFT_COM_OFFICE_2006_KEY_ENCRYPTOR_PASSWORD); + CTPasswordKeyEncryptor keyPass = keyEnc.addNewEncryptedPasswordKey(); + + keyPass.setSpinCount(ver.getSpinCount()); + + keyData.setSaltSize(header.getBlockSize()); + keyPass.setSaltSize(header.getBlockSize()); + + keyData.setBlockSize(header.getBlockSize()); + keyPass.setBlockSize(header.getBlockSize()); + + keyData.setKeyBits(header.getKeySize()); + keyPass.setKeyBits(header.getKeySize()); + + HashAlgorithm hashAlgo = header.getHashAlgorithmEx(); + keyData.setHashSize(hashAlgo.hashSize); + keyPass.setHashSize(hashAlgo.hashSize); + + STCipherAlgorithm.Enum xmlCipherAlgo = STCipherAlgorithm.Enum.forString(header.getCipherAlgorithm().xmlId); + if (xmlCipherAlgo == null) { + throw new EncryptedDocumentException("CipherAlgorithm "+header.getCipherAlgorithm()+" not supported."); + } + keyData.setCipherAlgorithm(xmlCipherAlgo); + keyPass.setCipherAlgorithm(xmlCipherAlgo); + + switch (header.getChainingMode()) { + case cbc: + keyData.setCipherChaining(STCipherChaining.CHAINING_MODE_CBC); + keyPass.setCipherChaining(STCipherChaining.CHAINING_MODE_CBC); + break; + case cfb: + keyData.setCipherChaining(STCipherChaining.CHAINING_MODE_CFB); + keyPass.setCipherChaining(STCipherChaining.CHAINING_MODE_CFB); + break; + default: + throw new EncryptedDocumentException("ChainingMode "+header.getChainingMode()+" not supported."); + } + + STHashAlgorithm.Enum xmlHashAlgo = STHashAlgorithm.Enum.forString(hashAlgo.ecmaString); + if (xmlHashAlgo == null) { + throw new EncryptedDocumentException("HashAlgorithm "+hashAlgo+" not supported."); + } + keyData.setHashAlgorithm(xmlHashAlgo); + keyPass.setHashAlgorithm(xmlHashAlgo); + + keyData.setSaltValue(header.getKeySalt()); + keyPass.setSaltValue(ver.getSalt()); + keyPass.setEncryptedVerifierHashInput(ver.getEncryptedVerifier()); + keyPass.setEncryptedVerifierHashValue(ver.getEncryptedVerifierHash()); + keyPass.setEncryptedKeyValue(ver.getEncryptedKey()); + + CTDataIntegrity hmacData = edRoot.addNewDataIntegrity(); + hmacData.setEncryptedHmacKey(header.getEncryptedHmacKey()); + hmacData.setEncryptedHmacValue(header.getEncryptedHmacValue()); + + for (AgileCertificateEntry ace : ver.getCertificates()) { + keyEnc = keyEncList.addNewKeyEncryptor(); + keyEnc.setUri(CTKeyEncryptor.Uri.HTTP_SCHEMAS_MICROSOFT_COM_OFFICE_2006_KEY_ENCRYPTOR_CERTIFICATE); + CTCertificateKeyEncryptor certData = keyEnc.addNewEncryptedCertificateKey(); + try { + certData.setX509Certificate(ace.x509.getEncoded()); + } catch (CertificateEncodingException e) { + throw new EncryptedDocumentException(e); + } + certData.setEncryptedKeyValue(ace.encryptedKey); + certData.setCertVerifier(ace.certVerifier); + } + + XmlOptions xo = new XmlOptions(); + xo.setCharacterEncoding("UTF-8"); + Map nsMap = new HashMap(); + nsMap.put("http://schemas.microsoft.com/office/2006/keyEncryptor/password","p"); + nsMap.put("http://schemas.microsoft.com/office/2006/keyEncryptor/certificate", "c"); + nsMap.put("http://schemas.microsoft.com/office/2006/encryption",""); + xo.setSaveSuggestedPrefixes(nsMap); + xo.setSaveNamespacesFirst(); + xo.setSaveAggressiveNamespaces(); + // setting standalone doesn't work with xmlbeans-2.3 + xo.setSaveNoXmlDecl(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + bos.write("\r\n".getBytes("UTF-8")); + ed.save(bos, xo); + + final byte buf[] = new byte[5000]; + LittleEndianByteArrayOutputStream leos = new LittleEndianByteArrayOutputStream(buf, 0); + EncryptionInfo info = builder.getInfo(); + + // EncryptionVersionInfo (4 bytes): A Version structure (section 2.1.4), where + // Version.vMajor MUST be 0x0004 and Version.vMinor MUST be 0x0004 + leos.writeShort(info.getVersionMajor()); + leos.writeShort(info.getVersionMinor()); + // Reserved (4 bytes): A value that MUST be 0x00000040 + leos.writeInt(0x40); + leos.write(bos.toByteArray()); + + dir.createDocument("EncryptionInfo", leos.getWriteIndex(), new POIFSWriterListener() { + public void processPOIFSWriterEvent(POIFSWriterEvent event) { + try { + event.getStream().write(buf, 0, event.getLimit()); + } catch (IOException e) { + throw new EncryptedDocumentException(e); + } + } + }); + } +} diff --git a/src/ooxml/java/org/apache/poi/util/OOXMLLite.java b/src/ooxml/java/org/apache/poi/util/OOXMLLite.java index cedc906edf..f6d809dc83 100644 --- a/src/ooxml/java/org/apache/poi/util/OOXMLLite.java +++ b/src/ooxml/java/org/apache/poi/util/OOXMLLite.java @@ -23,6 +23,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Enumeration; import java.util.HashMap; @@ -32,10 +33,13 @@ import java.util.Vector; import java.util.jar.JarEntry; import java.util.jar.JarFile; +import junit.framework.JUnit4TestAdapter; import junit.framework.TestCase; import junit.framework.TestSuite; import junit.textui.TestRunner; +import org.junit.Test; + /** * Build a 'lite' version of the ooxml-schemas.jar * @@ -103,9 +107,18 @@ public final class OOXMLLite { String cls = arg.replace(".class", ""); try { - @SuppressWarnings("unchecked") - Class test = (Class) Class.forName(cls); - suite.addTestSuite(test); + Class testclass = Class.forName(cls); + boolean isTest = TestCase.class.isAssignableFrom(testclass); + if (!isTest) { + for (Method m : testclass.getDeclaredMethods()) { + isTest = m.isAnnotationPresent(Test.class); + if (isTest) break; + } + } + + if (isTest) { + suite.addTest(new JUnit4TestAdapter(testclass)); + } } catch (ClassNotFoundException e) { throw new RuntimeException(e); } @@ -181,8 +194,12 @@ public final class OOXMLLite { Vector> classes = (Vector>) _classes.get(appLoader); Map> map = new HashMap>(); for (Class cls : classes) { - String jar = cls.getProtectionDomain().getCodeSource().getLocation().toString(); - if(jar.indexOf(ptrn) != -1) map.put(cls.getName(), cls); + try { + String jar = cls.getProtectionDomain().getCodeSource().getLocation().toString(); + if(jar.indexOf(ptrn) != -1) map.put(cls.getName(), cls); + } catch (NullPointerException e) { + continue; + } } return map; } catch (IllegalAccessException e) { diff --git a/src/ooxml/resources/org/apache/poi/poifs/crypt/encryptionCertificate.xsd b/src/ooxml/resources/org/apache/poi/poifs/crypt/encryptionCertificate.xsd new file mode 100644 index 0000000000..7423c85de0 --- /dev/null +++ b/src/ooxml/resources/org/apache/poi/poifs/crypt/encryptionCertificate.xsd @@ -0,0 +1,39 @@ + + + + + + + + + + + + A base64-encoded value that specifies the encrypted form of the intermediate key, which is encrypted with the public key contained within the X509Certificate attribute. + + + A base64-encoded value that specifies a DER-encoded X.509 certificate (1) used to encrypt the intermediate key. The certificate (1) MUST contain only the public portion of the public-private key pair. + + + A base64-encoded value that specifies the HMAC of the binary data obtained by base64-decoding the X509Certificate attribute. The hashing algorithm used to derive the HMAC MUST be the hashing algorithm specified for the Encryption.keyData element. The secret key used to derive the HMAC MUST be the intermediate key. If the intermediate key is reset, any CertificateKeyEncryptor elements are also reset to contain the new intermediate key, except that the certVerifier attribute MUST match the value calculated using the current intermediate key, to verify that the CertificateKeyEncryptor element actually encrypted the current intermediate key. If a CertificateKeyEncryptor element does not have a correct certVerifier attribute, it MUST be discarded. + + + + diff --git a/src/ooxml/resources/org/apache/poi/poifs/crypt/encryptionCertificate.xsdconfig b/src/ooxml/resources/org/apache/poi/poifs/crypt/encryptionCertificate.xsdconfig new file mode 100644 index 0000000000..73a27fa50a --- /dev/null +++ b/src/ooxml/resources/org/apache/poi/poifs/crypt/encryptionCertificate.xsdconfig @@ -0,0 +1,24 @@ + + + + + + + \ No newline at end of file diff --git a/src/ooxml/resources/org/apache/poi/poifs/crypt/encryptionInfo.xsd b/src/ooxml/resources/org/apache/poi/poifs/crypt/encryptionInfo.xsd new file mode 100644 index 0000000000..5b08560c3a --- /dev/null +++ b/src/ooxml/resources/org/apache/poi/poifs/crypt/encryptionInfo.xsd @@ -0,0 +1,259 @@ + + + + + + + + An unsigned integer that specifies the number of bytes used by a salt. It MUST be at least 1 and no greater than 65,536. + + + + + + + + + An unsigned integer that specifies the number of bytes used to encrypt one block of data. It MUST be at least 2, no greater than 4096, and a multiple of 2. + + + + + + + + + An unsigned integer that specifies the number of bits used by an encryption algorithm. It MUST be at least 8 and a multiple of 8. + + + + + + + + An unsigned integer that specifies the number of bytes used by a hash value. It MUST be at least 1, no greater than 65,536, and the same number of bytes as the hash algorithm emits. + + + + + + + + + An unsigned integer that specifies the number of times to iterate on a hash of a password. It MUST NOT be greater than 10,000,000. + + + + + + + + + modified for poi - list is restricted to given list in [ms-offcrypto] + A string that specifies the cipher algorithm. Values that are not defined MAY be used, and a compliant implementation is not required to support all defined values. Any algorithm that can be resolved by name by the underlying operating system can be used for hashing or encryption. Only block algorithms are supported for encryption. AES-128 is the default encryption algorithm, and SHA-1 is the default hashing algorithm if no other algorithms have been configured. + + + + + MUST conform to the AES algorithm. + + + + + MUST conform to the algorithm as specified in [RFC2268] (http://tools.ietf.org/html/rfc2268). The use of RC2 is not recommended. If RC2 is used with a key length of less than 128 bits, documents could interoperate incorrectly across different versions of Windows. + + + + + MUST NOT be used. + + + + + MUST conform to the DES algorithm. The use of DES is not recommended. If DES is used, the key length specified in the KeyBits element is required to be set to 64 for 56-bit encryption, and the key decrypted from encryptedKeyValue of KeyEncryptor is required to include the DES parity bits. + + + + + MUST conform to the algorithm as specified in [DRAFT-DESX] (http://tools.ietf.org/html/draft-ietf-ipsec-ciph-desx-00). The use of DESX is not recommended. If DESX is used, documents could interoperate incorrectly across different versions of Windows. + + + + + MUST conform to the algorithm as specified in [RFC1851] (http://tools.ietf.org/html/rfc1851). If 3DES or 3DES_112 is used, the key length specified in the KeyBits element is required to be set to 192 for 168-bit encryption and 128 for 112-bit encryption, and the key decrypted from encryptedKeyValue of KeyEncryptor is required to include the DES parity bits. + + + + + see 3DES + + + + + + + A string that specifies the chaining mode used by CipherAlgorithm. For more details about chaining modes, see [BCMO800-38A] (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf). + + + + + block chaining (CBC) + + + + + Cipher feedback chaining (CFB), with an 8-bit window + + + + + + + modified for poi - list is restricted to given list in [ms-offcrypto] + A string specifying a hashing algorithm. Values that are not defined MAY be used, and a compliant implementation is not required to support all defined values. + + + + + MUST conform to the algorithm as specified in [RFC4634] (http://tools.ietf.org/html/rfc4634). + + + + + see SHA1 + + + + + see SHA1 + + + + + see SHA1 + + + + + MUST conform to MD5. + + + + + MUST conform to the algorithm as specified in [RFC1320] (http://tools.ietf.org/html/rfc1320). + + + + + MUST conform to the algorithm as specified in [RFC1319] (http://tools.ietf.org/html/rfc1319). + + + + + MUST conform to the hash functions specified in [ISO/IEC 10118]. (https://en.wikipedia.org/wiki/RIPEMD) + + + + + see RIPEMD-128 (https://en.wikipedia.org/wiki/RIPEMD) + + + + + see RIPEMD-128 (https://en.wikipedia.org/wiki/ISO/IEC_10118-3) + + + + + + + A complex type that specifies the encryption used within this element. The saltValue attribute is a base64-encoded binary value that is randomly generated. The number of bytes required to decode the saltValue attribute MUST be equal to the value of the saltSize attribute. + + + + + + + + + + + + + A complex type that specifies data used to verify whether the encrypted data passes an integrity check. It MUST be generated using the method specified in section 2.3.4.14 (http://msdn.microsoft.com/en-us/library/dd924068(v=office.12).aspx). + + + + A base64-encoded value that specifies an encrypted key used in calculating the encryptedHmacValue. + + + + + A base64-encoded value that specifies an HMAC derived from encryptedHmacKey and the encrypted data. + + + + + + modified for POI + A complex type that specifies the parameters used to encrypt an intermediate key, which is used to perform the final encryption of the document. To ensure extensibility, arbitrary elements can be defined to encrypt the intermediate key. The intermediate key MUST be the same for all KeyEncryptor elements. + + + + + + + + modified for POI + + + + + + + + + + + + A sequence of KeyEncryptor elements. Exactly one KeyEncryptors element MUST be present, and the KeyEncryptors element MUST contain at least one KeyEncryptor. + + + + + + + + + + + modified for POI + All ECMA-376 documents [ECMA-376] encrypted by Microsoft Office using agile encryption will have a DataIntegrity element present. The schema allows for a DataIntegrity element to not be present because the encryption schema can be used by applications that do not create ECMA-376 documents [ECMA-376]. + + + + + The KeyEncryptor element, which MUST be used when encrypting password-protected agile encryption documents, is either a PasswordKeyEncryptor or a CertificateKeyEncryptor. Exactly one PasswordKeyEncryptor MUST be present. Zero or more CertificateKeyEncryptor elements are contained within the KeyEncryptors element. + + + + + + diff --git a/src/ooxml/resources/org/apache/poi/poifs/crypt/encryptionInfo.xsdconfig b/src/ooxml/resources/org/apache/poi/poifs/crypt/encryptionInfo.xsdconfig new file mode 100644 index 0000000000..c9474a0f3a --- /dev/null +++ b/src/ooxml/resources/org/apache/poi/poifs/crypt/encryptionInfo.xsdconfig @@ -0,0 +1,25 @@ + + + + + + + + \ No newline at end of file diff --git a/src/ooxml/resources/org/apache/poi/poifs/crypt/encryptionPassword.xsd b/src/ooxml/resources/org/apache/poi/poifs/crypt/encryptionPassword.xsd new file mode 100644 index 0000000000..79ae888a0e --- /dev/null +++ b/src/ooxml/resources/org/apache/poi/poifs/crypt/encryptionPassword.xsd @@ -0,0 +1,66 @@ + + + + + + + + + + + + A SaltSize that specifies the size of the salt for a PasswordKeyEncryptor. + + + A BlockSize that specifies the block size for a PasswordKeyEncryptor. + + + A KeyBits that specifies the number of bits for a PasswordKeyEncryptor. + + + A HashSize that specifies the size of the binary form of the hash for a PasswordKeyEncryptor. + + + A CipherAlgorithm that specifies the cipher algorithm for a PasswordKeyEncryptor. The cipher algorithm specified MUST be the same as the cipher algorithm specified for the Encryption.keyData element. + + + A CipherChaining that specifies the cipher chaining mode for a PasswordKeyEncryptor. + + + A HashAlgorithm that specifies the hashing algorithm for a PasswordKeyEncryptor. The hashing algorithm specified MUST be the same as the hashing algorithm specified for the Encryption.keyData element. + + + A base64-encoded binary byte array that specifies the salt value for a PasswordKeyEncryptor. The number of bytes required by the decoded form of this element MUST be saltSize. + + + A SpinCount that specifies the spin count for a PasswordKeyEncryptor. + + + A base64-encoded value that specifies the encrypted verifier hash input for a PasswordKeyEncryptor used in password verification. + + + A base64-encoded value that specifies the encrypted verifier hash value for a PasswordKeyEncryptor used in password verification. + + + A base64-encoded value that specifies the encrypted form of the intermediate key. + + + + diff --git a/src/ooxml/resources/org/apache/poi/poifs/crypt/encryptionPassword.xsdconfig b/src/ooxml/resources/org/apache/poi/poifs/crypt/encryptionPassword.xsdconfig new file mode 100644 index 0000000000..3a2bb2c8e9 --- /dev/null +++ b/src/ooxml/resources/org/apache/poi/poifs/crypt/encryptionPassword.xsdconfig @@ -0,0 +1,24 @@ + + + + + + + \ No newline at end of file diff --git a/src/testcases/org/apache/poi/poifs/crypt/AllPOIFSCryptoTests.java b/src/ooxml/testcases/org/apache/poi/poifs/crypt/AllPOIFSCryptoTests.java similarity index 76% rename from src/testcases/org/apache/poi/poifs/crypt/AllPOIFSCryptoTests.java rename to src/ooxml/testcases/org/apache/poi/poifs/crypt/AllPOIFSCryptoTests.java index d7aef1039f..fd8e56a745 100644 --- a/src/testcases/org/apache/poi/poifs/crypt/AllPOIFSCryptoTests.java +++ b/src/ooxml/testcases/org/apache/poi/poifs/crypt/AllPOIFSCryptoTests.java @@ -1,37 +1,36 @@ -/* ==================================================================== - 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; - } +/* ==================================================================== + 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 org.junit.runner.RunWith; +import org.junit.runners.Suite; + + +/** + * Tests for org.apache.poi.poifs.crypt + */ +@RunWith(Suite.class) +@Suite.SuiteClasses({ + TestEncryptionInfo.class + , TestDecryptor.class + , TestEncryptor.class + , TestAgileEncryptionParameters.class + , TestCertificateEncryption.class +}) +public final class AllPOIFSCryptoTests { } \ No newline at end of file diff --git a/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestAgileEncryptionParameters.java b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestAgileEncryptionParameters.java new file mode 100644 index 0000000000..469286606f --- /dev/null +++ b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestAgileEncryptionParameters.java @@ -0,0 +1,102 @@ +/* ==================================================================== + 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 static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.apache.poi.POIDataSamples; +import org.apache.poi.poifs.filesystem.POIFSFileSystem; +import org.apache.poi.util.IOUtils; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class TestAgileEncryptionParameters { + + static byte testData[]; + + @Parameter(value = 0) + public CipherAlgorithm ca; + @Parameter(value = 1) + public HashAlgorithm ha; + @Parameter(value = 2) + public ChainingMode cm; + + @Parameters + public static Collection data() { + CipherAlgorithm caList[] = { CipherAlgorithm.aes128, CipherAlgorithm.aes192, CipherAlgorithm.aes256, CipherAlgorithm.rc2, CipherAlgorithm.des, CipherAlgorithm.des3 }; + HashAlgorithm haList[] = { HashAlgorithm.sha1, HashAlgorithm.sha256, HashAlgorithm.sha384, HashAlgorithm.sha512, HashAlgorithm.md5 }; + ChainingMode cmList[] = { ChainingMode.cbc, ChainingMode.cfb }; + + List data = new ArrayList(); + for (CipherAlgorithm ca : caList) { + for (HashAlgorithm ha : haList) { + for (ChainingMode cm : cmList) { + data.add(new Object[]{ca,ha,cm}); + } + } + } + + return data; + } + + @BeforeClass + public static void initTestData() throws Exception { + InputStream testFile = POIDataSamples.getDocumentInstance().openResourceAsStream("SampleDoc.docx"); + testData = IOUtils.toByteArray(testFile); + testFile.close(); + } + + @Test + public void testAgileEncryptionModes() throws Exception { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + + POIFSFileSystem fsEnc = new POIFSFileSystem(); + EncryptionInfo infoEnc = new EncryptionInfo(fsEnc, EncryptionMode.agile, ca, ha, -1, -1, cm); + Encryptor enc = infoEnc.getEncryptor(); + enc.confirmPassword("foobaa"); + OutputStream os = enc.getDataStream(fsEnc); + os.write(testData); + os.close(); + bos.reset(); + fsEnc.writeFilesystem(bos); + + POIFSFileSystem fsDec = new POIFSFileSystem(new ByteArrayInputStream(bos.toByteArray())); + EncryptionInfo infoDec = new EncryptionInfo(fsDec); + Decryptor dec = infoDec.getDecryptor(); + boolean passed = dec.verifyPassword("foobaa"); + assertTrue(passed); + InputStream is = dec.getDataStream(fsDec); + byte actualData[] = IOUtils.toByteArray(is); + is.close(); + assertThat("Failed roundtrip - "+ca+"-"+ha+"-"+cm, testData, equalTo(actualData)); + } +} diff --git a/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestCertificateEncryption.java b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestCertificateEncryption.java new file mode 100644 index 0000000000..d74719cc00 --- /dev/null +++ b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestCertificateEncryption.java @@ -0,0 +1,193 @@ +/* ==================================================================== + 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 static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.Date; + +import org.apache.poi.POIDataSamples; +import org.apache.poi.poifs.crypt.agile.AgileDecryptor; +import org.apache.poi.poifs.crypt.agile.AgileEncryptionVerifier; +import org.apache.poi.poifs.filesystem.POIFSFileSystem; +import org.apache.poi.util.IOUtils; +import org.junit.BeforeClass; +import org.junit.Test; + +import sun.security.x509.AlgorithmId; +import sun.security.x509.CertificateAlgorithmId; +import sun.security.x509.CertificateIssuerName; +import sun.security.x509.CertificateSerialNumber; +import sun.security.x509.CertificateSubjectName; +import sun.security.x509.CertificateValidity; +import sun.security.x509.CertificateVersion; +import sun.security.x509.CertificateX509Key; +import sun.security.x509.X500Name; +import sun.security.x509.X509CertImpl; +import sun.security.x509.X509CertInfo; + +/** + * {@linkplain http://stackoverflow.com/questions/1615871/creating-an-x509-certificate-in-java-without-bouncycastle} + */ +public class TestCertificateEncryption { + /** + * how many days from now the Certificate is valid for + */ + static final int days = 1000; + /** + * the signing algorithm, eg "SHA1withRSA" + */ + static final String algorithm = "SHA1withRSA"; + static final String password = "foobaa"; + static final String certAlias = "poitest"; + /** + * the X.509 Distinguished Name, eg "CN=Test, L=London, C=GB" + */ + static final String certDN = "CN=poitest"; + // static final File pfxFile = TempFile.createTempFile("poitest", ".pfx"); + static byte pfxFileBytes[]; + + static class CertData { + KeyPair keypair; + X509Certificate x509; + } + + /** + * Create a self-signed X.509 Certificate + * + * The keystore generation / loading is split, because normally the keystore would + * already exist. + */ + @BeforeClass + public static void initKeystore() throws GeneralSecurityException, IOException { + CertData certData = new CertData(); + + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + keyGen.initialize(1024); + certData.keypair = keyGen.generateKeyPair(); + PrivateKey privkey = certData.keypair.getPrivate(); + PublicKey publkey = certData.keypair.getPublic(); + + X509CertInfo info = new X509CertInfo(); + Date from = new Date(); + Date to = new Date(from.getTime() + days * 86400000l); + CertificateValidity interval = new CertificateValidity(from, to); + BigInteger sn = new BigInteger(64, new SecureRandom()); + X500Name owner = new X500Name(certDN); + + info.set(X509CertInfo.VALIDITY, interval); + info.set(X509CertInfo.SERIAL_NUMBER, new CertificateSerialNumber(sn)); + info.set(X509CertInfo.SUBJECT, new CertificateSubjectName(owner)); + info.set(X509CertInfo.ISSUER, new CertificateIssuerName(owner)); + info.set(X509CertInfo.KEY, new CertificateX509Key(publkey)); + info.set(X509CertInfo.VERSION, new CertificateVersion(CertificateVersion.V3)); + AlgorithmId algo = new AlgorithmId(AlgorithmId.md5WithRSAEncryption_oid); + info.set(X509CertInfo.ALGORITHM_ID, new CertificateAlgorithmId(algo)); + + // Sign the cert to identify the algorithm that's used. + X509CertImpl cert = new X509CertImpl(info); + cert.sign(privkey, algorithm); + + // Update the algorith, and resign. + algo = (AlgorithmId)cert.get(X509CertImpl.SIG_ALG); + info.set(CertificateAlgorithmId.NAME + "." + CertificateAlgorithmId.ALGORITHM, algo); + cert = new X509CertImpl(info); + cert.sign(privkey, algorithm); + certData.x509 = cert; + + KeyStore keystore = KeyStore.getInstance("PKCS12"); + keystore.load(null, password.toCharArray()); + keystore.setKeyEntry(certAlias, certData.keypair.getPrivate(), password.toCharArray(), new Certificate[]{certData.x509}); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + keystore.store(bos, password.toCharArray()); + pfxFileBytes = bos.toByteArray(); + } + + public CertData loadKeystore() + throws GeneralSecurityException, IOException { + KeyStore keystore = KeyStore.getInstance("PKCS12"); + + InputStream fis = new ByteArrayInputStream(pfxFileBytes); + keystore.load(fis, password.toCharArray()); + + X509Certificate x509 = (X509Certificate)keystore.getCertificate(certAlias); + PrivateKey privateKey = (PrivateKey)keystore.getKey(certAlias, password.toCharArray()); + PublicKey publicKey = x509.getPublicKey(); + + CertData certData = new CertData(); + certData.keypair = new KeyPair(publicKey, privateKey); + certData.x509 = x509; + + return certData; + } + + @Test + public void testCertificateEncryption() throws Exception { + POIFSFileSystem fs = new POIFSFileSystem(); + EncryptionInfo info = new EncryptionInfo(fs, EncryptionMode.agile, CipherAlgorithm.aes192, HashAlgorithm.sha1, -1, -1, ChainingMode.cbc); + AgileEncryptionVerifier aev = (AgileEncryptionVerifier)info.getVerifier(); + CertData certData = loadKeystore(); + aev.addCertificate(certData.x509); + + Encryptor enc = info.getEncryptor(); + enc.confirmPassword("foobaa"); + + File file = POIDataSamples.getDocumentInstance().getFile("VariousPictures.docx"); + InputStream fis = new FileInputStream(file); + byte byteExpected[] = IOUtils.toByteArray(fis); + fis.close(); + + OutputStream os = enc.getDataStream(fs); + IOUtils.copy(new ByteArrayInputStream(byteExpected), os); + os.close(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + fs.writeFilesystem(bos); + bos.close(); + + fs = new POIFSFileSystem(new ByteArrayInputStream(bos.toByteArray())); + info = new EncryptionInfo(fs); + AgileDecryptor agDec = (AgileDecryptor)info.getDecryptor(); + boolean passed = agDec.verifyPassword(certData.keypair, certData.x509); + assertTrue("certificate verification failed", passed); + + fis = agDec.getDataStream(fs); + byte byteActual[] = IOUtils.toByteArray(fis); + fis.close(); + + assertThat(byteExpected, equalTo(byteActual)); + } +} diff --git a/src/testcases/org/apache/poi/poifs/crypt/TestDecryptor.java b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestDecryptor.java similarity index 97% rename from src/testcases/org/apache/poi/poifs/crypt/TestDecryptor.java rename to src/ooxml/testcases/org/apache/poi/poifs/crypt/TestDecryptor.java index dcef8d31cd..95a94c4667 100644 --- a/src/testcases/org/apache/poi/poifs/crypt/TestDecryptor.java +++ b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestDecryptor.java @@ -1,117 +1,118 @@ -/* ==================================================================== - 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.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -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()); - } - } - } - public void testDataLength() throws Exception { - POIFSFileSystem fs = new POIFSFileSystem(POIDataSamples.getPOIFSInstance().openResourceAsStream("protected_agile.docx")); - - EncryptionInfo info = new EncryptionInfo(fs); - - Decryptor d = Decryptor.getInstance(info); - - d.verifyPassword(Decryptor.DEFAULT_PASSWORD); - - InputStream is = d.getDataStream(fs); - - long len = d.getLength(); - assertEquals(12810, len); - - byte[] buf = new byte[(int)len]; - - is.read(buf); - - ZipInputStream zin = new ZipInputStream(new ByteArrayInputStream(buf)); - - while (true) { - ZipEntry entry = zin.getNextEntry(); - if (entry==null) { - break; - } - - while (zin.available()>0) { - zin.skip(zin.available()); - } - } - } - -} +/* ==================================================================== + 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.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import junit.framework.TestCase; + +import org.apache.poi.POIDataSamples; +import org.apache.poi.poifs.filesystem.POIFSFileSystem; + +/** + * @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()); + } + } + } + public void testDataLength() throws Exception { + POIFSFileSystem fs = new POIFSFileSystem(POIDataSamples.getPOIFSInstance().openResourceAsStream("protected_agile.docx")); + + EncryptionInfo info = new EncryptionInfo(fs); + + Decryptor d = Decryptor.getInstance(info); + + d.verifyPassword(Decryptor.DEFAULT_PASSWORD); + + InputStream is = d.getDataStream(fs); + + long len = d.getLength(); + assertEquals(12810, len); + + byte[] buf = new byte[(int)len]; + + is.read(buf); + + ZipInputStream zin = new ZipInputStream(new ByteArrayInputStream(buf)); + + while (true) { + ZipEntry entry = zin.getNextEntry(); + if (entry==null) { + break; + } + + while (zin.available()>0) { + zin.skip(zin.available()); + } + } + } + +} \ No newline at end of file diff --git a/src/testcases/org/apache/poi/poifs/crypt/TestEncryptionInfo.java b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestEncryptionInfo.java similarity index 71% rename from src/testcases/org/apache/poi/poifs/crypt/TestEncryptionInfo.java rename to src/ooxml/testcases/org/apache/poi/poifs/crypt/TestEncryptionInfo.java index 7869183c9f..698adb86d9 100644 --- a/src/testcases/org/apache/poi/poifs/crypt/TestEncryptionInfo.java +++ b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestEncryptionInfo.java @@ -1,60 +1,61 @@ -/* ==================================================================== - 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(32, info.getVerifier().getVerifierHash().length); - assertEquals(EncryptionHeader.PROVIDER_AES, info.getHeader().getProviderType()); - assertEquals("Microsoft Enhanced RSA and AES Cryptographic Provider", info.getHeader().getCspName()); - } - - public void testEncryptionInfoSHA512() throws Exception { - POIFSFileSystem fs = new POIFSFileSystem(POIDataSamples.getPOIFSInstance().openResourceAsStream("protected_sha512.xlsx")); - - EncryptionInfo info = new EncryptionInfo(fs); - - assertEquals(4, info.getVersionMajor()); - assertEquals(4, info.getVersionMinor()); - - 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); - assertEquals(EncryptionHeader.PROVIDER_AES, info.getHeader().getProviderType()); -// assertEquals("Microsoft Enhanced RSA and AES Cryptographic Provider", info.getHeader().getCspName()); - } -} +/* ==================================================================== + 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 static org.junit.Assert.assertEquals; + +import java.io.IOException; + +import org.apache.poi.POIDataSamples; +import org.apache.poi.poifs.filesystem.POIFSFileSystem; +import org.junit.Test; + +public class TestEncryptionInfo { + @Test + 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(CipherAlgorithm.aes128, info.getHeader().getCipherAlgorithm()); + assertEquals(HashAlgorithm.sha1, info.getHeader().getHashAlgorithmEx()); + assertEquals(128, info.getHeader().getKeySize()); + assertEquals(32, info.getVerifier().getEncryptedVerifierHash().length); + assertEquals(CipherProvider.aes, info.getHeader().getCipherProvider()); + assertEquals("Microsoft Enhanced RSA and AES Cryptographic Provider", info.getHeader().getCspName()); + } + + @Test + public void testEncryptionInfoSHA512() throws Exception { + POIFSFileSystem fs = new POIFSFileSystem(POIDataSamples.getPOIFSInstance().openResourceAsStream("protected_sha512.xlsx")); + + EncryptionInfo info = new EncryptionInfo(fs); + + assertEquals(4, info.getVersionMajor()); + assertEquals(4, info.getVersionMinor()); + + assertEquals(CipherAlgorithm.aes256, info.getHeader().getCipherAlgorithm()); + assertEquals(HashAlgorithm.sha512, info.getHeader().getHashAlgorithmEx()); + assertEquals(256, info.getHeader().getKeySize()); + assertEquals(64, info.getVerifier().getEncryptedVerifierHash().length); + assertEquals(CipherProvider.aes, info.getHeader().getCipherProvider()); +// assertEquals("Microsoft Enhanced RSA and AES Cryptographic Provider", info.getHeader().getCspName()); + } +} diff --git a/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestEncryptor.java b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestEncryptor.java new file mode 100644 index 0000000000..957ec10973 --- /dev/null +++ b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestEncryptor.java @@ -0,0 +1,253 @@ +/* ==================================================================== + 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 static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Iterator; + +import org.apache.poi.POIDataSamples; +import org.apache.poi.poifs.crypt.agile.AgileEncryptionHeader; +import org.apache.poi.poifs.filesystem.DirectoryNode; +import org.apache.poi.poifs.filesystem.DocumentNode; +import org.apache.poi.poifs.filesystem.Entry; +import org.apache.poi.poifs.filesystem.NPOIFSFileSystem; +import org.apache.poi.poifs.filesystem.POIFSFileSystem; +import org.apache.poi.util.BoundedInputStream; +import org.apache.poi.util.IOUtils; +import org.junit.Test; + +public class TestEncryptor { + @Test + public void testAgileEncryption() throws Exception { + File file = POIDataSamples.getDocumentInstance().getFile("bug53475-password-is-pass.docx"); + String pass = "pass"; + NPOIFSFileSystem nfs = new NPOIFSFileSystem(file); + + // Check the encryption details + EncryptionInfo infoExpected = new EncryptionInfo(nfs); + Decryptor decExpected = Decryptor.getInstance(infoExpected); + boolean passed = decExpected.verifyPassword(pass); + assertTrue("Unable to process: document is encrypted", passed); + + // extract the payload + InputStream is = decExpected.getDataStream(nfs); + byte payloadExpected[] = IOUtils.toByteArray(is); + is.close(); + + long decPackLenExpected = decExpected.getLength(); + assertEquals(decPackLenExpected, payloadExpected.length); + + is = nfs.getRoot().createDocumentInputStream("EncryptedPackage"); + is = new BoundedInputStream(is, is.available()-16); // ignore padding block + byte encPackExpected[] = IOUtils.toByteArray(is); + is.close(); + + // listDir(nfs.getRoot(), "orig", ""); + + nfs.close(); + + // check that same verifier/salt lead to same hashes + byte verifierSaltExpected[] = infoExpected.getVerifier().getSalt(); + byte verifierExpected[] = decExpected.getVerifier(); + byte keySalt[] = infoExpected.getHeader().getKeySalt(); + byte keySpec[] = decExpected.getSecretKey().getEncoded(); + byte integritySalt[] = decExpected.getIntegrityHmacKey(); + // the hmacs of the file always differ, as we use PKCS5-padding to pad the bytes + // whereas office just uses random bytes + // byte integrityHash[] = d.getIntegrityHmacValue(); + + POIFSFileSystem fs = new POIFSFileSystem(); + EncryptionInfo infoActual = new EncryptionInfo( + fs, EncryptionMode.agile + , infoExpected.getVerifier().getCipherAlgorithm() + , infoExpected.getVerifier().getHashAlgorithm() + , infoExpected.getHeader().getKeySize() + , infoExpected.getHeader().getBlockSize() + , infoExpected.getVerifier().getChainingMode() + ); + + Encryptor e = Encryptor.getInstance(infoActual); + e.confirmPassword(pass, keySpec, keySalt, verifierExpected, verifierSaltExpected, integritySalt); + + OutputStream os = e.getDataStream(fs); + IOUtils.copy(new ByteArrayInputStream(payloadExpected), os); + os.close(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + fs.writeFilesystem(bos); + + nfs = new NPOIFSFileSystem(new ByteArrayInputStream(bos.toByteArray())); + infoActual = new EncryptionInfo(nfs.getRoot()); + Decryptor decActual = Decryptor.getInstance(infoActual); + passed = decActual.verifyPassword(pass); + assertTrue("Unable to process: document is encrypted", passed); + + // extract the payload + is = decActual.getDataStream(nfs); + byte payloadActual[] = IOUtils.toByteArray(is); + is.close(); + + long decPackLenActual = decActual.getLength(); + + is = nfs.getRoot().createDocumentInputStream("EncryptedPackage"); + is = new BoundedInputStream(is, is.available()-16); // ignore padding block + byte encPackActual[] = IOUtils.toByteArray(is); + is.close(); + + // listDir(nfs.getRoot(), "copy", ""); + + nfs.close(); + + AgileEncryptionHeader aehExpected = (AgileEncryptionHeader)infoExpected.getHeader(); + AgileEncryptionHeader aehActual = (AgileEncryptionHeader)infoActual.getHeader(); + assertThat(aehExpected.getEncryptedHmacKey(), equalTo(aehActual.getEncryptedHmacKey())); + assertEquals(decPackLenExpected, decPackLenActual); + assertThat(payloadExpected, equalTo(payloadActual)); + assertThat(encPackExpected, equalTo(encPackActual)); + } + + @Test + public void testStandardEncryption() throws Exception { + File file = POIDataSamples.getDocumentInstance().getFile("bug53475-password-is-solrcell.docx"); + String pass = "solrcell"; + + NPOIFSFileSystem nfs = new NPOIFSFileSystem(file); + + // Check the encryption details + EncryptionInfo infoExpected = new EncryptionInfo(nfs); + Decryptor d = Decryptor.getInstance(infoExpected); + boolean passed = d.verifyPassword(pass); + assertTrue("Unable to process: document is encrypted", passed); + + // extract the payload + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + InputStream is = d.getDataStream(nfs); + IOUtils.copy(is, bos); + is.close(); + nfs.close(); + byte payloadExpected[] = bos.toByteArray(); + + // check that same verifier/salt lead to same hashes + byte verifierSaltExpected[] = infoExpected.getVerifier().getSalt(); + byte verifierExpected[] = d.getVerifier(); + byte keySpec[] = d.getSecretKey().getEncoded(); + byte keySalt[] = infoExpected.getHeader().getKeySalt(); + + + POIFSFileSystem fs = new POIFSFileSystem(); + EncryptionInfo infoActual = new EncryptionInfo( + fs, EncryptionMode.standard + , infoExpected.getVerifier().getCipherAlgorithm() + , infoExpected.getVerifier().getHashAlgorithm() + , infoExpected.getHeader().getKeySize() + , infoExpected.getHeader().getBlockSize() + , infoExpected.getVerifier().getChainingMode() + ); + + Encryptor e = Encryptor.getInstance(infoActual); + e.confirmPassword(pass, keySpec, keySalt, verifierExpected, verifierSaltExpected, null); + + assertThat(infoExpected.getVerifier().getEncryptedVerifier(), equalTo(infoActual.getVerifier().getEncryptedVerifier())); + assertThat(infoExpected.getVerifier().getEncryptedVerifierHash(), equalTo(infoActual.getVerifier().getEncryptedVerifierHash())); + + // now we use a newly generated salt/verifier and check + // if the file content is still the same + + fs = new POIFSFileSystem(); + infoActual = new EncryptionInfo( + fs, EncryptionMode.standard + , infoExpected.getVerifier().getCipherAlgorithm() + , infoExpected.getVerifier().getHashAlgorithm() + , infoExpected.getHeader().getKeySize() + , infoExpected.getHeader().getBlockSize() + , infoExpected.getVerifier().getChainingMode() + ); + + e = Encryptor.getInstance(infoActual); + e.confirmPassword(pass); + + OutputStream os = e.getDataStream(fs); + IOUtils.copy(new ByteArrayInputStream(payloadExpected), os); + os.close(); + + bos.reset(); + fs.writeFilesystem(bos); + + ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); + + // FileOutputStream fos = new FileOutputStream("encrypted.docx"); + // IOUtils.copy(bis, fos); + // fos.close(); + // bis.reset(); + + nfs = new NPOIFSFileSystem(bis); + infoExpected = new EncryptionInfo(nfs); + d = Decryptor.getInstance(infoExpected); + passed = d.verifyPassword(pass); + assertTrue("Unable to process: document is encrypted", passed); + + bos.reset(); + is = d.getDataStream(nfs); + IOUtils.copy(is, bos); + is.close(); + nfs.close(); + byte payloadActual[] = bos.toByteArray(); + + assertThat(payloadExpected, equalTo(payloadActual)); + } + + + private void listEntry(DocumentNode de, String ext, String path) throws IOException { + path += "\\" + de.getName().replace('\u0006', '_'); + System.out.println(ext+": "+path+" ("+de.getSize()+" bytes)"); + + String name = de.getName().replace('\u0006', '_'); + + InputStream is = ((DirectoryNode)de.getParent()).createDocumentInputStream(de); + FileOutputStream fos = new FileOutputStream("solr."+name+"."+ext); + IOUtils.copy(is, fos); + fos.close(); + is.close(); + } + + @SuppressWarnings("unused") + private void listDir(DirectoryNode dn, String ext, String path) throws IOException { + path += "\\" + dn.getName().replace('\u0006', '_'); + System.out.println(ext+": "+path+" ("+dn.getStorageClsid()+")"); + + Iterator iter = dn.getEntries(); + while (iter.hasNext()) { + Entry ent = iter.next(); + if (ent instanceof DirectoryNode) { + listDir((DirectoryNode)ent, ext, path); + } else { + listEntry((DocumentNode)ent, ext, path); + } + } + } +} diff --git a/src/ooxml/testcases/org/apache/poi/xwpf/TestXWPFBugs.java b/src/ooxml/testcases/org/apache/poi/xwpf/TestXWPFBugs.java index a0a97cd116..f0ddad5ba7 100644 --- a/src/ooxml/testcases/org/apache/poi/xwpf/TestXWPFBugs.java +++ b/src/ooxml/testcases/org/apache/poi/xwpf/TestXWPFBugs.java @@ -11,9 +11,10 @@ import javax.crypto.Cipher; import org.apache.poi.POIDataSamples; import org.apache.poi.openxml4j.opc.OPCPackage; +import org.apache.poi.poifs.crypt.CipherAlgorithm; import org.apache.poi.poifs.crypt.Decryptor; -import org.apache.poi.poifs.crypt.EncryptionHeader; import org.apache.poi.poifs.crypt.EncryptionInfo; +import org.apache.poi.poifs.crypt.HashAlgorithm; import org.apache.poi.poifs.filesystem.NPOIFSFileSystem; import org.apache.poi.xwpf.extractor.XWPFWordExtractor; import org.apache.poi.xwpf.usermodel.XWPFDocument; @@ -33,8 +34,8 @@ public class TestXWPFBugs { // Check the encryption details EncryptionInfo info = new EncryptionInfo(filesystem); assertEquals(128, info.getHeader().getKeySize()); - assertEquals(EncryptionHeader.ALGORITHM_AES_128, info.getHeader().getAlgorithm()); - assertEquals(EncryptionHeader.HASH_SHA1, info.getHeader().getHashAlgorithm()); + assertEquals(CipherAlgorithm.aes128, info.getHeader().getCipherAlgorithm()); + assertEquals(HashAlgorithm.sha1, info.getHeader().getHashAlgorithmEx()); // Check it can be decoded Decryptor d = Decryptor.getInstance(info); @@ -67,8 +68,8 @@ public class TestXWPFBugs { 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()); + assertEquals(CipherAlgorithm.aes256, info.getHeader().getCipherAlgorithm()); + assertEquals(HashAlgorithm.sha1, info.getHeader().getHashAlgorithmEx()); // Check it can be decoded Decryptor d = Decryptor.getInstance(info);