diff options
Diffstat (limited to 'src/ooxml')
20 files changed, 2549 insertions, 10 deletions
diff --git a/src/ooxml/java/org/apache/poi/POIXMLException.java b/src/ooxml/java/org/apache/poi/POIXMLException.java index 0c5ad44a51..82832ecff8 100644 --- a/src/ooxml/java/org/apache/poi/POIXMLException.java +++ b/src/ooxml/java/org/apache/poi/POIXMLException.java @@ -21,6 +21,7 @@ package org.apache.poi; * * @author Yegor Kozlov */ +@SuppressWarnings("serial") public final class POIXMLException extends RuntimeException{ /** * Create a new <code>POIXMLException</code> 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<inputLen; fillSize+=blockSize); + return fillSize; + } + + protected static byte[] hashInput(AgileEncryptionInfoBuilder builder, byte pwHash[], byte blockKey[], byte inputKey[], int cipherMode) { + EncryptionVerifier ver = builder.getVerifier(); + int keySize = builder.getDecryptor().getKeySizeInBytes(); + int blockSize = builder.getDecryptor().getBlockSizeInBytes(); + HashAlgorithm hashAlgo = ver.getHashAlgorithm(); + byte[] salt = ver.getSalt(); + + byte intermedKey[] = generateKey(pwHash, hashAlgo, blockKey, keySize); + SecretKey skey = new SecretKeySpec(intermedKey, ver.getCipherAlgorithm().jceId); + byte[] iv = generateIv(hashAlgo, salt, null, blockSize); + Cipher cipher = getCipher(skey, ver.getCipherAlgorithm(), ver.getChainingMode(), iv, cipherMode); + byte[] hashFinal; + + try { + inputKey = getBlock0(inputKey, getNextBlockSize(inputKey.length, blockSize)); + hashFinal = cipher.doFinal(inputKey); + return hashFinal; + } catch (GeneralSecurityException e) { + throw new EncryptedDocumentException(e); + } + } + + public InputStream getDataStream(DirectoryNode dir) throws IOException, GeneralSecurityException { + DocumentInputStream dis = dir.createDocumentInputStream("EncryptedPackage"); + _length = dis.readLong(); + + ChunkedCipherInputStream cipherStream = new ChunkedCipherInputStream(dis, _length); + return cipherStream; + } + + public long getLength(){ + if(_length == -1) throw new IllegalStateException("EcmaDecryptor.getDataStream() was not called"); + return _length; + } + + /** + * 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 ChunkedCipherInputStream extends InputStream { + private int _lastIndex = 0; + private long _pos = 0; + private final long _size; + private final InputStream _stream; + private byte[] _chunk; + private Cipher _cipher; + + public ChunkedCipherInputStream(DocumentInputStream stream, long size) + throws GeneralSecurityException { + EncryptionHeader header = info.getHeader(); + _size = size; + _stream = stream; + _cipher = getCipher(getSecretKey(), header.getCipherAlgorithm(), header.getChainingMode(), header.getKeySalt(), Cipher.DECRYPT_MODE); + } + + 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; + + if (available() <= 0) return -1; + + while (len > 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<AgileCertificateEntry> certList = new ArrayList<AgileCertificateEntry>(); + + + 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<CTKeyEncryptor> 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<AgileCertificateEntry> 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<String,String> nsMap = new HashMap<String,String>();
+ 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("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\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<? extends TestCase> test = (Class<? extends TestCase>) 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<Class<?>> classes = (Vector<Class<?>>) _classes.get(appLoader); Map<String, Class<?>> map = new HashMap<String, Class<?>>(); 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 @@ +<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ====================================================================
+ 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.
+ ====================================================================
+-->
+<xs:schema xmlns="http://schemas.microsoft.com/office/2006/keyEncryptor/certificate" xmlns:e="http://schemas.microsoft.com/office/2006/encryption" xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://schemas.microsoft.com/office/2006/keyEncryptor/certificate" elementFormDefault="qualified" attributeFormDefault="unqualified">
+ <xs:import namespace="http://schemas.microsoft.com/office/2006/encryption" schemaLocation="encryptionInfo.xsd"/>
+ <xs:simpleType name="ST_PasswordKeyEncryptorUri">
+ <xs:restriction base="xs:token">
+ <xs:enumeration value="http://schemas.microsoft.com/office/2006/keyEncryptor/certificate"/>
+ </xs:restriction>
+ </xs:simpleType>
+ <xs:complexType name="CT_CertificateKeyEncryptor">
+ <xs:attribute name="encryptedKeyValue" type="xs:base64Binary" use="required">
+ <xs:annotation><xs:documentation>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.</xs:documentation></xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="X509Certificate" type="xs:base64Binary" use="required">
+ <xs:annotation><xs:documentation>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.</xs:documentation></xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="certVerifier" type="xs:base64Binary" use="required">
+ <xs:annotation><xs:documentation>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.</xs:documentation></xs:annotation>
+ </xs:attribute>
+ </xs:complexType>
+ <xs:element name="encryptedKey" type="CT_CertificateKeyEncryptor"/>
+</xs:schema>
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 @@ +<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ====================================================================
+ 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.
+ ====================================================================
+-->
+<xb:config xmlns:xb="http://xml.apache.org/xmlbeans/2004/02/xbean/config" xmlns:c="http://schemas.microsoft.com/office/2006/keyEncryptor/certificate">
+
+<xb:qname name="c:encryptedKey" javaname="EncryptedCertificateKey"/>
+
+</xb:config>
\ 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 @@ +<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ====================================================================
+ 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.
+ ====================================================================
+-->
+<xs:schema xmlns="http://schemas.microsoft.com/office/2006/encryption" xmlns:p="http://schemas.microsoft.com/office/2006/keyEncryptor/password" xmlns:c="http://schemas.microsoft.com/office/2006/keyEncryptor/certificate" xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://schemas.microsoft.com/office/2006/encryption" elementFormDefault="qualified" attributeFormDefault="unqualified">
+ <xs:import namespace="http://schemas.microsoft.com/office/2006/keyEncryptor/password" schemaLocation="encryptionPassword.xsd"/>
+ <xs:import namespace="http://schemas.microsoft.com/office/2006/keyEncryptor/certificate" schemaLocation="encryptionCertificate.xsd"/>
+ <xs:simpleType name="ST_SaltSize">
+ <xs:annotation>
+ <xs:documentation>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.</xs:documentation>
+ </xs:annotation>
+ <xs:restriction base="xs:unsignedInt">
+ <xs:minInclusive value="1"/>
+ <xs:maxInclusive value="65536"/>
+ </xs:restriction>
+ </xs:simpleType>
+ <xs:simpleType name="ST_BlockSize">
+ <xs:annotation>
+ <xs:documentation>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.</xs:documentation>
+ </xs:annotation>
+ <xs:restriction base="xs:unsignedInt">
+ <xs:minInclusive value="2"/>
+ <xs:maxInclusive value="4096"/>
+ </xs:restriction>
+ </xs:simpleType>
+ <xs:simpleType name="ST_KeyBits">
+ <xs:annotation>
+ <xs:documentation>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.</xs:documentation>
+ </xs:annotation>
+ <xs:restriction base="xs:unsignedInt">
+ <xs:minInclusive value="8"/>
+ </xs:restriction>
+ </xs:simpleType>
+ <xs:simpleType name="ST_HashSize">
+ <xs:annotation>
+ <xs:documentation>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.</xs:documentation>
+ </xs:annotation>
+ <xs:restriction base="xs:unsignedInt">
+ <xs:minInclusive value="1"/>
+ <xs:maxInclusive value="65536"/>
+ </xs:restriction>
+ </xs:simpleType>
+ <xs:simpleType name="ST_SpinCount">
+ <xs:annotation>
+ <xs:documentation>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.</xs:documentation>
+ </xs:annotation>
+ <xs:restriction base="xs:unsignedInt">
+ <xs:minInclusive value="0"/>
+ <xs:maxInclusive value="10000000"/>
+ </xs:restriction>
+ </xs:simpleType>
+ <xs:simpleType name="ST_CipherAlgorithm">
+ <xs:annotation>
+ <xs:appinfo>modified for poi - list is restricted to given list in [ms-offcrypto]</xs:appinfo>
+ <xs:documentation>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.</xs:documentation>
+ </xs:annotation>
+ <xs:restriction base="xs:token">
+ <xs:enumeration value="AES">
+ <xs:annotation>
+ <xs:documentation>MUST conform to the AES algorithm.</xs:documentation>
+ </xs:annotation>
+ </xs:enumeration>
+ <xs:enumeration value="RC2">
+ <xs:annotation>
+ <xs:documentation>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.</xs:documentation>
+ </xs:annotation>
+ </xs:enumeration>
+ <xs:enumeration value="RC4">
+ <xs:annotation>
+ <xs:documentation>MUST NOT be used.</xs:documentation>
+ </xs:annotation>
+ </xs:enumeration>
+ <xs:enumeration value="DES">
+ <xs:annotation>
+ <xs:documentation>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.</xs:documentation>
+ </xs:annotation>
+ </xs:enumeration>
+ <xs:enumeration value="DESX">
+ <xs:annotation>
+ <xs:documentation>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.</xs:documentation>
+ </xs:annotation>
+ </xs:enumeration>
+ <xs:enumeration value="3DES">
+ <xs:annotation>
+ <xs:documentation>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.</xs:documentation>
+ </xs:annotation>
+ </xs:enumeration>
+ <xs:enumeration value="3DES_112">
+ <xs:annotation>
+ <xs:documentation>see 3DES</xs:documentation>
+ </xs:annotation>
+ </xs:enumeration>
+ </xs:restriction>
+ </xs:simpleType>
+ <xs:simpleType name="ST_CipherChaining">
+ <xs:annotation>
+ <xs:documentation>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).</xs:documentation>
+ </xs:annotation>
+ <xs:restriction base="xs:token">
+ <xs:enumeration value="ChainingModeCBC">
+ <xs:annotation>
+ <xs:documentation>block chaining (CBC)</xs:documentation>
+ </xs:annotation>
+ </xs:enumeration>
+ <xs:enumeration value="ChainingModeCFB">
+ <xs:annotation>
+ <xs:documentation>Cipher feedback chaining (CFB), with an 8-bit window</xs:documentation>
+ </xs:annotation>
+ </xs:enumeration>
+ </xs:restriction>
+ </xs:simpleType>
+ <xs:simpleType name="ST_HashAlgorithm">
+ <xs:annotation>
+ <xs:appinfo>modified for poi - list is restricted to given list in [ms-offcrypto]</xs:appinfo>
+ <xs:documentation>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.</xs:documentation>
+ </xs:annotation>
+ <xs:restriction base="xs:token">
+ <xs:enumeration value="SHA1">
+ <xs:annotation>
+ <xs:documentation>MUST conform to the algorithm as specified in [RFC4634] (http://tools.ietf.org/html/rfc4634).</xs:documentation>
+ </xs:annotation>
+ </xs:enumeration>
+ <xs:enumeration value="SHA256">
+ <xs:annotation>
+ <xs:documentation>see SHA1</xs:documentation>
+ </xs:annotation>
+ </xs:enumeration>
+ <xs:enumeration value="SHA384">
+ <xs:annotation>
+ <xs:documentation>see SHA1</xs:documentation>
+ </xs:annotation>
+ </xs:enumeration>
+ <xs:enumeration value="SHA512">
+ <xs:annotation>
+ <xs:documentation>see SHA1</xs:documentation>
+ </xs:annotation>
+ </xs:enumeration>
+ <xs:enumeration value="MD5">
+ <xs:annotation>
+ <xs:documentation>MUST conform to MD5.</xs:documentation>
+ </xs:annotation>
+ </xs:enumeration>
+ <xs:enumeration value="MD4">
+ <xs:annotation>
+ <xs:documentation>MUST conform to the algorithm as specified in [RFC1320] (http://tools.ietf.org/html/rfc1320).</xs:documentation>
+ </xs:annotation>
+ </xs:enumeration>
+ <xs:enumeration value="MD2">
+ <xs:annotation>
+ <xs:documentation>MUST conform to the algorithm as specified in [RFC1319] (http://tools.ietf.org/html/rfc1319).</xs:documentation>
+ </xs:annotation>
+ </xs:enumeration>
+ <xs:enumeration value="RIPEMD-128">
+ <xs:annotation>
+ <xs:documentation>MUST conform to the hash functions specified in [ISO/IEC 10118]. (https://en.wikipedia.org/wiki/RIPEMD)</xs:documentation>
+ </xs:annotation>
+ </xs:enumeration>
+ <xs:enumeration value="RIPEMD-160">
+ <xs:annotation>
+ <xs:documentation>see RIPEMD-128 (https://en.wikipedia.org/wiki/RIPEMD)</xs:documentation>
+ </xs:annotation>
+ </xs:enumeration>
+ <xs:enumeration value="WHIRLPOOL">
+ <xs:annotation>
+ <xs:documentation>see RIPEMD-128 (https://en.wikipedia.org/wiki/ISO/IEC_10118-3)</xs:documentation>
+ </xs:annotation>
+ </xs:enumeration>
+ </xs:restriction>
+ </xs:simpleType>
+ <xs:complexType name="CT_KeyData">
+ <xs:annotation>
+ <xs:documentation>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.</xs:documentation>
+ </xs:annotation>
+ <xs:attribute name="saltSize" type="ST_SaltSize" use="required"/>
+ <xs:attribute name="blockSize" type="ST_BlockSize" use="required"/>
+ <xs:attribute name="keyBits" type="ST_KeyBits" use="required"/>
+ <xs:attribute name="hashSize" type="ST_HashSize" use="required"/>
+ <xs:attribute name="cipherAlgorithm" type="ST_CipherAlgorithm" use="required"/>
+ <xs:attribute name="cipherChaining" type="ST_CipherChaining" use="required"/>
+ <xs:attribute name="hashAlgorithm" type="ST_HashAlgorithm" use="required"/>
+ <xs:attribute name="saltValue" type="xs:base64Binary" use="required"/>
+ </xs:complexType>
+ <xs:complexType name="CT_DataIntegrity">
+ <xs:annotation>
+ <xs:documentation>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).</xs:documentation>
+ </xs:annotation>
+ <xs:attribute name="encryptedHmacKey" type="xs:base64Binary" use="required">
+ <xs:annotation>
+ <xs:documentation>A base64-encoded value that specifies an encrypted key used in calculating the encryptedHmacValue.</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="encryptedHmacValue" type="xs:base64Binary" use="required">
+ <xs:annotation>
+ <xs:documentation>A base64-encoded value that specifies an HMAC derived from encryptedHmacKey and the encrypted data.</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ </xs:complexType>
+ <xs:complexType name="CT_KeyEncryptor">
+ <xs:annotation>
+ <xs:appinfo>modified for POI</xs:appinfo>
+ <xs:documentation>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.</xs:documentation>
+ </xs:annotation>
+ <xs:choice>
+ <xs:element ref="p:encryptedKey"/>
+ <xs:element ref="c:encryptedKey"/>
+ </xs:choice>
+ <xs:attribute name="uri">
+ <xs:annotation>
+ <xs:appinfo>modified for POI</xs:appinfo>
+ </xs:annotation>
+ <xs:simpleType>
+ <xs:restriction base="xs:token">
+ <xs:enumeration value="http://schemas.microsoft.com/office/2006/keyEncryptor/password"/>
+ <xs:enumeration value="http://schemas.microsoft.com/office/2006/keyEncryptor/certificate"/>
+ </xs:restriction>
+ </xs:simpleType>
+ </xs:attribute>
+ </xs:complexType>
+ <xs:complexType name="CT_KeyEncryptors">
+ <xs:annotation>
+ <xs:documentation>A sequence of KeyEncryptor elements. Exactly one KeyEncryptors element MUST be present, and the KeyEncryptors element MUST contain at least one KeyEncryptor.</xs:documentation>
+ </xs:annotation>
+ <xs:sequence>
+ <xs:element name="keyEncryptor" type="CT_KeyEncryptor" maxOccurs="unbounded"/>
+ </xs:sequence>
+ </xs:complexType>
+ <xs:complexType name="CT_Encryption">
+ <xs:sequence>
+ <xs:element name="keyData" type="CT_KeyData"/>
+ <xs:element name="dataIntegrity" type="CT_DataIntegrity">
+ <xs:annotation>
+ <xs:appinfo>modified for POI</xs:appinfo>
+ <xs:documentation>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].</xs:documentation>
+ </xs:annotation>
+ </xs:element>
+ <xs:element name="keyEncryptors" type="CT_KeyEncryptors">
+ <xs:annotation>
+ <xs:documentation>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.</xs:documentation>
+ </xs:annotation>
+ </xs:element>
+ </xs:sequence>
+ </xs:complexType>
+ <xs:element name="encryption" type="CT_Encryption"/>
+</xs:schema>
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 @@ +<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ====================================================================
+ 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.
+ ====================================================================
+-->
+<xb:config xmlns:xb="http://xml.apache.org/xmlbeans/2004/02/xbean/config" xmlns:c="http://schemas.microsoft.com/office/2006/keyEncryptor/certificate" xmlns:p="http://schemas.microsoft.com/office/2006/keyEncryptor/password">
+
+<xb:qname name="c:encryptedKey" javaname="EncryptedCertificateKey"/>
+<xb:qname name="p:encryptedKey" javaname="EncryptedPasswordKey"/>
+
+</xb:config>
\ 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 @@ +<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ====================================================================
+ 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.
+ ====================================================================
+-->
+<xs:schema xmlns="http://schemas.microsoft.com/office/2006/keyEncryptor/password" xmlns:e="http://schemas.microsoft.com/office/2006/encryption" xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://schemas.microsoft.com/office/2006/keyEncryptor/password" elementFormDefault="qualified" attributeFormDefault="unqualified">
+ <xs:import namespace="http://schemas.microsoft.com/office/2006/encryption" schemaLocation="encryptionInfo.xsd"/>
+ <xs:simpleType name="ST_PasswordKeyEncryptorUri">
+ <xs:restriction base="xs:token">
+ <xs:enumeration value="http://schemas.microsoft.com/office/2006/keyEncryptor/password"/>
+ </xs:restriction>
+ </xs:simpleType>
+ <xs:complexType name="CT_PasswordKeyEncryptor">
+ <xs:attribute name="saltSize" type="e:ST_SaltSize" use="required">
+ <xs:annotation><xs:documentation>A SaltSize that specifies the size of the salt for a PasswordKeyEncryptor.</xs:documentation></xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="blockSize" type="e:ST_BlockSize" use="required">
+ <xs:annotation><xs:documentation>A BlockSize that specifies the block size for a PasswordKeyEncryptor.</xs:documentation></xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="keyBits" type="e:ST_KeyBits" use="required">
+ <xs:annotation><xs:documentation>A KeyBits that specifies the number of bits for a PasswordKeyEncryptor.</xs:documentation></xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="hashSize" type="e:ST_HashSize" use="required">
+ <xs:annotation><xs:documentation>A HashSize that specifies the size of the binary form of the hash for a PasswordKeyEncryptor.</xs:documentation></xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="cipherAlgorithm" type="e:ST_CipherAlgorithm" use="required">
+ <xs:annotation><xs:documentation>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.</xs:documentation></xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="cipherChaining" type="e:ST_CipherChaining" use="required">
+ <xs:annotation><xs:documentation>A CipherChaining that specifies the cipher chaining mode for a PasswordKeyEncryptor.</xs:documentation></xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="hashAlgorithm" type="e:ST_HashAlgorithm" use="required">
+ <xs:annotation><xs:documentation>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.</xs:documentation></xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="saltValue" type="xs:base64Binary" use="required">
+ <xs:annotation><xs:documentation>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.</xs:documentation></xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="spinCount" type="e:ST_SpinCount" use="required">
+ <xs:annotation><xs:documentation>A SpinCount that specifies the spin count for a PasswordKeyEncryptor.</xs:documentation></xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="encryptedVerifierHashInput" type="xs:base64Binary" use="required">
+ <xs:annotation><xs:documentation>A base64-encoded value that specifies the encrypted verifier hash input for a PasswordKeyEncryptor used in password verification.</xs:documentation></xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="encryptedVerifierHashValue" type="xs:base64Binary" use="required">
+ <xs:annotation><xs:documentation>A base64-encoded value that specifies the encrypted verifier hash value for a PasswordKeyEncryptor used in password verification.</xs:documentation></xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="encryptedKeyValue" type="xs:base64Binary" use="required">
+ <xs:annotation><xs:documentation>A base64-encoded value that specifies the encrypted form of the intermediate key.</xs:documentation></xs:annotation>
+ </xs:attribute>
+ </xs:complexType>
+ <xs:element name="encryptedKey" type="CT_PasswordKeyEncryptor"/>
+</xs:schema>
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 @@ +<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ====================================================================
+ 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.
+ ====================================================================
+-->
+<xb:config xmlns:xb="http://xml.apache.org/xmlbeans/2004/02/xbean/config" xmlns:p="http://schemas.microsoft.com/office/2006/keyEncryptor/password">
+
+<xb:qname name="p:encryptedKey" javaname="EncryptedPasswordKey"/>
+
+</xb:config>
\ No newline at end of file diff --git a/src/ooxml/testcases/org/apache/poi/poifs/crypt/AllPOIFSCryptoTests.java b/src/ooxml/testcases/org/apache/poi/poifs/crypt/AllPOIFSCryptoTests.java new file mode 100644 index 0000000000..fd8e56a745 --- /dev/null +++ b/src/ooxml/testcases/org/apache/poi/poifs/crypt/AllPOIFSCryptoTests.java @@ -0,0 +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 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<Object[]> 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<Object[]> data = new ArrayList<Object[]>();
+ 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/ooxml/testcases/org/apache/poi/poifs/crypt/TestDecryptor.java b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestDecryptor.java new file mode 100644 index 0000000000..95a94c4667 --- /dev/null +++ b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestDecryptor.java @@ -0,0 +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 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/ooxml/testcases/org/apache/poi/poifs/crypt/TestEncryptionInfo.java b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestEncryptionInfo.java new file mode 100644 index 0000000000..698adb86d9 --- /dev/null +++ b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestEncryptionInfo.java @@ -0,0 +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 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<Entry> 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);
|