aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMaxim Valyanskiy <maxcom@apache.org>2011-05-10 10:38:17 +0000
committerMaxim Valyanskiy <maxcom@apache.org>2011-05-10 10:38:17 +0000
commit138bd6f94c47adb06d109b5b1a342a0b37547c0c (patch)
tree9bb911d648f98facb242f1839bd054c056ef0a6c
parente94feeee12d4356cf5e46b44894032b64f6d0110 (diff)
downloadpoi-138bd6f94c47adb06d109b5b1a342a0b37547c0c.tar.gz
poi-138bd6f94c47adb06d109b5b1a342a0b37547c0c.zip
bug#51165: Add support for OOXML Agile Encryption
git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1101397 13f79535-47bb-0310-9956-ffa450edef68
-rw-r--r--build.xml9
-rw-r--r--src/documentation/content/xdocs/encryption.xml84
-rw-r--r--src/java/org/apache/poi/poifs/crypt/AgileDecryptor.java244
-rw-r--r--src/java/org/apache/poi/poifs/crypt/Decryptor.java170
-rw-r--r--src/java/org/apache/poi/poifs/crypt/EcmaDecryptor.java132
-rw-r--r--src/java/org/apache/poi/poifs/crypt/EncryptionHeader.java88
-rw-r--r--src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java31
-rw-r--r--src/java/org/apache/poi/poifs/crypt/EncryptionVerifier.java114
-rw-r--r--src/testcases/org/apache/poi/poifs/crypt/AllPOIFSCryptoTests.java37
-rw-r--r--src/testcases/org/apache/poi/poifs/crypt/TestDecryptor.java (renamed from src/testcases/org/apache/poi/poifs/crypt/DecryptorTest.java)21
-rw-r--r--src/testcases/org/apache/poi/poifs/crypt/TestEncryptionInfo.java (renamed from src/testcases/org/apache/poi/poifs/crypt/EncryptionInfoTest.java)2
-rw-r--r--test-data/poifs/protected_agile.docxbin0 -> 19456 bytes
12 files changed, 789 insertions, 143 deletions
diff --git a/build.xml b/build.xml
index 9112de255f..5f4c5aada4 100644
--- a/build.xml
+++ b/build.xml
@@ -122,6 +122,9 @@ under the License.
<property name="main.commons-logging.jar" location="${main.lib}/commons-logging-1.1.jar"/>
<property name="main.commons-logging.url"
value="${repository.m2}/maven2/commons-logging/commons-logging/1.1/commons-logging-1.1.jar"/>
+ <property name="main.commons-codec.jar" location="${main.lib}/commons-codec-1.5.jar"/>
+ <property name="main.commons-codec.url"
+ value="${repository.m2}/maven2/commons-codec/commons-codec/1.5/commons-codec-1.5.jar"/>
<property name="main.log4j.jar" location="${main.lib}/log4j-1.2.13.jar"/>
<property name="main.log4j.url" value="${repository.m2}/maven2/log4j/log4j/1.2.13/log4j-1.2.13.jar"/>
<property name="main.junit.jar" location="${main.lib}/junit-3.8.1.jar"/>
@@ -166,6 +169,7 @@ under the License.
<path id="main.classpath">
<pathelement location="${main.commons-logging.jar}"/>
+ <pathelement location="${main.commons-codec.jar}"/>
<pathelement location="${main.log4j.jar}"/>
<pathelement location="${main.junit.jar}"/>
</path>
@@ -295,6 +299,7 @@ under the License.
<or>
<and>
<available file="${main.commons-logging.jar}"/>
+ <available file="${main.commons-codec.jar}"/>
<available file="${main.log4j.jar}"/>
<available file="${main.junit.jar}"/>
<available file="${main.ant.jar}"/>
@@ -312,6 +317,10 @@ under the License.
<param name="destfile" value="${main.commons-logging.jar}"/>
</antcall>
<antcall target="downloadfile">
+ <param name="sourcefile" value="${main.commons-codec.url}"/>
+ <param name="destfile" value="${main.commons-codec.jar}"/>
+ </antcall>
+ <antcall target="downloadfile">
<param name="sourcefile" value="${main.log4j.url}"/>
<param name="destfile" value="${main.log4j.jar}"/>
</antcall>
diff --git a/src/documentation/content/xdocs/encryption.xml b/src/documentation/content/xdocs/encryption.xml
new file mode 100644
index 0000000000..61a2a200dd
--- /dev/null
+++ b/src/documentation/content/xdocs/encryption.xml
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ====================================================================
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ ====================================================================
+-->
+<!DOCTYPE document PUBLIC "-//APACHE//DTD Documentation V1.3//EN" "./dtd/document-v13.dtd">
+
+
+<document>
+ <header>
+ <title>Apache POI - Encryption support</title>
+ <authors>
+ <person id="maxcom" name="Maxim Valyanskiy" email="maxcom@apache.org"/>
+ </authors>
+ </header>
+
+ <body>
+ <section><title>Overview</title>
+ <p>Apache POI contains support for reading few variants of encrypted office files: </p>
+ <ul>
+ <li>XLS - RC4 Encryption</li>
+ <li>XML-based formats (XLSX, DOCX and etc) - AES Encryption</li>
+ </ul>
+
+ <p>Some "write-protected" files are encrypted with build-in password, POI can read that files too.</p>
+ </section>
+
+ <section><title>XLS</title>
+ <p>When HSSF receive encrypted file, it tries to decode it with MSOffice build-in password.
+ Use static method setCurrentUserPassword(String password) of org.apache.poi.hssf.record.crypto.Biff8EncryptionKey to
+ set password. It sets thread local variable. Do not forget to reset it to null after text extraction.
+ </p>
+ </section>
+
+ <section><title>XML-based formats</title>
+ <p>XML-based formats are stored in OLE-package stream "EncryptedPackage". Use org.apache.poi.poifs.crypt.Decryptor
+ to decode file:</p>
+
+ <source>
+EncryptionInfo info = new EncryptionInfo(filesystem);
+Decryptor d = new Decryptor(info);
+
+try {
+ if (!d.verifyPassword(password)) {
+ throw new RuntimeException("Unable to process: document is encrypted");
+ }
+
+ InputStream dataStream = d.getDataStream(filesystem);
+
+ // parse dataStream
+
+} catch (GeneralSecurityException ex) {
+ throw new RuntimeException("Unable to process encrypted document", ex);
+}
+ </source>
+
+ <p>If you want to read file encrypted with build-in password, use Decryptor.DEFAULT_PASSWORD.</p>
+ </section>
+ </body>
+
+ <footer>
+ <legal>
+ Copyright (c) @year@ The Apache Software Foundation. All rights reserved.
+ </legal>
+ </footer>
+</document>
+
+
+
+
diff --git a/src/java/org/apache/poi/poifs/crypt/AgileDecryptor.java b/src/java/org/apache/poi/poifs/crypt/AgileDecryptor.java
new file mode 100644
index 0000000000..17ef47bb83
--- /dev/null
+++ b/src/java/org/apache/poi/poifs/crypt/AgileDecryptor.java
@@ -0,0 +1,244 @@
+/* ====================================================================
+ 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.util.Arrays;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.FilterInputStream;
+import java.io.ByteArrayInputStream;
+import java.security.MessageDigest;
+import java.security.GeneralSecurityException;
+import java.security.NoSuchAlgorithmException;
+import org.apache.poi.poifs.filesystem.POIFSFileSystem;
+import org.apache.poi.poifs.filesystem.DirectoryNode;
+import org.apache.poi.EncryptedDocumentException;
+
+import javax.crypto.Cipher;
+import javax.crypto.CipherInputStream;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+import javax.crypto.spec.IvParameterSpec;
+
+import org.apache.poi.poifs.filesystem.DirectoryNode;
+import org.apache.poi.poifs.filesystem.DocumentInputStream;
+import org.apache.poi.poifs.filesystem.POIFSFileSystem;
+import org.apache.poi.util.LittleEndian;
+
+/**
+ * @author Gary King
+ */
+public class AgileDecryptor extends Decryptor {
+
+ private final EncryptionInfo _info;
+ private SecretKey _secretKey;
+
+ 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();
+ int algorithm = verifier.getAlgorithm();
+ int mode = verifier.getCipherMode();
+
+ byte[] pwHash = hashPassword(_info, password);
+ byte[] iv = generateIv(algorithm, verifier.getSalt(), null);
+
+ SecretKey skey;
+ skey = new SecretKeySpec(generateKey(pwHash, kVerifierInputBlock), "AES");
+ Cipher cipher = getCipher(algorithm, mode, skey, iv);
+ byte[] verifierHashInput = cipher.doFinal(verifier.getVerifier());
+
+ MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
+ byte[] trimmed = new byte[verifier.getSalt().length];
+ System.arraycopy(verifierHashInput, 0, trimmed, 0, trimmed.length);
+ byte[] hashedVerifier = sha1.digest(trimmed);
+
+ skey = new SecretKeySpec(generateKey(pwHash, kHashedVerifierBlock), "AES");
+ iv = generateIv(algorithm, verifier.getSalt(), null);
+ cipher = getCipher(algorithm, mode, 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(algorithm, verifier.getSalt(), null);
+ cipher = getCipher(algorithm, mode, skey, iv);
+ byte[] inter = cipher.doFinal(verifier.getEncryptedKey());
+ byte[] keyspec = new byte[_info.getHeader().getKeySize() / 8];
+ 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");
+ long size = dis.readLong();
+ return new ChunkedCipherInputStream(dis, size);
+ }
+
+ 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(_info.getHeader().getAlgorithm(),
+ _info.getHeader().getCipherMode(),
+ _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, index);
+ byte[] iv = generateIv(_info.getHeader().getAlgorithm(),
+ _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(int algorithm, int mode, SecretKey key, byte[] vec)
+ throws GeneralSecurityException {
+ String name = null;
+ String chain = null;
+
+ if (algorithm == EncryptionHeader.ALGORITHM_AES_128 ||
+ algorithm == EncryptionHeader.ALGORITHM_AES_192 ||
+ algorithm == EncryptionHeader.ALGORITHM_AES_256)
+ name = "AES";
+
+ if (mode == EncryptionHeader.MODE_CBC)
+ chain = "CBC";
+ else if (mode == EncryptionHeader.MODE_CFB)
+ chain = "CFB";
+
+ Cipher cipher = Cipher.getInstance(name + "/" + chain + "/NoPadding");
+ IvParameterSpec iv = new IvParameterSpec(vec);
+ cipher.init(Cipher.DECRYPT_MODE, key, iv);
+ return cipher;
+ }
+
+ private byte[] getBlock(int algorithm, byte[] hash) {
+ byte[] result = new byte[getBlockSize(algorithm)];
+ 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);
+ return getBlock(_info.getVerifier().getAlgorithm(), sha1.digest(blockKey));
+ }
+
+ protected byte[] generateIv(int algorithm, byte[] salt, byte[] blockKey)
+ throws NoSuchAlgorithmException {
+
+
+ if (blockKey == null)
+ return getBlock(algorithm, salt);
+
+ MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
+ sha1.update(salt);
+ return getBlock(algorithm, sha1.digest(blockKey));
+ }
+} \ No newline at end of file
diff --git a/src/java/org/apache/poi/poifs/crypt/Decryptor.java b/src/java/org/apache/poi/poifs/crypt/Decryptor.java
index e97f41b41d..fb5b11f7b3 100644
--- a/src/java/org/apache/poi/poifs/crypt/Decryptor.java
+++ b/src/java/org/apache/poi/poifs/crypt/Decryptor.java
@@ -19,150 +19,74 @@ package org.apache.poi.poifs.crypt;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
-import java.security.GeneralSecurityException;
import java.security.MessageDigest;
+import java.security.GeneralSecurityException;
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.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;
-/**
- * @author Maxim Valyanskiy
- */
-public class Decryptor {
+public abstract class Decryptor {
public static final String DEFAULT_PASSWORD="VelvetSweatshop";
- private final EncryptionInfo info;
- private byte[] passwordHash;
+ public abstract InputStream getDataStream(DirectoryNode dir)
+ throws IOException, GeneralSecurityException;
+
+ public abstract boolean verifyPassword(String password)
+ throws GeneralSecurityException;
+
+ public static Decryptor getInstance(EncryptionInfo info) {
+ int major = info.getVersionMajor();
+ int minor = info.getVersionMinor();
- public Decryptor(EncryptionInfo info) {
- this.info = info;
+ if (major == 4 && minor == 4)
+ return new AgileDecryptor(info);
+ else if (minor == 2 && (major == 3 || major == 4))
+ return new EcmaDecryptor(info);
+ else
+ throw new EncryptedDocumentException("Unsupported version");
}
- private void generatePasswordHash(String password) throws NoSuchAlgorithmException {
+ public InputStream getDataStream(NPOIFSFileSystem fs) throws IOException, GeneralSecurityException {
+ return getDataStream(fs.getRoot());
+ }
+
+ public InputStream getDataStream(POIFSFileSystem fs) throws IOException, GeneralSecurityException {
+ return getDataStream(fs.getRoot());
+ }
+
+ protected static int getBlockSize(int algorithm) {
+ switch (algorithm) {
+ case EncryptionHeader.ALGORITHM_AES_128: return 16;
+ case EncryptionHeader.ALGORITHM_AES_192: return 24;
+ case EncryptionHeader.ALGORITHM_AES_256: return 32;
+ }
+ throw new EncryptedDocumentException("Unknown block size");
+ }
+
+ protected byte[] hashPassword(EncryptionInfo info,
+ String password) throws NoSuchAlgorithmException {
MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
-
- byte[] passwordBytes;
+ byte[] bytes;
try {
- passwordBytes = password.getBytes("UTF-16LE");
- } catch(UnsupportedEncodingException e) {
- throw new RuntimeException("Your JVM is broken - UTF16 not found!");
+ bytes = password.getBytes("UTF-16LE");
+ } catch (UnsupportedEncodingException e) {
+ throw new EncryptedDocumentException("UTF16 not supported");
}
sha1.update(info.getVerifier().getSalt());
- byte[] hash = sha1.digest(passwordBytes);
-
+ byte[] hash = sha1.digest(bytes);
byte[] iterator = new byte[4];
- for (int i = 0; i<50000; i++) {
- sha1.reset();
+ for (int i = 0; i < info.getVerifier().getSpinCount(); i++) {
+ sha1.reset();
LittleEndian.putInt(iterator, i);
sha1.update(iterator);
hash = sha1.digest(hash);
}
- passwordHash = hash;
- }
-
- private byte[] generateKey(int block) throws NoSuchAlgorithmException {
- MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
-
- sha1.update(passwordHash);
- byte[] blockValue = new byte[4];
- LittleEndian.putInt(blockValue, 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 {
- generatePasswordHash(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(POIFSFileSystem fs) throws IOException, GeneralSecurityException {
- return getDataStream(fs.getRoot());
- }
-
- public InputStream getDataStream(NPOIFSFileSystem fs) throws IOException, GeneralSecurityException {
- return getDataStream(fs.getRoot());
- }
-
- @SuppressWarnings("unused")
- public InputStream getDataStream(DirectoryNode dir) throws IOException, GeneralSecurityException {
- DocumentInputStream dis = dir.createDocumentInputStream("EncryptedPackage");
-
- long size = dis.readLong();
-
- return new CipherInputStream(dis, getCipher());
+ return hash;
}
-}
+} \ 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
new file mode 100644
index 0000000000..441a4335fe
--- /dev/null
+++ b/src/java/org/apache/poi/poifs/crypt/EcmaDecryptor.java
@@ -0,0 +1,132 @@
+/* ====================================================================
+ 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.io.UnsupportedEncodingException;
+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.poifs.filesystem.POIFSFileSystem;
+import org.apache.poi.util.LittleEndian;
+
+/**
+ * @author Maxim Valyanskiy
+ * @author Gary King
+ */
+public class EcmaDecryptor extends Decryptor {
+ private final EncryptionInfo info;
+ private byte[] passwordHash;
+
+ 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, 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");
+
+ long size = dis.readLong();
+
+ return new CipherInputStream(dis, getCipher());
+ }
+}
diff --git a/src/java/org/apache/poi/poifs/crypt/EncryptionHeader.java b/src/java/org/apache/poi/poifs/crypt/EncryptionHeader.java
index 893f8cc9c5..21dd25e99c 100644
--- a/src/java/org/apache/poi/poifs/crypt/EncryptionHeader.java
+++ b/src/java/org/apache/poi/poifs/crypt/EncryptionHeader.java
@@ -16,12 +16,19 @@
==================================================================== */
package org.apache.poi.poifs.crypt;
+import org.apache.commons.codec.binary.Base64;
import org.apache.poi.poifs.filesystem.DocumentInputStream;
import java.io.IOException;
+import java.io.ByteArrayInputStream;
+
+import org.w3c.dom.NamedNodeMap;
+import javax.xml.parsers.DocumentBuilderFactory;
+import org.apache.poi.EncryptedDocumentException;
/**
* @author Maxim Valyanskiy
+ * @author Gary King
*/
public class EncryptionHeader {
public static final int ALGORITHM_RC4 = 0x6801;
@@ -32,7 +39,11 @@ public class EncryptionHeader {
public static final int HASH_SHA1 = 0x8004;
public static final int PROVIDER_RC4 = 1;
- public static final int PROVIDER_AES = 0x18;
+ 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;
@@ -40,6 +51,8 @@ public class EncryptionHeader {
private final int hashAlgorithm;
private final int keySize;
private final int providerType;
+ private final int cipherMode;
+ private final byte[] keySalt;
private final String cspName;
public EncryptionHeader(DocumentInputStream is) throws IOException {
@@ -63,8 +76,75 @@ public class EncryptionHeader {
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;
+
+ int blockSize = Integer.parseInt(keyData.getNamedItem("blockSize").
+ getNodeValue());
+ String cipher = keyData.getNamedItem("cipherAlgorithm").getNodeValue();
+
+ if ("AES".equals(cipher)) {
+ providerType = PROVIDER_AES;
+ if (blockSize == 16)
+ algorithm = ALGORITHM_AES_128;
+ else if (blockSize == 24)
+ algorithm = ALGORITHM_AES_192;
+ else if (blockSize == 32)
+ algorithm = ALGORITHM_AES_256;
+ else
+ throw new EncryptedDocumentException("Unsupported key length");
+ } else {
+ throw new EncryptedDocumentException("Unsupported 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");
+
+ String hashAlg = keyData.getNamedItem("hashAlgorithm").getNodeValue();
+ int hashSize = Integer.parseInt(keyData.getNamedItem("hashSize")
+ .getNodeValue());
+
+ if ("SHA1".equals(hashAlg) && hashSize == 20)
+ hashAlgorithm = HASH_SHA1;
+ else
+ throw new EncryptedDocumentException("Unsupported hash algorithm");
+
+ 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 int getCipherMode() {
+ return cipherMode;
}
public int getFlags() {
@@ -87,6 +167,10 @@ public class EncryptionHeader {
return keySize;
}
+ public byte[] getKeySalt() {
+ return keySalt;
+ }
+
public int getProviderType() {
return providerType;
}
diff --git a/src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java b/src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java
index 68ead77428..70dd03799e 100644
--- a/src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java
+++ b/src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java
@@ -16,15 +16,16 @@
==================================================================== */
package org.apache.poi.poifs.crypt;
+import org.apache.poi.poifs.filesystem.DocumentEntry;
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;
/**
* @author Maxim Valyanskiy
+ * @author Gary King
*/
public class EncryptionInfo {
private final int versionMajor;
@@ -37,24 +38,30 @@ public class EncryptionInfo {
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();
-
- int hSize = dis.readInt();
- header = new EncryptionHeader(dis);
+ encryptionFlags = dis.readInt();
- if (header.getAlgorithm()==EncryptionHeader.ALGORITHM_RC4) {
- verifier = new EncryptionVerifier(dis, 20);
+ 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);
} else {
- verifier = new EncryptionVerifier(dis, 32);
+ 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);
+ }
}
}
diff --git a/src/java/org/apache/poi/poifs/crypt/EncryptionVerifier.java b/src/java/org/apache/poi/poifs/crypt/EncryptionVerifier.java
index 0eccf338f0..f4028ec78f 100644
--- a/src/java/org/apache/poi/poifs/crypt/EncryptionVerifier.java
+++ b/src/java/org/apache/poi/poifs/crypt/EncryptionVerifier.java
@@ -16,16 +16,103 @@
==================================================================== */
package org.apache.poi.poifs.crypt;
+import java.io.ByteArrayInputStream;
+
+import org.apache.commons.codec.binary.Base64;
+
import org.apache.poi.poifs.filesystem.DocumentInputStream;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.w3c.dom.NamedNodeMap;
+import javax.xml.parsers.DocumentBuilderFactory;
+import org.apache.poi.EncryptedDocumentException;
+
/**
* @author Maxim Valyanskiy
+ * @author Gary King
*/
public class EncryptionVerifier {
- private final byte[] salt = new byte[16];
- private final byte[] verifier = new byte[16];
+ 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();
+
+ if ("AES".equals(alg)) {
+ if (blockSize == 16)
+ algorithm = EncryptionHeader.ALGORITHM_AES_128;
+ else if (blockSize == 24)
+ algorithm = EncryptionHeader.ALGORITHM_AES_192;
+ else if (blockSize == 32)
+ algorithm = EncryptionHeader.ALGORITHM_AES_256;
+ else
+ throw new EncryptedDocumentException("Unsupported block 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();
@@ -34,13 +121,20 @@ public class EncryptionVerifier {
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 byte[] getSalt() {
@@ -54,4 +148,20 @@ public class EncryptionVerifier {
public byte[] getVerifierHash() {
return verifierHash;
}
+
+ public int getSpinCount() {
+ return spinCount;
+ }
+
+ public int getCipherMode() {
+ return cipherMode;
+ }
+
+ public int getAlgorithm() {
+ return algorithm;
+ }
+
+ public byte[] getEncryptedKey() {
+ return encryptedKey;
+ }
}
diff --git a/src/testcases/org/apache/poi/poifs/crypt/AllPOIFSCryptoTests.java b/src/testcases/org/apache/poi/poifs/crypt/AllPOIFSCryptoTests.java
new file mode 100644
index 0000000000..d7aef1039f
--- /dev/null
+++ b/src/testcases/org/apache/poi/poifs/crypt/AllPOIFSCryptoTests.java
@@ -0,0 +1,37 @@
+/* ====================================================================
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+==================================================================== */
+
+package org.apache.poi.poifs.crypt;
+
+import junit.framework.Test;
+import junit.framework.TestSuite;
+
+
+/**
+ * Tests for org.apache.poi.poifs.crypt
+ *
+ * @author Gary King
+ */
+public final class AllPOIFSCryptoTests {
+
+ public static Test suite() {
+ TestSuite result = new TestSuite(AllPOIFSCryptoTests.class.getName());
+ result.addTestSuite(TestDecryptor.class);
+ result.addTestSuite(TestEncryptionInfo.class);
+ return result;
+ }
+} \ No newline at end of file
diff --git a/src/testcases/org/apache/poi/poifs/crypt/DecryptorTest.java b/src/testcases/org/apache/poi/poifs/crypt/TestDecryptor.java
index bb317d77c8..27385719c8 100644
--- a/src/testcases/org/apache/poi/poifs/crypt/DecryptorTest.java
+++ b/src/testcases/org/apache/poi/poifs/crypt/TestDecryptor.java
@@ -27,14 +27,15 @@ import java.util.zip.ZipInputStream;
/**
* @author Maxim Valyanskiy
+ * @author Gary King
*/
-public class DecryptorTest extends TestCase {
+public class TestDecryptor extends TestCase {
public void testPasswordVerification() throws IOException, GeneralSecurityException {
POIFSFileSystem fs = new POIFSFileSystem(POIDataSamples.getPOIFSInstance().openResourceAsStream("protect.xlsx"));
EncryptionInfo info = new EncryptionInfo(fs);
- Decryptor d = new Decryptor(info);
+ Decryptor d = Decryptor.getInstance(info);
assertTrue(d.verifyPassword(Decryptor.DEFAULT_PASSWORD));
}
@@ -44,13 +45,27 @@ public class DecryptorTest extends TestCase {
EncryptionInfo info = new EncryptionInfo(fs);
- Decryptor d = new Decryptor(info);
+ Decryptor d = Decryptor.getInstance(info);
d.verifyPassword(Decryptor.DEFAULT_PASSWORD);
zipOk(fs, d);
}
+ public void testAgile() throws IOException, GeneralSecurityException {
+ POIFSFileSystem fs = new POIFSFileSystem(POIDataSamples.getPOIFSInstance().openResourceAsStream("protected_agile.docx"));
+
+ EncryptionInfo info = new EncryptionInfo(fs);
+
+ assertTrue(info.getVersionMajor() == 4 && info.getVersionMinor() == 4);
+
+ Decryptor d = Decryptor.getInstance(info);
+
+ assertTrue(d.verifyPassword(Decryptor.DEFAULT_PASSWORD));
+
+ zipOk(fs, d);
+ }
+
private void zipOk(POIFSFileSystem fs, Decryptor d) throws IOException, GeneralSecurityException {
ZipInputStream zin = new ZipInputStream(d.getDataStream(fs));
diff --git a/src/testcases/org/apache/poi/poifs/crypt/EncryptionInfoTest.java b/src/testcases/org/apache/poi/poifs/crypt/TestEncryptionInfo.java
index eb84727e33..62607e7e8a 100644
--- a/src/testcases/org/apache/poi/poifs/crypt/EncryptionInfoTest.java
+++ b/src/testcases/org/apache/poi/poifs/crypt/TestEncryptionInfo.java
@@ -25,7 +25,7 @@ import java.io.IOException;
/**
* @author Maxim Valyanskiy
*/
-public class EncryptionInfoTest extends TestCase {
+public class TestEncryptionInfo extends TestCase {
public void testEncryptionInfo() throws IOException {
POIFSFileSystem fs = new POIFSFileSystem(POIDataSamples.getPOIFSInstance().openResourceAsStream("protect.xlsx"));
diff --git a/test-data/poifs/protected_agile.docx b/test-data/poifs/protected_agile.docx
new file mode 100644
index 0000000000..a7de3ebe43
--- /dev/null
+++ b/test-data/poifs/protected_agile.docx
Binary files differ