Patch for Bug/Enhancement 55818 - add encryption support

git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1553336 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Andreas Beeker 2013-12-24 23:13:21 +00:00
parent 97e6fae71e
commit bf2b13b9fa
45 changed files with 4718 additions and 978 deletions

View File

@ -24,6 +24,7 @@
<classpathentry kind="lib" path="lib/hamcrest-core-1.3.jar"/>
<classpathentry kind="lib" path="lib/junit-4.11.jar"/>
<classpathentry kind="lib" path="ooxml-lib/ooxml-schemas-1.1.jar" sourcepath="ooxml-lib/ooxml-schemas-src-1.1.jar"/>
<classpathentry kind="lib" path="ooxml-lib/ooxml-encryption-1.1.jar" sourcepath="ooxml-lib/ooxml-encryption-src-1.1.jar"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
<classpathentry kind="output" path="build/eclipse"/>
</classpath>

View File

@ -116,6 +116,7 @@ under the License.
<property name="ooxml.output.test.dir" location="build/ooxml-test-classes"/>
<property name="ooxml.testokfile" location="build/ooxml-testokfile.txt"/>
<property name="ooxml.lite.output.dir" location="build/ooxml-lite-classes"/>
<property name="ooxml.encryption.xsd.dir" location="src/ooxml/resources/org/apache/poi/poifs/crypt"/>
<!-- Excelant: -->
<property name="excelant.resource.dir" value="src/excelant/resources"/>
@ -167,6 +168,11 @@ under the License.
<property name="ooxml.xsds.src.jar" location="${ooxml.lib}/ooxml-schemas-src-1.1.jar"/>
<property name="ooxml.xsds.jar" location="${ooxml.lib}/ooxml-schemas-1.1.jar"/>
<property name="ooxml.encryption.src.dir" location="build/ooxml-encryption-src"/>
<property name="ooxml.encryption.src.jar" location="${ooxml.lib}/ooxml-encryption-src-1.1.jar"/>
<property name="ooxml.encryption.jar" location="${ooxml.lib}/ooxml-encryption-1.1.jar"/>
<property name="maven.ooxml.xsds.version.id" value="1.0"/>
<property name="maven.ooxml.xsds.jar" value="ooxml-schemas-${maven.ooxml.xsds.version.id}.jar"/>
@ -210,6 +216,7 @@ under the License.
<path refid="main.classpath"/>
<pathelement location="${main.output.dir}"/>
<pathelement location="${scratchpad.output.dir}"/>
<pathelement location="${ooxml.encryption.jar}"/>
</path>
<path id="test.classpath">
@ -436,9 +443,13 @@ under the License.
<target name="check-compiled-ooxml-xsds" depends="fetch-ooxml-xsds">
<condition property="ooxml-compiled-xsds.present">
<or>
<and>
<available file="${ooxml.xsds.jar}"/>
</and>
<available file="${ooxml.xsds.jar}"/>
<isset property="disconnected"/>
</or>
</condition>
<condition property="ooxml-compiled-encryption-xsds.present">
<or>
<available file="${ooxml.encryption.jar}"/>
<isset property="disconnected"/>
</or>
</condition>
@ -485,6 +496,40 @@ under the License.
/>
</target>
<target name="compile-ooxml-encryption-xsds" unless="ooxml-compiled-encryption-xsds.present"
depends="check-jars,fetch-jars,check-compiled-ooxml-xsds"
description="Compiles the OOXML encryption xsd files into XmlBeans">
<taskdef name="xmlbean"
classname="org.apache.xmlbeans.impl.tool.XMLBean"
classpath="${ooxml.xmlbeans.jar}:${ooxml.jsr173.jar}"/>
<!-- We need a fair amount of memory to compile the xml schema, -->
<!-- but limit it in case it goes wrong! -->
<!-- Pick the right amount based on 32 vs 64 bit jvm -->
<condition property="ooxml.memory" value="768m" else="512m">
<equals arg1="${sun.arch.data.model}" arg2="64" />
</condition>
<xmlbean
schema="${ooxml.encryption.xsd.dir}"
srcgendir="${ooxml.encryption.src.dir}"
optimize="yes"
destfile="${ooxml.encryption.jar}"
javasource="1.5"
failonerror="true"
fork="true"
memoryMaximumSize="${ooxml.memory}"
>
<classpath refid="ooxml.classpath"/>
</xmlbean>
<!-- Now make a jar of the schema sources -->
<jar
basedir="${ooxml.encryption.src.dir}"
destfile="${ooxml.encryption.src.jar}"
/>
</target>
<target name="compile" depends="init, compile-main,
compile-scratchpad, compile-examples, compile-excelant"
description="Compiles the POI main classes, scratchpad and examples"/>
@ -571,7 +616,7 @@ under the License.
</copy>
</target>
<target name="compile-ooxml" depends="compile-main,compile-scratchpad,compile-ooxml-xsds">
<target name="compile-ooxml" depends="compile-main,compile-scratchpad,compile-ooxml-xsds,compile-ooxml-encryption-xsds">
<javac target="${jdk.version.class}"
source="${jdk.version.source}"
destdir="${ooxml.output.dir}"
@ -853,12 +898,23 @@ under the License.
</target>
<target name="compile-ooxml-lite" depends="compile-ooxml">
<property name="ooxml.lite-merged.dir" location="build/ooxml-lite-merged"/>
<mkdir dir="${ooxml.lite-merged.dir}"/>
<jar destfile="${ooxml.lite-merged.dir}/ooxml-lite-merged.jar">
<zipfileset includes="**/*" src="${ooxml.xsds.jar}"/>
<zipfileset includes="**/*" src="${ooxml.encryption.jar}"/>
</jar>
<java classname="org.apache.poi.util.OOXMLLite" fork="yes">
<classpath>
<pathelement path="${ooxml.lite-merged.dir}/ooxml-lite-merged.jar"/>
</classpath>
<classpath refid="test.ooxml.classpath"/>
<syspropertyset refid="junit.properties"/>
<jvmarg value="${poi.test.locale}"/>
<arg value="-ooxml"/>
<arg value="${ooxml.xsds.jar}"/>
<arg value="${ooxml.lite-merged.dir}/ooxml-lite-merged.jar"/>
<arg value="-test"/>
<arg value="${ooxml.output.test.dir}"/>
<arg value="-dest"/>
@ -951,7 +1007,7 @@ under the License.
description="Generates the API documentation">
<javadoc verbose="false" author="true" destdir="${apidocs.report.dir}"
windowtitle="POI API Documentation" use="true" version="true"
maxmemory="256M" additionalparam="-notimestamp">
maxmemory="384M" additionalparam="-notimestamp">
<packageset dir="${main.src}" defaultexcludes="yes">
<include name="org/apache/poi/**"/>

View File

@ -16,9 +16,18 @@
==================================================================== */
package org.apache.poi;
@SuppressWarnings("serial")
public class EncryptedDocumentException extends IllegalStateException
{
public EncryptedDocumentException(String s) {
super(s);
}
public EncryptedDocumentException(String message, Throwable cause) {
super(message, cause);
}
public EncryptedDocumentException(Throwable cause) {
super(cause);
}
}

View File

@ -1,268 +0,0 @@
/* ====================================================================
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==================================================================== */
package org.apache.poi.poifs.crypt;
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.apache.poi.EncryptedDocumentException;
import org.apache.poi.poifs.filesystem.DirectoryNode;
import org.apache.poi.poifs.filesystem.DocumentInputStream;
import org.apache.poi.util.LittleEndian;
/**
*
*/
public class AgileDecryptor extends Decryptor {
private final EncryptionInfo _info;
private SecretKey _secretKey;
private long _length = -1;
private static final byte[] kVerifierInputBlock;
private static final byte[] kHashedVerifierBlock;
private static final byte[] kCryptoKeyBlock;
static {
kVerifierInputBlock =
new byte[] { (byte)0xfe, (byte)0xa7, (byte)0xd2, (byte)0x76,
(byte)0x3b, (byte)0x4b, (byte)0x9e, (byte)0x79 };
kHashedVerifierBlock =
new byte[] { (byte)0xd7, (byte)0xaa, (byte)0x0f, (byte)0x6d,
(byte)0x30, (byte)0x61, (byte)0x34, (byte)0x4e };
kCryptoKeyBlock =
new byte[] { (byte)0x14, (byte)0x6e, (byte)0x0b, (byte)0xe7,
(byte)0xab, (byte)0xac, (byte)0xd0, (byte)0xd6 };
}
public boolean verifyPassword(String password) throws GeneralSecurityException {
EncryptionVerifier verifier = _info.getVerifier();
byte[] salt = verifier.getSalt();
byte[] pwHash = hashPassword(_info, password);
byte[] iv = generateIv(salt, null);
SecretKey skey;
skey = new SecretKeySpec(generateKey(pwHash, kVerifierInputBlock), "AES");
Cipher cipher = getCipher(skey, iv);
byte[] verifierHashInput = cipher.doFinal(verifier.getVerifier());
MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
byte[] trimmed = new byte[salt.length];
System.arraycopy(verifierHashInput, 0, trimmed, 0, trimmed.length);
byte[] hashedVerifier = sha1.digest(trimmed);
skey = new SecretKeySpec(generateKey(pwHash, kHashedVerifierBlock), "AES");
iv = generateIv(salt, null);
cipher = getCipher(skey, iv);
byte[] verifierHash = cipher.doFinal(verifier.getVerifierHash());
trimmed = new byte[hashedVerifier.length];
System.arraycopy(verifierHash, 0, trimmed, 0, trimmed.length);
if (Arrays.equals(trimmed, hashedVerifier)) {
skey = new SecretKeySpec(generateKey(pwHash, kCryptoKeyBlock), "AES");
iv = generateIv(salt, null);
cipher = getCipher(skey, iv);
byte[] inter = cipher.doFinal(verifier.getEncryptedKey());
byte[] keyspec = new byte[getKeySizeInBytes()];
System.arraycopy(inter, 0, keyspec, 0, keyspec.length);
_secretKey = new SecretKeySpec(keyspec, "AES");
return true;
} else {
return false;
}
}
public InputStream getDataStream(DirectoryNode dir) throws IOException, GeneralSecurityException {
DocumentInputStream dis = dir.createDocumentInputStream("EncryptedPackage");
_length = dis.readLong();
return new ChunkedCipherInputStream(dis, _length);
}
public long getLength(){
if(_length == -1) throw new IllegalStateException("EcmaDecryptor.getDataStream() was not called");
return _length;
}
protected AgileDecryptor(EncryptionInfo info) {
_info = info;
}
private class ChunkedCipherInputStream extends InputStream {
private int _lastIndex = 0;
private long _pos = 0;
private final long _size;
private final DocumentInputStream _stream;
private byte[] _chunk;
private Cipher _cipher;
public ChunkedCipherInputStream(DocumentInputStream stream, long size)
throws GeneralSecurityException {
_size = size;
_stream = stream;
_cipher = getCipher(_secretKey, _info.getHeader().getKeySalt());
}
public int read() throws IOException {
byte[] b = new byte[1];
if (read(b) == 1)
return b[0];
return -1;
}
public int read(byte[] b) throws IOException {
return read(b, 0, b.length);
}
public int read(byte[] b, int off, int len) throws IOException {
int total = 0;
while (len > 0) {
if (_chunk == null) {
try {
_chunk = nextChunk();
} catch (GeneralSecurityException e) {
throw new EncryptedDocumentException(e.getMessage());
}
}
int count = (int)(4096L - (_pos & 0xfff));
count = Math.min(available(), Math.min(count, len));
System.arraycopy(_chunk, (int)(_pos & 0xfff), b, off, count);
off += count;
len -= count;
_pos += count;
if ((_pos & 0xfff) == 0)
_chunk = null;
total += count;
}
return total;
}
public long skip(long n) throws IOException {
long start = _pos;
long skip = Math.min(available(), n);
if ((((_pos + skip) ^ start) & ~0xfff) != 0)
_chunk = null;
_pos += skip;
return skip;
}
public int available() throws IOException { return (int)(_size - _pos); }
public void close() throws IOException { _stream.close(); }
public boolean markSupported() { return false; }
private byte[] nextChunk() throws GeneralSecurityException, IOException {
int index = (int)(_pos >> 12);
byte[] blockKey = new byte[4];
LittleEndian.putInt(blockKey, 0, index);
byte[] iv = generateIv(_info.getHeader().getKeySalt(), blockKey);
_cipher.init(Cipher.DECRYPT_MODE, _secretKey, new IvParameterSpec(iv));
if (_lastIndex != index)
_stream.skip((index - _lastIndex) << 12);
byte[] block = new byte[Math.min(_stream.available(), 4096)];
_stream.readFully(block);
_lastIndex = index + 1;
return _cipher.doFinal(block);
}
}
private Cipher getCipher(SecretKey key, byte[] vec)
throws GeneralSecurityException {
String name = null;
String chain = null;
EncryptionVerifier verifier = _info.getVerifier();
switch (verifier.getAlgorithm()) {
case EncryptionHeader.ALGORITHM_AES_128:
case EncryptionHeader.ALGORITHM_AES_192:
case EncryptionHeader.ALGORITHM_AES_256:
name = "AES";
break;
default:
throw new EncryptedDocumentException("Unsupported algorithm");
}
// Ensure the JCE policies files allow for this sized key
if (Cipher.getMaxAllowedKeyLength(name) < _info.getHeader().getKeySize()) {
throw new EncryptedDocumentException("Export Restrictions in place - please install JCE Unlimited Strength Jurisdiction Policy files");
}
switch (verifier.getCipherMode()) {
case EncryptionHeader.MODE_CBC:
chain = "CBC";
break;
case EncryptionHeader.MODE_CFB:
chain = "CFB";
break;
default:
throw new EncryptedDocumentException("Unsupported chain mode");
}
Cipher cipher = Cipher.getInstance(name + "/" + chain + "/NoPadding");
IvParameterSpec iv = new IvParameterSpec(vec);
cipher.init(Cipher.DECRYPT_MODE, key, iv);
return cipher;
}
private byte[] getBlock(byte[] hash, int size) {
byte[] result = new byte[size];
Arrays.fill(result, (byte)0x36);
System.arraycopy(hash, 0, result, 0, Math.min(result.length, hash.length));
return result;
}
private byte[] generateKey(byte[] hash, byte[] blockKey) throws NoSuchAlgorithmException {
MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
sha1.update(hash);
byte[] key = sha1.digest(blockKey);
return getBlock(key, getKeySizeInBytes());
}
protected byte[] generateIv(byte[] salt, byte[] blockKey)
throws NoSuchAlgorithmException {
if (blockKey == null)
return getBlock(salt, getBlockSizeInBytes());
MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
sha1.update(salt);
return getBlock(sha1.digest(blockKey), getBlockSizeInBytes());
}
protected int getBlockSizeInBytes() {
return _info.getHeader().getBlockSize();
}
protected int getKeySizeInBytes() {
return _info.getHeader().getKeySize()/8;
}
}

View File

@ -0,0 +1,33 @@
/* ====================================================================
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==================================================================== */
package org.apache.poi.poifs.crypt;
public enum ChainingMode {
// ecb - only for standard encryption
ecb("ECB", 1),
cbc("CBC", 2),
/* Cipher feedback chaining (CFB), with an 8-bit window */
cfb("CFB8", 3);
public final String jceId;
public final int ecmaId;
ChainingMode(String jceId, int ecmaId) {
this.jceId = jceId;
this.ecmaId = ecmaId;
}
}

View File

@ -0,0 +1,79 @@
/* ====================================================================
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==================================================================== */
package org.apache.poi.poifs.crypt;
import org.apache.poi.EncryptedDocumentException;
public enum CipherAlgorithm {
// key size for rc4: 0x00000028 - 0x00000080 (inclusive) with 8-bit increments
// no block size, because its a streaming cipher
rc4(CipherProvider.rc4, "RC4", 0x6801, 0x40, new int[]{0x28,0x30,0x38,0x40,0x48,0x50,0x58,0x60,0x68,0x70,0x78,0x80}, -1, 20, "RC4", false),
// aes has always a block size of 128 - only its keysize may vary
aes128(CipherProvider.aes, "AES", 0x660E, 128, new int[]{128}, 16, 32, "AES", false),
aes192(CipherProvider.aes, "AES", 0x660F, 192, new int[]{192}, 16, 32, "AES", false),
aes256(CipherProvider.aes, "AES", 0x6610, 256, new int[]{256}, 16, 32, "AES", false),
rc2(null, "RC2", -1, 0x80, new int[]{0x28,0x30,0x38,0x40,0x48,0x50,0x58,0x60,0x68,0x70,0x78,0x80}, 8, 20, "RC2", false),
des(null, "DES", -1, 64, new int[]{64}, 8/*for 56-bit*/, 32, "DES", false),
// desx is not supported. Not sure, if it can be simulated by des3 somehow
des3(null, "DESede", -1, 192, new int[]{192}, 8, 32, "3DES", false),
// need bouncycastle provider for this one ...
// see http://stackoverflow.com/questions/4436397/3des-des-encryption-using-the-jce-generating-an-acceptable-key
des3_112(null, "DESede", -1, 128, new int[]{128}, 8, 32, "3DES_112", true),
;
public final CipherProvider provider;
public final String jceId;
public final int ecmaId;
public final int defaultKeySize;
public final int allowedKeySize[];
public final int blockSize;
public final int encryptedVerifierHashLength;
public final String xmlId;
public final boolean needsBouncyCastle;
CipherAlgorithm(CipherProvider provider, String jceId, int ecmaId, int defaultKeySize, int allowedKeySize[], int blockSize, int encryptedVerifierHashLength, String xmlId, boolean needsBouncyCastle) {
this.provider = provider;
this.jceId = jceId;
this.ecmaId = ecmaId;
this.defaultKeySize = defaultKeySize;
this.allowedKeySize = allowedKeySize;
this.blockSize = blockSize;
this.encryptedVerifierHashLength = encryptedVerifierHashLength;
this.xmlId = xmlId;
this.needsBouncyCastle = needsBouncyCastle;
}
public static CipherAlgorithm fromEcmaId(int ecmaId) {
for (CipherAlgorithm ca : CipherAlgorithm.values()) {
if (ca.ecmaId == ecmaId) return ca;
}
throw new EncryptedDocumentException("cipher algorithm not found");
}
public static CipherAlgorithm fromXmlId(String xmlId, int keySize) {
for (CipherAlgorithm ca : CipherAlgorithm.values()) {
if (!ca.xmlId.equals(xmlId)) continue;
for (int ks : ca.allowedKeySize) {
if (ks == keySize) return ca;
}
}
throw new EncryptedDocumentException("cipher algorithm not found");
}
}

View File

@ -0,0 +1,39 @@
/* ====================================================================
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==================================================================== */
package org.apache.poi.poifs.crypt;
import org.apache.poi.EncryptedDocumentException;
public enum CipherProvider {
rc4("RC4", 1),
aes("AES", 0x18);
public static CipherProvider fromEcmaId(int ecmaId) {
for (CipherProvider cp : CipherProvider.values()) {
if (cp.ecmaId == ecmaId) return cp;
}
throw new EncryptedDocumentException("cipher provider not found");
}
public final String jceId;
public final int ecmaId;
CipherProvider(String jceId, int ecmaId) {
this.jceId = jceId;
this.ecmaId = ecmaId;
}
}

View File

@ -0,0 +1,258 @@
/* ====================================================================
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==================================================================== */
package org.apache.poi.poifs.crypt;
import java.nio.charset.Charset;
import java.security.DigestException;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.Provider;
import java.security.Security;
import java.util.Arrays;
import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import org.apache.poi.EncryptedDocumentException;
import org.apache.poi.util.LittleEndian;
import org.apache.poi.util.LittleEndianConsts;
/**
* Helper functions used for standard and agile encryption
*/
public class CryptoFunctions {
/**
* 2.3.4.7 ECMA-376 Document Encryption Key Generation (Standard Encryption)
* 2.3.4.11 Encryption Key Generation (Agile Encryption)
*
* The encryption key for ECMA-376 document encryption [ECMA-376] using agile encryption MUST be
* generated by using the following method, which is derived from PKCS #5: Password-Based
* Cryptography Version 2.0 [RFC2898].
*
* Let H() be a hashing algorithm as determined by the PasswordKeyEncryptor.hashAlgorithm
* element, H_n be the hash data of the n-th iteration, and a plus sign (+) represent concatenation. The
* password MUST be provided as an array of Unicode characters. Limitations on the length of the
* password and the characters used by the password are implementation-dependent. The initial
* password hash is generated as follows:
*
* - H_0 = H(salt + password)
*
* The salt used MUST be generated randomly. The salt MUST be stored in the
* PasswordKeyEncryptor.saltValue element contained within the \EncryptionInfo stream (1) as
* specified in section 2.3.4.10. The hash is then iterated by using the following approach:
*
* - H_n = H(iterator + H_n-1)
*
* where iterator is an unsigned 32-bit value that is initially set to 0x00000000 and then incremented
* monotonically on each iteration until PasswordKey.spinCount iterations have been performed.
* The value of iterator on the last iteration MUST be one less than PasswordKey.spinCount.
*
* For POI, H_final will be calculated by {@link generateKey()}
*
* @param password
* @param hashAlgorithm
* @param salt
* @param spinCount
* @return
*/
public static byte[] hashPassword(String password, HashAlgorithm hashAlgorithm, byte salt[], int spinCount) {
// If no password was given, use the default
if (password == null) {
password = Decryptor.DEFAULT_PASSWORD;
}
MessageDigest hashAlg = getMessageDigest(hashAlgorithm);
hashAlg.update(salt);
byte[] hash = hashAlg.digest(getUtf16LeString(password));
byte[] iterator = new byte[LittleEndianConsts.INT_SIZE];
try {
for (int i = 0; i < spinCount; i++) {
LittleEndian.putInt(iterator, 0, i);
hashAlg.reset();
hashAlg.update(iterator);
hashAlg.update(hash);
hashAlg.digest(hash, 0, hash.length); // don't create hash buffer everytime new
}
} catch (DigestException e) {
throw new EncryptedDocumentException("error in password hashing");
}
return hash;
}
/**
* 2.3.4.12 Initialization Vector Generation (Agile Encryption)
*
* Initialization vectors are used in all cases for agile encryption. An initialization vector MUST be
* generated by using the following method, where H() is a hash function that MUST be the same as
* specified in section 2.3.4.11 and a plus sign (+) represents concatenation:
* 1. If a blockKey is provided, let IV be a hash of the KeySalt and the following value:
* blockKey: IV = H(KeySalt + blockKey)
* 2. If a blockKey is not provided, let IV be equal to the following value:
* KeySalt:IV = KeySalt.
* 3. If the number of bytes in the value of IV is less than the the value of the blockSize attribute
* corresponding to the cipherAlgorithm attribute, pad the array of bytes by appending 0x36 until
* the array is blockSize bytes. If the array of bytes is larger than blockSize bytes, truncate the
* array to blockSize bytes.
**/
public static byte[] generateIv(HashAlgorithm hashAlgorithm, byte[] salt, byte[] blockKey, int blockSize) {
byte iv[] = salt;
if (blockKey != null) {
MessageDigest hashAlgo = getMessageDigest(hashAlgorithm);
hashAlgo.update(salt);
iv = hashAlgo.digest(blockKey);
}
return getBlock36(iv, blockSize);
}
/**
* 2.3.4.11 Encryption Key Generation (Agile Encryption)
*
* ... continued ...
*
* The final hash data that is used for an encryption key is then generated by using the following
* method:
*
* - H_final = H(H_n + blockKey)
*
* where blockKey represents an array of bytes used to prevent two different blocks from encrypting
* to the same cipher text.
*
* If the size of the resulting H_final is smaller than that of PasswordKeyEncryptor.keyBits, the key
* MUST be padded by appending bytes with a value of 0x36. If the hash value is larger in size than
* PasswordKeyEncryptor.keyBits, the key is obtained by truncating the hash value.
*
* @param passwordHash
* @param hashAlgorithm
* @param blockKey
* @param keySize
* @return
*/
public static byte[] generateKey(byte[] passwordHash, HashAlgorithm hashAlgorithm, byte[] blockKey, int keySize) {
MessageDigest hashAlgo = getMessageDigest(hashAlgorithm);
hashAlgo.update(passwordHash);
byte[] key = hashAlgo.digest(blockKey);
return getBlock36(key, keySize);
}
public static Cipher getCipher(SecretKey key, CipherAlgorithm cipherAlgorithm, ChainingMode chain, byte[] vec, int cipherMode) {
return getCipher(key, cipherAlgorithm, chain, vec, cipherMode, null);
}
/**
*
*
* @param key
* @param chain
* @param vec
* @param cipherMode Cipher.DECRYPT_MODE or Cipher.ENCRYPT_MODE
* @return
* @throws GeneralSecurityException
*/
public static Cipher getCipher(SecretKey key, CipherAlgorithm cipherAlgorithm, ChainingMode chain, byte[] vec, int cipherMode, String padding) {
int keySizeInBytes = key.getEncoded().length;
if (padding == null) padding = "NoPadding";
try {
// Ensure the JCE policies files allow for this sized key
if (Cipher.getMaxAllowedKeyLength(key.getAlgorithm()) < keySizeInBytes*8) {
throw new EncryptedDocumentException("Export Restrictions in place - please install JCE Unlimited Strength Jurisdiction Policy files");
}
Cipher cipher;
if (cipherAlgorithm.needsBouncyCastle) {
registerBouncyCastle();
cipher = Cipher.getInstance(key.getAlgorithm() + "/" + chain.jceId + "/" + padding, "BC");
} else {
cipher = Cipher.getInstance(key.getAlgorithm() + "/" + chain.jceId + "/" + padding);
}
if (vec == null) {
cipher.init(cipherMode, key);
} else {
IvParameterSpec iv = new IvParameterSpec(vec);
cipher.init(cipherMode, key, iv);
}
return cipher;
} catch (GeneralSecurityException e) {
throw new EncryptedDocumentException(e);
}
}
public static byte[] getBlock36(byte[] hash, int size) {
return getBlockX(hash, size, (byte)0x36);
}
public static byte[] getBlock0(byte[] hash, int size) {
return getBlockX(hash, size, (byte)0);
}
private static byte[] getBlockX(byte[] hash, int size, byte fill) {
if (hash.length == size) return hash;
byte[] result = new byte[size];
Arrays.fill(result, fill);
System.arraycopy(hash, 0, result, 0, Math.min(result.length, hash.length));
return result;
}
public static byte[] getUtf16LeString(String str) {
Charset cs = Charset.forName("UTF-16LE");
return str.getBytes(cs);
}
public static MessageDigest getMessageDigest(HashAlgorithm hashAlgorithm) {
try {
if (hashAlgorithm.needsBouncyCastle) {
registerBouncyCastle();
return MessageDigest.getInstance(hashAlgorithm.jceId, "BC");
} else {
return MessageDigest.getInstance(hashAlgorithm.jceId);
}
} catch (GeneralSecurityException e) {
throw new EncryptedDocumentException("hash algo not supported", e);
}
}
public static Mac getMac(HashAlgorithm hashAlgorithm) {
try {
if (hashAlgorithm.needsBouncyCastle) {
registerBouncyCastle();
return Mac.getInstance(hashAlgorithm.jceHmacId, "BC");
} else {
return Mac.getInstance(hashAlgorithm.jceHmacId);
}
} catch (GeneralSecurityException e) {
throw new EncryptedDocumentException("hmac algo not supported", e);
}
}
@SuppressWarnings("unchecked")
private static void registerBouncyCastle() {
if (Security.getProvider("BC") != null) return;
try {
Class<Provider> clazz = (Class<Provider>)Class.forName("org.bouncycastle.jce.provider.BouncyCastleProvider");
Security.addProvider(clazz.newInstance());
} catch (Exception e) {
throw new EncryptedDocumentException("Only the BouncyCastle provider supports your encryption settings - please add it to the classpath.");
}
}
}

