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
This commit is contained in:
Maxim Valyanskiy 2011-05-10 10:38:17 +00:00
parent 5f671bd806
commit 6857223c5a
12 changed files with 789 additions and 143 deletions

View File

@ -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.jar" location="${main.lib}/commons-logging-1.1.jar"/>
<property name="main.commons-logging.url" <property name="main.commons-logging.url"
value="${repository.m2}/maven2/commons-logging/commons-logging/1.1/commons-logging-1.1.jar"/> 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.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.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"/> <property name="main.junit.jar" location="${main.lib}/junit-3.8.1.jar"/>
@ -166,6 +169,7 @@ under the License.
<path id="main.classpath"> <path id="main.classpath">
<pathelement location="${main.commons-logging.jar}"/> <pathelement location="${main.commons-logging.jar}"/>
<pathelement location="${main.commons-codec.jar}"/>
<pathelement location="${main.log4j.jar}"/> <pathelement location="${main.log4j.jar}"/>
<pathelement location="${main.junit.jar}"/> <pathelement location="${main.junit.jar}"/>
</path> </path>
@ -295,6 +299,7 @@ under the License.
<or> <or>
<and> <and>
<available file="${main.commons-logging.jar}"/> <available file="${main.commons-logging.jar}"/>
<available file="${main.commons-codec.jar}"/>
<available file="${main.log4j.jar}"/> <available file="${main.log4j.jar}"/>
<available file="${main.junit.jar}"/> <available file="${main.junit.jar}"/>
<available file="${main.ant.jar}"/> <available file="${main.ant.jar}"/>
@ -311,6 +316,10 @@ under the License.
<param name="sourcefile" value="${main.commons-logging.url}"/> <param name="sourcefile" value="${main.commons-logging.url}"/>
<param name="destfile" value="${main.commons-logging.jar}"/> <param name="destfile" value="${main.commons-logging.jar}"/>
</antcall> </antcall>
<antcall target="downloadfile">
<param name="sourcefile" value="${main.commons-codec.url}"/>
<param name="destfile" value="${main.commons-codec.jar}"/>
</antcall>
<antcall target="downloadfile"> <antcall target="downloadfile">
<param name="sourcefile" value="${main.log4j.url}"/> <param name="sourcefile" value="${main.log4j.url}"/>
<param name="destfile" value="${main.log4j.jar}"/> <param name="destfile" value="${main.log4j.jar}"/>

View File

@ -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>

View File

@ -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));
}
}

View File

@ -19,150 +19,74 @@ package org.apache.poi.poifs.crypt;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException; 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.NPOIFSFileSystem;
import org.apache.poi.poifs.filesystem.POIFSFileSystem; 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.LittleEndian;
/** public abstract class Decryptor {
* @author Maxim Valyanskiy
*/
public class Decryptor {
public static final String DEFAULT_PASSWORD="VelvetSweatshop"; public static final String DEFAULT_PASSWORD="VelvetSweatshop";
private final EncryptionInfo info; public abstract InputStream getDataStream(DirectoryNode dir)
private byte[] passwordHash; throws IOException, GeneralSecurityException;
public Decryptor(EncryptionInfo info) { public abstract boolean verifyPassword(String password)
this.info = info; throws GeneralSecurityException;
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
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"); MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
byte[] bytes;
byte[] passwordBytes;
try { try {
passwordBytes = password.getBytes("UTF-16LE"); bytes = password.getBytes("UTF-16LE");
} catch(UnsupportedEncodingException e) { } catch (UnsupportedEncodingException e) {
throw new RuntimeException("Your JVM is broken - UTF16 not found!"); throw new EncryptedDocumentException("UTF16 not supported");
} }
sha1.update(info.getVerifier().getSalt()); sha1.update(info.getVerifier().getSalt());
byte[] hash = sha1.digest(passwordBytes); byte[] hash = sha1.digest(bytes);
byte[] iterator = new byte[4]; 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); LittleEndian.putInt(iterator, i);
sha1.update(iterator); sha1.update(iterator);
hash = sha1.digest(hash); hash = sha1.digest(hash);
} }
passwordHash = hash; return 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());
}
}

