diff --git a/src/java/org/apache/poi/POIDocument.java b/src/java/org/apache/poi/POIDocument.java
index e61366b451..950e5eb3e4 100644
--- a/src/java/org/apache/poi/POIDocument.java
+++ b/src/java/org/apache/poi/POIDocument.java
@@ -20,6 +20,7 @@ package org.apache.poi;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
+import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
@@ -28,6 +29,7 @@ import org.apache.poi.hpsf.MutablePropertySet;
import org.apache.poi.hpsf.PropertySet;
import org.apache.poi.hpsf.PropertySetFactory;
import org.apache.poi.hpsf.SummaryInformation;
+import org.apache.poi.poifs.crypt.EncryptionInfo;
import org.apache.poi.poifs.filesystem.DirectoryEntry;
import org.apache.poi.poifs.filesystem.DirectoryNode;
import org.apache.poi.poifs.filesystem.DocumentInputStream;
@@ -163,14 +165,40 @@ public abstract class POIDocument {
* @return The value of the given property or null if it wasn't found.
*/
protected PropertySet getPropertySet(String setName) {
+ return getPropertySet(setName, null);
+ }
+
+ /**
+ * For a given named property entry, either return it or null if
+ * if it wasn't found
+ *
+ * @param setName The property to read
+ * @param encryptionInfo the encryption descriptor in case of cryptoAPI encryption
+ * @return The value of the given property or null if it wasn't found.
+ */
+ protected PropertySet getPropertySet(String setName, EncryptionInfo encryptionInfo) {
+ DirectoryNode dirNode = directory;
+
+ if (encryptionInfo != null) {
+ try {
+ InputStream is = encryptionInfo.getDecryptor().getDataStream(directory);
+ POIFSFileSystem poifs = new POIFSFileSystem(is);
+ is.close();
+ dirNode = poifs.getRoot();
+ } catch (Exception e) {
+ logger.log(POILogger.ERROR, "Error getting encrypted property set with name " + setName, e);
+ return null;
+ }
+ }
+
//directory can be null when creating new documents
- if (directory == null || !directory.hasEntry(setName))
+ if (dirNode == null || !dirNode.hasEntry(setName))
return null;
DocumentInputStream dis;
try {
// Find the entry, and get an input stream for it
- dis = directory.createDocumentInputStream( directory.getEntry(setName) );
+ dis = dirNode.createDocumentInputStream( dirNode.getEntry(setName) );
} catch(IOException ie) {
// Oh well, doesn't exist
logger.log(POILogger.WARN, "Error getting property set with name " + setName + "\n" + ie);
diff --git a/src/java/org/apache/poi/poifs/crypt/ChunkedCipherInputStream.java b/src/java/org/apache/poi/poifs/crypt/ChunkedCipherInputStream.java
new file mode 100644
index 0000000000..7d695a1eb3
--- /dev/null
+++ b/src/java/org/apache/poi/poifs/crypt/ChunkedCipherInputStream.java
@@ -0,0 +1,141 @@
+/* ====================================================================
+ 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 javax.crypto.Cipher;
+
+import org.apache.poi.EncryptedDocumentException;
+import org.apache.poi.util.Internal;
+import org.apache.poi.util.LittleEndianInput;
+import org.apache.poi.util.LittleEndianInputStream;
+
+@Internal
+public abstract class ChunkedCipherInputStream extends LittleEndianInputStream {
+ private final int chunkSize;
+ private final int chunkMask;
+ private final int chunkBits;
+
+ private int _lastIndex = 0;
+ private long _pos = 0;
+ private long _size;
+ private byte[] _chunk;
+ private Cipher _cipher;
+
+ public ChunkedCipherInputStream(LittleEndianInput stream, long size, int chunkSize)
+ throws GeneralSecurityException {
+ super((InputStream)stream);
+ _size = size;
+ this.chunkSize = chunkSize;
+ chunkMask = chunkSize-1;
+ chunkBits = Integer.bitCount(chunkMask);
+
+ _cipher = initCipherForBlock(null, 0);
+ }
+
+ protected abstract Cipher initCipherForBlock(Cipher existing, int block)
+ throws GeneralSecurityException;
+
+ public int read() throws IOException {
+ byte[] b = new byte[1];
+ if (read(b) == 1)
+ return b[0];
+ return -1;
+ }
+
+ // do not implement! -> recursion
+ // public int read(byte[] b) throws IOException;
+
+ 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(), e);
+ }
+ }
+ int count = (int)(chunkSize - (_pos & chunkMask));
+ int avail = available();
+ if (avail == 0) {
+ return total;
+ }
+ count = Math.min(avail, Math.min(count, len));
+ System.arraycopy(_chunk, (int)(_pos & chunkMask), b, off, count);
+ off += count;
+ len -= count;
+ _pos += count;
+ if ((_pos & chunkMask) == 0)
+ _chunk = null;
+ total += count;
+ }
+
+ return total;
+ }
+
+ @Override
+ public long skip(long n) throws IOException {
+ long start = _pos;
+ long skip = Math.min(available(), n);
+
+ if ((((_pos + skip) ^ start) & ~chunkMask) != 0)
+ _chunk = null;
+ _pos += skip;
+ return skip;
+ }
+
+ @Override
+ public int available() {
+ return (int)(_size - _pos);
+ }
+
+ @Override
+ public boolean markSupported() {
+ return false;
+ }
+
+ @Override
+ public synchronized void mark(int readlimit) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public synchronized void reset() throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ private byte[] nextChunk() throws GeneralSecurityException, IOException {
+ int index = (int)(_pos >> chunkBits);
+ initCipherForBlock(_cipher, index);
+
+ if (_lastIndex != index) {
+ super.skip((index - _lastIndex) << chunkBits);
+ }
+
+ byte[] block = new byte[Math.min(super.available(), chunkSize)];
+ super.read(block, 0, block.length);
+ _lastIndex = index + 1;
+ return _cipher.doFinal(block);
+ }
+}
diff --git a/src/java/org/apache/poi/poifs/crypt/ChunkedCipherOutputStream.java b/src/java/org/apache/poi/poifs/crypt/ChunkedCipherOutputStream.java
new file mode 100644
index 0000000000..8a2bf00454
--- /dev/null
+++ b/src/java/org/apache/poi/poifs/crypt/ChunkedCipherOutputStream.java
@@ -0,0 +1,171 @@
+/* ====================================================================
+ 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.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 javax.crypto.Cipher;
+
+import org.apache.poi.EncryptedDocumentException;
+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.Internal;
+import org.apache.poi.util.LittleEndian;
+import org.apache.poi.util.LittleEndianConsts;
+import org.apache.poi.util.TempFile;
+
+@Internal
+public abstract class ChunkedCipherOutputStream extends FilterOutputStream {
+ protected final int chunkSize;
+ protected final int chunkMask;
+ protected final int chunkBits;
+
+ private final byte[] _chunk;
+ private final File fileOut;
+ private final DirectoryNode dir;
+
+ private long _pos = 0;
+ private Cipher _cipher;
+
+ public ChunkedCipherOutputStream(DirectoryNode dir, int chunkSize) throws IOException, GeneralSecurityException {
+ super(null);
+ this.chunkSize = chunkSize;
+ chunkMask = chunkSize-1;
+ chunkBits = Integer.bitCount(chunkMask);
+ _chunk = new byte[chunkSize];
+
+ fileOut = TempFile.createTempFile("encrypted_package", "crypt");
+ fileOut.deleteOnExit();
+ this.out = new FileOutputStream(fileOut);
+ this.dir = dir;
+ _cipher = initCipherForBlock(null, 0, false);
+ }
+
+ protected abstract Cipher initCipherForBlock(Cipher existing, int block, boolean lastChunk)
+ throws GeneralSecurityException;
+
+ protected abstract void calculateChecksum(File fileOut, int oleStreamSize)
+ throws GeneralSecurityException, IOException;
+
+ protected abstract void createEncryptionInfoEntry(DirectoryNode dir, File tmpFile)
+ throws IOException, GeneralSecurityException;
+
+ 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 & chunkMask);
+ int nextLen = Math.min(chunkSize-posInChunk, len);
+ System.arraycopy(b, off, _chunk, posInChunk, nextLen);
+ _pos += nextLen;
+ off += nextLen;
+ len -= nextLen;
+ if ((_pos & chunkMask) == 0) {
+ try {
+ writeChunk();
+ } catch (GeneralSecurityException e) {
+ throw new IOException(e);
+ }
+ }
+ }
+ }
+
+ protected void writeChunk() throws IOException, GeneralSecurityException {
+ int posInChunk = (int)(_pos & chunkMask);
+ // 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 >> chunkBits);
+ boolean lastChunk;
+ if (posInChunk==0) {
+ index--;
+ posInChunk = chunkSize;
+ lastChunk = false;
+ } else {
+ // pad the last chunk
+ lastChunk = true;
+ }
+
+ _cipher = initCipherForBlock(_cipher, index, lastChunk);
+
+ int ciLen = _cipher.doFinal(_chunk, 0, posInChunk, _chunk);
+ out.write(_chunk, 0, ciLen);
+ }
+
+ public void close() throws IOException {
+ try {
+ writeChunk();
+
+ super.close();
+
+ int oleStreamSize = (int)(fileOut.length()+LittleEndianConsts.LONG_SIZE);
+ calculateChecksum(fileOut, oleStreamSize);
+ dir.createDocument("EncryptedPackage", oleStreamSize, new EncryptedPackageWriter());
+ createEncryptionInfoEntry(dir, fileOut);
+ } catch (GeneralSecurityException e) {
+ throw new IOException(e);
+ }
+ }
+
+ private class EncryptedPackageWriter implements POIFSWriterListener {
+ public void processPOIFSWriterEvent(POIFSWriterEvent event) {
+ try {
+ OutputStream os = event.getStream();
+ byte buf[] = new byte[chunkSize];
+
+ // 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
+ LittleEndian.putLong(buf, 0, _pos);
+ os.write(buf, 0, LittleEndian.LONG_SIZE);
+
+ FileInputStream fis = new FileInputStream(fileOut);
+ int readBytes;
+ while ((readBytes = fis.read(buf)) != -1) {
+ os.write(buf, 0, readBytes);
+ }
+ fis.close();
+
+ os.close();
+
+ fileOut.delete();
+ } catch (IOException e) {
+ throw new EncryptedDocumentException(e);
+ }
+ }
+ }
+}
diff --git a/src/java/org/apache/poi/poifs/crypt/CipherProvider.java b/src/java/org/apache/poi/poifs/crypt/CipherProvider.java
index de343a91dc..5ffe1d33ae 100644
--- a/src/java/org/apache/poi/poifs/crypt/CipherProvider.java
+++ b/src/java/org/apache/poi/poifs/crypt/CipherProvider.java
@@ -20,8 +20,8 @@ package org.apache.poi.poifs.crypt;
import org.apache.poi.EncryptedDocumentException;
public enum CipherProvider {
- rc4("RC4", 1),
- aes("AES", 0x18);
+ rc4("RC4", 1, "Microsoft Base Cryptographic Provider v1.0"),
+ aes("AES", 0x18, "Microsoft Enhanced RSA and AES Cryptographic Provider");
public static CipherProvider fromEcmaId(int ecmaId) {
for (CipherProvider cp : CipherProvider.values()) {
@@ -32,8 +32,10 @@ public enum CipherProvider {
public final String jceId;
public final int ecmaId;
- CipherProvider(String jceId, int ecmaId) {
+ public final String cipherProviderName;
+ CipherProvider(String jceId, int ecmaId, String cipherProviderName) {
this.jceId = jceId;
this.ecmaId = ecmaId;
+ this.cipherProviderName = cipherProviderName;
}
}
\ No newline at end of file
diff --git a/src/java/org/apache/poi/poifs/crypt/Decryptor.java b/src/java/org/apache/poi/poifs/crypt/Decryptor.java
index c2d0d5953b..af449290e8 100644
--- a/src/java/org/apache/poi/poifs/crypt/Decryptor.java
+++ b/src/java/org/apache/poi/poifs/crypt/Decryptor.java
@@ -30,12 +30,12 @@ import org.apache.poi.poifs.filesystem.POIFSFileSystem;
public abstract class Decryptor {
public static final String DEFAULT_PASSWORD="VelvetSweatshop";
- protected final EncryptionInfo info;
+ protected final EncryptionInfoBuilder builder;
private SecretKey secretKey;
private byte[] verifier, integrityHmacKey, integrityHmacValue;
- protected Decryptor(EncryptionInfo info) {
- this.info = info;
+ protected Decryptor(EncryptionInfoBuilder builder) {
+ this.builder = builder;
}
/**
@@ -56,7 +56,7 @@ public abstract class Decryptor {
throws GeneralSecurityException;
/**
- * Returns the length of the encytpted data that can be safely read with
+ * Returns the length of the encrypted data that can be safely read with
* {@link #getDataStream(org.apache.poi.poifs.filesystem.DirectoryNode)}.
* Just reading to the end of the input stream is not sufficient because there are
* normally padding bytes that must be discarded
@@ -120,4 +120,12 @@ public abstract class Decryptor {
protected void setIntegrityHmacValue(byte[] integrityHmacValue) {
this.integrityHmacValue = integrityHmacValue;
}
+
+ protected int getBlockSizeInBytes() {
+ return builder.getHeader().getBlockSize();
+ }
+
+ protected int getKeySizeInBytes() {
+ return builder.getHeader().getKeySize()/8;
+ }
}
\ No newline at end of file
diff --git a/src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java b/src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java
index 25f9b01e15..0418befe23 100644
--- a/src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java
+++ b/src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java
@@ -17,15 +17,19 @@
package org.apache.poi.poifs.crypt;
import static org.apache.poi.poifs.crypt.EncryptionMode.agile;
+import static org.apache.poi.poifs.crypt.EncryptionMode.binaryRC4;
+import static org.apache.poi.poifs.crypt.EncryptionMode.cryptoAPI;
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 org.apache.poi.util.BitField;
+import org.apache.poi.util.BitFieldFactory;
+import org.apache.poi.util.LittleEndianInput;
/**
*/
@@ -39,6 +43,31 @@ public class EncryptionInfo {
private final Decryptor decryptor;
private final Encryptor encryptor;
+ /**
+ * A flag that specifies whether CryptoAPI RC4 or ECMA-376 encryption
+ * ECMA-376 is used. It MUST be 1 unless flagExternal is 1. If flagExternal is 1, it MUST be 0.
+ */
+ public static BitField flagCryptoAPI = BitFieldFactory.getInstance(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.
+ */
+ public static BitField flagDocProps = BitFieldFactory.getInstance(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.
+ */
+ public static BitField flagExternal = BitFieldFactory.getInstance(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.
+ */
+ public static BitField flagAES = BitFieldFactory.getInstance(0x20);
+
+
public EncryptionInfo(POIFSFileSystem fs) throws IOException {
this(fs.getRoot());
}
@@ -48,18 +77,43 @@ public class EncryptionInfo {
}
public EncryptionInfo(DirectoryNode dir) throws IOException {
- DocumentInputStream dis = dir.createDocumentInputStream("EncryptionInfo");
+ this(dir.createDocumentInputStream("EncryptionInfo"), false);
+ }
+
+ public EncryptionInfo(LittleEndianInput dis, boolean isCryptoAPI) throws IOException {
+ final EncryptionMode encryptionMode;
versionMajor = dis.readShort();
versionMinor = dis.readShort();
- encryptionFlags = dis.readInt();
-
- EncryptionMode encryptionMode;
- if (versionMajor == agile.versionMajor
- && versionMinor == agile.versionMinor
- && encryptionFlags == agile.encryptionFlags) {
+
+ if (!isCryptoAPI
+ && versionMajor == binaryRC4.versionMajor
+ && versionMinor == binaryRC4.versionMinor) {
+ encryptionMode = binaryRC4;
+ encryptionFlags = -1;
+ } else if (!isCryptoAPI
+ && versionMajor == agile.versionMajor
+ && versionMinor == agile.versionMinor){
encryptionMode = agile;
- } else {
+ encryptionFlags = dis.readInt();
+ } else if (!isCryptoAPI
+ && 2 <= versionMajor && versionMajor <= 4
+ && versionMinor == standard.versionMinor) {
encryptionMode = standard;
+ encryptionFlags = dis.readInt();
+ } else if (isCryptoAPI
+ && 2 <= versionMajor && versionMajor <= 4
+ && versionMinor == cryptoAPI.versionMinor) {
+ encryptionMode = cryptoAPI;
+ encryptionFlags = dis.readInt();
+ } else {
+ encryptionFlags = dis.readInt();
+ throw new EncryptedDocumentException(
+ "Unknown encryption: version major: "+versionMajor+
+ " / version minor: "+versionMinor+
+ " / fCrypto: "+flagCryptoAPI.isSet(encryptionFlags)+
+ " / fExternal: "+flagExternal.isSet(encryptionFlags)+
+ " / fDocProps: "+flagDocProps.isSet(encryptionFlags)+
+ " / fAES: "+flagAES.isSet(encryptionFlags));
}
EncryptionInfoBuilder eib;
@@ -75,22 +129,35 @@ public class EncryptionInfo {
decryptor = eib.getDecryptor();
encryptor = eib.getEncryptor();
}
-
- public EncryptionInfo(POIFSFileSystem fs, EncryptionMode encryptionMode) throws IOException {
- this(fs.getRoot(), encryptionMode);
- }
+
+ /**
+ * @deprecated use constructor without fs parameter
+ */
+ @Deprecated
+ public EncryptionInfo(POIFSFileSystem fs, EncryptionMode encryptionMode) {
+ this(encryptionMode);
+ }
- public EncryptionInfo(NPOIFSFileSystem fs, EncryptionMode encryptionMode) throws IOException {
- this(fs.getRoot(), encryptionMode);
- }
+ /**
+ * @deprecated use constructor without fs parameter
+ */
+ @Deprecated
+ public EncryptionInfo(NPOIFSFileSystem fs, EncryptionMode encryptionMode) {
+ this(encryptionMode);
+ }
- public EncryptionInfo(
- DirectoryNode dir
- , EncryptionMode encryptionMode
- ) throws EncryptedDocumentException {
- this(dir, encryptionMode, null, null, -1, -1, null);
+ /**
+ * @deprecated use constructor without dir parameter
+ */
+ @Deprecated
+ public EncryptionInfo(DirectoryNode dir, EncryptionMode encryptionMode) {
+ this(encryptionMode);
}
+ /**
+ * @deprecated use constructor without fs parameter
+ */
+ @Deprecated
public EncryptionInfo(
POIFSFileSystem fs
, EncryptionMode encryptionMode
@@ -99,10 +166,14 @@ public class EncryptionInfo {
, int keyBits
, int blockSize
, ChainingMode chainingMode
- ) throws EncryptedDocumentException {
- this(fs.getRoot(), encryptionMode, cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode);
+ ) {
+ this(encryptionMode, cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode);
}
+ /**
+ * @deprecated use constructor without fs parameter
+ */
+ @Deprecated
public EncryptionInfo(
NPOIFSFileSystem fs
, EncryptionMode encryptionMode
@@ -111,10 +182,14 @@ public class EncryptionInfo {
, int keyBits
, int blockSize
, ChainingMode chainingMode
- ) throws EncryptedDocumentException {
- this(fs.getRoot(), encryptionMode, cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode);
+ ) {
+ this(encryptionMode, cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode);
}
+ /**
+ * @deprecated use constructor without dir parameter
+ */
+ @Deprecated
public EncryptionInfo(
DirectoryNode dir
, EncryptionMode encryptionMode
@@ -123,7 +198,36 @@ public class EncryptionInfo {
, int keyBits
, int blockSize
, ChainingMode chainingMode
- ) throws EncryptedDocumentException {
+ ) {
+ this(encryptionMode, cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode);
+ }
+
+ public EncryptionInfo(EncryptionMode encryptionMode) {
+ this(encryptionMode, null, null, -1, -1, null);
+ }
+
+ /**
+ * Constructs an EncryptionInfo from scratch
+ *
+ * @param encryptionMode see {@link EncryptionMode} for values, {@link EncryptionMode#cryptoAPI} is for
+ * internal use only, as it's record based
+ * @param cipherAlgorithm
+ * @param hashAlgorithm
+ * @param keyBits
+ * @param blockSize
+ * @param chainingMode
+ *
+ * @throws EncryptedDocumentException if the given parameters mismatch, e.g. only certain combinations
+ * of keyBits, blockSize are allowed for a given {@link CipherAlgorithm}
+ */
+ public EncryptionInfo(
+ EncryptionMode encryptionMode
+ , CipherAlgorithm cipherAlgorithm
+ , HashAlgorithm hashAlgorithm
+ , int keyBits
+ , int blockSize
+ , ChainingMode chainingMode
+ ) {
versionMajor = encryptionMode.versionMajor;
versionMinor = encryptionMode.versionMinor;
encryptionFlags = encryptionMode.encryptionFlags;
diff --git a/src/java/org/apache/poi/poifs/crypt/EncryptionInfoBuilder.java b/src/java/org/apache/poi/poifs/crypt/EncryptionInfoBuilder.java
index 0c31fc8fdc..e36d44da9e 100644
--- a/src/java/org/apache/poi/poifs/crypt/EncryptionInfoBuilder.java
+++ b/src/java/org/apache/poi/poifs/crypt/EncryptionInfoBuilder.java
@@ -18,13 +18,36 @@ package org.apache.poi.poifs.crypt;
import java.io.IOException;
-import org.apache.poi.poifs.filesystem.DocumentInputStream;
+import org.apache.poi.util.LittleEndianInput;
public interface EncryptionInfoBuilder {
- void initialize(EncryptionInfo ei, DocumentInputStream dis) throws IOException;
+ /**
+ * initialize the builder from a stream
+ */
+ void initialize(EncryptionInfo ei, LittleEndianInput dis) throws IOException;
+
+ /**
+ * initialize the builder from scratch
+ */
void initialize(EncryptionInfo ei, CipherAlgorithm cipherAlgorithm, HashAlgorithm hashAlgorithm, int keyBits, int blockSize, ChainingMode chainingMode);
+
+ /**
+ * @return the header data
+ */
EncryptionHeader getHeader();
+
+ /**
+ * @return the verifier data
+ */
EncryptionVerifier getVerifier();
+
+ /**
+ * @return the decryptor
+ */
Decryptor getDecryptor();
+
+ /**
+ * @return the encryptor
+ */
Encryptor getEncryptor();
}
diff --git a/src/java/org/apache/poi/poifs/crypt/EncryptionMode.java b/src/java/org/apache/poi/poifs/crypt/EncryptionMode.java
index 4d9114573f..86f4b8508a 100644
--- a/src/java/org/apache/poi/poifs/crypt/EncryptionMode.java
+++ b/src/java/org/apache/poi/poifs/crypt/EncryptionMode.java
@@ -17,9 +17,24 @@
package org.apache.poi.poifs.crypt;
+import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey;
+
+/**
+ * Office supports various encryption modes.
+ * The encryption is either based on the whole container ({@link #agile}, {@link #standard} or {@link #binaryRC4})
+ * or record based ({@link #cryptoAPI}). The record based encryption can't be accessed directly, but will be
+ * invoked by using the {@link Biff8EncryptionKey#setCurrentUserPassword(String)} before saving the document.
+ */
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);
+ /* @see 2.3.6 Office Binary Document RC4 Encryption */
+ binaryRC4("org.apache.poi.poifs.crypt.binaryrc4.BinaryRC4EncryptionInfoBuilder", 1, 1, 0x0),
+ /* @see 2.3.5 Office Binary Document RC4 CryptoAPI Encryption */
+ cryptoAPI("org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIEncryptionInfoBuilder", 4, 2, 0x04),
+ /* @see 2.3.4.5 \EncryptionInfo Stream (Standard Encryption) */
+ standard("org.apache.poi.poifs.crypt.standard.StandardEncryptionInfoBuilder", 4, 2, 0x24),
+ /* @see 2.3.4.10 \EncryptionInfo Stream (Agile Encryption) */
+ agile("org.apache.poi.poifs.crypt.agile.AgileEncryptionInfoBuilder", 4, 4, 0x40)
+ ;
public final String builder;
public final int versionMajor;
diff --git a/src/java/org/apache/poi/poifs/crypt/EncryptionVerifier.java b/src/java/org/apache/poi/poifs/crypt/EncryptionVerifier.java
index ecb90e08e2..9dafc11bf7 100644
--- a/src/java/org/apache/poi/poifs/crypt/EncryptionVerifier.java
+++ b/src/java/org/apache/poi/poifs/crypt/EncryptionVerifier.java
@@ -41,6 +41,7 @@ public abstract class EncryptionVerifier {
* The method name is misleading - you'll get the encrypted verifier, not the plain verifier
* @deprecated use getEncryptedVerifier()
*/
+ @Deprecated
public byte[] getVerifier() {
return encryptedVerifier;
}
@@ -53,6 +54,7 @@ public abstract class EncryptionVerifier {
* The method name is misleading - you'll get the encrypted verifier hash, not the plain verifier hash
* @deprecated use getEnryptedVerifierHash
*/
+ @Deprecated
public byte[] getVerifierHash() {
return encryptedVerifierHash;
}
@@ -76,6 +78,7 @@ public abstract class EncryptionVerifier {
/**
* @deprecated use getCipherAlgorithm().jceId
*/
+ @Deprecated
public String getAlgorithmName() {
return cipherAlgorithm.jceId;
}
diff --git a/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4Decryptor.java b/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4Decryptor.java
new file mode 100644
index 0000000000..89b2b1f766
--- /dev/null
+++ b/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4Decryptor.java
@@ -0,0 +1,131 @@
+/* ====================================================================
+ 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.binaryrc4;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.util.Arrays;
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+import org.apache.poi.EncryptedDocumentException;
+import org.apache.poi.poifs.crypt.*;
+import org.apache.poi.poifs.filesystem.DirectoryNode;
+import org.apache.poi.poifs.filesystem.DocumentInputStream;
+import org.apache.poi.util.LittleEndian;
+
+public class BinaryRC4Decryptor extends Decryptor {
+ private long _length = -1L;
+
+ private class BinaryRC4CipherInputStream extends ChunkedCipherInputStream {
+
+ protected Cipher initCipherForBlock(Cipher existing, int block)
+ throws GeneralSecurityException {
+ return BinaryRC4Decryptor.initCipherForBlock(existing, block, builder, getSecretKey(), Cipher.DECRYPT_MODE);
+ }
+
+ public BinaryRC4CipherInputStream(DocumentInputStream stream, long size)
+ throws GeneralSecurityException {
+ super(stream, size, 512);
+ }
+ }
+
+ protected BinaryRC4Decryptor(BinaryRC4EncryptionInfoBuilder builder) {
+ super(builder);
+ }
+
+ public boolean verifyPassword(String password) {
+ EncryptionVerifier ver = builder.getVerifier();
+ SecretKey skey = generateSecretKey(password, ver);
+ try {
+ Cipher cipher = initCipherForBlock(null, 0, builder, skey, Cipher.DECRYPT_MODE);
+ byte encryptedVerifier[] = ver.getEncryptedVerifier();
+ byte verifier[] = new byte[encryptedVerifier.length];
+ cipher.update(encryptedVerifier, 0, encryptedVerifier.length, verifier);
+ setVerifier(verifier);
+ byte encryptedVerifierHash[] = ver.getEncryptedVerifierHash();
+ byte verifierHash[] = cipher.doFinal(encryptedVerifierHash);
+ HashAlgorithm hashAlgo = ver.getHashAlgorithm();
+ MessageDigest hashAlg = CryptoFunctions.getMessageDigest(hashAlgo);
+ byte calcVerifierHash[] = hashAlg.digest(verifier);
+ if (Arrays.equals(calcVerifierHash, verifierHash)) {
+ setSecretKey(skey);
+ return true;
+ }
+ } catch (GeneralSecurityException e) {
+ throw new EncryptedDocumentException(e);
+ }
+ return false;
+ }
+
+ protected static Cipher initCipherForBlock(Cipher cipher, int block,
+ EncryptionInfoBuilder builder, SecretKey skey, int encryptMode)
+ throws GeneralSecurityException {
+ EncryptionVerifier ver = builder.getVerifier();
+ HashAlgorithm hashAlgo = ver.getHashAlgorithm();
+ byte blockKey[] = new byte[4];
+ LittleEndian.putUInt(blockKey, 0, block);
+ byte encKey[] = CryptoFunctions.generateKey(skey.getEncoded(), hashAlgo, blockKey, 16);
+ SecretKey key = new SecretKeySpec(encKey, skey.getAlgorithm());
+ if (cipher == null) {
+ EncryptionHeader em = builder.getHeader();
+ cipher = CryptoFunctions.getCipher(key, em.getCipherAlgorithm(), null, null, encryptMode);
+ } else {
+ cipher.init(encryptMode, key);
+ }
+ return cipher;
+ }
+
+ protected static SecretKey generateSecretKey(String password,
+ EncryptionVerifier ver) {
+ if (password.length() > 255)
+ password = password.substring(0, 255);
+ HashAlgorithm hashAlgo = ver.getHashAlgorithm();
+ MessageDigest hashAlg = CryptoFunctions.getMessageDigest(hashAlgo);
+ byte hash[] = hashAlg.digest(CryptoFunctions.getUtf16LeString(password));
+ byte salt[] = ver.getSalt();
+ hashAlg.reset();
+ for (int i = 0; i < 16; i++) {
+ hashAlg.update(hash, 0, 5);
+ hashAlg.update(salt);
+ }
+
+ hash = new byte[5];
+ System.arraycopy(hashAlg.digest(), 0, hash, 0, 5);
+ SecretKey skey = new SecretKeySpec(hash, ver.getCipherAlgorithm().jceId);
+ return skey;
+ }
+
+ public InputStream getDataStream(DirectoryNode dir) throws IOException,
+ GeneralSecurityException {
+ DocumentInputStream dis = dir.createDocumentInputStream("EncryptedPackage");
+ _length = dis.readLong();
+ BinaryRC4CipherInputStream cipherStream = new BinaryRC4CipherInputStream(dis, _length);
+ return cipherStream;
+ }
+
+ public long getLength() {
+ if (_length == -1L) {
+ throw new IllegalStateException("Decryptor.getDataStream() was not called");
+ }
+
+ return _length;
+ }
+}
\ No newline at end of file
diff --git a/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4EncryptionHeader.java b/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4EncryptionHeader.java
new file mode 100644
index 0000000000..1b811a1031
--- /dev/null
+++ b/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4EncryptionHeader.java
@@ -0,0 +1,44 @@
+/* ====================================================================
+ 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.binaryrc4;
+
+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.crypt.standard.EncryptionRecord;
+import org.apache.poi.util.LittleEndianByteArrayOutputStream;
+
+public class BinaryRC4EncryptionHeader extends EncryptionHeader implements
+ EncryptionRecord {
+
+ protected BinaryRC4EncryptionHeader() {
+ setCipherAlgorithm(CipherAlgorithm.rc4);
+ setKeySize(40);
+ setBlockSize(-1);
+ setCipherProvider(CipherProvider.rc4);
+ setHashAlgorithm(HashAlgorithm.md5);
+ setSizeExtra(0);
+ setFlags(0);
+ setCspName("");
+ setChainingMode(null);
+ }
+
+ public void write(LittleEndianByteArrayOutputStream littleendianbytearrayoutputstream) {
+ }
+}
diff --git a/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4EncryptionInfoBuilder.java b/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4EncryptionInfoBuilder.java
new file mode 100644
index 0000000000..10bf58d83b
--- /dev/null
+++ b/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4EncryptionInfoBuilder.java
@@ -0,0 +1,77 @@
+/* ====================================================================
+ 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.binaryrc4;
+
+import java.io.IOException;
+import org.apache.poi.poifs.crypt.*;
+import org.apache.poi.util.LittleEndianInput;
+
+public class BinaryRC4EncryptionInfoBuilder implements EncryptionInfoBuilder {
+
+ EncryptionInfo info;
+ BinaryRC4EncryptionHeader header;
+ BinaryRC4EncryptionVerifier verifier;
+ BinaryRC4Decryptor decryptor;
+ BinaryRC4Encryptor encryptor;
+
+ public BinaryRC4EncryptionInfoBuilder() {
+ }
+
+ public void initialize(EncryptionInfo info, LittleEndianInput dis)
+ throws IOException {
+ this.info = info;
+ int vMajor = info.getVersionMajor();
+ int vMinor = info.getVersionMinor();
+ assert (vMajor == 1 && vMinor == 1);
+
+ header = new BinaryRC4EncryptionHeader();
+ verifier = new BinaryRC4EncryptionVerifier(dis);
+ decryptor = new BinaryRC4Decryptor(this);
+ encryptor = new BinaryRC4Encryptor(this);
+ }
+
+ public void initialize(EncryptionInfo info,
+ CipherAlgorithm cipherAlgorithm, HashAlgorithm hashAlgorithm,
+ int keyBits, int blockSize, ChainingMode chainingMode) {
+ this.info = info;
+ header = new BinaryRC4EncryptionHeader();
+ verifier = new BinaryRC4EncryptionVerifier();
+ decryptor = new BinaryRC4Decryptor(this);
+ encryptor = new BinaryRC4Encryptor(this);
+ }
+
+ public BinaryRC4EncryptionHeader getHeader() {
+ return header;
+ }
+
+ public BinaryRC4EncryptionVerifier getVerifier() {
+ return verifier;
+ }
+
+ public BinaryRC4Decryptor getDecryptor() {
+ return decryptor;
+ }
+
+ public BinaryRC4Encryptor getEncryptor() {
+ return encryptor;
+ }
+
+ public EncryptionInfo getEncryptionInfo() {
+ return info;
+ }
+}
diff --git a/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4EncryptionVerifier.java b/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4EncryptionVerifier.java
new file mode 100644
index 0000000000..86cf4ac184
--- /dev/null
+++ b/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4EncryptionVerifier.java
@@ -0,0 +1,81 @@
+/* ====================================================================
+ 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.binaryrc4;
+
+import org.apache.poi.EncryptedDocumentException;
+import org.apache.poi.poifs.crypt.*;
+import org.apache.poi.poifs.crypt.standard.EncryptionRecord;
+import org.apache.poi.util.LittleEndianByteArrayOutputStream;
+import org.apache.poi.util.LittleEndianInput;
+
+public class BinaryRC4EncryptionVerifier extends EncryptionVerifier implements EncryptionRecord {
+
+ protected BinaryRC4EncryptionVerifier() {
+ setSpinCount(-1);
+ setCipherAlgorithm(CipherAlgorithm.rc4);
+ setChainingMode(null);
+ setEncryptedKey(null);
+ setHashAlgorithm(HashAlgorithm.md5);
+ }
+
+ protected BinaryRC4EncryptionVerifier(LittleEndianInput is) {
+ byte salt[] = new byte[16];
+ is.readFully(salt);
+ setSalt(salt);
+ byte encryptedVerifier[] = new byte[16];
+ is.readFully(encryptedVerifier);
+ setEncryptedVerifier(encryptedVerifier);
+ byte encryptedVerifierHash[] = new byte[16];
+ is.readFully(encryptedVerifierHash);
+ setEncryptedVerifierHash(encryptedVerifierHash);
+ setSpinCount(-1);
+ setCipherAlgorithm(CipherAlgorithm.rc4);
+ setChainingMode(null);
+ setEncryptedKey(null);
+ setHashAlgorithm(HashAlgorithm.md5);
+ }
+
+ protected void setSalt(byte salt[]) {
+ if (salt == null || salt.length != 16) {
+ throw new EncryptedDocumentException("invalid verifier salt");
+ }
+
+ super.setSalt(salt);
+ }
+
+ protected void setEncryptedVerifier(byte encryptedVerifier[]) {
+ super.setEncryptedVerifier(encryptedVerifier);
+ }
+
+ protected void setEncryptedVerifierHash(byte encryptedVerifierHash[]) {
+ super.setEncryptedVerifierHash(encryptedVerifierHash);
+ }
+
+ public void write(LittleEndianByteArrayOutputStream bos) {
+ byte salt[] = getSalt();
+ assert (salt.length == 16);
+ bos.write(salt);
+ byte encryptedVerifier[] = getEncryptedVerifier();
+ assert (encryptedVerifier.length == 16);
+ bos.write(encryptedVerifier);
+ byte encryptedVerifierHash[] = getEncryptedVerifierHash();
+ assert (encryptedVerifierHash.length == 16);
+ bos.write(encryptedVerifierHash);
+ }
+
+}
diff --git a/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4Encryptor.java b/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4Encryptor.java
new file mode 100644
index 0000000000..2cf2d93347
--- /dev/null
+++ b/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4Encryptor.java
@@ -0,0 +1,127 @@
+/* ====================================================================
+ 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.binaryrc4;
+
+import java.io.File;
+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.SecretKey;
+
+import org.apache.poi.EncryptedDocumentException;
+import org.apache.poi.poifs.crypt.ChunkedCipherOutputStream;
+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.Encryptor;
+import org.apache.poi.poifs.crypt.standard.EncryptionRecord;
+import org.apache.poi.poifs.filesystem.DirectoryNode;
+import org.apache.poi.util.LittleEndianByteArrayOutputStream;
+
+public class BinaryRC4Encryptor extends Encryptor {
+
+ private final BinaryRC4EncryptionInfoBuilder builder;
+
+ protected class BinaryRC4CipherOutputStream extends ChunkedCipherOutputStream {
+
+ protected Cipher initCipherForBlock(Cipher cipher, int block, boolean lastChunk)
+ throws GeneralSecurityException {
+ return BinaryRC4Decryptor.initCipherForBlock(cipher, block, builder, getSecretKey(), Cipher.ENCRYPT_MODE);
+ }
+
+ protected void calculateChecksum(File file, int i) {
+ }
+
+ protected void createEncryptionInfoEntry(DirectoryNode dir, File tmpFile)
+ throws IOException, GeneralSecurityException {
+ BinaryRC4Encryptor.this.createEncryptionInfoEntry(dir);
+ }
+
+ public BinaryRC4CipherOutputStream(DirectoryNode dir)
+ throws IOException, GeneralSecurityException {
+ super(dir, 512);
+ }
+ }
+
+ protected BinaryRC4Encryptor(BinaryRC4EncryptionInfoBuilder builder) {
+ this.builder = builder;
+ }
+
+ public void confirmPassword(String password) {
+ Random r = new SecureRandom();
+ byte salt[] = new byte[16];
+ byte verifier[] = new byte[16];
+ r.nextBytes(salt);
+ r.nextBytes(verifier);
+ confirmPassword(password, null, null, verifier, salt, null);
+ }
+
+ public void confirmPassword(String password, byte keySpec[],
+ byte keySalt[], byte verifier[], byte verifierSalt[],
+ byte integritySalt[]) {
+ BinaryRC4EncryptionVerifier ver = builder.getVerifier();
+ ver.setSalt(verifierSalt);
+ SecretKey skey = BinaryRC4Decryptor.generateSecretKey(password, ver);
+ setSecretKey(skey);
+ try {
+ Cipher cipher = BinaryRC4Decryptor.initCipherForBlock(null, 0, builder, skey, Cipher.ENCRYPT_MODE);
+ byte encryptedVerifier[] = new byte[16];
+ cipher.update(verifier, 0, 16, encryptedVerifier);
+ ver.setEncryptedVerifier(encryptedVerifier);
+ org.apache.poi.poifs.crypt.HashAlgorithm hashAlgo = ver
+ .getHashAlgorithm();
+ MessageDigest hashAlg = CryptoFunctions.getMessageDigest(hashAlgo);
+ byte calcVerifierHash[] = hashAlg.digest(verifier);
+ byte encryptedVerifierHash[] = cipher.doFinal(calcVerifierHash);
+ ver.setEncryptedVerifierHash(encryptedVerifierHash);
+ } catch (GeneralSecurityException e) {
+ throw new EncryptedDocumentException("Password confirmation failed", e);
+ }
+ }
+
+ public OutputStream getDataStream(DirectoryNode dir)
+ throws IOException, GeneralSecurityException {
+ OutputStream countStream = new BinaryRC4CipherOutputStream(dir);
+ return countStream;
+ }
+
+ protected int getKeySizeInBytes() {
+ return builder.getHeader().getKeySize() / 8;
+ }
+
+ protected void createEncryptionInfoEntry(DirectoryNode dir) throws IOException {
+ DataSpaceMapUtils.addDefaultDataSpace(dir);
+ final EncryptionInfo info = builder.getEncryptionInfo();
+ final BinaryRC4EncryptionHeader header = builder.getHeader();
+ final BinaryRC4EncryptionVerifier verifier = builder.getVerifier();
+ EncryptionRecord er = new EncryptionRecord() {
+ public void write(LittleEndianByteArrayOutputStream bos) {
+ bos.writeShort(info.getVersionMajor());
+ bos.writeShort(info.getVersionMinor());
+ header.write(bos);
+ verifier.write(bos);
+ }
+ };
+ DataSpaceMapUtils.createEncryptionEntry(dir, "EncryptionInfo", er);
+ }
+}
diff --git a/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIDecryptor.java b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIDecryptor.java
new file mode 100644
index 0000000000..7042adffec
--- /dev/null
+++ b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIDecryptor.java
@@ -0,0 +1,259 @@
+/* ====================================================================
+ 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.cryptoapi;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.util.Arrays;
+
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.ShortBufferException;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.apache.poi.EncryptedDocumentException;
+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.EncryptionInfoBuilder;
+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.poifs.filesystem.DocumentNode;
+import org.apache.poi.poifs.filesystem.POIFSFileSystem;
+import org.apache.poi.util.BitField;
+import org.apache.poi.util.BitFieldFactory;
+import org.apache.poi.util.BoundedInputStream;
+import org.apache.poi.util.IOUtils;
+import org.apache.poi.util.LittleEndian;
+import org.apache.poi.util.LittleEndianInputStream;
+
+public class CryptoAPIDecryptor extends Decryptor {
+
+ private long _length;
+
+ private class SeekableByteArrayInputStream extends ByteArrayInputStream {
+ Cipher cipher;
+ byte oneByte[] = { 0 };
+
+ public void seek(int pos) {
+ if (pos > count) {
+ throw new ArrayIndexOutOfBoundsException(pos);
+ }
+
+ this.pos = pos;
+ mark = pos;
+ }
+
+ public void setBlock(int block) throws GeneralSecurityException {
+ cipher = initCipherForBlock(cipher, block);
+ }
+
+ public synchronized int read() {
+ int ch = super.read();
+ if (ch == -1) return -1;
+ oneByte[0] = (byte) ch;
+ try {
+ cipher.update(oneByte, 0, 1, oneByte);
+ } catch (ShortBufferException e) {
+ throw new EncryptedDocumentException(e);
+ }
+ return oneByte[0];
+ }
+
+ public synchronized int read(byte b[], int off, int len) {
+ int readLen = super.read(b, off, len);
+ if (readLen ==-1) return -1;
+ try {
+ cipher.update(b, off, readLen, b, off);
+ } catch (ShortBufferException e) {
+ throw new EncryptedDocumentException(e);
+ }
+ return readLen;
+ }
+
+ public SeekableByteArrayInputStream(byte buf[])
+ throws GeneralSecurityException {
+ super(buf);
+ cipher = initCipherForBlock(null, 0);
+ }
+ }
+
+ static class StreamDescriptorEntry {
+ static BitField flagStream = BitFieldFactory.getInstance(1);
+
+ int streamOffset;
+ int streamSize;
+ int block;
+ int flags;
+ int reserved2;
+ String streamName;
+ }
+
+ protected CryptoAPIDecryptor(CryptoAPIEncryptionInfoBuilder builder) {
+ super(builder);
+ _length = -1L;
+ }
+
+ public boolean verifyPassword(String password) {
+ EncryptionVerifier ver = builder.getVerifier();
+ SecretKey skey = generateSecretKey(password, ver);
+ try {
+ Cipher cipher = initCipherForBlock(null, 0, builder, skey, Cipher.DECRYPT_MODE);
+ byte encryptedVerifier[] = ver.getEncryptedVerifier();
+ byte verifier[] = new byte[encryptedVerifier.length];
+ cipher.update(encryptedVerifier, 0, encryptedVerifier.length, verifier);
+ setVerifier(verifier);
+ byte encryptedVerifierHash[] = ver.getEncryptedVerifierHash();
+ byte verifierHash[] = cipher.doFinal(encryptedVerifierHash);
+ HashAlgorithm hashAlgo = ver.getHashAlgorithm();
+ MessageDigest hashAlg = CryptoFunctions.getMessageDigest(hashAlgo);
+ byte calcVerifierHash[] = hashAlg.digest(verifier);
+ if (Arrays.equals(calcVerifierHash, verifierHash)) {
+ setSecretKey(skey);
+ return true;
+ }
+ } catch (GeneralSecurityException e) {
+ throw new EncryptedDocumentException(e);
+ }
+ return false;
+ }
+
+ /**
+ * Initializes a cipher object for a given block index for decryption
+ *
+ * @param cipher may be null, otherwise the given instance is reset to the new block index
+ * @param block the block index, e.g. the persist/slide id (hslf)
+ * @return a new cipher object, if cipher was null, otherwise the reinitialized cipher
+ * @throws GeneralSecurityException
+ */
+ public Cipher initCipherForBlock(Cipher cipher, int block)
+ throws GeneralSecurityException {
+ return initCipherForBlock(cipher, block, builder, getSecretKey(), Cipher.DECRYPT_MODE);
+ }
+
+ protected static Cipher initCipherForBlock(Cipher cipher, int block,
+ EncryptionInfoBuilder builder, SecretKey skey, int encryptMode)
+ throws GeneralSecurityException {
+ EncryptionVerifier ver = builder.getVerifier();
+ HashAlgorithm hashAlgo = ver.getHashAlgorithm();
+ byte blockKey[] = new byte[4];
+ LittleEndian.putUInt(blockKey, 0, block);
+ MessageDigest hashAlg = CryptoFunctions.getMessageDigest(hashAlgo);
+ hashAlg.update(skey.getEncoded());
+ byte encKey[] = hashAlg.digest(blockKey);
+ EncryptionHeader header = builder.getHeader();
+ int keyBits = header.getKeySize();
+ encKey = CryptoFunctions.getBlock0(encKey, keyBits / 8);
+ if (keyBits == 40) {
+ encKey = CryptoFunctions.getBlock0(encKey, 16);
+ }
+ SecretKey key = new SecretKeySpec(encKey, skey.getAlgorithm());
+ if (cipher == null) {
+ cipher = CryptoFunctions.getCipher(key, header.getCipherAlgorithm(), null, null, encryptMode);
+ } else {
+ cipher.init(encryptMode, key);
+ }
+ return cipher;
+ }
+
+ protected static SecretKey generateSecretKey(String password, EncryptionVerifier ver) {
+ if (password.length() > 255) {
+ password = password.substring(0, 255);
+ }
+ HashAlgorithm hashAlgo = ver.getHashAlgorithm();
+ MessageDigest hashAlg = CryptoFunctions.getMessageDigest(hashAlgo);
+ hashAlg.update(ver.getSalt());
+ byte hash[] = hashAlg.digest(CryptoFunctions.getUtf16LeString(password));
+ SecretKey skey = new SecretKeySpec(hash, ver.getCipherAlgorithm().jceId);
+ return skey;
+ }
+
+ /**
+ * Decrypt the Document-/SummaryInformation and other optionally streams.
+ * Opposed to other crypto modes, cryptoapi is record based and can't be used
+ * to stream-decrypt a whole file
+ *
+ * @see 2.3.5.4 RC4 CryptoAPI Encrypted Summary Stream
+ */
+ @SuppressWarnings("unused")
+ public InputStream getDataStream(DirectoryNode dir)
+ throws IOException, GeneralSecurityException {
+ POIFSFileSystem fsOut = new POIFSFileSystem();
+ DocumentNode es = (DocumentNode) dir.getEntry("EncryptedSummary");
+ DocumentInputStream dis = dir.createDocumentInputStream(es);
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ IOUtils.copy(dis, bos);
+ dis.close();
+ SeekableByteArrayInputStream sbis = new SeekableByteArrayInputStream(bos.toByteArray());
+ LittleEndianInputStream leis = new LittleEndianInputStream(sbis);
+ int streamDescriptorArrayOffset = (int) leis.readUInt();
+ int streamDescriptorArraySize = (int) leis.readUInt();
+ sbis.skip(streamDescriptorArrayOffset - 8);
+ sbis.setBlock(0);
+ int encryptedStreamDescriptorCount = (int) leis.readUInt();
+ StreamDescriptorEntry entries[] = new StreamDescriptorEntry[encryptedStreamDescriptorCount];
+ for (int i = 0; i < encryptedStreamDescriptorCount; i++) {
+ StreamDescriptorEntry entry = new StreamDescriptorEntry();
+ entries[i] = entry;
+ entry.streamOffset = (int) leis.readUInt();
+ entry.streamSize = (int) leis.readUInt();
+ entry.block = leis.readUShort();
+ int nameSize = leis.readUByte();
+ entry.flags = leis.readUByte();
+ boolean isStream = StreamDescriptorEntry.flagStream.isSet(entry.flags);
+ entry.reserved2 = leis.readInt();
+ byte nameBuf[] = new byte[nameSize * 2];
+ leis.read(nameBuf);
+ entry.streamName = new String(nameBuf, Charset.forName("UTF-16LE"));
+ leis.readShort();
+ assert(entry.streamName.length() == nameSize);
+ }
+
+ for (StreamDescriptorEntry entry : entries) {
+ sbis.seek(entry.streamOffset);
+ sbis.setBlock(entry.block);
+ InputStream is = new BoundedInputStream(sbis, entry.streamSize);
+ fsOut.createDocument(is, entry.streamName);
+ }
+
+ leis.close();
+ sbis = null;
+ bos.reset();
+ fsOut.writeFilesystem(bos);
+ _length = bos.size();
+ ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
+ return bis;
+ }
+
+ /**
+ * @return the length of the stream returned by {@link #getDataStream(DirectoryNode)}
+ */
+ public long getLength() {
+ if (_length == -1L) {
+ throw new IllegalStateException("Decryptor.getDataStream() was not called");
+ }
+ return _length;
+ }
+}
diff --git a/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptionHeader.java b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptionHeader.java
new file mode 100644
index 0000000000..151b6588ae
--- /dev/null
+++ b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptionHeader.java
@@ -0,0 +1,62 @@
+/* ====================================================================
+ 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.cryptoapi;
+
+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.CipherProvider;
+import org.apache.poi.poifs.crypt.HashAlgorithm;
+import org.apache.poi.poifs.crypt.standard.StandardEncryptionHeader;
+import org.apache.poi.util.LittleEndianInput;
+
+public class CryptoAPIEncryptionHeader extends StandardEncryptionHeader {
+
+ public CryptoAPIEncryptionHeader(LittleEndianInput is) throws IOException {
+ super(is);
+ }
+
+ protected CryptoAPIEncryptionHeader(CipherAlgorithm cipherAlgorithm,
+ HashAlgorithm hashAlgorithm, int keyBits, int blockSize,
+ ChainingMode chainingMode) {
+ super(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode);
+ }
+
+ public void setKeySize(int keyBits) {
+ // Microsoft Base Cryptographic Provider is limited up to 40 bits
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375599(v=vs.85).aspx
+ boolean found = false;
+ for (int size : getCipherAlgorithm().allowedKeySize) {
+ if (size == keyBits) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ throw new EncryptedDocumentException("invalid keysize "+keyBits+" for cipher algorithm "+getCipherAlgorithm());
+ }
+ super.setKeySize(keyBits);
+ if (keyBits > 40) {
+ setCspName("Microsoft Enhanced Cryptographic Provider v1.0");
+ } else {
+ setCspName(CipherProvider.rc4.cipherProviderName);
+ }
+ }
+}
diff --git a/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptionInfoBuilder.java b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptionInfoBuilder.java
new file mode 100644
index 0000000000..2a8a872642
--- /dev/null
+++ b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptionInfoBuilder.java
@@ -0,0 +1,86 @@
+/* ====================================================================
+ 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.cryptoapi;
+
+import java.io.IOException;
+
+import org.apache.poi.poifs.crypt.*;
+import org.apache.poi.util.LittleEndianInput;
+
+public class CryptoAPIEncryptionInfoBuilder implements EncryptionInfoBuilder {
+ EncryptionInfo info;
+ CryptoAPIEncryptionHeader header;
+ CryptoAPIEncryptionVerifier verifier;
+ CryptoAPIDecryptor decryptor;
+ CryptoAPIEncryptor encryptor;
+
+ public CryptoAPIEncryptionInfoBuilder() {
+ }
+
+ /**
+ * initialize the builder from a stream
+ */
+ @SuppressWarnings("unused")
+ public void initialize(EncryptionInfo info, LittleEndianInput dis)
+ throws IOException {
+ this.info = info;
+ int hSize = dis.readInt();
+ header = new CryptoAPIEncryptionHeader(dis);
+ verifier = new CryptoAPIEncryptionVerifier(dis, header);
+ decryptor = new CryptoAPIDecryptor(this);
+ encryptor = new CryptoAPIEncryptor(this);
+ }
+
+ /**
+ * initialize the builder from scratch
+ */
+ 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 (keyBits == -1) keyBits = 0x28;
+ assert(cipherAlgorithm == CipherAlgorithm.rc4 && hashAlgorithm == HashAlgorithm.sha1);
+
+ header = new CryptoAPIEncryptionHeader(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode);
+ verifier = new CryptoAPIEncryptionVerifier(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode);
+ decryptor = new CryptoAPIDecryptor(this);
+ encryptor = new CryptoAPIEncryptor(this);
+ }
+
+ public CryptoAPIEncryptionHeader getHeader() {
+ return header;
+ }
+
+ public CryptoAPIEncryptionVerifier getVerifier() {
+ return verifier;
+ }
+
+ public CryptoAPIDecryptor getDecryptor() {
+ return decryptor;
+ }
+
+ public CryptoAPIEncryptor getEncryptor() {
+ return encryptor;
+ }
+
+ public EncryptionInfo getEncryptionInfo() {
+ return info;
+ }
+}
diff --git a/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptionVerifier.java b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptionVerifier.java
new file mode 100644
index 0000000000..160d1f9f91
--- /dev/null
+++ b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptionVerifier.java
@@ -0,0 +1,50 @@
+/* ====================================================================
+ 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.cryptoapi;
+
+import org.apache.poi.poifs.crypt.ChainingMode;
+import org.apache.poi.poifs.crypt.CipherAlgorithm;
+import org.apache.poi.poifs.crypt.HashAlgorithm;
+import org.apache.poi.poifs.crypt.standard.StandardEncryptionVerifier;
+import org.apache.poi.util.LittleEndianInput;
+
+public class CryptoAPIEncryptionVerifier extends StandardEncryptionVerifier {
+
+ protected CryptoAPIEncryptionVerifier(LittleEndianInput is,
+ CryptoAPIEncryptionHeader header) {
+ super(is, header);
+ }
+
+ protected CryptoAPIEncryptionVerifier(CipherAlgorithm cipherAlgorithm,
+ HashAlgorithm hashAlgorithm, int keyBits, int blockSize,
+ ChainingMode chainingMode) {
+ super(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode);
+ }
+
+ protected void setSalt(byte salt[]) {
+ super.setSalt(salt);
+ }
+
+ protected void setEncryptedVerifier(byte encryptedVerifier[]) {
+ super.setEncryptedVerifier(encryptedVerifier);
+ }
+
+ protected void setEncryptedVerifierHash(byte encryptedVerifierHash[]) {
+ super.setEncryptedVerifierHash(encryptedVerifierHash);
+ }
+}
diff --git a/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptor.java b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptor.java
new file mode 100644
index 0000000000..d237f50634
--- /dev/null
+++ b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptor.java
@@ -0,0 +1,255 @@
+/* ====================================================================
+ 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.cryptoapi;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+
+import org.apache.poi.EncryptedDocumentException;
+import org.apache.poi.hpsf.DocumentSummaryInformation;
+import org.apache.poi.hpsf.PropertySetFactory;
+import org.apache.poi.hpsf.SummaryInformation;
+import org.apache.poi.hpsf.WritingNotSupportedException;
+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.Encryptor;
+import org.apache.poi.poifs.crypt.HashAlgorithm;
+import org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIDecryptor.StreamDescriptorEntry;
+import org.apache.poi.poifs.crypt.standard.EncryptionRecord;
+import org.apache.poi.poifs.filesystem.DirectoryNode;
+import org.apache.poi.poifs.filesystem.DocumentInputStream;
+import org.apache.poi.util.IOUtils;
+import org.apache.poi.util.LittleEndian;
+import org.apache.poi.util.LittleEndianByteArrayOutputStream;
+
+public class CryptoAPIEncryptor extends Encryptor {
+ private final CryptoAPIEncryptionInfoBuilder builder;
+
+ protected CryptoAPIEncryptor(CryptoAPIEncryptionInfoBuilder builder) {
+ this.builder = builder;
+ }
+
+ public void confirmPassword(String password) {
+ Random r = new SecureRandom();
+ byte salt[] = new byte[16];
+ byte verifier[] = new byte[16];
+ r.nextBytes(salt);
+ r.nextBytes(verifier);
+ confirmPassword(password, null, null, verifier, salt, null);
+ }
+
+ public void confirmPassword(String password, byte keySpec[],
+ byte keySalt[], byte verifier[], byte verifierSalt[],
+ byte integritySalt[]) {
+ assert(verifier != null && verifierSalt != null);
+ CryptoAPIEncryptionVerifier ver = builder.getVerifier();
+ ver.setSalt(verifierSalt);
+ SecretKey skey = CryptoAPIDecryptor.generateSecretKey(password, ver);
+ setSecretKey(skey);
+ try {
+ Cipher cipher = initCipherForBlock(null, 0);
+ byte encryptedVerifier[] = new byte[verifier.length];
+ cipher.update(verifier, 0, verifier.length, encryptedVerifier);
+ ver.setEncryptedVerifier(encryptedVerifier);
+ HashAlgorithm hashAlgo = ver.getHashAlgorithm();
+ MessageDigest hashAlg = CryptoFunctions.getMessageDigest(hashAlgo);
+ byte calcVerifierHash[] = hashAlg.digest(verifier);
+ byte encryptedVerifierHash[] = cipher.doFinal(calcVerifierHash);
+ ver.setEncryptedVerifierHash(encryptedVerifierHash);
+ } catch (GeneralSecurityException e) {
+ throw new EncryptedDocumentException("Password confirmation failed", e);
+ }
+ }
+
+ /**
+ * Initializes a cipher object for a given block index for encryption
+ *
+ * @param cipher may be null, otherwise the given instance is reset to the new block index
+ * @param block the block index, e.g. the persist/slide id (hslf)
+ * @return a new cipher object, if cipher was null, otherwise the reinitialized cipher
+ * @throws GeneralSecurityException
+ */
+ public Cipher initCipherForBlock(Cipher cipher, int block)
+ throws GeneralSecurityException {
+ return CryptoAPIDecryptor.initCipherForBlock(cipher, block, builder, getSecretKey(), Cipher.ENCRYPT_MODE);
+ }
+
+ /**
+ * Encrypt the Document-/SummaryInformation and other optionally streams.
+ * Opposed to other crypto modes, cryptoapi is record based and can't be used
+ * to stream-encrypt a whole file
+ *
+ * @see 2.3.5.4 RC4 CryptoAPI Encrypted Summary Stream
+ */
+ public OutputStream getDataStream(DirectoryNode dir)
+ throws IOException, GeneralSecurityException {
+ CipherByteArrayOutputStream bos = new CipherByteArrayOutputStream();
+ byte buf[] = new byte[8];
+
+ bos.write(buf, 0, 8); // skip header
+ String entryNames[] = {
+ SummaryInformation.DEFAULT_STREAM_NAME,
+ DocumentSummaryInformation.DEFAULT_STREAM_NAME
+ };
+
+ List descList = new ArrayList();
+
+ int block = 0;
+ for (String entryName : entryNames) {
+ if (!dir.hasEntry(entryName)) continue;
+ StreamDescriptorEntry descEntry = new StreamDescriptorEntry();
+ descEntry.block = block;
+ descEntry.streamOffset = bos.size();
+ descEntry.streamName = entryName;
+ descEntry.flags = StreamDescriptorEntry.flagStream.setValue(0, 1);
+ descEntry.reserved2 = 0;
+
+ bos.setBlock(block);
+ DocumentInputStream dis = dir.createDocumentInputStream(entryName);
+ IOUtils.copy(dis, bos);
+ dis.close();
+
+ descEntry.streamSize = bos.size() - descEntry.streamOffset;
+ descList.add(descEntry);
+
+ dir.getEntry(entryName).delete();
+
+ block++;
+ }
+
+ int streamDescriptorArrayOffset = bos.size();
+
+ bos.setBlock(0);
+ LittleEndian.putUInt(buf, 0, descList.size());
+ bos.write(buf, 0, 4);
+
+ for (StreamDescriptorEntry sde : descList) {
+ LittleEndian.putUInt(buf, 0, sde.streamOffset);
+ bos.write(buf, 0, 4);
+ LittleEndian.putUInt(buf, 0, sde.streamSize);
+ bos.write(buf, 0, 4);
+ LittleEndian.putUShort(buf, 0, sde.block);
+ bos.write(buf, 0, 2);
+ LittleEndian.putUByte(buf, 0, (short)sde.streamName.length());
+ bos.write(buf, 0, 1);
+ LittleEndian.putUByte(buf, 0, (short)sde.flags);
+ bos.write(buf, 0, 1);
+ LittleEndian.putUInt(buf, 0, sde.reserved2);
+ bos.write(buf, 0, 4);
+ byte nameBytes[] = sde.streamName.getBytes(Charset.forName("UTF-16LE"));
+ bos.write(nameBytes, 0, nameBytes.length);
+ LittleEndian.putShort(buf, 0, (short)0); // null-termination
+ bos.write(buf, 0, 2);
+ }
+
+ int savedSize = bos.size();
+ int streamDescriptorArraySize = savedSize - streamDescriptorArrayOffset;
+ LittleEndian.putUInt(buf, 0, streamDescriptorArrayOffset);
+ LittleEndian.putUInt(buf, 4, streamDescriptorArraySize);
+
+ bos.reset();
+ bos.setBlock(0);
+ bos.write(buf, 0, 8);
+ bos.setSize(savedSize);
+
+ dir.createDocument("EncryptedSummary", new ByteArrayInputStream(bos.getBuf(), 0, savedSize));
+ DocumentSummaryInformation dsi = PropertySetFactory.newDocumentSummaryInformation();
+
+ try {
+ dsi.write(dir, DocumentSummaryInformation.DEFAULT_STREAM_NAME);
+ } catch (WritingNotSupportedException e) {
+ throw new IOException(e);
+ }
+
+ return bos;
+ }
+
+ protected int getKeySizeInBytes() {
+ return builder.getHeader().getKeySize() / 8;
+ }
+
+ protected void createEncryptionInfoEntry(DirectoryNode dir) throws IOException {
+ DataSpaceMapUtils.addDefaultDataSpace(dir);
+ final EncryptionInfo info = builder.getEncryptionInfo();
+ final CryptoAPIEncryptionHeader header = builder.getHeader();
+ final CryptoAPIEncryptionVerifier verifier = builder.getVerifier();
+ EncryptionRecord er = new EncryptionRecord() {
+ public void write(LittleEndianByteArrayOutputStream bos) {
+ bos.writeShort(info.getVersionMajor());
+ bos.writeShort(info.getVersionMinor());
+ header.write(bos);
+ verifier.write(bos);
+ }
+ };
+ DataSpaceMapUtils.createEncryptionEntry(dir, "EncryptionInfo", er);
+ }
+
+ private class CipherByteArrayOutputStream extends ByteArrayOutputStream {
+ Cipher cipher;
+ byte oneByte[] = { 0 };
+
+ public CipherByteArrayOutputStream() throws GeneralSecurityException {
+ setBlock(0);
+ }
+
+ public byte[] getBuf() {
+ return buf;
+ }
+
+ public void setSize(int count) {
+ this.count = count;
+ }
+
+ public void setBlock(int block) throws GeneralSecurityException {
+ cipher = initCipherForBlock(cipher, block);
+ }
+
+ public void write(int b) {
+ try {
+ oneByte[0] = (byte)b;
+ cipher.update(oneByte, 0, 1, oneByte, 0);
+ super.write(oneByte);
+ } catch (Exception e) {
+ throw new EncryptedDocumentException(e);
+ }
+ }
+
+ public void write(byte[] b, int off, int len) {
+ try {
+ cipher.update(b, off, len, b, off);
+ super.write(b, off, len);
+ } catch (Exception e) {
+ throw new EncryptedDocumentException(e);
+ }
+ }
+
+ }
+}
diff --git a/src/java/org/apache/poi/poifs/crypt/standard/StandardDecryptor.java b/src/java/org/apache/poi/poifs/crypt/standard/StandardDecryptor.java
index 86e31fb7a2..2b2c75b520 100644
--- a/src/java/org/apache/poi/poifs/crypt/standard/StandardDecryptor.java
+++ b/src/java/org/apache/poi/poifs/crypt/standard/StandardDecryptor.java
@@ -34,7 +34,7 @@ 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.EncryptionInfoBuilder;
import org.apache.poi.poifs.crypt.EncryptionVerifier;
import org.apache.poi.poifs.crypt.HashAlgorithm;
import org.apache.poi.poifs.filesystem.DirectoryNode;
@@ -47,12 +47,12 @@ import org.apache.poi.util.LittleEndian;
public class StandardDecryptor extends Decryptor {
private long _length = -1;
- protected StandardDecryptor(EncryptionInfo info) {
- super(info);
+ protected StandardDecryptor(EncryptionInfoBuilder builder) {
+ super(builder);
}
public boolean verifyPassword(String password) {
- EncryptionVerifier ver = info.getVerifier();
+ EncryptionVerifier ver = builder.getVerifier();
SecretKey skey = generateSecretKey(password, ver, getKeySizeInBytes());
Cipher cipher = getCipher(skey);
@@ -64,7 +64,11 @@ public class StandardDecryptor extends Decryptor {
byte[] calcVerifierHash = sha1.digest(verifier);
byte encryptedVerifierHash[] = ver.getEncryptedVerifierHash();
byte decryptedVerifierHash[] = cipher.doFinal(encryptedVerifierHash);
- byte[] verifierHash = truncateOrPad(decryptedVerifierHash, calcVerifierHash.length);
+
+ // see 2.3.4.9 Password Verification (Standard Encryption)
+ // ... The number of bytes used by the encrypted Verifier hash MUST be 32 ...
+ // TODO: check and trim/pad the hashes to 32
+ byte[] verifierHash = Arrays.copyOf(decryptedVerifierHash, calcVerifierHash.length);
if (Arrays.equals(calcVerifierHash, verifierHash)) {
setSecretKey(skey);
@@ -93,7 +97,7 @@ public class StandardDecryptor extends Decryptor {
System.arraycopy(x1, 0, x3, 0, x1.length);
System.arraycopy(x2, 0, x3, x1.length, x2.length);
- byte[] key = truncateOrPad(x3, keySize);
+ byte[] key = Arrays.copyOf(x3, keySize);
SecretKey skey = new SecretKeySpec(key, ver.getCipherAlgorithm().jceId);
return skey;
@@ -111,24 +115,8 @@ public class StandardDecryptor extends Decryptor {
return sha1.digest(buff);
}
- /**
- * 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
*
@@ -33,6 +35,7 @@ public class LittleEndianInputStream extends FilterInputStream implements Little
public LittleEndianInputStream(InputStream is) {
super(is);
}
+
public int available() {
try {
return super.available();
@@ -40,86 +43,75 @@ public class LittleEndianInputStream extends FilterInputStream implements Little
throw new RuntimeException(e);
}
}
+
public byte readByte() {
return (byte)readUByte();
}
+
public int readUByte() {
- int ch;
+ byte buf[] = new byte[1];
try {
- ch = in.read();
+ checkEOF(read(buf), 1);
} catch (IOException e) {
throw new RuntimeException(e);
}
- checkEOF(ch);
- return ch;
+ return LittleEndian.getUByte(buf);
}
+
public double readDouble() {
return Double.longBitsToDouble(readLong());
}
+
public int readInt() {
- int ch1;
- int ch2;
- int ch3;
- int ch4;
+ byte buf[] = new byte[LittleEndianConsts.INT_SIZE];
try {
- ch1 = in.read();
- ch2 = in.read();
- ch3 = in.read();
- ch4 = in.read();
+ checkEOF(read(buf), buf.length);
} catch (IOException e) {
throw new RuntimeException(e);
}
- checkEOF(ch1 | ch2 | ch3 | ch4);
- return (ch4 << 24) + (ch3 << 16) + (ch2 << 8) + (ch1 << 0);
+ return LittleEndian.getInt(buf);
}
+
+ /**
+ * get an unsigned int value from an InputStream
+ *
+ * @return the unsigned int (32-bit) value
+ * @exception IOException
+ * will be propagated back to the caller
+ * @exception BufferUnderrunException
+ * if the stream cannot provide enough bytes
+ */
+ public long readUInt() {
+ long retNum = readInt();
+ return retNum & 0x00FFFFFFFFl;
+ }
+
public long readLong() {
- int b0;
- int b1;
- int b2;
- int b3;
- int b4;
- int b5;
- int b6;
- int b7;
+ byte buf[] = new byte[LittleEndianConsts.LONG_SIZE];
try {
- b0 = in.read();
- b1 = in.read();
- b2 = in.read();
- b3 = in.read();
- b4 = in.read();
- b5 = in.read();
- b6 = in.read();
- b7 = in.read();
+ checkEOF(read(buf), LittleEndianConsts.LONG_SIZE);
} catch (IOException e) {
throw new RuntimeException(e);
}
- checkEOF(b0 | b1 | b2 | b3 | b4 | b5 | b6 | b7);
- return (((long)b7 << 56) +
- ((long)b6 << 48) +
- ((long)b5 << 40) +
- ((long)b4 << 32) +
- ((long)b3 << 24) +
- (b2 << 16) +
- (b1 << 8) +
- (b0 << 0));
+ return LittleEndian.getLong(buf);
}
+
public short readShort() {
return (short)readUShort();
}
+
public int readUShort() {
- int ch1;
- int ch2;
+ byte buf[] = new byte[LittleEndianConsts.SHORT_SIZE];
try {
- ch1 = in.read();
- ch2 = in.read();
+ checkEOF(read(buf), LittleEndianConsts.SHORT_SIZE);
} catch (IOException e) {
throw new RuntimeException(e);
}
- checkEOF(ch1 | ch2);
- return (ch2 << 8) + (ch1 << 0);
+ return LittleEndian.getUShort(buf);
}
- private static void checkEOF(int value) {
- if (value <0) {
+
+ private static void checkEOF(int actualBytes, int expectedBytes) {
+ if (expectedBytes != 0 && (actualBytes == -1 || actualBytes != expectedBytes)) {
throw new RuntimeException("Unexpected end-of-file");
}
}
@@ -129,16 +121,10 @@ public class LittleEndianInputStream extends FilterInputStream implements Little
}
public void readFully(byte[] buf, int off, int len) {
- int max = off+len;
- for(int i=off; i 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());
- AlgorithmParameterSpec aps;
- if (header.getCipherAlgorithm() == CipherAlgorithm.rc2) {
- aps = new RC2ParameterSpec(getSecretKey().getEncoded().length*8, iv);
- } else {
- aps = new IvParameterSpec(iv);
- }
-
- _cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), aps);
- 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;
}
}
diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptionInfoBuilder.java b/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptionInfoBuilder.java
index 10ca07674f..b778c1032a 100644
--- a/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptionInfoBuilder.java
+++ b/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptionInfoBuilder.java
@@ -26,7 +26,7 @@ import org.apache.poi.poifs.crypt.EncryptionInfo;
import org.apache.poi.poifs.crypt.EncryptionInfoBuilder;
import org.apache.poi.poifs.crypt.EncryptionMode;
import org.apache.poi.poifs.crypt.HashAlgorithm;
-import org.apache.poi.poifs.filesystem.DocumentInputStream;
+import org.apache.poi.util.LittleEndianInput;
import org.apache.xmlbeans.XmlException;
import com.microsoft.schemas.office.x2006.encryption.EncryptionDocument;
@@ -39,10 +39,10 @@ public class AgileEncryptionInfoBuilder implements EncryptionInfoBuilder {
AgileDecryptor decryptor;
AgileEncryptor encryptor;
- public void initialize(EncryptionInfo info, DocumentInputStream dis) throws IOException {
+ public void initialize(EncryptionInfo info, LittleEndianInput dis) throws IOException {
this.info = info;
- EncryptionDocument ed = parseDescriptor(dis);
+ EncryptionDocument ed = parseDescriptor((InputStream)dis);
header = new AgileEncryptionHeader(ed);
verifier = new AgileEncryptionVerifier(ed);
if (info.getVersionMajor() == EncryptionMode.agile.versionMajor
diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptor.java b/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptor.java
index ee3b71036f..51ced4c2cc 100644
--- a/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptor.java
+++ b/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptor.java
@@ -16,11 +16,11 @@
==================================================================== */
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.DataSpaceMapUtils.createEncryptionEntry;
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;
@@ -32,16 +32,12 @@ import static org.apache.poi.poifs.crypt.agile.AgileDecryptor.kVerifierInputBloc
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.security.spec.AlgorithmParameterSpec;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
@@ -49,28 +45,20 @@ 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.RC2ParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.apache.poi.EncryptedDocumentException;
-import org.apache.poi.poifs.crypt.CipherAlgorithm;
+import org.apache.poi.poifs.crypt.ChunkedCipherOutputStream;
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.crypt.standard.EncryptionRecord;
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;
@@ -87,9 +75,7 @@ import com.microsoft.schemas.office.x2006.keyEncryptor.password.CTPasswordKeyEnc
public class AgileEncryptor extends Encryptor {
private final AgileEncryptionInfoBuilder builder;
- @SuppressWarnings("unused")
private byte integritySalt[];
- private Mac integrityMD;
private byte pwHash[];
protected AgileEncryptor(AgileEncryptionInfoBuilder builder) {
@@ -214,10 +200,6 @@ public class AgileEncryptor extends Encryptor {
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());
@@ -234,182 +216,59 @@ public class AgileEncryptor extends Encryptor {
public OutputStream getDataStream(DirectoryNode dir)
throws IOException, GeneralSecurityException {
// TODO: initialize headers
- OutputStream countStream = new ChunkedCipherOutputStream(dir);
+ AgileCipherOutputStream countStream = new AgileCipherOutputStream(dir);
return countStream;
}
/**
- * 2.3.4.15 Data Encryption (Agile Encryption)
+ * 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.
*
- * 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 {
- AlgorithmParameterSpec aps;
- if (header.getCipherAlgorithm() == CipherAlgorithm.rc2) {
- aps = new RC2ParameterSpec(getSecretKey().getEncoded().length*8, iv);
- } else {
- aps = new IvParameterSpec(iv);
- }
-
- _cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(), aps);
- int ciLen = _cipher.doFinal(_chunk, 0, posInChunk, _chunk);
- out.write(_chunk, 0, ciLen);
- } catch (GeneralSecurityException e) {
- throw (IOException)new IOException().initCause(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 {
- final CTKeyEncryptor.Uri.Enum passwordUri =
- CTKeyEncryptor.Uri.HTTP_SCHEMAS_MICROSOFT_COM_OFFICE_2006_KEY_ENCRYPTOR_PASSWORD;
- final CTKeyEncryptor.Uri.Enum certificateUri =
- CTKeyEncryptor.Uri.HTTP_SCHEMAS_MICROSOFT_COM_OFFICE_2006_KEY_ENCRYPTOR_CERTIFICATE;
-
+ * 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.
+ **/
+ protected void updateIntegrityHMAC(File tmpFile, int oleStreamSize) throws GeneralSecurityException, IOException {
+ // as the integrity hmac needs to contain the StreamSize,
+ // it's not possible to calculate it on-the-fly while buffering
+ // TODO: add stream size parameter to getDataStream()
AgileEncryptionVerifier ver = builder.getVerifier();
+ HashAlgorithm hashAlgo = ver.getHashAlgorithm();
+ Mac integrityMD = CryptoFunctions.getMac(hashAlgo);
+ integrityMD.init(new SecretKeySpec(integritySalt, hashAlgo.jceHmacId));
+
+ byte buf[] = new byte[1024];
+ LittleEndian.putLong(buf, 0, oleStreamSize);
+ integrityMD.update(buf, 0, LittleEndian.LONG_SIZE);
+
+ FileInputStream fis = new FileInputStream(tmpFile);
+ int readBytes;
+ while ((readBytes = fis.read(buf)) != -1) {
+ integrityMD.update(buf, 0, readBytes);
+ }
+ fis.close();
+
+ byte hmacValue[] = integrityMD.doFinal();
+
AgileEncryptionHeader header = builder.getHeader();
+ int blockSize = header.getBlockSize();
+ byte iv[] = CryptoFunctions.generateIv(header.getHashAlgorithmEx(), header.getKeySalt(), kIntegrityValueBlock, blockSize);
+ Cipher cipher = CryptoFunctions.getCipher(getSecretKey(), header.getCipherAlgorithm(), header.getChainingMode(), iv, Cipher.ENCRYPT_MODE);
+ byte hmacValueFilled[] = getBlock0(hmacValue, getNextBlockSize(hmacValue.length, blockSize));
+ byte encryptedHmacValue[] = cipher.doFinal(hmacValueFilled);
+
+ header.setEncryptedHmacValue(encryptedHmacValue);
+ }
+
+ private final CTKeyEncryptor.Uri.Enum passwordUri =
+ CTKeyEncryptor.Uri.HTTP_SCHEMAS_MICROSOFT_COM_OFFICE_2006_KEY_ENCRYPTOR_PASSWORD;
+ private final CTKeyEncryptor.Uri.Enum certificateUri =
+ CTKeyEncryptor.Uri.HTTP_SCHEMAS_MICROSOFT_COM_OFFICE_2006_KEY_ENCRYPTOR_CERTIFICATE;
+
+ protected EncryptionDocument createEncryptionDocument() {
+ AgileEncryptionVerifier ver = builder.getVerifier();
+ AgileEncryptionHeader header = builder.getHeader();
EncryptionDocument ed = EncryptionDocument.Factory.newInstance();
CTEncryption edRoot = ed.addNewEncryption();
@@ -485,6 +344,10 @@ public class AgileEncryptor extends Encryptor {
certData.setCertVerifier(ace.certVerifier);
}
+ return ed;
+ }
+
+ protected void marshallEncryptionDocument(EncryptionDocument ed, LittleEndianByteArrayOutputStream os) {
XmlOptions xo = new XmlOptions();
xo.setCharacterEncoding("UTF-8");
Map nsMap = new HashMap();
@@ -494,33 +357,82 @@ public class AgileEncryptor extends Encryptor {
xo.setSaveSuggestedPrefixes(nsMap);
xo.setSaveNamespacesFirst();
xo.setSaveAggressiveNamespaces();
- // setting standalone doesn't work with xmlbeans-2.3
+
+ // setting standalone doesn't work with xmlbeans-2.3 & 2.6
+ // ed.documentProperties().setStandalone(true);
xo.setSaveNoXmlDecl();
-
ByteArrayOutputStream bos = new ByteArrayOutputStream();
- bos.write("\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(info.getEncryptionFlags());
- 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);
- }
- }
- });
+ try {
+ bos.write("\r\n".getBytes("UTF-8"));
+ ed.save(bos, xo);
+ os.write(bos.toByteArray());
+ } catch (IOException e) {
+ throw new EncryptedDocumentException("error marshalling encryption info document", e);
+ }
}
+
+ protected void createEncryptionInfoEntry(DirectoryNode dir, File tmpFile)
+ throws IOException, GeneralSecurityException {
+ DataSpaceMapUtils.addDefaultDataSpace(dir);
+
+ final EncryptionInfo info = builder.getInfo();
+
+ EncryptionRecord er = new EncryptionRecord(){
+ public void write(LittleEndianByteArrayOutputStream bos) {
+ // EncryptionVersionInfo (4 bytes): A Version structure (section 2.1.4), where
+ // Version.vMajor MUST be 0x0004 and Version.vMinor MUST be 0x0004
+ bos.writeShort(info.getVersionMajor());
+ bos.writeShort(info.getVersionMinor());
+ // Reserved (4 bytes): A value that MUST be 0x00000040
+ bos.writeInt(info.getEncryptionFlags());
+
+ EncryptionDocument ed = createEncryptionDocument();
+ marshallEncryptionDocument(ed, bos);
+ }
+ };
+
+ createEncryptionEntry(dir, "EncryptionInfo", er);
+ }
+
+
+ /**
+ * 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 AgileCipherOutputStream extends ChunkedCipherOutputStream {
+ public AgileCipherOutputStream(DirectoryNode dir) throws IOException, GeneralSecurityException {
+ super(dir, 4096);
+ }
+
+ @Override
+ protected Cipher initCipherForBlock(Cipher existing, int block, boolean lastChunk)
+ throws GeneralSecurityException {
+ return AgileDecryptor.initCipherForBlock(existing, block, lastChunk, builder, getSecretKey(), Cipher.ENCRYPT_MODE);
+ }
+
+ @Override
+ protected void calculateChecksum(File fileOut, int oleStreamSize)
+ throws GeneralSecurityException, IOException {
+ // integrityHMAC needs to be updated before the encryption document is created
+ updateIntegrityHMAC(fileOut, oleStreamSize);
+ }
+
+ @Override
+ protected void createEncryptionInfoEntry(DirectoryNode dir, File tmpFile)
+ throws IOException, GeneralSecurityException {
+ AgileEncryptor.this.createEncryptionInfoEntry(dir, tmpFile);
+ }
+ }
+
}
diff --git a/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestAgileEncryptionParameters.java b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestAgileEncryptionParameters.java
index 131a5c2e49..0d441d68da 100644
--- a/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestAgileEncryptionParameters.java
+++ b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestAgileEncryptionParameters.java
@@ -53,7 +53,7 @@ public class TestAgileEncryptionParameters {
@Parameter(value = 2)
public ChainingMode cm;
- @Parameters
+ @Parameters(name="{0} {1} {2}")
public static Collection