View File

@ -0,0 +1,365 @@
/* ====================================================================
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==================================================================== */
package org.apache.poi.poifs.crypt;
import java.io.IOException;
import java.nio.charset.Charset;
import org.apache.poi.EncryptedDocumentException;
import org.apache.poi.poifs.crypt.standard.EncryptionRecord;
import org.apache.poi.poifs.filesystem.DirectoryEntry;
import org.apache.poi.poifs.filesystem.DocumentEntry;
import org.apache.poi.poifs.filesystem.POIFSWriterEvent;
import org.apache.poi.poifs.filesystem.POIFSWriterListener;
import org.apache.poi.util.LittleEndianByteArrayOutputStream;
import org.apache.poi.util.LittleEndianConsts;
import org.apache.poi.util.LittleEndianInput;
import org.apache.poi.util.LittleEndianOutput;
public class DataSpaceMapUtils {
public static void addDefaultDataSpace(DirectoryEntry dir) throws IOException {
DataSpaceMapEntry dsme = new DataSpaceMapEntry(
new int[]{ 0 }
, new String[]{ "EncryptedPackage" }
, "StrongEncryptionDataSpace"
);
DataSpaceMap dsm = new DataSpaceMap(new DataSpaceMapEntry[]{dsme});
createEncryptionEntry(dir, "\u0006DataSpaces/DataSpaceMap", dsm);
DataSpaceDefinition dsd = new DataSpaceDefinition(new String[]{ "StrongEncryptionTransform" });
createEncryptionEntry(dir, "\u0006DataSpaces/DataSpaceInfo/StrongEncryptionDataSpace", dsd);
TransformInfoHeader tih = new TransformInfoHeader(
1
, "{FF9A3F03-56EF-4613-BDD5-5A41C1D07246}"
, "Microsoft.Container.EncryptionTransform"
, 1, 0, 1, 0, 1, 0
);
IRMDSTransformInfo irm = new IRMDSTransformInfo(tih, 0, null);
createEncryptionEntry(dir, "\u0006DataSpaces/TransformInfo/StrongEncryptionTransform/\u0006Primary", irm);
DataSpaceVersionInfo dsvi = new DataSpaceVersionInfo("Microsoft.Container.DataSpaces", 1, 0, 1, 0, 1, 0);
createEncryptionEntry(dir, "\u0006DataSpaces/Version", dsvi);
}
public static DocumentEntry createEncryptionEntry(DirectoryEntry dir, String path, EncryptionRecord out) throws IOException {
String parts[] = path.split("/");
for (int i=0; i<parts.length-1; i++) {
dir = dir.hasEntry(parts[i])
? (DirectoryEntry)dir.getEntry(parts[i])
: dir.createDirectory(parts[i]);
}
final byte buf[] = new byte[5000];
LittleEndianByteArrayOutputStream bos = new LittleEndianByteArrayOutputStream(buf, 0);
out.write(bos);
return dir.createDocument(parts[parts.length-1], bos.getWriteIndex(), new POIFSWriterListener(){
public void processPOIFSWriterEvent(POIFSWriterEvent event) {
try {
event.getStream().write(buf, 0, event.getLimit());
} catch (IOException e) {
throw new EncryptedDocumentException(e);
}
}
});
}
public static class DataSpaceMap implements EncryptionRecord {
DataSpaceMapEntry entries[];
public DataSpaceMap(DataSpaceMapEntry entries[]) {
this.entries = entries;
}
public DataSpaceMap(LittleEndianInput is) {
@SuppressWarnings("unused")
int length = is.readInt();
int entryCount = is.readInt();
entries = new DataSpaceMapEntry[entryCount];
for (int i=0; i<entryCount; i++) {
entries[i] = new DataSpaceMapEntry(is);
}
}
public void write(LittleEndianByteArrayOutputStream os) {
os.writeInt(8);
os.writeInt(entries.length);
for (DataSpaceMapEntry dsme : entries) {
dsme.write(os);
}
}
}
public static class DataSpaceMapEntry implements EncryptionRecord {
int referenceComponentType[];
String referenceComponent[];
String dataSpaceName;
public DataSpaceMapEntry(int referenceComponentType[], String referenceComponent[], String dataSpaceName) {
this.referenceComponentType = referenceComponentType;
this.referenceComponent = referenceComponent;
this.dataSpaceName = dataSpaceName;
}
public DataSpaceMapEntry(LittleEndianInput is) {
@SuppressWarnings("unused")
int length = is.readInt();
int referenceComponentCount = is.readInt();
referenceComponentType = new int[referenceComponentCount];
referenceComponent = new String[referenceComponentCount];
for (int i=0; i<referenceComponentCount; i++) {
referenceComponentType[i] = is.readInt();
referenceComponent[i] = readUnicodeLPP4(is);
}
dataSpaceName = readUnicodeLPP4(is);
}
public void write(LittleEndianByteArrayOutputStream os) {
int start = os.getWriteIndex();
LittleEndianOutput sizeOut = os.createDelayedOutput(LittleEndianConsts.INT_SIZE);
os.writeInt(referenceComponent.length);
for (int i=0; i<referenceComponent.length; i++) {
os.writeInt(referenceComponentType[i]);
writeUnicodeLPP4(os, referenceComponent[i]);
}
writeUnicodeLPP4(os, dataSpaceName);
sizeOut.writeInt(os.getWriteIndex()-start);
}
}
public static class DataSpaceDefinition implements EncryptionRecord {
String transformer[];
public DataSpaceDefinition(String transformer[]) {
this.transformer = transformer;
}
public DataSpaceDefinition(LittleEndianInput is) {
@SuppressWarnings("unused")
int headerLength = is.readInt();
int transformReferenceCount = is.readInt();
transformer = new String[transformReferenceCount];
for (int i=0; i<transformReferenceCount; i++) {
transformer[i] = readUnicodeLPP4(is);
}
}
public void write(LittleEndianByteArrayOutputStream bos) {
bos.writeInt(8);
bos.writeInt(transformer.length);
for (String str : transformer) {
writeUnicodeLPP4(bos, str);
}
}
}
public static class IRMDSTransformInfo implements EncryptionRecord {
TransformInfoHeader transformInfoHeader;
int extensibilityHeader;
String xrMLLicense;
public IRMDSTransformInfo(TransformInfoHeader transformInfoHeader, int extensibilityHeader, String xrMLLicense) {
this.transformInfoHeader = transformInfoHeader;
this.extensibilityHeader = extensibilityHeader;
this.xrMLLicense = xrMLLicense;
}
public IRMDSTransformInfo(LittleEndianInput is) {
transformInfoHeader = new TransformInfoHeader(is);
extensibilityHeader = is.readInt();
xrMLLicense = readUtf8LPP4(is);
// finish with 0x04 (int) ???
}
public void write(LittleEndianByteArrayOutputStream bos) {
transformInfoHeader.write(bos);
bos.writeInt(extensibilityHeader);
writeUtf8LPP4(bos, xrMLLicense);
bos.writeInt(4); // where does this 4 come from???
}
}
public static class TransformInfoHeader implements EncryptionRecord {
int transformType;
String transformerId;
String transformerName;
int readerVersionMajor = 1, readerVersionMinor = 0;
int updaterVersionMajor = 1, updaterVersionMinor = 0;
int writerVersionMajor = 1, writerVersionMinor = 0;
public TransformInfoHeader(
int transformType,
String transformerId,
String transformerName,
int readerVersionMajor, int readerVersionMinor,
int updaterVersionMajor, int updaterVersionMinor,
int writerVersionMajor, int writerVersionMinor
){
this.transformType = transformType;
this.transformerId = transformerId;
this.transformerName = transformerName;
this.readerVersionMajor = readerVersionMajor;
this.readerVersionMinor = readerVersionMinor;
this.updaterVersionMajor = updaterVersionMajor;
this.updaterVersionMinor = updaterVersionMinor;
this.writerVersionMajor = writerVersionMajor;
this.writerVersionMinor = writerVersionMinor;
}
public TransformInfoHeader(LittleEndianInput is) {
@SuppressWarnings("unused")
int length = is.readInt();
transformType = is.readInt();
transformerId = readUnicodeLPP4(is);
transformerName = readUnicodeLPP4(is);
readerVersionMajor = is.readShort();
readerVersionMinor = is.readShort();
updaterVersionMajor = is.readShort();
updaterVersionMinor = is.readShort();
writerVersionMajor = is.readShort();
writerVersionMinor = is.readShort();
}
public void write(LittleEndianByteArrayOutputStream bos) {
int start = bos.getWriteIndex();
LittleEndianOutput sizeOut = bos.createDelayedOutput(LittleEndianConsts.INT_SIZE);
bos.writeInt(transformType);
writeUnicodeLPP4(bos, transformerId);
sizeOut.writeInt(bos.getWriteIndex()-start);
writeUnicodeLPP4(bos, transformerName);
bos.writeShort(readerVersionMajor);
bos.writeShort(readerVersionMinor);
bos.writeShort(updaterVersionMajor);
bos.writeShort(updaterVersionMinor);
bos.writeShort(writerVersionMajor);
bos.writeShort(writerVersionMinor);
}
}
public static class DataSpaceVersionInfo implements EncryptionRecord {
String featureIdentifier;
int readerVersionMajor = 1, readerVersionMinor = 0;
int updaterVersionMajor = 1, updaterVersionMinor = 0;
int writerVersionMajor = 1, writerVersionMinor = 0;
public DataSpaceVersionInfo(LittleEndianInput is) {
featureIdentifier = readUnicodeLPP4(is);
readerVersionMajor = is.readShort();
readerVersionMinor = is.readShort();
updaterVersionMajor = is.readShort();
updaterVersionMinor = is.readShort();
writerVersionMajor = is.readShort();
writerVersionMinor = is.readShort();
}
public DataSpaceVersionInfo(
String featureIdentifier,
int readerVersionMajor, int readerVersionMinor,
int updaterVersionMajor, int updaterVersionMinor,
int writerVersionMajor, int writerVersionMinor
){
this.featureIdentifier = featureIdentifier;
this.readerVersionMajor = readerVersionMajor;
this.readerVersionMinor = readerVersionMinor;
this.updaterVersionMajor = updaterVersionMajor;
this.updaterVersionMinor = updaterVersionMinor;
this.writerVersionMajor = writerVersionMajor;
this.writerVersionMinor = writerVersionMinor;
}
public void write(LittleEndianByteArrayOutputStream bos) {
writeUnicodeLPP4(bos, featureIdentifier);
bos.writeShort(readerVersionMajor);
bos.writeShort(readerVersionMinor);
bos.writeShort(updaterVersionMajor);
bos.writeShort(updaterVersionMinor);
bos.writeShort(writerVersionMajor);
bos.writeShort(writerVersionMinor);
}
}
public static String readUnicodeLPP4(LittleEndianInput is) {
Charset cs = Charset.forName("UTF-16LE");
int length = is.readInt();
byte data[] = new byte[length];
is.readFully(data);
if (length%4==2) {
// Padding (variable): A set of bytes that MUST be of the correct size such that the size of the
// UNICODE-LP-P4 structure is a multiple of 4 bytes. If Padding is present, it MUST be exactly
// 2 bytes long, and each byte MUST be 0x00.
is.readShort();
}
return new String(data, 0, data.length, cs);
}
public static void writeUnicodeLPP4(LittleEndianOutput os, String str) {
Charset cs = Charset.forName("UTF-16LE");
byte buf[] = str.getBytes(cs);
os.writeInt(buf.length);
os.write(buf);
if (buf.length%4==2) {
os.writeShort(0);
}
}
public static String readUtf8LPP4(LittleEndianInput is) {
int length = is.readInt();
if (length == 0 || length == 4) {
@SuppressWarnings("unused")
int skip = is.readInt(); // ignore
return length == 0 ? null : "";
}
byte data[] = new byte[length];
is.readFully(data);
// Padding (variable): A set of bytes that MUST be of correct size such that the size of the UTF-8-LP-P4
// structure is a multiple of 4 bytes. If Padding is present, each byte MUST be 0x00. If
// the length is exactly 0x00000000, this specifies a null string, and the entire structure uses
// exactly 4 bytes. If the length is exactly 0x00000004, this specifies an empty string, and the
// entire structure also uses exactly 4 bytes
int scratchedBytes = length%4;
if (scratchedBytes > 0) {
for (int i=0; i<(4-scratchedBytes); i++) {
is.readByte();
}
}
Charset cs = Charset.forName("UTF-8");
return new String(data, 0, data.length, cs);
}
public static void writeUtf8LPP4(LittleEndianOutput os, String str) {
if (str == null || "".equals(str)) {
os.writeInt(str == null ? 0 : 4);
os.writeInt(0);
} else {
Charset cs = Charset.forName("UTF-8");
byte buf[] = str.getBytes(cs);
os.writeInt(buf.length);
os.write(buf);
int scratchBytes = buf.length%4;
if (scratchBytes > 0) {
for (int i=0; i<(4-scratchBytes); i++) {
os.writeByte(0);
}
}
}
}
}

View File