View File

@ -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());
}
}

View File

@ -16,12 +16,19 @@
==================================================================== */ ==================================================================== */
package org.apache.poi.poifs.crypt; package org.apache.poi.poifs.crypt;
import org.apache.commons.codec.binary.Base64;
import org.apache.poi.poifs.filesystem.DocumentInputStream; import org.apache.poi.poifs.filesystem.DocumentInputStream;
import java.io.IOException; 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 Maxim Valyanskiy
* @author Gary King
*/ */
public class EncryptionHeader { public class EncryptionHeader {
public static final int ALGORITHM_RC4 = 0x6801; 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 HASH_SHA1 = 0x8004;
public static final int PROVIDER_RC4 = 1; 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 flags;
private final int sizeExtra; private final int sizeExtra;
@ -40,6 +51,8 @@ public class EncryptionHeader {
private final int hashAlgorithm; private final int hashAlgorithm;
private final int keySize; private final int keySize;
private final int providerType; private final int providerType;
private final int cipherMode;
private final byte[] keySalt;
private final String cspName; private final String cspName;
public EncryptionHeader(DocumentInputStream is) throws IOException { public EncryptionHeader(DocumentInputStream is) throws IOException {
@ -63,8 +76,75 @@ public class EncryptionHeader {
builder.append(c); builder.append(c);
} }
cspName = builder.toString(); 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() { public int getFlags() {
@ -87,6 +167,10 @@ public class EncryptionHeader {
return keySize; return keySize;
} }
public byte[] getKeySalt() {
return keySalt;
}
public int getProviderType() { public int getProviderType() {
return providerType; return providerType;
} }

View File

@ -16,15 +16,16 @@
==================================================================== */ ==================================================================== */
package org.apache.poi.poifs.crypt; 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.DirectoryNode;
import org.apache.poi.poifs.filesystem.DocumentInputStream; 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.POIFSFileSystem;
import java.io.IOException; import java.io.IOException;
/** /**
* @author Maxim Valyanskiy * @author Maxim Valyanskiy
* @author Gary King
*/ */
public class EncryptionInfo { public class EncryptionInfo {
private final int versionMajor; private final int versionMajor;
@ -37,24 +38,30 @@ public class EncryptionInfo {
public EncryptionInfo(POIFSFileSystem fs) throws IOException { public EncryptionInfo(POIFSFileSystem fs) throws IOException {
this(fs.getRoot()); this(fs.getRoot());
} }
public EncryptionInfo(NPOIFSFileSystem fs) throws IOException {
this(fs.getRoot());
}
public EncryptionInfo(DirectoryNode dir) throws IOException { public EncryptionInfo(DirectoryNode dir) throws IOException {
DocumentInputStream dis = dir.createDocumentInputStream("EncryptionInfo"); DocumentInputStream dis = dir.createDocumentInputStream("EncryptionInfo");
versionMajor = dis.readShort(); versionMajor = dis.readShort();
versionMinor = dis.readShort(); versionMinor = dis.readShort();
encryptionFlags = dis.readInt(); encryptionFlags = dis.readInt();
int hSize = dis.readInt(); if (versionMajor == 4 && versionMinor == 4 && encryptionFlags == 0x40) {
StringBuilder builder = new StringBuilder();
header = new EncryptionHeader(dis); byte[] xmlDescriptor = new byte[dis.available()];
dis.read(xmlDescriptor);
if (header.getAlgorithm()==EncryptionHeader.ALGORITHM_RC4) { for (byte b : xmlDescriptor)
verifier = new EncryptionVerifier(dis, 20); builder.append((char)b);
String descriptor = builder.toString();
header = new EncryptionHeader(descriptor);
verifier = new EncryptionVerifier(descriptor);
} else { } 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);
}
} }
} }

View File

@ -16,16 +16,103 @@
==================================================================== */ ==================================================================== */
package org.apache.poi.poifs.crypt; 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.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 Maxim Valyanskiy
* @author Gary King
*/ */
public class EncryptionVerifier { public class EncryptionVerifier {
private final byte[] salt = new byte[16]; private final byte[] salt;
private final byte[] verifier = new byte[16]; private final byte[] verifier;
private final byte[] verifierHash; private final byte[] verifierHash;
private final byte[] encryptedKey;
private final int verifierHashSize; 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) { public EncryptionVerifier(DocumentInputStream is, int encryptedLength) {
int saltSize = is.readInt(); int saltSize = is.readInt();
@ -34,13 +121,20 @@ public class EncryptionVerifier {
throw new RuntimeException("Salt size != 16 !?"); throw new RuntimeException("Salt size != 16 !?");
} }
salt = new byte[16];
is.readFully(salt); is.readFully(salt);
verifier = new byte[16];
is.readFully(verifier); is.readFully(verifier);
verifierHashSize = is.readInt(); verifierHashSize = is.readInt();
verifierHash = new byte[encryptedLength]; verifierHash = new byte[encryptedLength];
is.readFully(verifierHash); is.readFully(verifierHash);
spinCount = 50000;
algorithm = EncryptionHeader.ALGORITHM_AES_128;
cipherMode = EncryptionHeader.MODE_ECB;
encryptedKey = null;
} }
public byte[] getSalt() { public byte[] getSalt() {
@ -54,4 +148,20 @@ public class EncryptionVerifier {
public byte[] getVerifierHash() { public byte[] getVerifierHash() {
return verifierHash; return verifierHash;
} }
public int getSpinCount() {
return spinCount;
}
public int getCipherMode() {
return cipherMode;
}
public int getAlgorithm() {
return algorithm;
}
public byte[] getEncryptedKey() {
return encryptedKey;
}
} }

View File

@ -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;
}
}

View File

@ -27,14 +27,15 @@ import java.util.zip.ZipInputStream;
/** /**
* @author Maxim Valyanskiy * @author Maxim Valyanskiy
* @author Gary King
*/ */
public class DecryptorTest extends TestCase { public class TestDecryptor extends TestCase {
public void testPasswordVerification() throws IOException, GeneralSecurityException { public void testPasswordVerification() throws IOException, GeneralSecurityException {
POIFSFileSystem fs = new POIFSFileSystem(POIDataSamples.getPOIFSInstance().openResourceAsStream("protect.xlsx")); POIFSFileSystem fs = new POIFSFileSystem(POIDataSamples.getPOIFSInstance().openResourceAsStream("protect.xlsx"));
EncryptionInfo info = new EncryptionInfo(fs); EncryptionInfo info = new EncryptionInfo(fs);
Decryptor d = new Decryptor(info); Decryptor d = Decryptor.getInstance(info);
assertTrue(d.verifyPassword(Decryptor.DEFAULT_PASSWORD)); assertTrue(d.verifyPassword(Decryptor.DEFAULT_PASSWORD));
} }
@ -44,13 +45,27 @@ public class DecryptorTest extends TestCase {
EncryptionInfo info = new EncryptionInfo(fs); EncryptionInfo info = new EncryptionInfo(fs);
Decryptor d = new Decryptor(info); Decryptor d = Decryptor.getInstance(info);
d.verifyPassword(Decryptor.DEFAULT_PASSWORD); d.verifyPassword(Decryptor.DEFAULT_PASSWORD);
zipOk(fs, d); 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 { private void zipOk(POIFSFileSystem fs, Decryptor d) throws IOException, GeneralSecurityException {
ZipInputStream zin = new ZipInputStream(d.getDataStream(fs)); ZipInputStream zin = new ZipInputStream(d.getDataStream(fs));

View File

@ -25,7 +25,7 @@ import java.io.IOException;
/** /**
* @author Maxim Valyanskiy * @author Maxim Valyanskiy
*/ */
public class EncryptionInfoTest extends TestCase { public class TestEncryptionInfo extends TestCase {
public void testEncryptionInfo() throws IOException { public void testEncryptionInfo() throws IOException {
POIFSFileSystem fs = new POIFSFileSystem(POIDataSamples.getPOIFSInstance().openResourceAsStream("protect.xlsx")); POIFSFileSystem fs = new POIFSFileSystem(POIDataSamples.getPOIFSInstance().openResourceAsStream("protect.xlsx"));

Binary file not shown.