aboutsummaryrefslogtreecommitdiffstats
path: root/src/java
diff options
context:
space:
mode:
Diffstat (limited to 'src/java')
-rw-r--r--src/java/org/apache/poi/EncryptedDocumentException.java9
-rw-r--r--src/java/org/apache/poi/poifs/crypt/AgileDecryptor.java268
-rw-r--r--src/java/org/apache/poi/poifs/crypt/ChainingMode.java33
-rw-r--r--src/java/org/apache/poi/poifs/crypt/CipherAlgorithm.java79
-rw-r--r--src/java/org/apache/poi/poifs/crypt/CipherProvider.java39
-rw-r--r--src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java258
-rw-r--r--src/java/org/apache/poi/poifs/crypt/DataSpaceMapUtils.java365
-rw-r--r--src/java/org/apache/poi/poifs/crypt/Decryptor.java96
-rw-r--r--src/java/org/apache/poi/poifs/crypt/EcmaDecryptor.java134
-rw-r--r--src/java/org/apache/poi/poifs/crypt/EncryptionHeader.java255
-rw-r--r--src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java135
-rw-r--r--src/java/org/apache/poi/poifs/crypt/EncryptionInfoBuilder.java30
-rw-r--r--src/java/org/apache/poi/poifs/crypt/EncryptionMode.java35
-rw-r--r--src/java/org/apache/poi/poifs/crypt/EncryptionVerifier.java208
-rw-r--r--src/java/org/apache/poi/poifs/crypt/Encryptor.java65
-rw-r--r--src/java/org/apache/poi/poifs/crypt/HashAlgorithm.java66
-rw-r--r--src/java/org/apache/poi/poifs/crypt/package.html44
-rw-r--r--src/java/org/apache/poi/poifs/crypt/standard/EncryptionRecord.java23
-rw-r--r--src/java/org/apache/poi/poifs/crypt/standard/StandardDecryptor.java158
-rw-r--r--src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptionHeader.java116
-rw-r--r--src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptionInfoBuilder.java106
-rw-r--r--src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptionVerifier.java112
-rw-r--r--src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptor.java218
23 files changed, 2105 insertions, 747 deletions
diff --git a/src/java/org/apache/poi/EncryptedDocumentException.java b/src/java/org/apache/poi/EncryptedDocumentException.java
index 4922d1c81d..12196c80cd 100644
--- a/src/java/org/apache/poi/EncryptedDocumentException.java
+++ b/src/java/org/apache/poi/EncryptedDocumentException.java
@@ -16,9 +16,18 @@
==================================================================== */
package org.apache.poi;
+@SuppressWarnings("serial")
public class EncryptedDocumentException extends IllegalStateException
{
public EncryptedDocumentException(String s) {
super(s);
}
+
+ public EncryptedDocumentException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public EncryptedDocumentException(Throwable cause) {
+ super(cause);
+ }
}
diff --git a/src/java/org/apache/poi/poifs/crypt/AgileDecryptor.java b/src/java/org/apache/poi/poifs/crypt/AgileDecryptor.java
deleted file mode 100644
index 401049b0dd..0000000000
--- a/src/java/org/apache/poi/poifs/crypt/AgileDecryptor.java
+++ /dev/null
@@ -1,268 +0,0 @@
-/* ====================================================================
- Licensed to the Apache Software Foundation (ASF) under one or more
- contributor license agreements. See the NOTICE file distributed with
- this work for additional information regarding copyright ownership.
- The ASF licenses this file to You under the Apache License, Version 2.0
- (the "License"); you may not use this file except in compliance with
- the License. You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-==================================================================== */
-package org.apache.poi.poifs.crypt;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.security.GeneralSecurityException;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.util.Arrays;
-
-import javax.crypto.Cipher;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.IvParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
-
-import org.apache.poi.EncryptedDocumentException;
-import org.apache.poi.poifs.filesystem.DirectoryNode;
-import org.apache.poi.poifs.filesystem.DocumentInputStream;
-import org.apache.poi.util.LittleEndian;
-
-/**
- *
- */
-public class AgileDecryptor extends Decryptor {
-
- private final EncryptionInfo _info;
- private SecretKey _secretKey;
- private long _length = -1;
-
- private static final byte[] kVerifierInputBlock;
- private static final byte[] kHashedVerifierBlock;
- private static final byte[] kCryptoKeyBlock;
-
- static {
- kVerifierInputBlock =
- new byte[] { (byte)0xfe, (byte)0xa7, (byte)0xd2, (byte)0x76,
- (byte)0x3b, (byte)0x4b, (byte)0x9e, (byte)0x79 };
- kHashedVerifierBlock =
- new byte[] { (byte)0xd7, (byte)0xaa, (byte)0x0f, (byte)0x6d,
- (byte)0x30, (byte)0x61, (byte)0x34, (byte)0x4e };
- kCryptoKeyBlock =
- new byte[] { (byte)0x14, (byte)0x6e, (byte)0x0b, (byte)0xe7,
- (byte)0xab, (byte)0xac, (byte)0xd0, (byte)0xd6 };
- }
-
- public boolean verifyPassword(String password) throws GeneralSecurityException {
- EncryptionVerifier verifier = _info.getVerifier();
- byte[] salt = verifier.getSalt();
-
- byte[] pwHash = hashPassword(_info, password);
- byte[] iv = generateIv(salt, null);
-
- SecretKey skey;
- skey = new SecretKeySpec(generateKey(pwHash, kVerifierInputBlock), "AES");
- Cipher cipher = getCipher(skey, iv);
- byte[] verifierHashInput = cipher.doFinal(verifier.getVerifier());
-
- MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
- byte[] trimmed = new byte[salt.length];
- System.arraycopy(verifierHashInput, 0, trimmed, 0, trimmed.length);
- byte[] hashedVerifier = sha1.digest(trimmed);
-
- skey = new SecretKeySpec(generateKey(pwHash, kHashedVerifierBlock), "AES");
- iv = generateIv(salt, null);
- cipher = getCipher(skey, iv);
- byte[] verifierHash = cipher.doFinal(verifier.getVerifierHash());
- trimmed = new byte[hashedVerifier.length];
- System.arraycopy(verifierHash, 0, trimmed, 0, trimmed.length);
-
- if (Arrays.equals(trimmed, hashedVerifier)) {
- skey = new SecretKeySpec(generateKey(pwHash, kCryptoKeyBlock), "AES");
- iv = generateIv(salt, null);
- cipher = getCipher(skey, iv);
- byte[] inter = cipher.doFinal(verifier.getEncryptedKey());
- byte[] keyspec = new byte[getKeySizeInBytes()];
- System.arraycopy(inter, 0, keyspec, 0, keyspec.length);
- _secretKey = new SecretKeySpec(keyspec, "AES");
- return true;
- } else {
- return false;
- }
- }
-
- public InputStream getDataStream(DirectoryNode dir) throws IOException, GeneralSecurityException {
- DocumentInputStream dis = dir.createDocumentInputStream("EncryptedPackage");
- _length = dis.readLong();
- return new ChunkedCipherInputStream(dis, _length);
- }
-
- public long getLength(){
- if(_length == -1) throw new IllegalStateException("EcmaDecryptor.getDataStream() was not called");
- return _length;
- }
-
- protected AgileDecryptor(EncryptionInfo info) {
- _info = info;
- }
-
- private class ChunkedCipherInputStream extends InputStream {
- private int _lastIndex = 0;
- private long _pos = 0;
- private final long _size;
- private final DocumentInputStream _stream;
- private byte[] _chunk;
- private Cipher _cipher;
-
- public ChunkedCipherInputStream(DocumentInputStream stream, long size)
- throws GeneralSecurityException {
- _size = size;
- _stream = stream;
- _cipher = getCipher(_secretKey, _info.getHeader().getKeySalt());
- }
-
- public int read() throws IOException {
- byte[] b = new byte[1];
- if (read(b) == 1)
- return b[0];
- return -1;
- }
-
- public int read(byte[] b) throws IOException {
- return read(b, 0, b.length);
- }
-
- public int read(byte[] b, int off, int len) throws IOException {
- int total = 0;
-
- while (len > 0) {
- if (_chunk == null) {
- try {
- _chunk = nextChunk();
- } catch (GeneralSecurityException e) {
- throw new EncryptedDocumentException(e.getMessage());
- }
- }
- int count = (int)(4096L - (_pos & 0xfff));
- count = Math.min(available(), Math.min(count, len));
- System.arraycopy(_chunk, (int)(_pos & 0xfff), b, off, count);
- off += count;
- len -= count;
- _pos += count;
- if ((_pos & 0xfff) == 0)
- _chunk = null;
- total += count;
- }
-
- return total;
- }
-
- public long skip(long n) throws IOException {
- long start = _pos;
- long skip = Math.min(available(), n);
-
- if ((((_pos + skip) ^ start) & ~0xfff) != 0)
- _chunk = null;
- _pos += skip;
- return skip;
- }
-
- public int available() throws IOException { return (int)(_size - _pos); }
- public void close() throws IOException { _stream.close(); }
- public boolean markSupported() { return false; }
-
- private byte[] nextChunk() throws GeneralSecurityException, IOException {
- int index = (int)(_pos >> 12);
- byte[] blockKey = new byte[4];
- LittleEndian.putInt(blockKey, 0, index);
- byte[] iv = generateIv(_info.getHeader().getKeySalt(), blockKey);
- _cipher.init(Cipher.DECRYPT_MODE, _secretKey, new IvParameterSpec(iv));
- if (_lastIndex != index)
- _stream.skip((index - _lastIndex) << 12);
-
- byte[] block = new byte[Math.min(_stream.available(), 4096)];
- _stream.readFully(block);
- _lastIndex = index + 1;
- return _cipher.doFinal(block);
- }
- }
-
- private Cipher getCipher(SecretKey key, byte[] vec)
- throws GeneralSecurityException {
-
- String name = null;
- String chain = null;
-
- EncryptionVerifier verifier = _info.getVerifier();
-
- switch (verifier.getAlgorithm()) {
- case EncryptionHeader.ALGORITHM_AES_128:
- case EncryptionHeader.ALGORITHM_AES_192:
- case EncryptionHeader.ALGORITHM_AES_256:
- name = "AES";
- break;
- default:
- throw new EncryptedDocumentException("Unsupported algorithm");
- }
-
- // Ensure the JCE policies files allow for this sized key
- if (Cipher.getMaxAllowedKeyLength(name) < _info.getHeader().getKeySize()) {
- throw new EncryptedDocumentException("Export Restrictions in place - please install JCE Unlimited Strength Jurisdiction Policy files");
- }
-
- switch (verifier.getCipherMode()) {
- case EncryptionHeader.MODE_CBC:
- chain = "CBC";
- break;
- case EncryptionHeader.MODE_CFB:
- chain = "CFB";
- break;
- default:
- throw new EncryptedDocumentException("Unsupported chain mode");
- }
-
- Cipher cipher = Cipher.getInstance(name + "/" + chain + "/NoPadding");
- IvParameterSpec iv = new IvParameterSpec(vec);
- cipher.init(Cipher.DECRYPT_MODE, key, iv);
- return cipher;
- }
-
- private byte[] getBlock(byte[] hash, int size) {
- byte[] result = new byte[size];
- Arrays.fill(result, (byte)0x36);
- System.arraycopy(hash, 0, result, 0, Math.min(result.length, hash.length));
- return result;
- }
-
- private byte[] generateKey(byte[] hash, byte[] blockKey) throws NoSuchAlgorithmException {
- MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
- sha1.update(hash);
- byte[] key = sha1.digest(blockKey);
- return getBlock(key, getKeySizeInBytes());
- }
-
- protected byte[] generateIv(byte[] salt, byte[] blockKey)
- throws NoSuchAlgorithmException {
-
-
- if (blockKey == null)
- return getBlock(salt, getBlockSizeInBytes());
-
- MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
- sha1.update(salt);
- return getBlock(sha1.digest(blockKey), getBlockSizeInBytes());
- }
-
- protected int getBlockSizeInBytes() {
- return _info.getHeader().getBlockSize();
- }
-
- protected int getKeySizeInBytes() {
- return _info.getHeader().getKeySize()/8;
- }
-}
diff --git a/src/java/org/apache/poi/poifs/crypt/ChainingMode.java b/src/java/org/apache/poi/poifs/crypt/ChainingMode.java
new file mode 100644
index 0000000000..7fccccfb25
--- /dev/null
+++ b/src/java/org/apache/poi/poifs/crypt/ChainingMode.java
@@ -0,0 +1,33 @@
+/* ====================================================================
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+==================================================================== */
+
+package org.apache.poi.poifs.crypt;
+
+public enum ChainingMode {
+ // ecb - only for standard encryption
+ ecb("ECB", 1),
+ cbc("CBC", 2),
+ /* Cipher feedback chaining (CFB), with an 8-bit window */
+ cfb("CFB8", 3);
+
+ public final String jceId;
+ public final int ecmaId;
+ ChainingMode(String jceId, int ecmaId) {
+ this.jceId = jceId;
+ this.ecmaId = ecmaId;
+ }
+} \ No newline at end of file
diff --git a/src/java/org/apache/poi/poifs/crypt/CipherAlgorithm.java b/src/java/org/apache/poi/poifs/crypt/CipherAlgorithm.java
new file mode 100644
index 0000000000..be507a6660
--- /dev/null
+++ b/src/java/org/apache/poi/poifs/crypt/CipherAlgorithm.java
@@ -0,0 +1,79 @@
+/* ====================================================================
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+==================================================================== */
+
+package org.apache.poi.poifs.crypt;
+
+import org.apache.poi.EncryptedDocumentException;
+
+public enum CipherAlgorithm {
+ // key size for rc4: 0x00000028 - 0x00000080 (inclusive) with 8-bit increments
+ // no block size, because its a streaming cipher
+ rc4(CipherProvider.rc4, "RC4", 0x6801, 0x40, new int[]{0x28,0x30,0x38,0x40,0x48,0x50,0x58,0x60,0x68,0x70,0x78,0x80}, -1, 20, "RC4", false),
+ // aes has always a block size of 128 - only its keysize may vary
+ aes128(CipherProvider.aes, "AES", 0x660E, 128, new int[]{128}, 16, 32, "AES", false),
+ aes192(CipherProvider.aes, "AES", 0x660F, 192, new int[]{192}, 16, 32, "AES", false),
+ aes256(CipherProvider.aes, "AES", 0x6610, 256, new int[]{256}, 16, 32, "AES", false),
+ rc2(null, "RC2", -1, 0x80, new int[]{0x28,0x30,0x38,0x40,0x48,0x50,0x58,0x60,0x68,0x70,0x78,0x80}, 8, 20, "RC2", false),
+ des(null, "DES", -1, 64, new int[]{64}, 8/*for 56-bit*/, 32, "DES", false),
+ // desx is not supported. Not sure, if it can be simulated by des3 somehow
+ des3(null, "DESede", -1, 192, new int[]{192}, 8, 32, "3DES", false),
+ // need bouncycastle provider for this one ...
+ // see http://stackoverflow.com/questions/4436397/3des-des-encryption-using-the-jce-generating-an-acceptable-key
+ des3_112(null, "DESede", -1, 128, new int[]{128}, 8, 32, "3DES_112", true),
+ ;
+
+ public final CipherProvider provider;
+ public final String jceId;
+ public final int ecmaId;
+ public final int defaultKeySize;
+ public final int allowedKeySize[];
+ public final int blockSize;
+ public final int encryptedVerifierHashLength;
+ public final String xmlId;
+ public final boolean needsBouncyCastle;
+
+ CipherAlgorithm(CipherProvider provider, String jceId, int ecmaId, int defaultKeySize, int allowedKeySize[], int blockSize, int encryptedVerifierHashLength, String xmlId, boolean needsBouncyCastle) {
+ this.provider = provider;
+ this.jceId = jceId;
+ this.ecmaId = ecmaId;
+ this.defaultKeySize = defaultKeySize;
+ this.allowedKeySize = allowedKeySize;
+ this.blockSize = blockSize;
+ this.encryptedVerifierHashLength = encryptedVerifierHashLength;
+ this.xmlId = xmlId;
+ this.needsBouncyCastle = needsBouncyCastle;
+ }
+
+ public static CipherAlgorithm fromEcmaId(int ecmaId) {
+ for (CipherAlgorithm ca : CipherAlgorithm.values()) {
+ if (ca.ecmaId == ecmaId) return ca;
+ }
+ throw new EncryptedDocumentException("cipher algorithm not found");
+ }
+
+ public static CipherAlgorithm fromXmlId(String xmlId, int keySize) {
+ for (CipherAlgorithm ca : CipherAlgorithm.values()) {
+ if (!ca.xmlId.equals(xmlId)) continue;
+ for (int ks : ca.allowedKeySize) {
+ if (ks == keySize) return ca;
+ }
+ }
+ throw new EncryptedDocumentException("cipher algorithm not found");
+ }
+
+
+} \ No newline at end of file
diff --git a/src/java/org/apache/poi/poifs/crypt/CipherProvider.java b/src/java/org/apache/poi/poifs/crypt/CipherProvider.java
new file mode 100644
index 0000000000..de343a91dc
--- /dev/null
+++ b/src/java/org/apache/poi/poifs/crypt/CipherProvider.java
@@ -0,0 +1,39 @@
+/* ====================================================================
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+==================================================================== */
+
+package org.apache.poi.poifs.crypt;
+
+import org.apache.poi.EncryptedDocumentException;
+
+public enum CipherProvider {
+ rc4("RC4", 1),
+ aes("AES", 0x18);
+
+ public static CipherProvider fromEcmaId(int ecmaId) {
+ for (CipherProvider cp : CipherProvider.values()) {
+ if (cp.ecmaId == ecmaId) return cp;
+ }
+ throw new EncryptedDocumentException("cipher provider not found");
+ }
+
+ public final String jceId;
+ public final int ecmaId;
+ CipherProvider(String jceId, int ecmaId) {
+ this.jceId = jceId;
+ this.ecmaId = ecmaId;
+ }
+} \ No newline at end of file
diff --git a/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java b/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java
new file mode 100644
index 0000000000..75bf1e85ea
--- /dev/null
+++ b/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java
@@ -0,0 +1,258 @@
+/* ====================================================================
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+==================================================================== */
+package org.apache.poi.poifs.crypt;
+
+import java.nio.charset.Charset;
+import java.security.DigestException;
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.security.Provider;
+import java.security.Security;
+import java.util.Arrays;
+
+import javax.crypto.Cipher;
+import javax.crypto.Mac;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+
+import org.apache.poi.EncryptedDocumentException;
+import org.apache.poi.util.LittleEndian;
+import org.apache.poi.util.LittleEndianConsts;
+
+/**
+ * Helper functions used for standard and agile encryption
+ */
+public class CryptoFunctions {
+ /**
+ * 2.3.4.7 ECMA-376 Document Encryption Key Generation (Standard Encryption)
+ * 2.3.4.11 Encryption Key Generation (Agile Encryption)
+ *
+ * The encryption key for ECMA-376 document encryption [ECMA-376] using agile encryption MUST be
+ * generated by using the following method, which is derived from PKCS #5: Password-Based
+ * Cryptography Version 2.0 [RFC2898].
+ *
+ * Let H() be a hashing algorithm as determined by the PasswordKeyEncryptor.hashAlgorithm
+ * element, H_n be the hash data of the n-th iteration, and a plus sign (+) represent concatenation. The
+ * password MUST be provided as an array of Unicode characters. Limitations on the length of the
+ * password and the characters used by the password are implementation-dependent. The initial
+ * password hash is generated as follows:
+ *
+ * - H_0 = H(salt + password)
+ *
+ * The salt used MUST be generated randomly. The salt MUST be stored in the
+ * PasswordKeyEncryptor.saltValue element contained within the \EncryptionInfo stream (1) as
+ * specified in section 2.3.4.10. The hash is then iterated by using the following approach:
+ *
+ * - H_n = H(iterator + H_n-1)
+ *
+ * where iterator is an unsigned 32-bit value that is initially set to 0x00000000 and then incremented
+ * monotonically on each iteration until PasswordKey.spinCount iterations have been performed.
+ * The value of iterator on the last iteration MUST be one less than PasswordKey.spinCount.
+ *
+ * For POI, H_final will be calculated by {@link generateKey()}
+ *
+ * @param password
+ * @param hashAlgorithm
+ * @param salt
+ * @param spinCount
+ * @return
+ */
+ public static byte[] hashPassword(String password, HashAlgorithm hashAlgorithm, byte salt[], int spinCount) {
+ // If no password was given, use the default
+ if (password == null) {
+ password = Decryptor.DEFAULT_PASSWORD;
+ }
+
+ MessageDigest hashAlg = getMessageDigest(hashAlgorithm);
+
+ hashAlg.update(salt);
+ byte[] hash = hashAlg.digest(getUtf16LeString(password));
+ byte[] iterator = new byte[LittleEndianConsts.INT_SIZE];
+
+ try {
+ for (int i = 0; i < spinCount; i++) {
+ LittleEndian.putInt(iterator, 0, i);
+ hashAlg.reset();
+ hashAlg.update(iterator);
+ hashAlg.update(hash);
+ hashAlg.digest(hash, 0, hash.length); // don't create hash buffer everytime new
+ }
+ } catch (DigestException e) {
+ throw new EncryptedDocumentException("error in password hashing");
+ }
+
+ return hash;
+ }
+
+ /**
+ * 2.3.4.12 Initialization Vector Generation (Agile Encryption)
+ *
+ * Initialization vectors are used in all cases for agile encryption. An initialization vector MUST be
+ * generated by using the following method, where H() is a hash function that MUST be the same as
+ * specified in section 2.3.4.11 and a plus sign (+) represents concatenation:
+ * 1. If a blockKey is provided, let IV be a hash of the KeySalt and the following value:
+ * blockKey: IV = H(KeySalt + blockKey)
+ * 2. If a blockKey is not provided, let IV be equal to the following value:
+ * KeySalt:IV = KeySalt.
+ * 3. If the number of bytes in the value of IV is less than the the value of the blockSize attribute
+ * corresponding to the cipherAlgorithm attribute, pad the array of bytes by appending 0x36 until
+ * the array is blockSize bytes. If the array of bytes is larger than blockSize bytes, truncate the
+ * array to blockSize bytes.
+ **/
+ public static byte[] generateIv(HashAlgorithm hashAlgorithm, byte[] salt, byte[] blockKey, int blockSize) {
+ byte iv[] = salt;
+ if (blockKey != null) {
+ MessageDigest hashAlgo = getMessageDigest(hashAlgorithm);
+ hashAlgo.update(salt);
+ iv = hashAlgo.digest(blockKey);
+ }
+ return getBlock36(iv, blockSize);
+ }
+
+ /**
+ * 2.3.4.11 Encryption Key Generation (Agile Encryption)
+ *
+ * ... continued ...
+ *
+ * The final hash data that is used for an encryption key is then generated by using the following
+ * method:
+ *
+ * - H_final = H(H_n + blockKey)
+ *
+ * where blockKey represents an array of bytes used to prevent two different blocks from encrypting
+ * to the same cipher text.
+ *
+ * If the size of the resulting H_final is smaller than that of PasswordKeyEncryptor.keyBits, the key
+ * MUST be padded by appending bytes with a value of 0x36. If the hash value is larger in size than
+ * PasswordKeyEncryptor.keyBits, the key is obtained by truncating the hash value.
+ *
+ * @param passwordHash
+ * @param hashAlgorithm
+ * @param blockKey
+ * @param keySize
+ * @return
+ */
+ public static byte[] generateKey(byte[] passwordHash, HashAlgorithm hashAlgorithm, byte[] blockKey, int keySize) {
+ MessageDigest hashAlgo = getMessageDigest(hashAlgorithm);
+ hashAlgo.update(passwordHash);
+ byte[] key = hashAlgo.digest(blockKey);
+ return getBlock36(key, keySize);
+ }
+
+ public static Cipher getCipher(SecretKey key, CipherAlgorithm cipherAlgorithm, ChainingMode chain, byte[] vec, int cipherMode) {
+ return getCipher(key, cipherAlgorithm, chain, vec, cipherMode, null);
+ }
+
+ /**
+ *
+ *
+ * @param key
+ * @param chain
+ * @param vec
+ * @param cipherMode Cipher.DECRYPT_MODE or Cipher.ENCRYPT_MODE
+ * @return
+ * @throws GeneralSecurityException
+ */
+ public static Cipher getCipher(SecretKey key, CipherAlgorithm cipherAlgorithm, ChainingMode chain, byte[] vec, int cipherMode, String padding) {
+ int keySizeInBytes = key.getEncoded().length;
+ if (padding == null) padding = "NoPadding";
+
+ try {
+ // Ensure the JCE policies files allow for this sized key
+ if (Cipher.getMaxAllowedKeyLength(key.getAlgorithm()) < keySizeInBytes*8) {
+ throw new EncryptedDocumentException("Export Restrictions in place - please install JCE Unlimited Strength Jurisdiction Policy files");
+ }
+
+ Cipher cipher;
+ if (cipherAlgorithm.needsBouncyCastle) {
+ registerBouncyCastle();
+ cipher = Cipher.getInstance(key.getAlgorithm() + "/" + chain.jceId + "/" + padding, "BC");
+ } else {
+ cipher = Cipher.getInstance(key.getAlgorithm() + "/" + chain.jceId + "/" + padding);
+ }
+
+ if (vec == null) {
+ cipher.init(cipherMode, key);
+ } else {
+ IvParameterSpec iv = new IvParameterSpec(vec);
+ cipher.init(cipherMode, key, iv);
+ }
+ return cipher;
+ } catch (GeneralSecurityException e) {
+ throw new EncryptedDocumentException(e);
+ }
+ }
+
+ public static byte[] getBlock36(byte[] hash, int size) {
+ return getBlockX(hash, size, (byte)0x36);
+ }
+
+ public static byte[] getBlock0(byte[] hash, int size) {
+ return getBlockX(hash, size, (byte)0);
+ }
+
+ private static byte[] getBlockX(byte[] hash, int size, byte fill) {
+ if (hash.length == size) return hash;
+
+ byte[] result = new byte[size];
+ Arrays.fill(result, fill);
+ System.arraycopy(hash, 0, result, 0, Math.min(result.length, hash.length));
+ return result;
+ }
+
+ public static byte[] getUtf16LeString(String str) {
+ Charset cs = Charset.forName("UTF-16LE");
+ return str.getBytes(cs);
+ }
+
+ public static MessageDigest getMessageDigest(HashAlgorithm hashAlgorithm) {
+ try {
+ if (hashAlgorithm.needsBouncyCastle) {
+ registerBouncyCastle();
+ return MessageDigest.getInstance(hashAlgorithm.jceId, "BC");
+ } else {
+ return MessageDigest.getInstance(hashAlgorithm.jceId);
+ }
+ } catch (GeneralSecurityException e) {
+ throw new EncryptedDocumentException("hash algo not supported", e);
+ }
+ }
+
+ public static Mac getMac(HashAlgorithm hashAlgorithm) {
+ try {
+ if (hashAlgorithm.needsBouncyCastle) {
+ registerBouncyCastle();
+ return Mac.getInstance(hashAlgorithm.jceHmacId, "BC");
+ } else {
+ return Mac.getInstance(hashAlgorithm.jceHmacId);
+ }
+ } catch (GeneralSecurityException e) {
+ throw new EncryptedDocumentException("hmac algo not supported", e);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private static void registerBouncyCastle() {
+ if (Security.getProvider("BC") != null) return;
+ try {
+ Class<Provider> clazz = (Class<Provider>)Class.forName("org.bouncycastle.jce.provider.BouncyCastleProvider");
+ Security.addProvider(clazz.newInstance());
+ } catch (Exception e) {
+ throw new EncryptedDocumentException("Only the BouncyCastle provider supports your encryption settings - please add it to the classpath.");
+ }
+ }
+}
diff --git a/src/java/org/apache/poi/poifs/crypt/DataSpaceMapUtils.java b/src/java/org/apache/poi/poifs/crypt/DataSpaceMapUtils.java
new file mode 100644
index 0000000000..963151ff97
--- /dev/null
+++ b/src/java/org/apache/poi/poifs/crypt/DataSpaceMapUtils.java
@@ -0,0 +1,365 @@
+/* ====================================================================
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+==================================================================== */
+
+package org.apache.poi.poifs.crypt;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+
+import org.apache.poi.EncryptedDocumentException;
+import org.apache.poi.poifs.crypt.standard.EncryptionRecord;
+import org.apache.poi.poifs.filesystem.DirectoryEntry;
+import org.apache.poi.poifs.filesystem.DocumentEntry;
+import org.apache.poi.poifs.filesystem.POIFSWriterEvent;
+import org.apache.poi.poifs.filesystem.POIFSWriterListener;
+import org.apache.poi.util.LittleEndianByteArrayOutputStream;
+import org.apache.poi.util.LittleEndianConsts;
+import org.apache.poi.util.LittleEndianInput;
+import org.apache.poi.util.LittleEndianOutput;
+
+public class DataSpaceMapUtils {
+ public static void addDefaultDataSpace(DirectoryEntry dir) throws IOException {
+ DataSpaceMapEntry dsme = new DataSpaceMapEntry(
+ new int[]{ 0 }
+ , new String[]{ "EncryptedPackage" }
+ , "StrongEncryptionDataSpace"
+ );
+ DataSpaceMap dsm = new DataSpaceMap(new DataSpaceMapEntry[]{dsme});
+ createEncryptionEntry(dir, "\u0006DataSpaces/DataSpaceMap", dsm);
+
+ DataSpaceDefinition dsd = new DataSpaceDefinition(new String[]{ "StrongEncryptionTransform" });
+ createEncryptionEntry(dir, "\u0006DataSpaces/DataSpaceInfo/StrongEncryptionDataSpace", dsd);
+
+ TransformInfoHeader tih = new TransformInfoHeader(
+ 1
+ , "{FF9A3F03-56EF-4613-BDD5-5A41C1D07246}"
+ , "Microsoft.Container.EncryptionTransform"
+ , 1, 0, 1, 0, 1, 0
+ );
+ IRMDSTransformInfo irm = new IRMDSTransformInfo(tih, 0, null);
+ createEncryptionEntry(dir, "\u0006DataSpaces/TransformInfo/StrongEncryptionTransform/\u0006Primary", irm);
+
+ DataSpaceVersionInfo dsvi = new DataSpaceVersionInfo("Microsoft.Container.DataSpaces", 1, 0, 1, 0, 1, 0);
+ createEncryptionEntry(dir, "\u0006DataSpaces/Version", dsvi);
+ }
+
+ public static DocumentEntry createEncryptionEntry(DirectoryEntry dir, String path, EncryptionRecord out) throws IOException {
+ String parts[] = path.split("/");
+ for (int i=0; i<parts.length-1; i++) {
+ dir = dir.hasEntry(parts[i])
+ ? (DirectoryEntry)dir.getEntry(parts[i])
+ : dir.createDirectory(parts[i]);
+ }
+
+ final byte buf[] = new byte[5000];
+ LittleEndianByteArrayOutputStream bos = new LittleEndianByteArrayOutputStream(buf, 0);
+ out.write(bos);
+
+ return dir.createDocument(parts[parts.length-1], bos.getWriteIndex(), new POIFSWriterListener(){
+ public void processPOIFSWriterEvent(POIFSWriterEvent event) {
+ try {
+ event.getStream().write(buf, 0, event.getLimit());
+ } catch (IOException e) {
+ throw new EncryptedDocumentException(e);
+ }
+ }
+ });
+ }
+
+ public static class DataSpaceMap implements EncryptionRecord {
+ DataSpaceMapEntry entries[];
+
+ public DataSpaceMap(DataSpaceMapEntry entries[]) {
+ this.entries = entries;
+ }
+
+ public DataSpaceMap(LittleEndianInput is) {
+ @SuppressWarnings("unused")
+ int length = is.readInt();
+ int entryCount = is.readInt();
+ entries = new DataSpaceMapEntry[entryCount];
+ for (int i=0; i<entryCount; i++) {
+ entries[i] = new DataSpaceMapEntry(is);
+ }
+ }
+
+ public void write(LittleEndianByteArrayOutputStream os) {
+ os.writeInt(8);
+ os.writeInt(entries.length);
+ for (DataSpaceMapEntry dsme : entries) {
+ dsme.write(os);
+ }
+ }
+ }
+
+ public static class DataSpaceMapEntry implements EncryptionRecord {
+ int referenceComponentType[];
+ String referenceComponent[];
+ String dataSpaceName;
+
+ public DataSpaceMapEntry(int referenceComponentType[], String referenceComponent[], String dataSpaceName) {
+ this.referenceComponentType = referenceComponentType;
+ this.referenceComponent = referenceComponent;
+ this.dataSpaceName = dataSpaceName;
+ }
+
+ public DataSpaceMapEntry(LittleEndianInput is) {
+ @SuppressWarnings("unused")
+ int length = is.readInt();
+ int referenceComponentCount = is.readInt();
+ referenceComponentType = new int[referenceComponentCount];
+ referenceComponent = new String[referenceComponentCount];
+ for (int i=0; i<referenceComponentCount; i++) {
+ referenceComponentType[i] = is.readInt();
+ referenceComponent[i] = readUnicodeLPP4(is);
+ }
+ dataSpaceName = readUnicodeLPP4(is);
+ }
+
+ public void write(LittleEndianByteArrayOutputStream os) {
+ int start = os.getWriteIndex();
+ LittleEndianOutput sizeOut = os.createDelayedOutput(LittleEndianConsts.INT_SIZE);
+ os.writeInt(referenceComponent.length);
+ for (int i=0; i<referenceComponent.length; i++) {
+ os.writeInt(referenceComponentType[i]);
+ writeUnicodeLPP4(os, referenceComponent[i]);
+ }
+ writeUnicodeLPP4(os, dataSpaceName);
+ sizeOut.writeInt(os.getWriteIndex()-start);
+ }
+ }
+
+ public static class DataSpaceDefinition implements EncryptionRecord {
+ String transformer[];
+
+ public DataSpaceDefinition(String transformer[]) {
+ this.transformer = transformer;
+ }
+
+ public DataSpaceDefinition(LittleEndianInput is) {
+ @SuppressWarnings("unused")
+ int headerLength = is.readInt();
+ int transformReferenceCount = is.readInt();
+ transformer = new String[transformReferenceCount];
+ for (int i=0; i<transformReferenceCount; i++) {
+ transformer[i] = readUnicodeLPP4(is);
+ }
+ }
+
+ public void write(LittleEndianByteArrayOutputStream bos) {
+ bos.writeInt(8);
+ bos.writeInt(transformer.length);
+ for (String str : transformer) {
+ writeUnicodeLPP4(bos, str);
+ }
+ }
+ }
+
+ public static class IRMDSTransformInfo implements EncryptionRecord {
+ TransformInfoHeader transformInfoHeader;
+ int extensibilityHeader;
+ String xrMLLicense;
+
+ public IRMDSTransformInfo(TransformInfoHeader transformInfoHeader, int extensibilityHeader, String xrMLLicense) {
+ this.transformInfoHeader = transformInfoHeader;
+ this.extensibilityHeader = extensibilityHeader;
+ this.xrMLLicense = xrMLLicense;
+ }
+
+ public IRMDSTransformInfo(LittleEndianInput is) {
+ transformInfoHeader = new TransformInfoHeader(is);
+ extensibilityHeader = is.readInt();
+ xrMLLicense = readUtf8LPP4(is);
+ // finish with 0x04 (int) ???
+ }
+
+ public void write(LittleEndianByteArrayOutputStream bos) {
+ transformInfoHeader.write(bos);
+ bos.writeInt(extensibilityHeader);
+ writeUtf8LPP4(bos, xrMLLicense);
+ bos.writeInt(4); // where does this 4 come from???
+ }
+ }
+
+ public static class TransformInfoHeader implements EncryptionRecord {
+ int transformType;
+ String transformerId;
+ String transformerName;
+ int readerVersionMajor = 1, readerVersionMinor = 0;
+ int updaterVersionMajor = 1, updaterVersionMinor = 0;
+ int writerVersionMajor = 1, writerVersionMinor = 0;
+
+ public TransformInfoHeader(
+ int transformType,
+ String transformerId,
+ String transformerName,
+ int readerVersionMajor, int readerVersionMinor,
+ int updaterVersionMajor, int updaterVersionMinor,
+ int writerVersionMajor, int writerVersionMinor
+ ){
+ this.transformType = transformType;
+ this.transformerId = transformerId;
+ this.transformerName = transformerName;
+ this.readerVersionMajor = readerVersionMajor;
+ this.readerVersionMinor = readerVersionMinor;
+ this.updaterVersionMajor = updaterVersionMajor;
+ this.updaterVersionMinor = updaterVersionMinor;
+ this.writerVersionMajor = writerVersionMajor;
+ this.writerVersionMinor = writerVersionMinor;
+ }
+
+ public TransformInfoHeader(LittleEndianInput is) {
+ @SuppressWarnings("unused")
+ int length = is.readInt();
+ transformType = is.readInt();
+ transformerId = readUnicodeLPP4(is);
+ transformerName = readUnicodeLPP4(is);
+ readerVersionMajor = is.readShort();
+ readerVersionMinor = is.readShort();
+ updaterVersionMajor = is.readShort();
+ updaterVersionMinor = is.readShort();
+ writerVersionMajor = is.readShort();
+ writerVersionMinor = is.readShort();
+ }
+
+ public void write(LittleEndianByteArrayOutputStream bos) {
+ int start = bos.getWriteIndex();
+ LittleEndianOutput sizeOut = bos.createDelayedOutput(LittleEndianConsts.INT_SIZE);
+ bos.writeInt(transformType);
+ writeUnicodeLPP4(bos, transformerId);
+ sizeOut.writeInt(bos.getWriteIndex()-start);
+ writeUnicodeLPP4(bos, transformerName);
+ bos.writeShort(readerVersionMajor);
+ bos.writeShort(readerVersionMinor);
+ bos.writeShort(updaterVersionMajor);
+ bos.writeShort(updaterVersionMinor);
+ bos.writeShort(writerVersionMajor);
+ bos.writeShort(writerVersionMinor);
+ }
+ }
+
+ public static class DataSpaceVersionInfo implements EncryptionRecord {
+ String featureIdentifier;
+ int readerVersionMajor = 1, readerVersionMinor = 0;
+ int updaterVersionMajor = 1, updaterVersionMinor = 0;
+ int writerVersionMajor = 1, writerVersionMinor = 0;
+
+ public DataSpaceVersionInfo(LittleEndianInput is) {
+ featureIdentifier = readUnicodeLPP4(is);
+ readerVersionMajor = is.readShort();
+ readerVersionMinor = is.readShort();
+ updaterVersionMajor = is.readShort();
+ updaterVersionMinor = is.readShort();
+ writerVersionMajor = is.readShort();
+ writerVersionMinor = is.readShort();
+ }
+
+ public DataSpaceVersionInfo(
+ String featureIdentifier,
+ int readerVersionMajor, int readerVersionMinor,
+ int updaterVersionMajor, int updaterVersionMinor,
+ int writerVersionMajor, int writerVersionMinor
+ ){
+ this.featureIdentifier = featureIdentifier;
+ this.readerVersionMajor = readerVersionMajor;
+ this.readerVersionMinor = readerVersionMinor;
+ this.updaterVersionMajor = updaterVersionMajor;
+ this.updaterVersionMinor = updaterVersionMinor;
+ this.writerVersionMajor = writerVersionMajor;
+ this.writerVersionMinor = writerVersionMinor;
+ }
+
+ public void write(LittleEndianByteArrayOutputStream bos) {
+ writeUnicodeLPP4(bos, featureIdentifier);
+ bos.writeShort(readerVersionMajor);
+ bos.writeShort(readerVersionMinor);
+ bos.writeShort(updaterVersionMajor);
+ bos.writeShort(updaterVersionMinor);
+ bos.writeShort(writerVersionMajor);
+ bos.writeShort(writerVersionMinor);
+ }
+ }
+
+ public static String readUnicodeLPP4(LittleEndianInput is) {
+ Charset cs = Charset.forName("UTF-16LE");
+ int length = is.readInt();
+ byte data[] = new byte[length];
+ is.readFully(data);
+ if (length%4==2) {
+ // Padding (variable): A set of bytes that MUST be of the correct size such that the size of the
+ // UNICODE-LP-P4 structure is a multiple of 4 bytes. If Padding is present, it MUST be exactly
+ // 2 bytes long, and each byte MUST be 0x00.
+ is.readShort();
+ }
+ return new String(data, 0, data.length, cs);
+ }
+
+ public static void writeUnicodeLPP4(LittleEndianOutput os, String str) {
+ Charset cs = Charset.forName("UTF-16LE");
+ byte buf[] = str.getBytes(cs);
+ os.writeInt(buf.length);
+ os.write(buf);
+ if (buf.length%4==2) {
+ os.writeShort(0);
+ }
+ }
+
+ public static String readUtf8LPP4(LittleEndianInput is) {
+ int length = is.readInt();
+ if (length == 0 || length == 4) {
+ @SuppressWarnings("unused")
+ int skip = is.readInt(); // ignore
+ return length == 0 ? null : "";
+ }
+
+ byte data[] = new byte[length];
+ is.readFully(data);
+
+ // Padding (variable): A set of bytes that MUST be of correct size such that the size of the UTF-8-LP-P4
+ // structure is a multiple of 4 bytes. If Padding is present, each byte MUST be 0x00. If
+ // the length is exactly 0x00000000, this specifies a null string, and the entire structure uses
+ // exactly 4 bytes. If the length is exactly 0x00000004, this specifies an empty string, and the
+ // entire structure also uses exactly 4 bytes
+ int scratchedBytes = length%4;
+ if (scratchedBytes > 0) {
+ for (int i=0; i<(4-scratchedBytes); i++) {
+ is.readByte();
+ }
+ }
+ Charset cs = Charset.forName("UTF-8");
+ return new String(data, 0, data.length, cs);
+ }
+
+ public static void writeUtf8LPP4(LittleEndianOutput os, String str) {
+ if (str == null || "".equals(str)) {
+ os.writeInt(str == null ? 0 : 4);
+ os.writeInt(0);
+ } else {
+ Charset cs = Charset.forName("UTF-8");
+ byte buf[] = str.getBytes(cs);
+ os.writeInt(buf.length);
+ os.write(buf);
+ int scratchBytes = buf.length%4;
+ if (scratchBytes > 0) {
+ for (int i=0; i<(4-scratchBytes); i++) {
+ os.writeByte(0);
+ }
+ }
+ }
+ }
+
+}
diff --git a/src/java/org/apache/poi/poifs/crypt/Decryptor.java b/src/java/org/apache/poi/poifs/crypt/Decryptor.java
index 39876f2f4c..c2d0d5953b 100644
--- a/src/java/org/apache/poi/poifs/crypt/Decryptor.java
+++ b/src/java/org/apache/poi/poifs/crypt/Decryptor.java
@@ -18,21 +18,26 @@ package org.apache.poi.poifs.crypt;
import java.io.IOException;
import java.io.InputStream;
-import java.io.UnsupportedEncodingException;
-import java.security.DigestException;
-import java.security.MessageDigest;
import java.security.GeneralSecurityException;
-import java.security.NoSuchAlgorithmException;
+
+import javax.crypto.SecretKey;
+
+import org.apache.poi.EncryptedDocumentException;
+import org.apache.poi.poifs.filesystem.DirectoryNode;
import org.apache.poi.poifs.filesystem.NPOIFSFileSystem;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
-import org.apache.poi.poifs.filesystem.DirectoryNode;
-import org.apache.poi.EncryptedDocumentException;
-import org.apache.poi.util.LittleEndian;
-import org.apache.poi.util.LittleEndianConsts;
public abstract class Decryptor {
public static final String DEFAULT_PASSWORD="VelvetSweatshop";
+
+ protected final EncryptionInfo info;
+ private SecretKey secretKey;
+ private byte[] verifier, integrityHmacKey, integrityHmacValue;
+ protected Decryptor(EncryptionInfo info) {
+ this.info = info;
+ }
+
/**
* Return a stream with decrypted data.
* <p>
@@ -68,15 +73,11 @@ public abstract class Decryptor {
public abstract long getLength();
public static Decryptor getInstance(EncryptionInfo info) {
- int major = info.getVersionMajor();
- int minor = info.getVersionMinor();
-
- if (major == 4 && minor == 4)
- return new AgileDecryptor(info);
- else if (minor == 2 && (major == 3 || major == 4))
- return new EcmaDecryptor(info);
- else
+ Decryptor d = info.getDecryptor();
+ if (d == null) {
throw new EncryptedDocumentException("Unsupported version");
+ }
+ return d;
}
public InputStream getDataStream(NPOIFSFileSystem fs) throws IOException, GeneralSecurityException {
@@ -86,40 +87,37 @@ public abstract class Decryptor {
public InputStream getDataStream(POIFSFileSystem fs) throws IOException, GeneralSecurityException {
return getDataStream(fs.getRoot());
}
+
+ // for tests
+ public byte[] getVerifier() {
+ return verifier;
+ }
- protected byte[] hashPassword(EncryptionInfo info,
- String password) throws NoSuchAlgorithmException {
- // If no password was given, use the default
- if (password == null) {
- password = DEFAULT_PASSWORD;
- }
-
- byte[] pass;
- try {
- pass = password.getBytes("UTF-16LE");
- } catch (UnsupportedEncodingException e) {
- throw new EncryptedDocumentException("UTF16 not supported");
- }
+ public SecretKey getSecretKey() {
+ return secretKey;
+ }
+
+ public byte[] getIntegrityHmacKey() {
+ return integrityHmacKey;
+ }
- byte[] salt = info.getVerifier().getSalt();
-
- MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
- sha1.update(salt);
- byte[] hash = sha1.digest(pass);
- byte[] iterator = new byte[LittleEndianConsts.INT_SIZE];
-
- try {
- for (int i = 0; i < info.getVerifier().getSpinCount(); i++) {
- LittleEndian.putInt(iterator, 0, i);
- sha1.reset();
- sha1.update(iterator);
- sha1.update(hash);
- sha1.digest(hash, 0, hash.length); // don't create hash buffer everytime new
- }
- } catch (DigestException e) {
- throw new EncryptedDocumentException("error in password hashing");
- }
-
- return hash;
+ public byte[] getIntegrityHmacValue() {
+ return integrityHmacValue;
+ }
+
+ protected void setSecretKey(SecretKey secretKey) {
+ this.secretKey = secretKey;
+ }
+
+ protected void setVerifier(byte[] verifier) {
+ this.verifier = verifier;
+ }
+
+ protected void setIntegrityHmacKey(byte[] integrityHmacKey) {
+ this.integrityHmacKey = integrityHmacKey;
+ }
+
+ protected void setIntegrityHmacValue(byte[] integrityHmacValue) {
+ this.integrityHmacValue = integrityHmacValue;
}
} \ No newline at end of file
diff --git a/src/java/org/apache/poi/poifs/crypt/EcmaDecryptor.java b/src/java/org/apache/poi/poifs/crypt/EcmaDecryptor.java
deleted file mode 100644
index 65e9be9089..0000000000
--- a/src/java/org/apache/poi/poifs/crypt/EcmaDecryptor.java
+++ /dev/null
@@ -1,134 +0,0 @@
-/* ====================================================================
- Licensed to the Apache Software Foundation (ASF) under one or more
- contributor license agreements. See the NOTICE file distributed with
- this work for additional information regarding copyright ownership.
- The ASF licenses this file to You under the Apache License, Version 2.0
- (the "License"); you may not use this file except in compliance with
- the License. You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-==================================================================== */
-package org.apache.poi.poifs.crypt;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.security.GeneralSecurityException;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.util.Arrays;
-
-import javax.crypto.Cipher;
-import javax.crypto.CipherInputStream;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.SecretKeySpec;
-
-import org.apache.poi.poifs.filesystem.DirectoryNode;
-import org.apache.poi.poifs.filesystem.DocumentInputStream;
-import org.apache.poi.util.LittleEndian;
-
-/**
- */
-public class EcmaDecryptor extends Decryptor {
- private final EncryptionInfo info;
- private byte[] passwordHash;
- private long _length = -1;
-
- public EcmaDecryptor(EncryptionInfo info) {
- this.info = info;
- }
-
- private byte[] generateKey(int block) throws NoSuchAlgorithmException {
- MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
-
- sha1.update(passwordHash);
- byte[] blockValue = new byte[4];
- LittleEndian.putInt(blockValue, 0, block);
- byte[] finalHash = sha1.digest(blockValue);
-
- int requiredKeyLength = info.getHeader().getKeySize()/8;
-
- byte[] buff = new byte[64];
-
- Arrays.fill(buff, (byte) 0x36);
-
- for (int i=0; i<finalHash.length; i++) {
- buff[i] = (byte) (buff[i] ^ finalHash[i]);
- }
-
- sha1.reset();
- byte[] x1 = sha1.digest(buff);
-
- Arrays.fill(buff, (byte) 0x5c);
- for (int i=0; i<finalHash.length; i++) {
- buff[i] = (byte) (buff[i] ^ finalHash[i]);
- }
-
- sha1.reset();
- byte[] x2 = sha1.digest(buff);
-
- byte[] x3 = new byte[x1.length + x2.length];
- System.arraycopy(x1, 0, x3, 0, x1.length);
- System.arraycopy(x2, 0, x3, x1.length, x2.length);
-
- return truncateOrPad(x3, requiredKeyLength);
- }
-
- public boolean verifyPassword(String password) throws GeneralSecurityException {
- passwordHash = hashPassword(info, password);
-
- Cipher cipher = getCipher();
-
- byte[] verifier = cipher.doFinal(info.getVerifier().getVerifier());
-
- MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
- byte[] calcVerifierHash = sha1.digest(verifier);
-
- byte[] verifierHash = truncateOrPad(cipher.doFinal(info.getVerifier().getVerifierHash()), calcVerifierHash.length);
-
- return Arrays.equals(calcVerifierHash, verifierHash);
- }
-
- /**
- * Returns a byte array of the requested length,
- * truncated or zero padded as needed.
- * Behaves like Arrays.copyOf in Java 1.6
- */
- private byte[] truncateOrPad(byte[] source, int length) {
- byte[] result = new byte[length];
- System.arraycopy(source, 0, result, 0, Math.min(length, source.length));
- if(length > source.length) {
- for(int i=source.length; i<length; i++) {
- result[i] = 0;
- }
- }
- return result;
- }
-
- private Cipher getCipher() throws GeneralSecurityException {
- byte[] key = generateKey(0);
- Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
- SecretKey skey = new SecretKeySpec(key, "AES");
- cipher.init(Cipher.DECRYPT_MODE, skey);
-
- return cipher;
- }
-
- public InputStream getDataStream(DirectoryNode dir) throws IOException, GeneralSecurityException {
- DocumentInputStream dis = dir.createDocumentInputStream("EncryptedPackage");
-
- _length = dis.readLong();
-
- return new CipherInputStream(dis, getCipher());
- }
-
- public long getLength(){
- if(_length == -1) throw new IllegalStateException("EcmaDecryptor.getDataStream() was not called");
- return _length;
- }
-}
diff --git a/src/java/org/apache/poi/poifs/crypt/EncryptionHeader.java b/src/java/org/apache/poi/poifs/crypt/EncryptionHeader.java
index e04c862363..adcf4c4275 100644
--- a/src/java/org/apache/poi/poifs/crypt/EncryptionHeader.java
+++ b/src/java/org/apache/poi/poifs/crypt/EncryptionHeader.java
@@ -16,197 +16,148 @@
==================================================================== */
package org.apache.poi.poifs.crypt;
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-
-import javax.xml.parsers.DocumentBuilderFactory;
-
-import org.apache.commons.codec.binary.Base64;
-import org.apache.poi.EncryptedDocumentException;
-import org.apache.poi.poifs.filesystem.DocumentInputStream;
-import org.apache.poi.util.LittleEndianConsts;
-import org.w3c.dom.NamedNodeMap;
/**
* Reads and processes OOXML Encryption Headers
* The constants are largely based on ZIP constants.
*/
-public class EncryptionHeader {
- public static final int ALGORITHM_RC4 = 0x6801;
- public static final int ALGORITHM_AES_128 = 0x660E;
- public static final int ALGORITHM_AES_192 = 0x660F;
- public static final int ALGORITHM_AES_256 = 0x6610;
-
- public static final int HASH_NONE = 0x0000;
- public static final int HASH_SHA1 = 0x8004;
- public static final int HASH_SHA256 = 0x800C;
- public static final int HASH_SHA384 = 0x800D;
- public static final int HASH_SHA512 = 0x800E;
-
- public static final int PROVIDER_RC4 = 1;
- public static final int PROVIDER_AES = 0x18;
-
- public static final int MODE_ECB = 1;
- public static final int MODE_CBC = 2;
- public static final int MODE_CFB = 3;
-
- private final int flags;
- private final int sizeExtra;
- private final int algorithm;
- private final int hashAlgorithm;
- private final int keySize;
- private final int blockSize;
- private final int providerType;
- private final int cipherMode;
- private final byte[] keySalt;
- private final String cspName;
-
- public EncryptionHeader(DocumentInputStream is) throws IOException {
- flags = is.readInt();
- sizeExtra = is.readInt();
- algorithm = is.readInt();
- hashAlgorithm = is.readInt();
- keySize = is.readInt();
- blockSize = keySize;
- providerType = is.readInt();
-
- is.readLong(); // skip reserved
-
- // CSPName may not always be specified
- // In some cases, the sale value of the EncryptionVerifier has the details
- is.mark(LittleEndianConsts.INT_SIZE+1);
- int checkForSalt = is.readInt();
- is.reset();
-
- if (checkForSalt == 16) {
- cspName = "";
- } else {
- StringBuilder builder = new StringBuilder();
- while (true) {
- char c = (char) is.readShort();
- if (c == 0) break;
- builder.append(c);
- }
- cspName = builder.toString();
- }
-
- cipherMode = MODE_ECB;
- keySalt = null;
- }
-
- public EncryptionHeader(String descriptor) throws IOException {
- NamedNodeMap keyData;
- try {
- ByteArrayInputStream is;
- is = new ByteArrayInputStream(descriptor.getBytes());
- keyData = DocumentBuilderFactory.newInstance()
- .newDocumentBuilder().parse(is)
- .getElementsByTagName("keyData").item(0).getAttributes();
- } catch (Exception e) {
- throw new EncryptedDocumentException("Unable to parse keyData");
- }
-
- keySize = Integer.parseInt(keyData.getNamedItem("keyBits")
- .getNodeValue());
- flags = 0;
- sizeExtra = 0;
- cspName = null;
-
- blockSize = Integer.parseInt(keyData.getNamedItem("blockSize").
- getNodeValue());
- String cipher = keyData.getNamedItem("cipherAlgorithm").getNodeValue();
-
- if ("AES".equals(cipher)) {
- providerType = PROVIDER_AES;
- switch (keySize) {
- case 128:
- algorithm = ALGORITHM_AES_128; break;
- case 192:
- algorithm = ALGORITHM_AES_192; break;
- case 256:
- algorithm = ALGORITHM_AES_256; break;
- default:
- throw new EncryptedDocumentException("Unsupported key length " + keySize);
- }
- } else {
- throw new EncryptedDocumentException("Unsupported cipher " + cipher);
- }
-
- String chaining = keyData.getNamedItem("cipherChaining").getNodeValue();
-
- if ("ChainingModeCBC".equals(chaining))
- cipherMode = MODE_CBC;
- else if ("ChainingModeCFB".equals(chaining))
- cipherMode = MODE_CFB;
- else
- throw new EncryptedDocumentException("Unsupported chaining mode " + chaining);
-
- String hashAlg = keyData.getNamedItem("hashAlgorithm").getNodeValue();
- int hashSize = Integer.parseInt(
- keyData.getNamedItem("hashSize").getNodeValue());
-
- if ("SHA1".equals(hashAlg) && hashSize == 20) {
- hashAlgorithm = HASH_SHA1;
- }
- else if ("SHA256".equals(hashAlg) && hashSize == 32) {
- hashAlgorithm = HASH_SHA256;
- }
- else if ("SHA384".equals(hashAlg) && hashSize == 64) {
- hashAlgorithm = HASH_SHA384;
- }
- else if ("SHA512".equals(hashAlg) && hashSize == 64) {
- hashAlgorithm = HASH_SHA512;
- }
- else {
- throw new EncryptedDocumentException("Unsupported hash algorithm: " +
- hashAlg + " @ " + hashSize + " bytes");
- }
-
- String salt = keyData.getNamedItem("saltValue").getNodeValue();
- int saltLength = Integer.parseInt(keyData.getNamedItem("saltSize")
- .getNodeValue());
- keySalt = Base64.decodeBase64(salt.getBytes());
- if (keySalt.length != saltLength)
- throw new EncryptedDocumentException("Invalid salt length");
- }
+public abstract class EncryptionHeader {
+ public static final int ALGORITHM_RC4 = CipherAlgorithm.rc4.ecmaId;
+ public static final int ALGORITHM_AES_128 = CipherAlgorithm.aes128.ecmaId;
+ public static final int ALGORITHM_AES_192 = CipherAlgorithm.aes192.ecmaId;
+ public static final int ALGORITHM_AES_256 = CipherAlgorithm.aes256.ecmaId;
+
+ public static final int HASH_NONE = HashAlgorithm.none.ecmaId;
+ public static final int HASH_SHA1 = HashAlgorithm.sha1.ecmaId;
+ public static final int HASH_SHA256 = HashAlgorithm.sha256.ecmaId;
+ public static final int HASH_SHA384 = HashAlgorithm.sha384.ecmaId;
+ public static final int HASH_SHA512 = HashAlgorithm.sha512.ecmaId;
+
+ public static final int PROVIDER_RC4 = CipherProvider.rc4.ecmaId;
+ public static final int PROVIDER_AES = CipherProvider.aes.ecmaId;
+
+ public static final int MODE_ECB = ChainingMode.ecb.ecmaId;
+ public static final int MODE_CBC = ChainingMode.cbc.ecmaId;
+ public static final int MODE_CFB = ChainingMode.cfb.ecmaId;
+
+ private int flags;
+ private int sizeExtra;
+ private CipherAlgorithm cipherAlgorithm;
+ private HashAlgorithm hashAlgorithm;
+ private int keyBits;
+ private int blockSize;
+ private CipherProvider providerType;
+ private ChainingMode chainingMode;
+ private byte[] keySalt;
+ private String cspName;
+
+ protected EncryptionHeader() {}
+ /**
+ * @deprecated use getChainingMode().ecmaId
+ */
public int getCipherMode() {
- return cipherMode;
+ return chainingMode.ecmaId;
+ }
+
+ public ChainingMode getChainingMode() {
+ return chainingMode;
+ }
+
+ protected void setChainingMode(ChainingMode chainingMode) {
+ this.chainingMode = chainingMode;
}
public int getFlags() {
return flags;
}
+
+ protected void setFlags(int flags) {
+ this.flags = flags;
+ }
public int getSizeExtra() {
return sizeExtra;
}
+
+ protected void setSizeExtra(int sizeExtra) {
+ this.sizeExtra = sizeExtra;
+ }
+ /**
+ * @deprecated use getCipherAlgorithm()
+ */
public int getAlgorithm() {
- return algorithm;
+ return cipherAlgorithm.ecmaId;
}
+ public CipherAlgorithm getCipherAlgorithm() {
+ return cipherAlgorithm;
+ }
+
+ protected void setCipherAlgorithm(CipherAlgorithm cipherAlgorithm) {
+ this.cipherAlgorithm = cipherAlgorithm;
+ }
+
+ /**
+ * @deprecated use getHashAlgorithmEx()
+ */
public int getHashAlgorithm() {
+ return hashAlgorithm.ecmaId;
+ }
+
+ public HashAlgorithm getHashAlgorithmEx() {
return hashAlgorithm;
}
+
+ protected void setHashAlgorithm(HashAlgorithm hashAlgorithm) {
+ this.hashAlgorithm = hashAlgorithm;
+ }
public int getKeySize() {
- return keySize;
+ return keyBits;
+ }
+
+ protected void setKeySize(int keyBits) {
+ this.keyBits = keyBits;
}
public int getBlockSize() {
return blockSize;
}
+ protected void setBlockSize(int blockSize) {
+ this.blockSize = blockSize;
+ }
+
public byte[] getKeySalt() {
return keySalt;
}
+
+ protected void setKeySalt(byte salt[]) {
+ this.keySalt = salt;
+ }
+ /**
+ * @deprecated use getCipherProvider()
+ */
public int getProviderType() {
- return providerType;
+ return providerType.ecmaId;
}
+ public CipherProvider getCipherProvider() {
+ return providerType;
+ }
+
+ protected void setCipherProvider(CipherProvider providerType) {
+ this.providerType = providerType;
+ }
+
public String getCspName() {
return cspName;
}
+
+ protected void setCspName(String cspName) {
+ this.cspName = cspName;
+ }
}
diff --git a/src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java b/src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java
index b6dd0f0b29..f2c1608112 100644
--- a/src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java
+++ b/src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java
@@ -16,56 +16,141 @@
==================================================================== */
package org.apache.poi.poifs.crypt;
+import static org.apache.poi.poifs.crypt.EncryptionMode.agile;
+import static org.apache.poi.poifs.crypt.EncryptionMode.standard;
+
+import java.io.IOException;
+
+import org.apache.poi.EncryptedDocumentException;
import org.apache.poi.poifs.filesystem.DirectoryNode;
import org.apache.poi.poifs.filesystem.DocumentInputStream;
import org.apache.poi.poifs.filesystem.NPOIFSFileSystem;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
-import java.io.IOException;
-
/**
*/
public class EncryptionInfo {
private final int versionMajor;
private final int versionMinor;
private final int encryptionFlags;
-
+
private final EncryptionHeader header;
private final EncryptionVerifier verifier;
+ private final Decryptor decryptor;
+ private final Encryptor encryptor;
public EncryptionInfo(POIFSFileSystem fs) throws IOException {
this(fs.getRoot());
}
+
public EncryptionInfo(NPOIFSFileSystem fs) throws IOException {
this(fs.getRoot());
}
+
public EncryptionInfo(DirectoryNode dir) throws IOException {
DocumentInputStream dis = dir.createDocumentInputStream("EncryptionInfo");
versionMajor = dis.readShort();
versionMinor = dis.readShort();
-
encryptionFlags = dis.readInt();
-
- if (versionMajor == 4 && versionMinor == 4 && encryptionFlags == 0x40) {
- 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 EncryptionHeader(descriptor);
- verifier = new EncryptionVerifier(descriptor);
+
+ EncryptionMode encryptionMode;
+ if (versionMajor == agile.versionMajor
+ && versionMinor == agile.versionMinor
+ && encryptionFlags == agile.encryptionFlags) {
+ encryptionMode = agile;
} else {
- int hSize = dis.readInt();
- header = new EncryptionHeader(dis);
- if (header.getAlgorithm()==EncryptionHeader.ALGORITHM_RC4) {
- verifier = new EncryptionVerifier(dis, 20);
- } else {
- verifier = new EncryptionVerifier(dis, 32);
- }
+ encryptionMode = standard;
+ }
+
+ EncryptionInfoBuilder eib;
+ try {
+ eib = getBuilder(encryptionMode);
+ } catch (ReflectiveOperationException e) {
+ throw new IOException(e);
}
+
+ eib.initialize(this, dis);
+ header = eib.getHeader();
+ verifier = eib.getVerifier();
+ decryptor = eib.getDecryptor();
+ encryptor = eib.getEncryptor();
}
+ public EncryptionInfo(POIFSFileSystem fs, EncryptionMode encryptionMode) throws IOException {
+ this(fs.getRoot(), encryptionMode);
+ }
+
+ public EncryptionInfo(NPOIFSFileSystem fs, EncryptionMode encryptionMode) throws IOException {
+ this(fs.getRoot(), encryptionMode);
+ }
+
+ public EncryptionInfo(
+ DirectoryNode dir
+ , EncryptionMode encryptionMode
+ ) throws EncryptedDocumentException {
+ this(dir, encryptionMode, null, null, -1, -1, null);
+ }
+
+ public EncryptionInfo(
+ POIFSFileSystem fs
+ , EncryptionMode encryptionMode
+ , CipherAlgorithm cipherAlgorithm
+ , HashAlgorithm hashAlgorithm
+ , int keyBits
+ , int blockSize
+ , ChainingMode chainingMode
+ ) throws EncryptedDocumentException {
+ this(fs.getRoot(), encryptionMode, cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode);
+ }
+
+ public EncryptionInfo(
+ NPOIFSFileSystem fs
+ , EncryptionMode encryptionMode
+ , CipherAlgorithm cipherAlgorithm
+ , HashAlgorithm hashAlgorithm
+ , int keyBits
+ , int blockSize
+ , ChainingMode chainingMode
+ ) throws EncryptedDocumentException {
+ this(fs.getRoot(), encryptionMode, cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode);
+ }
+
+ public EncryptionInfo(
+ DirectoryNode dir
+ , EncryptionMode encryptionMode
+ , CipherAlgorithm cipherAlgorithm
+ , HashAlgorithm hashAlgorithm
+ , int keyBits
+ , int blockSize
+ , ChainingMode chainingMode
+ ) throws EncryptedDocumentException {
+ versionMajor = encryptionMode.versionMajor;
+ versionMinor = encryptionMode.versionMinor;
+ encryptionFlags = encryptionMode.encryptionFlags;
+
+ EncryptionInfoBuilder eib;
+ try {
+ eib = getBuilder(encryptionMode);
+ } catch (ReflectiveOperationException e) {
+ throw new EncryptedDocumentException(e);
+ }
+
+ eib.initialize(this, cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode);
+
+ header = eib.getHeader();
+ verifier = eib.getVerifier();
+ decryptor = eib.getDecryptor();
+ encryptor = eib.getEncryptor();
+ }
+
+ protected static EncryptionInfoBuilder getBuilder(EncryptionMode encryptionMode)
+ throws ReflectiveOperationException {
+ ClassLoader cl = Thread.currentThread().getContextClassLoader();
+ EncryptionInfoBuilder eib;
+ eib = (EncryptionInfoBuilder)cl.loadClass(encryptionMode.builder).newInstance();
+ return eib;
+ }
+
public int getVersionMajor() {
return versionMajor;
}
@@ -85,4 +170,12 @@ public class EncryptionInfo {
public EncryptionVerifier getVerifier() {
return verifier;
}
+
+ public Decryptor getDecryptor() {
+ return decryptor;
+ }
+
+ public Encryptor getEncryptor() {
+ return encryptor;
+ }
}
diff --git a/src/java/org/apache/poi/poifs/crypt/EncryptionInfoBuilder.java b/src/java/org/apache/poi/poifs/crypt/EncryptionInfoBuilder.java
new file mode 100644
index 0000000000..0c31fc8fdc
--- /dev/null
+++ b/src/java/org/apache/poi/poifs/crypt/EncryptionInfoBuilder.java
@@ -0,0 +1,30 @@
+/* ====================================================================
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+==================================================================== */
+package org.apache.poi.poifs.crypt;
+
+import java.io.IOException;
+
+import org.apache.poi.poifs.filesystem.DocumentInputStream;
+
+public interface EncryptionInfoBuilder {
+ void initialize(EncryptionInfo ei, DocumentInputStream dis) throws IOException;
+ void initialize(EncryptionInfo ei, CipherAlgorithm cipherAlgorithm, HashAlgorithm hashAlgorithm, int keyBits, int blockSize, ChainingMode chainingMode);
+ EncryptionHeader getHeader();
+ EncryptionVerifier getVerifier();
+ Decryptor getDecryptor();
+ Encryptor getEncryptor();
+}
diff --git a/src/java/org/apache/poi/poifs/crypt/EncryptionMode.java b/src/java/org/apache/poi/poifs/crypt/EncryptionMode.java
new file mode 100644
index 0000000000..4d9114573f
--- /dev/null
+++ b/src/java/org/apache/poi/poifs/crypt/EncryptionMode.java
@@ -0,0 +1,35 @@
+/* ====================================================================
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+==================================================================== */
+
+package org.apache.poi.poifs.crypt;
+
+public enum EncryptionMode {
+ standard("org.apache.poi.poifs.crypt.standard.StandardEncryptionInfoBuilder", 4, 2, 0x24)
+ , agile("org.apache.poi.poifs.crypt.agile.AgileEncryptionInfoBuilder", 4, 4, 0x40);
+
+ public final String builder;
+ public final int versionMajor;
+ public final int versionMinor;
+ public final int encryptionFlags;
+
+ EncryptionMode(String builder, int versionMajor, int versionMinor, int encryptionFlags) {
+ this.builder = builder;
+ this.versionMajor = versionMajor;
+ this.versionMinor = versionMinor;
+ this.encryptionFlags = encryptionFlags;
+ }
+}
diff --git a/src/java/org/apache/poi/poifs/crypt/EncryptionVerifier.java b/src/java/org/apache/poi/poifs/crypt/EncryptionVerifier.java
index e29952bc9a..ecb90e08e2 100644
--- a/src/java/org/apache/poi/poifs/crypt/EncryptionVerifier.java
+++ b/src/java/org/apache/poi/poifs/crypt/EncryptionVerifier.java
@@ -16,155 +16,117 @@
==================================================================== */
package org.apache.poi.poifs.crypt;
-import java.io.ByteArrayInputStream;
-
-import javax.xml.parsers.DocumentBuilderFactory;
-
-import org.apache.commons.codec.binary.Base64;
-import org.apache.poi.EncryptedDocumentException;
-import org.apache.poi.poifs.filesystem.DocumentInputStream;
-import org.w3c.dom.NamedNodeMap;
-import org.w3c.dom.Node;
-import org.w3c.dom.NodeList;
/**
* Used when checking if a key is valid for a document
*/
-public class EncryptionVerifier {
- private final byte[] salt;
- private final byte[] verifier;
- private final byte[] verifierHash;
- private final byte[] encryptedKey;
- private final int verifierHashSize;
- private final int spinCount;
- private final int algorithm;
- private final int cipherMode;
-
- public EncryptionVerifier(String descriptor) {
- NamedNodeMap keyData = null;
- try {
- ByteArrayInputStream is;
- is = new ByteArrayInputStream(descriptor.getBytes());
- NodeList keyEncryptor = DocumentBuilderFactory.newInstance()
- .newDocumentBuilder().parse(is)
- .getElementsByTagName("keyEncryptor").item(0).getChildNodes();
- for (int i = 0; i < keyEncryptor.getLength(); i++) {
- Node node = keyEncryptor.item(i);
- if (node.getNodeName().equals("p:encryptedKey")) {
- keyData = node.getAttributes();
- break;
- }
- }
- if (keyData == null)
- throw new EncryptedDocumentException("");
- } catch (Exception e) {
- throw new EncryptedDocumentException("Unable to parse keyEncryptor");
- }
-
- spinCount = Integer.parseInt(keyData.getNamedItem("spinCount")
- .getNodeValue());
- verifier = Base64.decodeBase64(keyData
- .getNamedItem("encryptedVerifierHashInput")
- .getNodeValue().getBytes());
- salt = Base64.decodeBase64(keyData.getNamedItem("saltValue")
- .getNodeValue().getBytes());
-
- encryptedKey = Base64.decodeBase64(keyData
- .getNamedItem("encryptedKeyValue")
- .getNodeValue().getBytes());
-
- int saltSize = Integer.parseInt(keyData.getNamedItem("saltSize")
- .getNodeValue());
- if (saltSize != salt.length)
- throw new EncryptedDocumentException("Invalid salt size");
-
- verifierHash = Base64.decodeBase64(keyData
- .getNamedItem("encryptedVerifierHashValue")
- .getNodeValue().getBytes());
-
- int blockSize = Integer.parseInt(keyData.getNamedItem("blockSize")
- .getNodeValue());
-
- String alg = keyData.getNamedItem("cipherAlgorithm").getNodeValue();
-
- int keyBits = Integer.parseInt(keyData.getNamedItem("keyBits")
- .getNodeValue());
-
- if ("AES".equals(alg)) {
- switch (keyBits) {
- case 128:
- algorithm = EncryptionHeader.ALGORITHM_AES_128; break;
- case 192:
- algorithm = EncryptionHeader.ALGORITHM_AES_192; break;
- case 256:
- algorithm = EncryptionHeader.ALGORITHM_AES_256; break;
- default:
- throw new EncryptedDocumentException("Unsupported key size");
- }
- } else {
- throw new EncryptedDocumentException("Unsupported cipher");
- }
-
- String chain = keyData.getNamedItem("cipherChaining").getNodeValue();
- if ("ChainingModeCBC".equals(chain))
- cipherMode = EncryptionHeader.MODE_CBC;
- else if ("ChainingModeCFB".equals(chain))
- cipherMode = EncryptionHeader.MODE_CFB;
- else
- throw new EncryptedDocumentException("Unsupported chaining mode");
-
- verifierHashSize = Integer.parseInt(keyData.getNamedItem("hashSize")
- .getNodeValue());
- }
-
- public EncryptionVerifier(DocumentInputStream is, int encryptedLength) {
- int saltSize = is.readInt();
-
- if (saltSize!=16) {
- throw new RuntimeException("Salt size != 16 !?");
- }
-
- salt = new byte[16];
- is.readFully(salt);
- verifier = new byte[16];
- is.readFully(verifier);
-
- verifierHashSize = is.readInt();
-
- verifierHash = new byte[encryptedLength];
- is.readFully(verifierHash);
-
- spinCount = 50000;
- algorithm = EncryptionHeader.ALGORITHM_AES_128;
- cipherMode = EncryptionHeader.MODE_ECB;
- encryptedKey = null;
- }
+public abstract class EncryptionVerifier {
+ private byte[] salt;
+ private byte[] encryptedVerifier;
+ private byte[] encryptedVerifierHash;
+ private byte[] encryptedKey;
+ // protected int verifierHashSize;
+ private int spinCount;
+ private CipherAlgorithm cipherAlgorithm;
+ private ChainingMode chainingMode;
+ private HashAlgorithm hashAlgorithm;
+
+ protected EncryptionVerifier() {}
public byte[] getSalt() {
return salt;
}
+ /**
+ * The method name is misleading - you'll get the encrypted verifier, not the plain verifier
+ * @deprecated use getEncryptedVerifier()
+ */
public byte[] getVerifier() {
- return verifier;
+ return encryptedVerifier;
}
+ public byte[] getEncryptedVerifier() {
+ return encryptedVerifier;
+ }
+
+ /**
+ * The method name is misleading - you'll get the encrypted verifier hash, not the plain verifier hash
+ * @deprecated use getEnryptedVerifierHash
+ */
public byte[] getVerifierHash() {
- return verifierHash;
+ return encryptedVerifierHash;
}
+ public byte[] getEncryptedVerifierHash() {
+ return encryptedVerifierHash;
+ }
+
public int getSpinCount() {
return spinCount;
}
public int getCipherMode() {
- return cipherMode;
+ return chainingMode.ecmaId;
}
public int getAlgorithm() {
- return algorithm;
+ return cipherAlgorithm.ecmaId;
+ }
+
+ /**
+ * @deprecated use getCipherAlgorithm().jceId
+ */
+ public String getAlgorithmName() {
+ return cipherAlgorithm.jceId;
}
public byte[] getEncryptedKey() {
return encryptedKey;
}
+
+ public CipherAlgorithm getCipherAlgorithm() {
+ return cipherAlgorithm;
+ }
+
+ public HashAlgorithm getHashAlgorithm() {
+ return hashAlgorithm;
+ }
+
+ public ChainingMode getChainingMode() {
+ return chainingMode;
+ }
+
+ protected void setSalt(byte[] salt) {
+ this.salt = salt;
+ }
+
+ protected void setEncryptedVerifier(byte[] encryptedVerifier) {
+ this.encryptedVerifier = encryptedVerifier;
+ }
+
+ protected void setEncryptedVerifierHash(byte[] encryptedVerifierHash) {
+ this.encryptedVerifierHash = encryptedVerifierHash;
+ }
+
+ protected void setEncryptedKey(byte[] encryptedKey) {
+ this.encryptedKey = encryptedKey;
+ }
+
+ protected void setSpinCount(int spinCount) {
+ this.spinCount = spinCount;
+ }
+
+ protected void setCipherAlgorithm(CipherAlgorithm cipherAlgorithm) {
+ this.cipherAlgorithm = cipherAlgorithm;
+ }
+
+ protected void setChainingMode(ChainingMode chainingMode) {
+ this.chainingMode = chainingMode;
+ }
+
+ protected void setHashAlgorithm(HashAlgorithm hashAlgorithm) {
+ this.hashAlgorithm = hashAlgorithm;
+ }
+
+
}
diff --git a/src/java/org/apache/poi/poifs/crypt/Encryptor.java b/src/java/org/apache/poi/poifs/crypt/Encryptor.java
new file mode 100644
index 0000000000..abfd693306
--- /dev/null
+++ b/src/java/org/apache/poi/poifs/crypt/Encryptor.java
@@ -0,0 +1,65 @@
+/* ====================================================================
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+==================================================================== */
+package org.apache.poi.poifs.crypt;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.security.GeneralSecurityException;
+
+import javax.crypto.SecretKey;
+
+import org.apache.poi.poifs.filesystem.DirectoryNode;
+import org.apache.poi.poifs.filesystem.NPOIFSFileSystem;
+import org.apache.poi.poifs.filesystem.POIFSFileSystem;
+
+public abstract class Encryptor {
+ private SecretKey secretKey;
+
+ /**
+ * Return a output stream for encrypted data.
+ *
+ * @param dir the node to write to
+ * @return encrypted stream
+ */
+ public abstract OutputStream getDataStream(DirectoryNode dir)
+ throws IOException, GeneralSecurityException;
+
+ // for tests
+ public abstract void confirmPassword(String password, byte keySpec[], byte keySalt[], byte verifier[], byte verifierSalt[], byte integritySalt[]);
+
+ public abstract void confirmPassword(String password);
+
+ public static Encryptor getInstance(EncryptionInfo info) {
+ return info.getEncryptor();
+ }
+
+ public OutputStream getDataStream(NPOIFSFileSystem fs) throws IOException, GeneralSecurityException {
+ return getDataStream(fs.getRoot());
+ }
+
+ public OutputStream getDataStream(POIFSFileSystem fs) throws IOException, GeneralSecurityException {
+ return getDataStream(fs.getRoot());
+ }
+
+ public SecretKey getSecretKey() {
+ return secretKey;
+ }
+
+ protected void setSecretKey(SecretKey secretKey) {
+ this.secretKey = secretKey;
+ }
+}
diff --git a/src/java/org/apache/poi/poifs/crypt/HashAlgorithm.java b/src/java/org/apache/poi/poifs/crypt/HashAlgorithm.java
new file mode 100644
index 0000000000..cd62883ac4
--- /dev/null
+++ b/src/java/org/apache/poi/poifs/crypt/HashAlgorithm.java
@@ -0,0 +1,66 @@
+/* ====================================================================
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+==================================================================== */
+
+package org.apache.poi.poifs.crypt;
+
+import org.apache.poi.EncryptedDocumentException;
+
+public enum HashAlgorithm {
+ none ( "", 0x0000, "", 0, "", false),
+ sha1 ( "SHA-1", 0x8004, "SHA1", 20, "HmacSHA1", false),
+ sha256 ( "SHA-256", 0x800C, "SHA256", 32, "HmacSHA256", false),
+ sha384 ( "SHA-384", 0x800D, "SHA384", 48, "HmacSHA384", false),
+ sha512 ( "SHA-512", 0x800E, "SHA512", 64, "HmacSHA512", false),
+ /* only for agile encryption */
+ md5 ( "MD5", -1, "MD5", 16, "HmacMD5", false),
+ // although sunjc2 supports md2, hmac-md2 is only supported by bouncycastle
+ md2 ( "MD2", -1, "MD2", 16, "Hmac-MD2", true),
+ ripemd128("RipeMD128", -1, "RIPEMD-128", 16, "HMac-RipeMD128", true),
+ ripemd160("RipeMD160", -1, "RIPEMD-160", 20, "HMac-RipeMD160", true),
+ whirlpool("Whirlpool", -1, "WHIRLPOOL", 64, "HMac-Whirlpool", true),
+ ;
+
+ public final String jceId;
+ public final int ecmaId;
+ public final String ecmaString;
+ public final int hashSize;
+ public final String jceHmacId;
+ public final boolean needsBouncyCastle;
+
+ HashAlgorithm(String jceId, int ecmaId, String ecmaString, int hashSize, String jceHmacId, boolean needsBouncyCastle) {
+ this.jceId = jceId;
+ this.ecmaId = ecmaId;
+ this.ecmaString = ecmaString;
+ this.hashSize = hashSize;
+ this.jceHmacId = jceHmacId;
+ this.needsBouncyCastle = needsBouncyCastle;
+ }
+
+ public static HashAlgorithm fromEcmaId(int ecmaId) {
+ for (HashAlgorithm ha : values()) {
+ if (ha.ecmaId == ecmaId) return ha;
+ }
+ throw new EncryptedDocumentException("hash algorithm not found");
+ }
+
+ public static HashAlgorithm fromEcmaId(String ecmaString) {
+ for (HashAlgorithm ha : values()) {
+ if (ha.ecmaString.equals(ecmaString)) return ha;
+ }
+ throw new EncryptedDocumentException("hash algorithm not found");
+ }
+} \ No newline at end of file
diff --git a/src/java/org/apache/poi/poifs/crypt/package.html b/src/java/org/apache/poi/poifs/crypt/package.html
new file mode 100644
index 0000000000..969d5e1f8a
--- /dev/null
+++ b/src/java/org/apache/poi/poifs/crypt/package.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<!--
+ ====================================================================
+ 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.
+ ====================================================================
+-->
+<html>
+<head>
+</head>
+<body bgcolor="white">
+
+<p>Implementation of the <a href="http://msdn.microsoft.com/en-us/library/dd952186(v=office.12).aspx">ECMA-376 Document Encryption</a></p>
+<p>The implementation is split into three packages:</p>
+<ul>
+<li>This package contains common functions for both current implemented cipher modes.</li>
+<li>the {@link org.apache.poi.poifs.crypt.standard standard} package is part of the base poi jar and contains classes for the standard encryption ...</li>
+<li>the {@link org.apache.poi.poifs.crypt.agile agile} package is part of the poi ooxml jar and the provides agile encryption support.</li>
+</ul>
+
+<h2>Related Documentation</h2>
+
+Some implementations informations can be found under:
+<ul>
+<li><a href="http://poi.apache.org/encryption.html">Apache POI - Encryption support</a>
+</ul>
+
+<!-- Put @see and @since tags down here. -->
+@see org.apache.poi.poifs.crypt.standard
+@see org.apache.poi.poifs.crypt.agile
+</body>
+</html>
diff --git a/src/java/org/apache/poi/poifs/crypt/standard/EncryptionRecord.java b/src/java/org/apache/poi/poifs/crypt/standard/EncryptionRecord.java
new file mode 100644
index 0000000000..bf65fbe796
--- /dev/null
+++ b/src/java/org/apache/poi/poifs/crypt/standard/EncryptionRecord.java
@@ -0,0 +1,23 @@
+/* ====================================================================
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+==================================================================== */
+package org.apache.poi.poifs.crypt.standard;
+
+import org.apache.poi.util.LittleEndianByteArrayOutputStream;
+
+public interface EncryptionRecord {
+ void write(LittleEndianByteArrayOutputStream os);
+}
diff --git a/src/java/org/apache/poi/poifs/crypt/standard/StandardDecryptor.java b/src/java/org/apache/poi/poifs/crypt/standard/StandardDecryptor.java
new file mode 100644
index 0000000000..18729a1ff0
--- /dev/null
+++ b/src/java/org/apache/poi/poifs/crypt/standard/StandardDecryptor.java
@@ -0,0 +1,158 @@
+/* ====================================================================
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+==================================================================== */
+package org.apache.poi.poifs.crypt.standard;
+
+import static org.apache.poi.poifs.crypt.CryptoFunctions.hashPassword;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+import javax.crypto.Cipher;
+import javax.crypto.CipherInputStream;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.apache.poi.EncryptedDocumentException;
+import org.apache.poi.poifs.crypt.ChainingMode;
+import org.apache.poi.poifs.crypt.CryptoFunctions;
+import org.apache.poi.poifs.crypt.Decryptor;
+import org.apache.poi.poifs.crypt.EncryptionHeader;
+import org.apache.poi.poifs.crypt.EncryptionInfo;
+import org.apache.poi.poifs.crypt.EncryptionVerifier;
+import org.apache.poi.poifs.crypt.HashAlgorithm;
+import org.apache.poi.poifs.filesystem.DirectoryNode;
+import org.apache.poi.poifs.filesystem.DocumentInputStream;
+import org.apache.poi.util.BoundedInputStream;
+import org.apache.poi.util.LittleEndian;
+
+/**
+ */
+public class StandardDecryptor extends Decryptor {
+ private long _length = -1;
+
+ protected StandardDecryptor(EncryptionInfo info) {
+ super(info);
+ }
+
+ public boolean verifyPassword(String password) {
+ EncryptionVerifier ver = info.getVerifier();
+ SecretKey skey = generateSecretKey(password, ver, getKeySizeInBytes());
+ Cipher cipher = getCipher(skey);
+
+ try {
+ byte encryptedVerifier[] = ver.getEncryptedVerifier();
+ byte verifier[] = cipher.doFinal(encryptedVerifier);
+ setVerifier(verifier);
+ MessageDigest sha1 = MessageDigest.getInstance(ver.getHashAlgorithm().jceId);
+ byte[] calcVerifierHash = sha1.digest(verifier);
+ byte encryptedVerifierHash[] = ver.getEncryptedVerifierHash();
+ byte decryptedVerifierHash[] = cipher.doFinal(encryptedVerifierHash);
+ byte[] verifierHash = truncateOrPad(decryptedVerifierHash, calcVerifierHash.length);
+
+ if (Arrays.equals(calcVerifierHash, verifierHash)) {
+ setSecretKey(skey);
+ return true;
+ } else {
+ return false;
+ }
+ } catch (GeneralSecurityException e) {
+ throw new EncryptedDocumentException(e);
+ }
+ }
+
+ protected static SecretKey generateSecretKey(String password, EncryptionVerifier ver, int keySize) {
+ HashAlgorithm hashAlgo = ver.getHashAlgorithm();
+
+ byte pwHash[] = hashPassword(password, hashAlgo, ver.getSalt(), ver.getSpinCount());
+
+ byte[] blockKey = new byte[4];
+ LittleEndian.putInt(blockKey, 0, 0);
+
+ byte[] finalHash = CryptoFunctions.generateKey(pwHash, hashAlgo, blockKey, hashAlgo.hashSize);
+ byte x1[] = fillAndXor(finalHash, (byte) 0x36);
+ byte x2[] = fillAndXor(finalHash, (byte) 0x5c);
+
+ byte[] x3 = new byte[x1.length + x2.length];
+ System.arraycopy(x1, 0, x3, 0, x1.length);
+ System.arraycopy(x2, 0, x3, x1.length, x2.length);
+
+ byte[] key = truncateOrPad(x3, keySize);
+
+ SecretKey skey = new SecretKeySpec(key, ver.getCipherAlgorithm().jceId);
+ return skey;
+ }
+
+ protected static byte[] fillAndXor(byte hash[], byte fillByte) {
+ byte[] buff = new byte[64];
+ Arrays.fill(buff, fillByte);
+
+ for (int i=0; i<hash.length; i++) {
+ buff[i] = (byte) (buff[i] ^ hash[i]);
+ }
+
+ try {
+ MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
+ return sha1.digest(buff);
+ } catch (NoSuchAlgorithmException e) {
+ throw new EncryptedDocumentException("hash algo not supported", e);
+ }
+ }
+
+ /**
+ * Returns a byte array of the requested length,
+ * truncated or zero padded as needed.
+ * Behaves like Arrays.copyOf in Java 1.6
+ */
+ protected static byte[] truncateOrPad(byte[] source, int length) {
+ byte[] result = new byte[length];
+ System.arraycopy(source, 0, result, 0, Math.min(length, source.length));
+ if(length > source.length) {
+ for(int i=source.length; i<length; i++) {
+ result[i] = 0;
+ }
+ }
+ return result;
+ }
+
+ private Cipher getCipher(SecretKey key) {
+ EncryptionHeader em = info.getHeader();
+ ChainingMode cm = em.getChainingMode();
+ assert(cm == ChainingMode.ecb);
+ return CryptoFunctions.getCipher(key, em.getCipherAlgorithm(), cm, null, Cipher.DECRYPT_MODE);
+ }
+
+ public InputStream getDataStream(DirectoryNode dir) throws IOException {
+ DocumentInputStream dis = dir.createDocumentInputStream("EncryptedPackage");
+
+ _length = dis.readLong();
+
+ return new BoundedInputStream(new CipherInputStream(dis, getCipher(getSecretKey())), _length);
+ }
+
+ public long getLength(){
+ if(_length == -1) throw new IllegalStateException("Decryptor.getDataStream() was not called");
+ return _length;
+ }
+
+ protected int getKeySizeInBytes() {
+ return info.getHeader().getKeySize()/8;
+ }
+}
diff --git a/src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptionHeader.java b/src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptionHeader.java
new file mode 100644
index 0000000000..213cc0beb1
--- /dev/null
+++ b/src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptionHeader.java
@@ -0,0 +1,116 @@
+/* ====================================================================
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+==================================================================== */
+package org.apache.poi.poifs.crypt.standard;
+
+import static org.apache.poi.poifs.crypt.CryptoFunctions.getUtf16LeString;
+
+import java.io.IOException;
+
+import org.apache.poi.poifs.crypt.ChainingMode;
+import org.apache.poi.poifs.crypt.CipherAlgorithm;
+import org.apache.poi.poifs.crypt.CipherProvider;
+import org.apache.poi.poifs.crypt.EncryptionHeader;
+import org.apache.poi.poifs.crypt.HashAlgorithm;
+import org.apache.poi.poifs.filesystem.DocumentInputStream;
+import org.apache.poi.util.BitField;
+import org.apache.poi.util.LittleEndianByteArrayOutputStream;
+import org.apache.poi.util.LittleEndianConsts;
+import org.apache.poi.util.LittleEndianOutput;
+
+public class StandardEncryptionHeader extends EncryptionHeader implements EncryptionRecord {
+ // A flag that specifies whether CryptoAPI RC4 or ECMA-376 encryption
+ // [ECMA-376] is used. It MUST be 1 unless fExternal is 1. If fExternal is 1, it MUST be 0.
+ private static BitField flagsCryptoAPI = new BitField(0x04);
+
+ // A value that MUST be 0 if document properties are encrypted. The
+ // encryption of document properties is specified in section 2.3.5.4 [MS-OFFCRYPTO].
+ @SuppressWarnings("unused")
+ private static BitField flagsDocProps = new BitField(0x08);
+
+ // A value that MUST be 1 if extensible encryption is used,. If this value is 1,
+ // the value of every other field in this structure MUST be 0.
+ @SuppressWarnings("unused")
+ private static BitField flagsExternal = new BitField(0x10);
+
+ // A value that MUST be 1 if the protected content is an ECMA-376 document
+ // [ECMA-376]. If the fAES bit is 1, the fCryptoAPI bit MUST also be 1.
+ private static BitField flagsAES = new BitField(0x20);
+
+ protected StandardEncryptionHeader(DocumentInputStream is) throws IOException {
+ setFlags(is.readInt());
+ setSizeExtra(is.readInt());
+ setCipherAlgorithm(CipherAlgorithm.fromEcmaId(is.readInt()));
+ setHashAlgorithm(HashAlgorithm.fromEcmaId(is.readInt()));
+ setKeySize(is.readInt());
+ setBlockSize(getKeySize());
+ setCipherProvider(CipherProvider.fromEcmaId(is.readInt()));
+
+ is.readLong(); // skip reserved
+
+ // CSPName may not always be specified
+ // In some cases, the salt value of the EncryptionVerifier is the next chunk of data
+ is.mark(LittleEndianConsts.INT_SIZE+1);
+ int checkForSalt = is.readInt();
+ is.reset();
+
+ if (checkForSalt == 16) {
+ setCspName("");
+ } else {
+ StringBuilder builder = new StringBuilder();
+ while (true) {
+ char c = (char) is.readShort();
+ if (c == 0) break;
+ builder.append(c);
+ }
+ setCspName(builder.toString());
+ }
+
+ setChainingMode(ChainingMode.ecb);
+ setKeySalt(null);
+ }
+
+ protected StandardEncryptionHeader(CipherAlgorithm cipherAlgorithm, HashAlgorithm hashAlgorithm, int keyBits, int blockSize, ChainingMode chainingMode) {
+ setCipherAlgorithm(cipherAlgorithm);
+ setHashAlgorithm(hashAlgorithm);
+ setKeySize(keyBits);
+ setBlockSize(blockSize);
+ setCipherProvider(cipherAlgorithm.provider);
+ setFlags(flagsCryptoAPI.setBoolean(0, true)
+ | flagsAES.setBoolean(0, cipherAlgorithm.provider == CipherProvider.aes));
+ // see http://msdn.microsoft.com/en-us/library/windows/desktop/bb931357(v=vs.85).aspx for a full list
+ // setCspName("Microsoft Enhanced RSA and AES Cryptographic Provider");
+ }
+
+ public void write(LittleEndianByteArrayOutputStream bos) {
+ int startIdx = bos.getWriteIndex();
+ LittleEndianOutput sizeOutput = bos.createDelayedOutput(LittleEndianConsts.INT_SIZE);
+ bos.writeInt(getFlags());
+ bos.writeInt(0); // size extra
+ bos.writeInt(getCipherAlgorithm().ecmaId);
+ bos.writeInt(getHashAlgorithmEx().ecmaId);
+ bos.writeInt(getKeySize());
+ bos.writeInt(getCipherProvider().ecmaId);
+ bos.writeInt(0); // reserved1
+ bos.writeInt(0); // reserved2
+ if (getCspName() != null) {
+ bos.write(getUtf16LeString(getCspName()));
+ bos.writeShort(0);
+ }
+ int headerSize = bos.getWriteIndex()-startIdx-LittleEndianConsts.INT_SIZE;
+ sizeOutput.writeInt(headerSize);
+ }
+}
diff --git a/src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptionInfoBuilder.java b/src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptionInfoBuilder.java
new file mode 100644
index 0000000000..0480ec4594
--- /dev/null
+++ b/src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptionInfoBuilder.java
@@ -0,0 +1,106 @@
+/* ====================================================================
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+==================================================================== */
+package org.apache.poi.poifs.crypt.standard;
+
+import 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 StandardEncryptionInfoBuilder implements EncryptionInfoBuilder {
+
+ EncryptionInfo info;
+ StandardEncryptionHeader header;
+ StandardEncryptionVerifier verifier;
+ StandardDecryptor decryptor;
+ StandardEncryptor encryptor;
+
+ public void initialize(EncryptionInfo info, DocumentInputStream dis) throws IOException {
+ this.info = info;
+
+ @SuppressWarnings("unused")
+ int hSize = dis.readInt();
+ header = new StandardEncryptionHeader(dis);
+ verifier = new StandardEncryptionVerifier(dis, header);
+
+ if (info.getVersionMinor() == 2 && (info.getVersionMajor() == 3 || info.getVersionMajor() == 4)) {
+ decryptor = new StandardDecryptor(info);
+ }
+ }
+
+ public void initialize(EncryptionInfo info, CipherAlgorithm cipherAlgorithm, HashAlgorithm hashAlgorithm, int keyBits, int blockSize, ChainingMode chainingMode) {
+ this.info = info;
+
+ if (cipherAlgorithm == null) {
+ cipherAlgorithm = CipherAlgorithm.rc4;
+ }
+ if (hashAlgorithm == null) {
+ hashAlgorithm = HashAlgorithm.sha1;
+ }
+ if (hashAlgorithm != HashAlgorithm.sha1) {
+ throw new EncryptedDocumentException("Standard encryption only supports SHA-1.");
+ }
+ if (chainingMode == null) {
+ chainingMode = ChainingMode.ecb;
+ }
+ if (chainingMode != ChainingMode.ecb) {
+ throw new EncryptedDocumentException("Standard encryption only supports ECB 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 StandardEncryptionHeader(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode);
+ verifier = new StandardEncryptionVerifier(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode);
+ decryptor = new StandardDecryptor(info);
+ encryptor = new StandardEncryptor(this);
+ }
+
+ public StandardEncryptionHeader getHeader() {
+ return header;
+ }
+
+ public StandardEncryptionVerifier getVerifier() {
+ return verifier;
+ }
+
+ public StandardDecryptor getDecryptor() {
+ return decryptor;
+ }
+
+ public StandardEncryptor getEncryptor() {
+ return encryptor;
+ }
+
+ public EncryptionInfo getEncryptionInfo() {
+ return info;
+ }
+}
diff --git a/src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptionVerifier.java b/src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptionVerifier.java
new file mode 100644
index 0000000000..db9361793d
--- /dev/null
+++ b/src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptionVerifier.java
@@ -0,0 +1,112 @@
+/* ====================================================================
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+==================================================================== */
+package org.apache.poi.poifs.crypt.standard;
+
+import org.apache.poi.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.poi.poifs.filesystem.DocumentInputStream;
+import org.apache.poi.util.LittleEndianByteArrayOutputStream;
+
+/**
+ * Used when checking if a key is valid for a document
+ */
+public class StandardEncryptionVerifier extends EncryptionVerifier implements EncryptionRecord {
+ private static final int SPIN_COUNT = 50000;
+ private final int verifierHashSize;
+
+ protected StandardEncryptionVerifier(DocumentInputStream is, StandardEncryptionHeader header) {
+ int saltSize = is.readInt();
+
+ if (saltSize!=16) {
+ throw new RuntimeException("Salt size != 16 !?");
+ }
+
+ byte salt[] = new byte[16];
+ is.readFully(salt);
+ setSalt(salt);
+
+ byte encryptedVerifier[] = new byte[16];
+ is.readFully(encryptedVerifier);
+ setEncryptedVerifier(encryptedVerifier);
+
+ verifierHashSize = is.readInt();
+
+ byte encryptedVerifierHash[] = new byte[header.getCipherAlgorithm().encryptedVerifierHashLength];
+ is.readFully(encryptedVerifierHash);
+ setEncryptedVerifierHash(encryptedVerifierHash);
+
+ setSpinCount(SPIN_COUNT);
+ setCipherAlgorithm(CipherAlgorithm.aes128);
+ setChainingMode(ChainingMode.ecb);
+ setEncryptedKey(null);
+ setHashAlgorithm(HashAlgorithm.sha1);
+ }
+
+ protected StandardEncryptionVerifier(CipherAlgorithm cipherAlgorithm, HashAlgorithm hashAlgorithm, int keyBits, int blockSize, ChainingMode chainingMode) {
+ setCipherAlgorithm(cipherAlgorithm);
+ setHashAlgorithm(hashAlgorithm);
+ setChainingMode(chainingMode);
+ setSpinCount(SPIN_COUNT);
+ verifierHashSize = hashAlgorithm.hashSize;
+ }
+
+ // make method visible for this package
+ protected void setSalt(byte salt[]) {
+ if (salt == null || salt.length != 16) {
+ 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);
+ }
+
+ public void write(LittleEndianByteArrayOutputStream bos) {
+ // see [MS-OFFCRYPTO] - 2.3.4.9
+ byte salt[] = getSalt();
+ assert(salt.length == 16);
+ bos.writeInt(salt.length); // salt size
+ bos.write(salt);
+
+ // The resulting Verifier value MUST be an array of 16 bytes.
+ byte encryptedVerifier[] = getEncryptedVerifier();
+ assert(encryptedVerifier.length == 16);
+ bos.write(encryptedVerifier);
+
+ // The number of bytes used by the encrypted Verifier hash MUST be 32.
+ // The number of bytes used by the decrypted Verifier hash is given by
+ // the VerifierHashSize field, which MUST be 20
+ byte encryptedVerifierHash[] = getEncryptedVerifierHash();
+ assert(encryptedVerifierHash.length == 32);
+ bos.writeInt(20);
+ bos.write(encryptedVerifierHash);
+ }
+
+ protected int getVerifierHashSize() {
+ return verifierHashSize;
+ }
+}
diff --git a/src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptor.java b/src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptor.java
new file mode 100644
index 0000000000..236eac124a
--- /dev/null
+++ b/src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptor.java
@@ -0,0 +1,218 @@
+/* ====================================================================
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+==================================================================== */
+
+package org.apache.poi.poifs.crypt.standard;
+
+import static org.apache.poi.poifs.crypt.DataSpaceMapUtils.createEncryptionEntry;
+import static org.apache.poi.poifs.crypt.standard.StandardDecryptor.generateSecretKey;
+import static org.apache.poi.poifs.crypt.standard.StandardDecryptor.truncateOrPad;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.security.SecureRandom;
+import java.util.Random;
+
+import javax.crypto.Cipher;
+import javax.crypto.CipherOutputStream;
+import javax.crypto.SecretKey;
+
+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.EncryptionInfo;
+import org.apache.poi.poifs.crypt.EncryptionVerifier;
+import org.apache.poi.poifs.crypt.Encryptor;
+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.LittleEndianByteArrayOutputStream;
+import org.apache.poi.util.LittleEndianConsts;
+import org.apache.poi.util.LittleEndianOutputStream;
+import org.apache.poi.util.TempFile;
+
+public class StandardEncryptor extends Encryptor {
+ private final StandardEncryptionInfoBuilder builder;
+
+ protected StandardEncryptor(StandardEncryptionInfoBuilder builder) {
+ this.builder = builder;
+ }
+
+ public void confirmPassword(String password) {
+ // see [MS-OFFCRYPTO] - 2.3.3 EncryptionVerifier
+ Random r = new SecureRandom();
+ byte[] salt = new byte[16], verifier = new byte[16];
+ r.nextBytes(salt);
+ r.nextBytes(verifier);
+
+ confirmPassword(password, null, null, salt, verifier, null);
+ }
+
+
+ /**
+ * Fills the fields of verifier and header with the calculated hashes based
+ * on the password and a random salt
+ *
+ * see [MS-OFFCRYPTO] - 2.3.4.7 ECMA-376 Document Encryption Key Generation
+ */
+ public void confirmPassword(String password, byte keySpec[], byte keySalt[], byte verifier[], byte verifierSalt[], byte integritySalt[]) {
+ StandardEncryptionVerifier ver = builder.getVerifier();
+
+ ver.setSalt(verifierSalt);
+ SecretKey secretKey = generateSecretKey(password, ver, getKeySizeInBytes());
+ setSecretKey(secretKey);
+ Cipher cipher = getCipher(secretKey, null);
+
+ try {
+ byte encryptedVerifier[] = cipher.doFinal(verifier);
+ MessageDigest hashAlgo = MessageDigest.getInstance(ver.getHashAlgorithm().jceId);
+ byte calcVerifierHash[] = hashAlgo.digest(verifier);
+
+ // 2.3.3 EncryptionVerifier ...
+ // An array of bytes that contains the encrypted form of the
+ // hash of the randomly generated Verifier value. The length of the array MUST be the size of
+ // the encryption block size multiplied by the number of blocks needed to encrypt the hash of the
+ // Verifier. If the encryption algorithm is RC4, the length MUST be 20 bytes. If the encryption
+ // algorithm is AES, the length MUST be 32 bytes. After decrypting the EncryptedVerifierHash
+ // field, only the first VerifierHashSize bytes MUST be used.
+ int encVerHashSize = ver.getCipherAlgorithm().encryptedVerifierHashLength;
+ byte encryptedVerifierHash[] = cipher.doFinal(truncateOrPad(calcVerifierHash, encVerHashSize));
+
+ ver.setEncryptedVerifier(encryptedVerifier);
+ ver.setEncryptedVerifierHash(encryptedVerifierHash);
+ } catch (GeneralSecurityException e) {
+ throw new EncryptedDocumentException("Password confirmation failed", e);
+ }
+
+ }
+
+ private Cipher getCipher(SecretKey key, String padding) {
+ EncryptionVerifier ver = builder.getVerifier();
+ return CryptoFunctions.getCipher(key, ver.getCipherAlgorithm(), ver.getChainingMode(), null, Cipher.ENCRYPT_MODE, padding);
+ }
+
+ public OutputStream getDataStream(final DirectoryNode dir)
+ throws IOException, GeneralSecurityException {
+ createEncryptionInfoEntry(dir);
+ DataSpaceMapUtils.addDefaultDataSpace(dir);
+ OutputStream countStream = new StandardCipherOutputStream(dir);
+ return countStream;
+ }
+
+ protected class StandardCipherOutputStream extends FilterOutputStream implements POIFSWriterListener {
+ protected long countBytes;
+ protected final File fileOut;
+ protected final DirectoryNode dir;
+
+ protected StandardCipherOutputStream(DirectoryNode dir) throws IOException {
+ super(null);
+
+ this.dir = dir;
+ fileOut = TempFile.createTempFile("encrypted_package", "crypt");
+ FileOutputStream rawStream = new FileOutputStream(fileOut);
+
+ // although not documented, we need the same padding as with agile encryption
+ // and instead of calculating the missing bytes for the block size ourselves
+ // we leave it up to the CipherOutputStream, which generates/saves them on close()
+ // ... we can't use "NoPadding" here
+ //
+ // see also [MS-OFFCRYPT] - 2.3.4.15
+ // 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.
+ CipherOutputStream cryptStream = new CipherOutputStream(rawStream, getCipher(getSecretKey(), "PKCS5Padding"));
+
+ this.out = cryptStream;
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ out.write(b, off, len);
+ countBytes += len;
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ out.write(b);
+ countBytes++;
+ }
+
+ public void close() throws IOException {
+ // the CipherOutputStream adds the padding bytes on close()
+ super.close();
+ writeToPOIFS();
+ }
+
+ void writeToPOIFS() throws IOException {
+ 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(countBytes);
+
+ FileInputStream fis = new FileInputStream(fileOut);
+ IOUtils.copy(fis, leos);
+ fis.close();
+ fileOut.delete();
+
+ leos.close();
+ } catch (IOException e) {
+ throw new EncryptedDocumentException(e);
+ }
+ }
+ }
+
+ protected int getKeySizeInBytes() {
+ return builder.getHeader().getKeySize()/8;
+ }
+
+ protected void createEncryptionInfoEntry(DirectoryNode dir) throws IOException {
+ final EncryptionInfo info = builder.getEncryptionInfo();
+ final StandardEncryptionHeader header = builder.getHeader();
+ final StandardEncryptionVerifier verifier = builder.getVerifier();
+
+ EncryptionRecord er = new EncryptionRecord(){
+ public void write(LittleEndianByteArrayOutputStream bos) {
+ bos.writeShort(info.getVersionMajor());
+ bos.writeShort(info.getVersionMinor());
+ bos.writeInt(info.getEncryptionFlags());
+ header.write(bos);
+ verifier.write(bos);
+ }
+ };
+
+ createEncryptionEntry(dir, "EncryptionInfo", er);
+
+ // TODO: any properties???
+ }
+}