@ -18,21 +18,26 @@ package org.apache.poi.poifs.crypt;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.security.DigestException;
import java.security.MessageDigest;
import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.SecretKey;
import org.apache.poi.EncryptedDocumentException;
import org.apache.poi.poifs.filesystem.DirectoryNode;
import org.apache.poi.poifs.filesystem.NPOIFSFileSystem;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
import org.apache.poi.poifs.filesystem.DirectoryNode;
import org.apache.poi.EncryptedDocumentException;
import org.apache.poi.util.LittleEndian;
import org.apache.poi.util.LittleEndianConsts;
public abstract class Decryptor {
public static final String DEFAULT_PASSWORD="VelvetSweatshop";
protected final EncryptionInfo info;
private SecretKey secretKey;
private byte[] verifier, integrityHmacKey, integrityHmacValue;
protected Decryptor(EncryptionInfo info) {
this.info = info;
}
/**
* Return a stream with decrypted data.
* <p>
@ -68,15 +73,11 @@ public abstract class Decryptor {
public abstract long getLength();
public static Decryptor getInstance(EncryptionInfo info) {
int major = info.getVersionMajor();
int minor = info.getVersionMinor();
if (major == 4 && minor == 4)
return new AgileDecryptor(info);
else if (minor == 2 && (major == 3 || major == 4))
return new EcmaDecryptor(info);
else
Decryptor d = info.getDecryptor();
if (d == null) {
throw new EncryptedDocumentException("Unsupported version");
}
return d;
}
public InputStream getDataStream(NPOIFSFileSystem fs) throws IOException, GeneralSecurityException {
@ -86,40 +87,37 @@ public abstract class Decryptor {
public InputStream getDataStream(POIFSFileSystem fs) throws IOException, GeneralSecurityException {
return getDataStream(fs.getRoot());
}
// for tests
public byte[] getVerifier() {
return verifier;
}
protected byte[] hashPassword(EncryptionInfo info,
String password) throws NoSuchAlgorithmException {
// If no password was given, use the default
if (password == null) {
password = DEFAULT_PASSWORD;
}
byte[] pass;
try {
pass = password.getBytes("UTF-16LE");
} catch (UnsupportedEncodingException e) {
throw new EncryptedDocumentException("UTF16 not supported");
}
public SecretKey getSecretKey() {
return secretKey;
}
public byte[] getIntegrityHmacKey() {
return integrityHmacKey;
}
byte[] salt = info.getVerifier().getSalt();
MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
sha1.update(salt);
byte[] hash = sha1.digest(pass);
byte[] iterator = new byte[LittleEndianConsts.INT_SIZE];
try {
for (int i = 0; i < info.getVerifier().getSpinCount(); i++) {
LittleEndian.putInt(iterator, 0, i);
sha1.reset();
sha1.update(iterator);
sha1.update(hash);
sha1.digest(hash, 0, hash.length); // don't create hash buffer everytime new
}
} catch (DigestException e) {
throw new EncryptedDocumentException("error in password hashing");
}
return hash;
public byte[] getIntegrityHmacValue() {
return integrityHmacValue;
}
protected void setSecretKey(SecretKey secretKey) {
this.secretKey = secretKey;
}
protected void setVerifier(byte[] verifier) {
this.verifier = verifier;
}
protected void setIntegrityHmacKey(byte[] integrityHmacKey) {
this.integrityHmacKey = integrityHmacKey;
}
protected void setIntegrityHmacValue(byte[] integrityHmacValue) {
this.integrityHmacValue = integrityHmacValue;
}
}

View File

@ -1,134 +0,0 @@
/* ====================================================================
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==================================================================== */
package org.apache.poi.poifs.crypt;
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.apache.poi.poifs.filesystem.DirectoryNode;
import org.apache.poi.poifs.filesystem.DocumentInputStream;
import org.apache.poi.util.LittleEndian;
/**
*/
public class EcmaDecryptor extends Decryptor {
private final EncryptionInfo info;
private byte[] passwordHash;
private long _length = -1;
public EcmaDecryptor(EncryptionInfo info) {
this.info = info;
}
private byte[] generateKey(int block) throws NoSuchAlgorithmException {
MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
sha1.update(passwordHash);
byte[] blockValue = new byte[4];
LittleEndian.putInt(blockValue, 0, block);
byte[] finalHash = sha1.digest(blockValue);
int requiredKeyLength = info.getHeader().getKeySize()/8;
byte[] buff = new byte[64];
Arrays.fill(buff, (byte) 0x36);
for (int i=0; i<finalHash.length; i++) {
buff[i] = (byte) (buff[i] ^ finalHash[i]);
}
sha1.reset();
byte[] x1 = sha1.digest(buff);
Arrays.fill(buff, (byte) 0x5c);
for (int i=0; i<finalHash.length; i++) {
buff[i] = (byte) (buff[i] ^ finalHash[i]);
}
sha1.reset();
byte[] x2 = sha1.digest(buff);
byte[] x3 = new byte[x1.length + x2.length];
System.arraycopy(x1, 0, x3, 0, x1.length);
System.arraycopy(x2, 0, x3, x1.length, x2.length);
return truncateOrPad(x3, requiredKeyLength);
}
public boolean verifyPassword(String password) throws GeneralSecurityException {
passwordHash = hashPassword(info, password);
Cipher cipher = getCipher();
byte[] verifier = cipher.doFinal(info.getVerifier().getVerifier());
MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
byte[] calcVerifierHash = sha1.digest(verifier);
byte[] verifierHash = truncateOrPad(cipher.doFinal(info.getVerifier().getVerifierHash()), calcVerifierHash.length);
return Arrays.equals(calcVerifierHash, verifierHash);
}
/**
* Returns a byte array of the requested length,
* truncated or zero padded as needed.
* Behaves like Arrays.copyOf in Java 1.6
*/
private byte[] truncateOrPad(byte[] source, int length) {
byte[] result = new byte[length];
System.arraycopy(source, 0, result, 0, Math.min(length, source.length));
if(length > source.length) {
for(int i=source.length; i<length; i++) {
result[i] = 0;
}
}
return result;
}
private Cipher getCipher() throws GeneralSecurityException {
byte[] key = generateKey(0);
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
SecretKey skey = new SecretKeySpec(key, "AES");
cipher.init(Cipher.DECRYPT_MODE, skey);
return cipher;
}
public InputStream getDataStream(DirectoryNode dir) throws IOException, GeneralSecurityException {
DocumentInputStream dis = dir.createDocumentInputStream("EncryptedPackage");
_length = dis.readLong();
return new CipherInputStream(dis, getCipher());
}
public long getLength(){
if(_length == -1) throw new IllegalStateException("EcmaDecryptor.getDataStream() was not called");
return _length;
}
}

View File

@ -16,197 +16,148 @@
==================================================================== */
package org.apache.poi.poifs.crypt;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import javax.xml.parsers.DocumentBuilderFactory;
import org.apache.commons.codec.binary.Base64;
import org.apache.poi.EncryptedDocumentException;
import org.apache.poi.poifs.filesystem.DocumentInputStream;
import org.apache.poi.util.LittleEndianConsts;
import org.w3c.dom.NamedNodeMap;
/**
* Reads and processes OOXML Encryption Headers
* The constants are largely based on ZIP constants.
*/
public class EncryptionHeader {
public static final int ALGORITHM_RC4 = 0x6801;
public static final int ALGORITHM_AES_128 = 0x660E;
public static final int ALGORITHM_AES_192 = 0x660F;
public static final int ALGORITHM_AES_256 = 0x6610;
public abstract class EncryptionHeader {
public static final int ALGORITHM_RC4 = CipherAlgorithm.rc4.ecmaId;
public static final int ALGORITHM_AES_128 = CipherAlgorithm.aes128.ecmaId;
public static final int ALGORITHM_AES_192 = CipherAlgorithm.aes192.ecmaId;
public static final int ALGORITHM_AES_256 = CipherAlgorithm.aes256.ecmaId;
public static final int HASH_NONE = HashAlgorithm.none.ecmaId;
public static final int HASH_SHA1 = HashAlgorithm.sha1.ecmaId;
public static final int HASH_SHA256 = HashAlgorithm.sha256.ecmaId;
public static final int HASH_SHA384 = HashAlgorithm.sha384.ecmaId;
public static final int HASH_SHA512 = HashAlgorithm.sha512.ecmaId;
public static final int HASH_NONE = 0x0000;
public static final int HASH_SHA1 = 0x8004;
public static final int HASH_SHA256 = 0x800C;
public static final int HASH_SHA384 = 0x800D;
public static final int HASH_SHA512 = 0x800E;
public static final int PROVIDER_RC4 = CipherProvider.rc4.ecmaId;
public static final int PROVIDER_AES = CipherProvider.aes.ecmaId;
public static final int PROVIDER_RC4 = 1;
public static final int PROVIDER_AES = 0x18;
public static final int MODE_ECB = 1;
public static final int MODE_CBC = 2;
public static final int MODE_CFB = 3;
private final int flags;
private final int sizeExtra;
private final int algorithm;
private final int hashAlgorithm;
private final int keySize;
private final int blockSize;
private final int providerType;
private final int cipherMode;
private final byte[] keySalt;
private final String cspName;
public EncryptionHeader(DocumentInputStream is) throws IOException {
flags = is.readInt();
sizeExtra = is.readInt();
algorithm = is.readInt();
hashAlgorithm = is.readInt();
keySize = is.readInt();
blockSize = keySize;
providerType = is.readInt();
is.readLong(); // skip reserved
// CSPName may not always be specified
// In some cases, the sale value of the EncryptionVerifier has the details
is.mark(LittleEndianConsts.INT_SIZE+1);
int checkForSalt = is.readInt();
is.reset();
if (checkForSalt == 16) {
cspName = "";
} else {
StringBuilder builder = new StringBuilder();
while (true) {
char c = (char) is.readShort();
if (c == 0) break;
builder.append(c);
}
cspName = builder.toString();
}
cipherMode = MODE_ECB;
keySalt = null;
}
public EncryptionHeader(String descriptor) throws IOException {
NamedNodeMap keyData;
try {
ByteArrayInputStream is;
is = new ByteArrayInputStream(descriptor.getBytes());
keyData = DocumentBuilderFactory.newInstance()
.newDocumentBuilder().parse(is)
.getElementsByTagName("keyData").item(0).getAttributes();
} catch (Exception e) {
throw new EncryptedDocumentException("Unable to parse keyData");
}
keySize = Integer.parseInt(keyData.getNamedItem("keyBits")
.getNodeValue());
flags = 0;
sizeExtra = 0;
cspName = null;
blockSize = Integer.parseInt(keyData.getNamedItem("blockSize").
getNodeValue());
String cipher = keyData.getNamedItem("cipherAlgorithm").getNodeValue();
if ("AES".equals(cipher)) {
providerType = PROVIDER_AES;
switch (keySize) {
case 128:
algorithm = ALGORITHM_AES_128; break;
case 192:
algorithm = ALGORITHM_AES_192; break;
case 256:
algorithm = ALGORITHM_AES_256; break;
default:
throw new EncryptedDocumentException("Unsupported key length " + keySize);
}
} else {
throw new EncryptedDocumentException("Unsupported cipher " + cipher);
}
String chaining = keyData.getNamedItem("cipherChaining").getNodeValue();
if ("ChainingModeCBC".equals(chaining))
cipherMode = MODE_CBC;
else if ("ChainingModeCFB".equals(chaining))
cipherMode = MODE_CFB;
else
throw new EncryptedDocumentException("Unsupported chaining mode " + chaining);
String hashAlg = keyData.getNamedItem("hashAlgorithm").getNodeValue();
int hashSize = Integer.parseInt(
keyData.getNamedItem("hashSize").getNodeValue());
if ("SHA1".equals(hashAlg) && hashSize == 20) {
hashAlgorithm = HASH_SHA1;
}
else if ("SHA256".equals(hashAlg) && hashSize == 32) {
hashAlgorithm = HASH_SHA256;
}
else if ("SHA384".equals(hashAlg) && hashSize == 64) {
hashAlgorithm = HASH_SHA384;
}
else if ("SHA512".equals(hashAlg) && hashSize == 64) {
hashAlgorithm = HASH_SHA512;
}
else {
throw new EncryptedDocumentException("Unsupported hash algorithm: " +
hashAlg + " @ " + hashSize + " bytes");
}
String salt = keyData.getNamedItem("saltValue").getNodeValue();
int saltLength = Integer.parseInt(keyData.getNamedItem("saltSize")
.getNodeValue());
keySalt = Base64.decodeBase64(salt.getBytes());
if (keySalt.length != saltLength)
throw new EncryptedDocumentException("Invalid salt length");
}
public static final int MODE_ECB = ChainingMode.ecb.ecmaId;
public static final int MODE_CBC = ChainingMode.cbc.ecmaId;
public static final int MODE_CFB = ChainingMode.cfb.ecmaId;
private int flags;
private int sizeExtra;
private CipherAlgorithm cipherAlgorithm;
private HashAlgorithm hashAlgorithm;
private int keyBits;
private int blockSize;
private CipherProvider providerType;
private ChainingMode chainingMode;
private byte[] keySalt;
private String cspName;
protected EncryptionHeader() {}
/**
* @deprecated use getChainingMode().ecmaId
*/
public int getCipherMode() {
return cipherMode;
return chainingMode.ecmaId;
}
public ChainingMode getChainingMode() {
return chainingMode;
}
protected void setChainingMode(ChainingMode chainingMode) {
this.chainingMode = chainingMode;
}
public int getFlags() {
return flags;
}
protected void setFlags(int flags) {
this.flags = flags;
}
public int getSizeExtra() {
return sizeExtra;
}
public int getAlgorithm() {
return algorithm;
protected void setSizeExtra(int sizeExtra) {
this.sizeExtra = sizeExtra;
}
/**
* @deprecated use getCipherAlgorithm()
*/
public int getAlgorithm() {
return cipherAlgorithm.ecmaId;
}
public CipherAlgorithm getCipherAlgorithm() {
return cipherAlgorithm;
}
protected void setCipherAlgorithm(CipherAlgorithm cipherAlgorithm) {
this.cipherAlgorithm = cipherAlgorithm;
}
/**
* @deprecated use getHashAlgorithmEx()
*/
public int getHashAlgorithm() {
return hashAlgorithm.ecmaId;
}
public HashAlgorithm getHashAlgorithmEx() {
return hashAlgorithm;
}
protected void setHashAlgorithm(HashAlgorithm hashAlgorithm) {
this.hashAlgorithm = hashAlgorithm;
}
public int getKeySize() {
return keySize;
return keyBits;
}
protected void setKeySize(int keyBits) {
this.keyBits = keyBits;
}
public int getBlockSize() {
return blockSize;
}
protected void setBlockSize(int blockSize) {
this.blockSize = blockSize;
}
public byte[] getKeySalt() {
return keySalt;
}
public int getProviderType() {
return providerType;
protected void setKeySalt(byte salt[]) {
this.keySalt = salt;
}
/**
* @deprecated use getCipherProvider()
*/
public int getProviderType() {
return providerType.ecmaId;
}
public CipherProvider getCipherProvider() {
return providerType;
}
protected void setCipherProvider(CipherProvider providerType) {
this.providerType = providerType;
}
public String getCspName() {
return cspName;
}
protected void setCspName(String cspName) {
this.cspName = cspName;
}
}

View File

@ -16,56 +16,141 @@
==================================================================== */
package org.apache.poi.poifs.crypt;
import static org.apache.poi.poifs.crypt.EncryptionMode.agile;
import static org.apache.poi.poifs.crypt.EncryptionMode.standard;
import java.io.IOException;
import org.apache.poi.EncryptedDocumentException;
import org.apache.poi.poifs.filesystem.DirectoryNode;
import org.apache.poi.poifs.filesystem.DocumentInputStream;
import org.apache.poi.poifs.filesystem.NPOIFSFileSystem;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
import java.io.IOException;
/**
*/
public class EncryptionInfo {
private final int versionMajor;
private final int versionMinor;
private final int encryptionFlags;
private final EncryptionHeader header;
private final EncryptionVerifier verifier;
private final Decryptor decryptor;
private final Encryptor encryptor;
public EncryptionInfo(POIFSFileSystem fs) throws IOException {
this(fs.getRoot());
}
public EncryptionInfo(NPOIFSFileSystem fs) throws IOException {
this(fs.getRoot());
}
public EncryptionInfo(DirectoryNode dir) throws IOException {
DocumentInputStream dis = dir.createDocumentInputStream("EncryptionInfo");
versionMajor = dis.readShort();
versionMinor = dis.readShort();
encryptionFlags = dis.readInt();
if (versionMajor == 4 && versionMinor == 4 && encryptionFlags == 0x40) {
StringBuilder builder = new StringBuilder();
byte[] xmlDescriptor = new byte[dis.available()];
dis.read(xmlDescriptor);
for (byte b : xmlDescriptor)
builder.append((char)b);
String descriptor = builder.toString();
header = new EncryptionHeader(descriptor);
verifier = new EncryptionVerifier(descriptor);
EncryptionMode encryptionMode;
if (versionMajor == agile.versionMajor
&& versionMinor == agile.versionMinor
&& encryptionFlags == agile.encryptionFlags) {
encryptionMode = agile;
} else {
int hSize = dis.readInt();
header = new EncryptionHeader(dis);
if (header.getAlgorithm()==EncryptionHeader.ALGORITHM_RC4) {
verifier = new EncryptionVerifier(dis, 20);
} else {
verifier = new EncryptionVerifier(dis, 32);
}
encryptionMode = standard;
}
EncryptionInfoBuilder eib;
try {
eib = getBuilder(encryptionMode);
} catch (ReflectiveOperationException e) {
throw new IOException(e);
}
eib.initialize(this, dis);
header = eib.getHeader();
verifier = eib.getVerifier();
decryptor = eib.getDecryptor();
encryptor = eib.getEncryptor();
}
public EncryptionInfo(POIFSFileSystem fs, EncryptionMode encryptionMode) throws IOException {
this(fs.getRoot(), encryptionMode);
}
public EncryptionInfo(NPOIFSFileSystem fs, EncryptionMode encryptionMode) throws IOException {
this(fs.getRoot(), encryptionMode);
}
public EncryptionInfo(
DirectoryNode dir
, EncryptionMode encryptionMode
) throws EncryptedDocumentException {
this(dir, encryptionMode, null, null, -1, -1, null);
}
public EncryptionInfo(
POIFSFileSystem fs
, EncryptionMode encryptionMode
, CipherAlgorithm cipherAlgorithm
, HashAlgorithm hashAlgorithm
, int keyBits
, int blockSize
, ChainingMode chainingMode
) throws EncryptedDocumentException {
this(fs.getRoot(), encryptionMode, cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode);
}
public EncryptionInfo(
NPOIFSFileSystem fs
, EncryptionMode encryptionMode
, CipherAlgorithm cipherAlgorithm
, HashAlgorithm hashAlgorithm
, int keyBits
, int blockSize
, ChainingMode chainingMode
) throws EncryptedDocumentException {
this(fs.getRoot(), encryptionMode, cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode);
}
public EncryptionInfo(
DirectoryNode dir
, EncryptionMode encryptionMode
, CipherAlgorithm cipherAlgorithm
, HashAlgorithm hashAlgorithm
, int keyBits
, int blockSize
, ChainingMode chainingMode
) throws EncryptedDocumentException {
versionMajor = encryptionMode.versionMajor;
versionMinor = encryptionMode.versionMinor;
encryptionFlags = encryptionMode.encryptionFlags;
EncryptionInfoBuilder eib;
try {
eib = getBuilder(encryptionMode);
} catch (ReflectiveOperationException e) {
throw new EncryptedDocumentException(e);
}
eib.initialize(this, cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode);
header = eib.getHeader();
verifier = eib.getVerifier();
decryptor = eib.getDecryptor();
encryptor = eib.getEncryptor();
}
protected static EncryptionInfoBuilder getBuilder(EncryptionMode encryptionMode)
throws ReflectiveOperationException {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
EncryptionInfoBuilder eib;
eib = (EncryptionInfoBuilder)cl.loadClass(encryptionMode.builder).newInstance();
return eib;
}
public int getVersionMajor() {
return versionMajor;
}
@ -85,4 +170,12 @@ public class EncryptionInfo {
public EncryptionVerifier getVerifier() {
return verifier;
}
public Decryptor getDecryptor() {
return decryptor;
}
public Encryptor getEncryptor() {
return encryptor;
}
}

View File

@ -0,0 +1,30 @@
/* ====================================================================
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==================================================================== */
package org.apache.poi.poifs.crypt;
import java.io.IOException;
import org.apache.poi.poifs.filesystem.DocumentInputStream;
public interface EncryptionInfoBuilder {
void initialize(EncryptionInfo ei, DocumentInputStream dis) throws IOException;
void initialize(EncryptionInfo ei, CipherAlgorithm cipherAlgorithm, HashAlgorithm hashAlgorithm, int keyBits, int blockSize, ChainingMode chainingMode);
EncryptionHeader getHeader();
EncryptionVerifier getVerifier();
Decryptor getDecryptor();
Encryptor getEncryptor();
}

View File

@ -0,0 +1,35 @@
/* ====================================================================
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==================================================================== */
package org.apache.poi.poifs.crypt;
public enum EncryptionMode {
standard("org.apache.poi.poifs.crypt.standard.StandardEncryptionInfoBuilder", 4, 2, 0x24)
, agile("org.apache.poi.poifs.crypt.agile.AgileEncryptionInfoBuilder", 4, 4, 0x40);
public final String builder;
public final int versionMajor;
public final int versionMinor;
public final int encryptionFlags;
EncryptionMode(String builder, int versionMajor, int versionMinor, int encryptionFlags) {
this.builder = builder;
this.versionMajor = versionMajor;
this.versionMinor = versionMinor;
this.encryptionFlags = encryptionFlags;
}
}

View File

@ -16,155 +16,117 @@
==================================================================== */
package org.apache.poi.poifs.crypt;
import java.io.ByteArrayInputStream;
import javax.xml.parsers.DocumentBuilderFactory;
import org.apache.commons.codec.binary.Base64;
import org.apache.poi.EncryptedDocumentException;
import org.apache.poi.poifs.filesystem.DocumentInputStream;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
/**
* Used when checking if a key is valid for a document
*/
public class EncryptionVerifier {
private final byte[] salt;
private final byte[] verifier;
private final byte[] verifierHash;
private final byte[] encryptedKey;
private final int verifierHashSize;
private final int spinCount;
private final int algorithm;
private final int cipherMode;
public EncryptionVerifier(String descriptor) {
NamedNodeMap keyData = null;
try {
ByteArrayInputStream is;
is = new ByteArrayInputStream(descriptor.getBytes());
NodeList keyEncryptor = DocumentBuilderFactory.newInstance()
.newDocumentBuilder().parse(is)
.getElementsByTagName("keyEncryptor").item(0).getChildNodes();
for (int i = 0; i < keyEncryptor.getLength(); i++) {
Node node = keyEncryptor.item(i);
if (node.getNodeName().equals("p:encryptedKey")) {
keyData = node.getAttributes();
break;
}
}
if (keyData == null)
throw new EncryptedDocumentException("");
} catch (Exception e) {
throw new EncryptedDocumentException("Unable to parse keyEncryptor");
}
spinCount = Integer.parseInt(keyData.getNamedItem("spinCount")
.getNodeValue());
verifier = Base64.decodeBase64(keyData
.getNamedItem("encryptedVerifierHashInput")
.getNodeValue().getBytes());
salt = Base64.decodeBase64(keyData.getNamedItem("saltValue")
.getNodeValue().getBytes());
encryptedKey = Base64.decodeBase64(keyData
.getNamedItem("encryptedKeyValue")
.getNodeValue().getBytes());
int saltSize = Integer.parseInt(keyData.getNamedItem("saltSize")
.getNodeValue());
if (saltSize != salt.length)
throw new EncryptedDocumentException("Invalid salt size");
verifierHash = Base64.decodeBase64(keyData
.getNamedItem("encryptedVerifierHashValue")
.getNodeValue().getBytes());
int blockSize = Integer.parseInt(keyData.getNamedItem("blockSize")
.getNodeValue());
String alg = keyData.getNamedItem("cipherAlgorithm").getNodeValue();
int keyBits = Integer.parseInt(keyData.getNamedItem("keyBits")
.getNodeValue());
if ("AES".equals(alg)) {
switch (keyBits) {
case 128:
algorithm = EncryptionHeader.ALGORITHM_AES_128; break;
case 192:
algorithm = EncryptionHeader.ALGORITHM_AES_192; break;
case 256:
algorithm = EncryptionHeader.ALGORITHM_AES_256; break;
default:
throw new EncryptedDocumentException("Unsupported key size");
}
} else {
throw new EncryptedDocumentException("Unsupported cipher");
}
String chain = keyData.getNamedItem("cipherChaining").getNodeValue();
if ("ChainingModeCBC".equals(chain))
cipherMode = EncryptionHeader.MODE_CBC;
else if ("ChainingModeCFB".equals(chain))
cipherMode = EncryptionHeader.MODE_CFB;
else
throw new EncryptedDocumentException("Unsupported chaining mode");
verifierHashSize = Integer.parseInt(keyData.getNamedItem("hashSize")
.getNodeValue());
}
public EncryptionVerifier(DocumentInputStream is, int encryptedLength) {
int saltSize = is.readInt();
if (saltSize!=16) {
throw new RuntimeException("Salt size != 16 !?");
}
salt = new byte[16];
is.readFully(salt);
verifier = new byte[16];
is.readFully(verifier);
verifierHashSize = is.readInt();
verifierHash = new byte[encryptedLength];
is.readFully(verifierHash);
spinCount = 50000;
algorithm = EncryptionHeader.ALGORITHM_AES_128;
cipherMode = EncryptionHeader.MODE_ECB;
encryptedKey = null;
}
public abstract class EncryptionVerifier {
private byte[] salt;
private byte[] encryptedVerifier;
private byte[] encryptedVerifierHash;
private byte[] encryptedKey;
// protected int verifierHashSize;
private int spinCount;
private CipherAlgorithm cipherAlgorithm;
private ChainingMode chainingMode;
private HashAlgorithm hashAlgorithm;
protected EncryptionVerifier() {}
public byte[] getSalt() {
return salt;
}
/**
* The method name is misleading - you'll get the encrypted verifier, not the plain verifier
* @deprecated use getEncryptedVerifier()
*/
public byte[] getVerifier() {
return verifier;
return encryptedVerifier;
}
public byte[] getEncryptedVerifier() {
return encryptedVerifier;
}
/**
* The method name is misleading - you'll get the encrypted verifier hash, not the plain verifier hash
* @deprecated use getEnryptedVerifierHash
*/
public byte[] getVerifierHash() {
return verifierHash;
return encryptedVerifierHash;
}
public byte[] getEncryptedVerifierHash() {
return encryptedVerifierHash;
}
public int getSpinCount() {
return spinCount;
}
public int getCipherMode() {
return cipherMode;
return chainingMode.ecmaId;
}
public int getAlgorithm() {
return algorithm;
return cipherAlgorithm.ecmaId;
}
/**
* @deprecated use getCipherAlgorithm().jceId
*/
public String getAlgorithmName() {
return cipherAlgorithm.jceId;
}
public byte[] getEncryptedKey() {
return encryptedKey;
}
public CipherAlgorithm getCipherAlgorithm() {
return cipherAlgorithm;
}
public HashAlgorithm getHashAlgorithm() {
return hashAlgorithm;
}
public ChainingMode getChainingMode() {
return chainingMode;
}
protected void setSalt(byte[] salt) {
this.salt = salt;
}
protected void setEncryptedVerifier(byte[] encryptedVerifier) {
this.encryptedVerifier = encryptedVerifier;
}
protected void setEncryptedVerifierHash(byte[] encryptedVerifierHash) {
this.encryptedVerifierHash = encryptedVerifierHash;
}
protected void setEncryptedKey(byte[] encryptedKey) {
this.encryptedKey = encryptedKey;
}
protected void setSpinCount(int spinCount) {
this.spinCount = spinCount;
}
protected void setCipherAlgorithm(CipherAlgorithm cipherAlgorithm) {
this.cipherAlgorithm = cipherAlgorithm;
}
protected void setChainingMode(ChainingMode chainingMode) {
this.chainingMode = chainingMode;
}
protected void setHashAlgorithm(HashAlgorithm hashAlgorithm) {
this.hashAlgorithm = hashAlgorithm;
}
}

View File

@ -0,0 +1,65 @@
/* ====================================================================
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==================================================================== */
package org.apache.poi.poifs.crypt;
import java.io.IOException;
import java.io.OutputStream;
import java.security.GeneralSecurityException;
import javax.crypto.SecretKey;
import org.apache.poi.poifs.filesystem.DirectoryNode;
import org.apache.poi.poifs.filesystem.NPOIFSFileSystem;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
public abstract class Encryptor {
private SecretKey secretKey;
/**
* Return a output stream for encrypted data.
*
* @param dir the node to write to
* @return encrypted stream
*/
public abstract OutputStream getDataStream(DirectoryNode dir)
throws IOException, GeneralSecurityException;
// for tests
public abstract void confirmPassword(String password, byte keySpec[], byte keySalt[], byte verifier[], byte verifierSalt[], byte integritySalt[]);
public abstract void confirmPassword(String password);
public static Encryptor getInstance(EncryptionInfo info) {
return info.getEncryptor();
}
public OutputStream getDataStream(NPOIFSFileSystem fs) throws IOException, GeneralSecurityException {
return getDataStream(fs.getRoot());
}
public OutputStream getDataStream(POIFSFileSystem fs) throws IOException, GeneralSecurityException {
return getDataStream(fs.getRoot());
}
public SecretKey getSecretKey() {
return secretKey;
}
protected void setSecretKey(SecretKey secretKey) {
this.secretKey = secretKey;
}
}

View File

@ -0,0 +1,66 @@
/* ====================================================================
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==================================================================== */
package org.apache.poi.poifs.crypt;
import org.apache.poi.EncryptedDocumentException;
public enum HashAlgorithm {
none ( "", 0x0000, "", 0, "", false),
sha1 ( "SHA-1", 0x8004, "SHA1", 20, "HmacSHA1", false),
sha256 ( "SHA-256", 0x800C, "SHA256", 32, "HmacSHA256", false),
sha384 ( "SHA-384", 0x800D, "SHA384", 48, "HmacSHA384", false),
sha512 ( "SHA-512", 0x800E, "SHA512", 64, "HmacSHA512", false),
/* only for agile encryption */
md5 ( "MD5", -1, "MD5", 16, "HmacMD5", false),
// although sunjc2 supports md2, hmac-md2 is only supported by bouncycastle
md2 ( "MD2", -1, "MD2", 16, "Hmac-MD2", true),
ripemd128("RipeMD128", -1, "RIPEMD-128", 16, "HMac-RipeMD128", true),
ripemd160("RipeMD160", -1, "RIPEMD-160", 20, "HMac-RipeMD160", true),
whirlpool("Whirlpool", -1, "WHIRLPOOL", 64, "HMac-Whirlpool", true),
;
public final String jceId;
public final int ecmaId;
public final String ecmaString;
public final int hashSize;
public final String jceHmacId;
public final boolean needsBouncyCastle;
HashAlgorithm(String jceId, int ecmaId, String ecmaString, int hashSize, String jceHmacId, boolean needsBouncyCastle) {
this.jceId = jceId;
this.ecmaId = ecmaId;
this.ecmaString = ecmaString;
this.hashSize = hashSize;
this.jceHmacId = jceHmacId;
this.needsBouncyCastle = needsBouncyCastle;
}
public static HashAlgorithm fromEcmaId(int ecmaId) {
for (HashAlgorithm ha : values()) {
if (ha.ecmaId == ecmaId) return ha;
}
throw new EncryptedDocumentException("hash algorithm not found");
}
public static HashAlgorithm fromEcmaId(String ecmaString) {
for (HashAlgorithm ha : values()) {
if (ha.ecmaString.equals(ecmaString)) return ha;
}
throw new EncryptedDocumentException("hash algorithm not found");
}
}

View File

@ -0,0 +1,44 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<!--
====================================================================
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
====================================================================
-->
<html>
<head>
</head>
<body bgcolor="white">
<p>Implementation of the <a href="http://msdn.microsoft.com/en-us/library/dd952186(v=office.12).aspx">ECMA-376 Document Encryption</a></p>
<p>The implementation is split into three packages:</p>
<ul>
<li>This package contains common functions for both current implemented cipher modes.</li>
<li>the {@link org.apache.poi.poifs.crypt.standard standard} package is part of the base poi jar and contains classes for the standard encryption ...</li>
<li>the {@link org.apache.poi.poifs.crypt.agile agile} package is part of the poi ooxml jar and the provides agile encryption support.</li>
</ul>
<h2>Related Documentation</h2>
Some implementations informations can be found under:
<ul>
<li><a href="http://poi.apache.org/encryption.html">Apache POI - Encryption support</a>
</ul>
<!-- Put @see and @since tags down here. -->
@see org.apache.poi.poifs.crypt.standard
@see org.apache.poi.poifs.crypt.agile
</body>
</html>

View File

@ -0,0 +1,23 @@
/* ====================================================================
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==================================================================== */
package org.apache.poi.poifs.crypt.standard;
import org.apache.poi.util.LittleEndianByteArrayOutputStream;
public interface EncryptionRecord {
void write(LittleEndianByteArrayOutputStream os);
}

View File

@ -0,0 +1,158 @@
/* ====================================================================
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==================================================================== */
package org.apache.poi.poifs.crypt.standard;
import static org.apache.poi.poifs.crypt.CryptoFunctions.hashPassword;
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.apache.poi.EncryptedDocumentException;
import org.apache.poi.poifs.crypt.ChainingMode;
import org.apache.poi.poifs.crypt.CryptoFunctions;
import org.apache.poi.poifs.crypt.Decryptor;
import org.apache.poi.poifs.crypt.EncryptionHeader;
import org.apache.poi.poifs.crypt.EncryptionInfo;
import org.apache.poi.poifs.crypt.EncryptionVerifier;
import org.apache.poi.poifs.crypt.HashAlgorithm;
import org.apache.poi.poifs.filesystem.DirectoryNode;
import org.apache.poi.poifs.filesystem.DocumentInputStream;
import org.apache.poi.util.BoundedInputStream;
import org.apache.poi.util.LittleEndian;
/**
*/
public class StandardDecryptor extends Decryptor {
private long _length = -1;
protected StandardDecryptor(EncryptionInfo info) {
super(info);
}
public boolean verifyPassword(String password) {
EncryptionVerifier ver = info.getVerifier();
SecretKey skey = generateSecretKey(password, ver, getKeySizeInBytes());
Cipher cipher = getCipher(skey);
try {
byte encryptedVerifier[] = ver.getEncryptedVerifier();
byte verifier[] = cipher.doFinal(encryptedVerifier);
setVerifier(verifier);
MessageDigest sha1 = MessageDigest.getInstance(ver.getHashAlgorithm().jceId);
byte[] calcVerifierHash = sha1.digest(verifier);
byte encryptedVerifierHash[] = ver.getEncryptedVerifierHash();
byte decryptedVerifierHash[] = cipher.doFinal(encryptedVerifierHash);
byte[] verifierHash = truncateOrPad(decryptedVerifierHash, calcVerifierHash.length);
if (Arrays.equals(calcVerifierHash, verifierHash)) {
setSecretKey(skey);
return true;
} else {
return false;
}
} catch (GeneralSecurityException e) {
throw new EncryptedDocumentException(e);
}
}
protected static SecretKey generateSecretKey(String password, EncryptionVerifier ver, int keySize) {
HashAlgorithm hashAlgo = ver.getHashAlgorithm();
byte pwHash[] = hashPassword(password, hashAlgo, ver.getSalt(), ver.getSpinCount());
byte[] blockKey = new byte[4];
LittleEndian.putInt(blockKey, 0, 0);
byte[] finalHash = CryptoFunctions.generateKey(pwHash, hashAlgo, blockKey, hashAlgo.hashSize);
byte x1[] = fillAndXor(finalHash, (byte) 0x36);
byte x2[] = fillAndXor(finalHash, (byte) 0x5c);
byte[] x3 = new byte[x1.length + x2.length];
System.arraycopy(x1, 0, x3, 0, x1.length);
System.arraycopy(x2, 0, x3, x1.length, x2.length);
byte[] key = truncateOrPad(x3, keySize);
SecretKey skey = new SecretKeySpec(key, ver.getCipherAlgorithm().jceId);
return skey;
}
protected static byte[] fillAndXor(byte hash[], byte fillByte) {
byte[] buff = new byte[64];
Arrays.fill(buff, fillByte);
for (int i=0; i<hash.length; i++) {
buff[i] = (byte) (buff[i] ^ hash[i]);
}
try {
MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
return sha1.digest(buff);
} catch (NoSuchAlgorithmException e) {
throw new EncryptedDocumentException("hash algo not supported", e);
}
}
/**
* Returns a byte array of the requested length,
* truncated or zero padded as needed.
* Behaves like Arrays.copyOf in Java 1.6
*/
protected static byte[] truncateOrPad(byte[] source, int length) {
byte[] result = new byte[length];
System.arraycopy(source, 0, result, 0, Math.min(length, source.length));
if(length > source.length) {
for(int i=source.length; i<length; i++) {
result[i] = 0;
}
}
return result;
}
private Cipher getCipher(SecretKey key) {
EncryptionHeader em = info.getHeader();
ChainingMode cm = em.getChainingMode();
assert(cm == ChainingMode.ecb);
return CryptoFunctions.getCipher(key, em.getCipherAlgorithm(), cm, null, Cipher.DECRYPT_MODE);
}
public InputStream getDataStream(DirectoryNode dir) throws IOException {
DocumentInputStream dis = dir.createDocumentInputStream("EncryptedPackage");
_length = dis.readLong();
return new BoundedInputStream(new CipherInputStream(dis, getCipher(getSecretKey())), _length);
}
public long getLength(){
if(_length == -1) throw new IllegalStateException("Decryptor.getDataStream() was not called");
return _length;
}
protected int getKeySizeInBytes() {
return info.getHeader().getKeySize()/8;
}
}

View File

@ -0,0 +1,116 @@
/* ====================================================================
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==================================================================== */
package org.apache.poi.poifs.crypt.standard;
import static org.apache.poi.poifs.crypt.CryptoFunctions.getUtf16LeString;
import java.io.IOException;
import org.apache.poi.poifs.crypt.ChainingMode;
import org.apache.poi.poifs.crypt.CipherAlgorithm;
import org.apache.poi.poifs.crypt.CipherProvider;
import org.apache.poi.poifs.crypt.EncryptionHeader;
import org.apache.poi.poifs.crypt.HashAlgorithm;
import org.apache.poi.poifs.filesystem.DocumentInputStream;
import org.apache.poi.util.BitField;
import org.apache.poi.util.LittleEndianByteArrayOutputStream;
import org.apache.poi.util.LittleEndianConsts;
import org.apache.poi.util.LittleEndianOutput;
public class StandardEncryptionHeader extends EncryptionHeader implements EncryptionRecord {
// A flag that specifies whether CryptoAPI RC4 or ECMA-376 encryption
// [ECMA-376] is used. It MUST be 1 unless fExternal is 1. If fExternal is 1, it MUST be 0.
private static BitField flagsCryptoAPI = new BitField(0x04);
// A value that MUST be 0 if document properties are encrypted. The
// encryption of document properties is specified in section 2.3.5.4 [MS-OFFCRYPTO].
@SuppressWarnings("unused")
private static BitField flagsDocProps = new BitField(0x08);
// A value that MUST be 1 if extensible encryption is used,. If this value is 1,
// the value of every other field in this structure MUST be 0.
@SuppressWarnings("unused")
private static BitField flagsExternal = new BitField(0x10);
// A value that MUST be 1 if the protected content is an ECMA-376 document
// [ECMA-376]. If the fAES bit is 1, the fCryptoAPI bit MUST also be 1.
private static BitField flagsAES = new BitField(0x20);
protected StandardEncryptionHeader(DocumentInputStream is) throws IOException {
setFlags(is.readInt());
setSizeExtra(is.readInt());
setCipherAlgorithm(CipherAlgorithm.fromEcmaId(is.readInt()));
setHashAlgorithm(HashAlgorithm.fromEcmaId(is.readInt()));
setKeySize(is.readInt());
setBlockSize(getKeySize());
setCipherProvider(CipherProvider.fromEcmaId(is.readInt()));
is.readLong(); // skip reserved
// CSPName may not always be specified
// In some cases, the salt value of the EncryptionVerifier is the next chunk of data
is.mark(LittleEndianConsts.INT_SIZE+1);
int checkForSalt = is.readInt();
is.reset();
if (checkForSalt == 16) {
setCspName("");
} else {
StringBuilder builder = new StringBuilder();
while (true) {
char c = (char) is.readShort();
if (c == 0) break;
builder.append(c);
}
setCspName(builder.toString());
}
setChainingMode(ChainingMode.ecb);
setKeySalt(null);
}
protected StandardEncryptionHeader(CipherAlgorithm cipherAlgorithm, HashAlgorithm hashAlgorithm, int keyBits, int blockSize, ChainingMode chainingMode) {
setCipherAlgorithm(cipherAlgorithm);
setHashAlgorithm(hashAlgorithm);
setKeySize(keyBits);
setBlockSize(blockSize);
setCipherProvider(cipherAlgorithm.provider);
setFlags(flagsCryptoAPI.setBoolean(0, true)
| flagsAES.setBoolean(0, cipherAlgorithm.provider == CipherProvider.aes));
// see http://msdn.microsoft.com/en-us/library/windows/desktop/bb931357(v=vs.85).aspx for a full list
// setCspName("Microsoft Enhanced RSA and AES Cryptographic Provider");
}
public void write(LittleEndianByteArrayOutputStream bos) {
int startIdx = bos.getWriteIndex();
LittleEndianOutput sizeOutput = bos.createDelayedOutput(LittleEndianConsts.INT_SIZE);
bos.writeInt(getFlags());
bos.writeInt(0); // size extra
bos.writeInt(getCipherAlgorithm().ecmaId);
bos.writeInt(getHashAlgorithmEx().ecmaId);
bos.writeInt(getKeySize());
bos.writeInt(getCipherProvider().ecmaId);
bos.writeInt(0); // reserved1
bos.writeInt(0); // reserved2
if (getCspName() != null) {
bos.write(getUtf16LeString(getCspName()));
bos.writeShort(0);
}
int headerSize = bos.getWriteIndex()-startIdx-LittleEndianConsts.INT_SIZE;
sizeOutput.writeInt(headerSize);
}
}

View File

@ -0,0 +1,106 @@
/* ====================================================================
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==================================================================== */
package org.apache.poi.poifs.crypt.standard;
import java.io.IOException;
import org.apache.poi.EncryptedDocumentException;
import org.apache.poi.poifs.crypt.ChainingMode;
import org.apache.poi.poifs.crypt.CipherAlgorithm;
import org.apache.poi.poifs.crypt.EncryptionInfo;
import org.apache.poi.poifs.crypt.EncryptionInfoBuilder;
import org.apache.poi.poifs.crypt.HashAlgorithm;
import org.apache.poi.poifs.filesystem.DocumentInputStream;
public class StandardEncryptionInfoBuilder implements EncryptionInfoBuilder {
EncryptionInfo info;
StandardEncryptionHeader header;
StandardEncryptionVerifier verifier;
StandardDecryptor decryptor;
StandardEncryptor encryptor;
public void initialize(EncryptionInfo info, DocumentInputStream dis) throws IOException {
this.info = info;
@SuppressWarnings("unused")
int hSize = dis.readInt();
header = new StandardEncryptionHeader(dis);
verifier = new StandardEncryptionVerifier(dis, header);
if (info.getVersionMinor() == 2 && (info.getVersionMajor() == 3 || info.getVersionMajor() == 4)) {
decryptor = new StandardDecryptor(info);
}
}
public void initialize(EncryptionInfo info, CipherAlgorithm cipherAlgorithm, HashAlgorithm hashAlgorithm, int keyBits, int blockSize, ChainingMode chainingMode) {
this.info = info;
if (cipherAlgorithm == null) {
cipherAlgorithm = CipherAlgorithm.rc4;
}
if (hashAlgorithm == null) {
hashAlgorithm = HashAlgorithm.sha1;
}
if (hashAlgorithm != HashAlgorithm.sha1) {
throw new EncryptedDocumentException("Standard encryption only supports SHA-1.");
}
if (chainingMode == null) {
chainingMode = ChainingMode.ecb;
}
if (chainingMode != ChainingMode.ecb) {
throw new EncryptedDocumentException("Standard encryption only supports ECB chaining.");
}
if (keyBits == -1) {
keyBits = cipherAlgorithm.defaultKeySize;
}
if (blockSize == -1) {
blockSize = cipherAlgorithm.blockSize;
}
boolean found = false;
for (int ks : cipherAlgorithm.allowedKeySize) {
found |= (ks == keyBits);
}
if (!found) {
throw new EncryptedDocumentException("KeySize "+keyBits+" not allowed for Cipher "+cipherAlgorithm.toString());
}
header = new StandardEncryptionHeader(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode);
verifier = new StandardEncryptionVerifier(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode);
decryptor = new StandardDecryptor(info);
encryptor = new StandardEncryptor(this);
}
public StandardEncryptionHeader getHeader() {
return header;
}
public StandardEncryptionVerifier getVerifier() {
return verifier;
}
public StandardDecryptor getDecryptor() {
return decryptor;
}
public StandardEncryptor getEncryptor() {
return encryptor;
}
public EncryptionInfo getEncryptionInfo() {
return info;
}
}

View File

@ -0,0 +1,112 @@
/* ====================================================================
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==================================================================== */
package org.apache.poi.poifs.crypt.standard;
import org.apache.poi.EncryptedDocumentException;
import org.apache.poi.poifs.crypt.ChainingMode;
import org.apache.poi.poifs.crypt.CipherAlgorithm;
import org.apache.poi.poifs.crypt.EncryptionVerifier;
import org.apache.poi.poifs.crypt.HashAlgorithm;
import org.apache.poi.poifs.filesystem.DocumentInputStream;
import org.apache.poi.util.LittleEndianByteArrayOutputStream;
/**
* Used when checking if a key is valid for a document
*/
public class StandardEncryptionVerifier extends EncryptionVerifier implements EncryptionRecord {
private static final int SPIN_COUNT = 50000;
private final int verifierHashSize;
protected StandardEncryptionVerifier(DocumentInputStream is, StandardEncryptionHeader header) {
int saltSize = is.readInt();
if (saltSize!=16) {
throw new RuntimeException("Salt size != 16 !?");
}
byte salt[] = new byte[16];
is.readFully(salt);
setSalt(salt);
byte encryptedVerifier[] = new byte[16];
is.readFully(encryptedVerifier);
setEncryptedVerifier(encryptedVerifier);
verifierHashSize = is.readInt();
byte encryptedVerifierHash[] = new byte[header.getCipherAlgorithm().encryptedVerifierHashLength];
is.readFully(encryptedVerifierHash);
setEncryptedVerifierHash(encryptedVerifierHash);
setSpinCount(SPIN_COUNT);
setCipherAlgorithm(CipherAlgorithm.aes128);
setChainingMode(ChainingMode.ecb);
setEncryptedKey(null);
setHashAlgorithm(HashAlgorithm.sha1);
}
protected StandardEncryptionVerifier(CipherAlgorithm cipherAlgorithm, HashAlgorithm hashAlgorithm, int keyBits, int blockSize, ChainingMode chainingMode) {
setCipherAlgorithm(cipherAlgorithm);
setHashAlgorithm(hashAlgorithm);
setChainingMode(chainingMode);
setSpinCount(SPIN_COUNT);
verifierHashSize = hashAlgorithm.hashSize;
}
// make method visible for this package
protected void setSalt(byte salt[]) {
if (salt == null || salt.length != 16) {
throw new EncryptedDocumentException("invalid verifier salt");
}
super.setSalt(salt);
}
// make method visible for this package
protected void setEncryptedVerifier(byte encryptedVerifier[]) {
super.setEncryptedVerifier(encryptedVerifier);
}
// make method visible for this package
protected void setEncryptedVerifierHash(byte encryptedVerifierHash[]) {
super.setEncryptedVerifierHash(encryptedVerifierHash);
}
public void write(LittleEndianByteArrayOutputStream bos) {
// see [MS-OFFCRYPTO] - 2.3.4.9
byte salt[] = getSalt();
assert(salt.length == 16);
bos.writeInt(salt.length); // salt size
bos.write(salt);
// The resulting Verifier value MUST be an array of 16 bytes.
byte encryptedVerifier[] = getEncryptedVerifier();
assert(encryptedVerifier.length == 16);
bos.write(encryptedVerifier);
// The number of bytes used by the encrypted Verifier hash MUST be 32.
// The number of bytes used by the decrypted Verifier hash is given by
// the VerifierHashSize field, which MUST be 20
byte encryptedVerifierHash[] = getEncryptedVerifierHash();
assert(encryptedVerifierHash.length == 32);
bos.writeInt(20);
bos.write(encryptedVerifierHash);
}
protected int getVerifierHashSize() {
return verifierHashSize;
}
}

View File

@ -0,0 +1,218 @@
/* ====================================================================
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==================================================================== */
package org.apache.poi.poifs.crypt.standard;
import static org.apache.poi.poifs.crypt.DataSpaceMapUtils.createEncryptionEntry;
import static org.apache.poi.poifs.crypt.standard.StandardDecryptor.generateSecretKey;
import static org.apache.poi.poifs.crypt.standard.StandardDecryptor.truncateOrPad;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Random;
import javax.crypto.Cipher;
import javax.crypto.CipherOutputStream;
import javax.crypto.SecretKey;
import org.apache.poi.EncryptedDocumentException;
import org.apache.poi.poifs.crypt.CryptoFunctions;
import org.apache.poi.poifs.crypt.DataSpaceMapUtils;
import org.apache.poi.poifs.crypt.EncryptionInfo;
import org.apache.poi.poifs.crypt.EncryptionVerifier;
import org.apache.poi.poifs.crypt.Encryptor;
import org.apache.poi.poifs.filesystem.DirectoryNode;
import org.apache.poi.poifs.filesystem.POIFSWriterEvent;
import org.apache.poi.poifs.filesystem.POIFSWriterListener;
import org.apache.poi.util.IOUtils;
import org.apache.poi.util.LittleEndianByteArrayOutputStream;
import org.apache.poi.util.LittleEndianConsts;
import org.apache.poi.util.LittleEndianOutputStream;
import org.apache.poi.util.TempFile;
public class StandardEncryptor extends Encryptor {
private final StandardEncryptionInfoBuilder builder;
protected StandardEncryptor(StandardEncryptionInfoBuilder builder) {
this.builder = builder;
}
public void confirmPassword(String password) {
// see [MS-OFFCRYPTO] - 2.3.3 EncryptionVerifier
Random r = new SecureRandom();
byte[] salt = new byte[16], verifier = new byte[16];
r.nextBytes(salt);
r.nextBytes(verifier);
confirmPassword(password, null, null, salt, verifier, null);
}
/**
* Fills the fields of verifier and header with the calculated hashes based
* on the password and a random salt
*
* see [MS-OFFCRYPTO] - 2.3.4.7 ECMA-376 Document Encryption Key Generation
*/
public void confirmPassword(String password, byte keySpec[], byte keySalt[], byte verifier[], byte verifierSalt[], byte integritySalt[]) {
StandardEncryptionVerifier ver = builder.getVerifier();
ver.setSalt(verifierSalt);
SecretKey secretKey = generateSecretKey(password, ver, getKeySizeInBytes());
setSecretKey(secretKey);
Cipher cipher = getCipher(secretKey, null);
try {
byte encryptedVerifier[] = cipher.doFinal(verifier);
MessageDigest hashAlgo = MessageDigest.getInstance(ver.getHashAlgorithm().jceId);
byte calcVerifierHash[] = hashAlgo.digest(verifier);
// 2.3.3 EncryptionVerifier ...
// An array of bytes that contains the encrypted form of the
// hash of the randomly generated Verifier value. The length of the array MUST be the size of
// the encryption block size multiplied by the number of blocks needed to encrypt the hash of the
// Verifier. If the encryption algorithm is RC4, the length MUST be 20 bytes. If the encryption
// algorithm is AES, the length MUST be 32 bytes. After decrypting the EncryptedVerifierHash
// field, only the first VerifierHashSize bytes MUST be used.
int encVerHashSize = ver.getCipherAlgorithm().encryptedVerifierHashLength;
byte encryptedVerifierHash[] = cipher.doFinal(truncateOrPad(calcVerifierHash, encVerHashSize));
ver.setEncryptedVerifier(encryptedVerifier);
ver.setEncryptedVerifierHash(encryptedVerifierHash);
} catch (GeneralSecurityException e) {
throw new EncryptedDocumentException("Password confirmation failed", e);
}
}
private Cipher getCipher(SecretKey key, String padding) {
EncryptionVerifier ver = builder.getVerifier();
return CryptoFunctions.getCipher(key, ver.getCipherAlgorithm(), ver.getChainingMode(), null, Cipher.ENCRYPT_MODE, padding);
}
public OutputStream getDataStream(final DirectoryNode dir)
throws IOException, GeneralSecurityException {
createEncryptionInfoEntry(dir);
DataSpaceMapUtils.addDefaultDataSpace(dir);
OutputStream countStream = new StandardCipherOutputStream(dir);
return countStream;
}
protected class StandardCipherOutputStream extends FilterOutputStream implements POIFSWriterListener {
protected long countBytes;
protected final File fileOut;
protected final DirectoryNode dir;
protected StandardCipherOutputStream(DirectoryNode dir) throws IOException {
super(null);
this.dir = dir;
fileOut = TempFile.createTempFile("encrypted_package", "crypt");
FileOutputStream rawStream = new FileOutputStream(fileOut);
// although not documented, we need the same padding as with agile encryption
// and instead of calculating the missing bytes for the block size ourselves
// we leave it up to the CipherOutputStream, which generates/saves them on close()
// ... we can't use "NoPadding" here
//
// see also [MS-OFFCRYPT] - 2.3.4.15
// The final data block MUST be padded to the next integral multiple of the
// KeyData.blockSize value. Any padding bytes can be used. Note that the StreamSize
// field of the EncryptedPackage field specifies the number of bytes of
// unencrypted data as specified in section 2.3.4.4.
CipherOutputStream cryptStream = new CipherOutputStream(rawStream, getCipher(getSecretKey(), "PKCS5Padding"));
this.out = cryptStream;
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
out.write(b, off, len);
countBytes += len;
}
@Override
public void write(int b) throws IOException {
out.write(b);
countBytes++;
}
public void close() throws IOException {
// the CipherOutputStream adds the padding bytes on close()
super.close();
writeToPOIFS();
}
void writeToPOIFS() throws IOException {
int oleStreamSize = (int)(fileOut.length()+LittleEndianConsts.LONG_SIZE);
dir.createDocument("EncryptedPackage", oleStreamSize, this);
// TODO: any properties???
}
public void processPOIFSWriterEvent(POIFSWriterEvent event) {
try {
LittleEndianOutputStream leos = new LittleEndianOutputStream(event.getStream());
// StreamSize (8 bytes): An unsigned integer that specifies the number of bytes used by data
// encrypted within the EncryptedData field, not including the size of the StreamSize field.
// Note that the actual size of the \EncryptedPackage stream (1) can be larger than this
// value, depending on the block size of the chosen encryption algorithm
leos.writeLong(countBytes);
FileInputStream fis = new FileInputStream(fileOut);
IOUtils.copy(fis, leos);
fis.close();
fileOut.delete();
leos.close();
} catch (IOException e) {
throw new EncryptedDocumentException(e);
}
}
}
protected int getKeySizeInBytes() {
return builder.getHeader().getKeySize()/8;
}
protected void createEncryptionInfoEntry(DirectoryNode dir) throws IOException {
final EncryptionInfo info = builder.getEncryptionInfo();
final StandardEncryptionHeader header = builder.getHeader();
final StandardEncryptionVerifier verifier = builder.getVerifier();
EncryptionRecord er = new EncryptionRecord(){
public void write(LittleEndianByteArrayOutputStream bos) {
bos.writeShort(info.getVersionMajor());
bos.writeShort(info.getVersionMinor());
bos.writeInt(info.getEncryptionFlags());
header.write(bos);
verifier.write(bos);
}
};
createEncryptionEntry(dir, "EncryptionInfo", er);
// TODO: any properties???
}
}

View File

@ -21,6 +21,7 @@ package org.apache.poi;
*
* @author Yegor Kozlov
*/
@SuppressWarnings("serial")
public final class POIXMLException extends RuntimeException{
/**
* Create a new <code>POIXMLException</code> with no

View File

@ -0,0 +1,404 @@
/* ====================================================================
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.agile;
import static org.apache.poi.poifs.crypt.CryptoFunctions.generateIv;
import static org.apache.poi.poifs.crypt.CryptoFunctions.generateKey;
import static org.apache.poi.poifs.crypt.CryptoFunctions.getBlock0;
import static org.apache.poi.poifs.crypt.CryptoFunctions.getCipher;
import static org.apache.poi.poifs.crypt.CryptoFunctions.getMessageDigest;
import static org.apache.poi.poifs.crypt.CryptoFunctions.hashPassword;
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.MessageDigest;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.apache.poi.EncryptedDocumentException;
import org.apache.poi.poifs.crypt.CipherAlgorithm;
import org.apache.poi.poifs.crypt.CryptoFunctions;
import org.apache.poi.poifs.crypt.Decryptor;
import org.apache.poi.poifs.crypt.EncryptionHeader;
import org.apache.poi.poifs.crypt.EncryptionVerifier;
import org.apache.poi.poifs.crypt.HashAlgorithm;
import org.apache.poi.poifs.crypt.agile.AgileEncryptionVerifier.AgileCertificateEntry;
import org.apache.poi.poifs.filesystem.DirectoryNode;
import org.apache.poi.poifs.filesystem.DocumentInputStream;
import org.apache.poi.util.LittleEndian;
/**
* Decryptor implementation for Agile Encryption
*/
public class AgileDecryptor extends Decryptor {
private final AgileEncryptionInfoBuilder builder;
private long _length = -1;
protected static final byte[] kVerifierInputBlock;
protected static final byte[] kHashedVerifierBlock;
protected static final byte[] kCryptoKeyBlock;
protected static final byte[] kIntegrityKeyBlock;
protected static final byte[] kIntegrityValueBlock;
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 };
kIntegrityKeyBlock =
new byte[] { (byte)0x5f, (byte)0xb2, (byte)0xad, (byte)0x01,
(byte)0x0c, (byte)0xb9, (byte)0xe1, (byte)0xf6 };
kIntegrityValueBlock =
new byte[] { (byte)0xa0, (byte)0x67, (byte)0x7f, (byte)0x02,
(byte)0xb2, (byte)0x2c, (byte)0x84, (byte)0x33 };
}
protected AgileDecryptor(AgileEncryptionInfoBuilder builder) {
super(builder.getInfo());
this.builder = builder;
}
/**
* set decryption password
*/
public boolean verifyPassword(String password) throws GeneralSecurityException {
AgileEncryptionVerifier ver = builder.getVerifier();
AgileEncryptionHeader header = builder.getHeader();
HashAlgorithm hashAlgo = header.getHashAlgorithmEx();
CipherAlgorithm cipherAlgo = header.getCipherAlgorithm();
int blockSize = header.getBlockSize();
int keySize = header.getKeySize()/8;
byte[] pwHash = hashPassword(password, ver.getHashAlgorithm(), ver.getSalt(), ver.getSpinCount());
/**
* encryptedVerifierHashInput: This attribute MUST be generated by using the following steps:
* 1. Generate a random array of bytes with the number of bytes used specified by the saltSize
* attribute.
* 2. Generate an encryption key as specified in section 2.3.4.11 by using the user-supplied password,
* the binary byte array used to create the saltValue attribute, and a blockKey byte array
* consisting of the following bytes: 0xfe, 0xa7, 0xd2, 0x76, 0x3b, 0x4b, 0x9e, and 0x79.
* 3. Encrypt the random array of bytes generated in step 1 by using the binary form of the saltValue
* attribute as an initialization vector as specified in section 2.3.4.12. If the array of bytes is not an
* integral multiple of blockSize bytes, pad the array with 0x00 to the next integral multiple of
* blockSize bytes.
* 4. Use base64 to encode the result of step 3.
*/
byte verfierInputEnc[] = hashInput(builder, pwHash, kVerifierInputBlock, ver.getEncryptedVerifier(), Cipher.DECRYPT_MODE);
setVerifier(verfierInputEnc);
MessageDigest hashMD = getMessageDigest(hashAlgo);
byte[] verifierHash = hashMD.digest(verfierInputEnc);
/**
* encryptedVerifierHashValue: This attribute MUST be generated by using the following steps:
* 1. Obtain the hash value of the random array of bytes generated in step 1 of the steps for
* encryptedVerifierHashInput.
* 2. Generate an encryption key as specified in section 2.3.4.11 by using the user-supplied password,
* the binary byte array used to create the saltValue attribute, and a blockKey byte array
* consisting of the following bytes: 0xd7, 0xaa, 0x0f, 0x6d, 0x30, 0x61, 0x34, and 0x4e.
* 3. Encrypt the hash value obtained in step 1 by using the binary form of the saltValue attribute as
* an initialization vector as specified in section 2.3.4.12. If hashSize is not an integral multiple of
* blockSize bytes, pad the hash value with 0x00 to an integral multiple of blockSize bytes.
* 4. Use base64 to encode the result of step 3.
*/
byte verifierHashDec[] = hashInput(builder, pwHash, kHashedVerifierBlock, ver.getEncryptedVerifierHash(), Cipher.DECRYPT_MODE);
verifierHashDec = getBlock0(verifierHashDec, hashAlgo.hashSize);
/**
* encryptedKeyValue: This attribute MUST be generated by using the following steps:
* 1. Generate a random array of bytes that is the same size as specified by the
* Encryptor.KeyData.keyBits attribute of the parent element.
* 2. Generate an encryption key as specified in section 2.3.4.11, using the user-supplied password,
* the binary byte array used to create the saltValue attribute, and a blockKey byte array
* consisting of the following bytes: 0x14, 0x6e, 0x0b, 0xe7, 0xab, 0xac, 0xd0, and 0xd6.
* 3. Encrypt the random array of bytes generated in step 1 by using the binary form of the saltValue
* attribute as an initialization vector as specified in section 2.3.4.12. If the array of bytes is not an
* integral multiple of blockSize bytes, pad the array with 0x00 to an integral multiple of
* blockSize bytes.
* 4. Use base64 to encode the result of step 3.
*/
byte keyspec[] = hashInput(builder, pwHash, kCryptoKeyBlock, ver.getEncryptedKey(), Cipher.DECRYPT_MODE);
keyspec = getBlock0(keyspec, keySize);
SecretKeySpec secretKey = new SecretKeySpec(keyspec, ver.getCipherAlgorithm().jceId);
/**
* 1. Obtain the intermediate key by decrypting the encryptedKeyValue from a KeyEncryptor
* contained within the KeyEncryptors sequence. Use this key for encryption operations in the
* remaining steps of this section.
* 2. Generate a random array of bytes, known as Salt, of the same length as the value of the
* KeyData.hashSize attribute.
* 3. Encrypt the random array of bytes generated in step 2 by using the binary form of the
* KeyData.saltValue attribute and a blockKey byte array consisting of the following bytes: 0x5f,
* 0xb2, 0xad, 0x01, 0x0c, 0xb9, 0xe1, and 0xf6 used to form an initialization vector as specified in
* section 2.3.4.12. If the array of bytes is not an integral multiple of blockSize bytes, pad the
* array with 0x00 to the next integral multiple of blockSize bytes.
* 4. Assign the encryptedHmacKey attribute to the base64-encoded form of the result of step 3.
*/
byte vec[] = CryptoFunctions.generateIv(hashAlgo, header.getKeySalt(), kIntegrityKeyBlock, blockSize);
Cipher cipher = getCipher(secretKey, cipherAlgo, ver.getChainingMode(), vec, Cipher.DECRYPT_MODE);
byte hmacKey[] = cipher.doFinal(header.getEncryptedHmacKey());
hmacKey = getBlock0(hmacKey, hashAlgo.hashSize);
/**
* 5. Generate an HMAC, as specified in [RFC2104], of the encrypted form of the data (message),
* which the DataIntegrity element will verify by using the Salt generated in step 2 as the key.
* Note that the entire EncryptedPackage stream (1), including the StreamSize field, MUST be
* used as the message.
* 6. Encrypt the HMAC as in step 3 by using a blockKey byte array consisting of the following bytes:
* 0xa0, 0x67, 0x7f, 0x02, 0xb2, 0x2c, 0x84, and 0x33.
* 7. Assign the encryptedHmacValue attribute to the base64-encoded form of the result of step 6.
*/
vec = CryptoFunctions.generateIv(hashAlgo, header.getKeySalt(), kIntegrityValueBlock, blockSize);
cipher = getCipher(secretKey, cipherAlgo, ver.getChainingMode(), vec, Cipher.DECRYPT_MODE);
byte hmacValue[] = cipher.doFinal(header.getEncryptedHmacValue());
hmacValue = getBlock0(hmacValue, hashAlgo.hashSize);
if (Arrays.equals(verifierHashDec, verifierHash)) {
setSecretKey(secretKey);
setIntegrityHmacKey(hmacKey);
setIntegrityHmacValue(hmacValue);
return true;
} else {
return false;
}
}
/**
* instead of a password, it's also possible to decrypt via certificate.
* Warning: this code is experimental and hasn't been validated
*
* {@linkplain http://social.msdn.microsoft.com/Forums/en-US/cc9092bb-0c82-4b5b-ae21-abf643bdb37c/agile-encryption-with-certificates}
*
* @param keyPair
* @param x509
* @return
* @throws GeneralSecurityException
*/
public boolean verifyPassword(KeyPair keyPair, X509Certificate x509) throws GeneralSecurityException {
AgileEncryptionVerifier ver = builder.getVerifier();
AgileEncryptionHeader header = builder.getHeader();
HashAlgorithm hashAlgo = header.getHashAlgorithmEx();
CipherAlgorithm cipherAlgo = header.getCipherAlgorithm();
int blockSize = header.getBlockSize();
AgileCertificateEntry ace = null;
for (AgileCertificateEntry aceEntry : ver.getCertificates()) {
if (x509.equals(aceEntry.x509)) {
ace = aceEntry;
break;
}
}
if (ace == null) return false;
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate());
byte keyspec[] = cipher.doFinal(ace.encryptedKey);
SecretKeySpec secretKey = new SecretKeySpec(keyspec, ver.getCipherAlgorithm().jceId);
Mac x509Hmac = CryptoFunctions.getMac(hashAlgo);
x509Hmac.init(secretKey);
byte certVerifier[] = x509Hmac.doFinal(ace.x509.getEncoded());
byte vec[] = CryptoFunctions.generateIv(hashAlgo, header.getKeySalt(), kIntegrityKeyBlock, blockSize);
cipher = getCipher(secretKey, cipherAlgo, ver.getChainingMode(), vec, Cipher.DECRYPT_MODE);
byte hmacKey[] = cipher.doFinal(header.getEncryptedHmacKey());
hmacKey = getBlock0(hmacKey, hashAlgo.hashSize);
vec = CryptoFunctions.generateIv(hashAlgo, header.getKeySalt(), kIntegrityValueBlock, blockSize);
cipher = getCipher(secretKey, cipherAlgo, ver.getChainingMode(), vec, Cipher.DECRYPT_MODE);
byte hmacValue[] = cipher.doFinal(header.getEncryptedHmacValue());
hmacValue = getBlock0(hmacValue, hashAlgo.hashSize);
if (Arrays.equals(ace.certVerifier, certVerifier)) {
setSecretKey(secretKey);
setIntegrityHmacKey(hmacKey);
setIntegrityHmacValue(hmacValue);
return true;
} else {
return false;
}
}
protected static int getNextBlockSize(int inputLen, int blockSize) {
int fillSize;
for (fillSize=blockSize; fillSize<inputLen; fillSize+=blockSize);
return fillSize;
}
protected static byte[] hashInput(AgileEncryptionInfoBuilder builder, byte pwHash[], byte blockKey[], byte inputKey[], int cipherMode) {
EncryptionVerifier ver = builder.getVerifier();
int keySize = builder.getDecryptor().getKeySizeInBytes();
int blockSize = builder.getDecryptor().getBlockSizeInBytes();
HashAlgorithm hashAlgo = ver.getHashAlgorithm();
byte[] salt = ver.getSalt();
byte intermedKey[] = generateKey(pwHash, hashAlgo, blockKey, keySize);
SecretKey skey = new SecretKeySpec(intermedKey, ver.getCipherAlgorithm().jceId);
byte[] iv = generateIv(hashAlgo, salt, null, blockSize);
Cipher cipher = getCipher(skey, ver.getCipherAlgorithm(), ver.getChainingMode(), iv, cipherMode);
byte[] hashFinal;
try {
inputKey = getBlock0(inputKey, getNextBlockSize(inputKey.length, blockSize));
hashFinal = cipher.doFinal(inputKey);
return hashFinal;
} catch (GeneralSecurityException e) {
throw new EncryptedDocumentException(e);
}
}
public InputStream getDataStream(DirectoryNode dir) throws IOException, GeneralSecurityException {
DocumentInputStream dis = dir.createDocumentInputStream("EncryptedPackage");
_length = dis.readLong();
ChunkedCipherInputStream cipherStream = new ChunkedCipherInputStream(dis, _length);
return cipherStream;
}
public long getLength(){
if(_length == -1) throw new IllegalStateException("EcmaDecryptor.getDataStream() was not called");
return _length;
}
/**
* 2.3.4.15 Data Encryption (Agile Encryption)
*
* The EncryptedPackage stream (1) MUST be encrypted in 4096-byte segments to facilitate nearly
* random access while allowing CBC modes to be used in the encryption process.
* The initialization vector for the encryption process MUST be obtained by using the zero-based
* segment number as a blockKey and the binary form of the KeyData.saltValue as specified in
* section 2.3.4.12. The block number MUST be represented as a 32-bit unsigned integer.
* Data blocks MUST then be encrypted by using the initialization vector and the intermediate key
* obtained by decrypting the encryptedKeyValue from a KeyEncryptor contained within the
* KeyEncryptors sequence as specified in section 2.3.4.10. The final data block MUST be padded to
* the next integral multiple of the KeyData.blockSize value. Any padding bytes can be used. Note
* that the StreamSize field of the EncryptedPackage field specifies the number of bytes of
* unencrypted data as specified in section 2.3.4.4.
*/
private class ChunkedCipherInputStream extends InputStream {
private int _lastIndex = 0;
private long _pos = 0;
private final long _size;
private final InputStream _stream;
private byte[] _chunk;
private Cipher _cipher;
public ChunkedCipherInputStream(DocumentInputStream stream, long size)
throws GeneralSecurityException {
EncryptionHeader header = info.getHeader();
_size = size;
_stream = stream;
_cipher = getCipher(getSecretKey(), header.getCipherAlgorithm(), header.getChainingMode(), header.getKeySalt(), Cipher.DECRYPT_MODE);
}
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;
if (available() <= 0) return -1;
while (len > 0) {
if (_chunk == null) {
try {
_chunk = nextChunk();
} catch (GeneralSecurityException e) {
throw new EncryptedDocumentException(e.getMessage());
}
}
int count = (int)(4096L - (_pos & 0xfff));
int avail = available();
if (avail == 0) {
return total;
}
count = Math.min(avail, Math.min(count, len));
System.arraycopy(_chunk, (int)(_pos & 0xfff), b, off, count);
off += count;
len -= count;
_pos += count;
if ((_pos & 0xfff) == 0)
_chunk = null;
total += count;
}
return total;
}
public long skip(long n) throws IOException {
long start = _pos;
long skip = Math.min(available(), n);
if ((((_pos + skip) ^ start) & ~0xfff) != 0)
_chunk = null;
_pos += skip;
return skip;
}
public int available() throws IOException { return (int)(_size - _pos); }
public void close() throws IOException { _stream.close(); }
public boolean markSupported() { return false; }
private byte[] nextChunk() throws GeneralSecurityException, IOException {
int index = (int)(_pos >> 12);
byte[] blockKey = new byte[4];
LittleEndian.putInt(blockKey, 0, index);
EncryptionHeader header = info.getHeader();
byte[] iv = generateIv(header.getHashAlgorithmEx(), header.getKeySalt(), blockKey, getBlockSizeInBytes());
_cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), new IvParameterSpec(iv));
if (_lastIndex != index)
_stream.skip((index - _lastIndex) << 12);
byte[] block = new byte[Math.min(_stream.available(), 4096)];
_stream.read(block);
_lastIndex = index + 1;
return _cipher.doFinal(block);
}
}
protected int getBlockSizeInBytes() {
return info.getHeader().getBlockSize();
}
protected int getKeySizeInBytes() {
return info.getHeader().getKeySize()/8;
}
}

View File

@ -0,0 +1,130 @@
/* ====================================================================
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.agile;
import java.io.IOException;
import org.apache.poi.EncryptedDocumentException;
import org.apache.poi.poifs.crypt.ChainingMode;
import org.apache.poi.poifs.crypt.CipherAlgorithm;
import org.apache.poi.poifs.crypt.EncryptionHeader;
import org.apache.poi.poifs.crypt.HashAlgorithm;
import org.apache.xmlbeans.XmlException;
import com.microsoft.schemas.office.x2006.encryption.CTDataIntegrity;
import com.microsoft.schemas.office.x2006.encryption.CTKeyData;
import com.microsoft.schemas.office.x2006.encryption.EncryptionDocument;
import com.microsoft.schemas.office.x2006.encryption.STCipherChaining;
public class AgileEncryptionHeader extends EncryptionHeader {
private byte encryptedHmacKey[], encryptedHmacValue[];
public AgileEncryptionHeader(String descriptor) throws IOException {
EncryptionDocument ed;
try {
ed = EncryptionDocument.Factory.parse(descriptor);
} catch (XmlException e) {
throw new EncryptedDocumentException("Unable to parse encryption descriptor", e);
}
CTKeyData keyData;
try {
keyData = ed.getEncryption().getKeyData();
if (keyData == null) {
throw new NullPointerException("keyData not set");
}
} catch (Exception e) {
throw new EncryptedDocumentException("Unable to parse keyData");
}
setKeySize((int)keyData.getKeyBits());
setFlags(0);
setSizeExtra(0);
setCspName(null);
setBlockSize(keyData.getBlockSize());
int keyBits = (int)keyData.getKeyBits();
CipherAlgorithm ca = CipherAlgorithm.fromXmlId(keyData.getCipherAlgorithm().toString(), keyBits);
setCipherAlgorithm(ca);
setCipherProvider(ca.provider);
switch (keyData.getCipherChaining().intValue()) {
case STCipherChaining.INT_CHAINING_MODE_CBC:
setChainingMode(ChainingMode.cbc);
break;
case STCipherChaining.INT_CHAINING_MODE_CFB:
setChainingMode(ChainingMode.cfb);
break;
default:
throw new EncryptedDocumentException("Unsupported chaining mode - "+keyData.getCipherChaining().toString());
}
int hashSize = keyData.getHashSize();
HashAlgorithm ha = HashAlgorithm.fromEcmaId(keyData.getHashAlgorithm().toString());
setHashAlgorithm(ha);
if (getHashAlgorithmEx().hashSize != hashSize) {
throw new EncryptedDocumentException("Unsupported hash algorithm: " +
keyData.getHashAlgorithm() + " @ " + hashSize + " bytes");
}
int saltLength = keyData.getSaltSize();
setKeySalt(keyData.getSaltValue());
if (getKeySalt().length != saltLength) {
throw new EncryptedDocumentException("Invalid salt length");
}
CTDataIntegrity di = ed.getEncryption().getDataIntegrity();
setEncryptedHmacKey(di.getEncryptedHmacKey());
setEncryptedHmacValue(di.getEncryptedHmacValue());
}
public AgileEncryptionHeader(CipherAlgorithm algorithm, HashAlgorithm hashAlgorithm, int keyBits, int blockSize, ChainingMode chainingMode) {
setCipherAlgorithm(algorithm);
setHashAlgorithm(hashAlgorithm);
setKeySize(keyBits);
setBlockSize(blockSize);
setChainingMode(chainingMode);
}
// make method visible for this package
protected void setKeySalt(byte salt[]) {
if (salt == null || salt.length != getBlockSize()) {
throw new EncryptedDocumentException("invalid verifier salt");
}
super.setKeySalt(salt);
}
public byte[] getEncryptedHmacKey() {
return encryptedHmacKey;
}
protected void setEncryptedHmacKey(byte[] encryptedHmacKey) {
this.encryptedHmacKey = encryptedHmacKey;
}
public byte[] getEncryptedHmacValue() {
return encryptedHmacValue;
}
protected void setEncryptedHmacValue(byte[] encryptedHmacValue) {
this.encryptedHmacValue = encryptedHmacValue;
}
}

View File

@ -0,0 +1,111 @@
/* ====================================================================
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.agile;
import java.io.IOException;
import org.apache.poi.EncryptedDocumentException;
import org.apache.poi.poifs.crypt.ChainingMode;
import org.apache.poi.poifs.crypt.CipherAlgorithm;
import org.apache.poi.poifs.crypt.EncryptionInfo;
import org.apache.poi.poifs.crypt.EncryptionInfoBuilder;
import org.apache.poi.poifs.crypt.HashAlgorithm;
import org.apache.poi.poifs.filesystem.DocumentInputStream;
public class AgileEncryptionInfoBuilder implements EncryptionInfoBuilder {
EncryptionInfo info;
AgileEncryptionHeader header;
AgileEncryptionVerifier verifier;
AgileDecryptor decryptor;
AgileEncryptor encryptor;
public void initialize(EncryptionInfo info, DocumentInputStream dis) throws IOException {
this.info = info;
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 AgileEncryptionHeader(descriptor);
verifier = new AgileEncryptionVerifier(descriptor);
if (info.getVersionMajor() == 4 && info.getVersionMinor() == 4) {
decryptor = new AgileDecryptor(this);
}
}
public void initialize(EncryptionInfo info, CipherAlgorithm cipherAlgorithm, HashAlgorithm hashAlgorithm, int keyBits, int blockSize, ChainingMode chainingMode) {
this.info = info;
if (cipherAlgorithm == null) {
cipherAlgorithm = CipherAlgorithm.aes128;
}
if (cipherAlgorithm == CipherAlgorithm.rc4) {
throw new EncryptedDocumentException("RC4 must not be used with agile encryption.");
}
if (hashAlgorithm == null) {
hashAlgorithm = HashAlgorithm.sha1;
}
if (chainingMode == null) {
chainingMode = ChainingMode.cbc;
}
if (!(chainingMode == ChainingMode.cbc || chainingMode == ChainingMode.cfb)) {
throw new EncryptedDocumentException("Agile encryption only supports CBC/CFB chaining.");
}
if (keyBits == -1) {
keyBits = cipherAlgorithm.defaultKeySize;
}
if (blockSize == -1) {
blockSize = cipherAlgorithm.blockSize;
}
boolean found = false;
for (int ks : cipherAlgorithm.allowedKeySize) {
found |= (ks == keyBits);
}
if (!found) {
throw new EncryptedDocumentException("KeySize "+keyBits+" not allowed for Cipher "+cipherAlgorithm.toString());
}
header = new AgileEncryptionHeader(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode);
verifier = new AgileEncryptionVerifier(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode);
decryptor = new AgileDecryptor(this);
encryptor = new AgileEncryptor(this);
}
public AgileEncryptionHeader getHeader() {
return header;
}
public AgileEncryptionVerifier getVerifier() {
return verifier;
}
public AgileDecryptor getDecryptor() {
return decryptor;
}
public AgileEncryptor getEncryptor() {
return encryptor;
}
protected EncryptionInfo getInfo() {
return info;
}
}

View File

@ -0,0 +1,164 @@
/* ====================================================================
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.agile;
import java.io.ByteArrayInputStream;
import java.security.GeneralSecurityException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.apache.poi.EncryptedDocumentException;
import org.apache.poi.poifs.crypt.ChainingMode;
import org.apache.poi.poifs.crypt.CipherAlgorithm;
import org.apache.poi.poifs.crypt.EncryptionVerifier;
import org.apache.poi.poifs.crypt.HashAlgorithm;
import org.apache.xmlbeans.XmlException;
import com.microsoft.schemas.office.x2006.encryption.CTKeyEncryptor;
import com.microsoft.schemas.office.x2006.encryption.EncryptionDocument;
import com.microsoft.schemas.office.x2006.encryption.STCipherChaining;
import com.microsoft.schemas.office.x2006.keyEncryptor.certificate.CTCertificateKeyEncryptor;
import com.microsoft.schemas.office.x2006.keyEncryptor.password.CTPasswordKeyEncryptor;
/**
* Used when checking if a key is valid for a document
*/
public class AgileEncryptionVerifier extends EncryptionVerifier {
public static class AgileCertificateEntry {
X509Certificate x509;
byte encryptedKey[];
byte certVerifier[];
}
private List<AgileCertificateEntry> certList = new ArrayList<AgileCertificateEntry>();
public AgileEncryptionVerifier(String descriptor) {
EncryptionDocument ed;
try {
ed = EncryptionDocument.Factory.parse(descriptor);
} catch (XmlException e) {
throw new EncryptedDocumentException("Unable to parse encryption descriptor", e);
}
Iterator<CTKeyEncryptor> encList = ed.getEncryption().getKeyEncryptors().getKeyEncryptorList().iterator();
CTPasswordKeyEncryptor keyData;
try {
keyData = encList.next().getEncryptedPasswordKey();
if (keyData == null) {
throw new NullPointerException("encryptedKey not set");
}
} catch (Exception e) {
throw new EncryptedDocumentException("Unable to parse keyData", e);
}
int keyBits = (int)keyData.getKeyBits();
CipherAlgorithm ca = CipherAlgorithm.fromXmlId(keyData.getCipherAlgorithm().toString(), keyBits);
setCipherAlgorithm(ca);
int hashSize = keyData.getHashSize();
HashAlgorithm ha = HashAlgorithm.fromEcmaId(keyData.getHashAlgorithm().toString());
setHashAlgorithm(ha);
if (getHashAlgorithm().hashSize != hashSize) {
throw new EncryptedDocumentException("Unsupported hash algorithm: " +
keyData.getHashAlgorithm() + " @ " + hashSize + " bytes");
}
setSpinCount(keyData.getSpinCount());
setEncryptedVerifier(keyData.getEncryptedVerifierHashInput());
setSalt(keyData.getSaltValue());
setEncryptedKey(keyData.getEncryptedKeyValue());
setEncryptedVerifierHash(keyData.getEncryptedVerifierHashValue());
int saltSize = keyData.getSaltSize();
if (saltSize != getSalt().length)
throw new EncryptedDocumentException("Invalid salt size");
switch (keyData.getCipherChaining().intValue()) {
case STCipherChaining.INT_CHAINING_MODE_CBC:
setChainingMode(ChainingMode.cbc);
break;
case STCipherChaining.INT_CHAINING_MODE_CFB:
setChainingMode(ChainingMode.cfb);
break;
default:
throw new EncryptedDocumentException("Unsupported chaining mode - "+keyData.getCipherChaining().toString());
}
if (!encList.hasNext()) return;
try {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
while (encList.hasNext()) {
CTCertificateKeyEncryptor certKey = encList.next().getEncryptedCertificateKey();
AgileCertificateEntry ace = new AgileCertificateEntry();
ace.certVerifier = certKey.getCertVerifier();
ace.encryptedKey = certKey.getEncryptedKeyValue();
ace.x509 = (X509Certificate)cf.generateCertificate(new ByteArrayInputStream(certKey.getX509Certificate()));
certList.add(ace);
}
} catch (GeneralSecurityException e) {
throw new EncryptedDocumentException("can't parse X509 certificate", e);
}
}
public AgileEncryptionVerifier(CipherAlgorithm cipherAlgorithm, HashAlgorithm hashAlgorithm, int keyBits, int blockSize, ChainingMode chainingMode) {
setCipherAlgorithm(cipherAlgorithm);
setHashAlgorithm(hashAlgorithm);
setChainingMode(chainingMode);
setSpinCount(100000); // TODO: use parameter
}
protected void setSalt(byte salt[]) {
if (salt == null || salt.length != getCipherAlgorithm().blockSize) {
throw new EncryptedDocumentException("invalid verifier salt");
}
super.setSalt(salt);
}
// make method visible for this package
protected void setEncryptedVerifier(byte encryptedVerifier[]) {
super.setEncryptedVerifier(encryptedVerifier);
}
// make method visible for this package
protected void setEncryptedVerifierHash(byte encryptedVerifierHash[]) {
super.setEncryptedVerifierHash(encryptedVerifierHash);
}
// make method visible for this package
protected void setEncryptedKey(byte[] encryptedKey) {
super.setEncryptedKey(encryptedKey);
}
public void addCertificate(X509Certificate x509) {
AgileCertificateEntry ace = new AgileCertificateEntry();
ace.x509 = x509;
certList.add(ace);
}
public List<AgileCertificateEntry> getCertificates() {
return certList;
}
}

View File

@ -0,0 +1,511 @@
/* ====================================================================
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.agile;
import static org.apache.poi.poifs.crypt.CryptoFunctions.generateIv;
import static org.apache.poi.poifs.crypt.CryptoFunctions.getBlock0;
import static org.apache.poi.poifs.crypt.CryptoFunctions.getCipher;
import static org.apache.poi.poifs.crypt.CryptoFunctions.getMessageDigest;
import static org.apache.poi.poifs.crypt.CryptoFunctions.hashPassword;
import static org.apache.poi.poifs.crypt.agile.AgileDecryptor.getNextBlockSize;
import static org.apache.poi.poifs.crypt.agile.AgileDecryptor.hashInput;
import static org.apache.poi.poifs.crypt.agile.AgileDecryptor.kCryptoKeyBlock;
import static org.apache.poi.poifs.crypt.agile.AgileDecryptor.kHashedVerifierBlock;
import static org.apache.poi.poifs.crypt.agile.AgileDecryptor.kIntegrityKeyBlock;
import static org.apache.poi.poifs.crypt.agile.AgileDecryptor.kIntegrityValueBlock;
import static org.apache.poi.poifs.crypt.agile.AgileDecryptor.kVerifierInputBlock;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.security.cert.CertificateEncodingException;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.apache.poi.EncryptedDocumentException;
import org.apache.poi.poifs.crypt.CryptoFunctions;
import org.apache.poi.poifs.crypt.DataSpaceMapUtils;
import org.apache.poi.poifs.crypt.EncryptionHeader;
import org.apache.poi.poifs.crypt.EncryptionInfo;
import org.apache.poi.poifs.crypt.Encryptor;
import org.apache.poi.poifs.crypt.HashAlgorithm;
import org.apache.poi.poifs.crypt.agile.AgileEncryptionVerifier.AgileCertificateEntry;
import org.apache.poi.poifs.filesystem.DirectoryNode;
import org.apache.poi.poifs.filesystem.POIFSWriterEvent;
import org.apache.poi.poifs.filesystem.POIFSWriterListener;
import org.apache.poi.util.IOUtils;
import org.apache.poi.util.LittleEndian;
import org.apache.poi.util.LittleEndianByteArrayOutputStream;
import org.apache.poi.util.LittleEndianConsts;
import org.apache.poi.util.LittleEndianOutputStream;
import org.apache.poi.util.TempFile;
import org.apache.xmlbeans.XmlOptions;
import com.microsoft.schemas.office.x2006.encryption.CTDataIntegrity;
import com.microsoft.schemas.office.x2006.encryption.CTEncryption;
import com.microsoft.schemas.office.x2006.encryption.CTKeyData;
import com.microsoft.schemas.office.x2006.encryption.CTKeyEncryptor;
import com.microsoft.schemas.office.x2006.encryption.CTKeyEncryptors;
import com.microsoft.schemas.office.x2006.encryption.EncryptionDocument;
import com.microsoft.schemas.office.x2006.encryption.STCipherAlgorithm;
import com.microsoft.schemas.office.x2006.encryption.STCipherChaining;
import com.microsoft.schemas.office.x2006.encryption.STHashAlgorithm;
import com.microsoft.schemas.office.x2006.keyEncryptor.certificate.CTCertificateKeyEncryptor;
import com.microsoft.schemas.office.x2006.keyEncryptor.password.CTPasswordKeyEncryptor;
public class AgileEncryptor extends Encryptor {
private final AgileEncryptionInfoBuilder builder;
@SuppressWarnings("unused")
private byte integritySalt[];
private Mac integrityMD;
private byte pwHash[];
protected AgileEncryptor(AgileEncryptionInfoBuilder builder) {
this.builder = builder;
}
public void confirmPassword(String password) {
// see [MS-OFFCRYPTO] - 2.3.3 EncryptionVerifier
Random r = new SecureRandom();
int blockSize = builder.getHeader().getBlockSize();
int keySize = builder.getHeader().getKeySize()/8;
int hashSize = builder.getHeader().getHashAlgorithmEx().hashSize;
byte[] verifierSalt = new byte[blockSize]
, verifier = new byte[blockSize]
, keySalt = new byte[blockSize]
, keySpec = new byte[keySize]
, integritySalt = new byte[hashSize];
r.nextBytes(verifierSalt); // blocksize
r.nextBytes(verifier); // blocksize
r.nextBytes(keySalt); // blocksize
r.nextBytes(keySpec); // keysize
r.nextBytes(integritySalt); // hashsize
confirmPassword(password, keySpec, keySalt, verifierSalt, verifier, integritySalt);
}
public void confirmPassword(String password, byte keySpec[], byte keySalt[], byte verifier[], byte verifierSalt[], byte integritySalt[]) {
AgileEncryptionVerifier ver = builder.getVerifier();
ver.setSalt(verifierSalt);
AgileEncryptionHeader header = builder.getHeader();
header.setKeySalt(keySalt);
HashAlgorithm hashAlgo = ver.getHashAlgorithm();
int blockSize = header.getBlockSize();
pwHash = hashPassword(password, hashAlgo, verifierSalt, ver.getSpinCount());
/**
* encryptedVerifierHashInput: This attribute MUST be generated by using the following steps:
* 1. Generate a random array of bytes with the number of bytes used specified by the saltSize
* attribute.
* 2. Generate an encryption key as specified in section 2.3.4.11 by using the user-supplied password,
* the binary byte array used to create the saltValue attribute, and a blockKey byte array
* consisting of the following bytes: 0xfe, 0xa7, 0xd2, 0x76, 0x3b, 0x4b, 0x9e, and 0x79.
* 3. Encrypt the random array of bytes generated in step 1 by using the binary form of the saltValue
* attribute as an initialization vector as specified in section 2.3.4.12. If the array of bytes is not an
* integral multiple of blockSize bytes, pad the array with 0x00 to the next integral multiple of
* blockSize bytes.
* 4. Use base64 to encode the result of step 3.
*/
byte encryptedVerifier[] = hashInput(builder, pwHash, kVerifierInputBlock, verifier, Cipher.ENCRYPT_MODE);
ver.setEncryptedVerifier(encryptedVerifier);
/**
* encryptedVerifierHashValue: This attribute MUST be generated by using the following steps:
* 1. Obtain the hash value of the random array of bytes generated in step 1 of the steps for
* encryptedVerifierHashInput.
* 2. Generate an encryption key as specified in section 2.3.4.11 by using the user-supplied password,
* the binary byte array used to create the saltValue attribute, and a blockKey byte array
* consisting of the following bytes: 0xd7, 0xaa, 0x0f, 0x6d, 0x30, 0x61, 0x34, and 0x4e.
* 3. Encrypt the hash value obtained in step 1 by using the binary form of the saltValue attribute as
* an initialization vector as specified in section 2.3.4.12. If hashSize is not an integral multiple of
* blockSize bytes, pad the hash value with 0x00 to an integral multiple of blockSize bytes.
* 4. Use base64 to encode the result of step 3.
*/
MessageDigest hashMD = getMessageDigest(hashAlgo);
byte[] hashedVerifier = hashMD.digest(verifier);
byte encryptedVerifierHash[] = hashInput(builder, pwHash, kHashedVerifierBlock, hashedVerifier, Cipher.ENCRYPT_MODE);
ver.setEncryptedVerifierHash(encryptedVerifierHash);
/**
* encryptedKeyValue: This attribute MUST be generated by using the following steps:
* 1. Generate a random array of bytes that is the same size as specified by the
* Encryptor.KeyData.keyBits attribute of the parent element.
* 2. Generate an encryption key as specified in section 2.3.4.11, using the user-supplied password,
* the binary byte array used to create the saltValue attribute, and a blockKey byte array
* consisting of the following bytes: 0x14, 0x6e, 0x0b, 0xe7, 0xab, 0xac, 0xd0, and 0xd6.
* 3. Encrypt the random array of bytes generated in step 1 by using the binary form of the saltValue
* attribute as an initialization vector as specified in section 2.3.4.12. If the array of bytes is not an
* integral multiple of blockSize bytes, pad the array with 0x00 to an integral multiple of
* blockSize bytes.
* 4. Use base64 to encode the result of step 3.
*/
byte encryptedKey[] = hashInput(builder, pwHash, kCryptoKeyBlock, keySpec, Cipher.ENCRYPT_MODE);
ver.setEncryptedKey(encryptedKey);
SecretKey secretKey = new SecretKeySpec(keySpec, ver.getCipherAlgorithm().jceId);
setSecretKey(secretKey);
/*
* 2.3.4.14 DataIntegrity Generation (Agile Encryption)
*
* The DataIntegrity element contained within an Encryption element MUST be generated by using
* the following steps:
* 1. Obtain the intermediate key by decrypting the encryptedKeyValue from a KeyEncryptor
* contained within the KeyEncryptors sequence. Use this key for encryption operations in the
* remaining steps of this section.
* 2. Generate a random array of bytes, known as Salt, of the same length as the value of the
* KeyData.hashSize attribute.
* 3. Encrypt the random array of bytes generated in step 2 by using the binary form of the
* KeyData.saltValue attribute and a blockKey byte array consisting of the following bytes:
* 0x5f, 0xb2, 0xad, 0x01, 0x0c, 0xb9, 0xe1, and 0xf6 used to form an initialization vector as
* specified in section 2.3.4.12. If the array of bytes is not an integral multiple of blockSize
* bytes, pad the array with 0x00 to the next integral multiple of blockSize bytes.
* 4. Assign the encryptedHmacKey attribute to the base64-encoded form of the result of step 3.
* 5. Generate an HMAC, as specified in [RFC2104], of the encrypted form of the data (message),
* which the DataIntegrity element will verify by using the Salt generated in step 2 as the key.
* Note that the entire EncryptedPackage stream (1), including the StreamSize field, MUST be
* used as the message.
* 6. Encrypt the HMAC as in step 3 by using a blockKey byte array consisting of the following bytes:
* 0xa0, 0x67, 0x7f, 0x02, 0xb2, 0x2c, 0x84, and 0x33.
* 7. Assign the encryptedHmacValue attribute to the base64-encoded form of the result of step 6.
*/
this.integritySalt = integritySalt;
try {
byte vec[] = CryptoFunctions.generateIv(hashAlgo, header.getKeySalt(), kIntegrityKeyBlock, header.getBlockSize());
Cipher cipher = getCipher(secretKey, ver.getCipherAlgorithm(), ver.getChainingMode(), vec, Cipher.ENCRYPT_MODE);
byte filledSalt[] = getBlock0(integritySalt, getNextBlockSize(integritySalt.length, blockSize));
byte encryptedHmacKey[] = cipher.doFinal(filledSalt);
header.setEncryptedHmacKey(encryptedHmacKey);
this.integrityMD = CryptoFunctions.getMac(hashAlgo);
this.integrityMD.init(new SecretKeySpec(integritySalt, hashAlgo.jceHmacId));
cipher = Cipher.getInstance("RSA");
for (AgileCertificateEntry ace : ver.getCertificates()) {
cipher.init(Cipher.ENCRYPT_MODE, ace.x509.getPublicKey());
ace.encryptedKey = cipher.doFinal(getSecretKey().getEncoded());
Mac x509Hmac = CryptoFunctions.getMac(hashAlgo);
x509Hmac.init(getSecretKey());
ace.certVerifier = x509Hmac.doFinal(ace.x509.getEncoded());
}
} catch (GeneralSecurityException e) {
throw new EncryptedDocumentException(e);
}
}
public OutputStream getDataStream(DirectoryNode dir)
throws IOException, GeneralSecurityException {
// TODO: initialize headers
OutputStream countStream = new ChunkedCipherOutputStream(dir);
return countStream;
}
/**
* 2.3.4.15 Data Encryption (Agile Encryption)
*
* The EncryptedPackage stream (1) MUST be encrypted in 4096-byte segments to facilitate nearly
* random access while allowing CBC modes to be used in the encryption process.
* The initialization vector for the encryption process MUST be obtained by using the zero-based
* segment number as a blockKey and the binary form of the KeyData.saltValue as specified in
* section 2.3.4.12. The block number MUST be represented as a 32-bit unsigned integer.
* Data blocks MUST then be encrypted by using the initialization vector and the intermediate key
* obtained by decrypting the encryptedKeyValue from a KeyEncryptor contained within the
* KeyEncryptors sequence as specified in section 2.3.4.10. The final data block MUST be padded to
* the next integral multiple of the KeyData.blockSize value. Any padding bytes can be used. Note
* that the StreamSize field of the EncryptedPackage field specifies the number of bytes of
* unencrypted data as specified in section 2.3.4.4.
*/
private class ChunkedCipherOutputStream extends FilterOutputStream implements POIFSWriterListener {
private long _pos = 0;
private final byte[] _chunk = new byte[4096];
private Cipher _cipher;
private final File fileOut;
protected final DirectoryNode dir;
public ChunkedCipherOutputStream(DirectoryNode dir) throws IOException {
super(null);
fileOut = TempFile.createTempFile("encrypted_package", "crypt");
this.out = new FileOutputStream(fileOut);
this.dir = dir;
EncryptionHeader header = builder.getHeader();
_cipher = getCipher(getSecretKey(), header.getCipherAlgorithm(), header.getChainingMode(), null, Cipher.ENCRYPT_MODE);
}
public void write(int b) throws IOException {
write(new byte[]{(byte)b});
}
public void write(byte[] b) throws IOException {
write(b, 0, b.length);
}
public void write(byte[] b, int off, int len)
throws IOException {
if (len == 0) return;
if (len < 0 || b.length < off+len) {
throw new IOException("not enough bytes in your input buffer");
}
while (len > 0) {
int posInChunk = (int)(_pos & 0xfff);
int nextLen = Math.min(4096-posInChunk, len);
System.arraycopy(b, off, _chunk, posInChunk, nextLen);
_pos += nextLen;
off += nextLen;
len -= nextLen;
if ((_pos & 0xfff) == 0) {
writeChunk();
}
}
}
private void writeChunk() throws IOException {
EncryptionHeader header = builder.getHeader();
int blockSize = header.getBlockSize();
int posInChunk = (int)(_pos & 0xfff);
// normally posInChunk is 0, i.e. on the next chunk (-> index-1)
// but if called on close(), posInChunk is somewhere within the chunk data
int index = (int)(_pos >> 12);
if (posInChunk==0) {
index--;
posInChunk = 4096;
} else {
// pad the last chunk
_cipher = getCipher(getSecretKey(), header.getCipherAlgorithm(), header.getChainingMode(), null, Cipher.ENCRYPT_MODE, "PKCS5Padding");
}
byte[] blockKey = new byte[4];
LittleEndian.putInt(blockKey, 0, index);
byte[] iv = generateIv(header.getHashAlgorithmEx(), header.getKeySalt(), blockKey, blockSize);
try {
_cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(), new IvParameterSpec(iv));
int ciLen = _cipher.doFinal(_chunk, 0, posInChunk, _chunk);
out.write(_chunk, 0, ciLen);
} catch (GeneralSecurityException e) {
throw new IOException(e);
}
}
public void close() throws IOException {
writeChunk();
super.close();
writeToPOIFS();
}
void writeToPOIFS() throws IOException {
DataSpaceMapUtils.addDefaultDataSpace(dir);
/**
* Generate an HMAC, as specified in [RFC2104], of the encrypted form of the data (message),
* which the DataIntegrity element will verify by using the Salt generated in step 2 as the key.
* Note that the entire EncryptedPackage stream (1), including the StreamSize field, MUST be
* used as the message.
*
* Encrypt the HMAC as in step 3 by using a blockKey byte array consisting of the following bytes:
* 0xa0, 0x67, 0x7f, 0x02, 0xb2, 0x2c, 0x84, and 0x33.
**/
byte buf[] = new byte[4096];
LittleEndian.putLong(buf, 0, _pos);
integrityMD.update(buf, 0, LittleEndianConsts.LONG_SIZE);
InputStream fis = new FileInputStream(fileOut);
for (int readBytes; (readBytes = fis.read(buf)) != -1; integrityMD.update(buf, 0, readBytes));
fis.close();
AgileEncryptionHeader header = builder.getHeader();
int blockSize = header.getBlockSize();
byte hmacValue[] = integrityMD.doFinal();
byte iv[] = CryptoFunctions.generateIv(header.getHashAlgorithmEx(), header.getKeySalt(), kIntegrityValueBlock, header.getBlockSize());
Cipher cipher = CryptoFunctions.getCipher(getSecretKey(), header.getCipherAlgorithm(), header.getChainingMode(), iv, Cipher.ENCRYPT_MODE);
try {
byte hmacValueFilled[] = getBlock0(hmacValue, getNextBlockSize(hmacValue.length, blockSize));
byte encryptedHmacValue[] = cipher.doFinal(hmacValueFilled);
header.setEncryptedHmacValue(encryptedHmacValue);
} catch (GeneralSecurityException e) {
throw new EncryptedDocumentException(e);
}
createEncryptionInfoEntry(dir);
int oleStreamSize = (int)(fileOut.length()+LittleEndianConsts.LONG_SIZE);
dir.createDocument("EncryptedPackage", oleStreamSize, this);
// TODO: any properties???
}
public void processPOIFSWriterEvent(POIFSWriterEvent event) {
try {
LittleEndianOutputStream leos = new LittleEndianOutputStream(event.getStream());
// StreamSize (8 bytes): An unsigned integer that specifies the number of bytes used by data
// encrypted within the EncryptedData field, not including the size of the StreamSize field.
// Note that the actual size of the \EncryptedPackage stream (1) can be larger than this
// value, depending on the block size of the chosen encryption algorithm
leos.writeLong(_pos);
FileInputStream fis = new FileInputStream(fileOut);
IOUtils.copy(fis, leos);
fis.close();
fileOut.delete();
leos.close();
} catch (IOException e) {
throw new EncryptedDocumentException(e);
}
}
}
protected void createEncryptionInfoEntry(DirectoryNode dir) throws IOException {
AgileEncryptionVerifier ver = builder.getVerifier();
AgileEncryptionHeader header = builder.getHeader();
EncryptionDocument ed = EncryptionDocument.Factory.newInstance();
CTEncryption edRoot = ed.addNewEncryption();
CTKeyData keyData = edRoot.addNewKeyData();
CTKeyEncryptors keyEncList = edRoot.addNewKeyEncryptors();
CTKeyEncryptor keyEnc = keyEncList.addNewKeyEncryptor();
keyEnc.setUri(CTKeyEncryptor.Uri.HTTP_SCHEMAS_MICROSOFT_COM_OFFICE_2006_KEY_ENCRYPTOR_PASSWORD);
CTPasswordKeyEncryptor keyPass = keyEnc.addNewEncryptedPasswordKey();
keyPass.setSpinCount(ver.getSpinCount());
keyData.setSaltSize(header.getBlockSize());
keyPass.setSaltSize(header.getBlockSize());
keyData.setBlockSize(header.getBlockSize());
keyPass.setBlockSize(header.getBlockSize());
keyData.setKeyBits(header.getKeySize());
keyPass.setKeyBits(header.getKeySize());
HashAlgorithm hashAlgo = header.getHashAlgorithmEx();
keyData.setHashSize(hashAlgo.hashSize);
keyPass.setHashSize(hashAlgo.hashSize);
STCipherAlgorithm.Enum xmlCipherAlgo = STCipherAlgorithm.Enum.forString(header.getCipherAlgorithm().xmlId);
if (xmlCipherAlgo == null) {
throw new EncryptedDocumentException("CipherAlgorithm "+header.getCipherAlgorithm()+" not supported.");
}
keyData.setCipherAlgorithm(xmlCipherAlgo);
keyPass.setCipherAlgorithm(xmlCipherAlgo);
switch (header.getChainingMode()) {
case cbc:
keyData.setCipherChaining(STCipherChaining.CHAINING_MODE_CBC);
keyPass.setCipherChaining(STCipherChaining.CHAINING_MODE_CBC);
break;
case cfb:
keyData.setCipherChaining(STCipherChaining.CHAINING_MODE_CFB);
keyPass.setCipherChaining(STCipherChaining.CHAINING_MODE_CFB);
break;
default:
throw new EncryptedDocumentException("ChainingMode "+header.getChainingMode()+" not supported.");
}
STHashAlgorithm.Enum xmlHashAlgo = STHashAlgorithm.Enum.forString(hashAlgo.ecmaString);
if (xmlHashAlgo == null) {
throw new EncryptedDocumentException("HashAlgorithm "+hashAlgo+" not supported.");
}
keyData.setHashAlgorithm(xmlHashAlgo);
keyPass.setHashAlgorithm(xmlHashAlgo);
keyData.setSaltValue(header.getKeySalt());
keyPass.setSaltValue(ver.getSalt());
keyPass.setEncryptedVerifierHashInput(ver.getEncryptedVerifier());
keyPass.setEncryptedVerifierHashValue(ver.getEncryptedVerifierHash());
keyPass.setEncryptedKeyValue(ver.getEncryptedKey());
CTDataIntegrity hmacData = edRoot.addNewDataIntegrity();
hmacData.setEncryptedHmacKey(header.getEncryptedHmacKey());
hmacData.setEncryptedHmacValue(header.getEncryptedHmacValue());
for (AgileCertificateEntry ace : ver.getCertificates()) {
keyEnc = keyEncList.addNewKeyEncryptor();
keyEnc.setUri(CTKeyEncryptor.Uri.HTTP_SCHEMAS_MICROSOFT_COM_OFFICE_2006_KEY_ENCRYPTOR_CERTIFICATE);
CTCertificateKeyEncryptor certData = keyEnc.addNewEncryptedCertificateKey();
try {
certData.setX509Certificate(ace.x509.getEncoded());
} catch (CertificateEncodingException e) {
throw new EncryptedDocumentException(e);
}
certData.setEncryptedKeyValue(ace.encryptedKey);
certData.setCertVerifier(ace.certVerifier);
}
XmlOptions xo = new XmlOptions();
xo.setCharacterEncoding("UTF-8");
Map<String,String> nsMap = new HashMap<String,String>();
nsMap.put("http://schemas.microsoft.com/office/2006/keyEncryptor/password","p");
nsMap.put("http://schemas.microsoft.com/office/2006/keyEncryptor/certificate", "c");
nsMap.put("http://schemas.microsoft.com/office/2006/encryption","");
xo.setSaveSuggestedPrefixes(nsMap);
xo.setSaveNamespacesFirst();
xo.setSaveAggressiveNamespaces();
// setting standalone doesn't work with xmlbeans-2.3
xo.setSaveNoXmlDecl();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
bos.write("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n".getBytes("UTF-8"));
ed.save(bos, xo);
final byte buf[] = new byte[5000];
LittleEndianByteArrayOutputStream leos = new LittleEndianByteArrayOutputStream(buf, 0);
EncryptionInfo info = builder.getInfo();
// EncryptionVersionInfo (4 bytes): A Version structure (section 2.1.4), where
// Version.vMajor MUST be 0x0004 and Version.vMinor MUST be 0x0004
leos.writeShort(info.getVersionMajor());
leos.writeShort(info.getVersionMinor());
// Reserved (4 bytes): A value that MUST be 0x00000040
leos.writeInt(0x40);
leos.write(bos.toByteArray());
dir.createDocument("EncryptionInfo", leos.getWriteIndex(), new POIFSWriterListener() {
public void processPOIFSWriterEvent(POIFSWriterEvent event) {
try {
event.getStream().write(buf, 0, event.getLimit());
} catch (IOException e) {
throw new EncryptedDocumentException(e);
}
}
});
}
}

