<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"/>
<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>
<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}"/>
<param name="sourcefile" value="${main.commons-logging.url}"/>
<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}"/>
--- /dev/null
+<?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>
+
+
+
+
--- /dev/null
+/* ====================================================================
+ 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
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
--- /dev/null
+/* ====================================================================
+ 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());
+ }
+}
==================================================================== */
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;
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;
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 {
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() {
return keySize;
}
+ public byte[] getKeySalt() {
+ return keySalt;
+ }
+
public int getProviderType() {
return providerType;
}
==================================================================== */
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;
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);
+ }
}
}
==================================================================== */
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();
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() {
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;
+ }
}
--- /dev/null
+/* ====================================================================
+ 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
+++ /dev/null
-/* ====================================================================
- Licensed to the Apache Software Foundation (ASF) under one or more
- contributor license agreements. See the NOTICE file distributed with
- this work for additional information regarding copyright ownership.
- The ASF licenses this file to You under the Apache License, Version 2.0
- (the "License"); you may not use this file except in compliance with
- the License. You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-==================================================================== */
-package org.apache.poi.poifs.crypt;
-
-import junit.framework.TestCase;
-import org.apache.poi.POIDataSamples;
-import org.apache.poi.poifs.filesystem.POIFSFileSystem;
-
-import java.io.IOException;
-import java.security.GeneralSecurityException;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipInputStream;
-
-/**
- * @author Maxim Valyanskiy
- */
-public class DecryptorTest 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);
-
- assertTrue(d.verifyPassword(Decryptor.DEFAULT_PASSWORD));
- }
-
- public void testDecrypt() throws IOException, GeneralSecurityException {
- POIFSFileSystem fs = new POIFSFileSystem(POIDataSamples.getPOIFSInstance().openResourceAsStream("protect.xlsx"));
-
- EncryptionInfo info = new EncryptionInfo(fs);
-
- Decryptor d = new Decryptor(info);
-
- d.verifyPassword(Decryptor.DEFAULT_PASSWORD);
-
- zipOk(fs, d);
- }
-
- private void zipOk(POIFSFileSystem fs, Decryptor d) throws IOException, GeneralSecurityException {
- ZipInputStream zin = new ZipInputStream(d.getDataStream(fs));
-
- while (true) {
- ZipEntry entry = zin.getNextEntry();
- if (entry==null) {
- break;
- }
-
- while (zin.available()>0) {
- zin.skip(zin.available());
- }
- }
- }
-}
+++ /dev/null
-/* ====================================================================
- Licensed to the Apache Software Foundation (ASF) under one or more
- contributor license agreements. See the NOTICE file distributed with
- this work for additional information regarding copyright ownership.
- The ASF licenses this file to You under the Apache License, Version 2.0
- (the "License"); you may not use this file except in compliance with
- the License. You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-==================================================================== */
-package org.apache.poi.poifs.crypt;
-
-import junit.framework.TestCase;
-import org.apache.poi.POIDataSamples;
-import org.apache.poi.poifs.filesystem.POIFSFileSystem;
-
-import java.io.IOException;
-
-/**
- * @author Maxim Valyanskiy
- */
-public class EncryptionInfoTest extends TestCase {
- public void testEncryptionInfo() throws IOException {
- POIFSFileSystem fs = new POIFSFileSystem(POIDataSamples.getPOIFSInstance().openResourceAsStream("protect.xlsx"));
-
- EncryptionInfo info = new EncryptionInfo(fs);
-
- assertEquals(3, info.getVersionMajor());
- assertEquals(2, info.getVersionMinor());
-
- assertEquals(EncryptionHeader.ALGORITHM_AES_128, info.getHeader().getAlgorithm());
- assertEquals(EncryptionHeader.HASH_SHA1, info.getHeader().getHashAlgorithm());
- assertEquals(128, info.getHeader().getKeySize());
- assertEquals(EncryptionHeader.PROVIDER_AES, info.getHeader().getProviderType());
- assertEquals("Microsoft Enhanced RSA and AES Cryptographic Provider", info.getHeader().getCspName());
-
- assertEquals(32, info.getVerifier().getVerifierHash().length);
- }
-}
--- /dev/null
+/* ====================================================================
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+==================================================================== */
+package org.apache.poi.poifs.crypt;
+
+import junit.framework.TestCase;
+import org.apache.poi.POIDataSamples;
+import org.apache.poi.poifs.filesystem.POIFSFileSystem;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+/**
+ * @author Maxim Valyanskiy
+ * @author Gary King
+ */
+public class TestDecryptor extends TestCase {
+ public void testPasswordVerification() throws IOException, GeneralSecurityException {
+ POIFSFileSystem fs = new POIFSFileSystem(POIDataSamples.getPOIFSInstance().openResourceAsStream("protect.xlsx"));
+
+ EncryptionInfo info = new EncryptionInfo(fs);
+
+ Decryptor d = Decryptor.getInstance(info);
+
+ assertTrue(d.verifyPassword(Decryptor.DEFAULT_PASSWORD));
+ }
+
+ public void testDecrypt() throws IOException, GeneralSecurityException {
+ POIFSFileSystem fs = new POIFSFileSystem(POIDataSamples.getPOIFSInstance().openResourceAsStream("protect.xlsx"));
+
+ EncryptionInfo info = new EncryptionInfo(fs);
+
+ Decryptor d = Decryptor.getInstance(info);
+
+ d.verifyPassword(Decryptor.DEFAULT_PASSWORD);
+
+ zipOk(fs, d);
+ }
+
+ public void testAgile() throws IOException, GeneralSecurityException {
+ POIFSFileSystem fs = new POIFSFileSystem(POIDataSamples.getPOIFSInstance().openResourceAsStream("protected_agile.docx"));
+
+ EncryptionInfo info = new EncryptionInfo(fs);
+
+ assertTrue(info.getVersionMajor() == 4 && info.getVersionMinor() == 4);
+
+ Decryptor d = Decryptor.getInstance(info);
+
+ assertTrue(d.verifyPassword(Decryptor.DEFAULT_PASSWORD));
+
+ zipOk(fs, d);
+ }
+
+ private void zipOk(POIFSFileSystem fs, Decryptor d) throws IOException, GeneralSecurityException {
+ ZipInputStream zin = new ZipInputStream(d.getDataStream(fs));
+
+ while (true) {
+ ZipEntry entry = zin.getNextEntry();
+ if (entry==null) {
+ break;
+ }
+
+ while (zin.available()>0) {
+ zin.skip(zin.available());
+ }
+ }
+ }
+}
--- /dev/null
+/* ====================================================================
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+==================================================================== */
+package org.apache.poi.poifs.crypt;
+
+import junit.framework.TestCase;
+import org.apache.poi.POIDataSamples;
+import org.apache.poi.poifs.filesystem.POIFSFileSystem;
+
+import java.io.IOException;
+
+/**
+ * @author Maxim Valyanskiy
+ */
+public class TestEncryptionInfo extends TestCase {
+ public void testEncryptionInfo() throws IOException {
+ POIFSFileSystem fs = new POIFSFileSystem(POIDataSamples.getPOIFSInstance().openResourceAsStream("protect.xlsx"));
+
+ EncryptionInfo info = new EncryptionInfo(fs);
+
+ assertEquals(3, info.getVersionMajor());
+ assertEquals(2, info.getVersionMinor());
+
+ assertEquals(EncryptionHeader.ALGORITHM_AES_128, info.getHeader().getAlgorithm());
+ assertEquals(EncryptionHeader.HASH_SHA1, info.getHeader().getHashAlgorithm());
+ assertEquals(128, info.getHeader().getKeySize());
+ assertEquals(EncryptionHeader.PROVIDER_AES, info.getHeader().getProviderType());
+ assertEquals("Microsoft Enhanced RSA and AES Cryptographic Provider", info.getHeader().getCspName());
+
+ assertEquals(32, info.getVerifier().getVerifierHash().length);
+ }
+}