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:
parent
5f671bd806
commit
6857223c5a
@ -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}"/>
|
||||||
|
84
src/documentation/content/xdocs/encryption.xml
Normal file
84
src/documentation/content/xdocs/encryption.xml
Normal 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>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
244
src/java/org/apache/poi/poifs/crypt/AgileDecryptor.java
Normal file
244
src/java/org/apache/poi/poifs/crypt/AgileDecryptor.java
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
}
|
|
||||||
|
|
||||||
private void generatePasswordHash(String password) throws NoSuchAlgorithmException {
|
public static Decryptor getInstance(EncryptionInfo info) {
|
||||||
MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
|
int major = info.getVersionMajor();
|
||||||
|
int minor = info.getVersionMinor();
|
||||||
|
|
||||||
byte[] passwordBytes;
|
if (major == 4 && minor == 4)
|
||||||
try {
|
return new AgileDecryptor(info);
|
||||||
passwordBytes = password.getBytes("UTF-16LE");
|
else if (minor == 2 && (major == 3 || major == 4))
|
||||||
} catch(UnsupportedEncodingException e) {
|
return new EcmaDecryptor(info);
|
||||||
throw new RuntimeException("Your JVM is broken - UTF16 not found!");
|
else
|
||||||
}
|
throw new EncryptedDocumentException("Unsupported version");
|
||||||
|
|
||||||
sha1.update(info.getVerifier().getSalt());
|
|
||||||
byte[] hash = sha1.digest(passwordBytes);
|
|
||||||
|
|
||||||
byte[] iterator = new byte[4];
|
|
||||||
for (int i = 0; i<50000; 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 {
|
public InputStream getDataStream(NPOIFSFileSystem fs) throws IOException, GeneralSecurityException {
|
||||||
return getDataStream(fs.getRoot());
|
return getDataStream(fs.getRoot());
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
public InputStream getDataStream(POIFSFileSystem fs) throws IOException, GeneralSecurityException {
|
||||||
public InputStream getDataStream(DirectoryNode dir) throws IOException, GeneralSecurityException {
|
return getDataStream(fs.getRoot());
|
||||||
DocumentInputStream dis = dir.createDocumentInputStream("EncryptedPackage");
|
}
|
||||||
|
|
||||||
long size = dis.readLong();
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
return new CipherInputStream(dis, getCipher());
|
protected byte[] hashPassword(EncryptionInfo info,
|
||||||
|
String password) throws NoSuchAlgorithmException {
|
||||||
|
MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
|
||||||
|
byte[] bytes;
|
||||||
|
try {
|
||||||
|
bytes = password.getBytes("UTF-16LE");
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
throw new EncryptedDocumentException("UTF16 not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
sha1.update(info.getVerifier().getSalt());
|
||||||
|
byte[] hash = sha1.digest(bytes);
|
||||||
|
byte[] iterator = new byte[4];
|
||||||
|
|
||||||
|
for (int i = 0; i < info.getVerifier().getSpinCount(); i++) {
|
||||||
|
sha1.reset();
|
||||||
|
LittleEndian.putInt(iterator, i);
|
||||||
|
sha1.update(iterator);
|
||||||
|
hash = sha1.digest(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
return hash;
|
||||||
}
|
}
|
||||||
}
|
}
|
132
src/java/org/apache/poi/poifs/crypt/EcmaDecryptor.java
Normal file
132
src/java/org/apache/poi/poifs/crypt/EcmaDecryptor.java
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
@ -34,12 +41,18 @@ public class EncryptionHeader {
|
|||||||
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;
|
||||||
private final int algorithm;
|
private final int algorithm;
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
@ -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,26 +38,32 @@ 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();
|
||||||
|
|
||||||
|
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 {
|
||||||
int hSize = dis.readInt();
|
int hSize = dis.readInt();
|
||||||
|
|
||||||
header = new EncryptionHeader(dis);
|
header = new EncryptionHeader(dis);
|
||||||
|
|
||||||
if (header.getAlgorithm()==EncryptionHeader.ALGORITHM_RC4) {
|
if (header.getAlgorithm()==EncryptionHeader.ALGORITHM_RC4) {
|
||||||
verifier = new EncryptionVerifier(dis, 20);
|
verifier = new EncryptionVerifier(dis, 20);
|
||||||
} else {
|
} else {
|
||||||
verifier = new EncryptionVerifier(dis, 32);
|
verifier = new EncryptionVerifier(dis, 32);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public int getVersionMajor() {
|
public int getVersionMajor() {
|
||||||
return versionMajor;
|
return versionMajor;
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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));
|
||||||
|
|
@ -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"));
|
||||||
|
|
BIN
test-data/poifs/protected_agile.docx
Normal file
BIN
test-data/poifs/protected_agile.docx
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user