View File

@ -23,6 +23,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
@ -32,10 +33,13 @@ import java.util.Vector;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import junit.framework.JUnit4TestAdapter;
import junit.framework.TestCase;
import junit.framework.TestSuite;
import junit.textui.TestRunner;
import org.junit.Test;
/**
* Build a 'lite' version of the ooxml-schemas.jar
*
@ -103,9 +107,18 @@ public final class OOXMLLite {
String cls = arg.replace(".class", "");
try {
@SuppressWarnings("unchecked")
Class<? extends TestCase> test = (Class<? extends TestCase>) Class.forName(cls);
suite.addTestSuite(test);
Class<?> testclass = Class.forName(cls);
boolean isTest = TestCase.class.isAssignableFrom(testclass);
if (!isTest) {
for (Method m : testclass.getDeclaredMethods()) {
isTest = m.isAnnotationPresent(Test.class);
if (isTest) break;
}
}
if (isTest) {
suite.addTest(new JUnit4TestAdapter(testclass));
}
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
@ -181,8 +194,12 @@ public final class OOXMLLite {
Vector<Class<?>> classes = (Vector<Class<?>>) _classes.get(appLoader);
Map<String, Class<?>> map = new HashMap<String, Class<?>>();
for (Class<?> cls : classes) {
String jar = cls.getProtectionDomain().getCodeSource().getLocation().toString();
if(jar.indexOf(ptrn) != -1) map.put(cls.getName(), cls);
try {
String jar = cls.getProtectionDomain().getCodeSource().getLocation().toString();
if(jar.indexOf(ptrn) != -1) map.put(cls.getName(), cls);
} catch (NullPointerException e) {
continue;
}
}
return map;
} catch (IllegalAccessException e) {

View File

@ -0,0 +1,39 @@
<?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.
====================================================================
-->
<xs:schema xmlns="http://schemas.microsoft.com/office/2006/keyEncryptor/certificate" xmlns:e="http://schemas.microsoft.com/office/2006/encryption" xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://schemas.microsoft.com/office/2006/keyEncryptor/certificate" elementFormDefault="qualified" attributeFormDefault="unqualified">
<xs:import namespace="http://schemas.microsoft.com/office/2006/encryption" schemaLocation="encryptionInfo.xsd"/>
<xs:simpleType name="ST_PasswordKeyEncryptorUri">
<xs:restriction base="xs:token">
<xs:enumeration value="http://schemas.microsoft.com/office/2006/keyEncryptor/certificate"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="CT_CertificateKeyEncryptor">
<xs:attribute name="encryptedKeyValue" type="xs:base64Binary" use="required">
<xs:annotation><xs:documentation>A base64-encoded value that specifies the encrypted form of the intermediate key, which is encrypted with the public key contained within the X509Certificate attribute.</xs:documentation></xs:annotation>
</xs:attribute>
<xs:attribute name="X509Certificate" type="xs:base64Binary" use="required">
<xs:annotation><xs:documentation>A base64-encoded value that specifies a DER-encoded X.509 certificate (1) used to encrypt the intermediate key. The certificate (1) MUST contain only the public portion of the public-private key pair.</xs:documentation></xs:annotation>
</xs:attribute>
<xs:attribute name="certVerifier" type="xs:base64Binary" use="required">
<xs:annotation><xs:documentation>A base64-encoded value that specifies the HMAC of the binary data obtained by base64-decoding the X509Certificate attribute. The hashing algorithm used to derive the HMAC MUST be the hashing algorithm specified for the Encryption.keyData element. The secret key used to derive the HMAC MUST be the intermediate key. If the intermediate key is reset, any CertificateKeyEncryptor elements are also reset to contain the new intermediate key, except that the certVerifier attribute MUST match the value calculated using the current intermediate key, to verify that the CertificateKeyEncryptor element actually encrypted the current intermediate key. If a CertificateKeyEncryptor element does not have a correct certVerifier attribute, it MUST be discarded.</xs:documentation></xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:element name="encryptedKey" type="CT_CertificateKeyEncryptor"/>
</xs:schema>

View File

@ -0,0 +1,24 @@
<?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.
====================================================================
-->
<xb:config xmlns:xb="http://xml.apache.org/xmlbeans/2004/02/xbean/config" xmlns:c="http://schemas.microsoft.com/office/2006/keyEncryptor/certificate">
<xb:qname name="c:encryptedKey" javaname="EncryptedCertificateKey"/>
</xb:config>

View File

@ -0,0 +1,259 @@
<?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.
====================================================================
-->
<xs:schema xmlns="http://schemas.microsoft.com/office/2006/encryption" xmlns:p="http://schemas.microsoft.com/office/2006/keyEncryptor/password" xmlns:c="http://schemas.microsoft.com/office/2006/keyEncryptor/certificate" xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://schemas.microsoft.com/office/2006/encryption" elementFormDefault="qualified" attributeFormDefault="unqualified">
<xs:import namespace="http://schemas.microsoft.com/office/2006/keyEncryptor/password" schemaLocation="encryptionPassword.xsd"/>
<xs:import namespace="http://schemas.microsoft.com/office/2006/keyEncryptor/certificate" schemaLocation="encryptionCertificate.xsd"/>
<xs:simpleType name="ST_SaltSize">
<xs:annotation>
<xs:documentation>An unsigned integer that specifies the number of bytes used by a salt. It MUST be at least 1 and no greater than 65,536.</xs:documentation>
</xs:annotation>
<xs:restriction base="xs:unsignedInt">
<xs:minInclusive value="1"/>
<xs:maxInclusive value="65536"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="ST_BlockSize">
<xs:annotation>
<xs:documentation>An unsigned integer that specifies the number of bytes used to encrypt one block of data. It MUST be at least 2, no greater than 4096, and a multiple of 2.</xs:documentation>
</xs:annotation>
<xs:restriction base="xs:unsignedInt">
<xs:minInclusive value="2"/>
<xs:maxInclusive value="4096"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="ST_KeyBits">
<xs:annotation>
<xs:documentation>An unsigned integer that specifies the number of bits used by an encryption algorithm. It MUST be at least 8 and a multiple of 8.</xs:documentation>
</xs:annotation>
<xs:restriction base="xs:unsignedInt">
<xs:minInclusive value="8"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="ST_HashSize">
<xs:annotation>
<xs:documentation>An unsigned integer that specifies the number of bytes used by a hash value. It MUST be at least 1, no greater than 65,536, and the same number of bytes as the hash algorithm emits.</xs:documentation>
</xs:annotation>
<xs:restriction base="xs:unsignedInt">
<xs:minInclusive value="1"/>
<xs:maxInclusive value="65536"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="ST_SpinCount">
<xs:annotation>
<xs:documentation>An unsigned integer that specifies the number of times to iterate on a hash of a password. It MUST NOT be greater than 10,000,000.</xs:documentation>
</xs:annotation>
<xs:restriction base="xs:unsignedInt">
<xs:minInclusive value="0"/>
<xs:maxInclusive value="10000000"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="ST_CipherAlgorithm">
<xs:annotation>
<xs:appinfo>modified for poi - list is restricted to given list in [ms-offcrypto]</xs:appinfo>
<xs:documentation>A string that specifies the cipher algorithm. Values that are not defined MAY be used, and a compliant implementation is not required to support all defined values. Any algorithm that can be resolved by name by the underlying operating system can be used for hashing or encryption. Only block algorithms are supported for encryption. AES-128 is the default encryption algorithm, and SHA-1 is the default hashing algorithm if no other algorithms have been configured.</xs:documentation>
</xs:annotation>
<xs:restriction base="xs:token">
<xs:enumeration value="AES">
<xs:annotation>
<xs:documentation>MUST conform to the AES algorithm.</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="RC2">
<xs:annotation>
<xs:documentation>MUST conform to the algorithm as specified in [RFC2268] (http://tools.ietf.org/html/rfc2268). The use of RC2 is not recommended. If RC2 is used with a key length of less than 128 bits, documents could interoperate incorrectly across different versions of Windows.</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="RC4">
<xs:annotation>
<xs:documentation>MUST NOT be used.</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="DES">
<xs:annotation>
<xs:documentation>MUST conform to the DES algorithm. The use of DES is not recommended. If DES is used, the key length specified in the KeyBits element is required to be set to 64 for 56-bit encryption, and the key decrypted from encryptedKeyValue of KeyEncryptor is required to include the DES parity bits.</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="DESX">
<xs:annotation>
<xs:documentation>MUST conform to the algorithm as specified in [DRAFT-DESX] (http://tools.ietf.org/html/draft-ietf-ipsec-ciph-desx-00). The use of DESX is not recommended. If DESX is used, documents could interoperate incorrectly across different versions of Windows.</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="3DES">
<xs:annotation>
<xs:documentation>MUST conform to the algorithm as specified in [RFC1851] (http://tools.ietf.org/html/rfc1851). If 3DES or 3DES_112 is used, the key length specified in the KeyBits element is required to be set to 192 for 168-bit encryption and 128 for 112-bit encryption, and the key decrypted from encryptedKeyValue of KeyEncryptor is required to include the DES parity bits.</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="3DES_112">
<xs:annotation>
<xs:documentation>see 3DES</xs:documentation>
</xs:annotation>
</xs:enumeration>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="ST_CipherChaining">
<xs:annotation>
<xs:documentation>A string that specifies the chaining mode used by CipherAlgorithm. For more details about chaining modes, see [BCMO800-38A] (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf).</xs:documentation>
</xs:annotation>
<xs:restriction base="xs:token">
<xs:enumeration value="ChainingModeCBC">
<xs:annotation>
<xs:documentation>block chaining (CBC)</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="ChainingModeCFB">
<xs:annotation>
<xs:documentation>Cipher feedback chaining (CFB), with an 8-bit window</xs:documentation>
</xs:annotation>
</xs:enumeration>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="ST_HashAlgorithm">
<xs:annotation>
<xs:appinfo>modified for poi - list is restricted to given list in [ms-offcrypto]</xs:appinfo>
<xs:documentation>A string specifying a hashing algorithm. Values that are not defined MAY be used, and a compliant implementation is not required to support all defined values.</xs:documentation>
</xs:annotation>
<xs:restriction base="xs:token">
<xs:enumeration value="SHA1">
<xs:annotation>
<xs:documentation>MUST conform to the algorithm as specified in [RFC4634] (http://tools.ietf.org/html/rfc4634).</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="SHA256">
<xs:annotation>
<xs:documentation>see SHA1</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="SHA384">
<xs:annotation>
<xs:documentation>see SHA1</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="SHA512">
<xs:annotation>
<xs:documentation>see SHA1</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="MD5">
<xs:annotation>
<xs:documentation>MUST conform to MD5.</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="MD4">
<xs:annotation>
<xs:documentation>MUST conform to the algorithm as specified in [RFC1320] (http://tools.ietf.org/html/rfc1320).</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="MD2">
<xs:annotation>
<xs:documentation>MUST conform to the algorithm as specified in [RFC1319] (http://tools.ietf.org/html/rfc1319).</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="RIPEMD-128">
<xs:annotation>
<xs:documentation>MUST conform to the hash functions specified in [ISO/IEC 10118]. (https://en.wikipedia.org/wiki/RIPEMD)</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="RIPEMD-160">
<xs:annotation>
<xs:documentation>see RIPEMD-128 (https://en.wikipedia.org/wiki/RIPEMD)</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="WHIRLPOOL">
<xs:annotation>
<xs:documentation>see RIPEMD-128 (https://en.wikipedia.org/wiki/ISO/IEC_10118-3)</xs:documentation>
</xs:annotation>
</xs:enumeration>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="CT_KeyData">
<xs:annotation>
<xs:documentation>A complex type that specifies the encryption used within this element. The saltValue attribute is a base64-encoded binary value that is randomly generated. The number of bytes required to decode the saltValue attribute MUST be equal to the value of the saltSize attribute.</xs:documentation>
</xs:annotation>
<xs:attribute name="saltSize" type="ST_SaltSize" use="required"/>
<xs:attribute name="blockSize" type="ST_BlockSize" use="required"/>
<xs:attribute name="keyBits" type="ST_KeyBits" use="required"/>
<xs:attribute name="hashSize" type="ST_HashSize" use="required"/>
<xs:attribute name="cipherAlgorithm" type="ST_CipherAlgorithm" use="required"/>
<xs:attribute name="cipherChaining" type="ST_CipherChaining" use="required"/>
<xs:attribute name="hashAlgorithm" type="ST_HashAlgorithm" use="required"/>
<xs:attribute name="saltValue" type="xs:base64Binary" use="required"/>
</xs:complexType>
<xs:complexType name="CT_DataIntegrity">
<xs:annotation>
<xs:documentation>A complex type that specifies data used to verify whether the encrypted data passes an integrity check. It MUST be generated using the method specified in section 2.3.4.14 (http://msdn.microsoft.com/en-us/library/dd924068(v=office.12).aspx).</xs:documentation>
</xs:annotation>
<xs:attribute name="encryptedHmacKey" type="xs:base64Binary" use="required">
<xs:annotation>
<xs:documentation>A base64-encoded value that specifies an encrypted key used in calculating the encryptedHmacValue.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="encryptedHmacValue" type="xs:base64Binary" use="required">
<xs:annotation>
<xs:documentation>A base64-encoded value that specifies an HMAC derived from encryptedHmacKey and the encrypted data.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="CT_KeyEncryptor">
<xs:annotation>
<xs:appinfo>modified for POI</xs:appinfo>
<xs:documentation>A complex type that specifies the parameters used to encrypt an intermediate key, which is used to perform the final encryption of the document. To ensure extensibility, arbitrary elements can be defined to encrypt the intermediate key. The intermediate key MUST be the same for all KeyEncryptor elements.</xs:documentation>
</xs:annotation>
<xs:choice>
<xs:element ref="p:encryptedKey"/>
<xs:element ref="c:encryptedKey"/>
</xs:choice>
<xs:attribute name="uri">
<xs:annotation>
<xs:appinfo>modified for POI</xs:appinfo>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:token">
<xs:enumeration value="http://schemas.microsoft.com/office/2006/keyEncryptor/password"/>
<xs:enumeration value="http://schemas.microsoft.com/office/2006/keyEncryptor/certificate"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
</xs:complexType>
<xs:complexType name="CT_KeyEncryptors">
<xs:annotation>
<xs:documentation>A sequence of KeyEncryptor elements. Exactly one KeyEncryptors element MUST be present, and the KeyEncryptors element MUST contain at least one KeyEncryptor.</xs:documentation>
</xs:annotation>
<xs:sequence>
<xs:element name="keyEncryptor" type="CT_KeyEncryptor" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="CT_Encryption">
<xs:sequence>
<xs:element name="keyData" type="CT_KeyData"/>
<xs:element name="dataIntegrity" type="CT_DataIntegrity">
<xs:annotation>
<xs:appinfo>modified for POI</xs:appinfo>
<xs:documentation>All ECMA-376 documents [ECMA-376] encrypted by Microsoft Office using agile encryption will have a DataIntegrity element present. The schema allows for a DataIntegrity element to not be present because the encryption schema can be used by applications that do not create ECMA-376 documents [ECMA-376].</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="keyEncryptors" type="CT_KeyEncryptors">
<xs:annotation>
<xs:documentation>The KeyEncryptor element, which MUST be used when encrypting password-protected agile encryption documents, is either a PasswordKeyEncryptor or a CertificateKeyEncryptor. Exactly one PasswordKeyEncryptor MUST be present. Zero or more CertificateKeyEncryptor elements are contained within the KeyEncryptors element.</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
</xs:complexType>
<xs:element name="encryption" type="CT_Encryption"/>
</xs:schema>

View File

@ -0,0 +1,25 @@
<?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.
====================================================================
-->
<xb:config xmlns:xb="http://xml.apache.org/xmlbeans/2004/02/xbean/config" xmlns:c="http://schemas.microsoft.com/office/2006/keyEncryptor/certificate" xmlns:p="http://schemas.microsoft.com/office/2006/keyEncryptor/password">
<xb:qname name="c:encryptedKey" javaname="EncryptedCertificateKey"/>
<xb:qname name="p:encryptedKey" javaname="EncryptedPasswordKey"/>
</xb:config>

View File

@ -0,0 +1,66 @@
<?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.
====================================================================
-->
<xs:schema xmlns="http://schemas.microsoft.com/office/2006/keyEncryptor/password" xmlns:e="http://schemas.microsoft.com/office/2006/encryption" xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://schemas.microsoft.com/office/2006/keyEncryptor/password" elementFormDefault="qualified" attributeFormDefault="unqualified">
<xs:import namespace="http://schemas.microsoft.com/office/2006/encryption" schemaLocation="encryptionInfo.xsd"/>
<xs:simpleType name="ST_PasswordKeyEncryptorUri">
<xs:restriction base="xs:token">
<xs:enumeration value="http://schemas.microsoft.com/office/2006/keyEncryptor/password"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="CT_PasswordKeyEncryptor">
<xs:attribute name="saltSize" type="e:ST_SaltSize" use="required">
<xs:annotation><xs:documentation>A SaltSize that specifies the size of the salt for a PasswordKeyEncryptor.</xs:documentation></xs:annotation>
</xs:attribute>
<xs:attribute name="blockSize" type="e:ST_BlockSize" use="required">
<xs:annotation><xs:documentation>A BlockSize that specifies the block size for a PasswordKeyEncryptor.</xs:documentation></xs:annotation>
</xs:attribute>
<xs:attribute name="keyBits" type="e:ST_KeyBits" use="required">
<xs:annotation><xs:documentation>A KeyBits that specifies the number of bits for a PasswordKeyEncryptor.</xs:documentation></xs:annotation>
</xs:attribute>
<xs:attribute name="hashSize" type="e:ST_HashSize" use="required">
<xs:annotation><xs:documentation>A HashSize that specifies the size of the binary form of the hash for a PasswordKeyEncryptor.</xs:documentation></xs:annotation>
</xs:attribute>
<xs:attribute name="cipherAlgorithm" type="e:ST_CipherAlgorithm" use="required">
<xs:annotation><xs:documentation>A CipherAlgorithm that specifies the cipher algorithm for a PasswordKeyEncryptor. The cipher algorithm specified MUST be the same as the cipher algorithm specified for the Encryption.keyData element.</xs:documentation></xs:annotation>
</xs:attribute>
<xs:attribute name="cipherChaining" type="e:ST_CipherChaining" use="required">
<xs:annotation><xs:documentation>A CipherChaining that specifies the cipher chaining mode for a PasswordKeyEncryptor.</xs:documentation></xs:annotation>
</xs:attribute>
<xs:attribute name="hashAlgorithm" type="e:ST_HashAlgorithm" use="required">
<xs:annotation><xs:documentation>A HashAlgorithm that specifies the hashing algorithm for a PasswordKeyEncryptor. The hashing algorithm specified MUST be the same as the hashing algorithm specified for the Encryption.keyData element.</xs:documentation></xs:annotation>
</xs:attribute>
<xs:attribute name="saltValue" type="xs:base64Binary" use="required">
<xs:annotation><xs:documentation>A base64-encoded binary byte array that specifies the salt value for a PasswordKeyEncryptor. The number of bytes required by the decoded form of this element MUST be saltSize.</xs:documentation></xs:annotation>
</xs:attribute>
<xs:attribute name="spinCount" type="e:ST_SpinCount" use="required">
<xs:annotation><xs:documentation>A SpinCount that specifies the spin count for a PasswordKeyEncryptor.</xs:documentation></xs:annotation>
</xs:attribute>
<xs:attribute name="encryptedVerifierHashInput" type="xs:base64Binary" use="required">
<xs:annotation><xs:documentation>A base64-encoded value that specifies the encrypted verifier hash input for a PasswordKeyEncryptor used in password verification.</xs:documentation></xs:annotation>
</xs:attribute>
<xs:attribute name="encryptedVerifierHashValue" type="xs:base64Binary" use="required">
<xs:annotation><xs:documentation>A base64-encoded value that specifies the encrypted verifier hash value for a PasswordKeyEncryptor used in password verification.</xs:documentation></xs:annotation>
</xs:attribute>
<xs:attribute name="encryptedKeyValue" type="xs:base64Binary" use="required">
<xs:annotation><xs:documentation>A base64-encoded value that specifies the encrypted form of the intermediate key.</xs:documentation></xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:element name="encryptedKey" type="CT_PasswordKeyEncryptor"/>
</xs:schema>

View File

@ -0,0 +1,24 @@
<?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.
====================================================================
-->
<xb:config xmlns:xb="http://xml.apache.org/xmlbeans/2004/02/xbean/config" xmlns:p="http://schemas.microsoft.com/office/2006/keyEncryptor/password">
<xb:qname name="p:encryptedKey" javaname="EncryptedPasswordKey"/>
</xb:config>

View File

@ -1,37 +1,36 @@
/* ====================================================================
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;
}
/* ====================================================================
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==================================================================== */
package org.apache.poi.poifs.crypt;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
/**
* Tests for org.apache.poi.poifs.crypt
*/
@RunWith(Suite.class)
@Suite.SuiteClasses({
TestEncryptionInfo.class
, TestDecryptor.class
, TestEncryptor.class
, TestAgileEncryptionParameters.class
, TestCertificateEncryption.class
})
public final class AllPOIFSCryptoTests {
}

