diff options
Diffstat (limited to 'src/java')
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???
+ }
+}
|