View File

@ -0,0 +1,102 @@
/* ====================================================================
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 static org.hamcrest.core.IsEqual.equalTo;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.apache.poi.POIDataSamples;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
import org.apache.poi.util.IOUtils;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
@RunWith(Parameterized.class)
public class TestAgileEncryptionParameters {
static byte testData[];
@Parameter(value = 0)
public CipherAlgorithm ca;
@Parameter(value = 1)
public HashAlgorithm ha;
@Parameter(value = 2)
public ChainingMode cm;
@Parameters
public static Collection<Object[]> data() {
CipherAlgorithm caList[] = { CipherAlgorithm.aes128, CipherAlgorithm.aes192, CipherAlgorithm.aes256, CipherAlgorithm.rc2, CipherAlgorithm.des, CipherAlgorithm.des3 };
HashAlgorithm haList[] = { HashAlgorithm.sha1, HashAlgorithm.sha256, HashAlgorithm.sha384, HashAlgorithm.sha512, HashAlgorithm.md5 };
ChainingMode cmList[] = { ChainingMode.cbc, ChainingMode.cfb };
List<Object[]> data = new ArrayList<Object[]>();
for (CipherAlgorithm ca : caList) {
for (HashAlgorithm ha : haList) {
for (ChainingMode cm : cmList) {
data.add(new Object[]{ca,ha,cm});
}
}
}
return data;
}
@BeforeClass
public static void initTestData() throws Exception {
InputStream testFile = POIDataSamples.getDocumentInstance().openResourceAsStream("SampleDoc.docx");
testData = IOUtils.toByteArray(testFile);
testFile.close();
}
@Test
public void testAgileEncryptionModes() throws Exception {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
POIFSFileSystem fsEnc = new POIFSFileSystem();
EncryptionInfo infoEnc = new EncryptionInfo(fsEnc, EncryptionMode.agile, ca, ha, -1, -1, cm);
Encryptor enc = infoEnc.getEncryptor();
enc.confirmPassword("foobaa");
OutputStream os = enc.getDataStream(fsEnc);
os.write(testData);
os.close();
bos.reset();
fsEnc.writeFilesystem(bos);
POIFSFileSystem fsDec = new POIFSFileSystem(new ByteArrayInputStream(bos.toByteArray()));
EncryptionInfo infoDec = new EncryptionInfo(fsDec);
Decryptor dec = infoDec.getDecryptor();
boolean passed = dec.verifyPassword("foobaa");
assertTrue(passed);
InputStream is = dec.getDataStream(fsDec);
byte actualData[] = IOUtils.toByteArray(is);
is.close();
assertThat("Failed roundtrip - "+ca+"-"+ha+"-"+cm, testData, equalTo(actualData));
}
}

View File

@ -0,0 +1,193 @@
/* ====================================================================
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 static org.hamcrest.core.IsEqual.equalTo;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.Date;
import org.apache.poi.POIDataSamples;
import org.apache.poi.poifs.crypt.agile.AgileDecryptor;
import org.apache.poi.poifs.crypt.agile.AgileEncryptionVerifier;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
import org.apache.poi.util.IOUtils;
import org.junit.BeforeClass;
import org.junit.Test;
import sun.security.x509.AlgorithmId;
import sun.security.x509.CertificateAlgorithmId;
import sun.security.x509.CertificateIssuerName;
import sun.security.x509.CertificateSerialNumber;
import sun.security.x509.CertificateSubjectName;
import sun.security.x509.CertificateValidity;
import sun.security.x509.CertificateVersion;
import sun.security.x509.CertificateX509Key;
import sun.security.x509.X500Name;
import sun.security.x509.X509CertImpl;
import sun.security.x509.X509CertInfo;
/**
* {@linkplain http://stackoverflow.com/questions/1615871/creating-an-x509-certificate-in-java-without-bouncycastle}
*/
public class TestCertificateEncryption {
/**
* how many days from now the Certificate is valid for
*/
static final int days = 1000;
/**
* the signing algorithm, eg "SHA1withRSA"
*/
static final String algorithm = "SHA1withRSA";
static final String password = "foobaa";
static final String certAlias = "poitest";
/**
* the X.509 Distinguished Name, eg "CN=Test, L=London, C=GB"
*/
static final String certDN = "CN=poitest";
// static final File pfxFile = TempFile.createTempFile("poitest", ".pfx");
static byte pfxFileBytes[];
static class CertData {
KeyPair keypair;
X509Certificate x509;
}
/**
* Create a self-signed X.509 Certificate
*
* The keystore generation / loading is split, because normally the keystore would
* already exist.
*/
@BeforeClass
public static void initKeystore() throws GeneralSecurityException, IOException {
CertData certData = new CertData();
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(1024);
certData.keypair = keyGen.generateKeyPair();
PrivateKey privkey = certData.keypair.getPrivate();
PublicKey publkey = certData.keypair.getPublic();
X509CertInfo info = new X509CertInfo();
Date from = new Date();
Date to = new Date(from.getTime() + days * 86400000l);
CertificateValidity interval = new CertificateValidity(from, to);
BigInteger sn = new BigInteger(64, new SecureRandom());
X500Name owner = new X500Name(certDN);
info.set(X509CertInfo.VALIDITY, interval);
info.set(X509CertInfo.SERIAL_NUMBER, new CertificateSerialNumber(sn));
info.set(X509CertInfo.SUBJECT, new CertificateSubjectName(owner));
info.set(X509CertInfo.ISSUER, new CertificateIssuerName(owner));
info.set(X509CertInfo.KEY, new CertificateX509Key(publkey));
info.set(X509CertInfo.VERSION, new CertificateVersion(CertificateVersion.V3));
AlgorithmId algo = new AlgorithmId(AlgorithmId.md5WithRSAEncryption_oid);
info.set(X509CertInfo.ALGORITHM_ID, new CertificateAlgorithmId(algo));
// Sign the cert to identify the algorithm that's used.
X509CertImpl cert = new X509CertImpl(info);
cert.sign(privkey, algorithm);
// Update the algorith, and resign.
algo = (AlgorithmId)cert.get(X509CertImpl.SIG_ALG);
info.set(CertificateAlgorithmId.NAME + "." + CertificateAlgorithmId.ALGORITHM, algo);
cert = new X509CertImpl(info);
cert.sign(privkey, algorithm);
certData.x509 = cert;
KeyStore keystore = KeyStore.getInstance("PKCS12");
keystore.load(null, password.toCharArray());
keystore.setKeyEntry(certAlias, certData.keypair.getPrivate(), password.toCharArray(), new Certificate[]{certData.x509});
ByteArrayOutputStream bos = new ByteArrayOutputStream();
keystore.store(bos, password.toCharArray());
pfxFileBytes = bos.toByteArray();
}
public CertData loadKeystore()
throws GeneralSecurityException, IOException {
KeyStore keystore = KeyStore.getInstance("PKCS12");
InputStream fis = new ByteArrayInputStream(pfxFileBytes);
keystore.load(fis, password.toCharArray());
X509Certificate x509 = (X509Certificate)keystore.getCertificate(certAlias);
PrivateKey privateKey = (PrivateKey)keystore.getKey(certAlias, password.toCharArray());
PublicKey publicKey = x509.getPublicKey();
CertData certData = new CertData();
certData.keypair = new KeyPair(publicKey, privateKey);
certData.x509 = x509;
return certData;
}
@Test
public void testCertificateEncryption() throws Exception {
POIFSFileSystem fs = new POIFSFileSystem();
EncryptionInfo info = new EncryptionInfo(fs, EncryptionMode.agile, CipherAlgorithm.aes192, HashAlgorithm.sha1, -1, -1, ChainingMode.cbc);
AgileEncryptionVerifier aev = (AgileEncryptionVerifier)info.getVerifier();
CertData certData = loadKeystore();
aev.addCertificate(certData.x509);
Encryptor enc = info.getEncryptor();
enc.confirmPassword("foobaa");
File file = POIDataSamples.getDocumentInstance().getFile("VariousPictures.docx");
InputStream fis = new FileInputStream(file);
byte byteExpected[] = IOUtils.toByteArray(fis);
fis.close();
OutputStream os = enc.getDataStream(fs);
IOUtils.copy(new ByteArrayInputStream(byteExpected), os);
os.close();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
fs.writeFilesystem(bos);
bos.close();
fs = new POIFSFileSystem(new ByteArrayInputStream(bos.toByteArray()));
info = new EncryptionInfo(fs);
AgileDecryptor agDec = (AgileDecryptor)info.getDecryptor();
boolean passed = agDec.verifyPassword(certData.keypair, certData.x509);
assertTrue("certificate verification failed", passed);
fis = agDec.getDataStream(fs);
byte byteActual[] = IOUtils.toByteArray(fis);
fis.close();
assertThat(byteExpected, equalTo(byteActual));
}
}

View File

@ -1,117 +1,118 @@
/* ====================================================================
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==================================================================== */
package org.apache.poi.poifs.crypt;
import junit.framework.TestCase;
import org.apache.poi.POIDataSamples;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/**
* @author Maxim Valyanskiy
* @author Gary King
*/
public class TestDecryptor extends TestCase {
public void testPasswordVerification() throws IOException, GeneralSecurityException {
POIFSFileSystem fs = new POIFSFileSystem(POIDataSamples.getPOIFSInstance().openResourceAsStream("protect.xlsx"));
EncryptionInfo info = new EncryptionInfo(fs);
Decryptor d = Decryptor.getInstance(info);
assertTrue(d.verifyPassword(Decryptor.DEFAULT_PASSWORD));
}
public void testDecrypt() throws IOException, GeneralSecurityException {
POIFSFileSystem fs = new POIFSFileSystem(POIDataSamples.getPOIFSInstance().openResourceAsStream("protect.xlsx"));
EncryptionInfo info = new EncryptionInfo(fs);
Decryptor d = Decryptor.getInstance(info);
d.verifyPassword(Decryptor.DEFAULT_PASSWORD);
zipOk(fs, d);
}
public void testAgile() throws IOException, GeneralSecurityException {
POIFSFileSystem fs = new POIFSFileSystem(POIDataSamples.getPOIFSInstance().openResourceAsStream("protected_agile.docx"));
EncryptionInfo info = new EncryptionInfo(fs);
assertTrue(info.getVersionMajor() == 4 && info.getVersionMinor() == 4);
Decryptor d = Decryptor.getInstance(info);
assertTrue(d.verifyPassword(Decryptor.DEFAULT_PASSWORD));
zipOk(fs, d);
}
private void zipOk(POIFSFileSystem fs, Decryptor d) throws IOException, GeneralSecurityException {
ZipInputStream zin = new ZipInputStream(d.getDataStream(fs));
while (true) {
ZipEntry entry = zin.getNextEntry();
if (entry==null) {
break;
}
while (zin.available()>0) {
zin.skip(zin.available());
}
}
}
public void testDataLength() throws Exception {
POIFSFileSystem fs = new POIFSFileSystem(POIDataSamples.getPOIFSInstance().openResourceAsStream("protected_agile.docx"));
EncryptionInfo info = new EncryptionInfo(fs);
Decryptor d = Decryptor.getInstance(info);
d.verifyPassword(Decryptor.DEFAULT_PASSWORD);
InputStream is = d.getDataStream(fs);
long len = d.getLength();
assertEquals(12810, len);
byte[] buf = new byte[(int)len];
is.read(buf);
ZipInputStream zin = new ZipInputStream(new ByteArrayInputStream(buf));
while (true) {
ZipEntry entry = zin.getNextEntry();
if (entry==null) {
break;
}
while (zin.available()>0) {
zin.skip(zin.available());
}
}
}
}
/* ====================================================================
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.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import junit.framework.TestCase;
import org.apache.poi.POIDataSamples;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
/**
* @author Maxim Valyanskiy
* @author Gary King
*/
public class TestDecryptor extends TestCase {
public void testPasswordVerification() throws IOException, GeneralSecurityException {
POIFSFileSystem fs = new POIFSFileSystem(POIDataSamples.getPOIFSInstance().openResourceAsStream("protect.xlsx"));
EncryptionInfo info = new EncryptionInfo(fs);
Decryptor d = Decryptor.getInstance(info);
assertTrue(d.verifyPassword(Decryptor.DEFAULT_PASSWORD));
}
public void testDecrypt() throws IOException, GeneralSecurityException {
POIFSFileSystem fs = new POIFSFileSystem(POIDataSamples.getPOIFSInstance().openResourceAsStream("protect.xlsx"));
EncryptionInfo info = new EncryptionInfo(fs);
Decryptor d = Decryptor.getInstance(info);
d.verifyPassword(Decryptor.DEFAULT_PASSWORD);
zipOk(fs, d);
}
public void testAgile() throws IOException, GeneralSecurityException {
POIFSFileSystem fs = new POIFSFileSystem(POIDataSamples.getPOIFSInstance().openResourceAsStream("protected_agile.docx"));
EncryptionInfo info = new EncryptionInfo(fs);
assertTrue(info.getVersionMajor() == 4 && info.getVersionMinor() == 4);
Decryptor d = Decryptor.getInstance(info);
assertTrue(d.verifyPassword(Decryptor.DEFAULT_PASSWORD));
zipOk(fs, d);
}
private void zipOk(POIFSFileSystem fs, Decryptor d) throws IOException, GeneralSecurityException {
ZipInputStream zin = new ZipInputStream(d.getDataStream(fs));
while (true) {
ZipEntry entry = zin.getNextEntry();
if (entry==null) {
break;
}
while (zin.available()>0) {
zin.skip(zin.available());
}
}
}
public void testDataLength() throws Exception {
POIFSFileSystem fs = new POIFSFileSystem(POIDataSamples.getPOIFSInstance().openResourceAsStream("protected_agile.docx"));
EncryptionInfo info = new EncryptionInfo(fs);
Decryptor d = Decryptor.getInstance(info);
d.verifyPassword(Decryptor.DEFAULT_PASSWORD);
InputStream is = d.getDataStream(fs);
long len = d.getLength();
assertEquals(12810, len);
byte[] buf = new byte[(int)len];
is.read(buf);
ZipInputStream zin = new ZipInputStream(new ByteArrayInputStream(buf));
while (true) {
ZipEntry entry = zin.getNextEntry();
if (entry==null) {
break;
}
while (zin.available()>0) {
zin.skip(zin.available());
}
}
}
}

View File

@ -1,60 +1,61 @@
/* ====================================================================
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==================================================================== */
package org.apache.poi.poifs.crypt;
import junit.framework.TestCase;
import org.apache.poi.POIDataSamples;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
import java.io.IOException;
/**
* @author Maxim Valyanskiy
*/
public class TestEncryptionInfo extends TestCase {
public void testEncryptionInfo() throws IOException {
POIFSFileSystem fs = new POIFSFileSystem(POIDataSamples.getPOIFSInstance().openResourceAsStream("protect.xlsx"));
EncryptionInfo info = new EncryptionInfo(fs);
assertEquals(3, info.getVersionMajor());
assertEquals(2, info.getVersionMinor());
assertEquals(EncryptionHeader.ALGORITHM_AES_128, info.getHeader().getAlgorithm());
assertEquals(EncryptionHeader.HASH_SHA1, info.getHeader().getHashAlgorithm());
assertEquals(128, info.getHeader().getKeySize());
assertEquals(32, info.getVerifier().getVerifierHash().length);
assertEquals(EncryptionHeader.PROVIDER_AES, info.getHeader().getProviderType());
assertEquals("Microsoft Enhanced RSA and AES Cryptographic Provider", info.getHeader().getCspName());
}
public void testEncryptionInfoSHA512() throws Exception {
POIFSFileSystem fs = new POIFSFileSystem(POIDataSamples.getPOIFSInstance().openResourceAsStream("protected_sha512.xlsx"));
EncryptionInfo info = new EncryptionInfo(fs);
assertEquals(4, info.getVersionMajor());
assertEquals(4, info.getVersionMinor());
assertEquals(EncryptionHeader.ALGORITHM_AES_256, info.getHeader().getAlgorithm());
assertEquals(EncryptionHeader.HASH_SHA512, info.getHeader().getHashAlgorithm());
assertEquals(256, info.getHeader().getKeySize());
assertEquals(64, info.getVerifier().getVerifierHash().length);
assertEquals(EncryptionHeader.PROVIDER_AES, info.getHeader().getProviderType());
// assertEquals("Microsoft Enhanced RSA and AES Cryptographic Provider", info.getHeader().getCspName());
}
}
/* ====================================================================
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 static org.junit.Assert.assertEquals;
import java.io.IOException;
import org.apache.poi.POIDataSamples;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
import org.junit.Test;
public class TestEncryptionInfo {
@Test
public void testEncryptionInfo() throws IOException {
POIFSFileSystem fs = new POIFSFileSystem(POIDataSamples.getPOIFSInstance().openResourceAsStream("protect.xlsx"));
EncryptionInfo info = new EncryptionInfo(fs);
assertEquals(3, info.getVersionMajor());
assertEquals(2, info.getVersionMinor());
assertEquals(CipherAlgorithm.aes128, info.getHeader().getCipherAlgorithm());
assertEquals(HashAlgorithm.sha1, info.getHeader().getHashAlgorithmEx());
assertEquals(128, info.getHeader().getKeySize());
assertEquals(32, info.getVerifier().getEncryptedVerifierHash().length);
assertEquals(CipherProvider.aes, info.getHeader().getCipherProvider());
assertEquals("Microsoft Enhanced RSA and AES Cryptographic Provider", info.getHeader().getCspName());
}
@Test
public void testEncryptionInfoSHA512() throws Exception {
POIFSFileSystem fs = new POIFSFileSystem(POIDataSamples.getPOIFSInstance().openResourceAsStream("protected_sha512.xlsx"));
EncryptionInfo info = new EncryptionInfo(fs);
assertEquals(4, info.getVersionMajor());
assertEquals(4, info.getVersionMinor());
assertEquals(CipherAlgorithm.aes256, info.getHeader().getCipherAlgorithm());
assertEquals(HashAlgorithm.sha512, info.getHeader().getHashAlgorithmEx());
assertEquals(256, info.getHeader().getKeySize());
assertEquals(64, info.getVerifier().getEncryptedVerifierHash().length);
assertEquals(CipherProvider.aes, info.getHeader().getCipherProvider());
// assertEquals("Microsoft Enhanced RSA and AES Cryptographic Provider", info.getHeader().getCspName());
}
}

View File

@ -0,0 +1,253 @@
/* ====================================================================
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 static org.hamcrest.core.IsEqual.equalTo;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Iterator;
import org.apache.poi.POIDataSamples;
import org.apache.poi.poifs.crypt.agile.AgileEncryptionHeader;
import org.apache.poi.poifs.filesystem.DirectoryNode;
import org.apache.poi.poifs.filesystem.DocumentNode;
import org.apache.poi.poifs.filesystem.Entry;
import org.apache.poi.poifs.filesystem.NPOIFSFileSystem;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
import org.apache.poi.util.BoundedInputStream;
import org.apache.poi.util.IOUtils;
import org.junit.Test;
public class TestEncryptor {
@Test
public void testAgileEncryption() throws Exception {
File file = POIDataSamples.getDocumentInstance().getFile("bug53475-password-is-pass.docx");
String pass = "pass";
NPOIFSFileSystem nfs = new NPOIFSFileSystem(file);
// Check the encryption details
EncryptionInfo infoExpected = new EncryptionInfo(nfs);
Decryptor decExpected = Decryptor.getInstance(infoExpected);
boolean passed = decExpected.verifyPassword(pass);
assertTrue("Unable to process: document is encrypted", passed);
// extract the payload
InputStream is = decExpected.getDataStream(nfs);
byte payloadExpected[] = IOUtils.toByteArray(is);
is.close();
long decPackLenExpected = decExpected.getLength();
assertEquals(decPackLenExpected, payloadExpected.length);
is = nfs.getRoot().createDocumentInputStream("EncryptedPackage");
is = new BoundedInputStream(is, is.available()-16); // ignore padding block
byte encPackExpected[] = IOUtils.toByteArray(is);
is.close();
// listDir(nfs.getRoot(), "orig", "");
nfs.close();
// check that same verifier/salt lead to same hashes
byte verifierSaltExpected[] = infoExpected.getVerifier().getSalt();
byte verifierExpected[] = decExpected.getVerifier();
byte keySalt[] = infoExpected.getHeader().getKeySalt();
byte keySpec[] = decExpected.getSecretKey().getEncoded();
byte integritySalt[] = decExpected.getIntegrityHmacKey();
// the hmacs of the file always differ, as we use PKCS5-padding to pad the bytes
// whereas office just uses random bytes
// byte integrityHash[] = d.getIntegrityHmacValue();
POIFSFileSystem fs = new POIFSFileSystem();
EncryptionInfo infoActual = new EncryptionInfo(
fs, EncryptionMode.agile
, infoExpected.getVerifier().getCipherAlgorithm()
, infoExpected.getVerifier().getHashAlgorithm()
, infoExpected.getHeader().getKeySize()
, infoExpected.getHeader().getBlockSize()
, infoExpected.getVerifier().getChainingMode()
);
Encryptor e = Encryptor.getInstance(infoActual);
e.confirmPassword(pass, keySpec, keySalt, verifierExpected, verifierSaltExpected, integritySalt);
OutputStream os = e.getDataStream(fs);
IOUtils.copy(new ByteArrayInputStream(payloadExpected), os);
os.close();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
fs.writeFilesystem(bos);
nfs = new NPOIFSFileSystem(new ByteArrayInputStream(bos.toByteArray()));
infoActual = new EncryptionInfo(nfs.getRoot());
Decryptor decActual = Decryptor.getInstance(infoActual);
passed = decActual.verifyPassword(pass);
assertTrue("Unable to process: document is encrypted", passed);
// extract the payload
is = decActual.getDataStream(nfs);
byte payloadActual[] = IOUtils.toByteArray(is);
is.close();
long decPackLenActual = decActual.getLength();
is = nfs.getRoot().createDocumentInputStream("EncryptedPackage");
is = new BoundedInputStream(is, is.available()-16); // ignore padding block
byte encPackActual[] = IOUtils.toByteArray(is);
is.close();
// listDir(nfs.getRoot(), "copy", "");
nfs.close();
AgileEncryptionHeader aehExpected = (AgileEncryptionHeader)infoExpected.getHeader();
AgileEncryptionHeader aehActual = (AgileEncryptionHeader)infoActual.getHeader();
assertThat(aehExpected.getEncryptedHmacKey(), equalTo(aehActual.getEncryptedHmacKey()));
assertEquals(decPackLenExpected, decPackLenActual);
assertThat(payloadExpected, equalTo(payloadActual));
assertThat(encPackExpected, equalTo(encPackActual));
}
@Test
public void testStandardEncryption() throws Exception {
File file = POIDataSamples.getDocumentInstance().getFile("bug53475-password-is-solrcell.docx");
String pass = "solrcell";
NPOIFSFileSystem nfs = new NPOIFSFileSystem(file);
// Check the encryption details
EncryptionInfo infoExpected = new EncryptionInfo(nfs);
Decryptor d = Decryptor.getInstance(infoExpected);
boolean passed = d.verifyPassword(pass);
assertTrue("Unable to process: document is encrypted", passed);
// extract the payload
ByteArrayOutputStream bos = new ByteArrayOutputStream();
InputStream is = d.getDataStream(nfs);
IOUtils.copy(is, bos);
is.close();
nfs.close();
byte payloadExpected[] = bos.toByteArray();
// check that same verifier/salt lead to same hashes
byte verifierSaltExpected[] = infoExpected.getVerifier().getSalt();
byte verifierExpected[] = d.getVerifier();
byte keySpec[] = d.getSecretKey().getEncoded();
byte keySalt[] = infoExpected.getHeader().getKeySalt();
POIFSFileSystem fs = new POIFSFileSystem();
EncryptionInfo infoActual = new EncryptionInfo(
fs, EncryptionMode.standard
, infoExpected.getVerifier().getCipherAlgorithm()
, infoExpected.getVerifier().getHashAlgorithm()
, infoExpected.getHeader().getKeySize()
, infoExpected.getHeader().getBlockSize()
, infoExpected.getVerifier().getChainingMode()
);
Encryptor e = Encryptor.getInstance(infoActual);
e.confirmPassword(pass, keySpec, keySalt, verifierExpected, verifierSaltExpected, null);
assertThat(infoExpected.getVerifier().getEncryptedVerifier(), equalTo(infoActual.getVerifier().getEncryptedVerifier()));
assertThat(infoExpected.getVerifier().getEncryptedVerifierHash(), equalTo(infoActual.getVerifier().getEncryptedVerifierHash()));
// now we use a newly generated salt/verifier and check
// if the file content is still the same
fs = new POIFSFileSystem();
infoActual = new EncryptionInfo(
fs, EncryptionMode.standard
, infoExpected.getVerifier().getCipherAlgorithm()
, infoExpected.getVerifier().getHashAlgorithm()
, infoExpected.getHeader().getKeySize()
, infoExpected.getHeader().getBlockSize()
, infoExpected.getVerifier().getChainingMode()
);
e = Encryptor.getInstance(infoActual);
e.confirmPassword(pass);
OutputStream os = e.getDataStream(fs);
IOUtils.copy(new ByteArrayInputStream(payloadExpected), os);
os.close();
bos.reset();
fs.writeFilesystem(bos);
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
// FileOutputStream fos = new FileOutputStream("encrypted.docx");
// IOUtils.copy(bis, fos);
// fos.close();
// bis.reset();
nfs = new NPOIFSFileSystem(bis);
infoExpected = new EncryptionInfo(nfs);
d = Decryptor.getInstance(infoExpected);
passed = d.verifyPassword(pass);
assertTrue("Unable to process: document is encrypted", passed);
bos.reset();
is = d.getDataStream(nfs);
IOUtils.copy(is, bos);
is.close();
nfs.close();
byte payloadActual[] = bos.toByteArray();
assertThat(payloadExpected, equalTo(payloadActual));
}
private void listEntry(DocumentNode de, String ext, String path) throws IOException {
path += "\\" + de.getName().replace('\u0006', '_');
System.out.println(ext+": "+path+" ("+de.getSize()+" bytes)");
String name = de.getName().replace('\u0006', '_');
InputStream is = ((DirectoryNode)de.getParent()).createDocumentInputStream(de);
FileOutputStream fos = new FileOutputStream("solr."+name+"."+ext);
IOUtils.copy(is, fos);
fos.close();
is.close();
}
@SuppressWarnings("unused")
private void listDir(DirectoryNode dn, String ext, String path) throws IOException {
path += "\\" + dn.getName().replace('\u0006', '_');
System.out.println(ext+": "+path+" ("+dn.getStorageClsid()+")");
Iterator<Entry> iter = dn.getEntries();
while (iter.hasNext()) {
Entry ent = iter.next();
if (ent instanceof DirectoryNode) {
listDir((DirectoryNode)ent, ext, path);
} else {
listEntry((DocumentNode)ent, ext, path);
}
}
}
}

View File

@ -11,9 +11,10 @@ import javax.crypto.Cipher;
import org.apache.poi.POIDataSamples;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.poifs.crypt.CipherAlgorithm;
import org.apache.poi.poifs.crypt.Decryptor;
import org.apache.poi.poifs.crypt.EncryptionHeader;
import org.apache.poi.poifs.crypt.EncryptionInfo;
import org.apache.poi.poifs.crypt.HashAlgorithm;
import org.apache.poi.poifs.filesystem.NPOIFSFileSystem;
import org.apache.poi.xwpf.extractor.XWPFWordExtractor;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
@ -33,8 +34,8 @@ public class TestXWPFBugs {
// Check the encryption details
EncryptionInfo info = new EncryptionInfo(filesystem);
assertEquals(128, info.getHeader().getKeySize());
assertEquals(EncryptionHeader.ALGORITHM_AES_128, info.getHeader().getAlgorithm());
assertEquals(EncryptionHeader.HASH_SHA1, info.getHeader().getHashAlgorithm());
assertEquals(CipherAlgorithm.aes128, info.getHeader().getCipherAlgorithm());
assertEquals(HashAlgorithm.sha1, info.getHeader().getHashAlgorithmEx());
// Check it can be decoded
Decryptor d = Decryptor.getInstance(info);
@ -67,8 +68,8 @@ public class TestXWPFBugs {
EncryptionInfo info = new EncryptionInfo(filesystem);
assertEquals(16, info.getHeader().getBlockSize());
assertEquals(256, info.getHeader().getKeySize());
assertEquals(EncryptionHeader.ALGORITHM_AES_256, info.getHeader().getAlgorithm());
assertEquals(EncryptionHeader.HASH_SHA1, info.getHeader().getHashAlgorithm());
assertEquals(CipherAlgorithm.aes256, info.getHeader().getCipherAlgorithm());
assertEquals(HashAlgorithm.sha1, info.getHeader().getHashAlgorithmEx());
// Check it can be decoded
Decryptor d = Decryptor.getInstance(info);